diff --git a/.forge-snapshots/BinPoolManagerTest#testFuzzUpdateDynamicLPFee.snap b/.forge-snapshots/BinPoolManagerTest#testFuzzUpdateDynamicLPFee.snap index d3b3998..c18046f 100644 --- a/.forge-snapshots/BinPoolManagerTest#testFuzzUpdateDynamicLPFee.snap +++ b/.forge-snapshots/BinPoolManagerTest#testFuzzUpdateDynamicLPFee.snap @@ -1 +1 @@ -32499 \ No newline at end of file +32487 \ No newline at end of file diff --git a/.forge-snapshots/CLPoolManagerTest#testFuzzUpdateDynamicLPFee.snap b/.forge-snapshots/CLPoolManagerTest#testFuzzUpdateDynamicLPFee.snap index 8cd097e..14cc9be 100644 --- a/.forge-snapshots/CLPoolManagerTest#testFuzzUpdateDynamicLPFee.snap +++ b/.forge-snapshots/CLPoolManagerTest#testFuzzUpdateDynamicLPFee.snap @@ -1 +1 @@ -31956 \ No newline at end of file +31968 \ No newline at end of file diff --git a/src/ProtocolFeeController.sol b/src/ProtocolFeeController.sol index 8f23b95..ad6e2ad 100644 --- a/src/ProtocolFeeController.sol +++ b/src/ProtocolFeeController.sol @@ -50,12 +50,31 @@ contract ProtocolFeeController is IProtocolFeeController, Ownable2Step { emit ProtocolFeeSplitRatioUpdated(oldProtocolFeeSplitRatio, newProtocolFeeSplitRatio); } - /// @notice Get the protocol fee for a pool given the conditions of this contract - /// @return protocolFee The pool's protocol fee, expressed in hundredths of a bip. The upper 12 bits are for 1->0 + /// @notice Get the LP fee based on protocolFeeSplitRatio and total fee. This is useful for FE to calculate the LP fee + /// based on user's input when initializing a static fee pool + /// warning: if protocolFee is over 0.4% based on the totalFee, then it will be capped at 0.4% which means + /// lpFee in this case will charge more lpFee than expected i.e more than "1 - protocolFeeSplitRatio" + /// @param totalFee The total fee (including lpFee and protocolFee) for the pool, expressed in hundredths of a bip + /// @return lpFee The LP fee that can be passed in as poolKey.fee, expressed in hundredths of a bip + function getLPFeeFromTotalFee(uint24 totalFee) external view returns (uint24) { + /// @dev the formula is derived from the following equation: + /// poolKey.fee = lpFee = (totalFee - protocolFee) / (1 - protocolFee) + uint256 oneDirectionProtocolFee = totalFee * protocolFeeSplitRatio / ONE_HUNDRED_PERCENT_RATIO; + if (oneDirectionProtocolFee > ProtocolFeeLibrary.MAX_PROTOCOL_FEE) { + oneDirectionProtocolFee = ProtocolFeeLibrary.MAX_PROTOCOL_FEE; + } + + return uint24( + (totalFee - oneDirectionProtocolFee) * ONE_HUNDRED_PERCENT_RATIO + / (ONE_HUNDRED_PERCENT_RATIO - oneDirectionProtocolFee) + ); + } + + /// @inheritdoc IProtocolFeeController function protocolFeeForPool(PoolKey memory poolKey) external view override returns (uint24 protocolFee) { if (address(poolKey.poolManager) != poolManager) revert InvalidPoolManager(); - // otherwise, calculate the protocol fee based on the predefined rule + // 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 diff --git a/src/interfaces/IProtocolFeeController.sol b/src/interfaces/IProtocolFeeController.sol index 7609cc9..76ddd3b 100644 --- a/src/interfaces/IProtocolFeeController.sol +++ b/src/interfaces/IProtocolFeeController.sol @@ -8,7 +8,7 @@ interface IProtocolFeeController { /// @param poolKey The pool key to identify the pool. The controller may want to use attributes on the pool /// to determine the protocol fee, hence the entire key is needed. /// @return protocolFee The pool's protocol fee, expressed in hundredths of a bip. The upper 12 bits are for 1->0 - /// and the lower 12 are for 0->1. The maximum is 1000 - meaning the maximum protocol fee is 0.1%. + /// and the lower 12 are for 0->1. The maximum is 4000 - meaning the maximum protocol fee is 0.4%. /// the protocolFee is taken from the input first, then the lpFee is taken from the remaining input function protocolFeeForPool(PoolKey memory poolKey) external view returns (uint24 protocolFee); } diff --git a/test/ProtocolFeeController.t.sol b/test/ProtocolFeeController.t.sol index 203330d..5e9a931 100644 --- a/test/ProtocolFeeController.t.sol +++ b/test/ProtocolFeeController.t.sol @@ -110,6 +110,42 @@ contract ProtocolFeeControllerTest is Test, BinTestHelper, TokenFixture { } } + function testGetLPFeeFromTotalFee(uint24 totalFee, uint24 splitRatio) public { + totalFee = uint24(bound(totalFee, 0, LPFeeLibrary.ONE_HUNDRED_PERCENT_FEE)); + ProtocolFeeController controller = new ProtocolFeeController(address(clPoolManager)); + splitRatio = uint24(bound(splitRatio, 0, controller.ONE_HUNDRED_PERCENT_RATIO())); + controller.setProtocolFeeSplitRatio(splitRatio); + + // try to simulate the calculation the process of FE initialization pool + + // step1: calculate lpFee from totalFee + uint24 lpFee = controller.getLPFeeFromTotalFee(totalFee); + + assertGe(lpFee, 0); + assertLe(lpFee, totalFee); + + // step2: prepare the poolKey + PoolKey memory key = PoolKey({ + currency0: currency0, + currency1: currency1, + hooks: IHooks(address(0)), + poolManager: clPoolManager, + fee: lpFee, + parameters: bytes32(0).setTickSpacing(10) + }); + uint24 protocolFee = controller.protocolFeeForPool(key); + uint16 protocolFeeZeroForOne = protocolFee.getZeroForOneFee(); + + // verify the totalFee expected to be equal to protocolFee + (1 - protocolFee) * lpFee + assertApproxEqAbs( + totalFee, + protocolFeeZeroForOne.calculateSwapFee(lpFee), + // keeping the error within 0.01% (can't avoid due to precision loss) + 100, + "totalFee should be equal to protocolFee + (1 - protocolFee) * lpFee" + ); + } + function testProtocolFeeForPool(uint24 lpFee, uint256 protocolFeeRatio) public { lpFee = uint24(bound(lpFee, 0, LPFeeLibrary.ONE_HUNDRED_PERCENT_FEE)); ProtocolFeeController controller = new ProtocolFeeController(address(clPoolManager));