Raisebox Faucet

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

Critical Logic Error in `burnFaucetTokens` - Transfer Entire Contract Balance

Root + Impact

The burnFaucetTokens function contains a critical programming error where it transfers the entire contract balance to the owner instead of only the specified amountToBurn parameter. This causes the faucet to become completely empty and non-functional after any burn operation.

Description

  • The normal and expected behavior is that when the owner calls burnFaucetTokens(amountToBurn), the function should transfer ONLY the specified amountToBurn tokens from the contract to the owner's address, then burn that exact amount from the owner's balance, leaving the remaining tokens in the contract for users to claim.

  • The specific issue occurs on line 131 of RaiseBoxFaucet.sol where the function uses balanceOf(address(this)) (which represents the ENTIRE contract balance) instead of the amountToBurn parameter in the _transfer call. This means that regardless of what value is passed as amountToBurn, ALL tokens in the contract are transferred to the owner, only the specified amount is burned, and the contract is left with zero tokens.

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))); // BUG: Transfers ALL balance
@> _burn(msg.sender, amountToBurn); // But only burns the specified amount
}

Risk

Likelihood:

  • The owner will call this function whenever they need to reduce the token supply for economic reasons or to manage inflation

  • This bug triggers with 100% certainty every single time the function is called

  • There are no conditions or edge cases - it's a deterministic bug that always occurs

  • The function will execute successfully (no revert) but with completely unintended consequences

Impact:

  • The entire faucet contract balance is transferred to the owner after ANY burn operation, regardless of the amount specified

  • The contract becomes completely empty with zero token balance

  • All users attempting to claim tokens will receive a revert with "InsufficientContractBalance" error

  • The faucet functionality is permanently broken until the owner mints new tokens back to the contract

  • This violates the protocol requirement that "owner cannot claim faucet tokens" as the owner indirectly receives all tokens

  • The owner ends up with (totalBalance - amountToBurn) tokens in their wallet instead of zero

Proof of Concept

This test demonstrates how the burn function incorrectly transfers all tokens instead of just the specified amount, breaking the faucet.

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("H-1: burnFaucetTokens Critical Bug", function () {
let faucet, owner, user1;
const INITIAL_SUPPLY = ethers.parseEther("1000000000"); // 1 billion tokens
const AMOUNT_TO_BURN = ethers.parseEther("100"); // Owner wants to burn 100 tokens
beforeEach(async function () {
[owner, user1] = await ethers.getSigners();
const RaiseBoxFaucet = await ethers.getContractFactory("RaiseBoxFaucet");
faucet = await RaiseBoxFaucet.deploy(
"RaiseBoxToken",
"RBT",
ethers.parseEther("1000"),
ethers.parseEther("0.005"),
ethers.parseEther("1")
);
await faucet.waitForDeployment();
});
it("Should demonstrate the bug: entire balance transferred instead of amountToBurn", async function () {
// Check initial state
const contractBalanceBefore = await faucet.balanceOf(await faucet.getAddress());
const ownerBalanceBefore = await faucet.balanceOf(owner.address);
console.log("\n=== BEFORE BURN ===");
console.log(`Contract Balance: ${ethers.formatEther(contractBalanceBefore)} tokens`);
console.log(`Owner Balance: ${ethers.formatEther(ownerBalanceBefore)} tokens`);
console.log(`Amount to Burn: ${ethers.formatEther(AMOUNT_TO_BURN)} tokens`);
// Expected behavior after burning 100 tokens:
const expectedContractBalance = contractBalanceBefore - AMOUNT_TO_BURN;
const expectedOwnerBalance = 0n; // Owner should have 0 (tokens transferred and burned)
console.log("\n=== EXPECTED AFTER BURN ===");
console.log(`Contract Balance: ${ethers.formatEther(expectedContractBalance)} tokens`);
console.log(`Owner Balance: ${ethers.formatEther(expectedOwnerBalance)} tokens`);
// Execute burn function
await faucet.connect(owner).burnFaucetTokens(AMOUNT_TO_BURN);
// Check actual state after burn
const contractBalanceAfter = await faucet.balanceOf(await faucet.getAddress());
const ownerBalanceAfter = await faucet.balanceOf(owner.address);
console.log("\n=== ACTUAL AFTER BURN ===");
console.log(`Contract Balance: ${ethers.formatEther(contractBalanceAfter)} tokens`);
console.log(`Owner Balance: ${ethers.formatEther(ownerBalanceAfter)} tokens`);
// BUG DEMONSTRATION:
// Contract should have (INITIAL_SUPPLY - 100), but actually has 0
expect(contractBalanceAfter).to.equal(0); // Contract is EMPTY!
// Owner should have 0, but actually has (INITIAL_SUPPLY - 100)
expect(ownerBalanceAfter).to.equal(INITIAL_SUPPLY - AMOUNT_TO_BURN);
console.log("\n❌ BUG CONFIRMED:");
console.log(`- ALL ${ethers.formatEther(INITIAL_SUPPLY)} tokens transferred to owner`);
console.log(`- Only ${ethers.formatEther(AMOUNT_TO_BURN)} tokens burned`);
console.log(`- Owner received ${ethers.formatEther(ownerBalanceAfter)} tokens (should be 0)`);
console.log(`- Contract has ${ethers.formatEther(contractBalanceAfter)} tokens (should be ${ethers.formatEther(expectedContractBalance)})`);
// Try to claim as a user - should fail because contract is empty
await expect(
faucet.connect(user1).claimFaucetTokens()
).to.be.revertedWithCustomError(faucet, "RaiseBoxFaucet_InsufficientContractBalance");
console.log("\n💔 FAUCET IS NOW BROKEN: Users cannot claim tokens!");
});
});

Recommended Mitigation

The fix is straightforward: replace balanceOf(address(this)) with the amountToBurn parameter in the transfer call. This ensures only the specified amount is transferred to the owner before burning.

Explanation: The current implementation transfers the entire balance because it queries the contract's full balance at the time of transfer. By using the amountToBurn parameter instead, we ensure that only the amount the owner wants to burn is transferred. This maintains the remaining tokens in the contract for users to claim and preserves the faucet's functionality.

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);
}
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.