From 4351cc4f25db7aaff263555f454c1c402bbef217 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:41:16 +0800 Subject: [PATCH 1/9] feat: add position manager helper --- snapshots/BinPositionManagerHelperTest.json | 6 + src/pool-bin/BinPositionManagerHelper.sol | 171 ++++++++ .../IBinPositionManagerWithERC1155.sol | 10 + test/pool-bin/BinPositionManagerHelper.t.sol | 373 ++++++++++++++++++ test/pool-bin/helper/BinLiquidityHelper.sol | 3 + 5 files changed, 563 insertions(+) create mode 100644 snapshots/BinPositionManagerHelperTest.json create mode 100644 src/pool-bin/BinPositionManagerHelper.sol create mode 100644 src/pool-bin/interfaces/IBinPositionManagerWithERC1155.sol create mode 100644 test/pool-bin/BinPositionManagerHelper.t.sol diff --git a/snapshots/BinPositionManagerHelperTest.json b/snapshots/BinPositionManagerHelperTest.json new file mode 100644 index 0000000..e87757d --- /dev/null +++ b/snapshots/BinPositionManagerHelperTest.json @@ -0,0 +1,6 @@ +{ + "BinPositionManagerHelper size": "7015", + "test_addLiquidities_existingPool": "1043471", + "test_addLiquidities_existingPool_nativeToken": "961745", + "test_addLiquidities_newPool": "1167450" +} \ No newline at end of file diff --git a/src/pool-bin/BinPositionManagerHelper.sol b/src/pool-bin/BinPositionManagerHelper.sol new file mode 100644 index 0000000..f331e4f --- /dev/null +++ b/src/pool-bin/BinPositionManagerHelper.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2024 PancakeSwap +pragma solidity 0.8.26; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +import {Currency, CurrencyLibrary} from "infinity-core/src/types/Currency.sol"; +import {IBinPoolManager} from "infinity-core/src/pool-bin/interfaces/IBinPoolManager.sol"; +import {PoolKey} from "infinity-core/src/types/PoolKey.sol"; +import {PoolId} from "infinity-core/src/types/PoolId.sol"; +import {IVault} from "infinity-core/src/interfaces/IVault.sol"; + +import {Actions} from "../libraries/Actions.sol"; +import {BinCalldataDecoder} from "./libraries/BinCalldataDecoder.sol"; +import {CalldataDecoder} from "../libraries/CalldataDecoder.sol"; +import {IBinPositionManager} from "./interfaces/IBinPositionManager.sol"; +import {IBinPositionManagerWithERC1155} from "./interfaces/IBinPositionManagerWithERC1155.sol"; +import {Multicall} from "../base/Multicall.sol"; +import {BinTokenLibrary} from "./libraries/BinTokenLibrary.sol"; +import {IWETH9} from "../interfaces/external/IWETH9.sol"; + +import {console} from "forge-std/console.sol"; + +/// @title BinPositionManagerHelper +/// @notice Helper contract for adding liquidity to bin pool with additional slippage protection +contract BinPositionManagerHelper is Multicall { + using CalldataDecoder for bytes; + using BinCalldataDecoder for bytes; + using BinTokenLibrary for PoolId; + + /// @notice Thrown when an unexpected address sends ETH to this contract + error InvalidEthSender(); + /// @notice Thrown when theres multiple BIN_ADD_LIQUIDITY actions + error DuplicateAddLiquidity(); + /// @notice Thrown when there's unsupported action in the payload + error UnsupportedAction(); + /// @notice Thrown when no BIN_ADD_LIQUIDITY action is found in the payload + error NoAddLiquidityAction(); + /// @notice Thrown when minLiquidityParam's binIds and minLiquidities length mismatch + error MinLiquidityParamsLengthMismatch(); + /// @notice Thrown when slippage checks fail + error SlippageCheck(uint256 binId, uint256 liquidityAdded); + + struct MinLiquidityParams { + uint24[] binIds; + uint256[] minLiquidities; + } + + IBinPoolManager public immutable binPoolManager; + IBinPositionManagerWithERC1155 public immutable binPositionManager; + IAllowanceTransfer public immutable permit2; + IWETH9 public immutable WETH9; + + constructor( + IBinPoolManager _binPoolManager, + IBinPositionManagerWithERC1155 _binPositionManager, + IAllowanceTransfer _permit2, + IWETH9 _weth9 + ) { + binPoolManager = _binPoolManager; + binPositionManager = _binPositionManager; + permit2 = _permit2; + WETH9 = _weth9; + } + + /// @notice Add liquidities to bin pool with slippage protection + /// @param payload - encoded actions and parameters for bin position manager + /// @param deadline - deadline for the transaction + /// @param minLiquidityParam - amount of [binId, liquidity] to mint + /// @dev This function only support 1 BIN_ADD_LIQUIDITY call + function addLiquidities(bytes calldata payload, uint256 deadline, MinLiquidityParams memory minLiquidityParam) + external + payable + { + if (minLiquidityParam.binIds.length != minLiquidityParam.minLiquidities.length) { + revert MinLiquidityParamsLengthMismatch(); + } + + // Step 1: decode payload + IBinPositionManager.BinAddLiquidityParams memory liquidityParams = _getAddLiquidityParam(payload); + Currency currency0 = liquidityParams.poolKey.currency0; + Currency currency1 = liquidityParams.poolKey.currency1; + + // Step 2: Transfer token from user and permit2 approve so BinPositionManager can take the tokens later + if (!currency0.isNative()) { + // for native case, do not need to check msg.value, as binPositionManager will revert + permit2.transferFrom(msg.sender, address(this), liquidityParams.amount0Max, Currency.unwrap(currency0)); + _approveBinPm(currency0, liquidityParams.amount0Max); + } + permit2.transferFrom(msg.sender, address(this), liquidityParams.amount1Max, Currency.unwrap(currency1)); + _approveBinPm(currency1, liquidityParams.amount1Max); + + // Step 3a: Before Check user balance before + address[] memory owners = new address[](minLiquidityParam.binIds.length); + for (uint256 i = 0; i < minLiquidityParam.binIds.length; i++) { + owners[i] = msg.sender; + } + uint256[] memory tokenIds = new uint256[](minLiquidityParam.binIds.length); + PoolId poolId = liquidityParams.poolKey.toId(); + for (uint256 i; i < minLiquidityParam.binIds.length; i++) { + tokenIds[i] = poolId.toTokenId(minLiquidityParam.binIds[i]); + } + uint256[] memory balBefore = binPositionManager.balanceOfBatch(owners, tokenIds); + + // Step 3b: modify liquidities + binPositionManager.modifyLiquidities{value: msg.value}(payload, deadline); + + // Step 3c: Check user balance after + uint256[] memory balAfter = binPositionManager.balanceOfBatch(owners, tokenIds); + for (uint256 i = 0; i < minLiquidityParam.minLiquidities.length; i++) { + uint256 liquidityAdded = balAfter[i] - balBefore[i]; + if (liquidityAdded < minLiquidityParam.minLiquidities[i]) { + revert SlippageCheck(minLiquidityParam.binIds[i], liquidityAdded); + } + } + + // Step 4: refund the user of any balance t0, t1 in contract + if (currency0.balanceOfSelf() > 0) { + currency0.transfer(msg.sender, currency0.balanceOfSelf()); + } + if (currency1.balanceOfSelf() > 0) { + currency1.transfer(msg.sender, currency1.balanceOfSelf()); + } + } + + /// @notice Initialize a infinity PCS bin pool + /// @dev For a new pool, user will use multiCall[initializePool, addLiquidities], implementation copied from BinPositionManager + function initializePool(PoolKey memory key, uint24 activeId) external payable { + /// @dev if the pool revert due to other error (currencyOutOfOrder etc..), then the follow-up action to the pool will still revert accordingly + try binPoolManager.initialize(key, activeId) {} catch {} + } + + function _approveBinPm(Currency _currency, uint160 _amount) internal { + if (!_currency.isNative()) { + IERC20(Currency.unwrap(_currency)).approve(address(permit2), _amount); + permit2.approve(Currency.unwrap(_currency), address(binPositionManager), _amount, uint48(block.timestamp)); + } + } + + function _getAddLiquidityParam(bytes calldata payload) + internal + pure + returns (IBinPositionManager.BinAddLiquidityParams memory liquidityParams) + { + (bytes calldata actions, bytes[] calldata params) = payload.decodeActionsRouterParams(); + uint256 numActions = actions.length; + for (uint256 actionIndex = 0; actionIndex < numActions; actionIndex++) { + uint256 action = uint8(actions[actionIndex]); + + if (action == Actions.BIN_ADD_LIQUIDITY) { + if (liquidityParams.activeIdDesired != 0) revert DuplicateAddLiquidity(); + liquidityParams = params[actionIndex].decodeBinAddLiquidityParams(); + } + + // FE won't call BIN_ADD_LIQUIDITY_FROM_DELTAS + if (action == Actions.BIN_ADD_LIQUIDITY_FROM_DELTAS || action == Actions.BIN_REMOVE_LIQUIDITY) { + revert UnsupportedAction(); + } + } + + if (liquidityParams.activeIdDesired == 0) { + revert NoAddLiquidityAction(); + } + + return liquidityParams; + } + + receive() external payable { + if (msg.sender != address(WETH9) && msg.sender != address(binPositionManager)) revert InvalidEthSender(); + } +} diff --git a/src/pool-bin/interfaces/IBinPositionManagerWithERC1155.sol b/src/pool-bin/interfaces/IBinPositionManagerWithERC1155.sol new file mode 100644 index 0000000..7e74da1 --- /dev/null +++ b/src/pool-bin/interfaces/IBinPositionManagerWithERC1155.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IBinPositionManager} from "./IBinPositionManager.sol"; + +interface IBinPositionManagerWithERC1155 is IBinPositionManager { + function balanceOfBatch(address[] calldata owners, uint256[] calldata ids) + external + returns (uint256[] memory balances); +} diff --git a/test/pool-bin/BinPositionManagerHelper.t.sol b/test/pool-bin/BinPositionManagerHelper.t.sol new file mode 100644 index 0000000..725ccd4 --- /dev/null +++ b/test/pool-bin/BinPositionManagerHelper.t.sol @@ -0,0 +1,373 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +import {DeployPermit2} from "permit2/test/utils/DeployPermit2.sol"; + +import {Vault} from "infinity-core/src/Vault.sol"; +import {PoolKey} from "infinity-core/src/types/PoolKey.sol"; +import {IVault} from "infinity-core/src/interfaces/IVault.sol"; +import {IBinPoolManager} from "infinity-core/src/pool-bin/interfaces/IBinPoolManager.sol"; +import {BinPoolManager} from "infinity-core/src/pool-bin/BinPoolManager.sol"; +import {TokenFixture} from "infinity-core/test/helpers/TokenFixture.sol"; +import {IHooks} from "infinity-core/src/interfaces/IHooks.sol"; +import {BinPoolParametersHelper} from "infinity-core/src/pool-bin/libraries/BinPoolParametersHelper.sol"; +import {Currency, CurrencyLibrary} from "infinity-core/src/types/Currency.sol"; +import {BinHelper} from "infinity-core/src/pool-bin/libraries/BinHelper.sol"; +import {PriceHelper} from "infinity-core/src/pool-bin/libraries/PriceHelper.sol"; +import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; +import {BinPositionManager} from "../../src/pool-bin/BinPositionManager.sol"; +import {IBinPositionManager} from "../../src/pool-bin/interfaces/IBinPositionManager.sol"; +import {IWETH9} from "../../src/interfaces/external/IWETH9.sol"; +import {BinLiquidityHelper} from "./helper/BinLiquidityHelper.sol"; +import {Planner, Plan} from "../../src/libraries/Planner.sol"; +import {Actions} from "../../src/libraries/Actions.sol"; +import {ActionConstants} from "../../src/libraries/ActionConstants.sol"; + +import {BinPositionManagerHelper} from "../../src/pool-bin/BinPositionManagerHelper.sol"; +import {IBinPositionManagerWithERC1155} from "../../src/pool-bin/interfaces/IBinPositionManagerWithERC1155.sol"; + +contract BinPositionManagerHelperTest is Test, TokenFixture, DeployPermit2, BinLiquidityHelper { + using BinPoolParametersHelper for bytes32; + + error ContractSizeTooLarge(uint256 diff); + + bytes constant ZERO_BYTES = new bytes(0); + uint256 _deadline = block.timestamp + 1; + + Vault vault; + BinPoolManager poolManager; + BinPositionManager binPm; + IAllowanceTransfer permit2; + BinPositionManagerHelper binPmHelper; + + MockERC20 token0; + MockERC20 token1; + + PoolKey key1; // initialized pool + PoolKey key2; // uninitialized pool + PoolKey key3; // initialized pool with native eth + + bytes32 poolParam; + address alice = makeAddr("alice"); + uint24 activeId = 2 ** 23; // where token0 and token1 price is the same + + function setUp() public { + vault = new Vault(); + poolManager = new BinPoolManager(IVault(address(vault))); + vault.registerApp(address(poolManager)); + permit2 = IAllowanceTransfer(deployPermit2()); + initializeTokens(); + (token0, token1) = (MockERC20(Currency.unwrap(currency0)), MockERC20(Currency.unwrap(currency1))); + + binPm = new BinPositionManager( + IVault(address(vault)), IBinPoolManager(address(poolManager)), permit2, IWETH9(address(0)) + ); + + binPmHelper = new BinPositionManagerHelper( + IBinPoolManager(address(poolManager)), + IBinPositionManagerWithERC1155(address(binPm)), + permit2, + IWETH9(address(0)) + ); + + key1 = PoolKey({ + currency0: currency0, + currency1: currency1, + hooks: IHooks(address(0)), + poolManager: IBinPoolManager(address(poolManager)), + fee: uint24(1000), // 0.1% pool + parameters: poolParam.setBinStep(10) // binStep + }); + binPmHelper.initializePool(key1, activeId); + + key2 = PoolKey({ + currency0: currency0, + currency1: currency1, + hooks: IHooks(address(0)), + poolManager: IBinPoolManager(address(poolManager)), + fee: uint24(10_000), // 1% pool + parameters: poolParam.setBinStep(100) // binStep + }); + + key3 = PoolKey({ + currency0: CurrencyLibrary.NATIVE, + currency1: currency1, + hooks: IHooks(address(0)), + poolManager: IBinPoolManager(address(poolManager)), + fee: uint24(10_000), // 1% pool + parameters: poolParam.setBinStep(100) // binStep + }); + binPmHelper.initializePool(key3, activeId); + + // pre-req: alice approve permit2 -> binPmHelper for token0/token1 + approveBinPm(alice, key1, address(binPmHelper), permit2); + approveBinPm(alice, key3, address(binPmHelper), permit2); + } + + function test_bytecodeSize() public { + vm.snapshotValue("BinPositionManagerHelper size", address(binPmHelper).code.length); + + if (address(binPmHelper).code.length > 24576) { + revert ContractSizeTooLarge(address(binPmHelper).code.length - 24576); + } + } + + function test_addLiquidities_existingPool() public { + // before + token0.mint(alice, 4 ether); + token1.mint(alice, 4 ether); + assertEq(token0.balanceOf(alice), 4 ether); + assertEq(token1.balanceOf(alice), 4 ether); + + // step 1: prepare param liquidity. means roughly 3 bins + // with the following tokens in each bin: [1.5 token1, 1.5 token1 + 1.5 token0, 1.5 token0] + uint24[] memory binIds = getBinIds(activeId, 3); + IBinPositionManager.BinAddLiquidityParams memory param = + _getAddParams(key1, binIds, 3 ether, 3 ether, activeId, alice); + param.amount0Max = 3 ether * 1.1; // assume 10% slippage + param.amount1Max = 3 ether * 1.1; // assume 10% slippage + param.idSlippage = 0; // actveId is the same + + // step 2: prepare minLiquidity param -- since id slippage, is 0, we can just check activeId minLiquidity + uint24[] memory minLiquidityBinIds = new uint24[](1); + uint256[] memory minLiquiditys = new uint256[](1); + uint256 price = PriceHelper.getPriceFromId(activeId, key1.parameters.getBinStep()); + minLiquidityBinIds[0] = activeId; + minLiquiditys[0] = BinHelper.getLiquidity(1.5 ether, 1.5 ether, price) * 9999 / 10_000; // 0.01% slippage + BinPositionManagerHelper.MinLiquidityParams memory minLiquidityParam = + BinPositionManagerHelper.MinLiquidityParams({binIds: minLiquidityBinIds, minLiquidities: minLiquiditys}); + + // Step 3: prepare and call + Plan memory planner = Planner.init(); + planner.add(Actions.BIN_ADD_LIQUIDITY, abi.encode(param)); + planner.add(Actions.SETTLE_PAIR, abi.encode(currency0, currency1)); + bytes memory payload = planner.encode(); + vm.prank(alice); + binPmHelper.addLiquidities(payload, _deadline, minLiquidityParam); + vm.snapshotGasLastCall("test_addLiquidities_existingPool"); + + // after + assertEq(token0.balanceOf(alice), 1 ether); // initial 4 ether, then minus 3 ether added + assertEq(token1.balanceOf(alice), 1 ether); // initial 4 ether, then minus 3 ether added + } + + /// @dev ensure add liquidity works for pool with currency0 as native token + function test_addLiquidities_existingPool_nativeToken() public { + // before + vm.deal(alice, 4 ether); + token1.mint(alice, 4 ether); + assertEq(alice.balance, 4 ether); + assertEq(token1.balanceOf(alice), 4 ether); + + // step 1: prepare param liquidity. means roughly 3 bins + // with the following tokens in each bin: [1.5 token1, 1.5 token1 + 1.5 token0, 1.5 token0] + uint24[] memory binIds = getBinIds(activeId, 3); + IBinPositionManager.BinAddLiquidityParams memory param = + _getAddParams(key3, binIds, 3 ether, 3 ether, activeId, alice); + param.amount0Max = 3 ether * 1.1; // assume 10% slippage + param.amount1Max = 3 ether * 1.1; // assume 10% slippage + param.idSlippage = 0; // actveId is the same + + // step 2: prepare minLiquidity param -- since id slippage, is 0, we can just check activeId minLiquidity + uint24[] memory minLiquidityBinIds = new uint24[](1); + uint256[] memory minLiquiditys = new uint256[](1); + uint256 price = PriceHelper.getPriceFromId(activeId, key3.parameters.getBinStep()); + minLiquidityBinIds[0] = activeId; + minLiquiditys[0] = BinHelper.getLiquidity(1.5 ether, 1.5 ether, price) * 9999 / 10_000; // 0.01% slippage + BinPositionManagerHelper.MinLiquidityParams memory minLiquidityParam = + BinPositionManagerHelper.MinLiquidityParams({binIds: minLiquidityBinIds, minLiquidities: minLiquiditys}); + + // Step 3: prepare and call + Plan memory planner = Planner.init(); + planner.add(Actions.BIN_ADD_LIQUIDITY, abi.encode(param)); + planner.add(Actions.SETTLE_PAIR, abi.encode(key3.currency0, key3.currency1)); + planner.add(Actions.SWEEP, abi.encode(CurrencyLibrary.NATIVE, ActionConstants.MSG_SENDER)); + bytes memory payload = planner.encode(); + vm.prank(alice); + binPmHelper.addLiquidities{value: 4 ether}(payload, _deadline, minLiquidityParam); + vm.snapshotGasLastCall("test_addLiquidities_existingPool_nativeToken"); + + // after + assertEq(alice.balance, 1 ether); // initial 4 ether, then minus 3 ether added + assertEq(token1.balanceOf(alice), 1 ether); // initial 4 ether, then minus 3 ether added + } + + function test_addLiquidities_existingPool_slippage() public { + // before + token0.mint(alice, 4 ether); + token1.mint(alice, 4 ether); + + // step 1: prepare param liquidity. means roughly 3 bins + // with the following tokens in each bin: [1.5 token1, 1.5 token1 + 1.5 token0, 1.5 token0] + uint24[] memory binIds = getBinIds(activeId, 3); + IBinPositionManager.BinAddLiquidityParams memory param = + _getAddParams(key1, binIds, 2 ether, 2 ether, activeId, alice); + param.amount0Max = 2 ether * 1.1; // assume 10% slippage + param.amount1Max = 2 ether * 1.1; // assume 10% slippage + param.idSlippage = 0; // actveId is the same + + // step 2: prepare minLiquidity param -- since id slippage, is 0, we can just check activeId minLiquidity + uint24[] memory minLiquidityBinIds = new uint24[](1); + uint256[] memory minLiquiditys = new uint256[](1); + uint256 price = PriceHelper.getPriceFromId(activeId, key1.parameters.getBinStep()); + minLiquidityBinIds[0] = activeId; + // expected 1 liquidity more than default. its in 128.128 format + minLiquiditys[0] = BinHelper.getLiquidity(1 ether, 1 ether, price) * 105 / 100; // expect 5% more + BinPositionManagerHelper.MinLiquidityParams memory minLiquidityParam = + BinPositionManagerHelper.MinLiquidityParams({binIds: minLiquidityBinIds, minLiquidities: minLiquiditys}); + + // Step 3: prepare and call + Plan memory planner = Planner.init(); + planner.add(Actions.BIN_ADD_LIQUIDITY, abi.encode(param)); + planner.add(Actions.SETTLE_PAIR, abi.encode(currency0, currency1)); + bytes memory payload = planner.encode(); + vm.prank(alice); + vm.expectRevert( + abi.encodeWithSelector( + BinPositionManagerHelper.SlippageCheck.selector, + minLiquidityBinIds[0], + 680564733841876926926749214863536422911999999999999999000 // roughly BinHelper.getLiquidity(1 ether, 1 ether, price) + ) + ); + binPmHelper.addLiquidities(payload, _deadline, minLiquidityParam); + } + + /// @dev example test with multiCall (to include initializePool) + function test_addLiquidities_newPool() public { + // before + token0.mint(alice, 4 ether); + token1.mint(alice, 4 ether); + assertEq(token0.balanceOf(alice), 4 ether); + assertEq(token1.balanceOf(alice), 4 ether); + + // step 1: prepare param liquidity. means roughly 3 bins + // with the following tokens in each bin: [1.5 token1, 1.5 token1 + 1.5 token0, 1.5 token0] + uint24[] memory binIds = getBinIds(activeId, 3); + IBinPositionManager.BinAddLiquidityParams memory param = + _getAddParams(key2, binIds, 3 ether, 3 ether, activeId, alice); + param.amount0Max = 3 ether * 1.1; // assume 10% slippage + param.amount1Max = 3 ether * 1.1; // assume 10% slippage + param.idSlippage = 0; // actveId is the same + + // step 2: prepare minLiquidity param -- since id slippage, is 0, we can just check activeId minLiquidity + uint24[] memory minLiquidityBinIds = new uint24[](1); + uint256[] memory minLiquiditys = new uint256[](1); + uint256 price = PriceHelper.getPriceFromId(activeId, key2.parameters.getBinStep()); + minLiquidityBinIds[0] = activeId; + minLiquiditys[0] = BinHelper.getLiquidity(1.5 ether, 1.5 ether, price) * 9999 / 10_000; // 0.01% slippage + BinPositionManagerHelper.MinLiquidityParams memory minLiquidityParam = + BinPositionManagerHelper.MinLiquidityParams({binIds: minLiquidityBinIds, minLiquidities: minLiquiditys}); + + // Step 3: prepare and call + Plan memory planner = Planner.init(); + planner.add(Actions.BIN_ADD_LIQUIDITY, abi.encode(param)); + planner.add(Actions.SETTLE_PAIR, abi.encode(currency0, currency1)); + bytes memory payload = planner.encode(); + + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSelector(binPmHelper.initializePool.selector, key2, activeId, ZERO_BYTES); + calls[1] = abi.encodeWithSelector(binPmHelper.addLiquidities.selector, payload, _deadline, minLiquidityParam); + vm.prank(alice); + binPmHelper.multicall(calls); + vm.snapshotGasLastCall("test_addLiquidities_newPool"); + + // after + assertEq(token0.balanceOf(alice), 1 ether); // initial 4 ether, then minus 3 ether added + assertEq(token1.balanceOf(alice), 1 ether); // initial 4 ether, then minus 3 ether added + } + + function test_addLiquidities_newPool_DuplicateAddLiquidity() external { + // step 1: prepare param liquidity. + uint24[] memory binIds = getBinIds(activeId, 3); + IBinPositionManager.BinAddLiquidityParams memory param = + _getAddParams(key1, binIds, 3 ether, 3 ether, activeId, alice); + + // step 2: prepare minLiquidity param -- since id slippage, is 0, we can just check activeId minLiquidity + uint24[] memory minLiquidityBinIds = new uint24[](1); + uint256[] memory minLiquiditys = new uint256[](1); + BinPositionManagerHelper.MinLiquidityParams memory minLiquidityParam = + BinPositionManagerHelper.MinLiquidityParams({binIds: minLiquidityBinIds, minLiquidities: minLiquiditys}); + + // Step 3: prepare and call + Plan memory planner = Planner.init(); + planner.add(Actions.BIN_ADD_LIQUIDITY, abi.encode(param)); + planner.add(Actions.BIN_ADD_LIQUIDITY, abi.encode(param)); + planner.add(Actions.SETTLE_PAIR, abi.encode(currency0, currency1)); + bytes memory payload = planner.encode(); + vm.prank(alice); + vm.expectRevert(BinPositionManagerHelper.DuplicateAddLiquidity.selector); + binPmHelper.addLiquidities(payload, _deadline, minLiquidityParam); + } + + function test_addLiquidities_newPool_UnsupportedAction() external { + // step 1: prepare param liquidity. + uint24[] memory binIds = getBinIds(activeId, 3); + IBinPositionManager.BinAddLiquidityParams memory param = + _getAddParams(key1, binIds, 3 ether, 3 ether, activeId, alice); + + // step 2: prepare minLiquidity param -- since id slippage, is 0, we can just check activeId minLiquidity + uint24[] memory minLiquidityBinIds = new uint24[](1); + uint256[] memory minLiquiditys = new uint256[](1); + BinPositionManagerHelper.MinLiquidityParams memory minLiquidityParam = + BinPositionManagerHelper.MinLiquidityParams({binIds: minLiquidityBinIds, minLiquidities: minLiquiditys}); + + // Step 3: prepare and call + Plan memory planner = Planner.init(); + planner.add(Actions.BIN_ADD_LIQUIDITY_FROM_DELTAS, abi.encode(param)); + planner.add(Actions.SETTLE_PAIR, abi.encode(currency0, currency1)); + bytes memory payload = planner.encode(); + vm.prank(alice); + vm.expectRevert(BinPositionManagerHelper.UnsupportedAction.selector); + binPmHelper.addLiquidities(payload, _deadline, minLiquidityParam); + } + + function test_addLiquidities_newPool_NoAddLiquidityAction() external { + uint24[] memory minLiquidityBinIds = new uint24[](1); + uint256[] memory minLiquiditys = new uint256[](1); + BinPositionManagerHelper.MinLiquidityParams memory minLiquidityParam = + BinPositionManagerHelper.MinLiquidityParams({binIds: minLiquidityBinIds, minLiquidities: minLiquiditys}); + + // Prepare and call + Plan memory planner = Planner.init(); + planner.add(Actions.SETTLE_PAIR, abi.encode(currency0, currency1)); + bytes memory payload = planner.encode(); + vm.prank(alice); + vm.expectRevert(BinPositionManagerHelper.NoAddLiquidityAction.selector); + binPmHelper.addLiquidities(payload, _deadline, minLiquidityParam); + } + + function test_addLiquidities_newPool_MinLiquidityParamsLengthMismatch() external { + // step 1: prepare param liquidity. + uint24[] memory binIds = getBinIds(activeId, 3); + IBinPositionManager.BinAddLiquidityParams memory param = + _getAddParams(key1, binIds, 3 ether, 3 ether, activeId, alice); + + // step 2: prepare minLiquidity param -- since id slippage, is 0, we can just check activeId minLiquidity + uint24[] memory minLiquidityBinIds = new uint24[](1); + uint256[] memory minLiquiditys = new uint256[](2); + BinPositionManagerHelper.MinLiquidityParams memory minLiquidityParam = + BinPositionManagerHelper.MinLiquidityParams({binIds: minLiquidityBinIds, minLiquidities: minLiquiditys}); + + // Step 3: prepare and call + Plan memory planner = Planner.init(); + planner.add(Actions.BIN_ADD_LIQUIDITY, abi.encode(param)); + bytes memory payload = planner.encode(); + vm.prank(alice); + vm.expectRevert(BinPositionManagerHelper.MinLiquidityParamsLengthMismatch.selector); + binPmHelper.addLiquidities(payload, _deadline, minLiquidityParam); + } + + function _getMinLiquidityParam(uint24[] memory binIds, uint256[] memory minLiquidities) + internal + pure + returns (BinPositionManagerHelper.MinLiquidityParams memory) + { + require(binIds.length == minLiquidities.length, "BinPositionManagerHelper: length mismatch"); + + return BinPositionManagerHelper.MinLiquidityParams({binIds: binIds, minLiquidities: minLiquidities}); + } +} diff --git a/test/pool-bin/helper/BinLiquidityHelper.sol b/test/pool-bin/helper/BinLiquidityHelper.sol index d1cff0a..6b36a29 100644 --- a/test/pool-bin/helper/BinLiquidityHelper.sol +++ b/test/pool-bin/helper/BinLiquidityHelper.sol @@ -30,6 +30,9 @@ contract BinLiquidityHelper is Test { function approveBinPmForCurrency(address from, Currency currency, address binPm, IAllowanceTransfer permit2) internal { + // return early if currency is native, no need to approve + if (currency.isNative()) return; + vm.startPrank(from); // Because BinPm uses permit2, we must execute 2 permits/approvals. From 55f01a5245343fe1c8b6d5c8a0a5a93f18887d8a Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:52:32 +0800 Subject: [PATCH 2/9] feat: format code --- snapshots/BinPositionManagerHelperTest.json | 8 ++++---- src/pool-bin/BinPositionManagerHelper.sol | 6 ++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/snapshots/BinPositionManagerHelperTest.json b/snapshots/BinPositionManagerHelperTest.json index e87757d..e51c7c2 100644 --- a/snapshots/BinPositionManagerHelperTest.json +++ b/snapshots/BinPositionManagerHelperTest.json @@ -1,6 +1,6 @@ { - "BinPositionManagerHelper size": "7015", - "test_addLiquidities_existingPool": "1043471", - "test_addLiquidities_existingPool_nativeToken": "961745", - "test_addLiquidities_newPool": "1167450" + "BinPositionManagerHelper size": "6997", + "test_addLiquidities_existingPool": "1043390", + "test_addLiquidities_existingPool_nativeToken": "961664", + "test_addLiquidities_newPool": "1167369" } \ No newline at end of file diff --git a/src/pool-bin/BinPositionManagerHelper.sol b/src/pool-bin/BinPositionManagerHelper.sol index f331e4f..43401e2 100644 --- a/src/pool-bin/BinPositionManagerHelper.sol +++ b/src/pool-bin/BinPositionManagerHelper.sol @@ -92,12 +92,10 @@ contract BinPositionManagerHelper is Multicall { // Step 3a: Before Check user balance before address[] memory owners = new address[](minLiquidityParam.binIds.length); - for (uint256 i = 0; i < minLiquidityParam.binIds.length; i++) { - owners[i] = msg.sender; - } uint256[] memory tokenIds = new uint256[](minLiquidityParam.binIds.length); PoolId poolId = liquidityParams.poolKey.toId(); - for (uint256 i; i < minLiquidityParam.binIds.length; i++) { + for (uint256 i = 0; i < minLiquidityParam.binIds.length; i++) { + owners[i] = msg.sender; tokenIds[i] = poolId.toTokenId(minLiquidityParam.binIds[i]); } uint256[] memory balBefore = binPositionManager.balanceOfBatch(owners, tokenIds); From 0a8de4c90eea6bbcd0a0bae4df1026393a7e7110 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Fri, 25 Jul 2025 10:09:45 +0800 Subject: [PATCH 3/9] feat: include permit2 forwarder --- snapshots/BinPositionManagerHelperTest.json | 9 +-- src/pool-bin/BinPositionManagerHelper.sol | 14 ++-- test/pool-bin/BinPositionManagerHelper.t.sol | 75 +++++++++++++++++++- 3 files changed, 85 insertions(+), 13 deletions(-) diff --git a/snapshots/BinPositionManagerHelperTest.json b/snapshots/BinPositionManagerHelperTest.json index e51c7c2..82c85f0 100644 --- a/snapshots/BinPositionManagerHelperTest.json +++ b/snapshots/BinPositionManagerHelperTest.json @@ -1,6 +1,7 @@ { - "BinPositionManagerHelper size": "6997", - "test_addLiquidities_existingPool": "1043390", - "test_addLiquidities_existingPool_nativeToken": "961664", - "test_addLiquidities_newPool": "1167369" + "BinPositionManagerHelper size": "8405", + "test_addLiquidities_existingPool": "1043527", + "test_addLiquidities_existingPool_nativeToken": "961779", + "test_addLiquidities_newPool": "1167584", + "test_addLiquidities_newPool_WithPermit": "1226634" } \ No newline at end of file diff --git a/src/pool-bin/BinPositionManagerHelper.sol b/src/pool-bin/BinPositionManagerHelper.sol index 43401e2..3cc0d1d 100644 --- a/src/pool-bin/BinPositionManagerHelper.sol +++ b/src/pool-bin/BinPositionManagerHelper.sol @@ -10,20 +10,19 @@ import {PoolKey} from "infinity-core/src/types/PoolKey.sol"; import {PoolId} from "infinity-core/src/types/PoolId.sol"; import {IVault} from "infinity-core/src/interfaces/IVault.sol"; +import {IBinPositionManager} from "./interfaces/IBinPositionManager.sol"; +import {IBinPositionManagerWithERC1155} from "./interfaces/IBinPositionManagerWithERC1155.sol"; +import {IWETH9} from "../interfaces/external/IWETH9.sol"; import {Actions} from "../libraries/Actions.sol"; import {BinCalldataDecoder} from "./libraries/BinCalldataDecoder.sol"; import {CalldataDecoder} from "../libraries/CalldataDecoder.sol"; -import {IBinPositionManager} from "./interfaces/IBinPositionManager.sol"; -import {IBinPositionManagerWithERC1155} from "./interfaces/IBinPositionManagerWithERC1155.sol"; import {Multicall} from "../base/Multicall.sol"; +import {Permit2Forwarder} from "../base/Permit2Forwarder.sol"; import {BinTokenLibrary} from "./libraries/BinTokenLibrary.sol"; -import {IWETH9} from "../interfaces/external/IWETH9.sol"; - -import {console} from "forge-std/console.sol"; /// @title BinPositionManagerHelper /// @notice Helper contract for adding liquidity to bin pool with additional slippage protection -contract BinPositionManagerHelper is Multicall { +contract BinPositionManagerHelper is Multicall, Permit2Forwarder { using CalldataDecoder for bytes; using BinCalldataDecoder for bytes; using BinTokenLibrary for PoolId; @@ -48,7 +47,6 @@ contract BinPositionManagerHelper is Multicall { IBinPoolManager public immutable binPoolManager; IBinPositionManagerWithERC1155 public immutable binPositionManager; - IAllowanceTransfer public immutable permit2; IWETH9 public immutable WETH9; constructor( @@ -56,7 +54,7 @@ contract BinPositionManagerHelper is Multicall { IBinPositionManagerWithERC1155 _binPositionManager, IAllowanceTransfer _permit2, IWETH9 _weth9 - ) { + ) Permit2Forwarder(_permit2) { binPoolManager = _binPoolManager; binPositionManager = _binPositionManager; permit2 = _permit2; diff --git a/test/pool-bin/BinPositionManagerHelper.t.sol b/test/pool-bin/BinPositionManagerHelper.t.sol index 725ccd4..d464ff3 100644 --- a/test/pool-bin/BinPositionManagerHelper.t.sol +++ b/test/pool-bin/BinPositionManagerHelper.t.sol @@ -26,11 +26,19 @@ import {BinLiquidityHelper} from "./helper/BinLiquidityHelper.sol"; import {Planner, Plan} from "../../src/libraries/Planner.sol"; import {Actions} from "../../src/libraries/Actions.sol"; import {ActionConstants} from "../../src/libraries/ActionConstants.sol"; +import {Permit2SignatureHelpers} from "../shared/Permit2SignatureHelpers.sol"; +import {Permit2Forwarder} from "../../src/base/Permit2Forwarder.sol"; import {BinPositionManagerHelper} from "../../src/pool-bin/BinPositionManagerHelper.sol"; import {IBinPositionManagerWithERC1155} from "../../src/pool-bin/interfaces/IBinPositionManagerWithERC1155.sol"; -contract BinPositionManagerHelperTest is Test, TokenFixture, DeployPermit2, BinLiquidityHelper { +contract BinPositionManagerHelperTest is + Test, + Permit2SignatureHelpers, + TokenFixture, + DeployPermit2, + BinLiquidityHelper +{ using BinPoolParametersHelper for bytes32; error ContractSizeTooLarge(uint256 diff); @@ -280,6 +288,71 @@ contract BinPositionManagerHelperTest is Test, TokenFixture, DeployPermit2, BinL assertEq(token1.balanceOf(alice), 1 ether); // initial 4 ether, then minus 3 ether added } + /// @dev example test with multiCall (to include initializePool and permit for new user) + function test_addLiquidities_newPool_WithPermit() public { + (address bob, uint256 bobPK) = makeAddrAndKey("bob"); + + // before, require bob to approve token to permit2 + token0.mint(bob, 4 ether); + token1.mint(bob, 4 ether); + assertEq(token0.balanceOf(bob), 4 ether); + assertEq(token1.balanceOf(bob), 4 ether); + vm.startPrank(bob); + token0.approve(address(permit2), type(uint256).max); + token1.approve(address(permit2), type(uint256).max); + vm.stopPrank(); + + // step 1: prepare param liquidity. means roughly 3 bins + // with the following tokens in each bin: [1.5 token1, 1.5 token1 + 1.5 token0, 1.5 token0] + uint24[] memory binIds = getBinIds(activeId, 3); + IBinPositionManager.BinAddLiquidityParams memory param = + _getAddParams(key2, binIds, 3 ether, 3 ether, activeId, bob); + param.amount0Max = 3 ether * 1.1; // assume 10% slippage + param.amount1Max = 3 ether * 1.1; // assume 10% slippage + param.idSlippage = 0; // actveId is the same + + // step 2: prepare minLiquidity param -- since id slippage, is 0, we can just check activeId minLiquidity + uint24[] memory minLiquidityBinIds = new uint24[](1); + uint256[] memory minLiquiditys = new uint256[](1); + uint256 price = PriceHelper.getPriceFromId(activeId, key2.parameters.getBinStep()); + minLiquidityBinIds[0] = activeId; + minLiquiditys[0] = BinHelper.getLiquidity(1.5 ether, 1.5 ether, price) * 9999 / 10_000; // 0.01% slippage + BinPositionManagerHelper.MinLiquidityParams memory minLiquidityParam = + BinPositionManagerHelper.MinLiquidityParams({binIds: minLiquidityBinIds, minLiquidities: minLiquiditys}); + + // step 2b prepare the permit payload + uint160 permitAmount = type(uint160).max; + uint48 permitExpiration = uint48(block.timestamp + 10e18); + uint48 permitNonce = 0; + IAllowanceTransfer.PermitSingle memory permit0 = + defaultERC20PermitAllowance(Currency.unwrap(currency0), permitAmount, permitExpiration, permitNonce); + permit0.spender = address(binPmHelper); + bytes memory sig0 = getPermitSignature(permit0, bobPK, permit2.DOMAIN_SEPARATOR()); + IAllowanceTransfer.PermitSingle memory permit1 = + defaultERC20PermitAllowance(Currency.unwrap(currency1), permitAmount, permitExpiration, permitNonce); + permit1.spender = address(binPmHelper); + bytes memory sig1 = getPermitSignature(permit1, bobPK, permit2.DOMAIN_SEPARATOR()); + + // Step 3: prepare addLiquidities payload + Plan memory planner = Planner.init(); + planner.add(Actions.BIN_ADD_LIQUIDITY, abi.encode(param)); + planner.add(Actions.SETTLE_PAIR, abi.encode(currency0, currency1)); + bytes memory payload = planner.encode(); + + bytes[] memory calls = new bytes[](4); + calls[0] = abi.encodeWithSelector(binPmHelper.initializePool.selector, key2, activeId, ZERO_BYTES); + calls[1] = abi.encodeWithSelector(Permit2Forwarder.permit.selector, bob, permit0, sig0); + calls[2] = abi.encodeWithSelector(Permit2Forwarder.permit.selector, bob, permit1, sig1); + calls[3] = abi.encodeWithSelector(binPmHelper.addLiquidities.selector, payload, _deadline, minLiquidityParam); + vm.prank(bob); + binPmHelper.multicall(calls); + vm.snapshotGasLastCall("test_addLiquidities_newPool_WithPermit"); + + // after + assertEq(token0.balanceOf(bob), 1 ether); // initial 4 ether, then minus 3 ether added + assertEq(token1.balanceOf(bob), 1 ether); // initial 4 ether, then minus 3 ether added + } + function test_addLiquidities_newPool_DuplicateAddLiquidity() external { // step 1: prepare param liquidity. uint24[] memory binIds = getBinIds(activeId, 3); From 5d158297d824d6788fc8eaa6ca4d3b0a35223e07 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Fri, 25 Jul 2025 10:15:00 +0800 Subject: [PATCH 4/9] feat: add re-entrancy lock --- snapshots/BinPositionManagerHelperTest.json | 10 +++++----- src/pool-bin/BinPositionManagerHelper.sol | 7 +++++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/snapshots/BinPositionManagerHelperTest.json b/snapshots/BinPositionManagerHelperTest.json index 82c85f0..e9af0a5 100644 --- a/snapshots/BinPositionManagerHelperTest.json +++ b/snapshots/BinPositionManagerHelperTest.json @@ -1,7 +1,7 @@ { - "BinPositionManagerHelper size": "8405", - "test_addLiquidities_existingPool": "1043527", - "test_addLiquidities_existingPool_nativeToken": "961779", - "test_addLiquidities_newPool": "1167584", - "test_addLiquidities_newPool_WithPermit": "1226634" + "BinPositionManagerHelper size": "8579", + "test_addLiquidities_existingPool": "1043877", + "test_addLiquidities_existingPool_nativeToken": "962129", + "test_addLiquidities_newPool": "1167934", + "test_addLiquidities_newPool_WithPermit": "1226984" } \ No newline at end of file diff --git a/src/pool-bin/BinPositionManagerHelper.sol b/src/pool-bin/BinPositionManagerHelper.sol index 3cc0d1d..aef53a9 100644 --- a/src/pool-bin/BinPositionManagerHelper.sol +++ b/src/pool-bin/BinPositionManagerHelper.sol @@ -16,13 +16,14 @@ import {IWETH9} from "../interfaces/external/IWETH9.sol"; import {Actions} from "../libraries/Actions.sol"; import {BinCalldataDecoder} from "./libraries/BinCalldataDecoder.sol"; import {CalldataDecoder} from "../libraries/CalldataDecoder.sol"; +import {BinTokenLibrary} from "./libraries/BinTokenLibrary.sol"; import {Multicall} from "../base/Multicall.sol"; import {Permit2Forwarder} from "../base/Permit2Forwarder.sol"; -import {BinTokenLibrary} from "./libraries/BinTokenLibrary.sol"; +import {ReentrancyLock} from "../base/ReentrancyLock.sol"; /// @title BinPositionManagerHelper /// @notice Helper contract for adding liquidity to bin pool with additional slippage protection -contract BinPositionManagerHelper is Multicall, Permit2Forwarder { +contract BinPositionManagerHelper is Multicall, Permit2Forwarder, ReentrancyLock { using CalldataDecoder for bytes; using BinCalldataDecoder for bytes; using BinTokenLibrary for PoolId; @@ -69,6 +70,7 @@ contract BinPositionManagerHelper is Multicall, Permit2Forwarder { function addLiquidities(bytes calldata payload, uint256 deadline, MinLiquidityParams memory minLiquidityParam) external payable + isNotLocked { if (minLiquidityParam.binIds.length != minLiquidityParam.minLiquidities.length) { revert MinLiquidityParamsLengthMismatch(); @@ -126,6 +128,7 @@ contract BinPositionManagerHelper is Multicall, Permit2Forwarder { try binPoolManager.initialize(key, activeId) {} catch {} } + /// @dev Approve the bin position manager to spend the currency function _approveBinPm(Currency _currency, uint160 _amount) internal { if (!_currency.isNative()) { IERC20(Currency.unwrap(_currency)).approve(address(permit2), _amount); From 92f20e9ef2e899ea2e6607e20d8b90639e122624 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Fri, 25 Jul 2025 15:20:02 +0800 Subject: [PATCH 5/9] feat: add deployment script --- .../10_DeployBinPositionManagerHelper.s.sol | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 script/10_DeployBinPositionManagerHelper.s.sol diff --git a/script/10_DeployBinPositionManagerHelper.s.sol b/script/10_DeployBinPositionManagerHelper.s.sol new file mode 100644 index 0000000..7fba7f9 --- /dev/null +++ b/script/10_DeployBinPositionManagerHelper.s.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import "forge-std/Script.sol"; +import {BaseScript} from "./BaseScript.sol"; +import {IBinPoolManager} from "infinity-core/src/pool-bin/interfaces/IBinPoolManager.sol"; +import {IBinPositionManagerWithERC1155} from "../src/pool-bin/interfaces/IBinPositionManagerWithERC1155.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +import {IWETH9} from "../src/interfaces/external/IWETH9.sol"; +import {Create3Factory} from "pancake-create3-factory/src/Create3Factory.sol"; +import {BinPositionManagerHelper} from "../src/pool-bin/BinPositionManagerHelper.sol"; + +/** + * Pre-req: foundry on stable (1.0) otherwise verify will fail: ref https://github.com/foundry-rs/foundry/issues/9698 + * + * Step 1: Deploy + * forge script script/10_DeployBinPositionManagerHelper.s.sol:DeployBinPositionManagerHelper -vvv \ + * --rpc-url $RPC_URL \ + * --broadcast \ + * --slow \ + * --verify + */ +contract DeployBinPositionManagerHelper is BaseScript { + function getDeploymentSalt() public pure override returns (bytes32) { + return keccak256("INFINITY-PERIPHERY/BinPositionManagerHelper/xxxx"); + } + + function run() public { + Create3Factory factory = Create3Factory(getAddressFromConfig("create3Factory")); + + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + address binPoolManager = getAddressFromConfig("binPoolManager"); + emit log_named_address("binPoolManager", binPoolManager); + + address binPositionManager = getAddressFromConfig("binPositionManager"); + emit log_named_address("binPositionManager", binPoolManager); + + address permit2 = getAddressFromConfig("permit2"); + emit log_named_address("Permit2", permit2); + + address weth = getAddressFromConfig("weth"); + emit log_named_address("WETH", weth); + + bytes memory creationCodeData = abi.encode( + IBinPoolManager(binPoolManager), + IBinPositionManagerWithERC1155(binPositionManager), + IAllowanceTransfer(permit2), + IWETH9(weth) + ); + bytes memory creationCode = abi.encodePacked(type(BinPositionManagerHelper).creationCode, creationCodeData); + address binPositionManagerHelper = + factory.deploy(getDeploymentSalt(), creationCode, keccak256(creationCode), 0, new bytes(0), 0); + emit log_named_address("BinPositionManagerHelper", binPositionManagerHelper); + + vm.stopBroadcast(); + } +} From 69eebcd5702989e413f2af9679f5813d1e53f8ef Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Tue, 29 Jul 2025 13:09:02 +0800 Subject: [PATCH 6/9] bug: check liquidityParam.to for slippage --- snapshots/BinPositionManagerHelperTest.json | 11 +++--- src/pool-bin/BinPositionManagerHelper.sol | 2 +- test/pool-bin/BinPositionManagerHelper.t.sol | 41 ++++++++++++++++++++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/snapshots/BinPositionManagerHelperTest.json b/snapshots/BinPositionManagerHelperTest.json index e9af0a5..d821e3f 100644 --- a/snapshots/BinPositionManagerHelperTest.json +++ b/snapshots/BinPositionManagerHelperTest.json @@ -1,7 +1,8 @@ { - "BinPositionManagerHelper size": "8579", - "test_addLiquidities_existingPool": "1043877", - "test_addLiquidities_existingPool_nativeToken": "962129", - "test_addLiquidities_newPool": "1167934", - "test_addLiquidities_newPool_WithPermit": "1226984" + "BinPositionManagerHelper size": "8610", + "test_addLiquidities_existingPool": "1043905", + "test_addLiquidities_existingPool_bobReceiver": "1043905", + "test_addLiquidities_existingPool_nativeToken": "962157", + "test_addLiquidities_newPool": "1167962", + "test_addLiquidities_newPool_WithPermit": "1227012" } \ No newline at end of file diff --git a/src/pool-bin/BinPositionManagerHelper.sol b/src/pool-bin/BinPositionManagerHelper.sol index aef53a9..c6a36b3 100644 --- a/src/pool-bin/BinPositionManagerHelper.sol +++ b/src/pool-bin/BinPositionManagerHelper.sol @@ -95,7 +95,7 @@ contract BinPositionManagerHelper is Multicall, Permit2Forwarder, ReentrancyLock uint256[] memory tokenIds = new uint256[](minLiquidityParam.binIds.length); PoolId poolId = liquidityParams.poolKey.toId(); for (uint256 i = 0; i < minLiquidityParam.binIds.length; i++) { - owners[i] = msg.sender; + owners[i] = liquidityParams.to; tokenIds[i] = poolId.toTokenId(minLiquidityParam.binIds[i]); } uint256[] memory balBefore = binPositionManager.balanceOfBatch(owners, tokenIds); diff --git a/test/pool-bin/BinPositionManagerHelper.t.sol b/test/pool-bin/BinPositionManagerHelper.t.sol index d464ff3..85e3ec6 100644 --- a/test/pool-bin/BinPositionManagerHelper.t.sol +++ b/test/pool-bin/BinPositionManagerHelper.t.sol @@ -61,6 +61,7 @@ contract BinPositionManagerHelperTest is bytes32 poolParam; address alice = makeAddr("alice"); + address bob = makeAddr("bob"); uint24 activeId = 2 ** 23; // where token0 and token1 price is the same function setUp() public { @@ -163,6 +164,46 @@ contract BinPositionManagerHelperTest is assertEq(token1.balanceOf(alice), 1 ether); // initial 4 ether, then minus 3 ether added } + /// @dev mint to bob instead + function test_addLiquidities_existingPool_bobReceiver() public { + // before + token0.mint(alice, 4 ether); + token1.mint(alice, 4 ether); + assertEq(token0.balanceOf(alice), 4 ether); + assertEq(token1.balanceOf(alice), 4 ether); + + // step 1: prepare param liquidity. means roughly 3 bins + // with the following tokens in each bin: [1.5 token1, 1.5 token1 + 1.5 token0, 1.5 token0] + uint24[] memory binIds = getBinIds(activeId, 3); + IBinPositionManager.BinAddLiquidityParams memory param = + _getAddParams(key1, binIds, 3 ether, 3 ether, activeId, bob); + param.amount0Max = 3 ether * 1.1; // assume 10% slippage + param.amount1Max = 3 ether * 1.1; // assume 10% slippage + param.idSlippage = 0; // actveId is the same + + // step 2: prepare minLiquidity param -- since id slippage, is 0, we can just check activeId minLiquidity + uint24[] memory minLiquidityBinIds = new uint24[](1); + uint256[] memory minLiquiditys = new uint256[](1); + uint256 price = PriceHelper.getPriceFromId(activeId, key1.parameters.getBinStep()); + minLiquidityBinIds[0] = activeId; + minLiquiditys[0] = BinHelper.getLiquidity(1.5 ether, 1.5 ether, price) * 9999 / 10_000; // 0.01% slippage + BinPositionManagerHelper.MinLiquidityParams memory minLiquidityParam = + BinPositionManagerHelper.MinLiquidityParams({binIds: minLiquidityBinIds, minLiquidities: minLiquiditys}); + + // Step 3: prepare and call + Plan memory planner = Planner.init(); + planner.add(Actions.BIN_ADD_LIQUIDITY, abi.encode(param)); + planner.add(Actions.SETTLE_PAIR, abi.encode(currency0, currency1)); + bytes memory payload = planner.encode(); + vm.prank(alice); + binPmHelper.addLiquidities(payload, _deadline, minLiquidityParam); + vm.snapshotGasLastCall("test_addLiquidities_existingPool_bobReceiver"); + + // after + assertEq(token0.balanceOf(alice), 1 ether); // initial 4 ether, then minus 3 ether added + assertEq(token1.balanceOf(alice), 1 ether); // initial 4 ether, then minus 3 ether added + } + /// @dev ensure add liquidity works for pool with currency0 as native token function test_addLiquidities_existingPool_nativeToken() public { // before From ed12279efaea8e8a8a3127576c86ff00b6b18ee1 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Tue, 29 Jul 2025 13:39:23 +0800 Subject: [PATCH 7/9] feat: remove duplicate isNative check --- snapshots/BinPositionManagerHelperTest.json | 12 ++++++------ src/pool-bin/BinPositionManagerHelper.sol | 9 ++++----- test/pool-bin/BinPositionManagerHelper.t.sol | 5 +++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/snapshots/BinPositionManagerHelperTest.json b/snapshots/BinPositionManagerHelperTest.json index d821e3f..1d719dd 100644 --- a/snapshots/BinPositionManagerHelperTest.json +++ b/snapshots/BinPositionManagerHelperTest.json @@ -1,8 +1,8 @@ { - "BinPositionManagerHelper size": "8610", - "test_addLiquidities_existingPool": "1043905", - "test_addLiquidities_existingPool_bobReceiver": "1043905", - "test_addLiquidities_existingPool_nativeToken": "962157", - "test_addLiquidities_newPool": "1167962", - "test_addLiquidities_newPool_WithPermit": "1227012" + "BinPositionManagerHelper size": "8601", + "test_addLiquidities_existingPool": "1043871", + "test_addLiquidities_existingPool_bobReceiver": "1043871", + "test_addLiquidities_existingPool_nativeToken": "962140", + "test_addLiquidities_newPool": "1167928", + "test_addLiquidities_newPool_WithPermit": "1226978" } \ No newline at end of file diff --git a/src/pool-bin/BinPositionManagerHelper.sol b/src/pool-bin/BinPositionManagerHelper.sol index c6a36b3..7c33d68 100644 --- a/src/pool-bin/BinPositionManagerHelper.sol +++ b/src/pool-bin/BinPositionManagerHelper.sol @@ -128,12 +128,11 @@ contract BinPositionManagerHelper is Multicall, Permit2Forwarder, ReentrancyLock try binPoolManager.initialize(key, activeId) {} catch {} } - /// @dev Approve the bin position manager to spend the currency + /// @notice Approve the bin position manager to spend the currency + /// @dev assume currency is not native function _approveBinPm(Currency _currency, uint160 _amount) internal { - if (!_currency.isNative()) { - IERC20(Currency.unwrap(_currency)).approve(address(permit2), _amount); - permit2.approve(Currency.unwrap(_currency), address(binPositionManager), _amount, uint48(block.timestamp)); - } + IERC20(Currency.unwrap(_currency)).approve(address(permit2), _amount); + permit2.approve(Currency.unwrap(_currency), address(binPositionManager), _amount, uint48(block.timestamp)); } function _getAddLiquidityParam(bytes calldata payload) diff --git a/test/pool-bin/BinPositionManagerHelper.t.sol b/test/pool-bin/BinPositionManagerHelper.t.sol index 85e3ec6..0560426 100644 --- a/test/pool-bin/BinPositionManagerHelper.t.sol +++ b/test/pool-bin/BinPositionManagerHelper.t.sol @@ -61,7 +61,6 @@ contract BinPositionManagerHelperTest is bytes32 poolParam; address alice = makeAddr("alice"); - address bob = makeAddr("bob"); uint24 activeId = 2 ** 23; // where token0 and token1 price is the same function setUp() public { @@ -164,8 +163,10 @@ contract BinPositionManagerHelperTest is assertEq(token1.balanceOf(alice), 1 ether); // initial 4 ether, then minus 3 ether added } - /// @dev mint to bob instead + /// @dev mint to bob instead function test_addLiquidities_existingPool_bobReceiver() public { + address bob = makeAddr("bob"); + // before token0.mint(alice, 4 ether); token1.mint(alice, 4 ether); From 0ab63be95b83ad67ee22de8d374e2780f7259cf0 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Tue, 29 Jul 2025 14:26:46 +0800 Subject: [PATCH 8/9] fix: expecting accending binId -- no duplicates --- snapshots/BinPositionManagerHelperTest.json | 12 ++-- src/pool-bin/BinPositionManagerHelper.sol | 11 +++- test/pool-bin/BinPositionManagerHelper.t.sol | 62 ++++++++++++++++++++ 3 files changed, 77 insertions(+), 8 deletions(-) diff --git a/snapshots/BinPositionManagerHelperTest.json b/snapshots/BinPositionManagerHelperTest.json index 1d719dd..2ce0891 100644 --- a/snapshots/BinPositionManagerHelperTest.json +++ b/snapshots/BinPositionManagerHelperTest.json @@ -1,8 +1,8 @@ { - "BinPositionManagerHelper size": "8601", - "test_addLiquidities_existingPool": "1043871", - "test_addLiquidities_existingPool_bobReceiver": "1043871", - "test_addLiquidities_existingPool_nativeToken": "962140", - "test_addLiquidities_newPool": "1167928", - "test_addLiquidities_newPool_WithPermit": "1226978" + "BinPositionManagerHelper size": "8702", + "test_addLiquidities_existingPool": "1044057", + "test_addLiquidities_existingPool_bobReceiver": "1044057", + "test_addLiquidities_existingPool_nativeToken": "962326", + "test_addLiquidities_newPool": "1168114", + "test_addLiquidities_newPool_WithPermit": "1227164" } \ No newline at end of file diff --git a/src/pool-bin/BinPositionManagerHelper.sol b/src/pool-bin/BinPositionManagerHelper.sol index 7c33d68..c85a2f3 100644 --- a/src/pool-bin/BinPositionManagerHelper.sol +++ b/src/pool-bin/BinPositionManagerHelper.sol @@ -39,9 +39,12 @@ contract BinPositionManagerHelper is Multicall, Permit2Forwarder, ReentrancyLock /// @notice Thrown when minLiquidityParam's binIds and minLiquidities length mismatch error MinLiquidityParamsLengthMismatch(); /// @notice Thrown when slippage checks fail - error SlippageCheck(uint256 binId, uint256 liquidityAdded); + error SlippageCheck(uint24 binId, uint256 liquidityAdded); + /// @notice Thrown when invalid (duplicate or non accending) binIds are found in minLiquidityParam + error InvalidBinId(uint24 binid); struct MinLiquidityParams { + /// @dev expect accending order of binIds eg. [20, 21, 22] uint24[] binIds; uint256[] minLiquidities; } @@ -94,9 +97,13 @@ contract BinPositionManagerHelper is Multicall, Permit2Forwarder, ReentrancyLock address[] memory owners = new address[](minLiquidityParam.binIds.length); uint256[] memory tokenIds = new uint256[](minLiquidityParam.binIds.length); PoolId poolId = liquidityParams.poolKey.toId(); + uint24 tempBinId = 0; // check duplicate binId for (uint256 i = 0; i < minLiquidityParam.binIds.length; i++) { owners[i] = liquidityParams.to; - tokenIds[i] = poolId.toTokenId(minLiquidityParam.binIds[i]); + // Sanity check -- eg. assume binId is accending order, so this will check duplicate as well + if (tempBinId >= minLiquidityParam.binIds[i]) revert InvalidBinId(minLiquidityParam.binIds[i]); + tempBinId = minLiquidityParam.binIds[i]; + tokenIds[i] = poolId.toTokenId(tempBinId); } uint256[] memory balBefore = binPositionManager.balanceOfBatch(owners, tokenIds); diff --git a/test/pool-bin/BinPositionManagerHelper.t.sol b/test/pool-bin/BinPositionManagerHelper.t.sol index 0560426..7731112 100644 --- a/test/pool-bin/BinPositionManagerHelper.t.sol +++ b/test/pool-bin/BinPositionManagerHelper.t.sol @@ -124,6 +124,68 @@ contract BinPositionManagerHelperTest is } } + function test_addLiquidities_MinLiquidityParamsLengthMismatch() public { + // step 1: prepare param liquidity. means roughly 3 bins + // with the following tokens in each bin: [1.5 token1, 1.5 token1 + 1.5 token0, 1.5 token0] + uint24[] memory binIds = getBinIds(activeId, 3); + IBinPositionManager.BinAddLiquidityParams memory param = + _getAddParams(key1, binIds, 3 ether, 3 ether, activeId, alice); + + // step 2: prepare minLiquidity param -- since id slippage, is 0, we can just check activeId minLiquidity + uint24[] memory minLiquidityBinIds = new uint24[](1); + uint256[] memory minLiquiditys = new uint256[](2); + minLiquidityBinIds[0] = activeId; + minLiquiditys[0] = 1e18; + minLiquiditys[1] = 2e18; // mismatch length + BinPositionManagerHelper.MinLiquidityParams memory minLiquidityParam = + BinPositionManagerHelper.MinLiquidityParams({binIds: minLiquidityBinIds, minLiquidities: minLiquiditys}); + + Plan memory planner = Planner.init(); + planner.add(Actions.BIN_ADD_LIQUIDITY, abi.encode(param)); + planner.add(Actions.SETTLE_PAIR, abi.encode(currency0, currency1)); + bytes memory payload = planner.encode(); + vm.prank(alice); + vm.expectRevert(BinPositionManagerHelper.MinLiquidityParamsLengthMismatch.selector); + binPmHelper.addLiquidities(payload, _deadline, minLiquidityParam); + } + + function test_addLiquidities_InvalidBinId() public { + // before + token0.mint(alice, 4 ether); + token1.mint(alice, 4 ether); + assertEq(token0.balanceOf(alice), 4 ether); + assertEq(token1.balanceOf(alice), 4 ether); + + // step 1: prepare param liquidity. means roughly 3 bins + // with the following tokens in each bin: [1.5 token1, 1.5 token1 + 1.5 token0, 1.5 token0] + uint24[] memory binIds = getBinIds(activeId, 3); + IBinPositionManager.BinAddLiquidityParams memory param = + _getAddParams(key1, binIds, 3 ether, 3 ether, activeId, alice); + param.amount0Max = 3 ether * 1.1; // assume 10% slippage + param.amount1Max = 3 ether * 1.1; // assume 10% slippage + param.idSlippage = 0; // actveId is the same + + // step 2: prepare minLiquidity param -- since id slippage, is 0, we can just check activeId minLiquidity + uint24[] memory minLiquidityBinIds = new uint24[](2); + uint256[] memory minLiquiditys = new uint256[](2); + uint256 price = PriceHelper.getPriceFromId(activeId, key1.parameters.getBinStep()); + minLiquidityBinIds[0] = activeId; + minLiquidityBinIds[1] = activeId; // duplicate binId + minLiquiditys[0] = BinHelper.getLiquidity(1.5 ether, 1.5 ether, price) * 9999 / 10_000; // 0.01% slippage + minLiquiditys[1] = BinHelper.getLiquidity(1.5 ether, 1.5 ether, price) * 9999 / 10_000; // 0.01% slippage + BinPositionManagerHelper.MinLiquidityParams memory minLiquidityParam = + BinPositionManagerHelper.MinLiquidityParams({binIds: minLiquidityBinIds, minLiquidities: minLiquiditys}); + + // Step 3: prepare and call + Plan memory planner = Planner.init(); + planner.add(Actions.BIN_ADD_LIQUIDITY, abi.encode(param)); + planner.add(Actions.SETTLE_PAIR, abi.encode(currency0, currency1)); + bytes memory payload = planner.encode(); + vm.expectRevert(abi.encodeWithSelector(BinPositionManagerHelper.InvalidBinId.selector, activeId)); + vm.prank(alice); + binPmHelper.addLiquidities(payload, _deadline, minLiquidityParam); + } + function test_addLiquidities_existingPool() public { // before token0.mint(alice, 4 ether); From 8e7aab3a8c31a4b9dd26b9e6d14636e444574da6 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:46:07 +0800 Subject: [PATCH 9/9] feat: deploy on bnb and base --- script/10_DeployBinPositionManagerHelper.s.sol | 4 ++-- script/config/base-mainnet.json | 3 ++- script/config/bsc-mainnet.json | 3 ++- script/config/bsc-testnet.json | 3 ++- script/config/ethereum-mainnet.json | 3 ++- script/config/ethereum-sepolia.json | 3 ++- 6 files changed, 12 insertions(+), 7 deletions(-) diff --git a/script/10_DeployBinPositionManagerHelper.s.sol b/script/10_DeployBinPositionManagerHelper.s.sol index 7fba7f9..72bb84d 100644 --- a/script/10_DeployBinPositionManagerHelper.s.sol +++ b/script/10_DeployBinPositionManagerHelper.s.sol @@ -22,7 +22,7 @@ import {BinPositionManagerHelper} from "../src/pool-bin/BinPositionManagerHelper */ contract DeployBinPositionManagerHelper is BaseScript { function getDeploymentSalt() public pure override returns (bytes32) { - return keccak256("INFINITY-PERIPHERY/BinPositionManagerHelper/xxxx"); + return keccak256("INFINITY-PERIPHERY/BinPositionManagerHelper/1.0.0"); } function run() public { @@ -35,7 +35,7 @@ contract DeployBinPositionManagerHelper is BaseScript { emit log_named_address("binPoolManager", binPoolManager); address binPositionManager = getAddressFromConfig("binPositionManager"); - emit log_named_address("binPositionManager", binPoolManager); + emit log_named_address("binPositionManager", binPositionManager); address permit2 = getAddressFromConfig("permit2"); emit log_named_address("Permit2", permit2); diff --git a/script/config/base-mainnet.json b/script/config/base-mainnet.json index 9570df1..8fbdc3e 100644 --- a/script/config/base-mainnet.json +++ b/script/config/base-mainnet.json @@ -19,5 +19,6 @@ "factoryV2": "0x02a84c1b3BBD7401a5f7fa98a384EBC70bB5749E", "factoryStable": "0x64D74e1EAAe3176744b5767b93B7Bee39Cf7898F", "mixedQuoter": "0x2dCbF7B985c8C5C931818e4E107bAe8aaC8dAB7C", - "clTickLens": "0x8bcf30285413f25032fb983c2bf4defe29a33f3a" + "clTickLens": "0x8bcf30285413f25032fb983c2bf4defe29a33f3a", + "binPositionManagerHelper": "0xF4c3E0A83Ed93f6be55634C979e71c46c2453D4C" } diff --git a/script/config/bsc-mainnet.json b/script/config/bsc-mainnet.json index 929b440..4da193d 100644 --- a/script/config/bsc-mainnet.json +++ b/script/config/bsc-mainnet.json @@ -19,5 +19,6 @@ "factoryV2": "0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73", "factoryStable": "0x25a55f9f2279A54951133D503490342b50E5cd15", "mixedQuoter": "0x2dCbF7B985c8C5C931818e4E107bAe8aaC8dAB7C", - "clTickLens": "0x8BcF30285413F25032fb983C2bF4deFe29a33f3a" + "clTickLens": "0x8BcF30285413F25032fb983C2bF4deFe29a33f3a", + "binPositionManagerHelper": "0xF4c3E0A83Ed93f6be55634C979e71c46c2453D4C" } diff --git a/script/config/bsc-testnet.json b/script/config/bsc-testnet.json index 46f43bd..c932ae0 100644 --- a/script/config/bsc-testnet.json +++ b/script/config/bsc-testnet.json @@ -19,5 +19,6 @@ "factoryV2": "0x6725F303b657a9451d8BA641348b6761A6CC7a17", "factoryStable": "0xe6A00f8b819244e8Ab9Ea930e46449C2F20B6609", "mixedQuoter": "0xdf70a0A2DADC2a01dbE165702aa6dCdf034628b0", - "clTickLens": "0x05a732bb9A23256F57f9FdF255212979368Ece39" + "clTickLens": "0x05a732bb9A23256F57f9FdF255212979368Ece39", + "binPositionManagerHelper": "0xF4c3E0A83Ed93f6be55634C979e71c46c2453D4C" } \ No newline at end of file diff --git a/script/config/ethereum-mainnet.json b/script/config/ethereum-mainnet.json index 71cd903..623f7ca 100755 --- a/script/config/ethereum-mainnet.json +++ b/script/config/ethereum-mainnet.json @@ -13,5 +13,6 @@ "binQuoter": "0x", "clMigrator": "0x", "binMigrator": "0x", - "clTickLens": "0x" + "clTickLens": "0x", + "binPositionManagerHelper": "0x" } diff --git a/script/config/ethereum-sepolia.json b/script/config/ethereum-sepolia.json index 4e4cb96..41cb99d 100755 --- a/script/config/ethereum-sepolia.json +++ b/script/config/ethereum-sepolia.json @@ -17,5 +17,6 @@ "factoryV2": "0x1bdc540dEB9Ed1fA29964DeEcCc524A8f5e2198e", "factoryStable": "0x000000000000000000000000000000000000dEaD", "mixedQuoter": "0x3F47BDA24e069FC5f28B74d4FAB27EeBA006eF92", - "clTickLens": "0xE95fce3eEFcFf86B1d6a8951405432eA96505c98" + "clTickLens": "0xE95fce3eEFcFf86B1d6a8951405432eA96505c98", + "binPositionManagerHelper": "0x" }