RebateFi Hook

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

ReFi sells misclassified as buys, letting dumpers dodge the premium fee

Root + Impact

Description

  • Normal behavior: When a user buys the designated ReFi token, the hook should apply the configured buyFee (typically 0%) so buys are subsidized. When a user sells ReFi, the hook should apply the higher sellFee (e.g., 0.3%) to discourage dumping.

  • Actual behavior: _isReFiBuy misclassifies every swap whenever ReFi is currency0, so sells are treated as buys (no fee) and buys are treated as sells (penalized). This flips the intended economics and lets dumpers bypass the premium fee entirely.

// src/RebateFiHook.sol
function _isReFiBuy(PoolKey calldata key, bool zeroForOne) internal view returns (bool) {
bool IsReFiCurrency0 = Currency.unwrap(key.currency0) == ReFi;
@> if (IsReFiCurrency0) {
@> return zeroForOne; // BUG: direction is inverted
@> } else {
@> return !zeroForOne;
@> }
}

Risk

Likelihood:

  • Any pool where ReFi is listed as currency0 (50% of all pool creations) will immediately experience inverted fees.

  • Users only need to execute a normal zeroForOne swap to buy/sell; no special setup or permissions are required.

Impact:

  • Sellers never pay the configured premium fee, eliminating protocol revenue and removing the anti-dump mechanism.

  • Buyers are overcharged the sellFee, discouraging accumulation and wrecking the hook’s purpose.

Proof of Concept

Explanation: Deploy a pool with currency0 == ReFi, perform a standard sell swap (zeroForOne == true), and log the fee value returned by the hook—you’ll observe it equals buyFee instead of sellFee.

function test_Sell_BypassesFee_WhenReFiIsCurrency0() external {
// Arrange: deploy pool where currency0 == ReFi and currency1 == ETH
// (identical setup to existing tests)
vm.deal(user1, 1 ether);
vm.startPrank(user1);
reFiToken.approve(address(swapRouter), type(uint256).max);
// Act: sell ReFi -> ETH (zeroForOne == true because ReFi is currency0)
SwapParams memory sell = SwapParams({
zeroForOne: true, // selling ReFi
amountSpecified: -int256(0.01 ether),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
uint24 fee = swapRouter.swap(key, sell, DEFAULT_SETTINGS, ZERO_BYTES);
// Assert: hook reports buy fee (0) instead of sell fee (3000)
assertEq(fee, rebateHook.buyFee(), "sell should not use buy fee");
}

Recommended Mitigation

Explanation: Flip the logic so “buy” means the user receives ReFi; when ReFi is currency0, that happens only when zeroForOne == false.

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;
- }
+ // Buying ReFi means receiving the ReFi token.
+ if (IsReFiCurrency0) {
+ return !zeroForOne; // zeroForOne == true means selling currency0
+ } else {
+ 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!