RebateFi Hook

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

`_beforeInitialize` Token Check Logic Error

_beforeInitialize Token Check Logic Error

Description

  • Before pool initialization, the hook contract should verify that the pool must contain the specified ReFi token to ensure that only the correct trading pair can use this hook.

  • The token check logic in the _beforeInitialize function contains duplicate conditions. In reality, it only checks if currency1 is equal to ReFi, completely ignoring the check for currency0.

function _beforeInitialize(address, PoolKey calldata key, uint160) internal view override returns (bytes4) {
@> if (Currency.unwrap(key.currency1) != ReFi &&
@> Currency.unwrap(key.currency1) != ReFi) { // ❌ Both conditions check if currency1 != ReFi
revert ReFiNotInPool();
}
return BaseHook.beforeInitialize.selector;
}

Risk

Likelihood:

  • When creating a pool that includes the ReFi token but ReFi is in the currency0 position, this check will incorrectly revert.

  • When creating a pool that does not include the ReFi token but currency1 happens to be equal to the ReFi address, this check will incorrectly pass.

Impact:

  • Prevents Legitimate Pool Creation: If the pool is configured as (ReFi, ETH) with ReFi in the currency0 position, initialization will be incorrectly rejected.

  • Allows Illegitimate Pool Creation: If the pool is configured as (ETH, some other token that happens to be equal to the ReFi address), initialization will be incorrectly allowed.

  • Deployment Deadlock: Project owners may be unable to correctly deploy the ReFi trading pair they want, as it depends on the token ordering.

  • Inconsistent Functionality: Only half of the legitimate ReFi pools can successfully initialize, depending on the token's position in the PoolKey.

Proof of Concept

  • In the test folder, add RebateFiHookTest_02.t.sol, as follows:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "forge-std/console.sol";
import {Test} from "forge-std/Test.sol";
import {ReFiSwapRebateHook} from "../src/RebateFiHook.sol";
import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol";
import {PoolSwapTest} from "v4-core/test/PoolSwapTest.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 {SwapParams, ModifyLiquidityParams} from "v4-core/types/PoolOperation.sol";
//import {SwapParams, ModifyLiquidityParams} from "v4-core/libraries/Pool.sol";
import {CustomRevert} from "v4-core/libraries/CustomRevert.sol";
import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol";
import {ERC1155TokenReceiver} from "solmate/src/tokens/ERC1155.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {PoolId} from "v4-core/types/PoolId.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
import {IHooks} from "v4-core/interfaces/IHooks.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";
import {SqrtPriceMath} from "v4-core/libraries/SqrtPriceMath.sol";
import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol";
import {HookMiner} from "v4-periphery/src/utils/HookMiner.sol";
import {LPFeeLibrary} from "v4-core/libraries/LPFeeLibrary.sol";
contract TestReFiSwapRebateHook_02 is Test, Deployers, ERC1155TokenReceiver {
MockERC20 token;
MockERC20 reFiToken;
MockERC20 someBiggerToken;
ReFiSwapRebateHook public rebateHook;
Currency ethCurrency = Currency.wrap(address(0));
Currency tokenCurrency;
Currency reFiCurrency;
Currency someBiggerCurrency;
address user1 = address(0x1);
address user2 = address(0x2);
uint160 constant SQRT_PRICE_1_1_s = 79228162514264337593543950336;
function setUp() public {
// Deploy the Uniswap V4 PoolManager
deployFreshManagerAndRouters();
// Deploy the ERC20 token
token = new MockERC20("TOKEN", "TKN", 18);
tokenCurrency = Currency.wrap(address(token));
// Deploy the ReFi token
reFiToken = new MockERC20("ReFi Token", "ReFi", 18);
reFiCurrency = Currency.wrap(address(reFiToken));
// Deploy some bigger token
someBiggerToken = new MockERC20("BiggerToken", "BTKN", 18);
someBiggerCurrency = Currency.wrap(address(someBiggerToken));
// Mint tokens to test contract and users
token.mint(address(this), 1000 ether);
token.mint(user1, 1000 ether);
token.mint(user2, 1000 ether);
reFiToken.mint(address(this), 1000 ether);
reFiToken.mint(user1, 1000 ether);
reFiToken.mint(user2, 1000 ether);
someBiggerToken.mint(address(this), 1000 ether);
someBiggerToken.mint(user1, 1000 ether);
someBiggerToken.mint(user2, 1000 ether);
// Get creation code for hook
bytes memory creationCode = type(ReFiSwapRebateHook).creationCode;
bytes memory constructorArgs = abi.encode(manager, address(reFiToken));
// Find a salt that produces a valid hook address
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
);
// Deploy the hook with the mined salt
rebateHook = new ReFiSwapRebateHook{salt: salt}(manager, address(reFiToken));
require(address(rebateHook) == hookAddress, "Hook address mismatch");
// Approve tokens for the test contract
token.approve(address(swapRouter), type(uint256).max);
token.approve(address(modifyLiquidityRouter), type(uint256).max);
reFiToken.approve(address(rebateHook), type(uint256).max);
reFiToken.approve(address(swapRouter), type(uint256).max);
reFiToken.approve(address(modifyLiquidityRouter), type(uint256).max);
someBiggerToken.approve(address(rebateHook), type(uint256).max);
someBiggerToken.approve(address(swapRouter), type(uint256).max);
someBiggerToken.approve(address(modifyLiquidityRouter), type(uint256).max);
}
/* ══════════════════════════════════════════════════════════════════
INITIALIZATION TESTS
══════════════════════════════════════════════════════════════════ */
function test_ReFiSwapRebateHook_02_InitPoolFailed() public {
PoolKey memory key;
Currency currency0_ = reFiCurrency;
Currency currency1_ = someBiggerCurrency;
IHooks iHooks_ = IHooks(address(rebateHook));
uint24 fee_ = LPFeeLibrary.DYNAMIC_FEE_FLAG;
uint160 sqrtPriceX96_ = SQRT_PRICE_1_1_s;
PoolKey memory key_ = PoolKey(currency0_, currency1_, fee_, (fee_ == LPFeeLibrary.DYNAMIC_FEE_FLAG) ? int24(60) : int24(fee_ / 100 * 2), iHooks_);
bytes memory data = abi.encodeCall(IHooks.beforeInitialize, (address(this), key_, sqrtPriceX96_));
// console.log(address(rebateHook)); // 0x0C897A35742f0d25307c60b28bD526c0aA777080
// console.logBytes4(bytes4(data)); // 0xdc98354e
// console.logBytes4(ReFiSwapRebateHook.ReFiNotInPool.selector); // 0x7ac0d4d7
// console.logBytes4(Hooks.HookCallFailed.selector); // 0xa9e35b2f
bytes memory reason = abi.encodeWithSelector(ReFiSwapRebateHook.ReFiNotInPool.selector);
// bytes memory details = abi.encode(Hooks.HookCallFailed.selector);
bytes memory details = abi.encodePacked(Hooks.HookCallFailed.selector);
// Precise Error Capture
vm.expectRevert(
abi.encodeWithSelector(
CustomRevert.WrappedError.selector,
address(rebateHook),
bytes4(data),
reason,
details
)
);
// Initialize the pool with ReFi token using DYNAMIC_FEE_FLAG
(key, ) = initPool(
currency0_,
currency1_,
iHooks_,
fee_,
sqrtPriceX96_
);
}
}
  • Run:forge test --mt test_ReFiSwapRebateHook_02_InitPoolFailed -vvvv

  • Console output:

Ran 1 test for test/RebateFiHookTest_02.t.sol:TestReFiSwapRebateHook_02
[PASS] test_ReFiSwapRebateHook_02_InitPoolFailed() (gas: 31146)
Traces:
[31146] TestReFiSwapRebateHook_02::test_ReFiSwapRebateHook_02_InitPoolFailed()
├─ [0] VM::expectRevert(custom error 0xf28dceb3: 0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000010490bfb8650000000000000000000000000c897a35742f0d25307c60b28bd526c0aa777080dc98354e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000047ac0d4d7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004a9e35b2f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000)
│ └─ ← [Return]
├─ [10047] PoolManager::initialize(PoolKey({ currency0: 0x212224D2F2d262cd093eE13240ca4873fcCBbA3C, currency1: 0x2a07706473244BC757E10F2a9E86fB532828afe3, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x0C897A35742f0d25307c60b28bD526c0aA777080 }), 79228162514264337593543950336 [7.922e28])
│ ├─ [1598] ReFiSwapRebateHook::beforeInitialize(TestReFiSwapRebateHook_02: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], PoolKey({ currency0: 0x212224D2F2d262cd093eE13240ca4873fcCBbA3C, currency1: 0x2a07706473244BC757E10F2a9E86fB532828afe3, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x0C897A35742f0d25307c60b28bD526c0aA777080 }), 79228162514264337593543950336 [7.922e28])
│ │ └─ ← [Revert] ReFiNotInPool()
│ └─ ← [Revert] WrappedError(0x0C897A35742f0d25307c60b28bD526c0aA777080, 0xdc98354e, 0x7ac0d4d7, 0xa9e35b2f)
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 12.14ms (177.20µs CPU time)

Recommended Mitigation

function _beforeInitialize(address, PoolKey calldata key, uint160) internal view override returns (bytes4) {
- if (Currency.unwrap(key.currency1) != ReFi &&
+ if (Currency.unwrap(key.currency0) != ReFi &&
Currency.unwrap(key.currency1) != ReFi) {
revert ReFiNotInPool();
}
return BaseHook.beforeInitialize.selector;
}
Updates

Lead Judging Commences

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

Faulty pool check; only checks currency1 twice, omitting currency0.

Support

FAQs

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

Give us feedback!