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

Open
#16,991 0 comments 0 reactions 0 assignees View on GitHub
bug :bug:
Dominant language
C++
Stars
25.7k
Forks
6.2k
Avg merge
2d 19h
Merged PRs (30d)
29

Description

## 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 solidity::frontend::CompilerContext::compiledContract(const ContractDefinition &) const
Dynamic exception type: boost::wrapexcept
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(_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](https://github.com/argotorg/solidity/issues/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.

Contributor guide

Open the contributing guide

Research direction

Start in libsolidity/analysis/FunctionCallGraph.cpp, especially buildCreationGraph, buildDeployedGraph, visit(Identifier), and visit(MemberAccess), then inspect the call-graph tests under test/libsolidity/syntaxTests/bytecodeReferences/. Reproduce both examples through legacy and via-IR code generation. Done means the acyclic example compiles regardless of declaration order and the cyclic constant reference reports TypeError 7813 instead of an ICE, with regression coverage.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, solidity
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.