Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V4 CLMigrator #51

Merged
merged 15 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
735649
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
692512
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
736981
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
790796
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
750211
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
792171
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
735661
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
692524
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
736978
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
788801
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
748216
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
790173
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
107 changes: 107 additions & 0 deletions src/base/BaseMigrator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// 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} from "pancake-v4-core/src/types/Currency.sol";
import {IBaseMigrator} from "../interfaces/IBaseMigrator.sol";

contract BaseMigrator is IBaseMigrator, PeripheryImmutableState, Multicall, SelfPermit {
error INVALID_ETHER_SENDER();
error INSUFFICIENT_AMOUNTS_RECEIVED();

constructor(address _WETH9) PeripheryImmutableState(_WETH9) {}

function withdrawLiquidityFromV2(V2PoolParams calldata v2PoolParams)
// function withdrawLiquidityFromV2(address pair, uint256 amount, uint256 amount0Min, uint256 amount1Min)
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 (IPancakePair(v2PoolParams.pair).token1() == WETH9) {
(amount0Received, amount1Received) = (amount1Received, amount0Received);
}
}

function withdrawLiquidityFromV3(
address nfp,
IV3NonfungiblePositionManager.DecreaseLiquidityParams memory decreaseLiquidityParams,
bool collectFee
) internal returns (uint256 amount0Received, uint256 amount1Received) {
/// @notice decrease liquidity from v3#nfp, make sure migrator has been approved
(amount0Received, amount1Received) =
IV3NonfungiblePositionManager(nfp).decreaseLiquidity(decreaseLiquidityParams);

IV3NonfungiblePositionManager.CollectParams memory collectParams = IV3NonfungiblePositionManager.CollectParams({
tokenId: decreaseLiquidityParams.tokenId,
recipient: address(this),
amount0Max: collectFee ? type(uint128).max : SafeCast.toUint128(amount0Received),
amount1Max: collectFee ? type(uint128).max : SafeCast.toUint128(amount1Received)
});

(amount0Received, amount1Received) = IV3NonfungiblePositionManager(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
(,,, address token1,,,,,,,,) = IV3NonfungiblePositionManager(nfp).positions(decreaseLiquidityParams.tokenId);
if (token1 == WETH9) {
(amount0Received, amount1Received) = (amount1Received, amount0Received);
}
}

/// @dev receive extra tokens from user if necessary and normalize all the WETH to native ETH
Copy link
Collaborator

@ChefMist ChefMist Jul 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(not blocker)

could you add this somewhere? maybe in ICLMigrator for those core migrateFromV2 and migrateFromV3 method? feel like this might be good to flag out to external dev who are integrating

// @param: extraAmount0: if pool token0 is ETH and msg.value == 0, WETH will be taken from sender. Otherwise if pool token0 is ETH and msg.value !=0, method will assume user have sent extraAmount0 in msg.value```

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will update this right before i merge all 3 PRs ( avoid annoying rebasing 😂

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

function batchAndNormalizeTokens(Currency currency0, Currency currency1, uint256 extraAmount0, uint256 extraAmount1)
internal
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we can check extraAmount0 and extraAmount1 at the beginning .
if(extraAmount0 == 0 && extraAmount1 == 0) {return;}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unwrapping WETH will still be needed because we might receive WETH from source pool. Or do u think i should move IWETH9(WETH9).withdraw(ERC20(WETH9).balanceOf(address(this))); to a separate function ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point.
you can skip this suggestion.

Or add extra logic about the WETH withdraw before return.
Separate function is also a good solution.

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 MoreFundsAdded(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()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we check the ERC20(WETH9).balanceOf(address(this) > 0 ?
for example , if users try to migrate one v3 position which is out of range , so maybe it will get 0 amount for WETH , in this case , we still will go to call IWETH9(WETH9).withdraw , it will cost extra gas.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, updated

IWETH9(WETH9).withdraw(ERC20(WETH9).balanceOf(address(this)));
}
}

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);
}
}
33 changes: 33 additions & 0 deletions src/interfaces/IBaseMigrator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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";

interface IBaseMigrator is IPeripheryImmutableState, IMulticall, ISelfPermit {
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;
// the amount of token0 and token1 to be received after burning must be no less than these
uint256 amount0Min;
uint256 amount1Min;
}

struct V3PoolParams {
// the PancakeSwap v3-compatible NFP
address nfp;
uint256 tokenId;
uint128 liquidity;
uint256 amount0Min;
uint256 amount1Min;
// decide whether to collect fee
bool collectFee;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we can remove the collectFee flag.
Since we need to collect the liquidity amount , so it is better to collect the fee together.
Because fee number is always smaller than the liquidity amount.
Or we can keep this in the code, but FE use true as default.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this works as a checkbox at frontend page. Will double confirm tho.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per internal discussion: will keep it to give flexibility to user

}
}
56 changes: 56 additions & 0 deletions src/interfaces/external/IPancakePair.sol
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
Loading