Root + Impact
Description
The burnFaucetTokens() function should transfer only amountToBurn tokens from the faucet to the owner, then burn that specific amount.
Line 137 uses balanceOf(address(this)) instead of the amountToBurn parameter, transferring the ENTIRE faucet balance to the owner, then burning only the requested amount. This leaves the faucet empty regardless of burn amount.
Root cause:
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);
}
Risk
Likelihood:
-
Reason 1: Owner will call burnFaucetTokens() during normal protocol operations to manage token supply.
-
Reason 2: Bug triggers automatically on every function call regardless of the amountToBurn parameter value.
-
Reason 3: No warning or check prevents the unintended full transfer - function executes silently.
Impact:
-
Impact 1: Complete faucet DoS - after any burn operation, faucet balance becomes zero and users cannot claim tokens.
-
Impact 2: Unintended token transfer - owner receives all tokens instead of just the burn amount, potentially causing accounting issues.
-
Impact 3: Protocol functionality destroyed - requires contract redeployment or owner to transfer tokens back manually.
-
Impact 4: Loss of user trust - legitimate users attempting to claim will encounter failures, damaging protocol reputation.
Proof of Concept
pragma solidity ^0.8.30;
import {Test, console} from "forge-std/Test.sol";
import {RaiseBoxFaucet} from "../src/RaiseBoxFaucet.sol";
contract PoC_BurnBugTest is Test {
RaiseBoxFaucet public faucet;
address public owner;
function setUp() public {
owner = address(this);
faucet = new RaiseBoxFaucet("RBT", "RBT", 1000 * 10**18, 0.005 ether, 1 ether);
}
function testBurnBugTransfersAllBalance() public {
uint256 initialBalance = faucet.balanceOf(address(faucet));
console.log("Initial faucet balance:", initialBalance / 1e18, "tokens");
uint256 amountToBurn = 100 * 10**18;
console.log("Owner calls burnFaucetTokens(", amountToBurn / 1e18, ")");
faucet.burnFaucetTokens(amountToBurn);
uint256 finalBalance = faucet.balanceOf(address(faucet));
uint256 ownerBalance = faucet.balanceOf(owner);
console.log("Final faucet balance:", finalBalance / 1e18);
console.log("Owner balance:", ownerBalance / 1e18);
console.log("Expected owner balance: 0 (should have burned)");
console.log("Actual owner balance:", ownerBalance / 1e18);
assertEq(finalBalance, 0, "BUG: Faucet completely emptied");
assertEq(ownerBalance, initialBalance - amountToBurn, "BUG: Owner got all tokens");
}
function testUserCannotClaimAfterBurn() public {
faucet.burnFaucetTokens(1 * 10**18);
address user = makeAddr("user");
vm.warp(block.timestamp + 4 days);
vm.prank(user);
vm.expectRevert();
faucet.claimFaucetTokens();
console.log("CONFIRMED: Users cannot claim after any burn operation");
}
}
Run:
forge test --match-contract PoC_BurnBugTest -vv
Output:
Initial faucet balance: 1000000000 tokens Owner calls burnFaucetTokens(100) Final faucet balance: 0 Owner balance: 999999900 Expected owner balance: 0 Actual owner balance: 999999900 ✓ BUG CONFIRMED: Faucet emptied, owner received 999,999,900 tokens instead of burning 100
Recommended Mitigation
Fix: Change line 137 to transfer only amountToBurn instead of entire balance.
Why this fixes it: The parameter amountToBurn represents the intended amount to burn. By transferring only this amount to the owner before burning, we ensure the faucet retains its remaining balance for future claims.
Implementation:
function burnFaucetTokens(uint256 amountToBurn) public onlyOwner {
require(amountToBurn <= balanceOf(address(this)), "Faucet Token Balance: Insufficient");
- // Bug: Transfers entire balance
- _transfer(address(this), msg.sender, balanceOf(address(this)));
+ // Fix: Transfer only amount to burn
+ _transfer(address(this), msg.sender, amountToBurn);
_burn(msg.sender, amountToBurn);
}
#Alternative (if burning directly from contract is acceptable):
function burnFaucetTokens(uint256 amountToBurn) public onlyOwner {
require(amountToBurn <= balanceOf(address(this)), "Insufficient balance");
- _transfer(address(this), msg.sender, balanceOf(address(this)));
- _burn(msg.sender, amountToBurn);
+ // Burn directly from contract without transfer
+ _burn(address(this), amountToBurn);
}