@openzeppelin/compact-contracts 0.2.0 → 0.3.0-alpha

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 (33) hide show
  1. package/README.md +3 -3
  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/ElGamal.compact +473 -0
  7. package/multisig/ForwarderPrivate.compact +229 -0
  8. package/multisig/ForwarderShielded.compact +149 -0
  9. package/multisig/ForwarderUnshielded.compact +152 -0
  10. package/multisig/ProposalManager.compact +328 -0
  11. package/multisig/ShieldedTreasury.compact +201 -0
  12. package/multisig/ShieldedTreasuryStateless.compact +76 -0
  13. package/multisig/Signer.compact +312 -0
  14. package/multisig/SignerManager.compact +205 -0
  15. package/multisig/UnshieldedTreasury.compact +114 -0
  16. package/multisig/presets/ShieldedMultiSig.compact +236 -0
  17. package/multisig/presets/ShieldedMultiSigV2.compact +280 -0
  18. package/multisig/presets/ShieldedMultiSigV3.compact +368 -0
  19. package/multisig/presets/forwarder/ForwarderPrivate.compact +125 -0
  20. package/multisig/presets/forwarder/ForwarderShielded.compact +62 -0
  21. package/multisig/presets/forwarder/ForwarderUnshielded.compact +64 -0
  22. package/package.json +9 -5
  23. package/security/Allowlist.compact +101 -0
  24. package/security/Blocklist.compact +99 -0
  25. package/security/Initializable.compact +2 -2
  26. package/security/Pausable.compact +2 -2
  27. package/token/FungibleToken.compact +24 -82
  28. package/token/MultiToken.compact +21 -80
  29. package/token/NativeShieldedToken.compact +285 -0
  30. package/token/NativeShieldedTokenCore.compact +343 -0
  31. package/token/NativeShieldedTokenFamily.compact +287 -0
  32. package/token/NonFungibleToken.compact +25 -83
  33. package/utils/Utils.compact +86 -2
@@ -0,0 +1,368 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.0.1-alpha.1 (multisig/presets/ShieldedMultiSigV3.compact)
3
+
4
+ pragma language_version >= 0.23.0;
5
+
6
+ /**
7
+ * @title ShieldedMultiSigV3
8
+ * @description Privacy-preserving multisig token contract for tokenised
9
+ * deposits. Both minting and burning require threshold ECDSA
10
+ * authorization. No single party can unilaterally create or destroy tokens.
11
+ *
12
+ * Designed with the following features:
13
+ * - Supply only enters through authorized mint (no open deposit)
14
+ * - Supply only exits through authorized burn (no arbitrary transfers)
15
+ * - Non-transferability enforced by absence of transfer/execute circuits
16
+ * - Change coins from partial burns handled by the transaction layer
17
+ * - Operator discovers coins via ZswapOutput events from the indexer
18
+ *
19
+ * Uses Midnight's native shielded token primitives:
20
+ *
21
+ * - `mint` creates a new UTXO via `mintShieldedToken`, addressed to the
22
+ * contract. No external coin input; supply is produced on-chain.
23
+ * - `burn` consumes a UTXO via `sendShielded` to `burnAddress()`. Only
24
+ * coins of this contract's token type can be burned. Change is handled
25
+ * automatically by the transaction layer.
26
+ *
27
+ * Signer identities are stored as commitments: hashes of ECDSA public keys
28
+ * combined with an instance salt and domain separator. A counter
29
+ * provides replay protection, and also feeds `evolveNonce` to produce
30
+ * unique coin nonces on each mint.
31
+ *
32
+ * Operation domain prefixes ("multisig:mint:" / "multisig:burn:") in the message
33
+ * hash prevent signatures for one operation type from being replayed as
34
+ * the other.
35
+ *
36
+ * @notice ECDSA verification is stubbed. Replace `stubVerifySignature` with
37
+ * `ecdsaVerify`, and `persistentHash` with `keccak256`, once the Compact
38
+ * ECDSA and Keccak primitives are available.
39
+ */
40
+
41
+ import CompactStandardLibrary;
42
+
43
+ import "../Signer"<Bytes<32>> prefix Signer_;
44
+ import "../../utils/Utils" prefix Utils_;
45
+ // For testing
46
+ export { ZswapCoinPublicKey };
47
+
48
+ // ─── Types ──────────────────────────────────────────────────────
49
+
50
+ /**
51
+ * @description Accumulator for fold-based signature verification.
52
+ * Threads the valid count, previous commitment (for duplicate
53
+ * detection), and message hash through each iteration.
54
+ */
55
+ struct VerificationState {
56
+ validCount: Uint<8>,
57
+ prevCommitment: Bytes<32>,
58
+ msgHash: Bytes<32>
59
+ }
60
+
61
+ /**
62
+ * @description Input to persistentHash for computing signer commitments.
63
+ * Combines the ECDSA public key with an instance-specific salt and
64
+ * domain separator to produce a unique, unlinkable commitment.
65
+ */
66
+ struct SignerCommitmentInput {
67
+ pk: Bytes<64>,
68
+ salt: Bytes<32>,
69
+ domain: Bytes<32>
70
+ }
71
+
72
+ // ─── State ──────────────────────────────────────────────────────
73
+
74
+ export ledger _counter: Counter;
75
+ export ledger _coinNonce: Bytes<32>;
76
+ export ledger _instanceSalt: Bytes<32>;
77
+ export sealed ledger _tokenDomain: Bytes<32>;
78
+
79
+ // ─── Constructor ────────────────────────────────────────────────
80
+
81
+ /**
82
+ * @description Deploys the contract with 3 signer commitments and
83
+ * a threshold of 2.
84
+ *
85
+ * Each commitment is computed off-chain as:
86
+ * `persistentHash(SignerCommitmentInput { pk, instanceSalt, domain })`
87
+ * where domain is `pad(32, "multisig:signer:")`.
88
+ *
89
+ * `tokenDomain` is used with `kernel.self()` to derive the token color
90
+ * via `tokenType(_tokenDomain, kernel.self())`. Only coins of this color
91
+ * can be burned through this contract.
92
+ *
93
+ * `initCoinNonce` seeds the `evolveNonce` chain used to produce unique
94
+ * mint nonces. It must be cryptographically random.
95
+ *
96
+ * Requirements:
97
+ *
98
+ * - `signerCommitments` must not contain duplicates.
99
+ * - `instanceSalt` and `initCoinNonce` should be cryptographically random.
100
+ *
101
+ * @param {Bytes<32>} instanceSalt - Random salt for signer commitment derivation.
102
+ * @param {Bytes<32>} initCoinNonce - Initial coin nonce seed.
103
+ * @param {Bytes<32>} tokenDomain - Domain string used to derive this contract's token color.
104
+ * @param {Vector<3, Bytes<32>>} signerCommitments - Hashed signer identities.
105
+ */
106
+ constructor(
107
+ instanceSalt: Bytes<32>,
108
+ initCoinNonce: Bytes<32>,
109
+ tokenDomain: Bytes<32>,
110
+ signerCommitments: Vector<3, Bytes<32>>,
111
+ ) {
112
+ _instanceSalt = disclose(instanceSalt);
113
+ _coinNonce = disclose(initCoinNonce);
114
+ _tokenDomain = disclose(tokenDomain);
115
+ Signer_initialize<3>(signerCommitments, 2);
116
+ }
117
+
118
+ // ─── Mint ───────────────────────────────────────────────────────
119
+
120
+ /**
121
+ * @description Mints a new shielded coin addressed to the specified
122
+ * recipient, authorized by threshold signatures.
123
+ *
124
+ * Creates a new UTXO of this contract's token type via `mintShieldedToken`,
125
+ * addressed to the provided recipient. No external coin input is required;
126
+ * supply is produced on-chain.
127
+ *
128
+ * The message hash commits to:
129
+ * - operation domain ("multisig:mint:" for burn op replay protection)
130
+ * - contract address (cross-instance replay protection)
131
+ * - recipient (redirection protection)
132
+ * - counter value (replay protection)
133
+ * - amount
134
+ *
135
+ * Coin nonce uniqueness is guaranteed by `evolveNonce(_counter, _coinNonce)`
136
+ * after the counter has been incremented, binding each mint's nonce to a
137
+ * distinct counter value.
138
+ *
139
+ * @notice Replace `persistentHash` with `keccak256` and `stubVerifySignature`
140
+ * with `ecdsaVerify` once the Compact ECDSA and Keccak primitives are
141
+ * available, to match the custodian's HSM signing format.
142
+ *
143
+ * Requirements:
144
+ *
145
+ * - Both public keys must hash to registered signer commitments.
146
+ * - Both signatures must be valid over the mint message hash.
147
+ * - Signers must not be duplicates.
148
+ * - Threshold must be met.
149
+ *
150
+ * @param {Uint<64>} amount - The token amount to mint.
151
+ * @param {Either<ZswapCoinPublicKey, ContractAddress>} recipient - The address to receive the minted tokens.
152
+ * @param {Vector<2, Bytes<64>>} pubkeys - ECDSA public keys of approving signers.
153
+ * @param {Vector<2, Bytes<64>>} signatures - ECDSA signatures over the mint hash.
154
+ */
155
+ export circuit mint(
156
+ amount: Uint<64>,
157
+ recipient: Either<ZswapCoinPublicKey, ContractAddress>,
158
+ pubkeys: Vector<2, Bytes<64>>,
159
+ signatures: Vector<2, Bytes<64>>
160
+ ): [] {
161
+ const opNonce = _counter;
162
+ _counter.increment(1);
163
+
164
+ // Canonicalize recipient to ensure garbage values don't sully the hash
165
+ const canonRecipient = Utils_canonicalize<ZswapCoinPublicKey, ContractAddress>(recipient);
166
+ const recipientHash = persistentHash<Either<ZswapCoinPublicKey, ContractAddress>>(canonRecipient);
167
+
168
+ const msgHash = persistentHash<Vector<5, Bytes<32>>>([
169
+ pad(32, "multisig:mint:"),
170
+ kernel.self().bytes,
171
+ recipientHash,
172
+ opNonce as Bytes<32>,
173
+ amount as Bytes<32>
174
+ ]);
175
+
176
+ const initialState = VerificationState {
177
+ validCount: 0 as Uint<8>,
178
+ prevCommitment: pad(32, ""),
179
+ msgHash: msgHash
180
+ };
181
+
182
+ const finalState = fold(verifySignature, initialState, pubkeys, signatures);
183
+ // Thresh check not needed as it will always be 2
184
+ // Leaving it as defense-in-depth
185
+ Signer_assertThresholdMet(finalState.validCount);
186
+
187
+ _coinNonce = evolveNonce(_counter, _coinNonce);
188
+ mintShieldedToken(_tokenDomain, disclose(amount), _coinNonce, disclose(canonRecipient));
189
+ }
190
+
191
+ // ─── Burn ───────────────────────────────────────────────────────
192
+
193
+ /**
194
+ * @description Burns a shielded coin of this contract's token type,
195
+ * authorized by threshold signatures.
196
+ *
197
+ * Sends the specified amount of the supplied coin to `burnAddress()` via
198
+ * `sendShielded`. The nullifier is submitted on-chain, permanently marking
199
+ * the UTXO as spent.
200
+ *
201
+ * Change handling is automatic: if `amount < coin.value`, the transaction
202
+ * layer creates a change output addressed back to the contract. The operator
203
+ * discovers this change coin via `nextZswapLocalState.outputs` in the
204
+ * transaction's private result, then discovers its mt_index from the
205
+ * indexer events using the standard flow. No contract-level change logic
206
+ * is required.
207
+ *
208
+ * Only coins of this contract's token type can be burned. The operator
209
+ * supplies the `QualifiedShieldedCoinInfo` from the off-chain UTXO pool.
210
+ *
211
+ * The "multisig:burn:" domain prefix ensures burn signatures cannot be replayed
212
+ * as mint operations for the same parameters.
213
+ *
214
+ * @notice Replace `persistentHash` with `keccak256` and `stubVerifySignature`
215
+ * with `ecdsaVerify` once the Compact ECDSA and Keccak primitives are
216
+ * available, to match the custodian's HSM signing format.
217
+ *
218
+ * Requirements:
219
+ *
220
+ * - Both public keys must hash to registered signer commitments.
221
+ * - Both signatures must be valid over the burn message hash.
222
+ * - Signers must not be duplicates.
223
+ * - Threshold must be met.
224
+ * - coin.color must equal tokenType(_tokenDomain, kernel.self()).
225
+ * - coin.value must be >= amount.
226
+ *
227
+ * @param {QualifiedShieldedCoinInfo} coin - The coin to burn (from operator's UTXO pool).
228
+ * @param {Uint<64>} amount - The token amount to burn.
229
+ * @param {Vector<2, Bytes<64>>} pubkeys - ECDSA public keys of approving signers.
230
+ * @param {Vector<2, Bytes<64>>} signatures - ECDSA signatures over the burn hash.
231
+ */
232
+ export circuit burn(
233
+ coin: QualifiedShieldedCoinInfo,
234
+ amount: Uint<64>,
235
+ pubkeys: Vector<2, Bytes<64>>,
236
+ signatures: Vector<2, Bytes<64>>
237
+ ): [] {
238
+ const opNonce = _counter;
239
+ _counter.increment(1);
240
+
241
+ const msgHash = persistentHash<Vector<4, Bytes<32>>>([
242
+ pad(32, "multisig:burn:"),
243
+ kernel.self().bytes,
244
+ opNonce as Bytes<32>,
245
+ amount as Bytes<32>
246
+ ]);
247
+
248
+ const initialState = VerificationState {
249
+ validCount: 0 as Uint<8>,
250
+ prevCommitment: pad(32, ""),
251
+ msgHash: msgHash
252
+ };
253
+
254
+ const finalState = fold(verifySignature, initialState, pubkeys, signatures);
255
+ // Thresh check not needed as it will always be 2
256
+ // Leaving it as defense-in-depth
257
+ Signer_assertThresholdMet(finalState.validCount);
258
+
259
+ assert(coin.color == tokenType(_tokenDomain, kernel.self()), "Multisig: coin not from this contract");
260
+ assert(coin.value >= amount, "Multisig: insufficient coin value");
261
+
262
+ sendShielded(disclose(coin), shieldedBurnAddress(), disclose(amount));
263
+ }
264
+
265
+ // ─── Signature Verification ─────────────────────────────────────
266
+
267
+ /**
268
+ * @description Fold callback. Verifies one signer's approval.
269
+ *
270
+ * Computes the signer's commitment from their public key and the instance
271
+ * salt, checks for duplicates against the previous commitment, verifies
272
+ * registry membership, and validates the ECDSA signature.
273
+ *
274
+ * @notice Circuit signatures are fixed at Vector<2, ...>, so this contract
275
+ * supports only a fixed 2-of-3 configuration. Supporting more signatures
276
+ * (e.g. 3-of-N) requires a separate variant with a larger vector and a
277
+ * different duplicate-detection mechanism (sorted commitments or bitmap).
278
+ *
279
+ * @param {VerificationState} state - Accumulator threaded through fold.
280
+ * @param {Bytes<64>} pubkey - The signer's ECDSA public key.
281
+ * @param {Bytes<64>} signature - The signer's signature over msgHash.
282
+ * @returns {VerificationState} Updated accumulator.
283
+ */
284
+ circuit verifySignature(
285
+ state: VerificationState,
286
+ pubkey: Bytes<64>,
287
+ signature: Bytes<64>
288
+ ): VerificationState {
289
+ const commitment = _calculateSignerId(pubkey, _instanceSalt);
290
+
291
+ // Duplicate detection — sufficient for 2 signers only
292
+ assert(commitment != state.prevCommitment, "Multisig: duplicate signer");
293
+
294
+ Signer_assertSigner(commitment);
295
+
296
+ // TODO: Replace with ecdsaVerify + keccak256 when primitives are available
297
+ assert(stubVerifySignature(pubkey, state.msgHash, signature), "Multisig: invalid signature");
298
+
299
+ return VerificationState {
300
+ validCount: state.validCount + 1 as Uint<8>,
301
+ prevCommitment: commitment,
302
+ msgHash: state.msgHash
303
+ };
304
+ }
305
+
306
+ /**
307
+ * @description Computes a signer commitment from an ECDSA public key.
308
+ *
309
+ * The commitment is persistentHash(pk, salt, domain) where:
310
+ * - pk: the signer's ECDSA public key (64 bytes)
311
+ * - salt: instance-specific random value (prevents cross-contract correlation)
312
+ * - domain: "multisig:signer:" (domain separation)
313
+ *
314
+ * Pure circuit — callable off-chain by the deployer to compute
315
+ * commitments for the constructor.
316
+ *
317
+ * @param {Bytes<64>} pk - The ECDSA public key.
318
+ * @param {Bytes<32>} salt - The instance salt.
319
+ * @returns {Bytes<32>} The signer commitment.
320
+ */
321
+ export pure circuit _calculateSignerId(
322
+ pk: Bytes<64>,
323
+ salt: Bytes<32>
324
+ ): Bytes<32> {
325
+ return persistentHash<SignerCommitmentInput>(SignerCommitmentInput {
326
+ pk: pk,
327
+ salt: salt,
328
+ domain: pad(32, "multisig:signer:")
329
+ });
330
+ }
331
+
332
+ /**
333
+ * @description Stub for ECDSA signature verification.
334
+ * Always returns true. MUST be replaced before any non-test deployment.
335
+ */
336
+ circuit stubVerifySignature(
337
+ pubkey: Bytes<64>,
338
+ msgHash: Bytes<32>,
339
+ signature: Bytes<64>
340
+ ): Boolean {
341
+ return true;
342
+ }
343
+
344
+ // ─── View ───────────────────────────────────────────────────────
345
+
346
+ export circuit getNonce(): Uint<64> {
347
+ return _counter;
348
+ }
349
+
350
+ export circuit getTokenDomain(): Bytes<32> {
351
+ return _tokenDomain;
352
+ }
353
+
354
+ export circuit getTokenType(): Bytes<32> {
355
+ return tokenType(_tokenDomain, kernel.self());
356
+ }
357
+
358
+ export circuit getSignerCount(): Uint<8> {
359
+ return Signer_getSignerCount();
360
+ }
361
+
362
+ export circuit getThreshold(): Uint<8> {
363
+ return Signer_getThreshold();
364
+ }
365
+
366
+ export circuit isSigner(commitment: Bytes<32>): Boolean {
367
+ return Signer_isSigner(commitment);
368
+ }
@@ -0,0 +1,125 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/presets/forwarder/ForwarderPrivate.compact)
3
+
4
+ pragma language_version >= 0.23.0;
5
+
6
+ /**
7
+ * @title ForwarderPrivate
8
+ * @description Private-parent forwarder. The parent address is hidden
9
+ * behind a `persistentHash` commitment on the ledger. Coins dwell at
10
+ * the contract address after deposit; the operator drains them later
11
+ * by presenting the `(parentAddr, opSecret)` preimage at drain time.
12
+ *
13
+ * Knowledge of the preimage is the sole authorization gate. The
14
+ * operational secret is held off-chain by the deployer; losing it is
15
+ * equivalent to loss of a hot-wallet key. Two forwarders bound to the
16
+ * same parent with different operational secrets produce different
17
+ * commitments and are unlinkable on-chain.
18
+ *
19
+ * @notice Each forwarder is bound to a single parent at deploy. To
20
+ * change the parent, deploy a new forwarder; the old one remains
21
+ * functional for outstanding coins until drained.
22
+ */
23
+
24
+ import CompactStandardLibrary;
25
+ import "../../ForwarderPrivate" prefix ForwarderPrivate_;
26
+
27
+ export { ShieldedCoinInfo, QualifiedShieldedCoinInfo, ShieldedSendResult, ZswapCoinPublicKey };
28
+
29
+ /**
30
+ * @description Deploys the forwarder bound to a specific parent
31
+ * commitment. The deployer computes the commitment off-chain as
32
+ * `calculateParentCommitment(parentAddr, opSecret)` and passes it here.
33
+ *
34
+ * @param {Bytes<32>} parentCommitment - The commitment to the
35
+ * `(parentAddr, opSecret)` pair that the operator will present at drain.
36
+ */
37
+ constructor(parentCommitment: Bytes<32>) {
38
+ ForwarderPrivate_initialize(parentCommitment);
39
+ }
40
+
41
+ /**
42
+ * @description Receives a shielded coin into the forwarder's custody.
43
+ * No ledger write — the coin sits at the contract address until drained.
44
+ *
45
+ * @param {ShieldedCoinInfo} coin - The incoming shielded coin.
46
+ *
47
+ * @returns {[]} Empty tuple.
48
+ */
49
+ export circuit deposit(coin: ShieldedCoinInfo): [] {
50
+ ForwarderPrivate__deposit(coin);
51
+ }
52
+
53
+ /**
54
+ * @description Spends a previously-deposited shielded coin to the parent,
55
+ * a coin public key. The caller proves knowledge of `(parent, opSecret)`
56
+ * matching the stored commitment; the coin is sent as a shielded note, so the
57
+ * parent stays hidden on-chain. If the input coin's value exceeds `value`,
58
+ * change is re-emitted back to the contract for future drains.
59
+ *
60
+ * The parent is a `ZswapCoinPublicKey`, never a `ContractAddress`: a shielded
61
+ * send to a contract publishes that contract's address in cleartext, which
62
+ * would defeat the private-parent guarantee.
63
+ *
64
+ * Requirements:
65
+ *
66
+ * - `parent` must not be the zero key.
67
+ * - `calculateParentCommitment(parent.bytes, opSecret)` must equal the stored
68
+ * `_parentCommitment`.
69
+ * - `coin.value` must be >= `value` (enforced by `sendShielded`).
70
+ *
71
+ * @param {QualifiedShieldedCoinInfo} coin - The coin to spend.
72
+ * @param {ZswapCoinPublicKey} parent - The parent coin public key. Its 32
73
+ * bytes are the preimage to the stored commitment.
74
+ * @param {Bytes<32>} opSecret - The operational secret. Never appears
75
+ * on the public transcript.
76
+ *
77
+ * @warning **Losing the operational secret is permanent fund loss.** It
78
+ * is the sole drain authorization. No rotation, revocation, or recovery
79
+ * path exists. If the operator loses it, every shielded coin accumulated
80
+ * at this contract becomes inaccessible. Back it up offline before the
81
+ * first deposit.
82
+ *
83
+ * @param {Uint<128>} value - The amount to send.
84
+ *
85
+ * @returns {ShieldedSendResult} The result containing the sent coin and
86
+ * any change.
87
+ */
88
+ export circuit drain(
89
+ coin: QualifiedShieldedCoinInfo,
90
+ parent: ZswapCoinPublicKey,
91
+ opSecret: Bytes<32>,
92
+ value: Uint<128>
93
+ ): ShieldedSendResult {
94
+ return ForwarderPrivate__drain(coin, parent, opSecret, value);
95
+ }
96
+
97
+ /**
98
+ * @description Returns the stored parent commitment.
99
+ *
100
+ * @returns {Bytes<32>} The commitment set at deploy.
101
+ */
102
+ export circuit getParentCommitment(): Bytes<32> {
103
+ return ForwarderPrivate__parentCommitment;
104
+ }
105
+
106
+ /**
107
+ * @description Computes the parent commitment from a `(parentAddr, opSecret)`
108
+ * pair. Pure circuit — used off-chain by the deployer to compute the
109
+ * constructor argument, and inside `drain` for the preimage check.
110
+ *
111
+ * The commitment is domain-tagged
112
+ * (`pad(32, "ForwarderPrivate:commitment")`) to prevent preimage
113
+ * collisions with other `persistentHash` users in the system.
114
+ *
115
+ * @param {Bytes<32>} parentAddr - The parent address.
116
+ * @param {Bytes<32>} opSecret - The operational secret.
117
+ *
118
+ * @returns {Bytes<32>} The commitment `persistentHash([pad(32, "ForwarderPrivate:commitment"), parentAddr, opSecret])`.
119
+ */
120
+ export pure circuit calculateParentCommitment(
121
+ parentAddr: Bytes<32>,
122
+ opSecret: Bytes<32>
123
+ ): Bytes<32> {
124
+ return ForwarderPrivate__calculateParentCommitment(parentAddr, opSecret);
125
+ }
@@ -0,0 +1,62 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/presets/forwarder/ForwarderShielded.compact)
3
+
4
+ pragma language_version >= 0.23.0;
5
+
6
+ /**
7
+ * @title ForwarderShielded
8
+ * @description Public-parent forwarder for shielded coins. Receives a
9
+ * shielded coin and atomically forwards it to the configured parent
10
+ * recipient, a coin public key.
11
+ *
12
+ * The parent recipient is set at deploy time. This preset exposes no
13
+ * setter, so the parent is fixed for the life of the contract. Anyone
14
+ * may call `deposit`; the recipient is fixed, so there is no need for
15
+ * access control.
16
+ *
17
+ * The constructor accepts a `ZswapCoinPublicKey`: an atomic forward can
18
+ * only deliver to a recipient that needs no in-tx claim, and a shielded
19
+ * send to a non-participating contract is rejected. The parent is stored
20
+ * generically (`Either`) on the ledger so a future circuit upgrade can
21
+ * add contract-address support without a state migration.
22
+ */
23
+
24
+ import CompactStandardLibrary;
25
+ import "../../ForwarderShielded" prefix Forwarder_;
26
+
27
+ export { ZswapCoinPublicKey, ContractAddress, ShieldedCoinInfo, Either };
28
+
29
+ /**
30
+ * @description Deploys the forwarder bound to a specific parent
31
+ * recipient.
32
+ *
33
+ * @param {ZswapCoinPublicKey} parent - The coin public key that receives
34
+ * every forwarded coin.
35
+ */
36
+ constructor(parent: ZswapCoinPublicKey) {
37
+ Forwarder_initialize(parent);
38
+ }
39
+
40
+ /**
41
+ * @description Receives a shielded coin and atomically forwards it to
42
+ * the configured parent. The coin is claimed via `receiveShielded` and
43
+ * immediately re-sent via `sendImmediateShielded`.
44
+ *
45
+ * @param {ShieldedCoinInfo} coin - The incoming shielded coin.
46
+ *
47
+ * @returns {[]} Empty tuple.
48
+ */
49
+ export circuit deposit(coin: ShieldedCoinInfo): [] {
50
+ Forwarder__deposit(coin);
51
+ }
52
+
53
+ /**
54
+ * @description Returns the configured parent recipient as stored on the
55
+ * ledger (the generic `Either`; the `left` arm holds the coin public key).
56
+ *
57
+ * @returns {Either<ZswapCoinPublicKey, ContractAddress>} The parent
58
+ * recipient set at deploy.
59
+ */
60
+ export circuit getParent(): Either<ZswapCoinPublicKey, ContractAddress> {
61
+ return Forwarder__parent;
62
+ }
@@ -0,0 +1,64 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/presets/forwarder/ForwarderUnshielded.compact)
3
+
4
+ pragma language_version >= 0.23.0;
5
+
6
+ /**
7
+ * @title ForwarderUnshielded
8
+ * @description Public-parent forwarder for unshielded coins. Receives
9
+ * an unshielded amount of a given color and atomically forwards it to
10
+ * the configured parent recipient, a user address.
11
+ *
12
+ * Unshielded transfers are publicly visible on the chain: depositor,
13
+ * recipient, color, and amount all appear on the public transcript.
14
+ * Use `ForwarderShielded` instead when the deposit kind is shielded.
15
+ *
16
+ * The constructor accepts a `UserAddress`: an atomic forward can only
17
+ * deliver to a recipient that needs no in-tx claim, and an unshielded
18
+ * send to a non-participating contract is rejected. The parent is stored
19
+ * generically (`Either`) on the ledger so a future circuit upgrade can
20
+ * add contract-address support without a state migration.
21
+ *
22
+ * The parent recipient is set at deploy time. This preset exposes no
23
+ * setter, so the parent is fixed for the life of the contract.
24
+ */
25
+
26
+ import CompactStandardLibrary;
27
+ import "../../ForwarderUnshielded" prefix Forwarder_;
28
+
29
+ export { ContractAddress, UserAddress, Either };
30
+
31
+ /**
32
+ * @description Deploys the forwarder bound to a specific parent
33
+ * recipient.
34
+ *
35
+ * @param {UserAddress} parent - The user address that receives every
36
+ * forwarded amount.
37
+ */
38
+ constructor(parent: UserAddress) {
39
+ Forwarder_initialize(parent);
40
+ }
41
+
42
+ /**
43
+ * @description Receives an unshielded amount of `color` and atomically
44
+ * forwards it to the configured parent.
45
+ *
46
+ * @param {Bytes<32>} color - The token color.
47
+ * @param {Uint<128>} amount - The amount to deposit.
48
+ *
49
+ * @returns {[]} Empty tuple.
50
+ */
51
+ export circuit deposit(color: Bytes<32>, amount: Uint<128>): [] {
52
+ Forwarder__deposit(color, amount);
53
+ }
54
+
55
+ /**
56
+ * @description Returns the configured parent recipient as stored on the
57
+ * ledger (the generic `Either`; the `right` arm holds the user address).
58
+ *
59
+ * @returns {Either<ContractAddress, UserAddress>} The parent recipient
60
+ * set at deploy.
61
+ */
62
+ export circuit getParent(): Either<ContractAddress, UserAddress> {
63
+ return Forwarder__parent;
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openzeppelin/compact-contracts",
3
- "version": "0.2.0",
3
+ "version": "0.3.0-alpha",
4
4
  "description": "OpenZeppelin Compact contract library",
5
5
  "keywords": [
6
6
  "openzeppelin",
@@ -27,12 +27,15 @@
27
27
  "compact": "compact-compiler --exclude '*/archive/*'",
28
28
  "compact:access": "compact-compiler --dir access",
29
29
  "compact:archive": "compact-compiler --dir archive",
30
+ "compact:crypto": "compact-compiler --dir crypto",
31
+ "compact:multisig": "compact-compiler --dir multisig",
30
32
  "compact:security": "compact-compiler --dir security",
31
33
  "compact:token": "compact-compiler --dir token",
32
34
  "compact:utils": "compact-compiler --dir utils",
33
35
  "build": "compact-builder --hierarchical --out dist --clean-dist --exclude '*/archive/*' --exclude 'Mock*' --exclude '*.mock.compact' --copy package.json --copy ../README.md && find dist -type d -empty -delete",
34
36
  "test": "SKIP_ZK=true yarn run compact && vitest run",
35
37
  "test:coverage": "SKIP_ZK=true yarn run compact && vitest run --coverage",
38
+ "test:live": "yarn run compact && MIDNIGHT_BACKEND=live vitest run --config vitest.live.config.ts",
36
39
  "compact:integration": "SKIP_ZK=true compact compile test/integration/_mocks/SharedInitCollision.compact artifacts/SharedInitCollision && SKIP_ZK=true compact compile test/integration/_mocks/ComposedTokens.compact artifacts/ComposedTokens",
37
40
  "test:integration": "yarn run compact:integration && vitest run --config vitest.integration.config.ts",
38
41
  "types": "tsc -p tsconfig.json --noEmit",
@@ -45,12 +48,13 @@
45
48
  "@openzeppelin/compact-cli": "^0.0.2"
46
49
  },
47
50
  "devDependencies": {
48
- "@openzeppelin/compact-simulator": "^0.0.1",
51
+ "@openzeppelin/compact-simulator": "^0.2.0",
49
52
  "@tsconfig/node24": "^24.0.4",
50
- "@types/node": "25.9.1",
51
- "@vitest/coverage-v8": "^4.1.8",
53
+ "@types/node": "25.9.3",
54
+ "@vitest/coverage-v8": "^4.1.9",
55
+ "fast-check": "^4.8.0",
52
56
  "ts-node": "^10.9.2",
53
57
  "typescript": "^6.0.3",
54
- "vitest": "^4.1.8"
58
+ "vitest": "^4.1.9"
55
59
  }
56
60
  }