EVMAssembly import silently ignores evmVersion (emits PUSH0 on Paris)
- Dominant language
- C++
- Stars
- 25.7k
- Forks
- 6.2k
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 21
Description
## 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: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.
2. `libevmasm/Assembly.cpp:623` — `fromJSON` constructs:
```cpp
auto result = std::make_shared(
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`:
```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`:
```json
{"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`:
```cpp
// libevmasm/Assembly.h
static std::pair, std::vector> fromJSON(
Json const& _json,
std::vector const& _sourceList = {},
size_t _level = 0,
std::optional _eofVersion = std::nullopt,
langutil::EVMVersion _evmVersion = {} // NEW
);
// libevmasm/Assembly.cpp:623
auto result = std::make_shared(
_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.
Contributor guide
Research direction
Start with libevmasm/EVMAssemblyStack.cpp:48 and libevmasm/Assembly.cpp:623, then trace Assembly::fromJSON's recursive call and the EVMAssemblyStack entry point. Run the audit tests evm_version_push0_push0_paris.asmjson and evm_version_push0_shanghai.asmjson, along with isoltestTesting/push.asm. Done means Paris emits PUSH1 0x00 while Shanghai still emits PUSH0, with existing expectations updated accordingly.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, solidity
- Domain
- blockchain, compilers
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100