@0x/asset-swapper: Guess deployment nonce from transformer address.
`@0x/asset-swapper`: Fix ETH not being passed as the token to `transformERC20()`.
This commit is contained in:
@@ -57,6 +57,7 @@
|
||||
"@0x/web3-wrapper": "^7.0.7",
|
||||
"axios": "^0.19.2",
|
||||
"axios-mock-adapter": "^1.18.1",
|
||||
"ethereumjs-util": "^5.1.1",
|
||||
"heartbeats": "^5.0.1",
|
||||
"lodash": "^4.17.11"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ import { assetDataUtils, ERC20AssetData } from '@0x/order-utils';
|
||||
import { AssetProxyId } from '@0x/types';
|
||||
import { BigNumber, providerUtils } from '@0x/utils';
|
||||
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
|
||||
import * as ethjs from 'ethereumjs-util';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { constants } from '../constants';
|
||||
@@ -30,10 +31,17 @@ import { assert } from '../utils/assert';
|
||||
|
||||
// tslint:disable-next-line:custom-no-magic-numbers
|
||||
const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
|
||||
const { NULL_ADDRESS } = constants;
|
||||
const MAX_NONCE_GUESSES = 2048;
|
||||
|
||||
export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
public readonly provider: ZeroExProvider;
|
||||
public readonly chainId: number;
|
||||
public readonly transformerNonces: {
|
||||
wethTransformer: number;
|
||||
payTakerTransformer: number;
|
||||
fillQuoteTransformer: number;
|
||||
};
|
||||
|
||||
private readonly _transformFeature: ITransformERC20Contract;
|
||||
|
||||
@@ -49,6 +57,20 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
this.chainId = chainId;
|
||||
this.contractAddresses = contractAddresses;
|
||||
this._transformFeature = new ITransformERC20Contract(contractAddresses.exchangeProxy, supportedProvider);
|
||||
this.transformerNonces = {
|
||||
wethTransformer: findTransformerNonce(
|
||||
contractAddresses.transformers.wethTransformer,
|
||||
contractAddresses.exchangeProxyTransformerDeployer,
|
||||
),
|
||||
payTakerTransformer: findTransformerNonce(
|
||||
contractAddresses.transformers.payTakerTransformer,
|
||||
contractAddresses.exchangeProxyTransformerDeployer,
|
||||
),
|
||||
fillQuoteTransformer: findTransformerNonce(
|
||||
contractAddresses.transformers.fillQuoteTransformer,
|
||||
contractAddresses.exchangeProxyTransformerDeployer,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
public async getCalldataOrThrowAsync(
|
||||
@@ -56,24 +78,24 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
opts: Partial<SwapQuoteGetOutputOpts> = {},
|
||||
): Promise<CalldataInfo> {
|
||||
assert.isValidSwapQuote('quote', quote);
|
||||
const exchangeProxyOpts = {
|
||||
const { isFromETH, isToETH } = {
|
||||
...constants.DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
|
||||
...{
|
||||
extensionContractOpts: {
|
||||
isFromETH: false,
|
||||
isToETH: false,
|
||||
},
|
||||
...opts,
|
||||
}.extensionContractOpts as ExchangeProxyContractOpts;
|
||||
}.extensionContractOpts;
|
||||
|
||||
const sellToken = getTokenFromAssetData(quote.takerAssetData);
|
||||
const buyToken = getTokenFromAssetData(quote.makerAssetData);
|
||||
|
||||
// Build up the transforms.
|
||||
const transforms = [];
|
||||
if (exchangeProxyOpts.isFromETH) {
|
||||
if (isFromETH) {
|
||||
// Create a WETH wrapper if coming from ETH.
|
||||
transforms.push({
|
||||
transformer: this.contractAddresses.transformers.wethTransformer,
|
||||
deploymentNonce: this.transformerNonces.wethTransformer,
|
||||
data: encodeWethTransformerData({
|
||||
token: ETH_TOKEN_ADDRESS,
|
||||
amount: quote.worstCaseQuoteInfo.totalTakerAssetAmount,
|
||||
@@ -83,7 +105,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
|
||||
// This transformer will fill the quote.
|
||||
transforms.push({
|
||||
transformer: this.contractAddresses.transformers.fillQuoteTransformer,
|
||||
deploymentNonce: this.transformerNonces.fillQuoteTransformer,
|
||||
data: encodeFillQuoteTransformerData({
|
||||
sellToken,
|
||||
buyToken,
|
||||
@@ -95,10 +117,10 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
}),
|
||||
});
|
||||
|
||||
if (exchangeProxyOpts.isToETH) {
|
||||
if (isToETH) {
|
||||
// Create a WETH unwrapper if going to ETH.
|
||||
transforms.push({
|
||||
transformer: this.contractAddresses.transformers.wethTransformer,
|
||||
deploymentNonce: this.transformerNonces.wethTransformer,
|
||||
data: encodeWethTransformerData({
|
||||
token: this.contractAddresses.etherToken,
|
||||
amount: MAX_UINT256,
|
||||
@@ -108,7 +130,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
|
||||
// The final transformer will send all funds to the taker.
|
||||
transforms.push({
|
||||
transformer: this.contractAddresses.transformers.payTakerTransformer,
|
||||
deploymentNonce: this.transformerNonces.payTakerTransformer,
|
||||
data: encodePayTakerTransformerData({
|
||||
tokens: [sellToken, buyToken, ETH_TOKEN_ADDRESS],
|
||||
amounts: [],
|
||||
@@ -117,8 +139,8 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
|
||||
const calldataHexString = this._transformFeature
|
||||
.transformERC20(
|
||||
sellToken,
|
||||
buyToken,
|
||||
isFromETH ? ETH_TOKEN_ADDRESS : sellToken,
|
||||
isToETH ? ETH_TOKEN_ADDRESS : buyToken,
|
||||
quote.worstCaseQuoteInfo.totalTakerAssetAmount,
|
||||
quote.worstCaseQuoteInfo.makerAssetAmount,
|
||||
transforms,
|
||||
@@ -126,7 +148,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
||||
.getABIEncodedTransactionData();
|
||||
|
||||
let ethAmount = quote.worstCaseQuoteInfo.protocolFeeInWeiAmount;
|
||||
if (exchangeProxyOpts.isFromETH) {
|
||||
if (isFromETH) {
|
||||
ethAmount = ethAmount.plus(quote.worstCaseQuoteInfo.takerAssetAmount);
|
||||
}
|
||||
|
||||
@@ -159,3 +181,32 @@ function getTokenFromAssetData(assetData: string): string {
|
||||
// tslint:disable-next-line:no-unnecessary-type-assertion
|
||||
return (data as ERC20AssetData).tokenAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the nonce for a transformer given its deployer.
|
||||
* If `deployer` is the null address, zero will always be returned.
|
||||
*/
|
||||
export function findTransformerNonce(transformer: string, deployer: string = NULL_ADDRESS): number {
|
||||
if (deployer === NULL_ADDRESS) {
|
||||
return 0;
|
||||
}
|
||||
const lowercaseTransformer = transformer.toLowerCase();
|
||||
// Try to guess the nonce.
|
||||
for (let nonce = 0; nonce < MAX_NONCE_GUESSES; ++nonce) {
|
||||
const deployedAddress = getTransformerAddress(deployer, nonce);
|
||||
if (deployedAddress === lowercaseTransformer) {
|
||||
return nonce;
|
||||
}
|
||||
}
|
||||
throw new Error(`${deployer} did not deploy ${transformer}!`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the deployed address for a transformer given a deployer and nonce.
|
||||
*/
|
||||
export function getTransformerAddress(deployer: string, nonce: number): string {
|
||||
return ethjs.bufferToHex(
|
||||
// tslint:disable-next-line: custom-no-magic-numbers
|
||||
ethjs.rlphash([deployer, nonce] as any).slice(12),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@ import * as _ from 'lodash';
|
||||
import 'mocha';
|
||||
|
||||
import { constants } from '../src/constants';
|
||||
import { ExchangeProxySwapQuoteConsumer } from '../src/quote_consumers/exchange_proxy_swap_quote_consumer';
|
||||
import {
|
||||
ExchangeProxySwapQuoteConsumer,
|
||||
getTransformerAddress,
|
||||
} from '../src/quote_consumers/exchange_proxy_swap_quote_consumer';
|
||||
import { MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote } from '../src/types';
|
||||
import { OptimizedMarketOrder } from '../src/utils/market_operation_utils/types';
|
||||
|
||||
@@ -33,14 +36,16 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
|
||||
const CHAIN_ID = 1;
|
||||
const TAKER_TOKEN = randomAddress();
|
||||
const MAKER_TOKEN = randomAddress();
|
||||
const TRANSFORMER_DEPLOYER = randomAddress();
|
||||
const contractAddresses = {
|
||||
...getContractAddressesForChainOrThrow(CHAIN_ID),
|
||||
exchangeProxy: randomAddress(),
|
||||
exchangeProxyAllowanceTarget: randomAddress(),
|
||||
exchangeProxyTransformerDeployer: TRANSFORMER_DEPLOYER,
|
||||
transformers: {
|
||||
wethTransformer: randomAddress(),
|
||||
payTakerTransformer: randomAddress(),
|
||||
fillQuoteTransformer: randomAddress(),
|
||||
wethTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 1),
|
||||
payTakerTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 2),
|
||||
fillQuoteTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 3),
|
||||
},
|
||||
};
|
||||
let consumer: ExchangeProxySwapQuoteConsumer;
|
||||
@@ -150,7 +155,7 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
|
||||
{
|
||||
type: 'tuple[]',
|
||||
name: 'transformations',
|
||||
components: [{ type: 'address', name: 'transformer' }, { type: 'bytes', name: 'data' }],
|
||||
components: [{ type: 'uint32', name: 'deploymentNonce' }, { type: 'bytes', name: 'data' }],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -160,7 +165,7 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
|
||||
inputTokenAmount: BigNumber;
|
||||
minOutputTokenAmount: BigNumber;
|
||||
transformations: Array<{
|
||||
transformer: string;
|
||||
deploymentNonce: BigNumber;
|
||||
data: string;
|
||||
}>;
|
||||
}
|
||||
@@ -175,8 +180,14 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
|
||||
expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount);
|
||||
expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAssetAmount);
|
||||
expect(callArgs.transformations).to.be.length(2);
|
||||
expect(callArgs.transformations[0].transformer === contractAddresses.transformers.fillQuoteTransformer);
|
||||
expect(callArgs.transformations[1].transformer === contractAddresses.transformers.payTakerTransformer);
|
||||
expect(
|
||||
callArgs.transformations[0].deploymentNonce.toNumber() ===
|
||||
consumer.transformerNonces.fillQuoteTransformer,
|
||||
);
|
||||
expect(
|
||||
callArgs.transformations[1].deploymentNonce.toNumber() ===
|
||||
consumer.transformerNonces.payTakerTransformer,
|
||||
);
|
||||
const fillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data);
|
||||
expect(fillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell);
|
||||
expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(quote.takerAssetFillAmount);
|
||||
@@ -198,8 +209,14 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
|
||||
expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount);
|
||||
expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAssetAmount);
|
||||
expect(callArgs.transformations).to.be.length(2);
|
||||
expect(callArgs.transformations[0].transformer === contractAddresses.transformers.fillQuoteTransformer);
|
||||
expect(callArgs.transformations[1].transformer === contractAddresses.transformers.payTakerTransformer);
|
||||
expect(
|
||||
callArgs.transformations[0].deploymentNonce.toNumber() ===
|
||||
consumer.transformerNonces.fillQuoteTransformer,
|
||||
);
|
||||
expect(
|
||||
callArgs.transformations[1].deploymentNonce.toNumber() ===
|
||||
consumer.transformerNonces.payTakerTransformer,
|
||||
);
|
||||
const fillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data);
|
||||
expect(fillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Buy);
|
||||
expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(quote.makerAssetFillAmount);
|
||||
@@ -216,8 +233,8 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
|
||||
const quote = getRandomSellQuote();
|
||||
const callInfo = await consumer.getCalldataOrThrowAsync(quote);
|
||||
const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
|
||||
const transformers = callArgs.transformations.map(t => t.transformer);
|
||||
expect(transformers).to.not.include(contractAddresses.transformers.wethTransformer);
|
||||
const nonces = callArgs.transformations.map(t => t.deploymentNonce);
|
||||
expect(nonces).to.not.include(consumer.transformerNonces.wethTransformer);
|
||||
});
|
||||
|
||||
it('ETH -> ERC20 has a WETH transformer before the fill', async () => {
|
||||
@@ -226,7 +243,9 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
|
||||
extensionContractOpts: { isFromETH: true },
|
||||
});
|
||||
const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
|
||||
expect(callArgs.transformations[0].transformer).to.eq(contractAddresses.transformers.wethTransformer);
|
||||
expect(callArgs.transformations[0].deploymentNonce.toNumber()).to.eq(
|
||||
consumer.transformerNonces.wethTransformer,
|
||||
);
|
||||
const wethTransformerData = decodeWethTransformerData(callArgs.transformations[0].data);
|
||||
expect(wethTransformerData.amount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount);
|
||||
expect(wethTransformerData.token).to.eq(ETH_TOKEN_ADDRESS);
|
||||
@@ -238,7 +257,9 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
|
||||
extensionContractOpts: { isToETH: true },
|
||||
});
|
||||
const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
|
||||
expect(callArgs.transformations[1].transformer).to.eq(contractAddresses.transformers.wethTransformer);
|
||||
expect(callArgs.transformations[1].deploymentNonce.toNumber()).to.eq(
|
||||
consumer.transformerNonces.wethTransformer,
|
||||
);
|
||||
const wethTransformerData = decodeWethTransformerData(callArgs.transformations[1].data);
|
||||
expect(wethTransformerData.amount).to.bignumber.eq(MAX_UINT256);
|
||||
expect(wethTransformerData.token).to.eq(contractAddresses.etherToken);
|
||||
|
||||
Reference in New Issue
Block a user