Skip to content

feat(core/vm,core/state): EIP-8246 selfdestruct burn removal #35219, close #2766 - #2834

Merged
gzliudan merged 2 commits into
XinFinOrg:dev-upgradefrom
gzliudan:eip-8246-selfdestruct-burn
Oct 27, 2026
Merged

gzliudan merged 2 commits into
XinFinOrg:dev-upgradefrom
gzliudan:eip-8246-selfdestruct-burn

Conversation

@gzliudan

@gzliudan gzliudan commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Implements EIP-8246 (Remove SELFDESTRUCT Burn) for Amsterdam:

  • a contract created in the same transaction that executes SELFDESTRUCT with itself as beneficiary keeps its balance instead of burning it;
  • at transaction finalisation every account marked for destruction is reduced to a balance-only account - nonce reset to 0, code and storage cleared, balance untouched - instead of being deleted. An account left with a zero balance is empty and is still removed by EIP-161.

Implementation

Upstream go-ethereum ethereum#35219 keys both halves on the fork rules at finalisation, but this tree's StateDB carries no rules and its SelfDestruct clears the balance eagerly. The fork decision is therefore taken where the interpreter already knows it:

  • StateDB.SelfDestruct6780(addr, preserveBalance) takes the flag, derived from chainRules.IsAmsterdam in opSelfdestruct6780;
  • the destruct journal entry restores the object flags when it is reverted;
  • stateObject.selfDestructedPreserveBalance drives Finalise and hookedStateDB.Finalise;
  • opSelfdestruct6780 keeps the historical transfer pair for self-destruct-to-self until Amsterdam and drops it from Amsterdam onwards, so the pre-Amsterdam path is unchanged;

Finalise calls preserveDestructedBalance for a destructed account with a non-zero balance: it records the previous incarnation in stateObjectsDestruct, clears the per-address diff caches, rebuilds the account from an empty object that keeps its pre-block origin, and marks it updated. Keeping the origin keeps the block-level diff honest: the account is recorded as having existed at block start rather than as a fresh one, instead of relying on handleDestruction to repair the value at commit. handleDestruction then deletes the old storage trie at commit, exactly like a delete-and-resurrect within one block. The resulting nonce 0, empty code and empty storage root keep the address usable as a CREATE2 target, as the EIP requires.

Gas, the transfer to a distinct beneficiary, readability of the account until the end of the transaction, and the EIP-6780 create-in-same-transaction restriction are unchanged. EIP-7708 is not implemented in this repository, so unlike upstream there is no burn log to remove from the SELFDESTRUCT path.

Tests

core/state/statedb_eip8246_test.go:

  • preserved balance after finalisation, and after a commit/reopen round trip;
  • zero balance still deleted by EIP-161;
  • ether arriving after the destruct is preserved;
  • destruct-to-elsewhere followed by late funds keeps the remainder as a balance-only account;
  • reverting the selfdestruct restores balance, nonce, code and storage;
  • the pre-Amsterdam burn, both at the instruction and at finalisation;
  • tracer hooks: no burn hook on Amsterdam, unchanged burn hook before it;
  • the block-level diff of a preserved account records its pre-block origin (the reachable prefunded shape, where evm.create reuses the account instead of resetting it).

core/vm/eip8246_test.go pins the balance-change events of a self-destruct to self on both sides of the fork: two decreases and one increase before Amsterdam, none on Amsterdam with the balance preserved. That event sequence is the only observable effect of the interpreter's fork gate, so removing the gate makes the test fail. A distinct beneficiary is covered the same way: the transfer pair is pinned on both sides of the fork, and only a self-beneficiary destruct drops it (an existing and a new contract in each shape).

core/eip8246_test.go drives the same contract through block processing with params.AllEthashProtocolChanges (Amsterdam at block 0) and with Amsterdam disabled, checking the account fields in the post-state (balance, nonce, code, storage); checks that a same-transaction destruct-to-other whose marked account is refunded 7+8 wei by two later CALLs ends as a balance-only account holding 15 (ported from upstream ethereum#35364, TestEIP8246SelfdestructRefunded); and verifies that a preserved balance-only account can be re-created through CREATE2 in a later block (the second creation adds 1 wei to the preserved 5).

Verification

  • make all passes; gofmt/goimports clean.
  • golangci-lint run --new-from-rev=c2725ee4de ./core ./core/state/... ./core/vm/... reports 0 issues (base c2725ee4de, the current dev-upgrade tip).
  • the branch was rebased onto c2725ee4de and every command above was re-run on that base.
  • go test ./core/ ./core/state/... ./core/vm/... ./eth/tracers/... ./internal/ethapi/... -count=1 passes.
  • go mod tidy -diff is empty.
  • the pre-Amsterdam SELFDESTRUCT state roots are byte-identical to the pre-EIP-8246 baseline for four shapes (created-to-self, created-to-other, pre-existing-to-self, pre-existing-to-other), and the balance-change event sequence of a self-destruct to self is unchanged (2 decreases, 1 increase, as asserted by TestSelfdestruct6780EventForkGate); on Amsterdam the same call emits no balance events and keeps the balance.

Follow-ups

#2833 (the supply live tracer gate) has been merged into dev-upgrade as c2725ee4de, and this branch is rebased on top of it. eth/tracers/live/supply.go records that its Amsterdam gate is only correct once the consensus side of EIP-8246 is in effect: a chain scheduling Amsterdam without it would still burn the balance while the tracer stops reporting it. This PR is that consensus side, so the two have to ship together.

Ref: ethereum#35219
Closes #2766

@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d3b8300c-f4da-46cc-98fa-1e1566a8e314

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gzliudan
gzliudan force-pushed the eip-8246-selfdestruct-burn branch from 36b117e to 6d08fde Compare October 20, 2026 10:28
@gzliudan
gzliudan force-pushed the eip-8246-selfdestruct-burn branch 3 times, most recently from 2b719b4 to 4beef99 Compare October 20, 2026 15:01
@gzliudan
gzliudan force-pushed the eip-8246-selfdestruct-burn branch from 4beef99 to 31fe83a Compare October 20, 2026 23:51
gzliudan added a commit to gzliudan/XDPoSChain that referenced this pull request Oct 21, 2026
EIP-7708 emits a system log for every value transfer from Amsterdam onwards:
the transaction value, the value moved by a CALL or CREATE, and the value
moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the
ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount),
so indexers can consume it like a token transfer.

The gate sits where the fork rules are available: core.Transfer takes the
vm's params.Rules and emits after the balance move, while opSelfdestruct6780
emits when the beneficiary differs. Zero-value transfers and transfers to self
emit nothing.

No burn log is emitted, which is an ordering constraint rather than a
statement about this tree: EIP-8246 (ethereum#35219) removes the SELFDESTRUCT burn and
XinFinOrg#2834 lands that change here, but until XinFinOrg#2834 is merged a self-destruct still
destroys balances in two corners, both unlogged by this commit:

  - a contract created in the same transaction that self-destructs to itself
    has its balance destroyed by SelfDestruct6780 and Finalise;
  - a contract created in the same transaction that self-destructs and then
    receives funds has those funds destroyed at Finalise.

Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the
window is dormant; this prerequisite is why the change must merge after XinFinOrg#2834,
or in the same batch.

The log constructor takes *big.Int rather than upstream's *uint256.Int because
this tree's StateDB moves balances as big.Int.

Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing
(three transfer logs interleaved with the contract's events), the zero-value
and pre-Amsterdam cases, a transfer to self, a reverted call frame, and the
upstream boundary matrix: DELEGATECALL (no log), a CREATE2 endowment, a failed
CREATE rolling its log back, precompile and system-address recipients, the
priority-fee exclusion, and a direct core.Transfer fork-gate case pinning the
32-byte amount. core/state additionally pins that a log is reverted with its
snapshot.

Ref: ethereum#33645
Ref: ethereum#35617
Closes XinFinOrg#2767
gzliudan added a commit to gzliudan/XDPoSChain that referenced this pull request Oct 21, 2026
…eum#33645

EIP-7708 emits a system log for every value transfer from Amsterdam onwards:
the transaction value, the value moved by a CALL or CREATE, and the value
moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the
ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount),
so indexers can consume it like a token transfer.

The gate sits where the fork rules are available: core.Transfer takes the
vm's params.Rules and emits after the balance move, while opSelfdestruct6780
emits when the beneficiary differs. Zero-value transfers and transfers to self
emit nothing.

No burn log is emitted, which is an ordering constraint rather than a
statement about this tree: EIP-8246 (ethereum#35219) removes the SELFDESTRUCT burn and
XinFinOrg#2834 lands that change here, but until XinFinOrg#2834 is merged a self-destruct still
destroys balances in two corners, both unlogged by this commit:

  - a contract created in the same transaction that self-destructs to itself
    has its balance destroyed by SelfDestruct6780 and Finalise;
  - a contract created in the same transaction that self-destructs and then
    receives funds has those funds destroyed at Finalise.

Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the
window is dormant; this prerequisite is why the change must merge after XinFinOrg#2834,
or in the same batch.

The log constructor takes *big.Int rather than upstream's *uint256.Int because
this tree's StateDB moves balances as big.Int.

Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing
(three transfer logs interleaved with the contract's events), the zero-value
and pre-Amsterdam cases, a transfer to self, a reverted call frame, and the
upstream boundary matrix: DELEGATECALL (no log), a CREATE2 endowment, a failed
CREATE rolling its log back, precompile and system-address recipients, the
priority-fee exclusion, and a direct core.Transfer fork-gate case pinning the
32-byte amount. core/state additionally pins that a log is reverted with its
snapshot.

Ref: ethereum#33645
Ref: ethereum#35617
Closes XinFinOrg#2767
gzliudan added a commit to gzliudan/XDPoSChain that referenced this pull request Oct 22, 2026
…eum#33645

EIP-7708 emits a system log for every value transfer from Amsterdam onwards:
the transaction value, the value moved by a CALL or CREATE, and the value
moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the
ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount),
so indexers can consume it like a token transfer.

The gate sits where the fork rules are available: core.Transfer takes the
vm's params.Rules and emits after the balance move, while opSelfdestruct6780
emits when the beneficiary differs. Zero-value transfers and transfers to self
emit nothing.

No burn log is emitted, which is an ordering constraint rather than a
statement about this tree: EIP-8246 (ethereum#35219) removes the SELFDESTRUCT burn and
XinFinOrg#2834 lands that change here, but until XinFinOrg#2834 is merged a self-destruct still
destroys balances in two corners, both unlogged by this commit:

  - a contract created in the same transaction that self-destructs to itself
    has its balance destroyed by SelfDestruct6780 and Finalise;
  - a contract created in the same transaction that self-destructs and then
    receives funds has those funds destroyed at Finalise.

Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the
window is dormant; this prerequisite is why the change must merge after XinFinOrg#2834,
or in the same batch.

The log constructor takes *big.Int rather than upstream's *uint256.Int because
this tree's StateDB moves balances as big.Int.

Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing
(three transfer logs interleaved with the contract's events), the zero-value
and pre-Amsterdam cases, a transfer to self, a reverted call frame, and the
upstream boundary matrix: DELEGATECALL (no log), a CREATE2 endowment, a failed
CREATE rolling its log back, precompile and system-address recipients, the
priority-fee exclusion, and a direct core.Transfer fork-gate case pinning the
32-byte amount. core/state additionally pins that a log is reverted with its
snapshot.

Ref: ethereum#33645
Ref: ethereum#35617
Closes XinFinOrg#2767
@gzliudan
gzliudan force-pushed the eip-8246-selfdestruct-burn branch 2 times, most recently from d299682 to 88bcbff Compare October 22, 2026 17:23
@gzliudan
gzliudan force-pushed the eip-8246-selfdestruct-burn branch from 88bcbff to ee81b1a Compare October 24, 2026 01:51
gzliudan added a commit to gzliudan/XDPoSChain that referenced this pull request Oct 26, 2026
…eum#33645

EIP-7708 emits a system log for every value transfer from Amsterdam onwards:
the transaction value, the value moved by a CALL or CREATE, and the value
moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the
ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount),
so indexers can consume it like a token transfer.

The gate sits where the fork rules are available: core.Transfer takes the
vm's params.Rules and emits after the balance move, while opSelfdestruct6780
emits when the beneficiary differs. Zero-value transfers and transfers to self
emit nothing.

No burn log is emitted, which is an ordering constraint rather than a
statement about this tree: EIP-8246 (ethereum#35219) removes the SELFDESTRUCT burn and
XinFinOrg#2834 lands that change here, but until XinFinOrg#2834 is merged a self-destruct still
destroys balances in two corners, both unlogged by this commit:

  - a contract created in the same transaction that self-destructs to itself
    has its balance destroyed by SelfDestruct6780 and Finalise;
  - a contract created in the same transaction that self-destructs and then
    receives funds has those funds destroyed at Finalise.

Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the
window is dormant; this prerequisite is why the change must merge after XinFinOrg#2834,
or in the same batch.

The log constructor takes *big.Int rather than upstream's *uint256.Int because
this tree's StateDB moves balances as big.Int.

Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing
(three transfer logs interleaved with the contract's events), the zero-value
and pre-Amsterdam cases, a transfer to self, a reverted call frame, and the
upstream boundary matrix: DELEGATECALL (no log), a CREATE2 endowment, a failed
CREATE rolling its log back, precompile and system-address recipients, the
priority-fee exclusion, and a direct core.Transfer fork-gate case pinning the
32-byte amount. core/state additionally pins that a log is reverted with its
snapshot.

Ref: ethereum#33645
Ref: ethereum#35617
Closes XinFinOrg#2767
@gzliudan
gzliudan force-pushed the eip-8246-selfdestruct-burn branch 2 times, most recently from 5be2dd5 to defba4d Compare October 26, 2026 05:32
gzliudan added a commit to gzliudan/XDPoSChain that referenced this pull request Oct 26, 2026
…eum#33645

EIP-7708 emits a system log for every value transfer from Amsterdam onwards:
the transaction value, the value moved by a CALL or CREATE, and the value
moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the
ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount),
so indexers can consume it like a token transfer.

The gate sits where the fork rules are available: core.Transfer takes the
vm's params.Rules and emits after the balance move, while opSelfdestruct6780
emits when the beneficiary differs. Zero-value transfers and transfers to self
emit nothing.

No burn log is emitted, which is an ordering constraint rather than a
statement about this tree: EIP-8246 (ethereum#35219) removes the SELFDESTRUCT burn and
XinFinOrg#2834 lands that change here, but until XinFinOrg#2834 is merged a self-destruct still
destroys balances in two corners, both unlogged by this commit:

  - a contract created in the same transaction that self-destructs to itself
    has its balance destroyed by SelfDestruct6780 and Finalise;
  - a contract created in the same transaction that self-destructs and then
    receives funds has those funds destroyed at Finalise.

Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the
window is dormant; this prerequisite is why the change must merge after XinFinOrg#2834,
or in the same batch.

The log constructor takes *big.Int rather than upstream's *uint256.Int because
this tree's StateDB moves balances as big.Int.

Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing
(three transfer logs interleaved with the contract's events), the zero-value
and pre-Amsterdam cases, a transfer to self, a reverted call frame, and the
upstream boundary matrix: DELEGATECALL (no log), a CREATE2 endowment, a failed
CREATE rolling its log back, precompile and system-address recipients, the
priority-fee exclusion, and a direct core.Transfer fork-gate case pinning the
32-byte amount. core/state additionally pins that a log is reverted with its
snapshot.

Ref: ethereum#33645
Ref: ethereum#35617
Closes XinFinOrg#2767
@gzliudan
gzliudan force-pushed the eip-8246-selfdestruct-burn branch from defba4d to d0d19de Compare October 27, 2026 08:00
…ethereum#35219

EIP-8246 removes the remaining SELFDESTRUCT burns from Amsterdam onwards:

- a contract created in the same transaction that executes SELFDESTRUCT with
  itself as beneficiary keeps its balance instead of burning it;
- at transaction finalisation every account marked for destruction is reduced
  to a balance-only account - nonce reset to 0, code and storage cleared,
  balance untouched - instead of being deleted. An account left with a zero
  balance is empty and is still removed by EIP-161.

The upstream implementation (go-ethereum ethereum#35219) keys both halves on the fork
rules at finalisation, but this tree's StateDB carries no rules and its
SelfDestruct clears the balance eagerly. The decision is therefore taken where
the interpreter already knows the fork: SelfDestruct6780 takes a
preserveBalance flag derived from chainRules.IsAmsterdam, the destruct journal
entry clears the object flag when it is reverted, and Finalise and
hookedStateDB.Finalise read it off the object. The balance move itself stays
fork-gated, so the pre-Amsterdam path keeps its historical transfer pair and
only Amsterdam drops it. Everything else - gas, the transfer to a distinct
beneficiary, readability of the account until the end of the transaction, and
the EIP-6780 create-in-same-transaction restriction - is unchanged.

A preserved account is tracked in stateObjectsDestruct and rebuilt from an empty
object that keeps its pre-block origin, so handleDestruction deletes the old
storage trie at commit, exactly like a delete-and-resurrect within one block,
and the block-level diff records the account as having existed at block start
rather than as a fresh one. Nonce 0, empty code and an empty storage root keep
the address usable as a CREATE2 target, as the EIP requires.

EIP-7708 is not implemented in this repository, so unlike upstream there is no
burn log to remove from the SELFDESTRUCT path.

Tests: core/state covers the preserved balance, the zero-balance deletion,
ether arriving after the destruct, the destruct-to-elsewhere shape, revert, the
pre-Amsterdam burn, the tracer hooks and the block diff of a preserved account;
core drives the same contract through block processing with Amsterdam at block
0 and with Amsterdam disabled, checks a same-transaction destruct-to-other
followed by refunded funds (7+8 wei from two CALLs), and re-creates a preserved
balance-only account through CREATE2 in a later block. core/vm pins the balance-change events of a
self-destruct to self on both sides of the fork (two decreases and one increase
before Amsterdam, none on Amsterdam), the only observable effect of the
interpreter's fork gate.

Verified on c2725ee (the merge-base): make all and gofmt/goimports are clean,
golangci-lint --new-from-rev=c2725ee4de reports 0 issues, and go test ./core/
./core/state/... ./core/vm/... ./eth/tracers/... ./internal/ethapi/... passes.

Closes XinFinOrg#2766
Ref: ethereum#35219
…ss the fork

The event-level test only drove a self-destruct to the executing account, so
nothing pinned that a distinct beneficiary keeps the SubBalance/AddBalance pair
on both sides of the EIP-8246 fork: the interpreter gate has to drop only the
self-beneficiary part, and dropping the transfer pair for a foreign beneficiary
would move no funds at all.

The helper now reports the beneficiary balance as well, and four table rows
cover an existing and a new contract on each side of the fork. The extra
decrease of pre-amsterdam/new-contract-to-other is the 0 -> 0 destruct decrease
hookedStateDB.SelfDestruct6780 raises after the instruction already moved the
balance to the beneficiary, the same quirk the self-beneficiary rows pin.
@gzliudan
gzliudan force-pushed the eip-8246-selfdestruct-burn branch from d0d19de to fe4cf58 Compare October 27, 2026 08:24
@gzliudan
gzliudan requested a balanced review from Copilot October 27, 2026 08:50

Copilot AI 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.

🟡 Changes recommended

The prestate diff tracer still omits accounts whose balances survive SELFDESTRUCT under Amsterdam.

1 open issue
What changed in this PR

Implements Amsterdam’s EIP-8246 SELFDESTRUCT burn removal in the VM and state layer, complementing the previously merged supply-tracer gate.

Changes:

  • Preserves destructed accounts’ remaining balances while clearing nonce, code, and storage.
  • Propagates preservation flags through state copies and reverts, and suppresses removed-burn hooks.
  • Adds state, VM, and block-processing tests for fork behavior, late funds, persistence, and CREATE2 reuse.
File Description
core/​vm/​interface.go Adds the balance-preservation parameter to the state interface.
core/​vm/​instructions.go Gates SELFDESTRUCT balance handling on Amsterdam.
core/​vm/​eip8246_test.go Tests balance events across fork and beneficiary cases.
core/​state/​statedb.go Preserves balance-only accounts during finalisation.
core/​state/​statedb_hooked.go Suppresses balance-decrease and burn hooks for preserved balances.
core/​state/​statedb_eip8246_test.go Tests persistence, reverts, late funds, and state bookkeeping.
core/​state/​state_object.go Tracks and copies the preservation flag.
core/​state/​journal.go Restores preservation flags during journal reverts.
core/​eip8246_test.go Tests block-level behavior and CREATE2 recreation.

Review effort: Balanced

Comment thread core/state/statedb.go
@gzliudan
gzliudan merged commit eae4c6a into XinFinOrg:dev-upgrade Oct 27, 2026
16 checks passed
@gzliudan
gzliudan deleted the eip-8246-selfdestruct-burn branch October 27, 2026 09:15
gzliudan added a commit to gzliudan/XDPoSChain that referenced this pull request Oct 27, 2026
…eum#33645

EIP-7708 emits a system log for every value transfer from Amsterdam onwards:
the transaction value, the value moved by a CALL or CREATE, and the value
moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the
ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount),
so indexers can consume it like a token transfer.

The gate sits where the fork rules are available: core.Transfer takes the
vm's params.Rules and emits after the balance move, while opSelfdestruct6780
emits when the beneficiary differs. Zero-value transfers and transfers to self
emit nothing.

No burn log is emitted, which is an ordering constraint rather than a
statement about this tree: EIP-8246 (ethereum#35219) removes the SELFDESTRUCT burn and
destroys balances in two corners, both unlogged by this commit:

  - a contract created in the same transaction that self-destructs to itself
    has its balance destroyed by SelfDestruct6780 and Finalise;
  - a contract created in the same transaction that self-destructs and then
    receives funds has those funds destroyed at Finalise.

Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the
window is dormant; this prerequisite is why the change must merge after XinFinOrg#2834,
or in the same batch.

The log constructor takes *big.Int rather than upstream's *uint256.Int because
this tree's StateDB moves balances as big.Int.

Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing
(three transfer logs interleaved with the contract's events), the zero-value
and pre-Amsterdam cases, a transfer to self, a reverted call frame, and the
upstream boundary matrix: DELEGATECALL (no log), a CREATE2 endowment, a failed
CREATE rolling its log back, precompile and system-address recipients, the
priority-fee exclusion, and a direct core.Transfer fork-gate case pinning the
32-byte amount. core/state additionally pins that a log is reverted with its
snapshot.

Ref: ethereum#33645
Ref: ethereum#35617
Closes XinFinOrg#2767
gzliudan added a commit to gzliudan/XDPoSChain that referenced this pull request Oct 27, 2026
…eum#33645

EIP-7708 emits a system log for every value transfer from Amsterdam onwards:
the transaction value, the value moved by a CALL or CREATE, and the value
moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the
ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount),
so indexers can consume it like a token transfer.

The gate sits where the fork rules are available: core.Transfer takes the
vm's params.Rules and emits after the balance move, while opSelfdestruct6780
emits when the beneficiary differs. Zero-value transfers and transfers to self
emit nothing.

No burn log is emitted, which is an ordering constraint rather than a
statement about this tree: EIP-8246 (ethereum#35219) removes the SELFDESTRUCT burn and
destroys balances in two corners, both unlogged by this commit:

  - a contract created in the same transaction that self-destructs to itself
    has its balance destroyed by SelfDestruct6780 and Finalise;
  - a contract created in the same transaction that self-destructs and then
    receives funds has those funds destroyed at Finalise.

Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the
window is dormant; this prerequisite is why the change must merge after XinFinOrg#2834,
or in the same batch.

The log constructor takes *big.Int rather than upstream's *uint256.Int because
this tree's StateDB moves balances as big.Int.

Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing
(three transfer logs interleaved with the contract's events), the zero-value
and pre-Amsterdam cases, a transfer to self, a reverted call frame, and the
upstream boundary matrix: DELEGATECALL (no log), a CREATE2 endowment, a failed
CREATE rolling its log back, precompile and system-address recipients, the
priority-fee exclusion, and a direct core.Transfer fork-gate case pinning the
32-byte amount. core/state additionally pins that a log is reverted with its
snapshot.

Ref: ethereum#33645
Ref: ethereum#35617
Closes XinFinOrg#2767
gzliudan added a commit to gzliudan/XDPoSChain that referenced this pull request Oct 27, 2026
… logs ethereum#33645

EIP-7708 emits a system log for every value transfer from Amsterdam onwards:
the transaction value, the value moved by a CALL or CREATE, and the value
moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the
ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount),
so indexers can consume it like a token transfer.

The gate sits where the fork rules are available: core.Transfer takes the
vm's params.Rules and emits after the balance move, while opSelfdestruct6780
emits when the beneficiary differs. Zero-value transfers and transfers to self
emit nothing.

No burn log is emitted: EIP-8246 (go-ethereum ethereum#35219) removes the SELFDESTRUCT
burn entirely and landed before this change as XinFinOrg#2834, so a self-destruct that
names a different beneficiary is the only value move the instruction makes. A
self-destruct to self moves nothing and therefore logs nothing, in every fork.

Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the
window stays dormant until it is.

The log constructor takes *big.Int rather than upstream's *uint256.Int because
this tree's StateDB moves balances as big.Int.

Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing
(three transfer logs interleaved with the contract's events), the zero-value
and pre-Amsterdam cases, a transfer to self, an inner revert that keeps the
caller's own log, an outer revert that discards every log, the delegated-call
attribution, and the upstream boundary matrix: DELEGATECALL and CALLCODE (no
log), a CREATE2 endowment, a failed CREATE rolling its log back, precompile and
system-address recipients, the priority-fee exclusion, and a direct
core.Transfer fork-gate case pinning the 32-byte amount. core/state
additionally pins that a log is reverted with its snapshot.

Ref: ethereum#33645
Ref: ethereum#35617
Closes XinFinOrg#2767
gzliudan added a commit to gzliudan/XDPoSChain that referenced this pull request Oct 27, 2026
… logs ethereum#33645

EIP-7708 emits a system log for every value transfer from Amsterdam onwards:
the transaction value, the value moved by a CALL or CREATE, and the value
moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the
ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount),
so indexers can consume it like a token transfer.

The gate sits where the fork rules are available: core.Transfer takes the
vm's params.Rules and emits after the balance move, while opSelfdestruct6780
emits when the beneficiary differs. Zero-value transfers and transfers to self
emit nothing.

No burn log is emitted: EIP-8246 (go-ethereum ethereum#35219) removes the SELFDESTRUCT
burn entirely and landed before this change as XinFinOrg#2834, so a self-destruct that
names a different beneficiary is the only value move the instruction makes. A
self-destruct to self moves nothing and therefore logs nothing, in every fork.

Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the
window stays dormant until it is.

The log constructor takes *big.Int rather than upstream's *uint256.Int because
this tree's StateDB moves balances as big.Int.

Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing
(three transfer logs interleaved with the contract's events), the zero-value
and pre-Amsterdam cases, a transfer to self, an inner revert that keeps the
caller's own log, an outer revert that discards every log, the delegated-call
attribution, and the upstream boundary matrix: DELEGATECALL and CALLCODE (no
log), a CREATE2 endowment, a failed CREATE rolling its log back, precompile and
system-address recipients, the priority-fee exclusion, and a direct
core.Transfer fork-gate case pinning the 32-byte amount. core/state
additionally pins that a log is reverted with its snapshot.

Ref: ethereum#33645
Ref: ethereum#35617
Closes XinFinOrg#2767
gzliudan added a commit that referenced this pull request Oct 27, 2026
…708 ETH transfer logs, close #2767 (#2842)

* feat(core,core/vm,core/types,params): implement EIP-7708 ETH transfer logs ethereum#33645

EIP-7708 emits a system log for every value transfer from Amsterdam onwards:
the transaction value, the value moved by a CALL or CREATE, and the value
moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the
ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount),
so indexers can consume it like a token transfer.

The gate sits where the fork rules are available: core.Transfer takes the
vm's params.Rules and emits after the balance move, while opSelfdestruct6780
emits when the beneficiary differs. Zero-value transfers and transfers to self
emit nothing.

No burn log is emitted: EIP-8246 (go-ethereum ethereum#35219) removes the SELFDESTRUCT
burn entirely and landed before this change as #2834, so a self-destruct that
names a different beneficiary is the only value move the instruction makes. A
self-destruct to self moves nothing and therefore logs nothing, in every fork.

Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the
window stays dormant until it is.

The log constructor takes *big.Int rather than upstream's *uint256.Int because
this tree's StateDB moves balances as big.Int.

Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing
(three transfer logs interleaved with the contract's events), the zero-value
and pre-Amsterdam cases, a transfer to self, an inner revert that keeps the
caller's own log, an outer revert that discards every log, the delegated-call
attribution, and the upstream boundary matrix: DELEGATECALL and CALLCODE (no
log), a CREATE2 endowment, a failed CREATE rolling its log back, precompile and
system-address recipients, the priority-fee exclusion, and a direct
core.Transfer fork-gate case pinning the 32-byte amount. core/state
additionally pins that a log is reverted with its snapshot.

Ref: ethereum#33645
Ref: ethereum#35617
Closes #2767

* fix(internal/ethapi): suppress synthetic transfer logs after Amsterdam ethereum#35617

eth_simulateV1 synthesizes an ERC-7528 transfer log for every value move when
traceTransfers is set. From Amsterdam the protocol emits the EIP-7708 log for
the same transfer, so every transfer was reported twice.

Disable the synthetic log for Amsterdam blocks, mirroring upstream ethereum#35617. The
tracer still captures the protocol log through the OnLog hook, so traceTransfers
keeps reporting exactly one log per transfer.

The new test pins both sides of the fork: the synthetic ERC-7528 log before
Amsterdam and the single protocol log at params.SystemAddress after it.

The tracer comment now says that from Amsterdam onwards the protocol emits
these logs itself (EIP-7708) and the synthesis is off, so the documented format
only describes the synthesized pre-fork log while protocol logs carry
params.SystemAddress as their emitter. Upstream still carries the older comment
(zero address, no EIP-7708 note), so this is a local correction with no geth PR
to mirror.

Ref: ethereum#35617
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants