Skip to content

EIP-8246 (Remove SELFDESTRUCT Burn)#11949

Draft
Marchhill wants to merge 4 commits into
feature/eip-2780-reduce-intrinsic-tx-gasfrom
feature/eip-8246
Draft

EIP-8246 (Remove SELFDESTRUCT Burn)#11949
Marchhill wants to merge 4 commits into
feature/eip-2780-reduce-intrinsic-tx-gasfrom
feature/eip-8246

Conversation

@Marchhill

Copy link
Copy Markdown
Contributor

Removes the residual ETH-burn cases left by EIP-6780:

  • A self-targeting SELFDESTRUCT now moves no ETH and emits no log, regardless of whether the account was created in the same transaction.
  • At transaction finalization, accounts marked for destruction keep their balance: storage and code are cleared and the nonce is reset to 0. A resulting zero-balance account is still removed as empty per EIP-161, matching the spec and keeping CREATE2 redeployment unblocked.

Wires a new IsEip8246Enabled flag through IReleaseSpec / ReleaseSpec / decorators / chainspec (Eip8246TransitionTimestamp) and enables it in the Amsterdam fork. Adds Eip8246Tests covering same-transaction self-destruct to self/other, post-destruct funding, the zero-balance empty-account case, and the unchanged not-in-same-tx no-op, each pinned with EIP on and off.

Fixes Closes Resolves #

Please choose one of the keywords above to refer to the issue this PR solves followed by the issue number (e.g. Fixes #000). If no issue number, remove the line. Also, remove everything marked optional that is not applicable. Remove this note after reading.

Changes

  • List the changes

Types of changes

What types of changes does your code introduce?

  • Bugfix (a non-breaking change that fixes an issue)
  • New feature (a non-breaking change that adds functionality)
  • Breaking change (a change that causes existing functionality not to work as expected)
  • Optimization
  • Refactoring
  • Documentation update
  • Build-related changes
  • Other: Description

Testing

Requires testing

  • Yes
  • No

If yes, did you write tests?

  • Yes
  • No

Notes on testing

Optional. Remove if not applicable.

Documentation

Requires documentation update

  • Yes
  • No

If yes, link the PR to the docs update or the issue with the details labeled docs. Remove if not applicable.

Requires explanation in Release Notes

  • Yes
  • No

If yes, fill in the details here. Remove if not applicable.

Remarks

Optional. Remove if not applicable.

Removes the residual ETH-burn cases left by EIP-6780:

- A self-targeting SELFDESTRUCT now moves no ETH and emits no log,
  regardless of whether the account was created in the same transaction.
- At transaction finalization, accounts marked for destruction keep their
  balance: storage and code are cleared and the nonce is reset to 0. A
  resulting zero-balance account is still removed as empty per EIP-161,
  matching the spec and keeping CREATE2 redeployment unblocked.

Wires a new IsEip8246Enabled flag through IReleaseSpec / ReleaseSpec /
decorators / chainspec (Eip8246TransitionTimestamp) and enables it in the
Amsterdam fork. Adds Eip8246Tests covering same-transaction self-destruct
to self/other, post-destruct funding, the zero-balance empty-account case,
and the unchanged not-in-same-tx no-op, each pinned with EIP on and off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@claude

claude Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 5m 8s —— View job


EIP-8246 PR Review

  • Gather context and read changed files
  • Review spec/flag wiring
  • Review EVM SELFDESTRUCT instruction changes
  • Review TransactionProcessor finalization changes
  • Review test coverage
  • Post findings

Summary

The implementation is logically correct and the spec wiring is complete. The SELFDESTRUCT opcode change, the two finalization paths, and all the plumbing (IReleaseSpec, decorators, ChainSpec, Amsterdam fork) are internally consistent. However, the test suite has a significant gap and there are two issues in the deferred-finalization path that warrant attention before merge.


Findings

High

Tests only exercise the non-deferred finalization pathEip8246Tests constructs a Cancun-based spec without IsEip8037Enabled/IsEip7708Enabled, so destruction is handled by the !deferFinalization branch in ExecuteEvmCall. In Amsterdam (the fork that actually enables EIP-8246), EIP-8037 + EIP-7708 are also enabled, which routes all destruction through the separate FinalizeDestroyedAccount static helper in ExecuteEvmTransaction. That code path — with its EIP-8246 DeleteAccountCreateAccount(balance) logic — has zero test coverage. See inline →

Medium

  1. Deferred path: missing tracer.ReportRefund for destroy refund — The non-deferred loop calls tracer.ReportRefund(spec.GasCosts.DestroyRefund) per destroyed account; FinalizeDestroyedAccount does not. Tracers operating against Amsterdam blocks will silently miss these events. May be pre-existing but is now the production path for EIP-8246. See inline →

  2. Deferred vs non-deferred log type inconsistency — Deferred path emits TransferLog.CreateBurn(…), non-deferred emits TransferLog.CreateSelfDestruct(…) for the same semantic event (EIP-7708 burn log). If these are distinct EIP-7708 event types they need to be intentional and documented; if they should be the same, one site needs fixing. See inline →

  3. Missing CREATE2 redeployment test — The PR description explicitly calls out "keeping CREATE2 redeployment unblocked" for zero-balance self-destructs, but there is no test that attempts a CREATE2 re-deployment to the same address after the first deployment self-destructs with zero balance. See inline →

Low

  • PR description template not cleaned up — The body still contains "Fixes Closes Resolves #", un-ticked type-of-change checkboxes, and unreplaced placeholder sections.

What looks good

  • Opcode logic is correct. For inheritor == executingAccount, neither CreateAccount(inheritor, result) nor AddToBalance is reached (the executing contract must exist), so there is no double-count of the balance before the goto Stop. The pre-8246 burn path (SubtractFromBalance after AddSelfDestructLog) is cleanly bypassed only when RemoveSelfdestructBurn is true.
  • Finalization balance preservation is correct. ClearStorageDeleteAccountCreateAccount(addr, balance) produces a nonce-0, code-less, storage-free account with the original balance; the zero-balance case naturally drops to EIP-161 deletion. AssertBalanceOnly validates all three invariants.
  • Spec wiring is complete. IReleaseSpec, IReleaseSpecExtensions, ReleaseSpec, ReleaseSpecDecorator, OverridableReleaseSpec, ChainParameters, ChainSpecParamsJson, ChainSpecLoader, ChainSpecBasedSpecProvider, and Amsterdam are all updated consistently and the transition timestamp approach matches the rest of the codebase.
  • Existing EIP-6780 no-op is preserved. The goto Stop condition correctly keeps the pre-8246 cross-transaction no-op intact: selfdestructOnlyOnSameTx && !createInSameTx still fires when EIP-8246 is off.

{
private readonly ISpecProvider _specProvider =
new TestSpecProvider(new OverridableReleaseSpec(Cancun.Instance) { IsEip8246Enabled = eip8246Enabled });

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.

High: Tests only exercise the non-deferred finalization path

The spec here is Cancun without IsEip8037Enabled or IsEip7708Enabled, so deferFinalization = false in ExecuteEvmTransaction. In Amsterdam (the fork where EIP-8246 is actually enabled), both EIP-8037 and EIP-7708 are also active, which routes destruction through the separate FinalizeDestroyedAccount static helper instead.

That deferred path is not covered by any test in this PR — the EIP-8246 balance-preservation logic there (DeleteAccount followed by CreateAccount(toBeDestroyed, balance)) runs without test coverage.

Consider adding a fixture variant that enables EIP-8037 + EIP-7708 alongside EIP-8246 to drive the deferred path, e.g.:

private readonly ISpecProvider _specProvider =
    new TestSpecProvider(new OverridableReleaseSpec(Cancun.Instance)
    {
        IsEip8246Enabled = eip8246Enabled,
        IsEip8037Enabled = eip8246Enabled,
        IsEip7708Enabled = eip8246Enabled,
    });

Or a separate [TestFixture] that targets the Amsterdam spec directly.

UInt256 balance = worldState.GetBalance(toBeDestroyed);
if (!balance.IsZero)
// EIP-7708 logs the burn; suppressed once EIP-8246 stops burning.
if (!balance.IsZero && !removeSelfdestructBurn)

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.

Medium: Log type diverges from the non-deferred path

The non-deferred path (lines ~1278) emits TransferLog.CreateSelfDestruct(...) for the burn event, while this deferred path emits TransferLog.CreateBurn(...). If these are distinct EIP-7708 event types, trace consumers that handle both paths will see different topic/data layouts for the same semantic event.

If they are intentionally different, a brief comment explaining why would help future readers. If they should match, one of the two sites needs updating.

if (removeSelfdestructBurn && !balance.IsZero)
{
worldState.CreateAccount(toBeDestroyed, balance);
}

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.

Medium: Destroy refund not reported to tracer in deferred path

The non-deferred path calls tracer.ReportRefund(spec.GasCosts.DestroyRefund) for each destroyed account. FinalizeDestroyedAccount has no equivalent call, so tracers operating in Amsterdam (EIP-8037 + EIP-7708 active) never see the destroy refund events. This may be pre-existing, but with EIP-8246 now routing through this path in production it deserves explicit resolution — either adding the ReportRefund call here, or documenting that refund tracing is intentionally skipped in the deferred path.


Assert.That(TestState.GetBalance(inheritor), Is.EqualTo((UInt256)5.Ether));
Assert.That(TestState.AccountExists(child), Is.False);
}

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.

Medium: Missing CREATE2 redeployment test

The PR description explicitly says "keeping CREATE2 redeployment unblocked" as a key property of the EIP-8246 zero-balance case. There is no test that verifies a CREATE2 call to the same address succeeds after a zero-balance self-destruct.

Suggested additional test:

  1. CREATE2-deploy a contract with zero balance that self-destructs to self (same tx).
  2. Assert the address is gone.
  3. CREATE2-deploy the same init code to the same salt again in a subsequent transaction.
  4. Assert the new deployment succeeds and the address exists.

- Add an EIP-8037 + EIP-7708 fixture dimension so every EIP-8246 scenario
  also runs through the deferred FinalizeDestroyedAccount path (as in
  Amsterdam), covering the balance-preservation logic there.
- Add a CREATE2 redeployment test: a factory re-creates a self-destructed
  child at the same address across transactions, verifying the nonce reset
  keeps redeployment unblocked and the preserved balance accumulates.
- Report the destroy refund to the tracer in the deferred path, matching
  the inline path.
- Document the pre-existing Burn-vs-SelfDestruct log distinction between the
  post-fee and pre-fee finalization paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Marchhill

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 4m 40s —— View job


EIP-8246 PR Review

  • Gather context
  • Read changed files
  • Review spec/flag wiring
  • Review EVM SELFDESTRUCT instruction changes
  • Review TransactionProcessor finalization changes
  • Review test coverage
  • Post findings

Summary

All findings from the prior review have been resolved. The implementation is correct and the test suite is comprehensive. This PR is ready to merge.


Previous findings — all resolved

Prior finding Status
High: Tests only covered the non-deferred finalization path Fixed — four [TestFixture] combinations (eip8246Enabled × deferredFinalization) now drive both the inline and FinalizeDestroyedAccount paths for every scenario
Medium: Missing tracer.ReportRefund in deferred path Fixedif (tracingRefunds) tracer.ReportRefund(destroyRefund) is called in both the count > 1 loop and the count == 1 branch of ExecuteEvmTransaction
Medium: CreateBurn vs CreateSelfDestruct log type divergence Documented — added comment (TransactionProcessor.cs:341–344) explaining the intentional distinction: the deferred path runs after PayFees, so the balance that disappears includes priority fees → CreateBurn; the pre-fee path uses CreateSelfDestruct. Semantically correct.
Medium: Missing CREATE2 redeployment test FixedCreate2_redeploy_to_same_address_unblocked_after_self_destruct added, verifying that nonce-0 cleanup after EIP-8246 finalization keeps the same CREATE2 address redeployable

What looks good

Opcode logic (EvmInstructions.ControlFlow.cs:256–279)

The goto Stop condition is precisely right:

inheritor == executingAccount && (RemoveSelfdestructBurn || EIP-6780 cross-tx no-op)
  • EIP-8246 on: short-circuits the branch regardless of createInSameTx, so neither SubtractFromBalance nor AddSelfDestructLog runs.
  • EIP-8246 off: the EIP-6780 cross-tx case (selfdestructOnlyOnSameTx && !createInSameTx) is preserved exactly.
  • The pre-8246 same-tx self-burn path (when EIP-8246 is off and createInSameTx is true) still reaches SubtractFromBalance correctly.

Finalization balance preservation

Both finalization paths produce the same invariant: ClearStorage → DeleteAccount → CreateAccount(addr, balance) when removeSelfdestructBurn && !balance.IsZero. A zero-balance account naturally skips CreateAccount, falling through to EIP-161 empty-account removal. AssertBalanceOnly validates nonce=0, no code, and the expected balance.

Destroy-list gate

Accounts that hit the EIP-6780 cross-tx no-op are never added to destroyList (!selfdestructOnlyOnSameTx || createInSameTx is false), so FinalizeDestroyedAccount is never called for them — there is no spurious balance loss.

Spec wiring

IReleaseSpec, IReleaseSpecExtensions (RemoveSelfdestructBurn), ReleaseSpec, ReleaseSpecDecorator, OverridableReleaseSpec, ChainParameters, ChainSpecParamsJson, ChainSpecLoader, ChainSpecBasedSpecProvider, and Amsterdam are all updated consistently and match the transition-timestamp pattern used by every other EIP flag in the file.

Test coverage

Five scenarios × four fixtures (on/off × deferred/inline) = 20 test runs. The Create2_redeploy_to_same_address_unblocked_after_self_destruct test uses _selfDestructToSelf (raw ADDRESS; SELFDESTRUCT) as the CREATE2 init code — intentional: the child self-destructs during its own init, correctly exercising the same-tx path. The address computation uses the same bytecode hash, so the child address matches across both calls. ✓


Remaining low note

PR description template not cleaned up — the body still contains "Fixes Closes Resolves #", un-ticked type-of-change checkboxes, and unreplaced placeholder sections. No code impact; please clean up before merging.


Verdict: ready to merge (pending description cleanup).

Marchhill and others added 2 commits June 10, 2026 12:04
EIP-8246 is a Draft and is not part of the EEST `for_amsterdam` conformance
fixtures. Enabling it in the Amsterdam fork changed SELFDESTRUCT behaviour
for that fork, diverging from the reference post-states and failing the
Pyspec tests across all shards.

Keep the full implementation and spec/chainspec plumbing so the EIP can be
activated via the Eip8246Transition chainspec parameter once it is scheduled,
but leave it disabled in the named forks. Unit tests toggle the flag directly
via OverridableReleaseSpec, so coverage is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eip-8246

# Conflicts:
#	src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs
@github-actions

Copy link
Copy Markdown
Contributor

EVM Opcode Benchmark Diff

Aggregated runs: base=3, pr=3
Noisy rerun opcodes: EXTCODESIZE, LOG1

No significant regressions or improvements detected.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant