Raisebox Faucet

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

Inconsistent Daily Reset Logic

Author Revealed upon completion

Description:
The contract uses two different mechanisms for daily resets: lastDripDay for ETH drips and lastFaucetDripDay for claim counts. This creates timing inconsistencies and potential manipulation opportunities.

Impact:
Users might exploit timing differences to bypass daily limits or encounter unexpected behavior when limits reset at different times.

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;
function advanceBlockTime(uint256 duration_) internal {
vm.warp(duration_);
}
function setUp() public {
owner = makeAddr("owner");
user1 = makeAddr("user1");
user2 = makeAddr("user2");
vm.prank(owner);
faucet = new RaiseBoxFaucet(
"RaiseBoxToken",
"RBT",
1000 * 10**18, // 1000 tokens per claim
0.005 ether, // 0.005 ETH per first claim
1 ether // 1 ETH daily cap
);
vm.deal(address(faucet), 10 ether);
advanceBlockTime(3 days);
}
function test_InconsistentDailyReset_ETHResetsAtMidnight_ButTokensDoNot() public {
// 1) Reduce token daily limit to 1 (default is 100)
vm.prank(owner);
faucet.adjustDailyClaimLimit(99, false); // 100 -> 1
// Align to the start of "today" and then warp to 23:59:00
uint256 dayStart = (block.timestamp / 1 days) * 1 days;
uint256 justBeforeMidnight = dayStart + 1 days - 60; // 23:59:00
vm.warp(justBeforeMidnight);
// 2) user1 consumes the only allowed token claim at 23:59
uint256 ethDrip = faucet.sepEthAmountToDrip(); // 0.005 ether by default
uint256 preUser1Eth = user1.balance;
vm.prank(user1);
faucet.claimFaucetTokens();
// Token daily count reached 1
assertEq(faucet.dailyClaimCount(), 1, "token daily count should be 1 after late claim");
// ETH daily accounting increased by one drip
assertEq(user1.balance - preUser1Eth, ethDrip, "user1 received the first-time ETH drip");
// 3) New calendar day (00:00:01), but token claim still reverts (reset hasn't run yet)
uint256 justAfterMidnight = dayStart + 1 days + 1; // 00:00:01 next day
vm.warp(justAfterMidnight);
vm.expectRevert(RaiseBoxFaucet.RaiseBoxFaucet_DailyClaimLimitReached.selector);
vm.prank(user2);
faucet.claimFaucetTokens();
// 4) Bump token daily limit to 2 so we can proceed past the early limit check
vm.prank(owner);
faucet.adjustDailyClaimLimit(1, true); // 1 -> 2
// 5) Now user2 claims at 00:00:01
// - ETH daily accounting should RESET at calendar day boundary (so dailyDrips restarts from zero)
// - Token daily accounting does NOT reset here (rolling/late reset + wrong ordering),
// so the count increments to 2 instead of resetting to 0/1.
uint256 preUser2Eth = user2.balance;
vm.prank(user2);
faucet.claimFaucetTokens();
// Token side: still counting from previous day (no midnight reset); went from 1 -> 2
assertEq(faucet.dailyClaimCount(), 2, "token daily count continued across midnight (no reset)");
// ETH side: new day => counter reset happened inside the ETH branch (currentDay > lastDripDay),
// then increased by one drip for user2's first-time claim.
// So user2 gets one drip; and dailyDrips should reflect only today's drip(s).
assertEq(user2.balance - preUser2Eth, ethDrip, "user2 received ETH after midnight");
// Optional (if dailyDrips is public): it should be exactly one drip for the new day
// assertEq(faucet.dailyDrips(), ethDrip, "ETH daily counter reset at midnight");
}
}

Mitigation:
Use consistent daily reset logic across all features:

function getCurrentDay() private view returns (uint256) {
return block.timestamp / 1 days;
}

Use this function for both ETH and claim resets.

Support

FAQs

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