RebateFi Hook

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

Inverted buy/sell logic causes incorrect fee application

Root + Impact

Description

  • The ReFiSwapRebateHook is designed to apply asymmetric fees: 0% for buying ReFi (to incentivize purchases) and a configurable sell fee (default 0.3%) to discourage selling. The _isReFiBuy function determines swap direction to apply the correct fee.

  • The logic in _isReFiBuy is inverted. In Uniswap V4, zeroForOne = true means swapping currency0 for currency1 (sending currency0, receiving currency1). When ReFi is currency0, zeroForOne = true means the user is sending ReFi (selling), but the function incorrectly returns true (indicating a buy). This causes buy fees to be applied on sells and sell fees to be applied on buys.

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

Risk

Likelihood:

  • Every swap on any ReFi pool triggers this inverted logic

  • The bug is deterministic and occurs 100% of the time regardless of swap size or user

Impact:

  • Buyers are penalized with the sell fee instead of enjoying 0% fee, discouraging token purchases

  • Sellers pay 0% fee instead of the configured sell fee, enabling fee-free dumps

  • Protocol loses all intended sell fee revenue

  • Economic model is completely inverted, the protocol incentivizes selling and penalizes buying

Proof of Concept

The following test demonstrates the inverted fee logic by performing two swaps with the same input amounts: a buy (ETH → ReFi) and a sell (ReFi → ETH).

With the configured fees of 0% for buys and 5% for sells, the expected behavior is that buyers receive more output tokens than sellers (since buyers pay no fee while sellers pay 5%). However, due to the inverted logic, the opposite occurs: buyers receive less output because the 5% sell fee is incorrectly applied to their transaction, while sellers receive more output because the 0% buy fee is incorrectly applied to theirs.

The final assertion assertLt(reFiReceivedFromBuy, ethReceivedFromSell) proves this inversion, it should fail in a correctly implemented system but passes here, confirming the bug.

function test_POC_InvertedBuySellFeeLogic() public {
// Setup: 0% buy fee, 5% sell fee
uint24 expectedBuyFee = 0;
uint24 expectedSellFee = 50000; // 5% (50000 / 1_000_000)
rebateHook.ChangeFee(true, expectedBuyFee, true, expectedSellFee);
vm.deal(user1, 10 ether);
vm.startPrank(user1);
reFiToken.approve(address(swapRouter), type(uint256).max);
PoolSwapTest.TestSettings memory testSettings = PoolSwapTest.TestSettings({
takeClaims: false,
settleUsingBurn: false
});
// TEST 1: BUY ReFi (ETH -> ReFi) - Should have 0% fee
uint256 ethToBuy = 0.01 ether;
uint256 reFiBalanceBefore = reFiToken.balanceOf(user1);
swapRouter.swap{value: ethToBuy}(
key,
SwapParams({
zeroForOne: true, // ETH -> ReFi = BUYING ReFi
amountSpecified: -int256(ethToBuy),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
}),
testSettings,
ZERO_BYTES
);
uint256 reFiReceivedFromBuy = reFiToken.balanceOf(user1) - reFiBalanceBefore;
// TEST 2: SELL ReFi (ReFi -> ETH) - Should have 5% fee
uint256 reFiToSell = 0.01 ether;
uint256 ethBalanceBefore = user1.balance;
swapRouter.swap(
key,
SwapParams({
zeroForOne: false, // ReFi -> ETH = SELLING ReFi
amountSpecified: -int256(reFiToSell),
sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1
}),
testSettings,
ZERO_BYTES
);
uint256 ethReceivedFromSell = user1.balance - ethBalanceBefore;
vm.stopPrank();
// PROOF: If correct, BUY (0% fee) should receive MORE than SELL (5% fee)
// Due to bug: BUY receives LESS because 5% sell fee was wrongly applied
assertLt(
reFiReceivedFromBuy,
ethReceivedFromSell,
"BUG: Buy received less than Sell due to inverted fee logic"
);
}

Recommended Mitigation

The fix requires inverting the return values in both branches of the conditional.

When ReFi is currency0:

  • zeroForOne = true means sending ReFi → selling → should return false

  • zeroForOne = false means receiving ReFi → buying → should return true

  • Therefore, return !zeroForOne instead of zeroForOne

When ReFi is currency1:

  • zeroForOne = true means receiving ReFi → buying → should return true

  • zeroForOne = false means sending ReFi → selling → should return false

  • Therefore, return zeroForOne instead of !zeroForOne

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 (currency0) = receiving it = !zeroForOne
} else {
- return !zeroForOne;
+ return zeroForOne; // Buying ReFi (currency1) = receiving it = 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!