Raisebox Faucet

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

burnFaucetTokens() transfers entire contract balance instead of sending the amountToBurn, breaking faucet functionality

Author Revealed upon completion

The burnFaucetTokens() function transfers the entire contract token balance to the owner before burning, causing the contract balance to become 0 and breaking the faucet logic if a person tries to claim tokens from faucet.

Description

  • burnFaucetTokens(uint256 amountToBurn) should allow the owner to burn a specific number of faucet tokens while keeping the remaining tokens in the contract available for users to claim.

  • Currently, the function transfers the entire contract balance to the owner before burning, even if only a fraction of tokens is intended to be burned. This sets the contract balance to 0, causing subsequent user claims to fail and breaking the faucet’s core functionality.

// Root cause in the codebase with @> marks to highlight the relevant section
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
/*
@note : Why sending the entire balance of the contract to the owner, just send the amountToBurn
*/
@> _transfer(address(this), msg.sender, balanceOf(address(this)));
_burn(msg.sender, amountToBurn);
}

Risk

Likelihood:

  • Occurs whenever the owner tries to burn any portion of the faucet tokens while leaving some tokens in the contract.

  • Likely in normal faucet management operations.

Impact:

  • Users cannot claim faucet tokens, breaking the core functionality.

  • May cause temporary disruption in token distribution and user experience.

  • Could lead to loss of trust in the contract if claims fail unexpectedly.

Proof of Concept

This PoC demonstrates that when owner call burnFaucetTokens() all the balance is send to the owner's address and when a llegitimate user tries to claim tokens from the contract he/she isn't able to.

function testBurnTokens() public {
vm.prank(user1);
raiseBoxFaucet.claimFaucetTokens();
//assertEq(raiseBoxFaucet.balanceOf(user1), 1000 * 10 ** 18);
uint current_supply = raiseBoxFaucet.getFaucetTotalSupply();
vm.prank(owner);
raiseBoxFaucet.burnFaucetTokens(1000 * 10 ** 18);
console.log("The total Supplyy of the faucet now is: ", raiseBoxFaucet.getFaucetTotalSupply());
vm.prank(user2);
raiseBoxFaucet.claimFaucetTokens();
}
// This is the output of the test
Traces:
[297834] TestRaiseBoxFaucet::testBurnTokens()
├─ [0] VM::prank(user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF])
│ └─ ← [Return]
├─ [237354] RaiseBoxFaucet::claimFaucetTokens()
│ ├─ [0] user1::fallback{value: 50000000000000000}()
│ │ └─ ← [Stop]
│ ├─ emit SepEthDripped(claimant: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], amount: 50000000000000000 [5e16])
│ ├─ emit Transfer(from: RaiseBoxFaucet: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], to: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], value: 1000000000000000000000 [1e21])
│ ├─ emit Claimed(user: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], amount: 1000000000000000000000 [1e21])
│ └─ ← [Stop]
├─ [605] RaiseBoxFaucet::getFaucetTotalSupply() [staticcall]
│ └─ ← [Return] 999999000000000000000000000 [9.999e26]
├─ [0] VM::prank(TestRaiseBoxFaucet: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
│ └─ ← [Return]
├─ [33698] RaiseBoxFaucet::burnFaucetTokens(1000000000000000000000 [1e21])
│ ├─ emit Transfer(from: RaiseBoxFaucet: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], to: TestRaiseBoxFaucet: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], value: 999999000000000000000000000 [9.999e26])
│ ├─ emit Transfer(from: TestRaiseBoxFaucet: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], to: 0x0000000000000000000000000000000000000000, value: 1000000000000000000000 [1e21])
│ └─ ← [Stop]
├─ [605] RaiseBoxFaucet::getFaucetTotalSupply() [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log(0) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::prank(user2: [0x537C8f3d3E18dF5517a58B3fB9D9143697996802])
│ └─ ← [Return]
├─ [4106] RaiseBoxFaucet::claimFaucetTokens()
│ └─ ← [Revert] RaiseBoxFaucet_InsufficientContractBalance()
└─ ← [Revert] RaiseBoxFaucet_InsufficientContractBalance()
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 2.50ms (245.90µs CPU time)
Ran 1 test suite in 18.24ms (2.50ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/RaiseBoxFaucet.t.sol:TestRaiseBoxFaucet
[FAIL: RaiseBoxFaucet_InsufficientContractBalance()] testBurnTokens() (gas: 297834)
Encountered a total of 1 failing tests, 0 tests succeeded
// This is the console ouput
The total Supplyy of the faucet now is: 0

Recommended Mitigation

Well there can be 2 mitigations and you can choose either one

  1. Burn the tokens from the contract itself

  2. Send only the amountToBurn() to the owner's address

// The 1st mitigation can be like
- _transfer(address(this), msg.sender, balanceOf(address(this)));
- _burn(msg.sender, amountToBurn);
+ _burn(address(this),amountToBurn);
// The 2nd mitigation can be
- _transfer(address(this), msg.sender, balanceOf(address(this)));
+ _transfer(address(this), msg.sender, amountToBurn);
Updates

Lead Judging Commences

inallhonesty Lead Judge 1 day 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.