#16991·solidity

A `constant` holding `type(D).creationCode` records no bytecode dependency unless it is named by a plain identifier: valid, acyclic contracts fail to compile with an ICE depending on declaration order

Author: Lokkw0510Created Sep 9, 2026Updated Sep 9, 2026
Labelsbug :bug:

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:

  1. 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.
  2. 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)

solidity
// 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:

solidity
// 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

solidity
// 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.

solidity
// 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):

cpp
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:

cpp
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):
    cpp
    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.

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:

  • Circularity / intended remedy. #16345 is a genuine self-cycle and its title asks to "Disallow" it — that program should be rejected. Reproducer 1 here is acyclic and valid and should compile. The two fixes point in opposite directions.
  • Declaration order. Untouched by #16345 — a self-reference cannot be reordered. Here it is the whole axis, and the control that isolates it.
  • Access path. #16345 exercises only the ignored-getter branch (:85-86). The member-access branch (:167-204) is untouched by it. That the two differ is demonstrable: the same self-reference made internal and read by an identifier is correctly reported as TypeError 7813 rather than ICE-ing —
    solidity
    contract A {
        bytes constant code = type(A).creationCode;
        function f() public pure returns (bytes memory) { return code; }
    }
  • Root cause. #16345 frames it as a missing validation rule about self-reference; the cause here is the un-visited constant initializer in the call-graph builder, which is also what produces the acyclic failure.