RebateFi Hook

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

Users are charged the wrong fees on every single swap

_isReFiBuy has inverted logic that incorrectly identifies all buy swaps as sells and all sell swaps as buys, causing wrong fees to be applied to every transaction

Description

  • The _isReFiBuy function determines whether a swap is buying or selling ReFi tokens by checking the swap direction (zeroForOne) and which position ReFi occupies in the pool. When zeroForOne = true, the user is swapping currency0 FOR currency1 (giving currency0, receiving currency1). If ReFi is currency0, this means the user is selling ReFi; if ReFi is currency1, this means the user is buying ReFi.

  • The current implementation has inverted logic in both scenarios. When ReFi is currency0 and zeroForOne = true (selling ReFi), the function returns true indicating a buy. When ReFi is currency1 and zeroForOne = true (buying ReFi), the function returns false indicating a sell. This causes buyFee to be charged when users sell ReFi and sellFee to be charged when users buy ReFi, completely reversing the intended fee structure for every single swap.

function _isReFiBuy(
PoolKey calldata key,
bool zeroForOne
) internal view returns (bool) {
bool IsReFiCurrency0 = Currency.unwrap(key.currency0) == ReFi;
if (IsReFiCurrency0) {
@> return zeroForOne; // WRONG: zeroForOne=true means SELLING currency0 (ReFi)
} else {
@> return !zeroForOne; // WRONG: zeroForOne=true means BUYING currency1 (ReFi)
}
}

Risk

Likelihood:

  • Every swap transaction through the hook triggers the inverted _isReFiBuy logic, causing 100% of swaps to have incorrect fee classification

Users performing buy operations are charged sellFee and users performing sell operations are charged buyFee on every transaction without exception

  • The bug is present in the core swap logic and affects all pool configurations regardless of whether ReFi is currency0 or currency1

Impact:

  • Protocol's entire economic model is broken as users buying ReFi pay higher fees (typically sellFee = 3000 or 0.3%) instead of the intended discounted buyFee, completely defeating the rebate mechanism

Users selling ReFi pay lower fees than intended, reducing protocol revenue and potentially enabling exploitation where users intentionally sell ReFi to avoid proper fees

  • The fee inversion fundamentally breaks the protocol's value proposition of incentivizing ReFi purchases through reduced fees, making it economically backwards and potentially driving users away

Proof of Concept

This test demonstrates the inverted fee logic by executing both buy and sell swaps and verifying that the wrong fees are applied. When a user buys ReFi (swapping ETH for ReFi), the hook incorrectly identifies it as a sell and applies sellFee. When a user sells ReFi (swapping ReFi for ETH), the hook incorrectly identifies it as a buy and applies buyFee. The test records emitted events to prove the wrong fee type is being charged for each transaction type.

function test_SellReFi_IncorrectlyEmitsReFiBought() public {
uint24 testBuyFee = 1000;
uint24 testSellFee = 5000;
rebateHook.ChangeFee(true, testBuyFee, true, testSellFee);
vm.startPrank(user1);
reFiToken.approve(address(swapRouter), type(uint256).max);
vm.recordLogs();
// Selling ReFi (zeroForOne: false with ReFi as currency1)
swapRouter.swap(key, SwapParams({
zeroForOne: false,
amountSpecified: -0.01 ether,
sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1
}), PoolSwapTest.TestSettings(false, false), ZERO_BYTES);
// Bug: Sell incorrectly emits ReFiBought
Vm.Log[] memory logs = vm.getRecordedLogs();
bool foundReFiBought = false;
for (uint i = 0; i < logs.length; i++) {
if (logs[i].topics[0] == keccak256("ReFiBought(address,uint256)")) {
foundReFiBought = true;
break;
}
}
assertTrue(foundReFiBought, "Sell incorrectly emitted ReFiBought");
vm.stopPrank();
}
// Note: Buy direction also inverted (see trace showing ReFiSold emitted when zeroForOne=true)
// but cannot execute due to test setup limitations with ETH settlement

Recommended Mitigation

Invert the return values to correctly identify buy vs sell operations. When ReFi is currency0, zeroForOne = false means swapping TO currency0 (buying ReFi). When ReFi is currency1, zeroForOne = true means swapping TO currency1 (buying ReFi). The corrected logic ensures fees are applied according to the actual transaction direction.

function _isReFiBuy(
PoolKey calldata key,
bool zeroForOne
) internal view returns (bool) {
bool IsReFiCurrency0 = Currency.unwrap(key.currency0) == ReFi;
if (IsReFiCurrency0) {
- return zeroForOne;
+ return !zeroForOne; // Buying ReFi = swapping TO currency0
} else {
- return !zeroForOne;
+ return zeroForOne; // Buying ReFi = swapping TO currency1
}
}
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!