Skip to content

[FEATURE] public instance registration (enables factory pattern) #20771

@wei3erHase

Description

@wei3erHase

Problem Statement

Contract instance publishing is private-only, blocking the public factory pattern.

The publish_contract_instance_for_public_execution helper and ContractInstanceRegistry's publish_for_public_execution both use PrivateContext. As a result:

  1. Both initializers cannot be public – Factory + child cannot both have public initializers.
  2. Circular dependencies are hard – Two contracts needing each other's address (e.g. Governance ↔ Treasury) require a bootstrap.
  3. DX – Workaround (private init → enqueue child public init) is more complex.

Root cause: Registry's publish_for_public_execution is abi_private; aztec-nr helper uses get_contract_instance oracle (private-only).

Proposed Solution

Add a public path for publishing instances.

1. New public function on ContractInstanceRegistry

publish_for_public_execution_public (or similar) – abi_public, same args as private version. Uses PublicContext:

  • nullifier_exists_unsafe for class registration check (instead of assert_nullifier_exists)
  • deployer = universal_deploy ? zero : msg_sender
  • push_nullifier(address) (already supported in PublicContext)
  • emit_public_log for deployment event

The update function already shows public nullifier usage works.

2. aztec-nr helper

publish_contract_instance_for_public_execution_public (or similar) – takes PublicContext and instance params as arguments (no oracle).

3. Event format

Ensure public deployment events work with indexers/archiver (emit_public_log vs emit_private_log).

Example Use Case

Circular dependency – User deploys Governance; Governance's initializer spawns Treasury and passes self.address + config. (Or the other way around.) Class IDs known at deploy time:

// Treasury – spawned by Governance; needs governance address in its init
#[aztec]
pub contract Treasury {
    #[storage]
    struct Storage { governance: PublicMutable<AztecAddress>, fee_bps: PublicMutable<Field> }

    #[external("public")]
    #[initializer]
    fn constructor(governance_address: AztecAddress, fee_bps: Field) {
        self.governance.set(governance_address);
        self.fee_bps.set(fee_bps);
    }
}
// Governance – user deploys this; its init spawns Treasury with actual constructor args
#[aztec]
pub contract Governance {
    #[storage]
    struct Storage { treasury: PublicMutable<AztecAddress> }

    #[external("public")]
    #[initializer]
    fn constructor(
        treasury_salt: Field, treasury_public_keys: PublicKeys,
        fee_bps: Field,  // actual arg for Treasury.constructor
    ) {
        let registry = ContractInstanceRegistry::at(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS);
        let deployer = self.address;
        let treasury_class_id = ContractClassId::from_field(0x456);

        // Derive init_hash from args for Treasury.constructor(governance_address, fee_bps); uses hash_args, compute_initialization_hash
        let treasury_args_hash = hash_args([self.address.to_field(), fee_bps]);
        let treasury_init_hash = compute_initialization_hash(
            comptime { FunctionSelector::from_signature("constructor((Field),Field)") },
            treasury_args_hash,
        );

        // 1. Publish Treasury
        self.call_public(
            registry.publish_for_public_execution_public(
                treasury_salt, 
                treasury_class_id, 
                treasury_init_hash, 
                treasury_public_keys, 
                false
            ));

        // 2. Compute address, then call Treasury with actual args
        let treasury_address = AztecAddress::compute(
            treasury_public_keys,
            PartialAddress::compute(
                treasury_class_id, 
                treasury_salt, 
                treasury_init_hash, 
                deployer
            ));
        self.call_public(
            Treasury::at(treasury_address).constructor(self.address, fee_bps)
            );
        self.treasury.set(treasury_address);
    }
}

Single public tx; Treasury gets Governance address + fee_bps from the deployment args.

Alternative Solutions

Workarounds that avoid implementing public publish:

Alternative Drawback
Private factory init + enqueue child init Factory must be private; blocks public flows
Two-phase deploy (publish, then init) Two txs, worse UX
Circular deps: 2 txs + setters Pre-compute both addresses. Tx1: publish Gov, init with treasury=placeholder. Tx2: publish Treasury, init, call Gov.set_treasury. Works today. Drawback: 2 txs, setter required.
Status quo Public factory impossible

Additional Context

  • Feasibility: PublicContext has push_nullifier and nullifier_exists_unsafe; registry receives params as args (no oracle). See public_context.nr, ContractInstanceRegistry update fn.
  • Related: publish_contract_instance.nr, contract_instance_registry_contract/main.nr, e2e_amm.test.ts TODO(Make the AMM itself be a token #9480)

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-feature-requestType: Adding a brand new feature (not to be confused with improving an existing feature).from-communityThis originated from the community :)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions