RebateFi Hook

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

M01. Incorrect Buy/Sell Detection in Multi‑Hop Swaps

Root + Impact:

Description

In normal Uniswap V4 behavior, hooks determine the nature of a swap (buy vs sell) per pool, and each hook is responsible only for the pool it is attached to. If a swap is routed through multiple pools (multi‑hop), each hop should interpret swap direction correctly relative to its own token pair.

In the ReFiSwapRebateHook, the _isReFiBuy function classifies a swap as a buy or sell solely based on the boolean zeroForOne, assuming a simple 2‑token pool containing the ReFi token once. This logic fails when swaps occur through multi-hop paths, because the hook cannot reliably know whether the user is “buying ReFi” or is in an intermediate hop unrelated to ReFi. As a result, sell fees may incorrectly apply during legitimate multi-hop buys or buys may be incorrectly fee‑exempt while executing an intermediate hop.

// Root cause in the codebase with @> marks to highlight the relevant section
function _isReFiBuy(PoolKey calldata key, bool zeroForOne) internal view returns (bool) {
// @> This assumes pool always has ONE ReFi token and zeroForOne determines direction.
address tokenIn = zeroForOne ? Currency.unwrap(key.currency0) : Currency.unwrap(key.currency1);
// @> This fails for multi-hop because tokenIn may not be user’s actual entry token.
return tokenIn != ReFi;
}

The logic treats every pool swap as the “main swap” and misattributes buy/sell intent in multi-hop paths.

Risk

Likelihood:

  • Multi-hop routing occurs whenever users trade through aggregators (1inch, Matcha, CowSwap) or when liquidity is fragmented across multiple pools.

  • The hook is globally callable by any pool containing the hook’s address flags, so multi-hop paths routinely trigger it.

Impact:

  • The hook incorrectly charges sell fees on legitimate buys when a hop direction seems inverted.

  • The hook incorrectly applies zero buy fees on effective sells when the ReFi token is only touched in an intermediate hop.

Proof of Concept

Explanation

A user wants to buy ReFi using token X. The router chooses the path:

X → WETH → ReFi

During the first hop (X → WETH), zeroForOne may classify the hop as a sell relative to the pool’s ordering, and the ReFiHook applies a sell fee even though ReFi is not involved yet.
During the second hop (WETH → ReFi), the hook correctly sees a buy direction, but the first hop already incorrectly applied sell fees.

Below is a simplified Foundry test PoC:

function test_MultiHop_MisappliedSellFee() public {
// Prepare user and tokens
address user = address(0xA);
vm.deal(user, 10 ether);
tokenX.mint(user, 1000 ether);
tokenX.approve(address(swapRouter), type(uint256).max);
vm.startPrank(user);
// Multi-hop route X -> WETH -> ReFi
SwapParams memory hop1 = SwapParams({
zeroForOne: false, // Direction in pool[ X <-> WETH ]
amountSpecified: -int256(1 ether),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
SwapParams memory hop2 = SwapParams({
zeroForOne: true, // Direction in pool[ WETH <-> ReFi ]
amountSpecified: -int256(1 ether),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettings = PoolSwapTest.TestSettings({
takeClaims: false,
settleUsingBurn: false
});
// hop1: The hook incorrectly applies sell fee because tokenIn != ReFi
swapRouter.swap(poolXWETH, hop1, testSettings, ZERO_BYTES);
// hop2: The hook correctly applies buy logic
swapRouter.swap(poolWETHReFi, hop2, testSettings, ZERO_BYTES);
vm.stopPrank();
// Check that sell fee wrongly increased fee counters for hop1
(uint24 buyFee, uint24 sellFee) = rebateHook.getFeeConfig();
assertEq(sellFee, 3000, "Sell fee enabled");
assertGt(rebateHook.sellFeeCounter(), 0, "Sell fee incorrectly counted in hop1");
}

Written explanation:

  • The first hop does not involve ReFi, but _isReFiBuy misclassifies the direction.

  • As a result, the hook treats an unrelated swap as a “sell of ReFi”, charging sell fees.

  • The user ends up paying unintended fees, violating expected routing behavior.

Recommended Mitigation

Mitigation should modify the function _isReFiBuy to explicitly verify that the ReFi token is present in the pool before determining buy/sell direction. Only apply fee logic when ReFi is part of the pool.

function _isReFiBuy(PoolKey calldata key, bool zeroForOne) internal view returns (bool)
- address tokenIn = zeroForOne ? Currency.unwrap(key.currency0) : Currency.unwrap(key.currency1);
- return tokenIn != ReFi;
+ address token0 = Currency.unwrap(key.currency0);
+ address token1 = Currency.unwrap(key.currency1);
+ // Apply logic ONLY if the pool contains ReFi
+ if (token0 != ReFi && token1 != ReFi) return false;
+ // Determine buy/sell based on actual token movement relative to ReFi
+ bool tokenInIsReFi = zeroForOne ? (token0 == ReFi) : (token1 == ReFi);
+ return !tokenInIsReFi;
Updates

Lead Judging Commences

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