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:
libevmasm/EVMAssemblyStack.cpp:48 — analyze() calls Assembly::fromJSON(_assemblyJson, {}, 0, m_eofVersion) but does not pass m_evmVersion. fromJSON has no parameter for the EVM version at all.
libevmasm/Assembly.cpp:623 — fromJSON 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.
TL;DR
solc --standard-jsonwith{ "language": "EVMAssembly", "settings": { "evmVersion": "paris" } }produces bytecode for Osaka, not Paris. A{ "name": "PUSH", "value": "0" }item is emitted as0x5f(PUSH0) — an opcode that does not exist on Paris. Contracts deployed this way revert withINVALID OPCODEon a Paris-era EVM.Audited commit:
8471cf2ff005320b69535ee923e75edb569927d4(develop, 2026-05-21). Reproducible.Where the bug lives
Two co-operating omissions:
libevmasm/EVMAssemblyStack.cpp:48—analyze()callsAssembly::fromJSON(_assemblyJson, {}, 0, m_eofVersion)but does not passm_evmVersion.fromJSONhas no parameter for the EVM version at all.libevmasm/Assembly.cpp:623—fromJSONconstructs:EVMVersion{}is the default constructor, which resolves toVersion::Osaka(liblangutil/EVMVersion.h:180-184).EVMAssemblyStack::m_evmVersionis 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
solc --standard-jsonwithlanguage: "EVMAssembly"StandardCompiler.cpp:902-920parsessettings.evmVersion;:1352-1358forwards it toEVMAssemblyStack; the stack swallows it.evmasm::EVMAssemblyStackanalyze().solc --import-asm-jsonCLICommandLineParser.cpp:1123-1151rejects--evm-versionfor this mode, forcing the default.Assemblydirectly 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:
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 observed5fis invalid on Paris.The same result occurs with
"homestead","byzantium","london"— every pre-Shanghai target produces the same buggy5f5ff3, because theevmVersionsetting never reachesAssembly.Regression tests
Two test files exercise the bug under the existing
EVMAssemblyTestfixture (test/libevmasm/EVMAssemblyTest.cpp), placed under a newtest/libevmasm/evmAssemblyTests/audit/directory (isoltest discovers it via the recursive scan inboostTest.cpp).evm_version_push0_paris.asmjson(fails ondevelop):evm_version_push0_shanghai.asmjson(sanity check, passes):Run with
--evm-version=parisondevelop:Why the existing tests did not catch this
test/libevmasm/evmAssemblyTests/isoltestTesting/push.asmcoversPUSH 0but hard-codes5f6001…and is not version-restricted. Running it under--evm-version=parispasses because the buggy code emits5f, 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
_evmVersionthroughAssembly::fromJSON:With this change applied, the Paris regression test passes; the
isoltestTesting/push.asmexpectation needs to be updated as described above.