Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Unbalanced Merkle Tree Allows for Second Pre-image Attack, Enabling Unauthorized NFT Claims

Root + Impact

The contract's Merkle proof verification for unbalanced trees is flawed. An attacker can craft a valid proof for an unauthorized address, allowing them to claim NFTs and bypass the airdrop's access control. This vulnerability is evidenced by proofs with zero-hashes in output.json.

Description

The _verify function does not correctly handle unbalanced Merkle trees, where some leaves are hashed against a zero value. This allows an attacker to forge a proof for a malicious leaf by finding a second pre-image, making the contract trust an invalid claim.

// src/SnowmanAirdrop.sol
function _verify(
bytes32[] memory proof,
bytes32 root,
bytes32 leaf
) internal pure returns (bool) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
bytes32 proofElement = proof[i];
@> if (computedHash <= proofElement) {
@> computedHash = keccak256(abi.encodePacked(computedHash, proofElement));
} else {
@> computedHash = keccak256(abi.encodePacked(proofElement, computedHash));
}
}
return computedHash == root;
}

Risk

Likelihood:

  • An attacker can deterministically craft a malicious leaf and proof by observing the legitimate Merkle tree structure and identifying an unbalanced node. This doesn't rely on chance.

  • The vulnerability is in the core logic of the claim function and is always present. The output.json file demonstrates the existence of proofs that can be exploited.

Impact:

  • An attacker can claim Snowman NFTs without being on the whitelist, effectively stealing them.

  • The integrity of the airdrop is compromised, leading to a loss of trust and potential financial loss for the project.

Proof of Concept

An attacker identifies an unbalanced node from the legitimate tree, crafts a malicious leaf (attacker_address, amount), and calculates a proof_element to create a valid proof for their malicious leaf. They then call claim() with this forged proof to mint NFTs.

// Conceptual PoC
function testExploitUnbalancedMerkleTree() public {
// 1. Attacker identifies an unbalanced leaf and crafts a malicious leaf
// 2. Attacker calculates a malicious proof element to create a valid proof
// 3. Attacker calls claim() with the forged proof
vm.prank(attacker);
snowmanAirdrop.claim(attacker, maliciousAmount, maliciousProof);
// 4. Assert attacker now owns NFTs they were not whitelisted for
assertEq(snowman.balanceOf(attacker), maliciousAmount);
}

Recommended Mitigation

Do not use a custom Merkle proof implementation. Use OpenZeppelin's battle-tested MerkleProof.sol library which correctly handles unbalanced trees.

// src/SnowmanAirdrop.sol
import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
// ...
function claim(
address who,
uint256 amount,
bytes32[] calldata proof
) public {
bytes32 leaf = keccak256(abi.encodePacked(who, amount));
@> if (!MerkleProof.verify(proof, merkleRoot, leaf)) {
revert InvalidProof();
}
// ...
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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