Skip to content

[BUG] Importing a contract embeds its storage layout into the importer's artifact, silently changing its contract_class_id #21633

@wei3erHase

Description

@wei3erHase

What are you trying to do?

Deploy Contract A, which imports Contract B as a Nargo path dependency in order to call one of its functions. After B's storage layout changes (a state variable is added, removed, or reordered — with no change to any function signature), re-derive A's address using getContractInstanceFromDeployParams with the same deployer, salt, and constructor arguments.

Expected: the derived address is identical to the one registered on-chain — A's source is unchanged, so its contract_class_id should be unchanged.

Actual: the derived address differs from the on-chain address. A's contract_class_id has silently drifted because B's new storage slot constants were embedded into A's artifact.

Code Reference

Demonstrated in defi-wonderland/aztec-boilerplate#122.

The propagation chain is rooted in how artifactMetadataHash is computed:

// stdlib/src/contract/artifact_hash.ts
export function computeArtifactMetadataHash(artifact: ContractArtifact) {
  return sha256Fr(Buffer.from(deterministicStringify({ name: artifact.name, outputs: artifact.outputs }), 'utf-8'));
}

artifact.outputs is:

// stdlib/src/abi/abi.ts
outputs: {
  structs: Record<string, AbiType[]>;
  globals: Record<string, AbiValue[]>;   // ← B's storage slot constants land here
};

When A imports B, the Noir compiler emits B's storage slot assignments as compile-time globals and propagates them into A's outputs.globals. Because outputs is serialised and hashed into artifactMetadataHash, any change to B's storage layout changes A's hash chain:

B adds / removes / reorders a storage variable
  → B's slot constants change
  → those constants appear in A's outputs.globals
  → A's artifactMetadataHash  changes   (sha256(name + outputs))
  → A's artifactHash          changes
  → A's contract_class_id     changes
  → A's deployed address      changes

What does NOT propagate (and therefore does not affect A's class ID):

Change in B Propagates? Reason
Function body (same signature) No Private calls cross an oracle boundary; only the 4-byte selector is embedded in A's ACIR
Function signature Compile error A's generated call stubs reference the old signature
Add / remove / reorder storage variables Yes Slot globals are embedded in A's outputs.globals
Add unused functions No Dead stubs, not referenced in A's bytecode

Aztec Version

v4.1.0-rc.2

OS

No response

Browser (if relevant)

No response

Node Version

No response

Additional Context

The cross-contract call model is already interface-based at the circuit level: A's ACIR encodes only (target_address, selector, args_hash) — B's bytecode never crosses the oracle boundary. A developer inspecting how private calls work would reasonably conclude that nothing from B's internals can affect A's compilation. Storage layout is an entirely separate concern from function dispatch, and there is no call-site reason for A to know where B keeps its state.

Impact: any project that uses a path dependency to call another contract and derives its own address from its compiled artifact (e.g. in a solve or deployment script) will silently compute the wrong address whenever the upstream dependency's storage layout evolves, with no compile-time or runtime warning.

Workaround: declare a minimal interface contract for B — a stub that reproduces B's function signatures with empty bodies and no storage variables — and import the stub instead of B's full crate. Because the stub emits no slot globals, A's outputs.globals remains stable across any upstream storage changes, and A's class ID is unaffected.

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-bugType: Bug. Something is broken.from-communityThis originated from the community :)

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions