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.
function _beforeSwap(
address sender,
PoolKey calldata key,
SwapParams calldata params,
bytes calldata
) 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);
} else {
fee = sellFee;
uint256 feeAmount = (swapAmount * sellFee) / 1_000_000;
@> emit ReFiSold(sender, swapAmount, feeAmount);
}
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
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,
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();
}
│ │ │ ├─ [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
- 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
);
}