import { defaultAbiCoder } from '@ethersproject/abi' import { Fixture } from 'ethereum-waffle' import { constants, Contract, ContractTransaction, Wallet } from 'ethers' import { solidityPack } from 'ethers/lib/utils' import { ethers, waffle } from 'hardhat' import { MockTimeSwapRouter02, TestERC20 } from '../typechain' import completeFixture from './shared/completeFixture' import { ADDRESS_THIS, FeeAmount, TICK_SPACINGS } from './shared/constants' import { encodePriceSqrt } from './shared/encodePriceSqrt' import { expect } from './shared/expect' import { encodePath } from './shared/path' import { getMaxTick, getMinTick } from './shared/ticks' enum ApprovalType { NOT_REQUIRED, MAX, MAX_MINUS_ONE, ZERO_THEN_MAX, ZERO_THEN_MAX_MINUS_ONE, } describe('ApproveAndCall', function () { this.timeout(40000) let wallet: Wallet let trader: Wallet const swapRouterFixture: Fixture<{ factory: Contract router: MockTimeSwapRouter02 nft: Contract tokens: [TestERC20, TestERC20, TestERC20] }> = async (wallets, provider) => { const { factory, router, tokens, nft } = await completeFixture(wallets, provider) // approve & fund wallets for (const token of tokens) { await token.approve(nft.address, constants.MaxUint256) } return { factory, router, tokens, nft, } } let factory: Contract let router: MockTimeSwapRouter02 let nft: Contract let tokens: [TestERC20, TestERC20, TestERC20] let loadFixture: ReturnType function encodeSweepToken(token: string, amount: number) { const functionSignature = 'sweepToken(address,uint256)' return solidityPack( ['bytes4', 'bytes'], [router.interface.getSighash(functionSignature), defaultAbiCoder.encode(['address', 'uint256'], [token, amount])] ) } before('create fixture loader', async () => { ;[wallet, trader] = await (ethers as any).getSigners() loadFixture = waffle.createFixtureLoader([wallet, trader]) }) beforeEach('load fixture', async () => { ;({ factory, router, tokens, nft } = await loadFixture(swapRouterFixture)) }) describe('swap and add', () => { async function createPool(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('approvals', () => { it('#approveMax', async () => { let approvalType = await router.callStatic.getApprovalType(tokens[0].address, 123) expect(approvalType).to.be.eq(ApprovalType.MAX) await router.approveMax(tokens[0].address) approvalType = await router.callStatic.getApprovalType(tokens[0].address, 123) expect(approvalType).to.be.eq(ApprovalType.NOT_REQUIRED) }) it('#approveMax', async () => { await router.approveMax(tokens[0].address) }) it('#approveMaxMinusOne', async () => { await router.approveMaxMinusOne(tokens[0].address) }) describe('#approveZeroThenMax', async () => { it('from 0', async () => { await router.approveZeroThenMax(tokens[0].address) }) it('from max', async () => { await router.approveMax(tokens[0].address) await router.approveZeroThenMax(tokens[0].address) }) }) describe('#approveZeroThenMax', async () => { it('from 0', async () => { await router.approveZeroThenMaxMinusOne(tokens[0].address) }) it('from max', async () => { await router.approveMax(tokens[0].address) await router.approveZeroThenMaxMinusOne(tokens[0].address) }) }) }) it('#mint and #increaseLiquidity', async () => { await createPool(tokens[0].address, tokens[1].address) const pool = await factory.getPool(tokens[0].address, tokens[1].address, FeeAmount.MEDIUM) // approve in advance await router.approveMax(tokens[0].address) await router.approveMax(tokens[1].address) // send dummy amount of tokens to the pair in advance const amount = 1000 await tokens[0].transfer(router.address, amount) await tokens[1].transfer(router.address, amount) expect((await tokens[0].balanceOf(router.address)).toNumber()).to.be.eq(amount) expect((await tokens[1].balanceOf(router.address)).toNumber()).to.be.eq(amount) let poolBalance0Before = await tokens[0].balanceOf(pool) let poolBalance1Before = await tokens[1].balanceOf(pool) // perform the mint await router.mint({ token0: tokens[0].address, token1: tokens[1].address, fee: FeeAmount.MEDIUM, tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]), tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]), recipient: trader.address, amount0Min: 0, amount1Min: 0, }) expect((await tokens[0].balanceOf(router.address)).toNumber()).to.be.eq(0) expect((await tokens[1].balanceOf(router.address)).toNumber()).to.be.eq(0) expect((await tokens[0].balanceOf(pool)).toNumber()).to.be.eq(poolBalance0Before.toNumber() + amount) expect((await tokens[1].balanceOf(pool)).toNumber()).to.be.eq(poolBalance1Before.toNumber() + amount) expect((await nft.balanceOf(trader.address)).toNumber()).to.be.eq(1) // send more tokens await tokens[0].transfer(router.address, amount) await tokens[1].transfer(router.address, amount) // perform the increaseLiquidity await router.increaseLiquidity({ token0: tokens[0].address, token1: tokens[1].address, tokenId: 2, amount0Min: 0, amount1Min: 0, }) expect((await tokens[0].balanceOf(router.address)).toNumber()).to.be.eq(0) expect((await tokens[1].balanceOf(router.address)).toNumber()).to.be.eq(0) expect((await tokens[0].balanceOf(pool)).toNumber()).to.be.eq(poolBalance0Before.toNumber() + amount * 2) expect((await tokens[1].balanceOf(pool)).toNumber()).to.be.eq(poolBalance1Before.toNumber() + amount * 2) expect((await nft.balanceOf(trader.address)).toNumber()).to.be.eq(1) }) describe('single-asset add', () => { beforeEach('create 0-1 pool', async () => { await createPool(tokens[0].address, tokens[1].address) }) async function singleAssetAddExactInput( tokenIn: string, tokenOut: string, amountIn: number, amountOutMinimum: number ): Promise { // encode the exact input swap const params = { path: encodePath([tokenIn, tokenOut], [FeeAmount.MEDIUM]), recipient: ADDRESS_THIS, // have to send to the router, as it will be adding liquidity for the caller amountIn, amountOutMinimum, } // ensure that the swap fails if the limit is any tighter const amountOut = await router.connect(trader).callStatic.exactInput(params) expect(amountOut.toNumber()).to.be.eq(amountOutMinimum) const data = [router.interface.encodeFunctionData('exactInput', [params])] // encode the pull (we take the same as the amountOutMinimum, assuming a 50/50 range) data.push(router.interface.encodeFunctionData('pull', [tokenIn, amountOutMinimum])) // encode the approves data.push(router.interface.encodeFunctionData('approveMax', [tokenIn])) data.push(router.interface.encodeFunctionData('approveMax', [tokenOut])) // encode the add liquidity const [token0, token1] = tokenIn.toLowerCase() < tokenOut.toLowerCase() ? [tokenIn, tokenOut] : [tokenOut, tokenIn] const liquidityParams = { token0, token1, fee: FeeAmount.MEDIUM, tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]), tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]), recipient: trader.address, amount0Desired: amountOutMinimum, amount1Desired: amountOutMinimum, amount0Min: 0, amount1Min: 0, deadline: 2 ** 32, } data.push( router.interface.encodeFunctionData('callPositionManager', [ nft.interface.encodeFunctionData('mint', [liquidityParams]), ]) ) // encode the sweeps data.push(encodeSweepToken(tokenIn, 0)) data.push(encodeSweepToken(tokenOut, 0)) return router.connect(trader)['multicall(bytes[])'](data) } it('0 -> 1', async () => { const amountIn = 1000 const amountOutMinimum = 996 // prep for the swap + add by sending tokens await tokens[0].transfer(trader.address, amountIn + amountOutMinimum) await tokens[0].connect(trader).approve(router.address, amountIn + amountOutMinimum) const traderToken0BalanceBefore = await tokens[0].balanceOf(trader.address) const traderToken1BalanceBefore = await tokens[1].balanceOf(trader.address) expect(traderToken0BalanceBefore.toNumber()).to.be.eq(amountIn + amountOutMinimum) expect(traderToken1BalanceBefore.toNumber()).to.be.eq(0) const traderNFTBalanceBefore = await nft.balanceOf(trader.address) expect(traderNFTBalanceBefore.toNumber()).to.be.eq(0) await singleAssetAddExactInput(tokens[0].address, tokens[1].address, amountIn, amountOutMinimum) const traderToken0BalanceAfter = await tokens[0].balanceOf(trader.address) const traderToken1BalanceAfter = await tokens[1].balanceOf(trader.address) expect(traderToken0BalanceAfter.toNumber()).to.be.eq(0) expect(traderToken1BalanceAfter.toNumber()).to.be.eq(1) // dust const traderNFTBalanceAfter = await nft.balanceOf(trader.address) expect(traderNFTBalanceAfter.toNumber()).to.be.eq(1) }) }) describe('any-asset add', () => { beforeEach('create 0-1, 0-2, and 1-2 pools pools', async () => { await createPool(tokens[0].address, tokens[1].address) await createPool(tokens[0].address, tokens[2].address) await createPool(tokens[1].address, tokens[2].address) }) async function anyAssetAddExactInput( tokenStart: string, tokenA: string, tokenB: string, amountIn: number, amountOutMinimum: number ): Promise { // encode the exact input swaps let params = { path: encodePath([tokenStart, tokenA], [FeeAmount.MEDIUM]), recipient: ADDRESS_THIS, // have to send to the router, as it will be adding liquidity for the caller amountIn, amountOutMinimum, } // ensure that the swap fails if the limit is any tighter let amountOut = await router.connect(trader).callStatic.exactInput(params) expect(amountOut.toNumber()).to.be.eq(amountOutMinimum) let data = [router.interface.encodeFunctionData('exactInput', [params])] // encode the exact input swaps params = { path: encodePath([tokenStart, tokenB], [FeeAmount.MEDIUM]), recipient: ADDRESS_THIS, // have to send to the router, as it will be adding liquidity for the caller amountIn, amountOutMinimum, } // ensure that the swap fails if the limit is any tighter amountOut = await router.connect(trader).callStatic.exactInput(params) expect(amountOut.toNumber()).to.be.eq(amountOutMinimum) data.push(router.interface.encodeFunctionData('exactInput', [params])) // encode the approves data.push(router.interface.encodeFunctionData('approveMax', [tokenA])) data.push(router.interface.encodeFunctionData('approveMax', [tokenB])) // encode the add liquidity const [token0, token1] = tokenA.toLowerCase() < tokenB.toLowerCase() ? [tokenA, tokenB] : [tokenB, tokenA] const liquidityParams = { token0, token1, fee: FeeAmount.MEDIUM, tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]), tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]), recipient: trader.address, amount0Desired: amountOutMinimum, amount1Desired: amountOutMinimum, amount0Min: 0, amount1Min: 0, deadline: 2 ** 32, } data.push( router.interface.encodeFunctionData('callPositionManager', [ nft.interface.encodeFunctionData('mint', [liquidityParams]), ]) ) // encode the sweeps data.push(encodeSweepToken(tokenA, 0)) data.push(encodeSweepToken(tokenB, 0)) return router.connect(trader)['multicall(bytes[])'](data) } it('0 -> 1 and 0 -> 2', async () => { const amountIn = 1000 const amountOutMinimum = 996 // prep for the swap + add by sending tokens await tokens[0].transfer(trader.address, amountIn * 2) await tokens[0].connect(trader).approve(router.address, amountIn * 2) const traderToken0BalanceBefore = await tokens[0].balanceOf(trader.address) const traderToken1BalanceBefore = await tokens[1].balanceOf(trader.address) const traderToken2BalanceBefore = await tokens[2].balanceOf(trader.address) expect(traderToken0BalanceBefore.toNumber()).to.be.eq(amountIn * 2) expect(traderToken1BalanceBefore.toNumber()).to.be.eq(0) expect(traderToken2BalanceBefore.toNumber()).to.be.eq(0) const traderNFTBalanceBefore = await nft.balanceOf(trader.address) expect(traderNFTBalanceBefore.toNumber()).to.be.eq(0) await anyAssetAddExactInput(tokens[0].address, tokens[1].address, tokens[2].address, amountIn, amountOutMinimum) const traderToken0BalanceAfter = await tokens[0].balanceOf(trader.address) const traderToken1BalanceAfter = await tokens[1].balanceOf(trader.address) const traderToken2BalanceAfter = await tokens[2].balanceOf(trader.address) expect(traderToken0BalanceAfter.toNumber()).to.be.eq(0) expect(traderToken1BalanceAfter.toNumber()).to.be.eq(0) expect(traderToken2BalanceAfter.toNumber()).to.be.eq(0) const traderNFTBalanceAfter = await nft.balanceOf(trader.address) expect(traderNFTBalanceAfter.toNumber()).to.be.eq(1) }) }) }) })