Root + Impact
Description
-
The protocol implements dynamic swap fees where fees are represented in hundredths of a bip (i.e., units of 0.0001%), following Uniswap V4's fee convention [1, 2]. A fee value of 3000 should represent 0.3% (3000 × 0.0001% = 0.3%).
-
The _beforeSwap function returns the correct fee value to the pool manager for charging, but incorrectly calculates the fee amount for event emission by dividing by 100,000 instead of 1,000,000. This results in the ReFiSold event reporting a fee amount that is 10 times higher than what users actually pay.
function _beforeSwap(
address sender,
PoolKey calldata key,
SwapParams calldata params,
bytes calldata
) internal override returns (bytes4, BeforeSwapDelta, uint24) {
bool isReFiBuy = _isReFiBuy(key, params.zeroForOne);
uint256 swapAmount = params.amountSpecified < 0
? uint256(-params.amountSpecified)
: uint256(params.amountSpecified);
uint24 fee;
if (isReFiBuy) {
fee = buyFee;
emit ReFiBought(sender, swapAmount);
} else {
fee = sellFee;
@> uint256 feeAmount = (swapAmount * sellFee) / 100000;
emit ReFiSold(sender, swapAmount, feeAmount);
}
return (
BaseHook.beforeSwap.selector,
BeforeSwapDeltaLibrary.ZERO_DELTA,
fee | LPFeeLibrary.OVERRIDE_FEE_FLAG
);
}
Risk
Likelihood:
Impact:
-
Off-chain systems and indexers relying on the ReFiSold event receive incorrect fee data (10x inflated)
-
Financial reporting and accounting based on events will be inaccurate
-
Users monitoring events may incorrectly believe they are being overcharged, damaging protocol reputation
Proof of Concept
First, correct the inverted logic in _I
function _isReFiBuy(PoolKey calldata key, bool zeroForOne) internal view returns (bool) {
bool IsReFiCurrency0 = Currency.unwrap(key.currency0) == ReFi;
if (IsReFiCurrency0) {
- return zeroForOne;
+ return !zeroForOne;
} else {
- return !zeroForOne;
+ return zeroForOne;
}
}
Then, add this test to test/RebateFiHookTest.t.sol:
function test_FeeEventReportsInflatedAmount() public {
vm.startPrank(user1);
reFiToken.approve(address(swapRouter), type(uint256).max);
uint256 amount = 100 ether;
vm.expectEmit(true, true, true, true);
emit ReFiSwapRebateHook.ReFiSold(address(swapRouter), amount, 0.0003 ether);
SwapParams memory params = SwapParams({
zeroForOne: false,
amountSpecified: -int256(amount),
sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1
});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
swapRouter.swap(key, params, testSettings, ZERO_BYTES);
vm.stopPrank();
}
Recommended Mitigation
function _beforeSwap(
address sender,
PoolKey calldata key,
SwapParams calldata params,
bytes calldata
) internal override returns (bytes4, BeforeSwapDelta, uint24) {
bool isReFiBuy = _isReFiBuy(key, params.zeroForOne);
uint256 swapAmount = params.amountSpecified < 0
? uint256(-params.amountSpecified)
: uint256(params.amountSpecified);
uint24 fee;
if (isReFiBuy) {
fee = buyFee;
emit ReFiBought(sender, swapAmount);
} else {
fee = sellFee;
- uint256 feeAmount = (swapAmount * sellFee) / 100000;
+ uint256 feeAmount = (swapAmount * sellFee) / 1_000_000;
emit ReFiSold(sender, swapAmount, feeAmount);
}
return (
BaseHook.beforeSwap.selector,
BeforeSwapDeltaLibrary.ZERO_DELTA,
fee | LPFeeLibrary.OVERRIDE_FEE_FLAG
);
}