Normally, the RaiseBoxFaucet::claimFaucetTokens function allows users to claim Sepolia ETH and faucet tokens every 3-day cooldown period, updating RaiseBoxFaucet::lastClaimTime and RaiseBoxFaucet::dailyClaimCount to prevent multiple claims.
However, due to an external call made before state updates, through this external call a malicious contract can exploit reentrancy via its receive() function to call RaiseBoxFaucet::claimFaucetTokens again within the same transaction, resulting in claiming faucet tokens twice in a single call.
Likelihood:
This issue happens when the claimer(attacker) uses a smart contract instead of an EOA. Inside the smart contract, there is a receive() function, that triggers the reentrancy exploit.
The reentrancy is triggered during SepETH transfer, which happens before RaiseBoxFaucet::lastClaimTime and RaiseBoxFaucet::dailyClaimCount updation, allowing the attacker contract to call RaiseBoxFaucet::claimFaucetTokens function again within the same transaction.
Impact:
Because of this exploit, attacker can claim faucet tokens twice which sums to 2000 tokens instead of claiming once(sums to 1000 tokens).
Explanation
The attacker sets up a contract ReentrancyAttack which contains a receive() function with the malicious condition if (raiseBoxFaucet.getBalance(address(this)) < raiseBoxFaucet.getFaucetTotalSupply()) which is used to re-enter and call RaiseBoxFaucet::claimFaucetTokens function during the SepETH token transaction.
The ReentrancyAttack contract is initialized in RaiseBoxFaucet.t.sol::test_CanFaucetTokenClaimTwiceInSingleTransaction function, after that the attacker calls RaiseBoxFaucet::claimFaucetTokens from their attacker contract and SepETH token is transferred to ReentrancyAttack::receive function.
Inside the receive() function, a reentrancy exploit occurs, causing the faucet token to be transferred twice: once from the original call transaction and again from the reentrancy call transaction, resulting in receiving double the intended amount.
Intended Faucet token to claim: 1000000000000000000000 (1000 tokens)
Actual Faucet token claimed due to reentrancy: 2000000000000000000000 (2000 tokens)
Use OpenZeppelin ReentrancyGuard
Import ReentrancyGuard and mark claimFaucetTokens with the nonReentrant modifier. This prevents any reentrant calls to the function within the same transaction.
Update all state variables before external call.
Move the state updates lastClaimTime and dailyClaimCount before the SepETH transfer to prevent reentrancy exploiting the old state:
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.