@openzeppelin/compact-contracts 0.3.0-alpha.1 → 0.3.0-rc.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 (39) hide show
  1. package/access/AccessControl.compact +1 -1
  2. package/access/Ownable.compact +1 -1
  3. package/access/ShieldedAccessControl.compact +1 -1
  4. package/access/ZOwnablePK.compact +1 -1
  5. package/crypto/EcdhMask.compact +1 -1
  6. package/crypto/ElGamal.compact +44 -3
  7. package/multisig/ForwarderPrivate.compact +2 -2
  8. package/multisig/ForwarderShielded.compact +2 -2
  9. package/multisig/ForwarderUnshielded.compact +2 -2
  10. package/multisig/ProposalManager.compact +35 -1
  11. package/multisig/ShieldedTreasury.compact +34 -4
  12. package/multisig/ShieldedTreasuryStateless.compact +46 -17
  13. package/multisig/Signer.compact +45 -41
  14. package/multisig/UnshieldedTreasury.compact +44 -17
  15. package/multisig/presets/ShieldedMultiSig.compact +4 -4
  16. package/multisig/presets/ShieldedMultiSigV2.compact +2 -2
  17. package/multisig/presets/ShieldedMultiSigV3.compact +2 -2
  18. package/multisig/presets/forwarder/ForwarderPrivate.compact +1 -1
  19. package/multisig/presets/forwarder/ForwarderShielded.compact +1 -1
  20. package/multisig/presets/forwarder/ForwarderUnshielded.compact +1 -1
  21. package/package.json +3 -3
  22. package/security/Allowlist.compact +1 -1
  23. package/security/Blocklist.compact +1 -1
  24. package/security/Initializable.compact +1 -1
  25. package/security/Pausable.compact +1 -1
  26. package/token/ConfidentialFungibleToken.compact +289 -75
  27. package/token/FungibleToken.compact +1 -1
  28. package/token/MultiToken.compact +1 -1
  29. package/token/NativeShieldedToken.compact +60 -11
  30. package/token/NativeShieldedTokenCore.compact +41 -9
  31. package/token/NativeShieldedTokenFamily.compact +61 -11
  32. package/token/NonFungibleToken.compact +63 -19
  33. package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +1 -1
  34. package/token/extensions/NativeShieldedTokenDerivedNonce.compact +1 -1
  35. package/token/extensions/{NativeShieldedTokenFamilySupply.compact → NativeShieldedTokenFamilyPublicSupply.compact} +11 -11
  36. package/token/extensions/{NativeShieldedTokenSupply.compact → NativeShieldedTokenPublicSupply.compact} +11 -11
  37. package/token/extensions/{NativeShieldedTokenSupplyCore.compact → NativeShieldedTokenPublicSupplyCore.compact} +9 -9
  38. package/utils/Utils.compact +11 -1
  39. package/multisig/SignerManager.compact +0 -205
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (access/AccessControl.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-rc.1 (access/AccessControl.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (access/Ownable.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-rc.1 (access/Ownable.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (access/ShieldedAccessControl.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-rc.1 (access/ShieldedAccessControl.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (access/ZOwnablePK.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-rc.1 (access/ZOwnablePK.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (crypto/EcdhMask.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-rc.1 (crypto/EcdhMask.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (crypto/ElGamal.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-rc.1 (crypto/ElGamal.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -34,6 +34,13 @@ pragma language_version >= 0.23.0;
34
34
  * scalar. Feeding a raw `persistentHash` output into `ecMulGenerator` would
35
35
  * occasionally exceed the Jubjub scalar field order and fault at runtime.
36
36
  *
37
+ * @dev Domain separation. `persistentHash<Vector<N, Bytes<32>>>` is NOT
38
+ * domain-separating on its own. Two circuits hashing the same arity and
39
+ * element type share one hash domain, and the hash cannot tell which slot was
40
+ * "meant" as a tag. `secretToScalar` therefore prefixes a fixed constant, and
41
+ * that constant is placed FIRST so no call to `expandRandomness` (whose two
42
+ * slots are both caller-supplied) can reproduce the tuple by ordinary use.
43
+ *
37
44
  * @dev TRUST ASSUMPTION — subgroup membership. This is the module's most
38
45
  * load-bearing assumption. Every `JubjubPoint` reaching a curve operation is
39
46
  * assumed to be in the Jubjub prime-order subgroup, including points supplied by
@@ -102,11 +109,34 @@ module ElGamal {
102
109
  * @description Maps a 32-byte secret to a valid Jubjub scalar. See the
103
110
  * module-level hash-to-scalar note for why `degradeToTransient` is required.
104
111
  *
112
+ * @dev Domain separation here is load-bearing, not cosmetic. This output is
113
+ * the private key protecting every ciphertext held under an account. Public
114
+ * account identifiers derived from `Utils.computeAccountId`, for example,
115
+ * are computed as `persistentHash([secretKey])`, and that identifier
116
+ * is stored in the clear as a ledger map key. Untagged, this circuit
117
+ * would compute that same hash, so a secret used in both roles would
118
+ * have its encryption key recoverable from a published identifier by applying
119
+ * `degradeToTransient`. The tag makes the two derivations unrelated, so a
120
+ * wallet MAY safely derive its account secret and its encryption secret from
121
+ * common key material.
122
+ *
123
+ * @dev The tag is FIRST, deliberately. `expandRandomness` hashes
124
+ * `[seed, tag]` with both slots caller-supplied, so tagging this circuit as
125
+ * `[secret, tag]` would make `secretToScalar(x)` identical to
126
+ * `expandRandomness(x, <this tag>)` reachable by passing this domain string
127
+ * to the parameter that exists to receive domain strings. Leading with the
128
+ * tag means reproducing this output through `expandRandomness` would require
129
+ * passing the domain constant as the *seed*. Do not normalise the orderings.
130
+ *
105
131
  * @param secret - The 32-byte secret to map.
106
132
  * @return A Field guaranteed to be a valid Jubjub scalar.
107
133
  */
108
134
  export pure circuit secretToScalar(secret: Bytes<32>): Field {
109
- return degradeToTransient(persistentHash<Vector<1, Bytes<32>>>([secret]));
135
+ return degradeToTransient(
136
+ persistentHash<Vector<2, Bytes<32>>>(
137
+ [pad(32, "ElGamal:secretToScalar"), secret]
138
+ )
139
+ );
110
140
  }
111
141
 
112
142
  /**
@@ -452,6 +482,17 @@ module ElGamal {
452
482
  * wrong plaintext fails the decryption check. Lifted special case of
453
483
  * `assertDecryptsToPoint` with the message point `g^claimedValue`.
454
484
  *
485
+ * @notice `claimedValue` is the widest `Uint` Compact offers, so this parameter
486
+ * never bounds a caller. A narrower value costs proportionally fewer rows: the
487
+ * range constraint belongs to where the value is declared, not to this
488
+ * signature.
489
+ *
490
+ * @warning The claim domain must stay below the Jubjub subgroup order `l`:
491
+ * verification compares `g^claimedValue`, so two claims congruent mod `l` are
492
+ * indistinguishable. `Uint<248>` clears `l` (~2^251.86) by under 4 bits, and
493
+ * that width is set by Compact's cap rather than by this bound. 251 is the
494
+ * widest sound `Uint` if the cap ever rises.
495
+ *
455
496
  * Requirements:
456
497
  *
457
498
  * - `ek` is the secret for `pk`.
@@ -466,7 +507,7 @@ module ElGamal {
466
507
  ct: Ciphertext,
467
508
  pk: JubjubPoint,
468
509
  ek: Bytes<32>,
469
- claimedValue: Uint<128>
510
+ claimedValue: Uint<248>
470
511
  ): [] {
471
512
  assertDecryptsToPoint(ct, pk, ek, ecMulGenerator(claimedValue as Field));
472
513
  }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/ForwarderPrivate.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-rc.1 (multisig/ForwarderPrivate.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -110,7 +110,7 @@ module ForwarderPrivate {
110
110
  * would defeat the private-parent guarantee. A coin public key is encrypted
111
111
  * inside the Zswap output and never appears on the public transcript.
112
112
  *
113
- * @circuitInfo k=16, rows=41961
113
+ * @circuitInfo k=15, rows=23937
114
114
  *
115
115
  * Requirements:
116
116
  *
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/ForwarderShielded.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-rc.1 (multisig/ForwarderShielded.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -103,7 +103,7 @@ module ForwarderShielded {
103
103
  * `sendImmediateShielded` to the stored parent recipient (always the
104
104
  * `left` / coin-public-key arm today).
105
105
  *
106
- * @circuitInfo k=15, rows=18573
106
+ * @circuitInfo k=15, rows=24446
107
107
  *
108
108
  * Requirements:
109
109
  *
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/ForwarderUnshielded.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-rc.1 (multisig/ForwarderUnshielded.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -105,7 +105,7 @@ module ForwarderUnshielded {
105
105
  * forwards it to `_parent` via `sendUnshielded` (always the `right` /
106
106
  * user-address arm today).
107
107
  *
108
- * @circuitInfo k=9, rows=436
108
+ * @circuitInfo k=10, rows=565
109
109
  *
110
110
  * Requirements:
111
111
  *
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/ProposalManager.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-rc.1 (multisig/ProposalManager.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -12,12 +12,33 @@ pragma language_version >= 0.23.0;
12
12
  * Recipient type with a RecipientKind tag. Typed helper circuits
13
13
  * provide safe construction of recipients without exposing the
14
14
  * internal Bytes<32> representation to consumers.
15
+ *
16
+ * @warning PROPOSALS ARE PUBLIC. `_proposals` is exported ledger state, so
17
+ * every proposal's recipient address, token color, amount, and status is
18
+ * readable off-chain, and `_nextProposalId` reveals how many have been created.
19
+ * This holds regardless of which treasury a consumer composes: pairing this
20
+ * module with `ShieldedTreasuryStateless` still publishes the full intent of
21
+ * every transfer, even though the transfer itself is shielded. The transfer is
22
+ * private; the plan is not.
23
+ *
24
+ * @notice `RecipientKind` is the only thing distinguishing what a recipient's
25
+ * 32 bytes mean, the same value is read as a Zswap coin public key, a contract
26
+ * address, or a user address depending on the tag. `Contract` is accepted by
27
+ * both `toShieldedRecipient` and `toUnshieldedRecipient`, so the tag alone does
28
+ * not determine the rail; a consumer composing both treasuries could route the
29
+ * same proposal either way.
15
30
  */
16
31
  module ProposalManager {
17
32
  import CompactStandardLibrary;
18
33
 
19
34
  // ─── Types ──────────────────────────────────────────────────────
20
35
 
36
+ /**
37
+ * @description Proposal lifecycle states. `Inactive` is the zero value and is
38
+ * never stored: `_createProposal` writes `Active`, and every view asserts the
39
+ * proposal exists before reading, so a stored proposal can never read
40
+ * `Inactive`.
41
+ */
21
42
  export enum ProposalStatus {
22
43
  Inactive,
23
44
  Active,
@@ -176,6 +197,19 @@ module ProposalManager {
176
197
  /**
177
198
  * @description Creates a new proposal.
178
199
  *
200
+ * @warning The recipient is NOT validated beyond being well-typed. Two
201
+ * consequences the consumer must handle:
202
+ *
203
+ * - A `kind` the execution rail cannot accept (an `UnshieldedUser` recipient
204
+ * on a shielded treasury, say) creates a proposal that passes here and then
205
+ * reverts in `toShieldedRecipient` at execution. It stays `Active` and
206
+ * unexecutable on that rail until `_cancelProposal` retires it.
207
+ * - A zero or otherwise unspendable `address` is accepted, and funds sent to
208
+ * it on execution are unrecoverable.
209
+ *
210
+ * @notice `_nextProposalId` is incremented and then read back for the map key,
211
+ * making this a pinned read-modify-write.
212
+ *
179
213
  * @notice Access control is NOT enforced here.
180
214
  * The consuming contract must gate this behind its own
181
215
  * authorization policy.
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/ShieldedTreasury.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-rc.1 (multisig/ShieldedTreasury.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -18,6 +18,19 @@ pragma language_version >= 0.23.0;
18
18
  * purposes. The canonical balance query is `getTokenBalance`, which
19
19
  * reads the actual coin value from the UTXO map.
20
20
  *
21
+ * @warning This treasury's HOLDINGS ARE PUBLIC. `_coins` is exported ledger
22
+ * state, so every coin it holds is readable off-chain in plaintext (nonce,
23
+ * color, value, and Merkle index) as are the cumulative `_received` and
24
+ * `_sent` totals. "Shielded" here refers to the token rail, not to the
25
+ * treasury's balance: individual transfers are shielded, but what this
26
+ * contract holds is not. Anyone can observe the treasury's balance per color
27
+ * and identify the exact UTXO backing it.
28
+ *
29
+ * Storing the coin is what buys the convenience: the contract can spend later
30
+ * without the caller supplying the UTXO. If public holdings are unacceptable,
31
+ * use `ShieldedTreasuryStateless`, which stores nothing and has the operator
32
+ * supply the coin at spend time.
33
+ *
21
34
  * Underscore-prefixed circuits (_deposit, _send) have no access control
22
35
  * enforcement. The consuming contract must gate these behind its own
23
36
  * authorization policy.
@@ -42,9 +55,13 @@ module ShieldedTreasury {
42
55
  * The coin is then merged with any existing coin of the same color,
43
56
  * or inserted as a new entry if no coin of that color exists.
44
57
  *
45
- * Zero-value deposits are permitted. While currently a no-op
46
- * economically, they may serve as signaling mechanisms when events
47
- * are supported, or as decoy transactions for privacy.
58
+ * Zero-value deposits are permitted and are not always a no-op. On a color the
59
+ * treasury does not yet hold, a zero-value coin is inserted and occupies that
60
+ * color's single slot until a later deposit merges into it; if a coin of that
61
+ * color already exists, the zero merges into it via `mergeCoinImmediate` and
62
+ * the stored value is unchanged. They may serve as signaling mechanisms when
63
+ * events are supported. They are not useful as privacy decoys, this
64
+ * treasury's holdings are public (see the module header).
48
65
  *
49
66
  * @notice Access control is NOT enforced here.
50
67
  * The consuming contract must gate this behind its own
@@ -92,6 +109,12 @@ module ShieldedTreasury {
92
109
  * belongs to the treasury and must not be re-spent by the caller (doing
93
110
  * so would double-spend the recorded coin).
94
111
  *
112
+ * @notice The returned `ShieldedSendResult` carries the sent and change coins
113
+ * in plaintext, so sinking it (returning it from an entry point, writing it
114
+ * to ledger, passing it cross-contract) discloses them. `_coins` publishes only
115
+ * the retained change coin, and nothing at all when the send leaves no change,
116
+ * so the sent coin's plaintext is disclosed beyond anything in public state.
117
+ *
95
118
  * @notice Access control is NOT enforced here.
96
119
  * The consuming contract must gate this behind its own
97
120
  * authorization policy.
@@ -194,6 +217,13 @@ module ShieldedTreasury {
194
217
  * and cumulative sent totals for a color. Should equal
195
218
  * `getTokenBalance` if accounting is consistent.
196
219
  *
220
+ * @notice The subtraction is unguarded. `_send` cannot drive the sent total
221
+ * past the received total. Change re-insertion never increments `_received`
222
+ * so the invariant holds for any consumer that goes through `_deposit` and
223
+ * `_send`. A consumer that writes the exported `_coins` directly can break
224
+ * it, and this circuit then fails as an unprovable range violation rather
225
+ * than a named assert.
226
+ *
197
227
  * @param {Bytes<32>} color - The token color.
198
228
  *
199
229
  * @returns {Uint<128>} Received minus sent.
@@ -1,22 +1,38 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/ShieldedTreasuryStateless.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-rc.1 (multisig/ShieldedTreasuryStateless.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
6
6
  /**
7
- * @module ShieldedTreasury
8
- * @description Manages shielded (private) token deposits, accounting,
9
- * and transfers for multisig governance contracts.
7
+ * @module ShieldedTreasuryStateless
8
+ * @description Manages shielded (private) token deposits and transfers for
9
+ * multisig governance contracts, without any ledger state.
10
10
  *
11
- * Coins are stored on the contract ledger in a map keyed by token color,
12
- * with one UTXO per color. Deposits are merged with existing coins of
13
- * the same color via `mergeCoinImmediate`. This simplifies coin selection
14
- * at spend time — the executor doesn't need to choose between multiple
15
- * UTXOs of the same color.
11
+ * This module holds NO state: no coin map, no balances, no received/sent
12
+ * totals. Deposits call `receiveShielded` only, and spends take the coin as a
13
+ * circuit parameter. The operator discovers coin indices from `ZswapOutput`
14
+ * events via the indexer, constructs the `QualifiedShieldedCoinInfo` off-chain,
15
+ * and supplies it at spend time. Coin data therefore never reaches the public
16
+ * ledger, and there is no on-chain balance to query — contrast
17
+ * `ShieldedTreasury`, which stores one merged UTXO per color and tracks
18
+ * cumulative totals.
16
19
  *
17
- * Cumulative received and sent totals are tracked per color for audit
18
- * purposes. The canonical balance query is `getTokenBalance`, which
19
- * reads the actual coin value from the UTXO map.
20
+ * @notice Because nothing is tracked on-chain, this module cannot detect a
21
+ * double spend or a coin it does not own; the protocol rejects both at
22
+ * application time. Sufficiency is likewise enforced by `sendShielded` rather
23
+ * than by an assert here.
24
+ *
25
+ * @notice "No ledger state" is not the same as "unobservable". A coin
26
+ * commitment is a hash of the coin's data, so `sendShielded` can link a coin
27
+ * receive to its later spend; `_send` declares that disclosure on the
28
+ * consumer's behalf. Consumers that need to make the trade-off themselves
29
+ * should note it is only declared here for convenience — it could be moved to
30
+ * the caller. Separately, see `_send`'s warning about its return value, which
31
+ * carries coin plaintext.
32
+ *
33
+ * Underscore-prefixed circuits (_deposit, _send) have no access control
34
+ * enforcement. The consuming contract must gate these behind its own
35
+ * authorization policy.
20
36
  */
21
37
  module ShieldedTreasuryStateless {
22
38
  import CompactStandardLibrary;
@@ -40,13 +56,24 @@ module ShieldedTreasuryStateless {
40
56
  * Executes the shielded send and returns the result untouched.
41
57
  * `sendShielded` already routes any change back to this contract as a
42
58
  * self-owned output, so `result.change` is a live, spendable coin. The
43
- * implementing contract chooses what to do with it: persist it and pass
44
- * it back as the `coin` for a later spend, or spend it onward to a
59
+ * implementing contract chooses what to do with it: retain it off-chain and
60
+ * pass it back as the `coin` for a later spend, or spend it onward to a
45
61
  * different recipient in the same transaction via `sendImmediateShielded`.
46
62
  * (Re-spending change is only meaningful when it goes somewhere other than
47
63
  * self — `sendShielded` already routes it to self.) No balance or
48
64
  * received/sent accounting is tracked on the ledger.
49
65
  *
66
+ * @warning The returned `ShieldedSendResult` holds the sent and change coins
67
+ * in plaintext (`nonce`, `color`, `value`). Consuming it in-circuit is safe
68
+ * (passing `result.change` to `sendImmediateShielded`, for instance). Returning
69
+ * it from your entry point, writing it to ledger, or passing it to another
70
+ * contract publishes those coin details, including the amount left over from
71
+ * the coin that was spent. Note the compiler will NOT stop you: the
72
+ * `disclose` calls below clear the witness taint, so such a return compiles
73
+ * with no `disclose` of your own and no marker at the disclosure site. If you
74
+ * do not need in-circuit change handling, ignore the result. The change is
75
+ * recoverable off-chain from the transaction's private proving state.
76
+ *
50
77
  * @notice Access control is NOT enforced here.
51
78
  * The consuming contract must gate this behind its own
52
79
  * authorization policy.
@@ -72,9 +99,11 @@ module ShieldedTreasuryStateless {
72
99
  // `_send` returns it untouched: it must not re-spend the change here and
73
100
  // then hand that same (now-nullified) coin back via `result.change`. That
74
101
  // is the double spend a node rejects on the next spend. The implementing
75
- // contract decides what to do with the returned change: persist it and
76
- // spend it later, or route it onward itself. The rule is only that the
77
- // change must never be re-spent AND still treated as held.
102
+ // contract decides what to do with the returned change: retain it off-chain
103
+ // and spend it later, or route it onward itself. The rule is only that the
104
+ // change must never be re-spent AND still treated as held. "Retain" means
105
+ // off-chain. This module keeps no state, and writing the coin to ledger
106
+ // would publish its nonce, color, and value.
78
107
  return result;
79
108
  }
80
109
  }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/Signer.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-rc.1 (multisig/Signer.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -42,6 +42,25 @@ pragma language_version >= 0.23.0;
42
42
  * call `initialize` in the contract's constructor. Contracts MUST NOT
43
43
  * call `initialize` outside of the constructor context because
44
44
  * this could corrupt the signer set and threshold configuration.
45
+ *
46
+ * `_isInitialized` records only whether `initialize` has run; it gates
47
+ * re-entry into `initialize` and nothing else. No other circuit consults it,
48
+ * so a contract that configures signers through its own flow
49
+ * (`_addSigner` / `_setThreshold`) is fully functional without ever calling
50
+ * `initialize`. Provided it gates those circuits behind its own
51
+ * authorization policy. Leaving them ungated lets the first caller register
52
+ * their own signer set and take control of the contract.
53
+ *
54
+ * @notice Not asserting initialization on the guard and view circuits is a
55
+ * deliberate divergence from the other modules in this library, which do.
56
+ * Those modules gate a zero-valued identity (an owner, a role holder); this
57
+ * one gates an empty collection, which rejects by absence. Safety here comes
58
+ * from the state rather than from a flag: an empty registry makes `isSigner`
59
+ * false for every input, so `assertSigner` rejects every caller, and a zero
60
+ * `_threshold` makes `assertThresholdMet` reject before it compares any
61
+ * approval count regardless of how the threshold came to be zero. Contracts
62
+ * needing a "configured" signal beyond that should track one at the contract
63
+ * layer.
45
64
  */
46
65
  module Signer<T> {
47
66
  import CompactStandardLibrary;
@@ -66,8 +85,6 @@ module Signer<T> {
66
85
  * and an initial set of signers.
67
86
  * If used, it should only be called in the contract's constructor.
68
87
  *
69
- * @circuitInfo k=11, rows=1815
70
- *
71
88
  * Requirements:
72
89
  *
73
90
  * - If used, can only be called once (in the constructor).
@@ -98,37 +115,43 @@ module Signer<T> {
98
115
  /**
99
116
  * @description Asserts that the given caller is an active signer.
100
117
  *
101
- * @circuitInfo k=10, rows=585
102
- *
103
118
  * Requirements:
104
119
  *
105
- * - Contract must be initialized.
106
120
  * - `caller` must be a member of the signers registry.
107
121
  *
108
122
  * @param {T} caller - The identity to validate.
109
123
  * @returns {[]} Empty tuple.
110
124
  */
111
125
  export circuit assertSigner(caller: T): [] {
112
- assertInitialized();
113
126
  assert(isSigner(caller), "Signer: not a signer");
114
127
  }
115
128
 
116
129
  /**
117
130
  * @description Asserts that the given approval count meets the threshold.
118
131
  *
132
+ * @notice `approvalCount` is supplied by the consuming contract and is
133
+ * compared against the threshold only. This circuit never consults the signer
134
+ * registry. Ensuring the count reflects distinct, currently-registered signers
135
+ * is the consuming contract's responsibility, and a contract whose approval
136
+ * accounting is not itself gated by `assertSigner` gets no protection here.
137
+ *
138
+ * The count is deliberately left unbounded by `_signerCount`: approvals
139
+ * collected before a signer was removed must still count toward a threshold
140
+ * the remaining signers satisfy, and bounding the count would reject those.
141
+ * See the stale-signer-set guidance in the module header.
142
+ *
119
143
  * @circuitInfo k=9, rows=54
120
144
  *
121
145
  * Requirements:
122
146
  *
123
- * - Contract must be initialized.
124
- * - Ledger threshold must be set (not be zero).
147
+ * - Ledger threshold must be set (not be zero). An unconfigured module has a
148
+ * zero threshold, so this rejects before any approval count is compared.
125
149
  * - `approvalCount` must be >= threshold.
126
150
  *
127
151
  * @param {Uint<8>} approvalCount - The current number of approvals.
128
152
  * @returns {[]} Empty tuple.
129
153
  */
130
154
  export circuit assertThresholdMet(approvalCount: Uint<8>): [] {
131
- assertInitialized();
132
155
  assert(_threshold != 0, "Signer: threshold not set");
133
156
  assert(approvalCount >= _threshold, "Signer: threshold not met");
134
157
  }
@@ -136,42 +159,32 @@ module Signer<T> {
136
159
  // ─── View ──────────────────────────────────────────────────────────
137
160
 
138
161
  /**
139
- * @description Returns the current signer count.
162
+ * @description Returns the current signer count. Returns zero when no
163
+ * signers have been registered.
140
164
  *
141
165
  * @circuitInfo k=6, rows=26
142
166
  *
143
- * Requirements:
144
- *
145
- * - Contract must be initialized.
146
- *
147
167
  * @returns {Uint<8>} The number of active signers.
148
168
  */
149
169
  export circuit getSignerCount(): Uint<8> {
150
- assertInitialized();
151
170
  return _signerCount;
152
171
  }
153
172
 
154
173
  /**
155
- * @description Returns the approval threshold.
174
+ * @description Returns the approval threshold. Returns zero when the
175
+ * threshold has not been set.
156
176
  *
157
177
  * @circuitInfo k=6, rows=26
158
178
  *
159
- * Requirements:
160
- *
161
- * - Contract must be initialized.
162
- *
163
179
  * @returns {Uint<8>} The threshold.
164
180
  */
165
181
  export circuit getThreshold(): Uint<8> {
166
- assertInitialized();
167
182
  return _threshold;
168
183
  }
169
184
 
170
185
  /**
171
186
  * @description Returns whether the given account is an active signer.
172
187
  *
173
- * @circuitInfo k=10, rows=605
174
- *
175
188
  * @param {T} account - The account to check.
176
189
  * @returns {Boolean} True if the account is an active signer.
177
190
  */
@@ -188,8 +201,6 @@ module Signer<T> {
188
201
  * The consuming contract must gate this behind its own
189
202
  * authorization policy.
190
203
  *
191
- * @circuitInfo k=10, rows=598
192
- *
193
204
  * Requirements:
194
205
  *
195
206
  * - `signer` must not already be an active signer.
@@ -214,8 +225,6 @@ module Signer<T> {
214
225
  * The consuming contract must gate this behind its own
215
226
  * authorization policy.
216
227
  *
217
- * @circuitInfo k=10, rows=612
218
- *
219
228
  * Requirements:
220
229
  *
221
230
  * - `signer` must be an active signer.
@@ -263,6 +272,14 @@ module Signer<T> {
263
272
  * @warning This is intended for use during contract construction
264
273
  * or custom setup flows where signers may not yet be registered.
265
274
  *
275
+ * Leaving `newThreshold` above the signer count is permitted and expected
276
+ * mid-setup, and is not self-enforcing: `assertThresholdMet` compares
277
+ * `approvalCount` against the threshold alone, so a caller supplying a matching
278
+ * count satisfies it even against an empty registry. Contracts must not gate
279
+ * operations on `assertThresholdMet` while setup is incomplete. Lower the
280
+ * threshold with `_setThreshold`; `_changeThreshold` cannot lower one while the
281
+ * signer count is zero, because that would require a zero threshold.
282
+ *
266
283
  * @notice Access control is NOT enforced here.
267
284
  * The consuming contract must gate this behind its own
268
285
  * authorization policy. Use `_changeThreshold` for
@@ -284,19 +301,6 @@ module Signer<T> {
284
301
 
285
302
  // ─── Init guards ─────────────────────────────────────────────────────────
286
303
 
287
- /**
288
- * @description Asserts that the contract has been initialized, throwing an error if not.
289
- *
290
- * Requirements:
291
- *
292
- * - Contract must be initialized.
293
- *
294
- * @return {[]} - Empty tuple.
295
- */
296
- circuit assertInitialized(): [] {
297
- assert(_isInitialized, "Signer: contract not initialized");
298
- }
299
-
300
304
  /**
301
305
  * @description Asserts that the contract has not been initialized, throwing an error if it has.
302
306
  *