fix(exit-certificate): AET-12 :step E prevent uint64 overflow panic in decodeABIString#1684
Conversation
step E prevent uint64 overflow panic in decodeABIString
|
Claude finished @joanestebanr's task in 1m 24s —— View job PR Review: uint64 Overflow Fix in decodeABIString🚀 What's NewThis PR fixes a critical uint64 integer overflow vulnerability in 🐛 Bug Fixes
📋 Config UpdatesNone.
|
c9abfbb to
f9f0c35
Compare
A maliciously crafted L1 ERC-20 whose name()/symbol() returns an oversized ABI length field could overflow the uint64 bounds check in step_e.decodeABIString(): computing 64+strLen wrapped past MaxUint64 to a small value, bypassing the guard and triggering a slice out-of-range panic that aborts the exit certificate tool. Reject length fields that don't fit in uint64 and compare strLen against the remaining byte count without the overflowing addition. Add a regression test covering the overflow and oversized-length cases. Fixes #1680 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Allocate the ABI-string buffer at full size and copy into it instead of appending to a non-zero-length slice. Also drop the redundant loopvar copy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
f9f0c35 to
a5a1f5e
Compare
|
7882c20
into
feature/exit-certificate-tool
…deABIString (#1684) ## 🔄 Changes Summary - Fix a uint64 integer overflow in `step_e.decodeABIString()` (SP002). A maliciously crafted L1 ERC-20 whose `name()`/`symbol()` returns an oversized ABI length field could make `64+strLen` wrap past `MaxUint64` to a small value, bypassing the bounds guard and triggering a slice out-of-range **panic** that aborts the exit certificate tool for any chain with an unclaimed L1→L2 deposit of such a token. - Reject length fields that don't fit in `uint64` (`big.Int.IsUint64()`) and compare `strLen` against the remaining byte count without the overflowing addition (`strLen > uint64(len(data))-twoABIWords`, safe since `len(data) >= 64`). ##⚠️ Breaking Changes - None. ## 📋 Config Updates - None. ## ✅ Testing - 🤖 **Automatic**: Added `TestDecodeABIString` with regression cases — `MaxUint64` length field (the overflow case) and a length field larger than `uint64` — asserting no panic and `""` return, plus valid/empty/too-short/length-exceeds-data cases. Full `go test ./tools/exit_certificate/...` passes; `go vet` and `gofmt` clean. - 🖱️ **Manual**: n/a (deterministic decode logic covered by unit test). ## 🐞 Issues - Closes #1680 ## 📝 Notes - The guard now also rejects length fields exceeding `uint64` range, which `big.Int.Uint64()` would have silently truncated. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
AET-12 uint64 Addition Overflow In decodeABIString Bypasses Bounds Guard And Crashes The Process |
step E prevent uint64 overflow panic in decodeABIStringstep E prevent uint64 overflow panic in decodeABIString
…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>
…deABIString (#1684) ## 🔄 Changes Summary - Fix a uint64 integer overflow in `step_e.decodeABIString()` (SP002). A maliciously crafted L1 ERC-20 whose `name()`/`symbol()` returns an oversized ABI length field could make `64+strLen` wrap past `MaxUint64` to a small value, bypassing the bounds guard and triggering a slice out-of-range **panic** that aborts the exit certificate tool for any chain with an unclaimed L1→L2 deposit of such a token. - Reject length fields that don't fit in `uint64` (`big.Int.IsUint64()`) and compare `strLen` against the remaining byte count without the overflowing addition (`strLen > uint64(len(data))-twoABIWords`, safe since `len(data) >= 64`). ##⚠️ Breaking Changes - None. ## 📋 Config Updates - None. ## ✅ Testing - 🤖 **Automatic**: Added `TestDecodeABIString` with regression cases — `MaxUint64` length field (the overflow case) and a length field larger than `uint64` — asserting no panic and `""` return, plus valid/empty/too-short/length-exceeds-data cases. Full `go test ./tools/exit_certificate/...` passes; `go vet` and `gofmt` clean. - 🖱️ **Manual**: n/a (deterministic decode logic covered by unit test). ## 🐞 Issues - Closes #1680 ## 📝 Notes - The guard now also rejects length fields exceeding `uint64` range, which `big.Int.Uint64()` would have silently truncated. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…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>



🔄 Changes Summary
step_e.decodeABIString()(SP002). A maliciously crafted L1 ERC-20 whosename()/symbol()returns an oversized ABI length field could make64+strLenwrap pastMaxUint64to a small value, bypassing the bounds guard and triggering a slice out-of-range panic that aborts the exit certificate tool for any chain with an unclaimed L1→L2 deposit of such a token.uint64(big.Int.IsUint64()) and comparestrLenagainst the remaining byte count without the overflowing addition (strLen > uint64(len(data))-twoABIWords, safe sincelen(data) >= 64).📋 Config Updates
✅ Testing
TestDecodeABIStringwith regression cases —MaxUint64length field (the overflow case) and a length field larger thanuint64— asserting no panic and""return, plus valid/empty/too-short/length-exceeds-data cases. Fullgo test ./tools/exit_certificate/...passes;go vetandgofmtclean.🐞 Issues
📝 Notes
uint64range, whichbig.Int.Uint64()would have silently truncated.🤖 Generated with Claude Code