import { defaultAbiCoder } from '@ethersproject/abi' import { abi as PAIR_V2_ABI } from '@uniswap/v2-core/build/UniswapV2Pair.json' import { Fixture } from 'ethereum-waffle' import { BigNumber, constants, Contract, ContractTransaction, Wallet } from 'ethers' import { solidityPack } from 'ethers/lib/utils' import { ethers, waffle } from 'hardhat' import { IUniswapV2Pair, IWETH9, MockTimeSwapRouter02, MixedRouteQuoterV1, TestERC20 } from '../typechain' import completeFixture from './shared/completeFixture' import { computePoolAddress } from './shared/computePoolAddress' import { ADDRESS_THIS, CONTRACT_BALANCE, FeeAmount, MSG_SENDER, TICK_SPACINGS, V2_FEE_PLACEHOLDER, } from './shared/constants' import { encodePriceSqrt } from './shared/encodePriceSqrt' import { expandTo18Decimals } from './shared/expandTo18Decimals' import { expect } from './shared/expect' import { encodePath } from './shared/path' import { getMaxTick, getMinTick } from './shared/ticks' describe('SwapRouter', function () { this.timeout(40000) let wallet: Wallet let trader: Wallet const swapRouterFixture: Fixture<{ weth9: IWETH9 factory: Contract factoryV2: Contract router: MockTimeSwapRouter02 quoter: MixedRouteQuoterV1 nft: Contract tokens: [TestERC20, TestERC20, TestERC20] }> = async (wallets, provider) => { const { weth9, factory, factoryV2, router, tokens, nft } = await completeFixture(wallets, provider) // approve & fund wallets for (const token of tokens) { await token.approve(router.address, constants.MaxUint256) await token.approve(nft.address, constants.MaxUint256) await token.connect(trader).approve(router.address, constants.MaxUint256) await token.transfer(trader.address, expandTo18Decimals(1_000_000)) } const quoterFactory = await ethers.getContractFactory('MixedRouteQuoterV1') quoter = (await quoterFactory.deploy(factory.address, factoryV2.address, weth9.address)) as MixedRouteQuoterV1 return { weth9, factory, factoryV2, router, quoter, tokens, nft, } } let factory: Contract let factoryV2: Contract let weth9: IWETH9 let router: MockTimeSwapRouter02 let quoter: MixedRouteQuoterV1 let nft: Contract let tokens: [TestERC20, TestERC20, TestERC20] let getBalances: ( who: string ) => Promise<{ weth9: BigNumber token0: BigNumber token1: BigNumber token2: BigNumber }> let loadFixture: ReturnType function encodeUnwrapWETH9(amount: number) { const functionSignature = 'unwrapWETH9(uint256,address)' return solidityPack( ['bytes4', 'bytes'], [ router.interface.getSighash(functionSignature), defaultAbiCoder.encode(router.interface.functions[functionSignature].inputs, [amount, trader.address]), ] ) } function encodeSweep(token: string, amount: number, recipient: string) { const functionSignature = 'sweepToken(address,uint256,address)' return solidityPack( ['bytes4', 'bytes'], [ router.interface.getSighash(functionSignature), defaultAbiCoder.encode(router.interface.functions[functionSignature].inputs, [token, amount, recipient]), ] ) } before('create fixture loader', async () => { ;[wallet, trader] = await (ethers as any).getSigners() loadFixture = waffle.createFixtureLoader([wallet, trader]) }) // helper for getting weth and token balances beforeEach('load fixture', async () => { ;({ router, quoter, weth9, factory, factoryV2, tokens, nft } = await loadFixture(swapRouterFixture)) getBalances = async (who: string) => { const balances = await Promise.all([ weth9.balanceOf(who), tokens[0].balanceOf(who), tokens[1].balanceOf(who), tokens[2].balanceOf(who), ]) return { weth9: balances[0], token0: balances[1], token1: balances[2], token2: balances[3], } } }) // ensure the swap router never ends up with a balance afterEach('load fixture', async () => { const balances = await getBalances(router.address) expect(Object.values(balances).every((b) => b.eq(0))).to.be.eq(true) const balance = await waffle.provider.getBalance(router.address) expect(balance.eq(0)).to.be.eq(true) }) it('bytecode size', async () => { expect(((await router.provider.getCode(router.address)).length - 2) / 2).to.matchSnapshot() }) const liquidity = 1000000 async function createV3Pool(tokenAddressA: string, tokenAddressB: string) { if (tokenAddressA.toLowerCase() > tokenAddressB.toLowerCase()) [tokenAddressA, tokenAddressB] = [tokenAddressB, tokenAddressA] await nft.createAndInitializePoolIfNecessary(tokenAddressA, tokenAddressB, FeeAmount.MEDIUM, encodePriceSqrt(1, 1)) const liquidityParams = { token0: tokenAddressA, token1: tokenAddressB, fee: FeeAmount.MEDIUM, tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]), tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]), recipient: wallet.address, amount0Desired: 1000000, amount1Desired: 1000000, amount0Min: 0, amount1Min: 0, deadline: 2 ** 32, } return nft.mint(liquidityParams) } describe('swaps - v3', () => { async function createPoolWETH9(tokenAddress: string) { await weth9.deposit({ value: liquidity }) await weth9.approve(nft.address, constants.MaxUint256) return createV3Pool(weth9.address, tokenAddress) } beforeEach('create 0-1 and 1-2 pools', async () => { await createV3Pool(tokens[0].address, tokens[1].address) await createV3Pool(tokens[1].address, tokens[2].address) }) describe('#exactInput', () => { async function exactInput( tokens: string[], amountIn: number = 3, amountOutMinimum: number = 1 ): Promise { const inputIsWETH = weth9.address === tokens[0] const outputIsWETH9 = tokens[tokens.length - 1] === weth9.address const value = inputIsWETH ? amountIn : 0 const params = { path: encodePath(tokens, new Array(tokens.length - 1).fill(FeeAmount.MEDIUM)), recipient: outputIsWETH9 ? ADDRESS_THIS : MSG_SENDER, amountIn, amountOutMinimum, } const data = [router.interface.encodeFunctionData('exactInput', [params])] if (outputIsWETH9) { data.push(encodeUnwrapWETH9(amountOutMinimum)) } // ensure that the swap fails if the limit is any tighter const amountOut = await router.connect(trader).callStatic.exactInput(params, { value }) expect(amountOut.toNumber()).to.be.eq(amountOutMinimum) return router.connect(trader)['multicall(bytes[])'](data, { value }) } describe('single-pool', () => { it('0 -> 1', async () => { const pool = await factory.getPool(tokens[0].address, tokens[1].address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await exactInput(tokens.slice(0, 2).map((token) => token.address)) // get balances after const poolAfter = await getBalances(pool) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(3)) expect(traderAfter.token1).to.be.eq(traderBefore.token1.add(1)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.add(3)) expect(poolAfter.token1).to.be.eq(poolBefore.token1.sub(1)) }) it('1 -> 0', async () => { const pool = await factory.getPool(tokens[1].address, tokens[0].address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await exactInput( tokens .slice(0, 2) .reverse() .map((token) => token.address) ) // get balances after const poolAfter = await getBalances(pool) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.add(1)) expect(traderAfter.token1).to.be.eq(traderBefore.token1.sub(3)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1)) expect(poolAfter.token1).to.be.eq(poolBefore.token1.add(3)) }) }) describe('multi-pool', () => { it('0 -> 1 -> 2', async () => { const traderBefore = await getBalances(trader.address) await exactInput( tokens.map((token) => token.address), 5, 1 ) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(5)) expect(traderAfter.token2).to.be.eq(traderBefore.token2.add(1)) }) it('2 -> 1 -> 0', async () => { const traderBefore = await getBalances(trader.address) await exactInput(tokens.map((token) => token.address).reverse(), 5, 1) const traderAfter = await getBalances(trader.address) expect(traderAfter.token2).to.be.eq(traderBefore.token2.sub(5)) expect(traderAfter.token0).to.be.eq(traderBefore.token0.add(1)) }) it('events', async () => { await expect( exactInput( tokens.map((token) => token.address), 5, 1 ) ) .to.emit(tokens[0], 'Transfer') .withArgs( trader.address, computePoolAddress(factory.address, [tokens[0].address, tokens[1].address], FeeAmount.MEDIUM), 5 ) .to.emit(tokens[1], 'Transfer') .withArgs( computePoolAddress(factory.address, [tokens[0].address, tokens[1].address], FeeAmount.MEDIUM), router.address, 3 ) .to.emit(tokens[1], 'Transfer') .withArgs( router.address, computePoolAddress(factory.address, [tokens[1].address, tokens[2].address], FeeAmount.MEDIUM), 3 ) .to.emit(tokens[2], 'Transfer') .withArgs( computePoolAddress(factory.address, [tokens[1].address, tokens[2].address], FeeAmount.MEDIUM), trader.address, 1 ) }) }) describe('ETH input', () => { describe('WETH9', () => { beforeEach(async () => { await createPoolWETH9(tokens[0].address) }) it('WETH9 -> 0', async () => { const pool = await factory.getPool(weth9.address, tokens[0].address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await expect(exactInput([weth9.address, tokens[0].address])) .to.emit(weth9, 'Deposit') .withArgs(router.address, 3) // get balances after const poolAfter = await getBalances(pool) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.add(1)) expect(poolAfter.weth9).to.be.eq(poolBefore.weth9.add(3)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1)) }) it('WETH9 -> 0 -> 1', async () => { const traderBefore = await getBalances(trader.address) await expect(exactInput([weth9.address, tokens[0].address, tokens[1].address], 5)) .to.emit(weth9, 'Deposit') .withArgs(router.address, 5) const traderAfter = await getBalances(trader.address) expect(traderAfter.token1).to.be.eq(traderBefore.token1.add(1)) }) }) }) describe('ETH output', () => { describe('WETH9', () => { beforeEach(async () => { await createPoolWETH9(tokens[0].address) await createPoolWETH9(tokens[1].address) }) it('0 -> WETH9', async () => { const pool = await factory.getPool(tokens[0].address, weth9.address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await expect(exactInput([tokens[0].address, weth9.address])) .to.emit(weth9, 'Withdrawal') .withArgs(router.address, 1) // get balances after const poolAfter = await getBalances(pool) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(3)) expect(poolAfter.weth9).to.be.eq(poolBefore.weth9.sub(1)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.add(3)) }) it('0 -> 1 -> WETH9', async () => { // get balances before const traderBefore = await getBalances(trader.address) await expect(exactInput([tokens[0].address, tokens[1].address, weth9.address], 5)) .to.emit(weth9, 'Withdrawal') .withArgs(router.address, 1) // get balances after const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(5)) }) }) }) }) describe('#exactInputSingle', () => { async function exactInputSingle( tokenIn: string, tokenOut: string, amountIn: number = 3, amountOutMinimum: number = 1, sqrtPriceLimitX96?: BigNumber ): Promise { const inputIsWETH = weth9.address === tokenIn const outputIsWETH9 = tokenOut === weth9.address const value = inputIsWETH ? amountIn : 0 const params = { tokenIn, tokenOut, fee: FeeAmount.MEDIUM, recipient: outputIsWETH9 ? ADDRESS_THIS : MSG_SENDER, amountIn, amountOutMinimum, sqrtPriceLimitX96: sqrtPriceLimitX96 ?? 0, } const data = [router.interface.encodeFunctionData('exactInputSingle', [params])] if (outputIsWETH9) { data.push(encodeUnwrapWETH9(amountOutMinimum)) } // ensure that the swap fails if the limit is any tighter const amountOut = await router.connect(trader).callStatic.exactInputSingle(params, { value }) expect(amountOut.toNumber()).to.be.eq(amountOutMinimum) // optimized for the gas test return data.length === 1 ? router.connect(trader).exactInputSingle(params, { value }) : router.connect(trader)['multicall(bytes[])'](data, { value }) } it('0 -> 1', async () => { const pool = await factory.getPool(tokens[0].address, tokens[1].address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await exactInputSingle(tokens[0].address, tokens[1].address) // get balances after const poolAfter = await getBalances(pool) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(3)) expect(traderAfter.token1).to.be.eq(traderBefore.token1.add(1)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.add(3)) expect(poolAfter.token1).to.be.eq(poolBefore.token1.sub(1)) }) it('1 -> 0', async () => { const pool = await factory.getPool(tokens[1].address, tokens[0].address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await exactInputSingle(tokens[1].address, tokens[0].address) // get balances after const poolAfter = await getBalances(pool) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.add(1)) expect(traderAfter.token1).to.be.eq(traderBefore.token1.sub(3)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1)) expect(poolAfter.token1).to.be.eq(poolBefore.token1.add(3)) }) describe('ETH input', () => { describe('WETH9', () => { beforeEach(async () => { await createPoolWETH9(tokens[0].address) }) it('WETH9 -> 0', async () => { const pool = await factory.getPool(weth9.address, tokens[0].address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await expect(exactInputSingle(weth9.address, tokens[0].address)) .to.emit(weth9, 'Deposit') .withArgs(router.address, 3) // get balances after const poolAfter = await getBalances(pool) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.add(1)) expect(poolAfter.weth9).to.be.eq(poolBefore.weth9.add(3)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1)) }) }) }) describe('ETH output', () => { describe('WETH9', () => { beforeEach(async () => { await createPoolWETH9(tokens[0].address) await createPoolWETH9(tokens[1].address) }) it('0 -> WETH9', async () => { const pool = await factory.getPool(tokens[0].address, weth9.address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await expect(exactInputSingle(tokens[0].address, weth9.address)) .to.emit(weth9, 'Withdrawal') .withArgs(router.address, 1) // get balances after const poolAfter = await getBalances(pool) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(3)) expect(poolAfter.weth9).to.be.eq(poolBefore.weth9.sub(1)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.add(3)) }) }) }) }) describe('#exactOutput', () => { async function exactOutput( tokens: string[], amountOut: number = 1, amountInMaximum: number = 3 ): Promise { const inputIsWETH9 = tokens[0] === weth9.address const outputIsWETH9 = tokens[tokens.length - 1] === weth9.address const value = inputIsWETH9 ? amountInMaximum : 0 const params = { path: encodePath(tokens.slice().reverse(), new Array(tokens.length - 1).fill(FeeAmount.MEDIUM)), recipient: outputIsWETH9 ? ADDRESS_THIS : MSG_SENDER, amountOut, amountInMaximum, } const data = [router.interface.encodeFunctionData('exactOutput', [params])] if (inputIsWETH9) { data.push(router.interface.encodeFunctionData('refundETH')) } if (outputIsWETH9) { data.push(encodeUnwrapWETH9(amountOut)) } // ensure that the swap fails if the limit is any tighter const amountIn = await router.connect(trader).callStatic.exactOutput(params, { value }) expect(amountIn.toNumber()).to.be.eq(amountInMaximum) return router.connect(trader)['multicall(bytes[])'](data, { value }) } describe('single-pool', () => { it('0 -> 1', async () => { const pool = await factory.getPool(tokens[0].address, tokens[1].address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await exactOutput(tokens.slice(0, 2).map((token) => token.address)) // get balances after const poolAfter = await getBalances(pool) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(3)) expect(traderAfter.token1).to.be.eq(traderBefore.token1.add(1)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.add(3)) expect(poolAfter.token1).to.be.eq(poolBefore.token1.sub(1)) }) it('1 -> 0', async () => { const pool = await factory.getPool(tokens[1].address, tokens[0].address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await exactOutput( tokens .slice(0, 2) .reverse() .map((token) => token.address) ) // get balances after const poolAfter = await getBalances(pool) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.add(1)) expect(traderAfter.token1).to.be.eq(traderBefore.token1.sub(3)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1)) expect(poolAfter.token1).to.be.eq(poolBefore.token1.add(3)) }) }) describe('multi-pool', () => { it('0 -> 1 -> 2', async () => { const traderBefore = await getBalances(trader.address) await exactOutput( tokens.map((token) => token.address), 1, 5 ) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(5)) expect(traderAfter.token2).to.be.eq(traderBefore.token2.add(1)) }) it('2 -> 1 -> 0', async () => { const traderBefore = await getBalances(trader.address) await exactOutput(tokens.map((token) => token.address).reverse(), 1, 5) const traderAfter = await getBalances(trader.address) expect(traderAfter.token2).to.be.eq(traderBefore.token2.sub(5)) expect(traderAfter.token0).to.be.eq(traderBefore.token0.add(1)) }) it('events', async () => { await expect( exactOutput( tokens.map((token) => token.address), 1, 5 ) ) .to.emit(tokens[2], 'Transfer') .withArgs( computePoolAddress(factory.address, [tokens[2].address, tokens[1].address], FeeAmount.MEDIUM), trader.address, 1 ) .to.emit(tokens[1], 'Transfer') .withArgs( computePoolAddress(factory.address, [tokens[1].address, tokens[0].address], FeeAmount.MEDIUM), computePoolAddress(factory.address, [tokens[2].address, tokens[1].address], FeeAmount.MEDIUM), 3 ) .to.emit(tokens[0], 'Transfer') .withArgs( trader.address, computePoolAddress(factory.address, [tokens[1].address, tokens[0].address], FeeAmount.MEDIUM), 5 ) }) }) describe('ETH input', () => { describe('WETH9', () => { beforeEach(async () => { await createPoolWETH9(tokens[0].address) }) it('WETH9 -> 0', async () => { const pool = await factory.getPool(weth9.address, tokens[0].address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await expect(exactOutput([weth9.address, tokens[0].address])) .to.emit(weth9, 'Deposit') .withArgs(router.address, 3) // get balances after const poolAfter = await getBalances(pool) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.add(1)) expect(poolAfter.weth9).to.be.eq(poolBefore.weth9.add(3)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1)) }) it('WETH9 -> 0 -> 1', async () => { const traderBefore = await getBalances(trader.address) await expect(exactOutput([weth9.address, tokens[0].address, tokens[1].address], 1, 5)) .to.emit(weth9, 'Deposit') .withArgs(router.address, 5) const traderAfter = await getBalances(trader.address) expect(traderAfter.token1).to.be.eq(traderBefore.token1.add(1)) }) }) }) describe('ETH output', () => { describe('WETH9', () => { beforeEach(async () => { await createPoolWETH9(tokens[0].address) await createPoolWETH9(tokens[1].address) }) it('0 -> WETH9', async () => { const pool = await factory.getPool(tokens[0].address, weth9.address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await expect(exactOutput([tokens[0].address, weth9.address])) .to.emit(weth9, 'Withdrawal') .withArgs(router.address, 1) // get balances after const poolAfter = await getBalances(pool) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(3)) expect(poolAfter.weth9).to.be.eq(poolBefore.weth9.sub(1)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.add(3)) }) it('0 -> 1 -> WETH9', async () => { // get balances before const traderBefore = await getBalances(trader.address) await expect(exactOutput([tokens[0].address, tokens[1].address, weth9.address], 1, 5)) .to.emit(weth9, 'Withdrawal') .withArgs(router.address, 1) // get balances after const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(5)) }) }) }) }) describe('#exactOutputSingle', () => { async function exactOutputSingle( tokenIn: string, tokenOut: string, amountOut: number = 1, amountInMaximum: number = 3, sqrtPriceLimitX96?: BigNumber ): Promise { const inputIsWETH9 = tokenIn === weth9.address const outputIsWETH9 = tokenOut === weth9.address const value = inputIsWETH9 ? amountInMaximum : 0 const params = { tokenIn, tokenOut, fee: FeeAmount.MEDIUM, recipient: outputIsWETH9 ? ADDRESS_THIS : MSG_SENDER, amountOut, amountInMaximum, sqrtPriceLimitX96: sqrtPriceLimitX96 ?? 0, } const data = [router.interface.encodeFunctionData('exactOutputSingle', [params])] if (inputIsWETH9) { data.push(router.interface.encodeFunctionData('refundETH')) } if (outputIsWETH9) { data.push(encodeUnwrapWETH9(amountOut)) } // ensure that the swap fails if the limit is any tighter const amountIn = await router.connect(trader).callStatic.exactOutputSingle(params, { value }) expect(amountIn.toNumber()).to.be.eq(amountInMaximum) return router.connect(trader)['multicall(bytes[])'](data, { value }) } it('0 -> 1', async () => { const pool = await factory.getPool(tokens[0].address, tokens[1].address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await exactOutputSingle(tokens[0].address, tokens[1].address) // get balances after const poolAfter = await getBalances(pool) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(3)) expect(traderAfter.token1).to.be.eq(traderBefore.token1.add(1)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.add(3)) expect(poolAfter.token1).to.be.eq(poolBefore.token1.sub(1)) }) it('1 -> 0', async () => { const pool = await factory.getPool(tokens[1].address, tokens[0].address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await exactOutputSingle(tokens[1].address, tokens[0].address) // get balances after const poolAfter = await getBalances(pool) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.add(1)) expect(traderAfter.token1).to.be.eq(traderBefore.token1.sub(3)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1)) expect(poolAfter.token1).to.be.eq(poolBefore.token1.add(3)) }) describe('ETH input', () => { describe('WETH9', () => { beforeEach(async () => { await createPoolWETH9(tokens[0].address) }) it('WETH9 -> 0', async () => { const pool = await factory.getPool(weth9.address, tokens[0].address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await expect(exactOutputSingle(weth9.address, tokens[0].address)) .to.emit(weth9, 'Deposit') .withArgs(router.address, 3) // get balances after const poolAfter = await getBalances(pool) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.add(1)) expect(poolAfter.weth9).to.be.eq(poolBefore.weth9.add(3)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1)) }) }) }) describe('ETH output', () => { describe('WETH9', () => { beforeEach(async () => { await createPoolWETH9(tokens[0].address) await createPoolWETH9(tokens[1].address) }) it('0 -> WETH9', async () => { const pool = await factory.getPool(tokens[0].address, weth9.address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await expect(exactOutputSingle(tokens[0].address, weth9.address)) .to.emit(weth9, 'Withdrawal') .withArgs(router.address, 1) // get balances after const poolAfter = await getBalances(pool) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(3)) expect(poolAfter.weth9).to.be.eq(poolBefore.weth9.sub(1)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.add(3)) }) }) }) }) describe('*WithFee', () => { const feeRecipient = '0xfEE0000000000000000000000000000000000000' it('#sweepTokenWithFee', async () => { const amountOutMinimum = 100 const params = { path: encodePath([tokens[0].address, tokens[1].address], [FeeAmount.MEDIUM]), recipient: ADDRESS_THIS, amountIn: 102, amountOutMinimum: 0, } const functionSignature = 'sweepTokenWithFee(address,uint256,address,uint256,address)' const data = [ router.interface.encodeFunctionData('exactInput', [params]), solidityPack( ['bytes4', 'bytes'], [ router.interface.getSighash(functionSignature), defaultAbiCoder.encode( ['address', 'uint256', 'address', 'uint256', 'address'], [tokens[1].address, amountOutMinimum, trader.address, 100, feeRecipient] ), ] ), ] await router.connect(trader)['multicall(bytes[])'](data) const balance = await tokens[1].balanceOf(feeRecipient) expect(balance.eq(1)).to.be.eq(true) }) it('#unwrapWETH9WithFee', async () => { const startBalance = await waffle.provider.getBalance(feeRecipient) await createPoolWETH9(tokens[0].address) const amountOutMinimum = 100 const params = { path: encodePath([tokens[0].address, weth9.address], [FeeAmount.MEDIUM]), recipient: ADDRESS_THIS, amountIn: 102, amountOutMinimum: 0, } const functionSignature = 'unwrapWETH9WithFee(uint256,address,uint256,address)' const data = [ router.interface.encodeFunctionData('exactInput', [params]), solidityPack( ['bytes4', 'bytes'], [ router.interface.getSighash(functionSignature), defaultAbiCoder.encode( ['uint256', 'address', 'uint256', 'address'], [amountOutMinimum, trader.address, 100, feeRecipient] ), ] ), ] await router.connect(trader)['multicall(bytes[])'](data) const endBalance = await waffle.provider.getBalance(feeRecipient) expect(endBalance.sub(startBalance).eq(1)).to.be.eq(true) }) }) }) async function createV2Pool(tokenA: TestERC20, tokenB: TestERC20): Promise { await factoryV2.createPair(tokenA.address, tokenB.address) const pairAddress = await factoryV2.getPair(tokenA.address, tokenB.address) const pair = new ethers.Contract(pairAddress, PAIR_V2_ABI, wallet) as IUniswapV2Pair await tokenA.transfer(pair.address, liquidity) await tokenB.transfer(pair.address, liquidity) await pair.mint(wallet.address) return pair } describe('swaps - v2', () => { let pairs: IUniswapV2Pair[] let wethPairs: IUniswapV2Pair[] async function createPoolWETH9(token: TestERC20) { await weth9.deposit({ value: liquidity }) return createV2Pool((weth9 as unknown) as TestERC20, token) } beforeEach('create 0-1 and 1-2 pools', async () => { const pair01 = await createV2Pool(tokens[0], tokens[1]) const pair12 = await createV2Pool(tokens[1], tokens[2]) pairs = [pair01, pair12] }) describe('#swapExactTokensForTokens', () => { async function exactInput( tokens: string[], amountIn: number = 2, amountOutMinimum: number = 1 ): Promise { const inputIsWETH = weth9.address === tokens[0] const outputIsWETH9 = tokens[tokens.length - 1] === weth9.address const value = inputIsWETH ? amountIn : 0 const params: [number, number, string[], string] = [ amountIn, amountOutMinimum, tokens, outputIsWETH9 ? ADDRESS_THIS : MSG_SENDER, ] const data = [router.interface.encodeFunctionData('swapExactTokensForTokens', params)] if (outputIsWETH9) { data.push(encodeUnwrapWETH9(amountOutMinimum)) } // ensure that the swap fails if the limit is any tighter const paramsWithValue: [number, number, string[], string, { value: number }] = [...params, { value }] const amountOut = await router.connect(trader).callStatic.swapExactTokensForTokens(...paramsWithValue) expect(amountOut.toNumber()).to.be.eq(amountOutMinimum) return router.connect(trader)['multicall(bytes[])'](data, { value }) } describe('single-pool', () => { it('0 -> 1', async () => { // get balances before const poolBefore = await getBalances(pairs[0].address) const traderBefore = await getBalances(trader.address) await exactInput(tokens.slice(0, 2).map((token) => token.address)) // get balances after const poolAfter = await getBalances(pairs[0].address) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(2)) expect(traderAfter.token1).to.be.eq(traderBefore.token1.add(1)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.add(2)) expect(poolAfter.token1).to.be.eq(poolBefore.token1.sub(1)) }) it('1 -> 0', async () => { // get balances before const poolBefore = await getBalances(pairs[0].address) const traderBefore = await getBalances(trader.address) await exactInput( tokens .slice(0, 2) .reverse() .map((token) => token.address) ) // get balances after const poolAfter = await getBalances(pairs[0].address) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.add(1)) expect(traderAfter.token1).to.be.eq(traderBefore.token1.sub(2)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1)) expect(poolAfter.token1).to.be.eq(poolBefore.token1.add(2)) }) }) describe('multi-pool', () => { it('0 -> 1 -> 2', async () => { const traderBefore = await getBalances(trader.address) await exactInput( tokens.map((token) => token.address), 3, 1 ) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(3)) expect(traderAfter.token2).to.be.eq(traderBefore.token2.add(1)) }) it('2 -> 1 -> 0', async () => { const traderBefore = await getBalances(trader.address) await exactInput(tokens.map((token) => token.address).reverse(), 3, 1) const traderAfter = await getBalances(trader.address) expect(traderAfter.token2).to.be.eq(traderBefore.token2.sub(3)) expect(traderAfter.token0).to.be.eq(traderBefore.token0.add(1)) }) it('events', async () => { await expect( exactInput( tokens.map((token) => token.address), 3, 1 ) ) .to.emit(tokens[0], 'Transfer') .withArgs(trader.address, pairs[0].address, 3) .to.emit(tokens[1], 'Transfer') .withArgs(pairs[0].address, pairs[1].address, 2) .to.emit(tokens[2], 'Transfer') .withArgs(pairs[1].address, trader.address, 1) }) }) describe('ETH input', () => { describe('WETH9', () => { beforeEach(async () => { const pair = await createPoolWETH9(tokens[0]) wethPairs = [pair] }) it('WETH9 -> 0', async () => { // get balances before const poolBefore = await getBalances(wethPairs[0].address) const traderBefore = await getBalances(trader.address) await expect(exactInput([weth9.address, tokens[0].address])) .to.emit(weth9, 'Deposit') .withArgs(router.address, 2) // get balances after const poolAfter = await getBalances(wethPairs[0].address) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.add(1)) expect(poolAfter.weth9).to.be.eq(poolBefore.weth9.add(2)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1)) }) it('WETH9 -> 0 -> 1', async () => { const traderBefore = await getBalances(trader.address) await expect(exactInput([weth9.address, tokens[0].address, tokens[1].address], 3)) .to.emit(weth9, 'Deposit') .withArgs(router.address, 3) const traderAfter = await getBalances(trader.address) expect(traderAfter.token1).to.be.eq(traderBefore.token1.add(1)) }) }) }) describe('ETH output', () => { describe('WETH9', () => { beforeEach(async () => { const pair0 = await createPoolWETH9(tokens[0]) const pair1 = await createPoolWETH9(tokens[1]) wethPairs = [pair0, pair1] }) it('0 -> WETH9', async () => { // get balances before const poolBefore = await getBalances(wethPairs[0].address) const traderBefore = await getBalances(trader.address) await expect(exactInput([tokens[0].address, weth9.address])) .to.emit(weth9, 'Withdrawal') .withArgs(router.address, 1) // get balances after const poolAfter = await getBalances(wethPairs[0].address) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(2)) expect(poolAfter.weth9).to.be.eq(poolBefore.weth9.sub(1)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.add(2)) }) it('0 -> 1 -> WETH9', async () => { // get balances before const traderBefore = await getBalances(trader.address) await expect(exactInput([tokens[0].address, tokens[1].address, weth9.address], 3)) .to.emit(weth9, 'Withdrawal') .withArgs(router.address, 1) // get balances after const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(3)) }) }) }) }) describe('#swapTokensForExactTokens', () => { async function exactOutput( tokens: string[], amountOut: number = 1, amountInMaximum: number = 2 ): Promise { const inputIsWETH9 = tokens[0] === weth9.address const outputIsWETH9 = tokens[tokens.length - 1] === weth9.address const value = inputIsWETH9 ? amountInMaximum : 0 const params: [number, number, string[], string] = [ amountOut, amountInMaximum, tokens, outputIsWETH9 ? ADDRESS_THIS : MSG_SENDER, ] const data = [router.interface.encodeFunctionData('swapTokensForExactTokens', params)] if (inputIsWETH9) { data.push(router.interface.encodeFunctionData('refundETH')) } if (outputIsWETH9) { data.push(encodeUnwrapWETH9(amountOut)) } // ensure that the swap fails if the limit is any tighter const paramsWithValue: [number, number, string[], string, { value: number }] = [...params, { value }] const amountIn = await router.connect(trader).callStatic.swapTokensForExactTokens(...paramsWithValue) expect(amountIn.toNumber()).to.be.eq(amountInMaximum) return router.connect(trader)['multicall(bytes[])'](data, { value }) } describe('single-pool', () => { it('0 -> 1', async () => { // get balances before const poolBefore = await getBalances(pairs[0].address) const traderBefore = await getBalances(trader.address) await exactOutput(tokens.slice(0, 2).map((token) => token.address)) // get balances after const poolAfter = await getBalances(pairs[0].address) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(2)) expect(traderAfter.token1).to.be.eq(traderBefore.token1.add(1)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.add(2)) expect(poolAfter.token1).to.be.eq(poolBefore.token1.sub(1)) }) it('1 -> 0', async () => { // get balances before const poolBefore = await getBalances(pairs[0].address) const traderBefore = await getBalances(trader.address) await exactOutput( tokens .slice(0, 2) .reverse() .map((token) => token.address) ) // get balances after const poolAfter = await getBalances(pairs[0].address) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.add(1)) expect(traderAfter.token1).to.be.eq(traderBefore.token1.sub(2)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1)) expect(poolAfter.token1).to.be.eq(poolBefore.token1.add(2)) }) }) describe('multi-pool', () => { it('0 -> 1 -> 2', async () => { const traderBefore = await getBalances(trader.address) await exactOutput( tokens.map((token) => token.address), 1, 3 ) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(3)) expect(traderAfter.token2).to.be.eq(traderBefore.token2.add(1)) }) it('2 -> 1 -> 0', async () => { const traderBefore = await getBalances(trader.address) await exactOutput(tokens.map((token) => token.address).reverse(), 1, 3) const traderAfter = await getBalances(trader.address) expect(traderAfter.token2).to.be.eq(traderBefore.token2.sub(3)) expect(traderAfter.token0).to.be.eq(traderBefore.token0.add(1)) }) it('events', async () => { await expect( exactOutput( tokens.map((token) => token.address), 1, 3 ) ) .to.emit(tokens[0], 'Transfer') .withArgs(trader.address, pairs[0].address, 3) .to.emit(tokens[1], 'Transfer') .withArgs(pairs[0].address, pairs[1].address, 2) .to.emit(tokens[2], 'Transfer') .withArgs(pairs[1].address, trader.address, 1) }) }) describe('ETH input', () => { describe('WETH9', () => { beforeEach(async () => { const pair = await createPoolWETH9(tokens[0]) wethPairs = [pair] }) it('WETH9 -> 0', async () => { // get balances before const poolBefore = await getBalances(wethPairs[0].address) const traderBefore = await getBalances(trader.address) await expect(exactOutput([weth9.address, tokens[0].address])) .to.emit(weth9, 'Deposit') .withArgs(router.address, 2) // get balances after const poolAfter = await getBalances(wethPairs[0].address) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.add(1)) expect(poolAfter.weth9).to.be.eq(poolBefore.weth9.add(2)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1)) }) it('WETH9 -> 0 -> 1', async () => { const traderBefore = await getBalances(trader.address) await expect(exactOutput([weth9.address, tokens[0].address, tokens[1].address], 1, 3)) .to.emit(weth9, 'Deposit') .withArgs(router.address, 3) const traderAfter = await getBalances(trader.address) expect(traderAfter.token1).to.be.eq(traderBefore.token1.add(1)) }) }) }) describe('ETH output', () => { describe('WETH9', () => { beforeEach(async () => { const pair0 = await createPoolWETH9(tokens[0]) const pair1 = await createPoolWETH9(tokens[1]) wethPairs = [pair0, pair1] }) it('0 -> WETH9', async () => { // get balances before const poolBefore = await getBalances(wethPairs[0].address) const traderBefore = await getBalances(trader.address) await expect(exactOutput([tokens[0].address, weth9.address])) .to.emit(weth9, 'Withdrawal') .withArgs(router.address, 1) // get balances after const poolAfter = await getBalances(wethPairs[0].address) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(2)) expect(poolAfter.weth9).to.be.eq(poolBefore.weth9.sub(1)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.add(2)) }) it('0 -> 1 -> WETH9', async () => { // get balances before const traderBefore = await getBalances(trader.address) await expect(exactOutput([tokens[0].address, tokens[1].address, weth9.address], 1, 3)) .to.emit(weth9, 'Withdrawal') .withArgs(router.address, 1) // get balances after const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(3)) }) }) }) }) }) describe('swaps - v2 + v3', () => { beforeEach('create 0-1 and 1-2 pools', async () => { await createV3Pool(tokens[0].address, tokens[1].address) await createV3Pool(tokens[1].address, tokens[2].address) }) beforeEach('create 0-1 and 1-2 pools', async () => { await createV2Pool(tokens[0], tokens[1]) await createV2Pool(tokens[1], tokens[2]) }) async function exactInputV3( tokens: string[], amountIn: number = 3, amountOutMinimum: number = 1, recipient: string, skipAmountOutMinimumCheck: boolean = false ): Promise { const params = { path: encodePath(tokens, new Array(tokens.length - 1).fill(FeeAmount.MEDIUM)), recipient, amountIn, amountOutMinimum, } const data = [router.interface.encodeFunctionData('exactInput', [params])] if (!skipAmountOutMinimumCheck) { // ensure that the swap fails if the limit is any tighter const amountOut = await router.connect(trader).callStatic.exactInput(params) expect(amountOut.toNumber()).to.be.eq(amountOutMinimum) } return data } async function exactOutputV3( tokens: string[], amountOut: number = 1, amountInMaximum: number = 3, recipient: string ): Promise { const params = { path: encodePath(tokens.slice().reverse(), new Array(tokens.length - 1).fill(FeeAmount.MEDIUM)), recipient, amountOut, amountInMaximum, } const data = [router.interface.encodeFunctionData('exactOutput', [params])] // ensure that the swap fails if the limit is any tighter const amountIn = await router.connect(trader).callStatic.exactOutput(params) expect(amountIn.toNumber()).to.be.eq(amountInMaximum) return data } async function exactInputV2( tokens: string[], amountIn: number = 2, amountOutMinimum: number = 1, recipient: string, skipAmountOutMinimumCheck: boolean = false ): Promise { const params: [number, number, string[], string] = [amountIn, amountOutMinimum, tokens, recipient] const data = [router.interface.encodeFunctionData('swapExactTokensForTokens', params)] if (!skipAmountOutMinimumCheck) { // ensure that the swap fails if the limit is any tighter const amountOut = await router.connect(trader).callStatic.swapExactTokensForTokens(...params) expect(amountOut.toNumber()).to.be.eq(amountOutMinimum) } return data } async function exactOutputV2( tokens: string[], amountOut: number = 1, amountInMaximum: number = 2, recipient: string ): Promise { const params: [number, number, string[], string] = [amountOut, amountInMaximum, tokens, recipient] const data = [router.interface.encodeFunctionData('swapTokensForExactTokens', params)] // ensure that the swap fails if the limit is any tighter const amountIn = await router.connect(trader).callStatic.swapTokensForExactTokens(...params) expect(amountIn.toNumber()).to.be.eq(amountInMaximum) return data } describe('simple split route', async () => { it('sending directly', async () => { const swapV3 = await exactInputV3( tokens.slice(0, 2).map((token) => token.address), 3, 1, MSG_SENDER ) const swapV2 = await exactInputV2( tokens.slice(0, 2).map((token) => token.address), 2, 1, MSG_SENDER ) const traderBefore = await getBalances(trader.address) await router.connect(trader)['multicall(bytes[])']([...swapV3, ...swapV2]) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(5)) expect(traderAfter.token1).to.be.eq(traderBefore.token1.add(2)) }) it('sending to router and sweeping', async () => { const swapV3 = await exactInputV3( tokens.slice(0, 2).map((token) => token.address), 3, 1, ADDRESS_THIS ) const swapV2 = await exactInputV2( tokens.slice(0, 2).map((token) => token.address), 2, 1, ADDRESS_THIS ) const sweep = encodeSweep(tokens[1].address, 2, trader.address) const traderBefore = await getBalances(trader.address) await router.connect(trader)['multicall(bytes[])']([...swapV3, ...swapV2, sweep]) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(5)) expect(traderAfter.token1).to.be.eq(traderBefore.token1.add(2)) }) }) describe('merging', () => { // 0 ↘ // 0 → 1 → 2 it('exactIn x 2 + exactIn', async () => { const swapV3 = await exactInputV3( tokens.slice(0, 2).map((token) => token.address), 3, 1, ADDRESS_THIS ) const swapV2 = await exactInputV2( tokens.slice(0, 2).map((token) => token.address), 3, 2, ADDRESS_THIS ) const mergeSwap = await exactInputV3( tokens.slice(1, 3).map((token) => token.address), CONTRACT_BALANCE, 1, MSG_SENDER, true ) const traderBefore = await getBalances(trader.address) await router.connect(trader)['multicall(bytes[])']([...swapV3, ...swapV2, ...mergeSwap]) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(6)) expect(traderAfter.token2).to.be.eq(traderBefore.token1.add(1)) }) it('exactOut x 2 + exactIn', async () => { const swapV3 = await exactOutputV3( tokens.slice(0, 2).map((token) => token.address), 1, 3, ADDRESS_THIS ) const swapV2 = await exactOutputV2( tokens.slice(0, 2).map((token) => token.address), 2, 3, ADDRESS_THIS ) const mergeSwap = await exactInputV3( tokens.slice(1, 3).map((token) => token.address), CONTRACT_BALANCE, 1, MSG_SENDER, true ) const traderBefore = await getBalances(trader.address) await router.connect(trader)['multicall(bytes[])']([...swapV3, ...swapV2, ...mergeSwap]) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(6)) expect(traderAfter.token2).to.be.eq(traderBefore.token1.add(1)) }) }) describe('interleaving', () => { // 0 -V3-> 1 -V2-> 2 it('exactIn 0 -V3-> 1 -V2-> 2', async () => { const swapV3 = await exactInputV3( tokens.slice(0, 2).map((token) => token.address), 10, 8, ADDRESS_THIS ) const swapV2 = await exactInputV2( tokens.slice(1, 3).map((token) => token.address), CONTRACT_BALANCE, 7, MSG_SENDER, true ) const traderBefore = await getBalances(trader.address) await router.connect(trader)['multicall(bytes[])']([...swapV3, ...swapV2]) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(10)) expect(traderAfter.token2).to.be.eq(traderBefore.token2.add(7)) const routerAmountOut = traderAfter.token2.sub(traderBefore.token2) // expect to equal quoter output const { amountOut: quoterAmountOut } = await quoter.callStatic['quoteExactInput(bytes,uint256)']( encodePath([tokens[0].address, tokens[1].address, tokens[2].address], [FeeAmount.MEDIUM, V2_FEE_PLACEHOLDER]), 10 ) expect(quoterAmountOut.eq(routerAmountOut)).to.be.true }) it('exactIn 0 -V2-> 1 -V3-> 2', async () => { const swapV2 = await exactInputV2( tokens.slice(0, 2).map((token) => token.address), 10, 9, ADDRESS_THIS ) const swapV3 = await exactInputV3( tokens.slice(1, 3).map((token) => token.address), CONTRACT_BALANCE, 7, MSG_SENDER, true ) const traderBefore = await getBalances(trader.address) await router.connect(trader)['multicall(bytes[])']([...swapV2, ...swapV3]) const traderAfter = await getBalances(trader.address) expect(traderAfter.token0).to.be.eq(traderBefore.token0.sub(10)) expect(traderAfter.token2).to.be.eq(traderBefore.token2.add(7)) const routerAmountOut = traderAfter.token2.sub(traderBefore.token2) // expect to equal quoter output const { amountOut: quoterAmountOut } = await quoter.callStatic['quoteExactInput(bytes,uint256)']( encodePath([tokens[0].address, tokens[1].address, tokens[2].address], [V2_FEE_PLACEHOLDER, FeeAmount.MEDIUM]), 10 ) expect(quoterAmountOut.eq(routerAmountOut)).to.be.true }) }) }) })