Skip to content
This repository was archived by the owner on Jul 10, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Nargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
members = [
"src/token_contract",
"src/vault_contract",
"src/vault_deployer",
"src/cyclic_deployer",
"src/dripper",
"src/nft_contract",
"src/escrow_contract",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
[package]
name = "vault_deployer"
name = "cyclic_deployer"
authors = [""]
compiler_version = ">=1.0.0"
type = "contract"

[dependencies]
aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.3.0", directory = "noir-projects/aztec-nr/aztec" }
vault_contract = { path = "../vault_contract" }
token_contract = { path = "../token_contract" }
89 changes: 89 additions & 0 deletions src/cyclic_deployer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Cyclic Deployer Contract

`CyclicDeployer` deploys and wires **two contracts that depend on each other** in a single transaction. It is type-agnostic — it imports none of the contracts it deploys — so any mutually-dependent pair can reuse the same deployer.

The worked example throughout is the [`Vault`](../vault_contract) and its shares [`Token`](../token_contract): the token's `minter` is the vault, and the vault needs the token's address (see the vault [Deployment Guide](../vault_contract/README.md#deployment-guide)).

## The problem

A contract's address is derived from its constructor arguments and its deployer. When two contracts each take the other's address as a constructor argument, neither address can be computed first — a deploy-time cycle.

## How it breaks the cycle

The cycle is broken on one side, giving the two contracts their names:

- **`linked`** keeps no cross-contract reference in its constructor. Instead it exposes a setter (e.g. `set_shares_token`) that the deployer calls _after_ construction. Its address doesn't depend on the other contract, so it can be derived first.
- **`target`** embeds `linked`'s already-derived address directly in its constructor.

The deployer then completes the link by calling `linked`'s setter with `target`'s address.

## What `deploy` does

`deploy` is a regular private function, so one published `CyclicDeployer` instance can deploy any number of pairs — a fresh `salt` per deployment keeps their addresses distinct. In a single transaction it:

1. Derives both addresses, with the deployer instance as their `deployer`.
2. Publishes both contract instances.
3. Runs both constructors.
4. Calls `linked`'s setter with `target`'s derived address.
5. Optionally runs one action per contract (`linked`, then `target`), skipping any whose `calldata_hash` is zero.

Everything is dispatched **by hash**, which is what keeps the deployer type-agnostic: constructors and actions are precomputed calldata hashes, and the link is just a setter selector. The SDK supplies the matching preimages (see [Using it](#using-it)).

## Interface

```rust
/// Constructor-dispatch spec for one contract, all treated opaquely.
struct ContractSpec {
class_id: ContractClassId, // registered class to instantiate
init_selector: Field, // public constructor selector
init_args_hash: Field, // hash of the constructor args; drives the contract address
init_calldata_hash: Field, // hash of [selector, ..args]; dispatches the constructor
}

/// The deferred reference wired on `linked` after construction.
struct Link {
setter_selector: Field, // linked's setter to call (e.g. set_shares_token)
}

/// An optional side effect dispatched on one contract after wiring.
struct Action {
calldata_hash: Field, // hash of [selector, ..args]; zero = no action
}

/// Deploys and wires `linked` + `target`, then runs each non-zero action (`linked` before `target`).
#[external("private")]
fn deploy(
salt: Field, // unique per deployment
linked: ContractSpec, // its setter is wired by `link`
target: ContractSpec, // its derived address is injected into `linked`'s setter
link: Link,
linked_action: Action, // runs on `linked` (skip with a zero calldata_hash)
target_action: Action, // runs on `target` (skip with a zero calldata_hash)
) { /* ... */ }
```

## What it guarantees (and what it doesn't)

On-chain, the deployer guarantees only that:

- both contracts are deployed with the `CyclicDeployer` instance as their `deployer`, and
- `linked`'s setter is called with the `target` address the deployer derived itself (it can't be swapped off-chain), with the deployer instance as `msg_sender`.

It does **not** enforce any application logic on the deployed contracts. The contracts themselves must therefore:

- **gate the setter to their deployer** — otherwise anyone could call it and wire a different `target`;
- **make the setter one-shot** if the link must not change later — the deployer won't prevent a second call;
- **enforce each action's own authorization / one-shot rules** — actions are opaque to the deployer (e.g. the vault's `initial_deposit` is gated by an `initial_deposit_pending` flag baked into its address).

A constructor reference in the other direction (`target → linked`) is computed off-chain by the SDK and **not** enforced on-chain. Verify it from public state after deployment (e.g. that the shares token's `minter` is the vault).

## Using it

Publish the contract classes (e.g. `Vault`, `Token`) and the `CyclicDeployer` class once per network, and publish one `CyclicDeployer` instance to reuse for every deployment. Then, off-chain, the SDK must:

- build a `ContractSpec` per contract, embedding `linked`'s derived address into `target`'s constructor args;
- build a `Link` from `linked`'s setter selector, and an `Action` per contract (zero `calldata_hash` = no action);
- register both contract instances with the PXE so they resolve during publication; and
- attach every constructor and non-zero action calldata preimage as **extra hashed args**, so the hash-dispatched calls resolve.

Use a fresh, unpredictable `salt` per deployment — with a reused deployer instance, the salt is the only thing keeping each pair's addresses distinct. Precompute a contract's address ahead of time when an action needs an authwit signed against it (e.g. the vault's initial deposit).
180 changes: 180 additions & 0 deletions src/cyclic_deployer/src/main.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
use aztec::macros::aztec;

/// @title CyclicDeployer
/// @notice Atomically deploys and wires two cyclically-dependent contracts in a single transaction.
/// @dev When two contracts reference each other, each one's address depends on the other's -- a cycle, since
/// an address commits to its constructor arguments and deployer. The cycle is broken by deferring one
/// side's cross-reference out of its constructor into a setter the deployer wires after construction; the
/// other side keeps its reference in its constructor, resolved in dependency order off-chain.
///
/// The deployer is type-agnostic: it imports none of the contracts it deploys. Constructors are
/// dispatched by precomputed calldata hashes (`ContractSpec`) and the deferred link by a runtime selector
/// (`Link`), both treated opaquely. `linked` is the contract whose setter is wired; `target` is the
/// contract whose derived address is injected into that setter. In one transaction `deploy` derives both
/// addresses (with this instance as their deployer), publishes both instances, runs their constructors,
/// wires the link, and optionally runs one action per contract.
///
/// See the README for the dispatch model, trust assumptions, and the off-chain SDK steps.
#[aztec]
pub contract CyclicDeployer {
use aztec::{
macros::functions::{external, initialization_utils::compute_initialization_hash, internal},
protocol::{
abis::function_selector::FunctionSelector,
address::AztecAddress,
contract_class_id::ContractClassId,
contract_instance::ContractInstance,
public_keys::PublicKeys,
traits::{Deserialize, FromField, Serialize, ToField},
},
publish_contract_instance::publish_contract_instance_for_public_execution,
};

/// @notice Opaque constructor-dispatch spec for one contract of the deployment.
/// @dev The deployer derives the contract address from `class_id` + `init_selector` +
/// `init_args_hash`, and dispatches its public constructor by `init_calldata_hash`. All
/// four are caller-supplied and treated opaquely; the SDK is responsible for their consistency (e.g.
/// embedding a derived target address as a constructor argument, resolved in dependency order).
#[derive(Serialize, Deserialize)]
pub struct ContractSpec {
pub class_id: ContractClassId,
pub init_selector: Field,
pub init_args_hash: Field,
pub init_calldata_hash: Field,
}

/// @notice Deferred cross-reference wired on `linked` after construction.
/// @dev Only the setter selector is configurable; the deployer calls it with `target`'s derived address,
/// built into the calldata in-circuit, so an off-chain caller cannot substitute the argument.
#[derive(Serialize, Deserialize)]
pub struct Link {
pub setter_selector: Field,
}

/// @notice Optional side effect dispatched on a single contract after wiring.
/// @dev Dispatched by precomputed calldata hash (the SDK supplies the preimage as an extra hashed arg) and
/// treated opaquely. A zero `calldata_hash` means "no action" and is skipped; any other value enqueues
/// the call.
#[derive(Serialize, Deserialize)]
pub struct Action {
pub calldata_hash: Field,
}

/// @notice Atomically deploys and wires two cyclically-dependent contracts, optionally running one
/// action on each after wiring.
/// @dev Derives both addresses with this instance as their deployer, publishes both, runs their
/// constructors, wires the link, then dispatches each non-zero action (`linked` before `target`).
/// Every call runs with this contract as `msg_sender`. The deployer makes no guarantees about the
/// deployed contracts beyond this; see the README for the assumptions a caller must satisfy.
/// @param salt The salt used to derive both contract addresses (must be unique per deployment).
/// @param linked The contract whose setter is wired by `link`.
/// @param target The contract whose derived address is injected into `linked`'s setter; its constructor may embed `linked`.
/// @param link The deferred link to wire on `linked`.
/// @param linked_action Optional action dispatched on `linked` after wiring (skipped when its `calldata_hash` is zero).
/// @param target_action Optional action dispatched on `target` after wiring (skipped when its `calldata_hash` is zero).
#[external("private")]
fn deploy(
salt: Field,
linked: ContractSpec,
target: ContractSpec,
link: Link,
linked_action: Action,
target_action: Action,
) {
let linked_address = self.internal._deploy_contract(salt, linked);
let target_address = self.internal._deploy_contract(salt, target);

// Wire the deferred link: call `linked`'s setter with `target`'s derived address. The argument is
// built into the calldata in-circuit (it cannot be substituted off-chain) and the call runs with this
// contract as `msg_sender`.
self.context.call_public_function(
linked_address,
FunctionSelector::from_field(link.setter_selector),
[target_address.to_field()],
false,
);

// Run each optional action after wiring, `linked` before `target`. A zero calldata hash means no
// action, so each call is enqueued only for a non-zero hash.
self.internal._run_action(linked_address, linked_action);
self.internal._run_action(target_address, target_action);
}

/** ==========================================================
* ====================== INTERNALS ==========================
* ======================================================== */

/// @notice Derives a contract's address, publishes its instance, and dispatches its constructor.
/// @dev Called once per contract. The constructor is dispatched by its precomputed calldata hash; the SDK
/// supplies the matching preimage. This instance is recorded as the contract's deployer, which the
/// protocol's public initialization check enforces against `msg_sender`. Publishes run in private and
/// so always precede every public constructor; `linked` is deployed before `target` so `target`'s
/// constructor may embed `linked`'s address.
/// @return The derived contract address.
#[internal("private")]
fn _deploy_contract(salt: Field, spec: ContractSpec) -> AztecAddress {
// 1. Derive the contract address on-chain, with this instance as the deployer.
let address = _derive_instance(
spec.init_selector,
spec.init_args_hash,
spec.class_id,
salt,
self.address,
);

// 2. Publish the instance (reverts on a duplicate or already-published address).
publish_contract_instance_for_public_execution(context, address);

// 3. Run the public constructor by dispatching its precomputed calldata hash. `hide_msg_sender` is
// false so the constructor sees this contract as `msg_sender` (the recorded deployer).
context.call_public_function_with_calldata_hash(
address,
spec.init_calldata_hash,
false,
false,
);

address
}

/// @notice Dispatches an optional action on a single contract after wiring.
/// @dev A zero `calldata_hash` means "no action", so the public call is enqueued only for a non-zero hash.
/// The action is dispatched opaquely by calldata hash and runs with this contract as `msg_sender`.
/// @param address The contract to dispatch the action on.
/// @param action The optional action (skipped when its `calldata_hash` is zero).
#[internal("private")]
fn _run_action(address: AztecAddress, action: Action) {
if action.calldata_hash != 0 {
context.call_public_function_with_calldata_hash(
address,
action.calldata_hash,
false,
false,
);
}
}

/// @notice Derives a contract instance address from its class, salt, constructor, and deployer.
#[contract_library_method]
fn _derive_instance(
init_selector: Field,
init_args_hash: Field,
contract_class_id: ContractClassId,
salt: Field,
deployer: AztecAddress,
) -> AztecAddress {
let initialization_hash = compute_initialization_hash(
FunctionSelector::from_field(init_selector),
init_args_hash,
);

ContractInstance {
salt,
deployer,
contract_class_id,
initialization_hash,
public_keys: PublicKeys::default(),
}
.to_address()
}
}
Loading
Loading