@0x/asset-swapper: Add ExchangeProxySwapQuoteConsumer.

This commit is contained in:
Lawrence Forman
2020-06-01 15:15:06 -04:00
parent 9cab034448
commit 48ad39c1c7
9 changed files with 444 additions and 11 deletions

View File

@@ -89,6 +89,10 @@
{
"note": "Fix Uniswap V2 path ordering",
"pr": 2601
},
{
"note": "Add exchange proxy support",
"pr": 2591
}
]
},

View File

@@ -49,6 +49,7 @@
"@0x/assert": "^3.0.7",
"@0x/contract-addresses": "^4.9.0",
"@0x/contract-wrappers": "^13.6.3",
"@0x/contracts-zero-ex": "^0.1.0",
"@0x/json-schemas": "^5.0.7",
"@0x/order-utils": "^10.2.4",
"@0x/orderbook": "^2.2.5",

View File

@@ -60,6 +60,7 @@ export {
SwapQuoteConsumerError,
SignedOrderWithFillableAmounts,
SwapQuoteOrdersBreakdown,
ExchangeProxyContractOpts,
} from './types';
export {
ERC20BridgeSource,

View File

@@ -0,0 +1,156 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { ITransformERC20Contract } from '@0x/contract-wrappers';
import {
encodeFillQuoteTransformerData,
encodePayTakerTransformerData,
encodeWethTransformerData,
ETH_TOKEN_ADDRESS,
FillQuoteTransformerSide,
} from '@0x/contracts-zero-ex';
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 _ from 'lodash';
import { constants } from '../constants';
import {
CalldataInfo,
ExchangeProxyContractOpts,
MarketBuySwapQuote,
MarketOperation,
MarketSellSwapQuote,
SwapQuote,
SwapQuoteConsumerBase,
SwapQuoteConsumerOpts,
SwapQuoteExecutionOpts,
SwapQuoteGetOutputOpts,
} from '../types';
import { assert } from '../utils/assert';
// tslint:disable-next-line:custom-no-magic-numbers
const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
public readonly provider: ZeroExProvider;
public readonly chainId: number;
private readonly _transformFeature: ITransformERC20Contract;
constructor(
supportedProvider: SupportedProvider,
public readonly contractAddresses: ContractAddresses,
options: Partial<SwapQuoteConsumerOpts> = {},
) {
const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
assert.isNumber('chainId', chainId);
const provider = providerUtils.standardizeOrThrow(supportedProvider);
this.provider = provider;
this.chainId = chainId;
this.contractAddresses = contractAddresses;
this._transformFeature = new ITransformERC20Contract(contractAddresses.exchangeProxy, supportedProvider);
}
public async getCalldataOrThrowAsync(
quote: MarketBuySwapQuote | MarketSellSwapQuote,
opts: Partial<SwapQuoteGetOutputOpts> = {},
): Promise<CalldataInfo> {
assert.isValidSwapQuote('quote', quote);
const exchangeProxyOpts = {
...constants.DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
...{
isFromETH: false,
isToETH: false,
},
...opts,
}.extensionContractOpts as ExchangeProxyContractOpts;
const sellToken = getTokenFromAssetData(quote.takerAssetData);
const buyToken = getTokenFromAssetData(quote.makerAssetData);
// Build up the transforms.
const transforms = [];
if (exchangeProxyOpts.isFromETH) {
// Create a WETH wrapper if coming from ETH.
transforms.push({
transformer: this.contractAddresses.transformers.wethTransformer,
data: encodeWethTransformerData({
token: ETH_TOKEN_ADDRESS,
amount: quote.worstCaseQuoteInfo.totalTakerAssetAmount,
}),
});
}
// This transformer will fill the quote.
transforms.push({
transformer: this.contractAddresses.transformers.fillQuoteTransformer,
data: encodeFillQuoteTransformerData({
sellToken,
buyToken,
side: isBuyQuote(quote) ? FillQuoteTransformerSide.Buy : FillQuoteTransformerSide.Sell,
fillAmount: isBuyQuote(quote) ? quote.makerAssetFillAmount : quote.takerAssetFillAmount,
maxOrderFillAmounts: [],
orders: quote.orders,
signatures: quote.orders.map(o => o.signature),
}),
});
if (exchangeProxyOpts.isToETH) {
// Create a WETH unwrapper if going to ETH.
transforms.push({
transformer: this.contractAddresses.transformers.wethTransformer,
data: encodeWethTransformerData({
token: this.contractAddresses.etherToken,
amount: MAX_UINT256,
}),
});
}
// The final transformer will send all funds to the taker.
transforms.push({
transformer: this.contractAddresses.transformers.payTakerTransformer,
data: encodePayTakerTransformerData({
tokens: [sellToken, buyToken, ETH_TOKEN_ADDRESS],
amounts: [],
}),
});
const calldataHexString = this._transformFeature
.transformERC20(
sellToken,
buyToken,
quote.worstCaseQuoteInfo.totalTakerAssetAmount,
quote.worstCaseQuoteInfo.makerAssetAmount,
transforms,
)
.getABIEncodedTransactionData();
return {
calldataHexString,
ethAmount: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount,
toAddress: this._transformFeature.address,
allowanceTarget: this.contractAddresses.exchangeProxyAllowanceTarget,
};
}
// tslint:disable-next-line:prefer-function-over-method
public async executeSwapQuoteOrThrowAsync(
_quote: SwapQuote,
_opts: Partial<SwapQuoteExecutionOpts>,
): Promise<string> {
throw new Error('Execution not supported for Exchange Proxy quotes');
}
}
function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote {
return quote.type === MarketOperation.Buy;
}
function getTokenFromAssetData(assetData: string): string {
const data = assetDataUtils.decodeAssetDataOrThrow(assetData);
if (data.assetProxyId !== AssetProxyId.ERC20) {
throw new Error(`Unsupported exchange proxy quote asset type: ${data.assetProxyId}`);
}
// tslint:disable-next-line:no-unnecessary-type-assertion
return (data as ERC20AssetData).tokenAddress;
}

View File

@@ -25,7 +25,7 @@ export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase {
constructor(
supportedProvider: SupportedProvider,
contractAddresses: ContractAddresses,
public readonly contractAddresses: ContractAddresses,
options: Partial<SwapQuoteConsumerOpts> = {},
) {
const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
@@ -59,6 +59,7 @@ export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase {
calldataHexString,
ethAmount: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount,
toAddress: this._exchangeContract.address,
allowanceTarget: this.contractAddresses.erc20Proxy,
};
}

View File

@@ -19,16 +19,17 @@ import { affiliateFeeUtils } from '../utils/affiliate_fee_utils';
import { assert } from '../utils/assert';
import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils';
const { NULL_ADDRESS } = constants;
export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase {
public readonly provider: ZeroExProvider;
public readonly chainId: number;
private readonly _contractAddresses: ContractAddresses;
private readonly _forwarder: ForwarderContract;
constructor(
supportedProvider: SupportedProvider,
contractAddresses: ContractAddresses,
public readonly contractAddresses: ContractAddresses,
options: Partial<SwapQuoteConsumerOpts> = {},
) {
const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
@@ -36,7 +37,6 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase {
const provider = providerUtils.standardizeOrThrow(supportedProvider);
this.provider = provider;
this.chainId = chainId;
this._contractAddresses = contractAddresses;
this._forwarder = new ForwarderContract(contractAddresses.forwarder, supportedProvider);
}
@@ -90,6 +90,7 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase {
calldataHexString,
toAddress: this._forwarder.address,
ethAmount: ethAmountWithFees,
allowanceTarget: NULL_ADDRESS,
};
}
@@ -160,6 +161,6 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase {
}
private _getEtherTokenAssetDataOrThrow(): string {
return assetDataUtils.encodeERC20AssetData(this._contractAddresses.etherToken);
return assetDataUtils.encodeERC20AssetData(this.contractAddresses.etherToken);
}
}

View File

@@ -17,6 +17,7 @@ import {
import { assert } from '../utils/assert';
import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils';
import { ExchangeProxySwapQuoteConsumer } from './exchange_proxy_swap_quote_consumer';
import { ExchangeSwapQuoteConsumer } from './exchange_swap_quote_consumer';
import { ForwarderSwapQuoteConsumer } from './forwarder_swap_quote_consumer';
@@ -27,6 +28,7 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
private readonly _exchangeConsumer: ExchangeSwapQuoteConsumer;
private readonly _forwarderConsumer: ForwarderSwapQuoteConsumer;
private readonly _contractAddresses: ContractAddresses;
private readonly _exchangeProxyConsumer: ExchangeProxySwapQuoteConsumer;
public static getSwapQuoteConsumer(
supportedProvider: SupportedProvider,
@@ -45,6 +47,11 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
this._contractAddresses = options.contractAddresses || getContractAddressesForChainOrThrow(chainId);
this._exchangeConsumer = new ExchangeSwapQuoteConsumer(supportedProvider, this._contractAddresses, options);
this._forwarderConsumer = new ForwarderSwapQuoteConsumer(supportedProvider, this._contractAddresses, options);
this._exchangeProxyConsumer = new ExchangeProxySwapQuoteConsumer(
supportedProvider,
this._contractAddresses,
options,
);
}
/**
@@ -93,9 +100,13 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase {
}
private async _getConsumerForSwapQuoteAsync(opts: Partial<SwapQuoteGetOutputOpts>): Promise<SwapQuoteConsumerBase> {
if (opts.useExtensionContract === ExtensionContractType.Forwarder) {
return this._forwarderConsumer;
switch (opts.useExtensionContract) {
case ExtensionContractType.Forwarder:
return this._forwarderConsumer;
case ExtensionContractType.ExchangeProxy:
return this._exchangeProxyConsumer;
default:
return this._exchangeConsumer;
}
return this._exchangeConsumer;
}
}

View File

@@ -50,19 +50,22 @@ export interface SignedOrderWithFillableAmounts extends SignedOrder {
* calldataHexString: The hexstring of the calldata.
* toAddress: The contract address to call.
* ethAmount: The eth amount in wei to send with the smart contract call.
* allowanceTarget: The address the taker should grant an allowance to.
*/
export interface CalldataInfo {
calldataHexString: string;
toAddress: string;
ethAmount: BigNumber;
allowanceTarget: string;
}
/**
* Represents the varying smart contracts that can consume a valid swap quote
*/
export enum ExtensionContractType {
Forwarder = 'FORWARDER',
None = 'NONE',
Forwarder = 'FORWARDER',
ExchangeProxy = 'EXCHANGE_PROXY',
}
/**
@@ -97,7 +100,7 @@ export interface SwapQuoteConsumerOpts {
*/
export interface SwapQuoteGetOutputOpts {
useExtensionContract: ExtensionContractType;
extensionContractOpts?: ForwarderExtensionContractOpts | any;
extensionContractOpts?: ForwarderExtensionContractOpts | ExchangeProxyContractOpts | any;
}
/**
@@ -112,7 +115,6 @@ export interface SwapQuoteExecutionOpts extends SwapQuoteGetOutputOpts {
}
/**
* ethAmount: The amount of eth (in Wei) sent to the forwarder contract.
* feePercentage: percentage (up to 5%) of the taker asset paid to feeRecipient
* feeRecipient: address of the receiver of the feePercentage of taker asset
*/
@@ -121,6 +123,15 @@ export interface ForwarderExtensionContractOpts {
feeRecipient: string;
}
/**
* @param isFromETH Whether the input token is ETH.
* @param isToETH Whether the output token is ETH.
*/
export interface ExchangeProxyContractOpts {
isFromETH: boolean;
isToETH: boolean;
}
export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote;
export interface GetExtensionContractTypeOpts {

View File

@@ -0,0 +1,247 @@
import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
import { constants as contractConstants, getRandomInteger, Numberish, randomAddress } from '@0x/contracts-test-utils';
import {
decodeFillQuoteTransformerData,
decodePayTakerTransformerData,
decodeWethTransformerData,
ETH_TOKEN_ADDRESS,
FillQuoteTransformerSide,
} from '@0x/contracts-zero-ex';
import { assetDataUtils } from '@0x/order-utils';
import { Order } from '@0x/types';
import { AbiEncoder, BigNumber, hexUtils } from '@0x/utils';
import * as chai from 'chai';
import * as _ from 'lodash';
import 'mocha';
import { constants } from '../src/constants';
import { ExchangeProxySwapQuoteConsumer } 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';
import { chaiSetup } from './utils/chai_setup';
chaiSetup.configure();
const expect = chai.expect;
const { NULL_ADDRESS } = constants;
const { MAX_UINT256 } = contractConstants;
// tslint:disable: custom-no-magic-numbers
describe('ExchangeProxySwapQuoteConsumer', () => {
const CHAIN_ID = 1;
const TAKER_TOKEN = randomAddress();
const MAKER_TOKEN = randomAddress();
const contractAddresses = {
...getContractAddressesForChainOrThrow(CHAIN_ID),
exchangeProxy: randomAddress(),
exchangeProxyAllowanceTarget: randomAddress(),
transformers: {
wethTransformer: randomAddress(),
payTakerTransformer: randomAddress(),
fillQuoteTransformer: randomAddress(),
},
};
let consumer: ExchangeProxySwapQuoteConsumer;
before(async () => {
const fakeProvider = {
async sendAsync(): Promise<void> {
/* noop */
},
};
consumer = new ExchangeProxySwapQuoteConsumer(fakeProvider, contractAddresses, { chainId: CHAIN_ID });
});
function getRandomAmount(maxAmount: Numberish = '1e18'): BigNumber {
return getRandomInteger(1, maxAmount);
}
function createAssetData(token?: string): string {
return assetDataUtils.encodeERC20AssetData(token || randomAddress());
}
function getRandomOrder(): OptimizedMarketOrder {
return {
fillableMakerAssetAmount: getRandomAmount(),
fillableTakerFeeAmount: getRandomAmount(),
fillableTakerAssetAmount: getRandomAmount(),
fills: [],
chainId: CHAIN_ID,
exchangeAddress: contractAddresses.exchange,
expirationTimeSeconds: getRandomInteger(1, 2e9),
feeRecipientAddress: randomAddress(),
makerAddress: randomAddress(),
makerAssetAmount: getRandomAmount(),
takerAssetAmount: getRandomAmount(),
makerFee: getRandomAmount(),
takerFee: getRandomAmount(),
salt: getRandomAmount(2e9),
signature: hexUtils.random(66),
senderAddress: NULL_ADDRESS,
takerAddress: NULL_ADDRESS,
makerAssetData: createAssetData(MAKER_TOKEN),
takerAssetData: createAssetData(TAKER_TOKEN),
makerFeeAssetData: createAssetData(),
takerFeeAssetData: createAssetData(),
};
}
function getRandomQuote(side: MarketOperation): MarketBuySwapQuote | MarketSellSwapQuote {
return {
gasPrice: getRandomInteger(1, 1e9),
type: side,
makerAssetData: createAssetData(MAKER_TOKEN),
takerAssetData: createAssetData(TAKER_TOKEN),
orders: [getRandomOrder()],
bestCaseQuoteInfo: {
feeTakerAssetAmount: getRandomAmount(),
makerAssetAmount: getRandomAmount(),
gas: Math.floor(Math.random() * 8e6),
protocolFeeInWeiAmount: getRandomAmount(),
takerAssetAmount: getRandomAmount(),
totalTakerAssetAmount: getRandomAmount(),
},
worstCaseQuoteInfo: {
feeTakerAssetAmount: getRandomAmount(),
makerAssetAmount: getRandomAmount(),
gas: Math.floor(Math.random() * 8e6),
protocolFeeInWeiAmount: getRandomAmount(),
takerAssetAmount: getRandomAmount(),
totalTakerAssetAmount: getRandomAmount(),
},
...(side === MarketOperation.Buy
? { makerAssetFillAmount: getRandomAmount() }
: { takerAssetFillAmount: getRandomAmount() }),
} as any;
}
function getRandomSellQuote(): MarketSellSwapQuote {
return getRandomQuote(MarketOperation.Sell) as MarketSellSwapQuote;
}
function getRandomBuyQuote(): MarketBuySwapQuote {
return getRandomQuote(MarketOperation.Buy) as MarketBuySwapQuote;
}
type PlainOrder = Exclude<Order, ['chainId', 'exchangeAddress']>;
function cleanOrders(orders: OptimizedMarketOrder[]): PlainOrder[] {
return orders.map(
o =>
_.omit(o, [
'chainId',
'exchangeAddress',
'fillableMakerAssetAmount',
'fillableTakerAssetAmount',
'fillableTakerFeeAmount',
'fills',
'signature',
]) as PlainOrder,
);
}
const callDataEncoder = AbiEncoder.createMethod('transformERC20', [
{ type: 'address', name: 'inputToken' },
{ type: 'address', name: 'outputToken' },
{ type: 'uint256', name: 'inputTokenAmount' },
{ type: 'uint256', name: 'minOutputTokenAmount' },
{
type: 'tuple[]',
name: 'transformations',
components: [{ type: 'address', name: 'transformer' }, { type: 'bytes', name: 'data' }],
},
]);
interface CallArgs {
inputToken: string;
outputToken: string;
inputTokenAmount: BigNumber;
minOutputTokenAmount: BigNumber;
transformations: Array<{
transformer: string;
data: string;
}>;
}
describe('getCalldataOrThrow()', () => {
it('can produce a sell quote', async () => {
const quote = getRandomSellQuote();
const callInfo = await consumer.getCalldataOrThrowAsync(quote);
const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
expect(callArgs.inputToken).to.eq(TAKER_TOKEN);
expect(callArgs.outputToken).to.eq(MAKER_TOKEN);
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);
const fillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data);
expect(fillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell);
expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(quote.takerAssetFillAmount);
expect(fillQuoteTransformerData.orders).to.deep.eq(cleanOrders(quote.orders));
expect(fillQuoteTransformerData.signatures).to.deep.eq(quote.orders.map(o => o.signature));
expect(fillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN);
expect(fillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN);
const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[1].data);
expect(payTakerTransformerData.amounts).to.deep.eq([]);
expect(payTakerTransformerData.tokens).to.deep.eq([TAKER_TOKEN, MAKER_TOKEN, ETH_TOKEN_ADDRESS]);
});
it('can produce a buy quote', async () => {
const quote = getRandomBuyQuote();
const callInfo = await consumer.getCalldataOrThrowAsync(quote);
const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
expect(callArgs.inputToken).to.eq(TAKER_TOKEN);
expect(callArgs.outputToken).to.eq(MAKER_TOKEN);
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);
const fillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data);
expect(fillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Buy);
expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(quote.makerAssetFillAmount);
expect(fillQuoteTransformerData.orders).to.deep.eq(cleanOrders(quote.orders));
expect(fillQuoteTransformerData.signatures).to.deep.eq(quote.orders.map(o => o.signature));
expect(fillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN);
expect(fillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN);
const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[1].data);
expect(payTakerTransformerData.amounts).to.deep.eq([]);
expect(payTakerTransformerData.tokens).to.deep.eq([TAKER_TOKEN, MAKER_TOKEN, ETH_TOKEN_ADDRESS]);
});
it('ERC20 -> ERC20 does not have a WETH transformer', async () => {
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);
});
it('ETH -> ERC20 has a WETH transformer before the fill', async () => {
const quote = getRandomSellQuote();
const callInfo = await consumer.getCalldataOrThrowAsync(quote, {
extensionContractOpts: { isFromETH: true },
});
const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
expect(callArgs.transformations[0].transformer).to.eq(contractAddresses.transformers.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);
});
it('ERC20 -> ETH has a WETH transformer after the fill', async () => {
const quote = getRandomSellQuote();
const callInfo = await consumer.getCalldataOrThrowAsync(quote, {
extensionContractOpts: { isToETH: true },
});
const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
expect(callArgs.transformations[1].transformer).to.eq(contractAddresses.transformers.wethTransformer);
const wethTransformerData = decodeWethTransformerData(callArgs.transformations[1].data);
expect(wethTransformerData.amount).to.bignumber.eq(MAX_UINT256);
expect(wethTransformerData.token).to.eq(contractAddresses.etherToken);
});
});
});