Raisebox Faucet

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

Faulty burn logic allows owner to drain faucet token supply

[H-1] Faulty burn logic allows owner to drain faucet token supply

Description

The burnFaucetTokens() function is intended to burn tokens from the faucet contract’s balance. Instead, it transfers the entire faucet balance to the owner and burns only amountToBurn from the owner’s balance. This allows the owner (or any party that gains ownership) to effectively steal the remaining faucet tokens while appearing to perform a burn.

Example scenario:

  1. Faucet holds 1,000 tokens.

  2. Owner calls burnFaucetTokens(100).

  3. Faucet sends all 1,000 tokens to the owner.

  4. Owner burns 100 tokens.

  5. Owner keeps 900 tokens.

This completely breaks the faucet’s tokenomics and allows arbitrary token theft.

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
// @audit This allows the owner to drain the faucet token supply arbitrarily.
// Because the function transfers the full balance but only burns amountToBurn, the owner receives all faucet tokens and then only destroys amountToBurn.
// Any tokens in excess of amountToBurn remain in the owner’s wallet.
_transfer(address(this), msg.sender, balanceOf(address(this)));
_burn(msg.sender, amountToBurn);
}

Risk

Likelihood:

  • The function is onlyOwner, so any compromised or malicious owner can abuse it directly.

  • No safeguards exist to restrict the transfer to only amountToBurn.

Impact:

  • Owner can drain all faucet tokens instantly.

  • The faucet becomes non-functional since no tokens remain for claimers.

  • Tokenomics are effectively broken — the faucet loses its distribution mechanism.

Proof of Concept

function test_burnDrainExploit() public {
// Faucet has initial supply minted to itself
uint256 faucetInitialBalance = raiseBoxFaucet.balanceOf(address(raiseBoxFaucet));
console.log("Faucet initial balance:", faucetInitialBalance);
// Owner balance before
uint256 ownerBalanceBefore = raiseBoxFaucet.balanceOf(owner);
// Owner calls the vulnerable burn function
vm.prank(owner);
raiseBoxFaucet.burnFaucetTokens(100 ether);
uint256 ownerBalanceAfter = raiseBoxFaucet.balanceOf(owner);
uint256 faucetBalanceAfter = raiseBoxFaucet.balanceOf(address(raiseBoxFaucet));
console.log("Owner gained:", ownerBalanceAfter - ownerBalanceBefore);
console.log("Faucet remaining:", faucetBalanceAfter);
// Faucet should not lose all tokens — but it does
assertEq(faucetBalanceAfter, 0, "Faucet was drained!");
assertGt(ownerBalanceAfter - ownerBalanceBefore, 0, "Owner gained tokens!");
}
forge test --match-test test_burnDrainExploit -vvv

Recommended Mitigation

  • Burn directly from the contract’s balance instead of transferring first.

  • No token transfer to the owner should occur in a burn routine.

function burnFaucetTokens(uint256 amountToBurn) public onlyOwner {
uint256 faucetBalance = balanceOf(address(this));
require(amountToBurn <= faucetBalance, "Insufficient faucet balance");
- _transfer(address(this), msg.sender, balanceOf(address(this)));
- _burn(msg.sender, amountToBurn);
+ _burn(address(this), amountToBurn); // burn directly from contract balance
}
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.