735546619e
Signed-off-by: T-Hax <>
1274 lines
40 KiB
TypeScript
1274 lines
40 KiB
TypeScript
import { BigNumberish, constants, Wallet } from 'ethers'
|
|
import { waffle, ethers } from 'hardhat'
|
|
|
|
import { Fixture } from 'ethereum-waffle'
|
|
import {
|
|
TestPositionNFTOwner,
|
|
MockTimeNonfungiblePositionManager,
|
|
TestERC20,
|
|
IWETH9,
|
|
IUniswapV3Factory,
|
|
SwapRouter,
|
|
} from '../typechain'
|
|
import completeFixture from './shared/completeFixture'
|
|
import { computePoolAddress } from './shared/computePoolAddress'
|
|
import { FeeAmount, MaxUint128, TICK_SPACINGS } from './shared/constants'
|
|
import { encodePriceSqrt } from './shared/encodePriceSqrt'
|
|
import { expect } from './shared/expect'
|
|
import getPermitNFTSignature from './shared/getPermitNFTSignature'
|
|
import { encodePath } from './shared/path'
|
|
import poolAtAddress from './shared/poolAtAddress'
|
|
import snapshotGasCost from './shared/snapshotGasCost'
|
|
import { getMaxTick, getMinTick } from './shared/ticks'
|
|
import { expandTo18Decimals } from './shared/expandTo18Decimals'
|
|
import { sortedTokens } from './shared/tokenSort'
|
|
import { extractJSONFromURI } from './shared/extractJSONFromURI'
|
|
|
|
import { abi as IUniswapV3PoolABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json'
|
|
|
|
describe('NonfungiblePositionManager', () => {
|
|
let wallets: Wallet[]
|
|
let wallet: Wallet, other: Wallet
|
|
|
|
const nftFixture: Fixture<{
|
|
nft: MockTimeNonfungiblePositionManager
|
|
factory: IUniswapV3Factory
|
|
tokens: [TestERC20, TestERC20, TestERC20]
|
|
weth9: IWETH9
|
|
router: SwapRouter
|
|
}> = async (wallets, provider) => {
|
|
const { weth9, factory, tokens, nft, router } = await completeFixture(wallets, provider)
|
|
|
|
// approve & fund wallets
|
|
for (const token of tokens) {
|
|
await token.approve(nft.address, constants.MaxUint256)
|
|
await token.connect(other).approve(nft.address, constants.MaxUint256)
|
|
await token.transfer(other.address, expandTo18Decimals(1_000_000))
|
|
}
|
|
|
|
return {
|
|
nft,
|
|
factory,
|
|
tokens,
|
|
weth9,
|
|
router,
|
|
}
|
|
}
|
|
|
|
let factory: IUniswapV3Factory
|
|
let nft: MockTimeNonfungiblePositionManager
|
|
let tokens: [TestERC20, TestERC20, TestERC20]
|
|
let weth9: IWETH9
|
|
let router: SwapRouter
|
|
|
|
let loadFixture: ReturnType<typeof waffle.createFixtureLoader>
|
|
|
|
before('create fixture loader', async () => {
|
|
wallets = await (ethers as any).getSigners()
|
|
;[wallet, other] = wallets
|
|
|
|
loadFixture = waffle.createFixtureLoader(wallets)
|
|
})
|
|
|
|
beforeEach('load fixture', async () => {
|
|
;({ nft, factory, tokens, weth9, router } = await loadFixture(nftFixture))
|
|
})
|
|
|
|
it('bytecode size', async () => {
|
|
expect(((await nft.provider.getCode(nft.address)).length - 2) / 2).to.matchSnapshot()
|
|
})
|
|
|
|
describe('#createAndInitializePoolIfNecessary', () => {
|
|
it('creates the pool at the expected address', async () => {
|
|
const expectedAddress = computePoolAddress(
|
|
factory.address,
|
|
[tokens[0].address, tokens[1].address],
|
|
FeeAmount.MEDIUM
|
|
)
|
|
const code = await wallet.provider.getCode(expectedAddress)
|
|
expect(code).to.eq('0x')
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
const codeAfter = await wallet.provider.getCode(expectedAddress)
|
|
expect(codeAfter).to.not.eq('0x')
|
|
})
|
|
|
|
it('is payable', async () => {
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1),
|
|
{ value: 1 }
|
|
)
|
|
})
|
|
|
|
it('works if pool is created but not initialized', async () => {
|
|
const expectedAddress = computePoolAddress(
|
|
factory.address,
|
|
[tokens[0].address, tokens[1].address],
|
|
FeeAmount.MEDIUM
|
|
)
|
|
await factory.createPool(tokens[0].address, tokens[1].address, FeeAmount.MEDIUM)
|
|
const code = await wallet.provider.getCode(expectedAddress)
|
|
expect(code).to.not.eq('0x')
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(2, 1)
|
|
)
|
|
})
|
|
|
|
it('works if pool is created and initialized', async () => {
|
|
const expectedAddress = computePoolAddress(
|
|
factory.address,
|
|
[tokens[0].address, tokens[1].address],
|
|
FeeAmount.MEDIUM
|
|
)
|
|
await factory.createPool(tokens[0].address, tokens[1].address, FeeAmount.MEDIUM)
|
|
const pool = new ethers.Contract(expectedAddress, IUniswapV3PoolABI, wallet)
|
|
|
|
await pool.initialize(encodePriceSqrt(3, 1))
|
|
const code = await wallet.provider.getCode(expectedAddress)
|
|
expect(code).to.not.eq('0x')
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(4, 1)
|
|
)
|
|
})
|
|
|
|
it('could theoretically use eth via multicall', async () => {
|
|
const [token0, token1] = sortedTokens(weth9, tokens[0])
|
|
|
|
const createAndInitializePoolIfNecessaryData = nft.interface.encodeFunctionData(
|
|
'createAndInitializePoolIfNecessary',
|
|
[token0.address, token1.address, FeeAmount.MEDIUM, encodePriceSqrt(1, 1)]
|
|
)
|
|
|
|
await nft.multicall([createAndInitializePoolIfNecessaryData], { value: expandTo18Decimals(1) })
|
|
})
|
|
|
|
it('gas', async () => {
|
|
await snapshotGasCost(
|
|
nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('#mint', () => {
|
|
it('fails if pool does not exist', async () => {
|
|
await expect(
|
|
nft.mint({
|
|
token0: tokens[0].address,
|
|
token1: tokens[1].address,
|
|
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
recipient: wallet.address,
|
|
deadline: 1,
|
|
fee: FeeAmount.MEDIUM,
|
|
})
|
|
).to.be.reverted
|
|
})
|
|
|
|
it('fails if cannot transfer', async () => {
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
await tokens[0].approve(nft.address, 0)
|
|
await expect(
|
|
nft.mint({
|
|
token0: tokens[0].address,
|
|
token1: tokens[1].address,
|
|
fee: FeeAmount.MEDIUM,
|
|
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
recipient: wallet.address,
|
|
deadline: 1,
|
|
})
|
|
).to.be.revertedWith('STF')
|
|
})
|
|
|
|
it('creates a token', async () => {
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
|
|
await nft.mint({
|
|
token0: tokens[0].address,
|
|
token1: tokens[1].address,
|
|
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
fee: FeeAmount.MEDIUM,
|
|
recipient: other.address,
|
|
amount0Desired: 15,
|
|
amount1Desired: 15,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 10,
|
|
})
|
|
expect(await nft.balanceOf(other.address)).to.eq(1)
|
|
expect(await nft.tokenOfOwnerByIndex(other.address, 0)).to.eq(1)
|
|
const {
|
|
fee,
|
|
token0,
|
|
token1,
|
|
tickLower,
|
|
tickUpper,
|
|
liquidity,
|
|
tokensOwed0,
|
|
tokensOwed1,
|
|
feeGrowthInside0LastX128,
|
|
feeGrowthInside1LastX128,
|
|
} = await nft.positions(1)
|
|
expect(token0).to.eq(tokens[0].address)
|
|
expect(token1).to.eq(tokens[1].address)
|
|
expect(fee).to.eq(FeeAmount.MEDIUM)
|
|
expect(tickLower).to.eq(getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]))
|
|
expect(tickUpper).to.eq(getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]))
|
|
expect(liquidity).to.eq(15)
|
|
expect(tokensOwed0).to.eq(0)
|
|
expect(tokensOwed1).to.eq(0)
|
|
expect(feeGrowthInside0LastX128).to.eq(0)
|
|
expect(feeGrowthInside1LastX128).to.eq(0)
|
|
})
|
|
|
|
it('can use eth via multicall', async () => {
|
|
const [token0, token1] = sortedTokens(weth9, tokens[0])
|
|
|
|
// remove any approval
|
|
await weth9.approve(nft.address, 0)
|
|
|
|
const createAndInitializeData = nft.interface.encodeFunctionData('createAndInitializePoolIfNecessary', [
|
|
token0.address,
|
|
token1.address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1),
|
|
])
|
|
|
|
const mintData = nft.interface.encodeFunctionData('mint', [
|
|
{
|
|
token0: token0.address,
|
|
token1: token1.address,
|
|
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
fee: FeeAmount.MEDIUM,
|
|
recipient: other.address,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 1,
|
|
},
|
|
])
|
|
|
|
const refundETHData = nft.interface.encodeFunctionData('refundETH')
|
|
|
|
const balanceBefore = await wallet.getBalance()
|
|
await nft.multicall([createAndInitializeData, mintData, refundETHData], {
|
|
value: expandTo18Decimals(1),
|
|
gasPrice: 0, // necessary so the balance doesn't change by anything that's not spent
|
|
})
|
|
const balanceAfter = await wallet.getBalance()
|
|
expect(balanceBefore.sub(balanceAfter)).to.eq(100)
|
|
})
|
|
|
|
it('emits an event')
|
|
|
|
it('gas first mint for pool', async () => {
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
|
|
await snapshotGasCost(
|
|
nft.mint({
|
|
token0: tokens[0].address,
|
|
token1: tokens[1].address,
|
|
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
fee: FeeAmount.MEDIUM,
|
|
recipient: wallet.address,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 10,
|
|
})
|
|
)
|
|
})
|
|
|
|
it('gas first mint for pool using eth with zero refund', async () => {
|
|
const [token0, token1] = sortedTokens(weth9, tokens[0])
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
token0.address,
|
|
token1.address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
|
|
await snapshotGasCost(
|
|
nft.multicall(
|
|
[
|
|
nft.interface.encodeFunctionData('mint', [
|
|
{
|
|
token0: token0.address,
|
|
token1: token1.address,
|
|
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
fee: FeeAmount.MEDIUM,
|
|
recipient: wallet.address,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 10,
|
|
},
|
|
]),
|
|
nft.interface.encodeFunctionData('refundETH'),
|
|
],
|
|
{ value: 100 }
|
|
)
|
|
)
|
|
})
|
|
|
|
it('gas first mint for pool using eth with non-zero refund', async () => {
|
|
const [token0, token1] = sortedTokens(weth9, tokens[0])
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
token0.address,
|
|
token1.address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
|
|
await snapshotGasCost(
|
|
nft.multicall(
|
|
[
|
|
nft.interface.encodeFunctionData('mint', [
|
|
{
|
|
token0: token0.address,
|
|
token1: token1.address,
|
|
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
fee: FeeAmount.MEDIUM,
|
|
recipient: wallet.address,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 10,
|
|
},
|
|
]),
|
|
nft.interface.encodeFunctionData('refundETH'),
|
|
],
|
|
{ value: 1000 }
|
|
)
|
|
)
|
|
})
|
|
|
|
it('gas mint on same ticks', async () => {
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
|
|
await nft.mint({
|
|
token0: tokens[0].address,
|
|
token1: tokens[1].address,
|
|
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
fee: FeeAmount.MEDIUM,
|
|
recipient: other.address,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 10,
|
|
})
|
|
|
|
await snapshotGasCost(
|
|
nft.mint({
|
|
token0: tokens[0].address,
|
|
token1: tokens[1].address,
|
|
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
fee: FeeAmount.MEDIUM,
|
|
recipient: wallet.address,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 10,
|
|
})
|
|
)
|
|
})
|
|
|
|
it('gas mint for same pool, different ticks', async () => {
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
|
|
await nft.mint({
|
|
token0: tokens[0].address,
|
|
token1: tokens[1].address,
|
|
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
fee: FeeAmount.MEDIUM,
|
|
recipient: other.address,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 10,
|
|
})
|
|
|
|
await snapshotGasCost(
|
|
nft.mint({
|
|
token0: tokens[0].address,
|
|
token1: tokens[1].address,
|
|
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]) + TICK_SPACINGS[FeeAmount.MEDIUM],
|
|
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]) - TICK_SPACINGS[FeeAmount.MEDIUM],
|
|
fee: FeeAmount.MEDIUM,
|
|
recipient: wallet.address,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 10,
|
|
})
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('#increaseLiquidity', () => {
|
|
const tokenId = 1
|
|
beforeEach('create a position', async () => {
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
|
|
await nft.mint({
|
|
token0: tokens[0].address,
|
|
token1: tokens[1].address,
|
|
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
fee: FeeAmount.MEDIUM,
|
|
recipient: other.address,
|
|
amount0Desired: 1000,
|
|
amount1Desired: 1000,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 1,
|
|
})
|
|
})
|
|
|
|
it('increases position liquidity', async () => {
|
|
await nft.increaseLiquidity({
|
|
tokenId: tokenId,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 1,
|
|
})
|
|
const { liquidity } = await nft.positions(tokenId)
|
|
expect(liquidity).to.eq(1100)
|
|
})
|
|
|
|
it('emits an event')
|
|
|
|
it('can be paid with ETH', async () => {
|
|
const [token0, token1] = sortedTokens(tokens[0], weth9)
|
|
|
|
const tokenId = 1
|
|
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
token0.address,
|
|
token1.address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
|
|
const mintData = nft.interface.encodeFunctionData('mint', [
|
|
{
|
|
token0: token0.address,
|
|
token1: token1.address,
|
|
fee: FeeAmount.MEDIUM,
|
|
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
recipient: other.address,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 1,
|
|
},
|
|
])
|
|
const refundETHData = nft.interface.encodeFunctionData('unwrapWETH9', [0, other.address])
|
|
await nft.multicall([mintData, refundETHData], { value: expandTo18Decimals(1) })
|
|
|
|
const increaseLiquidityData = nft.interface.encodeFunctionData('increaseLiquidity', [
|
|
{
|
|
tokenId: tokenId,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 1,
|
|
},
|
|
])
|
|
await nft.multicall([increaseLiquidityData, refundETHData], { value: expandTo18Decimals(1) })
|
|
})
|
|
|
|
it('gas', async () => {
|
|
await snapshotGasCost(
|
|
nft.increaseLiquidity({
|
|
tokenId: tokenId,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 1,
|
|
})
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('#decreaseLiquidity', () => {
|
|
const tokenId = 1
|
|
beforeEach('create a position', async () => {
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
|
|
await nft.mint({
|
|
token0: tokens[0].address,
|
|
token1: tokens[1].address,
|
|
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
fee: FeeAmount.MEDIUM,
|
|
recipient: other.address,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 1,
|
|
})
|
|
})
|
|
|
|
it('emits an event')
|
|
|
|
it('fails if past deadline', async () => {
|
|
await nft.setTime(2)
|
|
await expect(
|
|
nft.connect(other).decreaseLiquidity({ tokenId, liquidity: 50, amount0Min: 0, amount1Min: 0, deadline: 1 })
|
|
).to.be.revertedWith('Transaction too old')
|
|
})
|
|
|
|
it('cannot be called by other addresses', async () => {
|
|
await expect(
|
|
nft.decreaseLiquidity({ tokenId, liquidity: 50, amount0Min: 0, amount1Min: 0, deadline: 1 })
|
|
).to.be.revertedWith('Not approved')
|
|
})
|
|
|
|
it('decreases position liquidity', async () => {
|
|
await nft.connect(other).decreaseLiquidity({ tokenId, liquidity: 25, amount0Min: 0, amount1Min: 0, deadline: 1 })
|
|
const { liquidity } = await nft.positions(tokenId)
|
|
expect(liquidity).to.eq(75)
|
|
})
|
|
|
|
it('is payable', async () => {
|
|
await nft
|
|
.connect(other)
|
|
.decreaseLiquidity({ tokenId, liquidity: 25, amount0Min: 0, amount1Min: 0, deadline: 1 }, { value: 1 })
|
|
})
|
|
|
|
it('accounts for tokens owed', async () => {
|
|
await nft.connect(other).decreaseLiquidity({ tokenId, liquidity: 25, amount0Min: 0, amount1Min: 0, deadline: 1 })
|
|
const { tokensOwed0, tokensOwed1 } = await nft.positions(tokenId)
|
|
expect(tokensOwed0).to.eq(24)
|
|
expect(tokensOwed1).to.eq(24)
|
|
})
|
|
|
|
it('can decrease for all the liquidity', async () => {
|
|
await nft.connect(other).decreaseLiquidity({ tokenId, liquidity: 100, amount0Min: 0, amount1Min: 0, deadline: 1 })
|
|
const { liquidity } = await nft.positions(tokenId)
|
|
expect(liquidity).to.eq(0)
|
|
})
|
|
|
|
it('cannot decrease for more than all the liquidity', async () => {
|
|
await expect(
|
|
nft.connect(other).decreaseLiquidity({ tokenId, liquidity: 101, amount0Min: 0, amount1Min: 0, deadline: 1 })
|
|
).to.be.reverted
|
|
})
|
|
|
|
it('cannot decrease for more than the liquidity of the nft position', async () => {
|
|
await nft.mint({
|
|
token0: tokens[0].address,
|
|
token1: tokens[1].address,
|
|
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
|
fee: FeeAmount.MEDIUM,
|
|
recipient: other.address,
|
|
amount0Desired: 200,
|
|
amount1Desired: 200,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 1,
|
|
})
|
|
await expect(
|
|
nft.connect(other).decreaseLiquidity({ tokenId, liquidity: 101, amount0Min: 0, amount1Min: 0, deadline: 1 })
|
|
).to.be.reverted
|
|
})
|
|
|
|
it('gas partial decrease', async () => {
|
|
await snapshotGasCost(
|
|
nft.connect(other).decreaseLiquidity({ tokenId, liquidity: 50, amount0Min: 0, amount1Min: 0, deadline: 1 })
|
|
)
|
|
})
|
|
|
|
it('gas complete decrease', async () => {
|
|
await snapshotGasCost(
|
|
nft.connect(other).decreaseLiquidity({ tokenId, liquidity: 100, amount0Min: 0, amount1Min: 0, deadline: 1 })
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('#collect', () => {
|
|
const tokenId = 1
|
|
beforeEach('create a position', async () => {
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
|
|
await nft.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: other.address,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 1,
|
|
})
|
|
})
|
|
|
|
it('emits an event')
|
|
|
|
it('cannot be called by other addresses', async () => {
|
|
await expect(
|
|
nft.collect({
|
|
tokenId,
|
|
recipient: wallet.address,
|
|
amount0Max: MaxUint128,
|
|
amount1Max: MaxUint128,
|
|
})
|
|
).to.be.revertedWith('Not approved')
|
|
})
|
|
|
|
it('cannot be called with 0 for both amounts', async () => {
|
|
await expect(
|
|
nft.connect(other).collect({
|
|
tokenId,
|
|
recipient: wallet.address,
|
|
amount0Max: 0,
|
|
amount1Max: 0,
|
|
})
|
|
).to.be.reverted
|
|
})
|
|
|
|
it('no op if no tokens are owed', async () => {
|
|
await expect(
|
|
nft.connect(other).collect({
|
|
tokenId,
|
|
recipient: wallet.address,
|
|
amount0Max: MaxUint128,
|
|
amount1Max: MaxUint128,
|
|
})
|
|
)
|
|
.to.not.emit(tokens[0], 'Transfer')
|
|
.to.not.emit(tokens[1], 'Transfer')
|
|
})
|
|
|
|
it('transfers tokens owed from burn', async () => {
|
|
await nft.connect(other).decreaseLiquidity({ tokenId, liquidity: 50, amount0Min: 0, amount1Min: 0, deadline: 1 })
|
|
const poolAddress = computePoolAddress(factory.address, [tokens[0].address, tokens[1].address], FeeAmount.MEDIUM)
|
|
await expect(
|
|
nft.connect(other).collect({
|
|
tokenId,
|
|
recipient: wallet.address,
|
|
amount0Max: MaxUint128,
|
|
amount1Max: MaxUint128,
|
|
})
|
|
)
|
|
.to.emit(tokens[0], 'Transfer')
|
|
.withArgs(poolAddress, wallet.address, 49)
|
|
.to.emit(tokens[1], 'Transfer')
|
|
.withArgs(poolAddress, wallet.address, 49)
|
|
})
|
|
|
|
it('gas transfers both', async () => {
|
|
await nft.connect(other).decreaseLiquidity({ tokenId, liquidity: 50, amount0Min: 0, amount1Min: 0, deadline: 1 })
|
|
await snapshotGasCost(
|
|
nft.connect(other).collect({
|
|
tokenId,
|
|
recipient: wallet.address,
|
|
amount0Max: MaxUint128,
|
|
amount1Max: MaxUint128,
|
|
})
|
|
)
|
|
})
|
|
|
|
it('gas transfers token0 only', async () => {
|
|
await nft.connect(other).decreaseLiquidity({ tokenId, liquidity: 50, amount0Min: 0, amount1Min: 0, deadline: 1 })
|
|
await snapshotGasCost(
|
|
nft.connect(other).collect({
|
|
tokenId,
|
|
recipient: wallet.address,
|
|
amount0Max: MaxUint128,
|
|
amount1Max: 0,
|
|
})
|
|
)
|
|
})
|
|
|
|
it('gas transfers token1 only', async () => {
|
|
await nft.connect(other).decreaseLiquidity({ tokenId, liquidity: 50, amount0Min: 0, amount1Min: 0, deadline: 1 })
|
|
await snapshotGasCost(
|
|
nft.connect(other).collect({
|
|
tokenId,
|
|
recipient: wallet.address,
|
|
amount0Max: 0,
|
|
amount1Max: MaxUint128,
|
|
})
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('#burn', () => {
|
|
const tokenId = 1
|
|
beforeEach('create a position', async () => {
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
|
|
await nft.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: other.address,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 1,
|
|
})
|
|
})
|
|
|
|
it('emits an event')
|
|
|
|
it('cannot be called by other addresses', async () => {
|
|
await expect(nft.burn(tokenId)).to.be.revertedWith('Not approved')
|
|
})
|
|
|
|
it('cannot be called while there is still liquidity', async () => {
|
|
await expect(nft.connect(other).burn(tokenId)).to.be.revertedWith('Not cleared')
|
|
})
|
|
|
|
it('cannot be called while there is still partial liquidity', async () => {
|
|
await nft.connect(other).decreaseLiquidity({ tokenId, liquidity: 50, amount0Min: 0, amount1Min: 0, deadline: 1 })
|
|
await expect(nft.connect(other).burn(tokenId)).to.be.revertedWith('Not cleared')
|
|
})
|
|
|
|
it('cannot be called while there is still tokens owed', async () => {
|
|
await nft.connect(other).decreaseLiquidity({ tokenId, liquidity: 100, amount0Min: 0, amount1Min: 0, deadline: 1 })
|
|
await expect(nft.connect(other).burn(tokenId)).to.be.revertedWith('Not cleared')
|
|
})
|
|
|
|
it('deletes the token', async () => {
|
|
await nft.connect(other).decreaseLiquidity({ tokenId, liquidity: 100, amount0Min: 0, amount1Min: 0, deadline: 1 })
|
|
await nft.connect(other).collect({
|
|
tokenId,
|
|
recipient: wallet.address,
|
|
amount0Max: MaxUint128,
|
|
amount1Max: MaxUint128,
|
|
})
|
|
await nft.connect(other).burn(tokenId)
|
|
await expect(nft.positions(tokenId)).to.be.revertedWith('Invalid token ID')
|
|
})
|
|
|
|
it('gas', async () => {
|
|
await nft.connect(other).decreaseLiquidity({ tokenId, liquidity: 100, amount0Min: 0, amount1Min: 0, deadline: 1 })
|
|
await nft.connect(other).collect({
|
|
tokenId,
|
|
recipient: wallet.address,
|
|
amount0Max: MaxUint128,
|
|
amount1Max: MaxUint128,
|
|
})
|
|
await snapshotGasCost(nft.connect(other).burn(tokenId))
|
|
})
|
|
})
|
|
|
|
describe('#transferFrom', () => {
|
|
const tokenId = 1
|
|
beforeEach('create a position', async () => {
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
|
|
await nft.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: other.address,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 1,
|
|
})
|
|
})
|
|
|
|
it('can only be called by authorized or owner', async () => {
|
|
await expect(nft.transferFrom(other.address, wallet.address, tokenId)).to.be.revertedWith(
|
|
'ERC721: transfer caller is not owner nor approved'
|
|
)
|
|
})
|
|
|
|
it('changes the owner', async () => {
|
|
await nft.connect(other).transferFrom(other.address, wallet.address, tokenId)
|
|
expect(await nft.ownerOf(tokenId)).to.eq(wallet.address)
|
|
})
|
|
|
|
it('removes existing approval', async () => {
|
|
await nft.connect(other).approve(wallet.address, tokenId)
|
|
expect(await nft.getApproved(tokenId)).to.eq(wallet.address)
|
|
await nft.transferFrom(other.address, wallet.address, tokenId)
|
|
expect(await nft.getApproved(tokenId)).to.eq(constants.AddressZero)
|
|
})
|
|
|
|
it('gas', async () => {
|
|
await snapshotGasCost(nft.connect(other).transferFrom(other.address, wallet.address, tokenId))
|
|
})
|
|
|
|
it('gas comes from approved', async () => {
|
|
await nft.connect(other).approve(wallet.address, tokenId)
|
|
await snapshotGasCost(nft.transferFrom(other.address, wallet.address, tokenId))
|
|
})
|
|
})
|
|
|
|
describe('#permit', () => {
|
|
it('emits an event')
|
|
|
|
describe('owned by eoa', () => {
|
|
const tokenId = 1
|
|
beforeEach('create a position', async () => {
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
|
|
await nft.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: other.address,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 1,
|
|
})
|
|
})
|
|
|
|
it('changes the operator of the position and increments the nonce', async () => {
|
|
const { v, r, s } = await getPermitNFTSignature(other, nft, wallet.address, tokenId, 1)
|
|
await nft.permit(wallet.address, tokenId, 1, v, r, s)
|
|
expect((await nft.positions(tokenId)).nonce).to.eq(1)
|
|
expect((await nft.positions(tokenId)).operator).to.eq(wallet.address)
|
|
})
|
|
|
|
it('cannot be called twice with the same signature', async () => {
|
|
const { v, r, s } = await getPermitNFTSignature(other, nft, wallet.address, tokenId, 1)
|
|
await nft.permit(wallet.address, tokenId, 1, v, r, s)
|
|
await expect(nft.permit(wallet.address, tokenId, 1, v, r, s)).to.be.reverted
|
|
})
|
|
|
|
it('fails with invalid signature', async () => {
|
|
const { v, r, s } = await getPermitNFTSignature(wallet, nft, wallet.address, tokenId, 1)
|
|
await expect(nft.permit(wallet.address, tokenId, 1, v + 3, r, s)).to.be.revertedWith('Invalid signature')
|
|
})
|
|
|
|
it('fails with signature not from owner', async () => {
|
|
const { v, r, s } = await getPermitNFTSignature(wallet, nft, wallet.address, tokenId, 1)
|
|
await expect(nft.permit(wallet.address, tokenId, 1, v, r, s)).to.be.revertedWith('Unauthorized')
|
|
})
|
|
|
|
it('fails with expired signature', async () => {
|
|
await nft.setTime(2)
|
|
const { v, r, s } = await getPermitNFTSignature(other, nft, wallet.address, tokenId, 1)
|
|
await expect(nft.permit(wallet.address, tokenId, 1, v, r, s)).to.be.revertedWith('Permit expired')
|
|
})
|
|
|
|
it('gas', async () => {
|
|
const { v, r, s } = await getPermitNFTSignature(other, nft, wallet.address, tokenId, 1)
|
|
await snapshotGasCost(nft.permit(wallet.address, tokenId, 1, v, r, s))
|
|
})
|
|
})
|
|
describe('owned by verifying contract', () => {
|
|
const tokenId = 1
|
|
let testPositionNFTOwner: TestPositionNFTOwner
|
|
|
|
beforeEach('deploy test owner and create a position', async () => {
|
|
testPositionNFTOwner = (await (
|
|
await ethers.getContractFactory('TestPositionNFTOwner')
|
|
).deploy()) as TestPositionNFTOwner
|
|
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
|
|
await nft.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: testPositionNFTOwner.address,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 1,
|
|
})
|
|
})
|
|
|
|
it('changes the operator of the position and increments the nonce', async () => {
|
|
const { v, r, s } = await getPermitNFTSignature(other, nft, wallet.address, tokenId, 1)
|
|
await testPositionNFTOwner.setOwner(other.address)
|
|
await nft.permit(wallet.address, tokenId, 1, v, r, s)
|
|
expect((await nft.positions(tokenId)).nonce).to.eq(1)
|
|
expect((await nft.positions(tokenId)).operator).to.eq(wallet.address)
|
|
})
|
|
|
|
it('fails if owner contract is owned by different address', async () => {
|
|
const { v, r, s } = await getPermitNFTSignature(other, nft, wallet.address, tokenId, 1)
|
|
await testPositionNFTOwner.setOwner(wallet.address)
|
|
await expect(nft.permit(wallet.address, tokenId, 1, v, r, s)).to.be.revertedWith('Unauthorized')
|
|
})
|
|
|
|
it('fails with signature not from owner', async () => {
|
|
const { v, r, s } = await getPermitNFTSignature(wallet, nft, wallet.address, tokenId, 1)
|
|
await testPositionNFTOwner.setOwner(other.address)
|
|
await expect(nft.permit(wallet.address, tokenId, 1, v, r, s)).to.be.revertedWith('Unauthorized')
|
|
})
|
|
|
|
it('fails with expired signature', async () => {
|
|
await nft.setTime(2)
|
|
const { v, r, s } = await getPermitNFTSignature(other, nft, wallet.address, tokenId, 1)
|
|
await testPositionNFTOwner.setOwner(other.address)
|
|
await expect(nft.permit(wallet.address, tokenId, 1, v, r, s)).to.be.revertedWith('Permit expired')
|
|
})
|
|
|
|
it('gas', async () => {
|
|
const { v, r, s } = await getPermitNFTSignature(other, nft, wallet.address, tokenId, 1)
|
|
await testPositionNFTOwner.setOwner(other.address)
|
|
await snapshotGasCost(nft.permit(wallet.address, tokenId, 1, v, r, s))
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('multicall exit', () => {
|
|
const tokenId = 1
|
|
beforeEach('create a position', async () => {
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
|
|
await nft.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: other.address,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 1,
|
|
})
|
|
})
|
|
|
|
async function exit({
|
|
nft,
|
|
liquidity,
|
|
tokenId,
|
|
amount0Min,
|
|
amount1Min,
|
|
recipient,
|
|
}: {
|
|
nft: MockTimeNonfungiblePositionManager
|
|
tokenId: BigNumberish
|
|
liquidity: BigNumberish
|
|
amount0Min: BigNumberish
|
|
amount1Min: BigNumberish
|
|
recipient: string
|
|
}) {
|
|
const decreaseLiquidityData = nft.interface.encodeFunctionData('decreaseLiquidity', [
|
|
{ tokenId, liquidity, amount0Min, amount1Min, deadline: 1 },
|
|
])
|
|
const collectData = nft.interface.encodeFunctionData('collect', [
|
|
{
|
|
tokenId,
|
|
recipient,
|
|
amount0Max: MaxUint128,
|
|
amount1Max: MaxUint128,
|
|
},
|
|
])
|
|
const burnData = nft.interface.encodeFunctionData('burn', [tokenId])
|
|
|
|
return nft.multicall([decreaseLiquidityData, collectData, burnData])
|
|
}
|
|
|
|
it('executes all the actions', async () => {
|
|
const pool = poolAtAddress(
|
|
computePoolAddress(factory.address, [tokens[0].address, tokens[1].address], FeeAmount.MEDIUM),
|
|
wallet
|
|
)
|
|
await expect(
|
|
exit({
|
|
nft: nft.connect(other),
|
|
tokenId,
|
|
liquidity: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
recipient: wallet.address,
|
|
})
|
|
)
|
|
.to.emit(pool, 'Burn')
|
|
.to.emit(pool, 'Collect')
|
|
})
|
|
|
|
it('gas', async () => {
|
|
await snapshotGasCost(
|
|
exit({
|
|
nft: nft.connect(other),
|
|
tokenId,
|
|
liquidity: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
recipient: wallet.address,
|
|
})
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('#tokenURI', async () => {
|
|
const tokenId = 1
|
|
beforeEach('create a position', async () => {
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
|
|
await nft.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: other.address,
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 1,
|
|
})
|
|
})
|
|
|
|
it('reverts for invalid token id', async () => {
|
|
await expect(nft.tokenURI(tokenId + 1)).to.be.reverted
|
|
})
|
|
|
|
it('returns a data URI with correct mime type', async () => {
|
|
expect(await nft.tokenURI(tokenId)).to.match(/data:application\/json;base64,.+/)
|
|
})
|
|
|
|
it('content is valid JSON and structure', async () => {
|
|
const content = extractJSONFromURI(await nft.tokenURI(tokenId))
|
|
expect(content).to.haveOwnProperty('name').is.a('string')
|
|
expect(content).to.haveOwnProperty('description').is.a('string')
|
|
expect(content).to.haveOwnProperty('image').is.a('string')
|
|
})
|
|
})
|
|
|
|
describe('fees accounting', () => {
|
|
beforeEach('create two positions', async () => {
|
|
await nft.createAndInitializePoolIfNecessary(
|
|
tokens[0].address,
|
|
tokens[1].address,
|
|
FeeAmount.MEDIUM,
|
|
encodePriceSqrt(1, 1)
|
|
)
|
|
// nft 1 earns 25% of fees
|
|
await nft.mint({
|
|
token0: tokens[0].address,
|
|
token1: tokens[1].address,
|
|
fee: FeeAmount.MEDIUM,
|
|
tickLower: getMinTick(FeeAmount.MEDIUM),
|
|
tickUpper: getMaxTick(FeeAmount.MEDIUM),
|
|
amount0Desired: 100,
|
|
amount1Desired: 100,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 1,
|
|
recipient: wallet.address,
|
|
})
|
|
// nft 2 earns 75% of fees
|
|
await nft.mint({
|
|
token0: tokens[0].address,
|
|
token1: tokens[1].address,
|
|
fee: FeeAmount.MEDIUM,
|
|
tickLower: getMinTick(FeeAmount.MEDIUM),
|
|
tickUpper: getMaxTick(FeeAmount.MEDIUM),
|
|
|
|
amount0Desired: 300,
|
|
amount1Desired: 300,
|
|
amount0Min: 0,
|
|
amount1Min: 0,
|
|
deadline: 1,
|
|
recipient: wallet.address,
|
|
})
|
|
})
|
|
|
|
describe('10k of token0 fees collect', () => {
|
|
beforeEach('swap for ~10k of fees', async () => {
|
|
const swapAmount = 3_333_333
|
|
await tokens[0].approve(router.address, swapAmount)
|
|
await router.exactInput({
|
|
recipient: wallet.address,
|
|
deadline: 1,
|
|
path: encodePath([tokens[0].address, tokens[1].address], [FeeAmount.MEDIUM]),
|
|
amountIn: swapAmount,
|
|
amountOutMinimum: 0,
|
|
})
|
|
})
|
|
it('expected amounts', async () => {
|
|
const { amount0: nft1Amount0, amount1: nft1Amount1 } = await nft.callStatic.collect({
|
|
tokenId: 1,
|
|
recipient: wallet.address,
|
|
amount0Max: MaxUint128,
|
|
amount1Max: MaxUint128,
|
|
})
|
|
const { amount0: nft2Amount0, amount1: nft2Amount1 } = await nft.callStatic.collect({
|
|
tokenId: 2,
|
|
recipient: wallet.address,
|
|
amount0Max: MaxUint128,
|
|
amount1Max: MaxUint128,
|
|
})
|
|
expect(nft1Amount0).to.eq(2501)
|
|
expect(nft1Amount1).to.eq(0)
|
|
expect(nft2Amount0).to.eq(7503)
|
|
expect(nft2Amount1).to.eq(0)
|
|
})
|
|
|
|
it('actually collected', async () => {
|
|
const poolAddress = computePoolAddress(
|
|
factory.address,
|
|
[tokens[0].address, tokens[1].address],
|
|
FeeAmount.MEDIUM
|
|
)
|
|
|
|
await expect(
|
|
nft.collect({
|
|
tokenId: 1,
|
|
recipient: wallet.address,
|
|
amount0Max: MaxUint128,
|
|
amount1Max: MaxUint128,
|
|
})
|
|
)
|
|
.to.emit(tokens[0], 'Transfer')
|
|
.withArgs(poolAddress, wallet.address, 2501)
|
|
.to.not.emit(tokens[1], 'Transfer')
|
|
await expect(
|
|
nft.collect({
|
|
tokenId: 2,
|
|
recipient: wallet.address,
|
|
amount0Max: MaxUint128,
|
|
amount1Max: MaxUint128,
|
|
})
|
|
)
|
|
.to.emit(tokens[0], 'Transfer')
|
|
.withArgs(poolAddress, wallet.address, 7503)
|
|
.to.not.emit(tokens[1], 'Transfer')
|
|
})
|
|
})
|
|
})
|
|
})
|