@airdao/astra-universal-router
Version:
Smart contracts for Universal Router
612 lines (496 loc) • 23.1 kB
text/typescript
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))
}
})