Raisebox Faucet

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

[L-01] - Global daily counter in `claimFaucetTokens` allows limit bypass

Root + Impact

Description

The claimFaucetTokens function implements a daily ETH drip limit using a global dailyDrips counter. Users should only be able to receive ETH drips up to the maxDailyDrips limit per day. However, the counter resets incorrectly, allowing users who claimed before the reset to bypass the daily limit.

The specific issue is that dailyDrips is a global counter that increments for all users but doesn't properly track per-user claims across day boundaries, allowing repeat claimers to receive ETH even after the daily limit is reached.

// Root cause in the codebase
function claimFaucetTokens() external {
// ... token transfer ...
if (dailyDrips < maxDailyDrips) { // @> Global counter, not per-user
(bool success, ) = msg.sender.call{value: faucetDrip}("");
require(success, "ETH transfer failed");
dailyDrips++; // @> Increments for all users
}
}

Risk

Likelihood:

  • Occurs naturally during normal operation

  • Affects users who claim across day boundaries

  • No special conditions required

  • Common scenario in daily usage

Impact:

  • Daily ETH limit can be bypassed

  • Unfair distribution of ETH rewards

  • Some users receive more ETH than intended

  • Protocol economics affected

Proof of Concept

This test demonstrates how the daily limit can be bypassed:

  1. Setup: We set maxDailyDrips to 2 (only 2 ETH drips per day)

  2. Day 1: User1 and User2 claim (dailyDrips = 2, limit reached)

  3. Day 2: Counter resets, User1 claims again (gets ETH despite claiming yesterday)

  4. Result: User1 received ETH on both days, bypassing the intent of daily limits

The issue occurs because:

  • dailyDrips is a global counter, not per-user

  • Counter resets at day boundary

  • Users who claimed before reset can claim again

  • No tracking of which users already received ETH that day

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {RaiseBoxFaucet} from "../src/RaiseBoxFaucet.sol";
import {RaiseBoxToken} from "../src/RaiseBoxToken.sol";
contract DailyDripsResetTest is Test {
RaiseBoxFaucet faucet;
RaiseBoxToken token;
address owner = makeAddr("owner");
address user1 = makeAddr("user1");
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
function setUp() public {
vm.startPrank(owner);
token = new RaiseBoxToken();
faucet = new RaiseBoxFaucet(address(token));
token.mintFaucetTokens(address(faucet), 1_000_000 * 10**18);
vm.deal(address(faucet), 100 ether);
vm.stopPrank();
}
function testDailyLimitBypass() public {
// Day 1: User1 and User2 claim (reach daily limit)
vm.prank(user1);
faucet.claimFaucetTokens();
vm.prank(user2);
faucet.claimFaucetTokens();
assertEq(faucet.dailyDrips(), 2); // Daily limit reached
// User3 cannot claim (limit reached)
vm.prank(user3);
faucet.claimFaucetTokens();
assertEq(address(user3).balance, 0); // No ETH received
// Day 2: Wait 3 days for cooldown
vm.warp(block.timestamp + 3 days);
// Counter resets, User1 can claim again
vm.prank(user1);
faucet.claimFaucetTokens();
// User1 received ETH on both Day 1 and Day 2
// This bypasses the daily limit intent
assertTrue(address(user1).balance > 0);
// The issue: dailyDrips resets but doesn't prevent
// users who already claimed from claiming again
}
}

Recommended Mitigation

Implement per-user daily tracking:

+ mapping(address => uint256) public lastETHClaimDay;
function claimFaucetTokens() external {
// ... token transfer ...
+ uint256 currentDay = block.timestamp / 1 days;
+
+ // Check if user already claimed ETH today
+ require(
+ currentDay > lastETHClaimDay[msg.sender],
+ "Already claimed ETH today"
+ );
if (dailyDrips < maxDailyDrips) {
(bool success, ) = msg.sender.call{value: faucetDrip}("");
require(success, "ETH transfer failed");
dailyDrips++;
+ lastETHClaimDay[msg.sender] = currentDay;
}
}

Or use a more robust daily limit system:

+ mapping(uint256 => uint256) public dripsPerDay; // day => count
+ mapping(address => mapping(uint256 => bool)) public hasClaimedETHToday;
function claimFaucetTokens() external {
// ... token transfer ...
+ uint256 currentDay = block.timestamp / 1 days;
+
+ require(
+ !hasClaimedETHToday[msg.sender][currentDay],
+ "Already claimed ETH today"
+ );
- if (dailyDrips < maxDailyDrips) {
+ if (dripsPerDay[currentDay] < maxDailyDrips) {
(bool success, ) = msg.sender.call{value: faucetDrip}("");
require(success, "ETH transfer failed");
- dailyDrips++;
+ dripsPerDay[currentDay]++;
+ hasClaimedETHToday[msg.sender][currentDay] = true;
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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