Raisebox Faucet

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

# Inconsistent Daily Reset Logic Causes Denial of Token Claims

Description

The claimFaucetTokens function in the RaiseBoxFaucet contract uses inconsistent logic for resetting the daily claim counters for Sepolia ETH drips (dailyDrips) and token claims (dailyClaimCount). The ETH drip reset uses currentDay = block.timestamp / 24 hours, which resets dailyDrips based on a precise 24-hour boundary (Unix timestamp divided by 86400 seconds). In contrast, the token claim reset uses if (block.timestamp > lastFaucetDripDay + 1 days), which depends on the last claim timestamp (lastFaucetDripDay) plus a 24-hour period. This misalignment can cause the dailyClaimCount to not reset when expected, leading to a denial of service (DoS) where legitimate users are unable to claim tokens due to the dailyClaimLimit being reached prematurely.

Severity

Medium

Risk

The inconsistent reset logic can prevent users from claiming tokens on a new day if the lastFaucetDripDay + 1 days condition is not met, even though the ETH drip counter resets correctly. This disrupts the faucet’s functionality, especially in a testnet environment where users rely on timely token distribution for testing purposes. In a mainnet context, this could lead to user dissatisfaction and loss of trust in the protocol.

Impact

  • Denial of Service: Users may be unable to claim tokens due to dailyClaimCount not resetting, even when a new day has started for ETH drips.

  • Inconsistent Behavior: The mismatch between ETH and token reset logic creates confusion and unreliable faucet operation.

  • User Experience Degradation: Legitimate users are blocked from claiming tokens, impacting the faucet’s purpose of facilitating testnet interactions.

Tools Used

  • Manual code review

  • Foundry (for Proof of Concept testing)

Recommended Mitigation

  1. Unified Reset Logic: Use the same reset mechanism for both dailyDrips and dailyClaimCount. Adopt the currentDay = block.timestamp / 24 hours logic for both to ensure consistent daily resets.

  2. Update State Variables: Store the last reset day for both ETH and token claims in a single variable (e.g., lastResetDay) to avoid discrepancies.

  3. Documentation: Clearly document the reset logic to ensure developers understand the daily boundary behavior.

Modified Code Example:

function claimFaucetTokens() public {
faucetClaimer = msg.sender;
if (block.timestamp < (lastClaimTime[faucetClaimer] + CLAIM_COOLDOWN)) {
revert RaiseBoxFaucet_ClaimCooldownOn();
}
if (faucetClaimer == address(0) || faucetClaimer == address(this) || faucetClaimer == Ownable.owner()) {
revert RaiseBoxFaucet_OwnerOrZeroOrContractAddressCannotCallClaim();
}
if (balanceOf(address(this)) <= faucetDrip) {
revert RaiseBoxFaucet_InsufficientContractBalance();
}
if (dailyClaimCount >= dailyClaimLimit) {
revert RaiseBoxFaucet_DailyClaimLimitReached();
}
uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0;
dailyClaimCount = 0; // Unified reset for both counters
}
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
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"
);
}
}
lastClaimTime[faucetClaimer] = block.timestamp;
dailyClaimCount++;
_transfer(address(this), faucetClaimer, faucetDrip);
emit Claimed(msg.sender, faucetDrip);
}

Proof of Concept

The following Foundry test demonstrates the bug by showing how the inconsistent reset logic prevents a user from claiming tokens on a new day due to dailyClaimCount not resetting.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "forge-std/Test.sol";
import "../src/RaiseBoxFaucet.sol";
contract RaiseBoxFaucetTest is Test {
RaiseBoxFaucet faucet;
address owner = address(0x1);
address claimer1 = address(0x2);
address claimer2 = address(0x3);
address claimer3 = address(0x4);
function setUp() public {
vm.startPrank(owner);
faucet = new RaiseBoxFaucet("Faucet Token", "FTK", 1000 * 10**18, 0.005 ether, 0.01 ether);
vm.deal(address(faucet), 1 ether); // Fund contract with 1 ETH
vm.stopPrank();
}
function testInconsistentDailyResetLogic() public {
// Set dailyClaimLimit to 2 for testing
vm.prank(owner);
faucet.adjustDailyClaimLimit(98, false); // Set dailyClaimLimit to 2 (100 - 98 = 2)
// Claimer1 claims tokens at 3.5 days (to pass initial cooldown check)
vm.warp(302400); // 3 days + 12 hours = 259200 + 43200 = 302400 seconds, currentDay = 3
vm.startPrank(claimer1);
faucet.claimFaucetTokens(); // First claim, dailyClaimCount = 1
assertEq(faucet.dailyClaimCount(), 1);
assertEq(faucet.lastFaucetDripDay(), 302400);
assertEq(faucet.lastDripDay(), 3); // currentDay = 302400 / 86400 = 3
assertEq(faucet.dailyDrips(), 0.005 ether); // ETH dripped
vm.stopPrank();
// Move to 4 days + 1 minute (new day for ETH drip)
vm.warp(345660); // 4 days + 1 minute = 345600 + 60 = 345660 seconds, currentDay = 4
uint256 currentDay = block.timestamp / (24 hours);
assertEq(currentDay, 4); // New day for ETH drip reset
// Claimer2 claims: passes limit check (1 < 2), resets ETH drip, but not token count
vm.startPrank(claimer2);
faucet.claimFaucetTokens(); // Succeeds, dailyClaimCount++ to 2, ETH resets
assertEq(faucet.dailyClaimCount(), 2);
assertEq(faucet.lastDripDay(), 4); // ETH reset happened
assertEq(faucet.dailyDrips(), 0.005 ether); // New ETH drip for claimer2
vm.stopPrank();
// Claimer3 attempts to claim: fails due to dailyClaimLimitReached (count=2 >=2)
vm.startPrank(claimer3);
vm.expectRevert(RaiseBoxFaucet.RaiseBoxFaucet_DailyClaimLimitReached.selector);
faucet.claimFaucetTokens(); // Fails, demonstrating DoS
vm.stopPrank();
// Verify no reset for token count, but ETH was reset
assertEq(faucet.dailyClaimCount(), 2); // Not reset
assertEq(faucet.lastDripDay(), 4); // ETH reset
assertEq(faucet.dailyDrips(), 0.005 ether); // ETH drip after reset
}
}

Explanation of PoC:

  • The test deploys the RaiseBoxFaucet contract with a dailyClaimLimit of 2 for simplicity.

  • claimer1 claims tokens at 3.5 days (timestamp: 302400 seconds, currentDay = 3), setting dailyClaimCount = 1, lastFaucetDripDay = 302400, and lastDripDay = 3.

  • The time is warped to 4 days + 1 minute (timestamp: 345660 seconds, currentDay = 4), which is a new day for ETH drips, so during claimer2's claim, dailyDrips resets to 0 and lastDripDay updates to 4.

  • However, the token reset condition (block.timestamp > lastFaucetDripDay + 1 days = 345660 > 302400 + 86400 = 388800) is false, so dailyClaimCount is not reset and increments to 2.

  • claimer3 attempts to claim but fails due to RaiseBoxFaucet_DailyClaimLimitReached (since dailyClaimCount = 2 >= 2), demonstrating the DoS: on a new day, the token count carries over, blocking additional claims even though ETH resets correctly. If the logic was consistent, the count would reset, allowing more claims.

Updates

Lead Judging Commences

inallhonesty Lead Judge 19 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Inconsistent day calculation methods cause desynchronization between ETH and token daily resets.

Support

FAQs

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