Raisebox Faucet

First Flight #50
Beginner FriendlySolidity
100 EXP
Submission Details
Impact: high
Likelihood: high

burnFaucetTokens() Can Burn the Entire Faucet Supply

Author Revealed upon completion

Root + Impact

Description

Expected Behavior:

The burnFaucetTokens() function should allow the owner to burn only a specific amount of tokens that belong to the faucet’s internal reserves, while leaving enough supply to serve faucet users.

Actual Behavior:

The function currently transfers the entire faucet balance to the owner before burning the requested amount.

This means that if the owner passes a smaller _amountToBurn, the remaining tokens stay in the owner’s wallet — effectively removing them from the faucet’s circulation permanently.


Root Cause:

The entire contract’s balance is transferred to the owner before burning a portion.

This breaks the faucet’s token supply integrity, since any unburned tokens stay with the owner instead of returning to the faucet.

// Root cause in the codebase with @> marks to highlight the relevant section
function burnFaucetTokens(uint256 amountToBurn) public onlyOwner {
require(amountToBurn <= balanceOf(address(this)), "Faucet Token Balance: Insufficient");
// transfer faucet balance to owner first before burning
@> _transfer(address(this), msg.sender, balanceOf(address(this)));
@> _burn(msg.sender, amountToBurn);
}

Risk

Likelihood

High: This function is owner-only but highly likely to occur, since it is part of maintenance and may be misused during manual burning or rebalancing.

Impact

1.Faucet becomes depleted of tokens, making further user claims impossible.

2.The owner unintentionally holds excess unburned tokens.

3.Creates a centralization risk where faucet funds depend on the owner returning tokens manually.



Proof of Concept

Explanation:

The faucet ends up with zero tokens even though only 10 ether was burned proving the entire supply is transferred first, leaving the faucet empty.

pragma solidity ^0.8.30;
import "forge-std/Test.sol";
import "../src/RaiseBoxFaucet.sol";
contract BurnFaucetPoCTest is Test {
RaiseBoxFaucet faucet;
address owner = address(0x123);
function setUp() public {
vm.deal(owner, 1 ether);
vm.startPrank(owner);
faucet = new RaiseBoxFaucet("RaiseBox", "RBF", 100 ether, 0.01 ether, 1 ether);
vm.stopPrank();
}
function testFaucetBurnTransfersAllBalance() public {
vm.startPrank(owner);
uint256 beforeBalance = faucet.balanceOf(address(faucet));
faucet.burnFaucetTokens(10 ether); // burns 10, but moves all to owner
uint256 afterBalance = faucet.balanceOf(address(faucet));
vm.stopPrank();
assertEq(afterBalance, 0, "Faucet balance should remain for distribution!");
emit log_named_uint("Owner token gain", faucet.balanceOf(owner));
}
}

Recommended Mitigation

Explanation


Removes the redundant _transfer call that empties the faucet.

Burns directly from the contract’s own balance, preserving remaining supply for future claims.

Adds an event to improve visibility of burned amounts.

Prevents accidental depletion or misallocation of faucet reserves.

- remove this code
+ add this code
@@
function burnFaucetTokens(uint256 amountToBurn) public onlyOwner {
require(amountToBurn <= balanceOf(address(this)), "Faucet Token Balance: Insufficient");
- // transfer faucet balance to owner first before burning
- _transfer(address(this), msg.sender, balanceOf(address(this)));
-
- _burn(msg.sender, amountToBurn);
-}
+ // directly burn from the faucet contract's own balance
+ _burn(address(this), amountToBurn);
+
+ emit FaucetTokensBurned(address(this), amountToBurn);
+}

Support

FAQs

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