I was auditing the ZK verification flow and noticed that verifyAuditScoreProof stores the proof hash after verification but never checks whether that hash was already used.
contracts/src/ZkAuditVerifier.sol around lines 144–157:
bytes32 proofHash = keccak256(abi.encodePacked(a[0], a[1], b[0][0], b[0][1], c[0], c[1]));
auditScoreProofs[tokenId][auditId] = AuditScoreProof({
verified: true,
score: score,
verifiedAt: block.timestamp,
proofHash: proofHash
});
There is no require(!usedProofs[proofHash], "PROOF_REPLAYED") guard. A valid proof for (tokenId=1, auditId=1) can be re-submitted to reset verifiedAt to the current block, making a stale audit appear freshly verified. Worse, if the public inputs are flexible enough, the same proof could be submitted for a different (tokenId, auditId) pair.
The fix is a global mapping(bytes32 => bool) private _usedProofHashes checked and set atomically on each submission.
I was auditing the ZK verification flow and noticed that
verifyAuditScoreProofstores the proof hash after verification but never checks whether that hash was already used.contracts/src/ZkAuditVerifier.solaround lines 144–157:There is no
require(!usedProofs[proofHash], "PROOF_REPLAYED")guard. A valid proof for(tokenId=1, auditId=1)can be re-submitted to resetverifiedAtto the current block, making a stale audit appear freshly verified. Worse, if the public inputs are flexible enough, the same proof could be submitted for a different(tokenId, auditId)pair.The fix is a global
mapping(bytes32 => bool) private _usedProofHasheschecked and set atomically on each submission.