Skip to content

Commit

Permalink
V4 CLMigrator (#51)
Browse files Browse the repository at this point in the history
* feat: implement v4Migrator for cl-pool

* feat: added same price slippage check for migrateFromV2

* feat: using actual receive amount when adding liquidity to v4

* test: added test for clMigrator

* fix: take into consideration of extra funds when calc refund

* optimization: avoid unnecessary approve in old token cases

* optimization: avoid unnecessary refund call after adding liquidity

* optimization: avoid unnecessary WETH unwrap

* fix: extra check so that non-owner can not steal funds from lpV3 token

* typo

* refactor: restructure baseMigrator a bit so that more can be reused across pool type

* chore: typo and renaming

* V4 BinMigrator (#59)

* feat: impl binPool migrator

* refactor: restructure & renaming accordingly

* test: add tests cases for binPool migrator

* chore: renaming as well to align with clMigrator

* fix: add check to prevent token mismatch between source and target pool

* feat: added refundETH function and necessary comments

* optimization: avoid duplicate external call when query v2/v3 pool info

* feat: support selfPermitForERC721 (#62)

* feat: support selfPermitForERC721

* docs: added comments suggesting users to use selfPermitERC721IfNecessary

* test: added tests to prevent ppl from removing payable keyword from external functions

* docs: add explanation about the case where extra token0 is ETH
  • Loading branch information
chefburger authored Jul 18, 2024
1 parent b19c709 commit bcbacc9
Show file tree
Hide file tree
Showing 57 changed files with 5,232 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1017615
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
977598
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1022017
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1096580
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1056639
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1094456
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1017627
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
977610
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1022014
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1094562
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1054621
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1092434
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
736974
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
693861
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
738290
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
793398
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
752832
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
794750
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
736986
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
693873
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
738287
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
791380
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
750814
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
792728
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ fs_permissions = [
{ access = "read-write", path = ".forge-snapshots/" },
{ access = "read", path = "./foundry-out" },
{ access = "read", path = "./script/config" },
{ access = "read", path = "./test/bin/" },
]
evm_version = 'cancun'

Expand Down
201 changes: 201 additions & 0 deletions src/base/BaseMigrator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2024 PancakeSwap
pragma solidity ^0.8.19;

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, 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, SelfPermitERC721 {
constructor(address _WETH9) PeripheryImmutableState(_WETH9) {}

/// @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)
{
// burn v2 liquidity to this address
IPancakePair(v2PoolParams.pair).transferFrom(msg.sender, v2PoolParams.pair, v2PoolParams.migrateAmount);
(amount0Received, amount1Received) = IPancakePair(v2PoolParams.pair).burn(address(this));

// same price slippage check as v3
if (amount0Received < v2PoolParams.amount0Min || amount1Received < v2PoolParams.amount1Min) {
revert INSUFFICIENT_AMOUNTS_RECEIVED();
}

/// @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 (shouldReversePair) {
(amount0Received, amount1Received) = (amount1Received, amount0Received);
}
}

/// @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)
{
IV3NonfungiblePositionManager nfp = IV3NonfungiblePositionManager(v3PoolParams.nfp);
uint256 tokenId = v3PoolParams.tokenId;
///@dev make sure the caller is the owner of the token
/// otherwise once the token is approved to migrator, anyone can steal money through this function
if (msg.sender != nfp.ownerOf(tokenId)) {
revert NOT_TOKEN_OWNER();
}

/// @notice decrease liquidity from v3#nfp, make sure migrator has been approved
IV3NonfungiblePositionManager.DecreaseLiquidityParams memory decreaseLiquidityParams =
IV3NonfungiblePositionManager.DecreaseLiquidityParams({
tokenId: tokenId,
liquidity: v3PoolParams.liquidity,
amount0Min: v3PoolParams.amount0Min,
amount1Min: v3PoolParams.amount1Min,
deadline: v3PoolParams.deadline
});
(amount0Received, amount1Received) = nfp.decreaseLiquidity(decreaseLiquidityParams);

/// @notice collect tokens from v3#nfp (including fee if necessary)
IV3NonfungiblePositionManager.CollectParams memory collectParams = IV3NonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: address(this),
amount0Max: v3PoolParams.collectFee ? type(uint128).max : SafeCast.toUint128(amount0Received),
amount1Max: v3PoolParams.collectFee ? type(uint128).max : SafeCast.toUint128(amount1Received)
});
(amount0Received, amount1Received) = nfp.collect(collectParams);

/// @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 (shouldReversePair) {
(amount0Received, amount1Received) = (amount1Received, amount0Received);
}
}

/// @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
{
ERC20 token0 = ERC20(Currency.unwrap(currency0));
ERC20 token1 = ERC20(Currency.unwrap(currency1));

if (extraAmount0 > 0) {
if (currency0.isNative() && msg.value == 0) {
// we assume that user wants to send WETH
SafeTransferLib.safeTransferFrom(ERC20(WETH9), msg.sender, address(this), extraAmount0);
} else if (!currency0.isNative()) {
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 ExtraFundsAdded(address(token0), address(token1), extraAmount0, extraAmount1);
}

// even if user sends native ETH, we still need to unwrap the part from source pool
if (currency0.isNative()) {
uint256 wethBalance = ERC20(WETH9).balanceOf(address(this));
if (wethBalance > 0) IWETH9(WETH9).withdraw(wethBalance);
}
}

/// @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) {
return;
}
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();
}
}
}
}
39 changes: 39 additions & 0 deletions src/base/SelfPermitERC721.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
51 changes: 51 additions & 0 deletions src/interfaces/IBaseMigrator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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 {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, ISelfPermitERC721 {
error TOKEN_NOT_MATCH();
error INVALID_ETHER_SENDER();
error INSUFFICIENT_AMOUNTS_RECEIVED();
error NOT_TOKEN_OWNER();

/// @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;
// the amount of v2 lp token to be withdrawn
uint256 migrateAmount;
// the amount of token0 and token1 to be received after burning must be no less than these
uint256 amount0Min;
uint256 amount1Min;
}

/// @notice Parameters for removing liquidity from v3
struct V3PoolParams {
// the PancakeSwap v3-compatible NFP
address nfp;
uint256 tokenId;
uint128 liquidity;
uint256 amount0Min;
uint256 amount1Min;
// decide whether to collect fee
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;
}
Loading

0 comments on commit bcbacc9

Please sign in to comment.