RebateFi Hook

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

RebateFi Hook Security Audit Report

Root + Impact

This audit identified 6 critical vulnerabilities and 3 medium-risk issues in the RebateFi Hook smart contract. The protocol implements an innovative asymmetric fee structure for ReFi tokens on Uniswap V4, but contains several security flaws that require immediate attention before production deployment.

[H-1] Pool Initialization Always Fails Due to Logic Error

Location: RebateFiHook.sol:131-137 (_beforeInitialize)

Description:

The _beforeInitialize function contains a critical logic error that checks key.currency1 twice instead of checking both currency0 and currency1. This causes all pool initializations to fail.

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:

This occurs on every pool initialization due to incorrect logic. No user interaction needed; deterministic failure.

Impact:

  • Protocol completely non-functional - no pools can be initialized

  • All deployment attempts will fail

  • Users cannot interact with the protocol

Proof of Concept

The condition currency1 != ReFi && currency1 != ReFi is always false, causing the revert when ReFi is not in currency1, even if it's in currency0.

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

Recommended Mitigation

  1. Remove the duplicate key.currency1 and replace it with 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) {
+ Currency.unwrap(key.currency0 != ReFi) {
revert ReFiNotInPool();
}
return BaseHook.beforeInitialize.selector;
}

[H-2] Incorrect Swap Direction Detection Allows Fee Bypass

Location:RebateFiHook.sol:191-199 (_isReFiBuy)

Description:

The _isReFiBuy function has inverted logic that causes incorrect fee application based on swap direction.

function _isReFiBuy(PoolKey calldata key, bool zeroForOne) internal view returns (bool) {
bool IsReFiCurrency0 = Currency.unwrap(key.currency0) == ReFi;
if (IsReFiCurrency0) {
@> return zeroForOne;
} else {
return !zeroForOne;
}
}

Risk

Likelihood:

All swaps are misclassified whenever ReFi is currency0. This can be triggered easily by configuring the pool in this common layout

Impact:

  • When ReFi is currency0:

    • zeroForOne=true swaps currency0→currency1 (ReFi→ETH) = SELL, but detected as BUY (0% fee applied to sells!)

    • zeroForOne=false swaps currency1→currency0 (ETH→ReFi) = BUY, but detected as SELL (0.3% fee applied to buys!)

  • Completely inverts the economic model

  • ReFi sellers avoid fees while buyers are overcharged

  • Attackers can structure the pool to always use ReFi as currency0 to exploit this misclassification

  • Protocol fails to generate revenue, undermining its tokenomics and fee structure

Proof of Concept

Correct direction logic for different ReFi positions

ReFi Position zeroForOne Swap Direction Expected _isReFiBuy
currency0 true ReFi → ETH (SELL) false
currency0 false ETH → ReFi (BUY) true
currency1 true ETH → ReFi (BUY) true
currency1 false ReFi → ETH (SELL) false
function _isReFiBuy(PoolKey calldata key, bool zeroForOne) internal view returns (bool) {
bool IsReFiCurrency0 = Currency.unwrap(key.currency0) == ReFi;
// Buy ReFi = ETH -> ReFi = currency1 -> currency0
// Correct logic: return !zeroForOne in both cases
return !zeroForOne;
}

Recommended Mitigation

  1. Invert the condition to properly detect buys when ReFi is either token

- function _isReFiBuy(PoolKey calldata key, bool zeroForOne) internal view returns (bool) {
- bool IsReFiCurrency0 = Currency.unwrap(key.currency0) == ReFi;
- if (IsReFiCurrency0) {
- return zeroForOne;
- } else {
- return !zeroForOne;
- }
- }
+ function _isReFiBuy(PoolKey calldata key, bool zeroForOne) internal view returns (bool) {
+ bool IsReFiCurrency0 = Currency.unwrap(key.currency0) == ReFi;
+ return !zeroForOne;
+ }

[H-3] No Upper Bound on Fees - Owner Can Set Confiscatory Rates

Location: RebateFiHook.sol:87-95 (ChangeFee)

Description:

The ChangeFeefunction allows the contract owner to update the buyFeeand sellFee values used by the _beforeSwap hook. However, there is no upper bound on these values. Since fees are expressed in Uniswap V4 as millionth (1e6), value above 1_000_000 imply a fee greater than 100%, and anything above it violates the Uniswap V4 protocol cap for dynamic fees.

function ChangeFee(
bool _isBuyFee,
uint24 _buyFee,
bool _isSellFee,
uint24 _sellFee
) external onlyOwner {
@> if(_isBuyFee) buyFee = _buyFee;
@> if(_isSellFee) sellFee = _sellFee;
}

Risk

Likelihood:

This is a privileged operation and can be executed by the owner at any time. It's trivial to trigger and is deterministic

Impact:

  • Confiscatory trades - Owner can set sellFee= 1_000_000 resulting in 100% of tokens being taken

  • Protocol unusable - Setting an invalid fee causes swaps to revert

  • Stealth rug pull - Owner can front-run users by raising fees pre-trade

  • Loss of user trust - Arbitrary fees undermine market integrity

Proof of Concept

// Owner can do this:
rebateHook.ChangeFee(false, 0, true, 5_000_000);
// Now ALL sells have 500% fee and users lose everything

Recommended Mitigation

  1. Set a maximum MAX_FEE as a state variable

  2. Set a conditions where _buyFee and _sellFee are below the MAX_FEE threshold

- function ChangeFee(bool _isBuyFee, uint24 _buyFee, bool _isSellFee, uint24 _sellFee) external onlyOwner {
- if (_isBuyFee) buyFee = _buyFee;
- if (_isSellFee) sellFee = _sellFee;
- }
+ uint24 public constant MAX_FEE = 100000; // 10% maximum
+ function ChangeFee(
+ bool _isBuyFee,
+ uint24 _buyFee,
+ bool _isSellFee,
+ uint24 _sellFee
+ ) external onlyOwner {
+ if(_isBuyFee) {
+ require(_buyFee <= MAX_FEE, "Buy fee too high");
+ buyFee = _buyFee;
+ }
+ if(_isSellFee) {
+ require(_sellFee <= MAX_FEE, "Sell fee too high");
+ sellFee = _sellFee;
+ }
+ }

[H-4] Missing Return Value Check on Token Transfer

Location:
RebateFiHook.sol:77 (withdrawTokens)

Description:

The withdrawTokensfunction uses transfer() without checking the return value. Some ERC20 tokens return false on failure instead of reverting.

function withdrawTokens(address token, address to, uint256 amount) external onlyOwner {
@> IERC20(token).transfer(to, amount);
emit TokensWithdrawn(to, token, amount);
}

Risk

Likelihood:

Any ERC20 token using a non-standard transfer()return value (e.g, USDT, MKR, BNB) can trigger this, silently causing fund loss

Impact:

  • Silent failure of token withdrawal

  • Owner may believe withdrawal succeeded due to event emission

  • Funds may become stuck in contract

  • Security assumptions about withdrawal logic break

Proof of Concept

function withdrawTokens(address token, address to, uint256 amount) external onlyOwner {
require(IERC20(token).transfer(to, amount), "Transfer failed");
emit TokensWithdrawn(token, to, amount);
}

Recommended Mitigation

- function withdrawTokens(address token, address to, uint256 amount) external onlyOwner {
- IERC20(token).transfer(to, amount);
- emit TokensWithdrawn(to, token, amount);
- }
+ function withdrawTokens(address token, address to, uint256 amount) external onlyOwner {
+ require(IERC20(token).transfer(to, amount), "Transfer failed");
+ emit TokensWithdrawn(token, to, amount);
+ }

[H-5] No Reentrancy Protection on Token Withdrawal

Location:
RebateFiHook.sol:72-79 (withdrawTokens)

Description:

The withdrawTokensfunction does not implement any reentrancy protection. A malicious token may perform a reentrant call during transfer()if the token contract invokes callbacks or external logic.

@> function withdrawTokens(address token, address to, uint256 amount) external onlyOwner {
IERC20(token).transfer(to, amount);
emit TokensWithdrawn(to, token, amount);
}

Risk

Likelihood:

Medium - Requires a malicious ERC20 token with custom logic. Common in attack scenarios, but unlikely with standard tokens

Impact:

  • Reentrant callback can result in repeated withdrawals

  • Owner may lose controle of assets

  • Full balance can be drained if withdrawal is recursive

Proof of Concept

import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract ReFiSwapRebateHook is BaseHook, Ownable, ReentrancyGuard {
function withdrawTokens(address token, address to, uint256 amount)
external
onlyOwner
nonReentrant
{
IERC20(token).transfer(to, amount);
emit TokensWithdrawn(token, to, amount);
}
}

Recommended Mitigation

  1. Recommended to use ReentrancyGuard from OpenZeppelin to prevent from reentrancy attack

- function withdrawTokens(address token, address to, uint256 amount) external onlyOwner {
- IERC20(token).transfer(to, amount);
- emit TokensWithdrawn(to, token, amount);
- }
+ import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
+ contract ReFiSwapRebateHook is BaseHook, Ownable, ReentrancyGuard {
+ function withdrawTokens(address token, address to, uint256 amount)
+ external
+ onlyOwner
+ nonReentrant
+ {
+ IERC20(token).transfer(to, amount);
+ emit TokensWithdrawn(token, to, amount);
+ }
+ }

[M-1] Incorrect Event Parameter Order

Location:
RebateFiHook.sol:78

Description:

The TokensWithdrawnevent has parameters in wrong order compared to function call

event TokensWithdrawn(address indexed token, address indexed to, uint256 amount);
function withdrawTokens(address token, address to, uint256 amount) external onlyOwner {
IERC20(token).transfer(to, amount);
@> emit TokensWithdrawn(to, token, amount);
}

Risk

Likelihood:

High - This is a deterministic mistake and always happen when event is emitted

Impact:

  • Off-chain analytics misinterpret withdrawals

  • Event logs become misleading

  • Audit trails and compliance data are corrupted

Proof of Concept

  1. Fixed the correct emission from emit TokensWithdrawn(to, token, amount)to emit TokensWithdrawn(token, to, amount)

event TokensWithdrawn(address indexed token, address indexed to, uint256 amount);
function withdrawTokens(address token, address to, uint256 amount) external onlyOwner {
IERC20(token).transfer(to, amount);
emit TokensWithdrawn(token, to, amount);
}

Recommended Mitigation

event TokensWithdrawn(address indexed token, address indexed to, uint256 amount);
function withdrawTokens(address token, address to, uint256 amount) external onlyOwner {
IERC20(token).transfer(to, amount);
- emit TokensWithdrawn(to, token, amount);
+ emit TokensWithdrawn(token, to, amount);
}

[M-2] No Zero Address Validation

Location:
RebateFiHook.sol:62 (constructor), RebateFiHook.sol:77 (withdrawTokens)

Description:

Missing validation for zero addresses in critical functions

constructor(IPoolManager _poolManager, address _ReFi)
BaseHook(_poolManager)
Ownable(msg.sender)
{
@> ReFi = _ReFi;
}
function withdrawTokens(address token, address to, uint256 amount) external onlyOwner {
@> IERC20(token).transfer(to, amount);
}

Risk

Likelihood:

Medium - Can occur accidentally or due to misconfiguration

Impact:

  • Tokens sent to address(0) are unrecoverable

  • ReFi set to zero breaks fee logic

  • Malfuction of critical hooks

Proof of Concept

constructor(IPoolManager _poolManager, address _ReFi)
BaseHook(_poolManager)
Ownable(msg.sender)
{
// Add a condition to guard against address(0)
ReFi = _ReFi;
}
function withdrawTokens(address token, address to, uint256 amount) external onlyOwner {
// Add a condition to guard against address(0)
IERC20(token).transfer(to, amount);
}

Recommended Mitigation

  1. Add a condition to protect against address(0)

constructor(IPoolManager _poolManager, address _ReFi)
BaseHook(_poolManager)
Ownable(msg.sender)
{
+ require(_ReFi != address(0), "Invalid Address");
ReFi = _ReFi;
}
function withdrawTokens(address token, address to, uint256 amount) external onlyOwner {
+ require(to != address(0), "Invalid Recipient");
IERC20(token).transfer(to, amount);
}
Updates

Lead Judging Commences

chaossr Lead Judge 12 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

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

Inverted buy/sell logic when ReFi is currency0, leading to incorrect fee application.

Support

FAQs

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

Give us feedback!