Raisebox Faucet

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

`RaiseBoxFaucet:claimFaucetTokens` function resets `dailyClaimCount` too late, causing the protocol's claim functionality to completely fail after reaching `dailyClaimLimit`

Author Revealed upon completion

RaiseBoxFaucet:claimFaucetTokens function resets dailyClaimCount too late, causing the protocol's claim functionality to completely fail after reaching dailyClaimLimit

Description

  • Under normal circumstances, the maximum number of claims per day is dailyClaimLimit, which implicitly tells users that "after one day, dailyClaimCount will be reset to 0".

  • However, this ideal scenario only occurs when "the function is called with dailyClaimCount < dailyClaimLimit, and exactly at that moment, a day has passed".

  • This means that if the above condition is not met—i.e., "the function is called with dailyClaimCount < dailyClaimLimit, but a day has not passed at that moment"—then subsequently dailyClaimCount = dailyClaimLimit.

  • Immediately after, every subsequent call to the claim function (even if a day has passed) will revert due to dailyClaimCount >= dailyClaimLimit.

function claimFaucetTokens() public {
// Checks
faucetClaimer = msg.sender;
// (lastClaimTime[faucetClaimer] == 0);
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();
}
// drip sepolia eth to first time claimers if supply hasn't ran out or sepolia drip not paused**
// still checks
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0;
// dailyClaimCount = 0;
}
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"
);
}
} else {
dailyDrips = 0;
}
/**
*
* @param lastFaucetDripDay tracks the last day a claim was made
* @notice resets the @param dailyClaimCount every 24 hours
*/
@> if (block.timestamp > lastFaucetDripDay + 1 days) {
lastFaucetDripDay = block.timestamp;
@> dailyClaimCount = 0;
}
// Effects
lastClaimTime[faucetClaimer] = block.timestamp;
dailyClaimCount++;
// Interactions
_transfer(address(this), faucetClaimer, faucetDrip);
emit Claimed(msg.sender, faucetDrip);
}

Risk

Likelihood:

  • This will inevitably occur whenever a large number of users intensively perform claim actions within a single day.

Impact:

  • The entire protocol temporarily loses its claim functionality until the administrator uses adjustDailyClaimLimit to increase the maximum claim count, which clearly does not align with expectations.

Proof of Concept

  • Add the following to RaiseBoxFaucet.t.sol:

function test__claimFaucetTokensUntilFailed() public {
RaiseBoxFaucet testRaiseBoxFaucet = new RaiseBoxFaucet(
"raiseboxtoken",
"RB",
1000 * 10 ** 18,
0.005 ether,
0.5 ether
);
for (uint i=0; i < 100; i++) {
address userTmp = vm.addr(i + 100);
vm.prank(userTmp);
testRaiseBoxFaucet.claimFaucetTokens();
vm.warp(block.timestamp + 10);
}
vm.prank(user1);
vm.expectRevert(abi.encodeWithSelector(RaiseBoxFaucet.RaiseBoxFaucet_DailyClaimLimitReached.selector));
testRaiseBoxFaucet.claimFaucetTokens();
vm.warp(block.timestamp + 1 days);
vm.prank(user2);
vm.expectRevert(abi.encodeWithSelector(RaiseBoxFaucet.RaiseBoxFaucet_DailyClaimLimitReached.selector));
testRaiseBoxFaucet.claimFaucetTokens();
vm.prank(owner);
testRaiseBoxFaucet.adjustDailyClaimLimit(testRaiseBoxFaucet.dailyClaimLimit() + 1, true);
vm.prank(user3);
testRaiseBoxFaucet.claimFaucetTokens();
}

Recommended Mitigation

  • Before performing revert RaiseBoxFaucet_DailyClaimLimitReached();, implement flexible checks to directly reset dailyClaimCount = 0;

  • As shown below:

function claimFaucetTokens() public {
// Checks
faucetClaimer = msg.sender;
// (lastClaimTime[faucetClaimer] == 0);
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();
+ if (block.timestamp > lastFaucetDripDay + 1 days) {
+ lastFaucetDripDay = block.timestamp;
+ dailyClaimCount = 0;
+ }
+ else {
+ revert RaiseBoxFaucet_DailyClaimLimitReached();
+ }
}
// drip sepolia eth to first time claimers if supply hasn't ran out or sepolia drip not paused**
// still checks
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0;
// dailyClaimCount = 0;
}
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"
);
}
} else {
dailyDrips = 0;
}
/**
*
* @param lastFaucetDripDay tracks the last day a claim was made
* @notice resets the @param dailyClaimCount every 24 hours
*/
if (block.timestamp > lastFaucetDripDay + 1 days) {
lastFaucetDripDay = block.timestamp;
dailyClaimCount = 0;
}
// Effects
lastClaimTime[faucetClaimer] = block.timestamp;
dailyClaimCount++;
// Interactions
_transfer(address(this), faucetClaimer, faucetDrip);
emit Claimed(msg.sender, faucetDrip);
}

Support

FAQs

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