RebateFi Hook

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

Hook rejects pools

Root + Impact

Description

  • This hook is designed to work with pairs where ReFi is either currency0 or currency1 as can be seen from the _isReFiBuy() function having logic for both cases

  • The validation logic has mistakenly checked currency1 twice instead of checking both currency0 and currency1 that's why making the hook reject any pool where ReFi is currency0 and only accept pools where ReFi is currency1

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

Risk

Likelihood:

  • Any attempt to initialize a pool with ReFi as currency0 will always revert

  • Restricts protocol to half of possible pool configs

Impact:

  • It limits pool deployment flexibility


Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Test} from "forge-std/Test.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {Currency} from "v4-core/types/Currency.sol";
import {ReFiSwapRebateHook} from "../src/RebateFiHook.sol";
contract Currency0RejectionTest is Test {
ReFiSwapRebateHook hook;
address refiToken = address(0x123);
address otherToken = address(0x456);
function setUp() public {
IPoolManager poolManager = IPoolManager(address(0x789));
hook = new ReFiSwapRebateHook(poolManager, refiToken);
}
function testRejectsReFiAsCurrency0() public {
PoolKey memory poolKey = PoolKey({
currency0: Currency.wrap(refiToken), // ReFi as currency0
currency1: Currency.wrap(otherToken),
fee: 0x800000,
tickSpacing: 60,
hooks: hook
});
// Should revert with ReFiNotInPool despite ReFi being in the pool
vm.expectRevert(ReFiSwapRebateHook.ReFiNotInPool.selector);
hook.beforeInitialize(address(this), poolKey, 0, "");
}
function testAcceptsReFiAsCurrency1() public view {
PoolKey memory poolKey = PoolKey({
currency0: Currency.wrap(otherToken),
currency1: Currency.wrap(refiToken), // ReFi as currency1
fee: 0x800000,
tickSpacing: 60,
hooks: hook
});
hook.beforeInitialize(address(this), poolKey, 0, "");
}
function testCurrency0LogicUnreachable() public view {
PoolKey memory poolKey = PoolKey({
currency0: Currency.wrap(otherToken),
currency1: Currency.wrap(refiToken),
fee: 0x800000,
tickSpacing: 60,
hooks: hook
});
// Only currency1 path is reachable
bool isReFiCurrency0 = Currency.unwrap(poolKey.currency0) == refiToken;
assertFalse(isReFiCurrency0, "ReFi can never be currency0");
}
}

Recommended Mitigation

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!