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.
function claimFaucetTokens() external {
if (dailyDrips < maxDailyDrips) {
(bool success, ) = msg.sender.call{value: faucetDrip}("");
require(success, "ETH transfer failed");
dailyDrips++;
}
}
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:
Setup: We set maxDailyDrips to 2 (only 2 ETH drips per day)
Day 1: User1 and User2 claim (dailyDrips = 2, limit reached)
Day 2: Counter resets, User1 claims again (gets ETH despite claiming yesterday)
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
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 {
vm.prank(user1);
faucet.claimFaucetTokens();
vm.prank(user2);
faucet.claimFaucetTokens();
assertEq(faucet.dailyDrips(), 2);
vm.prank(user3);
faucet.claimFaucetTokens();
assertEq(address(user3).balance, 0);
vm.warp(block.timestamp + 3 days);
vm.prank(user1);
faucet.claimFaucetTokens();
assertTrue(address(user1).balance > 0);
}
}
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;
}
}