UNPKG

@airdao/astra-universal-router

Version:

Smart contracts for Universal Router

1,233 lines (1,072 loc) 53.4 kB
import { TransactionReceipt } from '@ethersproject/abstract-provider' import { Pair } from '@airdao/astra-classic-sdk' import { FeeAmount } from '@airdao/astra-cl-sdk' import { parseEvents, CLASSIC_EVENTS, CL_EVENTS } from './shared/parseEvents' import { expect } from './shared/expect' import { encodePath } from './shared/swapRouter02Helpers' import { BigNumber, BigNumberish, Contract } from 'ethers' import { ERC20, ERC20__factory, ISAMB, ISAMB__factory, Permit2, UniversalRouter } from '../../typechain' import { BOND, KOS, resetFork, SAMB, USDC } from './shared/testnetForkHelpers' import { ADDRESS_THIS, ALICE_ADDRESS, CONTRACT_BALANCE, DEADLINE, AMB_ADDRESS, MAX_UINT, MAX_UINT160, MSG_SENDER, ONE_PERCENT_BIPS, SOURCE_MSG_SENDER, SOURCE_ROUTER, TOKEN_ABI, } from './shared/constants' import { expandTo18DecimalsBN, expandTo6DecimalsBN } from './shared/helpers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import deployUniversalRouter, { deployPermit2 } from './shared/deployUniversalRouter' import { RoutePlanner, CommandType } from './shared/planner' import hre from 'hardhat' import { getPermitSignature, getPermitBatchSignature, PermitSingle } from './shared/protocolHelpers/permit2' const { ethers } = hre describe('Astra Classic and CL Tests:', () => { let alice: SignerWithAddress let bob: SignerWithAddress let router: UniversalRouter let permit2: Permit2 let bondContract: Contract let sambContract: ISAMB let usdcContract: ERC20 let planner: RoutePlanner before(async () => { await hre.network.provider.request({ method: 'hardhat_impersonateAccount', params: [ALICE_ADDRESS], }) await hre.network.provider.request({ method: 'hardhat_setBalance', params: [ALICE_ADDRESS, '0x10000000000000000000000'], }) alice = await ethers.getSigner(ALICE_ADDRESS) bob = (await ethers.getSigners())[1] bondContract = ERC20__factory.connect(BOND.address, bob) sambContract = ISAMB__factory.connect(SAMB.address, bob) usdcContract = ERC20__factory.connect(USDC.address, bob) }) beforeEach(async () => { await resetFork() await hre.network.provider.request({ method: 'hardhat_impersonateAccount', params: [ALICE_ADDRESS], }) alice = await ethers.getSigner(ALICE_ADDRESS) await hre.network.provider.request({ method: 'hardhat_setBalance', params: [ALICE_ADDRESS, '0x10000000000000000000000'], }) bob = (await ethers.getSigners())[1] await ISAMB__factory.connect(SAMB.address, alice) .deposit({ value: expandTo18DecimalsBN(1000) }) .then(async (t) => await t.wait()) permit2 = (await deployPermit2()).connect(bob) as Permit2 router = (await deployUniversalRouter(permit2)).connect(bob) as UniversalRouter planner = new RoutePlanner() // alice gives bob some tokens await bondContract.connect(alice).transfer(bob.address, expandTo18DecimalsBN(100000)) await sambContract.connect(alice).transfer(bob.address, expandTo18DecimalsBN(100)) await usdcContract.connect(alice).transfer(bob.address, expandTo6DecimalsBN(100000)) // Bob max-approves the permit2 contract to access his BOND and SAMB await bondContract.connect(bob).approve(permit2.address, MAX_UINT) await sambContract.connect(bob).approve(permit2.address, MAX_UINT) await usdcContract.connect(bob).approve(permit2.address, MAX_UINT) }) describe('Trade on Astra with Permit2, giving approval every time', () => { describe('ERC20 --> ERC20', () => { let permit: PermitSingle it('Classic exactIn, permiting the exact amount', async () => { const amountInBOND = expandTo18DecimalsBN(100) const minAmountOutSAMB = expandTo18DecimalsBN(0.03) // second bob signs a permit to allow the router to access his BOND permit = { details: { token: BOND.address, amount: amountInBOND, expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, spender: router.address, sigDeadline: DEADLINE, } const sig = await getPermitSignature(permit, bob, permit2) // 1) permit the router to access funds, 2) withdraw the funds into the pair, 3) trade planner.addCommand(CommandType.PERMIT2_PERMIT, [permit, sig]) planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ MSG_SENDER, amountInBOND, minAmountOutSAMB, [BOND.address, SAMB.address], SOURCE_MSG_SENDER, ]) const { sambBalanceBefore, sambBalanceAfter, bondBalanceAfter, bondBalanceBefore } = await executeRouter( planner ) expect(sambBalanceAfter.sub(sambBalanceBefore)).to.be.gte(minAmountOutSAMB) expect(bondBalanceBefore.sub(bondBalanceAfter)).to.be.eq(amountInBOND) }) it('Classic exactOut, permiting the maxAmountIn', async () => { const maxAmountInBOND = expandTo18DecimalsBN(3000) const amountOutSAMB = expandTo18DecimalsBN(1) // second bob signs a permit to allow the router to access his BOND permit = { details: { token: BOND.address, amount: maxAmountInBOND, expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, spender: router.address, sigDeadline: DEADLINE, } const sig = await getPermitSignature(permit, bob, permit2) // 1) permit the router to access funds, 2) trade - the transfer happens within the trade for exactOut planner.addCommand(CommandType.PERMIT2_PERMIT, [permit, sig]) planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_OUT, [ MSG_SENDER, amountOutSAMB, maxAmountInBOND, [BOND.address, SAMB.address], SOURCE_MSG_SENDER, ]) const { sambBalanceBefore, sambBalanceAfter, bondBalanceAfter, bondBalanceBefore } = await executeRouter( planner ) expect(sambBalanceAfter.sub(sambBalanceBefore)).to.be.eq(amountOutSAMB) expect(bondBalanceBefore.sub(bondBalanceAfter)).to.be.lte(maxAmountInBOND) }) it('Classic exactIn, swapping more than max_uint160 should revert', async () => { const max_uint = BigNumber.from(MAX_UINT160) const minAmountOutSAMB = expandTo18DecimalsBN(0.03) // second bob signs a permit to allow the router to access his BOND permit = { details: { token: BOND.address, amount: max_uint, expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, spender: router.address, sigDeadline: DEADLINE, } const sig = await getPermitSignature(permit, bob, permit2) // 1) permit the router to access funds, 2) withdraw the funds into the pair, 3) trade planner.addCommand(CommandType.PERMIT2_PERMIT, [permit, sig]) planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ MSG_SENDER, BigNumber.from(MAX_UINT160).add(1), minAmountOutSAMB, [BOND.address, SAMB.address], SOURCE_MSG_SENDER, ]) const testCustomErrors = await (await ethers.getContractFactory('TestCustomErrors')).deploy() await expect(executeRouter(planner)).to.be.revertedWithCustomError(testCustomErrors, 'UnsafeCast') }) it('CL exactIn, permiting the exact amount', async () => { const amountInBOND = expandTo18DecimalsBN(100) const minAmountOutSAMB = expandTo18DecimalsBN(0.03) // first bob approves permit2 to access his BOND await bondContract.connect(bob).approve(permit2.address, MAX_UINT) // second bob signs a permit to allow the router to access his BOND permit = { details: { token: BOND.address, amount: amountInBOND, expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, spender: router.address, sigDeadline: DEADLINE, } const sig = await getPermitSignature(permit, bob, permit2) const path = encodePathExactInput([BOND.address, SAMB.address]) // 1) permit the router to access funds, 2) trade, which takes the funds directly from permit2 planner.addCommand(CommandType.PERMIT2_PERMIT, [permit, sig]) planner.addCommand(CommandType.CL_SWAP_EXACT_IN, [ MSG_SENDER, amountInBOND, minAmountOutSAMB, path, SOURCE_MSG_SENDER, ]) const { sambBalanceBefore, sambBalanceAfter, bondBalanceAfter, bondBalanceBefore } = await executeRouter( planner ) expect(sambBalanceAfter.sub(sambBalanceBefore)).to.be.gte(minAmountOutSAMB) expect(bondBalanceBefore.sub(bondBalanceAfter)).to.be.eq(amountInBOND) }) it('CL exactOut, permiting the exact amount', async () => { const maxAmountInBOND = expandTo18DecimalsBN(3000) const amountOutSAMB = expandTo18DecimalsBN(1) // first bob approves permit2 to access his BOND await bondContract.connect(bob).approve(permit2.address, MAX_UINT) // second bob signs a permit to allow the router to access his BOND permit = { details: { token: BOND.address, amount: maxAmountInBOND, expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, spender: router.address, sigDeadline: DEADLINE, } const sig = await getPermitSignature(permit, bob, permit2) const path = encodePathExactOutput([BOND.address, SAMB.address]) // 1) permit the router to access funds, 2) trade, which takes the funds directly from permit2 planner.addCommand(CommandType.PERMIT2_PERMIT, [permit, sig]) planner.addCommand(CommandType.CL_SWAP_EXACT_OUT, [ MSG_SENDER, amountOutSAMB, maxAmountInBOND, path, SOURCE_MSG_SENDER, ]) const { sambBalanceBefore, sambBalanceAfter, bondBalanceAfter, bondBalanceBefore } = await executeRouter( planner ) expect(sambBalanceAfter.sub(sambBalanceBefore)).to.be.eq(amountOutSAMB) expect(bondBalanceBefore.sub(bondBalanceAfter)).to.be.lte(maxAmountInBOND) }) }) }) describe('Trade on AstraClassic', () => { const amountIn: BigNumber = expandTo18DecimalsBN(5) beforeEach(async () => { // for these tests Bob gives the router max approval on permit2 await permit2.approve(BOND.address, router.address, MAX_UINT160, DEADLINE) await permit2.approve(SAMB.address, router.address, MAX_UINT160, DEADLINE) }) describe('ERC20 --> ERC20', () => { it('completes a Classic exactIn swap', async () => { const minAmountOut = expandTo18DecimalsBN(0.0001) planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ MSG_SENDER, amountIn, minAmountOut, [BOND.address, SAMB.address], SOURCE_MSG_SENDER, ]) const { sambBalanceBefore, sambBalanceAfter } = await executeRouter(planner) expect(sambBalanceAfter.sub(sambBalanceBefore)).to.be.gt(minAmountOut) }) it('completes a Classic exactOut swap', async () => { const amountOut = expandTo18DecimalsBN(1) planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_OUT, [ MSG_SENDER, amountOut, expandTo18DecimalsBN(10000), [SAMB.address, BOND.address], SOURCE_MSG_SENDER, ]) planner.addCommand(CommandType.SWEEP, [SAMB.address, MSG_SENDER, 0]) const { bondBalanceBefore, bondBalanceAfter } = await executeRouter(planner) expect(bondBalanceAfter.sub(bondBalanceBefore)).to.be.gt(amountOut) }) it('exactIn trade, where an output fee is taken', async () => { // back to the router so someone can take a fee planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ ADDRESS_THIS, amountIn, 1, [BOND.address, SAMB.address], SOURCE_MSG_SENDER, ]) planner.addCommand(CommandType.PAY_PORTION, [SAMB.address, alice.address, ONE_PERCENT_BIPS]) planner.addCommand(CommandType.SWEEP, [SAMB.address, MSG_SENDER, 1]) const { commands, inputs } = planner const sambBalanceBeforeAlice = await sambContract.balanceOf(alice.address) const sambBalanceBeforeBob = await sambContract.balanceOf(bob.address) await router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE) const sambBalanceAfterAlice = await sambContract.balanceOf(alice.address) const sambBalanceAfterBob = await sambContract.balanceOf(bob.address) const aliceFee = sambBalanceAfterAlice.sub(sambBalanceBeforeAlice) const bobEarnings = sambBalanceAfterBob.sub(sambBalanceBeforeBob) expect(bobEarnings).to.be.gt(0) expect(aliceFee).to.be.gt(0) // total fee is 1% of bob's output expect(aliceFee.add(bobEarnings).mul(ONE_PERCENT_BIPS).div(10_000)).to.eq(aliceFee) }) it('completes a Classic exactIn swap with longer path', async () => { const minAmountOut = expandTo18DecimalsBN(0.0001) planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ MSG_SENDER, amountIn, minAmountOut, [BOND.address, USDC.address, SAMB.address], SOURCE_MSG_SENDER, ]) const { sambBalanceBefore, sambBalanceAfter } = await executeRouter(planner) expect(sambBalanceAfter.sub(sambBalanceBefore)).to.be.gt(minAmountOut) }) }) describe('ERC20 --> AMB', () => { it('completes a Classic exactIn swap', async () => { planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ ADDRESS_THIS, amountIn, 1, [BOND.address, SAMB.address], SOURCE_MSG_SENDER, ]) planner.addCommand(CommandType.UNWRAP_SAMB, [MSG_SENDER, 0]) const { gasSpent, ambBalanceBefore, ambBalanceAfter, classicSwapEventArgs } = await executeRouter(planner) const { amount0Out: sambTraded } = classicSwapEventArgs! expect(ambBalanceAfter.sub(ambBalanceBefore)).to.eq(sambTraded.sub(gasSpent)) }) it('completes a Classic exactOut swap', async () => { const amountOut = expandTo18DecimalsBN(1) planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_OUT, [ ADDRESS_THIS, amountOut, expandTo18DecimalsBN(10000), [BOND.address, SAMB.address], SOURCE_MSG_SENDER, ]) planner.addCommand(CommandType.UNWRAP_SAMB, [MSG_SENDER, amountOut]) planner.addCommand(CommandType.SWEEP, [BOND.address, MSG_SENDER, 0]) const { gasSpent, ambBalanceBefore, ambBalanceAfter, classicSwapEventArgs } = await executeRouter(planner) const { amount0Out: sambTraded } = classicSwapEventArgs! expect(ambBalanceAfter.sub(ambBalanceBefore)).to.eq(amountOut.sub(gasSpent)) expect(sambTraded).to.eq(amountOut) }) it('completes a Classic exactOut swap, with AMB fee', async () => { const amountOut = expandTo18DecimalsBN(1) const totalPortion = amountOut.mul(ONE_PERCENT_BIPS).div(10000) const actualAmountOut = amountOut.sub(totalPortion) planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_OUT, [ ADDRESS_THIS, amountOut, expandTo18DecimalsBN(10000), [BOND.address, SAMB.address], SOURCE_MSG_SENDER, ]) planner.addCommand(CommandType.UNWRAP_SAMB, [ADDRESS_THIS, amountOut]) planner.addCommand(CommandType.PAY_PORTION, [AMB_ADDRESS, alice.address, ONE_PERCENT_BIPS]) planner.addCommand(CommandType.SWEEP, [AMB_ADDRESS, MSG_SENDER, 0]) const { commands, inputs } = planner await expect(router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE)).to.changeEtherBalances( [alice, bob], [totalPortion, actualAmountOut] ) }) }) describe('AMB --> ERC20', () => { it('completes a Classic exactIn swap', async () => { const minAmountOut = expandTo18DecimalsBN(49) const pairAddress = Pair.getAddress(BOND, SAMB) planner.addCommand(CommandType.WRAP_AMB, [pairAddress, amountIn]) // amountIn of 0 because the samb is already in the pair planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ MSG_SENDER, 0, minAmountOut, [SAMB.address, BOND.address], SOURCE_MSG_SENDER, ]) const { bondBalanceBefore, bondBalanceAfter, classicSwapEventArgs } = await executeRouter(planner, amountIn) const { amount1Out: bondTraded } = classicSwapEventArgs! expect(bondBalanceAfter.sub(bondBalanceBefore)).to.be.gt(minAmountOut) expect(bondBalanceAfter.sub(bondBalanceBefore)).to.equal(bondTraded) }) it('completes a Classic exactOut swap', async () => { const amountOut = expandTo18DecimalsBN(100) const value = expandTo18DecimalsBN(11) planner.addCommand(CommandType.WRAP_AMB, [ADDRESS_THIS, value]) planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_OUT, [ MSG_SENDER, amountOut, expandTo18DecimalsBN(11), [SAMB.address, BOND.address], SOURCE_ROUTER, ]) planner.addCommand(CommandType.UNWRAP_SAMB, [MSG_SENDER, 0]) const { ambBalanceBefore, ambBalanceAfter, bondBalanceBefore, bondBalanceAfter, classicSwapEventArgs, gasSpent, } = await executeRouter(planner, value) const { amount1Out: bondTraded, amount0In: sambTraded } = classicSwapEventArgs! expect(bondBalanceAfter.sub(bondBalanceBefore)).gt(amountOut) // rounding expect(bondBalanceAfter.sub(bondBalanceBefore)).eq(bondTraded) expect(ambBalanceBefore.sub(ambBalanceAfter)).to.eq(sambTraded.add(gasSpent)) }) }) }) describe('Trade on AstraCL', () => { const amountIn: BigNumber = expandTo18DecimalsBN(500) const amountInMax: BigNumber = expandTo18DecimalsBN(2000) const amountOut: BigNumber = expandTo18DecimalsBN(1) beforeEach(async () => { // for these tests Bob gives the router max approval on permit2 await permit2.approve(BOND.address, router.address, MAX_UINT160, DEADLINE) await permit2.approve(SAMB.address, router.address, MAX_UINT160, DEADLINE) }) const addCLExactInTrades = ( planner: RoutePlanner, numTrades: BigNumberish, amountOutMin: BigNumberish, recipient?: string, tokens: string[] = [BOND.address, SAMB.address], tokenSource: boolean = SOURCE_MSG_SENDER ) => { const path = encodePathExactInput(tokens) for (let i = 0; i < Number(numTrades); i++) { planner.addCommand(CommandType.CL_SWAP_EXACT_IN, [ recipient ?? MSG_SENDER, amountIn, amountOutMin, path, tokenSource, ]) } } describe('ERC20 --> ERC20', () => { it('completes a CL exactIn swap', async () => { const amountOutMin: BigNumber = expandTo18DecimalsBN(0.0005) addCLExactInTrades(planner, 1, amountOutMin) const { sambBalanceBefore, sambBalanceAfter, clSwapEventArgs } = await executeRouter(planner) const { amount1: sambTraded } = clSwapEventArgs! expect(sambBalanceAfter.sub(sambBalanceBefore)).to.be.gte(amountOutMin) expect(sambBalanceAfter.sub(sambBalanceBefore)).to.eq(sambTraded.mul(-1)) }) it('completes a CL exactIn swap with longer path', async () => { const amountOutMin: number = 3 * 10 ** 6 addCLExactInTrades( planner, 1, amountOutMin, MSG_SENDER, [BOND.address, SAMB.address, USDC.address], SOURCE_MSG_SENDER ) const { bondBalanceBefore, bondBalanceAfter, sambBalanceBefore, sambBalanceAfter, usdcBalanceBefore, usdcBalanceAfter, } = await executeRouter(planner) expect(bondBalanceBefore.sub(amountIn)).to.eq(bondBalanceAfter) expect(sambBalanceAfter).to.eq(sambBalanceBefore) expect(usdcBalanceAfter.sub(usdcBalanceBefore)).to.be.gte(amountOutMin) }) it('completes a CL exactOut swap', async () => { // trade BOND in for SAMB out const tokens = [BOND.address, SAMB.address] const path = encodePathExactOutput(tokens) planner.addCommand(CommandType.CL_SWAP_EXACT_OUT, [MSG_SENDER, amountOut, amountInMax, path, SOURCE_MSG_SENDER]) const { sambBalanceBefore, sambBalanceAfter, clSwapEventArgs } = await executeRouter(planner) const { amount0: bondTraded } = clSwapEventArgs! expect(sambBalanceAfter.sub(sambBalanceBefore)).to.eq(amountOut) expect(bondTraded).to.be.lt(amountInMax) }) it('completes a CL exactOut swap with longer path', async () => { // trade BOND in for SAMB out const tokens = [BOND.address, USDC.address, SAMB.address] const path = encodePathExactOutput(tokens) // for these tests Bob gives the router max approval on permit2 // await permit2.approve(BOND.address, router.address, MAX_UINT160, DEADLINE) planner.addCommand(CommandType.CL_SWAP_EXACT_OUT, [MSG_SENDER, amountOut, amountInMax, path, SOURCE_MSG_SENDER]) const { commands, inputs } = planner const balanceWethBefore = await sambContract.balanceOf(bob.address) await router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE) const balanceWethAfter = await sambContract.balanceOf(bob.address) expect(balanceWethAfter.sub(balanceWethBefore)).to.eq(amountOut) }) }) describe('ERC20 --> AMB', () => { it('completes a CL exactIn swap', async () => { const amountOutMin: BigNumber = expandTo18DecimalsBN(0.0005) addCLExactInTrades(planner, 1, amountOutMin, ADDRESS_THIS) planner.addCommand(CommandType.UNWRAP_SAMB, [MSG_SENDER, 0]) const { ambBalanceBefore, ambBalanceAfter, clSwapEventArgs, gasSpent } = await executeRouter(planner) const { amount1: sambTraded } = clSwapEventArgs! expect(ambBalanceAfter.sub(ambBalanceBefore)).to.be.gte(amountOutMin.sub(gasSpent)) expect(ambBalanceAfter.sub(ambBalanceBefore)).to.eq(sambTraded.mul(-1).sub(gasSpent)) }) it('completes a CL exactOut swap', async () => { // trade BOND in for SAMB out const tokens = [BOND.address, SAMB.address] const path = encodePathExactOutput(tokens) planner.addCommand(CommandType.CL_SWAP_EXACT_OUT, [ ADDRESS_THIS, amountOut, amountInMax, path, SOURCE_MSG_SENDER, ]) planner.addCommand(CommandType.UNWRAP_SAMB, [MSG_SENDER, amountOut]) const { ambBalanceBefore, ambBalanceAfter, gasSpent } = await executeRouter(planner) expect(ambBalanceAfter.sub(ambBalanceBefore)).to.eq(amountOut.sub(gasSpent)) }) }) describe('AMB --> ERC20', () => { it('completes a CL exactIn swap', async () => { const tokens = [SAMB.address, BOND.address] const amountOutMin: BigNumber = expandTo18DecimalsBN(0.0005) planner.addCommand(CommandType.WRAP_AMB, [ADDRESS_THIS, amountIn]) addCLExactInTrades(planner, 1, amountOutMin, MSG_SENDER, tokens, SOURCE_ROUTER) const { ambBalanceBefore, ambBalanceAfter, bondBalanceBefore, bondBalanceAfter, gasSpent } = await executeRouter(planner, amountIn) expect(ambBalanceBefore.sub(ambBalanceAfter)).to.eq(amountIn.add(gasSpent)) expect(bondBalanceAfter.sub(bondBalanceBefore)).to.be.gte(amountOutMin) }) it('completes a CL exactOut swap', async () => { const tokens = [SAMB.address, BOND.address] const path = encodePathExactOutput(tokens) planner.addCommand(CommandType.WRAP_AMB, [ADDRESS_THIS, amountInMax]) planner.addCommand(CommandType.CL_SWAP_EXACT_OUT, [MSG_SENDER, amountOut, amountInMax, path, SOURCE_ROUTER]) planner.addCommand(CommandType.UNWRAP_SAMB, [MSG_SENDER, 0]) const { ambBalanceBefore, ambBalanceAfter, bondBalanceBefore, bondBalanceAfter, gasSpent, clSwapEventArgs } = await executeRouter(planner, amountInMax) const { amount0: bondTraded, amount1: sambTraded } = clSwapEventArgs! expect(bondBalanceBefore.sub(bondBalanceAfter)).to.eq(bondTraded) expect(ambBalanceBefore.sub(ambBalanceAfter)).to.eq(sambTraded.add(gasSpent)) }) }) }) describe('Mixing Classic and CL', () => { beforeEach(async () => { // for these tests Bob gives the router max approval on permit2 await permit2.approve(BOND.address, router.address, MAX_UINT160, DEADLINE) await permit2.approve(SAMB.address, router.address, MAX_UINT160, DEADLINE) await permit2.approve(USDC.address, router.address, MAX_UINT160, DEADLINE) }) describe('Interleaving routes', () => { it('CL, then Classic', async () => { const clTokens = [BOND.address, USDC.address] const classicTokens = [USDC.address, SAMB.address] const clAmountIn: BigNumber = expandTo18DecimalsBN(5) const clAmountOutMin = 0 const classicAmountOutMin = expandTo18DecimalsBN(0.0005) planner.addCommand(CommandType.CL_SWAP_EXACT_IN, [ Pair.getAddress(USDC, SAMB), clAmountIn, clAmountOutMin, encodePathExactInput(clTokens), SOURCE_MSG_SENDER, ]) // amountIn of 0 because the USDC is already in the pair planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ MSG_SENDER, 0, classicAmountOutMin, classicTokens, SOURCE_MSG_SENDER, ]) const { sambBalanceBefore, sambBalanceAfter, classicSwapEventArgs } = await executeRouter(planner) const { amount1Out: sambTraded } = classicSwapEventArgs! expect(sambBalanceAfter.sub(sambBalanceBefore)).to.eq(sambTraded) }) it('Classic, then CL', async () => { const classicTokens = [BOND.address, USDC.address] const clTokens = [USDC.address, SAMB.address] const classicAmountIn: BigNumber = expandTo18DecimalsBN(5) const classicAmountOutMin = 0 // doesnt matter how much USDC it is, what matters is the end of the trade const clAmountOutMin = expandTo18DecimalsBN(0.0005) planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ ADDRESS_THIS, classicAmountIn, classicAmountOutMin, classicTokens, SOURCE_MSG_SENDER, ]) planner.addCommand(CommandType.CL_SWAP_EXACT_IN, [ MSG_SENDER, CONTRACT_BALANCE, clAmountOutMin, encodePathExactInput(clTokens), SOURCE_ROUTER, ]) const { sambBalanceBefore, sambBalanceAfter, clSwapEventArgs } = await executeRouter(planner) const { amount1: sambTraded } = clSwapEventArgs! expect(sambBalanceAfter.sub(sambBalanceBefore)).to.eq(sambTraded.mul(-1)) }) }) describe('Split routes', () => { it('ERC20 --> ERC20 split Classic and Classic different routes, each two hop, with explicit permit transfer from', async () => { const route1 = [BOND.address, USDC.address, SAMB.address] const route2 = [BOND.address, KOS.address, SAMB.address] const classicAmountIn1: BigNumber = expandTo18DecimalsBN(20) const classicAmountIn2: BigNumber = expandTo18DecimalsBN(30) const minAmountOut1 = expandTo18DecimalsBN(0.005) const minAmountOut2 = expandTo18DecimalsBN(0.0075) // 1) transfer funds into BOND-USDC and BOND-KOS pairs to trade planner.addCommand(CommandType.PERMIT2_TRANSFER_FROM, [ BOND.address, Pair.getAddress(BOND, USDC), classicAmountIn1, ]) planner.addCommand(CommandType.PERMIT2_TRANSFER_FROM, [ BOND.address, Pair.getAddress(BOND, KOS), classicAmountIn2, ]) // 2) trade route1 and return tokens to bob planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut1, route1, SOURCE_MSG_SENDER]) // 3) trade route2 and return tokens to bob planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut2, route2, SOURCE_MSG_SENDER]) const { sambBalanceBefore, sambBalanceAfter } = await executeRouter(planner) expect(sambBalanceAfter.sub(sambBalanceBefore)).to.be.gte(minAmountOut1.add(minAmountOut2)) }) it('ERC20 --> ERC20 split Classic and Classic different routes, each two hop, with explicit permit transfer from batch', async () => { const route1 = [BOND.address, USDC.address, SAMB.address] const route2 = [BOND.address, KOS.address, SAMB.address] const classicAmountIn1: BigNumber = expandTo18DecimalsBN(20) const classicAmountIn2: BigNumber = expandTo18DecimalsBN(30) const minAmountOut1 = expandTo18DecimalsBN(0.005) const minAmountOut2 = expandTo18DecimalsBN(0.0075) const BATCH_TRANSFER = [ { from: bob.address, to: Pair.getAddress(BOND, USDC), amount: classicAmountIn1, token: BOND.address, }, { from: bob.address, to: Pair.getAddress(BOND, KOS), amount: classicAmountIn2, token: BOND.address, }, ] // 1) transfer funds into BOND-USDC and BOND-KOS pairs to trade planner.addCommand(CommandType.PERMIT2_TRANSFER_FROM_BATCH, [BATCH_TRANSFER]) // 2) trade route1 and return tokens to bob planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut1, route1, SOURCE_MSG_SENDER]) // 3) trade route2 and return tokens to bob planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [MSG_SENDER, 0, minAmountOut2, route2, SOURCE_MSG_SENDER]) const { sambBalanceBefore, sambBalanceAfter } = await executeRouter(planner) expect(sambBalanceAfter.sub(sambBalanceBefore)).to.be.gte(minAmountOut1.add(minAmountOut2)) }) it('ERC20 --> ERC20 split Classic and Classic different routes, each two hop, without explicit permit', async () => { const route1 = [BOND.address, USDC.address, SAMB.address] const route2 = [BOND.address, KOS.address, SAMB.address] const classicAmountIn1: BigNumber = expandTo18DecimalsBN(20) const classicAmountIn2: BigNumber = expandTo18DecimalsBN(30) const minAmountOut1 = expandTo18DecimalsBN(0.005) const minAmountOut2 = expandTo18DecimalsBN(0.0075) // 1) trade route1 and return tokens to bob planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ MSG_SENDER, classicAmountIn1, minAmountOut1, route1, SOURCE_MSG_SENDER, ]) // 2) trade route2 and return tokens to bob planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ MSG_SENDER, classicAmountIn2, minAmountOut2, route2, SOURCE_MSG_SENDER, ]) const { sambBalanceBefore, sambBalanceAfter } = await executeRouter(planner) expect(sambBalanceAfter.sub(sambBalanceBefore)).to.be.gte(minAmountOut1.add(minAmountOut2)) }) it('ERC20 --> ERC20 split Classic and Classic different routes, different input tokens, each two hop, with batch permit', async () => { const route1 = [BOND.address, SAMB.address, USDC.address] const route2 = [SAMB.address, BOND.address, USDC.address] const classicAmountIn1: BigNumber = expandTo18DecimalsBN(20) const classicAmountIn2: BigNumber = expandTo18DecimalsBN(5) const minAmountOut1 = BigNumber.from(0.005 * 10 ** 6) const minAmountOut2 = BigNumber.from(0.0075 * 10 ** 6) const BATCH_PERMIT = { details: [ { token: BOND.address, amount: classicAmountIn1, expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, { token: SAMB.address, amount: classicAmountIn2, 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 BOND-USDC and BOND-KOS pairs to trade planner.addCommand(CommandType.PERMIT2_PERMIT_BATCH, [BATCH_PERMIT, sig]) // 2) trade route1 and return tokens to bob planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ MSG_SENDER, classicAmountIn1, minAmountOut1, route1, SOURCE_MSG_SENDER, ]) // 3) trade route2 and return tokens to bob planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ MSG_SENDER, classicAmountIn2, minAmountOut2, route2, SOURCE_MSG_SENDER, ]) const { usdcBalanceBefore, usdcBalanceAfter } = await executeRouter(planner) expect(usdcBalanceAfter.sub(usdcBalanceBefore)).to.be.gte(minAmountOut1.add(minAmountOut2)) }) it('ERC20 --> ERC20 CL trades with different input tokens with batch permit and batch transfer', async () => { const route1 = [BOND.address, SAMB.address] const route2 = [SAMB.address, USDC.address] const clAmountIn1: BigNumber = expandTo18DecimalsBN(20) const clAmountIn2: BigNumber = expandTo18DecimalsBN(5) const minAmountOut1SAMB = BigNumber.from(0) const minAmountOut1USDC = BigNumber.from(0.005 * 10 ** 6) const minAmountOut2USDC = BigNumber.from(0.0075 * 10 ** 6) const BATCH_PERMIT = { details: [ { token: BOND.address, amount: clAmountIn1, expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, { token: SAMB.address, amount: clAmountIn2, 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: clAmountIn1, token: BOND.address, }, { from: bob.address, to: router.address, amount: clAmountIn2, token: SAMB.address, }, ] const sig = await getPermitBatchSignature(BATCH_PERMIT, bob, permit2) // 1) permit bond and samb to be spent by router planner.addCommand(CommandType.PERMIT2_PERMIT_BATCH, [BATCH_PERMIT, sig]) // 2) transfer bond and samb into router to use contract balance planner.addCommand(CommandType.PERMIT2_TRANSFER_FROM_BATCH, [BATCH_TRANSFER]) // clSwapExactInput(recipient, amountIn, amountOutMin, path, payer); // 2) trade route1 and return tokens to router for the second trade planner.addCommand(CommandType.CL_SWAP_EXACT_IN, [ ADDRESS_THIS, CONTRACT_BALANCE, minAmountOut1SAMB, encodePathExactInput(route1), SOURCE_ROUTER, ]) // 3) trade route2 and return tokens to bob planner.addCommand(CommandType.CL_SWAP_EXACT_IN, [ MSG_SENDER, CONTRACT_BALANCE, minAmountOut1USDC.add(minAmountOut2USDC), encodePathExactInput(route2), SOURCE_ROUTER, ]) const { usdcBalanceBefore, usdcBalanceAfter } = await executeRouter(planner) expect(usdcBalanceAfter.sub(usdcBalanceBefore)).to.be.gte(minAmountOut1USDC.add(minAmountOut2USDC)) }) it('ERC20 --> ERC20 split Classic and CL, one hop', async () => { const tokens = [BOND.address, SAMB.address] const classicAmountIn: BigNumber = expandTo18DecimalsBN(2) const clAmountIn: BigNumber = expandTo18DecimalsBN(3) const minAmountOut = expandTo18DecimalsBN(0.0005) // Classic trades BOND for USDC, sending the tokens back to the router for cl trade planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ ADDRESS_THIS, classicAmountIn, 0, tokens, SOURCE_MSG_SENDER, ]) // CL trades USDC for SAMB, trading the whole balance, with a recipient of Alice planner.addCommand(CommandType.CL_SWAP_EXACT_IN, [ ADDRESS_THIS, clAmountIn, 0, encodePathExactInput(tokens), SOURCE_MSG_SENDER, ]) // aggregate slippage check planner.addCommand(CommandType.SWEEP, [SAMB.address, MSG_SENDER, minAmountOut]) const { sambBalanceBefore, sambBalanceAfter, classicSwapEventArgs, clSwapEventArgs } = await executeRouter( planner ) const { amount1Out: sambOutClassic } = classicSwapEventArgs! let { amount1: sambOutCL } = clSwapEventArgs! // expect(bondBalanceBefore.sub(bondBalanceAfter)).to.eq(classicAmountIn.add(clAmountIn)) // TODO: with permit2 can check from alice's balance expect(sambBalanceAfter.sub(sambBalanceBefore)).to.eq(sambOutClassic.sub(sambOutCL)) }) it('AMB --> ERC20 split Classic and CL, one hop', async () => { const tokens = [SAMB.address, USDC.address] const classicAmountIn: BigNumber = expandTo18DecimalsBN(2) const clAmountIn: BigNumber = expandTo18DecimalsBN(3) const value = classicAmountIn.add(clAmountIn) planner.addCommand(CommandType.WRAP_AMB, [ADDRESS_THIS, value]) planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ADDRESS_THIS, classicAmountIn, 0, tokens, SOURCE_ROUTER]) planner.addCommand(CommandType.CL_SWAP_EXACT_IN, [ ADDRESS_THIS, clAmountIn, 0, encodePathExactInput(tokens), SOURCE_MSG_SENDER, ]) // aggregate slippage check planner.addCommand(CommandType.SWEEP, [USDC.address, MSG_SENDER, 0.0005 * 10 ** 6]) const { usdcBalanceBefore, usdcBalanceAfter, classicSwapEventArgs, clSwapEventArgs } = await executeRouter( planner, value ) const { amount0Out: usdcOutClassic } = classicSwapEventArgs! let { amount0: usdcOutCL } = clSwapEventArgs! usdcOutCL = usdcOutCL.mul(-1) expect(usdcBalanceAfter.sub(usdcBalanceBefore)).to.eq(usdcOutClassic.add(usdcOutCL)) }) it('ERC20 --> AMB split Classic and CL, one hop', async () => { const tokens = [BOND.address, SAMB.address] const classicAmountIn: BigNumber = expandTo18DecimalsBN(20) const clAmountIn: BigNumber = expandTo18DecimalsBN(30) planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ ADDRESS_THIS, classicAmountIn, 0, tokens, SOURCE_MSG_SENDER, ]) planner.addCommand(CommandType.CL_SWAP_EXACT_IN, [ ADDRESS_THIS, clAmountIn, 0, encodePathExactInput(tokens), SOURCE_MSG_SENDER, ]) // aggregate slippage check planner.addCommand(CommandType.UNWRAP_SAMB, [MSG_SENDER, expandTo18DecimalsBN(0.0005)]) const { ambBalanceBefore, ambBalanceAfter, gasSpent, classicSwapEventArgs, clSwapEventArgs } = await executeRouter(planner) const { amount1Out: sambOutClassic } = classicSwapEventArgs! let { amount1: sambOutCL } = clSwapEventArgs! sambOutCL = sambOutCL.mul(-1) expect(ambBalanceAfter.sub(ambBalanceBefore)).to.eq(sambOutClassic.add(sambOutCL).sub(gasSpent)) }) it('ERC20 --> AMB split Classic and CL, exactOut, one hop', async () => { const tokens = [BOND.address, SAMB.address] const classicAmountOut: BigNumber = expandTo18DecimalsBN(0.5) const clAmountOut: BigNumber = expandTo18DecimalsBN(1) const path = encodePathExactOutput(tokens) const maxAmountIn = expandTo18DecimalsBN(4000) const fullAmountOut = classicAmountOut.add(clAmountOut) planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_OUT, [ ADDRESS_THIS, classicAmountOut, maxAmountIn, [BOND.address, SAMB.address], SOURCE_MSG_SENDER, ]) planner.addCommand(CommandType.CL_SWAP_EXACT_OUT, [ ADDRESS_THIS, clAmountOut, maxAmountIn, path, SOURCE_MSG_SENDER, ]) // aggregate slippage check planner.addCommand(CommandType.UNWRAP_SAMB, [MSG_SENDER, fullAmountOut]) const { ambBalanceBefore, ambBalanceAfter, gasSpent } = await executeRouter(planner) // TODO: permit2 test alice doesn't send more than maxAmountIn BOND expect(ambBalanceAfter.sub(ambBalanceBefore)).to.eq(fullAmountOut.sub(gasSpent)) }) describe('Batch reverts', () => { let subplan: RoutePlanner let planOneTokens: string[] let planTwoTokens: string[] const planOneClassicAmountIn: BigNumber = expandTo18DecimalsBN(2) const planOneCLAmountIn: BigNumber = expandTo18DecimalsBN(3) const planTwoCLAmountIn = expandTo6DecimalsBN(5) beforeEach(async () => { subplan = new RoutePlanner() planOneTokens = [BOND.address, SAMB.address] planTwoTokens = [USDC.address, SAMB.address] }) it('2 sub-plans, neither fails', async () => { // first split route sub-plan. BOND->SAMB, 2 routes on Classic and CL. const planOneWethMinOut = expandTo18DecimalsBN(0.0005) // Classic trades BOND for USDC, sending the tokens back to the router for cl trade subplan.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ ADDRESS_THIS, planOneClassicAmountIn, 0, planOneTokens, SOURCE_MSG_SENDER, ]) // CL trades USDC for SAMB, trading the whole balance, with a recipient of Alice subplan.addCommand(CommandType.CL_SWAP_EXACT_IN, [ ADDRESS_THIS, planOneCLAmountIn, 0, encodePathExactInput(planOneTokens), SOURCE_MSG_SENDER, ]) // aggregate slippage check subplan.addCommand(CommandType.SWEEP, [SAMB.address, MSG_SENDER, planOneWethMinOut]) // add the subplan to the main planner planner.addSubPlan(subplan) subplan = new RoutePlanner() // second split route sub-plan. USDC->SAMB, 1 route on CL const sambMinAmountOut2 = expandTo18DecimalsBN(0.0005) // Add the trade to the sub-plan subplan.addCommand(CommandType.CL_SWAP_EXACT_IN, [ MSG_SENDER, planTwoCLAmountIn, sambMinAmountOut2, encodePathExactInput(planTwoTokens), SOURCE_MSG_SENDER, ]) // add the second subplan to the main planner planner.addSubPlan(subplan) const { usdcBalanceBefore, usdcBalanceAfter, bondBalanceBefore, bondBalanceAfter } = await executeRouter( planner ) expect(bondBalanceBefore.sub(bondBalanceAfter)).to.eq(planOneClassicAmountIn.add(planOneCLAmountIn)) expect(usdcBalanceBefore.sub(usdcBalanceAfter)).to.eq(planTwoCLAmountIn) }) it('2 sub-plans, the first fails', async () => { // first split route sub-plan. BOND->SAMB, 2 routes on Classic and CL. // FAIL: large samb amount out to cause a failure const planOneWethMinOut = expandTo18DecimalsBN(1) // Classic trades BOND for USDC, sending the tokens back to the router for cl trade subplan.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ ADDRESS_THIS, planOneClassicAmountIn, 0, planOneTokens, SOURCE_MSG_SENDER, ]) // CL trades USDC for SAMB, trading the whole balance, with a recipient of Alice subplan.addCommand(CommandType.CL_SWAP_EXACT_IN, [ ADDRESS_THIS, planOneCLAmountIn, 0, encodePathExactInput(planOneTokens), SOURCE_MSG_SENDER, ]) // aggregate slippage check subplan.addCommand(CommandType.SWEEP, [SAMB.address, MSG_SENDER, planOneWethMinOut]) // add the subplan to the main planner planner.addSubPlan(subplan) subplan = new RoutePlanner() // second split route sub-plan. USDC->SAMB, 1 route on CL const sambMinAmountOut2 = expandTo18DecimalsBN(0.0005) // Add the trade to the sub-plan subplan.addCommand(CommandType.CL_SWAP_EXACT_IN, [ MSG_SENDER, planTwoCLAmountIn, sambMinAmountOut2, encodePathExactInput(planTwoTokens), SOURCE_MSG_SENDER, ]) // add the second subplan to the main planner planner.addSubPlan(subplan) const { usdcBalanceBefore, usdcBalanceAfter, bondBalanceBefore, bondBalanceAfter } = await executeRouter( planner ) // bond balance should be unchanged as the samb sweep failed expect(bondBalanceBefore).to.eq(bondBalanceAfter) // usdc is the second trade so the balance has changed expect(usdcBalanceBefore.sub(usdcBalanceAfter)).to.eq(planTwoCLAmountIn) }) it('2 sub-plans, both fail but the transaction succeeds', async () => { // first split route sub-plan. BOND->SAMB, 2 routes on Classic and CL. // FAIL: large amount out to cause the swap to revert const planOneWethMinOut = expandTo18DecimalsBN(1) // Classic trades BOND for USDC, sending the tokens back to the router for cl trade subplan.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ ADDRESS_THIS, planOneClassicAmountIn, 0, planOneTokens, SOURCE_MSG_SENDER, ]) // CL trades USDC for SAMB, trading the whole balance, with a recipient of Alice subplan.addCommand(CommandType.CL_SWAP_EXACT_IN, [ ADDRESS_THIS, planOneCLAmountIn, 0, encodePathExactInput(planOneTokens), SOURCE_MSG_SENDER, ]) // aggregate slippage check subplan.addCommand(CommandType.SWEEP, [SAMB.address, MSG_SENDER, planOneWethMinOut]) // add the subplan to the main planner planner.addSubPlan(subplan) subplan = new RoutePlanner() // second split route sub-plan. USDC->SAMB, 1 route on CL // FAIL: large amount out to cause the swap to revert const sambMinAmountOut2 = expandTo18DecimalsBN(1) // Add the trade to the sub-plan subplan.addCommand(CommandType.CL_SWAP_EXACT_IN, [ MSG_SENDER, planTwoCLAmountIn, sambMinAmountOut2, encodePathExactInput(planTwoTokens), SOURCE_MSG_SENDER, ]) // add the second subplan to the main planner planner.addSubPlan(subplan) const { usdcBalanceBefore, usdcBalanceAfter, bondBalanceBefore, bondBalanceAfter } = await executeRouter( planner ) // bond and usdc balances both unchanged because both trades failed expect(bondBalanceBefore).to.eq(bondBalanceAfter) expect(usdcBalanceBefore).to.eq(usdcBalanceAfter) }) it('2 sub-plans, second sub plan fails', async () => { // first split route sub-plan. BOND->SAMB, 2 routes on Classic and CL. const planOneWethMinOut = expandTo18DecimalsBN(0.0005) // Classic trades BOND for USDC, sending the tokens back to the router for cl trade subplan.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ ADDRESS_THIS, planOneClassicAmountIn, 0, planOneTokens, SOURCE_MSG_SENDER, ]) // CL trades USDC for SAMB, trading the whole balance, with a recipient of Alice subplan.addCommand(CommandType.CL_SWAP_EXACT_IN, [ ADDRESS_THIS, planOneCLAmountIn, 0, encodePathExactInput(planOneTokens), SOURCE_MSG_SENDER, ]) // aggregate slippage check subplan.addCommand(CommandType.SWEEP, [SAMB.address, MSG_SENDER, planOneWethMinOut]) // add the subplan to the main planner planner.addSubPlan(subplan) subplan = new RoutePlanner() // second split route sub-plan. USDC->SAMB, 1 route on CL // FAIL: large amount out to cause the swap to revert const sambMinAmountOut2 = exp