argotorg / argotorg/solidity

EVMAssembly import silently ignores evmVersion (emits PUSH0 on Paris)

Open
#16,759 1 comment 0 reactions 0 assignees View on GitHub
bug :bug: experimental low effort low impact should have
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.