Raisebox Faucet

First Flight #50
Beginner FriendlySolidity
100 EXP
Submission Details
Severity: high
Valid

[H-2] `dailyDrips` falsely resets to 0, allowing the contract's sepolia ETH funds to be drained faster than intended

Author Revealed upon completion

[H-2] dailyDrips falsely resets to 0, allowing the contract's sepolia ETH funds to be drained faster than intended

Description

  • Expected bahaviour The total sepolia ETH dripped by the faucet in one day should not surpass the threshold defined in the state variable dailySepEthCap.

  • Problematic bahaviour When non-first time users claim tokens, the dailyDrips state variable falsely resets to 0. By resetting dailyDrips to zero, the faucet can be manipulated to send more sep ETH in a day than the maximum daily ETH amount set in dailySepEthCap.

function claimFaucetTokens() public {
.
.
.
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;
@> }
.
.
.
}

Risk

Likelihood: High

  • This bug resets dailyDrips whenever a non-first time user makes a new token claim.

Impact: Medium - High

  • This vulnerability allows the contract's sepolia ETH funds to be drained faster than intended. The severity of the drain depends on the dailyClaimLimit. The higher the daily claim limit is, the more sepolia ETH funds can be drained from the contract, potentially allowing a full drain within a day.

Proof of Concept

As a PoC add the following test to the Foundry test suite and run with forge test --mt test_dailyDrips_FalselyResets_AllowingMoreSepETHDrips.

Hypothetical Scenario

  1. Day 1: Owner increases the dailyClaimLimit to 200.

  2. The maximum number of sepolia eth drips that can be processed are 100, based on the contract's set up.

  3. First time user claims tokens.

  4. Cooldown passes (3 days)

  5. Day 5: 100 first time users claim tokens and receive ETH. The max sep ETH limit has been reached.

  6. A second-time user claims tokens again --> dailyDrips is reset from 0.5 ETH to 0 ETH.

  7. Another 99 first time users claim tokens and receive ETH. Total SEP ETH sent is higher than the 0.5 ETH limit.

function test_dailyDrips_FalselyResets_AllowingMoreSepETHDrips() public {
///////////
// Day 1 //
///////////
// Owner increases dailyClaimLimit by 100:
vm.prank(owner);
raiseBoxFaucet.adjustDailyClaimLimit(100, true);
// Assert that the new dailyClaimLimit has increased to 200:
assertTrue(raiseBoxFaucet.dailyClaimLimit() == 200, "dailyClaimLimit is not 200");
// Assert that the maximum number of daily sep eth claims is 100 based on the 0.5 ETH dailySepEthCap defined in setUp():
uint256 maxSepEthDailyClaims = raiseBoxFaucet.dailySepEthCap() / raiseBoxFaucet.sepEthAmountToDrip();
assertTrue(maxSepEthDailyClaims == 100, "The maximum number of allowed daily sep ETH drips is not 100");
// First-time user: newUser claims tokens
address newUser = makeAddr("newUser");
vm.prank(newUser);
raiseBoxFaucet.claimFaucetTokens();
/////////////////
// Day 2, 3, 4 //
/////////////////
// 4. Fast forward 3 days and 1 hour (> than 3 day cooldown)
vm.warp(block.timestamp + raiseBoxFaucet.CLAIM_COOLDOWN() + 1 hours);
///////////
// Day 5 //
///////////
// 100 first-time users claim tokens and the daily sep eth drip limit is reached:
uint256 totalEthDripped;
for(uint256 i=0; i < 100; i++) {
string memory claimer = string.concat("user",vm.toString(i));
address user = makeAddr(claimer);
vm.prank(user);
raiseBoxFaucet.claimFaucetTokens();
totalEthDripped += raiseBoxFaucet.sepEthAmountToDrip();
}
// Assert that dailyDrips have reached the cap threshold:
assertTrue(totalEthDripped == raiseBoxFaucet.dailySepEthCap(), "ETH drips have not reached the limit");
// Second-time user claims tokens and resets the dailyDrips variable to 0:
assertTrue(raiseBoxFaucet.getHasClaimedEth(newUser), "newUser is a first time user");
vm.prank(newUser);
raiseBoxFaucet.claimFaucetTokens();
assertTrue(raiseBoxFaucet.dailyDrips() == 0, "Daily drips value is not zero");
// Another set of 99 first time users can now receive sep ETH:
for(uint256 i=100; i < 199; i++) {
string memory claimer = string.concat("user",vm.toString(i));
address user = makeAddr(claimer);
vm.prank(user);
raiseBoxFaucet.claimFaucetTokens();
}
totalEthDripped += raiseBoxFaucet.dailyDrips();
// Assert that the total amount of sep ETH dispensed is higher that the daily ETH cap:
assertTrue(totalEthDripped > raiseBoxFaucet.dailySepEthCap(), "The daily sep ETH cap has not been exceeded");
// Assert that the contract's ETH balance is zero
assertTrue(address(raiseBoxFaucet).balance == 0, "Sep ETH balance is not 0");
}

Recommended Mitigation

To mitigate this vulnerability remove the else block that resets dailyDrips to zero:

function claimFaucetTokens() public {
.
.
.
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;
- }
.
.
.
}

The resets of dailyDrips and dailyClaimCount should also use a consistent calculation of days passed, which will be addressed as a separate finding.

Updates

Lead Judging Commences

inallhonesty Lead Judge 1 day ago
Submission Judgement Published
Validated
Assigned finding tags:

dailyDrips Reset Bug

Support

FAQs

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