# Merge 02, batch `el-transfer`: Erigon and Reth vs EIP-7708 (ETH transfer logs) and EIP-8246 (SELFDESTRUCT without burn), Amsterdam

This merge combines two item results: `item-erigon` (SHA-256 `aa2d83a8…cb997`) and `item-reth` (SHA-256 `b45d835b…99200`). Both were present and non-empty. Neither item ran anything against a network or node. Neither raised a candidate. This merge adds no findings. Everything below comes from the two items, and each row says which item it came from.

EIP status as the items read it: EIP-7708 is Last Call with a 2026-11-01 deadline and requires 1559, 4788, 6780 and 8246 (item-reth). EIP-8246 is Last Call with a 2026-11-04 deadline (both items). Neither EIP is live on mainnet, and Amsterdam is not activated (item-reth). Sources: https://eips.ethereum.org/EIPS/eip-7708, https://eips.ethereum.org/EIPS/eip-8246.

## 1. Pins

| Client | Branch | Commit | Spec commit (ethereum/execution-specs `forks/bogota`) | Source item |
|---|---|---|---|---|
| Erigon (erigontech/erigon) | `main` | `f32a39d1e5f87a9ec3a9ce2858171c74acc3c242` | `7f2f4c9bf6d73b09ecb8b24eb14d08bf0cf0247b` | item-erigon |
| Reth (paradigmxyz/reth) | `main` | `e0378eb691aa2770bb96fd4ecec2f4445a2a3e7c` (2026-10-27T12:11:23Z) | `7f2f4c9bf6d73b09ecb8b24eb14d08bf0cf0247b` (2026-10-27T17:40:55Z) | both items |
| Reth EVM: alloy-rs/evm2 (rev pinned in Reth `Cargo.toml`) | rev | `4a53ec080dfbb904e98e3ca8f0abcc11ab8abae0` | same | both items |
| Geth (ethereum/go-ethereum), comparison only | `master` | `8817f99f92852f8ac53e596de6202dd9e3494065` (2026-10-27T03:32:47Z) | same | item-reth |

Pin sources: https://api.github.com/repos/erigontech/erigon/commits/main, https://api.github.com/repos/paradigmxyz/reth/commits/main, https://api.github.com/repos/ethereum/execution-specs/commits/forks/bogota, https://api.github.com/repos/ethereum/go-ethereum/commits/master.

Notes on the pins and citations:
- **Where Reth's code lives.** Reth keeps no 7708/8246 logic in its own tree. It runs in evm2. item-erigon cites `Cargo.toml#L440-L443`, with a `[patch]` at L674-L679. item-reth cites `Cargo.toml` ~L441-443 and the replacing PR https://github.com/paradigmxyz/reth/pull/25002. item-erigon names one in-tree reference: `crates/rpc/rpc-eth-api/src/helpers/call.rs#L219-L221`.
- **item-reth line numbers are approximate.** item-reth had no working shell (`apply-seccomp: write /proc/self/setgroups ... Permission denied`). It read code through a summarising fetch tool, so its line numbers carry `~` and may be off by a few lines. Each one names the enclosing function. This merge did not re-pin them, because that would add work of the merge's own.
- **The items disagree on one line number.** For evm2's finalization, item-erigon cites `crates/evm2/src/evm/state/mod.rs#L870-L892`, and item-reth cites `fn finalize_transaction` at ~L660 in the same file at the same rev. The items do not settle this. item-erigon's range came from a clone; item-reth's is approximate.
- Spec paths are under `src/ethereum/forks/amsterdam/`, abbreviated `S:`.
- **Spec files each item read.** item-erigon read `vm/__init__.py`, `vm/instructions/system.py`, `fork.py`, `transactions.py`, `vm/interpreter.py` and `state_tracker.py`. item-reth read the same files except `transactions.py`.

## 2. Candidates

**Neither item raised a candidate, so there is no `<client>-<n>` candidate to copy.**
- item-erigon: "No candidate meets the bar. In the paths reviewed I found no difference between Erigon and the spec in the emit rules, log encoding, SELFDESTRUCT value movement, same-transaction deletion, balance-only finalization, or the dead-beneficiary state gas."
- item-reth: "No candidate is raised. For every rule in the brief, Reth (evm2) produces the same gas, state, logs and block-validity result as the execution-specs reference."

Both items also recorded observations that sit below the candidate bar. They are copied in full below so nothing is dropped. They are numbered `<client>-O<n>` so they are not mistaken for candidates.

### Erigon-O1 (observation, not a candidate): EIP-8246 in the parallel-execution and BAL path

- **Spec:** the spec's net BAL entries for an account created and then destroyed in one transaction. If the account keeps its balance, that includes the nonce reset to 0 (`S:state_tracker.py#L494-L515`).
- **Client:** Erigon `execution/state/versionmap_writeview.go#L200-L240` and `execution/state/versionedio.go#L1974` at `f32a39d…`. The item could not verify that this path reproduces the spec's entries.
- **Trigger:** not established. The item "did not trace the parallel path to a concrete divergence".
- **Confidence:** low. Only open issues point at it, and it was not traced.
- **Severity if real:** block validity (BAL mismatch).
- **Disproof tried:** item-erigon read `vanishedInTx` and the finalization code, and the serial path matches the spec.
- **Reth comparison (item-erigon):** `evm2 crates/evm2/src/evm/state/mod.rs#L870-L892` implements the same preserve rule in a sink-based design. BAL output was not checked.
- **Known-check:** known work in this area, not claimed as a bug. Open: https://github.com/erigontech/erigon/issues/23238 (preserved balance wiped on read), https://github.com/erigontech/erigon/issues/24906 (same-tx create + SELFDESTRUCT in parallel exec), PR https://github.com/erigontech/erigon/pull/24096 (unmerged; preserve 8246 balance reads), PR https://github.com/erigontech/erigon/pull/25266 (pre-8246 BAL burn fix). Merged 2026-10-22: PR https://github.com/erigontech/erigon/pull/25160 (no writes for create-and-destroy in one tx).

### Erigon-O2 (observation, not a spec difference): an Erigon-only switch for 7708

- **Spec:** has no switch to turn off EIP-7708.
- **Client:** Erigon gates 7708 with `IsEIPEnabled(7708)`. That can be switched off through `DisabledEIPs` (`execution/chain/chain_config.go#L946-L958`).
- **Trigger:** only on chains that configure `DisabledEIPs` to include 7708.
- **Confidence / severity:** not an Ethereum L1 divergence, so "cosmetic at most" (item-erigon).
- **Known-check:** none recorded for this point.

### Reth-O1 (observation, not a candidate): storage of an EIP-8246 "survivor" created over pre-existing storage

1. **Spec.** EIP-8246 says accounts marked for selfdestruction are, at finalization, modified so that "all storage is cleared" (https://eips.ethereum.org/EIPS/eip-8246). The reference (`fork.py`, `process_transaction`) runs `clear_account_preserving_balance(tx_env.state, address)` for each `accounts_to_delete`. In `state_tracker.py` that function calls `destroy_storage(tx_state, address)` and sets `nonce = 0` and `code_hash = EMPTY_CODE_HASH`. In addition, `vm/interpreter.py` `process_create` already runs `destroy_storage(tx_state, evm.current_target)` when the contract is created.
2. **Client.** evm2 `crates/evm2/src/evm/state/mod.rs`, `fn finalize_transaction` (~L660):
   ```rust
   if eip8246 && !account.balance().is_zero() {
       account.reset_selfdestructed_for_finalization();
       // Like revm, a created survivor keeps its unloaded old storage, which later
       // transactions read as zero.
       account.is_created()
   } else { account.delete_for_finalization(); false }
   ...
   if keeps_storage { storage.zero_loaded() } else { storage.wipe() }
   ```
   The PR that introduced this is https://github.com/alloy-rs/evm2/pull/734 (merged 2026-10-23). It says a created survivor "keeps its old storage, zeroes only the loaded slots, and emits no storage wipe". It adds that `CacheDB`/`Cache::merge` "apply persisted changes where old slots survive, as in revm's bundle". Open draft https://github.com/alloy-rs/evm2/pull/562 ("derive storage resets from account lifecycle") touches the same area.
3. **Condition (trigger).** This only matters if a contract is created at an address that already holds storage but has zero nonce and no code, and then self-destructs in the same transaction while it still holds a balance. In that case the spec's post-state has empty storage for the account. Reth's persisted storage root would also be empty only if Reth wipes storage on the account's "created" status. item-reth could not read that part of Reth's state-stream code. Per EIP-7610, mainnet has 28 such addresses and no known way to deploy to them. The case can only arise from synthetic pre-state (test fixtures, devnets).
4. **Geth.** Not compared.
5. **Confidence: low.** Disproof tried: evm2 reads such slots as zero (PR 734), so execution results agree. Only the persisted root is in doubt, and that needs the Reth side, which was not read. Also, a created account normally has no old storage, so in every reachable case `zero_loaded()` and `wipe()` give the same result. item-reth notes that the account-creation-over-storage path is not EIP-7708/8246 logic and that parts of it are live on mainnet. It gave no further details of that path, and none are given here.
6. **Severity if real:** state-root mismatch, which would be a consensus split, on synthetic pre-state only.
- **Known-check:** not found in the searches in section 4. evm2 #734 documents the behaviour as intended ("like revm").

## 3. Cross-client table

Each cell reads "agrees with spec", "differs (see …)" or "not checked". No cell says "differs", because no item found a difference. Where an observation from section 2 touches a rule, the cell names it.

**Where the cells come from:**
- **Erigon column:** item-erigon.
- **Reth column:** item-reth, plus the evm2 cross-check in item-erigon's table, which agrees on every row where it was read.
- **Geth column:** item-reth, which used Geth only for comparison.

Line citations from item-erigon are at `f32a39d…` (Erigon) and `4a53ec0` (evm2). item-reth's line numbers are approximate (`~`).

### 3.1 The EIP-7708 log

| # | Rule | Spec | Erigon | Reth (evm2) | Geth |
|---|---|---|---|---|---|
| 1 | LOG3 from SYSTEM_ADDRESS (`0xff…fe`). topic0 `0xddf252ad…b3ef` = keccak256(`Transfer(address,address,uint256)`). Topics 1 and 2 are the addresses left-padded to 32 bytes. Data is the amount as 32-byte big-endian. | `S:vm/__init__.py#L247-L281`; `TRANSFER_TOPIC`/`SYSTEM_ADDRESS` ~L35-38 | agrees with spec: `execution/protocol/misc/eip7708.go#L34-L50` | agrees with spec: `crates/evm2/src/evm/mod.rs#L2002-L2015`, free fn `eip7708_transfer_log`. item-reth did not see the bytes of the `EIP7708_TRANSFER_TOPIC` constant. | agrees with spec: `core/types/log.go` `EthTransferLog` |
| 2 | Zero amount emits nothing | `S:vm/__init__.py#L268-L269` (`if transfer_amount == 0: return`) | agrees with spec: `eip7708.go#L64` (`!amount.IsZero()`); `instructions.go#L1501` | agrees with spec: `evm/mod.rs#L2003` (`value.is_zero() \|\| from == to`) | agrees with spec |
| 3 | Self-transfer emits nothing (CALL to self, CALLCODE, tx to self) | `S:vm/interpreter.py#L425-L431` (`caller != current_target`); SELFDESTRUCT `beneficiary != originator` | agrees with spec: `eip7708.go#L64` (`sender != recipient`) | agrees with spec: `evm/mod.rs#L2003` (`from == to`) | agrees with spec |
| 4 | 7708/8246 active only from Amsterdam, with no switch to turn them off | spec fork scoping | agrees with spec for L1. Erigon-only `DisabledEIPs` switch, see Erigon-O2 (`execution/chain/chain_config.go#L946-L958`) | agrees with spec: `version/mod.rs` `AMSTERDAM { features: [EIP8037, EIP7708, EIP8246, EIP2780], … }` | agrees with spec (`rules.IsAmsterdam`) |
| 5 | No gas charged for the log | `S:vm/__init__.py#L247-L281` | agrees with spec: `AddLog` (`intra_block_state.go#L562-L570`) is outside the gas functions | agrees with spec: `log_eip7708_transfer` only pushes the log | agrees with spec |
| 6 | The transfer log does not add SYSTEM_ADDRESS to the BAL | log construction touches no state; tracker #3217 | not checked | agrees with spec: `self.log` → `state.log`, no account load | agrees with spec |
| 7 | Logs from a reverted frame or a failed tx are dropped | `S:vm/interpreter.py#L414`; `process_top_level` `if evm.error: logs = ()` | agrees with spec: `intra_block_state.go#L2456`, `#L2482` (`logs.truncate(snapshot.logCount)`) | agrees with spec: `evm/mod.rs#L1834-L1836` (`rollback`); checkpoint in `execute_call_message` | agrees with spec |
| 8 | Log collection gate (`logs_observed()`) cannot change gas or state | n/a | not checked | agrees with spec for consensus (`LOG_COLLECTION` is on by default) | not checked |
| 9 | Log index and tx index are assigned by the state | `S:fork.py#L1074-L1086` | agrees with spec: `intra_block_state.go#L560-L561`, `#L538-L539` | agrees with spec: shared `State.logs` vector | agrees with spec (same vector) |
| 10 | Receipt encoding and bloom contents for the synthetic logs | log goes to receipt | not checked ("not independently verified") | agrees with spec: `crates/ethereum/evm/src/executor.rs` `build_receipt` ~L325-340 | not checked |
| 11 | Deposit-request parsing is not confused by transfer logs | n/a | not checked | agrees with spec: `execution.rs` `block_requests_from_receipts` ~L322-342 filters by deposit contract | not checked |

### 3.2 Which value movements emit a log

| # | Rule | Spec | Erigon | Reth (evm2) | Geth |
|---|---|---|---|---|---|
| 12 | Tx value, CALL, CREATE/CREATE2: the log is emitted right after the balance move, before any later log, so the tx-value log comes first | `S:vm/interpreter.py#L418-L431` | agrees with spec: `execution/vm/evm.go#L358-L365` (call), `#L742-L752` (create), `eip7708.go#L52-L67` | agrees with spec: `evm/mod.rs#L910-L918`; `execute_call_message`; `prepare_initial_frame` | agrees with spec: `core/evm.go` `Transfer` from `EVM.Call` |
| 13 | Precompile and EIP-7702-delegated targets: the log goes to the called account, and precompile calls also log | `S:vm/interpreter.py#L418-L436` | agrees with spec: `evm.go#L362` uses `addr` before the `isPrecompile` switch at `#L367-L369` | agrees with spec: `message.destination`, before `contains_precompile` | agrees with spec |
| 14 | CALLCODE with value: no log | caller == current_target | agrees with spec (same guard) | agrees with spec (`MessageKind::CallCode`, `from == to`) | agrees with spec |
| 15 | DELEGATECALL / STATICCALL: no value, no log | no value | agrees with spec (DELEGATECALL goes through the same guard; STATICCALL not stated) | agrees with spec | not checked |
| 16 | CREATE collision or out of funds: no log | `generic_create` returns before `process_create` | not checked (create-path order checked in row 12) | agrees with spec: `create_message_account` returns `CreateCollision`/`OutOfFunds` before the log | agrees with spec (`create`) |
| 17 | SELFDESTRUCT logs only when the beneficiary is not the originator and the balance is non-zero | `S:vm/instructions/system.py#L762-L767` | agrees with spec: `instructions.go#L1501-L1503` | agrees with spec: `evm/mod.rs#L1866-L1871` | agrees with spec: `opSelfdestruct6780` |
| 18 | Fees, coinbase tips and burns: no log | `S:fork.py#L1096-L1115` (`create_ether`) | agrees with spec: only emit sites are `eip7708.go#L65` and `instructions.go#L1502` | agrees with spec: `default_settle_gas` ~L683-708 | not checked |
| 19 | Withdrawals: no log | `process_withdrawals` (`create_ether`) | agrees with spec (same grep of emit sites) | agrees with spec: `post_block_balance_state_changes` ~L484-560 | not checked |
| 20 | System calls (4788, 2935, 7002, 7251): no log | value 0; logs not in block | agrees with spec: zero value skips `Transfer` at `evm.go#L352-L357` | agrees with spec: `execute_system_call` | agrees with spec |

### 3.3 SELFDESTRUCT (EIP-8246 with EIP-6780 and EIP-8037)

| # | Rule | Spec | Erigon | Reth (evm2) | Geth |
|---|---|---|---|---|---|
| 21 | SELFDESTRUCT to self (created this tx, or pre-existing): balance unchanged, no burn, no log | `S:vm/instructions/system.py#L763-L767` | agrees with spec: `instructions.go#L1462-L1473` | agrees with spec: `evm/mod.rs#L1866-L1878` (burn skipped under EIP8246) | agrees with spec (`!IsAmsterdam` guards SubBalance) |
| 22 | Marked for deletion only if created in the same tx | `S:vm/instructions/system.py#L769-L771` | agrees with spec: `instructions.go#L1455-L1479` | agrees with spec: `evm/mod.rs#L1862`, `#L1879-L1881` | agrees with spec (`IsNewContract`) |
| 23 | Finalization: non-zero balance keeps the balance, with nonce 0, empty code and cleared storage; zero balance means the account is deleted | `S:fork.py#L1088-L1089`; `S:state_tracker.py#L494-L515` | agrees with spec on the serial path: `intra_block_state.go#L2504-L2533`. Parallel/BAL path unverified, see Erigon-O1 | agrees with spec: `evm/state/mod.rs#L870-L892` (item-erigon), `finalize_transaction` ~L660 (item-reth), except the synthetic-pre-state storage edge, see Reth-O1 | not checked |
| 24 | Net BAL entries for an account created and then destroyed (incl. nonce reset) under parallel execution | `S:state_tracker.py#L494-L515` | not verified, see Erigon-O1 | not checked (BAL output not checked) | not checked |
| 25 | ETH received after SELFDESTRUCT in the same tx is kept | `clear_account_preserving_balance` | not checked | agrees with spec | not checked |
| 26 | Only accounts from successful frames are finalized | `process_top_level`: `if evm.error: accounts_to_delete = set()` | not checked | agrees with spec (`State::rollback`) | not checked |
| 27 | Pre-8246 burn hook cannot fire under 8246 | n/a | agrees with spec: `intra_block_state.go#L2509-L2511` is unreachable for non-zero balance under 8246 | agrees with spec (burn branch requires `!feature(EIP8246)`) | agrees with spec |
| 28 | Gas: base 5000 plus cold access, checked before state access | `gas_cost` + `check_gas` before access | not checked | agrees with spec: `instructions/system.rs` `selfdestruct` ~L419-447 (`skip_cold_load`, `ColdLoadSkipped`) | not checked |
| 29 | Dead-beneficiary charge (ACCOUNT_WRITE exec gas, then NEW_ACCOUNT state gas) only if the beneficiary is dead and the balance is non-zero | `S:vm/instructions/system.py#L743-L756` | agrees with spec: `execution/vm/operations_acl.go#L278-L297` | agrees with spec: `should_charge_new_account_gas`, `selfdestruct_cost`, `spend_state` | not checked |
| 30 | Beneficiary touched for BAL even with a zero transfer | `S:vm/instructions/system.py#L735-L737` | agrees with spec: `operations_acl.go#L287-L289` | not checked | not checked |
| 31 | State change runs before the gas charge (evm2) instead of the spec's order | gas charged, then `move_ether` | not checked | agrees with spec (equivalent: `rollback` undoes it, and the BAL read happens in both orders) | not checked |

The two items agree with each other on every rule both covered. item-erigon read evm2 where it could, and found that Reth "agrees with both the spec and Erigon on every rule where I read it".

## 4. Known-check summary

Searches were unauthenticated, via `api.github.com/search/issues`.

| Area | Result | Item |
|---|---|---|
| Candidates | None raised, so nothing to mark known or unknown | both |
| Erigon, 8246 parallel-exec/BAL (Erigon-O1) | Known open work: #23238, #24906, PR #24096 (open, unmerged), PR #25266 (open); PR #25160 merged 2026-10-22. Not claimed as a bug. | item-erigon |
| Erigon, 7708 | PR https://github.com/erigontech/erigon/pull/24402 closed (eth_simulateV1 skips synthetic transfer logs); https://github.com/erigontech/erigon/issues/22224 closed (coinbase and burnt-fee exclusivity in parallel exec) | item-erigon |
| Erigon queries | `repo:erigontech/erigon 7708`, `… 8246`, `… selfdestruct amsterdam`, `repo:ethereum/execution-specs 8246`, `… 7708 bogota`. execution-specs returned no 7708/8246 issue. The Test Release Tracker was not located through search. | item-erigon |
| Reth `7708` | 8 hits, none about consensus or execution of EIP-7708. Mostly RPC `eth_simulateV1`: #27110 (merged), #27963 (open), #26854 (closed), #27968 (closed, not merged), #26865 (merged) | item-reth |
| Reth `8246` | #27849 (merged), #26865. Neither reports a bug. | item-reth |
| Reth `"transfer log"` | 23 hits, all RPC or tracing (e.g. #26627, #24729, #24816, #14052) | item-reth |
| Reth `selfdestruct burn` | #25002 only | item-reth |
| alloy-rs/evm2 `7708 OR 8246 OR selfdestruct` | 50 hits, 23 seen. Relevant: #205 (merged), #668 (merged), #734 (merged 2026-10-23), #562 (draft, open), #396 (merged), #342 (merged). None reports an open spec divergence. | item-reth |
| Reth-O1 | Not found. evm2 #734 documents the behaviour as intended. | item-reth |
| execution-specs Test Release Tracker | https://github.com/ethereum/execution-specs/issues/3217 `tests@v21.0.0` (open) lists 7708 and 8246 with completed items: no SYSTEM_ADDRESS in the BAL; same-tx SELFDESTRUCT clears code, nonce and storage while keeping balance. #3889 `bogota-devnet@v2.0.0` mentions Reth but not 7708/8246. | item-reth (item-erigon did not locate it) |
| Hive (https://hive.ethpandaops.io/glamsterdam/listing.jsonl) | Erigon: `eels/consume-enginex` 83258/83258 pass, `eels/execute` 173/173 (2026-10-26 runs). Reth: `eels/consume-rlp` 44155 pass / 6 fail (2.8.1-dev+def3e3c5, 2026-10-26T03:47Z); `eels/consume-engine` 42599/1626; `eels/consume-enginex` 80164/3094 (2.8.1-dev+3c8b4e84, 2026-10-26T10:02Z); `eels/execute` 173/0. Neither item mapped individual tests to 7708 or 8246. Reth's per-test result files (62-117 MB) were too large to fetch, so it is unknown whether any Reth failure is a 7708/8246 test. | both |

## 5. What was not covered

- **Erigon:** receipt encoding and bloom for the synthetic logs; the parallel-execution/BAL path for 8246 (Erigon-O1).
- **Reth:** the state-stream code that decides whether a "created" survivor's storage is wiped (Reth-O1); BAL output for create-then-destroy.
- **item-reth line numbers** are approximate (no shell). evm2's `finalize_transaction` location differs between the items (L870-L892 vs ~L660).
- **Geth** was used only as a comparison column by item-reth. Its finalization and gas paths were not read.
- **Other execution clients** (Nethermind, Besu) are outside this merge.
