@openzeppelin/compact-contracts 0.4.0-alpha.6 → 0.4.0

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 (42) hide show
  1. package/access/AccessControl.compact +1 -1
  2. package/access/Ownable.compact +1 -3
  3. package/access/ShieldedAccessControl.compact +44 -41
  4. package/access/ZOwnablePK.compact +1 -3
  5. package/crypto/EcdhMask.compact +11 -1
  6. package/crypto/Ecdsa.compact +1 -1
  7. package/crypto/Eip712.compact +1 -1
  8. package/crypto/ElGamal.compact +1 -1
  9. package/multisig/ForwarderPrivate.compact +17 -6
  10. package/multisig/ForwarderShielded.compact +1 -1
  11. package/multisig/ForwarderUnshielded.compact +1 -1
  12. package/multisig/ProposalManager.compact +65 -127
  13. package/multisig/RecipientTypes.compact +48 -0
  14. package/multisig/Secp256k1EcdsaMultisig.compact +1 -1
  15. package/multisig/ShieldedTreasury.compact +20 -8
  16. package/multisig/ShieldedTreasuryStateless.compact +11 -5
  17. package/multisig/Signer.compact +1 -1
  18. package/multisig/UnshieldedTreasury.compact +24 -11
  19. package/multisig/examples/ForwarderPrivateExample.compact +3 -2
  20. package/multisig/examples/ForwarderShieldedExample.compact +1 -1
  21. package/multisig/examples/ForwarderUnshieldedExample.compact +1 -1
  22. package/multisig/examples/NativeShieldedTokenIssuerExample.compact +1 -1
  23. package/multisig/examples/ShieldedMultiSigV2Example.compact +2 -2
  24. package/multisig/presets/NativeShieldedTokenIssuer.compact +5 -2
  25. package/multisig/presets/ShieldedMultiSigV2.compact +16 -10
  26. package/package.json +1 -1
  27. package/security/Allowlist.compact +1 -1
  28. package/security/Blocklist.compact +1 -1
  29. package/security/Initializable.compact +1 -3
  30. package/security/Pausable.compact +1 -1
  31. package/token/ConfidentialFungibleToken.compact +70 -22
  32. package/token/FungibleToken.compact +1 -3
  33. package/token/MultiToken.compact +1 -3
  34. package/token/NativeShieldedToken.compact +11 -3
  35. package/token/NativeShieldedTokenCore.compact +1 -1
  36. package/token/NativeShieldedTokenFamily.compact +1 -1
  37. package/token/NonFungibleToken.compact +1 -3
  38. package/token/examples/ZOwnableNativeShieldedTokenExample.compact +1 -1
  39. package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +1 -1
  40. package/token/extensions/NativeShieldedTokenDerivedNonce.compact +1 -1
  41. package/utils/EvmAbi.compact +1 -1
  42. package/utils/Utils.compact +1 -1
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (access/AccessControl.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (access/AccessControl.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (access/Ownable.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (access/Ownable.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -117,8 +117,6 @@ module Ownable {
117
117
  * @description Initializes the contract by setting the `initialOwner`.
118
118
  * This must be called in the contract's constructor.
119
119
  *
120
- * @constraints k=10, rows=626
121
- *
122
120
  * Requirements:
123
121
  *
124
122
  * - Contract is not already initialized.
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (access/ShieldedAccessControl.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (access/ShieldedAccessControl.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -108,8 +108,8 @@ pragma language_version >= 0.26.0;
108
108
  * depends on the number of plausible `(role, accountId)` pairings. Since role identifiers
109
109
  * are public, the anonymity set is effectively the number of plausible account IDs per role.
110
110
  * - Outside observers will know when roles are granted and revoked.
111
- * - Outside observers can infer the total number of role grants made across all roles—not per-role counts,
112
- * but the cumulative total.
111
+ * - Outside observers can infer the total number of distinct `(role, accountId)` pairings granted
112
+ * across all roles, not per-role counts.
113
113
  * - Outside observers can link calls made by the same role instance across time.
114
114
  * - Users can be retroactively deanonymized if their secret key is exposed.
115
115
  * - Outside observers will NOT be able to identify the holder of any role
@@ -133,29 +133,20 @@ pragma language_version >= 0.26.0;
133
133
  * - It's strongly recommended to use cryptographically secure random values for the `_instanceSalt`.
134
134
  * Failure to do so may weaken privacy guarantees.
135
135
  * - The `_instanceSalt` is immutable and used to differentiate deployments.
136
- * - The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots.
137
- * Deployers should monitor slot consumption off-chain. A careless admin can exhaust
138
- * capacity through repeated grants of the same active (role, accountId) pairing.
139
- * Duplicate grants are benign from a security perspective. They produce identical
140
- * commitments and a single nullifier invalidates all copies; however, they waste tree capacity.
141
- * Implementing contracts are responsible for mitigating tree exhaustion risk.
136
+ * - The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots. Each distinct
137
+ * `(role, accountId)` pairing consumes one slot; re-granting an active pairing is a no-op.
142
138
  * - A single `secretKey` compromise exposes all roles held by that user within the contract
143
139
  * instance, since all roles share the same `accountId`. Users requiring compartmentalization
144
140
  * between roles should use separate keys per role and accept the additional key management overhead.
145
141
  * In the event of key compromise, the user should coordinate with admins to revoke all roles
146
142
  * under the compromised `accountId` and re-grant under a new identity.
147
- * - Admins can revoke `(role, accountId)` pairings that were never granted, since on-chain
148
- * non-membership proofs are not available for the Merkle tree. This permanently burns the
149
- * nullifier for that pairing, blocking any future grant. Admin trust is a fundamental
150
- * assumption of this system. Off-chain validation should confirm that a pairing was actually
151
- * granted before submitting a revocation transaction.
152
- * - `renounceRole` allows burning nullifiers for roles the caller does not currently hold,
153
- * provided they possess the correct `secretKey`. This requires knowledge of the secret key,
154
- * at which point the `accountId` should be considered fully compromised regardless.
143
+ * - A `(role, accountId)` pairing can only be revoked after it was granted, so the nullifier
144
+ * set holds at most one entry per granted pairing.
155
145
  * - The Merkle tree capacity of 2^20 leaf slots is a permanent, irrecoverable resource.
156
146
  * There is no mechanism to reclaim slots after revocation, no upgrade path, and no emergency
157
147
  * recovery. Once exhausted, the contract is permanently unable to grant new roles. Deployers
158
- * must monitor slot consumption off-chain and plan capacity accordingly.
148
+ * must monitor slot consumption off-chain and plan capacity accordingly. The exported tree
149
+ * reports occupancy via `firstFree()` (TypeScript) and `isFull()` (circuit).
159
150
  *
160
151
  * @notice Using the SHA256 hashing function comes at a significant performance cost. In the future, we
161
152
  * plan on migrating to a ZK-friendly hashing function when an implementation is available.
@@ -213,6 +204,14 @@ module ShieldedAccessControl {
213
204
  */
214
205
  export ledger _roleCommitmentNullifiers: Set<RoleNullifier>;
215
206
 
207
+ /**
208
+ * @description Nullifiers of every pairing ever granted. Makes a repeated grant a no-op so
209
+ * one pairing can never consume more than one `_operatorRoles` slot, and gates revocation
210
+ * so a nullifier can only exist for a granted pairing.
211
+ * @type {Set<RoleNullifier>} _grantedRoleNullifiers
212
+ */
213
+ export ledger _grantedRoleNullifiers: Set<RoleNullifier>;
214
+
216
215
  /**
217
216
  * @sealed @ledger _instanceSalt
218
217
  * @description A per-instance value provided at initialization used to namespace
@@ -395,12 +394,11 @@ module ShieldedAccessControl {
395
394
 
396
395
  /**
397
396
  * @description Grants `role` to `accountId` by inserting a role commitment unique to the
398
- * `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Duplicate role commitments can be issued
399
- * so long as they remain unrevoked. This does not yield any additional authority and simply wastes
400
- * limited Merkle tree storage slots. Once revoked, a role cannot be re-granted to the same `accountId`. A new
397
+ * `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Granting an already active
398
+ * pairing is a no-op. Once revoked, a role cannot be re-granted to the same `accountId`. A new
401
399
  * `accountId` must be generated to be re-authorized for a revoked `role`.
402
400
  *
403
- * @constraints k=15, rows=28995
401
+ * @constraints k=15, rows=29071
404
402
  *
405
403
  * Requirements:
406
404
  *
@@ -429,9 +427,8 @@ module ShieldedAccessControl {
429
427
 
430
428
  /**
431
429
  * @description Grants `role` to `accountId` by inserting a role commitment unique to the
432
- * `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Duplicate role commitments can be issued
433
- * so long as they remain unrevoked. This does not yield any additional authority and simply wastes
434
- * limited Merkle tree storage slots. Once revoked, a role cannot be re-granted to the same `accountId`. A new
430
+ * `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Granting an already active
431
+ * pairing is a no-op. Once revoked, a role cannot be re-granted to the same `accountId`. A new
435
432
  * `accountId` must be generated to be re-authorized for a revoked `role`.
436
433
  *
437
434
  * Internal circuit without access restriction.
@@ -439,12 +436,10 @@ module ShieldedAccessControl {
439
436
  * @warning Exposing this circuit directly in an implementing contract would allow anyone to grant
440
437
  * roles without authorization. It must be wrapped with appropriate access control.
441
438
  *
442
- * @warning The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots.
443
- * Deployers should monitor slot consumption off-chain. Duplicate grants waste tree capacity
444
- * but are otherwise benign. A single nullifier invalidates all duplicate commitments.
445
- * Implementing contracts are responsible for mitigating tree exhaustion risk.
439
+ * @warning The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots, one per
440
+ * distinct pairing. Deployers should monitor slot consumption off-chain.
446
441
  *
447
- * @constraints k=14, rows=12250
442
+ * @constraints k=14, rows=12326
448
443
  *
449
444
  * Requirements:
450
445
  *
@@ -474,15 +469,17 @@ module ShieldedAccessControl {
474
469
  * @notice Roles are often managed via {grantRole} and {revokeRole}: this circuit's
475
470
  * purpose is to provide a mechanism for accounts to lose their privileges
476
471
  * if they are compromised (such as when a trusted device is misplaced).
472
+ * Consumers with no need for self-revocation should not export this circuit.
477
473
  *
478
474
  * @warning Outside observers may be able to use timing and pattern analysis to weaken pseudonymity
479
475
  * guarantees if renounceRole is used in tandem with other on-chain actions.
480
476
  *
481
- * @constraints k=14, rows=14440
477
+ * @constraints k=14, rows=14442
482
478
  *
483
479
  * Requirements:
484
480
  *
485
481
  * - The caller must provide a valid `accountId` for the role.
482
+ * - The `(role, accountId)` pairing must have been granted.
486
483
  * - The `(role, accountId)` pairing must not be already revoked.
487
484
  * - Contract is initialized.
488
485
  *
@@ -508,15 +505,15 @@ module ShieldedAccessControl {
508
505
  /**
509
506
  * @description Permanently revokes `role` from `accountId` by inserting a role nullifier into the
510
507
  * `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for
511
- * `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible
512
- * so a `(role, accountId)` pairing that does not exist can still be revoked.
508
+ * `role`.
513
509
  *
514
- * @constraints k=15, rows=27005
510
+ * @constraints k=15, rows=27007
515
511
  *
516
512
  * Requirements:
517
513
  *
518
514
  * - caller must prove they're an admin for `role`.
519
515
  * - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing.
516
+ * - The `(role, accountId)` pairing must have been granted.
520
517
  * - The `(role, accountId)` pairing must not be already revoked.
521
518
  * - Contract is initialized.
522
519
  *
@@ -541,18 +538,18 @@ module ShieldedAccessControl {
541
538
  /**
542
539
  * @description Permanently revokes `role` from `accountId` by inserting a role nullifier into the
543
540
  * `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for
544
- * `role`. At this time, proofs of non-membership on values in the Merkle tree LDT are not possible
545
- * so a `(role, accountId)` pairing that does not exist can still be revoked.
541
+ * `role`.
546
542
  *
547
543
  * Internal circuit without access restriction.
548
544
  *
549
545
  * @warning Exposing this circuit directly in an implementing contract would allow anyone to revoke
550
546
  * roles without authorization. It must be wrapped with appropriate access control.
551
547
  *
552
- * @constraints k=14, rows=10260
548
+ * @constraints k=14, rows=10262
553
549
  *
554
550
  * Requirements:
555
551
  *
552
+ * - The `(role, accountId)` pairing must have been granted.
556
553
  * - The `(role, accountId)` pairing must not be already revoked.
557
554
  * - Contract is initialized.
558
555
  *
@@ -573,13 +570,14 @@ module ShieldedAccessControl {
573
570
  /**
574
571
  * @description Core business logic for the grant/revoke role circuits. Asserts that the
575
572
  * `(role, accountId)` pairing has not already been revoked. On success, dispatches on
576
- * `updateType`: a `Grant` inserts the role commitment into `_operatorRoles`, and
577
- * a `Revoke` inserts the nullifier into `_roleCommitmentNullifiers`.
573
+ * `updateType`: a `Grant` inserts the role commitment into `_operatorRoles` unless the
574
+ * nullifier is already in `_grantedRoleNullifiers`, and a `Revoke` asserts the nullifier
575
+ * is in `_grantedRoleNullifiers` before inserting it into `_roleCommitmentNullifiers`.
578
576
  *
579
577
  * Disclosures:
580
578
  *
581
579
  * - A nullifier for the respective role commitment.
582
- * - A role commitment (on Grant only).
580
+ * - A role commitment (on first Grant only).
583
581
  *
584
582
  * @notice The nullifier is disclosed via `_roleCommitmentNullifiers.member` on every call,
585
583
  * regardless of the update type. Since `Set.member` publicly reveals its argument, the
@@ -602,9 +600,14 @@ module ShieldedAccessControl {
602
600
  const isRevoked = _roleCommitmentNullifiers.member(disclose(roleNullifier));
603
601
  assert(!isRevoked, "ShieldedAccessControl: role is already revoked");
604
602
 
603
+ const isGranted = _grantedRoleNullifiers.member(disclose(roleNullifier));
605
604
  if (updateType == UpdateType.Grant) {
606
- _operatorRoles.insert(disclose(roleCommitment));
605
+ if (!isGranted) {
606
+ _grantedRoleNullifiers.insert(disclose(roleNullifier));
607
+ _operatorRoles.insert(disclose(roleCommitment));
608
+ }
607
609
  } else {
610
+ assert(isGranted, "ShieldedAccessControl: role was never granted");
608
611
  _roleCommitmentNullifiers.insert(disclose(roleNullifier));
609
612
  }
610
613
  }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (access/ZOwnablePK.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (access/ZOwnablePK.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -106,8 +106,6 @@ module ZOwnablePK {
106
106
  * @warning The `ownerId` must be calculated prior to contract deployment using the SHA256 hashing algorithm.
107
107
  * Using any other algorithm will result in a permanent loss of contract access.
108
108
  *
109
- * @constraints k=13, rows=6765
110
- *
111
109
  * Requirements:
112
110
  *
113
111
  * - Contract is not initialized.
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (crypto/EcdhMask.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (crypto/EcdhMask.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -49,6 +49,8 @@ pragma language_version >= 0.26.0;
49
49
  * an out-of-range scalar: `g^e = identity` captures both `e = 0` and any
50
50
  * `e ≡ 0 (mod ℓ)`. (Guarding `e != 0` directly would instead lean on the runtime
51
51
  * faulting `ecMul` for scalars `≥ ℓ`; the point guard removes that dependency.)
52
+ * `decrypt` guards the same point, refusing an identity ephemeral rather than
53
+ * unmasking against that public constant.
52
54
  * These are the only weak inputs (for a subgroup key, `S = identity` iff the
53
55
  * ephemeral point is the identity).
54
56
  *
@@ -162,6 +164,10 @@ module EcdhMask {
162
164
  * @description Recovers the value from a ciphertext using the recipient's
163
165
  * secret scalar. Pure and off-chain (no proof needed to read a received value).
164
166
  *
167
+ * Requirements:
168
+ *
169
+ * - `ciphertext.ephemeralPk` is not the identity point.
170
+ *
165
171
  * @param ciphertext - The ciphertext to decrypt.
166
172
  * @param ekScalar - The recipient's secret scalar (`crypto/ElGamal`'s
167
173
  * `secretToScalar(EK)`).
@@ -170,6 +176,10 @@ module EcdhMask {
170
176
  * which is `< 2^128`).
171
177
  */
172
178
  export pure circuit decrypt(ciphertext: Ciphertext, ekScalar: JubjubScalar, domain: Bytes<32>): Field {
179
+ // `encrypt` never emits an identity ephemeral; one here is a malformed
180
+ // ciphertext whose mask is a public constant, so refuse rather than decrypt.
181
+ assert(ciphertext.ephemeralPk != ecMulGenerator(0 as JubjubScalar),
182
+ "EcdhMask: identity ephemeral");
173
183
  const sShared = ecMul(ciphertext.ephemeralPk, ekScalar);
174
184
  const mask = kdf(sShared, domain);
175
185
  return ciphertext.ct - mask;
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (crypto/Ecdsa.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (crypto/Ecdsa.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (crypto/Eip712.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (crypto/Eip712.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (crypto/ElGamal.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (crypto/ElGamal.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (multisig/ForwarderPrivate.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (multisig/ForwarderPrivate.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -10,15 +10,25 @@ pragma language_version >= 0.26.0;
10
10
  * Deposits accumulate at the contract (no atomic forward); the operator
11
11
  * drains coins by presenting the `(parent, opSecret)` preimage at drain
12
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.
13
+ * the parent never appears on the ledger. The parent is deliberately a coin
14
+ * public key and not a contract address: a shielded send to a contract
15
+ * publishes that contract's address in cleartext, which would defeat the
16
+ * guarantee.
17
+ *
18
+ * @notice The commitment hides the parent from the public, not from a
19
+ * depositor. A contract-owned coin's nullifier is a hash of the coin and the
20
+ * contract address, and `sendShielded` derives the nonces of the sent coin
21
+ * and of the change from a public hash of the spent coin's nonce. Whoever
22
+ * knows a deposited coin can therefore find the drain that spends it, recover
23
+ * the drained amount (on a partial drain the change commitment has the value
24
+ * as its only unknown), and test candidate parent keys against the sent
25
+ * coin's commitment. A depositor who can guess the parent can confirm it.
16
26
  *
17
27
  * Knowledge of the preimage is the sole authorization gate — there is
18
28
  * no signer set and no nullifier scheme. The operational secret is
19
29
  * held off-chain; the contract never sees it except during a drain.
20
30
  * Two forwarders bound to the same parent with different operational
21
- * secrets produce different commitments and are unlinkable on-chain.
31
+ * secrets produce different commitments, so the ledger does not link them.
22
32
  *
23
33
  * Underscore-prefixed circuits have no access control beyond the
24
34
  * preimage check inside `_drain`. State access is gated by the
@@ -103,7 +113,8 @@ module ForwarderPrivate {
103
113
  * @description Spends a previously-deposited shielded coin to the parent,
104
114
  * a coin public key. The caller proves knowledge of `(parent, opSecret)`
105
115
  * matching the stored commitment; the coin is sent as a plain shielded note,
106
- * so the parent stays hidden on-chain. If `coin.value` exceeds `value`,
116
+ * so the parent stays hidden from anyone who holds no deposited coin (see the
117
+ * module notice). If `coin.value` exceeds `value`,
107
118
  * `sendShielded` routes the change back to this contract as a self-owned
108
119
  * output; `_drain` returns it untouched via `result.change`. The
109
120
  * implementing contract chooses what to do with it: leave it dwelling at
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (multisig/ForwarderShielded.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (multisig/ForwarderShielded.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (multisig/ForwarderUnshielded.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (multisig/ForwarderUnshielded.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5