UNPKG

@uniswap/universal-router

Version:

Smart contracts for Universal Router

1,281 lines (998 loc) 64.8 kB
import type { Contract } from '@ethersproject/contracts' import { expect } from './shared/expect' import { BigNumber } from 'ethers' import { UniversalRouter, INonfungiblePositionManager, PositionManager } from '../../typechain' import { abi as TOKEN_ABI } from '../../artifacts/solmate/src/tokens/ERC20.sol/ERC20.json' import { resetFork, WETH, DAI, USDC, V3_NFT_POSITION_MANAGER } from './shared/mainnetForkHelpers' import { abi as POOL_MANAGER_ABI } from '../../artifacts/@uniswap/v4-core/src/PoolManager.sol/PoolManager.json' import { ZERO_ADDRESS, ALICE_ADDRESS, MAX_UINT, MAX_UINT128, OPEN_DELTA, SOURCE_ROUTER, CONTRACT_BALANCE, } from './shared/constants' import { expandTo18DecimalsBN, expandTo6DecimalsBN } from './shared/helpers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import deployUniversalRouter from './shared/deployUniversalRouter' import { RoutePlanner, CommandType } from './shared/planner' import { V4Planner, Actions } from './shared/v4Planner' import hre from 'hardhat' import getPermitNFTSignature from './shared/getPermitNFTSignature' import getPermitV4Signature from './shared/getPermitV4Signature' import { ADDRESS_ZERO, FeeAmount } from '@uniswap/v3-sdk' import { encodeERC721Permit, encodeDecreaseLiquidity, encodeCollect, encodeBurn, encodeModifyLiquidities, encodeERC721PermitV4, } from './shared/encodeCall' import { executeRouter } from './shared/executeRouter' import { USDC_WETH, ETH_USDC } from './shared/v4Helpers' import { parseEvents } from './shared/parseEvents' const { ethers } = hre const poolManagerInterface = new ethers.utils.Interface(POOL_MANAGER_ABI) describe('V3 to V4 Migration Tests:', () => { let alice: SignerWithAddress let bob: SignerWithAddress let eve: SignerWithAddress let router: UniversalRouter let daiContract: Contract let wethContract: Contract let usdcContract: Contract let planner: RoutePlanner let v4Planner: V4Planner let v3NFTPositionManager: INonfungiblePositionManager let v4PositionManagerAddress: string let v4PositionManager: PositionManager let tokenIdv3: BigNumber beforeEach(async () => { await resetFork() await hre.network.provider.request({ method: 'hardhat_impersonateAccount', params: [ALICE_ADDRESS], }) alice = await ethers.getSigner(ALICE_ADDRESS) bob = (await ethers.getSigners())[1] eve = (await ethers.getSigners())[2] daiContract = new ethers.Contract(DAI.address, TOKEN_ABI, bob) wethContract = new ethers.Contract(WETH.address, TOKEN_ABI, bob) usdcContract = new ethers.Contract(USDC.address, TOKEN_ABI, bob) v3NFTPositionManager = V3_NFT_POSITION_MANAGER.connect(bob) as INonfungiblePositionManager router = (await deployUniversalRouter(bob.address)) as UniversalRouter v4PositionManagerAddress = await router.V4_POSITION_MANAGER() v4PositionManager = (await ethers.getContractAt('PositionManager', v4PositionManagerAddress)) as PositionManager planner = new RoutePlanner() v4Planner = new V4Planner() // alice gives bob some tokens await daiContract.connect(alice).transfer(bob.address, expandTo18DecimalsBN(100000)) await wethContract.connect(alice).transfer(bob.address, expandTo18DecimalsBN(100)) await usdcContract.connect(alice).transfer(bob.address, expandTo6DecimalsBN(100000)) }) describe('V3 Commands', () => { beforeEach(async () => { // Bob max-approves the v3PM to access his USDC and WETH await usdcContract.connect(bob).approve(v3NFTPositionManager.address, MAX_UINT) await wethContract.connect(bob).approve(v3NFTPositionManager.address, MAX_UINT) let bobUSDCBalanceBefore = await usdcContract.balanceOf(bob.address) let bobWETHBalanceBefore = await wethContract.balanceOf(bob.address) // need to mint the nft to bob const tx = await v3NFTPositionManager.mint({ token0: USDC.address, token1: WETH.address, fee: FeeAmount.LOW, tickLower: 0, tickUpper: 194980, amount0Desired: expandTo6DecimalsBN(2500), amount1Desired: expandTo18DecimalsBN(1), amount0Min: 0, amount1Min: 0, recipient: bob.address, deadline: MAX_UINT, }) let bobUSDCBalanceAfter = await usdcContract.balanceOf(bob.address) let bobWETHBalanceAfter = await wethContract.balanceOf(bob.address) let usdcSpent = bobUSDCBalanceBefore.sub(bobUSDCBalanceAfter) let wethSpent = bobWETHBalanceBefore.sub(bobWETHBalanceAfter) // check that the USDC and WETH were spent expect(usdcSpent > 0 || wethSpent > 0) const receipt = await tx.wait() const transferEvent = receipt.events?.find((event) => event.event === 'IncreaseLiquidity') tokenIdv3 = transferEvent?.args?.tokenId }) describe('erc721permit', () => { it('erc721 permit succeeds', async () => { const { v, r, s } = await getPermitNFTSignature(bob, v3NFTPositionManager, router.address, tokenIdv3, MAX_UINT) const erc721PermitParams = { spender: router.address, tokenId: tokenIdv3, deadline: MAX_UINT, v: v, r: r, s: s, } const encodedErc721PermitCall = encodeERC721Permit(erc721PermitParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedErc721PermitCall]) expect((await v3NFTPositionManager.positions(tokenIdv3)).operator).to.eq(ZERO_ADDRESS) // bob permits the router to spend token await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) expect((await v3NFTPositionManager.positions(tokenIdv3)).operator).to.eq(router.address) }) it('need to call permit when executing V3_POSITION_MANAGER_PERMIT command', async () => { let position = await v3NFTPositionManager.positions(tokenIdv3) let liquidity = position.liquidity const decreaseParams = { tokenId: tokenIdv3, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: MAX_UINT, } const encodedDecreaseCall = encodeDecreaseLiquidity(decreaseParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedDecreaseCall]) // trying to execute the permit commmand by calling decrease liquidity await expect( executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) ).to.be.revertedWithCustomError(router, 'InvalidAction') }) it('only owner of the token can generate a signature to permit another address', async () => { // eve is not the owner of the token const { v, r, s } = await getPermitNFTSignature(eve, v3NFTPositionManager, router.address, tokenIdv3, MAX_UINT) const erc721PermitParams = { spender: router.address, tokenId: tokenIdv3, deadline: MAX_UINT, v: v, r: r, s: s, } const encodedErc721PermitCall = encodeERC721Permit(erc721PermitParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedErc721PermitCall]) // eve generated a signature for bob's token - fails since eve is not the owner await expect( executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) ).to.be.revertedWithCustomError(router, 'ExecutionFailed') }) it('other address can call permit on behalf of someone as long as owner of the token generated the signature properly', async () => { const { v, r, s } = await getPermitNFTSignature(bob, v3NFTPositionManager, router.address, tokenIdv3, MAX_UINT) const erc721PermitParams = { spender: router.address, tokenId: tokenIdv3, deadline: MAX_UINT, v: v, r: r, s: s, } const encodedErc721PermitCall = encodeERC721Permit(erc721PermitParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedErc721PermitCall]) expect((await v3NFTPositionManager.positions(tokenIdv3)).operator).to.eq(ZERO_ADDRESS) // eve can permit the router for bob using bob's signature await executeRouter(planner, eve, router, wethContract, daiContract, usdcContract) expect((await v3NFTPositionManager.positions(tokenIdv3)).operator).to.eq(router.address) }) }) describe('decrease liquidity', () => { it('decrease liquidity succeeds', async () => { // first we need to permit the router to spend the nft const { v, r, s } = await getPermitNFTSignature(bob, v3NFTPositionManager, router.address, tokenIdv3, MAX_UINT) const erc721PermitParams = { spender: router.address, tokenId: tokenIdv3, deadline: MAX_UINT, v: v, r: r, s: s, } const encodedErc721PermitCall = encodeERC721Permit(erc721PermitParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedErc721PermitCall]) let position = await v3NFTPositionManager.positions(tokenIdv3) let liquidity = position.liquidity expect(liquidity).to.be.gt(0) let owed0Before = position.tokensOwed0 let owed1Before = position.tokensOwed1 const decreaseParams = { tokenId: tokenIdv3, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: MAX_UINT, } const encodedDecreaseCall = encodeDecreaseLiquidity(decreaseParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedDecreaseCall]) await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) position = await v3NFTPositionManager.positions(tokenIdv3) liquidity = position.liquidity let owed0After = position.tokensOwed0 let owed1After = position.tokensOwed1 expect(liquidity).to.eq(0) expect(owed0After).to.be.gt(owed0Before) expect(owed1After).to.be.gt(owed1Before) }) it('cannot decrease liquidity without permiting the router', async () => { let position = await v3NFTPositionManager.positions(tokenIdv3) let liquidity = position.liquidity const decreaseParams = { tokenId: tokenIdv3, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: MAX_UINT, } const encodedDecreaseCall = encodeDecreaseLiquidity(decreaseParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedDecreaseCall]) await expect( executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) ).to.be.revertedWithCustomError(router, 'ExecutionFailed') }) it('cannot call decrease liquidity with improper function selector', async () => { // first we need to permit the router to spend the nft const { v, r, s } = await getPermitNFTSignature(bob, v3NFTPositionManager, router.address, tokenIdv3, MAX_UINT) const erc721PermitParams = { spender: router.address, tokenId: tokenIdv3, deadline: MAX_UINT, v: v, r: r, s: s, } const encodedErc721PermitCall = encodeERC721Permit(erc721PermitParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedErc721PermitCall]) let position = await v3NFTPositionManager.positions(tokenIdv3) let liquidity = position.liquidity const BAD_DECREASE_LIQUIDITY_STRUCT = '(uint256 tokenId,uint256 liquidity,uint256 amount0Min,uint256 amount1Min,uint256 deadline)' const decreaseParams = { tokenId: tokenIdv3, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: MAX_UINT, } const abi = new ethers.utils.AbiCoder() const encodedParams = abi.encode([BAD_DECREASE_LIQUIDITY_STRUCT], [decreaseParams]) const functionSignature = ethers.utils .id('decreaseLiquidity((uint256,uint128,uint256,uint256))') .substring(0, 10) const encodedCall = functionSignature + encodedParams.substring(2) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedCall]) await expect( executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) ).to.be.revertedWithCustomError(router, 'InvalidAction') }) it('fails if decrease liquidity call fails', async () => { // first we need to permit the router to spend the nft const { v, r, s } = await getPermitNFTSignature(bob, v3NFTPositionManager, router.address, tokenIdv3, MAX_UINT) const erc721PermitParams = { spender: router.address, tokenId: tokenIdv3, deadline: MAX_UINT, v: v, r: r, s: s, } const encodedErc721PermitCall = encodeERC721Permit(erc721PermitParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedErc721PermitCall]) let position = await v3NFTPositionManager.positions(tokenIdv3) let liquidity = position.liquidity // set the deadline to 0 const decreaseParams = { tokenId: tokenIdv3, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: '0' } const encodedDecreaseCall = encodeDecreaseLiquidity(decreaseParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedDecreaseCall]) // call to decrease liquidity fails since the deadline is set to 0 await expect( executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) ).to.be.revertedWithCustomError(router, 'ExecutionFailed') }) it('cannot call decrease liquidity if not authorized', async () => { // bob creates a signature for the router to spend the token const { v, r, s } = await getPermitNFTSignature(bob, v3NFTPositionManager, router.address, tokenIdv3, MAX_UINT) const erc721PermitParams = { spender: router.address, tokenId: tokenIdv3, deadline: MAX_UINT, v: v, r: r, s: s, } const encodedErc721PermitCall = encodeERC721Permit(erc721PermitParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedErc721PermitCall]) await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) planner = new RoutePlanner() // transfer the token to eve await v3NFTPositionManager.transferFrom(bob.address, eve.address, tokenIdv3) let position = await v3NFTPositionManager.positions(tokenIdv3) let liquidity = position.liquidity const decreaseParams = { tokenId: tokenIdv3, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: MAX_UINT, } const encodedDecreaseCall = encodeDecreaseLiquidity(decreaseParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedDecreaseCall]) // bob is trying to use the token that is now owned by eve. he is not authorized to do so await expect( executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) ).to.be.revertedWithCustomError(router, 'NotAuthorizedForToken') }) it('eve permits bob for all tokens - he can call decrease even though he is not the owner', async () => { // transfer the token to eve await v3NFTPositionManager.transferFrom(bob.address, eve.address, tokenIdv3) // eve permits bob to spend all of her tokens await v3NFTPositionManager.connect(eve).setApprovalForAll(bob.address, true) // eve creates a signature for the router to spend the token let { v, r, s } = await getPermitNFTSignature(eve, v3NFTPositionManager, router.address, tokenIdv3, MAX_UINT) const erc721PermitParams = { spender: router.address, tokenId: tokenIdv3, deadline: MAX_UINT, v: v, r: r, s: s, } const encodedErc721PermitCall = encodeERC721Permit(erc721PermitParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedErc721PermitCall]) let position = await v3NFTPositionManager.positions(tokenIdv3) let liquidity = position.liquidity const params = { tokenId: tokenIdv3, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: MAX_UINT } const encodedDecreaseCall = encodeDecreaseLiquidity(params) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedDecreaseCall]) await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) }) it('eve permits bob for the token and approves router for all her tokens - he can call decrease even though he is not the owner', async () => { // transfer the token to eve await v3NFTPositionManager.transferFrom(bob.address, eve.address, tokenIdv3) // eve approves the router to spend all of her tokens await v3NFTPositionManager.connect(eve).setApprovalForAll(router.address, true) // eve creates a signature for bob to spend the token let { v, r, s } = await getPermitNFTSignature(eve, v3NFTPositionManager, bob.address, tokenIdv3, MAX_UINT) const erc721PermitParams = { spender: bob.address, tokenId: tokenIdv3, deadline: MAX_UINT, v: v, r: r, s: s, } const encodedErc721PermitCall = encodeERC721Permit(erc721PermitParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedErc721PermitCall]) let position = await v3NFTPositionManager.positions(tokenIdv3) let liquidity = position.liquidity const params = { tokenId: tokenIdv3, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: MAX_UINT } const encodedDecreaseCall = encodeDecreaseLiquidity(params) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedDecreaseCall]) await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) }) }) describe('collect liquidity', () => { it('collect succeeds', async () => { let bobToken0BalanceBefore = await usdcContract.balanceOf(bob.address) let bobToken1BalanceBefore = await wethContract.balanceOf(bob.address) // first we need to permit the router to spend the nft let { v, r, s } = await getPermitNFTSignature(bob, v3NFTPositionManager, router.address, tokenIdv3, MAX_UINT) const erc721PermitParams = { spender: router.address, tokenId: tokenIdv3, deadline: MAX_UINT, v: v, r: r, s: s, } const encodedErc721PermitCall = encodeERC721Permit(erc721PermitParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedErc721PermitCall]) let position = await v3NFTPositionManager.positions(tokenIdv3) let liquidity = position.liquidity const decreaseParams = { tokenId: tokenIdv3, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: MAX_UINT, } const encodedDecreaseCall = encodeDecreaseLiquidity(decreaseParams) const collectParams = { tokenId: tokenIdv3, recipient: bob.address, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128, } const encodedCollectCall = encodeCollect(collectParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedDecreaseCall]) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedCollectCall]) await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) position = await v3NFTPositionManager.positions(tokenIdv3) let owed0 = position.tokensOwed0 let owed1 = position.tokensOwed1 expect(owed0).to.eq(0) expect(owed1).to.eq(0) let bobToken0BalanceAfter = await usdcContract.balanceOf(bob.address) let bobToken1BalanceAfter = await wethContract.balanceOf(bob.address) // bob is the recipient - he should have received the owed tokens expect(bobToken0BalanceAfter).to.be.gt(bobToken0BalanceBefore) expect(bobToken1BalanceAfter).to.be.gt(bobToken1BalanceBefore) }) it('collecting the correct amount', async () => { // first we need to permit the router to spend the nft let { v, r, s } = await getPermitNFTSignature(bob, v3NFTPositionManager, router.address, tokenIdv3, MAX_UINT) const erc721PermitParams = { spender: router.address, tokenId: tokenIdv3, deadline: MAX_UINT, v: v, r: r, s: s, } const encodedErc721PermitCall = encodeERC721Permit(erc721PermitParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedErc721PermitCall]) let position = await v3NFTPositionManager.positions(tokenIdv3) let liquidity = position.liquidity const decreaseParams = { tokenId: tokenIdv3, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: MAX_UINT, } const encodedDecreaseCall = encodeDecreaseLiquidity(decreaseParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedDecreaseCall]) await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) let bobToken0BalanceBefore: BigNumber = await usdcContract.balanceOf(bob.address) let bobToken1BalanceBefore: BigNumber = await wethContract.balanceOf(bob.address) position = await v3NFTPositionManager.positions(tokenIdv3) let owed0Before = position.tokensOwed0 let owed1Before = position.tokensOwed1 let liquidityBefore = position.liquidity expect(liquidityBefore).to.be.eq(0) planner = new RoutePlanner() const collectParams = { tokenId: tokenIdv3, recipient: bob.address, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128, } const encodedCollectCall = encodeCollect(collectParams) await v3NFTPositionManager.setApprovalForAll(eve.address, true) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedCollectCall]) await executeRouter(planner, eve, router, wethContract, daiContract, usdcContract) position = await v3NFTPositionManager.positions(tokenIdv3) let owed0After = position.tokensOwed0 let owed1After = position.tokensOwed1 expect(owed0After).to.eq(0) expect(owed1After).to.eq(0) let bobToken0BalanceAfter: BigNumber = await usdcContract.balanceOf(bob.address) let bobToken1BalanceAfter: BigNumber = await wethContract.balanceOf(bob.address) // bob is the recipient - he should have received the owed tokens expect(bobToken0BalanceAfter.sub(bobToken0BalanceBefore)).to.be.eq(owed0Before) expect(bobToken1BalanceAfter.sub(bobToken1BalanceBefore)).to.be.eq(owed1Before) }) it('collect succeeds with router as recipient', async () => { let routerToken0BalanceBefore = await usdcContract.balanceOf(router.address) let routerToken1BalanceBefore = await wethContract.balanceOf(router.address) // router should have no balance of the tokens expect(routerToken0BalanceBefore).to.be.eq(0) expect(routerToken1BalanceBefore).to.be.eq(0) // first we need to permit the router to spend the nft let { v, r, s } = await getPermitNFTSignature(bob, v3NFTPositionManager, router.address, tokenIdv3, MAX_UINT) const erc721PermitParams = { spender: router.address, tokenId: tokenIdv3, deadline: MAX_UINT, v: v, r: r, s: s, } const encodedErc721PermitCall = encodeERC721Permit(erc721PermitParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedErc721PermitCall]) let position = await v3NFTPositionManager.positions(tokenIdv3) let liquidity = position.liquidity const decreaseParams = { tokenId: tokenIdv3, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: MAX_UINT, } const encodedDecreaseCall = encodeDecreaseLiquidity(decreaseParams) const collectParams = { tokenId: tokenIdv3, recipient: router.address, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128, } const encodedCollectCall = encodeCollect(collectParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedDecreaseCall]) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedCollectCall]) await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) let routerToken0BalanceAfter = await usdcContract.balanceOf(router.address) let routerToken1BalanceAfter = await wethContract.balanceOf(router.address) // router is the recipient - router should have received the owed tokens // (there is sweep function if necessary) expect(routerToken0BalanceAfter).to.be.gt(routerToken0BalanceBefore) expect(routerToken1BalanceAfter).to.be.gt(routerToken1BalanceBefore) }) it('cannot call collect with improper signature', async () => { // first we need to permit the router to spend the nft let { v, r, s } = await getPermitNFTSignature(bob, v3NFTPositionManager, router.address, tokenIdv3, MAX_UINT) const erc721PermitParams = { spender: router.address, tokenId: tokenIdv3, deadline: MAX_UINT, v: v, r: r, s: s, } const encodedErc721PermitCall = encodeERC721Permit(erc721PermitParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedErc721PermitCall]) let position = await v3NFTPositionManager.positions(tokenIdv3) let liquidity = position.liquidity const decreaseParams = { tokenId: tokenIdv3, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: MAX_UINT, } const encodedDecreaseCall = encodeDecreaseLiquidity(decreaseParams) const COLLECT_STRUCT = '(uint256 tokenId,address recipient,uint256 amount0Max,uint256 amount1Max)' const collectParams = { tokenId: tokenIdv3, recipient: bob.address, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128, } const abi = new ethers.utils.AbiCoder() const encodedCollectParams = abi.encode([COLLECT_STRUCT], [collectParams]) const functionSignatureCollect = ethers.utils.id('collect((uint256,address,uint128))').substring(0, 10) const encodedCollectCall = functionSignatureCollect + encodedCollectParams.substring(2) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedDecreaseCall]) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedCollectCall]) await expect( executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) ).to.be.revertedWithCustomError(router, 'InvalidAction') }) it('cannot call collect with improper params', async () => { // first we need to permit the router to spend the nft let { v, r, s } = await getPermitNFTSignature(bob, v3NFTPositionManager, router.address, tokenIdv3, MAX_UINT) const erc721PermitParams = { spender: router.address, tokenId: tokenIdv3, deadline: MAX_UINT, v: v, r: r, s: s, } const encodedErc721PermitCall = encodeERC721Permit(erc721PermitParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedErc721PermitCall]) let position = await v3NFTPositionManager.positions(tokenIdv3) let liquidity = position.liquidity const decreaseParams = { tokenId: tokenIdv3, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: MAX_UINT, } const encodedDecreaseCall = encodeDecreaseLiquidity(decreaseParams) const COLLECT_STRUCT = '(uint256 tokenId,address recipient,uint256 amount0Max)' const collectParams = { tokenId: tokenIdv3, recipient: bob.address, amount0Max: MAX_UINT128 } const abi = new ethers.utils.AbiCoder() const encodedCollectParams = abi.encode([COLLECT_STRUCT], [collectParams]) const functionSignatureCollect = ethers.utils.id('collect((uint256,address,uint128,uint128))').substring(0, 10) const encodedCollectCall = functionSignatureCollect + encodedCollectParams.substring(2) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedDecreaseCall]) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedCollectCall]) await expect( executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) ).to.be.revertedWithCustomError(router, 'ExecutionFailed') }) it('cannot call collect if the router is not approved for that tokenid', async () => { // first we need to permit the router to spend the nft let { v, r, s } = await getPermitNFTSignature(bob, v3NFTPositionManager, router.address, tokenIdv3, MAX_UINT) const erc721PermitParams = { spender: router.address, tokenId: tokenIdv3, deadline: MAX_UINT, v: v, r: r, s: s, } const encodedErc721PermitCall = encodeERC721Permit(erc721PermitParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedErc721PermitCall]) await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) planner = new RoutePlanner() let position = await v3NFTPositionManager.positions(tokenIdv3) let liquidity = position.liquidity // approved on the decrease call const decreaseParams = { tokenId: tokenIdv3, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: MAX_UINT, } const encodedDecreaseCall = encodeDecreaseLiquidity(decreaseParams) // not approved on the collect call const collectParams = { tokenId: BigNumber.from(1), recipient: bob.address, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128, } const encodedCollectCall = encodeCollect(collectParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedDecreaseCall]) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedCollectCall]) await expect( executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) ).to.be.revertedWithCustomError(router, 'NotAuthorizedForToken') }) it('address cannot call collect if unapproved for that tokenid', async () => { // first we need to permit the router to spend the nft let { v, r, s } = await getPermitNFTSignature(bob, v3NFTPositionManager, router.address, tokenIdv3, MAX_UINT) const erc721PermitParams = { spender: router.address, tokenId: tokenIdv3, deadline: MAX_UINT, v: v, r: r, s: s, } const encodedErc721PermitCall = encodeERC721Permit(erc721PermitParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedErc721PermitCall]) await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) planner = new RoutePlanner() let position = await v3NFTPositionManager.positions(tokenIdv3) let liquidity = position.liquidity // approved on the decrease call const decreaseParams = { tokenId: tokenIdv3, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: MAX_UINT, } const encodedDecreaseCall = encodeDecreaseLiquidity(decreaseParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedDecreaseCall]) await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) const collectParams = { tokenId: tokenIdv3, recipient: eve.address, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128, } const encodedCollectCall = encodeCollect(collectParams) planner = new RoutePlanner() planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedCollectCall]) // not approved on the collect call await expect( executeRouter(planner, eve, router, wethContract, daiContract, usdcContract) ).to.be.revertedWithCustomError(router, 'NotAuthorizedForToken') }) }) describe('burn liquidity', () => { it('burn succeeds', async () => { // first we need to permit the router to spend the nft let { v, r, s } = await getPermitNFTSignature(bob, v3NFTPositionManager, router.address, tokenIdv3, MAX_UINT) const erc721PermitParams = { spender: router.address, tokenId: tokenIdv3, deadline: MAX_UINT, v: v, r: r, s: s, } const encodedErc721PermitCall = encodeERC721Permit(erc721PermitParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedErc721PermitCall]) let position = await v3NFTPositionManager.positions(tokenIdv3) let liquidity = position.liquidity const decreaseParams = { tokenId: tokenIdv3, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: MAX_UINT, } const encodedDecreaseCall = encodeDecreaseLiquidity(decreaseParams) const collectParams = { tokenId: tokenIdv3, recipient: bob.address, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128, } const encodedCollectCall = encodeCollect(collectParams) const encodedBurnCall = encodeBurn(tokenIdv3) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedDecreaseCall]) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedCollectCall]) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedBurnCall]) await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) expect(await v3NFTPositionManager.balanceOf(bob.address)).to.eq(0) }) it('burn fails if you arent approved spender of nft', async () => { // first we need to permit the router to spend the nft let { v, r, s } = await getPermitNFTSignature(bob, v3NFTPositionManager, router.address, tokenIdv3, MAX_UINT) const erc721PermitParams = { spender: router.address, tokenId: tokenIdv3, deadline: MAX_UINT, v: v, r: r, s: s, } const encodedErc721PermitCall = encodeERC721Permit(erc721PermitParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [encodedErc721PermitCall]) let position = await v3NFTPositionManager.positions(tokenIdv3) let liquidity = position.liquidity const decreaseParams = { tokenId: tokenIdv3, liquidity: liquidity, amount0Min: 0, amount1Min: 0, deadline: MAX_UINT, } const encodedDecreaseCall = encodeDecreaseLiquidity(decreaseParams) const collectParams = { tokenId: tokenIdv3, recipient: bob.address, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128, } const encodedCollectCall = encodeCollect(collectParams) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedDecreaseCall]) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedCollectCall]) // bob decreases and collects the liquidity await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) planner = new RoutePlanner() const encodedBurnCall = encodeBurn(tokenIdv3) planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [encodedBurnCall]) // eve tries to burn the token - she is not approved to do so await expect( executeRouter(planner, eve, router, wethContract, daiContract, usdcContract) ).to.be.revertedWithCustomError(router, 'NotAuthorizedForToken') }) }) }) describe('V4 Commands', () => { beforeEach(async () => { // initialize new pool on v4 planner.addCommand(CommandType.V4_INITIALIZE_POOL, [USDC_WETH.poolKey, USDC_WETH.price]) await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) planner = new RoutePlanner() }) it('initializes a pool', async () => { const poolKey = { currency0: USDC.address, currency1: WETH.address, fee: FeeAmount.HIGH, // to make it different to USDC_WETH.poolKey tickSpacing: 10, hooks: '0x0000000000000000000000000000000000000000', } planner.addCommand(CommandType.V4_INITIALIZE_POOL, [poolKey, USDC_WETH.price]) let tx = await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) // check that an initialize event was emitted on the pool manager let receipt = tx.receipt let txEvents = parseEvents(poolManagerInterface, receipt) const { name } = txEvents[0]! expect(name).to.eq('Initialize') const { currency0, currency1, sqrtPriceX96 } = txEvents[0]!.args expect(currency0).to.eq(USDC_WETH.poolKey.currency0) expect(currency1).to.eq(USDC_WETH.poolKey.currency1) expect(sqrtPriceX96).to.eq(USDC_WETH.price) }) it('mint v4 succeeds', async () => { // transfer to v4posm await usdcContract.connect(bob).transfer(v4PositionManager.address, expandTo6DecimalsBN(100000)) await wethContract.connect(bob).transfer(v4PositionManager.address, expandTo18DecimalsBN(100)) v4Planner.addAction(Actions.MINT_POSITION, [ USDC_WETH.poolKey, USDC_WETH.tickLower, USDC_WETH.tickUpper, '6000000', MAX_UINT128, MAX_UINT128, bob.address, '0x', ]) v4Planner.addAction(Actions.SETTLE, [USDC.address, OPEN_DELTA, SOURCE_ROUTER]) v4Planner.addAction(Actions.SETTLE, [WETH.address, OPEN_DELTA, SOURCE_ROUTER]) v4Planner.addAction(Actions.SWEEP, [USDC.address, bob.address]) v4Planner.addAction(Actions.SWEEP, [WETH.address, bob.address]) const calldata = encodeModifyLiquidities({ unlockData: v4Planner.finalize(), deadline: MAX_UINT }) planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [calldata]) // assert that the posm holds tokens before executeRouter expect(await usdcContract.balanceOf(v4PositionManager.address)).to.eq(expandTo6DecimalsBN(100000)) expect(await wethContract.balanceOf(v4PositionManager.address)).to.eq(expandTo18DecimalsBN(100)) // bob does not own a position expect(await v4PositionManager.balanceOf(bob.address)).to.eq(0) let expectedTokenId = await v4PositionManager.nextTokenId() let bobUSDCBalanceBefore = await usdcContract.balanceOf(bob.address) let bobWETHBalanceBefore = await wethContract.balanceOf(bob.address) await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) // bob successfully sweeped his usdc and weth from the v4 position manager expect(await wethContract.balanceOf(v4PositionManager.address)).to.eq(0) expect(await usdcContract.balanceOf(v4PositionManager.address)).to.eq(0) expect(await usdcContract.balanceOf(bob.address)).to.be.gt(bobUSDCBalanceBefore) expect(await wethContract.balanceOf(bob.address)).to.be.gt(bobWETHBalanceBefore) // bob owns a position expect(await v4PositionManager.balanceOf(bob.address)).to.eq(1) expect(await v4PositionManager.ownerOf(expectedTokenId)).to.eq(bob.address) }) it('an address can mint on behalf of another address', async () => { // transfer to v4posm await usdcContract.connect(bob).transfer(v4PositionManager.address, expandTo6DecimalsBN(100000)) await wethContract.connect(bob).transfer(v4PositionManager.address, expandTo18DecimalsBN(100)) // mint params for bob v4Planner.addAction(Actions.MINT_POSITION, [ USDC_WETH.poolKey, USDC_WETH.tickLower, USDC_WETH.tickUpper, '6000000', MAX_UINT128, MAX_UINT128, bob.address, '0x', ]) v4Planner.addAction(Actions.SETTLE, [USDC.address, OPEN_DELTA, SOURCE_ROUTER]) v4Planner.addAction(Actions.SETTLE, [WETH.address, OPEN_DELTA, SOURCE_ROUTER]) v4Planner.addAction(Actions.SWEEP, [USDC.address, bob.address]) v4Planner.addAction(Actions.SWEEP, [WETH.address, bob.address]) const calldata = encodeModifyLiquidities({ unlockData: v4Planner.finalize(), deadline: MAX_UINT }) planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [calldata]) // bob does not own a position expect(await v4PositionManager.balanceOf(bob.address)).to.eq(0) // alice does not own a position expect(await v4PositionManager.balanceOf(alice.address)).to.eq(0) let expectedTokenId = await v4PositionManager.nextTokenId() // alice mints a position for bob await executeRouter(planner, alice, router, wethContract, daiContract, usdcContract) // bob owns a position expect(await v4PositionManager.balanceOf(bob.address)).to.eq(1) // alice does not own a position expect(await v4PositionManager.balanceOf(alice.address)).to.eq(0) expect(await v4PositionManager.ownerOf(expectedTokenId)).to.eq(bob.address) }) it('erc721 permit on v4 fails with invalid selector', async () => { // transfer to v4posm await usdcContract.connect(bob).transfer(v4PositionManager.address, expandTo6DecimalsBN(100000)) await wethContract.connect(bob).transfer(v4PositionManager.address, expandTo18DecimalsBN(100)) // mint position first v4Planner.addAction(Actions.MINT_POSITION, [ USDC_WETH.poolKey, USDC_WETH.tickLower, USDC_WETH.tickUpper, '6000000', MAX_UINT128, MAX_UINT128, bob.address, '0x', ]) v4Planner.addAction(Actions.SETTLE, [USDC.address, OPEN_DELTA, SOURCE_ROUTER]) v4Planner.addAction(Actions.SETTLE, [WETH.address, OPEN_DELTA, SOURCE_ROUTER]) v4Planner.addAction(Actions.SWEEP, [USDC.address, bob.address]) v4Planner.addAction(Actions.SWEEP, [WETH.address, bob.address]) let calldata = encodeModifyLiquidities({ unlockData: v4Planner.finalize(), deadline: MAX_UINT }) planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [calldata]) let expectedTokenId = await v4PositionManager.nextTokenId() // router is not approved to spend the token expect(await v4PositionManager.getApproved(expectedTokenId)).to.eq(ZERO_ADDRESS) const { compact } = await getPermitV4Signature( bob, v4PositionManager, router.address, expectedTokenId, MAX_UINT, { nonce: 1 } ) const erc721PermitParams = { spender: router.address, tokenId: expectedTokenId, deadline: MAX_UINT, nonce: 1, signature: compact, } const encodedErc721PermitCall = encodeERC721PermitV4(erc721PermitParams) planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [encodedErc721PermitCall]) await expect( executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) ).to.be.revertedWithCustomError(router, 'InvalidAction') }) it('increase v4 fails', async () => { // transfer to v4posm await usdcContract.connect(bob).transfer(v4PositionManager.address, expandTo6DecimalsBN(100000)) await wethContract.connect(bob).transfer(v4PositionManager.address, expandTo18DecimalsBN(100)) // mint position first v4Planner.addAction(Actions.MINT_POSITION, [ USDC_WETH.poolKey, USDC_WETH.tickLower, USDC_WETH.tickUpper, '6000000', MAX_UINT128, MAX_UINT128, bob.address, '0x', ]) v4Planner.addAction(Actions.SETTLE, [USDC.address, OPEN_DELTA, SOURCE_ROUTER]) v4Planner.addAction(Actions.SETTLE, [WETH.address, OPEN_DELTA, SOURCE_ROUTER]) v4Planner.addAction(Actions.SWEEP, [USDC.address, bob.address]) v4Planner.addAction(Actions.SWEEP, [WETH.address, bob.address]) let calldata = encodeModifyLiquidities({ unlockData: v4Planner.finalize(), deadline: MAX_UINT }) planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [calldata]) let expectedTokenId = await v4PositionManager.nextTokenId() await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) // bob owns a position expect(await v4PositionManager.balanceOf(bob.address)).to.eq(1) // increase position second planner = new RoutePlanner() v4Planner = new V4Planner() await usdcContract.connect(bob).transfer(v4PositionManager.address, expandTo6DecimalsBN(10000)) await wethContract.connect(bob).transfer(v4PositionManager.address, expandTo18DecimalsBN(10)) v4Planner.addAction(Actions.INCREASE_LIQUIDITY, [expectedTokenId, '6000000', MAX_UINT128, MAX_UINT128, '0x']) v4Planner.addAction(Actions.SETTLE, [USDC.address, OPEN_DELTA, SOURCE_ROUTER]) v4Planner.addAction(Actions.SETTLE, [WETH.address, OPEN_DELTA, SOURCE_ROUTER]) v4Planner.addAction(Actions.SWEEP, [USDC.address, bob.address]) v4Planner.addAction(Actions.SWEEP, [WETH.address, bob.address]) calldata = encodeModifyLiquidities({ unlockData: v4Planner.finalize(), deadline: MAX_UINT }) planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [calldata]) // assert that the posm holds tokens before executeRouter expect(await usdcContract.balanceOf(v4PositionManager.address)).to.eq(expandTo6DecimalsBN(10000)) expect(await wethContract.balanceOf(v4PositionManager.address)).to.eq(expandTo18DecimalsBN(10)) await expect( executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) ).to.be.revertedWithCustomError(router, 'OnlyMintAllowed') }) it('decrease v4 does not succeed', async () => { // first mint the v4 nft // transfer to v4posm await usdcContract.connect(bob).transfer(v4PositionManager.address, expandTo6DecimalsBN(100000)) await wethContract.connect(bob).transfer(v4PositionManager.address, expandTo18DecimalsBN(100)) v4Planner.addAction(Actions.MINT_POSITION, [ USDC_WETH.poolKey, USDC_WETH.tickLower, USDC_WETH.tickUpper, '6000000', MAX_UINT128, MAX_UINT128, bob.address, '0x', ]) v4Planner.addAction(Actions.SETTLE, [USDC.address, OPEN_DELTA, SOURCE_ROUTER]) v4Planner.addAction(Actions.SETTLE, [WETH.address, OPEN_DELTA, SOURCE_ROUTER]) v4Planner.addAction(Actions.SWEEP, [USDC.address, bob.address]) v4Planner.addAction(Actions.SWEEP, [WETH.address, bob.address]) let calldata = encodeModifyLiquidities({ unlockData: v4Planner.finalize(), deadline: MAX_UINT }) planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [calldata]) let expectedTokenId = await v4PositionManager.nextTokenId() await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) // try to decrease the position second planner = new RoutePlanner() v4Planner = new V4Planner() v4Planner.addAction(Actions.DECREASE_LIQUIDITY, [expectedTokenId, '6000000', 0, 0, '0x']) v4Planner.addAction(Actions.CLOSE_CURRENCY, [USDC.address]) v4Planner.addAction(Actions.CLOSE_CURRENCY, [WETH.address]) calldata = encodeModifyLiquidities({ unlockData: v4Planner.finalize(), deadline: MAX_UINT }) planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [calldata]) await expect( executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) ).to.be.revertedWithCustomError(router, 'OnlyMintAllowed') }) it('burn v4 does not succeed', async () => { // first mint the v4 nft // transfer to v4posm await usdcContract.connect(bob).transfer(v4PositionManager.address, expandTo6DecimalsBN(100000)) await wethContract.connect(bob).transfer(v4PositionManager.address, expandTo18DecimalsBN(100)) v4Planner.addAction(Actions.MINT_POSITION, [ USDC_WETH.poolKey, USDC_WETH.tickLower, USDC_WETH.tickUpper, '6000000', MAX_UINT128, MAX_UINT128, bob.address, '0x', ]) v4Planner.addAction(Actions.SETTLE, [USDC.address, OPEN_DELTA, SOURCE_ROUTER]) v4Planner.addAction(Actions.SETTLE, [WETH.address, OPEN_DELTA, SOURCE_ROUTER]) v4Planner.addAction(Actions.SWEEP, [USDC.address, bob.address])