Repository navigation
Migration audit: validate snapshot ownership and reward completeness #4646
Description
Context
The snapshot CSV is the source of truth for migration entitlements. The audit read the actual producer in #4613 at 249d7f843081fd20fea5f364b5094820fc29339f, not current main. That PR is still open/conflicting. Coordinate this fix with its owner; the fix depends on that producer being integrated, and must not be implemented against unrelated main code. No dependency on the Hub/backend fixes.
Epic: https://github.com/zeta-chain/zeta-migration/issues/65
Local audit: ~/.agents/features/zeta-solana-migration/research/audit_findings/2026-10-14/README.md; snapshot.md, snapshot-refutation.md, specialist-accounting-edge-cases.md and snapshot-repro/ hold exact evidence. The saved historical H17619782 export matches all 5,765,292 holder rows and pool/remainder categories. No actual historical missing balance was demonstrated.
Fixes
-
SNAP-01 (Medium): omitted staking ownership passes every producer check.
cmd/zetatool/snapshot/parse.go:24-46accepts absent staking lists;compute.go:138-192credits only records present;:233derives remainder as supply minus attribution. CLI checks atcmd/zetatool/cli/snapshot.go:224-250therefore accept a missing delegation or unbonding entry. A consistent synthetic fixture has holder 110, remainder 5 and supply 115. Removing its unpinned 100 delegation or unbonding record, while leaving bank supply unchanged, gives holder 10 and remainder 105 with exit 0 even under--supply-checkand a valid funded pin. Canonical export/height/hash controls substantially lower likelihood; this is a missing acceptance check, not a public claimant modifying the input.Change: validate the full staking population before filtering pins, modules or other nonclaimable holders. Sum delegation shares by validator with SDK decimal semantics and compare with each validator's declared total. Reject duplicate/unresolved records. Reconcile the two pool balances against validator-token liabilities by status and every current unbonding entry. Use validator token totals for backing; individually floored holder claims naturally leave dust. Explain each remainder category and the conversion/attribution rounding separately. Keep SDK valuation and current pin/minimum/payout decisions; no new service or new claim type.
A simple
pool >= listed liabilitiescheck will still accept the missing-unbonding fixture. Verify the actual chain/SDK pool invariant. If legitimate excess is permitted, require independent height-H evidence explaining it before publication; do not silently approve any positive residual, or falsely reject a chain-valid residual. A record count from the same incomplete JSON is not independent evidence. By default, report and refuse unexplained residuals. Document the exact reviewed evidence and acceptance step for any chain-valid excess, whether implemented in the producer or existing publication procedure. A generic skip-supply-check escape does not satisfy this requirement. Use existing canonical export/provenance plumbing for the smallest secure acceptance check.Verify: complete fixtures and the archived real export pass. Removing one delegation fails with that validator's share mismatch. Removing one unbonding entry produces a specific unexplained-backing/completeness refusal. Restoring it restores the pass. Cover pinned/zero/nonstandard holders with stake and unbonding, all validator statuses, slash-adjusted shares, fractional rounding, zero-token validators, legitimate evidenced residuals and duplicate identities. Assert the intended errors. Remove each new guard term in a disposable mutation check and confirm its regression fails.
-
SNAP-01, reward input coverage: the parser ignores distribution state, so an export with unwithdrawn staking rewards also passes. A paired fixture has supply 125. With 10 still outstanding and backed in the distribution module, output is holder 110/remainder 15; after settling the same reward into bank, output is holder 120/remainder 5. Both currently exit0. Require the expected zero-height distribution postcondition before accepting the input: the distribution module is present, and remaining current rewards, outstanding rewards and accumulated commissions satisfy the canonical export's zero-state guarantee. Keep explained community-pool fractions as remainder; never credit them twice. The backend's mandatory
--for-zero-heightalready protects its normal path, so this is part of the existing acceptance gap, not a demonstrated failed historical payout. Test omitted distribution and nonzero residual rewards/commission explicitly; valid zero-height exports pass. Ordinary distribution rewards and observer emissions are separate ledgers.
Accounting evidence and limits
User clarification, 2026-10-14: observer emissions are handled offline through a consolidated collection address that migrates to the foundation treasury. Contract balances, including pool backing, also migrate to the treasury. Individual observer or contract-beneficiary allocation is not a migration prerequisite. The user accepts this treatment because observers are primarily internal and most pools are company-funded. This is an allocation decision, not evidence that every underlying holder is the company.
At final H, identify the actual emissions collection address and contract inventory, reconcile their liquid, staked and unbonding balances, and record the treasury destination. Count each source balance once through its existing snapshot row or pinned/module remainder route. A pre-H emissions transfer moves backing from the module to the collection address; never credit both. Confirm treasury delivery separately from snapshot accounting. Ordinary delegator rewards and validator commissions still require the canonical export checks. No observer liability census or LP/LST/lending beneficiary reconstruction is required to approve this treasury allocation. Preserve archive retention, minimum policy and the remaining delivery work. SNAP-01 still applies to omitted staking ownership and unsettled ordinary distribution rewards.
Not in this issue
No balance redistribution, remainder payout, threshold change, contract unpinning, LP/LST claim implementation, live withdrawal or deployment. The feature-local emissions helper has a separate epic checklist entry; it is not a node-repository file. The Hub's hidden eligible sibling and wallet switch are separate fixes.
Done when
- Both omission fixtures fail for the intended completeness reason; restored fixtures and the historical export pass.
- All holder, pin, module, staking-rounding and conversion-dust totals reconcile without double counting backing.
- Snapshot unit/CLI tests pass with the producer's compatible Go toolchain; the audit used Go 1.23.9 because newer Go failed in its old Sonic dependency.
- Update producer/runbook documentation with exact acceptance guarantees and remaining final-H evidence. CI passes; open a reviewed PR coordinated with feat(snapshot): add ZETA snapshot tooling (state export + per-address balance compute) #4613 and the repo's required checks.
Activity
kingpinXD commentedon Oct 14, 2026
Settling the open question on pool excess, plus where this lands:
x/staking/keeper/genesis.go:156-171panics on import unless the bonded pool equals bonded validator tokens and the not-bonded pool equals unbonded/unbonding validator tokens plus every unbonding entry. Both pools are also inblockedReceivingModAcc(app/app.go:186-194). So the producer refuses any difference; no "explained excess" path is needed.feat/snapshot-export(feat(snapshot): add ZETA snapshot tooling (state export + per-address balance compute) #4613), which never needs to merge. This fix goes in as a PR into that branch, tested locally withGOTOOLCHAIN=go1.23.9.kingpinXD commentedon Oct 15, 2026
Done in #4647, merged into feat/snapshot-export as b9b0ea5: per-validator share sums, exact staking pool balances, the zero-height distribution checks including the SDK's distribution module-balance rule, and the remainder broken out by category. Both omission fixtures fail with their specific errors, and the H17619782 re-run writes a byte-identical CSV (sha256 bd62b8bd…cb93). zetatool is built from that branch for the final snapshot; #4613 does not merge.