diff --git a/contracts/base/Dispatcher.sol b/contracts/base/Dispatcher.sol index 37730d60..80618855 100644 --- a/contracts/base/Dispatcher.sol +++ b/contracts/base/Dispatcher.sol @@ -15,6 +15,8 @@ import {IAllowanceTransfer} from 'permit2/src/interfaces/IAllowanceTransfer.sol' import {IERC721Permit} from '@uniswap/v3-periphery/contracts/interfaces/IERC721Permit.sol'; import {ActionConstants} from '@uniswap/v4-periphery/src/libraries/ActionConstants.sol'; import {CalldataDecoder} from '@uniswap/v4-periphery/src/libraries/CalldataDecoder.sol'; +import {PoolKey} from '@uniswap/v4-core/src/types/PoolKey.sol'; +import {IPoolManager} from '@uniswap/v4-core/src/interfaces/IPoolManager.sol'; /// @title Decodes and Executes Commands /// @notice Called by the UniversalRouter contract to efficiently decode and execute a singular command @@ -24,8 +26,6 @@ abstract contract Dispatcher is Payments, V2SwapRouter, V3SwapRouter, V4SwapRout error InvalidCommandType(uint256 commandType); error BalanceTooLow(); - error InvalidAction(bytes4 action); - error NotAuthorizedForToken(uint256 tokenId); /// @notice Executes encoded commands along with provided inputs. /// @param commands A set of concatenated commands, each 1 byte in length @@ -269,12 +269,21 @@ abstract contract Dispatcher is Payments, V2SwapRouter, V3SwapRouter, V4SwapRout } (success, output) = address(V3_POSITION_MANAGER).call(inputs); - } else if (command == Commands.V4_POSITION_CALL) { + } else if (command == Commands.V4_INITIALIZE_POOL) { + PoolKey calldata poolKey; + uint160 sqrtPriceX96; + assembly { + poolKey := inputs.offset + sqrtPriceX96 := calldataload(add(inputs.offset, 0xa0)) + } + (success, output) = + address(poolManager).call(abi.encodeCall(IPoolManager.initialize, (poolKey, sqrtPriceX96))); + } else if (command == Commands.V4_POSITION_MANAGER_CALL) { // should only call modifyLiquidities() to mint - // do not permit or approve this contract over a v4 position or someone could use this command to decrease, burn, or transfer your position + _checkV4PositionManagerCall(inputs); (success, output) = address(V4_POSITION_MANAGER).call{value: address(this).balance}(inputs); } else { - // placeholder area for commands 0x13-0x20 + // placeholder area for commands 0x15-0x20 revert InvalidCommandType(command); } } diff --git a/contracts/libraries/Commands.sol b/contracts/libraries/Commands.sol index 55277f16..416a4ceb 100644 --- a/contracts/libraries/Commands.sol +++ b/contracts/libraries/Commands.sol @@ -35,8 +35,9 @@ library Commands { uint256 constant V4_SWAP = 0x10; uint256 constant V3_POSITION_MANAGER_PERMIT = 0x11; uint256 constant V3_POSITION_MANAGER_CALL = 0x12; - uint256 constant V4_POSITION_CALL = 0x13; - // COMMAND_PLACEHOLDER = 0x14 -> 0x20 + uint256 constant V4_INITIALIZE_POOL = 0x13; + uint256 constant V4_POSITION_MANAGER_CALL = 0x14; + // COMMAND_PLACEHOLDER = 0x15 -> 0x20 // Command Types where 0x21<=value<=0x3f uint256 constant EXECUTE_SUB_PLAN = 0x21; diff --git a/contracts/modules/MigratorImmutables.sol b/contracts/modules/MigratorImmutables.sol index e13fc4dc..0874058f 100644 --- a/contracts/modules/MigratorImmutables.sol +++ b/contracts/modules/MigratorImmutables.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.24; import {INonfungiblePositionManager} from '@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol'; import {IPositionManager} from '@uniswap/v4-periphery/src/interfaces/IPositionManager.sol'; +import {IPoolManager} from '@uniswap/v4-core/src/interfaces/IPoolManager.sol'; struct MigratorParameters { address v3PositionManager; @@ -12,9 +13,9 @@ struct MigratorParameters { /// @title Migrator Immutables /// @notice Immutable state for liquidity-migration contracts contract MigratorImmutables { - /// @notice v3PositionManager address + /// @notice v3 PositionManager address INonfungiblePositionManager public immutable V3_POSITION_MANAGER; - /// @notice v4PositionManager address + /// @notice v4 PositionManager address IPositionManager public immutable V4_POSITION_MANAGER; constructor(MigratorParameters memory params) { diff --git a/contracts/modules/Payments.sol b/contracts/modules/Payments.sol index e0630203..022e7cab 100644 --- a/contracts/modules/Payments.sol +++ b/contracts/modules/Payments.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.24; import {Constants} from '../libraries/Constants.sol'; import {ActionConstants} from '@uniswap/v4-periphery/src/libraries/ActionConstants.sol'; -import {BipsLibrary} from '@uniswap/v4-core/src/libraries/BipsLibrary.sol'; +import {BipsLibrary} from '@uniswap/v4-periphery/src/libraries/BipsLibrary.sol'; import {PaymentsImmutables} from '../modules/PaymentsImmutables.sol'; import {SafeTransferLib} from 'solmate/src/utils/SafeTransferLib.sol'; import {ERC20} from 'solmate/src/tokens/ERC20.sol'; diff --git a/contracts/modules/V3ToV4Migrator.sol b/contracts/modules/V3ToV4Migrator.sol index 27dff7f1..7eab43c6 100644 --- a/contracts/modules/V3ToV4Migrator.sol +++ b/contracts/modules/V3ToV4Migrator.sol @@ -3,10 +3,18 @@ pragma solidity ^0.8.24; import {MigratorImmutables} from '../modules/MigratorImmutables.sol'; import {INonfungiblePositionManager} from '@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol'; +import {Actions} from '@uniswap/v4-periphery/src/libraries/Actions.sol'; +import {CalldataDecoder} from '@uniswap/v4-periphery/src/libraries/CalldataDecoder.sol'; /// @title V3 to V4 Migrator /// @notice A contract that migrates liquidity from Uniswap V3 to V4 abstract contract V3ToV4Migrator is MigratorImmutables { + using CalldataDecoder for bytes; + + error InvalidAction(bytes4 action); + error OnlyMintAllowed(); + error NotAuthorizedForToken(uint256 tokenId); + /// @dev validate if an action is decreaseLiquidity, collect, or burn function isValidAction(bytes4 selector) internal pure returns (bool) { return selector == INonfungiblePositionManager.decreaseLiquidity.selector @@ -20,4 +28,38 @@ abstract contract V3ToV4Migrator is MigratorImmutables { return caller == owner || V3_POSITION_MANAGER.getApproved(tokenId) == caller || V3_POSITION_MANAGER.isApprovedForAll(owner, caller); } + + /// @dev check that the v4 position manager call is a safe call + /// of the position-altering Actions, we only allow Actions.MINT + /// this is because, if a user could be tricked into approving the UniversalRouter for + /// their position, an attacker could take their fees, or drain their entire position + function _checkV4PositionManagerCall(bytes calldata inputs) internal view { + bytes4 selector; + assembly { + selector := calldataload(inputs.offset) + } + if (selector != V4_POSITION_MANAGER.modifyLiquidities.selector) { + revert InvalidAction(selector); + } + + // slice is `abi.encode(bytes unlockData, uint256 deadline)` + bytes calldata slice = inputs[4:]; + // the first bytes(0) extracts the unlockData parameter from modifyLiquidities + // unlockData = `abi.encode(bytes actions, bytes[] params)` + // the second bytes(0) extracts the actions parameter from unlockData + bytes calldata actions = slice.toBytes(0).toBytes(0); + + uint256 numActions = actions.length; + + for (uint256 actionIndex = 0; actionIndex < numActions; actionIndex++) { + uint256 action = uint8(actions[actionIndex]); + + if ( + action == Actions.INCREASE_LIQUIDITY || action == Actions.DECREASE_LIQUIDITY + || action == Actions.BURN_POSITION + ) { + revert OnlyMintAllowed(); + } + } + } } diff --git a/lib/v4-periphery b/lib/v4-periphery index 3819809f..dd76ef0f 160000 --- a/lib/v4-periphery +++ b/lib/v4-periphery @@ -1 +1 @@ -Subproject commit 3819809f8c3aa6224ece98e83abe5fbfd15a0788 +Subproject commit dd76ef0fc94bac93cd40e57b01c9395f54bb924c diff --git a/package.json b/package.json index 3464d848..8b61c702 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "access": "public", "provenance": true }, - "version": "2.0.0-beta.1", + "version": "2.0.0-beta.2", "keywords": [ "uniswap", "router", diff --git a/test/integration-tests/UniswapV2.test.ts b/test/integration-tests/UniswapV2.test.ts index 7ecd34c3..0a18e7f0 100644 --- a/test/integration-tests/UniswapV2.test.ts +++ b/test/integration-tests/UniswapV2.test.ts @@ -74,7 +74,7 @@ describe('Uniswap V2 Tests:', () => { beforeEach(async () => { // cancel the permit on DAI - await permit2.approve(DAI.address, ADDRESS_ZERO, 0, 0) + await permit2.approve(DAI.address, router.address, 0, 0) }) it('V2 exactIn, permiting the exact amount', async () => { diff --git a/test/integration-tests/V3ToV4Migration.test.ts b/test/integration-tests/V3ToV4Migration.test.ts index 79e08e7e..cfe62d21 100644 --- a/test/integration-tests/V3ToV4Migration.test.ts +++ b/test/integration-tests/V3ToV4Migration.test.ts @@ -4,6 +4,7 @@ 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, @@ -32,8 +33,11 @@ import { } 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 @@ -969,7 +973,34 @@ describe('V3 to V4 Migration Tests:', () => { describe('V4 Commands', () => { beforeEach(async () => { // initialize new pool on v4 - await v4PositionManager.connect(bob).initializePool(USDC_WETH.poolKey, USDC_WETH.price) + 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 () => { @@ -1065,7 +1096,7 @@ describe('V3 to V4 Migration Tests:', () => { expect(await v4PositionManager.ownerOf(expectedTokenId)).to.eq(bob.address) }) - it('erc721 permit on v4 succeeds', async () => { + 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)) @@ -1117,161 +1148,12 @@ describe('V3 to V4 Migration Tests:', () => { planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [encodedErc721PermitCall]) - // bob permits the router to spend token - await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) - - // router is approved to spend the token - expect(await v4PositionManager.getApproved(expectedTokenId)).to.eq(router.address) - }) - - it('erc721 permit and unpermit on v4 succeeds', 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() - - let { compact } = await getPermitV4Signature(bob, v4PositionManager, router.address, expectedTokenId, MAX_UINT, { - nonce: 1, - }) - - let erc721PermitParams = { - spender: router.address, - tokenId: expectedTokenId, - deadline: MAX_UINT, - nonce: 1, - signature: compact, - } - - let encodedErc721PermitCall = encodeERC721PermitV4(erc721PermitParams) - - planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [encodedErc721PermitCall]) - - compact = ( - await getPermitV4Signature(bob, v4PositionManager, ZERO_ADDRESS, expectedTokenId, MAX_UINT, { nonce: 2 }) - ).compact - - erc721PermitParams = { - spender: ZERO_ADDRESS, - tokenId: expectedTokenId, - deadline: MAX_UINT, - nonce: 2, - signature: compact, - } - - encodedErc721PermitCall = encodeERC721PermitV4(erc721PermitParams) - - planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [encodedErc721PermitCall]) - - // bob permits the router to spend token - await executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) - - // router is no longer approved to spend the token - expect(await v4PositionManager.getApproved(expectedTokenId)).to.eq(ZERO_ADDRESS) - }) - - it('increase v4 succeeds by permitting router', 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)) - - // need to permit the router first to increase liquidity on the LP nft - let { compact } = await getPermitV4Signature(bob, v4PositionManager, router.address, expectedTokenId, MAX_UINT, { - nonce: 1, - }) - - let erc721PermitParams = { - spender: router.address, - tokenId: expectedTokenId, - deadline: MAX_UINT, - nonce: 1, - signature: compact, - } - - let encodedErc721PermitCall = encodeERC721PermitV4(erc721PermitParams) - - planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [encodedErc721PermitCall]) - - 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 executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) - - // bob successfully sweeped his usdc and weth from the v4 position manager - expect(await usdcContract.balanceOf(v4PositionManager.address)).to.eq(0) - expect(await wethContract.balanceOf(v4PositionManager.address)).to.eq(0) - - // bob still owns a position - expect(await v4PositionManager.balanceOf(bob.address)).to.eq(1) - expect(await v4PositionManager.ownerOf(expectedTokenId)).to.eq(bob.address) + await expect( + executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) + ).to.be.revertedWithCustomError(router, 'InvalidAction') }) - it('increase v4 succeeds by permitting and unpermitting router', async () => { + 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)) @@ -1308,23 +1190,6 @@ describe('V3 to V4 Migration Tests:', () => { await usdcContract.connect(bob).transfer(v4PositionManager.address, expandTo6DecimalsBN(10000)) await wethContract.connect(bob).transfer(v4PositionManager.address, expandTo18DecimalsBN(10)) - // need to permit the router first to increase liquidity on the LP nft - let { compact } = await getPermitV4Signature(bob, v4PositionManager, router.address, expectedTokenId, MAX_UINT, { - nonce: 1, - }) - - let erc721PermitParams = { - spender: router.address, - tokenId: expectedTokenId, - deadline: MAX_UINT, - nonce: 1, - signature: compact, - } - - let encodedErc721PermitCall = encodeERC721PermitV4(erc721PermitParams) - - planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [encodedErc721PermitCall]) - v4Planner.addAction(Actions.INCREASE_LIQUIDITY, [expectedTokenId, '6000000', MAX_UINT128, MAX_UINT128, '0x']) v4Planner.addAction(Actions.SETTLE, [USDC.address, OPEN_DELTA, SOURCE_ROUTER]) @@ -1336,201 +1201,16 @@ describe('V3 to V4 Migration Tests:', () => { planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [calldata]) - // then we need to "un-permit" the router (permit address 0) so that the router can no longer spend the nft - // if the router is not unpermitted, anyone can call V4_POSITION_MANAGER_CALL and decrease / burn the position - compact = ( - await getPermitV4Signature(bob, v4PositionManager, ZERO_ADDRESS, expectedTokenId, MAX_UINT, { nonce: 2 }) - ).compact - - erc721PermitParams = { - spender: ZERO_ADDRESS, - tokenId: expectedTokenId, - deadline: MAX_UINT, - nonce: 2, - signature: compact, - } - - encodedErc721PermitCall = encodeERC721PermitV4(erc721PermitParams) - - planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [encodedErc721PermitCall]) - // 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 executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) - - // bob successfully sweeped his usdc and weth from the v4 position manager - expect(await usdcContract.balanceOf(v4PositionManager.address)).to.eq(0) - expect(await wethContract.balanceOf(v4PositionManager.address)).to.eq(0) - - // bob still owns a position - expect(await v4PositionManager.balanceOf(bob.address)).to.eq(1) - expect(await v4PositionManager.ownerOf(expectedTokenId)).to.eq(bob.address) - - // router is no longer approved - expect(await v4PositionManager.getApproved(expectedTokenId)).to.eq(ZERO_ADDRESS) - }) - - // should only be done if they are also using the owner's signature to unpermit the router at the end - it('an address can increase a position on behalf of another address if permitted', async () => { - // mint position first - 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() - - // bob mints himself a position - 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)) - - // need to permit the router to spend the nft - let { compact } = await getPermitV4Signature(bob, v4PositionManager, router.address, expectedTokenId, MAX_UINT, { - nonce: 1, - }) - - let erc721PermitParams = { - spender: router.address, - tokenId: expectedTokenId, - deadline: MAX_UINT, - nonce: 1, - signature: compact, - } - - let encodedErc721PermitCall = encodeERC721PermitV4(erc721PermitParams) - - planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [encodedErc721PermitCall]) - - // increase params for bob's position - 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]) - - // caller needs to "un-permit" the router (permit address 0) so that the router can no longer spend the nft - // if the router is not unpermitted, anyone can call V4_POSITION_MANAGER_CALL and decrease / burn the position - compact = ( - await getPermitV4Signature(bob, v4PositionManager, ZERO_ADDRESS, expectedTokenId, MAX_UINT, { nonce: 2 }) - ).compact - - erc721PermitParams = { - spender: ZERO_ADDRESS, - tokenId: expectedTokenId, - deadline: MAX_UINT, - nonce: 2, - signature: compact, - } - - encodedErc721PermitCall = encodeERC721PermitV4(erc721PermitParams) - - planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [encodedErc721PermitCall]) - - // alice increases the position for bob - await executeRouter(planner, alice, router, wethContract, daiContract, usdcContract) - - // bob still 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) - - // router is no longer approved - expect(await v4PositionManager.getApproved(expectedTokenId)).to.eq(ZERO_ADDRESS) - }) - - it('decrease v4 does not succeed because UR is not approved', 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() - - // try to decrease the position without approval - 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, 'ExecutionFailed') - - // succeeds if UR is approved (but should not happen!) - await v4PositionManager.connect(bob).approve(router.address, expectedTokenId) - - // alice calls the router to decrease bob's position - await executeRouter(planner, alice, router, wethContract, daiContract, usdcContract) - - // bob still owns a position - expect(await v4PositionManager.balanceOf(bob.address)).to.eq(1) + ).to.be.revertedWithCustomError(router, 'OnlyMintAllowed') }) - it('decrease v4 does not succeed because UR is not approved', async () => { + it('decrease v4 does not succeed', async () => { // first mint the v4 nft // transfer to v4posm await usdcContract.connect(bob).transfer(v4PositionManager.address, expandTo6DecimalsBN(100000)) @@ -1564,7 +1244,6 @@ describe('V3 to V4 Migration Tests:', () => { planner = new RoutePlanner() v4Planner = new V4Planner() - // try to decrease the position without approval v4Planner.addAction(Actions.DECREASE_LIQUIDITY, [expectedTokenId, '6000000', 0, 0, '0x']) v4Planner.addAction(Actions.CLOSE_CURRENCY, [USDC.address]) @@ -1576,19 +1255,10 @@ describe('V3 to V4 Migration Tests:', () => { await expect( executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) - ).to.be.revertedWithCustomError(router, 'ExecutionFailed') - - // succeeds if UR is approved (but should not happen!) - await v4PositionManager.connect(bob).approve(router.address, expectedTokenId) - - // alice calls the router to decrease bob's position - await executeRouter(planner, alice, router, wethContract, daiContract, usdcContract) - - // bob still owns a position - expect(await v4PositionManager.balanceOf(bob.address)).to.eq(1) + ).to.be.revertedWithCustomError(router, 'OnlyMintAllowed') }) - it('burn v4 does not succeed because UR is not approved', async () => { + it('burn v4 does not succeed', async () => { // first mint the v4 nft // transfer to v4posm await usdcContract.connect(bob).transfer(v4PositionManager.address, expandTo6DecimalsBN(100000)) @@ -1622,7 +1292,6 @@ describe('V3 to V4 Migration Tests:', () => { planner = new RoutePlanner() v4Planner = new V4Planner() - // try to decrease the position without approval v4Planner.addAction(Actions.BURN_POSITION, [expectedTokenId, 0, 0, '0x']) v4Planner.addAction(Actions.CLOSE_CURRENCY, [USDC.address]) @@ -1634,40 +1303,18 @@ describe('V3 to V4 Migration Tests:', () => { await expect( executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) - ).to.be.revertedWithCustomError(router, 'ExecutionFailed') - - // succeeds if UR is approved (but should not happen!) - await v4PositionManager.connect(bob).approve(router.address, expectedTokenId) - - // alice calls the router to burn bob's position - await executeRouter(planner, alice, router, wethContract, daiContract, usdcContract) - - // bob's position is burned - expect(await v4PositionManager.balanceOf(bob.address)).to.eq(0) + ).to.be.revertedWithCustomError(router, 'OnlyMintAllowed') }) }) describe('Migration', () => { - beforeEach(async () => { - // initialize new pool on v4 - await v4PositionManager.connect(bob).initializePool( - { - currency0: USDC.address, - currency1: WETH.address, - fee: FeeAmount.LOW, - tickSpacing: 10, - hooks: '0x0000000000000000000000000000000000000000', - }, - '79228162514264337593543950336' - ) - - await v4PositionManager.connect(bob).initializePool(ETH_USDC.poolKey, ETH_USDC.price) - }) - it('migrate with minting succeeds', async () => { + it('migrate with initialize pool and minting succeeds', 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) + planner.addCommand(CommandType.V4_INITIALIZE_POOL, [USDC_WETH.poolKey, USDC_WETH.price]) + // mint the nft to bob on v3 const tx = await v3NFTPositionManager.mint({ token0: USDC.address, @@ -1741,11 +1388,14 @@ describe('V3 to V4 Migration Tests:', () => { expect(await v4PositionManager.ownerOf(expectedTokenId)).to.eq(bob.address) }) - it('migrate with increasing succeeds', async () => { + it('migrate with increasing does not succeed', async () => { // transfer to v4posm await usdcContract.connect(bob).transfer(v4PositionManager.address, expandTo6DecimalsBN(100000)) await wethContract.connect(bob).transfer(v4PositionManager.address, expandTo18DecimalsBN(100)) + // initialize the pool + await v4PositionManager.connect(bob).initializePool(USDC_WETH.poolKey, USDC_WETH.price) + v4Planner.addAction(Actions.MINT_POSITION, [ USDC_WETH.poolKey, USDC_WETH.tickLower, @@ -1868,28 +1518,18 @@ describe('V3 to V4 Migration Tests:', () => { planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [encodedErc721PermitCall]) - 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) - - // bob does not own a v3position - expect(await v3NFTPositionManager.balanceOf(bob.address)).to.eq(0) - - // bob still owns a v4position - expect(await v4PositionManager.balanceOf(bob.address)).to.eq(1) - expect(await v4PositionManager.ownerOf(expectedTokenId)).to.eq(bob.address) - - // router is no longer approved - expect(await v4PositionManager.getApproved(expectedTokenId)).to.eq(ZERO_ADDRESS) + await expect( + executeRouter(planner, bob, router, wethContract, daiContract, usdcContract) + ).to.be.revertedWithCustomError(router, 'InvalidAction') }) - it('migrate a weth position into an eth position by forwarding eth', async () => { + it('migrate a weth position into an eth position by forwarding eth, with initialize pool', 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) + planner.addCommand(CommandType.V4_INITIALIZE_POOL, [ETH_USDC.poolKey, ETH_USDC.price]) + // mint the nft to bob on v3 const tx = await v3NFTPositionManager.mint({ token0: USDC.address, diff --git a/test/integration-tests/gas-tests/V3ToV4Migration.gas.test.ts b/test/integration-tests/gas-tests/V3ToV4Migration.gas.test.ts index 7c4ddbd7..df1bce04 100644 --- a/test/integration-tests/gas-tests/V3ToV4Migration.gas.test.ts +++ b/test/integration-tests/gas-tests/V3ToV4Migration.gas.test.ts @@ -17,7 +17,6 @@ import { } from '../shared/constants' import { expandTo18DecimalsBN, expandTo6DecimalsBN } from '../shared/helpers' import getPermitNFTSignature from '../shared/getPermitNFTSignature' -import getPermitV4Signature from '../shared/getPermitV4Signature' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import hre from 'hardhat' import { RoutePlanner, CommandType } from '../shared/planner' @@ -28,10 +27,9 @@ import { encodeCollect, encodeBurn, encodeModifyLiquidities, - encodeERC721PermitV4, + encodeInitializePool, } from '../shared/encodeCall' const { ethers } = hre -import { executeRouter } from '../shared/executeRouter' import { USDC_WETH, ETH_USDC } from '../shared/v4Helpers' import { V4Planner, Actions } from '../shared/v4Planner' @@ -258,10 +256,13 @@ describe('V3 to V4 Migration Gas Tests', () => { }) describe('V4 Commands', () => { - beforeEach(async () => { - // initialize new pool on v4 - await v4PositionManager.connect(bob).initializePool(USDC_WETH.poolKey, USDC_WETH.price) - await v4PositionManager.connect(bob).initializePool(ETH_USDC.poolKey, ETH_USDC.price) + describe('initialize pool', () => { + it('gas: initialize a pool', async () => { + planner.addCommand(CommandType.V4_INITIALIZE_POOL, [USDC_WETH.poolKey, USDC_WETH.price]) + + const { commands, inputs } = planner + await snapshotGasCost(router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE)) + }) }) describe('mint', () => { @@ -270,6 +271,8 @@ describe('V3 to V4 Migration Gas Tests', () => { await usdcContract.connect(bob).transfer(v4PositionManager.address, expandTo6DecimalsBN(100000)) await wethContract.connect(bob).transfer(v4PositionManager.address, expandTo18DecimalsBN(100)) + await v4PositionManager.connect(bob).initializePool(USDC_WETH.poolKey, USDC_WETH.price) + v4Planner.addAction(Actions.MINT_POSITION, [ USDC_WETH.poolKey, USDC_WETH.tickLower, @@ -299,6 +302,8 @@ describe('V3 to V4 Migration Gas Tests', () => { await usdcContract.connect(bob).approve(v3NFTPositionManager.address, MAX_UINT) await wethContract.connect(bob).approve(v3NFTPositionManager.address, MAX_UINT) + await v4PositionManager.connect(bob).initializePool(USDC_WETH.poolKey, USDC_WETH.price) + // mint the nft to bob on v3 const tx = await v3NFTPositionManager.mint({ token0: USDC.address, @@ -391,6 +396,8 @@ describe('V3 to V4 Migration Gas Tests', () => { await usdcContract.connect(bob).approve(v3NFTPositionManager.address, MAX_UINT) await wethContract.connect(bob).approve(v3NFTPositionManager.address, MAX_UINT) + await v4PositionManager.connect(bob).initializePool(ETH_USDC.poolKey, USDC_WETH.price) + // mint the nft to bob on v3 const tx = await v3NFTPositionManager.mint({ token0: USDC.address, @@ -482,247 +489,5 @@ describe('V3 to V4 Migration Gas Tests', () => { await snapshotGasCost(router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE)) }) }) - - describe('increase', () => { - it('gas: increase', async () => { - // mint position first - 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) - - // 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)) - - // need to approve the router to spend the nft - let { compact } = await getPermitV4Signature( - bob, - v4PositionManager, - router.address, - expectedTokenId, - MAX_UINT, - { nonce: 1 } - ) - - let erc721PermitParams = { - spender: router.address, - tokenId: expectedTokenId, - deadline: MAX_UINT, - nonce: 1, - signature: compact, - } - - let encodedErc721PermitCall = encodeERC721PermitV4(erc721PermitParams) - - planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [encodedErc721PermitCall]) - - 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]) - - compact = ( - await getPermitV4Signature(bob, v4PositionManager, ZERO_ADDRESS, expectedTokenId, MAX_UINT, { nonce: 2 }) - ).compact - - erc721PermitParams = { - spender: ZERO_ADDRESS, - tokenId: expectedTokenId, - deadline: MAX_UINT, - nonce: 2, - signature: compact, - } - - encodedErc721PermitCall = encodeERC721PermitV4(erc721PermitParams) - - planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [encodedErc721PermitCall]) - - const { commands, inputs } = planner - await snapshotGasCost(router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE)) - }) - - it('gas: migrate and increase', async () => { - // mint position first - 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) - - planner = new RoutePlanner() - v4Planner = new V4Planner() - - // migrate - // 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) - - // mint the nft to bob on v3 - 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, - }) - - const receipt = await tx.wait() - const transferEvent = receipt.events?.find((event) => event.event === 'IncreaseLiquidity') - let tokenIdv3 = transferEvent?.args?.tokenId - - // permit, decrease, collect, burn - 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, - } - - let 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) - - // set receiver to v4posm - const collectParams = { - tokenId: tokenIdv3, - recipient: v4PositionManager.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]) - - // need to approve the router to spend the nft - let { compact } = await getPermitV4Signature( - bob, - v4PositionManager, - router.address, - expectedTokenId, - MAX_UINT, - { nonce: 1 } - ) - - let erc721PermitParamsV4 = { - spender: router.address, - tokenId: expectedTokenId, - deadline: MAX_UINT, - nonce: 1, - signature: compact, - } - - encodedErc721PermitCall = encodeERC721PermitV4(erc721PermitParamsV4) - - planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [encodedErc721PermitCall]) - - 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]) - - compact = ( - await getPermitV4Signature(bob, v4PositionManager, ZERO_ADDRESS, expectedTokenId, MAX_UINT, { nonce: 2 }) - ).compact - - erc721PermitParamsV4 = { - spender: ZERO_ADDRESS, - tokenId: expectedTokenId, - deadline: MAX_UINT, - nonce: 2, - signature: compact, - } - - encodedErc721PermitCall = encodeERC721PermitV4(erc721PermitParamsV4) - - planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [encodedErc721PermitCall]) - - const { commands, inputs } = planner - await snapshotGasCost(router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE)) - }) - }) }) }) diff --git a/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap index b23cde60..8563b78d 100644 --- a/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/Uniswap.gas.test.ts.snap @@ -3,42 +3,42 @@ exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, both fail but the transaction succeeds 1`] = ` Object { "calldataByteLength": 1764, - "gasUsed": 270118, + "gasUsed": 270178, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, neither fails 1`] = ` Object { "calldataByteLength": 1764, - "gasUsed": 245841, + "gasUsed": 245901, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, second sub plan fails 1`] = ` Object { "calldataByteLength": 1764, - "gasUsed": 245841, + "gasUsed": 245901, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, the first fails 1`] = ` Object { "calldataByteLength": 1764, - "gasUsed": 270118, + "gasUsed": 270178, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Interleaving routes gas: V2, then V3 1`] = ` Object { "calldataByteLength": 836, - "gasUsed": 189511, + "gasUsed": 189541, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Interleaving routes gas: V3, then V2 1`] = ` Object { "calldataByteLength": 836, - "gasUsed": 177094, + "gasUsed": 177124, } `; @@ -73,35 +73,35 @@ Object { exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V3, one hop 1`] = ` Object { "calldataByteLength": 996, - "gasUsed": 176938, + "gasUsed": 176968, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V3, one hop, ADDRESS_THIS flag 1`] = ` Object { "calldataByteLength": 996, - "gasUsed": 176713, + "gasUsed": 176743, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ETH split V2 and V3, exactOut, one hop 1`] = ` Object { "calldataByteLength": 964, - "gasUsed": 192148, + "gasUsed": 192178, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ETH split V2 and V3, one hop 1`] = ` Object { "calldataByteLength": 964, - "gasUsed": 184812, + "gasUsed": 184842, } `; exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ETH --> ERC20 split V2 and V3, one hop 1`] = ` Object { "calldataByteLength": 1124, - "gasUsed": 191854, + "gasUsed": 191884, } `; @@ -283,69 +283,69 @@ Object { exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, one hop 1`] = ` Object { "calldataByteLength": 516, - "gasUsed": 105477, + "gasUsed": 105507, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, three hops 1`] = ` Object { "calldataByteLength": 548, - "gasUsed": 253814, + "gasUsed": 253904, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, two hops 1`] = ` Object { "calldataByteLength": 548, - "gasUsed": 177031, + "gasUsed": 177091, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, one hop 1`] = ` Object { "calldataByteLength": 516, - "gasUsed": 112912, + "gasUsed": 112942, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, three hops 1`] = ` Object { "calldataByteLength": 548, - "gasUsed": 248860, + "gasUsed": 248950, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, two hops 1`] = ` Object { "calldataByteLength": 548, - "gasUsed": 172590, + "gasUsed": 172650, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ETH gas: exactIn swap 1`] = ` Object { "calldataByteLength": 644, - "gasUsed": 121807, + "gasUsed": 121837, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ETH gas: exactOut swap 1`] = ` Object { "calldataByteLength": 644, - "gasUsed": 129314, + "gasUsed": 129344, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ETH --> ERC20 gas: exactIn swap 1`] = ` Object { "calldataByteLength": 644, - "gasUsed": 215359, + "gasUsed": 215389, } `; exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ETH --> ERC20 gas: exactOut swap 1`] = ` Object { "calldataByteLength": 772, - "gasUsed": 124495, + "gasUsed": 124525, } `; diff --git a/test/integration-tests/gas-tests/__snapshots__/UniversalRouter.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/UniversalRouter.gas.test.ts.snap index 535edaa9..87034c84 100644 --- a/test/integration-tests/gas-tests/__snapshots__/UniversalRouter.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/UniversalRouter.gas.test.ts.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`UniversalRouter Gas Tests gas: bytecode size 1`] = `19173`; +exports[`UniversalRouter Gas Tests gas: bytecode size 1`] = `19706`; diff --git a/test/integration-tests/gas-tests/__snapshots__/UniversalVSSwapRouter.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/UniversalVSSwapRouter.gas.test.ts.snap index ad0a15e7..2e5aba51 100644 --- a/test/integration-tests/gas-tests/__snapshots__/UniversalVSSwapRouter.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/UniversalVSSwapRouter.gas.test.ts.snap @@ -7,32 +7,32 @@ Object { } `; -exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Max Approval Swap 1`] = `1104136`; +exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Max Approval Swap 1`] = `1104436`; -exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Sign Per Swap 1`] = `1138498`; +exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Sign Per Swap 1`] = `1138798`; exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps SwapRouter02 1`] = `1124979`; -exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Max Approval Swap 1`] = `3081019`; +exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Max Approval Swap 1`] = `3081919`; -exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Sign Per Swap 1`] = `3234344`; +exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Sign Per Swap 1`] = `3235244`; exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps SwapRouter02 1`] = `3195011`; -exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Max Approval Swap 1`] = `4102561`; +exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Max Approval Swap 1`] = `4103761`; -exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Sign Per Swap 1`] = `4306287`; +exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Sign Per Swap 1`] = `4307487`; exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions SwapRouter02 1`] = `4282374`; -exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Max Approval Swap 1`] = `508748`; +exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Max Approval Swap 1`] = `508868`; -exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Sign Per Swap 1`] = `509066`; +exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Sign Per Swap 1`] = `509186`; exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap SwapRouter02 1`] = `500008`; -exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Max Approval Swap 1`] = `299568`; +exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Max Approval Swap 1`] = `299628`; -exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Sign Per Swap 1`] = `299504`; +exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Sign Per Swap 1`] = `299564`; exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap SwapRouter02 1`] = `270033`; diff --git a/test/integration-tests/gas-tests/__snapshots__/V3ToV4Migration.gas.test.ts.snap b/test/integration-tests/gas-tests/__snapshots__/V3ToV4Migration.gas.test.ts.snap index d2090132..067fc66a 100644 --- a/test/integration-tests/gas-tests/__snapshots__/V3ToV4Migration.gas.test.ts.snap +++ b/test/integration-tests/gas-tests/__snapshots__/V3ToV4Migration.gas.test.ts.snap @@ -28,37 +28,30 @@ Object { } `; -exports[`V3 to V4 Migration Gas Tests V4 Commands increase gas: increase 1`] = ` +exports[`V3 to V4 Migration Gas Tests V4 Commands initialize pool gas: initialize a pool 1`] = ` Object { - "calldataByteLength": 2084, - "gasUsed": 228073, -} -`; - -exports[`V3 to V4 Migration Gas Tests V4 Commands increase gas: migrate and increase 1`] = ` -Object { - "calldataByteLength": 2980, - "gasUsed": 452916, + "calldataByteLength": 452, + "gasUsed": 58324, } `; exports[`V3 to V4 Migration Gas Tests V4 Commands mint gas: migrate and mint 1`] = ` Object { "calldataByteLength": 2500, - "gasUsed": 591168, + "gasUsed": 592670, } `; exports[`V3 to V4 Migration Gas Tests V4 Commands mint gas: migrate weth position into eth position with forwarding 1`] = ` Object { "calldataByteLength": 2788, - "gasUsed": 595307, + "gasUsed": 572027, } `; exports[`V3 to V4 Migration Gas Tests V4 Commands mint gas: mint 1`] = ` Object { "calldataByteLength": 1604, - "gasUsed": 437596, + "gasUsed": 439098, } `; diff --git a/test/integration-tests/shared/planner.ts b/test/integration-tests/shared/planner.ts index 19aac888..f99af681 100644 --- a/test/integration-tests/shared/planner.ts +++ b/test/integration-tests/shared/planner.ts @@ -25,7 +25,8 @@ export enum CommandType { V4_SWAP = 0x10, V3_POSITION_MANAGER_PERMIT = 0x11, V3_POSITION_MANAGER_CALL = 0x12, - V4_POSITION_MANAGER_CALL = 0x13, + V4_INITIALIZE_POOL = 0x13, + V4_POSITION_MANAGER_CALL = 0x14, EXECUTE_SUB_PLAN = 0x21, } @@ -37,6 +38,8 @@ const REVERTIBLE_COMMANDS = new Set([CommandType.EXECUTE_SUB_PLAN]) const PERMIT_STRUCT = '((address token,uint160 amount,uint48 expiration,uint48 nonce) details, address spender, uint256 sigDeadline)' +const POOL_KEY_STRUCT = '(address currency0,address currency1,uint24 fee,int24 tickSpacing,address hooks)' + const PERMIT_BATCH_STRUCT = '((address token,uint160 amount,uint48 expiration,uint48 nonce)[] details, address spender, uint256 sigDeadline)' @@ -70,6 +73,7 @@ const ABI_DEFINITION: { [key in CommandType]: string[] } = { [CommandType.V4_SWAP]: ['bytes', 'bytes[]'], [CommandType.V3_POSITION_MANAGER_PERMIT]: ['bytes'], [CommandType.V3_POSITION_MANAGER_CALL]: ['bytes'], + [CommandType.V4_INITIALIZE_POOL]: [POOL_KEY_STRUCT, 'uint160'], [CommandType.V4_POSITION_MANAGER_CALL]: ['bytes'], }