Raisebox Faucet

First Flight #50
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: high
Valid

Daily ETH Cap is Bypass-able, Allowing Complete Drain of Faucet's ETH

Root + Impact

Description

  • The contract is designed to distribute a small amount of Sepolia ETH to first-time claimers, with a strict daily total limit defined by dailySepEthCap. The dailyDrips variable is supposed to track the amount of ETH dripped each day to enforce this cap.

  • When a user who has already claimed ETH calls the function, the **dailyDrips **counter is incorrectly reset to zero.

  • A malicious actor can exploit this by using one "repeat claimer" account to reset the daily ETH counter between claims from multiple "new claimer" accounts, completely bypassing the **dailySepEthCap **and draining all ETH from the contract.

// Root cause in the codebase with @> marks to highlight the relevant section
function claimFaucetTokens() public {
// ...
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
// ... logic to drip ETH for first-time claimers
}
@> else {
@> dailyDrips = 0;
@> }
//...
}

Risk

Likelihood:

  • Reason 1: An attacker can execute this exploit with as few as two addresses and a small amount of gas.

  • Reason 2: The exploit is deterministic and can be repeated in a single transaction block until the contract's entire ETH balance is depleted.

Impact:

  • Impact 1: Total and direct loss of all ETH funds held within the faucet contract.

  • Impact 2: The feature designed to help new users with gas fees is rendered useless, undermining a key purpose of the faucet.

Proof of Concept

A Foundry test can simulate this attack pattern to prove the vulnerability. The test uses one "repeat claimer" account to reset the daily ETH counter for multiple "new claimer" accounts.

function test_drainEthByBypassingDailyCap() public {
// Setup: Fund faucet with 0.1 ETH. Set daily cap to 0.02 ETH and drip to 0.01 ETH.
vm.deal(address(faucet), 0.1 ether);
// Pre-condition: Make user1 a "repeat claimer" by having them claim once and fast-forwarding time.
vm.prank(user1);
faucet.claimFaucetTokens();
vm.warp(block.timestamp + 4 days); // Move past 3-day cooldown
// The daily cap (0.02 ETH) should only allow two claims.
// 1. First new account claims 0.01 ETH. dailyDrips is now 0.01 ether.
vm.prank(attacker1);
faucet.claimFaucetTokens();
assertEq(faucet.dailyDrips(), 0.01 ether);
// 2. Repeat claimer calls the function. This resets dailyDrips to 0.
vm.prank(user1);
faucet.claimFaucetTokens();
assertEq(faucet.dailyDrips(), 0);
// 3. Second new account claims 0.01 ETH. The check passes because dailyDrips was reset.
vm.prank(attacker2);
faucet.claimFaucetTokens();
assertEq(faucet.dailyDrips(), 0.01 ether);
// 4. Attacker proves a third claim is possible, exceeding the daily cap.
vm.prank(user1); // Reset again
faucet.claimFaucetTokens();
vm.prank(attacker3); // Third new account
faucet.claimFaucetTokens();
// The faucet has now given out 0.03 ETH, exceeding its 0.02 ETH daily cap.
assertLt(address(faucet).balance, 0.08 ether);
}

Recommended Mitigation

The only change required is to remove the vulnerable else block. By removing it, the intended daily reset logic, which is already present, can function securely.

function claimFaucetTokens() public {
// ...
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0;
}
if (dailyDrips + sepEthAmountToDrip <= dailySepEthCap && address(this).balance >= sepEthAmountToDrip) {
// ... (rest of the drip logic remains the same)
} else {
// ... (emit SepEthDripSkipped event)
}
}
- else {
- dailyDrips = 0;
- }
// ...
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 5 days ago
Submission Judgement Published
Validated
Assigned finding tags:

dailyDrips Reset Bug

Support

FAQs

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