Skip to content

feat(exit-certificate): state-dump Step A, zero-address & premint handling, ignoreAddresses, capMode (EAT-02)#1701

Draft
joanestebanr wants to merge 13 commits into
feature/exit-certificate-toolfrom
feat/exit-certificate-tool_eat02-passive_erc20_omitted
Draft

feat(exit-certificate): state-dump Step A, zero-address & premint handling, ignoreAddresses, capMode (EAT-02)#1701
joanestebanr wants to merge 13 commits into
feature/exit-certificate-toolfrom
feat/exit-certificate-tool_eat02-passive_erc20_omitted

Conversation

@joanestebanr

Copy link
Copy Markdown
Collaborator

🔄 Changes Summary

  • Step A replaced (imported from feat(exit-certificate): alternative Step A (state dump + Transfer logs) #1687): addresses are discovered via a debug_accountRange state dump + Transfer event logs per wrapped token, instead of debug_traceTransaction over the whole chain history — O(#accounts) vs O(#txs), and it finds passive token-only holders that tracing structurally misses. Strategy selectable via options.addressDiscovery (auto | stateDump | logs | both); auto falls back to receipt harvesting when debug_accountRange is unavailable. Unlike feat(exit-certificate): alternative Step A (state dump + Transfer logs) #1687 it lands as the Step A (no opt-in aalt variant); the trace-based A1/A2 implementation is removed.
  • Zero address kept in Step A0x00…00 can hold value like any other account (a plain transfer(0x0, amount) is not a burn and native ETH can be sent there); dropping it left that value uncovered and the certificate unbalanced against the LBT.
  • options.nativeSCLockedFromContracts (imported from feat(exit-certificate): alternative Step A (state dump + Transfer logs) #1687): Step C measures the native token's SC-locked value from the actual ETH balances held by contracts (bridge reserve excluded) instead of LBT − EOA, which underflows and clamps to 0 on chains with a native genesis premint, silently dropping contract-held ETH.
  • options.genesisPrefundETHWei: Step F discounts genesis-minted native (no matching agglayer deposit) from the native LBT entry before comparing against the agglayer balance and certificate sum.
  • options.ignoreAddresses: balances of the listed accounts are fetched (recorded in step-b-ignored-balances.json) but excluded from the EOA exits; their value rolls into SC-locked and is bridged to exitAddress instead.
  • options.capMode (amount default | appearance): allocation order when Step F caps a mismatched certificate (ignoreBalanceMismatch=true) — amount caps the largest holders first.
  • Step F dumps the agglayer LBT to step-f-agglayer-lbt.json whenever agglayerAdminURL is set, even when the check fails.
  • Docs: README/CLAUDE.md audit fixes and documentation of all the new options.

⚠️ Breaking Changes

  • 🛠️ Config: options.ignoreOnTraceError removed (the trace-based Step A no longer exists).
  • 🔌 API/CLI: --step a1 / --step a2 removed (a has no sub-steps anymore); step-a1-*, step-a2-* and step-a-failed-traces.json are no longer produced.

📋 Config Updates

  • 🧾 New options (see parameters.toml.example): addressDiscovery = "auto", nativeSCLockedFromContracts = false, genesisPrefundETHWei = "", ignoreAddresses = [], capMode = "amount".

✅ Testing

  • 🤖 Automatic: go test ./tools/exit_certificate/... and golangci-lint clean. New unit tests for Step A discovery (pagination, dialect detection, Transfer extraction, auto fallback, truncation/empty-dump guards), zero-address handling, native SC-locked from contracts, genesis prefund and cap allocation.
  • 🖱️ Manual: the state-dump Step A approach was validated in feat(exit-certificate): alternative Step A (state dump + Transfer logs) #1687 against Bali (block 24,000,064): ~2m45s vs ~7h46m for the trace-based scan, 321,581 addresses (99.95% coverage of the trace set plus 5,987 extra real holders).

🐞 Issues

🔗 Related PRs

🤖 Generated with Claude Code

joanestebanr and others added 13 commits July 1, 2026 14:05
Add an optional options.ignoreAddresses list of accounts whose balances
must not be returned to them in the exit certificate. Step B1 still
fetches their ETH and token balances (recorded in
step-b-ignored-balances.json for traceability) but excludes them from
both EOABalances and Accumulated via extractIgnoredBalances. Because
their value no longer counts as EOA-held, it rolls into the per-token
SC-locked total and Step D bridges it to exitAddress, so the certificate
still balances against the LBT total (Step F stays green) and no exit is
ever created back to the ignored address.

LoadConfig validates each entry as a non-zero hex address. Documented in
the tool CLAUDE.md and both parameters examples.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When Step F caps a certificate (ignoreBalanceMismatch=true), the new
options.capMode selects how each token's budget is allocated to its
exits: "appearance" (default, the previous behavior) serves exits in the
order they appear; "amount" serves the largest-amount exits first, so big
holders are kept intact and small ones are capped/dropped once the budget
runs out. In both modes the surviving exits are emitted in their original
order, so downstream ordering (Step G) is unaffected.

Refactor capCertificateExits to compute the per-exit outcome over a
priority order (capAllocationOrder) and emit in original order; extract
capExitCopy. LoadConfig validates capMode. Docs and both parameters
examples updated; stale "proportional scaling" wording corrected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Flip the CapModeByAmount allocation order from largest-first to
smallest-first, so the budget is consumed by the small exits and the
largest-amount exits are the first to be capped/dropped once it runs out.
Surviving exits are still emitted in their original order. Update tests
and docs accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make "amount" the default cap allocation mode (largest exits capped
first) instead of "appearance". Update the default, tests, docs and both
parameters examples.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step F now queries admin_getTokenBalance once at the very start whenever
agglayerAdminURL is configured and writes the full response (the agglayer's
local balance tree for l2NetworkId) to step-f-agglayer-lbt.json before any
comparison runs. The dump therefore persists even when Step F later fails on
a balance mismatch. In agglayer mode the same response is reused for the
comparison to avoid a second RPC round-trip.

Also adds scripts/get-agglayer-lbt.sh, a standalone curl helper that issues
the same admin_getTokenBalance request from the command line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-agglayer-lbt filename

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…r LBT dump in README

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ken is optional

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- stepAWindowSize default is 150000, not 5000 (README + stale struct comment)
- document rollupManagerAddress config field (used by Step WAIT)
- Step D also builds ERC-20 holder-bridge exits (3rd category)
- Step C also writes step-c-holder-bridges.json
- step-e-exit-certificate.json is conditional (not written on unclaimed-abort path)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…genesis-minted native in Step F

Genesis pre-funded native tokens inflate the native-token LBT total without a
matching agglayer deposit, forcing the Step F comparison to mismatch (and the
certificate to be capped). The new options.genesisPrefundETHWei lets the operator
declare that amount (in Wei); Step F subtracts it from the native LBT entry (the
gas token, identified by a zero WrappedTokenAddress) before both the agglayer and
offline comparisons — flooring at zero — so the check balances against the
genuinely bridged amount. Affects the Step F comparison and cap budget only; the
Step 0 LBT and Step C SC-locked totals are untouched. Validated by LoadConfig as a
non-negative base-10 integer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Transfer logs (from #1687)

Step A now discovers value-holding addresses from the final state instead of
replaying the whole chain history with debug_traceTransaction:

- debug_accountRange state dump at the target block (geth/erigon dialect
  auto-detected) -> every ETH holder and contract in O(#accounts).
- Transfer event logs per wrapped token from genesis -> every token holder,
  including token-only EOAs that tracing structurally misses.
- options.addressDiscovery selects the strategy (auto | stateDump | logs |
  both); auto falls back to receipt harvesting when debug_accountRange is
  unavailable.

The trace-based A1/A2 implementation is removed along with the a1/a2
sub-steps, ignoreOnTraceError and the step-a1/a2/failed-traces files.
Step A writes the same step-a-addresses.json, so Steps B-D are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cate balances the LBT

The zero address was unconditionally dropped from the collected address set
(final Step A filter, Transfer-log topic extraction and receipt harvesting).
But 0x000...000 can hold value like any other account: a plain
transfer(0x0, amount) is not a burn (the tokens stay in totalSupply) and
native ETH can be sent there too. Dropping it left that value uncovered by
the certificate, so the per-token totals did not reconcile with the LBT.

The zero address is now treated like any other address: collected in Step A,
balance-scanned in Step B and covered by the certificate exits.

Fixes #1700

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… native SC-locked from contract balances (from #1687)

On chains with a native genesis premint, the Step C formula
LBT − EOA_accumulated underflows for the native token (EOA balances include
the premint but the LBT only measures bridge outflow), gets clamped to 0,
and the ETH actually held by contracts silently disappears from the
certificate.

With options.nativeSCLockedFromContracts=true, Step C instead measures the
native SC-locked value directly: it sums eth_getBalance of every Step B
contract at the target block (excluding the L2 bridge, whose balance is the
un-released native reserve) and uses that as the native token's SC-locked
value. Wrapped tokens keep the LBT − EOA formula. Defaults to false.

In this mode --step c needs the L2 RPC, the Step 0 target block and
step-b-contract-addresses.json (run Step B first). Usually combined with
options.genesisPrefundETHWei so the Step F comparison also accounts for the
premint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Jul 3, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant