@openzeppelin/compact-contracts 0.3.0-rc.1 → 0.4.0-alpha.2

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 (46) hide show
  1. package/README.md +14 -4
  2. package/access/AccessControl.compact +14 -14
  3. package/access/Ownable.compact +16 -10
  4. package/access/ShieldedAccessControl.compact +26 -2
  5. package/access/ZOwnablePK.compact +9 -9
  6. package/crypto/EcdhMask.compact +5 -5
  7. package/crypto/Ecdsa.compact +111 -0
  8. package/crypto/Eip712.compact +168 -0
  9. package/crypto/ElGamal.compact +33 -107
  10. package/multisig/EcdsaSignerManager.compact +246 -0
  11. package/multisig/ForwarderPrivate.compact +12 -6
  12. package/multisig/ForwarderShielded.compact +3 -3
  13. package/multisig/ForwarderUnshielded.compact +3 -3
  14. package/multisig/ProposalManager.compact +195 -23
  15. package/multisig/ShieldedTreasury.compact +29 -2
  16. package/multisig/ShieldedTreasuryStateless.compact +2 -2
  17. package/multisig/Signer.compact +28 -80
  18. package/multisig/UnshieldedTreasury.compact +10 -2
  19. package/multisig/{presets/forwarder/ForwarderPrivate.compact → examples/ForwarderPrivateExample.compact} +4 -4
  20. package/multisig/{presets/forwarder/ForwarderShielded.compact → examples/ForwarderShieldedExample.compact} +4 -4
  21. package/multisig/{presets/forwarder/ForwarderUnshielded.compact → examples/ForwarderUnshieldedExample.compact} +4 -4
  22. package/multisig/examples/NativeShieldedTokenIssuerExample.compact +143 -0
  23. package/multisig/examples/ShieldedMultiSigV2Example.compact +77 -0
  24. package/multisig/presets/NativeShieldedTokenIssuer.compact +542 -0
  25. package/multisig/presets/ShieldedMultiSigV2.compact +267 -259
  26. package/package.json +31 -25
  27. package/security/Allowlist.compact +6 -6
  28. package/security/Blocklist.compact +6 -6
  29. package/security/Initializable.compact +9 -5
  30. package/security/Pausable.compact +11 -7
  31. package/token/ConfidentialFungibleToken.compact +143 -270
  32. package/token/FungibleToken.compact +36 -22
  33. package/token/MultiToken.compact +16 -16
  34. package/token/NativeShieldedToken.compact +17 -66
  35. package/token/NativeShieldedTokenCore.compact +26 -48
  36. package/token/NativeShieldedTokenFamily.compact +17 -67
  37. package/token/NonFungibleToken.compact +40 -79
  38. package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +12 -7
  39. package/token/extensions/NativeShieldedTokenDerivedNonce.compact +3 -3
  40. package/token/extensions/NativeShieldedTokenFamilyPublicSupply.compact +8 -8
  41. package/token/extensions/NativeShieldedTokenPublicSupply.compact +8 -8
  42. package/token/extensions/NativeShieldedTokenPublicSupplyCore.compact +6 -6
  43. package/utils/EvmAbi.compact +134 -0
  44. package/utils/Utils.compact +2 -12
  45. package/multisig/presets/ShieldedMultiSig.compact +0 -227
  46. package/multisig/presets/ShieldedMultiSigV3.compact +0 -368
@@ -1,368 +0,0 @@
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 `shieldedBurnAddress()`. 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 `shieldedBurnAddress()` 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
- }