TreasureHunt.claim() checks for double-claims at line 88 by reading claimed[_treasureHash]. _treasureHash is declared as bytes32 private immutable at line 35 but is never assigned in the constructor (lines 67-75). Solc 0.8.27 encodes zero at every read site for an unassigned immutable, so the guard permanently reads claimed[bytes32(0)], which cannot be flipped because the circuit rejects the zero hash. The write at line 104 correctly targets claimed[treasureHash] (the user-supplied parameter), so the guard and the write operate on different mapping slots. The dedup check is dead code, and claimsCount >= MAX_TREASURES is the only thing bounding losses to 100 ETH.
Likelihood: near-certain. Any attacker with one valid secret produces 10 proofs for distinct recipients and submits them. Impact: complete loss of the 100 ETH prize pool.
The project's own test at contracts/test/TreasureHunt.t.sol:134-147 has vm.expectRevert() commented out at line 144 — the suite passes while double-claim succeeds, which is direct evidence of the bug on main.
Self-contained Forge test. No dependency on the generated Verifier.sol or fixture files; the verifier is mocked via vm.mockCall. Drop into contracts/test/ and run forge test --match-contract PoC_C01 -vv.
Actual result:
Slither's default detector also catches the root cause:
Read the function parameter instead of the uninitialized immutable, and delete the immutable so the naming collision cannot recur:
Also restore the regression test:
In `claim()`, the guard uses `claimed[_treasureHash]`, where `_treasureHash` is an immutable state variable that is never initialized to the caller-supplied treasure identifier, while the contract later marks `claimed[treasureHash] = true` using the function argument instead. As a result, the duplicate-claim check and the state update are performed against different keys, which means a previously claimed treasure is not actually blocked from being claimed again with the same valid proof and `treasureHash`. This breaks a core invariant of the protocol described in the README, namely, that each treasure can only be redeemed once, and allows one valid treasure/proof pair to be reused to drain rewards repeatedly until either the `MAX_TREASURES` cap or the contract balance is exhausted.
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.