@openzeppelin/compact-contracts 0.2.0 → 0.3.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +10 -10
  2. package/access/AccessControl.compact +3 -35
  3. package/access/Ownable.compact +5 -51
  4. package/access/ShieldedAccessControl.compact +2 -2
  5. package/access/ZOwnablePK.compact +2 -2
  6. package/crypto/EcdhMask.compact +177 -0
  7. package/crypto/ElGamal.compact +473 -0
  8. package/multisig/ForwarderPrivate.compact +233 -0
  9. package/multisig/ForwarderShielded.compact +149 -0
  10. package/multisig/ForwarderUnshielded.compact +152 -0
  11. package/multisig/ProposalManager.compact +328 -0
  12. package/multisig/ShieldedTreasury.compact +204 -0
  13. package/multisig/ShieldedTreasuryStateless.compact +80 -0
  14. package/multisig/Signer.compact +312 -0
  15. package/multisig/SignerManager.compact +205 -0
  16. package/multisig/UnshieldedTreasury.compact +114 -0
  17. package/multisig/presets/ShieldedMultiSig.compact +227 -0
  18. package/multisig/presets/ShieldedMultiSigV2.compact +280 -0
  19. package/multisig/presets/ShieldedMultiSigV3.compact +368 -0
  20. package/multisig/presets/forwarder/ForwarderPrivate.compact +125 -0
  21. package/multisig/presets/forwarder/ForwarderShielded.compact +62 -0
  22. package/multisig/presets/forwarder/ForwarderUnshielded.compact +64 -0
  23. package/package.json +32 -16
  24. package/security/Allowlist.compact +101 -0
  25. package/security/Blocklist.compact +99 -0
  26. package/security/Initializable.compact +2 -2
  27. package/security/Pausable.compact +2 -2
  28. package/token/ConfidentialFungibleToken.compact +1180 -0
  29. package/token/FungibleToken.compact +24 -82
  30. package/token/MultiToken.compact +21 -80
  31. package/token/NativeShieldedToken.compact +285 -0
  32. package/token/NativeShieldedTokenCore.compact +343 -0
  33. package/token/NativeShieldedTokenFamily.compact +287 -0
  34. package/token/NonFungibleToken.compact +25 -83
  35. package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +90 -0
  36. package/token/extensions/NativeShieldedTokenDerivedNonce.compact +70 -0
  37. package/token/extensions/NativeShieldedTokenFamilySupply.compact +145 -0
  38. package/token/extensions/NativeShieldedTokenSupply.compact +142 -0
  39. package/token/extensions/NativeShieldedTokenSupplyCore.compact +163 -0
  40. package/utils/Utils.compact +86 -2
@@ -0,0 +1,233 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/ForwarderPrivate.compact)
3
+
4
+ pragma language_version >= 0.23.0;
5
+
6
+ /**
7
+ * @module ForwarderPrivate
8
+ * @description Private-parent forwarder primitives. The parent is a coin
9
+ * public key, hidden behind a `persistentHash` commitment on the ledger.
10
+ * Deposits accumulate at the contract (no atomic forward); the operator
11
+ * drains coins by presenting the `(parent, opSecret)` preimage at drain
12
+ * time. The drained coin is sent as a shielded note to the parent key, so
13
+ * the parent stays hidden on-chain. The parent is deliberately a coin public
14
+ * key and not a contract address: a shielded send to a contract publishes
15
+ * that contract's address in cleartext, which would defeat the guarantee.
16
+ *
17
+ * Knowledge of the preimage is the sole authorization gate — there is
18
+ * no signer set and no nullifier scheme. The operational secret is
19
+ * held off-chain; the contract never sees it except during a drain.
20
+ * Two forwarders bound to the same parent with different operational
21
+ * secrets produce different commitments and are unlinkable on-chain.
22
+ *
23
+ * Underscore-prefixed circuits have no access control beyond the
24
+ * preimage check inside `_drain`. State access is gated by the
25
+ * initialization pattern: every state-touching circuit asserts the
26
+ * module has been initialized via `initialize`. The pure
27
+ * `_calculateParentCommitment` helper does not access state and is
28
+ * intentionally callable without initialization.
29
+ */
30
+ module ForwarderPrivate {
31
+ import CompactStandardLibrary;
32
+ import "../utils/Utils" prefix Utils_;
33
+
34
+ // ─── State ──────────────────────────────────────────────────────
35
+
36
+ export ledger _parentCommitment: Bytes<32>;
37
+
38
+ /**
39
+ * @description Initialization flag, tracked per-module to avoid the compiler's
40
+ * shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the
41
+ * Initializable module for rationale.
42
+ */
43
+ export ledger _isInitialized: Boolean;
44
+
45
+ // ─── Init ───────────────────────────────────────────────────────
46
+
47
+ /**
48
+ * @description Initializes the forwarder with a parent commitment.
49
+ * Called once from the preset constructor. The commitment is
50
+ * immutable after init.
51
+ *
52
+ * Requirements:
53
+ *
54
+ * - Contract must not be initialized.
55
+ * - `parentCommitment` must not be the zero bytes. A zero commitment
56
+ * is the sole drain gate and would leave every deposited coin
57
+ * permanently unrecoverable (no preimage exists for `default<Bytes<32>>`
58
+ * under the domain-tagged hash).
59
+ *
60
+ * @param {Bytes<32>} parentCommitment - Domain-tagged
61
+ * `persistentHash([pad(32, "ForwarderPrivate:commitment"), parentAddr, opSecret])`
62
+ * computed off-chain by the deployer (see `_calculateParentCommitment`).
63
+ *
64
+ * @returns {[]} Empty tuple.
65
+ */
66
+ export circuit initialize(parentCommitment: Bytes<32>): [] {
67
+ assert(parentCommitment != default<Bytes<32>>, "ForwarderPrivate: zero commitment");
68
+ assertNotInitialized();
69
+ _isInitialized = true;
70
+ _parentCommitment = disclose(parentCommitment);
71
+ }
72
+
73
+ // ─── Deposit ────────────────────────────────────────────────────
74
+
75
+ /**
76
+ * @description Receives a shielded coin into the forwarder's custody.
77
+ * No ledger write — coins dwell at the contract address until drained.
78
+ *
79
+ * @circuitInfo k=13, rows=6538
80
+ *
81
+ * Requirements:
82
+ *
83
+ * - Contract must be initialized.
84
+ *
85
+ * @param {ShieldedCoinInfo} coin - The incoming shielded coin.
86
+ *
87
+ * @returns {[]} Empty tuple.
88
+ */
89
+ export circuit _deposit(coin: ShieldedCoinInfo): [] {
90
+ assertInitialized();
91
+ receiveShielded(disclose(coin));
92
+ }
93
+
94
+ // ─── Drain ──────────────────────────────────────────────────────
95
+
96
+ /**
97
+ * @description Spends a previously-deposited shielded coin to the parent,
98
+ * a coin public key. The caller proves knowledge of `(parent, opSecret)`
99
+ * matching the stored commitment; the coin is sent as a plain shielded note,
100
+ * so the parent stays hidden on-chain. If `coin.value` exceeds `value`,
101
+ * `sendShielded` routes the change back to this contract as a self-owned
102
+ * output; `_drain` returns it untouched via `result.change`. The
103
+ * implementing contract chooses what to do with it: leave it dwelling at
104
+ * the contract for a future drain, or spend it onward to a different
105
+ * recipient in the same transaction via `sendImmediateShielded`.
106
+ *
107
+ * The parent is a `ZswapCoinPublicKey`, never a `ContractAddress`: a shielded
108
+ * send to a contract publishes that contract's address in cleartext on the
109
+ * transaction (the protocol must route the coin to a named contract), which
110
+ * would defeat the private-parent guarantee. A coin public key is encrypted
111
+ * inside the Zswap output and never appears on the public transcript.
112
+ *
113
+ * @circuitInfo k=16, rows=41961
114
+ *
115
+ * Requirements:
116
+ *
117
+ * - Contract must be initialized.
118
+ * - `parent` must not be the zero key.
119
+ * - `_calculateParentCommitment(parent.bytes, opSecret) == _parentCommitment`.
120
+ * - `coin.value` must be >= `value` (enforced by `sendShielded`).
121
+ *
122
+ * @param {QualifiedShieldedCoinInfo} coin - The coin to spend.
123
+ * @param {ZswapCoinPublicKey} parent - The parent coin public key. Its 32
124
+ * bytes are the preimage to the stored commitment.
125
+ * @param {Bytes<32>} opSecret - The operational secret. Never appears
126
+ * on the public transcript.
127
+ *
128
+ * @warning **Losing the operational secret is permanent fund loss.** It
129
+ * is the sole drain authorization, with no rotation, revocation, or
130
+ * recovery path. If the operator misplaces it, every shielded coin
131
+ * accumulated at this contract is forever inaccessible, equivalent to
132
+ * losing a hot-wallet private key. Back it up offline before the first
133
+ * deposit and treat it with the same hygiene as a signing key.
134
+ *
135
+ * @param {Uint<128>} value - The amount to send.
136
+ *
137
+ * @returns {ShieldedSendResult} The result containing the sent coin
138
+ * and any change.
139
+ */
140
+ export circuit _drain(
141
+ coin: QualifiedShieldedCoinInfo,
142
+ parent: ZswapCoinPublicKey,
143
+ opSecret: Bytes<32>,
144
+ value: Uint<128>
145
+ ): ShieldedSendResult {
146
+ assertInitialized();
147
+
148
+ // Reject a zero parent before the commitment gate.
149
+ assert(!Utils_isKeyZero(parent), "ForwarderPrivate: zero parent");
150
+
151
+ // Commitment gate — the preimage is the parent key's 32 bytes.
152
+ assert(
153
+ _calculateParentCommitment(parent.bytes, opSecret) == _parentCommitment,
154
+ "ForwarderPrivate: invalid parent"
155
+ );
156
+
157
+ // Send to the parent coin public key (the `left` arm). `disclose` reveals
158
+ // nothing: a coin-public-key recipient is encrypted in the Zswap output, so
159
+ // the parent never appears on the public transcript. A contract recipient
160
+ // is intentionally unsupported — it would publish the address in cleartext.
161
+ const result = sendShielded(
162
+ disclose(coin),
163
+ left<ZswapCoinPublicKey, ContractAddress>(disclose(parent)),
164
+ disclose(value)
165
+ );
166
+
167
+ // `sendShielded` already emits any change as a fresh output owned by this
168
+ // contract (dwelling at the contract address for future drains). `_drain`
169
+ // returns it untouched: it must not re-spend the change here and then hand
170
+ // that same (now-nullified) coin back via `result.change` — that is the
171
+ // double spend a node rejects on the next drain. A caller is free to spend
172
+ // the returned change onward itself (e.g. route it to another recipient);
173
+ // the rule is only that the change must never be re-spent AND still treated
174
+ // as held.
175
+ return result;
176
+ }
177
+
178
+ // ─── Pure helpers ───────────────────────────────────────────────
179
+
180
+ /**
181
+ * @description Computes the parent commitment from `(parentAddr, opSecret)`.
182
+ * Pure circuit — used off-chain by the deployer to compute the
183
+ * constructor argument, and inside `_drain` for the preimage check.
184
+ * Callable without initialization.
185
+ *
186
+ * The first hash input is a fixed domain tag
187
+ * (`pad(32, "ForwarderPrivate:commitment")`). The tag prevents
188
+ * preimage collisions with other `persistentHash` users in the
189
+ * system that hash two `Bytes<32>` values — a colliding preimage
190
+ * crafted under a different domain cannot satisfy this commitment.
191
+ *
192
+ * @param {Bytes<32>} parentAddr - The parent address.
193
+ * @param {Bytes<32>} opSecret - The operational secret.
194
+ *
195
+ * @returns {Bytes<32>} `persistentHash([pad(32, "ForwarderPrivate:commitment"), parentAddr, opSecret])`.
196
+ */
197
+ export pure circuit _calculateParentCommitment(
198
+ parentAddr: Bytes<32>,
199
+ opSecret: Bytes<32>
200
+ ): Bytes<32> {
201
+ return persistentHash<Vector<3, Bytes<32>>>(
202
+ [pad(32, "ForwarderPrivate:commitment"), parentAddr, opSecret]
203
+ );
204
+ }
205
+
206
+ // ─── Init guards ────────────────────────────────────────────────
207
+
208
+ /**
209
+ * @description Asserts that the contract has been initialized, throwing an error if not.
210
+ *
211
+ * Requirements:
212
+ *
213
+ * - Contract must be initialized.
214
+ *
215
+ * @return {[]} - Empty tuple.
216
+ */
217
+ circuit assertInitialized(): [] {
218
+ assert(_isInitialized, "ForwarderPrivate: contract not initialized");
219
+ }
220
+
221
+ /**
222
+ * @description Asserts that the contract has not been initialized, throwing an error if it has.
223
+ *
224
+ * Requirements:
225
+ *
226
+ * - Contract must not be initialized.
227
+ *
228
+ * @return {[]} - Empty tuple.
229
+ */
230
+ circuit assertNotInitialized(): [] {
231
+ assert(!_isInitialized, "ForwarderPrivate: contract already initialized");
232
+ }
233
+ }
@@ -0,0 +1,149 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/ForwarderShielded.compact)
3
+
4
+ pragma language_version >= 0.23.0;
5
+
6
+ /**
7
+ * @module ForwarderShielded
8
+ * @description Public-parent forwarder for shielded coins. Provides an
9
+ * atomic forward pattern: receive a shielded coin and immediately send
10
+ * it to the configured parent recipient.
11
+ *
12
+ * The parent is stored generically as
13
+ * `Either<ZswapCoinPublicKey, ContractAddress>` so the ledger layout is
14
+ * forward-compatible, but `initialize` accepts only a `ZswapCoinPublicKey`
15
+ * today and stores it in the `left` arm. A contract address is the
16
+ * unsupported arm: an atomic forward delivers the coin in the same
17
+ * transaction as the deposit, and a shielded send to a contract is only
18
+ * valid if that contract claims the output in the same transaction. The
19
+ * only contract executing in `_deposit` is this forwarder, so a
20
+ * third-party contract parent would never claim the output and the
21
+ * deposit would be rejected by the node. A coin public key needs no
22
+ * in-tx claim, so it is the only recipient an atomic forward can reach.
23
+ *
24
+ * Contract-address support is reserved for a future circuit upgrade (via
25
+ * the Contract Maintenance Authority) once cross-contract UTXO delivery
26
+ * exists. Because the ledger already stores the generic `Either`, that
27
+ * upgrade changes only the circuits (verifier keys), not the state
28
+ * layout — no state migration is required.
29
+ *
30
+ * Underscore-prefixed circuits have no access control. The forwarder is
31
+ * intentionally permissionless — the recipient is fixed at init, so any
32
+ * caller may deposit without compromising the parent. State access is
33
+ * gated by a per-module `_isInitialized` flag: every circuit asserts the
34
+ * contract has been initialized via `initialize`.
35
+ *
36
+ * The parent is set once at init and the module exposes no setter, so a
37
+ * preset that omits one keeps the parent fixed. A consuming contract
38
+ * that needs to change the parent may add its own setter circuit.
39
+ *
40
+ * @notice The deployer must set a parent that can spend the forwarded
41
+ * coins. Forwarding to a key no one controls strands every deposit
42
+ * permanently. The zero-key guard rejects only the all-zero key, not an
43
+ * otherwise-unspendable one.
44
+ */
45
+ module ForwarderShielded {
46
+ import CompactStandardLibrary;
47
+ import "../utils/Utils" prefix Utils_;
48
+
49
+ // ─── State ──────────────────────────────────────────────────────
50
+
51
+ /**
52
+ * @description The parent recipient, stored generically as
53
+ * `Either<ZswapCoinPublicKey, ContractAddress>` for forward compatibility.
54
+ * `initialize` only ever writes the `left` (coin public key) arm today. See
55
+ * the module header.
56
+ */
57
+ export ledger _parent: Either<ZswapCoinPublicKey, ContractAddress>;
58
+
59
+ /**
60
+ * @description Initialization flag, tracked per-module to avoid the compiler's
61
+ * shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the
62
+ * Initializable module for rationale.
63
+ */
64
+ export ledger _isInitialized: Boolean;
65
+
66
+ // ─── Init ───────────────────────────────────────────────────────
67
+
68
+ /**
69
+ * @description Initializes the forwarder with a parent recipient.
70
+ * Called once from the preset constructor. The coin public key is
71
+ * stored in the `left` arm of the generic `_parent` ledger field.
72
+ *
73
+ * Requirements:
74
+ *
75
+ * - Contract must not be initialized.
76
+ * - `parent` must not be the zero key. A zero parent would forward
77
+ * every deposit to an unspendable key with no recovery path.
78
+ *
79
+ * @param {ZswapCoinPublicKey} parent - The coin public key that
80
+ * receives every forwarded coin. The parameter is deliberately narrower
81
+ * than the `_parent` ledger type: only a coin public key can be
82
+ * delivered to by an atomic forward today (see the module header).
83
+ *
84
+ * @notice Beyond the zero-key check, the caller must ensure `parent` can
85
+ * spend forwarded coins. An unspendable parent strands every deposit with
86
+ * no recovery path.
87
+ *
88
+ * @returns {[]} Empty tuple.
89
+ */
90
+ export circuit initialize(parent: ZswapCoinPublicKey): [] {
91
+ assert(!Utils_isKeyZero(parent), "ForwarderShielded: zero parent");
92
+ assertNotInitialized();
93
+ _isInitialized = true;
94
+ _parent = left<ZswapCoinPublicKey, ContractAddress>(disclose(parent));
95
+ }
96
+
97
+ // ─── Deposit ────────────────────────────────────────────────────
98
+
99
+ /**
100
+ * @description Receives a shielded coin and atomically forwards it to
101
+ * `_parent`. The coin is claimed at the protocol level via
102
+ * `receiveShielded`, then immediately re-sent via
103
+ * `sendImmediateShielded` to the stored parent recipient (always the
104
+ * `left` / coin-public-key arm today).
105
+ *
106
+ * @circuitInfo k=15, rows=18573
107
+ *
108
+ * Requirements:
109
+ *
110
+ * - Contract must be initialized.
111
+ *
112
+ * @param {ShieldedCoinInfo} coin - The incoming shielded coin.
113
+ *
114
+ * @returns {[]} Empty tuple.
115
+ */
116
+ export circuit _deposit(coin: ShieldedCoinInfo): [] {
117
+ assertInitialized();
118
+ receiveShielded(disclose(coin));
119
+ sendImmediateShielded(disclose(coin), _parent, disclose(coin.value));
120
+ }
121
+
122
+ // ─── Init guards ────────────────────────────────────────────────
123
+
124
+ /**
125
+ * @description Asserts that the contract has been initialized, throwing an error if not.
126
+ *
127
+ * Requirements:
128
+ *
129
+ * - Contract must be initialized.
130
+ *
131
+ * @returns {[]} Empty tuple.
132
+ */
133
+ circuit assertInitialized(): [] {
134
+ assert(_isInitialized, "ForwarderShielded: contract not initialized");
135
+ }
136
+
137
+ /**
138
+ * @description Asserts that the contract has not been initialized, throwing an error if it has.
139
+ *
140
+ * Requirements:
141
+ *
142
+ * - Contract must not be initialized.
143
+ *
144
+ * @returns {[]} Empty tuple.
145
+ */
146
+ circuit assertNotInitialized(): [] {
147
+ assert(!_isInitialized, "ForwarderShielded: contract already initialized");
148
+ }
149
+ }
@@ -0,0 +1,152 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/ForwarderUnshielded.compact)
3
+
4
+ pragma language_version >= 0.23.0;
5
+
6
+ /**
7
+ * @module ForwarderUnshielded
8
+ * @description Public-parent forwarder for unshielded coins. Provides an
9
+ * atomic forward pattern: receive an unshielded amount of a given color
10
+ * and immediately send it to the configured parent recipient.
11
+ *
12
+ * The parent is stored generically as
13
+ * `Either<ContractAddress, UserAddress>` so the ledger layout is
14
+ * forward-compatible, but `initialize` accepts only a `UserAddress`
15
+ * today and stores it in the `right` arm. A contract address is the
16
+ * unsupported arm: an atomic forward delivers the amount in the same
17
+ * transaction as the deposit, and an unshielded send to a contract is
18
+ * only valid if that contract claims the amount in the same transaction
19
+ * (the same `receiveUnshielded` step this forwarder runs to accept the
20
+ * deposit). The only contract executing in `_deposit` is this forwarder,
21
+ * so a third-party contract parent would never claim the output and the
22
+ * deposit would be rejected by the node. A user address needs no in-tx
23
+ * claim, so it is the only recipient an atomic forward can reach.
24
+ *
25
+ * Contract-address support is reserved for a future circuit upgrade (via
26
+ * the Contract Maintenance Authority) once cross-contract UTXO delivery
27
+ * exists. Because the ledger already stores the generic `Either`, that
28
+ * upgrade changes only the circuits (verifier keys), not the state
29
+ * layout — no state migration is required.
30
+ *
31
+ * Unshielded transfers are publicly visible on the chain: depositor,
32
+ * recipient, color, and amount all appear on the public transcript.
33
+ *
34
+ * Underscore-prefixed circuits have no access control. The forwarder is
35
+ * intentionally permissionless — the recipient is fixed at init, so any
36
+ * caller may deposit without compromising the parent. State access is
37
+ * gated by a per-module `_isInitialized` flag: every circuit asserts the
38
+ * contract has been initialized via `initialize`.
39
+ *
40
+ * The parent is set once at init and the module exposes no setter, so a
41
+ * preset that omits one keeps the parent fixed. A consuming contract
42
+ * that needs to change the parent may add its own setter circuit.
43
+ *
44
+ * @notice The deployer must set a parent that can spend the forwarded
45
+ * amounts. Forwarding to an address no one controls strands every deposit
46
+ * permanently. The zero-address guard rejects only the all-zero address,
47
+ * not an otherwise-unspendable one.
48
+ */
49
+ module ForwarderUnshielded {
50
+ import CompactStandardLibrary;
51
+
52
+ // ─── State ──────────────────────────────────────────────────────
53
+
54
+ /**
55
+ * @description The parent recipient, stored generically as
56
+ * `Either<ContractAddress, UserAddress>` for forward compatibility.
57
+ * `initialize` only ever writes the `right` (user address) arm today. See
58
+ * the module header.
59
+ */
60
+ export ledger _parent: Either<ContractAddress, UserAddress>;
61
+
62
+ /**
63
+ * @description Initialization flag, tracked per-module to avoid the compiler's
64
+ * shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the
65
+ * Initializable module for rationale.
66
+ */
67
+ export ledger _isInitialized: Boolean;
68
+
69
+ // ─── Init ───────────────────────────────────────────────────────
70
+
71
+ /**
72
+ * @description Initializes the forwarder with a parent recipient.
73
+ * Called once from the preset constructor. The user address is stored
74
+ * in the `right` arm of the generic `_parent` ledger field.
75
+ *
76
+ * Requirements:
77
+ *
78
+ * - Contract must not be initialized.
79
+ * - `parent` must not be the zero address. A zero parent would forward
80
+ * every deposit to an unspendable address with no recovery path.
81
+ *
82
+ * @param {UserAddress} parent - The user address that receives every
83
+ * forwarded amount. The parameter is deliberately narrower than the
84
+ * `_parent` ledger type: only a user address can be delivered to by an
85
+ * atomic forward today (see the module header).
86
+ *
87
+ * @notice Beyond the zero-address check, the caller must ensure `parent`
88
+ * can spend forwarded amounts. An unspendable parent strands every
89
+ * deposit with no recovery path.
90
+ *
91
+ * @returns {[]} Empty tuple.
92
+ */
93
+ export circuit initialize(parent: UserAddress): [] {
94
+ const isZero = default<UserAddress> == parent;
95
+ assert(!isZero, "ForwarderUnshielded: zero parent");
96
+ assertNotInitialized();
97
+ _isInitialized = true;
98
+ _parent = right<ContractAddress, UserAddress>(disclose(parent));
99
+ }
100
+
101
+ // ─── Deposit ────────────────────────────────────────────────────
102
+
103
+ /**
104
+ * @description Receives an unshielded amount of `color` and atomically
105
+ * forwards it to `_parent` via `sendUnshielded` (always the `right` /
106
+ * user-address arm today).
107
+ *
108
+ * @circuitInfo k=9, rows=436
109
+ *
110
+ * Requirements:
111
+ *
112
+ * - Contract must be initialized.
113
+ *
114
+ * @param {Bytes<32>} color - The token color.
115
+ * @param {Uint<128>} amount - The amount to deposit.
116
+ *
117
+ * @returns {[]} Empty tuple.
118
+ */
119
+ export circuit _deposit(color: Bytes<32>, amount: Uint<128>): [] {
120
+ assertInitialized();
121
+ receiveUnshielded(disclose(color), disclose(amount));
122
+ sendUnshielded(disclose(color), disclose(amount), _parent);
123
+ }
124
+
125
+ // ─── Init guards ────────────────────────────────────────────────
126
+
127
+ /**
128
+ * @description Asserts that the contract has been initialized, throwing an error if not.
129
+ *
130
+ * Requirements:
131
+ *
132
+ * - Contract must be initialized.
133
+ *
134
+ * @returns {[]} Empty tuple.
135
+ */
136
+ circuit assertInitialized(): [] {
137
+ assert(_isInitialized, "ForwarderUnshielded: contract not initialized");
138
+ }
139
+
140
+ /**
141
+ * @description Asserts that the contract has not been initialized, throwing an error if it has.
142
+ *
143
+ * Requirements:
144
+ *
145
+ * - Contract must not be initialized.
146
+ *
147
+ * @returns {[]} Empty tuple.
148
+ */
149
+ circuit assertNotInitialized(): [] {
150
+ assert(!_isInitialized, "ForwarderUnshielded: contract already initialized");
151
+ }
152
+ }