argotorg / argotorg/solidity

Warning 6417 (`type(C).runtimeCode` may differ from deployed bytecode) is not emitted when the constructor's inline assembly sits in a modifier

Open
#16,957 2 comments 0 reactions 1 assignee Claimed by @r0qs View on GitHub
bug :bug: low effort low impact
Dominant language
C++
Stars
25.7k
Forks
6.2k
Avg merge
2d 19h
Merged PRs (30d)
29

Description

## Description

Warning 6417 tells the user that `type(C).runtimeCode` might not match what is actually deployed,
because `C`'s constructor uses inline assembly. It is not emitted when that inline assembly is
written in a **modifier applied to the constructor**, even though a constructor's modifiers always
execute as part of construction.

`libsolidity/analysis/StaticAnalyzer.cpp:56-62`:

```cpp
class Checker: public ASTConstVisitor
{
public:
Checker(FunctionDefinition const& _f) { _f.accept(*this); }
bool visit(InlineAssembly const&) override { assemblySeen = true; return false; }
bool assemblySeen = false;
};
```

`_f.accept(*this)` walks the constructor's own `FunctionDefinition`. That subtree contains the
`ModifierInvocation` nodes (name and arguments) but not the `ModifierDefinition` bodies, which hang
off the contract elsewhere. So `assemblySeen` stays `false` and the check at `:233` reports no
assembly.

Inheritance *is* handled — `checkInternal` iterates `linearizedBaseContracts`, and assembly in a
base constructor does warn — so this is one missing edge rather than a check that never traverses.

**Expected:** warning 6417, as for the same assembly written directly in the constructor body.

**Actual:** no diagnostic at all.

### A sibling analysis in the same directory does look inside constructor modifiers

`ViewPureChecker` treats a modifier's body as part of the function that applies it, and has a
dedicated message for the constructor case (`ViewPureChecker.cpp:274-283`, error 4006):

```solidity
contract A {
modifier needsValue() { require(msg.value >= 1); _; }
constructor() needsValue() {}
}
```

```
Error: This modifier uses "msg.value" or "callvalue()" and thus the constructor has to be payable.
```

So the compiler already propagates a constructor modifier's contents to the constructor for one
analysis, and does not for this one. That asymmetry is the reason I think this is an oversight
rather than a deliberate scope limit.

There is also precedent for treating this exact shape as a bug:
[#3770](https://github.com/argotorg/solidity/issues/3770) — *"Modifier that uses `msg.value` should
generate a warning if used in non-payable function"* — was labelled `bug :bug:` and fixed by
[#4590](https://github.com/argotorg/solidity/pull/4590). Same situation: a diagnostic that did not
look into the body of a modifier applied to a function.

## Environment

- Compiler version: 0.8.36+commit.8a079791 (the cited lines are byte-identical on current
`develop`; the code dates to `4669b06ab`, 2019-01-15, which introduced the warning)
- Compilation pipeline (legacy, IR, SSA CFG): all — this is in the analysis phase, before codegen
- Target EVM version (as per compiler settings): default; not EVM-version dependent
- Framework/IDE (e.g. Foundry, Hardhat, Remix): `solc` command line directly
- EVM execution environment / backend / blockchain client: n/a — compile-time only
- Operating system: macOS 26.5.2

## Steps to Reproduce

### 1. The control — assembly directly in the constructor (warning is emitted)

```solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;
contract A { constructor() { assembly { let z := 1 } } }
contract B { function f() external pure returns (bytes memory) { return type(A).runtimeCode; } }
```

```
$ solc --bin control.sol
Warning: The constructor of the contract (or its base) uses inline assembly. Because of that,
it might be that the deployed bytecode is different from type(...).runtimeCode.
```

### 2. The same assembly moved into a constructor modifier (no warning)

```solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;
contract A {
modifier m() { assembly { let z := 1 } _; }
constructor() m() {}
}
contract B { function f() external pure returns (bytes memory) { return type(A).runtimeCode; } }
```

```
$ solc --bin modifier.sol
(no diagnostics)
```

### 3. The case the warning exists for: the deployed bytecode really does differ

```solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;
contract A {
modifier replaceRuntime() { assembly { mstore(0, 0x42) return(0, 32) } _; }
constructor() replaceRuntime() {}
function real() external pure returns (uint) { return 1; }
}
contract B { function f() external pure returns (bytes memory) { return type(A).runtimeCode; } }
```

`A`'s creation code is

```
6080604052348015600e575f5ffd5b50 6042 5f 52 6020 5f f3 fe
mstore(0, 0x42) return(0, 32)
```

so the deployed bytecode is 32 bytes, while `type(A).runtimeCode` is the normal runtime code
containing `real()`. That is exactly the divergence 6417 describes, and the only diagnostic emitted
is an unrelated `Warning: Unreachable code.` for the now-dead `_;`.

## Notes
I searched the issue tracker before opening this (`6417`, `ConstructorUsesAssembly`, `runtimeCode`
with assembly / modifier / "deployed bytecode differs", `StaticAnalyzer` + modifier, "assembly in
modifier", and the PR that introduced the warning,
[#5775](https://github.com/argotorg/solidity/pull/5775), whose discussion never mentions modifiers)
and found nothing covering this. I may still have missed prior art — please close as a duplicate
if so.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.