RebateFi Hook

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

Buy/Sell Logic Is Reversed in _isReFiBuy (Critical Economic Failure)

Root + Impact

Description

Under expected behavior, the hook must apply:

  • Reduced fees on buys of the ReFi token.

  • Higher fees on sells of the ReFi token.

However, the _isReFiBuy() logic is implemented incorrectly.
When ReFi is currency1 (which is always the case due to a separate initialization bug), the function classifies every buy as a sell and every sell as a buy.

This fully inverts the economic incentives that the protocol intends to enforce.

The root cause is highlighted below:

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

Risk

Likelihood:

  • ReFi is always currency1 due to another bug in _beforeInitialize(), so this logic runs in the faulty branch 100% of the time.

  • Every swap involving the ReFi token will be misclassified.

  • No user can ever receive the intended fee structure.

Impact:

  • Buyers are charged premium sell fees, making accumulation unattractive.

  • Sellers are charged reduced buy fees, making dumping cheaper.

  • The tokenomics of ReFi reverse entirely, harming liquidity, price stability, and protocol sustainability.

  • Protocol revenue and incentives behave opposite to design, breaking the project’s core mechanism.

Proof of Concept

The following PoC shows how swap direction is always interpreted incorrectly when ReFi is currency1.

Explanation

  • In Uniswap V4:

    • zeroForOne = true → swap token0 → token1

    • zeroForOne = false → swap token1 → token0

  • If ReFi is currency1, then:

    • A buy means token0 → token1 → zeroForOne = true

    • A sell means token1 → token0 → zeroForOne = false

The hook mislabels these:

// Setup for the PoC where ReFi = currency1
key.currency0 = address(USDC);
key.currency1 = address(ReFi);
// ---- USER BUYS ReFi ----
bool zeroForOne = true; // token0 -> token1
bool isBuy = _isReFiBuy(key, zeroForOne);
// Expected: true (buying ReFi)
// Actual: false (treated as SELL)
// ---- USER SELLS ReFi ----
zeroForOne = false; // token1 -> token0
isBuy = _isReFiBuy(key, zeroForOne);
// Expected: false (selling ReFi)
// Actual: true (treated as BUY)

This PoC shows that every buy is treated as a sell and every sell is treated as a buy.

Recommended Mitigation

Explanation

To determine if a user is buying ReFi, we must check:

  • If ReFi is currency0, a buy is token1 -> token0zeroForOne = false

  • If ReFi is currency1, a buy is token0 -> token1zeroForOne = true

The corrected logic directly reflects this.

function _isReFiBuy(PoolKey calldata key, bool zeroForOne) internal view returns (bool) {
bool isReFiToken0 = Currency.unwrap(key.currency0) == ReFi;
- if (isReFiToken0) {
- return zeroForOne;
- } else {
- return !zeroForOne;
- }
+ // Buy ReFi if:
+ // - ReFi is token0 and user swaps token1 -> token0 (zeroForOne == false)
+ // - ReFi is token1 and user swaps token0 -> token1 (zeroForOne == true)
+ if (isReFiToken0) {
+ return !zeroForOne;
+ } else {
+ return zeroForOne;
+ }
}
Updates

Lead Judging Commences

chaossr Lead Judge 8 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!