Skip to content

feat(exit-certificate): adapt certificate generation to testnet genesis premints (genesisPrefundETHWei, capMode, nativeSCLockedFromContracts)#1694

Merged
joanestebanr merged 23 commits into
feature/exit-certificate-toolfrom
feat/exit-certificate-ignore-address
Jul 6, 2026
Merged

feat(exit-certificate): adapt certificate generation to testnet genesis premints (genesisPrefundETHWei, capMode, nativeSCLockedFromContracts)#1694
joanestebanr merged 23 commits into
feature/exit-certificate-toolfrom
feat/exit-certificate-ignore-address

Conversation

@joanestebanr

@joanestebanr joanestebanr commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

🎯 Motivation / Context

This PR adapts the exit-certificate tool to the particularities of testnets, where native funds
are pre-loaded at genesis (e.g. 110,000 ETH on Kurtosis enclaves, 100,000 ETH on Cardona). That
premint breaks several steps of the pipeline, because it inflates account balances without any
matching agglayer deposit:

  • The Step F balance check fails: the certificate's native exits include the premint, but the
    agglayer balance and the Step 0 LBT only count genuinely bridged funds.
  • The Step C native SC-locked formula (LBT − EOA_accumulated) underflows, clamps to 0 and
    silently drops contract-held ETH from the certificate.
  • The premint itself has no collateral on the agglayer, so it can never be bridged out — the
    certificate must always be capped to the bridged amount.

🔄 Changes Summary

  • options.genesisPrefundETHWei — declares the native amount (Wei, decimal string) preminted at
    genesis:
    • Step B verifies the declared value against the detected genesis ETH preload total (sum of
      balances at block 0); a mismatch is fatal even with ignoreGenesisBalance=true, since Step F
      would otherwise discount the wrong amount. The genesis-preload error message now explains that
      pre-funding accounts at genesis is not allowed and points at ignoreGenesisBalance=true.
    • Step F subtracts the value from the native-token certificate sum before both the
      agglayer three-way and offline comparisons (via discountGenesisPrefund, floored at zero),
      logging the certificate total, the pre-fund and the difference — so the check balances against
      the genuinely bridged amount.
    • The pre-fund has no agglayer collateral, so even when every check matches Step F emits
      step-f-capped-certificate.json trimming the native exits to min(agglayer, lbt) — no
      ignoreBalanceMismatch needed.
    • Validated by LoadConfig (non-negative base-10 integer).
  • options.capMode"amount" (default) or "appearance". Controls how Step F allocates each
    token's cap budget when trimming exits. "amount" serves the smallest-amount exits first so the
    largest holders (the premint whales) are capped/dropped first; "appearance" serves exits in
    appearance order. Surviving exits keep their original order in both modes. capCertificateExits
    reworked into a greedy per-token allocator.
  • options.nativeSCLockedFromContracts — when true (the default), Step C measures the native
    token's SC-locked value directly by summing the ETH held by contract accounts at targetBlock
    (excluding the L2 bridge reserve), instead of deriving it as LBT − EOA_accumulated, which
    underflows on premint chains. Wrapped tokens keep the formula; set to false to fall back to the
    derivation for the native token.
  • Step A no longer excludes the zero address0x000…000 can hold value (e.g. burned funds)
    that the certificate must account for, so it stays in the collected address set.
  • Step F agglayer LBT dump — whenever agglayerAdminURL is set, Step F queries
    admin_getTokenBalance once up front and writes the raw LBT to step-f-agglayer-lbt.json,
    regardless of comparison mode (skipped when OutputDir is unset, i.e. hand-built configs). In
    agglayer mode the response is reused (no second RPC). Added scripts/get-agglayer-lbt.sh to fetch
    the same LBT manually.
  • Step F mismatch logs now include the signed certificate−agglayer / certificate−lbt
    differences (collapsed into one when LBT == agglayer).

⚠️ Breaking Changes

  • capMode defaults to "amount": with ignoreBalanceMismatch=true, the default cap outcome
    changes (previously appearance-order allocation).
  • nativeSCLockedFromContracts defaults to true: Step C now measures the native SC-locked value
    from contract balances by default (set to false for the previous LBT − EOA derivation).
  • Step A now includes the zero address in the collected set, so its balances can appear in the
    certificate.

📋 Config Updates

Three new fields under options (all reflected in parameters.toml.example /
parameters.json.example):

  • 🧾 genesisPrefundETHWei — amount of native token preminted at genesis, in Wei, as a
    decimal string.
    • Purpose: tells the tool how much native value exists on the chain without agglayer collateral.
      Step B verifies it against the preload actually detected at block 0 (a mismatch aborts, even
      with ignoreGenesisBalance=true); Step F discounts it from the native certificate sum before
      the balance comparison and always emits a capped certificate trimming the native exits to
      min(agglayer, lbt), since the premint can never be bridged out.
    • Values: "" (default — feature disabled, no discount and no premint capping) or any
      non-negative base-10 integer, e.g. "100000000000000000000000" (100,000 ETH). Negative or
      non-numeric values are rejected by LoadConfig.
  • 🧾 capMode — allocation order used when Step F trims a token's bridge exits to its cap
    budget.
    • Purpose: decides which holders absorb the cut when the certificate's exits exceed the
      collateral (mismatch with ignoreBalanceMismatch=true, or the premint capping above).
    • Values: "amount" (default) — serves the smallest exits first, so the largest holders
      (typically the premint whales) are capped/dropped first and small legitimate holders keep their
      full exits; "appearance" — serves exits in the order they appear in the certificate. Any other
      value is rejected by LoadConfig. Surviving exits keep their original order in both modes.
  • 🧾 nativeSCLockedFromContracts — how Step C computes the native token's SC-locked value
    (the amount bridged to exitAddress).
    • Purpose: the derived formula LBT − EOA_accumulated underflows on premint chains (EOA
      balances include the premint, the LBT does not), clamping to 0 and silently dropping
      contract-held ETH from the certificate.
    • Values: true (default) — measure it directly by summing eth_getBalance of every contract
      account at targetBlock, excluding the L2 bridge (its balance is the un-released native
      reserve); false — keep the LBT − EOA derivation. Wrapped tokens always use the formula,
      regardless of this flag.

Example configs updated accordingly:

  • 🧾 config-examples/zkevm-cardona.toml sets genesisPrefundETHWei to the 100,000 ETH minted at
    genesis on Cardona.
  • 🧾 scripts/configuration_based_on_kurtosis.sh sets genesisPrefundETHWei to the 110,000 ETH
    preminted by the Kurtosis enclave (overridable via the GENESIS_PREFUND_ETH_WEI env var).

✅ Testing

  • 🤖 Automatic: go test ./tools/exit_certificate/... — added tests for the pre-fund discount
    and always-cap behaviour (TestDiscountGenesisPrefund,
    TestCompareTokenBalances_GenesisPrefundDiscount, TestRunStepF_PrefundMatchedStillCapsToLBT,
    TestRunStepF_NoPrefundNoCapOnAllMatch), the Step B preload verification
    (TestCheckDeclaredGenesisPrefund, TestRunStepB1GenesisPrefundDeclared), the Step C contract
    measurement (TestRunStepC_NativeSCLockedFromContracts, TestSumContractNativeBalances,
    TestApplyNativeContractLocked), cap allocation (TestLoadConfig_CapMode,
    TestCapCertificateExits_ByAmount*), config loading (TestLoadConfig_GenesisPrefundETHWei) and
    the LBT dump guard (TestRunStepF_EmptyOutputDir_SkipsLBTDump).
  • 🧪 Manual: full pipeline run against a Kurtosis enclave
    (scripts/configuration_based_on_kurtosis.sh).

📝 Notes

  • An ignoreAddresses option (exclude specific accounts from the certificate exits) was initially
    part of this PR but was removed before merging: it is not in use and complicated other in-flight
    PRs. The branch name still reflects it; the branch will be deleted after merge.
  • README/CLAUDE.md docs were audited against the code and corrected (e.g. stepAWindowSize default,
    rollupManagerAddress, Step D holder-bridge exits, conditional Step C/E outputs).

🤖 Generated with Claude Code

joanestebanr and others added 6 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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fbd239ec2b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/exit_certificate/step_b.go Outdated
joanestebanr and others added 3 commits July 2, 2026 09:52
…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>
@joanestebanr joanestebanr self-assigned this Jul 2, 2026
@joanestebanr joanestebanr added enhancement New feature or request exit_certificate_tool Tool to create a final exit certificate labels Jul 2, 2026
@joanestebanr joanestebanr requested a review from Copilot July 2, 2026 09:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new exit-certificate configuration knobs to handle testnet “genesis-preload” balances (via address exclusion and configurable capping strategy), and improves Step F traceability by always persisting the agglayer-side LBT when an admin endpoint is configured.

Changes:

  • Introduces options.ignoreAddresses (validated) and wires Step B outputs to record+exclude those balances (step-b-ignored-balances.json).
  • Adds options.capMode (amount default / appearance) and updates Step F capping logic to allocate per-token budget according to the selected mode.
  • Updates Step F to dump agglayer LBT to step-f-agglayer-lbt.json when agglayerAdminURL is set, plus adds a helper script to fetch it manually.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tools/exit_certificate/types.go Extends Step B/F result structs and clarifies capped-certificate semantics.
tools/exit_certificate/step_f.go Adds agglayer LBT dump + capMode-aware capping allocator implementation.
tools/exit_certificate/step_f_test.go Updates existing cap tests and adds new coverage for CapModeByAmount.
tools/exit_certificate/step_b.go Extracts and removes ignored-address balances before building EOA/accumulated outputs.
tools/exit_certificate/step_b_helpers_test.go Adds unit test for extractIgnoredBalances.
tools/exit_certificate/scripts/get-agglayer-lbt.sh New helper script to fetch agglayer LBT via admin_getTokenBalance.
tools/exit_certificate/run.go Persists ignored-balances output and threads capMode into final-certificate capping.
tools/exit_certificate/README.md Documents new options and updated Step F behavior/output.
tools/exit_certificate/parameters.toml.example Adds capMode and ignoreAddresses examples and updates Step F description.
tools/exit_certificate/parameters.json.example Adds capMode and ignoreAddresses examples.
tools/exit_certificate/filenames.go Adds new output filenames for ignored balances and agglayer LBT dump.
tools/exit_certificate/config.go Adds options, defaults, validation, and merging for ignoreAddresses and capMode.
tools/exit_certificate/config_test.go Adds tests for ignoreAddresses and capMode validation/defaulting.
tools/exit_certificate/CLAUDE.md Updates internal tool documentation to reflect new options and Step F LBT dump.

Comment thread tools/exit_certificate/step_f.go Outdated
joanestebanr and others added 3 commits July 2, 2026 18:00
…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>
The option is not in use and complicates other in-flight PRs. Removes the
Options field, LoadConfig validation, extractIgnoredBalances in Step B1, the
step-b-ignored-balances.json output, and all related tests, examples and docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dona example config

Cardona minted 100000 ETH at genesis, which inflates the native LBT total
without a matching agglayer deposit; declaring it lets Step F balance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@joanestebanr joanestebanr changed the title feat(exit-certificate): add ignoreAddresses & capMode options, always dump agglayer LBT in Step F feat(exit-certificate): adapt Step F balance check to testnet genesis pre-funds (genesisPrefundETHWei, capMode) Jul 6, 2026
joanestebanr and others added 8 commits July 6, 2026 09:59
RunStepF wrote step-f-agglayer-lbt.json via cfg.Options.OutputDir, which is
empty for programmatically built configs (unit tests), dropping the file in
the process's working directory. LoadConfig always sets OutputDir, so an
empty value now skips the dump. Tests cover both the dump and the skip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ted genesis preload

The genesis-preload error now explains that pre-funding accounts at genesis
is not allowed and points at options.ignoreGenesisBalance=true to accept it.

When options.genesisPrefundETHWei is set, Step B additionally checks that the
declared amount equals the detected genesis ETH preload total. A mismatch is
fatal even with ignoreGenesisBalance=true, since Step F would subtract the
wrong amount from the native LBT entry.

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

The Step 0 native LBT only counts funds bridged in through the bridge
contract (balance at block 0 minus balance at target), so it already matches
the agglayer — the genesis pre-fund never inflates it. The inflated side is
the certificate: the pre-funded balances sit in accounts and become bridge
exits. Subtracting from the LBT also zeroed the cap budget
(min(agglayer, lbt)), which would drop every native exit when capping.

Step F now discounts the pre-fund from the native-token certificate sum in
both comparison modes, logging the certificate total, the declared pre-fund
and the resulting difference. The cap budget is untouched.

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

Each ❌ MISMATCH line now includes the signed certificate−agglayer and/or
certificate−lbt difference, so the size of the discrepancy is readable
without manual arithmetic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ch log

When the LBT and agglayer amounts are equal, certificate−agglayer and
certificate−lbt are the same value; show a single difference in that case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The zero address can hold value (e.g. burned funds) that the certificate
must account for, so Step A1/A2 and the merge no longer drop it from the
collected address set, and receipt parsing no longer filters it out.

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>
Measuring the native SC-locked value from actual contract balances is the
correct behaviour on any chain (and required on premint chains, where
LBT − EOA underflows); the LBT − EOA derivation remains available by
explicitly setting the option to false.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@agglayer agglayer deleted a comment from claude Bot Jul 6, 2026
@joanestebanr

Copy link
Copy Markdown
Collaborator Author

@claude

@joanestebanr

Copy link
Copy Markdown
Collaborator Author

@codex review

@claude

claude Bot commented Jul 6, 2026

Copy link
Copy Markdown

Claude finished @joanestebanr's task in 1m 57s —— View job


PR Review Complete ✅

🚀 What's New

This PR adapts the exit-certificate tool for testnets where funds are pre-loaded at genesis (typically 100,000 ETH). Genesis-injected native funds inflate the native-token LBT total without a matching agglayer deposit, causing Step F balance check failures. The PR introduces:

  • genesisPrefundETHWei option: Declares the genesis-minted amount so Step F subtracts it from the native LBT entry before comparing
  • capMode option: Controls how Step F allocates cap budgets when trimming exits ("amount" favors small holders, "appearance" uses original order)
  • nativeSCLockedFromContracts (default true): Step C measures native SC-locked value from contract ETH balances instead of LBT − EOA derivation
  • Step F agglayer LBT dump: Always persists step-f-agglayer-lbt.json when agglayerAdminURL is set
  • Step A includes zero address: 0x000…000 can hold value (e.g., burned funds) and is now included in address collection

🐛 Bug Fixes

None - this is primarily a feature enhancement for testnet support.

📋 Config Updates

  • genesisPrefundETHWei: Optional decimal string in Wei (e.g., "100000000000000000000000" for 100,000 ETH). Default: ""
  • capMode: "amount" (default) or "appearance". Default: "amount"
  • nativeSCLockedFromContracts: Boolean, default: true
  • Example: Cardona config now sets genesisPrefundETHWei = "100000000000000000000000"

⚠️ Breaking Changes

Minor behavior change: capMode defaults to "amount" (previously behavior was appearance-order). When ignoreBalanceMismatch=true, the default cap outcome changes to favor small holders over large ones.


Code Review Findings

🔴 Required Issues

None found - code quality is high with appropriate safety checks.

🟡 Suggested Improvements

1. Magic Number in decimalBase Constant (config.go, step_b.go, step_f.go)

Priority: Low
Location: Multiple files use base-10 parsing with inline decimalBase references

The code uses decimalBase (value 10) for base-10 integer parsing throughout. This is fine, but decimalBase isn't defined - I suspect it's defined elsewhere as a const. Consider making it a package-level constant if not already:

const decimalBase = 10

Current locations using it:

  • config.go:242, 214, 309, 321, 417
  • step_b.go:214
  • step_f.go:139, 154, 309, 415

2. Verbose Step F Mismatch Logging

Priority: Low
Location: step_f.go:93-108

The mismatch logging includes redundant information when LBT equals agglayer. The conditional formatting helps, but could be simplified:

if c.LBTAmount != "" && c.LBTAmount != c.AgglayerAmount {
    // Show both diffs only when different
}

This is actually well-handled already - good job!

3. Consider Extracting Cap Allocation Logic

Priority: Low
Location: step_f.go:477-589

The capCertificateExits function is quite complex (110+ lines). The logic is clear, but consider extracting the allocation order computation and cap application into separate helper functions for better testability:

// computeCapOutcomes(exits, remaining, mode) -> []capExit
// applyCapOutcomes(exits, outcomes) -> []*BridgeExit

Current implementation is acceptable, but this would improve maintainability.

4. Genesis Prefund Validation Edge Case

Priority: Low
Location: step_b.go:210-226 (checkDeclaredGenesisPrefund)

The validation compares declared genesis prefund against detected EOA balances at block 0. However, if an EOA was funded at genesis but is not in the Step A address collection (because it never transacted), the comparison would be incorrect. This is probably an acceptable risk since:

  • Such addresses would also be missing from the certificate
  • The guard is already strong enough for typical cases

Consider adding a note in the documentation about this edge case.

🟢 Optional Observations

1. Excellent Test Coverage

The PR includes comprehensive test coverage:

  • TestLoadConfig_GenesisPrefundETHWei
  • TestLoadConfig_CapMode
  • TestCapCertificateExits_ByAmountCapsLargest
  • TestCapCertificateExits_ByAmountDropsLargest
  • TestCheckDeclaredGenesisPrefund
  • TestRunStepB1GenesisPrefundDeclared
  • TestDiscountGenesisPrefund
  • TestCompareTokenBalances_GenesisPrefundDiscount
  • TestRunStepC_NativeSCLockedFromContracts

SonarQube reports 91.1% coverage on new code - excellent!

2. Good Separation of Concerns

The implementation cleanly separates:

  • Config validation (LoadConfig)
  • Genesis prefund detection (checkGenesisBalances)
  • Genesis prefund discount (discountGenesisPrefund)
  • Cap allocation logic (capCertificateExits, capAllocationOrder)

3. Comprehensive Documentation

README.md and CLAUDE.md are well-maintained and accurately reflect the new options. The PR description is exceptionally detailed.

4. Robust Error Handling

  • Genesis prefund mismatch is never suppressed (even with ignoreGenesisBalance=true)
  • Negative values are caught at config loading time
  • Invalid cap modes are rejected early
  • Step F LBT dump is skipped safely when OutputDir is empty

5. Good Logging Practices

The implementation includes appropriate logging at all levels:

  • Info level for normal operations
  • Warn level for mismatches and edge cases (e.g., genesis preload)
  • Debug level for detailed cap operation traces

Security Considerations

✅ No Security Issues Found

The implementation includes appropriate security measures:

  1. Input Validation: genesisPrefundETHWei is validated as non-negative base-10 integer at config load time
  2. Genesis Prefund Verification: Declared prefund is verified against detected genesis balances (line step_b.go:179-181)
  3. Underflow Protection: Genesis prefund discount is floored at zero (step_f.go:173-177)
  4. Safe Cap Budget: Cap budget stays min(agglayer, lbt) regardless of prefund discount
  5. No Privilege Escalation: New features don't introduce privilege escalation risks

Performance Considerations

✅ No Performance Concerns

  • Step F LBT dump: Only writes when OutputDir is set (avoids accidental file pollution)
  • Native contract balance sum: Uses existing batch RPC infrastructure with appropriate concurrency limits
  • Cap allocation: O(n log n) for amount-based sorting, which is acceptable for typical exit counts
  • Genesis balance check: Reuses existing fetchETHBalances infrastructure

Code Quality Assessment

✅ Excellent Overall

  • Go best practices: Follows Go conventions (errors wrapped with context, early returns, clear naming)
  • Repository standards: Adheres to CLAUDE.md guidelines (uses require in tests, proper error wrapping)
  • Type safety: Uses *big.Int for all amount handling (no float precision issues)
  • Immutability: capExitCopy creates deep copies to avoid modifying original certificate
  • Testing: Comprehensive unit tests with good edge case coverage
  • Documentation: README accurately describes new options with examples

Recommendations

For Immediate Merge

This PR is ready to merge. No blocking issues found.

For Future Improvements

  1. Consider extracting capCertificateExits helper functions for better testability (low priority)
  2. Document the genesis prefund EOA collection edge case in README (low priority)
  3. Verify decimalBase is defined as a package constant (trivial)

Summary

This is a well-designed, thoroughly tested, and carefully implemented feature for handling testnet genesis pre-funds. The code quality is high, with appropriate safety checks, comprehensive test coverage (91.1%), and excellent documentation. The implementation correctly handles the complex interaction between genesis prefunds, LBT totals, and agglayer balance comparisons.

Recommendation: ✅ Approve and merge

The minor suggestions above are optional improvements that don't block merging. Great work! 🎉


…ig generator

The Kurtosis enclave premints 110000 ETH at genesis, so the generated config
now declares it in options.genesisPrefundETHWei (overridable via the
GENESIS_PREFUND_ETH_WEI env var) for the Step F discount and the Step B
preload verification.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f7ad6bc5c9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/exit_certificate/step_f.go
joanestebanr and others added 2 commits July 6, 2026 15:09
…ficate

capCertificateExits budgeted every token with a RemainingBalance, including
matched ones. That was a harmless no-op before (match implied sum == budget),
but with the genesis pre-fund discount the native token can match while its
raw exits exceed min(agglayer, lbt) — so capping triggered by an unrelated
token would silently trim the pre-funded ETH. Matched tokens are now skipped,
which is also what the docs already described.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…yer, lbt)

The genesis pre-fund has no agglayer collateral, so it can never generate
bridge exits. Reverts the previous skip-matched-tokens capping: every token
is budgeted again (a no-op for matched tokens except the discounted native),
and finalizeStepFResult now produces the capped certificate even when every
check matches — trimming the native exits to min(agglayer, lbt) without
requiring ignoreBalanceMismatch.

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

sonarqubecloud Bot commented Jul 6, 2026

Copy link
Copy Markdown

@joanestebanr joanestebanr changed the title feat(exit-certificate): adapt Step F balance check to testnet genesis pre-funds (genesisPrefundETHWei, capMode) feat(exit-certificate): adapt certificate generation to testnet genesis premints (genesisPrefundETHWei, capMode, nativeSCLockedFromContracts) Jul 6, 2026
@joanestebanr joanestebanr merged commit 0aa3e8c into feature/exit-certificate-tool Jul 6, 2026
25 of 26 checks passed
@joanestebanr joanestebanr deleted the feat/exit-certificate-ignore-address branch July 6, 2026 14:08
joanestebanr added a commit that referenced this pull request Jul 8, 2026
…T-02) and zero adress issue (AET-08) (#1701)

## 🔄 Changes Summary
- **Step A replaced**: 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. Both sources always run and merge (the
dump covers native-ETH holders and contracts; the logs cover token
holders): they have complementary blind spots, so there is no strategy
option and no fallback — an unusable `debug_accountRange` fails Step A
instead of degrading silently. The trace-based A1/A2 implementation is
removed.
- **Zero address kept in Step A** — `0x00…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.
- **Extra ERC-20 holders discovered in Step A** — the `Transfer`-log
scan also covers `options.extraErc20Contracts` (deduplicated against the
wrapped tokens, and it runs even with no wrapped tokens). Step B3 only
probes `balanceOf` against the Step A address set, so a passive holder
of an extra token (no ETH/nonce/code, never touched a wrapped token) was
invisible to both sources and their collateral share flowed to
`exitAddress`; discovery stays in Step A (B3 never discovers addresses),
so `step-a-addresses.json` remains the complete address universe in
single-step mode.
- **`options.capMode` gains `"none"`, the new default** — capping the
certificate is a lossy operation and must now be opted into explicitly.
With `"none"`, Step F fails (`errCapForbidden`) as soon as any bridge
exit would have to be trimmed or dropped, on every capping path: the
mismatch cap (`ignoreBalanceMismatch=true`), the genesis pre-fund trim
(which caps even on allMatch, so `genesisPrefundETHWei` now requires a
trimming mode) and the final-certificate re-cap in the pipeline. A no-op
cap (everything fits the budget) still passes. `"amount"`/`"appearance"`
keep their previous trimming behavior.
- The Kurtosis config generator (`configuration_based_on_kurtosis.sh`)
now emits `capMode: "amount"` (its config declares
`genesisPrefundETHWei`, whose premint trim needs a trimming mode) and
`ignoreBalanceMismatch: false` (real mismatches must abort).
- **E2E harness extended with the AET-02 scenario and split into
numbered stages** (`test/e2e-exit_certificate/`): `10-run_network` →
`20-prepare_network` → `30-stop_sequencer` →
`40-generate_exit_certificate` → `45-check_exit_certificate` →
`50-submit_exit_certificate` → `60-claim_exit_certificate_funds` (the
old `run_exit_tool.sh` is gone). The sequencer stop now halts only the
block producers (`op-batcher`/`op-cl`), keeping the `op-el` RPC alive so
the tool can still read the frozen L2 state.

## ⚠️ Breaking Changes
- 🛠️ **Config**: `options.capMode` now defaults to `"none"`: runs that
relied on the previous `"amount"` default to trim the certificate (any
`genesisPrefundETHWei` run, or `ignoreBalanceMismatch=true` with real
mismatches) now fail in Step F unless `capMode` is set explicitly to
`"amount"` or `"appearance"`.
- 🛠️ **Config**: `options.ignoreOnTraceError` and
`options.stepAWindowSize` removed (the trace-based Step A and the
receipt-harvesting fallback no longer exist). `options.addressDiscovery`
(present in intermediate revisions of this PR, never released) is also
gone — Step A always runs both sources.
- 🔌 **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
- 🧾 `capMode` accepts `"none"` (new default) besides `"amount"` and
`"appearance"`; `zkevm-cardona.toml` sets `capMode = "amount"`
explicitly because its `genesisPrefundETHWei` always requires trimming.

## ✅ Testing
- 🤖 **Automatic**: `go test ./tools/exit_certificate/...` and
`golangci-lint` clean. New unit tests for Step A discovery (pagination,
dialect detection, Transfer extraction, source merging, extra-ERC-20
scanning — including token dedup and the no-wrapped-tokens case —,
truncation/empty-dump/dump-unavailable guards), zero-address handling,
and capMode `"none"` (no-op pass-through, fail-on-trim, fail-on-drop,
plus RunStepF-level prefund and mismatch failures).
- 🧪 **E2E (covers the AET-02 fix end-to-end)**:
`test/e2e-exit_certificate/run_e2e_test.sh` reproduces the exact bug
scenario on a Kurtosis enclave — `20-prepare_network.sh` deploys a
`MintableERC20` on L1, bridges it to L2 and transfers part of the
wrapped balance to a **passive account** that never sends any L2
transaction (asserted: nonce 0 and zero native balance, so it is
invisible to the state dump and only discoverable through `Transfer`
logs). After generating the certificate, `45-check_exit_certificate.sh`
asserts it contains exactly two bridge exits for that ERC-20 — the
active holder and the passive recipient, each with the exact expected
amount — before the certificate is submitted, settled on L1 and the
funds claimed. With the old trace-based Step A this check fails (the
passive recipient is missing); with the new Step A it passes.
- 🖱️ **Manual**: the state-dump Step A approach was validated in #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
- Closes #1699 — AET-02: passive ERC-20 recipients (accounts that only
ever *received* tokens) are structurally invisible to
`debug_traceTransaction`, so they were omitted from the certificate and
their balances wrongly absorbed into the SC-locked residual. The
`Transfer`-log discovery in the new Step A covers them.
- #1700 — the zero-address fix included here (second commit) is what
that issue asked for; the issue was already closed manually.
- Closes #1707 — AET-08: `dialBridge()` binds the bridge contract
without rejecting a zero `BridgeAddr`, so the lite syncer would silently
scan the wrong address and `BuildTree()` would return the zero hash as
the LER without any error.

Not addressed here (still open, tracked elsewhere): #1680 is already
fixed by #1684 (merged into the feature branch; it will auto-close when
the feature branch reaches `develop`), and #1693 (AET-01) is being
handled in `fix/exit-certificate-tool_aet01-native_token`.

## 🔗 Related PRs
- Imports and supersedes #1687 (its Step A lands here as the default,
not as the opt-in `aalt` step)
- Rebased on top of #1694, which already landed `genesisPrefundETHWei`,
`capMode`, `nativeSCLockedFromContracts` and the Step F agglayer LBT
dump — those changes are no longer part of this PR. The
`ignoreAddresses` option was dropped intentionally (no longer wanted).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
joanestebanr added a commit that referenced this pull request Jul 10, 2026
…is premints (genesisPrefundETHWei, capMode, nativeSCLockedFromContracts) (#1694)

## 🎯 Motivation / Context
This PR adapts the exit-certificate tool to the particularities of
**testnets**, where native funds
are pre-loaded at genesis (e.g. 110,000 ETH on Kurtosis enclaves,
100,000 ETH on Cardona). That
premint breaks several steps of the pipeline, because it inflates
account balances without any
matching agglayer deposit:

- The **Step F** balance check fails: the certificate's native exits
include the premint, but the
agglayer balance and the Step 0 LBT only count genuinely bridged funds.
- The **Step C** native SC-locked formula (`LBT − EOA_accumulated`)
underflows, clamps to 0 and
  silently drops contract-held ETH from the certificate.
- The premint itself has **no collateral on the agglayer**, so it can
never be bridged out — the
  certificate must always be capped to the bridged amount.

## 🔄 Changes Summary
- **`options.genesisPrefundETHWei`** — declares the native amount (Wei,
decimal string) preminted at
  genesis:
- **Step B** verifies the declared value against the detected genesis
ETH preload total (sum of
balances at block 0); a mismatch is fatal even with
`ignoreGenesisBalance=true`, since Step F
would otherwise discount the wrong amount. The genesis-preload error
message now explains that
pre-funding accounts at genesis is not allowed and points at
`ignoreGenesisBalance=true`.
- **Step F** subtracts the value from the native-token **certificate
sum** before both the
agglayer three-way and offline comparisons (via
`discountGenesisPrefund`, floored at zero),
logging the certificate total, the pre-fund and the difference — so the
check balances against
    the genuinely bridged amount.
- The pre-fund has **no agglayer collateral**, so even when every check
matches Step F emits
`step-f-capped-certificate.json` trimming the native exits to
`min(agglayer, lbt)` — no
    `ignoreBalanceMismatch` needed.
  - Validated by `LoadConfig` (non-negative base-10 integer).
- **`options.capMode`** — `"amount"` (default) or `"appearance"`.
Controls how Step F allocates each
token's cap budget when trimming exits. `"amount"` serves the
smallest-amount exits first so the
largest holders (the premint whales) are capped/dropped first;
`"appearance"` serves exits in
appearance order. Surviving exits keep their original order in both
modes. `capCertificateExits`
  reworked into a greedy per-token allocator.
- **`options.nativeSCLockedFromContracts`** — when `true` (the default),
Step C measures the native
token's SC-locked value directly by summing the ETH held by contract
accounts at `targetBlock`
(excluding the L2 bridge reserve), instead of deriving it as `LBT −
EOA_accumulated`, which
underflows on premint chains. Wrapped tokens keep the formula; set to
`false` to fall back to the
  derivation for the native token.
- **Step A no longer excludes the zero address** — `0x000…000` can hold
value (e.g. burned funds)
that the certificate must account for, so it stays in the collected
address set.
- **Step F agglayer LBT dump** — whenever `agglayerAdminURL` is set,
Step F queries
`admin_getTokenBalance` once up front and writes the raw LBT to
`step-f-agglayer-lbt.json`,
regardless of comparison mode (skipped when `OutputDir` is unset, i.e.
hand-built configs). In
agglayer mode the response is reused (no second RPC). Added
`scripts/get-agglayer-lbt.sh` to fetch
  the same LBT manually.
- **Step F mismatch logs** now include the signed `certificate−agglayer`
/ `certificate−lbt`
  differences (collapsed into one when LBT == agglayer).

## ⚠️ Breaking Changes
- `capMode` defaults to `"amount"`: with `ignoreBalanceMismatch=true`,
the default cap outcome
  changes (previously appearance-order allocation).
- `nativeSCLockedFromContracts` defaults to `true`: Step C now measures
the native SC-locked value
from contract balances by default (set to `false` for the previous `LBT
− EOA` derivation).
- Step A now includes the zero address in the collected set, so its
balances can appear in the
  certificate.

## 📋 Config Updates
Three new fields under `options` (all reflected in
`parameters.toml.example` /
`parameters.json.example`):

- 🧾 **`genesisPrefundETHWei`** — amount of native token preminted at
genesis, in **Wei**, as a
  **decimal string**.
- *Purpose:* tells the tool how much native value exists on the chain
without agglayer collateral.
Step B verifies it against the preload actually detected at block 0 (a
mismatch aborts, even
with `ignoreGenesisBalance=true`); Step F discounts it from the native
certificate sum before
the balance comparison and always emits a capped certificate trimming
the native exits to
    `min(agglayer, lbt)`, since the premint can never be bridged out.
- *Values:* `""` (default — feature disabled, no discount and no premint
capping) or any
non-negative base-10 integer, e.g. `"100000000000000000000000"` (100,000
ETH). Negative or
    non-numeric values are rejected by `LoadConfig`.
- 🧾 **`capMode`** — allocation order used when Step F trims a token's
bridge exits to its cap
  budget.
- *Purpose:* decides **which holders absorb the cut** when the
certificate's exits exceed the
collateral (mismatch with `ignoreBalanceMismatch=true`, or the premint
capping above).
- *Values:* `"amount"` (default) — serves the smallest exits first, so
the largest holders
(typically the premint whales) are capped/dropped first and small
legitimate holders keep their
full exits; `"appearance"` — serves exits in the order they appear in
the certificate. Any other
value is rejected by `LoadConfig`. Surviving exits keep their original
order in both modes.
- 🧾 **`nativeSCLockedFromContracts`** — how Step C computes the
**native** token's SC-locked value
  (the amount bridged to `exitAddress`).
- *Purpose:* the derived formula `LBT − EOA_accumulated` underflows on
premint chains (EOA
balances include the premint, the LBT does not), clamping to 0 and
silently dropping
    contract-held ETH from the certificate.
- *Values:* `true` (default) — measure it directly by summing
`eth_getBalance` of every contract
account at `targetBlock`, excluding the L2 bridge (its balance is the
un-released native
reserve); `false` — keep the `LBT − EOA` derivation. Wrapped tokens
always use the formula,
    regardless of this flag.

Example configs updated accordingly:

- 🧾 `config-examples/zkevm-cardona.toml` sets `genesisPrefundETHWei` to
the 100,000 ETH minted at
  genesis on Cardona.
- 🧾 `scripts/configuration_based_on_kurtosis.sh` sets
`genesisPrefundETHWei` to the 110,000 ETH
preminted by the Kurtosis enclave (overridable via the
`GENESIS_PREFUND_ETH_WEI` env var).

## ✅ Testing
- 🤖 **Automatic**: `go test ./tools/exit_certificate/...` — added tests
for the pre-fund discount
  and always-cap behaviour (`TestDiscountGenesisPrefund`,
`TestCompareTokenBalances_GenesisPrefundDiscount`,
`TestRunStepF_PrefundMatchedStillCapsToLBT`,
`TestRunStepF_NoPrefundNoCapOnAllMatch`), the Step B preload
verification
(`TestCheckDeclaredGenesisPrefund`,
`TestRunStepB1GenesisPrefundDeclared`), the Step C contract
measurement (`TestRunStepC_NativeSCLockedFromContracts`,
`TestSumContractNativeBalances`,
`TestApplyNativeContractLocked`), cap allocation
(`TestLoadConfig_CapMode`,
`TestCapCertificateExits_ByAmount*`), config loading
(`TestLoadConfig_GenesisPrefundETHWei`) and
  the LBT dump guard (`TestRunStepF_EmptyOutputDir_SkipsLBTDump`).
- 🧪 **Manual**: full pipeline run against a Kurtosis enclave
  (`scripts/configuration_based_on_kurtosis.sh`).

## 📝 Notes
- An `ignoreAddresses` option (exclude specific accounts from the
certificate exits) was initially
part of this PR but was removed before merging: it is not in use and
complicated other in-flight
PRs. The branch name still reflects it; the branch will be deleted after
merge.
- README/CLAUDE.md docs were audited against the code and corrected
(e.g. `stepAWindowSize` default,
`rollupManagerAddress`, Step D holder-bridge exits, conditional Step C/E
outputs).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
joanestebanr added a commit that referenced this pull request Jul 10, 2026
…T-02) and zero adress issue (AET-08) (#1701)

## 🔄 Changes Summary
- **Step A replaced**: 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. Both sources always run and merge (the
dump covers native-ETH holders and contracts; the logs cover token
holders): they have complementary blind spots, so there is no strategy
option and no fallback — an unusable `debug_accountRange` fails Step A
instead of degrading silently. The trace-based A1/A2 implementation is
removed.
- **Zero address kept in Step A** — `0x00…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.
- **Extra ERC-20 holders discovered in Step A** — the `Transfer`-log
scan also covers `options.extraErc20Contracts` (deduplicated against the
wrapped tokens, and it runs even with no wrapped tokens). Step B3 only
probes `balanceOf` against the Step A address set, so a passive holder
of an extra token (no ETH/nonce/code, never touched a wrapped token) was
invisible to both sources and their collateral share flowed to
`exitAddress`; discovery stays in Step A (B3 never discovers addresses),
so `step-a-addresses.json` remains the complete address universe in
single-step mode.
- **`options.capMode` gains `"none"`, the new default** — capping the
certificate is a lossy operation and must now be opted into explicitly.
With `"none"`, Step F fails (`errCapForbidden`) as soon as any bridge
exit would have to be trimmed or dropped, on every capping path: the
mismatch cap (`ignoreBalanceMismatch=true`), the genesis pre-fund trim
(which caps even on allMatch, so `genesisPrefundETHWei` now requires a
trimming mode) and the final-certificate re-cap in the pipeline. A no-op
cap (everything fits the budget) still passes. `"amount"`/`"appearance"`
keep their previous trimming behavior.
- The Kurtosis config generator (`configuration_based_on_kurtosis.sh`)
now emits `capMode: "amount"` (its config declares
`genesisPrefundETHWei`, whose premint trim needs a trimming mode) and
`ignoreBalanceMismatch: false` (real mismatches must abort).
- **E2E harness extended with the AET-02 scenario and split into
numbered stages** (`test/e2e-exit_certificate/`): `10-run_network` →
`20-prepare_network` → `30-stop_sequencer` →
`40-generate_exit_certificate` → `45-check_exit_certificate` →
`50-submit_exit_certificate` → `60-claim_exit_certificate_funds` (the
old `run_exit_tool.sh` is gone). The sequencer stop now halts only the
block producers (`op-batcher`/`op-cl`), keeping the `op-el` RPC alive so
the tool can still read the frozen L2 state.

## ⚠️ Breaking Changes
- 🛠️ **Config**: `options.capMode` now defaults to `"none"`: runs that
relied on the previous `"amount"` default to trim the certificate (any
`genesisPrefundETHWei` run, or `ignoreBalanceMismatch=true` with real
mismatches) now fail in Step F unless `capMode` is set explicitly to
`"amount"` or `"appearance"`.
- 🛠️ **Config**: `options.ignoreOnTraceError` and
`options.stepAWindowSize` removed (the trace-based Step A and the
receipt-harvesting fallback no longer exist). `options.addressDiscovery`
(present in intermediate revisions of this PR, never released) is also
gone — Step A always runs both sources.
- 🔌 **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
- 🧾 `capMode` accepts `"none"` (new default) besides `"amount"` and
`"appearance"`; `zkevm-cardona.toml` sets `capMode = "amount"`
explicitly because its `genesisPrefundETHWei` always requires trimming.

## ✅ Testing
- 🤖 **Automatic**: `go test ./tools/exit_certificate/...` and
`golangci-lint` clean. New unit tests for Step A discovery (pagination,
dialect detection, Transfer extraction, source merging, extra-ERC-20
scanning — including token dedup and the no-wrapped-tokens case —,
truncation/empty-dump/dump-unavailable guards), zero-address handling,
and capMode `"none"` (no-op pass-through, fail-on-trim, fail-on-drop,
plus RunStepF-level prefund and mismatch failures).
- 🧪 **E2E (covers the AET-02 fix end-to-end)**:
`test/e2e-exit_certificate/run_e2e_test.sh` reproduces the exact bug
scenario on a Kurtosis enclave — `20-prepare_network.sh` deploys a
`MintableERC20` on L1, bridges it to L2 and transfers part of the
wrapped balance to a **passive account** that never sends any L2
transaction (asserted: nonce 0 and zero native balance, so it is
invisible to the state dump and only discoverable through `Transfer`
logs). After generating the certificate, `45-check_exit_certificate.sh`
asserts it contains exactly two bridge exits for that ERC-20 — the
active holder and the passive recipient, each with the exact expected
amount — before the certificate is submitted, settled on L1 and the
funds claimed. With the old trace-based Step A this check fails (the
passive recipient is missing); with the new Step A it passes.
- 🖱️ **Manual**: the state-dump Step A approach was validated in #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
- Closes #1699 — AET-02: passive ERC-20 recipients (accounts that only
ever *received* tokens) are structurally invisible to
`debug_traceTransaction`, so they were omitted from the certificate and
their balances wrongly absorbed into the SC-locked residual. The
`Transfer`-log discovery in the new Step A covers them.
- #1700 — the zero-address fix included here (second commit) is what
that issue asked for; the issue was already closed manually.
- Closes #1707 — AET-08: `dialBridge()` binds the bridge contract
without rejecting a zero `BridgeAddr`, so the lite syncer would silently
scan the wrong address and `BuildTree()` would return the zero hash as
the LER without any error.

Not addressed here (still open, tracked elsewhere): #1680 is already
fixed by #1684 (merged into the feature branch; it will auto-close when
the feature branch reaches `develop`), and #1693 (AET-01) is being
handled in `fix/exit-certificate-tool_aet01-native_token`.

## 🔗 Related PRs
- Imports and supersedes #1687 (its Step A lands here as the default,
not as the opt-in `aalt` step)
- Rebased on top of #1694, which already landed `genesisPrefundETHWei`,
`capMode`, `nativeSCLockedFromContracts` and the Step F agglayer LBT
dump — those changes are no longer part of this PR. The
`ignoreAddresses` option was dropped intentionally (no longer wanted).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request exit_certificate_tool Tool to create a final exit certificate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants