Component: ghost-preclaims @ 1538567, finder.py line 16 (parser.add_argument("--block", default="latest", ...)), ~lines 88, 95, 104, 106.
Description: The default "latest" is passed raw as block_identifier to every RPC call (totalSupply, ownerOf, collateralBalances). There is no pinning to a concrete block number, and the block number is not stored in the output. Worse: even within a single run, totalSupply is fetched at the start and per-token queries follow with time.sleep(0.3) between them, so the chain head can advance mid-scan and tokens in one snapshot come from different blocks.
Impact: Identical CLI input at different times produces divergent snapshots and downstream merkle roots, silently (no block field in the JSON to detect it).
Reproduction: Static line-by-line analysis. Mechanism: "latest".isdigit() → False → the string "latest" is passed to all block_identifier params. Suggested reproduction: two runs of finder.py --rpc <local-anvil-fork> (no --block) with one new block mined in between → snapshots/<chain>.json differs.
Severity: Medium. Reason: real silent state divergence from identical input. Not High because the operator CAN pin --block <n> manually.
Recommendation: (1) Resolve "latest" once at startup to an integer (w3.eth.block_number) and use that integer for all calls; (2) write the block number into the snapshot JSON (a "block" field per file); (3) consider making --block required.
Verification: VERIFIED (static; live execution outside the task’s rules).
Thank you for your report!
You’re correct that anyone who wants to reach the same result should run it with the exact same flags. As it happens, we have a few issues with latest, so the only reliable way is to run it in historical mode.
In any case, this CLI - like any other - will require the user to pass CLI args, for example --bounty or whatever it ends up being called. The idea was to allow flexibility in case the community decides to set a minimum for the fact of owning an NFT. Moreover, because of multiple networks, it’s hard to predict hardcoded default values. Any thoughts on this?
Thanks for the context. I agree historical mode is the reliable path and that hardcoding defaults across networks would be wrong. But the core of the finding is not about flexibility, it is about silent divergence:
- Even a single run with identical flags can mix blocks:
totalSupply is read at the start and per-token queries follow with time.sleep(0.3) between them, so the head can advance mid-scan. “Same flags” does not guarantee “same result” even back-to-back.
- The output records no block number, so two divergent snapshots produce divergent Merkle roots with nothing in the JSON to tell them apart downstream.
A minimal fix keeps all the flexibility you describe: resolve "latest" once at startup (w3.eth.block_number) and use that integer for every RPC call in the run, then write it into the snapshot JSON as a "block" field. No hardcoded per-network defaults, --bounty-style flags stay as they are, but runs become reproducible and divergent outputs become detectable. Would that fit the CLI’s direction?
Gotcha - that totally makes sense, because some networks have a pretty large number of NFTs (up to 200k), so the state can diverge during the process. Maybe the best approach is to simply remove the latest optionality and make --block mandatory in any case. Any thoughts?
I agree, making --block mandatory is cleaner than my resolve-once proposal. It removes the footgun instead of papering over it.
One addition worth keeping from the report: write the block number into the snapshot JSON (alongside the rows). With --block mandatory, runs are reproducible, but the output still carries no record of which block produced it. A “block” field makes divergent outputs detectable downstream and gives forester something to check before building a root. That also covers the resume-across-blocks case: if a snapshot is resumed with a different --block, the mismatch is visible instead of silent.
So: mandatory --block plus a block field in the JSON. For the large-NFT networks you mentioned, the operator picks a finalized block once and every run is pinned to it.