Component: ghost-preclaims @ 1538567b4651f9daddef534192723ff671693d35 — forester.py (around lines 122-134).
Description
output_packages is keyed by address, so when one address owns several NFTs the later entries deterministically overwrite the earlier ones:
for item in snapshot_data:
...
output_packages[item['address']] = {
"index": item["index"],
"token_id": item["token_id"],
"shares": item["shares"],
"merkle_proof": ...
}
Only the last-processed NFT per address keeps a proof. Every other position owned by the same address can never be claimed. This is a correctness bug with direct claim impact, not nondeterminism: re-running is byte-identical (verified across two different PYTHONHASHSEED values).
Impact
Multi-NFT owners lose every position except the last one, silently. Verified against the committed production data: snapshots/11155111.json holds 145 items but preclaims/11155111.json has only 139 keys. Concrete case: 0x2E65b5A2c13688046165423cC2174b922B887bA5 holds 7 tokens (indexes 4, 7, 8, 11, 15, 30, 33) but only index 33 gets a Merkle proof, so 6 claims are unclaimable. Duplicate addresses also exist in several other committed snapshot files (e.g. 1.json, 137.json, 56.json), so the data loss is not limited to one chain.
Reproduction
Local only (Python venv, no network), run from the repo root:
import json
from collections import Counter
snap = json.load(open("snapshots/11155111.json"))
d = json.load(open("preclaims/11155111.json"))
print("snapshot items:", len(snap), "| preclaim keys:", len(d))
# -> snapshot items: 145 | preclaim keys: 139
c = Counter(x["address"] for x in snap)
a = [x for x, n in c.items() if n > 1][0]
print(a, [(x["index"], x["token_id"]) for x in snap if x["address"] == a])
print("kept:", d[a]["index"], d[a]["token_id"])
# -> 0x2E65b5A2c13688046165423cC2174b922B887bA5 [(4,5),(7,8),(8,9),(11,12),(15,16),(30,31),(33,34)] kept: 33 34
Severity: High
Reason: real, verified loss of claims in committed production data (6 claims lost for one address alone). Not Critical because no third party can steal funds; the loss is claims the rightful owner cannot make.
Recommendation
Key output_packages by index/token_id instead of by address, or make each per-address value a list of per-token proofs. Re-run forester.py over the existing snapshots to regenerate complete preclaim files.
Verification status
VERIFIED. Reproduced locally against the committed data files; deterministic.