UNPKG

@airdao/astra-universal-router

Version:

Smart contracts for Universal Router

612 lines (496 loc) 23.1 kB
import { encodeSqrtRatioX96, FeeAmount, Pool, TickMath, Route as CLRouteSDK } from '@airdao/astra-cl-sdk' import { Pair, Route as ClassicRouteSDK } from 'astra-classic-sdk' import { encodePath, expandTo18Decimals } from '../shared/swapRouter02Helpers' import { BigNumber } from 'ethers' import { SwapRouter } from '@airdao/astra-router-sdk' import { executeSwapRouter02Swap, resetFork, SAMB, BOND, USDC, KOS, approveSwapRouter02, } from '../shared/testnetForkHelpers' import { ALICE_ADDRESS, DEADLINE, MAX_UINT, MAX_UINT160, SOURCE_MSG_SENDER } from '../shared/constants' import { 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 { UniversalRouter, Permit2, ERC20__factory, ERC20 } from '../../../typechain' import { getPermitSignature, PermitSingle } from '../shared/protocolHelpers/permit2' import { CurrencyAmount, Percent, Token, TradeType } from '@airdao/astra-sdk-core' import snapshotGasCost from '@uniswap/snapshot-gas-cost' import { IRoute, Trade } from '@airdao/astra-router-sdk' const { ethers } = hre describe('Astra UX Tests gas:', () => { let alice: SignerWithAddress let bob: SignerWithAddress let router: UniversalRouter let permit2: Permit2 let usdcContract: ERC20 let planner: RoutePlanner let SIMPLE_SWAP: Trade<Token, Token, TradeType.EXACT_INPUT> let COMPLEX_SWAP: Trade<Token, Token, TradeType.EXACT_INPUT> let MAX_PERMIT: PermitSingle let SIMPLE_SWAP_PERMIT: PermitSingle let COMPLEX_SWAP_PERMIT: PermitSingle beforeEach(async () => { await resetFork() 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] usdcContract = ERC20__factory.connect(USDC.address, alice) permit2 = (await deployPermit2()).connect(bob) as Permit2 router = (await deployUniversalRouter(permit2)).connect(bob) as UniversalRouter planner = new RoutePlanner() // Alice gives bob some tokens await usdcContract.connect(alice).transfer(bob.address, expandTo6DecimalsBN(10000000)) /* Simple Swap = 1000 USDC —CL→ AMB —CL→ BOND Complex Swap = 3000 USDC —CL—> AMB — CL—> BOND 4000 USDC —CL—> KOS —CL—>BOND 3000 USDC —Classic—> BOND */ const createPool = (tokenA: Token, tokenB: Token, fee: FeeAmount) => { return new Pool(tokenA, tokenB, fee, sqrtRatioX96, 1_000_000, TickMath.getTickAtSqrtRatio(sqrtRatioX96)) } const sqrtRatioX96 = encodeSqrtRatioX96(1, 1) const USDC_SAMB = createPool(USDC, SAMB, FeeAmount.HIGH) const BOND_SAMB = createPool(BOND, SAMB, FeeAmount.HIGH) const USDC_KOS = createPool(USDC, KOS, FeeAmount.LOWEST) const KOS_BOND = createPool(BOND, KOS, FeeAmount.LOWEST) const USDC_BOND_Classic = new Pair( CurrencyAmount.fromRawAmount(USDC, 10000000), CurrencyAmount.fromRawAmount(BOND, 10000000) ) const simpleSwapAmountInUSDC = CurrencyAmount.fromRawAmount(USDC, expandTo6DecimalsBN(1000).toString()) const complexSwapAmountInSplit1 = CurrencyAmount.fromRawAmount(USDC, expandTo6DecimalsBN(3000).toString()) const complexSwapAmountInSplit2 = CurrencyAmount.fromRawAmount(USDC, expandTo6DecimalsBN(4000).toString()) const complexSwapAmountInSplit3 = CurrencyAmount.fromRawAmount(USDC, expandTo6DecimalsBN(3000).toString()) SIMPLE_SWAP = new Trade({ clRoutes: [ { routecl: new CLRouteSDK([USDC_SAMB, BOND_SAMB], USDC, BOND), inputAmount: simpleSwapAmountInUSDC, outputAmount: CurrencyAmount.fromRawAmount(BOND, expandTo18Decimals(1000)), }, ], classicRoutes: [], tradeType: TradeType.EXACT_INPUT, }) COMPLEX_SWAP = new Trade({ clRoutes: [ { routecl: new CLRouteSDK([USDC_SAMB, BOND_SAMB], USDC, BOND), inputAmount: complexSwapAmountInSplit1, outputAmount: CurrencyAmount.fromRawAmount(BOND, expandTo18Decimals(3000)), }, { routecl: new CLRouteSDK([USDC_KOS, KOS_BOND], USDC, BOND), inputAmount: complexSwapAmountInSplit2, outputAmount: CurrencyAmount.fromRawAmount(BOND, expandTo18Decimals(4000)), }, ], classicRoutes: [ { routeclassic: new ClassicRouteSDK([USDC_BOND_Classic], USDC, BOND), inputAmount: complexSwapAmountInSplit3, outputAmount: CurrencyAmount.fromRawAmount(BOND, expandTo18Decimals(3000)), }, ], tradeType: TradeType.EXACT_INPUT, }) MAX_PERMIT = { details: { token: COMPLEX_SWAP.inputAmount.currency.address, amount: BigNumber.from(MAX_UINT160), expiration: DEADLINE, // not the end of time, cheaper gas-wise nonce: 0, // this is his first trade }, spender: router.address, sigDeadline: DEADLINE, } SIMPLE_SWAP_PERMIT = { details: { token: SIMPLE_SWAP.inputAmount.currency.address, amount: BigNumber.from(SIMPLE_SWAP.inputAmount.quotient.toString()), expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, spender: router.address, sigDeadline: DEADLINE, } COMPLEX_SWAP_PERMIT = { details: { token: COMPLEX_SWAP.inputAmount.currency.address, amount: BigNumber.from(COMPLEX_SWAP.inputAmount.quotient.toString()), expiration: 0, // expiration of 0 is block.timestamp nonce: 0, // this is his first trade }, spender: router.address, sigDeadline: DEADLINE, } }) async function executeTradeUniversalRouter( planner: RoutePlanner, trade: Trade<Token, Token, TradeType.EXACT_INPUT>, overrideRouter?: UniversalRouter ): Promise<BigNumber> { for (let i = 0; i < trade.swaps.length; i++) { let swap = trade.swaps[i] let route = trade.routes[i] let amountIn = BigNumber.from(swap.inputAmount.quotient.toString()) if (swap.route.protocol == 'Classic') { let pathAddresses = routeToAddresses(route) planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [ bob.address, amountIn, 0, pathAddresses, SOURCE_MSG_SENDER, ]) } else if (swap.route.protocol == 'CL') { let path = encodePathExactInput(route) planner.addCommand(CommandType.CL_SWAP_EXACT_IN, [bob.address, amountIn, 0, path, SOURCE_MSG_SENDER]) } else { console.log('invalid protocol') } } const { commands, inputs } = planner const tx = await (overrideRouter ?? router)['execute(bytes,bytes[])'](commands, inputs) const gasUsed = (await tx.wait()).gasUsed return gasUsed } describe('Approvals', async () => { it('Cost for infinite approval of permit2/swaprouter02 contract', async () => { // Bob max-approves the permit2 contract to access his BOND and SAMB await snapshotGasCost(await usdcContract.approve(permit2.address, MAX_UINT)) }) }) describe('Comparisons', async () => { let approvePermit2Gas: BigNumber let approveSwapRouter02Gas: BigNumber beforeEach(async () => { // bob has already given his infinite approval of USDC to permit2 const permitApprovalTx = await usdcContract.connect(bob).approve(permit2.address, MAX_UINT) approvePermit2Gas = (await permitApprovalTx.wait()).gasUsed const swapRouter02ApprovalTx = (await approveSwapRouter02(bob, USDC))! approveSwapRouter02Gas = swapRouter02ApprovalTx.gasUsed }) describe('One Time Swapper - Simple Swap', async () => { it('SwapRouter02', async () => { const { calldata } = SwapRouter.swapCallParameters(SIMPLE_SWAP, { slippageTolerance: new Percent(10, 100), recipient: bob.address, deadlineOrPreviousBlockhash: DEADLINE, }) const swapTx = await (await executeSwapRouter02Swap({ value: '0', calldata }, bob)).wait() const swapGas = swapTx.gasUsed await snapshotGasCost(approveSwapRouter02Gas.add(swapGas)) }) it('Permit2 Sign Per Swap', async () => { const sig = await getPermitSignature(SIMPLE_SWAP_PERMIT, bob, permit2) planner.addCommand(CommandType.PERMIT2_PERMIT, [SIMPLE_SWAP_PERMIT, sig]) const gasUsed = await executeTradeUniversalRouter(planner, SIMPLE_SWAP) await snapshotGasCost(approvePermit2Gas.add(gasUsed)) }) it('Permit2 Max Approval Swap', async () => { const sig = await getPermitSignature(MAX_PERMIT, bob, permit2) planner.addCommand(CommandType.PERMIT2_PERMIT, [MAX_PERMIT, sig]) const gasUsed = await executeTradeUniversalRouter(planner, SIMPLE_SWAP) await snapshotGasCost(approvePermit2Gas.add(gasUsed)) }) }) describe('One Time Swapper - Complex Swap', async () => { it('SwapRouter02', async () => { const { calldata } = SwapRouter.swapCallParameters(COMPLEX_SWAP, { slippageTolerance: new Percent(50, 100), recipient: bob.address, deadlineOrPreviousBlockhash: DEADLINE, }) const swapTx = await (await executeSwapRouter02Swap({ value: '0', calldata }, bob)).wait() const swapGas = swapTx.gasUsed await snapshotGasCost(approveSwapRouter02Gas.add(swapGas)) }) it('Permit2 Sign Per Swap', async () => { // sign the permit for this swap const sig = await getPermitSignature(COMPLEX_SWAP_PERMIT, bob, permit2) planner.addCommand(CommandType.PERMIT2_PERMIT, [COMPLEX_SWAP_PERMIT, sig]) const gasUsed = await executeTradeUniversalRouter(planner, COMPLEX_SWAP) await snapshotGasCost(approvePermit2Gas.add(gasUsed)) }) it('Permit2 Max Approval Swap', async () => { // send approval for the total input amount const sig = await getPermitSignature(MAX_PERMIT, bob, permit2) planner.addCommand(CommandType.PERMIT2_PERMIT, [MAX_PERMIT, sig]) const gasUsed = await executeTradeUniversalRouter(planner, COMPLEX_SWAP) await snapshotGasCost(approvePermit2Gas.add(gasUsed)) }) }) describe('Casual Swapper - 3 swaps', async () => { it('SwapRouter02', async () => { const { calldata: callDataComplex } = SwapRouter.swapCallParameters(COMPLEX_SWAP, { slippageTolerance: new Percent(50, 100), recipient: bob.address, deadlineOrPreviousBlockhash: DEADLINE, }) const { calldata: callDataSimple } = SwapRouter.swapCallParameters(SIMPLE_SWAP, { slippageTolerance: new Percent(50, 100), recipient: bob.address, deadlineOrPreviousBlockhash: DEADLINE, }) let totalGas = approveSwapRouter02Gas // Swap 1 (complex) const tx1 = await executeSwapRouter02Swap({ value: '0', calldata: callDataComplex }, bob) totalGas = totalGas.add((await tx1.wait()).gasUsed) // Swap 2 (complex) const tx2 = await executeSwapRouter02Swap({ value: '0', calldata: callDataComplex }, bob) totalGas = totalGas.add((await tx2.wait()).gasUsed) // Swap 3 (simple) const tx3 = await executeSwapRouter02Swap({ value: '0', calldata: callDataSimple }, bob) totalGas = totalGas.add((await tx3.wait()).gasUsed) await snapshotGasCost(totalGas) }) it('Permit2 Sign Per Swap', async () => { let totalGas = approvePermit2Gas // Swap 1: complex let sig = await getPermitSignature(COMPLEX_SWAP_PERMIT, bob, permit2) planner.addCommand(CommandType.PERMIT2_PERMIT, [COMPLEX_SWAP_PERMIT, sig]) let gasUsed = await executeTradeUniversalRouter(planner, COMPLEX_SWAP) totalGas = totalGas.add(gasUsed) planner = new RoutePlanner() // Swap 2: complex sig = await getPermitSignature(COMPLEX_SWAP_PERMIT, bob, permit2) planner.addCommand(CommandType.PERMIT2_PERMIT, [COMPLEX_SWAP_PERMIT, sig]) gasUsed = await executeTradeUniversalRouter(planner, COMPLEX_SWAP) totalGas = totalGas.add(gasUsed) planner = new RoutePlanner() // Swap 3: simple sig = await getPermitSignature(SIMPLE_SWAP_PERMIT, bob, permit2) planner.addCommand(CommandType.PERMIT2_PERMIT, [SIMPLE_SWAP_PERMIT, sig]) gasUsed = await executeTradeUniversalRouter(planner, SIMPLE_SWAP) totalGas = totalGas.add(gasUsed) await snapshotGasCost(totalGas) }) it('Permit2 Max Approval Swap', async () => { let totalGas = approvePermit2Gas // Swap 1: complex, but give max approval no more approvals needed let sig = await getPermitSignature(MAX_PERMIT, bob, permit2) planner.addCommand(CommandType.PERMIT2_PERMIT, [MAX_PERMIT, sig]) let gasUsed = await executeTradeUniversalRouter(planner, COMPLEX_SWAP) totalGas = totalGas.add(gasUsed) planner = new RoutePlanner() // Swap 2: complex gasUsed = await executeTradeUniversalRouter(planner, COMPLEX_SWAP) totalGas = totalGas.add(gasUsed) planner = new RoutePlanner() // Swap 3: simple gasUsed = await executeTradeUniversalRouter(planner, SIMPLE_SWAP) totalGas = totalGas.add(gasUsed) await snapshotGasCost(totalGas) }) }) describe('Frequent Swapper - 10 swaps', async () => { it('SwapRouter02', async () => { const { calldata: callDataComplex } = SwapRouter.swapCallParameters(COMPLEX_SWAP, { slippageTolerance: new Percent(50, 100), recipient: bob.address, deadlineOrPreviousBlockhash: DEADLINE, }) const { calldata: callDataSimple } = SwapRouter.swapCallParameters(SIMPLE_SWAP, { slippageTolerance: new Percent(50, 100), recipient: bob.address, deadlineOrPreviousBlockhash: DEADLINE, }) let totalGas = approveSwapRouter02Gas // Do 5 complex swaps for (let i = 0; i < 5; i++) { const tx = await executeSwapRouter02Swap({ value: '0', calldata: callDataComplex }, bob) totalGas = totalGas.add((await tx.wait()).gasUsed) } // Do 5 simple swaps for (let i = 0; i < 5; i++) { const tx = await executeSwapRouter02Swap({ value: '0', calldata: callDataSimple }, bob) totalGas = totalGas.add((await tx.wait()).gasUsed) } await snapshotGasCost(totalGas) }) it('Permit2 Sign Per Swap', async () => { let totalGas = approvePermit2Gas let sig: string let gasUsed: BigNumber // Do 5 complex swaps for (let i = 0; i < 5; i++) { sig = await getPermitSignature(COMPLEX_SWAP_PERMIT, bob, permit2) planner.addCommand(CommandType.PERMIT2_PERMIT, [COMPLEX_SWAP_PERMIT, sig]) gasUsed = await executeTradeUniversalRouter(planner, COMPLEX_SWAP) totalGas = totalGas.add(gasUsed) planner = new RoutePlanner() } // Do 5 simple swaps for (let i = 0; i < 5; i++) { sig = await getPermitSignature(SIMPLE_SWAP_PERMIT, bob, permit2) planner.addCommand(CommandType.PERMIT2_PERMIT, [SIMPLE_SWAP_PERMIT, sig]) gasUsed = await executeTradeUniversalRouter(planner, SIMPLE_SWAP) totalGas = totalGas.add(gasUsed) planner = new RoutePlanner() } await snapshotGasCost(totalGas) }) it('Permit2 Max Approval Swap', async () => { let totalGas = approvePermit2Gas let gasUsed: BigNumber // The first trade contains a max permit, all others contain no permit let sig = await getPermitSignature(MAX_PERMIT, bob, permit2) planner.addCommand(CommandType.PERMIT2_PERMIT, [MAX_PERMIT, sig]) // Do 5 complex swaps for (let i = 0; i < 5; i++) { gasUsed = await executeTradeUniversalRouter(planner, COMPLEX_SWAP) totalGas = totalGas.add(gasUsed) planner = new RoutePlanner() } // Do 5 simple swaps for (let i = 0; i < 5; i++) { gasUsed = await executeTradeUniversalRouter(planner, SIMPLE_SWAP) totalGas = totalGas.add(gasUsed) planner = new RoutePlanner() } await snapshotGasCost(totalGas) }) }) describe('Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions', async () => { it('SwapRouter02', async () => { const { calldata: callDataComplex } = SwapRouter.swapCallParameters(COMPLEX_SWAP, { slippageTolerance: new Percent(50, 100), recipient: bob.address, deadlineOrPreviousBlockhash: DEADLINE, }) const { calldata: callDataSimple } = SwapRouter.swapCallParameters(SIMPLE_SWAP, { slippageTolerance: new Percent(50, 100), recipient: bob.address, deadlineOrPreviousBlockhash: DEADLINE, }) let totalGas = approveSwapRouter02Gas // Do 5 complex swaps on protocol 1 for (let i = 0; i < 5; i++) { const tx = await executeSwapRouter02Swap({ value: '0', calldata: callDataComplex }, bob) totalGas = totalGas.add((await tx.wait()).gasUsed) } // Launch SwapRouter03 const router2 = (await deployUniversalRouter(permit2)).connect(bob) as UniversalRouter const router2ApprovalTx = (await approveSwapRouter02(bob, USDC, router2.address))! totalGas = totalGas.add(router2ApprovalTx.gasUsed) // Do 5 simple swaps on SwapRouter03 for (let i = 0; i < 5; i++) { const tx = await executeSwapRouter02Swap({ value: '0', calldata: callDataSimple }, bob) totalGas = totalGas.add((await tx.wait()).gasUsed) } // Launch SwapRouter04 const router3 = (await deployUniversalRouter(permit2)).connect(bob) as UniversalRouter const router3ApprovalTx = (await approveSwapRouter02(bob, USDC, router3.address))! totalGas = totalGas.add(router3ApprovalTx.gasUsed) // Do 5 simple swaps on SwapRouter04 for (let i = 0; i < 5; i++) { const tx = await executeSwapRouter02Swap({ value: '0', calldata: callDataSimple }, bob) totalGas = totalGas.add((await tx.wait()).gasUsed) } await snapshotGasCost(totalGas) }) it('Permit2 Sign Per Swap', async () => { let totalGas = approvePermit2Gas let sig: string let gasUsed: BigNumber // Do 5 complex swaps for (let i = 0; i < 5; i++) { sig = await getPermitSignature(COMPLEX_SWAP_PERMIT, bob, permit2) planner.addCommand(CommandType.PERMIT2_PERMIT, [COMPLEX_SWAP_PERMIT, sig]) gasUsed = await executeTradeUniversalRouter(planner, COMPLEX_SWAP) totalGas = totalGas.add(gasUsed) planner = new RoutePlanner() } // Launch Universal Router classic const router2 = (await deployUniversalRouter(permit2)).connect(bob) as UniversalRouter // Do 5 simple swaps for (let i = 0; i < 5; i++) { SIMPLE_SWAP_PERMIT.spender = router2.address sig = await getPermitSignature(SIMPLE_SWAP_PERMIT, bob, permit2) planner.addCommand(CommandType.PERMIT2_PERMIT, [SIMPLE_SWAP_PERMIT, sig]) gasUsed = await executeTradeUniversalRouter(planner, SIMPLE_SWAP, router2) totalGas = totalGas.add(gasUsed) planner = new RoutePlanner() } // Launch Universal Router cl const router3 = (await deployUniversalRouter(permit2)).connect(bob) as UniversalRouter // Do 5 simple swaps for (let i = 0; i < 5; i++) { SIMPLE_SWAP_PERMIT.spender = router3.address sig = await getPermitSignature(SIMPLE_SWAP_PERMIT, bob, permit2) planner.addCommand(CommandType.PERMIT2_PERMIT, [SIMPLE_SWAP_PERMIT, sig]) gasUsed = await executeTradeUniversalRouter(planner, SIMPLE_SWAP, router3) totalGas = totalGas.add(gasUsed) planner = new RoutePlanner() } await snapshotGasCost(totalGas) }) it('Permit2 Max Approval Swap', async () => { let totalGas = approvePermit2Gas let gasUsed: BigNumber // The first trade contains a max permit, all others contain no permit let sig = await getPermitSignature(MAX_PERMIT, bob, permit2) planner.addCommand(CommandType.PERMIT2_PERMIT, [MAX_PERMIT, sig]) // Do 5 complex swaps for (let i = 0; i < 5; i++) { gasUsed = await executeTradeUniversalRouter(planner, COMPLEX_SWAP) totalGas = totalGas.add(gasUsed) planner = new RoutePlanner() } // Launch Universal Router classic const router2 = (await deployUniversalRouter(permit2)).connect(bob) as UniversalRouter MAX_PERMIT.spender = router2.address let calldata2 = await getPermitSignature(MAX_PERMIT, bob, permit2) planner.addCommand(CommandType.PERMIT2_PERMIT, [MAX_PERMIT, calldata2]) // Do 5 simple swaps for (let i = 0; i < 5; i++) { gasUsed = await executeTradeUniversalRouter(planner, SIMPLE_SWAP, router2) totalGas = totalGas.add(gasUsed) planner = new RoutePlanner() } // Launch Universal Router cl const router3 = (await deployUniversalRouter(permit2)).connect(bob) as UniversalRouter MAX_PERMIT.spender = router3.address let calldata3 = await getPermitSignature(MAX_PERMIT, bob, permit2) planner.addCommand(CommandType.PERMIT2_PERMIT, [MAX_PERMIT, calldata3]) // Do 5 simple swaps for (let i = 0; i < 5; i++) { gasUsed = await executeTradeUniversalRouter(planner, SIMPLE_SWAP, router3) totalGas = totalGas.add(gasUsed) planner = new RoutePlanner() } await snapshotGasCost(totalGas) }) }) }) function encodePathExactInput(route: IRoute<Token, Token, Pool | Pair>) { const addresses = routeToAddresses(route) const feeTiers = new Array(addresses.length - 1) for (let i = 0; i < feeTiers.length; i++) { feeTiers[i] = addresses[i] == SAMB.address || addresses[i + 1] == SAMB.address ? FeeAmount.HIGH : FeeAmount.LOWEST } return encodePath(addresses, feeTiers) } function routeToAddresses(route: IRoute<Token, Token, Pool | Pair>) { const tokens = route.path return tokens.map((t) => (t.isNative ? ethers.constants.AddressZero : t.address)) } })