Skip to content

EVMAssembly import silently ignores evmVersion (emits PUSH0 on Paris) #16759

Description

@msooseth

TL;DR

solc --standard-json with { "language": "EVMAssembly", "settings": { "evmVersion": "paris" } } produces bytecode for Osaka, not Paris. A { "name": "PUSH", "value": "0" } item is emitted as 0x5f (PUSH0) — an opcode that does not exist on Paris. Contracts deployed this way revert with INVALID OPCODE on a Paris-era EVM.

Audited commit: 8471cf2ff005320b69535ee923e75edb569927d4 (develop, 2026-05-21). Reproducible.

Where the bug lives

Two co-operating omissions:

  1. libevmasm/EVMAssemblyStack.cpp:48analyze() calls Assembly::fromJSON(_assemblyJson, {}, 0, m_eofVersion) but does not pass m_evmVersion. fromJSON has no parameter for the EVM version at all.
  2. libevmasm/Assembly.cpp:623fromJSON constructs:
    auto result = std::make_shared<Assembly>(
        EVMVersion{}, _level == 0 /*_creation*/, _eofVersion, "" /*_name*/);
    EVMVersion{} is the default constructor, which resolves to Version::Osaka (liblangutil/EVMVersion.h:180-184).

EVMAssemblyStack::m_evmVersion is dead state — set in the constructor (EVMAssemblyStack.h:42) and never read. The wrong default propagates into every downstream consumer of the imported Assembly: assemblePush (Assembly.cpp:1202), codeSize (Assembly.cpp:130), Inliner (Assembly.cpp:881), PeepholeOptimiser (Assembly.cpp:899), CommonSubexpressionEliminator (Assembly.cpp:954), ConstantOptimisationMethod::optimiseConstants (Assembly.cpp:996).

Reachable entry points

Entry point Affected? Why
solc --standard-json with language: "EVMAssembly" Yes StandardCompiler.cpp:902-920 parses settings.evmVersion; :1352-1358 forwards it to EVMAssemblyStack; the stack swallows it.
Programmatic users of evmasm::EVMAssemblyStack Yes Same analyze().
solc --import-asm-json CLI No CommandLineParser.cpp:1123-1151 rejects --evm-version for this mode, forcing the default.
Normal Solidity / Yul compilation No Those paths construct Assembly directly with the correct EVM version.

Standalone reproducer

standard-json-paris.json:

{
  "language": "EVMAssembly",
  "settings": {
    "evmVersion": "paris",
    "experimental": true,
    "outputSelection": {"input.json": {"": ["evm.bytecode.object", "evm.bytecode.opcodes"]}}
  },
  "sources": {
    "input.json": {
      "assemblyJson": {
        ".code": [
          { "name": "PUSH", "value": "0" },
          { "name": "PUSH", "value": "0" },
          { "name": "RETURN" }
        ]
      }
    }
  }
}

Run:

$ solc --standard-json standard-json-paris.json

Actual output on develop:

{"contracts":{"input.json":{"":{
  "evm":{
    "bytecode":{
      "object":"5f5ff3",
      "opcodes":"PUSH0 PUSH0 RETURN "
    }
  }
}}}}

Expected for a Paris target: 60006000f3 (PUSH1 0x00 PUSH1 0x00 RETURN). The observed 5f is invalid on Paris.

The same result occurs with "homestead", "byzantium", "london" — every pre-Shanghai target produces the same buggy 5f5ff3, because the evmVersion setting never reaches Assembly.

Regression tests

Two test files exercise the bug under the existing EVMAssemblyTest fixture (test/libevmasm/EVMAssemblyTest.cpp), placed under a new test/libevmasm/evmAssemblyTests/audit/ directory (isoltest discovers it via the recursive scan in boostTest.cpp).

evm_version_push0_paris.asmjson (fails on develop):

{
    ".code": [
        {"name": "PUSH", "value": "0"}
    ]
}
// ====
// EVMVersion: =paris
// outputs: Bytecode,Opcodes
// ----
// Bytecode: 6000
// Opcodes: PUSH1 0x0

evm_version_push0_shanghai.asmjson (sanity check, passes):

{
    ".code": [
        {"name": "PUSH", "value": "0"}
    ]
}
// ====
// EVMVersion: =shanghai
// outputs: Bytecode,Opcodes
// ----
// Bytecode: 5f
// Opcodes: PUSH0

Run with --evm-version=paris on develop:

error: in "evmAssemblyTests/audit/evm_version_push0_paris": Test expectation mismatch.
Expected: Bytecode: 6000 / Opcodes: PUSH1 0x0
Obtained: Bytecode: 5f   / Opcodes: PUSH0

Why the existing tests did not catch this

test/libevmasm/evmAssemblyTests/isoltestTesting/push.asm covers PUSH 0 but hard-codes 5f6001… and is not version-restricted. Running it under --evm-version=paris passes because the buggy code emits 5f, matching the expectation. The current test codifies the bug as expected behaviour for every EVM version simultaneously. After a fix, this expectation needs to be split per EVM version (e.g. EVMVersion: >=shanghai).

Suggested fix

Plumb _evmVersion through Assembly::fromJSON:

// libevmasm/Assembly.h
static std::pair<std::shared_ptr<Assembly>, std::vector<std::string>> fromJSON(
    Json const& _json,
    std::vector<std::string> const& _sourceList = {},
    size_t _level = 0,
    std::optional<uint8_t> _eofVersion = std::nullopt,
    langutil::EVMVersion _evmVersion = {}    // NEW
);

// libevmasm/Assembly.cpp:623
auto result = std::make_shared<Assembly>(
    _evmVersion, _level == 0 /*_creation*/, _eofVersion, "");

// recursive call at Assembly.cpp:690
auto [subAssembly, emptySourceList] = Assembly::fromJSON(
    value,
    _level == 0 ? parsedSourceList : _sourceList,
    _level + 1,
    _eofVersion,
    _evmVersion);   // NEW

// libevmasm/EVMAssemblyStack.cpp:48
std::tie(m_evmAssembly, m_sourceList) =
    evmasm::Assembly::fromJSON(_assemblyJson, {}, 0, m_eofVersion, m_evmVersion);

With this change applied, the Paris regression test passes; the isoltestTesting/push.asm expectation needs to be updated as described above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug 🐛experimentallow effortThere is not much implementation work to be done. The task is very easy or tiny.low impactChanges are not very noticeable or potential benefits are limited.should haveWe like the idea but it’s not important enough to be a part of the roadmap.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions