diff --git a/.gitmodules b/.gitmodules index 8ee3268..65e8918 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,9 +15,3 @@ [submodule "lib/forge-gas-snapshot"] path = lib/forge-gas-snapshot url = https://github.com/marktoda/forge-gas-snapshot -[submodule "lib/v2-core"] - path = lib/v2-core - url = https://github.com/Uniswap/v2-core -[submodule "lib/v3-periphery"] - path = lib/v3-periphery - url = https://github.com/Uniswap/v3-periphery diff --git a/lib/v2-core b/lib/v2-core deleted file mode 160000 index 4dd5906..0000000 --- a/lib/v2-core +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4dd59067c76dea4a0e8e4bfdda41877a6b16dedc diff --git a/lib/v3-periphery b/lib/v3-periphery deleted file mode 160000 index 80f26c8..0000000 --- a/lib/v3-periphery +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 80f26c86c57b8a5e4b913f42844d4c8bd274d058 diff --git a/src/base/BaseMigrator.sol b/src/base/BaseMigrator.sol index 32c7f2f..7838c94 100644 --- a/src/base/BaseMigrator.sol +++ b/src/base/BaseMigrator.sol @@ -2,11 +2,22 @@ // Copyright (C) 2024 PancakeSwap pragma solidity ^0.8.19; -import {IUniswapV2Pair} from "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; -import {INonfungiblePositionManager} from "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {SafeTransferLib, ERC20} from "solmate/utils/SafeTransferLib.sol"; +import {IPancakePair} from "../interfaces/external/IPancakePair.sol"; +import {IV3NonfungiblePositionManager} from "../interfaces/external/IV3NonfungiblePositionManager.sol"; +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 {IBaseMigrator} from "../interfaces/IBaseMigrator.sol"; + +contract BaseMigrator is IBaseMigrator, PeripheryImmutableState, Multicall, SelfPermit { + error NOT_WETH9(); + + address public constant NATIVE = address(0); -contract BaseMigrator is PeripheryImmutableState, Multicall, SelfPermit { constructor(address _WETH9) PeripheryImmutableState(_WETH9) {} function withdrawLiquidityFromV2(address pair, uint256 amount) @@ -14,40 +25,73 @@ contract BaseMigrator is PeripheryImmutableState, Multicall, SelfPermit { returns (uint256 amount0Received, uint256 amount1Received) { // burn v2 liquidity to this address - IUniswapV2Pair(pair).transferFrom(msg.sender, pair, amount); - return IUniswapV2Pair(pair).burn(address(this)); + IPancakePair(pair).transferFrom(msg.sender, pair, amount); + return IPancakePair(pair).burn(address(this)); } function withdrawLiquidityFromV3( address nfp, - INonfungiblePositionManager.DecreaseLiquidityParams decreaseLiquidityParams, + IV3NonfungiblePositionManager.DecreaseLiquidityParams memory decreaseLiquidityParams, bool collectFee ) internal returns (uint256 amount0Received, uint256 amount1Received) { // TODO: consider batching decreaseLiquidity and collect /// @notice decrease liquidity from v3#nfp, make sure migrator has been approved - (amount0Received, amount1Received) = INonfungiblePositionManager(nfp).decreaseLiquidity(decreaseLiquidityParams); + (amount0Received, amount1Received) = + IV3NonfungiblePositionManager(nfp).decreaseLiquidity(decreaseLiquidityParams); - INonfungiblePositionManager.CollectParams collectParams = INonfungiblePositionManager.CollectParams({ + IV3NonfungiblePositionManager.CollectParams memory collectParams = IV3NonfungiblePositionManager.CollectParams({ tokenId: decreaseLiquidityParams.tokenId, recipient: address(this), - amount0Max: collectFee ? type(uint128).max : amount0Received, - amount1Max: collectFee ? type(uint128).max : amount1Received + amount0Max: collectFee ? type(uint128).max : SafeCast.toUint128(amount0Received), + amount1Max: collectFee ? type(uint128).max : SafeCast.toUint128(amount1Received) }); - return INonfungiblePositionManager(nfp).collect(collectParams); + return IV3NonfungiblePositionManager(nfp).collect(collectParams); } - function refund(address token, address to, uint256 amount) internal { - if (token == WETH9) { - IWETH9(WETH9).withdraw(amount); - TransferHelper.safeTransferETH(to, amount); - } else { - TransferHelper.safeTransfer(token, to, amount); + /// @dev receive extra tokens from user if necessary and normalize all the WETH to native ETH + function batchAndNormalizeTokens(Currency currency0, Currency currency1, uint256 extraAmount0, uint256 extraAmount1) + internal + { + ERC20 token0 = ERC20(Currency.unwrap(currency0)); + ERC20 token1 = ERC20(Currency.unwrap(currency1)); + + if (extraAmount0 > 0) { + if (address(token0) == NATIVE && msg.value == 0) { + // we assume that user wants to send WETH + SafeTransferLib.safeTransferFrom(ERC20(WETH9), msg.sender, address(this), extraAmount0); + } else if (address(token0) != NATIVE) { + SafeTransferLib.safeTransferFrom(token0, msg.sender, address(this), extraAmount0); + } + } + + /// @dev token1 cant be NATIVE + if (extraAmount1 > 0) { + SafeTransferLib.safeTransferFrom(token1, msg.sender, address(this), extraAmount1); + } + + if (extraAmount0 != 0 || extraAmount1 != 0) { + emit MoreFundsAdded(address(token0), address(token1), extraAmount0, extraAmount1); + } + + // even if user sends native ETH, we still need to unwrap the part from source pool + if (address(token0) == NATIVE) { + IWETH9(WETH9).withdraw(ERC20(WETH9).balanceOf(address(this))); } } + function approveMax(Currency currency, address to) internal { + ERC20 token = ERC20(Currency.unwrap(currency)); + if (token.allowance(address(this), to) == type(uint256).max) { + return; + } + SafeTransferLib.safeApprove(token, to, type(uint256).max); + } + receive() external payable { - require(msg.sender == WETH9, "Not WETH9"); + if (msg.sender != WETH9) { + revert NOT_WETH9(); + } } } diff --git a/src/interfaces/IBaseMigrator.sol b/src/interfaces/IBaseMigrator.sol new file mode 100644 index 0000000..1fdd4d5 --- /dev/null +++ b/src/interfaces/IBaseMigrator.sol @@ -0,0 +1,27 @@ +// 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"; + +interface IBaseMigrator { + event MoreFundsAdded(address currency0, address currency1, uint256 extraAmount0, uint256 extraAmount1); + + struct V2PoolParams { + // the PancakeSwap v2-compatible pair + address pair; + // the amount of v2 lp token to be withdrawn + uint256 migrateAmount; + } + + struct V3PoolParams { + // the PancakeSwap v3-compatible NFP + address nfp; + uint256 tokenId; + uint128 liquidity; + uint256 amount0Min; + uint256 amount1Min; + // decide whether to collect fee + bool collectFee; + } +} diff --git a/src/interfaces/external/IPancakePair.sol b/src/interfaces/external/IPancakePair.sol new file mode 100644 index 0000000..99d5452 --- /dev/null +++ b/src/interfaces/external/IPancakePair.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.5.0; + +/// @notice Copying from PancakeSwap V2 Pair +/// https://github.com/pancakeswap/pancake-swap-core-v2/blob/master/contracts/interfaces/IPancakePair.sol +interface IPancakePair { + event Approval(address indexed owner, address indexed spender, uint256 value); + event Transfer(address indexed from, address indexed to, uint256 value); + + function name() external pure returns (string memory); + function symbol() external pure returns (string memory); + function decimals() external pure returns (uint8); + function totalSupply() external view returns (uint256); + function balanceOf(address owner) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); + + function approve(address spender, uint256 value) external returns (bool); + function transfer(address to, uint256 value) external returns (bool); + function transferFrom(address from, address to, uint256 value) external returns (bool); + + function DOMAIN_SEPARATOR() external view returns (bytes32); + function PERMIT_TYPEHASH() external pure returns (bytes32); + function nonces(address owner) external view returns (uint256); + + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external; + + event Mint(address indexed sender, uint256 amount0, uint256 amount1); + event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); + event Swap( + address indexed sender, + uint256 amount0In, + uint256 amount1In, + uint256 amount0Out, + uint256 amount1Out, + address indexed to + ); + event Sync(uint112 reserve0, uint112 reserve1); + + function MINIMUM_LIQUIDITY() external pure returns (uint256); + function factory() external view returns (address); + function token0() external view returns (address); + function token1() external view returns (address); + function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); + function price0CumulativeLast() external view returns (uint256); + function price1CumulativeLast() external view returns (uint256); + function kLast() external view returns (uint256); + + function mint(address to) external returns (uint256 liquidity); + function burn(address to) external returns (uint256 amount0, uint256 amount1); + function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external; + function skim(address to) external; + function sync() external; + + function initialize(address, address) external; +} diff --git a/src/interfaces/external/IV3NonfungiblePositionManager.sol b/src/interfaces/external/IV3NonfungiblePositionManager.sol new file mode 100644 index 0000000..0dcceeb --- /dev/null +++ b/src/interfaces/external/IV3NonfungiblePositionManager.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.7.5; +pragma abicoder v2; + +/// @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 { + /// @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 + /// @param liquidity The amount by which liquidity for the NFT position was increased + /// @param amount0 The amount of token0 that was paid for the increase in liquidity + /// @param amount1 The amount of token1 that was paid for the increase in liquidity + event IncreaseLiquidity(uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); + /// @notice Emitted when liquidity is decreased for a position NFT + /// @param tokenId The ID of the token for which liquidity was decreased + /// @param liquidity The amount by which liquidity for the NFT position was decreased + /// @param amount0 The amount of token0 that was accounted for the decrease in liquidity + /// @param amount1 The amount of token1 that was accounted for the decrease in liquidity + event DecreaseLiquidity(uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); + /// @notice Emitted when tokens are collected for a position NFT + /// @dev The amounts reported may not be exactly equivalent to the amounts transferred, due to rounding behavior + /// @param tokenId The ID of the token for which underlying tokens were collected + /// @param recipient The address of the account that received the collected tokens + /// @param amount0 The amount of token0 owed to the position that was collected + /// @param amount1 The amount of token1 owed to the position that was collected + event Collect(uint256 indexed tokenId, address recipient, uint256 amount0, uint256 amount1); + + /// @notice Returns the position information associated with a given token ID. + /// @dev Throws if the token ID is not valid. + /// @param tokenId The ID of the token that represents the position + /// @return nonce The nonce for permits + /// @return operator The address that is approved for spending + /// @return token0 The address of the token0 for a specific pool + /// @return token1 The address of the token1 for a specific pool + /// @return fee The fee associated with the pool + /// @return tickLower The lower end of the tick range for the position + /// @return tickUpper The higher end of the tick range for the position + /// @return liquidity The liquidity of the position + /// @return feeGrowthInside0LastX128 The fee growth of token0 as of the last action on the individual position + /// @return feeGrowthInside1LastX128 The fee growth of token1 as of the last action on the individual position + /// @return tokensOwed0 The uncollected amount of token0 owed to the position as of the last computation + /// @return tokensOwed1 The uncollected amount of token1 owed to the position as of the last computation + function positions(uint256 tokenId) + external + view + returns ( + uint96 nonce, + address operator, + address token0, + address token1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ); + + struct MintParams { + address token0; + address token1; + uint24 fee; + int24 tickLower; + int24 tickUpper; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + address recipient; + uint256 deadline; + } + + /// @notice Creates a new position wrapped in a NFT + /// @dev Call this when the pool does exist and is initialized. Note that if the pool is created but not initialized + /// a method does not exist, i.e. the pool is assumed to be initialized. + /// @param params The params necessary to mint a position, encoded as `MintParams` in calldata + /// @return tokenId The ID of the token that represents the minted position + /// @return liquidity The amount of liquidity for this position + /// @return amount0 The amount of token0 + /// @return amount1 The amount of token1 + function mint(MintParams calldata params) + external + payable + returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); + + struct IncreaseLiquidityParams { + uint256 tokenId; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + /// @notice Increases the amount of liquidity in a position, with tokens paid by the `msg.sender` + /// @param params tokenId The ID of the token for which liquidity is being increased, + /// amount0Desired The desired amount of token0 to be spent, + /// amount1Desired The desired amount of token1 to be spent, + /// amount0Min The minimum amount of token0 to spend, which serves as a slippage check, + /// amount1Min The minimum amount of token1 to spend, which serves as a slippage check, + /// deadline The time by which the transaction must be included to effect the change + /// @return liquidity The new liquidity amount as a result of the increase + /// @return amount0 The amount of token0 to acheive resulting liquidity + /// @return amount1 The amount of token1 to acheive resulting liquidity + function increaseLiquidity(IncreaseLiquidityParams calldata params) + external + payable + returns (uint128 liquidity, uint256 amount0, uint256 amount1); + + struct DecreaseLiquidityParams { + uint256 tokenId; + uint128 liquidity; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + /// @notice Decreases the amount of liquidity in a position and accounts it to the position + /// @param params tokenId The ID of the token for which liquidity is being decreased, + /// amount The amount by which liquidity will be decreased, + /// amount0Min The minimum amount of token0 that should be accounted for the burned liquidity, + /// amount1Min The minimum amount of token1 that should be accounted for the burned liquidity, + /// deadline The time by which the transaction must be included to effect the change + /// @return amount0 The amount of token0 accounted to the position's tokens owed + /// @return amount1 The amount of token1 accounted to the position's tokens owed + function decreaseLiquidity(DecreaseLiquidityParams calldata params) + external + payable + returns (uint256 amount0, uint256 amount1); + + struct CollectParams { + uint256 tokenId; + address recipient; + uint128 amount0Max; + uint128 amount1Max; + } + + /// @notice Collects up to a maximum amount of fees owed to a specific position to the recipient + /// @param params tokenId The ID of the NFT for which tokens are being collected, + /// recipient The account that should receive the tokens, + /// amount0Max The maximum amount of token0 to collect, + /// amount1Max The maximum amount of token1 to collect + /// @return amount0 The amount of fees collected in token0 + /// @return amount1 The amount of fees collected in token1 + function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1); + + /// @notice Burns a token ID, which deletes it from the NFT contract. The token must have 0 liquidity and all tokens + /// must be collected first. + /// @param tokenId The ID of the token that is being burned + function burn(uint256 tokenId) external payable; +} diff --git a/src/pool-cl/CLMigrator.sol b/src/pool-cl/CLMigrator.sol index 9f66c4d..d7ab58f 100644 --- a/src/pool-cl/CLMigrator.sol +++ b/src/pool-cl/CLMigrator.sol @@ -3,144 +3,106 @@ pragma solidity ^0.8.19; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; -import {BaseMigrator} from "../base/BaseMigrator.sol"; -import {ICLMigrator} from "./interfaces/ICLMigrator.sol"; +import {BaseMigrator, IV3NonfungiblePositionManager} from "../base/BaseMigrator.sol"; +import {ICLMigrator, PoolKey} from "./interfaces/ICLMigrator.sol"; import {INonfungiblePositionManager} from "./interfaces/INonfungiblePositionManager.sol"; +import {Currency} from "pancake-v4-core/src/types/Currency.sol"; contract CLMigrator is ICLMigrator, BaseMigrator { - using LowGasSafeMath for uint256; - INonfungiblePositionManager public immutable nonfungiblePositionManager; constructor(address _WETH9, address _nonfungiblePositionManager) BaseMigrator(_WETH9) { - nonfungiblePositionManager = _nonfungiblePositionManager; + nonfungiblePositionManager = INonfungiblePositionManager(_nonfungiblePositionManager); } - function migrateFromV2(MigrateFromV2Params calldata params) external override { - // 1. burn v2 liquidity to this address + function migrateFromV2( + V2PoolParams calldata v2PoolParams, + INonfungiblePositionManager.MintParams calldata v4MintParams, + uint256 extraAmount0, + uint256 extraAmount1 + ) external payable override { (uint256 amount0Received, uint256 amount1Received) = - withdrawLiquidityFromV2(params.pair, params.liquidityToMigrate); - - if (params.amount0In != 0) { - SafeTransferLib.safeTransferFrom(params.poolKey.currency0, msg.sender, address(this), params.amount0In); - } - if (params.amount1In != 0) { - SafeTransferLib.safeTransferFrom(params.poolKey.currency1, msg.sender, address(this), params.amount1In); - } - - // 2. mint v4 position token, token sent to recipient - // TO BE CONFIRMED: - // Consider the case from a WETH pool to a ETH pool (v2 not support ETH pool but v4 does): - // in that case we might need to unwrap WETH to ETH and send to the nfp contract - // but how many token should be sent to the nfp contract ? - // i don't see nfp have refund logic, what if the token consumed is less than the token sent ? - SafeTransferLib.safeApprove(params.poolKey.currency0, address(nonfungiblePositionManager), amount0Desired); - SafeTransferLib.safeApprove(params.poolKey.currency1, address(nonfungiblePositionManager), amount1Desired); - - // TODO: init the pool if necessary + withdrawLiquidityFromV2(v2PoolParams.pair, v2PoolParams.migrateAmount); - (uint256 tokenId, uint128 liquidity, uint256 amount0Consumed, uint256 amount1Consumed) = - nonfungiblePositionManager.mint( - MintParams({ - PoolKey: params.poolKey, - tickLower: params.tickLower, - tickUpper: params.tickUpper, - salt: params.salt, - amount0Desired: params.amount0Desired, - amount1Desired: params.amount1Desired, - amount0Min: params.amount0Min, - amount1Min: params.amount1Min, - recipient: params.recipient, - deadline: params.deadline - }) + /// @notice if user mannually specify the price range, they might need to send extra token + batchAndNormalizeTokens( + v4MintParams.poolKey.currency0, v4MintParams.poolKey.currency1, extraAmount0, extraAmount1 ); - // TODO: any other check here? + (,, uint256 amount0Consumed, uint256 amount1Consumed) = _addLiquidityToTargetPool(v4MintParams); - // 3. clear allowance and refund if necessary - if (params.amount0Desired > amount0Consumed) { - SafeTransferLib.safeApprove(params.poolKey.currency0, address(nonfungiblePositionManager), 0); + // refund if necessary, ETH is supported by CurrencyLib + unchecked { + if (amount0Received > amount0Consumed) { + v4MintParams.poolKey.currency0.transfer(v4MintParams.recipient, amount0Received - amount0Consumed); + } + if (amount1Received > amount1Consumed) { + v4MintParams.poolKey.currency1.transfer(v4MintParams.recipient, amount1Received - amount1Consumed); + } } - if (params.amount1Desired > amount1Consumed) { - SafeTransferLib.safeApprove(params.poolKey.currency1, address(nonfungiblePositionManager), 0); - } - - if (amount0Received > amount0Consumed) { - refund(params.poolKey.currency0, params.recipient, amount0Received - amount0Consumed); - } - - if (amount1Received > amount1Consumed) { - refund(params.poolKey.currency1, params.recipient, amount1Received - amount1Consumed); - } - - // TODO: confirm whether we need any events here } - function migrateFromV3(MigrateFromV3Params calldata params) external override { - // 1. burn v3 liquidity to this address - (uint256 amount0Received, uint256 amount1Received) = withdrawLiquidityFromV3( - params.nfp, - INonfungiblePositionManager.DecreaseLiquidityParams({ - tokenId: params.tokenId, - liquidity: params.liquidity, - amount0Min: params.amount0MinForV3, - amount1Min: params.amount1MinForV3 - }), - params.collectFee - ); - - if (params.amount0In != 0) { - SafeTransferLib.safeTransferFrom(params.poolKey.currency0, msg.sender, address(this), params.amount0In); - } - if (params.amount1In != 0) { - SafeTransferLib.safeTransferFrom(params.poolKey.currency1, msg.sender, address(this), params.amount1In); - } - - // 2. mint v4 position token, token sent to recipient - // TO BE CONFIRMED: - // Consider the case from a WETH pool to a ETH pool (v3 not support ETH pool but v4 does): - // in that case we might need to unwrap WETH to ETH and send to the nfp contract - // but how many token should be sent to the nfp contract ? - // i don't see nfp have refund logic, what if the token consumed is less than the token sent ? - SafeTransferLib.safeApprove(params.poolKey.currency0, address(nonfungiblePositionManager), amount0Desired); - SafeTransferLib.safeApprove(params.poolKey.currency1, address(nonfungiblePositionManager), amount1Desired); - - // TODO: init the pool if necessary + function migrateFromV3( + V3PoolParams calldata v3PoolParams, + INonfungiblePositionManager.MintParams calldata v4MintParams, + uint256 extraAmount0, + uint256 extraAmount1 + ) external payable override { + IV3NonfungiblePositionManager.DecreaseLiquidityParams memory decreaseLiquidityParams = + IV3NonfungiblePositionManager.DecreaseLiquidityParams({ + tokenId: v3PoolParams.tokenId, + liquidity: v3PoolParams.liquidity, + amount0Min: v3PoolParams.amount0Min, + amount1Min: v3PoolParams.amount1Min, + deadline: v4MintParams.deadline + }); + (uint256 amount0Received, uint256 amount1Received) = + withdrawLiquidityFromV3(v3PoolParams.nfp, decreaseLiquidityParams, v3PoolParams.collectFee); - (uint256 tokenId, uint128 liquidity, uint256 amount0Consumed, uint256 amount1Consumed) = - nonfungiblePositionManager.mint( - MintParams({ - PoolKey: params.poolKey, - tickLower: params.tickLower, - tickUpper: params.tickUpper, - salt: params.salt, - amount0Desired: params.amount0Desired, - amount1Desired: params.amount1Desired, - amount0Min: params.amount0Min, - amount1Min: params.amount1Min, - recipient: params.recipient, - deadline: params.deadline - }) + /// @notice if user mannually specify the price range, they need to send extra token + batchAndNormalizeTokens( + v4MintParams.poolKey.currency0, v4MintParams.poolKey.currency1, extraAmount0, extraAmount1 ); - // TODO: any other check here? - - // 3. clear allowance and refund if necessary - if (params.amount0Desired > amount0Consumed) { - SafeTransferLib.safeApprove(params.poolKey.currency0, address(nonfungiblePositionManager), 0); - } - if (params.amount1Desired > amount1Consumed) { - SafeTransferLib.safeApprove(params.poolKey.currency1, address(nonfungiblePositionManager), 0); - } + (,, uint256 amount0Consumed, uint256 amount1Consumed) = _addLiquidityToTargetPool(v4MintParams); - if (amount0Received > amount0Consumed) { - refund(params.poolKey.currency0, params.recipient, amount0Received - amount0Consumed); + // refund if necessary, ETH is supported by CurrencyLib + unchecked { + if (amount0Received > amount0Consumed) { + v4MintParams.poolKey.currency0.transfer(v4MintParams.recipient, amount0Received - amount0Consumed); + } + if (amount1Received > amount1Consumed) { + v4MintParams.poolKey.currency1.transfer(v4MintParams.recipient, amount1Received - amount1Consumed); + } } + } - if (amount1Received > amount1Consumed) { - refund(params.poolKey.currency1, params.recipient, amount1Received - amount1Consumed); + function _addLiquidityToTargetPool(INonfungiblePositionManager.MintParams calldata params) + internal + returns (uint256 tokenId, uint128 liquidity, uint256 amount0Consumed, uint256 amount1Consumed) + { + /// @dev currency1 cant be NATIVE + bool nativePair = Currency.unwrap(params.poolKey.currency0) == NATIVE; + if (!nativePair) { + approveMax(params.poolKey.currency0, address(nonfungiblePositionManager)); } + approveMax(params.poolKey.currency1, address(nonfungiblePositionManager)); + + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeWithSelector(nonfungiblePositionManager.mint.selector, params); + data[1] = abi.encodeWithSelector(nonfungiblePositionManager.refundETH.selector); + bytes[] memory ret = nonfungiblePositionManager.multicall{value: nativePair ? params.amount0Desired : 0}(data); + (tokenId, liquidity, amount0Consumed, amount1Consumed) = + abi.decode(ret[0], (uint256, uint128, uint256, uint256)); + } - // TODO: confirm whether we need any events here + /// @notice Planned to be batched with migration operations through multicall to save gas + function initialize(PoolKey memory poolKey, uint160 sqrtPriceX96, bytes calldata hookData) + external + payable + override + returns (int24 tick) + { + return nonfungiblePositionManager.initialize(poolKey, sqrtPriceX96, hookData); } } diff --git a/src/pool-cl/interfaces/ICLMigrator.sol b/src/pool-cl/interfaces/ICLMigrator.sol index 746c5bb..3eae86e 100644 --- a/src/pool-cl/interfaces/ICLMigrator.sol +++ b/src/pool-cl/interfaces/ICLMigrator.sol @@ -3,57 +3,37 @@ 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"; +import {INonfungiblePositionManager} from "./INonfungiblePositionManager.sol"; -interface ICLMigrator { - struct MigrateFromV2Params { - // source v2 pool params - address pair; // the PancakeSwap v2-compatible pair - uint256 liquidityToMigrate; // the amount of liquidity to migrate - // target v4 pool params - PoolKey poolKey; - int24 tickLower; - int24 tickUpper; - bytes32 salt; - uint256 amount0Desired; - uint256 amount1Desired; - uint256 amount0Min; - uint256 amount1Min; - address recipient; - uint256 deadline; - // amt0, amt1 that will be added to the pool - // usually when ppl specify price range manually - uint256 amount0In; - uint256 amount1In; - bool refundAsETH; - } +interface ICLMigrator is IBaseMigrator { + function migrateFromV2( + V2PoolParams calldata v2PoolParams, + // exact target v4#clpool mintParams + INonfungiblePositionManager.MintParams calldata v4MintParams, + // extra funds to be added + uint256 extraAmount0, + uint256 extraAmount1 + ) external payable; - struct MigrateFromV3Params { - // source v3 pool params - address nfp; // the PancakeSwap v3-compatible NFP - uint256 tokenId; - uint128 liquidity; - uint256 amount0MinForV3; - uint256 amount1MinForV3; - bool collectFee; - // target v4 pool params - PoolKey poolKey; // the target v4 pool - int24 tickLower; - int24 tickUpper; - bytes32 salt; - uint256 amount0Desired; - uint256 amount1Desired; - uint256 amount0Min; - uint256 amount1Min; - address recipient; - uint256 deadline; - // amt0, amt1 that will be added to the pool - // usually when ppl specify price range manually - uint256 amount0In; - uint256 amount1In; - bool refundAsETH; - } + function migrateFromV3( + V3PoolParams calldata v3PoolParams, + // exact target v4#clpool mintParams + INonfungiblePositionManager.MintParams calldata v4MintParams, + // extra funds to be added + uint256 extraAmount0, + uint256 extraAmount1 + ) external payable; - function migrateFromV2(MigrateFromV2Params calldata params) external; - - function migrateFromV3(MigrateFromV3Params calldata params) external; + /// @notice Initialize the pool state for a given pool ID. + /// @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 + /// @param hookData Hook data for the pool + /// @return tick Pool tick + function initialize(PoolKey memory poolKey, uint160 sqrtPriceX96, bytes calldata hookData) + external + payable + returns (int24 tick); } diff --git a/src/pool-cl/interfaces/INonfungiblePositionManager.sol b/src/pool-cl/interfaces/INonfungiblePositionManager.sol index b794c57..6bfe935 100644 --- a/src/pool-cl/interfaces/INonfungiblePositionManager.sol +++ b/src/pool-cl/interfaces/INonfungiblePositionManager.sol @@ -11,16 +11,20 @@ import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/I import {IERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; import {IERC721Permit} from "./IERC721Permit.sol"; import {ICLPeripheryImmutableState} from "./ICLPeripheryImmutableState.sol"; +import {IPeripheryPayments} from "../../interfaces/IPeripheryPayments.sol"; +import {IMulticall} from "../../interfaces/IMulticall.sol"; /// @title Non-fungible token for positions /// @notice Wraps PancakeSwap V4 positions in a non-fungible token interface which allows for them to be transferred /// and authorized. interface INonfungiblePositionManager is + IPeripheryPayments, ILockCallback, ICLPeripheryImmutableState, IERC721Metadata, IERC721Enumerable, - IERC721Permit + IERC721Permit, + IMulticall { error NotOwnerOrOperator(); error InvalidLiquidityDecreaseAmount();