idle_moloch/lib/swap-router-contracts/test/ApproveAndCall.spec.ts

389 lines
15 KiB
TypeScript

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<typeof waffle.createFixtureLoader>
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<ContractTransaction> {
// 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<ContractTransaction> {
// 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)
})
})
})
})