RebateFi Hook

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

Owner Has Unrestricted Ability to Confiscate User Funds by Setting a 100% Sell Fee

Root + Impact

Description

Normal Behavior: The owner uses the ChangeFee() function to set transparent, asymmetric buy/sell fees to manage token economics. The swappers pay these fees when trading, expecting a functional and non-confiscatory rate.

  • Specific Issue: The owner possesses unrestricted administrative control over the core economic mechanism. The ability to modify the sell fee rate via ChangeFee() without any hard limit (fee cap) or time lock allows a malicious or compromised owner to instantly set a 100% sell fee. Since swappers are explicitly limited from bypassing hook-enforced fees, this action effectively traps all ReFi tokens held by users in the pool, resulting in total loss of principal upon selling.

//

Risk

Likelihood

High

Reason 1

This vulnerability is present immediately upon deployment and remains present throughout the hook's existence.

Reason 2

A malicious or compromised owner's private key can execute the attack with a single transaction at any time, requiring no preconditions other than the existence of the deployed hook.

Impact

Critical

Impact 1

Total loss of swappers' deposited funds (ReFi tokens), as the 100% sell fee traps the tokens in the contract. Swappers cannot sell them without losing the entire principal.

Impact 2

Complete loss of trust and abandonment of the RebateFi protocol, as the core economic incentive (fee structure) can be instantly corrupted by a single entity.

Proof of Concept

Explanation: By executing setSellFee(10000), the owner can ensure that when a swapper attempts to sell tokens, the fee calculation (amountToSell - fee) results in zero tokens returned to the user. This is a direct loss of user principal, confirming the confiscatory nature of the vulnerability.

pragma solidity ^0.8.19;
contract PoCVulnerableHook {
address public owner;
uint256 public sellFeeRate; // 10000 = 100%
constructor(uint256 _initialSellFeeRate) {
owner = msg.sender;
sellFeeRate = _initialSellFeeRate;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
// @> Root cause: The owner can set sellFeeRate to any arbitrary value, including 10000.
function setSellFee(uint256 _newSellFeeRate) public onlyOwner {
sellFeeRate = _newSellFeeRate;
}
// Simulated function representing the core logic of the hook during a SELL swap
function simulateSell(uint256 amountToSell) public view returns (uint256 tokensReceivedBySwapper) {
// Calculate fee: (Amount * Rate) / Basis (10000)
uint256 fee = (amountToSell * sellFeeRate) / 10000;
// --- Exploit Execution ---
// 1. Owner calls setSellFee(10000); sellFeeRate is now 10000.
// 2. User calls simulateSell(100);
// fee = (100 * 10000) / 10000 = 100.
// 3. User receives: 100 - 100 = 0.
if (fee >= amountToSell) {
return 0;
}
tokensReceivedBySwapper = amountToSell - fee;
}
}

Recommended Mitigation

Mitigation Explanation:

  1. Fee Cap (MAX_SELL_FEE_RATE): The proposeFeeChange function now includes a require check ensuring the proposed sell fee cannot exceed a reasonable, predefined value (e.g., 10%). This eliminates the possibility of a 100% confiscatory fee, severely limiting the maximum potential loss for users.

  2. Time Lock Mechanism: The single, immediate ChangeFee() function is replaced by a two-step process (proposeFeeChange and activateFeeChange).

    • The fee change is proposed and registered with an activation time (e.g., 48 hours in the future).

    • The change can only be activated once the timelock has expired. This delay provides users with sufficient time to exit the pool if the proposed new fee is deemed unfavorable, preventing the owner from rug-pulling users instantly.

  3. Governance Transfer: It is highly recommended to transfer the onlyOwner role to a Multi-Signature (Multi-Sig) wallet or a DAO governance contract. This decentralizes control and requires consensus from multiple, independent parties to execute fee changes, further reducing the risk of a single malicious actor.

diff --git a/contracts/RebateFiHook.sol b/contracts/RebateFiHook.sol
index 1234abc..5678def 100644
--- a/contracts/RebateFiHook.sol
+++ b/contracts/RebateFiHook.sol
@@ -5,11 +5,19 @@
contract RebateFiHook {
address public owner;
uint256 public buyFeeRate;
uint256 public sellFeeRate;
+ uint256 public constant MAX_SELL_FEE_RATE = 1000; // Hard cap at 10%
+ uint256 public feeChangeDelay = 48 hours; // 2 days timelock
+ uint256 public pendingSellFeeRate;
+ uint256 public pendingBuyFeeRate;
+ uint256 public feeChangeActivationTime;
- function ChangeFee(uint256 _buyFee, uint256 _sellFee) external onlyOwner {
+ // 1. Propose the new fee change, enforcing a hard cap
+ function proposeFeeChange(uint256 _newBuyFee, uint256 _newSellFee) external onlyOwner {
+ require(_newSellFee <= MAX_SELL_FEE_RATE, "Proposed sell fee exceeds hard cap");
+ pendingBuyFeeRate = _newBuyFee;
+ pendingSellFeeRate = _newSellFee;
+ feeChangeActivationTime = block.timestamp + feeChangeDelay;
+ emit FeeChangeProposed(_newBuyFee, _newSellFee, feeChangeActivationTime);
+ }
+
+ // 2. Activate the proposed fees after the timelock
+ function activateFeeChange() external onlyOwner {
+ require(block.timestamp >= feeChangeActivationTime, "Fee change not yet active");
+ buyFeeRate = pendingBuyFeeRate;
+ sellFeeRate = pendingSellFeeRate;
+ feeChangeActivationTime = 0; // Reset
+ emit FeeChangeActivated(buyFeeRate, sellFeeRate);
+ }
+
+ // Existing ChangeFee is removed and replaced by the two-step process
- buyFee = _buyFee;
- sellFee = _sellFee;
- }
}
Updates

Lead Judging Commences

chaossr Lead Judge 8 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!