RebateFi Hook

First Flight #53
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Severity: high
Valid

he `ReFiSwapRebateHook::_isReFiBuy` returns wrong value, causing `ReFiSwapRebateHook::_beforeSwap` function to apply fee on ReFi token buy.

Incorrect ReFi buy detection → Fees and ReFiSold event are applied when users buy ReFi (opposite of intended behavior)

Description

  • The hook is designed to incentivise buying ReFi by charging little/no fee on ReFi purchases and a higher fee when users sell ReFi.

  • _isReFiBuy determines whether the current swap direction is a ReFi purchase. It returns true when the user is receiving ReFi (buy) and false when the user is sending ReFi (sell).

  • Due to the logic being inverted in both branches, the function returns the opposite value in every possible pool configuration, causing _beforeSwap to apply the sell fee and emit ReFiSold whenever a user buys ReFi, and vice-versa.

// Root cause in the codebase with @> marks to highlight the relevant section
function _isReFiBuy(PoolKey calldata key, bool zeroForOne) internal view returns (bool) {
@> bool IsReFiCurrency0 = Currency.unwrap(key.currency0) == ReFi;
if (IsReFiCurrency0) {
return zeroForOne;
} else {
return !zeroForOne;
}

Risk

Likelihood:

  • Every swap executed on any pool that contains ReFi triggers the bug

  • The inversion affects 100% of buy and sell transactions regardless of token ordering

Impact:

  • Users are charged the higher “sell” fee when they buy ReFi — directly contradicting the core product incentive

  • The ReFiSold event is emitted on buys instead of sells, making on-chain analytics and rebate accounting completely backwards

Proof of Concept

Add the following code snippet to the RebateFiHookTest.t.sol test file.

This snippet of code is to demonstrate that on ReFi buy the _beforeSwap function will apply fee on ReFi token buy and emit the ReFiSold event.

function test_BuyReFi_EmitsReFiSoldEvent() public {
uint256 ethAmount = 0.01 ether;
// Fund user and record initial balances
vm.deal(user1, 1 ether);
//rebateHook.ChangeFee(true, 0, true, 0);
(uint24 buyFee, uint24 sellFee) = rebateHook.getFeeConfig();
console.log("buyFee", buyFee);
console.log("sellFee", sellFee);
vm.startPrank(user1);
SwapParams memory params = SwapParams({
zeroForOne: true, // ETH -> ReFi
amountSpecified: -int256(ethAmount),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
vm.recordLogs();
// Perform swap
swapRouter.swap{value: ethAmount}(key, params, testSettings, ZERO_BYTES);
Vm.Log[] memory entries = vm.getRecordedLogs();
bool foundReFiSold = false; // Move outside the loop
bytes32 expectedSig = keccak256("ReFiSold(address,uint256,uint256)");
for (uint256 i = 0; i < entries.length; i++) {
Vm.Log memory logEntry = entries[i];
console.log("logEntry", address(uint160(uint256(logEntry.topics[1]))));
// Check if this log entry matches the ReFiSold event signature
if (logEntry.topics.length > 0 && logEntry.topics[0] == expectedSig) {
foundReFiSold = true;
break; // Found it, no need to continue
}
}
// Assert after checking all logs
assertTrue(foundReFiSold, "ReFiSold event not emitted in logs");
vm.stopPrank();
}

Recommended Mitigation

Possible mitigation is to modify the _isReFiBuy function according.

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;
}
}
Updates

Lead Judging Commences

chaossr Lead Judge 12 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Inverted buy/sell logic when ReFi is currency0, leading to incorrect fee application.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!