RebateFi Hook

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

Router address emitted instead of user in ReFiBought/ReFiSold events

Description

  • The hook should emit ReFiBought and ReFiSold events that identify the actual trader (buyer/seller) who initiated the swap, so indexers, analytics, and compliance systems can attribute trades correctly.

  • In Uniswap v4 hook callbacks, the sender parameter of _beforeSwap() is the caller of the pool manager (typically the Uniswap router), not the end‑user EOA/contract that originated the trade. The hook uses this sender when emitting ReFiBought/ReFiSold, so events always show the router address (or another intermediary) rather than the real trader.

// Root cause in the codebase with @> marks to highlight the relevant section
function _beforeSwap(
address sender, // <-- router or intermediary, not the end trader
PoolKey calldata key,
SwapParams calldata params,
bytes calldata /* data */
) internal override returns (bytes4, BeforeSwapDelta, uint24) {
bool isReFiBuy = _isReFiBuy(key, params.zeroForOne);
uint256 swapAmount = params.amountSpecified < 0
? uint256(-params.amountSpecified)
: uint256(params.amountSpecified);
uint24 fee;
if (isReFiBuy) {
fee = buyFee;
@> emit ReFiBought(sender, swapAmount); // <-- emits router/intermediary address
} else {
fee = sellFee;
uint256 feeAmount = (swapAmount * sellFee) / 1_000_000;
@> emit ReFiSold(sender, swapAmount, feeAmount); // <-- emits router/intermediary address
}
return (
BaseHook.beforeSwap.selector,
BeforeSwapDeltaLibrary.ZERO_DELTA,
fee | LPFeeLibrary.OVERRIDE_FEE_FLAG
);
}

Risk

Likelihood: High

  • Happens on every swap processed through routers or other integrator contracts (the standard path in production).

  • Persists across all pools and directions; there is no configuration that makes sender equal to the actual trader by default.

Impact: Low

  • Attribution errors: Off‑chain analytics and compliance pipelines will attribute buys/sells to the router instead of the actual trader, breaking user‑level metrics, KYT, AML tracking, rebates, and loyalty programs.

  • Operational confusion: Alerts and dashboards keyed by the buyer/seller field will misreport who performed actions, complicating incident response and revenue sharing.

Proof of Concept

  • The following test will fail due to various reasons.

event ReFiSold(address indexed seller, uint256 amount, uint256 fee);
function testIsReFiSoldEventEmitted() public {
uint256 reFiAmount = 0.01 ether;
vm.startPrank(user1);
reFiToken.approve(address(swapRouter), type(uint256).max);
SwapParams memory params = SwapParams({
zeroForOne: false, // ReFi -> ETH
amountSpecified: -int256(reFiAmount),
sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1
});
PoolSwapTest.TestSettings memory testSettings = PoolSwapTest.TestSettings({
takeClaims: false,
settleUsingBurn: false
});
vm.expectEmit(true, false, false, false);
emit ReFiSold(user1, reFiAmount, (reFiAmount * 3000) / 1_000_00);
swapRouter.swap(key, params, testSettings, ZERO_BYTES);
vm.stopPrank();
}
  • Run this test with -vvv flag and observe that the first address in the emitted event is not the user's address.

│ │ │ ├─ [549] PoolManager::exttload(0xc6bd81b73d1fd76b6ab7b2a3e675f602f162633118b2ec0b74d6456669dd8579) [staticcall]
│ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
│ │ │ ├─ [2825] MockERC20::balanceOf(ECRecover: [0x0000000000000000000000000000000000000001]) [staticcall]
│ │ │ │ └─ ← [Return] 1000000000000000000000 [1e21]
│ │ │ ├─ [2825] MockERC20::balanceOf(PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ │ │ │ └─ ← [Return] 100000000000000000 [1e17]
│ │ │ ├─ [549] PoolManager::exttload(0xec1690143798fa6d9be0e2ae9987311729015febf65e065811da6492f94dd07b) [staticcall]
│ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
│ │ │ ├─ [44251] PoolManager::swap(PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x212224D2F2d262cd093eE13240ca4873fcCBbA3C, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0xb0Ba8cb9e47aB51e40B7c5665c151E511fA17080 }), SwapParams({ zeroForOne: false, amountSpecified: -10000000000000000 [-1e16], sqrtPriceLimitX96: 1461446703485210103287273052203988822378723970341 [1.461e48] }), 0x)
│ │ │ │ ├─ [6251] ReFiSwapRebateHook::beforeSwap(PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x212224D2F2d262cd093eE13240ca4873fcCBbA3C, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0xb0Ba8cb9e47aB51e40B7c5665c151E511fA17080 }), SwapParams({ zeroForOne: false, amountSpecified: -10000000000000000 [-1e16], sqrtPriceLimitX96: 1461446703485210103287273052203988822378723970341 [1.461e48] }), 0x)
│ │ │ │ │ ├─ emit ReFiBought(buyer: PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], amount: 10000000000000000 [1e16])
│ │ │ │ │ └─ ← [Return] 0x575e24b4, 0, 4194304 [4.194e6]
│ │ │ │ ├─ emit Swap(id: 0x2096077d27a3dc94d6cdd3cb057ae22cabeb103220ef6d0d4d8d3a6c81c3e9a1, sender: PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], amount0: 9997005541990553 [9.997e15], amount1: -10000000000000000 [-1e16], sqrtPriceX96: 79251894161187818237737846152 [7.925e28], liquidity: 33385024970969944913 [3.338e19], tick: 5, fee: 0)
  • The buyer is not the user1's address but the PoolSwapTest, which in real scenario would correspond to the Uniswap router address.

  • The reason why the ReFiBought event is emitted here is a separate bug, which is described in a separate report.

Recommended Mitigation

  • ince the hook cannot reliably infer the end trader from the sender parameter, pass the trader explicitly via the hook’s bytes calldata and decode it in _beforeSwap().

- function _beforeSwap(
- address sender,
- PoolKey calldata key,
- SwapParams calldata params,
- bytes calldata
- ) internal override returns (bytes4, BeforeSwapDelta, uint24) {
+ function _beforeSwap(
+ address sender, // router or integrator
+ PoolKey calldata key,
+ SwapParams calldata params,
+ bytes calldata data // encode the actual trader here
+ ) internal override returns (bytes4, BeforeSwapDelta, uint24) {
+ // Expect ABI: abi.encode(address trader)
+ address trader = sender; // default to sender
+ if (data.length == 32) {
+ trader = abi.decode(data, (address));
+ }
bool isReFiBuy = _isReFiBuy(key, params.zeroForOne);
uint256 swapAmount = params.amountSpecified < 0
? uint256(-params.amountSpecified)
: uint256(params.amountSpecified);
uint24 fee;
if (isReFiBuy) {
fee = buyFee;
- emit ReFiBought(sender, swapAmount);
+ emit ReFiBought(trader, swapAmount);
} else {
fee = sellFee;
uint256 feeAmount = (swapAmount * sellFee) / 1_000_000;
- emit ReFiSold(sender, swapAmount, feeAmount);
+ emit ReFiSold(trader, swapAmount, feeAmount);
}
return (
BaseHook.beforeSwap.selector,
BeforeSwapDeltaLibrary.ZERO_DELTA,
fee | LPFeeLibrary.OVERRIDE_FEE_FLAG
);
}
Updates

Lead Judging Commences

chaossr Lead Judge 11 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Router address emitted instead of user in ReFiBought/ReFiSold events

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!