Raisebox Faucet

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

burnFaucetTokens Drains Entire Faucet Supply

Root + Impact

Description

  • The burnFaucetTokens() function is designed to allow the owner to burn a specified amount of faucet tokens when needed, helping manage the token supply by permanently removing tokens from circulation.

  • The function transfers the ENTIRE contract balance to the owner on line 132, but only burns the amountToBurn parameter on line 134, allowing the owner to effectively steal all faucet tokens while appearing to burn only a small amount.

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))); // ❌ Transfers ALL tokens
_burn(msg.sender, amountToBurn); // Only burns the specified amount
}

Risk

Likelihood: High

  • The owner has legitimate reasons to call burnFaucetTokens() during normal operations (supply management, testing, emergency response)

  • The function executes successfully without any warnings or failed transactions - the owner may not realize they've drained the faucet

  • No events are emitted showing the actual transfer amount versus burn amount, making the discrepancy invisible to observers

  • The comment on lines 130-131 suggests this behavior is intentional but misunderstands the ERC20 _burn() function which doesn't require the caller to own tokens

Impact: Critical

  • The entire faucet token supply can be transferred to the owner in a single transaction disguised as a burn operation

  • Users will be unable to claim tokens after this occurs, breaking core protocol functionality

  • If owner calls burnFaucetTokens(100 * 10**18) on a faucet with 1,000,000 tokens, owner receives 999,900 tokens that should remain in the faucet

  • This violates the protocol's stated limitation that "owner cannot claim faucet tokens" (README line 37)

Proof of Concept

function testBurnFunctionDrainsFaucet() public {
// Initial state: Contract has full supply, owner has 0 tokens
uint256 initialFaucetBalance = raiseBoxFaucet.balanceOf(address(raiseBoxFaucet));
uint256 initialOwnerBalance = raiseBoxFaucet.balanceOf(owner);
assertEq(initialFaucetBalance, INITIAL_SUPPLY_MINTED); // 1 billion tokens
assertEq(initialOwnerBalance, 0);
// Owner decides to "burn" 1000 tokens to test the function
uint256 amountToBurn = 1000 * 10 ** 18;
vm.prank(owner);
raiseBoxFaucet.burnFaucetTokens(amountToBurn);
// Expected behavior:
// - Faucet balance should decrease by 1000 tokens
// - Owner should have 0 tokens (they were burned)
// - Total supply should decrease by 1000 tokens
// ACTUAL behavior (VULNERABILITY):
uint256 finalFaucetBalance = raiseBoxFaucet.balanceOf(address(raiseBoxFaucet));
uint256 finalOwnerBalance = raiseBoxFaucet.balanceOf(owner);
assertEq(finalFaucetBalance, 0, "VULN: Faucet completely drained!");
assertEq(finalOwnerBalance, INITIAL_SUPPLY_MINTED - amountToBurn, "VULN: Owner received almost all tokens!");
// Owner now has 999,999,000 tokens that should be in the faucet
// Only 1,000 tokens were actually burned
assertTrue(finalOwnerBalance > 999_000_000 * 10 ** 18, "Owner stole the faucet supply");
// Faucet is now broken - users cannot claim
vm.prank(user1);
vm.expectRevert(RaiseBoxFaucet.RaiseBoxFaucet_InsufficientContractBalance.selector);
raiseBoxFaucet.claimFaucetTokens();
}
function testBurnFunctionComparisonWithIntendedBehavior() public {
// Demonstrate what SHOULD happen vs what DOES happen
uint256 faucetBalance = 1_000_000 * 10 ** 18;
uint256 burnAmount = 100 * 10 ** 18;
console.log("=== INTENDED BEHAVIOR ===");
console.log("Faucet balance before:", faucetBalance);
console.log("Amount to burn:", burnAmount);
console.log("Expected faucet balance after:", faucetBalance - burnAmount);
console.log("Expected owner balance after:", 0);
console.log("Expected total supply decrease:", burnAmount);
console.log("\n=== ACTUAL BEHAVIOR (BUG) ===");
vm.prank(owner);
raiseBoxFaucet.burnFaucetTokens(burnAmount);
console.log("Actual faucet balance after:", raiseBoxFaucet.balanceOf(address(raiseBoxFaucet)));
console.log("Actual owner balance after:", raiseBoxFaucet.balanceOf(owner));
console.log("Actual total supply decrease:", burnAmount);
console.log("\n=== IMPACT ===");
console.log("Tokens stolen by owner:", faucetBalance - burnAmount);
console.log("Percentage of supply stolen:", ((faucetBalance - burnAmount) * 100) / faucetBalance, "%");
}

PoC Explanation: The first test demonstrates that when the owner calls burnFaucetTokens(1000) intending to burn 1000 tokens from a faucet containing 1 billion tokens, the function actually transfers ALL 1 billion tokens to the owner and only burns 1000, leaving the owner with 999,999,000 tokens and the faucet completely drained. The second test provides a side-by-side comparison showing the massive discrepancy between intended behavior (burn 100 tokens, faucet retains 999,900) versus actual behavior (owner receives 999,900 tokens, only 100 burned).

Recommended Mitigation

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(address(this), msg.sender, amountToBurn);
_burn(msg.sender, amountToBurn);
}

Mitigation Explanation: Change the transfer amount from balanceOf(address(this)) (entire contract balance) to amountToBurn (the specified amount). This ensures only the intended burn amount is transferred to the owner before burning. The original comment suggesting that the owner needs all tokens before burning is based on a misunderstanding - the ERC20 _burn() function only requires the caller to have sufficient balance to burn the specified amount, not the entire supply.

Updates

Lead Judging Commences

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