Root + Impact
Description:
The burnFaucetTokens function is intended to burn a specified amount of faucet tokens held by the contract. Instead, it first transfers the entire token balance from the contract to the owner, and only then burns amountToBurn from the owner’s balance. This leaves the contract with zero tokens, preventing future claims.
As a result, claimFaucetTokens() will revert with InsufficientContractBalance after any call to burnFaucetTokens, even if the intent was to burn just a small portion.
function burnFaucetTokens(uint256 amountToBurn) public onlyOwner {
require(amountToBurn <= balanceOf(address(this)), "Faucet Token Balance: Insufficient");
_transfer(address(this), msg.sender, balanceOf(address(this))); <@ Transfers the entire contract balance to the owner
_burn(msg.sender, amountToBurn);
}
Risk
Likelihood:
-
When the owner performs a normal burn with a small amountToBurn, the function transfers all faucet tokens to the owner first, draining the faucet and halting claims.
-
Scheduled/admin scripts that call the burn will repeatedly drain the faucet’s balance, causing recurring outages even without operator mistakes.
Impact:
Users can no longer claim tokens (claimFaucetTokens reverts due to empty faucet).
Proof of Concept (Foundry Test)
Paste this test function in your test file and run:
forge test -mt testBurnTransfersAllInsteadOfAmountToBurn -vvvv
function testBurnTransfersAllInsteadOfAmountToBurn() public {
uint256 initialContractBalance = raiseBoxFaucet.getBalance(address(raiseBoxFaucet));
uint256 amountToBurn = 2 * 10 ** 18;
uint256 ownerBalanceBefore = raiseBoxFaucet.getBalance(owner);
vm.startPrank(owner);
raiseBoxFaucet.burnFaucetTokens(amountToBurn);
vm.stopPrank();
uint256 contractBalanceAfter = raiseBoxFaucet.getBalance(address(raiseBoxFaucet));
uint256 ownerBalanceAfter = raiseBoxFaucet.getBalance(owner);
assertEq(contractBalanceAfter, 0, "Contract balance should be 0 (drained)");
assertEq(
ownerBalanceAfter,
ownerBalanceBefore + initialContractBalance - amountToBurn,
"Owner did not receive full faucet balance minus burned amount"
);
vm.prank(user1);
vm.expectRevert();
raiseBoxFaucet.claimFaucetTokens();
}
Recommended Mitigation
Fix 1 --> Transfer the desired amount to the owner, and burn only what you intend.
Fix 2 --> Don’t transfer anything to the owner. Burn directly from the contract’s balance.
Fix 1:
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);
}
Fix 2:
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);
}