RebateFi Hook

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

Initialization denial of service(DoS)

Root + Impact

Description

  • Describe the normal behavior in one or more sentences

  • Explain the specific issue or problem in one or more sentences

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

Risk

Likelihood:

  • Reason 1 : The _beforeInitialize function only checks if key.currency1 is the ReFi token. It always ignores the address of key.currency0. ReFi is assigned to key.currency0 (i.e. address(ReFi) < address(OtherPair)). so we will have incorrect reverts of intialization .

Impact:

  • Impact 1: The hook always incorrectly blocks the deployment of valid ReFi pools whenever address(ReFi) < address(OtherPair)

    which results the pool always can't be intilized (i.e, address(ReFi) < address(OtherPair))

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol";
import {ReFiSwapRebateHook} from "../src/RebateFiHook.sol";
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
import {Currency} from "v4-core/types/Currency.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
import {HookMiner} from "v4-periphery/src/utils/HookMiner.sol";
import {LPFeeLibrary} from "v4-core/libraries/LPFeeLibrary.sol";
import {CustomRevert} from "v4-core/libraries/CustomRevert.sol";
/**
* @title PoC: Initialization DoS
* @notice Explains and demonstrates the Denial of Service vulnerability during pool initialization.
*
* The `RebateFiHook` intends to ensure the ReFi token is part of the pool. However, `_beforeInitialize`
* incorrectly checks `key.currency1` twice, ignoring `key.currency0`. If ReFi is the lower address
* (currency0), the check fails and reverts, preventing pool creation.
*
* Mitigation:
* Modify `_beforeInitialize` to check `key.currency0` as well.
* if (Currency.unwrap(key.currency0) != ReFi && Currency.unwrap(key.currency1) != ReFi) ...
*/
contract InitializationDoS is Deployers {
MockERC20 reFi;
MockERC20 other;
ReFiSwapRebateHook hook;
function setUp() public {
deployFreshManager();
reFi = new MockERC20("ReFi", "REFI", 18);
// Force address(ReFi) < address(Other)
do { other=new MockERC20("Other", "OTHR", 18); } while (address(other) <= address(reFi));
// Deploy the ReFi hook via CREATE2 with a precomputed salt so its address and flags are deterministic.
(, bytes32 salt) = HookMiner.find(
address(this),
uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG),
type(ReFiSwapRebateHook).creationCode,
abi.encode(manager, address(reFi))
);
hook = new ReFiSwapRebateHook{salt: salt}(manager, address(reFi));
}
function test_WhenReFiIsCurrency0() public {
try manager.initialize(PoolKey(Currency.wrap(address(reFi)), Currency.wrap(address(other)), LPFeeLibrary.DYNAMIC_FEE_FLAG, 60, hook), 79228162514264337593543950336) {
fail("Should revert");
} catch (bytes memory err) {
// Verify it's a WrappedError
bytes4 sel; assembly { sel := mload(add(err, 32)) }
assertEq(sel, CustomRevert.WrappedError.selector);
// Decode inner error (skip 4-byte selector)
bytes memory data = new bytes(err.length - 4);
for (uint i = 0; i < data.length; i++) data[i] = err[i + 4];
(,, bytes memory reason,) = abi.decode(data, (address, bytes4, bytes, bytes));
// Verify inner error is ReFiNotInPool
assembly { sel := mload(add(reason, 32)) }
assertEq(sel, ReFiSwapRebateHook.ReFiNotInPool.selector);
}
}
}

Recommended Mitigation

//fixed the condition to check for key.currency0
function _beforeInitialize(address, PoolKey calldata key, uint160) internal view override returns (bytes4) {
-- if (Currency.unwrap(key.currency1) != ReFi &&
-- 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!