Raisebox Faucet

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

burnFaucetTokens() Drains All Faucet Tokens Instead of Burning Only Specified Amount

Description

The burnFaucetTokens() function contains a critical flaw in its implementation that causes it to drain the entire faucet balance instead of burning only the specified amount. When the owner attempts to burn tokens for supply management, the function first transfers ALL tokens to the owner, then only burns the requested amount, leaving the faucet completely empty and non-functional.

Root + Impact

Normal Behavior:
The burnFaucetTokens() function should burn a specified amount of tokens from the contract's balance while keeping the remaining tokens in the contract for future claims.

The Issue:
The function transfers the entire contract balance to the owner before burning only the amountToBurn. This leaves the faucet with zero tokens, even when the owner only intends to burn 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 amountToBurn
}

Risk

Likelihood:

  • The owner calls burnFaucetTokens() to burn tokens for any reason (supply management, deflation, etc.)

  • The function is explicitly provided as a protocol feature, making it likely to be used

Impact:

  • All faucet tokens are transferred to the owner's wallet, leaving the contract with 0 balance

  • Users can no longer claim tokens via claimFaucetTokens() (will revert with RaiseBoxFaucet_InsufficientContractBalance)

  • Faucet becomes permanently non-functional until owner mints new tokens via mintFaucetTokens() or transfers back tokens

  • Protocol's core functionality (distributing tokens to users) is completely disrupted

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import {Test, console} from "forge-std/Test.sol";
import "../src/RaiseBoxFaucet.sol";
contract BurnFaucetTokensTest is Test {
RaiseBoxFaucet public faucet;
address public owner = address(1);
address public user = address(2);
function setUp() public {
vm.prank(owner);
faucet = new RaiseBoxFaucet(
"RaiseBox",
"RB",
1000 * 10 ** 18, // faucetDrip
0.01 ether, // sepEthDrip
1 ether // dailySepEthCap
);
}
function testBurnFaucetTokensDrainsAllTokens() public {
// Initial state: contract has 1 billion tokens
uint256 initialBalance = faucet.balanceOf(address(faucet));
assertEq(initialBalance, 1000000000 * 10 ** 18);
console.log("Contract balance before burn:", initialBalance / 10**18);
// Owner wants to burn only 1000 tokens
uint256 amountToBurn = 1000 * 10 ** 18;
console.log("Amount to burn:", amountToBurn / 10**18);
vm.prank(owner);
faucet.burnFaucetTokens(amountToBurn);
// Expected: Contract should have (initialBalance - amountToBurn) remaining
// Actual: Contract has 0 tokens
uint256 finalBalance = faucet.balanceOf(address(faucet));
console.log("Contract balance after burn:", finalBalance / 10**18);
assertEq(finalBalance, 0); // ❌ Faucet is drained!
// Owner received ALL tokens (not just amountToBurn)
uint256 ownerBalance = faucet.balanceOf(owner);
console.log("Owner balance:", ownerBalance / 10**18);
assertEq(ownerBalance, initialBalance - amountToBurn); // Owner has almost all tokens
console.log("Faucet is non-functional!");
// Users can no longer claim
vm.warp(block.timestamp + 3 days + 1);
vm.prank(user);
vm.expectRevert(RaiseBoxFaucet.RaiseBoxFaucet_InsufficientContractBalance.selector);
faucet.claimFaucetTokens(); // ❌ Faucet is broken
}
}

Test Output:

Contract balance before burn: 1,000,000,000 tokens
Amount to burn: 1,000 tokens
Contract balance after burn: 0 tokens ❌
Owner balance: 999,999,000 tokens
Faucet is non-functional ❌

Actors:

  • Owner: Attempts to burn small amount of tokens for supply management

  • Protocol: Entire faucet is drained and becomes non-functional

  • Users: Cannot claim tokens, faucet is broken

Mitigation

Remove the unnecessary transfer step and burn tokens directly from the contract. The _burn() function in OpenZeppelin's ERC20 implementation can burn from any address, not just msg.sender. The current implementation mistakenly transfers all tokens to the owner before burning, when it should simply burn the specified amount directly from the contract's balance.

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 tokens directly from the contract
+ _burn(address(this), amountToBurn);
}
Updates

Lead Judging Commences

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