UNPKG

@uniswap/universal-router

Version:

Smart contracts for Universal Router

897 lines (789 loc) 33.3 kB
import type { Contract } from '@ethersproject/contracts' import { Pair } from '@uniswap/v2-sdk' import { expect } from './shared/expect' import { BigNumber } from 'ethers' import { IPermit2, PoolManager, PositionManager, UniversalRouter } from '../../typechain' import { abi as TOKEN_ABI } from '../../artifacts/solmate/src/tokens/ERC20.sol/ERC20.json' import { resetFork, WETH, DAI, USDC, USDT, PERMIT2 } from './shared/mainnetForkHelpers' import { ADDRESS_THIS, ALICE_ADDRESS, CONTRACT_BALANCE, DEADLINE, ETH_ADDRESS, MAX_UINT, MAX_UINT160, MSG_SENDER, OPEN_DELTA, SOURCE_MSG_SENDER, SOURCE_ROUTER, } from './shared/constants' import { expandTo18DecimalsBN, expandTo6DecimalsBN } from './shared/helpers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import deployUniversalRouter from './shared/deployUniversalRouter' import { RoutePlanner, CommandType } from './shared/planner' import hre from 'hardhat' import { getPermitBatchSignature } from './shared/protocolHelpers/permit2' import { encodePathExactInput, encodePathExactOutput } from './shared/swapRouter02Helpers' import { executeRouter } from './shared/executeRouter' import { Actions, V4Planner } from './shared/v4Planner' import { addLiquidityToV4Pool, DAI_USDC, deployV4PoolManager, encodeMultihopExactInPath, ETH_USDC, initializeV4Pool, USDC_WETH, } from './shared/v4Helpers' const { ethers } = hre describe('Uniswap V2, V3, and V4 Tests:', () => { let alice: SignerWithAddress let bob: SignerWithAddress let router: UniversalRouter let permit2: IPermit2 let daiContract: Contract let wethContract: Contract let usdcContract: Contract let planner: RoutePlanner let v4Planner: V4Planner let v4PoolManager: PoolManager let v4PositionManager: PositionManager // current market ETH price at block const USD_ETH_PRICE = 3820 beforeEach(async () => { await resetFork() await hre.network.provider.request({ method: 'hardhat_impersonateAccount', params: [ALICE_ADDRESS], }) alice = await ethers.getSigner(ALICE_ADDRESS) bob = (await ethers.getSigners())[1] daiContract = new ethers.Contract(DAI.address, TOKEN_ABI, bob) wethContract = new ethers.Contract(WETH.address, TOKEN_ABI, bob) usdcContract = new ethers.Contract(USDC.address, TOKEN_ABI, bob) permit2 = PERMIT2.connect(bob) as IPermit2 v4PoolManager = (await deployV4PoolManager(bob.address)).connect(bob) as PoolManager router = (await deployUniversalRouter(undefined, v4PoolManager.address)).connect(bob) as UniversalRouter v4PositionManager = (await ethers.getContractAt('PositionManager', await router.V4_POSITION_MANAGER())).connect( bob ) as PositionManager planner = new RoutePlanner() v4Planner = new V4Planner() // alice gives bob some tokens await daiContract.connect(alice).transfer(bob.address, expandTo18DecimalsBN(1000000)) await wethContract.connect(alice).transfer(bob.address, expandTo18DecimalsBN(1000)) await usdcContract.connect(alice).transfer(bob.address, expandTo6DecimalsBN(50000000)) // Bob max-approves the permit2 contract to access his DAI and WETH await daiContract.connect(bob).approve(permit2.address, MAX_UINT) await wethContract.connect(bob).approve(permit2.address, MAX_UINT) await usdcContract.connect(bob).approve(permit2.address, MAX_UINT) // for these tests Bob gives the router max approval on permit2 await permit2.approve(DAI.address, router.address, MAX_UINT160, DEADLINE) await permit2.approve(WETH.address, router.address, MAX_UINT160, DEADLINE) await permit2.approve(USDC.address, router.address, MAX_UINT160, DEADLINE) // for setting up pools, bob gives position manager approval on permit2 await permit2.approve(DAI.address, v4PositionManager.address, MAX_UINT160, DEADLINE) await permit2.approve(WETH.address, v4PositionManager.address, MAX_UINT160, DEADLINE) await permit2.approve(USDC.address, v4PositionManager.address, MAX_UINT160, DEADLINE) // bob initializes 3 v4 pools await initializeV4Pool(v4PoolManager, USDC_WETH.poolKey, USDC_WETH.price) await initializeV4Pool(v4PoolManager, DAI_USDC.poolKey, DAI_USDC.price) await initializeV4Pool(v4PoolManager, ETH_USDC.poolKey, ETH_USDC.price) // bob adds liquidity to the pools await addLiquidityToV4Pool(v4PositionManager, USDC_WETH, expandTo18DecimalsBN(2).toString(), bob) await addLiquidityToV4Pool(v4PositionManager, DAI_USDC, expandTo18DecimalsBN(400).toString(), bob) await addLiquidityToV4Pool(v4PositionManager, ETH_USDC, expandTo18DecimalsBN(0.1).toString(), bob) }) describe('Interleaving routes', () => { it('V3, then V2', async () => { const v3Tokens = [DAI.address, USDC.address] const v2Tokens = [USDC.address, WETH.address] const v3AmountIn: BigNumber = expandTo18DecimalsBN(5) const v3AmountOutMin = 0 const v2AmountOutMin = expandTo18DecimalsBN(0.0005) planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ Pair.getAddress(USDC, WETH), v3AmountIn, v3AmountOutMin, encodePathExactInput(v3Tokens), SOURCE_MSG_SENDER, ]) // amountIn of 0 because the USDC is already in the pair planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, v2AmountOutMin, v2Tokens, SOURCE_MSG_SENDER]) const { wethBalanceBefore, wethBalanceAfter, v2SwapEventArgs } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) const { amount1Out: wethTraded } = v2SwapEventArgs! expect(wethBalanceAfter.sub(wethBalanceBefore)).to.eq(wethTraded) }) it('V2, then V3', async () => { const v2Tokens = [DAI.address, USDC.address] const v3Tokens = [USDC.address, WETH.address] const v2AmountIn: BigNumber = expandTo18DecimalsBN(5) const v2AmountOutMin = 0 // doesnt matter how much USDC it is, what matters is the end of the trade const v3AmountOutMin = expandTo18DecimalsBN(0.0005) planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ ADDRESS_THIS, v2AmountIn, v2AmountOutMin, v2Tokens, SOURCE_MSG_SENDER, ]) planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ MSG_SENDER, CONTRACT_BALANCE, v3AmountOutMin, encodePathExactInput(v3Tokens), SOURCE_ROUTER, ]) const { wethBalanceBefore, wethBalanceAfter, v3SwapEventArgs } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) const { amount1: wethTraded } = v3SwapEventArgs! expect(wethBalanceAfter.sub(wethBalanceBefore)).to.eq(wethTraded.mul(-1)) }) }) describe('Split routes', () => { it('ERC20 --> ERC20 split V2 and V2 different routes, each two hop, with explicit permit transfer from', async () => { const route1 = [DAI.address, USDC.address, WETH.address] const route2 = [DAI.address, USDT.address, WETH.address] const v2AmountIn1: BigNumber = expandTo18DecimalsBN(20) const v2AmountIn2: BigNumber = expandTo18DecimalsBN(30) const minAmountOut1 = expandTo18DecimalsBN(0.005) const minAmountOut2 = expandTo18DecimalsBN(0.0075) // 1) transfer funds into DAI-USDC and DAI-USDT pairs to trade planner.addCommand(CommandType.PERMIT2_TRANSFER_FROM, [DAI.address, Pair.getAddress(DAI, USDC), v2AmountIn1]) planner.addCommand(CommandType.PERMIT2_TRANSFER_FROM, [DAI.address, Pair.getAddress(DAI, USDT), v2AmountIn2]) // 2) trade route1 and return tokens to bob planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut1, route1, SOURCE_MSG_SENDER]) // 3) trade route2 and return tokens to bob planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut2, route2, SOURCE_MSG_SENDER]) const { wethBalanceBefore, wethBalanceAfter } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) expect(wethBalanceAfter.sub(wethBalanceBefore)).to.be.gte(minAmountOut1.add(minAmountOut2)) }) it('ERC20 --> ERC20 split V2 and V2 different routes, each two hop, with explicit permit transfer from batch', async () => { const route1 = [DAI.address, USDC.address, WETH.address] const route2 = [DAI.address, USDT.address, WETH.address] const v2AmountIn1: BigNumber = expandTo18DecimalsBN(20) const v2AmountIn2: BigNumber = expandTo18DecimalsBN(30) const minAmountOut1 = expandTo18DecimalsBN(0.005) const minAmountOut2 = expandTo18DecimalsBN(0.0075) const BATCH_TRANSFER = [ { from: bob.address, to: Pair.getAddress(DAI, USDC), amount: v2AmountIn1, token: DAI.address, }, { from: bob.address, to: Pair.getAddress(DAI, USDT), amount: v2AmountIn2, token: DAI.address, }, ] // 1) transfer funds into DAI-USDC and DAI-USDT pairs to trade planner.addCommand(CommandType.PERMIT2_TRANSFER_FROM_BATCH, [BATCH_TRANSFER]) // 2) trade route1 and return tokens to bob planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut1, route1, SOURCE_MSG_SENDER]) // 3) trade route2 and return tokens to bob planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut2, route2, SOURCE_MSG_SENDER]) const { wethBalanceBefore, wethBalanceAfter } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) expect(wethBalanceAfter.sub(wethBalanceBefore)).to.be.gte(minAmountOut1.add(minAmountOut2)) }) it('ERC20 --> ERC20 split V2 and V2 different routes, each two hop, without explicit permit', async () => { const route1 = [DAI.address, USDC.address, WETH.address] const route2 = [DAI.address, USDT.address, WETH.address] const v2AmountIn1: BigNumber = expandTo18DecimalsBN(20) const v2AmountIn2: BigNumber = expandTo18DecimalsBN(30) const minAmountOut1 = expandTo18DecimalsBN(0.005) const minAmountOut2 = expandTo18DecimalsBN(0.0075) // 1) trade route1 and return tokens to bob planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ MSG_SENDER, v2AmountIn1, minAmountOut1, route1, SOURCE_MSG_SENDER, ]) // 2) trade route2 and return tokens to bob planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ MSG_SENDER, v2AmountIn2, minAmountOut2, route2, SOURCE_MSG_SENDER, ]) const { wethBalanceBefore, wethBalanceAfter } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) expect(wethBalanceAfter.sub(wethBalanceBefore)).to.be.gte(minAmountOut1.add(minAmountOut2)) }) it('PERMIT2 batch can silently fail', async () => { const v2AmountIn1: BigNumber = expandTo18DecimalsBN(20) const v2AmountIn2: BigNumber = expandTo18DecimalsBN(5) const BATCH_PERMIT = { details: [ { token: DAI.address, amount: v2AmountIn1, expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, { token: WETH.address, amount: v2AmountIn2, expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, ], spender: router.address, sigDeadline: DEADLINE, } const sig = await getPermitBatchSignature(BATCH_PERMIT, bob, permit2) // transfer funds into DAI-USDC and DAI-USDT pairs to trade // do not allow revert planner.addCommand(CommandType.PERMIT2_PERMIT_BATCH, [BATCH_PERMIT, sig]) // allow revert planner.addCommand(CommandType.PERMIT2_PERMIT_BATCH, [BATCH_PERMIT, sig], true) let nonce = (await permit2.allowance(bob.address, DAI.address, router.address)).nonce expect(nonce).to.eq(0) await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) nonce = (await permit2.allowance(bob.address, DAI.address, router.address)).nonce expect(nonce).to.eq(1) }) it('ERC20 --> ERC20 split V2 and V2 different routes, different input tokens, each two hop, with batch permit', async () => { const route1 = [DAI.address, WETH.address, USDC.address] const route2 = [WETH.address, DAI.address, USDC.address] const v2AmountIn1: BigNumber = expandTo18DecimalsBN(20) const v2AmountIn2: BigNumber = expandTo18DecimalsBN(5) const minAmountOut1 = BigNumber.from(0.005 * 10 ** 6) const minAmountOut2 = BigNumber.from(0.0075 * 10 ** 6) const BATCH_PERMIT = { details: [ { token: DAI.address, amount: v2AmountIn1, expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, { token: WETH.address, amount: v2AmountIn2, expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, ], spender: router.address, sigDeadline: DEADLINE, } const sig = await getPermitBatchSignature(BATCH_PERMIT, bob, permit2) // 1) transfer funds into DAI-USDC and DAI-USDT pairs to trade planner.addCommand(CommandType.PERMIT2_PERMIT_BATCH, [BATCH_PERMIT, sig]) // 2) trade route1 and return tokens to bob planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ MSG_SENDER, v2AmountIn1, minAmountOut1, route1, SOURCE_MSG_SENDER, ]) // 3) trade route2 and return tokens to bob planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ MSG_SENDER, v2AmountIn2, minAmountOut2, route2, SOURCE_MSG_SENDER, ]) const { usdcBalanceBefore, usdcBalanceAfter } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) expect(usdcBalanceAfter.sub(usdcBalanceBefore)).to.be.gte(minAmountOut1.add(minAmountOut2)) }) it('ERC20 --> ERC20 V3 trades with different input tokens with batch permit and batch transfer', async () => { const route1 = [DAI.address, WETH.address] const route2 = [WETH.address, USDC.address] const v3AmountIn1: BigNumber = expandTo18DecimalsBN(20) const v3AmountIn2: BigNumber = expandTo18DecimalsBN(5) const minAmountOut1WETH = BigNumber.from(0) const minAmountOut1USDC = BigNumber.from(0.005 * 10 ** 6) const minAmountOut2USDC = BigNumber.from(0.0075 * 10 ** 6) const BATCH_PERMIT = { details: [ { token: DAI.address, amount: v3AmountIn1, expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, { token: WETH.address, amount: v3AmountIn2, expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, ], spender: router.address, sigDeadline: DEADLINE, } const BATCH_TRANSFER = [ { from: bob.address, to: router.address, amount: v3AmountIn1, token: DAI.address, }, { from: bob.address, to: router.address, amount: v3AmountIn2, token: WETH.address, }, ] const sig = await getPermitBatchSignature(BATCH_PERMIT, bob, permit2) // 1) permit dai and weth to be spent by router planner.addCommand(CommandType.PERMIT2_PERMIT_BATCH, [BATCH_PERMIT, sig]) // 2) transfer dai and weth into router to use contract balance planner.addCommand(CommandType.PERMIT2_TRANSFER_FROM_BATCH, [BATCH_TRANSFER]) // v3SwapExactInput(recipient, amountIn, amountOutMin, path, payer); // 2) trade route1 and return tokens to router for the second trade planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ ADDRESS_THIS, CONTRACT_BALANCE, minAmountOut1WETH, encodePathExactInput(route1), SOURCE_ROUTER, ]) // 3) trade route2 and return tokens to bob planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ MSG_SENDER, CONTRACT_BALANCE, minAmountOut1USDC.add(minAmountOut2USDC), encodePathExactInput(route2), SOURCE_ROUTER, ]) const { usdcBalanceBefore, usdcBalanceAfter } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) expect(usdcBalanceAfter.sub(usdcBalanceBefore)).to.be.gte(minAmountOut1USDC.add(minAmountOut2USDC)) }) it('ERC20 --> ERC20 split V2 and V3, one hop', async () => { const tokens = [DAI.address, WETH.address] const v2AmountIn: BigNumber = expandTo18DecimalsBN(2) const v3AmountIn: BigNumber = expandTo18DecimalsBN(3) const minAmountOut = expandTo18DecimalsBN(0.0005) // V2 trades DAI for USDC, sending the tokens back to the router for v3 trade planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ADDRESS_THIS, v2AmountIn, 0, tokens, SOURCE_MSG_SENDER]) // V3 trades USDC for WETH, trading the whole balance, with a recipient of Alice planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ ADDRESS_THIS, v3AmountIn, 0, encodePathExactInput(tokens), SOURCE_MSG_SENDER, ]) // aggregate slippage check planner.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, minAmountOut]) const { wethBalanceBefore, wethBalanceAfter, v2SwapEventArgs, v3SwapEventArgs } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) const { amount1Out: wethOutV2 } = v2SwapEventArgs! let { amount1: wethOutV3 } = v3SwapEventArgs! // expect(daiBalanceBefore.sub(daiBalanceAfter)).to.eq(v2AmountIn.add(v3AmountIn)) // TODO: with permit2 can check from alice's balance expect(wethBalanceAfter.sub(wethBalanceBefore)).to.eq(wethOutV2.sub(wethOutV3)) }) it('ETH --> ERC20 split V2 and V3, one hop', async () => { const tokens = [WETH.address, USDC.address] const v2AmountIn: BigNumber = expandTo18DecimalsBN(2) const v3AmountIn: BigNumber = expandTo18DecimalsBN(3) const value = v2AmountIn.add(v3AmountIn) planner.addCommand(CommandType.WRAP_ETH, [ADDRESS_THIS, value]) planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ADDRESS_THIS, v2AmountIn, 0, tokens, SOURCE_ROUTER]) planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ ADDRESS_THIS, v3AmountIn, 0, encodePathExactInput(tokens), SOURCE_MSG_SENDER, ]) // aggregate slippage check planner.addCommand(CommandType.SWEEP, [USDC.address, MSG_SENDER, 0.0005 * 10 ** 6]) const { usdcBalanceBefore, usdcBalanceAfter, v2SwapEventArgs, v3SwapEventArgs } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract, value ) const { amount0Out: usdcOutV2 } = v2SwapEventArgs! let { amount0: usdcOutV3 } = v3SwapEventArgs! usdcOutV3 = usdcOutV3.mul(-1) expect(usdcBalanceAfter.sub(usdcBalanceBefore)).to.eq(usdcOutV2.add(usdcOutV3)) }) it('ERC20 --> ETH split V2 and V3, one hop', async () => { const tokens = [DAI.address, WETH.address] const v2AmountIn: BigNumber = expandTo18DecimalsBN(20) const v3AmountIn: BigNumber = expandTo18DecimalsBN(30) planner.addCommand(CommandType.V2_SWAP_EXACT_IN, [ADDRESS_THIS, v2AmountIn, 0, tokens, SOURCE_MSG_SENDER]) planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ ADDRESS_THIS, v3AmountIn, 0, encodePathExactInput(tokens), SOURCE_MSG_SENDER, ]) // aggregate slippage check planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, expandTo18DecimalsBN(0.0005)]) const { ethBalanceBefore, ethBalanceAfter, gasSpent, v2SwapEventArgs, v3SwapEventArgs } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) const { amount1Out: wethOutV2 } = v2SwapEventArgs! let { amount1: wethOutV3 } = v3SwapEventArgs! wethOutV3 = wethOutV3.mul(-1) expect(ethBalanceAfter.sub(ethBalanceBefore)).to.eq(wethOutV2.add(wethOutV3).sub(gasSpent)) }) it('ERC20 --> ETH split V2 and V3, exactOut, one hop', async () => { const tokens = [DAI.address, WETH.address] const v2AmountOut: BigNumber = expandTo18DecimalsBN(0.5) const v3AmountOut: BigNumber = expandTo18DecimalsBN(1) const path = encodePathExactOutput(tokens) const maxAmountIn = expandTo18DecimalsBN(4000) const fullAmountOut = v2AmountOut.add(v3AmountOut) planner.addCommand(CommandType.V2_SWAP_EXACT_OUT, [ ADDRESS_THIS, v2AmountOut, maxAmountIn, [DAI.address, WETH.address], SOURCE_MSG_SENDER, ]) planner.addCommand(CommandType.V3_SWAP_EXACT_OUT, [ ADDRESS_THIS, v3AmountOut, maxAmountIn, path, SOURCE_MSG_SENDER, ]) // aggregate slippage check planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, fullAmountOut]) const { ethBalanceBefore, ethBalanceAfter, gasSpent } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) // TODO: permit2 test alice doesn't send more than maxAmountIn DAI expect(ethBalanceAfter.sub(ethBalanceBefore)).to.eq(fullAmountOut.sub(gasSpent)) }) it('ERC20 --> ERC20 split V4 and V4 different routes, with wrap, aggregate slippage', async () => { // route 1: DAI -> USDC -> WETH // route 2: DAI -> USDC -> ETH, then router wraps ETH -> WETH const route1 = [DAI_USDC.poolKey, USDC_WETH.poolKey] const route2 = [DAI_USDC.poolKey, ETH_USDC.poolKey] const v4AmountIn1 = expandTo18DecimalsBN(100) const v4AmountIn2 = expandTo18DecimalsBN(150) const aggregateMinOut = expandTo18DecimalsBN(250 / Math.floor(USD_ETH_PRICE * 1.01)) let currencyIn = daiContract.address // add first split to v4 planner v4Planner.addAction(Actions.SWAP_EXACT_IN, [ { currencyIn, path: encodeMultihopExactInPath(route1, currencyIn), amountIn: v4AmountIn1, amountOutMinimum: 0, }, ]) // add second split to v4 planner v4Planner.addAction(Actions.SWAP_EXACT_IN, [ { currencyIn, path: encodeMultihopExactInPath(route2, currencyIn), amountIn: v4AmountIn2, amountOutMinimum: 0, }, ]) // settle all DAI with no limit v4Planner.addAction(Actions.SETTLE_ALL, [currencyIn, v4AmountIn1.add(v4AmountIn2)]) // take all the WETH and all the ETH into the router v4Planner.addAction(Actions.TAKE, [WETH.address, ADDRESS_THIS, OPEN_DELTA]) v4Planner.addAction(Actions.TAKE, [ETH_ADDRESS, ADDRESS_THIS, OPEN_DELTA]) planner.addCommand(CommandType.V4_SWAP, [v4Planner.actions, v4Planner.params]) // wrap all the ETH into WETH planner.addCommand(CommandType.WRAP_ETH, [ADDRESS_THIS, CONTRACT_BALANCE]) // now we can send the WETH to the user, with aggregate slippage check planner.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, aggregateMinOut]) const { daiBalanceBefore, daiBalanceAfter, wethBalanceBefore, wethBalanceAfter } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) expect(wethBalanceAfter.sub(wethBalanceBefore)).to.be.gte(aggregateMinOut) expect(daiBalanceBefore.sub(daiBalanceAfter)).to.be.eq(v4AmountIn1.add(v4AmountIn2)) }) describe('Batch reverts', () => { let subplan: RoutePlanner const planOneTokens = [DAI.address, WETH.address] const planTwoTokens = [USDC.address, WETH.address] const planOneV2AmountIn: BigNumber = expandTo18DecimalsBN(2) const planOneV3AmountIn: BigNumber = expandTo18DecimalsBN(3) const planTwoV3AmountIn = expandTo6DecimalsBN(5) beforeEach(async () => { subplan = new RoutePlanner() }) it('2 sub-plans, neither fails', async () => { // first split route sub-plan. DAI->WETH, 2 routes on V2 and V3. const planOneWethMinOut = expandTo18DecimalsBN(0.0005) // V2 trades DAI for USDC, sending the tokens back to the router for v3 trade subplan.addCommand(CommandType.V2_SWAP_EXACT_IN, [ ADDRESS_THIS, planOneV2AmountIn, 0, planOneTokens, SOURCE_MSG_SENDER, ]) // V3 trades USDC for WETH, trading the whole balance, with a recipient of Alice subplan.addCommand(CommandType.V3_SWAP_EXACT_IN, [ ADDRESS_THIS, planOneV3AmountIn, 0, encodePathExactInput(planOneTokens), SOURCE_MSG_SENDER, ]) // aggregate slippage check subplan.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, planOneWethMinOut]) // add the subplan to the main planner planner.addSubPlan(subplan) subplan = new RoutePlanner() // second split route sub-plan. USDC->WETH, 1 route on V3 const wethMinAmountOut2 = expandTo18DecimalsBN(0.0005) // Add the trade to the sub-plan subplan.addCommand(CommandType.V3_SWAP_EXACT_IN, [ MSG_SENDER, planTwoV3AmountIn, wethMinAmountOut2, encodePathExactInput(planTwoTokens), SOURCE_MSG_SENDER, ]) // add the second subplan to the main planner planner.addSubPlan(subplan) const { usdcBalanceBefore, usdcBalanceAfter, daiBalanceBefore, daiBalanceAfter } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) expect(daiBalanceBefore.sub(daiBalanceAfter)).to.eq(planOneV2AmountIn.add(planOneV3AmountIn)) expect(usdcBalanceBefore.sub(usdcBalanceAfter)).to.eq(planTwoV3AmountIn) }) it('2 sub-plans, the first fails', async () => { // first split route sub-plan. DAI->WETH, 2 routes on V2 and V3. // FAIL: large weth amount out to cause a failure const planOneWethMinOut = expandTo18DecimalsBN(1) // V2 trades DAI for USDC, sending the tokens back to the router for v3 trade subplan.addCommand(CommandType.V2_SWAP_EXACT_IN, [ ADDRESS_THIS, planOneV2AmountIn, 0, planOneTokens, SOURCE_MSG_SENDER, ]) // V3 trades USDC for WETH, trading the whole balance, with a recipient of Alice subplan.addCommand(CommandType.V3_SWAP_EXACT_IN, [ ADDRESS_THIS, planOneV3AmountIn, 0, encodePathExactInput(planOneTokens), SOURCE_MSG_SENDER, ]) // aggregate slippage check subplan.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, planOneWethMinOut]) // add the subplan to the main planner planner.addSubPlan(subplan) subplan = new RoutePlanner() // second split route sub-plan. USDC->WETH, 1 route on V3 const wethMinAmountOut2 = expandTo18DecimalsBN(0.0005) // Add the trade to the sub-plan subplan.addCommand(CommandType.V3_SWAP_EXACT_IN, [ MSG_SENDER, planTwoV3AmountIn, wethMinAmountOut2, encodePathExactInput(planTwoTokens), SOURCE_MSG_SENDER, ]) // add the second subplan to the main planner planner.addSubPlan(subplan) const { usdcBalanceBefore, usdcBalanceAfter, daiBalanceBefore, daiBalanceAfter } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) // dai balance should be unchanged as the weth sweep failed expect(daiBalanceBefore).to.eq(daiBalanceAfter) // usdc is the second trade so the balance has changed expect(usdcBalanceBefore.sub(usdcBalanceAfter)).to.eq(planTwoV3AmountIn) }) it('2 sub-plans, both fail but the transaction succeeds', async () => { // first split route sub-plan. DAI->WETH, 2 routes on V2 and V3. // FAIL: large amount out to cause the swap to revert const planOneWethMinOut = expandTo18DecimalsBN(1) // V2 trades DAI for USDC, sending the tokens back to the router for v3 trade subplan.addCommand(CommandType.V2_SWAP_EXACT_IN, [ ADDRESS_THIS, planOneV2AmountIn, 0, planOneTokens, SOURCE_MSG_SENDER, ]) // V3 trades USDC for WETH, trading the whole balance, with a recipient of Alice subplan.addCommand(CommandType.V3_SWAP_EXACT_IN, [ ADDRESS_THIS, planOneV3AmountIn, 0, encodePathExactInput(planOneTokens), SOURCE_MSG_SENDER, ]) // aggregate slippage check subplan.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, planOneWethMinOut]) // add the subplan to the main planner planner.addSubPlan(subplan) subplan = new RoutePlanner() // second split route sub-plan. USDC->WETH, 1 route on V3 // FAIL: large amount out to cause the swap to revert const wethMinAmountOut2 = expandTo18DecimalsBN(1) // Add the trade to the sub-plan subplan.addCommand(CommandType.V3_SWAP_EXACT_IN, [ MSG_SENDER, planTwoV3AmountIn, wethMinAmountOut2, encodePathExactInput(planTwoTokens), SOURCE_MSG_SENDER, ]) // add the second subplan to the main planner planner.addSubPlan(subplan) const { usdcBalanceBefore, usdcBalanceAfter, daiBalanceBefore, daiBalanceAfter } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) // dai and usdc balances both unchanged because both trades failed expect(daiBalanceBefore).to.eq(daiBalanceAfter) expect(usdcBalanceBefore).to.eq(usdcBalanceAfter) }) it('2 sub-plans, second sub plan fails', async () => { // first split route sub-plan. DAI->WETH, 2 routes on V2 and V3. const planOneWethMinOut = expandTo18DecimalsBN(0.0005) // V2 trades DAI for USDC, sending the tokens back to the router for v3 trade subplan.addCommand(CommandType.V2_SWAP_EXACT_IN, [ ADDRESS_THIS, planOneV2AmountIn, 0, planOneTokens, SOURCE_MSG_SENDER, ]) // V3 trades USDC for WETH, trading the whole balance, with a recipient of Alice subplan.addCommand(CommandType.V3_SWAP_EXACT_IN, [ ADDRESS_THIS, planOneV3AmountIn, 0, encodePathExactInput(planOneTokens), SOURCE_MSG_SENDER, ]) // aggregate slippage check subplan.addCommand(CommandType.SWEEP, [WETH.address, MSG_SENDER, planOneWethMinOut]) // add the subplan to the main planner planner.addSubPlan(subplan) subplan = new RoutePlanner() // second split route sub-plan. USDC->WETH, 1 route on V3 // FAIL: large amount out to cause the swap to revert const wethMinAmountOut2 = expandTo18DecimalsBN(1) // Add the trade to the sub-plan subplan.addCommand(CommandType.V3_SWAP_EXACT_IN, [ MSG_SENDER, planTwoV3AmountIn, wethMinAmountOut2, encodePathExactInput(planTwoTokens), SOURCE_MSG_SENDER, ]) // add the second subplan to the main planner planner.addSubPlan(subplan) const { usdcBalanceBefore, usdcBalanceAfter, daiBalanceBefore, daiBalanceAfter } = await executeRouter( planner, bob, router, wethContract, daiContract, usdcContract ) // dai balance has changed as this trade should succeed expect(daiBalanceBefore.sub(daiBalanceAfter)).to.eq(planOneV2AmountIn.add(planOneV3AmountIn)) // usdc is unchanged as the second trade should have failed expect(usdcBalanceBefore).to.eq(usdcBalanceAfter) }) }) }) })