Snowman Merkle Airdrop

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

Inconsistent encoding of Merkle leaves using `abi.encode(data)` allows multiple valid roots for same dataset, breaking proof reproducibility

Root + Impact

Description

Normal behavior:
Merkle trees should be generated using a deterministic encoding of input data, so that the same inputs always yield the same root and proof structure. Typically this involves abi.encodePacked(a, b) or manual formatting that produces predictable bytes.

Issue:
This script uses abi.encode(data) where data is a bytes32[] array. Solidity prepends array offset and length (64 bytes total), so ltrim64() is called to remove those bytes. But this fix is fragile, as changes in memory layout, array size, or tooling could still cause inconsistencies.

data[j] = bytes32(uint256(uint160(value))); // address
...
data[j] = bytes32(value); // uint256
...
leafs[i] = keccak256(bytes.concat(keccak256(ltrim64(abi.encode(data)))));
// @> Risk: ltrim64(abi.encode(...)) may vary across executions

Risk

Likelihood: Medium

  1. This occurs any time abi.encode(data) is used on a dynamically-sized memory array of bytes32, which introduces a prefix offset and length due to Solidity ABI encoding rules.

  2. The script tries to “fix” this using ltrim64() (from Murky), but this is not a robust or formally safe solution for consistent leaf hashing across environments or runtimes.

Impact: Medium

  1. Merkle roots generated by this script may not match the same input set processed elsewhere, breaking airdrop proof verification and causing users to lose access to rewards.

  2. Subtle differences in byte encoding between environments (like Foundry vs JS Merkle tools) result in different roots, undermining trust in the airdrop process.


Proof of Concept

The PoC shows how abi.encode(data) produces memory-aligned bytes that are not stable across contexts. Even trimming 64 bytes with ltrim64() can still result in misaligned roots, breaking reproducibility across tools or between devs and the deployed contract.

function testInconsistentLeafEncoding() public {
bytes32 ;
data[0] = bytes32(uint256(uint160(0xA1))); // address
data[1] = bytes32(uint256(100)); // amount
bytes memory encoded = abi.encode(data);
bytes memory trimmed = ltrim64(encoded);
bytes32 hashed = keccak256(bytes.concat(keccak256(trimmed)));
// This hash is sensitive to ABI layout and memory alignment.
// Re-encoding this same logical input in JS or different Solidity version may produce a different hash.
console.logBytes32(hashed);
}

Recommended Mitigation

Replace the indirect abi.encode(...) + ltrim64(...) + keccak256 logic with a clean, deterministic abi.encodePacked(...) of the address and amount. This guarantees consistent leaf encoding across all environments and tools.

  • abi.encodePacked(a, b) produces a tightly packed format, ideal for hashing.

  • Eliminates reliance on Murky’s ltrim64() hack and memory-offset assumptions.

  • Fixes reproducibility across Foundry, JavaScript, or Etherscan-based Merkle tools.

- leafs[i] = keccak256(bytes.concat(keccak256(ltrim64(abi.encode(data)))));
+ leafs[i] = keccak256(abi.encodePacked(data[0], data[1]));
Updates

Lead Judging Commences

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

Support

FAQs

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