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:
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.
The contest is complete and the rewards are being distributed.