Skip to content
This repository was archived by the owner on Jul 10, 2026. It is now read-only.

Commit 1a680cf

Browse files
committed
feat: replace vault deployer with generic cyclic deployer
1 parent 4ae194b commit 1a680cf

12 files changed

Lines changed: 580 additions & 446 deletions

File tree

Nargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
members = [
33
"src/token_contract",
44
"src/vault_contract",
5-
"src/vault_deployer",
5+
"src/cyclic_deployer",
66
"src/dripper",
77
"src/nft_contract",
88
"src/escrow_contract",
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
[package]
2-
name = "vault_deployer"
2+
name = "cyclic_deployer"
33
authors = [""]
44
compiler_version = ">=1.0.0"
55
type = "contract"
66

77
[dependencies]
88
aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.3.0", directory = "noir-projects/aztec-nr/aztec" }
9-
vault_contract = { path = "../vault_contract" }
10-
token_contract = { path = "../token_contract" }

src/cyclic_deployer/README.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Cyclic Deployer Contract
2+
3+
The `CyclicDeployer` atomically deploys and wires **two cyclically-dependent contracts** in a single transaction, working around the two-step initialization such a pair would otherwise require. The canonical example in this repository is the [`Vault`](../vault_contract) and its AIP-20 shares [`Token`](../token_contract) (see the vault [Deployment Guide](../vault_contract/README.md#deployment-guide)), and that pair is used as the worked example throughout — but the contract is type-agnostic and imports none of the contracts it deploys.
4+
5+
## The deploy-time cycle
6+
7+
Two contracts can reference each other so that each one's address depends on the other's. A contract's address commits to its constructor arguments and deployer, so when each address depends on the other neither can be computed first — a true cycle. (In the example: the shares token's `minter` is the vault, and the vault must learn its shares token address.)
8+
9+
Following the [cyclic-deployment standard](../../.supervision/circular_dep_deployer/agent_output.md), the cycle is broken by **deferring a minimal set of cross-references** out of the constructors. For a two-contract cycle exactly one edge is deferred:
10+
11+
- One contract — `linked` — keeps nothing cross-contract in its constructor and defers its back-reference into a one-shot, deployer-gated `set_<target>(AztecAddress)` setter. (The vault keeps only `asset` and `vault_offset`, and defers `set_shares_token`.)
12+
- The other contract — `target` — keeps its reference in its constructor, because it is resolvable in dependency order: `linked` depends on nothing cross-contract, so it is derived first, and the derived `linked` address is then embedded as a constructor argument of `target`. (The shares token keeps `minter = vault` in its constructor.)
13+
14+
This leaves a single deferred edge (`linked`'s setter), which the deployer enforces on-chain. The two contracts are named by this deferred-link role rather than any parent/child hierarchy: `linked` is the contract whose setter is wired, and `target` is the contract whose derived address is injected into it.
15+
16+
## A reusable, spec-independent deployer
17+
18+
`publish_contract_instance_for_public_execution` is private-only, so the deploy-and-wire choreography must run inside a private function. `CyclicDeployer` follows the standard's core design decision: **`deploy` is an ordinary private function, not a constructor.** Because the deploy logic does not live in the deployer's initializer, the deployer instance's address does not depend on the contracts it deploys — so a single published `CyclicDeployer` instance can deploy any number of pairs, with the per-deployment `salt` acting as the sole address disambiguator.
19+
20+
This contract is intentionally **specific to the two-contract topology** (exactly two contracts, one deferred setter, and up to one optional action per contract) rather than the standard's fully generic N-contract form that accepts arbitrary contracts and index-referenced links. It does, however, adopt the standard's **hash-based dispatch model** and its data shapes, so it is fully **type-agnostic**:
21+
22+
- Each contract's constructor is described by a `ContractSpec { class_id, init_selector, init_args_hash, init_calldata_hash }`. The SDK precomputes the `init_args_hash` (which drives the contract's address) and the `init_calldata_hash` (which dispatches the public constructor), and the deployer treats both opaquely — it never constructs calldata from typed arguments.
23+
- The deferred edge is described by a `Link { setter_selector }`. The deployer dispatches `linked`'s setter by this runtime selector, building the derived `target` address into the calldata in-circuit, so it needs no compile-time-typed setter stub.
24+
25+
A consequence of opaque dispatch is that any cross-reference kept in a constructor (`target` embedding `linked`) is resolved **off-chain**: the SDK derives `linked` first and embeds it when computing `target`'s hashes. The on-chain guarantee is therefore limited to the deferred link, whose argument is the address the deployer itself derived — so the `linked → target` link is enforced in-circuit while a `target → linked` constructor reference is SDK-trusted. This matches the generic standard, which is type-agnostic and cannot inspect constructor arguments.
26+
27+
In one transaction, `deploy`:
28+
29+
1. Derives both contract addresses on-chain from each `ContractSpec`'s `class_id` + `init_selector` + `init_args_hash` and the deployment salt, with this `CyclicDeployer` instance set as their (non-universal) `deployer`. `linked` is derived before `target` so `target`'s constructor may embed `linked`.
30+
2. Calls `publish_contract_instance_for_public_execution` for both instances.
31+
3. Runs both public constructors by dispatching their precomputed `init_calldata_hash` (with `hide_msg_sender = false`, so each constructor sees this contract as `msg_sender`).
32+
4. Wires the deferred link by calling the `Link`'s `setter_selector` on `linked` with the derived `target` address, built in-circuit.
33+
5. Optionally runs one authorized `Action` per contract after wiring — `linked_action` on `linked`, then `target_action` on `target`. Each action is enqueued only when its `calldata_hash` is non-zero, so a deployment with no actions, one action, or two actions all go through the same `deploy` entrypoint.
34+
35+
The derive/publish/construct work is shared by a single `_deploy_contract` helper that handles one contract from its `ContractSpec`; `deploy` calls it once per contract.
36+
37+
Each `Action` is opaque (its arguments are SDK-supplied via the calldata preimage); the target contract enforces its own one-shot / authorization semantics, so the deployer needs no knowledge of the action's arguments. (In the example: the vault's `initial_deposit` as the `linked_action`, whose `init_args_hash` must commit to `initial_deposit_pending = true` to arm that one-shot action.) Richer choreography — multiple actions on the same contract, or a defined order beyond linked-then-target — is intentionally left to the generic N-contract deployer or to an integrator's own action entrypoint.
38+
39+
Because both contracts record this `CyclicDeployer` instance as their non-universal `deployer`, and every contract call runs with this contract as `msg_sender`, the protocol's public initialization check, `publish_contract_instance_for_public_execution`, and the deferred setter's deployer-gate all pass.
40+
41+
## Inputs
42+
43+
```rust
44+
/// @notice Opaque constructor-dispatch spec for one contract of the deployment.
45+
struct ContractSpec {
46+
class_id: ContractClassId, // registered class to instantiate
47+
init_selector: Field, // public constructor selector (drives the initialization hash)
48+
init_args_hash: Field, // hash of the constructor ARGS; drives the contract address
49+
init_calldata_hash: Field, // hash of [selector, ..args]; dispatches the public constructor
50+
}
51+
52+
/// @notice Deferred cross-reference wired after construction, enforced on-chain.
53+
struct Link {
54+
setter_selector: Field, // linked's setter to invoke (e.g. set_shares_token)
55+
}
56+
57+
/// @notice Optional authorized side effect run on a single contract after wiring.
58+
struct Action {
59+
calldata_hash: Field, // hash of [selector, ..args]; zero = no action, else dispatched opaquely
60+
}
61+
```
62+
63+
## Functions
64+
65+
### deploy
66+
67+
```rust
68+
/// @notice Atomically deploys and wires two cyclically-dependent contracts, optionally running one
69+
/// authorized action on each after wiring
70+
/// @dev Each action is an opaque public call dispatched by calldata hash; a zero `calldata_hash` means
71+
/// "no action" and is skipped. Actions run after wiring, `linked` before `target`, each with this
72+
/// contract as `msg_sender` so the target's deployer gate passes. Any authwit an action relies on
73+
/// must be prepared by the SDK against the precomputed address before this tx.
74+
/// @param salt The salt used to derive both contract addresses (must be unique per deployment)
75+
/// @param linked The contract whose setter is wired by `link` (it defers a cross-reference)
76+
/// @param target The contract whose derived address is injected into `linked`'s setter; its constructor may embed `linked`
77+
/// @param link The deferred link to wire (`linked`'s `set_<target>` setter)
78+
/// @param linked_action Optional action dispatched on `linked` after wiring (skipped when its `calldata_hash` is zero)
79+
/// @param target_action Optional action dispatched on `target` after wiring (skipped when its `calldata_hash` is zero)
80+
#[external("private")]
81+
fn deploy(
82+
salt: Field,
83+
linked: ContractSpec,
84+
target: ContractSpec,
85+
link: Link,
86+
linked_action: Action,
87+
target_action: Action,
88+
) { /* ... */ }
89+
```
90+
91+
## Usage Notes
92+
93+
- Publish the participating contract classes (e.g. `Vault`, `Token`) and the `CyclicDeployer` class once per network, and publish a `CyclicDeployer` instance once. The same instance can be reused for every deployment.
94+
- The SDK is responsible for the off-chain half of the choreography. For each contract it assembles a `ContractSpec` from the `class_id`, the constructor selector, the `init_args_hash` (`hash_args(args)`), and the `init_calldata_hash` (`hash_calldata_array([selector, ...args])`) — embedding the derived `linked` address into `target`'s constructor arguments — and a `Link` from `linked`'s setter selector. For each contract it wants to act on, it assembles an `Action` from the action's calldata hash (`hash_calldata_array([selector, ...args])`); for a contract with no action it passes an `Action` with a zero `calldata_hash`. It must then:
95+
- register both contract instance preimages with the PXE before submitting the transaction, so the `get_contract_instance` oracle can resolve them during publication; and
96+
- supply both constructor calldata preimages — plus the calldata preimage of each non-zero action — as **extra hashed args** on the transaction, so the deployer's hash-dispatched public calls resolve.
97+
- Both contract instances use the `CyclicDeployer` instance as their `deployer` and reuse the deployment `salt` as their own salt. Their addresses can therefore be precomputed off-chain from the `CyclicDeployer` instance address, the deployment salt, the corresponding `ContractClassId`, and the corresponding initialization hash (derived from the constructor selector and `init_args_hash`). Precomputing a contract's address is required when its action relies on an authwit signed against it (e.g. the vault's initial deposit on `linked`).
98+
- Use a fresh, unpredictable `salt` per deployment. Under a reused `CyclicDeployer` instance the `deployer` field is constant, so the salt is the only thing that keeps each pair's addresses distinct.

0 commit comments

Comments
 (0)