diff --git a/packages/asset-swapper/package.json b/packages/asset-swapper/package.json index a63c3b8171..1788cc8811 100644 --- a/packages/asset-swapper/package.json +++ b/packages/asset-swapper/package.json @@ -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" }, diff --git a/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts b/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts index 4c55dada7e..2f50554da0 100644 --- a/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts +++ b/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts @@ -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( @@ -73,7 +95,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { if (exchangeProxyOpts.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, @@ -98,7 +120,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { if (exchangeProxyOpts.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: [], @@ -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), + ); +} diff --git a/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts b/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts index a84399174c..9f09e29879 100644 --- a/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts +++ b/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts @@ -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);