protocol/contracts/zero-ex/test/features/multiplex_test.ts
Lawrence Forman 164a5d44d9
bsc<->development rebase (#189)
* FQT: Pack Protocol/source name into source ID (#162)

* `@0x/contracts-zero-ex`: Encode protocol ID and source name in bridge source ID
`@0x/asset-swapper`: Use new bridge source ID encoding.

* fix linter issues

* contracts cleanup (#164)

* `@0x/contracts-zero-ex`: Add PancakeSwapFeature

* `@0x/contracts-zero-ex`: Remove tokenspender/allowance target/greedy tokens stuff.'
`@0x/contract-addresses`: Add BSC addresses. Remove exchangeProxyAllowanceTarget.
`@0x/migrations`: Remove exchangeProxyAllowanceTarget.

* Update contracts/zero-ex/contracts/src/features/IPancakeSwapFeature.sol

Co-authored-by: mzhu25 <mchl.zhu.96@gmail.com>

* `@0x/contracts-zero-ex`: Add sushiswap support to PancakeSwap

* `@0x/contract-artifacts`: Regenerate artifacts
`@0x/contract-wrappers`: Regenerate wrappers

* `@0x/contract-addresses`: Add BSC addresses

Co-authored-by: mzhu25 <mchl.zhu.96@gmail.com>

Co-authored-by: mzhu25 <mchl.zhu.96@gmail.com>

* feat: Better chain support (#163)

* feat: Better chain support

* feat: better chain support refactor deployment constants (#166)

* proliferate the chainId

* Refactor sampler to remove DeploymentConstants dependency and fixed addresses

* Rework WETH out, replacing with address(0)

* wat

* hack DeploymentConstants for now

* proliferate the chainId

* Refactor sampler to remove DeploymentConstants dependency and fixed addresses

* remove duped network addresses

* Rework the bridge source encoder

* Use the constants NATIVE_FEE_TOKEN in EP consumer

* `@0x/contract-addresses`: Fix WBNB address (#170)

Co-authored-by: Lawrence Forman <lawrence@0xproject.com>

* multichain enable cakez vip (#171)

* feat: Better chain support

* feat: better chain support refactor deployment constants (#166)

* proliferate the chainId

* Refactor sampler to remove DeploymentConstants dependency and fixed addresses

* Rework WETH out, replacing with address(0)

* wat

* hack DeploymentConstants for now

* proliferate the chainId

* Refactor sampler to remove DeploymentConstants dependency and fixed addresses

* remove duped network addresses

* `asset-swapper`: enable pancake VIP route generation

Co-authored-by: Jacob Evans <jacob@dekz.net>
Co-authored-by: Lawrence Forman <me@merklejerk.com>

* `@0x/contracts-zero-ex`: Fix `PancakeSwapFeature` sushi values (#172)

* `@0x/contracts-zero-ex`: Fix `PancakeSwapFeature` sushi values

* `@0x/contracts-zero-ex`: I am a bad protocologist

Co-authored-by: Lawrence Forman <me@merklejerk.com>

* feat: BSC Nerve + Dodo + Nerve + Ellipsis (#181)

* feat: BSC Nerve + DODO v1

* CHANGELOGs

* Remove extra balance fetch

* Add Belt

* Added Ellipsis

* Update FQT address

* `@0x/contracts-zero-ex`: Delete TokenSpenderFeature and get stuff compiling

* `@0x/asset-swapper`: fix compilation

* prettier

* `@0x/asset-swapper`: Truncate LiquidityProvider source ID name

* Update packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts

Co-authored-by: Jacob Evans <jacob@dekz.net>

* Update packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts

Co-authored-by: Jacob Evans <jacob@dekz.net>

* `@0x/contracts-zero-ex`: Fix BakerySwap on PackageSwapFeature (#190)

* address review comments

Co-authored-by: mzhu25 <mchl.zhu.96@gmail.com>
Co-authored-by: Jacob Evans <jacob@dekz.net>
Co-authored-by: Lawrence Forman <me@merklejerk.com>
2021-03-31 18:49:44 -04:00

766 lines
36 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
artifacts as erc20Artifacts,
ERC20TokenContract,
WETH9Contract,
WETH9DepositEventArgs,
WETH9Events,
WETH9WithdrawalEventArgs,
} from '@0x/contracts-erc20';
import { blockchainTests, constants, expect, filterLogsToArguments, toBaseUnitAmount } from '@0x/contracts-test-utils';
import {
BridgeProtocol,
encodeBridgeSourceId,
encodeFillQuoteTransformerData,
encodePayTakerTransformerData,
FillQuoteTransformerOrderType,
FillQuoteTransformerSide,
findTransformerNonce,
RfqOrder,
SIGNATURE_ABI,
} from '@0x/protocol-utils';
import { AbiEncoder, BigNumber, logUtils } from '@0x/utils';
import * as _ from 'lodash';
import { artifacts } from '../artifacts';
import { abis } from '../utils/abis';
import { getRandomRfqOrder } from '../utils/orders';
import {
BridgeAdapterBridgeFillEventArgs,
BridgeAdapterEvents,
IUniswapV2PairEvents,
IUniswapV2PairSwapEventArgs,
IZeroExContract,
IZeroExEvents,
IZeroExRfqOrderFilledEventArgs,
MultiplexFeatureContract,
MultiplexFeatureEvents,
MultiplexFeatureLiquidityProviderSwapEventArgs,
SimpleFunctionRegistryFeatureContract,
} from '../wrappers';
const HIGH_BIT = new BigNumber(2).pow(255);
function encodeFractionalFillAmount(frac: number): BigNumber {
return HIGH_BIT.plus(new BigNumber(frac).times('1e18').integerValue());
}
const EP_GOVERNOR = '0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e';
const DAI_WALLET = '0xbe0eb53f46cd790cd13851d5eff43d12404d33e8';
const WETH_WALLET = '0x1e0447b19bb6ecfdae1e4ae1694b0c3659614e4e';
const USDC_WALLET = '0xbe0eb53f46cd790cd13851d5eff43d12404d33e8';
blockchainTests.configure({
fork: {
unlockedAccounts: [EP_GOVERNOR, DAI_WALLET, WETH_WALLET, USDC_WALLET],
},
});
interface WrappedBatchCall {
selector: string;
sellAmount: BigNumber;
data: string;
}
blockchainTests.fork.skip('Multiplex feature', env => {
const DAI_ADDRESS = '0x6b175474e89094c44da98b954eedeac495271d0f';
const dai = new ERC20TokenContract(DAI_ADDRESS, env.provider, env.txDefaults);
const ETH_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
const WETH_ADDRESS = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
const weth = new WETH9Contract(WETH_ADDRESS, env.provider, env.txDefaults);
const USDC_ADDRESS = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48';
const USDT_ADDRESS = '0xdac17f958d2ee523a2206206994597c13d831ec7';
const usdt = new ERC20TokenContract(USDT_ADDRESS, env.provider, env.txDefaults);
const LON_ADDRESS = '0x0000000000095413afc295d19edeb1ad7b71c952';
const PLP_SANDBOX_ADDRESS = '0x407b4128e9ecad8769b2332312a9f655cb9f5f3a';
const WETH_DAI_PLP_ADDRESS = '0x1db681925786441ba82adefac7bf492089665ca0';
const WETH_USDC_PLP_ADDRESS = '0x8463c03c0c57ff19fa8b431e0d3a34e2df89888e';
const USDC_USDT_PLP_ADDRESS = '0xc340ef96449514cea4dfa11d847a06d7f03d437c';
const BALANCER_WETH_DAI = '0x8b6e6e7b5b3801fed2cafd4b22b8a16c2f2db21a';
const CURVE_BRIDGE_SOURCE_ID = encodeBridgeSourceId(BridgeProtocol.Curve, 'Curve');
const BALANCER_BRIDGE_SOURCE_ID = encodeBridgeSourceId(BridgeProtocol.Bancor, 'Balancer');
const fqtNonce = findTransformerNonce(
'0xfa6282736af206cb4cfc5cb786d82aecdf1186f9',
'0x39dce47a67ad34344eab877eae3ef1fa2a1d50bb',
);
const payTakerNonce = findTransformerNonce(
'0x4638a7ebe75b911b995d0ec73a81e4f85f41f24e',
'0x39dce47a67ad34344eab877eae3ef1fa2a1d50bb',
);
let zeroEx: IZeroExContract;
let multiplex: MultiplexFeatureContract;
let rfqMaker: string;
let flashWalletAddress: string;
before(async () => {
const erc20Abis = _.mapValues(erc20Artifacts, v => v.compilerOutput.abi);
[rfqMaker] = await env.getAccountAddressesAsync();
zeroEx = new IZeroExContract('0xdef1c0ded9bec7f1a1670819833240f027b25eff', env.provider, env.txDefaults, {
...abis,
...erc20Abis,
});
flashWalletAddress = await zeroEx.getTransformWallet().callAsync();
const registry = new SimpleFunctionRegistryFeatureContract(zeroEx.address, env.provider, env.txDefaults, {
...abis,
...erc20Abis,
});
multiplex = new MultiplexFeatureContract(zeroEx.address, env.provider, env.txDefaults, {
...abis,
...erc20Abis,
});
const multiplexImpl = await MultiplexFeatureContract.deployFrom0xArtifactAsync(
artifacts.MultiplexFeature,
env.provider,
env.txDefaults,
artifacts,
zeroEx.address,
WETH_ADDRESS,
PLP_SANDBOX_ADDRESS,
);
await registry
.extend(multiplex.getSelector('batchFill'), multiplexImpl.address)
.awaitTransactionSuccessAsync({ from: EP_GOVERNOR, gasPrice: 0 }, { shouldValidate: false });
await registry
.extend(multiplex.getSelector('multiHopFill'), multiplexImpl.address)
.awaitTransactionSuccessAsync({ from: EP_GOVERNOR, gasPrice: 0 }, { shouldValidate: false });
await dai
.approve(zeroEx.address, constants.MAX_UINT256)
.awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false });
await weth
.transfer(rfqMaker, toBaseUnitAmount(100))
.awaitTransactionSuccessAsync({ from: WETH_WALLET, gasPrice: 0 }, { shouldValidate: false });
await weth
.approve(zeroEx.address, constants.MAX_UINT256)
.awaitTransactionSuccessAsync({ from: rfqMaker, gasPrice: 0 }, { shouldValidate: false });
});
describe('batchFill', () => {
let rfqDataEncoder: AbiEncoder.DataType;
let uniswapCall: WrappedBatchCall;
let sushiswapCall: WrappedBatchCall;
let plpCall: WrappedBatchCall;
let rfqCall: WrappedBatchCall;
let rfqOrder: RfqOrder;
before(async () => {
rfqDataEncoder = AbiEncoder.create([
{ name: 'order', type: 'tuple', components: RfqOrder.STRUCT_ABI },
{ name: 'signature', type: 'tuple', components: SIGNATURE_ABI },
]);
rfqOrder = getRandomRfqOrder({
maker: rfqMaker,
verifyingContract: zeroEx.address,
chainId: 1,
takerToken: DAI_ADDRESS,
makerToken: WETH_ADDRESS,
makerAmount: toBaseUnitAmount(100),
takerAmount: toBaseUnitAmount(100),
txOrigin: DAI_WALLET,
});
rfqCall = {
selector: zeroEx.getSelector('_fillRfqOrder'),
sellAmount: toBaseUnitAmount(1),
data: rfqDataEncoder.encode({
order: rfqOrder,
signature: await rfqOrder.getSignatureWithProviderAsync(env.provider),
}),
};
const uniswapDataEncoder = AbiEncoder.create([
{ name: 'tokens', type: 'address[]' },
{ name: 'isSushi', type: 'bool' },
]);
const plpDataEncoder = AbiEncoder.create([
{ name: 'provider', type: 'address' },
{ name: 'auxiliaryData', type: 'bytes' },
]);
uniswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
sellAmount: toBaseUnitAmount(1.01),
data: uniswapDataEncoder.encode({ tokens: [DAI_ADDRESS, WETH_ADDRESS], isSushi: false }),
};
sushiswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
sellAmount: toBaseUnitAmount(1.02),
data: uniswapDataEncoder.encode({ tokens: [DAI_ADDRESS, WETH_ADDRESS], isSushi: true }),
};
plpCall = {
selector: multiplex.getSelector('_sellToLiquidityProvider'),
sellAmount: toBaseUnitAmount(1.03),
data: plpDataEncoder.encode({
provider: WETH_DAI_PLP_ADDRESS,
auxiliaryData: constants.NULL_BYTES,
}),
};
});
it('MultiplexFeature.batchFill(RFQ, unused Uniswap fallback)', async () => {
const batchFillData = {
inputToken: DAI_ADDRESS,
outputToken: WETH_ADDRESS,
sellAmount: rfqCall.sellAmount,
calls: [rfqCall, uniswapCall],
};
const tx = await multiplex
.batchFill(batchFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false });
logUtils.log(`${tx.gasUsed} gas used`);
const [rfqEvent] = filterLogsToArguments<IZeroExRfqOrderFilledEventArgs>(
tx.logs,
IZeroExEvents.RfqOrderFilled,
);
expect(rfqEvent.maker).to.equal(rfqMaker);
expect(rfqEvent.taker).to.equal(DAI_WALLET);
expect(rfqEvent.makerToken).to.equal(WETH_ADDRESS);
expect(rfqEvent.takerToken).to.equal(DAI_ADDRESS);
expect(rfqEvent.takerTokenFilledAmount).to.bignumber.equal(rfqCall.sellAmount);
expect(rfqEvent.makerTokenFilledAmount).to.bignumber.equal(rfqCall.sellAmount);
});
it('MultiplexFeature.batchFill(expired RFQ, Uniswap fallback)', async () => {
const expiredRfqOrder = getRandomRfqOrder({
maker: rfqMaker,
verifyingContract: zeroEx.address,
chainId: 1,
takerToken: DAI_ADDRESS,
makerToken: WETH_ADDRESS,
makerAmount: toBaseUnitAmount(100),
takerAmount: toBaseUnitAmount(100),
txOrigin: DAI_WALLET,
expiry: new BigNumber(0),
});
const expiredRfqCall = {
selector: zeroEx.getSelector('_fillRfqOrder'),
sellAmount: toBaseUnitAmount(1.23),
data: rfqDataEncoder.encode({
order: expiredRfqOrder,
signature: await expiredRfqOrder.getSignatureWithProviderAsync(env.provider),
}),
};
const batchFillData = {
inputToken: DAI_ADDRESS,
outputToken: WETH_ADDRESS,
sellAmount: expiredRfqCall.sellAmount,
calls: [expiredRfqCall, uniswapCall],
};
const tx = await multiplex
.batchFill(batchFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false });
logUtils.log(`${tx.gasUsed} gas used`);
const [uniswapEvent] = filterLogsToArguments<IUniswapV2PairSwapEventArgs>(
tx.logs,
IUniswapV2PairEvents.Swap,
);
expect(uniswapEvent.sender, 'Uniswap Swap event sender').to.equal(zeroEx.address);
expect(uniswapEvent.to, 'Uniswap Swap event to').to.equal(DAI_WALLET);
expect(
BigNumber.max(uniswapEvent.amount0In, uniswapEvent.amount1In),
'Uniswap Swap event input amount',
).to.bignumber.equal(uniswapCall.sellAmount);
expect(
BigNumber.max(uniswapEvent.amount0Out, uniswapEvent.amount1Out),
'Uniswap Swap event output amount',
).to.bignumber.gt(0);
});
it('MultiplexFeature.batchFill(expired RFQ, Balancer FQT fallback)', async () => {
const expiredRfqOrder = getRandomRfqOrder({
maker: rfqMaker,
verifyingContract: zeroEx.address,
chainId: 1,
takerToken: DAI_ADDRESS,
makerToken: WETH_ADDRESS,
makerAmount: toBaseUnitAmount(100),
takerAmount: toBaseUnitAmount(100),
txOrigin: DAI_WALLET,
expiry: new BigNumber(0),
});
const expiredRfqCall = {
selector: zeroEx.getSelector('_fillRfqOrder'),
sellAmount: toBaseUnitAmount(1.23),
data: rfqDataEncoder.encode({
order: expiredRfqOrder,
signature: await expiredRfqOrder.getSignatureWithProviderAsync(env.provider),
}),
};
const poolEncoder = AbiEncoder.create([{ name: 'poolAddress', type: 'address' }]);
const fqtData = encodeFillQuoteTransformerData({
side: FillQuoteTransformerSide.Sell,
sellToken: DAI_ADDRESS,
buyToken: WETH_ADDRESS,
bridgeOrders: [
{
source: BALANCER_BRIDGE_SOURCE_ID,
takerTokenAmount: expiredRfqCall.sellAmount,
makerTokenAmount: expiredRfqCall.sellAmount,
bridgeData: poolEncoder.encode([BALANCER_WETH_DAI]),
},
],
limitOrders: [],
rfqOrders: [],
fillSequence: [FillQuoteTransformerOrderType.Bridge],
fillAmount: expiredRfqCall.sellAmount,
refundReceiver: constants.NULL_ADDRESS,
});
const payTakerData = encodePayTakerTransformerData({
tokens: [WETH_ADDRESS],
amounts: [constants.MAX_UINT256],
});
const transformERC20Encoder = AbiEncoder.create([
{
name: 'transformations',
type: 'tuple[]',
components: [{ name: 'deploymentNonce', type: 'uint32' }, { name: 'data', type: 'bytes' }],
},
{ name: 'ethValue', type: 'uint256' },
]);
const balancerFqtCall = {
selector: zeroEx.getSelector('_transformERC20'),
sellAmount: expiredRfqCall.sellAmount,
data: transformERC20Encoder.encode({
transformations: [
{
deploymentNonce: fqtNonce,
data: fqtData,
},
{
deploymentNonce: payTakerNonce,
data: payTakerData,
},
],
ethValue: constants.ZERO_AMOUNT,
}),
};
const batchFillData = {
inputToken: DAI_ADDRESS,
outputToken: WETH_ADDRESS,
sellAmount: expiredRfqCall.sellAmount,
calls: [expiredRfqCall, balancerFqtCall],
};
const tx = await multiplex
.batchFill(batchFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false });
logUtils.log(`${tx.gasUsed} gas used`);
const [bridgeFillEvent] = filterLogsToArguments<BridgeAdapterBridgeFillEventArgs>(
tx.logs,
BridgeAdapterEvents.BridgeFill,
);
expect(bridgeFillEvent.source).to.bignumber.equal(BALANCER_BRIDGE_SOURCE_ID);
expect(bridgeFillEvent.inputToken).to.equal(DAI_ADDRESS);
expect(bridgeFillEvent.outputToken).to.equal(WETH_ADDRESS);
expect(bridgeFillEvent.inputTokenAmount).to.bignumber.equal(expiredRfqCall.sellAmount);
expect(bridgeFillEvent.outputTokenAmount).to.bignumber.gt(0);
});
it('MultiplexFeature.batchFill(Sushiswap, PLP, Uniswap, RFQ)', async () => {
const batchFillData = {
inputToken: DAI_ADDRESS,
outputToken: WETH_ADDRESS,
sellAmount: BigNumber.sum(
sushiswapCall.sellAmount,
plpCall.sellAmount,
uniswapCall.sellAmount,
rfqCall.sellAmount,
),
calls: [sushiswapCall, plpCall, uniswapCall, rfqCall],
};
const tx = await multiplex
.batchFill(batchFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false });
logUtils.log(`${tx.gasUsed} gas used`);
const [sushiswapEvent, uniswapEvent] = filterLogsToArguments<IUniswapV2PairSwapEventArgs>(
tx.logs,
IUniswapV2PairEvents.Swap,
);
expect(sushiswapEvent.sender, 'Sushiswap Swap event sender').to.equal(zeroEx.address);
expect(sushiswapEvent.to, 'Sushiswap Swap event to').to.equal(DAI_WALLET);
expect(
BigNumber.max(sushiswapEvent.amount0In, sushiswapEvent.amount1In),
'Sushiswap Swap event input amount',
).to.bignumber.equal(sushiswapCall.sellAmount);
expect(
BigNumber.max(sushiswapEvent.amount0Out, sushiswapEvent.amount1Out),
'Sushiswap Swap event output amount',
).to.bignumber.gt(0);
expect(uniswapEvent.sender, 'Uniswap Swap event sender').to.equal(zeroEx.address);
expect(uniswapEvent.to, 'Uniswap Swap event to').to.equal(DAI_WALLET);
expect(
BigNumber.max(uniswapEvent.amount0In, uniswapEvent.amount1In),
'Uniswap Swap event input amount',
).to.bignumber.equal(uniswapCall.sellAmount);
expect(
BigNumber.max(uniswapEvent.amount0Out, uniswapEvent.amount1Out),
'Uniswap Swap event output amount',
).to.bignumber.gt(0);
const [plpEvent] = filterLogsToArguments<MultiplexFeatureLiquidityProviderSwapEventArgs>(
tx.logs,
MultiplexFeatureEvents.LiquidityProviderSwap,
);
expect(plpEvent.inputToken, 'LiquidityProviderSwap event inputToken').to.equal(batchFillData.inputToken);
expect(plpEvent.outputToken, 'LiquidityProviderSwap event outputToken').to.equal(batchFillData.outputToken);
expect(plpEvent.inputTokenAmount, 'LiquidityProviderSwap event inputToken').to.bignumber.equal(
plpCall.sellAmount,
);
expect(plpEvent.outputTokenAmount, 'LiquidityProviderSwap event outputTokenAmount').to.bignumber.gt(0);
expect(plpEvent.provider, 'LiquidityProviderSwap event provider address').to.equal(WETH_DAI_PLP_ADDRESS);
expect(plpEvent.recipient, 'LiquidityProviderSwap event recipient address').to.equal(DAI_WALLET);
const [rfqEvent] = filterLogsToArguments<IZeroExRfqOrderFilledEventArgs>(
tx.logs,
IZeroExEvents.RfqOrderFilled,
);
expect(rfqEvent.maker).to.equal(rfqMaker);
expect(rfqEvent.taker).to.equal(DAI_WALLET);
expect(rfqEvent.makerToken).to.equal(WETH_ADDRESS);
expect(rfqEvent.takerToken).to.equal(DAI_ADDRESS);
expect(rfqEvent.takerTokenFilledAmount).to.bignumber.equal(rfqCall.sellAmount);
expect(rfqEvent.makerTokenFilledAmount).to.bignumber.equal(rfqCall.sellAmount);
});
});
describe('multiHopFill', () => {
let uniswapDataEncoder: AbiEncoder.DataType;
let plpDataEncoder: AbiEncoder.DataType;
let curveEncoder: AbiEncoder.DataType;
let transformERC20Encoder: AbiEncoder.DataType;
let batchFillEncoder: AbiEncoder.DataType;
let multiHopFillEncoder: AbiEncoder.DataType;
before(async () => {
uniswapDataEncoder = AbiEncoder.create([
{ name: 'tokens', type: 'address[]' },
{ name: 'isSushi', type: 'bool' },
]);
plpDataEncoder = AbiEncoder.create([
{ name: 'provider', type: 'address' },
{ name: 'auxiliaryData', type: 'bytes' },
]);
curveEncoder = AbiEncoder.create([
{ name: 'curveAddress', type: 'address' },
{ name: 'exchangeFunctionSelector', type: 'bytes4' },
{ name: 'fromTokenIdx', type: 'int128' },
{ name: 'toTokenIdx', type: 'int128' },
]);
transformERC20Encoder = AbiEncoder.create([
{
name: 'transformations',
type: 'tuple[]',
components: [{ name: 'deploymentNonce', type: 'uint32' }, { name: 'data', type: 'bytes' }],
},
{ name: 'ethValue', type: 'uint256' },
]);
batchFillEncoder = AbiEncoder.create([
{
name: 'calls',
type: 'tuple[]',
components: [
{ name: 'selector', type: 'bytes4' },
{ name: 'sellAmount', type: 'uint256' },
{ name: 'data', type: 'bytes' },
],
},
{ name: 'ethValue', type: 'uint256' },
]);
multiHopFillEncoder = AbiEncoder.create([
{ name: 'tokens', type: 'address[]' },
{
name: 'calls',
type: 'tuple[]',
components: [{ name: 'selector', type: 'bytes4' }, { name: 'data', type: 'bytes' }],
},
{ name: 'ethValue', type: 'uint256' },
]);
});
it('MultiplexFeature.multiHopFill(DAI Curve> USDC Uni> WETH unwrap> ETH)', async () => {
const sellAmount = toBaseUnitAmount(1000000); // 1M DAI
const fqtData = encodeFillQuoteTransformerData({
side: FillQuoteTransformerSide.Sell,
sellToken: DAI_ADDRESS,
buyToken: USDC_ADDRESS,
bridgeOrders: [
{
source: CURVE_BRIDGE_SOURCE_ID,
takerTokenAmount: sellAmount,
makerTokenAmount: sellAmount,
bridgeData: curveEncoder.encode([
'0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', // 3-pool
'0x3df02124', // `exchange` selector
0, // DAI
1, // USDC
]),
},
],
limitOrders: [],
rfqOrders: [],
fillSequence: [FillQuoteTransformerOrderType.Bridge],
fillAmount: sellAmount,
refundReceiver: constants.NULL_ADDRESS,
});
const payTakerData = encodePayTakerTransformerData({
tokens: [USDC_ADDRESS],
amounts: [constants.MAX_UINT256],
});
const curveFqtCall = {
selector: zeroEx.getSelector('_transformERC20'),
sellAmount,
data: transformERC20Encoder.encode({
transformations: [
{
deploymentNonce: fqtNonce,
data: fqtData,
},
{
deploymentNonce: payTakerNonce,
data: payTakerData,
},
],
ethValue: constants.ZERO_AMOUNT,
}),
};
const uniswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
data: uniswapDataEncoder.encode({ tokens: [USDC_ADDRESS, WETH_ADDRESS], isSushi: false }),
};
const unwrapEthCall = {
selector: weth.getSelector('withdraw'),
data: constants.NULL_BYTES,
};
const multiHopFillData = {
tokens: [DAI_ADDRESS, USDC_ADDRESS, WETH_ADDRESS, ETH_TOKEN_ADDRESS],
sellAmount,
calls: [curveFqtCall, uniswapCall, unwrapEthCall],
};
const tx = await multiplex
.multiHopFill(multiHopFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false });
logUtils.log(`${tx.gasUsed} gas used`);
const [bridgeFillEvent] = filterLogsToArguments<BridgeAdapterBridgeFillEventArgs>(
tx.logs,
BridgeAdapterEvents.BridgeFill,
);
expect(bridgeFillEvent.source).to.bignumber.equal(CURVE_BRIDGE_SOURCE_ID);
expect(bridgeFillEvent.inputToken).to.equal(DAI_ADDRESS);
expect(bridgeFillEvent.outputToken).to.equal(USDC_ADDRESS);
expect(bridgeFillEvent.inputTokenAmount).to.bignumber.equal(sellAmount);
expect(bridgeFillEvent.outputTokenAmount).to.bignumber.gt(0);
const [uniswapEvent] = filterLogsToArguments<IUniswapV2PairSwapEventArgs>(
tx.logs,
IUniswapV2PairEvents.Swap,
);
expect(uniswapEvent.sender, 'Uniswap Swap event sender').to.equal(zeroEx.address);
expect(uniswapEvent.to, 'Uniswap Swap event to').to.equal(zeroEx.address);
const uniswapInputAmount = BigNumber.max(uniswapEvent.amount0In, uniswapEvent.amount1In);
expect(uniswapInputAmount, 'Uniswap Swap event input amount').to.bignumber.equal(
bridgeFillEvent.outputTokenAmount,
);
const uniswapOutputAmount = BigNumber.max(uniswapEvent.amount0Out, uniswapEvent.amount1Out);
expect(uniswapOutputAmount, 'Uniswap Swap event output amount').to.bignumber.gt(0);
const [wethWithdrawalEvent] = filterLogsToArguments<WETH9WithdrawalEventArgs>(
tx.logs,
WETH9Events.Withdrawal,
);
expect(wethWithdrawalEvent._owner, 'WETH Withdrawal event _owner').to.equal(zeroEx.address);
expect(wethWithdrawalEvent._value, 'WETH Withdrawal event _value').to.bignumber.equal(uniswapOutputAmount);
});
it('MultiplexFeature.multiHopFill(ETH wrap-> WETH Uni> USDC Curve> DAI)', async () => {
const sellAmount = toBaseUnitAmount(1); // 1 ETH
const fqtData = encodeFillQuoteTransformerData({
side: FillQuoteTransformerSide.Sell,
sellToken: USDC_ADDRESS,
buyToken: DAI_ADDRESS,
bridgeOrders: [
{
source: CURVE_BRIDGE_SOURCE_ID,
takerTokenAmount: constants.MAX_UINT256,
makerTokenAmount: constants.MAX_UINT256,
bridgeData: curveEncoder.encode([
'0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', // 3-pool
'0x3df02124', // `exchange` selector
1, // USDC
0, // DAI
]),
},
],
limitOrders: [],
rfqOrders: [],
fillSequence: [FillQuoteTransformerOrderType.Bridge],
fillAmount: constants.MAX_UINT256,
refundReceiver: constants.NULL_ADDRESS,
});
const payTakerData = encodePayTakerTransformerData({
tokens: [DAI_ADDRESS],
amounts: [constants.MAX_UINT256],
});
const curveFqtCall = {
selector: zeroEx.getSelector('_transformERC20'),
data: transformERC20Encoder.encode({
transformations: [
{
deploymentNonce: fqtNonce,
data: fqtData,
},
{
deploymentNonce: payTakerNonce,
data: payTakerData,
},
],
ethValue: constants.ZERO_AMOUNT,
}),
};
const uniswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, USDC_ADDRESS], isSushi: false }),
};
const wrapEthCall = {
selector: weth.getSelector('deposit'),
data: constants.NULL_BYTES,
};
const multiHopFillData = {
tokens: [ETH_TOKEN_ADDRESS, WETH_ADDRESS, USDC_ADDRESS, DAI_ADDRESS],
sellAmount,
calls: [wrapEthCall, uniswapCall, curveFqtCall],
};
const tx = await multiplex
.multiHopFill(multiHopFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync(
{ from: rfqMaker, gasPrice: 0, value: sellAmount },
{ shouldValidate: false },
);
logUtils.log(`${tx.gasUsed} gas used`);
const [wethDepositEvent] = filterLogsToArguments<WETH9DepositEventArgs>(tx.logs, WETH9Events.Deposit);
expect(wethDepositEvent._owner, 'WETH Deposit event _owner').to.equal(zeroEx.address);
expect(wethDepositEvent._value, 'WETH Deposit event _value').to.bignumber.equal(sellAmount);
const [uniswapEvent] = filterLogsToArguments<IUniswapV2PairSwapEventArgs>(
tx.logs,
IUniswapV2PairEvents.Swap,
);
expect(uniswapEvent.sender, 'Uniswap Swap event sender').to.equal(zeroEx.address);
expect(uniswapEvent.to, 'Uniswap Swap event to').to.equal(flashWalletAddress);
const uniswapInputAmount = BigNumber.max(uniswapEvent.amount0In, uniswapEvent.amount1In);
expect(uniswapInputAmount, 'Uniswap Swap event input amount').to.bignumber.equal(sellAmount);
const uniswapOutputAmount = BigNumber.max(uniswapEvent.amount0Out, uniswapEvent.amount1Out);
expect(uniswapOutputAmount, 'Uniswap Swap event output amount').to.bignumber.gt(0);
const [bridgeFillEvent] = filterLogsToArguments<BridgeAdapterBridgeFillEventArgs>(
tx.logs,
BridgeAdapterEvents.BridgeFill,
);
expect(bridgeFillEvent.source).to.bignumber.equal(CURVE_BRIDGE_SOURCE_ID);
expect(bridgeFillEvent.inputToken).to.equal(USDC_ADDRESS);
expect(bridgeFillEvent.outputToken).to.equal(DAI_ADDRESS);
expect(bridgeFillEvent.inputTokenAmount).to.bignumber.equal(uniswapOutputAmount);
expect(bridgeFillEvent.outputTokenAmount).to.bignumber.gt(0);
});
it.skip('MultiplexFeature.multiHopFill() complex scenario', async () => {
/*
/PLP> USDC
/ \
/ PLP
/Uni (via USDC)\
/ V
ETH wrap> WETH Uni/Sushi> USDT Sushi> LON
\ ^
Uni /
*/
// Taker has to have approved the EP for the intermediate tokens :/
await weth
.approve(zeroEx.address, constants.MAX_UINT256)
.awaitTransactionSuccessAsync({ from: rfqMaker, gasPrice: 0 }, { shouldValidate: false });
await usdt
.approve(zeroEx.address, constants.MAX_UINT256)
.awaitTransactionSuccessAsync({ from: rfqMaker, gasPrice: 0 }, { shouldValidate: false });
const sellAmount = toBaseUnitAmount(1); // 1 ETH
const wethUsdcPlpCall = {
selector: multiplex.getSelector('_sellToLiquidityProvider'),
data: plpDataEncoder.encode({
provider: WETH_USDC_PLP_ADDRESS,
auxiliaryData: constants.NULL_BYTES,
}),
};
const usdcUsdtPlpCall = {
selector: multiplex.getSelector('_sellToLiquidityProvider'),
data: plpDataEncoder.encode({
provider: USDC_USDT_PLP_ADDRESS,
auxiliaryData: constants.NULL_BYTES,
}),
};
const wethUsdcUsdtMultiHopCall = {
selector: multiplex.getSelector('_multiHopFill'),
sellAmount: encodeFractionalFillAmount(0.25),
data: multiHopFillEncoder.encode({
tokens: [WETH_ADDRESS, USDC_ADDRESS, USDT_ADDRESS],
calls: [wethUsdcPlpCall, usdcUsdtPlpCall],
ethValue: constants.ZERO_AMOUNT,
}),
};
const wethUsdcUsdtUniswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
sellAmount: encodeFractionalFillAmount(0.25),
data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, USDC_ADDRESS, USDT_ADDRESS], isSushi: false }),
};
const wethUsdtUniswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
sellAmount: encodeFractionalFillAmount(0.25),
data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, USDT_ADDRESS], isSushi: false }),
};
const wethUsdtSushiswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
sellAmount: encodeFractionalFillAmount(0.25),
data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, USDT_ADDRESS], isSushi: true }),
};
const wethUsdtBatchCall = {
selector: multiplex.getSelector('_batchFill'),
data: batchFillEncoder.encode({
calls: [
wethUsdcUsdtMultiHopCall,
wethUsdcUsdtUniswapCall,
wethUsdtUniswapCall,
wethUsdtSushiswapCall,
],
ethValue: constants.ZERO_AMOUNT,
}),
};
const usdtLonSushiCall = {
selector: multiplex.getSelector('_sellToUniswap'),
data: uniswapDataEncoder.encode({ tokens: [USDT_ADDRESS, LON_ADDRESS], isSushi: true }),
};
const wethUsdtLonMultiHopCall = {
selector: multiplex.getSelector('_multiHopFill'),
sellAmount: encodeFractionalFillAmount(0.8),
data: multiHopFillEncoder.encode({
tokens: [WETH_ADDRESS, USDT_ADDRESS],
calls: [wethUsdtBatchCall, usdtLonSushiCall],
ethValue: constants.ZERO_AMOUNT,
}),
};
const wethLonUniswapCall = {
selector: multiplex.getSelector('_sellToUniswap'),
sellAmount: encodeFractionalFillAmount(0.2),
data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, LON_ADDRESS], isSushi: false }),
};
const wethLonBatchFillCall = {
selector: multiplex.getSelector('_batchFill'),
data: batchFillEncoder.encode({
calls: [wethUsdtLonMultiHopCall, wethLonUniswapCall],
ethValue: constants.ZERO_AMOUNT,
}),
};
const wrapEthCall = {
selector: weth.getSelector('deposit'),
data: constants.NULL_BYTES,
};
const multiHopFillData = {
tokens: [ETH_TOKEN_ADDRESS, WETH_ADDRESS, LON_ADDRESS],
sellAmount,
calls: [wrapEthCall, wethLonBatchFillCall],
};
const tx = await multiplex
.multiHopFill(multiHopFillData, constants.ZERO_AMOUNT)
.awaitTransactionSuccessAsync(
{ from: rfqMaker, gasPrice: 0, value: sellAmount },
{ shouldValidate: false },
);
logUtils.log(`${tx.gasUsed} gas used`);
});
});
});