RebateFi Hook

First Flight #53
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Impact: medium
Likelihood: high
Invalid

Inconsistent Event Data for Exact Output Swaps (Logic Error + Data Integrity)

Description:
The hook logs the swapAmount in ReFiBought and ReFiSold events.
swapAmount is derived from params.amountSpecified.

uint256 swapAmount = params.amountSpecified < 0
? uint256(-params.amountSpecified)
: uint256(params.amountSpecified);

In Uniswap V4:

  • For Exact Input swaps, amountSpecified is negative (Input Amount).

  • For Exact Output swaps, amountSpecified is positive (Output Amount).

Consequently:

  • If a user buys ReFi (Exact Input), the event logs the Input Amount (ETH).

  • If a user buys ReFi (Exact Output), the event logs the Output Amount (ReFi).

This inconsistency means the "Amount" field in the event has different units (Input Currency vs Output Currency) depending on the swap type.
Furthermore, for ReFiSold (Exact Output), the event logs the ETH amount (Output) but implies it is the ReFi amount sold. The fee calculation also uses this ETH amount as the base, which is incorrect as fees are charged on the input asset.

Impact:
Off-chain indexers and analytics will record incorrect volumes and fees. Users relying on these events for accounting will have corrupted data.

Proof of Concept:
test/M1_EventData.t.sol demonstrates that an Exact Output swap logs the Output Amount, whereas Exact Input logs Input Amount.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Test} from "forge-std/Test.sol";
import {ReFiSwapRebateHook} from "../src/RebateFiHook.sol";
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
import {PoolManager} from "v4-core/PoolManager.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {HookMiner} from "v4-periphery/src/utils/HookMiner.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
import {PoolSwapTest} from "v4-core/test/PoolSwapTest.sol";
import {SwapParams, ModifyLiquidityParams} from "v4-core/types/PoolOperation.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";
import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol";
import {LPFeeLibrary} from "v4-core/libraries/LPFeeLibrary.sol";
contract M1_EventDataTest is Test, Deployers {
using CurrencyLibrary for Currency;
ReFiSwapRebateHook public rebateHook;
MockERC20 reFiToken;
MockERC20 token;
Currency ethCurrency = Currency.wrap(address(0));
Currency reFiCurrency;
function setUp() public {
deployFreshManagerAndRouters();
reFiToken = new MockERC20("ReFi Token", "ReFi", 18);
reFiCurrency = Currency.wrap(address(reFiToken));
token = new MockERC20("TOKEN", "TKN", 18);
reFiToken.mint(address(this), 1000 ether);
token.mint(address(this), 1000 ether);
bytes memory creationCode = type(ReFiSwapRebateHook).creationCode;
bytes memory constructorArgs = abi.encode(manager, address(reFiToken));
uint160 flags = uint160(
Hooks.BEFORE_INITIALIZE_FLAG |
Hooks.AFTER_INITIALIZE_FLAG |
Hooks.BEFORE_SWAP_FLAG
);
(address hookAddress, bytes32 salt) = HookMiner.find(
address(this),
flags,
creationCode,
constructorArgs
);
rebateHook = new ReFiSwapRebateHook{salt: salt}(manager, address(reFiToken));
reFiToken.approve(address(rebateHook), type(uint256).max);
reFiToken.approve(address(swapRouter), type(uint256).max);
reFiToken.approve(address(modifyLiquidityRouter), type(uint256).max);
(key, ) = initPool(
ethCurrency,
reFiCurrency,
rebateHook,
LPFeeLibrary.DYNAMIC_FEE_FLAG,
SQRT_PRICE_1_1
);
modifyLiquidityRouter.modifyLiquidity{value: 10 ether}(
key,
ModifyLiquidityParams({
tickLower: -60,
tickUpper: 60,
liquidityDelta: 10 ether,
salt: bytes32(0)
}),
ZERO_BYTES
);
}
function test_M1_ExactOutput_LogsWrongAmount() public {
uint256 outputAmount = 1 ether;
vm.deal(address(this), 10 ether);
vm.expectEmit(true, true, true, true);
emit ReFiSwapRebateHook.ReFiSold(address(swapRouter), outputAmount, (outputAmount * 3000) / 100000);
swapRouter.swap{value: 10 ether}( // Send enough ETH
key,
SwapParams({
zeroForOne: true,
amountSpecified: int256(outputAmount), // Positive = Exact Output
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
}),
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
ZERO_BYTES
);
}
}

Recommended Mitigation:
To accurately log the ReFi amount in all cases, the hook would need to know the unspecified amount (which is not available in beforeSwap).
Alternatively, explicitly log the currency address along with the amount, or only log amountSpecified and zeroForOne so indexers can decode it.
Best approach: Use afterSwap to capture the actual deltas, or accept that beforeSwap can only log amountSpecified and clarify the units in the event (e.g. event SwapAttempt(int256 amountSpecified, ...)).

Updates

Lead Judging Commences

chaossr Lead Judge 11 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!