UNPKG

@airdao/swap-router-contracts

Version:

Smart contracts for swapping on Astra Classic and CL

1,343 lines (1,114 loc) 59.2 kB
import { defaultAbiCoder } from '@ethersproject/abi' import { abi as PAIR_V2_ABI } from '@airdao/astra-contracts/artifacts/contracts/core/AstraPair.sol/AstraPair.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 { IAstraPair, ISAMB, 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, CLASSIC_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<{ samb: ISAMB factory: Contract factoryClassic: Contract router: MockTimeSwapRouter02 quoter: MixedRouteQuoterV1 nft: Contract tokens: [TestERC20, TestERC20, TestERC20] }> = async (wallets, provider) => { const { samb, factory, factoryClassic, 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, factoryClassic.address, samb.address)) as MixedRouteQuoterV1 return { samb, factory, factoryClassic, router, quoter, tokens, nft, } } let factory: Contract let factoryClassic: Contract let samb: ISAMB let router: MockTimeSwapRouter02 let quoter: MixedRouteQuoterV1 let nft: Contract let tokens: [TestERC20, TestERC20, TestERC20] let getBalances: ( who: string ) => Promise<{ samb: BigNumber token0: BigNumber token1: BigNumber token2: BigNumber }> let loadFixture: ReturnType<typeof waffle.createFixtureLoader> function encodeUnwrapSAMB(amount: number) { const functionSignature = 'unwrapSAMB(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, samb, factory, factoryClassic, tokens, nft } = await loadFixture(swapRouterFixture)) getBalances = async (who: string) => { const balances = await Promise.all([ samb.balanceOf(who), tokens[0].balanceOf(who), tokens[1].balanceOf(who), tokens[2].balanceOf(who), ]) return { samb: 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 createPoolSAMB(tokenAddress: string) { await samb.deposit({ value: liquidity }) await samb.approve(nft.address, constants.MaxUint256) return createV3Pool(samb.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<ContractTransaction> { const inputIsSAMB = samb.address === tokens[0] const outputIsSAMB = tokens[tokens.length - 1] === samb.address const value = inputIsSAMB ? amountIn : 0 const params = { path: encodePath(tokens, new Array(tokens.length - 1).fill(FeeAmount.MEDIUM)), recipient: outputIsSAMB ? ADDRESS_THIS : MSG_SENDER, amountIn, amountOutMinimum, } const data = [router.interface.encodeFunctionData('exactInput', [params])] if (outputIsSAMB) { data.push(encodeUnwrapSAMB(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('AMB input', () => { describe('SAMB', () => { beforeEach(async () => { await createPoolSAMB(tokens[0].address) }) it('SAMB -> 0', async () => { const pool = await factory.getPool(samb.address, tokens[0].address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await expect(exactInput([samb.address, tokens[0].address])) .to.emit(samb, '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.samb).to.be.eq(poolBefore.samb.add(3)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1)) }) it('SAMB -> 0 -> 1', async () => { const traderBefore = await getBalances(trader.address) await expect(exactInput([samb.address, tokens[0].address, tokens[1].address], 5)) .to.emit(samb, 'Deposit') .withArgs(router.address, 5) const traderAfter = await getBalances(trader.address) expect(traderAfter.token1).to.be.eq(traderBefore.token1.add(1)) }) }) }) describe('AMB output', () => { describe('SAMB', () => { beforeEach(async () => { await createPoolSAMB(tokens[0].address) await createPoolSAMB(tokens[1].address) }) it('0 -> SAMB', async () => { const pool = await factory.getPool(tokens[0].address, samb.address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await expect(exactInput([tokens[0].address, samb.address])) .to.emit(samb, '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.samb).to.be.eq(poolBefore.samb.sub(1)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.add(3)) }) it('0 -> 1 -> SAMB', async () => { // get balances before const traderBefore = await getBalances(trader.address) await expect(exactInput([tokens[0].address, tokens[1].address, samb.address], 5)) .to.emit(samb, '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<ContractTransaction> { const inputIsSAMB = samb.address === tokenIn const outputIsSAMB = tokenOut === samb.address const value = inputIsSAMB ? amountIn : 0 const params = { tokenIn, tokenOut, fee: FeeAmount.MEDIUM, recipient: outputIsSAMB ? ADDRESS_THIS : MSG_SENDER, amountIn, amountOutMinimum, sqrtPriceLimitX96: sqrtPriceLimitX96 ?? 0, } const data = [router.interface.encodeFunctionData('exactInputSingle', [params])] if (outputIsSAMB) { data.push(encodeUnwrapSAMB(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('AMB input', () => { describe('SAMB', () => { beforeEach(async () => { await createPoolSAMB(tokens[0].address) }) it('SAMB -> 0', async () => { const pool = await factory.getPool(samb.address, tokens[0].address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await expect(exactInputSingle(samb.address, tokens[0].address)) .to.emit(samb, '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.samb).to.be.eq(poolBefore.samb.add(3)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1)) }) }) }) describe('AMB output', () => { describe('SAMB', () => { beforeEach(async () => { await createPoolSAMB(tokens[0].address) await createPoolSAMB(tokens[1].address) }) it('0 -> SAMB', async () => { const pool = await factory.getPool(tokens[0].address, samb.address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await expect(exactInputSingle(tokens[0].address, samb.address)) .to.emit(samb, '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.samb).to.be.eq(poolBefore.samb.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<ContractTransaction> { const inputIsSAMB = tokens[0] === samb.address const outputIsSAMB = tokens[tokens.length - 1] === samb.address const value = inputIsSAMB ? amountInMaximum : 0 const params = { path: encodePath(tokens.slice().reverse(), new Array(tokens.length - 1).fill(FeeAmount.MEDIUM)), recipient: outputIsSAMB ? ADDRESS_THIS : MSG_SENDER, amountOut, amountInMaximum, } const data = [router.interface.encodeFunctionData('exactOutput', [params])] if (inputIsSAMB) { data.push(router.interface.encodeFunctionData('refundAMB')) } if (outputIsSAMB) { data.push(encodeUnwrapSAMB(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('AMB input', () => { describe('SAMB', () => { beforeEach(async () => { await createPoolSAMB(tokens[0].address) }) it('SAMB -> 0', async () => { const pool = await factory.getPool(samb.address, tokens[0].address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await expect(exactOutput([samb.address, tokens[0].address])) .to.emit(samb, '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.samb).to.be.eq(poolBefore.samb.add(3)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1)) }) it('SAMB -> 0 -> 1', async () => { const traderBefore = await getBalances(trader.address) await expect(exactOutput([samb.address, tokens[0].address, tokens[1].address], 1, 5)) .to.emit(samb, 'Deposit') .withArgs(router.address, 5) const traderAfter = await getBalances(trader.address) expect(traderAfter.token1).to.be.eq(traderBefore.token1.add(1)) }) }) }) describe('AMB output', () => { describe('SAMB', () => { beforeEach(async () => { await createPoolSAMB(tokens[0].address) await createPoolSAMB(tokens[1].address) }) it('0 -> SAMB', async () => { const pool = await factory.getPool(tokens[0].address, samb.address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await expect(exactOutput([tokens[0].address, samb.address])) .to.emit(samb, '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.samb).to.be.eq(poolBefore.samb.sub(1)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.add(3)) }) it('0 -> 1 -> SAMB', async () => { // get balances before const traderBefore = await getBalances(trader.address) await expect(exactOutput([tokens[0].address, tokens[1].address, samb.address], 1, 5)) .to.emit(samb, '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<ContractTransaction> { const inputIsSAMB = tokenIn === samb.address const outputIsSAMB = tokenOut === samb.address const value = inputIsSAMB ? amountInMaximum : 0 const params = { tokenIn, tokenOut, fee: FeeAmount.MEDIUM, recipient: outputIsSAMB ? ADDRESS_THIS : MSG_SENDER, amountOut, amountInMaximum, sqrtPriceLimitX96: sqrtPriceLimitX96 ?? 0, } const data = [router.interface.encodeFunctionData('exactOutputSingle', [params])] if (inputIsSAMB) { data.push(router.interface.encodeFunctionData('refundAMB')) } if (outputIsSAMB) { data.push(encodeUnwrapSAMB(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('AMB input', () => { describe('SAMB', () => { beforeEach(async () => { await createPoolSAMB(tokens[0].address) }) it('SAMB -> 0', async () => { const pool = await factory.getPool(samb.address, tokens[0].address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await expect(exactOutputSingle(samb.address, tokens[0].address)) .to.emit(samb, '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.samb).to.be.eq(poolBefore.samb.add(3)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1)) }) }) }) describe('AMB output', () => { describe('SAMB', () => { beforeEach(async () => { await createPoolSAMB(tokens[0].address) await createPoolSAMB(tokens[1].address) }) it('0 -> SAMB', async () => { const pool = await factory.getPool(tokens[0].address, samb.address, FeeAmount.MEDIUM) // get balances before const poolBefore = await getBalances(pool) const traderBefore = await getBalances(trader.address) await expect(exactOutputSingle(tokens[0].address, samb.address)) .to.emit(samb, '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.samb).to.be.eq(poolBefore.samb.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('#unwrapSAMBWithFee', async () => { const startBalance = await waffle.provider.getBalance(feeRecipient) await createPoolSAMB(tokens[0].address) const amountOutMinimum = 100 const params = { path: encodePath([tokens[0].address, samb.address], [FeeAmount.MEDIUM]), recipient: ADDRESS_THIS, amountIn: 102, amountOutMinimum: 0, } const functionSignature = 'unwrapSAMBWithFee(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<IAstraPair> { await factoryClassic.createPair(tokenA.address, tokenB.address) const pairAddress = await factoryClassic.getPair(tokenA.address, tokenB.address) const pair = new ethers.Contract(pairAddress, PAIR_V2_ABI, wallet) as IAstraPair await tokenA.transfer(pair.address, liquidity) await tokenB.transfer(pair.address, liquidity) await pair.mint(wallet.address) return pair } describe('swaps - v2', () => { let pairs: IAstraPair[] let wethPairs: IAstraPair[] async function createPoolSAMB(token: TestERC20) { await samb.deposit({ value: liquidity }) return createV2Pool((samb 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<ContractTransaction> { const inputIsSAMB = samb.address === tokens[0] const outputIsSAMB = tokens[tokens.length - 1] === samb.address const value = inputIsSAMB ? amountIn : 0 const params: [number, number, string[], string] = [ amountIn, amountOutMinimum, tokens, outputIsSAMB ? ADDRESS_THIS : MSG_SENDER, ] const data = [router.interface.encodeFunctionData('swapExactTokensForTokens', params)] if (outputIsSAMB) { data.push(encodeUnwrapSAMB(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('AMB input', () => { describe('SAMB', () => { beforeEach(async () => { const pair = await createPoolSAMB(tokens[0]) wethPairs = [pair] }) it('SAMB -> 0', async () => { // get balances before const poolBefore = await getBalances(wethPairs[0].address) const traderBefore = await getBalances(trader.address) await expect(exactInput([samb.address, tokens[0].address])) .to.emit(samb, '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.samb).to.be.eq(poolBefore.samb.add(2)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1)) }) it('SAMB -> 0 -> 1', async () => { const traderBefore = await getBalances(trader.address) await expect(exactInput([samb.address, tokens[0].address, tokens[1].address], 3)) .to.emit(samb, 'Deposit') .withArgs(router.address, 3) const traderAfter = await getBalances(trader.address) expect(traderAfter.token1).to.be.eq(traderBefore.token1.add(1)) }) }) }) describe('AMB output', () => { describe('SAMB', () => { beforeEach(async () => { const pair0 = await createPoolSAMB(tokens[0]) const pair1 = await createPoolSAMB(tokens[1]) wethPairs = [pair0, pair1] }) it('0 -> SAMB', async () => { // get balances before const poolBefore = await getBalances(wethPairs[0].address) const traderBefore = await getBalances(trader.address) await expect(exactInput([tokens[0].address, samb.address])) .to.emit(samb, '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.samb).to.be.eq(poolBefore.samb.sub(1)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.add(2)) }) it('0 -> 1 -> SAMB', async () => { // get balances before const traderBefore = await getBalances(trader.address) await expect(exactInput([tokens[0].address, tokens[1].address, samb.address], 3)) .to.emit(samb, '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<ContractTransaction> { const inputIsSAMB = tokens[0] === samb.address const outputIsSAMB = tokens[tokens.length - 1] === samb.address const value = inputIsSAMB ? amountInMaximum : 0 const params: [number, number, string[], string] = [ amountOut, amountInMaximum, tokens, outputIsSAMB ? ADDRESS_THIS : MSG_SENDER, ] const data = [router.interface.encodeFunctionData('swapTokensForExactTokens', params)] if (inputIsSAMB) { data.push(router.interface.encodeFunctionData('refundAMB')) } if (outputIsSAMB) { data.push(encodeUnwrapSAMB(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('AMB input', () => { describe('SAMB', () => { beforeEach(async () => { const pair = await createPoolSAMB(tokens[0]) wethPairs = [pair] }) it('SAMB -> 0', async () => { // get balances before const poolBefore = await getBalances(wethPairs[0].address) const traderBefore = await getBalances(trader.address) await expect(exactOutput([samb.address, tokens[0].address])) .to.emit(samb, '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.samb).to.be.eq(poolBefore.samb.add(2)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1)) }) it('SAMB -> 0 -> 1', async () => { const traderBefore = await getBalances(trader.address) await expect(exactOutput([samb.address, tokens[0].address, tokens[1].address], 1, 3)) .to.emit(samb, 'Deposit') .withArgs(router.address, 3) const traderAfter = await getBalances(trader.address) expect(traderAfter.token1).to.be.eq(traderBefore.token1.add(1)) }) }) }) describe('AMB output', () => { describe('SAMB', () => { beforeEach(async () => { const pair0 = await createPoolSAMB(tokens[0]) const pair1 = await createPoolSAMB(tokens[1]) wethPairs = [pair0, pair1] }) it('0 -> SAMB', async () => { // get balances before const poolBefore = await getBalances(wethPairs[0].address) const traderBefore = await getBalances(trader.address) await expect(exactOutput([tokens[0].address, samb.address])) .to.emit(samb, '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.samb).to.be.eq(poolBefore.samb.sub(1)) expect(poolAfter.token0).to.be.eq(poolBefore.token0.add(2)) }) it('0 -> 1 -> SAMB', async () => { // get balances before const traderBefore = await getBalances(trader.address) await expect(exactOutput([tokens[0].address, tokens[1].address, samb.address], 1, 3)) .to.emit(samb, '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(