Raisebox Faucet

First Flight #50
Beginner FriendlySolidity
100 EXP
Submission Details
Impact: medium
Likelihood: medium

Burn Function Drains Faucet to Owner

Author Revealed upon completion

Root + Impact

Description

**Root Cause:**
The burn function transfers the entire faucet token balance to the owner before burning only the specified amount.
**Normal Behavior:**
Owner should be able to burn a specific amount of tokens from the faucet.
// Root cause in the codebase: The function transfers before it burns.
// @> This line transfers the ENTIRE contract balance to the owner.
_transfer(address(this), msg.sender, balanceOf(address(this)));
// @> This line only burns the specified amount from the owner's now-full balance.
_burn(msg.sender, amountToBurn);

Risk

Likelihood:

  • The vulnerability is triggered whenever the owner calls the burnFaucetTokens() function, regardless of the amountToBurn specified.

Impact:

  • Complete Drain of Faucet Supply: An owner can call this function with amountToBurn = 1 to steal the entire token supply intended for the community, rendering the faucet inoperable.

  • Centralization Risk and Trust Violation: The function serves as a hidden "rug pull" mechanism, giving the owner complete control to drain the contract at any time and violating the trust of the users.

Proof of Concept

The exploit can be demonstrated with a simple scenario:

  1. The faucet contract holds 10,000,000 tokens. The owner's wallet holds 0 tokens.

  2. The owner calls burnFaucetTokens(1).

  3. Inside the function, the _transfer call sends all 10,000,000 tokens to the owner. The faucet's balance is now 0.

  4. Next, the _burn call destroys 1 token from the owner's balance.

  5. Final State: The faucet contract is empty and useless. The owner's wallet now holds 9,999,999 tokens.

function testBurnFunctionDrainsFaucetToOwner() public {
uint256 faucetInitialBalance = raiseBoxFaucet.getFaucetTotalSupply();
uint256 ownerInitialBalance = raiseBoxFaucet.getBalance(owner);
// Owner calls burnFaucetTokens with a small amount
uint256 amountToBurn = 1000 * 10 ** 18; // 1000 tokens
vm.prank(owner);
raiseBoxFaucet.burnFaucetTokens(amountToBurn);
uint256 faucetFinalBalance = raiseBoxFaucet.getFaucetTotalSupply();
uint256 ownerFinalBalance = raiseBoxFaucet.getBalance(owner);
// Faucet should have 0 tokens (all transferred to owner)
assertEq(faucetFinalBalance, 0, "Faucet should be drained to 0");
// Owner should have received ALL faucet tokens, not just the burned amount
assertTrue(
ownerFinalBalance > ownerInitialBalance + amountToBurn,
"Owner received more than just the burned amount"
);
// Owner should have received the entire initial faucet balance (minus the burned amount)
assertEq(
ownerFinalBalance - ownerInitialBalance,
faucetInitialBalance - amountToBurn,
"Owner should have received the entire faucet balance minus burned amount"
);
}

Recommended Mitigation

Explanation

To fix this vulnerability, the function should burn tokens directly from the contract's own balance (address(this)) instead of performing a transfer to the owner first. The internal _burn(account, amount) function allows specifying the account from which to burn tokens.

This change correctly implements the intended burn functionality without creating a withdrawal backdoor.

function burnFaucetTokens(uint256 amountToBurn) public onlyOwner {
require(amountToBurn <= balanceOf(address(this)), "Faucet Token Balance: Insufficient");
- _transfer(address(this), msg.sender, balanceOf(address(this)));
- _burn(msg.sender, amountToBurn);
+ _burn(address(this), amountToBurn);
}

Support

FAQs

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