Raisebox Faucet

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

Sybilable daily limit DoS on faucet claims

Root + Impact

Global dailyClaimLimit without Sybil resistance lets an attacker exhaust the day’s quota, blocking all honest users from receiving tokens.

Description

  • The faucet enforces a single integer dailyClaimLimit and increments dailyClaimCount each time someone claims, intending to stop more than N payouts per day.

  • An adversary can cycle through many addresses (or contracts) to hit the limit early, because the function never tracks unique claimers or enforces a per-address ceiling beyond the cooldown.

if (dailyClaimCount >= dailyClaimLimit) {
@> revert RaiseBoxFaucet_DailyClaimLimitReached();
}
...
@> dailyClaimCount++;

Risk

Likelihood:

  • The limit is global, so a bot that batch-sends 100 transactions right after the day resets will always lock the faucet for the rest of the period.

  • Nothing prevents the owner from reducing dailyClaimLimit to a very small number, making accidental or malicious lockouts more frequent.

Impact:

  • Legitimate users are starved of both ETH and tokens despite the faucet having sufficient balances.

  • Ecosystem integrations relying on continuous faucet availability suffer downtime and reputational damage.

Proof of Concept

The PoC loops through multiple identities to exhaust dailyClaimLimit, locking genuine users out for the rest of the day.

for (uint256 i = 0; i < faucet.dailyClaimLimit(); i++) {
faucet.claimFaucetTokens(); // each call from a fresh EOAs/contract hits the global counter
}
// Subsequent users revert with RaiseBoxFaucet_DailyClaimLimitReached.

Recommended Mitigation

Extending the state tracking as shown enforces per-day and per-user quotas, neutralizing the Sybil DoS vector.

--- a/src/RaiseBoxFaucet.sol
+++ b/src/RaiseBoxFaucet.sol
@@ -10,6 +10,7 @@ contract RaiseBoxFaucet is ERC20, Ownable {
mapping(address => uint256) private lastClaimTime;
mapping(address => bool) private hasClaimedEth;
+ mapping(address => uint256) private dailyClaimsByAddress;
address public faucetClaimer;
@@ -17,6 +18,9 @@ contract RaiseBoxFaucet is ERC20, Ownable {
uint256 public dailyClaimLimit = 100;
+ // Maximum claims per address per day to prevent multi-address attacks
+ uint256 public maxClaimsPerAddressPerDay = 1;
+
//= 1000 * 10 ** 18;
// assuming 18 decimals
uint256 public faucetDrip;
@@ -28,6 +32,8 @@ contract RaiseBoxFaucet is ERC20, Ownable {
uint256 public lastFaucetDripDay;
+ uint256 public lastResetDay;
+
uint256 public dailyDrips;
// Sep Eth drip for first timer claimers = 0.01 ether;
@@ -54,6 +60,7 @@ contract RaiseBoxFaucet is ERC20, Ownable {
sepEthAmountToDrip = sepEthDrip_;
dailySepEthCap = dailySepEthCap_;
+ lastResetDay = block.timestamp / 1 days;
_mint(address(this), INITIAL_SUPPLY); // mint initial supply to contract on deployment
}
@@ -83,6 +90,7 @@ contract RaiseBoxFaucet is ERC20, Ownable {
error RaiseBoxFaucet_OwnerOrZeroOrContractAddressCannotCallClaim();
error RaiseBoxFaucet_DailyClaimLimitReached();
error RaiseBoxFaucet_InsufficientContractBalance();
+ error RaiseBoxFaucet_AddressDailyClaimLimitReached();
// -----------------------------------------------------------------------
// OWNER FUNCTIONS
@@ -117,6 +125,24 @@ contract RaiseBoxFaucet is ERC20, Ownable {
_burn(msg.sender, amountToBurn);
}
+ /// @notice Set maximum claims per address per day
+ /// @dev Helps prevent multi-address DoS attacks
+ /// @param newLimit New maximum claims per address per day
+ function setMaxClaimsPerAddressPerDay(uint256 newLimit) external onlyOwner {
+ require(newLimit > 0, "Limit must be greater than 0");
+ maxClaimsPerAddressPerDay = newLimit;
+ }
+
+ /// @notice Reset daily claim counters manually (emergency function)
+ /// @dev Should only be used in exceptional circumstances
+ function resetDailyCounters() external onlyOwner {
+ uint256 currentDay = block.timestamp / 1 days;
+ lastResetDay = currentDay;
+ lastFaucetDripDay = block.timestamp;
+ dailyClaimCount = 0;
+ // Note: Individual address counters will be reset naturally on next claim
+ }
+
/// @notice Adjust the daily claim limit for the contract
/// @dev Increases or decreases the `dailyClaimLimit` by the given amount
/// @param by The amount to adjust the `dailyClaimLimit` by
@@ -125,6 +151,10 @@ contract RaiseBoxFaucet is ERC20, Ownable {
function adjustDailyClaimLimit(uint256 by, bool increaseClaimLimit) public onlyOwner {
if (increaseClaimLimit) {
dailyClaimLimit += by;
} else {
+ // Prevent owner from setting limit below current daily claim count
+ if (dailyClaimLimit - by < dailyClaimCount) {
+ revert RaiseBoxFaucet_CurrentClaimLimitIsLessThanBy();
+ }
if (by > dailyClaimLimit) {
revert RaiseBoxFaucet_CurrentClaimLimitIsLessThanBy();
}
@@ -148,6 +178,20 @@ contract RaiseBoxFaucet is ERC20, Ownable {
revert RaiseBoxFaucet_OwnerOrZeroOrContractAddressCannotCallClaim();
}
+ // Reset daily counters if a new day has started
+ uint256 currentDay = block.timestamp / 1 days;
+ if (currentDay > lastResetDay) {
+ lastResetDay = currentDay;
+ dailyClaimCount = 0;
+ // lastFaucetDripDay is handled separately below
+ }
+
+ // Check per-address daily limit
+ if (dailyClaimsByAddress[faucetClaimer] >= maxClaimsPerAddressPerDay) {
+ revert RaiseBoxFaucet_AddressDailyClaimLimitReached();
+ }
+
if (balanceOf(address(this)) <= faucetDrip) {
revert RaiseBoxFaucet_InsufficientContractBalance();
}
@@ -193,6 +237,8 @@ contract RaiseBoxFaucet is ERC20, Ownable {
if (block.timestamp > lastFaucetDripDay + 1 days) {
lastFaucetDripDay = block.timestamp;
dailyClaimCount = 0;
+ // Reset per-address claim counts (handled via currentDay check above)
+ delete dailyClaimsByAddress[faucetClaimer];
}
// Effects
lastClaimTime[faucetClaimer] = block.timestamp;
dailyClaimCount++;
+ dailyClaimsByAddress[faucetClaimer]++;
// Interactions
_transfer(address(this), faucetClaimer, faucetDrip);
Updates

Lead Judging Commences

inallhonesty Lead Judge 11 days ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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