Raisebox Faucet

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

Deposited ETH becomes permanently locked

Root + Impact

ETH sent to the faucet becomes permanently locked because no owner-only withdrawal path exists, creating stranded capital risk.

Description

  • The faucet contract accepts ETH via receive() and fallback() so it can top up the drip balance and absorb donations.

  • Without a withdrawal routine, excess ETH (or accidental transfers) cannot be recovered, so pausing the drip or exceeding demand traps value indefinitely.

@>receive() external payable {
@> emit DonationReceived(msg.sender, msg.value);
}
@>fallback() external payable {
@> emit DonationReceived(msg.sender, msg.value);
}

Risk

Likelihood:

  • Top-ups and donations are part of normal maintenance, so ETH regularly accumulates beyond immediate drip needs.

  • Operational incidents (pause toggles, parameter mistakes) happen, leaving ETH unused with no manual escape hatch.

Impact:

  • The project’s treasury or community donors lose access to funds, and a redeploy becomes the only way to recover them.

  • Locked ETH inflates the apparent faucet liability, skewing accounting and potentially triggering insolvency concerns.

Proof of Concept

Depositing ETH and then attempting to retrieve it in the PoC highlights that the contract exposes no withdrawal path.

// Owner deposits ETH for future drips
payable(address(faucet)).transfer(100 ether);
// Later, sepEthDripsPaused = true; but there is no method to recover the unused 100 ether.

Recommended Mitigation

Adding the guarded withdrawal function gives operators a safe way to reclaim unused ETH without jeopardizing daily drips.

/// @notice Pauses or unpauses Sepolia ETH drips
/// @param _paused True to pause, false to resume
function toggleEthDripPause(bool _paused) external onlyOwner {
sepEthDripsPaused = _paused;
emit SepEthDripsPaused(_paused);
}
+ /// @notice Allows owner to withdraw excess ETH from the contract
+ /// @dev Ensures enough ETH remains for the daily drip cap to prevent disruption
+ /// @param amount Amount of ETH to withdraw
+ function withdrawExcessEth(uint256 amount) external onlyOwner {
+ require(amount > 0, "Amount must be greater than 0");
+ require(address(this).balance >= amount, "Insufficient contract balance");
+
+ // Safety check: ensure enough ETH remains for at least one day of drips
+ // This prevents owner from withdrawing ETH needed for pending claims
+ uint256 remainingBalance = address(this).balance - amount;
+ require(remainingBalance >= dailySepEthCap, "Must leave enough ETH for daily cap");
+
+ (bool success,) = payable(owner()).call{value: amount}("");
+ require(success, "ETH transfer failed");
+ }
Updates

Lead Judging Commences

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