# Erigon vs spec: EIP-7708 (ETH transfer logs) and EIP-8246 (SELFDESTRUCT without burn), Amsterdam

Public-page review. I read code only; nothing was run against any network or node, and no test was executed.

## 1. Pins (newest commit on each branch, via api.github.com, 2026-10-27)

| Repo / branch | Commit |
|---|---|
| erigontech/erigon `main` (shallow clone) | `f32a39d1e5f87a9ec3a9ce2858171c74acc3c242` |
| ethereum/execution-specs `forks/bogota` (no `master`) | `7f2f4c9bf6d73b09ecb8b24eb14d08bf0cf0247b` |
| paradigmxyz/reth `main` | `e0378eb691aa2770bb96fd4ecec2f4445a2a3e7c` |
| alloy-rs/evm2, the EVM crate Reth pins in `Cargo.toml` | `4a53ec080dfbb904e98e3ca8f0abcc11ab8abae0` |

- Reth keeps no 7708/8246 logic in its own tree. It lives in evm2 (`Cargo.toml#L440-L443`, via `[patch]` at L674-L679). The one in-tree reference is `crates/rpc/rpc-eth-api/src/helpers/call.rs#L219-L221`.
- Spec paths are under `src/ethereum/forks/amsterdam/`. They are abbreviated `S:` below.
- `S:vm/interpreter.py` and `S:state_tracker.py` were not on the brief's list. I added them because they hold the call-path emit site and `move_ether`.
- EIP texts read: https://eips.ethereum.org/EIPS/eip-7708 and https://eips.ethereum.org/EIPS/eip-8246 (EIP-8246 is Last Call, deadline 2026-11-04).

## 2. Candidates

**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. I report none rather than a weak one.

Two observations are not spec differences:
- **Parallel-execution and BAL machinery for 8246.** I could not verify that Erigon's parallel execution, versionmap and BAL path (`execution/state/versionmap_writeview.go#L200-L240`, `execution/state/versionedio.go#L1974`) reproduces the spec's net BAL entries for a created-then-destroyed account. For a balance-preserved account that includes the nonce reset to 0 (`S:state_tracker.py#L494-L515`). Erigon's own tracker lists open work on this area (section 5).
  - Confidence that this is a real bug: low. Only open issues point at it, and I did not trace it.
  - Severity if real: block validity (BAL mismatch).
  - What I tried: I read `vanishedInTx` and the finalization code, and the serial path matches the spec. I did not trace the parallel path to a concrete divergence.
  - Reth for comparison: `evm2 crates/evm2/src/evm/state/mod.rs#L870-L892` implements the same preserve rule in a sink-based design; I did not check BAL output.
- **Erigon-only gate on 7708.** Erigon gates 7708 with `IsEIPEnabled(7708)`, which can be switched off through `DisabledEIPs` (`execution/chain/chain_config.go#L946-L958`). The spec has no such switch. This only affects chains that configure it. It is not an Ethereum L1 divergence, so cosmetic at most.

## 3. Rules checked and found correct

Erigon paths are at `f32a39d…`. Spec paths are at `7f2f4c9…`.

| Rule | Spec | Erigon | Reth (evm2 `4a53ec0`) |
|---|---|---|---|
| Log is a LOG3 from SYSTEM_ADDRESS (`0xff…fe`), topic0 `0xddf252ad…b3ef`, topics 1 and 2 left-padded to 32 bytes, data a 32-byte big-endian amount | `S:vm/__init__.py#L247-L281`; EIP-7708 table | `execution/protocol/misc/eip7708.go#L34-L50` | `crates/evm2/src/evm/mod.rs#L2002-L2015` |
| Zero amount emits nothing | `S:vm/__init__.py#L268-L269` | `eip7708.go#L64` (`!amount.IsZero()`); `instructions.go#L1501` | `evm/mod.rs#L2003` |
| Self-transfer emits nothing (CALL to self, CALLCODE, tx to self) | `S:vm/interpreter.py#L425-L431` (`caller != current_target`) | `eip7708.go#L64` (`sender != recipient`); CALLCODE and DELEGATECALL go through the same guard | `evm/mod.rs#L2003` (`from == to`) |
| Value-bearing CALL, CALLCODE, tx value and CREATE/CREATE2: log emitted right after the balance move, before any later log, so the tx-value log comes first | `S:vm/interpreter.py#L418-L431` (inside `process_call`, before precompile or code runs) | `execution/vm/evm.go#L358-L365` (call path) and `#L742-L752` (create path, after `CreateAccount` and nonce set) via `misc.Transfer`; `eip7708.go#L52-L67` | `evm/mod.rs#L910-L918` |
| Delegated (EIP-7702) targets and precompiles: log goes to the called account, not the delegate; precompile calls also log | `S:vm/interpreter.py#L418-L436` (transfer precedes the precompile branch) | `evm.go#L362` uses `addr`, the called account, before the `isPrecompile` switch at `#L367-L369` | not read in detail |
| SELFDESTRUCT log only when beneficiary differs from originator and balance is non-zero | `S:vm/instructions/system.py#L762-L767` | `instructions.go#L1501-L1503` | `evm/mod.rs#L1866-L1871` |
| SELFDESTRUCT to self leaves the balance unchanged, with no log | `S:vm/instructions/system.py#L763-L767` | `instructions.go#L1462-L1473` (move only when `self != beneficiary`) | `evm/mod.rs#L1866-L1878` (EIP8246 skips the burn) |
| Marked for deletion only if created in the same transaction | `S:vm/instructions/system.py#L769-L771` | `instructions.go#L1455-L1479` (`IsNewContract`, then `Selfdestruct(self, true)`) | `evm/mod.rs#L1862`, `#L1879-L1881` |
| Finalization: a destroyed account with non-zero balance keeps the balance, nonce 0, empty code, cleared storage; a zero-balance one is deleted | `S:fork.py#L1088-L1089`; `S:state_tracker.py#L494-L515`; EIP-8246 | `execution/state/intra_block_state.go#L2504-L2533` (`sdPreserveBalance`: Nonce=0, CodeHash=empty, `deleted=false`; otherwise deleted at `#L2507`) | `evm/state/mod.rs#L870-L892` |
| Dead-beneficiary state gas and the extra account-write gas apply only if the beneficiary is dead and the originator's balance is non-zero | `S:vm/instructions/system.py#L743-L756` | `execution/vm/operations_acl.go#L278-L297` (`empty && !balance.IsZero()`, then `AccountWriteCostEIP8038` and `StateGasNewAccount`) | not read |
| SELFDESTRUCT touches the beneficiary for BAL even with a zero transfer | `S:vm/instructions/system.py#L735-L737` | `operations_acl.go#L287-L289` | not read |
| Logs from a reverted frame or a failed transaction are dropped | `S:vm/interpreter.py#L414` (`snapshot = copy_tx_state`; the log lives on the EVM frame) | `intra_block_state.go#L2456` and `#L2482` (`logs.truncate(snapshot.logCount)`) | `evm/mod.rs#L1834-L1836` (`rollback`) |
| Withdrawals, fees, coinbase tips and system calls emit no transfer log | `S:fork.py#L1096-L1115` (`create_ether`, no log) | The only emit sites are `eip7708.go#L65` and `instructions.go#L1502` (grepped over `execution/`). System calls with zero value skip `Transfer` at `evm.go#L352-L357` | no emit sites other than `log_eip7708_transfer` |
| No gas is charged for the log | `S:vm/__init__.py#L247-L281` (appends to `evm.logs` only) | `AddLog` (`intra_block_state.go#L562-L570`) is called outside the gas functions | not read |
| Log index and tx index are assigned by the state, not the caller | block-level numbering in `S:fork.py#L1074-L1086` | `intra_block_state.go#L560-L561` and `#L538-L539` | not read |

One difference exists but changes no results. Erigon's pre-8246 branch burns, and its finalization burn hook (`intra_block_state.go#L2509-L2511`) cannot fire under 8246, because the preserve branch is taken first for any non-zero balance.

Not independently verified: receipt encoding and bloom contents for the synthetic logs, and Reth's gas and BAL behavior beyond the lines above.

## 4. Reth agreement summary

Reth (through evm2) agrees with both the spec and Erigon on every rule where I read it. I found no point where Reth sides with Erigon against the spec, because I found no point where Erigon differs from the spec.

## 5. Known-check

Searches ran through api.github.com/search/issues, unauthenticated. Searches on execution-specs returned no 8246 or 7708 issue. The Test Release Tracker issues were not located through search.

| Topic | Result |
|---|---|
| No spec difference found | Nothing to mark known or unknown. |
| Erigon, EIP-8246 parallel-exec and BAL area (context for the section 2 observation) | Known work, not claimed as a bug: [#23238](https://github.com/erigontech/erigon/issues/23238) open (preserved balance wiped on read); [#24906](https://github.com/erigontech/erigon/issues/24906) open (same-tx create + SELFDESTRUCT in parallel exec); [PR #24096](https://github.com/erigontech/erigon/pull/24096) open, unmerged (preserve 8246 balance reads); [PR #25266](https://github.com/erigontech/erigon/pull/25266) open (pre-8246 BAL burn fix); [PR #25160](https://github.com/erigontech/erigon/pull/25160) merged 2026-10-22 (no writes for create-and-destroy in one tx). |
| Erigon, 7708 | [PR #24402](https://github.com/erigontech/erigon/pull/24402) closed, eth_simulateV1 skips synthetic transfer logs; [#22224](https://github.com/erigontech/erigon/issues/22224) closed (coinbase and burnt-fee exclusivity in parallel exec). |
| Queries used | `repo:erigontech/erigon 7708`, `repo:erigontech/erigon 8246`, `repo:erigontech/erigon selfdestruct amsterdam`, `repo:ethereum/execution-specs 8246`, `repo:ethereum/execution-specs 7708 bogota` |
| Hive, [listing.jsonl](https://hive.ethpandaops.io/glamsterdam/listing.jsonl) | The Erigon rows I read (`eels/consume-enginex`, 83258 of 83258 passed; `eels/execute`, 173 of 173) were 2026-10-26 runs, with Nethermind and Reth rows alongside. I did not map individual tests to 7708 or 8246. |

## 6. URLs read

- https://api.github.com/repos/erigontech/erigon/commits/main
- https://api.github.com/repos/ethereum/execution-specs/commits/forks/bogota
- https://api.github.com/repos/paradigmxyz/reth/commits/main
- https://api.github.com/repos/ethereum/execution-specs/git/trees/7f2f4c9bf6d73b09ecb8b24eb14d08bf0cf0247b?recursive=1
- raw.githubusercontent.com/ethereum/execution-specs/7f2f4c9…/src/ethereum/forks/amsterdam/: `vm/__init__.py`, `vm/instructions/system.py`, `fork.py`, `transactions.py`, `vm/interpreter.py`, `state_tracker.py`
- https://eips.ethereum.org/EIPS/eip-7708, https://eips.ethereum.org/EIPS/eip-8246
- Shallow clones of https://github.com/erigontech/erigon, https://github.com/paradigmxyz/reth and https://github.com/alloy-rs/evm2
- The issue and PR URLs in section 5, and https://hive.ethpandaops.io/glamsterdam/listing.jsonl
