SNARKeling Treasure Hunt

First Flight #59
Beginner FriendlyGameFiFoundry
100 EXP
Submission Details
Impact: low
Likelihood: low

claim() performs external verifier call before state updates, violating CEI pattern and making nonReentrant the sole protection against double-claims

Author Revealed upon completion

Root + Impact

Description

  • The claim() function follows a C-I-E-I pattern instead of the correct C-E-I pattern. The external call to verifier.verify() happens before claimsCount and claimed are updated. This means if a malicious or compromised verifier re-enters claim() during verification, the state has not yet been updated and all checks pass again. The nonReentrant modifier currently prevents this, but it is load-bearing for two independent reasons simultaneously — reentrancy protection AND replay protection (due to the broken claimed check in L-1). Removing or bypassing the guard would make the contract instantly drainable in a single transaction.

function claim(...) external nonReentrant() {
// Checks
if (claimed[_treasureHash]) revert AlreadyClaimed(treasureHash);
// @> Interaction BEFORE effects — external call to verifier
bool ok = verifier.verify(proof, publicInputs);
if (!ok) revert InvalidProof();
// @> Effects come after the external call — too late
_incrementClaimsCount();
_markClaimed(treasureHash);
// Second interaction
(bool sent, ) = recipient.call{value: REWARD}("");
}

Risk

Likelihood:

  • Requires a malicious or compromised verifier contract

  • Owner is trusted, so verifier swap is unlikely to be malicious

  • Low likelihood under normal operation

Impact:

  • If nonReentrant is ever removed, a malicious verifier can reenter claim() before state updates and drain the contract in a single transaction

  • nonReentrant is currently carrying two separate responsibilities — removing it for any reason exposes both reentrancy and replay vectors simultaneously

  • Correct CEI costs nothing to implement

Recommended Mitigation

bool ok = verifier.verify(proof, publicInputs);
if (!ok) revert InvalidProof();
+ _incrementClaimsCount();
+ _markClaimed(treasureHash);
- _incrementClaimsCount();
- _markClaimed(treasureHash);
(bool sent, ) = recipient.call{value: REWARD}("");

Support

FAQs

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

Give us feedback!