Raisebox Faucet

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

Owner Can Steal Entire Faucet Token Supply via burnFaucetTokens

Root + Impact

Description

Normal Behavior: The burnFaucetTokens() function should allow the owner to burn a specific amount of tokens from the faucet contract's balance. For example, if the contract holds 1,000,000 tokens and the owner calls burnFaucetTokens(50000), exactly 50,000 tokens should be burned and removed from circulation, leaving 950,000 tokens in the contract.

Actual Issue: The function transfers the entire contract balance to the owner first, then only burns the specified amountToBurn. This means the owner receives (totalBalance - amountToBurn) tokens instead of burning them, effectively stealing tokens from the faucet that were meant to be destroyed.

https://github.com/CodeHawks-Contests/2025-10-raisebox-faucet/blob/main/src/RaiseBoxFaucet.sol#L127-L135

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)));// @audit Transfers ENTIRE balance to owner, not just amountToBurn
_burn(msg.sender, amountToBurn);// @audit Only burns the specified amount, owner keeps the rest
}

Risk

Likelihood:

  • This occurs every time the owner calls burnFaucetTokens() with any amount less than the total contract balance

  • The owner will naturally call this function when the faucet accumulates excess tokens or when reducing supply is needed

  • No special conditions required - the bug triggers on normal function execution

Impact:

  • Token Supply Manipulation: Owner can drain all faucet tokens while appearing to "burn" them, breaking the tokenomics

  • Faucet Becomes Non-Functional: After a single burn call, the contract has 0 tokens left, preventing all future claims until the owner mints/refills

  • Trust Violation: Undermines trust in the faucet system, as the total supply can be covertly siphoned to the owner, potentially leading to premature token shortages for legitimate users and violating the "cannot claim faucet tokens" owner restriction.

  • Accounting Fraud: External observers see burn events but miss the hidden transfer, believing more tokens were destroyed than actually were

Proof of Concept

The PoC shows that when the owner attempts to burn only 1,000 tokens from a faucet holding 1,000,000,000 tokens, the function mistakenly transfers the entire 1,000,000,000 token balance to the owner but only burns the requested 1,000 tokens, leaving the owner with 999,999,000 stolen tokens. This completely empties the faucet contract (final balance = 0) while the owner illegitimately receives 999,999,000 tokens that should have remained in the faucet for user claims.

function testBurnFaucetTokensOwnerStealsExcess() public {
// Initial state
uint256 initialContractBalance = raiseBoxFaucet.getFaucetTotalSupply();
uint256 initialOwnerBalance = raiseBoxFaucet.getBalance(owner);
uint256 initialTotalSupply = raiseBoxFaucet.totalSupply();
assertEq(initialContractBalance, INITIAL_SUPPLY_MINTED, "Contract should hold initial supply");
assertEq(initialOwnerBalance, 0, "Owner should have zero tokens initially");
// Owner burns only a small amount (1000 tokens) while contract has full supply
uint256 amountToBurn = 1000 * 10 ** 18;
vm.prank(owner);
raiseBoxFaucet.burnFaucetTokens(amountToBurn);
// Post-burn state
uint256 finalContractBalance = raiseBoxFaucet.getFaucetTotalSupply();
uint256 finalOwnerBalance = raiseBoxFaucet.getBalance(owner);
uint256 finalTotalSupply = raiseBoxFaucet.totalSupply();
console.log("Final contract balance:", finalContractBalance);
console.log("Final owner balance:", finalOwnerBalance);
console.log("Final total supply:", finalTotalSupply);
// Assertions proving the exploit
assertEq(finalContractBalance, 0, "Contract drained entirely due to full transfer");
assertEq(finalOwnerBalance, initialContractBalance - amountToBurn, "Owner stole excess tokens (full balance minus burn)");
assertEq(finalTotalSupply, initialTotalSupply - amountToBurn, "Total supply reduced only by burn amount");
}

Test Output:

Ran 1 test for test/RaiseBoxFaucet.t.sol:TestRaiseBoxFaucet
[PASS] testBurnFaucetTokensOwnerStealsExcess() (gas: 66168)
Logs:
Final contract balance: 0
Final owner balance: 999999000000000000000000000
Final total supply: 999999000000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.97ms (794.70µs CPU time)

Recommended Mitigation

To mitigate this, transfer exactly amountToBurn to owner then burn (if intended to burn via owner). If the design requires owner to hold the tokens before burning, transfer only amountToBurn to msg.sender then burn from 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)));
+ // Transfer only the amount to be burned to owner
+ _transfer(address(this), msg.sender, amountToBurn);
_burn(msg.sender, amountToBurn);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 10 days 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.