Raisebox Faucet

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

dailyDrips Reset Logic Flaw

Root + Impact

Description

  • Normal Behavior: The dailyDrips counter should track the total Sepolia ETH distributed daily and reset to 0 only at the start of a new day to ensure daily ETH distribution does not exceed the dailySepEthCap limit. RaiseBoxFaucet.sol:185-191

  • Vulnerability Issue: When non-first-time claimers call the function or when SEP ETH drips are paused, the dailyDrips counter is incorrectly reset to 0, completely breaking the daily ETH cap tracking mechanism. RaiseBoxFaucet.sol:184-212

// Root cause in the codebase with @> marks to highlight the relevant section
// src/RaiseBoxFaucet.sol:185-213
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0;
// dailyClaimCount = 0;
}
if (dailyDrips + sepEthAmountToDrip <= dailySepEthCap && address(this).balance >= sepEthAmountToDrip) {
hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
if (success) {
emit SepEthDripped(faucetClaimer, sepEthAmountToDrip);
} else {
revert RaiseBoxFaucet_EthTransferFailed();
}
} else {
emit SepEthDripSkipped(
faucetClaimer,
address(this).balance < sepEthAmountToDrip ? "Faucet out of ETH" : "Daily ETH cap reached"
);
}
} else {
@> dailyDrips = 0; // Incorrect reset logic
}

Risk

Likelihood:

  • Triggers every time a user who has already claimed ETH calls claimFaucetTokens() (can claim tokens every 3 days)

  • Triggers when SEP ETH drips are paused and any user attempts to claim

Impact:

  • Daily ETH cap mechanism completely fails, allowing unlimited first-time users to claim ETH on the same day, far exceeding the dailySepEthCap limit

  • Contract's Sepolia ETH resources will be rapidly depleted, preventing subsequent first-time users from receiving gas fee subsidies

Proof of Concept

function testDailyDripsResetVulnerability() public {
// Setup: Fund contract with ETH for drips
vm.deal(address(raiseBoxFaucet), 10 ether);
vm.prank(owner);
// Record initial state
uint256 initialDailyDrips = raiseBoxFaucet.dailyDrips();
assertEq(initialDailyDrips, 0, "Initial dailyDrips should be 0");
// Step 1: First-time user1 claims (receives ETH)
vm.prank(user1);
raiseBoxFaucet.claimFaucetTokens();
uint256 dailyDripsAfterUser1 = raiseBoxFaucet.dailyDrips();
assertEq(dailyDripsAfterUser1, 0.005 ether, "dailyDrips should be 0.005 after first claim");
// Step 2: First-time user2 claims (receives ETH)
vm.prank(user2);
raiseBoxFaucet.claimFaucetTokens();
uint256 dailyDripsAfterUser2 = raiseBoxFaucet.dailyDrips();
assertEq(dailyDripsAfterUser2, 0.01 ether, "dailyDrips should be 0.01 after second claim");
// Step 3: VULNERABILITY - User1 claims again (NOT first-time anymore)
// This should NOT reset dailyDrips, but it does!
vm.warp(block.timestamp + 3 days); // Wait for cooldown
vm.prank(user3);
raiseBoxFaucet.claimFaucetTokens();
uint256 dailyDripsAfterUser3 = raiseBoxFaucet.dailyDrips();
assertEq(dailyDripsAfterUser3, 0.005 ether, "after cooldown dailyDrips should be 0.005 after first claim");
vm.prank(user1);
raiseBoxFaucet.claimFaucetTokens();
// PROOF OF VULNERABILITY: dailyDrips is reset to 0!
uint256 dailyDripsAfterUser1SecondClaim = raiseBoxFaucet.dailyDrips();
assertEq(dailyDripsAfterUser1SecondClaim, 0, "BUG: dailyDrips reset to 0!");
}
}
Traces:
├─ [150754] RaiseBoxFaucet::claimFaucetTokens()
│ ├─ [0] user3::fallback{value: 5000000000000000}()
│ │ └─ ← [Stop]
│ ├─ emit SepEthDripped(claimant: user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], amount: 5000000000000000 [5e15])
│ ├─ emit Transfer(from: RaiseBoxFaucet: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], to: user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], value: 1000000000000000000000 [1e21])
│ ├─ emit Claimed(user: user3: [0xc0A55e2205B289a967823662B841Bd67Aa362Aec], amount: 1000000000000000000000 [1e21])
│ └─ ← [Stop]
├─ [536] RaiseBoxFaucet::dailyDrips() [staticcall]
│ └─ ← [Return] 5000000000000000 [5e15]
├─ [0] VM::assertEq(5000000000000000 [5e15], 5000000000000000 [5e15], "after cooldown dailyDrips should be 0.005 after first claim") [staticcall]
│ └─ ← [Return]
├─ [0] VM::prank(user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF])
│ └─ ← [Return]
├─ [8631] RaiseBoxFaucet::claimFaucetTokens()
│ ├─ emit Transfer(from: RaiseBoxFaucet: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], to: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], value: 1000000000000000000000 [1e21])
│ ├─ emit Claimed(user: user1: [0x29E3b139f4393aDda86303fcdAa35F60Bb7092bF], amount: 1000000000000000000000 [1e21])
│ └─ ← [Stop]
├─ [536] RaiseBoxFaucet::dailyDrips() [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::assertEq(0, 0, "BUG: dailyDrips reset to 0!") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.95ms (356.69µs CPU time)

Recommended Mitigation

- remove this code
+ add this code
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) {
hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
if (success) {
emit SepEthDripped(faucetClaimer, sepEthAmountToDrip);
} else {
revert RaiseBoxFaucet_EthTransferFailed();
}
} else {
emit SepEthDripSkipped(
faucetClaimer,
address(this).balance < sepEthAmountToDrip ? "Faucet out of ETH" : "Daily ETH cap reached"
);
}
-} else {
- dailyDrips = 0;
}
Updates

Lead Judging Commences

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