Description
The burnFaucetTokens() function contains a critical logic error where it transfers the entire contract token balance to the owner but only burns the specified amountToBurn. This mismatch can lead to unintended token accumulation in the owner's account or confusion about the actual circulating supply.
Expected Behavior
The function should transfer only the amountToBurn to the owner and then burn exactly that amount, maintaining consistency between transfer and burn operations.
Actual Behavior
The function transfers balanceOf(address(this)) (the entire contract balance) to the owner but only burns amountToBurn, which may be less than the transferred amount. This creates a discrepancy where tokens are removed from the contract but not all of them are burned.
Root Cause
On line 132, the function uses balanceOf(address(this)) to transfer all tokens instead of using the amountToBurn parameter:
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);
}
Scenario Example:
-
Contract has 10,000 tokens
-
Owner calls burnFaucetTokens(5,000)
-
Function transfers 10,000 tokens to owner
-
Function burns 5,000 tokens from owner
-
Result: Owner keeps 5,000 tokens that should have remained in the contract
Risk Assessment
Impact
If exploited or misused:
Token supply manipulation: Owner can extract tokens from the faucet without properly burning them
Faucet depletion: Contract loses all tokens even when only partial burn was intended
Supply accounting errors: Total supply tracking becomes inconsistent
Loss of faucet functionality: After calling this function, the faucet has zero tokens regardless of amountToBurn value
Likelihood
Medium likelihood because:
-
Requires owner to call the function (privileged access)
-
The function is likely to be called during normal operations (refilling/managing supply)
-
The bug will manifest every time the function is called with amountToBurn < balanceOf(address(this))
-
No checks prevent the inconsistent behavior
Proof of Concept
pragma solidity ^0.8.30;
import "forge-std/Test.sol";
import "../src/RaiseBoxFaucet.sol";
contract BurnLogicExploitTest is Test {
RaiseBoxFaucet faucet;
address owner;
function setUp() public {
owner = address(this);
faucet = new RaiseBoxFaucet(
"raiseboxtoken",
"RB",
1000 * 10 ** 18,
0.005 ether,
0.5 ether
);
}
function testBurnLogicError() public {
uint256 initialContractBalance = faucet.balanceOf(address(faucet));
uint256 initialOwnerBalance = faucet.balanceOf(owner);
console.log("Initial contract balance:", initialContractBalance / 1e18, "tokens");
console.log("Initial owner balance:", initialOwnerBalance / 1e18, "tokens");
uint256 amountToBurn = initialContractBalance / 2;
console.log("Amount owner wants to burn:", amountToBurn / 1e18, "tokens");
faucet.burnFaucetTokens(amountToBurn);
uint256 finalContractBalance = faucet.balanceOf(address(faucet));
uint256 finalOwnerBalance = faucet.balanceOf(owner);
uint256 totalSupply = faucet.totalSupply();
console.log("Final contract balance:", finalContractBalance / 1e18, "tokens");
console.log("Final owner balance:", finalOwnerBalance / 1e18, "tokens");
console.log("Total supply after burn:", totalSupply / 1e18, "tokens");
console.log("");
console.log("BUG: Owner wanted to burn 500M but received them instead!");
assertEq(finalContractBalance, 0, "Contract should have 0 tokens (BUG: all transferred)");
assertEq(finalOwnerBalance, amountToBurn, "Owner has tokens that weren't burned");
assertGt(finalOwnerBalance, 0, "Owner received tokens instead of burning them all");
assertTrue(finalContractBalance == 0, "Faucet is completely drained");
assertTrue(finalOwnerBalance > 0, "Owner has leftover tokens");
}
}
Console Output
Initial contract balance: 1000000000 tokens
Initial owner balance: 0 tokens
Amount owner wants to burn: 500000000 tokens
Final contract balance: 0 tokens
Final owner balance: 500000000 tokens
Total supply after burn: 500000000 tokens
BUG: Owner wanted to burn 500M but received them instead!
ISSUE: Owner wanted to burn 500M tokens but received them instead!
- Expected: Contract keeps 500M, burns 500M → Total supply = 500M
- Actual: Owner receives 1B, burns 500M → Owner keeps 500M unburned tokens
Recommended Mitigation
Fix the function to only transfer the amountToBurn instead of the entire balance:
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);
}
function burnFaucetTokens(uint256 amountToBurn) public onlyOwner {
require(amountToBurn <= balanceOf(address(this)), "Faucet Token Balance: Insufficient");
_transfer(address(this), msg.sender, amountToBurn);
_burn(msg.sender, amountToBurn);
}
Explanation
The fix ensures that:
Consistency: Only the amount intended to be burned is transferred to the owner
Predictable behavior: The contract retains any tokens not being burned
Correct accounting: Total supply decreases by exactly amountToBurn
Faucet preservation: The faucet can continue operating with remaining tokens after a partial burn