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
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;
return !zeroForOne;
}
Recommended Mitigation
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
rebateHook.ChangeFee(false, 0, true, 5_000_000);
Recommended Mitigation
Set a maximum MAX_FEE as a state variable
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
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
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)
{
ReFi = _ReFi;
}
function withdrawTokens(address token, address to, uint256 amount) external onlyOwner {
IERC20(token).transfer(to, amount);
}
Recommended Mitigation
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);
}