917 lines
33 KiB
TypeScript
917 lines
33 KiB
TypeScript
import { Fixture } from 'ethereum-waffle'
|
|
import { BigNumber, constants, Contract, ContractTransaction, Wallet } from 'ethers'
|
|
import { waffle, ethers } from 'hardhat'
|
|
import { IWETH9, MockTimeNonfungiblePositionManager, MockTimeSwapRouter, TestERC20 } from '../typechain'
|
|
import completeFixture from './shared/completeFixture'
|
|
import { FeeAmount, TICK_SPACINGS } 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'
|
|
import { computePoolAddress } from './shared/computePoolAddress'
|
|
|
|
describe('SwapRouter', function () {
|
|
this.timeout(40000)
|
|
let wallet: Wallet
|
|
let trader: Wallet
|
|
|
|
const swapRouterFixture: Fixture<{
|
|
weth9: IWETH9
|
|
factory: Contract
|
|
router: MockTimeSwapRouter
|
|
nft: MockTimeNonfungiblePositionManager
|
|
tokens: [TestERC20, TestERC20, TestERC20]
|
|
}> = async (wallets, provider) => {
|
|
const { weth9, factory, 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))
|
|
}
|
|
|
|
return {
|
|
weth9,
|
|
factory,
|
|
router,
|
|
tokens,
|
|
nft,
|
|
}
|
|
}
|
|
|
|
let factory: Contract
|
|
let weth9: IWETH9
|
|
let router: MockTimeSwapRouter
|
|
let nft: MockTimeNonfungiblePositionManager
|
|
let tokens: [TestERC20, TestERC20, TestERC20]
|
|
let getBalances: (
|
|
who: string
|
|
) => Promise<{
|
|
weth9: BigNumber
|
|
token0: BigNumber
|
|
token1: BigNumber
|
|
token2: BigNumber
|
|
}>
|
|
|
|
let loadFixture: ReturnType<typeof waffle.createFixtureLoader>
|
|
|
|
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, weth9, factory, tokens, nft } = await loadFixture(swapRouterFixture))
|
|
|
|
getBalances = async (who: string) => {
|
|
const balances = await Promise.all([
|
|
weth9.balanceOf(who),
|
|
tokens[0].balanceOf(who),
|
|
tokens[1].balanceOf(who),
|
|
tokens[2].balanceOf(who),
|
|
])
|
|
return {
|
|
weth9: 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()
|
|
})
|
|
|
|
describe('swaps', () => {
|
|
const liquidity = 1000000
|
|
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: 1,
|
|
}
|
|
|
|
return nft.mint(liquidityParams)
|
|
}
|
|
|
|
async function createPoolWETH9(tokenAddress: string) {
|
|
await weth9.deposit({ value: liquidity })
|
|
await weth9.approve(nft.address, constants.MaxUint256)
|
|
return createPool(weth9.address, tokenAddress)
|
|
}
|
|
|
|
beforeEach('create 0-1 and 1-2 pools', async () => {
|
|
await createPool(tokens[0].address, tokens[1].address)
|
|
await createPool(tokens[1].address, tokens[2].address)
|
|
})
|
|
|
|
describe('#exactInput', () => {
|
|
async function exactInput(
|
|
tokens: string[],
|
|
amountIn: number = 3,
|
|
amountOutMinimum: number = 1
|
|
): Promise<ContractTransaction> {
|
|
const inputIsWETH = weth9.address === tokens[0]
|
|
const outputIsWETH9 = tokens[tokens.length - 1] === weth9.address
|
|
|
|
const value = inputIsWETH ? amountIn : 0
|
|
|
|
const params = {
|
|
path: encodePath(tokens, new Array(tokens.length - 1).fill(FeeAmount.MEDIUM)),
|
|
recipient: outputIsWETH9 ? constants.AddressZero : trader.address,
|
|
deadline: 1,
|
|
amountIn,
|
|
amountOutMinimum,
|
|
}
|
|
|
|
const data = [router.interface.encodeFunctionData('exactInput', [params])]
|
|
if (outputIsWETH9)
|
|
data.push(router.interface.encodeFunctionData('unwrapWETH9', [amountOutMinimum, trader.address]))
|
|
|
|
// ensure that the swap fails if the limit is any tighter
|
|
params.amountOutMinimum += 1
|
|
await expect(router.connect(trader).exactInput(params, { value })).to.be.revertedWith('Too little received')
|
|
params.amountOutMinimum -= 1
|
|
|
|
// optimized for the gas test
|
|
return data.length === 1
|
|
? router.connect(trader).exactInput(params, { value })
|
|
: router.connect(trader).multicall(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('ETH input', () => {
|
|
describe('WETH9', () => {
|
|
beforeEach(async () => {
|
|
await createPoolWETH9(tokens[0].address)
|
|
})
|
|
|
|
it('WETH9 -> 0', async () => {
|
|
const pool = await factory.getPool(weth9.address, tokens[0].address, FeeAmount.MEDIUM)
|
|
|
|
// get balances before
|
|
const poolBefore = await getBalances(pool)
|
|
const traderBefore = await getBalances(trader.address)
|
|
|
|
await expect(exactInput([weth9.address, tokens[0].address]))
|
|
.to.emit(weth9, '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.weth9).to.be.eq(poolBefore.weth9.add(3))
|
|
expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1))
|
|
})
|
|
|
|
it('WETH9 -> 0 -> 1', async () => {
|
|
const traderBefore = await getBalances(trader.address)
|
|
|
|
await expect(exactInput([weth9.address, tokens[0].address, tokens[1].address], 5))
|
|
.to.emit(weth9, 'Deposit')
|
|
.withArgs(router.address, 5)
|
|
|
|
const traderAfter = await getBalances(trader.address)
|
|
|
|
expect(traderAfter.token1).to.be.eq(traderBefore.token1.add(1))
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('ETH output', () => {
|
|
describe('WETH9', () => {
|
|
beforeEach(async () => {
|
|
await createPoolWETH9(tokens[0].address)
|
|
await createPoolWETH9(tokens[1].address)
|
|
})
|
|
|
|
it('0 -> WETH9', async () => {
|
|
const pool = await factory.getPool(tokens[0].address, weth9.address, FeeAmount.MEDIUM)
|
|
|
|
// get balances before
|
|
const poolBefore = await getBalances(pool)
|
|
const traderBefore = await getBalances(trader.address)
|
|
|
|
await expect(exactInput([tokens[0].address, weth9.address]))
|
|
.to.emit(weth9, '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.weth9).to.be.eq(poolBefore.weth9.sub(1))
|
|
expect(poolAfter.token0).to.be.eq(poolBefore.token0.add(3))
|
|
})
|
|
|
|
it('0 -> 1 -> WETH9', async () => {
|
|
// get balances before
|
|
const traderBefore = await getBalances(trader.address)
|
|
|
|
await expect(exactInput([tokens[0].address, tokens[1].address, weth9.address], 5))
|
|
.to.emit(weth9, '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 inputIsWETH = weth9.address === tokenIn
|
|
const outputIsWETH9 = tokenOut === weth9.address
|
|
|
|
const value = inputIsWETH ? amountIn : 0
|
|
|
|
const params = {
|
|
tokenIn,
|
|
tokenOut,
|
|
fee: FeeAmount.MEDIUM,
|
|
sqrtPriceLimitX96:
|
|
sqrtPriceLimitX96 ?? tokenIn.toLowerCase() < tokenOut.toLowerCase()
|
|
? BigNumber.from('4295128740')
|
|
: BigNumber.from('1461446703485210103287273052203988822378723970341'),
|
|
recipient: outputIsWETH9 ? constants.AddressZero : trader.address,
|
|
deadline: 1,
|
|
amountIn,
|
|
amountOutMinimum,
|
|
}
|
|
|
|
const data = [router.interface.encodeFunctionData('exactInputSingle', [params])]
|
|
if (outputIsWETH9)
|
|
data.push(router.interface.encodeFunctionData('unwrapWETH9', [amountOutMinimum, trader.address]))
|
|
|
|
// ensure that the swap fails if the limit is any tighter
|
|
params.amountOutMinimum += 1
|
|
await expect(router.connect(trader).exactInputSingle(params, { value })).to.be.revertedWith(
|
|
'Too little received'
|
|
)
|
|
params.amountOutMinimum -= 1
|
|
|
|
// optimized for the gas test
|
|
return data.length === 1
|
|
? router.connect(trader).exactInputSingle(params, { value })
|
|
: router.connect(trader).multicall(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('ETH input', () => {
|
|
describe('WETH9', () => {
|
|
beforeEach(async () => {
|
|
await createPoolWETH9(tokens[0].address)
|
|
})
|
|
|
|
it('WETH9 -> 0', async () => {
|
|
const pool = await factory.getPool(weth9.address, tokens[0].address, FeeAmount.MEDIUM)
|
|
|
|
// get balances before
|
|
const poolBefore = await getBalances(pool)
|
|
const traderBefore = await getBalances(trader.address)
|
|
|
|
await expect(exactInputSingle(weth9.address, tokens[0].address))
|
|
.to.emit(weth9, '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.weth9).to.be.eq(poolBefore.weth9.add(3))
|
|
expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1))
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('ETH output', () => {
|
|
describe('WETH9', () => {
|
|
beforeEach(async () => {
|
|
await createPoolWETH9(tokens[0].address)
|
|
await createPoolWETH9(tokens[1].address)
|
|
})
|
|
|
|
it('0 -> WETH9', async () => {
|
|
const pool = await factory.getPool(tokens[0].address, weth9.address, FeeAmount.MEDIUM)
|
|
|
|
// get balances before
|
|
const poolBefore = await getBalances(pool)
|
|
const traderBefore = await getBalances(trader.address)
|
|
|
|
await expect(exactInputSingle(tokens[0].address, weth9.address))
|
|
.to.emit(weth9, '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.weth9).to.be.eq(poolBefore.weth9.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 inputIsWETH9 = tokens[0] === weth9.address
|
|
const outputIsWETH9 = tokens[tokens.length - 1] === weth9.address
|
|
|
|
const value = inputIsWETH9 ? amountInMaximum : 0
|
|
|
|
const params = {
|
|
path: encodePath(tokens.slice().reverse(), new Array(tokens.length - 1).fill(FeeAmount.MEDIUM)),
|
|
recipient: outputIsWETH9 ? constants.AddressZero : trader.address,
|
|
deadline: 1,
|
|
amountOut,
|
|
amountInMaximum,
|
|
}
|
|
|
|
const data = [router.interface.encodeFunctionData('exactOutput', [params])]
|
|
if (inputIsWETH9) data.push(router.interface.encodeFunctionData('unwrapWETH9', [0, trader.address]))
|
|
if (outputIsWETH9) data.push(router.interface.encodeFunctionData('unwrapWETH9', [amountOut, trader.address]))
|
|
|
|
// ensure that the swap fails if the limit is any tighter
|
|
params.amountInMaximum -= 1
|
|
await expect(router.connect(trader).exactOutput(params, { value })).to.be.revertedWith('Too much requested')
|
|
params.amountInMaximum += 1
|
|
|
|
return router.connect(trader).multicall(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('ETH input', () => {
|
|
describe('WETH9', () => {
|
|
beforeEach(async () => {
|
|
await createPoolWETH9(tokens[0].address)
|
|
})
|
|
|
|
it('WETH9 -> 0', async () => {
|
|
const pool = await factory.getPool(weth9.address, tokens[0].address, FeeAmount.MEDIUM)
|
|
|
|
// get balances before
|
|
const poolBefore = await getBalances(pool)
|
|
const traderBefore = await getBalances(trader.address)
|
|
|
|
await expect(exactOutput([weth9.address, tokens[0].address]))
|
|
.to.emit(weth9, '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.weth9).to.be.eq(poolBefore.weth9.add(3))
|
|
expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1))
|
|
})
|
|
|
|
it('WETH9 -> 0 -> 1', async () => {
|
|
const traderBefore = await getBalances(trader.address)
|
|
|
|
await expect(exactOutput([weth9.address, tokens[0].address, tokens[1].address], 1, 5))
|
|
.to.emit(weth9, 'Deposit')
|
|
.withArgs(router.address, 5)
|
|
|
|
const traderAfter = await getBalances(trader.address)
|
|
|
|
expect(traderAfter.token1).to.be.eq(traderBefore.token1.add(1))
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('ETH output', () => {
|
|
describe('WETH9', () => {
|
|
beforeEach(async () => {
|
|
await createPoolWETH9(tokens[0].address)
|
|
await createPoolWETH9(tokens[1].address)
|
|
})
|
|
|
|
it('0 -> WETH9', async () => {
|
|
const pool = await factory.getPool(tokens[0].address, weth9.address, FeeAmount.MEDIUM)
|
|
|
|
// get balances before
|
|
const poolBefore = await getBalances(pool)
|
|
const traderBefore = await getBalances(trader.address)
|
|
|
|
await expect(exactOutput([tokens[0].address, weth9.address]))
|
|
.to.emit(weth9, '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.weth9).to.be.eq(poolBefore.weth9.sub(1))
|
|
expect(poolAfter.token0).to.be.eq(poolBefore.token0.add(3))
|
|
})
|
|
|
|
it('0 -> 1 -> WETH9', async () => {
|
|
// get balances before
|
|
const traderBefore = await getBalances(trader.address)
|
|
|
|
await expect(exactOutput([tokens[0].address, tokens[1].address, weth9.address], 1, 5))
|
|
.to.emit(weth9, '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 inputIsWETH9 = tokenIn === weth9.address
|
|
const outputIsWETH9 = tokenOut === weth9.address
|
|
|
|
const value = inputIsWETH9 ? amountInMaximum : 0
|
|
|
|
const params = {
|
|
tokenIn,
|
|
tokenOut,
|
|
fee: FeeAmount.MEDIUM,
|
|
recipient: outputIsWETH9 ? constants.AddressZero : trader.address,
|
|
deadline: 1,
|
|
amountOut,
|
|
amountInMaximum,
|
|
sqrtPriceLimitX96:
|
|
sqrtPriceLimitX96 ?? tokenIn.toLowerCase() < tokenOut.toLowerCase()
|
|
? BigNumber.from('4295128740')
|
|
: BigNumber.from('1461446703485210103287273052203988822378723970341'),
|
|
}
|
|
|
|
const data = [router.interface.encodeFunctionData('exactOutputSingle', [params])]
|
|
if (inputIsWETH9) data.push(router.interface.encodeFunctionData('refundETH'))
|
|
if (outputIsWETH9) data.push(router.interface.encodeFunctionData('unwrapWETH9', [amountOut, trader.address]))
|
|
|
|
// ensure that the swap fails if the limit is any tighter
|
|
params.amountInMaximum -= 1
|
|
await expect(router.connect(trader).exactOutputSingle(params, { value })).to.be.revertedWith(
|
|
'Too much requested'
|
|
)
|
|
params.amountInMaximum += 1
|
|
|
|
return router.connect(trader).multicall(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('ETH input', () => {
|
|
describe('WETH9', () => {
|
|
beforeEach(async () => {
|
|
await createPoolWETH9(tokens[0].address)
|
|
})
|
|
|
|
it('WETH9 -> 0', async () => {
|
|
const pool = await factory.getPool(weth9.address, tokens[0].address, FeeAmount.MEDIUM)
|
|
|
|
// get balances before
|
|
const poolBefore = await getBalances(pool)
|
|
const traderBefore = await getBalances(trader.address)
|
|
|
|
await expect(exactOutputSingle(weth9.address, tokens[0].address))
|
|
.to.emit(weth9, '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.weth9).to.be.eq(poolBefore.weth9.add(3))
|
|
expect(poolAfter.token0).to.be.eq(poolBefore.token0.sub(1))
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('ETH output', () => {
|
|
describe('WETH9', () => {
|
|
beforeEach(async () => {
|
|
await createPoolWETH9(tokens[0].address)
|
|
await createPoolWETH9(tokens[1].address)
|
|
})
|
|
|
|
it('0 -> WETH9', async () => {
|
|
const pool = await factory.getPool(tokens[0].address, weth9.address, FeeAmount.MEDIUM)
|
|
|
|
// get balances before
|
|
const poolBefore = await getBalances(pool)
|
|
const traderBefore = await getBalances(trader.address)
|
|
|
|
await expect(exactOutputSingle(tokens[0].address, weth9.address))
|
|
.to.emit(weth9, '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.weth9).to.be.eq(poolBefore.weth9.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: router.address,
|
|
deadline: 1,
|
|
amountIn: 102,
|
|
amountOutMinimum,
|
|
}
|
|
|
|
const data = [
|
|
router.interface.encodeFunctionData('exactInput', [params]),
|
|
router.interface.encodeFunctionData('sweepTokenWithFee', [
|
|
tokens[1].address,
|
|
amountOutMinimum,
|
|
trader.address,
|
|
100,
|
|
feeRecipient,
|
|
]),
|
|
]
|
|
|
|
await router.connect(trader).multicall(data)
|
|
|
|
const balance = await tokens[1].balanceOf(feeRecipient)
|
|
expect(balance.eq(1)).to.be.eq(true)
|
|
})
|
|
|
|
it('#unwrapWETH9WithFee', async () => {
|
|
const startBalance = await waffle.provider.getBalance(feeRecipient)
|
|
await createPoolWETH9(tokens[0].address)
|
|
|
|
const amountOutMinimum = 100
|
|
const params = {
|
|
path: encodePath([tokens[0].address, weth9.address], [FeeAmount.MEDIUM]),
|
|
recipient: router.address,
|
|
deadline: 1,
|
|
amountIn: 102,
|
|
amountOutMinimum,
|
|
}
|
|
|
|
const data = [
|
|
router.interface.encodeFunctionData('exactInput', [params]),
|
|
router.interface.encodeFunctionData('unwrapWETH9WithFee', [
|
|
amountOutMinimum,
|
|
trader.address,
|
|
100,
|
|
feeRecipient,
|
|
]),
|
|
]
|
|
|
|
await router.connect(trader).multicall(data)
|
|
const endBalance = await waffle.provider.getBalance(feeRecipient)
|
|
expect(endBalance.sub(startBalance).eq(1)).to.be.eq(true)
|
|
})
|
|
})
|
|
})
|
|
})
|