Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
79 changes: 53 additions & 26 deletions lib/eevm/opcodes/system/creation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -131,36 +131,54 @@ defmodule EEVM.Opcodes.System.Creation do
stack
)
else
deposit_cost = Dynamic.code_deposit_cost(byte_size(runtime_code))

if child_result.gas >= deposit_cost do
db_after_deploy =
child_result.db
|> Database.put_code(new_address, runtime_code)
|> Database.set_nonce(new_address, 1)

{:ok, stack_after_create} = Stack.push(stack, new_address)

created_addresses_after =
MapSet.put(child_result.created_addresses, new_address)

{:ok,
state_after_initcode_cost
|> Map.put(:stack, stack_after_create)
|> Map.put(:memory, memory_after_read)
|> Map.put(:db, db_after_deploy)
|> Map.put(:logs, state_after_initcode_cost.logs ++ child_result.logs)
|> Map.put(:accessed_addresses, child_result.accessed_addresses)
|> Map.put(:accessed_storage_keys, child_result.accessed_storage_keys)
|> Map.put(:created_addresses, created_addresses_after)
|> Map.put(:gas, child_result.gas - deposit_cost)
|> Map.put(:return_data, child_result.return_data)
|> MachineState.advance_pc()}
else
# EIP-3541 (London): reject runtime code whose first byte is 0xEF.
# The 0xEF prefix is reserved for the EVM Object Format (EOF).
# This check runs after EIP-170 size validation and after init code
# execution — so init code gas is already consumed but no code is
# deposited. Empty runtime code is explicitly allowed.
if reject_eip_3541_runtime_code?(runtime_code) do
create_failed(
%{state_after_initcode_cost | db: db_after_nonce},
stack
)
else
deposit_cost = Dynamic.code_deposit_cost(byte_size(runtime_code))

if child_result.gas >= deposit_cost do
db_after_deploy =
child_result.db
|> Database.put_code(new_address, runtime_code)
|> Database.set_nonce(new_address, 1)

{:ok, stack_after_create} = Stack.push(stack, new_address)

created_addresses_after =
MapSet.put(child_result.created_addresses, new_address)

{:ok,
state_after_initcode_cost
|> Map.put(:stack, stack_after_create)
|> Map.put(:memory, memory_after_read)
|> Map.put(:db, db_after_deploy)
|> Map.put(
:logs,
state_after_initcode_cost.logs ++ child_result.logs
)
|> Map.put(:accessed_addresses, child_result.accessed_addresses)
|> Map.put(
:accessed_storage_keys,
child_result.accessed_storage_keys
)
|> Map.put(:created_addresses, created_addresses_after)
|> Map.put(:gas, child_result.gas - deposit_cost)
|> Map.put(:return_data, child_result.return_data)
|> MachineState.advance_pc()}
else
create_failed(
%{state_after_initcode_cost | db: db_after_nonce},
stack
)
end
end
end
else
Expand Down Expand Up @@ -257,6 +275,15 @@ defmodule EEVM.Opcodes.System.Creation do
end
end

# EIP-3541 (London): returns true when runtime code must be rejected.
# CREATE/CREATE2 cannot deploy bytecode whose first byte is 0xEF — that
# prefix is reserved for the EVM Object Format (EOF). Empty runtime code
# is explicitly allowed; only a non-empty 0xEF-prefixed result is rejected.
@spec reject_eip_3541_runtime_code?(binary()) :: boolean()
defp reject_eip_3541_runtime_code?(runtime_code) do
byte_size(runtime_code) > 0 and :binary.first(runtime_code) == 0xEF
end

defp create_failed(state, stack) do
{:ok, stack_after_create} = Stack.push(stack, 0)
{:ok, %{state | stack: stack_after_create} |> MachineState.advance_pc()}
Expand Down
116 changes: 116 additions & 0 deletions test/opcodes/system/eip_3541_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
defmodule EEVM.Opcodes.System.EIP3541Test do
use ExUnit.Case, async: true

import EEVM.TestSupport.BytecodeHelpers

alias EEVM.{Database, WorldState}

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

# Builds init code (bytecode) that, when executed, stores `runtime_code` in
# memory byte-by-byte and then RETURNs it as the deployed runtime code.
defp init_code_returning(runtime_code) do
stores =
runtime_code
|> :binary.bin_to_list()
|> Enum.with_index()
|> Enum.map(fn {byte, offset} ->
IO.iodata_to_binary([push(byte), push(offset), <<0x53>>])
end)

IO.iodata_to_binary([
stores,
push(byte_size(runtime_code)),
push(0),
<<0xF3>>
])
end

defp push(0), do: <<0x60, 0x00>>
defp push(n) when n <= 0xFF, do: <<0x60, n>>
defp push(n) when n <= 0xFFFF, do: <<0x61, n::unsigned-big-16>>

# ---------------------------------------------------------------------------
# EIP-3541 rejection tests
# ---------------------------------------------------------------------------

describe "EIP-3541 runtime prefix rejection" do
test "CREATE rejects runtime code starting with 0xEF — returns 0, no code stored" do
init_code = init_code_returning(<<0xEF, 0xAA>>)
code = build_create_program(init_code, :create, 0)
initial_gas = 300_000

result =
EEVM.execute(code,
gas: initial_gas,
world_state: WorldState.new(%{0 => %{balance: 10}})
)

# CREATE pushes 0 to signal failed deployment
assert result.status == :stopped
assert EEVM.stack_values(result) == [0]

# Gas was consumed by init code execution (rejection does not refund it)
assert result.gas < initial_gas

# Nonce was incremented (sender nonce advances before deployment attempt)
assert Database.get_nonce(result.db, 0) == 1

# The sender's own code slot is empty — nothing was deployed
assert Database.get_code(result.db, 0) == <<>>
end

test "CREATE2 rejects runtime code starting with 0xEF — returns 0" do
init_code = init_code_returning(<<0xEF, 0xBB>>)
code = build_create_program(init_code, :create2, 0, 1)

result = EEVM.execute(code, world_state: WorldState.new(%{0 => %{balance: 10}}))

assert result.status == :stopped
assert EEVM.stack_values(result) == [0]
end

test "CREATE allows empty runtime code — deployment succeeds" do
# RETURN(0, 0) — returns zero bytes, i.e. empty runtime code
init_code = <<0x60, 0x00, 0x60, 0x00, 0xF3>>
code = build_create_program(init_code, :create, 0)

result = EEVM.execute(code, world_state: WorldState.new(%{0 => %{balance: 10}}))
[created_address] = EEVM.stack_values(result)

assert result.status == :stopped
assert created_address != 0
assert Database.get_code(result.db, created_address) == <<>>
end

test "CREATE allows runtime code that does not start with 0xEF" do
runtime_code = <<0x60, 0x00>>
init_code = init_code_returning(runtime_code)
code = build_create_program(init_code, :create, 0)

result = EEVM.execute(code, world_state: WorldState.new(%{0 => %{balance: 10}}))
[created_address] = EEVM.stack_values(result)

assert result.status == :stopped
assert created_address != 0
assert Database.get_code(result.db, created_address) == runtime_code
end

test "0xEF bytes inside init code do not trigger rejection — only runtime prefix matters" do
# Init code: PUSH1 0xEF, POP (uses 0xEF as data but returns non-0xEF runtime)
ef_using_prefix = <<0x60, 0xEF, 0x50>>
safe_runtime = <<0x60, 0x00>>
init_code = ef_using_prefix <> init_code_returning(safe_runtime)
code = build_create_program(init_code, :create, 0)

result = EEVM.execute(code, world_state: WorldState.new(%{0 => %{balance: 10}}))
[created_address] = EEVM.stack_values(result)

assert result.status == :stopped
assert created_address != 0
assert Database.get_code(result.db, created_address) == safe_runtime
end
end
end
Loading