Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e4ee738
feat(spec-specs): Add transfer log for all `CALL*` and `SELFDESTRUCT`
Carsons-Eels Jan 13, 2026
580f790
fix(spec-specs): emit account closure logs in lexicographical order
Carsons-Eels Jan 22, 2026
a08c64b
feat(test-tests): add eip-7708 eth transfer log tests
spencer-tb Jan 16, 2026
4ccec9a
fix(spec-specs): Refactor topic strings to match EIP
Carsons-Eels Jan 22, 2026
d5f902c
fix(spec-tests): formatting fixes so static checks pass
Carsons-Eels Jan 22, 2026
cb61106
fix(spec-specs): Move account closure log emission before priority fe…
Carsons-Eels Jan 23, 2026
c962c0e
feat(test-specs): add extra eip-7708 test coverage (#2062)
spencer-tb Jan 23, 2026
2e94c41
feat(spec,test): EIP-7708 spec updates for self as target (#2086)
fselmo Jan 29, 2026
fb5df33
feat(test): extend EIP-7708 tests from cases in tracker (#1875) (#2106)
fselmo Jan 30, 2026
513aaa7
fix(test): Use new ``pre_alloc_mutable`` marker for eip7708 test (#2199)
fselmo Feb 12, 2026
7ae0d53
fix(test): Update test to account for recent Initcode updates
fselmo Feb 13, 2026
1f1d757
🧹 chore: Remove duplicate test (#2210)
fselmo Feb 13, 2026
34f9953
♻️ refactor(tests): Rename EIP 7708 selfdestruct log to burn (#2211)
raxhvl Feb 25, 2026
5eaa1ee
fix(specs): Merge issues
marioevz Mar 24, 2026
0fbab5b
fix(tests): Merge issues
marioevz Mar 24, 2026
4c40535
refactor(test-forks): Add EIP-7708
marioevz Apr 8, 2026
a1861be
refactor(tests): Condition EIP-7708 tests to EIP inclusion
marioevz Apr 8, 2026
782b78d
feat(tests): EIP-7708 - finalization burn log ordering + coinbase fee…
spencer-tb Apr 20, 2026
ee16914
fix(tests): rename GAS_CODE_DEPOSIT_PER_BYTE to CODE_DEPOSIT_PER_BYTE
spencer-tb Apr 20, 2026
70cf0a3
fix(tests): EIP-7708 + 8037 cross-EIP fixes
marioevz Apr 21, 2026
155c3e1
feat(tests): Add EIP-7708 checks to 6780 tests (#2743)
marioevz Apr 27, 2026
7c84033
feat(tests): cover EIP-7708 CREATE log rollback on outer revert (#2785)
danceratopz May 11, 2026
8e7231c
fix(specs-spec, tests): CR comment fixes
Carsons-Eels May 13, 2026
d5857ef
Apply suggestion from @marioevz
marioevz May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
EIP-7708: ETH transfers and burns emit a log.

All ETH transfers and burns emit a log.

https://eips.ethereum.org/EIPS/eip-7708
"""

from ....base_fork import BaseFork


class EIP7708(BaseFork):
"""EIP-7708 class."""

pass
26 changes: 24 additions & 2 deletions src/ethereum/forks/amsterdam/fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
State,
apply_changes_to_state,
)
from ethereum.utils.byte import left_pad_zero_bytes

from . import vm
from .block_access_lists import (
Expand Down Expand Up @@ -1054,6 +1055,7 @@ def process_transaction(
)
set_account_balance(tx_state, sender, sender_balance_after_refund)

# transfer miner fees
coinbase_balance_after_mining_fee = get_account(
tx_state, block_env.coinbase
).balance + U256(transaction_fee)
Expand All @@ -1062,6 +1064,26 @@ def process_transaction(
tx_state, block_env.coinbase, coinbase_balance_after_mining_fee
)

# EIP-7708: Emit burn logs for balances held by accounts marked for
# deletion AFTER miner fee transfer.
finalization_logs: List[Log] = []
for address in sorted(tx_output.accounts_to_delete):
balance = get_account(tx_state, address).balance
if balance > U256(0):
padded_address = left_pad_zero_bytes(address, 32)
finalization_logs.append(
Log(
address=vm.SYSTEM_ADDRESS,
topics=(
vm.BURN_TOPIC,
Hash32(padded_address),
),
data=balance.to_be_bytes32(),
)
)

all_logs = tx_output.logs + tuple(finalization_logs)

if coinbase_balance_after_mining_fee == 0 and account_exists_and_is_empty(
tx_state, block_env.coinbase
):
Expand All @@ -1071,7 +1093,7 @@ def process_transaction(
block_output.blob_gas_used += tx_blob_gas_used

receipt = make_receipt(
tx, tx_output.error, block_output.block_gas_used, tx_output.logs
tx, tx_output.error, block_output.block_gas_used, all_logs
)

receipt_key = rlp.encode(Uint(index))
Expand All @@ -1083,7 +1105,7 @@ def process_transaction(
receipt,
)

block_output.block_logs += tx_output.logs
block_output.block_logs += all_logs

for address in tx_output.accounts_to_delete:
destroy_account(tx_state, address)
Expand Down
82 changes: 81 additions & 1 deletion src/ethereum/forks/amsterdam/vm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@
from ethereum_types.bytes import Bytes, Bytes0, Bytes32
from ethereum_types.numeric import U64, U256, Uint

from ethereum.crypto.hash import Hash32
from ethereum.crypto.hash import Hash32, keccak256
from ethereum.exceptions import EthereumException
from ethereum.merkle_patricia_trie import Trie
from ethereum.state import Address
from ethereum.utils.byte import left_pad_zero_bytes

from ..block_access_lists import BlockAccessList, BlockAccessListBuilder
from ..blocks import Log, Receipt, Withdrawal
Expand All @@ -30,6 +31,12 @@
from ..transactions import LegacyTransaction

__all__ = ("Environment", "Evm", "Message")
TRANSFER_TOPIC = keccak256(b"Transfer(address,address,uint256)")
BURN_TOPIC = keccak256(b"Burn(address,uint256)")
SYSTEM_ADDRESS = Address(
bytes.fromhex("fffffffffffffffffffffffffffffffffffffffe")
)
CALL_SUCCESS = U256(1)


@dataclass
Expand Down Expand Up @@ -195,3 +202,76 @@ def incorporate_child_on_error(evm: Evm, child_evm: Evm) -> None:

"""
evm.gas_left += child_evm.gas_left


def emit_transfer_log(
evm: Evm,
sender: Address,
recipient: Address,
transfer_amount: U256,
) -> None:
"""
Emit a LOG3 for all ETH transfers satisfying EIP-7708.

Parameters
----------
evm :
The state of the ethereum virtual machine
sender :
The account address sending the transfer
recipient :
The account address receiving the transfer
transfer_amount :
The amount of ETH transacted

"""
if transfer_amount == 0:
return

padded_sender = left_pad_zero_bytes(sender, 32)
padded_recipient = left_pad_zero_bytes(recipient, 32)
log_entry = Log(
address=SYSTEM_ADDRESS,
topics=(
TRANSFER_TOPIC,
Hash32(padded_sender),
Hash32(padded_recipient),
),
data=transfer_amount.to_be_bytes32(),
)

evm.logs = evm.logs + (log_entry,)


def emit_burn_log(
evm: Evm,
account: Address,
amount: U256,
) -> None:
"""
Emit a LOG2 for ETH burn per EIP-7708.

Parameters
----------
evm :
The state of the ethereum virtual machine
account :
The account address whose ETH is being burned
amount :
The amount of ETH being burned

"""
if amount == 0:
return

padded_account = left_pad_zero_bytes(account, 32)
log_entry = Log(
address=SYSTEM_ADDRESS,
topics=(
BURN_TOPIC,
Hash32(padded_account),
),
data=amount.to_be_bytes32(),
)

evm.logs = evm.logs + (log_entry,)
18 changes: 13 additions & 5 deletions src/ethereum/forks/amsterdam/vm/instructions/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@
calculate_delegation_cost,
)
from .. import (
CALL_SUCCESS,
Evm,
Message,
emit_burn_log,
emit_transfer_log,
incorporate_child_on_error,
incorporate_child_on_success,
)
Expand Down Expand Up @@ -338,7 +341,7 @@ def generic_call(
else:
incorporate_child_on_success(evm, child_evm)
evm.return_data = child_evm.output
push(evm.stack, U256(1))
push(evm.stack, CALL_SUCCESS)

actual_output_size = min(memory_output_size, U256(len(child_evm.output)))
memory_write(
Expand Down Expand Up @@ -428,6 +431,7 @@ def call(evm: Evm) -> None:
)
charge_gas(evm, message_call_gas.cost + extend_memory.cost)

# OPERATION
evm.memory += b"\x00" * extend_memory.expand_by
sender_balance = get_account(tx_state, evm.message.current_target).balance
if sender_balance < value:
Expand Down Expand Up @@ -606,11 +610,15 @@ def selfdestruct(evm: Evm) -> None:
# Transfer balance
move_ether(tx_state, originator, beneficiary, originator_balance)

# register account for deletion only if it was created
# in the same transaction
# Emit transfer or burn log
if originator in tx_state.created_accounts and beneficiary == originator:
emit_burn_log(evm, originator, originator_balance)
elif beneficiary != originator:
emit_transfer_log(evm, originator, beneficiary, originator_balance)

# Register account for deletion iff created in same transaction
if originator in tx_state.created_accounts:
# If beneficiary is the same as originator, then
# the ether is burnt.
# If beneficiary and originator are the same then the ether is burnt.
set_account_balance(tx_state, originator, U256(0))
evm.accounts_to_delete.add(originator)

Expand Down
8 changes: 6 additions & 2 deletions src/ethereum/forks/amsterdam/vm/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
from ..vm.eoa_delegation import get_delegated_code_address, set_delegation
from ..vm.gas import GasCosts, charge_gas
from ..vm.precompiled_contracts.mapping import PRE_COMPILED_CONTRACTS
from . import Evm
from . import Evm, emit_transfer_log
from .exceptions import (
AddressCollision,
ExceptionalHalt,
Expand Down Expand Up @@ -262,7 +262,6 @@ def process_message(message: Message) -> Evm:
accessed_storage_keys=message.accessed_storage_keys,
)

# take snapshot of state before processing the message
snapshot = copy_tx_state(tx_state)

if message.should_transfer_value and message.value != 0:
Expand All @@ -272,7 +271,12 @@ def process_message(message: Message) -> Evm:
message.current_target,
message.value,
)
if message.caller != message.current_target:
emit_transfer_log(
evm, message.caller, message.current_target, message.value
)

# Execute message code and handle errors
try:
if evm.message.code_address in PRE_COMPILED_CONTRACTS:
if not message.disable_precompiles:
Expand Down
1 change: 1 addition & 0 deletions tests/amsterdam/eip7708_eth_transfer_logs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Cross-client EIP-7708 Tests."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
general/code_coverage/eels = EIP-7708 specific code (emit_transfer_log in vm/__init__.py) has full coverage; check codecov for src/ethereum/forks/amsterdam/vm/__init__.py and src/ethereum/forks/amsterdam/vm/interpreter.py
general/code_coverage/test_coverage = Run with `--cov` flag; 99% coverage on vm/__init__.py, 92% on interpreter.py
general/code_coverage/missed_lines = Missed lines are general VM infrastructure (static context errors, gas accounting, error handling) not related to EIP-7708
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
opcode = EIP does not introduce a new opcode
precompile = EIP does not introduce a new precompile
removed_precompile = EIP does not remove a precompile
system_contract = EIP does not introduce a new system contract
transaction_type = EIP does not introduce a new transaction type
block_header_field = EIP does not add any new block header fields
block_body_field = EIP does not add any new block body fields
gas_cost_changes = EIP does not introduce any gas cost changes
gas_refunds_changes = EIP does not introduce any gas refund changes
blob_count_changes = EIP does not introduce any blob count changes
execution_layer_request = EIP does not introduce an execution layer request
new_transaction_validity_constraint = EIP does not introduce a new transaction validity constraint
modified_transaction_validity_constraint = EIP does not introduce a modified transaction validity constraint
block_level_constraint = EIP does not introduce a block-level validation constraint
61 changes: 61 additions & 0 deletions tests/amsterdam/eip7708_eth_transfer_logs/spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Defines EIP-7708 specification constants and functions."""

from dataclasses import dataclass

from execution_testing import Address, Bytes, Hash, TransactionLog, keccak256


@dataclass(frozen=True)
class ReferenceSpec:
"""Defines the reference spec version and git path."""

git_path: str
version: str


ref_spec_7708 = ReferenceSpec(
"EIPS/eip-7708.md", "43a7f15cd1105f308086bed6a61e3155039271fc"
)


@dataclass(frozen=True)
class Spec:
"""
Parameters from the EIP-7708 specifications as defined at
https://eips.ethereum.org/EIPS/eip-7708.
"""

SYSTEM_ADDRESS: Address = Address(
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE
)
TRANSFER_TOPIC: Hash = Hash(
keccak256(b"Transfer(address,address,uint256)")
)
BURN_TOPIC: Hash = Hash(keccak256(b"Burn(address,uint256)"))


def transfer_log(
sender: Address, recipient: Address, amount: int
) -> TransactionLog:
"""Create an expected Transfer log for EIP-7708."""
return TransactionLog(
address=Spec.SYSTEM_ADDRESS,
topics=[
Spec.TRANSFER_TOPIC,
Hash(bytes(sender).rjust(32, b"\x00")),
Hash(bytes(recipient).rjust(32, b"\x00")),
],
data=Bytes(amount.to_bytes(32, "big")),
)


def burn_log(contract_address: Address, amount: int | None) -> TransactionLog:
"""Create an expected Burn log for EIP-7708."""
return TransactionLog(
address=Spec.SYSTEM_ADDRESS,
topics=[
Spec.BURN_TOPIC,
Hash(bytes(contract_address).rjust(32, b"\x00")),
],
data=Bytes(amount.to_bytes(32, "big")) if amount is not None else None,
)
Loading
Loading