Raisebox Faucet

First Flight #50
Beginner FriendlySolidity
100 EXP
Submission Details
Impact: high
Likelihood: high

Mint–Burn exploit enables owner to seize almost the entire token supply burning 1 single token.

Author Revealed upon completion

By exploiting the ability to mint tokens until the supply reaches type(uint256).max and then burning 1 token to take control of almost the entire supply, it can make the faucet unusable forever.

Description

  • The contract should allow the owner to burn tokens held by the contract by directly burning them from the contract's balance.

  • The problem is that the burnFaucetTokens function accepts a parameter that defines the amount of tokens to burn, then sends the entire balance of the contract to the owner and burns from the owner's balance the amount of tokens specified by the parameter, regardless of the size of the balance that was sent from the contract to the owner.

  • Owner can mint almost the entire supply (type(uint256).max - tokens held by users) to the contract, call the burn function and take control of the entire contract token balance. The contract remains with 0 tokens and cannot distribute through the faucet function.

// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood:

  • Reason 1 It can happen at any moment the owner decides to take almost full control of the tokens.

  • Reason 2

Impact:

  • Impact 1 The contract becomes non-functional, as any call to claimFaucetTokens reverts due to the contract having an insufficient balance.

  • Impact 2

Proof of Concept

1 - owner burn 1 token to avoid revert on the require balance < 1000 * 10 ** 18.
2- mint all the possible tokens (max uint256 - totalsupply) to exclude tokens from other users

3- burn 1 token and get all the balance contract (almost the entire supply), now the balance contract is 0 and no other tokens can be minted

4- users try to use the faucet but it will always revert due to insufficient balance

function testOwnerCanClaimAlmostAllTheSupplyBurningOneToken() public {
vm.startPrank(owner);
//burn 1 token to avoid revert on the require balance < 1000 * 10 ** 18
raiseBoxFaucet.burnFaucetTokens(1);
uint256 allTokenMintable = type(uint256).max - raiseBoxFaucet.totalSupply();
uint256 initialOwnerBalance = raiseBoxFaucet.balanceOf(owner);
//mint all possible tokens
raiseBoxFaucet.mintFaucetTokens(address(raiseBoxFaucet), allTokenMintable);
//burn 1 token and transfer all the token on owner balance
raiseBoxFaucet.burnFaucetTokens(1);
console.log("owner Balance: ", raiseBoxFaucet.balanceOf(owner));
console.log("result: ", initialOwnerBalance + allTokenMintable -1);
assertEq(raiseBoxFaucet.balanceOf(owner), initialOwnerBalance + allTokenMintable -1);
vm.prank(user1);
vm.expectRevert();
raiseBoxFaucet.claimFaucetTokens();
}

Recommended Mitigation

  • To mitigate this issue it is necessary to prevent transferring the tokens from the contract to the owner before the burn. Inside _burn replace msg.sender with address(this) and the tokens will be burned directly in the contract so the owner cannot seize them.

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)));
+ _burn(address(this), amountToBurn);
}

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.