diff --git a/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2IncludingInit.snap b/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2IncludingInit.snap new file mode 100644 index 0000000..c5ce088 --- /dev/null +++ b/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2IncludingInit.snap @@ -0,0 +1 @@ +1017615 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutInit.snap b/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutInit.snap new file mode 100644 index 0000000..bfa5bac --- /dev/null +++ b/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutInit.snap @@ -0,0 +1 @@ +977598 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutNativeToken.snap b/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutNativeToken.snap new file mode 100644 index 0000000..e40db38 --- /dev/null +++ b/.forge-snapshots/BinMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutNativeToken.snap @@ -0,0 +1 @@ +1022017 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3IncludingInit.snap b/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3IncludingInit.snap new file mode 100644 index 0000000..5a7fd6b --- /dev/null +++ b/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3IncludingInit.snap @@ -0,0 +1 @@ +1096580 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutInit.snap b/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutInit.snap new file mode 100644 index 0000000..b4db3f3 --- /dev/null +++ b/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutInit.snap @@ -0,0 +1 @@ +1056639 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutNativeToken.snap b/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutNativeToken.snap new file mode 100644 index 0000000..11bbe69 --- /dev/null +++ b/.forge-snapshots/BinMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutNativeToken.snap @@ -0,0 +1 @@ +1094456 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2IncludingInit.snap b/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2IncludingInit.snap new file mode 100644 index 0000000..432348a --- /dev/null +++ b/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2IncludingInit.snap @@ -0,0 +1 @@ +1017627 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2WithoutInit.snap b/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2WithoutInit.snap new file mode 100644 index 0000000..b5e449b --- /dev/null +++ b/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2WithoutInit.snap @@ -0,0 +1 @@ +977610 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2WithoutNativeToken.snap b/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2WithoutNativeToken.snap new file mode 100644 index 0000000..d35a8e0 --- /dev/null +++ b/.forge-snapshots/BinMigratorFromUniswapV2Test#testMigrateFromV2WithoutNativeToken.snap @@ -0,0 +1 @@ +1022014 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3IncludingInit.snap b/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3IncludingInit.snap new file mode 100644 index 0000000..5284644 --- /dev/null +++ b/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3IncludingInit.snap @@ -0,0 +1 @@ +1094562 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3WithoutInit.snap b/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3WithoutInit.snap new file mode 100644 index 0000000..0798bc2 --- /dev/null +++ b/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3WithoutInit.snap @@ -0,0 +1 @@ +1054621 \ No newline at end of file diff --git a/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3WithoutNativeToken.snap b/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3WithoutNativeToken.snap new file mode 100644 index 0000000..2802d5f --- /dev/null +++ b/.forge-snapshots/BinMigratorFromUniswapV3Test#testMigrateFromV3WithoutNativeToken.snap @@ -0,0 +1 @@ +1092434 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2IncludingInit.snap b/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2IncludingInit.snap index e7da379..2fd85a1 100644 --- a/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2IncludingInit.snap +++ b/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2IncludingInit.snap @@ -1 +1 @@ -735570 \ No newline at end of file +736974 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutInit.snap b/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutInit.snap index 7a68bf4..1c78123 100644 --- a/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutInit.snap +++ b/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutInit.snap @@ -1 +1 @@ -692493 \ No newline at end of file +693861 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutNativeToken.snap b/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutNativeToken.snap index a5a965d..577725f 100644 --- a/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutNativeToken.snap +++ b/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutNativeToken.snap @@ -1 +1 @@ -736950 \ No newline at end of file +738290 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3IncludingInit.snap b/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3IncludingInit.snap index 1339a8f..ca570ab 100644 --- a/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3IncludingInit.snap +++ b/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3IncludingInit.snap @@ -1 +1 @@ -792734 \ No newline at end of file +793398 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutInit.snap b/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutInit.snap index c5787ea..01facce 100644 --- a/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutInit.snap +++ b/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutInit.snap @@ -1 +1 @@ -752222 \ No newline at end of file +752832 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutNativeToken.snap b/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutNativeToken.snap index 81bfead..ffc5693 100644 --- a/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutNativeToken.snap +++ b/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutNativeToken.snap @@ -1 +1 @@ -794168 \ No newline at end of file +794750 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2IncludingInit.snap b/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2IncludingInit.snap index 13b947f..26cffde 100644 --- a/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2IncludingInit.snap +++ b/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2IncludingInit.snap @@ -1 +1 @@ -735582 \ No newline at end of file +736986 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2WithoutInit.snap b/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2WithoutInit.snap index 7d1afb5..f8e8bf1 100644 --- a/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2WithoutInit.snap +++ b/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2WithoutInit.snap @@ -1 +1 @@ -692505 \ No newline at end of file +693873 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2WithoutNativeToken.snap b/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2WithoutNativeToken.snap index c4e1288..7ab44b7 100644 --- a/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2WithoutNativeToken.snap +++ b/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2WithoutNativeToken.snap @@ -1 +1 @@ -736947 \ No newline at end of file +738287 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3IncludingInit.snap b/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3IncludingInit.snap index f605b1c..473137d 100644 --- a/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3IncludingInit.snap +++ b/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3IncludingInit.snap @@ -1 +1 @@ -790716 \ No newline at end of file +791380 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3WithoutInit.snap b/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3WithoutInit.snap index 88b1574..79eb1ec 100644 --- a/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3WithoutInit.snap +++ b/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3WithoutInit.snap @@ -1 +1 @@ -750204 \ No newline at end of file +750814 \ No newline at end of file diff --git a/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3WithoutNativeToken.snap b/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3WithoutNativeToken.snap index 5212cb2..0a4c561 100644 --- a/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3WithoutNativeToken.snap +++ b/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3WithoutNativeToken.snap @@ -1 +1 @@ -792146 \ No newline at end of file +792728 \ No newline at end of file diff --git a/src/base/BaseMigrator.sol b/src/base/BaseMigrator.sol index c5b822e..e264bce 100644 --- a/src/base/BaseMigrator.sol +++ b/src/base/BaseMigrator.sol @@ -10,13 +10,58 @@ import {IWETH9} from "../interfaces/external/IWETH9.sol"; import {PeripheryImmutableState} from "./PeripheryImmutableState.sol"; import {Multicall} from "./Multicall.sol"; import {SelfPermit} from "./SelfPermit.sol"; -import {Currency} from "pancake-v4-core/src/types/Currency.sol"; +import {Currency, CurrencyLibrary} from "pancake-v4-core/src/types/Currency.sol"; +import {SelfPermitERC721} from "./SelfPermitERC721.sol"; import {IBaseMigrator} from "../interfaces/IBaseMigrator.sol"; -contract BaseMigrator is IBaseMigrator, PeripheryImmutableState, Multicall, SelfPermit { +contract BaseMigrator is IBaseMigrator, PeripheryImmutableState, Multicall, SelfPermit, SelfPermitERC721 { constructor(address _WETH9) PeripheryImmutableState(_WETH9) {} - function withdrawLiquidityFromV2(V2PoolParams calldata v2PoolParams) + /// @notice refund native ETH to caller + /// This is useful when the caller sends more ETH then he specifies in arguments + function refundETH() external payable override { + if (address(this).balance > 0) CurrencyLibrary.NATIVE.transfer(msg.sender, address(this).balance); + } + + /// @notice compare if tokens from v2 pair are the same as token0/token1. Revert with + /// `TOKEN_NOT_MATCH` if tokens does not match + /// @param v2Pair the address of v2 pair + /// @param token0 token0 of v4 poolKey + /// @param token1 token1 of v4 poolKey + /// @return shouldReversePair if the order of tokens from v2 pair is different from v4 pair (only when WETH is involved) + function checkTokensOrderAndMatchFromV2(address v2Pair, Currency token0, Currency token1) + internal + view + returns (bool shouldReversePair) + { + address token0V2 = IPancakePair(v2Pair).token0(); + address token1V2 = IPancakePair(v2Pair).token1(); + return _checkIfTokenPairMatchAndOrder(token0V2, token1V2, token0, token1); + } + + /// @notice compare if tokens from v3 pool are the same as token0/token1. Revert with + /// `TOKEN_NOT_MATCH` if tokens does not match + /// @param nfp the address of v3#nfp + /// @param tokenId the tokenId of v3 pool + /// @param token0 token0 of v4 poolKey + /// @param token1 token1 of v4 poolKey + /// @return shouldReversePair if the order of tokens from v3 pool is different from v4 pair (only when WETH is involved) + function checkTokensOrderAndMatchFromV3(address nfp, uint256 tokenId, Currency token0, Currency token1) + internal + view + returns (bool shouldReversePair) + { + (,, address token0V3, address token1V3,,,,,,,,) = IV3NonfungiblePositionManager(nfp).positions(tokenId); + return _checkIfTokenPairMatchAndOrder(token0V3, token1V3, token0, token1); + } + + /// @notice withdraw liquidity from v2 pool (fee will always be included) + /// It may revert if amount0/amount1 received is less than expected + /// @param v2PoolParams the parameters to withdraw liquidity from v2 pool + /// @param shouldReversePair if the order of tokens from v2 pair is different from v4 pair (only when WETH is involved) + /// @return amount0Received the actual amount of token0 received (in order of v4 pool) + /// @return amount1Received the actual amount of token1 received (in order of v4 pool) + function withdrawLiquidityFromV2(V2PoolParams calldata v2PoolParams, bool shouldReversePair) internal returns (uint256 amount0Received, uint256 amount1Received) { @@ -31,12 +76,18 @@ contract BaseMigrator is IBaseMigrator, PeripheryImmutableState, Multicall, Self /// @notice the order may mismatch with v4 pool when WETH is invovled /// the following check makes sure that the output always match the order of v4 pool - if (IPancakePair(v2PoolParams.pair).token1() == WETH9) { + if (shouldReversePair) { (amount0Received, amount1Received) = (amount1Received, amount0Received); } } - function withdrawLiquidityFromV3(V3PoolParams calldata v3PoolParams) + /// @notice withdraw liquidity from v3 pool and collect fee if specified in `v3PoolParams` + /// It may revert if the caller is not the owner of the token or amount0/amount1 received is less than expected + /// @param v3PoolParams the parameters to withdraw liquidity from v3 pool + /// @param shouldReversePair if the order of tokens from v3 pool is different from v4 pair (only when WETH is involved) + /// @return amount0Received the actual amount of token0 received (in order of v4 pool) + /// @return amount1Received the actual amount of token1 received (in order of v4 pool) + function withdrawLiquidityFromV3(V3PoolParams calldata v3PoolParams, bool shouldReversePair) internal returns (uint256 amount0Received, uint256 amount1Received) { @@ -70,13 +121,12 @@ contract BaseMigrator is IBaseMigrator, PeripheryImmutableState, Multicall, Self /// @notice the order may mismatch with v4 pool when WETH is invovled /// the following check makes sure that the output always match the order of v4 pool - (,,, address token1,,,,,,,,) = nfp.positions(tokenId); - if (token1 == WETH9) { + if (shouldReversePair) { (amount0Received, amount1Received) = (amount1Received, amount0Received); } } - /// @dev receive extra tokens from user if necessary and normalize all the WETH to native ETH + /// @notice receive extra tokens from user if specifies in arguments and normalize all the WETH to native ETH function batchAndNormalizeTokens(Currency currency0, Currency currency1, uint256 extraAmount0, uint256 extraAmount1) internal { @@ -98,7 +148,7 @@ contract BaseMigrator is IBaseMigrator, PeripheryImmutableState, Multicall, Self } if (extraAmount0 != 0 || extraAmount1 != 0) { - emit MoreFundsAdded(address(token0), address(token1), extraAmount0, extraAmount1); + emit ExtraFundsAdded(address(token0), address(token1), extraAmount0, extraAmount1); } // even if user sends native ETH, we still need to unwrap the part from source pool @@ -108,6 +158,7 @@ contract BaseMigrator is IBaseMigrator, PeripheryImmutableState, Multicall, Self } } + /// @notice approve the maximum amount of token if the current allowance is insufficient for following operations function approveMaxIfNeeded(Currency currency, address to, uint256 amount) internal { ERC20 token = ERC20(Currency.unwrap(currency)); if (token.allowance(address(this), to) >= amount) { @@ -115,4 +166,36 @@ contract BaseMigrator is IBaseMigrator, PeripheryImmutableState, Multicall, Self } SafeTransferLib.safeApprove(token, to, type(uint256).max); } + + /// @notice Check and revert if tokens from both v2/v3 and v4 pair does not match + /// Return true if match but v2v3Token1 is WETH which should be ETH in v4 pair + /// @param v2v3Token0 token0 from v2/v3 pair + /// @param v2v3Token1 token1 from v2/v3 pair + /// @param v4Token0 token0 from v4 pair + /// @param v4Token1 token1 from v4 pair + /// @return shouldReversePair if the order of tokens from v2/v3 pair is different from v4 pair (only when WETH is involved) + function _checkIfTokenPairMatchAndOrder( + address v2v3Token0, + address v2v3Token1, + Currency v4Token0, + Currency v4Token1 + ) private view returns (bool shouldReversePair) { + if (v4Token0.isNative() && v2v3Token0 == WETH9) { + if (Currency.unwrap(v4Token1) != v2v3Token1) { + revert TOKEN_NOT_MATCH(); + } + } else if (v4Token0.isNative() && v2v3Token1 == WETH9) { + if (Currency.unwrap(v4Token1) != v2v3Token0) { + revert TOKEN_NOT_MATCH(); + } + shouldReversePair = true; + } else { + /// @dev the order of token0 and token1 is always sorted + /// v2: https://github.com/pancakeswap/pancake-swap-core-v2/blob/38aad83854a46a82ea0e31988ff3cddb2bffb71a/contracts/PancakeFactory.sol#L27 + /// v3: https://github.com/pancakeswap/pancake-v3-contracts/blob/5cc479f0c5a98966c74d94700057b8c3ca629afd/projects/v3-core/contracts/PancakeV3Factory.sol#L66 + if (Currency.unwrap(v4Token0) != v2v3Token0 || Currency.unwrap(v4Token1) != v2v3Token1) { + revert TOKEN_NOT_MATCH(); + } + } + } } diff --git a/src/base/SelfPermitERC721.sol b/src/base/SelfPermitERC721.sol new file mode 100644 index 0000000..745ef1b --- /dev/null +++ b/src/base/SelfPermitERC721.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap +pragma solidity ^0.8.19; + +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC721Permit} from "../pool-cl/interfaces/IERC721Permit.sol"; +import {ISelfPermitERC721} from "../interfaces/ISelfPermitERC721.sol"; + +/// @title Self Permit For ERC721 +/// @notice Functionality to call permit on any EIP-2612-compliant token for use in the route +/// @dev These functions are expected to be embedded in multicalls to allow EOAs to approve a contract and call a function +/// that requires an approval in a single transaction. +abstract contract SelfPermitERC721 is ISelfPermitERC721 { + /// @inheritdoc ISelfPermitERC721 + function selfPermitERC721(address token, uint256 tokenId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + public + payable + override + { + IERC721Permit(token).permit(address(this), tokenId, deadline, v, r, s); + } + + /// @inheritdoc ISelfPermitERC721 + function selfPermitERC721IfNecessary( + address token, + uint256 tokenId, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external payable override { + if ( + IERC721(token).getApproved(tokenId) != address(this) + && !IERC721(token).isApprovedForAll(IERC721(token).ownerOf(tokenId), address(this)) + ) { + selfPermitERC721(token, tokenId, deadline, v, r, s); + } + } +} diff --git a/src/interfaces/IBaseMigrator.sol b/src/interfaces/IBaseMigrator.sol index afd3b6a..445b706 100644 --- a/src/interfaces/IBaseMigrator.sol +++ b/src/interfaces/IBaseMigrator.sol @@ -6,14 +6,22 @@ import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; import {IPeripheryImmutableState} from "./IPeripheryImmutableState.sol"; import {IMulticall} from "./IMulticall.sol"; import {ISelfPermit} from "./ISelfPermit.sol"; +import {ISelfPermitERC721} from "./ISelfPermitERC721.sol"; -interface IBaseMigrator is IPeripheryImmutableState, IMulticall, ISelfPermit { +interface IBaseMigrator is IPeripheryImmutableState, IMulticall, ISelfPermit, ISelfPermitERC721 { + error TOKEN_NOT_MATCH(); error INVALID_ETHER_SENDER(); error INSUFFICIENT_AMOUNTS_RECEIVED(); error NOT_TOKEN_OWNER(); - event MoreFundsAdded(address currency0, address currency1, uint256 extraAmount0, uint256 extraAmount1); + /// @notice The event emitted when extra funds are added to the migrator + /// @param currency0 the address of the token0 + /// @param currency1 the address of the token1 + /// @param extraAmount0 the amount of extra token0 + /// @param extraAmount1 the amount of extra token1 + event ExtraFundsAdded(address currency0, address currency1, uint256 extraAmount0, uint256 extraAmount1); + /// @notice Parameters for removing liquidity from v2 struct V2PoolParams { // the PancakeSwap v2-compatible pair address pair; @@ -24,6 +32,7 @@ interface IBaseMigrator is IPeripheryImmutableState, IMulticall, ISelfPermit { uint256 amount1Min; } + /// @notice Parameters for removing liquidity from v3 struct V3PoolParams { // the PancakeSwap v3-compatible NFP address nfp; @@ -35,4 +44,8 @@ interface IBaseMigrator is IPeripheryImmutableState, IMulticall, ISelfPermit { bool collectFee; uint256 deadline; } + + /// @notice refund native ETH to caller + /// This is useful when the caller sends more ETH then he specifies in arguments + function refundETH() external payable; } diff --git a/src/interfaces/ISelfPermitERC721.sol b/src/interfaces/ISelfPermitERC721.sol new file mode 100644 index 0000000..29dd736 --- /dev/null +++ b/src/interfaces/ISelfPermitERC721.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title Self Permit For ERC721 +/// @notice Functionality to call permit on any EIP-2612-compliant token +/// This is for PancakeSwapV3 styled Nonfungible Position Manager which supports permit extension +interface ISelfPermitERC721 { + /// @notice Permits this contract to spend a given position token from `msg.sender` + /// @dev The `owner` is always msg.sender and the `spender` is always address(this). + /// @param token The address of the token spent + /// @param tokenId The token ID of the token spent + /// @param deadline A timestamp, the current blocktime must be less than or equal to this timestamp + /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` + /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` + /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` + function selfPermitERC721(address token, uint256 tokenId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external + payable; + + /// @notice Permits this contract to spend a given token from `msg.sender` + /// @dev The `owner` is always msg.sender and the `spender` is always address(this). + /// Please always use selfPermitERC721IfNecessary if possible prevent calls from failing due to a frontrun of a call to #selfPermitERC721. + /// For details check https://github.com/pancakeswap/pancake-v4-periphery/pull/62#discussion_r1675410282 + /// @param token The address of the token spent + /// @param tokenId The token ID of the token spent + /// @param deadline A timestamp, the current blocktime must be less than or equal to this timestamp + /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` + /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` + /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` + function selfPermitERC721IfNecessary( + address token, + uint256 tokenId, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external payable; +} diff --git a/src/interfaces/external/IV3NonfungiblePositionManager.sol b/src/interfaces/external/IV3NonfungiblePositionManager.sol index 5189a1d..6c5fe1d 100644 --- a/src/interfaces/external/IV3NonfungiblePositionManager.sol +++ b/src/interfaces/external/IV3NonfungiblePositionManager.sol @@ -2,13 +2,13 @@ pragma solidity >=0.7.5; pragma abicoder v2; -import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC721Permit} from "../../pool-cl/interfaces/IERC721Permit.sol"; /// @title Non-fungible token for positions /// @notice Wraps PancakeSwap V3 positions in a non-fungible token interface which allows for them to be transferred /// and authorized. Copying from PancakeSwap-V3 /// https://github.com/pancakeswap/pancake-v3-contracts/blob/main/projects/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol -interface IV3NonfungiblePositionManager is IERC721 { +interface IV3NonfungiblePositionManager is IERC721Permit { /// @notice Emitted when liquidity is increased for a position NFT /// @dev Also emitted when a token is minted /// @param tokenId The ID of the token for which liquidity was increased diff --git a/src/pool-bin/BinMigrator.sol b/src/pool-bin/BinMigrator.sol new file mode 100644 index 0000000..8cb6e6f --- /dev/null +++ b/src/pool-bin/BinMigrator.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap +pragma solidity ^0.8.19; + +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +import {BaseMigrator, IV3NonfungiblePositionManager} from "../base/BaseMigrator.sol"; +import {IBinMigrator, PoolKey} from "./interfaces/IBinMigrator.sol"; +import {IBinFungiblePositionManager} from "./interfaces/IBinFungiblePositionManager.sol"; +import {Currency} from "pancake-v4-core/src/types/Currency.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +contract BinMigrator is IBinMigrator, BaseMigrator { + IBinFungiblePositionManager public immutable binFungiblePositionManager; + + constructor(address _WETH9, address _binFungiblePositionManager) BaseMigrator(_WETH9) { + binFungiblePositionManager = IBinFungiblePositionManager(_binFungiblePositionManager); + } + + /// @inheritdoc IBinMigrator + function migrateFromV2( + V2PoolParams calldata v2PoolParams, + V4BinPoolParams calldata v4PoolParams, + uint256 extraAmount0, + uint256 extraAmount1 + ) external payable override { + bool shouldReversePair = checkTokensOrderAndMatchFromV2( + v2PoolParams.pair, v4PoolParams.poolKey.currency0, v4PoolParams.poolKey.currency1 + ); + (uint256 amount0Received, uint256 amount1Received) = withdrawLiquidityFromV2(v2PoolParams, shouldReversePair); + + /// @notice if user mannually specify the price range, they might need to send extra token + batchAndNormalizeTokens( + v4PoolParams.poolKey.currency0, v4PoolParams.poolKey.currency1, extraAmount0, extraAmount1 + ); + + uint256 amount0Input = amount0Received + extraAmount0; + uint256 amount1Input = amount1Received + extraAmount1; + IBinFungiblePositionManager.AddLiquidityParams memory addLiquidityParams = IBinFungiblePositionManager + .AddLiquidityParams({ + poolKey: v4PoolParams.poolKey, + amount0: SafeCast.toUint128(amount0Input), + amount1: SafeCast.toUint128(amount1Input), + amount0Min: v4PoolParams.amount0Min, + amount1Min: v4PoolParams.amount1Min, + activeIdDesired: v4PoolParams.activeIdDesired, + idSlippage: v4PoolParams.idSlippage, + deltaIds: v4PoolParams.deltaIds, + distributionX: v4PoolParams.distributionX, + distributionY: v4PoolParams.distributionY, + to: v4PoolParams.to, + deadline: v4PoolParams.deadline + }); + (uint256 amount0Consumed, uint256 amount1Consumed,,) = _addLiquidityToTargetPool(addLiquidityParams); + + // refund if necessary, ETH is supported by CurrencyLib + unchecked { + if (amount0Input > amount0Consumed) { + v4PoolParams.poolKey.currency0.transfer(v4PoolParams.to, amount0Input - amount0Consumed); + } + if (amount1Input > amount1Consumed) { + v4PoolParams.poolKey.currency1.transfer(v4PoolParams.to, amount1Input - amount1Consumed); + } + } + } + + /// @inheritdoc IBinMigrator + function migrateFromV3( + V3PoolParams calldata v3PoolParams, + V4BinPoolParams calldata v4PoolParams, + uint256 extraAmount0, + uint256 extraAmount1 + ) external payable override { + bool shouldReversePair = checkTokensOrderAndMatchFromV3( + v3PoolParams.nfp, v3PoolParams.tokenId, v4PoolParams.poolKey.currency0, v4PoolParams.poolKey.currency1 + ); + (uint256 amount0Received, uint256 amount1Received) = withdrawLiquidityFromV3(v3PoolParams, shouldReversePair); + + /// @notice if user mannually specify the price range, they need to send extra token + batchAndNormalizeTokens( + v4PoolParams.poolKey.currency0, v4PoolParams.poolKey.currency1, extraAmount0, extraAmount1 + ); + + uint256 amount0Input = amount0Received + extraAmount0; + uint256 amount1Input = amount1Received + extraAmount1; + IBinFungiblePositionManager.AddLiquidityParams memory addLiquidityParams = IBinFungiblePositionManager + .AddLiquidityParams({ + poolKey: v4PoolParams.poolKey, + amount0: SafeCast.toUint128(amount0Input), + amount1: SafeCast.toUint128(amount1Input), + amount0Min: v4PoolParams.amount0Min, + amount1Min: v4PoolParams.amount1Min, + activeIdDesired: v4PoolParams.activeIdDesired, + idSlippage: v4PoolParams.idSlippage, + deltaIds: v4PoolParams.deltaIds, + distributionX: v4PoolParams.distributionX, + distributionY: v4PoolParams.distributionY, + to: v4PoolParams.to, + deadline: v4PoolParams.deadline + }); + (uint256 amount0Consumed, uint256 amount1Consumed,,) = _addLiquidityToTargetPool(addLiquidityParams); + + // refund if necessary, ETH is supported by CurrencyLib + unchecked { + if (amount0Input > amount0Consumed) { + v4PoolParams.poolKey.currency0.transfer(v4PoolParams.to, amount0Input - amount0Consumed); + } + if (amount1Input > amount1Consumed) { + v4PoolParams.poolKey.currency1.transfer(v4PoolParams.to, amount1Input - amount1Consumed); + } + } + } + + /// @dev adding liquidity to target bin pool, collect surplus ETH if necessary + /// @param params bin position manager add liquidity params + /// @return amount0Consumed the actual amount of token0 consumed + /// @return amount1Consumed the actual amount of token1 consumed + /// @return tokenIds the list of the id of the position token minted + /// @return liquidityMinted the list of the amount of the position token minted + function _addLiquidityToTargetPool(IBinFungiblePositionManager.AddLiquidityParams memory params) + internal + returns ( + uint128 amount0Consumed, + uint128 amount1Consumed, + uint256[] memory tokenIds, + uint256[] memory liquidityMinted + ) + { + /// @dev currency1 cant be NATIVE + bool nativePair = params.poolKey.currency0.isNative(); + if (!nativePair) { + approveMaxIfNeeded(params.poolKey.currency0, address(binFungiblePositionManager), params.amount0); + } + approveMaxIfNeeded(params.poolKey.currency1, address(binFungiblePositionManager), params.amount1); + + (amount0Consumed, amount1Consumed, tokenIds, liquidityMinted) = + binFungiblePositionManager.addLiquidity{value: nativePair ? params.amount0 : 0}(params); + + // receive surplus ETH from positionManager + if (nativePair && params.amount0 > amount0Consumed) { + binFungiblePositionManager.refundETH(); + } + } + + /// @inheritdoc IBinMigrator + /// @notice Planned to be batched with migration operations through multicall to save gas + function initialize(PoolKey memory poolKey, uint24 activeId, bytes calldata hookData) external payable override { + return binFungiblePositionManager.initialize(poolKey, activeId, hookData); + } + + receive() external payable { + if (msg.sender != address(binFungiblePositionManager) && msg.sender != WETH9) { + revert INVALID_ETHER_SENDER(); + } + } +} diff --git a/src/pool-bin/interfaces/IBinFungiblePositionManager.sol b/src/pool-bin/interfaces/IBinFungiblePositionManager.sol index 81e32fa..4a6a2b6 100644 --- a/src/pool-bin/interfaces/IBinFungiblePositionManager.sol +++ b/src/pool-bin/interfaces/IBinFungiblePositionManager.sol @@ -10,8 +10,10 @@ import {BalanceDelta} from "pancake-v4-core/src/types/BalanceDelta.sol"; import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; import {PoolId} from "pancake-v4-core/src/types/PoolId.sol"; import {IBinFungibleToken} from "./IBinFungibleToken.sol"; +import {IPeripheryPayments} from "../../interfaces/IPeripheryPayments.sol"; +import {IMulticall} from "../../interfaces/IMulticall.sol"; -interface IBinFungiblePositionManager is IBinFungibleToken { +interface IBinFungiblePositionManager is IBinFungibleToken, IPeripheryPayments, IMulticall { error OnlyVaultCaller(); error IdOverflows(int256); error IdDesiredOverflows(uint24); diff --git a/src/pool-bin/interfaces/IBinMigrator.sol b/src/pool-bin/interfaces/IBinMigrator.sol new file mode 100644 index 0000000..1437245 --- /dev/null +++ b/src/pool-bin/interfaces/IBinMigrator.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap +pragma solidity ^0.8.19; + +import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; +import {IBaseMigrator} from "../../interfaces/IBaseMigrator.sol"; +import {IV3NonfungiblePositionManager} from "../../interfaces/external/IV3NonfungiblePositionManager.sol"; + +interface IBinMigrator is IBaseMigrator { + /// @notice same fields as IBinFungiblePositionManager.AddLiquidityParams + /// except amount0/amount1 which will be calculated by migrator + struct V4BinPoolParams { + PoolKey poolKey; + // uint128 amount0; + // uint128 amount1; + uint128 amount0Min; + uint128 amount1Min; + uint256 activeIdDesired; + uint256 idSlippage; + int256[] deltaIds; + uint256[] distributionX; + uint256[] distributionY; + address to; + uint256 deadline; + } + + /// @notice Migrate liquidity from v2 to v4 + /// @param v2PoolParams ncessary info for removing liqudity the source v2 pool + /// @param v4PoolParams necessary info for adding liquidity the target v4 bin-pool + /// @param extraAmount0 the extra amount of token0 that user wants to add (optional, usually 0) + /// @param extraAmount1 the extra amount of token1 that user wants to add (optional, usually 0) + function migrateFromV2( + V2PoolParams calldata v2PoolParams, + V4BinPoolParams calldata v4PoolParams, + // extra funds to be added + uint256 extraAmount0, + uint256 extraAmount1 + ) external payable; + + /// @notice Migrate liquidity from v3 to v4 + /// @param v3PoolParams ncessary info for removing liqudity the source v3 pool + /// @param v4PoolParams necessary info for adding liquidity the target v4 bin-pool + /// @param extraAmount0 the extra amount of token0 that user wants to add (optional, usually 0) + /// @param extraAmount1 the extra amount of token1 that user wants to add (optional, usually 0) + function migrateFromV3( + V3PoolParams calldata v3PoolParams, + V4BinPoolParams calldata v4PoolParams, + // extra funds to be added + uint256 extraAmount0, + uint256 extraAmount1 + ) external payable; + + /// @notice Initialize a pool for a given pool key, the function will forwards the call to the BinPoolManager + /// @dev Call this when the pool does not exist and is not initialized + /// @param poolKey The pool key + /// @param activeId The active id of the pool + /// @param hookData Hook data for the pool + function initialize(PoolKey memory poolKey, uint24 activeId, bytes calldata hookData) external payable; +} diff --git a/src/pool-cl/CLMigrator.sol b/src/pool-cl/CLMigrator.sol index e66c3c9..ac6bdb5 100644 --- a/src/pool-cl/CLMigrator.sol +++ b/src/pool-cl/CLMigrator.sol @@ -15,13 +15,18 @@ contract CLMigrator is ICLMigrator, BaseMigrator { nonfungiblePositionManager = INonfungiblePositionManager(_nonfungiblePositionManager); } + /// @inheritdoc ICLMigrator function migrateFromV2( V2PoolParams calldata v2PoolParams, V4CLPoolParams calldata v4PoolParams, uint256 extraAmount0, uint256 extraAmount1 ) external payable override { - (uint256 amount0Received, uint256 amount1Received) = withdrawLiquidityFromV2(v2PoolParams); + bool shouldReversePair = checkTokensOrderAndMatchFromV2( + v2PoolParams.pair, v4PoolParams.poolKey.currency0, v4PoolParams.poolKey.currency1 + ); + + (uint256 amount0Received, uint256 amount1Received) = withdrawLiquidityFromV2(v2PoolParams, shouldReversePair); /// @notice if user mannually specify the price range, they might need to send extra token batchAndNormalizeTokens( @@ -55,13 +60,17 @@ contract CLMigrator is ICLMigrator, BaseMigrator { } } + /// @inheritdoc ICLMigrator function migrateFromV3( V3PoolParams calldata v3PoolParams, V4CLPoolParams calldata v4PoolParams, uint256 extraAmount0, uint256 extraAmount1 ) external payable override { - (uint256 amount0Received, uint256 amount1Received) = withdrawLiquidityFromV3(v3PoolParams); + bool shouldReversePair = checkTokensOrderAndMatchFromV3( + v3PoolParams.nfp, v3PoolParams.tokenId, v4PoolParams.poolKey.currency0, v4PoolParams.poolKey.currency1 + ); + (uint256 amount0Received, uint256 amount1Received) = withdrawLiquidityFromV3(v3PoolParams, shouldReversePair); /// @notice if user mannually specify the price range, they need to send extra token batchAndNormalizeTokens( @@ -95,6 +104,12 @@ contract CLMigrator is ICLMigrator, BaseMigrator { } } + /// @dev adding liquidity to target cl pool, collect surplus ETH if necessary + /// @param params cl position manager add liquidity params + /// @return tokenId the id of the newly minted position token + /// @return liquidity the amount of liquidity minted + /// @return amount0Consumed the actual amount of token0 consumed + /// @return amount1Consumed the actual amount of token1 consumed function _addLiquidityToTargetPool(INonfungiblePositionManager.MintParams memory params) internal returns (uint256 tokenId, uint128 liquidity, uint256 amount0Consumed, uint256 amount1Consumed) @@ -115,6 +130,7 @@ contract CLMigrator is ICLMigrator, BaseMigrator { } } + /// @inheritdoc ICLMigrator /// @notice Planned to be batched with migration operations through multicall to save gas function initialize(PoolKey memory poolKey, uint160 sqrtPriceX96, bytes calldata hookData) external diff --git a/src/pool-cl/interfaces/ICLMigrator.sol b/src/pool-cl/interfaces/ICLMigrator.sol index 733c350..4cc000b 100644 --- a/src/pool-cl/interfaces/ICLMigrator.sol +++ b/src/pool-cl/interfaces/ICLMigrator.sol @@ -21,6 +21,11 @@ interface ICLMigrator is IBaseMigrator { uint256 deadline; } + /// @notice Migrate liquidity from v2 to v4 + /// @param v2PoolParams ncessary info for removing liqudity the source v2 pool + /// @param v4PoolParams necessary info for adding liquidity the target v4 cl-pool + /// @param extraAmount0 the extra amount of token0 that user wants to add (optional, usually 0) + /// @param extraAmount1 the extra amount of token1 that user wants to add (optional, usually 0) function migrateFromV2( V2PoolParams calldata v2PoolParams, V4CLPoolParams calldata v4PoolParams, @@ -29,6 +34,11 @@ interface ICLMigrator is IBaseMigrator { uint256 extraAmount1 ) external payable; + /// @notice Migrate liquidity from v3 to v4 + /// @param v3PoolParams ncessary info for removing liqudity the source v3 pool + /// @param v4PoolParams necessary info for adding liquidity the target v4 cl-pool + /// @param extraAmount0 the extra amount of token0 that user wants to add (optional, usually 0) + /// @param extraAmount1 the extra amount of token1 that user wants to add (optional, usually 0) function migrateFromV3( V3PoolParams calldata v3PoolParams, V4CLPoolParams calldata v4PoolParams, @@ -37,7 +47,7 @@ interface ICLMigrator is IBaseMigrator { uint256 extraAmount1 ) external payable; - /// @notice Initialize the pool state for a given pool ID. + /// @notice Initialize a pool for a given pool key, the function will forwards the call to the CLPoolManager /// @dev Call this when the pool does not exist and is not initialized. /// @param poolKey The pool key /// @param sqrtPriceX96 The initial sqrt price of the pool diff --git a/test/pool-bin/migrator/BinMigratorFromPancakeswapV2.t.sol b/test/pool-bin/migrator/BinMigratorFromPancakeswapV2.t.sol new file mode 100644 index 0000000..1469eb9 --- /dev/null +++ b/test/pool-bin/migrator/BinMigratorFromPancakeswapV2.t.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {BinMigratorFromV2} from "./BinMigratorFromV2.sol"; + +contract BinMigratorFromPancakeswapV2Test is BinMigratorFromV2 { + function _getBytecodePath() internal pure override returns (string memory) { + // Create a Pancakeswap V2 pair + // relative to the root of the project + // https://etherscan.io/address/0x1097053Fd2ea711dad45caCcc45EfF7548fCB362#code + return "./test/bin/pcsV2Factory.bytecode"; + } + + function _getContractName() internal pure override returns (string memory) { + return "BinMigratorFromPancakeswapV2Test"; + } +} diff --git a/test/pool-bin/migrator/BinMigratorFromPancakeswapV3.t.sol b/test/pool-bin/migrator/BinMigratorFromPancakeswapV3.t.sol new file mode 100644 index 0000000..b4a6f9a --- /dev/null +++ b/test/pool-bin/migrator/BinMigratorFromPancakeswapV3.t.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {BinMigratorFromV3} from "./BinMigratorFromV3.sol"; + +contract BinMigratorFromPancakeswapV3Test is BinMigratorFromV3 { + function _getDeployerBytecodePath() internal pure override returns (string memory) { + // https://etherscan.io/address/0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9#code + return "./test/bin/pcsV3Deployer.bytecode"; + } + + function _getFactoryBytecodePath() internal pure override returns (string memory) { + // https://etherscan.io/address/0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865#code + return "./test/bin/pcsV3Factory.bytecode"; + } + + function _getNfpmBytecodePath() internal pure override returns (string memory) { + // https://etherscan.io/address/0x46A15B0b27311cedF172AB29E4f4766fbE7F4364#code + return "./test/bin/pcsV3Nfpm.bytecode"; + } + + function _getContractName() internal pure override returns (string memory) { + return "BinMigratorFromPancakeswapV3Test"; + } +} diff --git a/test/pool-bin/migrator/BinMigratorFromUniswapV2.t.sol b/test/pool-bin/migrator/BinMigratorFromUniswapV2.t.sol new file mode 100644 index 0000000..d0b28e4 --- /dev/null +++ b/test/pool-bin/migrator/BinMigratorFromUniswapV2.t.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {BinMigratorFromV2} from "./BinMigratorFromV2.sol"; + +contract BinMigratorFromUniswapV2Test is BinMigratorFromV2 { + function _getBytecodePath() internal pure override returns (string memory) { + // Create a Uniswap V2 pair + // relative to the root of the project + // https://etherscan.io/address/0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f#code + return "./test/bin/uniV2Factory.bytecode"; + } + + function _getContractName() internal pure override returns (string memory) { + return "BinMigratorFromUniswapV2Test"; + } +} diff --git a/test/pool-bin/migrator/BinMigratorFromUniswapV3.t.sol b/test/pool-bin/migrator/BinMigratorFromUniswapV3.t.sol new file mode 100644 index 0000000..c1b5825 --- /dev/null +++ b/test/pool-bin/migrator/BinMigratorFromUniswapV3.t.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {BinMigratorFromV3} from "./BinMigratorFromV3.sol"; + +contract BinMigratorFromUniswapV3Test is BinMigratorFromV3 { + function _getDeployerBytecodePath() internal pure override returns (string memory) { + return ""; + } + + function _getFactoryBytecodePath() internal pure override returns (string memory) { + // https://etherscan.io/address/0x1F98431c8aD98523631AE4a59f267346ea31F984#code + return "./test/bin/uniV3Factory.bytecode"; + } + + function _getNfpmBytecodePath() internal pure override returns (string memory) { + // https://etherscan.io/address/0xC36442b4a4522E871399CD717aBDD847Ab11FE88#code + return "./test/bin/uniV3Nfpm.bytecode"; + } + + function _getContractName() internal pure override returns (string memory) { + return "BinMigratorFromUniswapV3Test"; + } +} diff --git a/test/pool-bin/migrator/BinMigratorFromV2.sol b/test/pool-bin/migrator/BinMigratorFromV2.sol new file mode 100644 index 0000000..d03efe1 --- /dev/null +++ b/test/pool-bin/migrator/BinMigratorFromV2.sol @@ -0,0 +1,922 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {OldVersionHelper} from "../../helpers/OldVersionHelper.sol"; +import {IPancakePair} from "../../../src/interfaces/external/IPancakePair.sol"; +import {WETH} from "solmate/tokens/WETH.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {BinMigrator} from "../../../src/pool-bin/BinMigrator.sol"; +import {IBinMigrator, IBaseMigrator} from "../../../src/pool-bin/interfaces/IBinMigrator.sol"; +import {BinFungiblePositionManager} from "../../../src/pool-bin/BinFungiblePositionManager.sol"; +import {Vault} from "pancake-v4-core/src/Vault.sol"; +import {BinPoolManager} from "pancake-v4-core/src/pool-bin/BinPoolManager.sol"; +import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; +import {BinPoolParametersHelper} from "pancake-v4-core/src/pool-bin/libraries/BinPoolParametersHelper.sol"; +import {Currency} from "pancake-v4-core/src/types/Currency.sol"; +import {IPoolManager} from "pancake-v4-core/src/interfaces/IPoolManager.sol"; +import {IHooks} from "pancake-v4-core/src/interfaces/IHooks.sol"; +import {PoolId, PoolIdLibrary} from "pancake-v4-core/src/types/PoolId.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {LiquidityParamsHelper, IBinFungiblePositionManager} from "../helpers/LiquidityParamsHelper.sol"; +import {BinTokenLibrary} from "../../../src/pool-bin/libraries/BinTokenLibrary.sol"; + +interface IPancakeV2LikePairFactory { + function createPair(address tokenA, address tokenB) external returns (address pair); +} + +abstract contract BinMigratorFromV2 is OldVersionHelper, LiquidityParamsHelper, GasSnapshot { + using BinPoolParametersHelper for bytes32; + using PoolIdLibrary for PoolKey; + using BinTokenLibrary for PoolId; + + // 1 tokenX = 1 tokenY + uint24 public constant ACTIVE_BIN_ID = 2 ** 23; + + WETH weth; + MockERC20 token0; + MockERC20 token1; + + Vault vault; + BinPoolManager poolManager; + BinFungiblePositionManager binFungiblePositionManager; + IBinMigrator migrator; + PoolKey poolKey; + PoolKey poolKeyWithoutNativeToken; + + IPancakeV2LikePairFactory v2Factory; + IPancakePair v2Pair; + IPancakePair v2PairWithoutNativeToken; + + function _getBytecodePath() internal pure virtual returns (string memory); + + function _getContractName() internal pure virtual returns (string memory); + + function setUp() public { + weth = new WETH(); + token0 = new MockERC20("Token0", "TKN0", 18); + token1 = new MockERC20("Token1", "TKN1", 18); + (token0, token1) = token0 < token1 ? (token0, token1) : (token1, token0); + + // init v4 nfpm & migrator + vault = new Vault(); + poolManager = new BinPoolManager(vault, 3000); + vault.registerApp(address(poolManager)); + binFungiblePositionManager = new BinFungiblePositionManager(vault, poolManager, address(weth)); + migrator = new BinMigrator(address(weth), address(binFungiblePositionManager)); + + poolKey = PoolKey({ + // WETH after migration will be native token + currency0: Currency.wrap(address(0)), + currency1: Currency.wrap(address(token0)), + hooks: IHooks(address(0)), + poolManager: poolManager, + fee: 0, + parameters: bytes32(0).setBinStep(1) + }); + + poolKeyWithoutNativeToken = poolKey; + poolKeyWithoutNativeToken.currency0 = Currency.wrap(address(token0)); + poolKeyWithoutNativeToken.currency1 = Currency.wrap(address(token1)); + + // make sure the contract has enough balance + // WETH: 100 ether + // Token: 100 ether + // ETH: 90 ether + deal(address(this), 1000 ether); + weth.deposit{value: 100 ether}(); + token0.mint(address(this), 100 ether); + token1.mint(address(this), 100 ether); + + v2Factory = IPancakeV2LikePairFactory(createContractThroughBytecode(_getBytecodePath())); + v2Pair = IPancakePair(v2Factory.createPair(address(weth), address(token0))); + v2PairWithoutNativeToken = IPancakePair(v2Factory.createPair(address(token0), address(token1))); + } + + function testMigrateFromV2IncludingInit() public { + // 1. mint some liquidity to the v2 pair + _mintV2Liquidity(v2Pair); + uint256 lpTokenBefore = v2Pair.balanceOf(address(this)); + assertGt(lpTokenBefore, 0); + + // 2. make sure migrator can transfer user's v2 lp token + v2Pair.approve(address(migrator), lpTokenBefore); + + IBaseMigrator.V2PoolParams memory v2PoolParams = IBaseMigrator.V2PoolParams({ + pair: address(v2Pair), + migrateAmount: lpTokenBefore, + // minor precision loss is acceptable + amount0Min: 9.999 ether, + amount1Min: 9.999 ether + }); + + IBinFungiblePositionManager.AddLiquidityParams memory params = + _getAddParams(poolKey, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this)); + + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: params.poolKey, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: params.deltaIds, + distributionX: params.distributionX, + distributionY: params.distributionY, + to: params.to, + deadline: params.deadline + }); + + // 3. multicall, combine initialize and migrateFromV2 + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeWithSelector(migrator.initialize.selector, poolKey, ACTIVE_BIN_ID, bytes("")); + data[1] = abi.encodeWithSelector(migrator.migrateFromV2.selector, v2PoolParams, v4BinPoolParams, 0, 0); + snapStart(string(abi.encodePacked(_getContractName(), "#testMigrateFromV2IncludingInit"))); + migrator.multicall(data); + snapEnd(); + + // necessary checks + // v2 pair should be burned already + assertEq(v2Pair.balanceOf(address(this)), 0); + + // make sure liuqidty is minted to the correct pool + assertApproxEqAbs(address(vault).balance, 10 ether, 0.000001 ether); + assertApproxEqAbs(token0.balanceOf(address(vault)), 10 ether, 0.000001 ether); + + uint256 positionId0 = poolKey.toId().toTokenId(ACTIVE_BIN_ID - 1); + uint256 positionId1 = poolKey.toId().toTokenId(ACTIVE_BIN_ID); + uint256 positionId2 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 1); + uint256 positionId3 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 2); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId0), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId1), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId2), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId3), 0); + + (PoolId poolId, Currency currency0, Currency currency1, uint24 fee, uint24 binId) = + binFungiblePositionManager.positions(positionId0); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID - 1); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId1); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId2); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID + 1); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3); + } + + function testMigrateFromV2TokenMismatch() public { + // 1. mint some liquidity to the v2 pair + _mintV2Liquidity(v2Pair); + uint256 lpTokenBefore = v2Pair.balanceOf(address(this)); + assertGt(lpTokenBefore, 0); + + // 2. make sure migrator can transfer user's v2 lp token + v2Pair.approve(address(migrator), lpTokenBefore); + + IBaseMigrator.V2PoolParams memory v2PoolParams = IBaseMigrator.V2PoolParams({ + pair: address(v2Pair), + migrateAmount: lpTokenBefore, + // minor precision loss is acceptable + amount0Min: 9.999 ether, + amount1Min: 9.999 ether + }); + + IBinFungiblePositionManager.AddLiquidityParams memory params = + _getAddParams(poolKey, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this)); + + // v2 weth, token0 + // v4 ETH, token1 + PoolKey memory poolKeyMismatch = poolKey; + poolKeyMismatch.currency1 = Currency.wrap(address(token1)); + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: poolKeyMismatch, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: params.deltaIds, + distributionX: params.distributionX, + distributionY: params.distributionY, + to: params.to, + deadline: params.deadline + }); + + // 3. multicall, combine initialize and migrateFromV2 + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeWithSelector(migrator.initialize.selector, poolKeyMismatch, ACTIVE_BIN_ID, bytes("")); + data[1] = abi.encodeWithSelector(migrator.migrateFromV2.selector, v2PoolParams, v4BinPoolParams, 0, 0); + vm.expectRevert(); + migrator.multicall(data); + + { + // v2 weth, token0 + // v4 token0, token1 + poolKeyMismatch.currency0 = Currency.wrap(address(token0)); + poolKeyMismatch.currency1 = Currency.wrap(address(token1)); + v4BinPoolParams.poolKey = poolKeyMismatch; + data = new bytes[](2); + data[0] = abi.encodeWithSelector(migrator.initialize.selector, poolKeyMismatch, ACTIVE_BIN_ID, bytes("")); + data[1] = abi.encodeWithSelector(migrator.migrateFromV2.selector, v2PoolParams, v4BinPoolParams, 0, 0); + vm.expectRevert(); + migrator.multicall(data); + } + } + + function testMigrateFromV2WithoutInit() public { + // 1. mint some liquidity to the v2 pair + _mintV2Liquidity(v2Pair); + uint256 lpTokenBefore = v2Pair.balanceOf(address(this)); + assertGt(lpTokenBefore, 0); + + // 2. make sure migrator can transfer user's v2 lp token + v2Pair.approve(address(migrator), lpTokenBefore); + + // 3. initialize the pool + migrator.initialize(poolKey, ACTIVE_BIN_ID, bytes("")); + + IBaseMigrator.V2PoolParams memory v2PoolParams = IBaseMigrator.V2PoolParams({ + pair: address(v2Pair), + migrateAmount: lpTokenBefore, + // minor precision loss is acceptable + amount0Min: 9.999 ether, + amount1Min: 9.999 ether + }); + + IBinFungiblePositionManager.AddLiquidityParams memory params = + _getAddParams(poolKey, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this)); + + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: params.poolKey, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: params.deltaIds, + distributionX: params.distributionX, + distributionY: params.distributionY, + to: params.to, + deadline: params.deadline + }); + + // 4. migrateFromV2 + snapStart(string(abi.encodePacked(_getContractName(), "#testMigrateFromV2WithoutInit"))); + migrator.migrateFromV2(v2PoolParams, v4BinPoolParams, 0, 0); + snapEnd(); + + // necessary checks + // v2 pair should be burned already + assertEq(v2Pair.balanceOf(address(this)), 0); + + // make sure liuqidty is minted to the correct pool + assertApproxEqAbs(address(vault).balance, 10 ether, 0.000001 ether); + assertApproxEqAbs(token0.balanceOf(address(vault)), 10 ether, 0.000001 ether); + + uint256 positionId0 = poolKey.toId().toTokenId(ACTIVE_BIN_ID - 1); + uint256 positionId1 = poolKey.toId().toTokenId(ACTIVE_BIN_ID); + uint256 positionId2 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 1); + uint256 positionId3 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 2); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId0), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId1), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId2), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId3), 0); + + (PoolId poolId, Currency currency0, Currency currency1, uint24 fee, uint24 binId) = + binFungiblePositionManager.positions(positionId0); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID - 1); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId1); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId2); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID + 1); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3); + } + + function testMigrateFromV2WithoutNativeToken() public { + // 1. mint some liquidity to the v2 pair + _mintV2Liquidity(v2PairWithoutNativeToken); + uint256 lpTokenBefore = v2PairWithoutNativeToken.balanceOf(address(this)); + assertGt(lpTokenBefore, 0); + + // 2. make sure migrator can transfer user's v2 lp token + v2PairWithoutNativeToken.approve(address(migrator), lpTokenBefore); + + // 3. initialize the pool + migrator.initialize(poolKeyWithoutNativeToken, ACTIVE_BIN_ID, bytes("")); + + IBaseMigrator.V2PoolParams memory v2PoolParams = IBaseMigrator.V2PoolParams({ + pair: address(v2PairWithoutNativeToken), + migrateAmount: lpTokenBefore, + // minor precision loss is acceptable + amount0Min: 9.999 ether, + amount1Min: 9.999 ether + }); + + IBinFungiblePositionManager.AddLiquidityParams memory params = _getAddParams( + poolKeyWithoutNativeToken, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this) + ); + + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: params.poolKey, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: params.deltaIds, + distributionX: params.distributionX, + distributionY: params.distributionY, + to: params.to, + deadline: params.deadline + }); + + // 4. migrate from v2 to v4 + snapStart(string(abi.encodePacked(_getContractName(), "#testMigrateFromV2WithoutNativeToken"))); + migrator.migrateFromV2(v2PoolParams, v4BinPoolParams, 0, 0); + snapEnd(); + + // necessary checks + // v2 pair should be burned already + assertEq(v2PairWithoutNativeToken.balanceOf(address(this)), 0); + + // make sure liuqidty is minted to the correct pool + assertApproxEqAbs(token0.balanceOf(address(vault)), 10 ether, 0.000001 ether); + assertApproxEqAbs(token1.balanceOf(address(vault)), 10 ether, 0.000001 ether); + + uint256 positionId0 = poolKeyWithoutNativeToken.toId().toTokenId(ACTIVE_BIN_ID - 1); + uint256 positionId1 = poolKeyWithoutNativeToken.toId().toTokenId(ACTIVE_BIN_ID); + uint256 positionId2 = poolKeyWithoutNativeToken.toId().toTokenId(ACTIVE_BIN_ID + 1); + uint256 positionId3 = poolKeyWithoutNativeToken.toId().toTokenId(ACTIVE_BIN_ID + 2); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId0), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId1), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId2), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId3), 0); + + (PoolId poolId, Currency currency0, Currency currency1, uint24 fee, uint24 binId) = + binFungiblePositionManager.positions(positionId0); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKeyWithoutNativeToken.toId())); + assertEq(Currency.unwrap(currency0), address(token0)); + assertEq(Currency.unwrap(currency1), address(token1)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID - 1); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId1); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKeyWithoutNativeToken.toId())); + assertEq(Currency.unwrap(currency0), address(token0)); + assertEq(Currency.unwrap(currency1), address(token1)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId2); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKeyWithoutNativeToken.toId())); + assertEq(Currency.unwrap(currency0), address(token0)); + assertEq(Currency.unwrap(currency1), address(token1)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID + 1); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3); + } + + function testMigrateFromV2AddExtraAmount() public { + // 1. mint some liquidity to the v2 pair + _mintV2Liquidity(v2Pair); + uint256 lpTokenBefore = v2Pair.balanceOf(address(this)); + assertGt(lpTokenBefore, 0); + + // 2. make sure migrator can transfer user's v2 lp token + v2Pair.approve(address(migrator), lpTokenBefore); + + // 3. initialize the pool + migrator.initialize(poolKey, ACTIVE_BIN_ID, bytes("")); + + IBaseMigrator.V2PoolParams memory v2PoolParams = IBaseMigrator.V2PoolParams({ + pair: address(v2Pair), + migrateAmount: lpTokenBefore, + // minor precision loss is acceptable + amount0Min: 9.999 ether, + amount1Min: 9.999 ether + }); + + IBinFungiblePositionManager.AddLiquidityParams memory params = + _getAddParams(poolKey, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this)); + + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: params.poolKey, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: params.deltaIds, + distributionX: params.distributionX, + distributionY: params.distributionY, + to: params.to, + deadline: params.deadline + }); + + uint256 balance0Before = address(this).balance; + uint256 balance1Before = token0.balanceOf(address(this)); + + IERC20(address(token0)).approve(address(migrator), 20 ether); + // 4. migrate from v2 to v4 + migrator.migrateFromV2{value: 20 ether}(v2PoolParams, v4BinPoolParams, 20 ether, 20 ether); + + // necessary checks + // consumed extra 20 ether from user + assertApproxEqAbs(balance0Before - address(this).balance, 20 ether, 0.000001 ether); + assertEq(balance1Before - token0.balanceOf(address(this)), 20 ether); + // WETH balance unchanged + assertEq(weth.balanceOf(address(this)), 90 ether); + + // v2 pair should be burned already + assertEq(v2Pair.balanceOf(address(this)), 0); + + // make sure liuqidty is minted to the correct pool + assertApproxEqAbs(address(vault).balance, 30 ether, 0.000001 ether); + assertApproxEqAbs(token0.balanceOf(address(vault)), 30 ether, 0.000001 ether); + + uint256 positionId0 = poolKey.toId().toTokenId(ACTIVE_BIN_ID - 1); + uint256 positionId1 = poolKey.toId().toTokenId(ACTIVE_BIN_ID); + uint256 positionId2 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 1); + uint256 positionId3 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 2); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId0), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId1), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId2), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId3), 0); + + (PoolId poolId, Currency currency0, Currency currency1, uint24 fee, uint24 binId) = + binFungiblePositionManager.positions(positionId0); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID - 1); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId1); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId2); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID + 1); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3); + } + + function testMigrateFromV2AddExtraAmountThroughWETH() public { + // 1. mint some liquidity to the v2 pair + _mintV2Liquidity(v2Pair); + uint256 lpTokenBefore = v2Pair.balanceOf(address(this)); + assertGt(lpTokenBefore, 0); + + // 2. make sure migrator can transfer user's v2 lp token + v2Pair.approve(address(migrator), lpTokenBefore); + + // 3. initialize the pool + migrator.initialize(poolKey, ACTIVE_BIN_ID, bytes("")); + + IBaseMigrator.V2PoolParams memory v2PoolParams = IBaseMigrator.V2PoolParams({ + pair: address(v2Pair), + migrateAmount: lpTokenBefore, + // minor precision loss is acceptable + amount0Min: 9.999 ether, + amount1Min: 9.999 ether + }); + + IBinFungiblePositionManager.AddLiquidityParams memory params = + _getAddParams(poolKey, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this)); + + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: params.poolKey, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: params.deltaIds, + distributionX: params.distributionX, + distributionY: params.distributionY, + to: params.to, + deadline: params.deadline + }); + + uint256 balance0Before = address(this).balance; + uint256 balance1Before = token0.balanceOf(address(this)); + + weth.approve(address(migrator), 20 ether); + IERC20(address(token0)).approve(address(migrator), 20 ether); + // 4. migrate from v2 to v4, not sending ETH denotes pay by WETH + migrator.migrateFromV2(v2PoolParams, v4BinPoolParams, 20 ether, 20 ether); + + // necessary checks + // consumed extra 20 ether from user + // native token balance unchanged + assertApproxEqAbs(balance0Before - address(this).balance, 0 ether, 0.000001 ether); + assertEq(balance1Before - token0.balanceOf(address(this)), 20 ether); + // consumed 20 ether WETH + assertEq(weth.balanceOf(address(this)), 70 ether); + + // v2 pair should be burned already + assertEq(v2Pair.balanceOf(address(this)), 0); + + // make sure liuqidty is minted to the correct pool + assertApproxEqAbs(address(vault).balance, 30 ether, 0.000001 ether); + assertApproxEqAbs(token0.balanceOf(address(vault)), 30 ether, 0.000001 ether); + + uint256 positionId0 = poolKey.toId().toTokenId(ACTIVE_BIN_ID - 1); + uint256 positionId1 = poolKey.toId().toTokenId(ACTIVE_BIN_ID); + uint256 positionId2 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 1); + uint256 positionId3 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 2); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId0), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId1), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId2), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId3), 0); + + (PoolId poolId, Currency currency0, Currency currency1, uint24 fee, uint24 binId) = + binFungiblePositionManager.positions(positionId0); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID - 1); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId1); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId2); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID + 1); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3); + } + + function testMigrateFromV2Refund() public { + // 1. mint some liquidity to the v2 pair + _mintV2Liquidity(v2Pair, 10 ether, 10 ether); + uint256 lpTokenBefore = v2Pair.balanceOf(address(this)); + assertGt(lpTokenBefore, 0); + + // 2. make sure migrator can transfer user's v2 lp token + v2Pair.approve(address(migrator), lpTokenBefore); + + // 3. initialize the pool + migrator.initialize(poolKey, ACTIVE_BIN_ID, bytes("")); + + IBaseMigrator.V2PoolParams memory v2PoolParams = IBaseMigrator.V2PoolParams({ + pair: address(v2Pair), + migrateAmount: lpTokenBefore, + // the order of token0 and token1 respect to the pair + // but may mismatch the order of v4 pool key when WETH is invovled + amount0Min: 9.99 ether, + amount1Min: 9.99 ether + }); + + IBinFungiblePositionManager.AddLiquidityParams memory params = + _getAddParams(poolKey, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this)); + + int256[] memory deltaIds = new int256[](2); + deltaIds[0] = params.deltaIds[0]; + deltaIds[1] = params.deltaIds[1]; + + uint256[] memory distributionX = new uint256[](2); + distributionX[0] = params.distributionX[0]; + distributionX[1] = params.distributionX[1]; + + uint256[] memory distributionY = new uint256[](2); + distributionY[0] = params.distributionY[0]; + distributionY[1] = params.distributionY[1]; + + // delete the last distribution point so that the refund is triggered + // we expect to get 50% of tokenX back + // (0, 50%) (50%, 50%) (50%, 0) => (0, 50%) (50%, 50%) + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: params.poolKey, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: deltaIds, + distributionX: distributionX, + distributionY: distributionY, + to: params.to, + deadline: params.deadline + }); + + uint256 balance0Before = address(this).balance; + uint256 balance1Before = token0.balanceOf(address(this)); + + // 4. migrate from v2 to v4, not sending ETH denotes pay by WETH + migrator.migrateFromV2(v2PoolParams, v4BinPoolParams, 0, 0); + + // necessary checks + // refund 5 ether in the form of native token + assertApproxEqAbs(address(this).balance - balance0Before, 5 ether, 0.000001 ether); + assertEq(balance1Before - token0.balanceOf(address(this)), 0 ether); + // WETH balance unchanged + assertEq(weth.balanceOf(address(this)), 90 ether); + + // v2 pair should be burned already + assertEq(v2Pair.balanceOf(address(this)), 0); + + // make sure liuqidty is minted to the correct pool + assertApproxEqAbs(address(vault).balance, 5 ether, 0.000001 ether); + assertApproxEqAbs(token0.balanceOf(address(vault)), 10 ether, 0.000001 ether); + + uint256 positionId0 = poolKey.toId().toTokenId(ACTIVE_BIN_ID - 1); + uint256 positionId1 = poolKey.toId().toTokenId(ACTIVE_BIN_ID); + uint256 positionId2 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 1); + uint256 positionId3 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 2); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId0), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId1), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId2), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId3), 0); + + (PoolId poolId, Currency currency0, Currency currency1, uint24 fee, uint24 binId) = + binFungiblePositionManager.positions(positionId0); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID - 1); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId1); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId2); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3); + } + + function testMigrateFromV2RefundNonNativeToken() public { + // 1. mint some liquidity to the v2 pair + _mintV2Liquidity(v2PairWithoutNativeToken, 10 ether, 10 ether); + uint256 lpTokenBefore = v2PairWithoutNativeToken.balanceOf(address(this)); + assertGt(lpTokenBefore, 0); + + // 2. make sure migrator can transfer user's v2 lp token + v2PairWithoutNativeToken.approve(address(migrator), lpTokenBefore); + + // 3. initialize the pool + migrator.initialize(poolKeyWithoutNativeToken, ACTIVE_BIN_ID, bytes("")); + + IBaseMigrator.V2PoolParams memory v2PoolParams = IBaseMigrator.V2PoolParams({ + pair: address(v2PairWithoutNativeToken), + migrateAmount: lpTokenBefore, + // the order of token0 and token1 respect to the pair + // but may mismatch the order of v4 pool key when WETH is invovled + amount0Min: 9.999 ether, + amount1Min: 9.999 ether + }); + + IBinFungiblePositionManager.AddLiquidityParams memory params = _getAddParams( + poolKeyWithoutNativeToken, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this) + ); + + int256[] memory deltaIds = new int256[](2); + deltaIds[0] = params.deltaIds[0]; + deltaIds[1] = params.deltaIds[1]; + + uint256[] memory distributionX = new uint256[](2); + distributionX[0] = params.distributionX[0]; + distributionX[1] = params.distributionX[1]; + + uint256[] memory distributionY = new uint256[](2); + distributionY[0] = params.distributionY[0]; + distributionY[1] = params.distributionY[1]; + + // delete the last distribution point so that the refund is triggered + // we expect to get 50% of tokenX back + // (0, 50%) (50%, 50%) (50%, 0) => (0, 50%) (50%, 50%) + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: params.poolKey, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: deltaIds, + distributionX: distributionX, + distributionY: distributionY, + to: params.to, + deadline: params.deadline + }); + + uint256 balance0Before = token0.balanceOf(address(this)); + uint256 balance1Before = token1.balanceOf(address(this)); + + // 4. migrate from v2 to v4 + migrator.migrateFromV2(v2PoolParams, v4BinPoolParams, 0, 0); + + // necessary checks + + // refund 5 ether of token0 + assertApproxEqAbs(token0.balanceOf(address(this)) - balance0Before, 5 ether, 0.000001 ether); + assertEq(balance1Before - token1.balanceOf(address(this)), 0 ether); + // WETH balance unchanged + assertEq(weth.balanceOf(address(this)), 100 ether); + + // v2 pair should be burned already + assertEq(v2PairWithoutNativeToken.balanceOf(address(this)), 0); + + // make sure liuqidty is minted to the correct pool + assertApproxEqAbs(token0.balanceOf(address(vault)), 5 ether, 0.000001 ether); + assertApproxEqAbs(token1.balanceOf(address(vault)), 10 ether, 0.000001 ether); + + uint256 positionId0 = poolKeyWithoutNativeToken.toId().toTokenId(ACTIVE_BIN_ID - 1); + uint256 positionId1 = poolKeyWithoutNativeToken.toId().toTokenId(ACTIVE_BIN_ID); + uint256 positionId2 = poolKeyWithoutNativeToken.toId().toTokenId(ACTIVE_BIN_ID + 1); + uint256 positionId3 = poolKeyWithoutNativeToken.toId().toTokenId(ACTIVE_BIN_ID + 2); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId0), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId1), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId2), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId3), 0); + + (PoolId poolId, Currency currency0, Currency currency1, uint24 fee, uint24 binId) = + binFungiblePositionManager.positions(positionId0); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKeyWithoutNativeToken.toId())); + assertEq(Currency.unwrap(currency0), address(token0)); + assertEq(Currency.unwrap(currency1), address(token1)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID - 1); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId1); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKeyWithoutNativeToken.toId())); + assertEq(Currency.unwrap(currency0), address(token0)); + assertEq(Currency.unwrap(currency1), address(token1)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId2); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3); + } + + function testMigrateFromV2ThroughOffchainSign() public { + // 1. mint some liquidity to the v2 pair + _mintV2Liquidity(v2Pair); + uint256 lpTokenBefore = v2Pair.balanceOf(address(this)); + assertGt(lpTokenBefore, 0); + + // 2. make sure migrator can transfer user's v2 lp token + // v2Pair.approve(address(migrator), lpTokenBefore); + (address userAddr, uint256 userPrivateKey) = makeAddrAndKey("user"); + + // 2.a transfer the lp token to the user + v2Pair.transfer(userAddr, lpTokenBefore); + + uint256 ddl = block.timestamp + 100; + // 2.b prepare the hash + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + userAddr, + address(migrator), + lpTokenBefore, + v2Pair.nonces(userAddr), + ddl + ) + ); + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", v2Pair.DOMAIN_SEPARATOR(), structHash)); + + // 2.c generate the signature + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, hash); + + IBaseMigrator.V2PoolParams memory v2PoolParams = IBaseMigrator.V2PoolParams({ + pair: address(v2Pair), + migrateAmount: lpTokenBefore, + // minor precision loss is acceptable + amount0Min: 9.999 ether, + amount1Min: 9.999 ether + }); + + IBinFungiblePositionManager.AddLiquidityParams memory params = + _getAddParams(poolKey, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this)); + + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: params.poolKey, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: params.deltaIds, + distributionX: params.distributionX, + distributionY: params.distributionY, + to: params.to, + deadline: params.deadline + }); + + // 3. multicall, combine permit, initialize and migrateFromV2 + bytes[] memory data = new bytes[](3); + data[0] = abi.encodeWithSelector(migrator.selfPermit.selector, v2Pair, lpTokenBefore, ddl, v, r, s); + data[1] = abi.encodeWithSelector(migrator.initialize.selector, poolKey, ACTIVE_BIN_ID, bytes("")); + data[2] = abi.encodeWithSelector(migrator.migrateFromV2.selector, v2PoolParams, v4BinPoolParams, 0, 0); + vm.prank(userAddr); + migrator.multicall(data); + + // necessary checks + // v2 pair should be burned already + assertEq(v2Pair.balanceOf(address(this)), 0); + + // make sure liuqidty is minted to the correct pool + assertApproxEqAbs(address(vault).balance, 10 ether, 0.000001 ether); + assertApproxEqAbs(token0.balanceOf(address(vault)), 10 ether, 0.000001 ether); + + uint256 positionId0 = poolKey.toId().toTokenId(ACTIVE_BIN_ID - 1); + uint256 positionId1 = poolKey.toId().toTokenId(ACTIVE_BIN_ID); + uint256 positionId2 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 1); + uint256 positionId3 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 2); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId0), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId1), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId2), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId3), 0); + + (PoolId poolId, Currency currency0, Currency currency1, uint24 fee, uint24 binId) = + binFungiblePositionManager.positions(positionId0); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID - 1); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId1); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId2); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID + 1); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3); + } + + function _mintV2Liquidity(IPancakePair pair) public { + IERC20(pair.token0()).transfer(address(pair), 10 ether); + IERC20(pair.token1()).transfer(address(pair), 10 ether); + + pair.mint(address(this)); + } + + function _mintV2Liquidity(IPancakePair pair, uint256 amount0, uint256 amount1) public { + IERC20(pair.token0()).transfer(address(pair), amount0); + IERC20(pair.token1()).transfer(address(pair), amount1); + + pair.mint(address(this)); + } + + receive() external payable {} +} diff --git a/test/pool-bin/migrator/BinMigratorFromV3.sol b/test/pool-bin/migrator/BinMigratorFromV3.sol new file mode 100644 index 0000000..2f192d1 --- /dev/null +++ b/test/pool-bin/migrator/BinMigratorFromV3.sol @@ -0,0 +1,1163 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {OldVersionHelper} from "../../helpers/OldVersionHelper.sol"; +import {IPancakePair} from "../../../src/interfaces/external/IPancakePair.sol"; +import {WETH} from "solmate/tokens/WETH.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {BinMigrator} from "../../../src/pool-bin/BinMigrator.sol"; +import {IBinMigrator, IBaseMigrator} from "../../../src/pool-bin/interfaces/IBinMigrator.sol"; +import {BinFungiblePositionManager} from "../../../src/pool-bin/BinFungiblePositionManager.sol"; +import {Vault} from "pancake-v4-core/src/Vault.sol"; +import {BinPoolManager} from "pancake-v4-core/src/pool-bin/BinPoolManager.sol"; +import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol"; +import {BinPoolParametersHelper} from "pancake-v4-core/src/pool-bin/libraries/BinPoolParametersHelper.sol"; +import {Currency} from "pancake-v4-core/src/types/Currency.sol"; +import {IPoolManager} from "pancake-v4-core/src/interfaces/IPoolManager.sol"; +import {IHooks} from "pancake-v4-core/src/interfaces/IHooks.sol"; +import {PoolId, PoolIdLibrary} from "pancake-v4-core/src/types/PoolId.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {IV3NonfungiblePositionManager} from "../../../src/interfaces/external/IV3NonfungiblePositionManager.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {LiquidityParamsHelper, IBinFungiblePositionManager} from "../helpers/LiquidityParamsHelper.sol"; +import {BinTokenLibrary} from "../../../src/pool-bin/libraries/BinTokenLibrary.sol"; + +interface IPancakeV3LikePairFactory { + function createPool(address tokenA, address tokenB, uint24 fee) external returns (address pool); +} + +abstract contract BinMigratorFromV3 is OldVersionHelper, LiquidityParamsHelper, GasSnapshot { + using BinPoolParametersHelper for bytes32; + using PoolIdLibrary for PoolKey; + using BinTokenLibrary for PoolId; + + uint160 public constant INIT_SQRT_PRICE = 79228162514264337593543950336; + // 1 tokenX = 1 tokenY + uint24 public constant ACTIVE_BIN_ID = 2 ** 23; + + WETH weth; + MockERC20 token0; + MockERC20 token1; + + Vault vault; + BinPoolManager poolManager; + BinFungiblePositionManager binFungiblePositionManager; + IBinMigrator migrator; + PoolKey poolKey; + PoolKey poolKeyWithoutNativeToken; + + IPancakeV3LikePairFactory v3Factory; + IV3NonfungiblePositionManager v3Nfpm; + + function _getDeployerBytecodePath() internal pure virtual returns (string memory); + function _getFactoryBytecodePath() internal pure virtual returns (string memory); + function _getNfpmBytecodePath() internal pure virtual returns (string memory); + + function _getContractName() internal pure virtual returns (string memory); + + function setUp() public { + weth = new WETH(); + token0 = new MockERC20("Token0", "TKN0", 18); + token1 = new MockERC20("Token1", "TKN1", 18); + (token0, token1) = token0 < token1 ? (token0, token1) : (token1, token0); + + // init v4 nfpm & migrator + vault = new Vault(); + poolManager = new BinPoolManager(vault, 3000); + vault.registerApp(address(poolManager)); + binFungiblePositionManager = new BinFungiblePositionManager(vault, poolManager, address(weth)); + migrator = new BinMigrator(address(weth), address(binFungiblePositionManager)); + + poolKey = PoolKey({ + // WETH after migration will be native token + currency0: Currency.wrap(address(0)), + currency1: Currency.wrap(address(token0)), + hooks: IHooks(address(0)), + poolManager: poolManager, + fee: 0, + parameters: bytes32(0).setBinStep(1) + }); + + poolKeyWithoutNativeToken = poolKey; + poolKeyWithoutNativeToken.currency0 = Currency.wrap(address(token0)); + poolKeyWithoutNativeToken.currency1 = Currency.wrap(address(token1)); + + // make sure the contract has enough balance + // WETH: 100 ether + // Token: 100 ether + // ETH: 90 ether + deal(address(this), 1000 ether); + weth.deposit{value: 100 ether}(); + token0.mint(address(this), 100 ether); + token1.mint(address(this), 100 ether); + + // pcs v3 + if (bytes(_getDeployerBytecodePath()).length != 0) { + address deployer = createContractThroughBytecode(_getDeployerBytecodePath()); + v3Factory = IPancakeV3LikePairFactory( + createContractThroughBytecode(_getFactoryBytecodePath(), toBytes32(address(deployer))) + ); + (bool success,) = deployer.call(abi.encodeWithSignature("setFactoryAddress(address)", address(v3Factory))); + require(success, "setFactoryAddress failed"); + v3Nfpm = IV3NonfungiblePositionManager( + createContractThroughBytecode( + _getNfpmBytecodePath(), + toBytes32(deployer), + toBytes32(address(v3Factory)), + toBytes32(address(weth)), + 0 + ) + ); + } else { + v3Factory = IPancakeV3LikePairFactory(createContractThroughBytecode(_getFactoryBytecodePath())); + + v3Nfpm = IV3NonfungiblePositionManager( + createContractThroughBytecode( + _getNfpmBytecodePath(), toBytes32(address(v3Factory)), toBytes32(address(weth)), 0 + ) + ); + } + + // make sure v3Nfpm has allowance + weth.approve(address(v3Nfpm), type(uint256).max); + token0.approve(address(v3Nfpm), type(uint256).max); + token1.approve(address(v3Nfpm), type(uint256).max); + } + + function testMigrateFromV3IncludingInit() public { + // 1. mint some liquidity to the v3 pool + _mintV3Liquidity(address(weth), address(token0)); + assertEq(v3Nfpm.ownerOf(1), address(this)); + (,,,,,,, uint128 liquidityFromV3Before,,,,) = v3Nfpm.positions(1); + assertGt(liquidityFromV3Before, 0); + + // 2. make sure migrator can transfer user's v3 lp token + v3Nfpm.approve(address(migrator), 1); + + IBaseMigrator.V3PoolParams memory v3PoolParams = IBaseMigrator.V3PoolParams({ + nfp: address(v3Nfpm), + tokenId: 1, + liquidity: liquidityFromV3Before, + amount0Min: 9.9 ether, + amount1Min: 9.9 ether, + collectFee: false, + deadline: block.timestamp + 100 + }); + + IBinFungiblePositionManager.AddLiquidityParams memory params = + _getAddParams(poolKey, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this)); + + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: params.poolKey, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: params.deltaIds, + distributionX: params.distributionX, + distributionY: params.distributionY, + to: params.to, + deadline: params.deadline + }); + + // 3. multicall, combine initialize and migrateFromV3 + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeWithSelector(migrator.initialize.selector, poolKey, ACTIVE_BIN_ID, bytes("")); + data[1] = abi.encodeWithSelector(migrator.migrateFromV3.selector, v3PoolParams, v4BinPoolParams, 0, 0); + snapStart(string(abi.encodePacked(_getContractName(), "#testMigrateFromV3IncludingInit"))); + migrator.multicall(data); + snapEnd(); + + // necessary checks + // v3 liqudity should be 0 + (,,,,,,, uint128 liquidityFromV3After,,,,) = v3Nfpm.positions(1); + assertEq(liquidityFromV3After, 0); + + // make sure liuqidty is minted to the correct pooA + assertApproxEqAbs(address(vault).balance, 10 ether, 0.000001 ether); + assertApproxEqAbs(token0.balanceOf(address(vault)), 10 ether, 0.000001 ether); + + uint256 positionId0 = poolKey.toId().toTokenId(ACTIVE_BIN_ID - 1); + uint256 positionId1 = poolKey.toId().toTokenId(ACTIVE_BIN_ID); + uint256 positionId2 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 1); + uint256 positionId3 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 2); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId0), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId1), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId2), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId3), 0); + + (PoolId poolId, Currency currency0, Currency currency1, uint24 fee, uint24 binId) = + binFungiblePositionManager.positions(positionId0); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID - 1); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId1); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId2); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID + 1); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3); + } + + function testMigrateFromV3TokenMismatch() public { + // 1. mint some liquidity to the v3 pool + _mintV3Liquidity(address(weth), address(token0)); + assertEq(v3Nfpm.ownerOf(1), address(this)); + (,,,,,,, uint128 liquidityFromV3Before,,,,) = v3Nfpm.positions(1); + assertGt(liquidityFromV3Before, 0); + + // 2. make sure migrator can transfer user's v3 lp token + v3Nfpm.approve(address(migrator), 1); + + IBaseMigrator.V3PoolParams memory v3PoolParams = IBaseMigrator.V3PoolParams({ + nfp: address(v3Nfpm), + tokenId: 1, + liquidity: liquidityFromV3Before, + amount0Min: 9.9 ether, + amount1Min: 9.9 ether, + collectFee: false, + deadline: block.timestamp + 100 + }); + + IBinFungiblePositionManager.AddLiquidityParams memory params = + _getAddParams(poolKey, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this)); + + // v3 weth, token0 + // v4 ETH, token1 + PoolKey memory poolKeyMismatch = poolKey; + poolKeyMismatch.currency1 = Currency.wrap(address(token1)); + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: poolKeyMismatch, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: params.deltaIds, + distributionX: params.distributionX, + distributionY: params.distributionY, + to: params.to, + deadline: params.deadline + }); + + // 3. multicall, combine initialize and migrateFromV3 + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeWithSelector(migrator.initialize.selector, poolKeyMismatch, ACTIVE_BIN_ID, bytes("")); + data[1] = abi.encodeWithSelector(migrator.migrateFromV3.selector, v3PoolParams, v4BinPoolParams, 0, 0); + vm.expectRevert(); + migrator.multicall(data); + + { + // v3 weth, token0 + // v4 token0, token1 + poolKeyMismatch.currency0 = Currency.wrap(address(token0)); + poolKeyMismatch.currency1 = Currency.wrap(address(token1)); + v4BinPoolParams.poolKey = poolKeyMismatch; + data = new bytes[](2); + data[0] = abi.encodeWithSelector(migrator.initialize.selector, poolKeyMismatch, ACTIVE_BIN_ID, bytes("")); + data[1] = abi.encodeWithSelector(migrator.migrateFromV3.selector, v3PoolParams, v4BinPoolParams, 0, 0); + vm.expectRevert(); + migrator.multicall(data); + } + } + + function testMigrateFromV3WithoutInit() public { + // 1. mint some liquidity to the v3 pool + _mintV3Liquidity(address(weth), address(token0)); + assertEq(v3Nfpm.ownerOf(1), address(this)); + (,,,,,,, uint128 liquidityFromV3Before,,,,) = v3Nfpm.positions(1); + assertGt(liquidityFromV3Before, 0); + + // 2. make sure migrator can transfer user's v3 lp token + v3Nfpm.approve(address(migrator), 1); + + // 3. initialize the pool + migrator.initialize(poolKey, ACTIVE_BIN_ID, bytes("")); + + IBaseMigrator.V3PoolParams memory v3PoolParams = IBaseMigrator.V3PoolParams({ + nfp: address(v3Nfpm), + tokenId: 1, + liquidity: liquidityFromV3Before, + amount0Min: 9.9 ether, + amount1Min: 9.9 ether, + collectFee: false, + deadline: block.timestamp + 100 + }); + + IBinFungiblePositionManager.AddLiquidityParams memory params = + _getAddParams(poolKey, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this)); + + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: params.poolKey, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: params.deltaIds, + distributionX: params.distributionX, + distributionY: params.distributionY, + to: params.to, + deadline: params.deadline + }); + + // 4. migrateFromV3 directly given pool has been initialized + snapStart(string(abi.encodePacked(_getContractName(), "#testMigrateFromV3WithoutInit"))); + migrator.migrateFromV3(v3PoolParams, v4BinPoolParams, 0, 0); + snapEnd(); + + // necessary checks + // v3 liqudity should be 0 + (,,,,,,, uint128 liquidityFromV3After,,,,) = v3Nfpm.positions(1); + assertEq(liquidityFromV3After, 0); + + // make sure liuqidty is minted to the correct pool + assertApproxEqAbs(address(vault).balance, 10 ether, 0.000001 ether); + assertApproxEqAbs(token0.balanceOf(address(vault)), 10 ether, 0.000001 ether); + + uint256 positionId0 = poolKey.toId().toTokenId(ACTIVE_BIN_ID - 1); + uint256 positionId1 = poolKey.toId().toTokenId(ACTIVE_BIN_ID); + uint256 positionId2 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 1); + uint256 positionId3 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 2); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId0), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId1), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId2), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId3), 0); + + (PoolId poolId, Currency currency0, Currency currency1, uint24 fee, uint24 binId) = + binFungiblePositionManager.positions(positionId0); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID - 1); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId1); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId2); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID + 1); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3); + } + + function testMigrateFromV3WithoutNativeToken() public { + // 1. mint some liquidity to the v3 pool + _mintV3Liquidity(address(token0), address(token1)); + + assertEq(v3Nfpm.ownerOf(1), address(this)); + (,,,,,,, uint128 liquidityFromV3Before,,,,) = v3Nfpm.positions(1); + assertGt(liquidityFromV3Before, 0); + + // 2. make sure migrator can transfer user's v3 lp token + v3Nfpm.approve(address(migrator), 1); + + // 3. initialize the pool + migrator.initialize(poolKeyWithoutNativeToken, ACTIVE_BIN_ID, bytes("")); + + IBaseMigrator.V3PoolParams memory v3PoolParams = IBaseMigrator.V3PoolParams({ + nfp: address(v3Nfpm), + tokenId: 1, + liquidity: liquidityFromV3Before, + amount0Min: 9.9 ether, + amount1Min: 9.9 ether, + collectFee: false, + deadline: block.timestamp + 100 + }); + + IBinFungiblePositionManager.AddLiquidityParams memory params = _getAddParams( + poolKeyWithoutNativeToken, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this) + ); + + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: params.poolKey, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: params.deltaIds, + distributionX: params.distributionX, + distributionY: params.distributionY, + to: params.to, + deadline: params.deadline + }); + + // 4. migrate from v3 to v4 + snapStart(string(abi.encodePacked(_getContractName(), "#testMigrateFromV3WithoutNativeToken"))); + migrator.migrateFromV3(v3PoolParams, v4BinPoolParams, 0, 0); + snapEnd(); + + // necessary checks + // v3 liqudity should be 0 + (,,,,,,, uint128 liquidityFromV3After,,,,) = v3Nfpm.positions(1); + assertEq(liquidityFromV3After, 0); + + // make sure liuqidty is minted to the correct pool + assertApproxEqAbs(token0.balanceOf(address(vault)), 10 ether, 0.000001 ether); + assertApproxEqAbs(token1.balanceOf(address(vault)), 10 ether, 0.000001 ether); + + uint256 positionId0 = poolKeyWithoutNativeToken.toId().toTokenId(ACTIVE_BIN_ID - 1); + uint256 positionId1 = poolKeyWithoutNativeToken.toId().toTokenId(ACTIVE_BIN_ID); + uint256 positionId2 = poolKeyWithoutNativeToken.toId().toTokenId(ACTIVE_BIN_ID + 1); + uint256 positionId3 = poolKeyWithoutNativeToken.toId().toTokenId(ACTIVE_BIN_ID + 2); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId0), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId1), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId2), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId3), 0); + + (PoolId poolId, Currency currency0, Currency currency1, uint24 fee, uint24 binId) = + binFungiblePositionManager.positions(positionId0); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKeyWithoutNativeToken.toId())); + assertEq(Currency.unwrap(currency0), address(token0)); + assertEq(Currency.unwrap(currency1), address(token1)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID - 1); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId1); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKeyWithoutNativeToken.toId())); + assertEq(Currency.unwrap(currency0), address(token0)); + assertEq(Currency.unwrap(currency1), address(token1)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId2); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKeyWithoutNativeToken.toId())); + assertEq(Currency.unwrap(currency0), address(token0)); + assertEq(Currency.unwrap(currency1), address(token1)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID + 1); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3); + } + + function testMigrateFromV3AddExtraAmount() public { + // 1. mint some liquidity to the v3 pool + _mintV3Liquidity(address(weth), address(token0)); + assertEq(v3Nfpm.ownerOf(1), address(this)); + (,,,,,,, uint128 liquidityFromV3Before,,,,) = v3Nfpm.positions(1); + assertGt(liquidityFromV3Before, 0); + + // 2. make sure migrator can transfer user's v3 lp token + v3Nfpm.approve(address(migrator), 1); + + // 3. init the pool + migrator.initialize(poolKey, ACTIVE_BIN_ID, bytes("")); + + IBaseMigrator.V3PoolParams memory v3PoolParams = IBaseMigrator.V3PoolParams({ + nfp: address(v3Nfpm), + tokenId: 1, + liquidity: liquidityFromV3Before, + amount0Min: 9.9 ether, + amount1Min: 9.9 ether, + collectFee: false, + deadline: block.timestamp + 100 + }); + + IBinFungiblePositionManager.AddLiquidityParams memory params = + _getAddParams(poolKey, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this)); + + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: params.poolKey, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: params.deltaIds, + distributionX: params.distributionX, + distributionY: params.distributionY, + to: params.to, + deadline: params.deadline + }); + + uint256 balance0Before = address(this).balance; + uint256 balance1Before = token0.balanceOf(address(this)); + + IERC20(address(token0)).approve(address(migrator), 20 ether); + // 4. migrate from v3 to v4 + migrator.migrateFromV3{value: 20 ether}(v3PoolParams, v4BinPoolParams, 20 ether, 20 ether); + + // necessary checks + // consumed extra 20 ether from user + assertApproxEqAbs(balance0Before - address(this).balance, 20 ether, 0.000001 ether); + assertApproxEqAbs(balance1Before - token0.balanceOf(address(this)), 20 ether, 0.000001 ether); + // WETH balance unchanged + assertEq(weth.balanceOf(address(this)), 90 ether); + + // v3 liqudity should be 0 + (,,,,,,, uint128 liquidityFromV3After,,,,) = v3Nfpm.positions(1); + assertEq(liquidityFromV3After, 0); + + // make sure liuqidty is minted to the correct pool + assertApproxEqAbs(address(vault).balance, 30 ether, 0.000001 ether); + assertApproxEqAbs(token0.balanceOf(address(vault)), 30 ether, 0.000001 ether); + + uint256 positionId0 = poolKey.toId().toTokenId(ACTIVE_BIN_ID - 1); + uint256 positionId1 = poolKey.toId().toTokenId(ACTIVE_BIN_ID); + uint256 positionId2 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 1); + uint256 positionId3 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 2); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId0), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId1), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId2), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId3), 0); + + (PoolId poolId, Currency currency0, Currency currency1, uint24 fee, uint24 binId) = + binFungiblePositionManager.positions(positionId0); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID - 1); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId1); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId2); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID + 1); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3); + } + + function testMigrateFromV3AddExtraAmountThroughWETH() public { + // 1. mint some liquidity to the v3 pool + _mintV3Liquidity(address(weth), address(token0)); + assertEq(v3Nfpm.ownerOf(1), address(this)); + (,,,,,,, uint128 liquidityFromV3Before,,,,) = v3Nfpm.positions(1); + assertGt(liquidityFromV3Before, 0); + + // 2. make sure migrator can transfer user's v3 lp token + v3Nfpm.approve(address(migrator), 1); + + // 3. init the pool + migrator.initialize(poolKey, ACTIVE_BIN_ID, bytes("")); + + IBaseMigrator.V3PoolParams memory v3PoolParams = IBaseMigrator.V3PoolParams({ + nfp: address(v3Nfpm), + tokenId: 1, + liquidity: liquidityFromV3Before, + amount0Min: 9.9 ether, + amount1Min: 9.9 ether, + collectFee: false, + deadline: block.timestamp + 100 + }); + + IBinFungiblePositionManager.AddLiquidityParams memory params = + _getAddParams(poolKey, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this)); + + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: params.poolKey, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: params.deltaIds, + distributionX: params.distributionX, + distributionY: params.distributionY, + to: params.to, + deadline: params.deadline + }); + + uint256 balance0Before = address(this).balance; + uint256 balance1Before = token0.balanceOf(address(this)); + + weth.approve(address(migrator), 20 ether); + IERC20(address(token0)).approve(address(migrator), 20 ether); + // 4. migrate from v3 to v4, not sending ETH denotes pay by WETH + migrator.migrateFromV3(v3PoolParams, v4BinPoolParams, 20 ether, 20 ether); + + // necessary checks + // consumed extra 20 ether from user + // native token balance unchanged + assertApproxEqAbs(address(this).balance - balance0Before, 0 ether, 0.000001 ether); + assertApproxEqAbs(balance1Before - token0.balanceOf(address(this)), 20 ether, 0.00001 ether); + // consumed 20 ether WETH + assertEq(weth.balanceOf(address(this)), 70 ether); + + // v3 liqudity should be 0 + (,,,,,,, uint128 liquidityFromV3After,,,,) = v3Nfpm.positions(1); + assertEq(liquidityFromV3After, 0); + + // make sure liuqidty is minted to the correct pool + assertApproxEqAbs(address(vault).balance, 30 ether, 0.000001 ether); + assertApproxEqAbs(token0.balanceOf(address(vault)), 30 ether, 0.000001 ether); + + uint256 positionId0 = poolKey.toId().toTokenId(ACTIVE_BIN_ID - 1); + uint256 positionId1 = poolKey.toId().toTokenId(ACTIVE_BIN_ID); + uint256 positionId2 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 1); + uint256 positionId3 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 2); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId0), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId1), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId2), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId3), 0); + + (PoolId poolId, Currency currency0, Currency currency1, uint24 fee, uint24 binId) = + binFungiblePositionManager.positions(positionId0); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID - 1); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId1); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId2); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID + 1); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3); + } + + function testMigrateFromV3Refund() public { + // 1. mint some liquidity to the v3 pool + _mintV3Liquidity(address(weth), address(token0)); + assertEq(v3Nfpm.ownerOf(1), address(this)); + (,,,,,,, uint128 liquidityFromV3Before,,,,) = v3Nfpm.positions(1); + assertGt(liquidityFromV3Before, 0); + + // 2. make sure migrator can transfer user's v3 lp token + v3Nfpm.approve(address(migrator), 1); + + // 3. init the pool + migrator.initialize(poolKey, ACTIVE_BIN_ID, bytes("")); + + IBaseMigrator.V3PoolParams memory v3PoolParams = IBaseMigrator.V3PoolParams({ + nfp: address(v3Nfpm), + tokenId: 1, + liquidity: liquidityFromV3Before, + amount0Min: 0, + amount1Min: 0, + collectFee: false, + deadline: block.timestamp + 100 + }); + + // adding half of the liquidity to the pool + IBinFungiblePositionManager.AddLiquidityParams memory params = + _getAddParams(poolKey, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this)); + + int256[] memory deltaIds = new int256[](2); + deltaIds[0] = params.deltaIds[0]; + deltaIds[1] = params.deltaIds[1]; + + uint256[] memory distributionX = new uint256[](2); + distributionX[0] = params.distributionX[0]; + distributionX[1] = params.distributionX[1]; + + uint256[] memory distributionY = new uint256[](2); + distributionY[0] = params.distributionY[0]; + distributionY[1] = params.distributionY[1]; + + // delete the last distribution point so that the refund is triggered + // we expect to get 50% of tokenX back + // (0, 50%) (50%, 50%) (50%, 0) => (0, 50%) (50%, 50%) + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: params.poolKey, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: deltaIds, + distributionX: distributionX, + distributionY: distributionY, + to: params.to, + deadline: params.deadline + }); + + uint256 balance0Before = address(this).balance; + uint256 balance1Before = token0.balanceOf(address(this)); + + // 4. migrate from v3 to v4, not sending ETH denotes pay by WETH + migrator.migrateFromV3(v3PoolParams, v4BinPoolParams, 0, 0); + + // necessary checks + // refund 5 ether in the form of native token + assertApproxEqAbs(address(this).balance - balance0Before, 5.0 ether, 0.1 ether); + assertApproxEqAbs(token0.balanceOf(address(this)) - balance1Before, 0 ether, 1); + // WETH balance unchanged + assertApproxEqAbs(weth.balanceOf(address(this)), 90 ether, 0.1 ether); + + // v3 liqudity should be 0 + (,,,,,,, uint128 liquidityFromV3After,,,,) = v3Nfpm.positions(1); + assertEq(liquidityFromV3After, 0); + + // make sure liuqidty is minted to the correct pool + assertApproxEqAbs(address(vault).balance, 5 ether, 0.000001 ether); + assertApproxEqAbs(token0.balanceOf(address(vault)), 10 ether, 0.000001 ether); + + uint256 positionId0 = poolKey.toId().toTokenId(ACTIVE_BIN_ID - 1); + uint256 positionId1 = poolKey.toId().toTokenId(ACTIVE_BIN_ID); + uint256 positionId2 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 1); + uint256 positionId3 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 2); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId0), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId1), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId2), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId3), 0); + + (PoolId poolId, Currency currency0, Currency currency1, uint24 fee, uint24 binId) = + binFungiblePositionManager.positions(positionId0); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID - 1); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId1); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId2); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3); + } + + function testMigrateFromV3RefundNonNativeToken() public { + // 1. mint some liquidity to the v3 pool + _mintV3Liquidity(address(token0), address(token1)); + assertEq(v3Nfpm.ownerOf(1), address(this)); + (,,,,,,, uint128 liquidityFromV3Before,,,,) = v3Nfpm.positions(1); + assertGt(liquidityFromV3Before, 0); + + // 2. make sure migrator can transfer user's v3 lp token + v3Nfpm.approve(address(migrator), 1); + + // 3. init the pool + migrator.initialize(poolKeyWithoutNativeToken, ACTIVE_BIN_ID, bytes("")); + + IBaseMigrator.V3PoolParams memory v3PoolParams = IBaseMigrator.V3PoolParams({ + nfp: address(v3Nfpm), + tokenId: 1, + liquidity: liquidityFromV3Before, + amount0Min: 0, + amount1Min: 0, + collectFee: false, + deadline: block.timestamp + 100 + }); + + // adding half of the liquidity to the pool + IBinFungiblePositionManager.AddLiquidityParams memory params = _getAddParams( + poolKeyWithoutNativeToken, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this) + ); + + int256[] memory deltaIds = new int256[](2); + deltaIds[0] = params.deltaIds[0]; + deltaIds[1] = params.deltaIds[1]; + + uint256[] memory distributionX = new uint256[](2); + distributionX[0] = params.distributionX[0]; + distributionX[1] = params.distributionX[1]; + + uint256[] memory distributionY = new uint256[](2); + distributionY[0] = params.distributionY[0]; + distributionY[1] = params.distributionY[1]; + + // delete the last distribution point so that the refund is triggered + // we expect to get 50% of tokenX back + // (0, 50%) (50%, 50%) (50%, 0) => (0, 50%) (50%, 50%) + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: params.poolKey, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: deltaIds, + distributionX: distributionX, + distributionY: distributionY, + to: params.to, + deadline: params.deadline + }); + + uint256 balance0Before = token0.balanceOf(address(this)); + uint256 balance1Before = token1.balanceOf(address(this)); + + // 4. migrate from v3 to v4 + migrator.migrateFromV3(v3PoolParams, v4BinPoolParams, 0, 0); + + // necessary checks + + // refund 5 ether of token0 + assertApproxEqAbs(token0.balanceOf(address(this)) - balance0Before, 5 ether, 0.1 ether); + assertApproxEqAbs(token1.balanceOf(address(this)) - balance1Before, 0 ether, 1); + // WETH balance unchanged + assertEq(weth.balanceOf(address(this)), 100 ether); + + // v3 liqudity should be 0 + (,,,,,,, uint128 liquidityFromV3After,,,,) = v3Nfpm.positions(1); + assertEq(liquidityFromV3After, 0); + + // make sure liuqidty is minted to the correct pool + assertApproxEqAbs(token0.balanceOf(address(vault)), 5 ether, 0.000001 ether); + assertApproxEqAbs(token1.balanceOf(address(vault)), 10 ether, 0.000001 ether); + + uint256 positionId0 = poolKeyWithoutNativeToken.toId().toTokenId(ACTIVE_BIN_ID - 1); + uint256 positionId1 = poolKeyWithoutNativeToken.toId().toTokenId(ACTIVE_BIN_ID); + uint256 positionId2 = poolKeyWithoutNativeToken.toId().toTokenId(ACTIVE_BIN_ID + 1); + uint256 positionId3 = poolKeyWithoutNativeToken.toId().toTokenId(ACTIVE_BIN_ID + 2); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId0), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId1), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId2), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId3), 0); + + (PoolId poolId, Currency currency0, Currency currency1, uint24 fee, uint24 binId) = + binFungiblePositionManager.positions(positionId0); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKeyWithoutNativeToken.toId())); + assertEq(Currency.unwrap(currency0), address(token0)); + assertEq(Currency.unwrap(currency1), address(token1)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID - 1); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId1); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKeyWithoutNativeToken.toId())); + assertEq(Currency.unwrap(currency0), address(token0)); + assertEq(Currency.unwrap(currency1), address(token1)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId2); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3); + } + + function testMigrateFromV3FromNonOwner() public { + // 1. mint some liquidity to the v3 pool + _mintV3Liquidity(address(weth), address(token0)); + assertEq(v3Nfpm.ownerOf(1), address(this)); + (,,,,,,, uint128 liquidityFromV3Before,,,,) = v3Nfpm.positions(1); + assertGt(liquidityFromV3Before, 0); + + // 2. make sure migrator can transfer user's v3 lp token + v3Nfpm.approve(address(migrator), 1); + + // 3. init the pool + migrator.initialize(poolKey, ACTIVE_BIN_ID, bytes("")); + + IBaseMigrator.V3PoolParams memory v3PoolParams = IBaseMigrator.V3PoolParams({ + nfp: address(v3Nfpm), + tokenId: 1, + // half of the liquidity + liquidity: liquidityFromV3Before / 2, + amount0Min: 9.9 ether / 2, + amount1Min: 9.9 ether / 2, + collectFee: false, + deadline: block.timestamp + 100 + }); + + IBinFungiblePositionManager.AddLiquidityParams memory params = + _getAddParams(poolKey, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this)); + + int256[] memory deltaIds = new int256[](2); + deltaIds[0] = params.deltaIds[0]; + deltaIds[1] = params.deltaIds[1]; + + uint256[] memory distributionX = new uint256[](2); + distributionX[0] = params.distributionX[0]; + distributionX[1] = params.distributionX[1]; + + uint256[] memory distributionY = new uint256[](2); + distributionY[0] = params.distributionY[0]; + distributionY[1] = params.distributionY[1]; + + // delete the last distribution point so that the refund is triggered + // we expect to get 50% of tokenX back + // (0, 50%) (50%, 50%) (50%, 0) => (0, 50%) (50%, 50%) + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: params.poolKey, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: deltaIds, + distributionX: distributionX, + distributionY: distributionY, + to: params.to, + deadline: params.deadline + }); + + // 4. migrate half + migrator.migrateFromV3(v3PoolParams, v4BinPoolParams, 0, 0); + + // make sure there are still liquidity left in v3 position token + (,,,,,,, uint128 liquidityFromV3After,,,,) = v3Nfpm.positions(1); + assertEq(liquidityFromV3After, liquidityFromV3Before - liquidityFromV3Before / 2); + + // 5. make sure non-owner can't migrate the rest + vm.expectRevert(IBaseMigrator.NOT_TOKEN_OWNER.selector); + vm.prank(makeAddr("someone")); + migrator.migrateFromV3(v3PoolParams, v4BinPoolParams, 0, 0); + } + + function testMigrateFromV3ThroughOffchainSign() public { + // 1. mint some liquidity to the v3 pool + _mintV3Liquidity(address(weth), address(token0)); + assertEq(v3Nfpm.ownerOf(1), address(this)); + (uint96 nonce,,,,,,, uint128 liquidityFromV3Before,,,,) = v3Nfpm.positions(1); + assertGt(liquidityFromV3Before, 0); + + // 2. make sure migrator can transfer user's v3 lp token through offchain sign + // v3Nfpm.approve(address(migrator), 1); + (address userAddr, uint256 userPrivateKey) = makeAddrAndKey("user"); + + // 2.a transfer the lp token to the user + v3Nfpm.transferFrom(address(this), userAddr, 1); + + uint256 ddl = block.timestamp + 100; + // 2.b prepare the hash + bytes32 structHash = keccak256(abi.encode(v3Nfpm.PERMIT_TYPEHASH(), address(migrator), 1, nonce, ddl)); + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", v3Nfpm.DOMAIN_SEPARATOR(), structHash)); + + // 2.c generate the signature + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, hash); + + IBaseMigrator.V3PoolParams memory v3PoolParams = IBaseMigrator.V3PoolParams({ + nfp: address(v3Nfpm), + tokenId: 1, + liquidity: liquidityFromV3Before, + amount0Min: 9.9 ether, + amount1Min: 9.9 ether, + collectFee: false, + deadline: block.timestamp + 100 + }); + + IBinFungiblePositionManager.AddLiquidityParams memory params = + _getAddParams(poolKey, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this)); + + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: params.poolKey, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: params.deltaIds, + distributionX: params.distributionX, + distributionY: params.distributionY, + to: params.to, + deadline: params.deadline + }); + + // 3. multicall, combine selfPermitERC721, initialize and migrateFromV3 + bytes[] memory data = new bytes[](3); + data[0] = abi.encodeWithSelector(migrator.selfPermitERC721.selector, v3Nfpm, 1, ddl, v, r, s); + data[1] = abi.encodeWithSelector(migrator.initialize.selector, poolKey, ACTIVE_BIN_ID, bytes("")); + data[2] = abi.encodeWithSelector(migrator.migrateFromV3.selector, v3PoolParams, v4BinPoolParams, 0, 0); + vm.prank(userAddr); + migrator.multicall(data); + + // necessary checks + // v3 liqudity should be 0 + (,,,,,,, uint128 liquidityFromV3After,,,,) = v3Nfpm.positions(1); + assertEq(liquidityFromV3After, 0); + + // make sure liuqidty is minted to the correct pooA + assertApproxEqAbs(address(vault).balance, 10 ether, 0.000001 ether); + assertApproxEqAbs(token0.balanceOf(address(vault)), 10 ether, 0.000001 ether); + + uint256 positionId0 = poolKey.toId().toTokenId(ACTIVE_BIN_ID - 1); + uint256 positionId1 = poolKey.toId().toTokenId(ACTIVE_BIN_ID); + uint256 positionId2 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 1); + uint256 positionId3 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 2); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId0), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId1), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId2), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId3), 0); + + (PoolId poolId, Currency currency0, Currency currency1, uint24 fee, uint24 binId) = + binFungiblePositionManager.positions(positionId0); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID - 1); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId1); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId2); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID + 1); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3); + } + + function testMigrateFromV3ThroughOffchainSignPayWithETH() public { + // 1. mint some liquidity to the v3 pool + _mintV3Liquidity(address(weth), address(token0)); + assertEq(v3Nfpm.ownerOf(1), address(this)); + (uint96 nonce,,,,,,, uint128 liquidityFromV3Before,,,,) = v3Nfpm.positions(1); + assertGt(liquidityFromV3Before, 0); + + // 2. make sure migrator can transfer user's v3 lp token through offchain sign + // v3Nfpm.approve(address(migrator), 1); + (address userAddr, uint256 userPrivateKey) = makeAddrAndKey("user"); + + // 2.a transfer the lp token to the user + v3Nfpm.transferFrom(address(this), userAddr, 1); + + uint256 ddl = block.timestamp + 100; + // 2.b prepare the hash + bytes32 structHash = keccak256(abi.encode(v3Nfpm.PERMIT_TYPEHASH(), address(migrator), 1, nonce, ddl)); + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", v3Nfpm.DOMAIN_SEPARATOR(), structHash)); + + // 2.c generate the signature + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, hash); + + IBaseMigrator.V3PoolParams memory v3PoolParams = IBaseMigrator.V3PoolParams({ + nfp: address(v3Nfpm), + tokenId: 1, + liquidity: liquidityFromV3Before, + amount0Min: 9.9 ether, + amount1Min: 9.9 ether, + collectFee: false, + deadline: block.timestamp + 100 + }); + + IBinFungiblePositionManager.AddLiquidityParams memory params = + _getAddParams(poolKey, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this)); + + IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({ + poolKey: params.poolKey, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + activeIdDesired: params.activeIdDesired, + idSlippage: params.idSlippage, + deltaIds: params.deltaIds, + distributionX: params.distributionX, + distributionY: params.distributionY, + to: params.to, + deadline: params.deadline + }); + + // make the guy rich + token0.transfer(userAddr, 10 ether); + deal(userAddr, 10 ether); + + vm.prank(userAddr); + token0.approve(address(migrator), 10 ether); + + // 3. multicall, combine selfPermitERC721, initialize and migrateFromV3 + bytes[] memory data = new bytes[](3); + data[0] = abi.encodeWithSelector(migrator.selfPermitERC721.selector, v3Nfpm, 1, ddl, v, r, s); + data[1] = abi.encodeWithSelector(migrator.initialize.selector, poolKey, ACTIVE_BIN_ID, bytes("")); + data[2] = + abi.encodeWithSelector(migrator.migrateFromV3.selector, v3PoolParams, v4BinPoolParams, 10 ether, 10 ether); + vm.prank(userAddr); + migrator.multicall{value: 10 ether}(data); + + // necessary checks + // v3 liqudity should be 0 + (,,,,,,, uint128 liquidityFromV3After,,,,) = v3Nfpm.positions(1); + assertEq(liquidityFromV3After, 0); + + // make sure liuqidty is minted to the correct pooA + assertApproxEqAbs(address(vault).balance, 20 ether, 0.000001 ether); + assertApproxEqAbs(token0.balanceOf(address(vault)), 20 ether, 0.000001 ether); + + uint256 positionId0 = poolKey.toId().toTokenId(ACTIVE_BIN_ID - 1); + uint256 positionId1 = poolKey.toId().toTokenId(ACTIVE_BIN_ID); + uint256 positionId2 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 1); + uint256 positionId3 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 2); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId0), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId1), 0); + assertGt(binFungiblePositionManager.balanceOf(address(this), positionId2), 0); + assertEq(binFungiblePositionManager.balanceOf(address(this), positionId3), 0); + + (PoolId poolId, Currency currency0, Currency currency1, uint24 fee, uint24 binId) = + binFungiblePositionManager.positions(positionId0); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID - 1); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId1); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID); + + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId2); + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(binId, ACTIVE_BIN_ID + 1); + + vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector); + (poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3); + } + + function _mintV3Liquidity(address _token0, address _token1) internal { + (_token0, _token1) = _token0 < _token1 ? (_token0, _token1) : (_token1, _token0); + v3Nfpm.createAndInitializePoolIfNecessary(_token0, _token1, 500, INIT_SQRT_PRICE); + IV3NonfungiblePositionManager.MintParams memory mintParams = IV3NonfungiblePositionManager.MintParams({ + token0: _token0, + token1: _token1, + fee: 500, + tickLower: -100, + tickUpper: 100, + amount0Desired: 10 ether, + amount1Desired: 10 ether, + amount0Min: 0, + amount1Min: 0, + recipient: address(this), + deadline: block.timestamp + 100 + }); + + v3Nfpm.mint(mintParams); + } + + receive() external payable {} +} diff --git a/test/pool-cl/migrator/CLMigratorFromV2.sol b/test/pool-cl/migrator/CLMigratorFromV2.sol index 7c3d28e..b8e6205 100644 --- a/test/pool-cl/migrator/CLMigratorFromV2.sol +++ b/test/pool-cl/migrator/CLMigratorFromV2.sol @@ -165,6 +165,59 @@ abstract contract CLMigratorFromV2 is OldVersionHelper, GasSnapshot { assertApproxEqAbs(token0.balanceOf(address(vault)), 10 ether, 0.000001 ether); } + function testMigrateFromV2TokenMismatch() public { + // 1. mint some liquidity to the v2 pair + _mintV2Liquidity(v2Pair); + uint256 lpTokenBefore = v2Pair.balanceOf(address(this)); + + // 2. make sure migrator can transfer user's v2 lp token + v2Pair.approve(address(migrator), lpTokenBefore); + + IBaseMigrator.V2PoolParams memory v2PoolParams = IBaseMigrator.V2PoolParams({ + pair: address(v2Pair), + migrateAmount: lpTokenBefore, + // minor precision loss is acceptable + amount0Min: 9.999 ether, + amount1Min: 9.999 ether + }); + + // v2 weth, token0 + // v4 ETH, token1 + PoolKey memory poolKeyMismatch = poolKey; + poolKeyMismatch.currency1 = Currency.wrap(address(token1)); + ICLMigrator.V4CLPoolParams memory v4MintParams = ICLMigrator.V4CLPoolParams({ + poolKey: poolKeyMismatch, + tickLower: -100, + tickUpper: 100, + salt: bytes32(0), + amount0Min: 0, + amount1Min: 0, + recipient: address(this), + deadline: block.timestamp + 100 + }); + + // 3. multicall, combine initialize and migrateFromV2 + uint160 initSqrtPrice = 79228162514264337593543950336; + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeWithSelector(migrator.initialize.selector, poolKeyMismatch, initSqrtPrice, bytes("")); + data[1] = abi.encodeWithSelector(migrator.migrateFromV2.selector, v2PoolParams, v4MintParams, 0, 0); + vm.expectRevert(); + migrator.multicall(data); + + { + // v2 weth, token0 + // v4 token0, token1 + poolKeyMismatch.currency0 = Currency.wrap(address(token0)); + poolKeyMismatch.currency1 = Currency.wrap(address(token1)); + v4MintParams.poolKey = poolKeyMismatch; + data = new bytes[](2); + data[0] = abi.encodeWithSelector(migrator.initialize.selector, poolKeyMismatch, initSqrtPrice, bytes("")); + data[1] = abi.encodeWithSelector(migrator.migrateFromV2.selector, v2PoolParams, v4MintParams, 0, 0); + vm.expectRevert(); + migrator.multicall(data); + } + } + function testMigrateFromV2WithoutInit() public { // 1. mint some liquidity to the v2 pair _mintV2Liquidity(v2Pair); @@ -667,6 +720,104 @@ abstract contract CLMigratorFromV2 is OldVersionHelper, GasSnapshot { assertApproxEqAbs(token1.balanceOf(address(vault)), 5 ether, 0.000001 ether); } + function testMigrateFromV2ThroughOffchainSign() public { + // 1. mint some liquidity to the v2 pair + _mintV2Liquidity(v2Pair); + uint256 lpTokenBefore = v2Pair.balanceOf(address(this)); + assertGt(lpTokenBefore, 0); + + // 2. instead of approve, we generate a offchain signature here + // v2Pair.approve(address(migrator), lpTokenBefore); + (address userAddr, uint256 userPrivateKey) = makeAddrAndKey("user"); + + // 2.a transfer the lp token to the user + v2Pair.transfer(userAddr, lpTokenBefore); + + uint256 ddl = block.timestamp + 100; + + // 2.b prepare the hash + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + userAddr, + address(migrator), + lpTokenBefore, + v2Pair.nonces(userAddr), + ddl + ) + ); + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", v2Pair.DOMAIN_SEPARATOR(), structHash)); + + // 2.c generate the signature + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, hash); + + IBaseMigrator.V2PoolParams memory v2PoolParams = IBaseMigrator.V2PoolParams({ + pair: address(v2Pair), + migrateAmount: lpTokenBefore, + // minor precision loss is acceptable + amount0Min: 9.999 ether, + amount1Min: 9.999 ether + }); + + ICLMigrator.V4CLPoolParams memory v4MintParams = ICLMigrator.V4CLPoolParams({ + poolKey: poolKey, + tickLower: -100, + tickUpper: 100, + salt: bytes32(0), + amount0Min: 0, + amount1Min: 0, + recipient: address(this), + deadline: ddl + }); + + // 3. multicall, combine permit, initialize and migrateFromV2 + uint160 initSqrtPrice = 79228162514264337593543950336; + bytes[] memory data = new bytes[](3); + data[0] = abi.encodeWithSelector(migrator.selfPermit.selector, v2Pair, lpTokenBefore, ddl, v, r, s); + data[1] = abi.encodeWithSelector(migrator.initialize.selector, poolKey, initSqrtPrice, bytes("")); + data[2] = abi.encodeWithSelector(migrator.migrateFromV2.selector, v2PoolParams, v4MintParams, 0, 0); + vm.prank(userAddr); + migrator.multicall(data); + + // necessary checks + // v2 pair should be burned already + assertEq(v2Pair.balanceOf(address(this)), 0); + + // make sure liuqidty is minted to the correct pool + assertEq(nonfungiblePoolManager.ownerOf(1), address(this)); + ( + , + , + PoolId poolId, + Currency currency0, + Currency currency1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1, + bytes32 salt + ) = nonfungiblePoolManager.positions(1); + + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(tickLower, -100); + assertEq(tickUpper, 100); + assertEq(liquidity, 2005104164790027832367); + assertEq(feeGrowthInside0LastX128, 0); + assertEq(feeGrowthInside1LastX128, 0); + assertEq(tokensOwed0, 0); + assertEq(tokensOwed1, 0); + assertEq(salt, bytes32(0)); + assertApproxEqAbs(address(vault).balance, 10 ether, 0.000001 ether); + assertApproxEqAbs(token0.balanceOf(address(vault)), 10 ether, 0.000001 ether); + } + function _mintV2Liquidity(IPancakePair pair) public { IERC20(pair.token0()).transfer(address(pair), 10 ether); IERC20(pair.token1()).transfer(address(pair), 10 ether); diff --git a/test/pool-cl/migrator/CLMigratorFromV3.sol b/test/pool-cl/migrator/CLMigratorFromV3.sol index 5fa6900..9961306 100644 --- a/test/pool-cl/migrator/CLMigratorFromV3.sol +++ b/test/pool-cl/migrator/CLMigratorFromV3.sol @@ -200,6 +200,61 @@ abstract contract CLMigratorFromV3 is OldVersionHelper, GasSnapshot { assertApproxEqAbs(token0.balanceOf(address(vault)), 10 ether, 0.000001 ether); } + function testMigrateFromV3TokenMismatch() public { + // 1. mint some liquidity to the v3 pool + _mintV3Liquidity(address(weth), address(token0)); + assertEq(v3Nfpm.ownerOf(1), address(this)); + (,,,,,,, uint128 liquidityFromV3Before,,,,) = v3Nfpm.positions(1); + + // 2. make sure migrator can transfer user's v3 lp token + v3Nfpm.approve(address(migrator), 1); + + IBaseMigrator.V3PoolParams memory v3PoolParams = IBaseMigrator.V3PoolParams({ + nfp: address(v3Nfpm), + tokenId: 1, + liquidity: liquidityFromV3Before, + amount0Min: 9.9 ether, + amount1Min: 9.9 ether, + collectFee: false, + deadline: block.timestamp + 100 + }); + + // v3 weth, token0 + // v4 ETH, token1 + PoolKey memory poolKeyMismatch = poolKey; + poolKeyMismatch.currency1 = Currency.wrap(address(token1)); + ICLMigrator.V4CLPoolParams memory v4MintParams = ICLMigrator.V4CLPoolParams({ + poolKey: poolKeyMismatch, + tickLower: -100, + tickUpper: 100, + salt: bytes32(0), + amount0Min: 0, + amount1Min: 0, + recipient: address(this), + deadline: block.timestamp + 100 + }); + + // 3. multicall, combine initialize and migrateFromV3 + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeWithSelector(migrator.initialize.selector, poolKey, INIT_SQRT_PRICE, bytes("")); + data[1] = abi.encodeWithSelector(migrator.migrateFromV3.selector, v3PoolParams, v4MintParams, 0, 0); + vm.expectRevert(); + migrator.multicall(data); + + { + // v3 weth, token0 + // v4 token0, token1 + poolKeyMismatch.currency0 = Currency.wrap(address(token0)); + poolKeyMismatch.currency1 = Currency.wrap(address(token1)); + v4MintParams.poolKey = poolKeyMismatch; + data = new bytes[](2); + data[0] = abi.encodeWithSelector(migrator.initialize.selector, poolKey, INIT_SQRT_PRICE, bytes("")); + data[1] = abi.encodeWithSelector(migrator.migrateFromV3.selector, v3PoolParams, v4MintParams, 0, 0); + vm.expectRevert(); + migrator.multicall(data); + } + } + function testMigrateFromV3WithoutInit() public { // 1. mint some liquidity to the v3 pool _mintV3Liquidity(address(weth), address(token0)); @@ -762,6 +817,196 @@ abstract contract CLMigratorFromV3 is OldVersionHelper, GasSnapshot { migrator.migrateFromV3(v3PoolParams, v4MintParams, 0, 0); } + function testMigrateFromV3ThroughOffchainSign() public { + // 1. mint some liquidity to the v3 pool + _mintV3Liquidity(address(weth), address(token0)); + assertEq(v3Nfpm.ownerOf(1), address(this)); + (uint96 nonce,,,,,,, uint128 liquidityFromV3Before,,,,) = v3Nfpm.positions(1); + assertGt(liquidityFromV3Before, 0); + + // 2. make sure migrator can transfer user's v3 lp token through offchain sign + // v3Nfpm.approve(address(migrator), 1); + (address userAddr, uint256 userPrivateKey) = makeAddrAndKey("user"); + + // 2.a transfer the lp token to the user + v3Nfpm.transferFrom(address(this), userAddr, 1); + + uint256 ddl = block.timestamp + 100; + // 2.b prepare the hash + bytes32 structHash = keccak256(abi.encode(v3Nfpm.PERMIT_TYPEHASH(), address(migrator), 1, nonce, ddl)); + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", v3Nfpm.DOMAIN_SEPARATOR(), structHash)); + + // 2.c generate the signature + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, hash); + + IBaseMigrator.V3PoolParams memory v3PoolParams = IBaseMigrator.V3PoolParams({ + nfp: address(v3Nfpm), + tokenId: 1, + liquidity: liquidityFromV3Before, + amount0Min: 9.9 ether, + amount1Min: 9.9 ether, + collectFee: false, + deadline: block.timestamp + 100 + }); + + ICLMigrator.V4CLPoolParams memory v4MintParams = ICLMigrator.V4CLPoolParams({ + poolKey: poolKey, + tickLower: -100, + tickUpper: 100, + salt: bytes32(0), + amount0Min: 0, + amount1Min: 0, + recipient: address(this), + deadline: block.timestamp + 100 + }); + + // 3. multicall, combine selfPermitERC721, initialize and migrateFromV3 + bytes[] memory data = new bytes[](3); + data[0] = abi.encodeWithSelector(migrator.selfPermitERC721.selector, v3Nfpm, 1, ddl, v, r, s); + data[1] = abi.encodeWithSelector(migrator.initialize.selector, poolKey, INIT_SQRT_PRICE, bytes("")); + data[2] = abi.encodeWithSelector(migrator.migrateFromV3.selector, v3PoolParams, v4MintParams, 0, 0); + vm.prank(userAddr); + migrator.multicall(data); + + // necessary checks + // v3 liqudity should be 0 + (,,,,,,, uint128 liquidityFromV3After,,,,) = v3Nfpm.positions(1); + assertEq(liquidityFromV3After, 0); + + // make sure liuqidty is minted to the correct pool + assertEq(nonfungiblePoolManager.ownerOf(1), address(this)); + ( + , + , + PoolId poolId, + Currency currency0, + Currency currency1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1, + bytes32 salt + ) = nonfungiblePoolManager.positions(1); + + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(tickLower, -100); + assertEq(tickUpper, 100); + assertEq(liquidity, 2005104164790028032677); + assertEq(feeGrowthInside0LastX128, 0); + assertEq(feeGrowthInside1LastX128, 0); + assertEq(tokensOwed0, 0); + assertEq(tokensOwed1, 0); + assertEq(salt, bytes32(0)); + assertApproxEqAbs(address(vault).balance, 10 ether, 0.000001 ether); + assertApproxEqAbs(token0.balanceOf(address(vault)), 10 ether, 0.000001 ether); + } + + function testMigrateFromV3ThroughOffchainSignPayWithETH() public { + // 1. mint some liquidity to the v3 pool + _mintV3Liquidity(address(weth), address(token0)); + assertEq(v3Nfpm.ownerOf(1), address(this)); + (uint96 nonce,,,,,,, uint128 liquidityFromV3Before,,,,) = v3Nfpm.positions(1); + assertGt(liquidityFromV3Before, 0); + + // 2. make sure migrator can transfer user's v3 lp token through offchain sign + // v3Nfpm.approve(address(migrator), 1); + (address userAddr, uint256 userPrivateKey) = makeAddrAndKey("user"); + + // 2.a transfer the lp token to the user + v3Nfpm.transferFrom(address(this), userAddr, 1); + + uint256 ddl = block.timestamp + 100; + // 2.b prepare the hash + bytes32 structHash = keccak256(abi.encode(v3Nfpm.PERMIT_TYPEHASH(), address(migrator), 1, nonce, ddl)); + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", v3Nfpm.DOMAIN_SEPARATOR(), structHash)); + + // 2.c generate the signature + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, hash); + + IBaseMigrator.V3PoolParams memory v3PoolParams = IBaseMigrator.V3PoolParams({ + nfp: address(v3Nfpm), + tokenId: 1, + liquidity: liquidityFromV3Before, + amount0Min: 9.9 ether, + amount1Min: 9.9 ether, + collectFee: false, + deadline: block.timestamp + 100 + }); + + ICLMigrator.V4CLPoolParams memory v4MintParams = ICLMigrator.V4CLPoolParams({ + poolKey: poolKey, + tickLower: -100, + tickUpper: 100, + salt: bytes32(0), + amount0Min: 0 ether, + amount1Min: 0 ether, + recipient: address(this), + deadline: block.timestamp + 100 + }); + + // make the guy rich + token0.transfer(userAddr, 10 ether); + deal(userAddr, 10 ether); + + vm.prank(userAddr); + token0.approve(address(migrator), 10 ether); + + // 3. multicall, combine selfPermitERC721, initialize and migrateFromV3 + bytes[] memory data = new bytes[](3); + data[0] = abi.encodeWithSelector(migrator.selfPermitERC721.selector, v3Nfpm, 1, ddl, v, r, s); + data[1] = abi.encodeWithSelector(migrator.initialize.selector, poolKey, INIT_SQRT_PRICE, bytes("")); + data[2] = + abi.encodeWithSelector(migrator.migrateFromV3.selector, v3PoolParams, v4MintParams, 10 ether, 10 ether); + vm.prank(userAddr); + migrator.multicall{value: 10 ether}(data); + + // necessary checks + // v3 liqudity should be 0 + (,,,,,,, uint128 liquidityFromV3After,,,,) = v3Nfpm.positions(1); + assertEq(liquidityFromV3After, 0); + + // make sure liuqidty is minted to the correct pool + assertEq(nonfungiblePoolManager.ownerOf(1), address(this)); + ( + , + , + PoolId poolId, + Currency currency0, + Currency currency1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1, + bytes32 salt + ) = nonfungiblePoolManager.positions(1); + + assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId())); + assertEq(Currency.unwrap(currency0), address(0)); + assertEq(Currency.unwrap(currency1), address(token0)); + assertEq(fee, 0); + assertEq(tickLower, -100); + assertEq(tickUpper, 100); + assertEq(liquidity, 4010208329580056065555); + assertEq(feeGrowthInside0LastX128, 0); + assertEq(feeGrowthInside1LastX128, 0); + assertEq(tokensOwed0, 0); + assertEq(tokensOwed1, 0); + assertEq(salt, bytes32(0)); + assertApproxEqAbs(address(vault).balance, 20 ether, 0.000001 ether); + assertApproxEqAbs(token0.balanceOf(address(vault)), 20 ether, 0.000001 ether); + } + function _mintV3Liquidity(address _token0, address _token1) internal { (_token0, _token1) = _token0 < _token1 ? (_token0, _token1) : (_token1, _token0); v3Nfpm.createAndInitializePoolIfNecessary(_token0, _token1, 500, INIT_SQRT_PRICE);