Repository navigation
merkle-allowlist tool emits keccak256 Ethereum proofs: incompatible with claim_with_proof SHA-256 verification #425
Description
Problem
tools/merkle-allowlist builds allowlist proofs for an Ethereum contract, not for the Soroban aid_escrow contract that actually verifies them. The tool's leaf and hash primitives are incompatible with the contract's verifier on every axis:
// tools/merkle-allowlist/index.js
const { ethers } = require('ethers');
const { MerkleTree } = require('merkletreejs');
const keccak256 = require('keccak256');
function makeLeaf(entry) {
// keccak256(abi.encodePacked(address, amount))
return ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(['address','uint256'],
[entry.address.toLowerCase(), ethers.BigNumber.from(entry.amount).toString()]));
}tools/merkle-allowlist/package.json depends on ethers ^5.7.2, merkletreejs, and keccak256 — the EVM stack. The contract verifier instead uses SHA-256 over a different leaf:
// app/onchain/contracts/aid_escrow/src/lib.rs
fn hash_address(env: &Env, address: &Address) -> [u8; 32] {
let digest = env.crypto().sha256(&data); // sha256(claimant_address_string)
...
}Consequences, distinctly:
- Proofs are silently useless. A root generated by
tools/merkle-allowlistis keccak256-based and amount-bound, butverify_merkle_proof_for_claimantrecomputes a sha256(address-string) leaf. Every recipient proof against such a root fails withError::InvalidProof. The tool prints a "valid" result locally (tree.verify(...)succeeds against its own keccak256 tree) while the on-chain claim always rejects it — a false-positive smoke signal. - Optional "on-chain" verification targets the wrong chain.
maybeCallOnchaininstantiatesethers.JsonRpcProvider+ethers.ContractagainstMERKLE_CONTRACT_ADDRESS/MERKLE_CONTRACT_ABI_PATH, which cannot talk to a Soroban contract. The environment variables have no Stellar/Soroban equivalent. - No tool produces the contract's actual leaf. The contract leaf is
sha256(claimant.to_string()); nothing in the repo generates that leaf format, so there is no supported way to publish a root forclaim_with_proof'smerkle_rootmetadata.
docs/security/audit-plan.md explicitly scopes "Merkle Proofs: Verification limits and prevention of resource-exhaustion DoS vectors on Stellar/Soroban", so this mismatch sits exactly in the audited surface.
Root cause
The tool was built against the EVM Merkle pattern (keccak256 + abi.encodePacked + ethers) and never reconciled with the Soroban contract's env.crypto().sha256 verifier and address-string leaf.
Why this is architecturally hard
- The leaf encoding is a cross-repo contract. The tool and the contract must agree on a canonical leaf byte string (address string only vs. address+amount, and its exact byte layout). Getting this wrong only moves the bug; the fix is to define the encoding, document it, and encode it identically in JS and Rust.
- SHA-256 with sorted-pair Merkle in JS. The contract sorts sibling pairs by byte order (
current <= sibling) and hashesleft || right(32+32 bytes) with SHA-256. The tool must replicate that pairing rule withcrypto's sha256, notmerkletreejs's keccak256 assumptions — a real implementation, not a dependency swap. - Existing roots break. Any root already published in package
merkle_rootmetadata (if any were generated) becomes invalid under a corrected encoding; the fix must state whether old roots are migrated or abandoned. - It feeds the amount-binding question. The tool binds
amountinto the leaf while the contract binds only the address; fixing the hash/ecosystem must be coordinated with the separate amount-binding issue so the tool and contract converge, not diverge further.
Proposed design
Rewrite tools/merkle-allowlist on Node's crypto.createHash('sha256') with a documented leaf format that matches hash_address (sha256 of the Address string), implement the contract's left/right sorted-pair combine, and drop ethers/merkletreejs/keccak256. Keep the tamper/negative test scenarios but assert them against the Soroban verifier. Offer a makeLeaf that matches the contract exactly, and note the amount-binding question as a dependency rather than silently encoding amount.
Downstream impact
The tool is the only path to producing merkle_root metadata consumed by claim_with_proof in app/onchain/contracts/aid_escrow/src/lib.rs. Any leaf/hash change here must be validated against that verifier, and docs/onchain/api.md / the contract README should document the canonical encoding so indexers and future tools reuse it.
Acceptance criteria
Tool
-
node index.jsgenerates a root and proof thatverify_merkle_proof_for_claimant(sha256, sorted-pair) accepts for a known leaf; a test or smoke script demonstrates on-chain acceptance. - The
valid,invalid_proof_path,wrong_recipient,wrong_leaf, andmismatched_rootscenarios all behave correctly against the Soroban leaf encoding. -
ethers,merkletreejs, andkeccak256are removed fromtools/merkle-allowlist/package.json.
Documentation
-
tools/merkle-allowlist/README.mddocuments the exact leaf byte string and hash/pairing algorithm so Rust and JS stay in sync.
Out of scope
Binding amount (or package_id) into the contract leaf is a separate issue; do not silently add it here. Contract-side proof-length and merkle_root_expires_at behavior are also separate.
Getting started
Files: tools/merkle-allowlist/index.js, tools/merkle-allowlist/package.json, app/onchain/contracts/aid_escrow/src/lib.rs (hash_address, hash_pair, verify_merkle_proof_for_claimant).
cd tools/merkle-allowlist
npm start # node index.jsGood first files to read: src/lib.rs hash_address/hash_pair/verify_merkle_proof_for_claimant (the exact algorithm to replicate), then tools/merkle-allowlist/index.js.
Activity
ibrahimmosouf-png commentedon Aug 19, 2026
Hi team, I want to work on this issue, please assign me I have experience.
grantfox-oss commentedon Aug 20, 2026
🦊 GrantFox — @ibrahimmosouf-png has been assigned to this issue as part of the Third Campaign campaign!
Next steps:
Closes #425)Good luck! Track your progress on GrantFox.
grantfox-oss commentedon Aug 20, 2026
🎉 This issue has been marked as completed on GrantFox as part of the Third Campaign campaign!
@ibrahimmosouf-png's PR #448 was approved and merged by @kilodesodiq-arch.
🏆 @ibrahimmosouf-png: You earned 35 FoxPoints for this contribution! Your current tier: Explorer (624 total points). Track your full progress on GrantFox.
👏 Great work, @ibrahimmosouf-png! Keep contributing to ChainForgee.