Raisebox Faucet

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

Vulnerability in claimFaucetTokens

Root + Impact

Description

  • Normal behavior: Users should be able to claim faucet tokens and, if it’s their first claim, receive a small Sepolia ETH drip. Claims must respect cooldown periods, daily token claim limits, and daily ETH drip caps.

  • Specific issue: The function uses a global state variable faucetClaimer for racking the caller and resets dailyDrips even when the user is ineligible for ETH drip. Additionally, dailyClaimCount and lastClaimTime were previously not updated correctly, and comparisons with contract balance (<= faucetDrip) may prevent valid claims.

> address public faucetClaimer;
function claimFaucetTokens() public {
> faucetClaimer = msg.sender;
if (block.timestamp < (lastClaimTime[faucetClaimer] + CLAIM_COOLDOWN)) {
revert RaiseBoxFaucet_ClaimCooldownOn();
}
...
> dailyDrips = 0; // resets even if user cannot drip ETH
...
lastClaimTime[faucetClaimer] = block.timestamp;
dailyClaimCount++;
_transfer(address(this), faucetClaimer, faucetDrip);
}

Risk

Likelihood:

  • Race conditions occur whenever multiple users interact with the faucet in the same block or close time frame.

  • Daily claim limits may not be enforced correctly due to improper updates of dailyClaimCount.

  • ETH drips can be reset incorrectly, allowing first-time claimers to bypass caps.

Impact:

  • Users may bypass cooldown periods or daily limits, resulting in unintended faucet token distribution.

  • ETH drips could exceed the intended daily cap.

  • Potential for privilege or logic abuse by exploiting race conditions.

Proof of Concept

This PoC demonstrates that the faucet function allows multiple issues due to the use of a global faucetClaimer variable, improper dailyDrips reset, and potential cooldown bypass. By executing claims in sequence or with multiple users in the same block, an attacker could bypass daily limits or receive unintended ETH drips.

function testClaimFaucetTokens_SingleUser_FirstTime() public {
vm.prank(user1);
raiseBoxFaucet.claimFaucetTokens();
assertEq(raiseBoxFaucet.getBalance(user1), raiseBoxFaucet.faucetDrip());
assertEq(address(user1).balance, raiseBoxFaucet.sepEthAmountToDrip());
assertEq(raiseBoxFaucet.dailyDrips(), raiseBoxFaucet.sepEthAmountToDrip());
assertEq(raiseBoxFaucet.getUserLastClaimTime(user1), block.timestamp);
assertTrue(raiseBoxFaucet.getHasClaimedEth(user1));
}
function testClaimFaucetTokens_CooldownEnforced() public {
vm.prank(user1);
raiseBoxFaucet.claimFaucetTokens();
vm.prank(user1);
vm.expectRevert(RaiseBoxFaucet.RaiseBoxFaucet_ClaimCooldownOn.selector);
raiseBoxFaucet.claimFaucetTokens();
vm.warp(block.timestamp + raiseBoxFaucet.CLAIM_COOLDOWN());
vm.prank(user1);
raiseBoxFaucet.claimFaucetTokens();
assertEq(raiseBoxFaucet.getBalance(user1), raiseBoxFaucet.faucetDrip() * 2);
}
function testClaimFaucetTokens_EthDripPaused() public {
raiseBoxFaucet.toggleEthDripPause(true);
vm.prank(user1);
raiseBoxFaucet.claimFaucetTokens();
assertEq(address(user1).balance, 0, "No ETH should be dripped when paused");
assertTrue(raiseBoxFaucet.getHasClaimedEth(user1) == false);
}
function testClaimFaucetTokens_DailyDripResets() public {
vm.prank(user1);
raiseBoxFaucet.claimFaucetTokens();
uint256 initialDailyDrips = raiseBoxFaucet.dailyDrips();
assertEq(initialDailyDrips, raiseBoxFaucet.sepEthAmountToDrip());
vm.warp(block.timestamp + 1 days);
vm.prank(user2);
raiseBoxFaucet.claimFaucetTokens();
assertEq(raiseBoxFaucet.dailyDrips(), raiseBoxFaucet.sepEthAmountToDrip());
}
function testMultipleUsersClaim() public {
address[2] memory users = [user1, user2];
for (uint256 i = 0; i < users.length; i++) {
vm.prank(users[i]);
raiseBoxFaucet.claimFaucetTokens();
assertEq(raiseBoxFaucet.getBalance(users[i]), raiseBoxFaucet.faucetDrip());
assertEq(address(users[i]).balance, raiseBoxFaucet.sepEthAmountToDrip());
assertTrue(raiseBoxFaucet.getHasClaimedEth(users[i]));
}
}

Recommended Mitigation

Use currentDay = block.timestamp / 1 days consistently to handle day resets for daily claim counts and ETH drips.

  • Consider adding ReentrancyGuard for additional safety during ETH drip transfers.

  • Emit an event for each claim for auditability:

- address public faucetClaimer;
- faucetClaimer = msg.sender;
- dailyDrips = 0; // resets even for ineligible users
+ address faucetClaimer = msg.sender; // use local variable
+ // reset dailyDrips only at start of a new day
+ uint256 currentDay = block.timestamp / 1 days;
+ if (currentDay > lastDripDay) {
+ lastDripDay = currentDay;
+ dailyDrips = 0;
+ }
+ // update lastClaimTime and dailyClaimCount after successful claim
+ lastClaimTime[faucetClaimer] = block.timestamp;
+ dailyClaimCount++;
Updates

Lead Judging Commences

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

Support

FAQs

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