@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:
Lawrence Forman
2020-06-09 13:38:04 -04:00
parent f1f6aa7d80
commit 7b298939e2
3 changed files with 99 additions and 26 deletions

View File

@@ -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"
},

View File

@@ -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),
);
}

View File

@@ -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);