Warning 6417 (`type(C).runtimeCode` may differ from deployed bytecode) is not emitted when the constructor's inline assembly sits in a modifier
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:
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):
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 — "Modifier that uses msg.value should
generate a warning if used in non-payable function" — was labelled bug :bug: and fixed by
#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 to4669b06ab, 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):
solccommand 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)
// 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)
// 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
// 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, whose discussion never mentions modifiers)
and found nothing covering this. I may still have missed prior art — please close as a duplicate
if so.
Source: argotorg/solidity