Raisebox Faucet

First Flight #50
Beginner FriendlySolidity
100 EXP
Submission Details
Severity: medium
Valid

Burn function transfers entire faucet balance to owner before burning, so Owner can drain all faucet tokens while only burning a small portion

Author Revealed upon completion

Root + Impact

Description

The burnFaucetTokens(uint256 amountToBurn) function transfers the entire token balance held by the faucet contract to the owner, and only then burns amountToBurn from the owner’s balance. This sequence lets the owner retain any excess tokens (i.e., balanceOf(address(this)) - amountToBurn) in their personal account. It contradicts the faucet’s stated behavior that the owner cannot claim faucet tokens and that burning should reduce the faucet’s holdings, not enrich the owner.

function burnFaucetTokens(uint256 amountToBurn) public onlyOwner {
require(amountToBurn <= balanceOf(address(this)), "Faucet Token Balance: Insufficient");
// transfer faucet balance to owner first before burning
// ensures owner has a balance before _burn (owner only function) can be called successfully
@> _transfer(address(this), msg.sender, balanceOf(address(this)));
_burn(msg.sender, amountToBurn);
}

Risk

Likelihood:

  • The vulnerable burnFaucetTokens() function is public and callable by the owner at any time, with no additional guardrails or timelocks. This means exploitation occurs as soon as the owner (or a compromised owner key) executes the function, regardless of system state.

  • The code path is directly incentivized, since the owner gains liquid faucet tokens instantly while only burning a fraction, making exploitation highly probable in real-world conditions or in the event of key compromise.


Impact:

  • The owner can extract all faucet tokens while only burning a negligible fraction.

  • Faucet loses its token reserves, halting all user claims.

Proof of Concept

Attack Scenario

  1. Faucet holds the INITIAL_SUPPLY_MINTED.

  2. Owner calls burnFaucetTokens(10e18).

  3. Contract transfers all tokens to the owner, then burns only 10e18.

  4. Result: owner keeps a **huge amount of **tokens; the faucet balance becomes zero.

Paste the following test in RaiseBoxFaucet.t.sol

function test_OwnerDrainsFaucetViaBurnKeepsRemainder() public {
// Pre-state
uint256 faucetBalBefore = raiseBoxFaucet.getFaucetTotalSupply();
assertEq(faucetBalBefore, INITIAL_SUPPLY_MINTED, "faucet should hold initial supply");
assertEq(raiseBoxFaucet.getBalance(owner), 0, "owner starts with zero faucet tokens");
// Action: owner "burns" a small amount
uint256 amountToBurn = 10 * 1e18;
vm.prank(owner);
raiseBoxFaucet.burnFaucetTokens(amountToBurn);
// Post-state
//The entire faucet balance was first transferred to owner,
// then only `amountToBurn` was burned from the owner.
assertEq(raiseBoxFaucet.getFaucetTotalSupply(), 0, "faucet balance should be zero after transfer-out");
assertEq(
raiseBoxFaucet.getBalance(owner),
faucetBalBefore - amountToBurn,
"owner keeps the remainder after burning only a small portion"
);
assertEq(
raiseBoxFaucet.totalSupply(),
faucetBalBefore - amountToBurn,
"only the burned amount is removed from totalSupply"
);
}

Recommended Mitigation

Burn directly from the faucet’s balance without transferring to the owner:

function burnFaucetTokens(uint256 amountToBurn) public onlyOwner {
require(amountToBurn <= balanceOf(address(this)), "Faucet Token Balance: Insufficient");
// transfer faucet balance to owner first before burning
// ensures owner has a balance before _burn (owner only function) can be called successfully
- _transfer(address(this), msg.sender, balanceOf(address(this)))
- _burn(msg.sender, amountToBurn);
+ _burn(address(this), amountToBurn);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 1 day ago
Submission Judgement Published
Validated
Assigned finding tags:

Unnecessary and convoluted logic in burnFaucetTokens

Support

FAQs

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