In claimFaucetTokens(), the faucet sends ETH to first-time claimers using a low-level call:
Problem:
The external call is made before updating all critical state variables (e.g., lastClaimTime and dailyClaimCount).
A malicious contract can reenter claimFaucetTokens() during the ETH transfer and claim tokens two times before the state is updated. Why is it exaclty 2 times, explanind in the POC section below.
Likelihood: High
| Factor | Observation | Likelihood Influence |
|---|---|---|
| Access Level | Publicly callable by anyone | High |
| Exploit Complexity | Low — can use a reentrant smart contract | High |
| Detectability | Visible during faucet use | Medium |
| Impact | Loss of funds and tokens | High |
Impact: High
| Impact Area | Description |
|---|---|
| Funds | Malicious user can drain tokens from the faucet two times per cooldown period. |
| Security | Classic reentrancy vulnerability; could compromise the faucet’s integrity. |
| Fairness | Legitimate users may be blocked or receive fewer tokens. |
| Reputation | Exploit could damage trust in the protocol. |
Why it’s limited to two drips?
Outer call (first drip)
On the first claimFaucetTokens() call the contract sets hasClaimedEth[msg.sender] = true (for first-time claimers) before doing the external call{value: ...}("").
Then the contract calls the attacker’s receive() while the outer invocation is still executing.
First nested call (second drip)
Inside receive(), a nested claimFaucetTokens() call runs. Because lastClaimTime and dailyClaimCount are still not updated by the outer call yet, the nested call passes the cooldown and limit checks.
hasClaimedEth is already true, so the nested call does not perform another ETH .call{value:...} but it does perform the token _transfer and then executes its own state updates — including setting lastClaimTime[msg.sender] = block.timestamp.
Any further nested attempts (third and beyond) fail
After the first nested call completes, lastClaimTime is now set. Any further nested call from the same receive() will hit: if (block.timestamp < lastClaimTime[msg.sender] + CLAIM_COOLDOWN) revert ClaimCooldownOn(); and thus revert. So you get no third successful token drip.
Nevertheless, this is still a real vulnerability: even one extra nested drip is unacceptable. Fix it by applying Checks → Effects → Interactions
Move the external call after updating all critical state variables
Optionally, add nonReentrant modifier from OpenZeppelin’s ReentrancyGuard.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.