Root Cause
burnFaucetTokens(uint256 amountToBurn) performs a pre-burn full-balance transfer from the faucet to the owner, then burns only amountToBurn. This ignores the parameter during transfer and drains faucet reserves.
Impact
Full faucet drain: The contract’s token reserves become 0 after any successful call.
Unintended owner enrichment: The owner keeps faucetBalance − amountToBurn, breaking faucet distribution and rendering the mechanism non-functional.
Expected Behavior
A faucet “burn” should reduce supply by exactly amountToBurn from the faucet’s holdings, leaving the remaining faucet balance intact for user claims (or transfer exactly amountToBurn to the burner first, then burn).
Actual Behavior
The function transfers the entire faucet balance to the owner:
Total supply decreases by amountToBurn (which can mislead shallow tests), but the faucet reserves are already fully withdrawn to the owner.
Likelihood: Medium
Reason 1: A single privileged invocation by the owner (or anyone who controls the owner key) always produces a full drain; no special state required.
Reason 2: The pattern “transfer then burn” is common during maintenance; this flawed implementation will be executed during normal operations, not only under adversarial conditions.
Impact: High
Impact 1: Funds at direct risk — faucet reserves go to zero in one call.
Impact 2: Protocol DoS / tokenomics disruption — claimFaucetTokens reverts post-drain due to insufficient contract balance; users can no longer claim.
Severity Recommendation: High (Impact: High; Likelihood: Medium due to onlyOwner)
Unit tests (Foundry) — file: test/BurnFaucetTokensDrain.t.sol
Key tests (all PASS against the vulnerable code):
test_BurnOneWei_DrainsAll()
Call burnFaucetTokens(1)
Observed:
balanceOf(faucet) == 0 (full drain)
balanceOf(owner) == initialFaucetBalance - 1 (owner windfall)
totalSupply == initialTotalSupply - 1
test_SmallBurn_DrainsFaucet()
Call burnFaucetTokens(1_000 ether)
Observed: faucet drained; owner receives faucetBalance - 1_000 ether; totalSupply decreases by 1_000 ether.
test_AfterDrain_ClaimIsNonFunctional()
After a drain, claimFaucetTokens() reverts with RaiseBoxFaucet_InsufficientContractBalance.
testFuzz_AnyAmount_DrainsFaucet(uint256 amount)
For any valid amount in [1, faucetBalance], faucet drains to zero and owner receives the windfall.
Illustrative trace excerpts (from forge test -vvvv):
Reproduce locally:
Preferred fix — burn directly from the faucet contract
If a pre-transfer is strictly required by design, transfer only the amount to burn
Hardening (optional):
Emit event BurnFromFaucet(uint256 amount).
Use a dedicated role (e.g., BURN_MANAGER) rather than owner.
Add operational safeguards as needed (rate-limit, timelock, cap per tx).
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.