Skip to content

Commit

Permalink
V4 BinMigrator (#59)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
chefburger authored Jul 18, 2024
1 parent 8ed8199 commit 3503133
Show file tree
Hide file tree
Showing 42 changed files with 3,024 additions and 29 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
@@ -1 +1 @@
735570
736974
Original file line number Diff line number Diff line change
@@ -1 +1 @@
692493
693861
Original file line number Diff line number Diff line change
@@ -1 +1 @@
736950
738290
Original file line number Diff line number Diff line change
@@ -1 +1 @@
792734
793398
Original file line number Diff line number Diff line change
@@ -1 +1 @@
752222
752832
Original file line number Diff line number Diff line change
@@ -1 +1 @@
794168
794750
Original file line number Diff line number Diff line change
@@ -1 +1 @@
735582
736986
Original file line number Diff line number Diff line change
@@ -1 +1 @@
692505
693873
Original file line number Diff line number Diff line change
@@ -1 +1 @@
736947
738287
Original file line number Diff line number Diff line change
@@ -1 +1 @@
790716
791380
Original file line number Diff line number Diff line change
@@ -1 +1 @@
750204
750814
Original file line number Diff line number Diff line change
@@ -1 +1 @@
792146
792728
101 changes: 92 additions & 9 deletions src/base/BaseMigrator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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)
{
Expand Down Expand Up @@ -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
{
Expand All @@ -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
Expand All @@ -108,11 +158,44 @@ 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) {
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);
}
}
}
17 changes: 15 additions & 2 deletions src/interfaces/IBaseMigrator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
}
38 changes: 38 additions & 0 deletions src/interfaces/ISelfPermitERC721.sol
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 2 additions & 2 deletions src/interfaces/external/IV3NonfungiblePositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 3503133

Please sign in to comment.