Description
A constant state variable may be initialised with type(D).creationCode — this is supported and is
covered by checked-in tests (test/libsolidity/syntaxTests/metaTypes/codeAccessIsConstant.sol,
test/libsolidity/syntaxTests/constants/initialization/type_info.sol).
The call-graph builder, however, only ever descends into a constant's initializer when the constant
is named by a plain identifier. When the same constant is named by a member access
(L.K), or when it is a public constant reached through its generated getter, the initializer
is never visited, so the type(D).creationCode inside it never records a bytecode dependency on
D.
Two things break as a result:
- A valid, acyclic contract fails to compile with an internal compiler error, purely because
of the order in which the two contracts are declared. Swapping the declarations makes it
compile, with no other change.
- The circular-bytecode-reference check (
TypeError 7813) is silently skipped for genuine
cycles routed through such a constant. Those also end in an ICE instead of the diagnostic.
Both pipelines are affected (legacy and via IR).
Environment
- Compiler versions: 0.8.4 through 0.8.36 (0.8.0 unaffected)
- Pipelines: legacy and via IR, both affected
- EVM version: default
- OS: macOS
Reproducer 1 — valid acyclic contract, ICE (4 lines)
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract C { bytes public constant K = type(D).creationCode; }
contract D { uint public x = 1; }
Expected: bytecode for C and D.
Actual:
$ solc --bin order.sol
Internal compiler error:
/solidity/libsolidity/codegen/CompilerContext.cpp(255): Throw in function std::shared_ptr<evmasm::Assembly> solidity::frontend::CompilerContext::compiledContract(const ContractDefinition &) const
Dynamic exception type: boost::wrapexcept<solidity::langutil::InternalCompilerError>
std::exception::what: Compiled contract not found.
[solidity::util::tag_comment*] = Compiled contract not found.
$ solc --via-ir --bin order.sol
Internal compiler error: ... Invalid IR generated:
Error (3517): Unknown data object "D_12".
--> :106:36:
|
106 | let _1 := datasize("D_12")
| ^^^^^^
Control — move D above C and it compiles. This is the only change:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract D { uint public x = 1; }
contract C { bytes public constant K = type(D).creationCode; }
Second control — adding one reachable direct reference to type(D).creationCode anywhere in
C restores the missing edge and the original order compiles. (Putting the same expression in an
unreachable internal function does not, which is consistent with the reachability model and
confirms the missing dependency edge is the cause.)
Reproducer 2 — the same hole reached by member access, and a real cycle that is not reported
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
library LD { bytes constant K = type(D).creationCode; }
library LC { bytes constant K = type(C).creationCode; }
contract C { function f() public pure returns (bytes memory) { return LD.K; } }
contract D { function f() public pure returns (bytes memory) { return LC.K; } }
This is a genuine circular bytecode reference. Expected: TypeError 7813. Actual: the same
Compiled contract not found. ICE — the cycle check never sees the edge.
Control: the same two libraries and the same cycle, with the constant read by an identifier
inside the library instead of by member access from outside, is reported correctly.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
library LD { bytes constant K = type(D).creationCode;
function g() internal pure returns (bytes memory) { return K; } }
library LC { bytes constant K = type(C).creationCode;
function g() internal pure returns (bytes memory) { return K; } }
contract C { function f() public pure returns (bytes memory) { return LD.g(); } }
contract D { function f() public pure returns (bytes memory) { return LC.g(); } }
Error: Circular reference to contract bytecode either via "new" or "type(...).creationCode" / "type(...).runtimeCode".
The direct form (return type(D).creationCode; in the function body) and the file-level-constant
form are also both reported correctly. Identifier vs member access is the only axis that matters.
Cause
libsolidity/analysis/FunctionCallGraph.cpp.
buildCreationGraph deliberately skips constants when seeding from state variables (:41-43):
for (auto const* stateVar: base->stateVariables())
if (!stateVar->isConstant())
stateVar->accept(builder);
so a constant's initializer is reached from exactly one place — visit(Identifier) (:143-152),
which recurses on demand:
if (auto const* variable = dynamic_cast<VariableDeclaration const*>(_identifier.annotation().referencedDeclaration))
if (variable->isConstant())
{
solAssert(variable->isStateVariable() || variable->isFileLevelVariable(), "");
variable->accept(*this); // <-- the recursion
}
Two ways of naming the same constant never reach that branch:
visit(MemberAccess) (:167-204) has no constant-variable branch at all. It computes
functionType/functionDef and returns early at :183-184 when the member is not a function, so
L.K descends only into the expression L (a ContractDefinition) and never into K's value.
buildDeployedGraph explicitly ignores public-variable getters (:85-86):
else
// If it's not a function, it must be a getter of a public variable; we ignore those
solAssert(variable, "");
Either way the m_graph.bytecodeDependency.emplace(...) at :178 never runs.
CompilerStack::createAndAssignCallGraphs (:146-149) then copies an empty dependency set into
annotation().contractDependencies, and both findAndReportCyclicContractDependencies (:180-182)
and the compilation order are computed without the edge.
Versions*
Tested on 0.8.0, 0.8.4, 0.8.10, 0.8.17, 0.8.20, 0.8.22, 0.8.26, 0.8.28, 0.8.30, 0.8.34, 0.8.36
(Reproducer 1):
- legacy: clean on 0.8.0, ICE on 0.8.4 through 0.8.36 (every version tested).
- via IR: clean on 0.8.0, 0.8.4 and 0.8.10; ICE on 0.8.17 through 0.8.36.
So the legacy failure arrives with the 0.8.1-0.8.4 call-graph rework and the via-IR pipeline joins
it later, once it started resolving these data objects the same way.
FunctionCallGraph.cpp is unchanged on develop (last commit de1a017cc, 2023-08-14).
Test coverage
test/libsolidity/syntaxTests/bytecodeReferences/ is dedicated to this invariant and covers cycles
through library functions (library_function_circular_reference.sol, library_called.sol), but
has no case for a cycle through a library constant.
Separately, syntaxTests/metaTypes/codeAccessIsConstant.sol and
syntaxTests/constants/initialization/type_info.sol both bless the exact construct — and
codeAccessIsConstant.sol even declares B after the contract holding the constant. They pass
because syntax tests stop before code generation; the same sources ICE under --bin. So no
checked-in expectation needs to change here, but the fix does need coverage at a tier that runs
codegen.
#16345 reports the same ICE string for
contract A { bytes constant public code = type(A).creationCode; }. That is a different defect,
and its fix would not address this one:
Description
A constant state variable may be initialised with
type(D).creationCode— this is supported and iscovered by checked-in tests (
test/libsolidity/syntaxTests/metaTypes/codeAccessIsConstant.sol,test/libsolidity/syntaxTests/constants/initialization/type_info.sol).The call-graph builder, however, only ever descends into a constant's initializer when the constant
is named by a plain identifier. When the same constant is named by a member access
(
L.K), or when it is a public constant reached through its generated getter, the initializeris never visited, so the
type(D).creationCodeinside it never records a bytecode dependency onD.Two things break as a result:
of the order in which the two contracts are declared. Swapping the declarations makes it
compile, with no other change.
TypeError 7813) is silently skipped for genuinecycles routed through such a constant. Those also end in an ICE instead of the diagnostic.
Both pipelines are affected (legacy and via IR).
Environment
Reproducer 1 — valid acyclic contract, ICE (4 lines)
Expected: bytecode for
CandD.Actual:
Control — move
DaboveCand it compiles. This is the only change:Second control — adding one reachable direct reference to
type(D).creationCodeanywhere inCrestores the missing edge and the original order compiles. (Putting the same expression in anunreachable internal function does not, which is consistent with the reachability model and
confirms the missing dependency edge is the cause.)
Reproducer 2 — the same hole reached by member access, and a real cycle that is not reported
This is a genuine circular bytecode reference. Expected:
TypeError 7813. Actual: the sameCompiled contract not found.ICE — the cycle check never sees the edge.Control: the same two libraries and the same cycle, with the constant read by an identifier
inside the library instead of by member access from outside, is reported correctly.
The direct form (
return type(D).creationCode;in the function body) and the file-level-constantform are also both reported correctly. Identifier vs member access is the only axis that matters.
Cause
libsolidity/analysis/FunctionCallGraph.cpp.buildCreationGraphdeliberately skips constants when seeding from state variables (:41-43):so a constant's initializer is reached from exactly one place —
visit(Identifier)(:143-152),which recurses on demand:
Two ways of naming the same constant never reach that branch:
visit(MemberAccess)(:167-204) has no constant-variable branch at all. It computesfunctionType/functionDefand returns early at:183-184when the member is not a function, soL.Kdescends only into the expressionL(aContractDefinition) and never intoK's value.buildDeployedGraphexplicitly ignores public-variable getters (:85-86):Either way the
m_graph.bytecodeDependency.emplace(...)at:178never runs.CompilerStack::createAndAssignCallGraphs(:146-149) then copies an empty dependency set intoannotation().contractDependencies, and bothfindAndReportCyclicContractDependencies(:180-182)and the compilation order are computed without the edge.
Versions*
Tested on 0.8.0, 0.8.4, 0.8.10, 0.8.17, 0.8.20, 0.8.22, 0.8.26, 0.8.28, 0.8.30, 0.8.34, 0.8.36
(Reproducer 1):
So the legacy failure arrives with the 0.8.1-0.8.4 call-graph rework and the via-IR pipeline joins
it later, once it started resolving these data objects the same way.
FunctionCallGraph.cppis unchanged ondevelop(last commitde1a017cc, 2023-08-14).Test coverage
test/libsolidity/syntaxTests/bytecodeReferences/is dedicated to this invariant and covers cyclesthrough library functions (
library_function_circular_reference.sol,library_called.sol), buthas no case for a cycle through a library constant.
Separately,
syntaxTests/metaTypes/codeAccessIsConstant.solandsyntaxTests/constants/initialization/type_info.solboth bless the exact construct — andcodeAccessIsConstant.soleven declaresBafter the contract holding the constant. They passbecause syntax tests stop before code generation; the same sources ICE under
--bin. So nochecked-in expectation needs to change here, but the fix does need coverage at a tier that runs
codegen.
Relation to #16345
#16345 reports the same ICE string for
contract A { bytes constant public code = type(A).creationCode; }. That is a different defect,and its fix would not address this one:
"Disallow" it — that program should be rejected. Reproducer 1 here is acyclic and valid
and should compile. The two fixes point in opposite directions.
whole axis, and the control that isolates it.
:85-86). The member-accessbranch (
:167-204) is untouched by it. That the two differ is demonstrable: the sameself-reference made
internaland read by an identifier is correctly reported asTypeError 7813rather than ICE-ing —
is the un-visited constant initializer in the call-graph builder, which is also what produces the
acyclic failure.