Raisebox Faucet

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

[H-01] - Parameter mismatch in `burnFaucetTokens` allows owner to steal all tokens

Root + Impact

Description

The burnFaucetTokens function is designed to allow the owner to burn a specified amount of faucet tokens. The owner should only be able to burn the exact amount specified in the amountToBurn parameter.

The specific issue is a parameter mismatch where the function transfers the entire balance to the owner but only burns the amountToBurn, allowing the owner to steal the difference between the total balance and the burn amount.

// Root cause in the codebase
function burnFaucetTokens(uint256 amountToBurn) external onlyOwner {
uint256 balance = faucetToken.balanceOf(address(this));
require(balance > 0, "No tokens to burn");
faucetToken.transfer(msg.sender, balance); // @> Transfers ALL tokens
faucetToken.burn(amountToBurn); // @> Only burns amountToBurn
emit FaucetTokensBurned(amountToBurn);
}

Risk

Likelihood:

  • Owner can call this function at any time with any amount

  • No special conditions or prerequisites required

  • Single transaction exploit with immediate profit

  • Can be executed repeatedly to drain all tokens

Impact:

  • Complete theft of all faucet tokens (1 billion tokens)

  • Permanent loss of user assets with no recovery mechanism

  • Complete breakdown of the faucet system

  • Users cannot claim any tokens after theft

Proof of Concept

This test demonstrates how the owner can steal all faucet tokens:

  1. Setup: We give the faucet contract 1 billion tokens

  2. Attack: The owner calls burnFaucetTokens(1) to burn only 1 token

  3. Result: The owner receives all 1 billion tokens while only 1 token is burned

The exploit works because:

  • The function reads the entire balance with balanceOf(address(this))

  • It transfers this entire balance to the owner

  • But only burns the amountToBurn parameter

  • The difference (999,999,999 tokens) is stolen by the owner

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {RaiseBoxFaucet} from "../src/RaiseBoxFaucet.sol";
import {RaiseBoxToken} from "../src/RaiseBoxToken.sol";
contract OwnerTheftTest is Test {
RaiseBoxFaucet faucet;
RaiseBoxToken token;
address owner = makeAddr("owner");
uint256 constant INITIAL_SUPPLY = 1_000_000_000 * 10**18; // 1 billion tokens
function setUp() public {
vm.startPrank(owner);
token = new RaiseBoxToken();
faucet = new RaiseBoxFaucet(address(token));
// Mint tokens to faucet
token.mintFaucetTokens(address(faucet), INITIAL_SUPPLY);
vm.stopPrank();
}
function testOwnerStealsAllTokens() public {
uint256 faucetBalanceBefore = token.balanceOf(address(faucet));
uint256 ownerBalanceBefore = token.balanceOf(owner);
assertEq(faucetBalanceBefore, INITIAL_SUPPLY);
assertEq(ownerBalanceBefore, 0);
// Attack: Owner burns only 1 token but receives all tokens
vm.prank(owner);
faucet.burnFaucetTokens(1);
uint256 faucetBalanceAfter = token.balanceOf(address(faucet));
uint256 ownerBalanceAfter = token.balanceOf(owner);
// Verify theft: Owner received all tokens, only 1 was burned
assertEq(ownerBalanceAfter, INITIAL_SUPPLY); // Owner got all tokens
assertEq(faucetBalanceAfter, 0); // Faucet is empty
}
}

Recommended Mitigation

function burnFaucetTokens(uint256 amountToBurn) external onlyOwner {
uint256 balance = faucetToken.balanceOf(address(this));
require(balance > 0, "No tokens to burn");
+ require(balance >= amountToBurn, "Insufficient balance to burn");
- faucetToken.transfer(msg.sender, balance);
+ faucetToken.transfer(msg.sender, amountToBurn);
faucetToken.burn(amountToBurn);
emit FaucetTokensBurned(amountToBurn);
}

Or alternatively, remove the transfer entirely if the intent is only to burn:

function burnFaucetTokens(uint256 amountToBurn) external onlyOwner {
uint256 balance = faucetToken.balanceOf(address(this));
require(balance > 0, "No tokens to burn");
+ require(balance >= amountToBurn, "Insufficient balance to burn");
- faucetToken.transfer(msg.sender, balance);
faucetToken.burn(amountToBurn);
emit FaucetTokensBurned(amountToBurn);
}
Updates

Lead Judging Commences

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