Skip to content

Commit 04585d1

Browse files
authored
feat: add position manager helper (#89)
* feat: add position manager helper * feat: format code * feat: include permit2 forwarder * feat: add re-entrancy lock * feat: add deployment script * bug: check liquidityParam.to for slippage * feat: remove duplicate isNative check * fix: expecting accending binId -- no duplicates * feat: deploy on bnb and base
1 parent 41dc95b commit 04585d1

11 files changed

Lines changed: 816 additions & 5 deletions
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.24;
3+
4+
import "forge-std/Script.sol";
5+
import {BaseScript} from "./BaseScript.sol";
6+
import {IBinPoolManager} from "infinity-core/src/pool-bin/interfaces/IBinPoolManager.sol";
7+
import {IBinPositionManagerWithERC1155} from "../src/pool-bin/interfaces/IBinPositionManagerWithERC1155.sol";
8+
import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
9+
import {IWETH9} from "../src/interfaces/external/IWETH9.sol";
10+
import {Create3Factory} from "pancake-create3-factory/src/Create3Factory.sol";
11+
import {BinPositionManagerHelper} from "../src/pool-bin/BinPositionManagerHelper.sol";
12+
13+
/**
14+
* Pre-req: foundry on stable (1.0) otherwise verify will fail: ref https://github.com/foundry-rs/foundry/issues/9698
15+
*
16+
* Step 1: Deploy
17+
* forge script script/10_DeployBinPositionManagerHelper.s.sol:DeployBinPositionManagerHelper -vvv \
18+
* --rpc-url $RPC_URL \
19+
* --broadcast \
20+
* --slow \
21+
* --verify
22+
*/
23+
contract DeployBinPositionManagerHelper is BaseScript {
24+
function getDeploymentSalt() public pure override returns (bytes32) {
25+
return keccak256("INFINITY-PERIPHERY/BinPositionManagerHelper/1.0.0");
26+
}
27+
28+
function run() public {
29+
Create3Factory factory = Create3Factory(getAddressFromConfig("create3Factory"));
30+
31+
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
32+
vm.startBroadcast(deployerPrivateKey);
33+
34+
address binPoolManager = getAddressFromConfig("binPoolManager");
35+
emit log_named_address("binPoolManager", binPoolManager);
36+
37+
address binPositionManager = getAddressFromConfig("binPositionManager");
38+
emit log_named_address("binPositionManager", binPositionManager);
39+
40+
address permit2 = getAddressFromConfig("permit2");
41+
emit log_named_address("Permit2", permit2);
42+
43+
address weth = getAddressFromConfig("weth");
44+
emit log_named_address("WETH", weth);
45+
46+
bytes memory creationCodeData = abi.encode(
47+
IBinPoolManager(binPoolManager),
48+
IBinPositionManagerWithERC1155(binPositionManager),
49+
IAllowanceTransfer(permit2),
50+
IWETH9(weth)
51+
);
52+
bytes memory creationCode = abi.encodePacked(type(BinPositionManagerHelper).creationCode, creationCodeData);
53+
address binPositionManagerHelper =
54+
factory.deploy(getDeploymentSalt(), creationCode, keccak256(creationCode), 0, new bytes(0), 0);
55+
emit log_named_address("BinPositionManagerHelper", binPositionManagerHelper);
56+
57+
vm.stopBroadcast();
58+
}
59+
}

script/config/base-mainnet.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@
1919
"factoryV2": "0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E",
2020
"factoryStable": "0x64D74e1EAAe3176744b5767b93B7Bee39Cf7898F",
2121
"mixedQuoter": "0x2dCbF7B985c8C5C931818e4E107bAe8aaC8dAB7C",
22-
"clTickLens": "0x8bcf30285413f25032fb983c2bf4defe29a33f3a"
22+
"clTickLens": "0x8bcf30285413f25032fb983c2bf4defe29a33f3a",
23+
"binPositionManagerHelper": "0xF4c3E0A83Ed93f6be55634C979e71c46c2453D4C"
2324
}

script/config/bsc-mainnet.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@
1919
"factoryV2": "0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73",
2020
"factoryStable": "0x25a55f9f2279A54951133D503490342b50E5cd15",
2121
"mixedQuoter": "0x2dCbF7B985c8C5C931818e4E107bAe8aaC8dAB7C",
22-
"clTickLens": "0x8BcF30285413F25032fb983C2bF4deFe29a33f3a"
22+
"clTickLens": "0x8BcF30285413F25032fb983C2bF4deFe29a33f3a",
23+
"binPositionManagerHelper": "0xF4c3E0A83Ed93f6be55634C979e71c46c2453D4C"
2324
}

script/config/bsc-testnet.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@
1919
"factoryV2": "0x6725F303b657a9451d8BA641348b6761A6CC7a17",
2020
"factoryStable": "0xe6A00f8b819244e8Ab9Ea930e46449C2F20B6609",
2121
"mixedQuoter": "0xdf70a0A2DADC2a01dbE165702aa6dCdf034628b0",
22-
"clTickLens": "0x05a732bb9A23256F57f9FdF255212979368Ece39"
22+
"clTickLens": "0x05a732bb9A23256F57f9FdF255212979368Ece39",
23+
"binPositionManagerHelper": "0xF4c3E0A83Ed93f6be55634C979e71c46c2453D4C"
2324
}

script/config/ethereum-mainnet.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@
1313
"binQuoter": "0x",
1414
"clMigrator": "0x",
1515
"binMigrator": "0x",
16-
"clTickLens": "0x"
16+
"clTickLens": "0x",
17+
"binPositionManagerHelper": "0x"
1718
}

script/config/ethereum-sepolia.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@
1717
"factoryV2": "0x1bdc540dEB9Ed1fA29964DeEcCc524A8f5e2198e",
1818
"factoryStable": "0x000000000000000000000000000000000000dEaD",
1919
"mixedQuoter": "0x3F47BDA24e069FC5f28B74d4FAB27EeBA006eF92",
20-
"clTickLens": "0xE95fce3eEFcFf86B1d6a8951405432eA96505c98"
20+
"clTickLens": "0xE95fce3eEFcFf86B1d6a8951405432eA96505c98",
21+
"binPositionManagerHelper": "0x"
2122
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"BinPositionManagerHelper size": "8702",
3+
"test_addLiquidities_existingPool": "1044057",
4+
"test_addLiquidities_existingPool_bobReceiver": "1044057",
5+
"test_addLiquidities_existingPool_nativeToken": "962326",
6+
"test_addLiquidities_newPool": "1168114",
7+
"test_addLiquidities_newPool_WithPermit": "1227164"
8+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
// Copyright (C) 2024 PancakeSwap
3+
pragma solidity 0.8.26;
4+
5+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6+
import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
7+
import {Currency, CurrencyLibrary} from "infinity-core/src/types/Currency.sol";
8+
import {IBinPoolManager} from "infinity-core/src/pool-bin/interfaces/IBinPoolManager.sol";
9+
import {PoolKey} from "infinity-core/src/types/PoolKey.sol";
10+
import {PoolId} from "infinity-core/src/types/PoolId.sol";
11+
import {IVault} from "infinity-core/src/interfaces/IVault.sol";
12+
13+
import {IBinPositionManager} from "./interfaces/IBinPositionManager.sol";
14+
import {IBinPositionManagerWithERC1155} from "./interfaces/IBinPositionManagerWithERC1155.sol";
15+
import {IWETH9} from "../interfaces/external/IWETH9.sol";
16+
import {Actions} from "../libraries/Actions.sol";
17+
import {BinCalldataDecoder} from "./libraries/BinCalldataDecoder.sol";
18+
import {CalldataDecoder} from "../libraries/CalldataDecoder.sol";
19+
import {BinTokenLibrary} from "./libraries/BinTokenLibrary.sol";
20+
import {Multicall} from "../base/Multicall.sol";
21+
import {Permit2Forwarder} from "../base/Permit2Forwarder.sol";
22+
import {ReentrancyLock} from "../base/ReentrancyLock.sol";
23+
24+
/// @title BinPositionManagerHelper
25+
/// @notice Helper contract for adding liquidity to bin pool with additional slippage protection
26+
contract BinPositionManagerHelper is Multicall, Permit2Forwarder, ReentrancyLock {
27+
using CalldataDecoder for bytes;
28+
using BinCalldataDecoder for bytes;
29+
using BinTokenLibrary for PoolId;
30+
31+
/// @notice Thrown when an unexpected address sends ETH to this contract
32+
error InvalidEthSender();
33+
/// @notice Thrown when theres multiple BIN_ADD_LIQUIDITY actions
34+
error DuplicateAddLiquidity();
35+
/// @notice Thrown when there's unsupported action in the payload
36+
error UnsupportedAction();
37+
/// @notice Thrown when no BIN_ADD_LIQUIDITY action is found in the payload
38+
error NoAddLiquidityAction();
39+
/// @notice Thrown when minLiquidityParam's binIds and minLiquidities length mismatch
40+
error MinLiquidityParamsLengthMismatch();
41+
/// @notice Thrown when slippage checks fail
42+
error SlippageCheck(uint24 binId, uint256 liquidityAdded);
43+
/// @notice Thrown when invalid (duplicate or non accending) binIds are found in minLiquidityParam
44+
error InvalidBinId(uint24 binid);
45+
46+
struct MinLiquidityParams {
47+
/// @dev expect accending order of binIds eg. [20, 21, 22]
48+
uint24[] binIds;
49+
uint256[] minLiquidities;
50+
}
51+
52+
IBinPoolManager public immutable binPoolManager;
53+
IBinPositionManagerWithERC1155 public immutable binPositionManager;
54+
IWETH9 public immutable WETH9;
55+
56+
constructor(
57+
IBinPoolManager _binPoolManager,
58+
IBinPositionManagerWithERC1155 _binPositionManager,
59+
IAllowanceTransfer _permit2,
60+
IWETH9 _weth9
61+
) Permit2Forwarder(_permit2) {
62+
binPoolManager = _binPoolManager;
63+
binPositionManager = _binPositionManager;
64+
permit2 = _permit2;
65+
WETH9 = _weth9;
66+
}
67+
68+
/// @notice Add liquidities to bin pool with slippage protection
69+
/// @param payload - encoded actions and parameters for bin position manager
70+
/// @param deadline - deadline for the transaction
71+
/// @param minLiquidityParam - amount of [binId, liquidity] to mint
72+
/// @dev This function only support 1 BIN_ADD_LIQUIDITY call
73+
function addLiquidities(bytes calldata payload, uint256 deadline, MinLiquidityParams memory minLiquidityParam)
74+
external
75+
payable
76+
isNotLocked
77+
{
78+
if (minLiquidityParam.binIds.length != minLiquidityParam.minLiquidities.length) {
79+
revert MinLiquidityParamsLengthMismatch();
80+
}
81+
82+
// Step 1: decode payload
83+
IBinPositionManager.BinAddLiquidityParams memory liquidityParams = _getAddLiquidityParam(payload);
84+
Currency currency0 = liquidityParams.poolKey.currency0;
85+
Currency currency1 = liquidityParams.poolKey.currency1;
86+
87+
// Step 2: Transfer token from user and permit2 approve so BinPositionManager can take the tokens later
88+
if (!currency0.isNative()) {
89+
// for native case, do not need to check msg.value, as binPositionManager will revert
90+
permit2.transferFrom(msg.sender, address(this), liquidityParams.amount0Max, Currency.unwrap(currency0));
91+
_approveBinPm(currency0, liquidityParams.amount0Max);
92+
}
93+
permit2.transferFrom(msg.sender, address(this), liquidityParams.amount1Max, Currency.unwrap(currency1));
94+
_approveBinPm(currency1, liquidityParams.amount1Max);
95+
96+
// Step 3a: Before Check user balance before
97+
address[] memory owners = new address[](minLiquidityParam.binIds.length);
98+
uint256[] memory tokenIds = new uint256[](minLiquidityParam.binIds.length);
99+
PoolId poolId = liquidityParams.poolKey.toId();
100+
uint24 tempBinId = 0; // check duplicate binId
101+
for (uint256 i = 0; i < minLiquidityParam.binIds.length; i++) {
102+
owners[i] = liquidityParams.to;
103+
// Sanity check -- eg. assume binId is accending order, so this will check duplicate as well
104+
if (tempBinId >= minLiquidityParam.binIds[i]) revert InvalidBinId(minLiquidityParam.binIds[i]);
105+
tempBinId = minLiquidityParam.binIds[i];
106+
tokenIds[i] = poolId.toTokenId(tempBinId);
107+
}
108+
uint256[] memory balBefore = binPositionManager.balanceOfBatch(owners, tokenIds);
109+
110+
// Step 3b: modify liquidities
111+
binPositionManager.modifyLiquidities{value: msg.value}(payload, deadline);
112+
113+
// Step 3c: Check user balance after
114+
uint256[] memory balAfter = binPositionManager.balanceOfBatch(owners, tokenIds);
115+
for (uint256 i = 0; i < minLiquidityParam.minLiquidities.length; i++) {
116+
uint256 liquidityAdded = balAfter[i] - balBefore[i];
117+
if (liquidityAdded < minLiquidityParam.minLiquidities[i]) {
118+
revert SlippageCheck(minLiquidityParam.binIds[i], liquidityAdded);
119+
}
120+
}
121+
122+
// Step 4: refund the user of any balance t0, t1 in contract
123+
if (currency0.balanceOfSelf() > 0) {
124+
currency0.transfer(msg.sender, currency0.balanceOfSelf());
125+
}
126+
if (currency1.balanceOfSelf() > 0) {
127+
currency1.transfer(msg.sender, currency1.balanceOfSelf());
128+
}
129+
}
130+
131+
/// @notice Initialize a infinity PCS bin pool
132+
/// @dev For a new pool, user will use multiCall[initializePool, addLiquidities], implementation copied from BinPositionManager
133+
function initializePool(PoolKey memory key, uint24 activeId) external payable {
134+
/// @dev if the pool revert due to other error (currencyOutOfOrder etc..), then the follow-up action to the pool will still revert accordingly
135+
try binPoolManager.initialize(key, activeId) {} catch {}
136+
}
137+
138+
/// @notice Approve the bin position manager to spend the currency
139+
/// @dev assume currency is not native
140+
function _approveBinPm(Currency _currency, uint160 _amount) internal {
141+
IERC20(Currency.unwrap(_currency)).approve(address(permit2), _amount);
142+
permit2.approve(Currency.unwrap(_currency), address(binPositionManager), _amount, uint48(block.timestamp));
143+
}
144+
145+
function _getAddLiquidityParam(bytes calldata payload)
146+
internal
147+
pure
148+
returns (IBinPositionManager.BinAddLiquidityParams memory liquidityParams)
149+
{
150+
(bytes calldata actions, bytes[] calldata params) = payload.decodeActionsRouterParams();
151+
uint256 numActions = actions.length;
152+
for (uint256 actionIndex = 0; actionIndex < numActions; actionIndex++) {
153+
uint256 action = uint8(actions[actionIndex]);
154+
155+
if (action == Actions.BIN_ADD_LIQUIDITY) {
156+
if (liquidityParams.activeIdDesired != 0) revert DuplicateAddLiquidity();
157+
liquidityParams = params[actionIndex].decodeBinAddLiquidityParams();
158+
}
159+
160+
// FE won't call BIN_ADD_LIQUIDITY_FROM_DELTAS
161+
if (action == Actions.BIN_ADD_LIQUIDITY_FROM_DELTAS || action == Actions.BIN_REMOVE_LIQUIDITY) {
162+
revert UnsupportedAction();
163+
}
164+
}
165+
166+
if (liquidityParams.activeIdDesired == 0) {
167+
revert NoAddLiquidityAction();
168+
}
169+
170+
return liquidityParams;
171+
}
172+
173+
receive() external payable {
174+
if (msg.sender != address(WETH9) && msg.sender != address(binPositionManager)) revert InvalidEthSender();
175+
}
176+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import {IBinPositionManager} from "./IBinPositionManager.sol";
5+
6+
interface IBinPositionManagerWithERC1155 is IBinPositionManager {
7+
function balanceOfBatch(address[] calldata owners, uint256[] calldata ids)
8+
external
9+
returns (uint256[] memory balances);
10+
}

0 commit comments

Comments
 (0)