Skip to content

Commit

Permalink
feat: implement v4Migrator for cl-pool
Browse files Browse the repository at this point in the history
  • Loading branch information
chefburger committed Jul 3, 2024
1 parent bd5b369 commit f5ac980
Show file tree
Hide file tree
Showing 11 changed files with 413 additions and 194 deletions.
6 changes: 0 additions & 6 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 0 additions & 1 deletion lib/v2-core
Submodule v2-core deleted from 4dd590
1 change: 0 additions & 1 deletion lib/v3-periphery
Submodule v3-periphery deleted from 80f26c
2 changes: 0 additions & 2 deletions remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,3 @@ ds-test/=lib/forge-std/lib/ds-test/src/
pancake-v4-core/=lib/pancake-v4-core/
@openzeppelin/=lib/openzeppelin-contracts/
solmate/=lib/solmate/src/
@uniswap/v2-core/=lib/v2-core/
@uniswap/v3-periphery/=lib/v3-periphery/
80 changes: 62 additions & 18 deletions src/base/BaseMigrator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,96 @@
// 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)
internal
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();
}
}
}
27 changes: 27 additions & 0 deletions src/interfaces/IBaseMigrator.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
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;
}
156 changes: 156 additions & 0 deletions src/interfaces/external/IV3NonfungiblePositionManager.sol
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit f5ac980

Please sign in to comment.