From 391bb90e445ab6f569fad924a14155626ec33aac Mon Sep 17 00:00:00 2001 From: chefburger Date: Mon, 23 Jun 2025 12:39:57 +0800 Subject: [PATCH 1/2] feat: update the default protocolFee for dynamic fee pool to 3bps --- src/ProtocolFeeController.sol | 33 +++++++- test/ProtocolFeeController.t.sol | 129 +++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 2 deletions(-) diff --git a/src/ProtocolFeeController.sol b/src/ProtocolFeeController.sol index e9cfcff..ef1b1ab 100644 --- a/src/ProtocolFeeController.sol +++ b/src/ProtocolFeeController.sol @@ -18,6 +18,9 @@ contract ProtocolFeeController is IProtocolFeeController, Ownable2Step { /// @notice throw when the pool manager saved does not match the pool manager from the pool key error InvalidPoolManager(); + /// @notice throw when the default protocol fee for dynamic fee pool is invalid i.e. greater than 0.4% + error InvalidDefaultProtocolFeeForDynamicFeePool(); + /// @notice throw when the protocol fee split ratio is invalid i.e. greater than 100% error InvalidProtocolFeeSplitRatio(); @@ -30,6 +33,15 @@ contract ProtocolFeeController is IProtocolFeeController, Ownable2Step { address public immutable poolManager; + /// @notice the default protocol fee for dynamic fee pool, + /// every newly created dynamic fee pool will have this default protocol fee + /// @dev 1000 = 0.1%, the initial setting is 0.3% i.e. 3bps + uint24 public defaultProtocolFeeForDynamicFeePool = 3000; + + event DefaultProtocolFeeForDynamicFeePoolUpdated( + uint24 oldDefaultProtocolFeeForDynamicFeePool, uint24 newDefaultProtocolFeeForDynamicFeePool + ); + event ProtocolFeeSplitRatioUpdated(uint256 oldProtocolFeeSplitRatio, uint256 newProtocolFeeSplitRatio); /// @notice emit when the protocol fee is collected @@ -39,6 +51,23 @@ contract ProtocolFeeController is IProtocolFeeController, Ownable2Step { poolManager = _poolManager; } + /// @notice Set the default protocol fee for dynamic fee pool, this will only impact the newly created dynamic fee pool + /// all those existing dynamic fee pools will not be affected, they will keep using the default protocol fee set at the time of creation + /// @param newDefaultProtocolFeeForDynamicFeePool 1000 = 0.1%, the initial setting is 0.3% i.e. 3bps + function setDefaultProtocolFeeForDynamicFeePool(uint24 newDefaultProtocolFeeForDynamicFeePool) external onlyOwner { + // cap the protocol fee at 0.4%, if it's over the limit we revert the tx + if (newDefaultProtocolFeeForDynamicFeePool > ProtocolFeeLibrary.MAX_PROTOCOL_FEE) { + revert InvalidDefaultProtocolFeeForDynamicFeePool(); + } + + uint24 oldDefaultProtocolFeeForDynamicFeePool = defaultProtocolFeeForDynamicFeePool; + defaultProtocolFeeForDynamicFeePool = newDefaultProtocolFeeForDynamicFeePool; + + emit DefaultProtocolFeeForDynamicFeePoolUpdated( + oldDefaultProtocolFeeForDynamicFeePool, newDefaultProtocolFeeForDynamicFeePool + ); + } + /// @notice Set the ratio of the protocol fee in the total fee /// @param newProtocolFeeSplitRatio 30e4 would mean 30% of the total fee goes to protocol function setProtocolFeeSplitRatio(uint256 newProtocolFeeSplitRatio) external onlyOwner { @@ -77,8 +106,8 @@ contract ProtocolFeeController is IProtocolFeeController, Ownable2Step { // calculate the protocol fee based on the predefined rule uint256 lpFee = poolKey.fee; if (lpFee == LPFeeLibrary.DYNAMIC_FEE_FLAG) { - /// @notice for dynamic fee pools, the default protocol fee is 0 - return _buildProtocolFee(0); + /// @notice for dynamic fee pools, the default protocol fee is set separately + return _buildProtocolFee(defaultProtocolFeeForDynamicFeePool); } else if (protocolFeeSplitRatio == 0) { return _buildProtocolFee(0); } else if (protocolFeeSplitRatio == ONE_HUNDRED_PERCENT_RATIO) { diff --git a/test/ProtocolFeeController.t.sol b/test/ProtocolFeeController.t.sol index 53a7516..a9aef32 100644 --- a/test/ProtocolFeeController.t.sol +++ b/test/ProtocolFeeController.t.sol @@ -27,6 +27,7 @@ import {BalanceDelta} from "../src/types/BalanceDelta.sol"; import {BinTestHelper} from "./pool-bin/helpers/BinTestHelper.sol"; import {BinSwapHelper} from "./pool-bin/helpers/BinSwapHelper.sol"; import {BinLiquidityHelper} from "./pool-bin/helpers/BinLiquidityHelper.sol"; +import {HooksContract} from "./libraries/Hooks/HooksContract.sol"; contract ProtocolFeeControllerTest is Test, BinTestHelper, TokenFixture { using CLPoolParametersHelper for bytes32; @@ -43,6 +44,8 @@ contract ProtocolFeeControllerTest is Test, BinTestHelper, TokenFixture { BinSwapHelper public binSwapHelper; BinLiquidityHelper public binLiquidityHelper; + HooksContract public hooksContract; + function setUp() public { initializeTokens(); @@ -58,6 +61,8 @@ contract ProtocolFeeControllerTest is Test, BinTestHelper, TokenFixture { IERC20(Currency.unwrap(currency1)).approve(address(binSwapHelper), 1000 ether); IERC20(Currency.unwrap(currency0)).approve(address(binLiquidityHelper), 1000 ether); IERC20(Currency.unwrap(currency1)).approve(address(binLiquidityHelper), 1000 ether); + + hooksContract = new HooksContract(0); } function testOwnerTransfer() public { @@ -90,6 +95,32 @@ contract ProtocolFeeControllerTest is Test, BinTestHelper, TokenFixture { assertEq(controller.owner(), makeAddr("newOwner")); } + function testSetDefaultProtocolFeeForDynamicFeePool(uint24 newDefaultProtocolFeeForDynamicFeePool) public { + ProtocolFeeController controller = new ProtocolFeeController(address(clPoolManager)); + + // it should start with 0.3% as default + assertEq(controller.defaultProtocolFeeForDynamicFeePool(), 3000); + + { + // must from owner + vm.prank(makeAddr("someone")); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, makeAddr("someone"))); + controller.setDefaultProtocolFeeForDynamicFeePool(newDefaultProtocolFeeForDynamicFeePool); + } + + if (newDefaultProtocolFeeForDynamicFeePool > ProtocolFeeLibrary.MAX_PROTOCOL_FEE) { + vm.expectRevert(ProtocolFeeController.InvalidDefaultProtocolFeeForDynamicFeePool.selector); + controller.setDefaultProtocolFeeForDynamicFeePool(newDefaultProtocolFeeForDynamicFeePool); + } else { + vm.expectEmit(true, true, true, true); + emit ProtocolFeeController.DefaultProtocolFeeForDynamicFeePoolUpdated( + 3000, newDefaultProtocolFeeForDynamicFeePool + ); + controller.setDefaultProtocolFeeForDynamicFeePool(newDefaultProtocolFeeForDynamicFeePool); + assertEq(controller.defaultProtocolFeeForDynamicFeePool(), newDefaultProtocolFeeForDynamicFeePool); + } + } + function testSetProcotolFeeSplitRatio(uint256 newProtocolFeeSplitRatio) public { ProtocolFeeController controller = new ProtocolFeeController(address(clPoolManager)); @@ -363,6 +394,104 @@ contract ProtocolFeeControllerTest is Test, BinTestHelper, TokenFixture { } } + function testCLDynamicPoolInitWithProtolFeeControllerFuzz(uint24 newDefaultProtocolFeeForDynamicFeePool) public { + ProtocolFeeController controller = new ProtocolFeeController(address(clPoolManager)); + newDefaultProtocolFeeForDynamicFeePool = + uint24(bound(newDefaultProtocolFeeForDynamicFeePool, 0, ProtocolFeeLibrary.MAX_PROTOCOL_FEE)); + + clPoolManager.setProtocolFeeController(controller); + + PoolKey memory key = PoolKey({ + currency0: currency0, + currency1: currency1, + hooks: hooksContract, + poolManager: clPoolManager, + fee: LPFeeLibrary.DYNAMIC_FEE_FLAG, + parameters: bytes32(0).setTickSpacing(10) + }); + clPoolManager.initialize(key, Constants.SQRT_RATIO_1_1); + + (,, uint24 actualProtocolFee,) = clPoolManager.getSlot0(key.toId()); + + // under default rule protocol fee must be equal for both directions + uint16 protocolFeeZeroForOne = actualProtocolFee.getZeroForOneFee(); + uint16 protocolFeeOneForZero = actualProtocolFee.getOneForZeroFee(); + assertEq(protocolFeeOneForZero, protocolFeeZeroForOne); + assertEq(protocolFeeOneForZero, 3000); + + controller.setDefaultProtocolFeeForDynamicFeePool(newDefaultProtocolFeeForDynamicFeePool); + + key.parameters = bytes32(0).setTickSpacing(30); + clPoolManager.initialize(key, Constants.SQRT_RATIO_1_1); + + (,, actualProtocolFee,) = clPoolManager.getSlot0(key.toId()); + // under default rule protocol fee must be equal for both directions + protocolFeeZeroForOne = actualProtocolFee.getZeroForOneFee(); + protocolFeeOneForZero = actualProtocolFee.getOneForZeroFee(); + assertEq(protocolFeeOneForZero, protocolFeeZeroForOne); + assertEq(protocolFeeOneForZero, newDefaultProtocolFeeForDynamicFeePool); + + // verify the original pool is not affected + { + key.parameters = bytes32(0).setTickSpacing(10); + + (,, actualProtocolFee,) = clPoolManager.getSlot0(key.toId()); + // under default rule protocol fee must be equal for both directions + protocolFeeZeroForOne = actualProtocolFee.getZeroForOneFee(); + protocolFeeOneForZero = actualProtocolFee.getOneForZeroFee(); + assertEq(protocolFeeOneForZero, protocolFeeZeroForOne); + } + } + + function testBinDynamicPoolInitWithProtolFeeControllerFuzz(uint24 newDefaultProtocolFeeForDynamicFeePool) public { + ProtocolFeeController controller = new ProtocolFeeController(address(binPoolManager)); + newDefaultProtocolFeeForDynamicFeePool = + uint24(bound(newDefaultProtocolFeeForDynamicFeePool, 0, ProtocolFeeLibrary.MAX_PROTOCOL_FEE)); + + binPoolManager.setProtocolFeeController(controller); + + PoolKey memory key = PoolKey({ + currency0: currency0, + currency1: currency1, + hooks: hooksContract, + poolManager: binPoolManager, + fee: LPFeeLibrary.DYNAMIC_FEE_FLAG, + parameters: bytes32(0).setBinStep(1) + }); + binPoolManager.initialize(key, ID_ONE); + + (, uint24 actualProtocolFee,) = binPoolManager.getSlot0(key.toId()); + + // under default rule protocol fee must be equal for both directions + uint16 protocolFeeZeroForOne = actualProtocolFee.getZeroForOneFee(); + uint16 protocolFeeOneForZero = actualProtocolFee.getOneForZeroFee(); + assertEq(protocolFeeOneForZero, protocolFeeZeroForOne); + assertEq(protocolFeeOneForZero, 3000); + + controller.setDefaultProtocolFeeForDynamicFeePool(newDefaultProtocolFeeForDynamicFeePool); + + key.parameters = bytes32(0).setBinStep(5); + binPoolManager.initialize(key, ID_ONE); + + (, actualProtocolFee,) = binPoolManager.getSlot0(key.toId()); + // under default rule protocol fee must be equal for both directions + protocolFeeZeroForOne = actualProtocolFee.getZeroForOneFee(); + protocolFeeOneForZero = actualProtocolFee.getOneForZeroFee(); + assertEq(protocolFeeOneForZero, protocolFeeZeroForOne); + assertEq(protocolFeeOneForZero, newDefaultProtocolFeeForDynamicFeePool); + + // verify the original pool is not affected + { + key.parameters = bytes32(0).setBinStep(1); + + (, actualProtocolFee,) = binPoolManager.getSlot0(key.toId()); + // under default rule protocol fee must be equal for both directions + protocolFeeZeroForOne = actualProtocolFee.getZeroForOneFee(); + protocolFeeOneForZero = actualProtocolFee.getOneForZeroFee(); + assertEq(protocolFeeOneForZero, protocolFeeZeroForOne); + } + } + function testSetProtocolFeeForCLPool(uint24 newProtocolFee) public { ProtocolFeeController controller = new ProtocolFeeController(address(clPoolManager)); clPoolManager.setProtocolFeeController(controller); From c52d52f9817a3ccb2099b23171d8991e5ff044f3 Mon Sep 17 00:00:00 2001 From: chefburger Date: Mon, 23 Jun 2025 13:06:00 +0800 Subject: [PATCH 2/2] fix: update to 0.03% instead of 0.3% --- src/ProtocolFeeController.sol | 6 +++--- test/ProtocolFeeController.t.sol | 13 ++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/ProtocolFeeController.sol b/src/ProtocolFeeController.sol index ef1b1ab..b22102d 100644 --- a/src/ProtocolFeeController.sol +++ b/src/ProtocolFeeController.sol @@ -35,8 +35,8 @@ contract ProtocolFeeController is IProtocolFeeController, Ownable2Step { /// @notice the default protocol fee for dynamic fee pool, /// every newly created dynamic fee pool will have this default protocol fee - /// @dev 1000 = 0.1%, the initial setting is 0.3% i.e. 3bps - uint24 public defaultProtocolFeeForDynamicFeePool = 3000; + /// @dev 1000 = 0.1%, the initial setting is 0.03% i.e. 3bps + uint24 public defaultProtocolFeeForDynamicFeePool = 300; event DefaultProtocolFeeForDynamicFeePoolUpdated( uint24 oldDefaultProtocolFeeForDynamicFeePool, uint24 newDefaultProtocolFeeForDynamicFeePool @@ -53,7 +53,7 @@ contract ProtocolFeeController is IProtocolFeeController, Ownable2Step { /// @notice Set the default protocol fee for dynamic fee pool, this will only impact the newly created dynamic fee pool /// all those existing dynamic fee pools will not be affected, they will keep using the default protocol fee set at the time of creation - /// @param newDefaultProtocolFeeForDynamicFeePool 1000 = 0.1%, the initial setting is 0.3% i.e. 3bps + /// @param newDefaultProtocolFeeForDynamicFeePool 1000 = 0.1%, the initial setting is 0.03% i.e. 3bps function setDefaultProtocolFeeForDynamicFeePool(uint24 newDefaultProtocolFeeForDynamicFeePool) external onlyOwner { // cap the protocol fee at 0.4%, if it's over the limit we revert the tx if (newDefaultProtocolFeeForDynamicFeePool > ProtocolFeeLibrary.MAX_PROTOCOL_FEE) { diff --git a/test/ProtocolFeeController.t.sol b/test/ProtocolFeeController.t.sol index a9aef32..e9a9f80 100644 --- a/test/ProtocolFeeController.t.sol +++ b/test/ProtocolFeeController.t.sol @@ -37,6 +37,9 @@ contract ProtocolFeeControllerTest is Test, BinTestHelper, TokenFixture { /// @notice 100% in hundredths of a bip uint256 private constant ONE_HUNDRED_PERCENT_RATIO = 1e6; + /// @dev the initial setting of the default protocol fee for dynamic fee pool is 0.03% i.e. 3bps + uint24 private constant DEFAULT_PROTOCOL_FEE_FOR_DYNAMIC_FEE_POOL = 300; + Vault vault; CLPoolManager clPoolManager; BinPoolManager binPoolManager; @@ -98,8 +101,8 @@ contract ProtocolFeeControllerTest is Test, BinTestHelper, TokenFixture { function testSetDefaultProtocolFeeForDynamicFeePool(uint24 newDefaultProtocolFeeForDynamicFeePool) public { ProtocolFeeController controller = new ProtocolFeeController(address(clPoolManager)); - // it should start with 0.3% as default - assertEq(controller.defaultProtocolFeeForDynamicFeePool(), 3000); + // it should start with 0.03% as default + assertEq(controller.defaultProtocolFeeForDynamicFeePool(), DEFAULT_PROTOCOL_FEE_FOR_DYNAMIC_FEE_POOL); { // must from owner @@ -114,7 +117,7 @@ contract ProtocolFeeControllerTest is Test, BinTestHelper, TokenFixture { } else { vm.expectEmit(true, true, true, true); emit ProtocolFeeController.DefaultProtocolFeeForDynamicFeePoolUpdated( - 3000, newDefaultProtocolFeeForDynamicFeePool + DEFAULT_PROTOCOL_FEE_FOR_DYNAMIC_FEE_POOL, newDefaultProtocolFeeForDynamicFeePool ); controller.setDefaultProtocolFeeForDynamicFeePool(newDefaultProtocolFeeForDynamicFeePool); assertEq(controller.defaultProtocolFeeForDynamicFeePool(), newDefaultProtocolFeeForDynamicFeePool); @@ -417,7 +420,7 @@ contract ProtocolFeeControllerTest is Test, BinTestHelper, TokenFixture { uint16 protocolFeeZeroForOne = actualProtocolFee.getZeroForOneFee(); uint16 protocolFeeOneForZero = actualProtocolFee.getOneForZeroFee(); assertEq(protocolFeeOneForZero, protocolFeeZeroForOne); - assertEq(protocolFeeOneForZero, 3000); + assertEq(protocolFeeOneForZero, DEFAULT_PROTOCOL_FEE_FOR_DYNAMIC_FEE_POOL); controller.setDefaultProtocolFeeForDynamicFeePool(newDefaultProtocolFeeForDynamicFeePool); @@ -466,7 +469,7 @@ contract ProtocolFeeControllerTest is Test, BinTestHelper, TokenFixture { uint16 protocolFeeZeroForOne = actualProtocolFee.getZeroForOneFee(); uint16 protocolFeeOneForZero = actualProtocolFee.getOneForZeroFee(); assertEq(protocolFeeOneForZero, protocolFeeZeroForOne); - assertEq(protocolFeeOneForZero, 3000); + assertEq(protocolFeeOneForZero, DEFAULT_PROTOCOL_FEE_FOR_DYNAMIC_FEE_POOL); controller.setDefaultProtocolFeeForDynamicFeePool(newDefaultProtocolFeeForDynamicFeePool);