Merkle Proof Verification
Detects insecure Merkle proof verification patterns vulnerable to second preimage attacks and proof malleability.
Merkle Proof Verification
Overview
The Merkle proof verification detector identifies custom Merkle tree implementations that do not double-hash leaf values before verification. Without double-hashing, the proof is vulnerable to second preimage attacks: an attacker can construct a valid proof for a value that was never included in the original tree by reinterpreting an internal tree node as a leaf.
Custom Merkle implementations have caused $10M+ in whitelist bypasses, unauthorized NFT minting, and airdrop theft. Contracts that roll their own verification instead of using audited libraries carry elevated risk.
Why This Is an Issue
In a standard Merkle tree, internal nodes are computed as hash(left || right). If leaf values are hashed with the same function without distinction (hash(leaf)), an attacker who knows the value of an internal node can submit it as a leaf with a shortened proof. The verifier cannot distinguish between a genuine leaf and an internal node because both are produced by the same hash function.
Double-hashing — hash(hash(leaf)) — ensures that leaf hashes are in a different domain than internal node hashes, preventing this confusion.
How to Resolve
// Before: Vulnerable -- single hash of leaf
function verify(bytes32[] calldata proof, bytes32 root, address account, uint256 amount) public pure returns (bool) {
bytes32 leaf = keccak256(abi.encodePacked(account, amount));
return MerkleProof.verify(proof, root, leaf); // Second preimage risk
}
// After: Fixed -- double hash of leaf
function verify(bytes32[] calldata proof, bytes32 root, address account, uint256 amount) public pure returns (bool) {
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));
return MerkleProof.verify(proof, root, leaf);
}
Examples
Vulnerable
function claimAirdrop(bytes32[] calldata proof, uint256 amount) external {
bytes32 leaf = keccak256(abi.encodePacked(msg.sender, amount));
require(MerkleProof.verify(proof, merkleRoot, leaf), "Invalid proof");
require(!claimed[msg.sender], "Already claimed");
claimed[msg.sender] = true;
token.transfer(msg.sender, amount);
}
Fixed
function claimAirdrop(bytes32[] calldata proof, uint256 amount) external {
// Double-hash prevents second preimage attacks
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, amount))));
require(MerkleProof.verify(proof, merkleRoot, leaf), "Invalid proof");
require(!claimed[msg.sender], "Already claimed");
claimed[msg.sender] = true;
token.transfer(msg.sender, amount);
}
Sample Sigvex Output
[HIGH] merkle-proof-verification
Potential insecure Merkle proof verification in claimAirdrop
Location: claimAirdrop @ block 0, instruction 0
Confidence: 0.70
Function claimAirdrop appears to implement Merkle proof verification
(iterative hashing with loop control flow) but does not double-hash
leaf values before verification.
Detection Methodology
- Function name matching: Identifies functions with names containing
merkle,proof,verify,claim,whitelist, orairdrop. - Pattern matching: Detects iterative hashing patterns (multiple
KECCAK256operations with loop control flow) that characterize Merkle proof walking. - Double-hash check: Examines the first few blocks for two consecutive
KECCAK256operations in the same block, which indicates the leaf is double-hashed before entering the proof loop. - Proxy suppression: Skips proxy contracts where iterative hashing in the fallback/dispatch function is for selector routing, not Merkle verification.
Limitations
False positives: Functions that perform iterative hashing for non-Merkle purposes (e.g., hash chains, commit-reveal schemes) may be flagged if their name matches Merkle-related patterns. Contracts using OpenZeppelin’s MerkleProof library are suppressed. False negatives: Merkle verification performed in a library contract via DELEGATECALL is not analyzed in the calling contract’s context.
Related Detectors
- Hash Collision — detects hash collision risks from
abi.encodePackedwith dynamic types - Signature Verification — detects improper signature checks