Raisebox Faucet

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

First-time claimants face 3-day wait to retry for free Sepolia ETH

First-time claimants can miss out on their free Sepolia ETH for around 3 days due to how RaiseBoxFaucet::claimFaucetTokens is implemented

Description

  • The RaiseBoxFaucet contract allows first-time claimants to claim both tokens and Sepolia ETH, which are helpful for new users to test the testnet of their future protocol. To ensure that, the contract has implemented claimFaucetTokens function, which checks if the claimant is a first-time claimant, and if so, sends them both tokens and Sepolia ETH. Moreover, the next claim for tokens can be made only after 3 days.

  • But, the way the function is implemented, a first-time claimant can miss out on their free Sepolia ETH if the contract is out of Sepolia ETH, or the daily cap for Sepolia ETH is reached. However, the issue is that the claimant will do get their tokens, but won't get their Sepolia ETH for around 3 days.

    function claimFaucetTokens() public {
    // ...
    @> if (block.timestamp < (lastClaimTime[faucetClaimer] + CLAIM_COOLDOWN)) { // 3 days cooldown
    @> revert RaiseBoxFaucet__ClaimCooldown();
    }
    // ...
    @> if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) { // Check for first-time claimant
    // ...
    @> if (address(this).balance >= sepEthAmountToDrip && (dailyDrips + sepEthAmountToDrip) <= dailySepEthCap) { // Check for sepETH availability and daily cap, and if it fails...
    // ...
    @> else {
    emit SepEthDripSkipped(
    faucetClaimer,
    address(this).balance < sepEthAmountToDrip ? "Faucet out of ETH" : "Daily ETH cap reached"
    );
    // No revert here, so the user will still get tokens
    }
    // ...
    @> lastClaimTime[faucetClaimer] = block.timestamp;
    // ...
    @> _transfer(address(this), faucetClaimer, faucetDrip);
    }

  • The user must wait the 3-day cooldown to try again for the sepETH. If the contract also remains out of ETH at the next attempt (worst possible scenario), the user may never receive sepETH — otherwise they simply face delayed receipt. This is clearly not the intended behaviour.

Risk

Likelihood: Low/Medium

  • It depends on the contract's Sepolia ETH balance and the daily cap being reached. If the contract is well-funded and the daily cap is high enough, the chances of a first-time claimant missing out on their free Sepolia ETH are reduced.

Impact: Medium

  • The user will miss out on their free Sepolia ETH for around 3 days, which wasn't the intended behaviour. However, they will still receive their tokens and ETH too, if they are lucky enough. So the impact is not as severe as losing both tokens and Sepolia ETH.

Proof of Concept

  • Add this test case to the existing RaiseBoxFaucet.t.sol file:

    function test__UserHasToWait3DaysToGetHisFreeSepEth() public {
    // Setup
    // Creating a new instance of RaiseBoxFaucet contract with no donation yet
    RaiseBoxFaucet raiseBox = new RaiseBoxFaucet(
    "raiseBoxFaucet",
    "RBF",
    1000 * 10 ** 18,
    0.005 ether,
    1 ether
    );
    console.log("User1 Initial Faucet Token Balance:", raiseBox.getBalance(user1));
    console.log("User1 Initial SepEth Balance:", address(user1).balance);
    // User 1 tries to claim tokens and sepEth for the first time
    vm.prank(user1);
    raiseBox.claimFaucetTokens();
    console.log();
    console.log("User1 tries to claim for the first time...");
    console.log();
    // Asserts
    assertEq(raiseBox.getBalance(user1), 1000 * 10 ** 18);
    console.log("User1 Faucet Token Balance after the claim:", raiseBox.getBalance(user1));
    assertEq(address(user1).balance, 0);
    console.log("User1 SepEth Balance after the claim:", address(user1).balance);
    // User 1 tries to claim again, as he didn't got his free sepEth
    vm.prank(user1);
    vm.expectRevert();
    raiseBox.claimFaucetTokens(); // This will revert, as 3 days haven't passed yet
    console.log();
    console.log("User1 tries to claim again...");
    console.log("Claim reverted, as 3 days haven't passed yet.");
    // Fast forward time by 3 days, and funding the contract with some sepEth
    advanceBlockTime(block.timestamp + 3 days);
    vm.deal(address(raiseBox), 1 ether); // Funding the contract with 1 sepEth
    console.log();
    console.log("Now 3 days have passed, and the contract is funded with 1 SepEth.");
    // User 1 tries to claim again after 3 days
    vm.prank(user1);
    raiseBox.claimFaucetTokens(); // This should succeed now
    console.log();
    console.log("User1 tries to claim again after 3 days...");
    console.log();
    // Asserts
    assertEq(raiseBox.getBalance(user1), 2000 * 10 ** 18);
    console.log("User1 Faucet Token Balance after the second claim:", raiseBox.getBalance(user1));
    assertEq(address(user1).balance, 0.005 ether);
    console.log("User1 SepEth Balance after the second claim:", address(user1).balance);
    }

  • Run the above test using the following command:

    forge test --mt test__UserHasToWait3DaysToGetHisFreeSepEth -vv

  • Logs:

    Ran 1 test for test/RaiseBoxFaucet.t.sol:TestRaiseBoxFaucet
    [PASS] test__UserHasToWait3DaysToGetHisFreeSepEth() (gas: 2578504)
    Logs:
    User1 Initial Faucet Token Balance: 0
    User1 Initial SepEth Balance: 0
    User1 tries to claim for the first time...
    User1 Faucet Token Balance after the claim: 1000000000000000000000
    User1 SepEth Balance after the claim: 0
    User1 tries to claim again...
    Claim reverted, as 3 days haven't passed yet.
    Now 3 days have passed, and the contract is funded with 1 SepEth.
    User1 tries to claim again after 3 days...
    User1 Faucet Token Balance after the second claim: 2000000000000000000000
    User1 SepEth Balance after the second claim: 5000000000000000
    Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.02ms (414.74µs CPU time)

Recommended Mitigation

There are two ways to mitigate this issue:

  1. Add a revert in the else block (lines 205-210) to ensure that if a first-time claimant cannot receive their free Sepolia ETH due to the contract being out of Sepolia ETH or the daily cap being reached, the entire transaction reverts, and they do not receive their tokens either. This way, they can try again immediately without waiting for 3 days.

    + error RaiseBoxFaucet__SepEthDripFailed();
    ...
    else {
    emit SepEthDripSkipped(
    faucetClaimer,
    address(this).balance < sepEthAmountToDrip ? "Faucet out of ETH" : "Daily ETH cap reached"
    );
    + revert RaiseBoxFaucet__SepEthDripFailed();
    }

  2. Alternatively, if the protocol doesn't want to restrict the transfer of tokens even if the Sepolia ETH drip fails, then try implementing a separate function for claiming Sepolia ETH, which can be called independently of the token claim function. This way, users can claim their tokens without any restrictions, and they can attempt to claim their Sepolia ETH at any time without waiting for three days.

Updates

Lead Judging Commences

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

User can't retry for ETH without waiting 3 days, even though they never received it

Support

FAQs

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