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