Skip to content

Commit

Permalink
feat: support selfPermitForERC721 (#62)
Browse files Browse the repository at this point in the history
* 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 ebf3464 commit c331872
Show file tree
Hide file tree
Showing 33 changed files with 708 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1017502
1017615
Original file line number Diff line number Diff line change
@@ -1 +1 @@
977507
977598
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1021926
1022017
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1096558
1096580
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1056627
1056639
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1094444
1094456
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1017514
1017627
Original file line number Diff line number Diff line change
@@ -1 +1 @@
977519
977610
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1021923
1022014
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1094540
1094562
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1054609
1054621
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1092422
1092434
Original file line number Diff line number Diff line change
@@ -1 +1 @@
736921
736974
Original file line number Diff line number Diff line change
@@ -1 +1 @@
693830
693861
Original file line number Diff line number Diff line change
@@ -1 +1 @@
738259
738290
Original file line number Diff line number Diff line change
@@ -1 +1 @@
793354
793398
Original file line number Diff line number Diff line change
@@ -1 +1 @@
752798
752832
Original file line number Diff line number Diff line change
@@ -1 +1 @@
794716
794750
Original file line number Diff line number Diff line change
@@ -1 +1 @@
736933
736986
Original file line number Diff line number Diff line change
@@ -1 +1 @@
693842
693873
Original file line number Diff line number Diff line change
@@ -1 +1 @@
738256
738287
Original file line number Diff line number Diff line change
@@ -1 +1 @@
791336
791380
Original file line number Diff line number Diff line change
@@ -1 +1 @@
750780
750814
Original file line number Diff line number Diff line change
@@ -1 +1 @@
792694
792728
3 changes: 2 additions & 1 deletion src/base/BaseMigrator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ 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 {
contract BaseMigrator is IBaseMigrator, PeripheryImmutableState, Multicall, SelfPermit, SelfPermitERC721 {
constructor(address _WETH9) PeripheryImmutableState(_WETH9) {}

/// @notice refund native ETH to caller
Expand Down
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);
}
}
}
3 changes: 2 additions & 1 deletion src/interfaces/IBaseMigrator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ 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();
Expand Down
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
105 changes: 105 additions & 0 deletions test/pool-bin/migrator/BinMigratorFromV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,111 @@ abstract contract BinMigratorFromV2 is OldVersionHelper, LiquidityParamsHelper,
(poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3);
}

function testMigrateFromV2ThroughOffchainSign() public {
// 1. mint some liquidity to the v2 pair
_mintV2Liquidity(v2Pair);
uint256 lpTokenBefore = v2Pair.balanceOf(address(this));
assertGt(lpTokenBefore, 0);

// 2. make sure migrator can transfer user's v2 lp token
// v2Pair.approve(address(migrator), lpTokenBefore);
(address userAddr, uint256 userPrivateKey) = makeAddrAndKey("user");

// 2.a transfer the lp token to the user
v2Pair.transfer(userAddr, lpTokenBefore);

uint256 ddl = block.timestamp + 100;
// 2.b prepare the hash
bytes32 structHash = keccak256(
abi.encode(
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),
userAddr,
address(migrator),
lpTokenBefore,
v2Pair.nonces(userAddr),
ddl
)
);
bytes32 hash = keccak256(abi.encodePacked("\x19\x01", v2Pair.DOMAIN_SEPARATOR(), structHash));

// 2.c generate the signature
(uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, hash);

IBaseMigrator.V2PoolParams memory v2PoolParams = IBaseMigrator.V2PoolParams({
pair: address(v2Pair),
migrateAmount: lpTokenBefore,
// minor precision loss is acceptable
amount0Min: 9.999 ether,
amount1Min: 9.999 ether
});

IBinFungiblePositionManager.AddLiquidityParams memory params =
_getAddParams(poolKey, getBinIds(ACTIVE_BIN_ID, 3), 10 ether, 10 ether, ACTIVE_BIN_ID, address(this));

IBinMigrator.V4BinPoolParams memory v4BinPoolParams = IBinMigrator.V4BinPoolParams({
poolKey: params.poolKey,
amount0Min: params.amount0Min,
amount1Min: params.amount1Min,
activeIdDesired: params.activeIdDesired,
idSlippage: params.idSlippage,
deltaIds: params.deltaIds,
distributionX: params.distributionX,
distributionY: params.distributionY,
to: params.to,
deadline: params.deadline
});

// 3. multicall, combine permit, initialize and migrateFromV2
bytes[] memory data = new bytes[](3);
data[0] = abi.encodeWithSelector(migrator.selfPermit.selector, v2Pair, lpTokenBefore, ddl, v, r, s);
data[1] = abi.encodeWithSelector(migrator.initialize.selector, poolKey, ACTIVE_BIN_ID, bytes(""));
data[2] = abi.encodeWithSelector(migrator.migrateFromV2.selector, v2PoolParams, v4BinPoolParams, 0, 0);
vm.prank(userAddr);
migrator.multicall(data);

// necessary checks
// v2 pair should be burned already
assertEq(v2Pair.balanceOf(address(this)), 0);

// make sure liuqidty is minted to the correct pool
assertApproxEqAbs(address(vault).balance, 10 ether, 0.000001 ether);
assertApproxEqAbs(token0.balanceOf(address(vault)), 10 ether, 0.000001 ether);

uint256 positionId0 = poolKey.toId().toTokenId(ACTIVE_BIN_ID - 1);
uint256 positionId1 = poolKey.toId().toTokenId(ACTIVE_BIN_ID);
uint256 positionId2 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 1);
uint256 positionId3 = poolKey.toId().toTokenId(ACTIVE_BIN_ID + 2);
assertGt(binFungiblePositionManager.balanceOf(address(this), positionId0), 0);
assertGt(binFungiblePositionManager.balanceOf(address(this), positionId1), 0);
assertGt(binFungiblePositionManager.balanceOf(address(this), positionId2), 0);
assertEq(binFungiblePositionManager.balanceOf(address(this), positionId3), 0);

(PoolId poolId, Currency currency0, Currency currency1, uint24 fee, uint24 binId) =
binFungiblePositionManager.positions(positionId0);
assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId()));
assertEq(Currency.unwrap(currency0), address(0));
assertEq(Currency.unwrap(currency1), address(token0));
assertEq(fee, 0);
assertEq(binId, ACTIVE_BIN_ID - 1);

(poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId1);
assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId()));
assertEq(Currency.unwrap(currency0), address(0));
assertEq(Currency.unwrap(currency1), address(token0));
assertEq(fee, 0);
assertEq(binId, ACTIVE_BIN_ID);

(poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId2);
assertEq(PoolId.unwrap(poolId), PoolId.unwrap(poolKey.toId()));
assertEq(Currency.unwrap(currency0), address(0));
assertEq(Currency.unwrap(currency1), address(token0));
assertEq(fee, 0);
assertEq(binId, ACTIVE_BIN_ID + 1);

vm.expectRevert(IBinFungiblePositionManager.InvalidTokenID.selector);
(poolId, currency0, currency1, fee, binId) = binFungiblePositionManager.positions(positionId3);
}

function _mintV2Liquidity(IPancakePair pair) public {
IERC20(pair.token0()).transfer(address(pair), 10 ether);
IERC20(pair.token1()).transfer(address(pair), 10 ether);
Expand Down
Loading

0 comments on commit c331872

Please sign in to comment.