@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 (multisig/ProposalManager.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (multisig/ProposalManager.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -8,14 +8,15 @@ pragma language_version >= 0.26.0;
8
8
  * @description Token-agnostic proposal lifecycle management for multisig
9
9
  * governance contracts.
10
10
  *
11
- * Supports shielded and unshielded proposals through a unified
12
- * Recipient type with a RecipientKind tag. Typed helper circuits
13
- * provide safe construction of recipients without exposing the
14
- * internal Bytes<32> representation to consumers.
11
+ * Supports shielded and unshielded proposals through the `Recipient` type
12
+ * and its `RecipientKind` tag, declared in `RecipientTypes` and re-exported
13
+ * here. Typed helper circuits provide safe construction of recipients
14
+ * without exposing the internal Bytes<32> representation to consumers.
15
15
  *
16
16
  * @warning PROPOSALS ARE PUBLIC. `_proposals` is exported ledger state, so
17
- * every proposal's recipient address, token color, amount, and state is
18
- * readable off-chain, and `_nextProposalId` reveals how many have been created.
17
+ * every proposal's recipient address, token color, amount, expiry, and status
18
+ * is readable off-chain, and `_nextProposalId` reveals how many have been
19
+ * created.
19
20
  * This holds regardless of which treasury a consumer composes: pairing this
20
21
  * module with `ShieldedTreasuryStateless` still publishes the full intent of
21
22
  * every transfer, even though the transfer itself is shielded. The transfer is
@@ -30,18 +31,23 @@ pragma language_version >= 0.26.0;
30
31
  */
31
32
  module ProposalManager {
32
33
  import CompactStandardLibrary;
34
+ import "./RecipientTypes";
35
+
36
+ export { Recipient, RecipientKind };
33
37
 
34
38
  // ─── Types ──────────────────────────────────────────────────────
35
39
 
36
40
  /**
37
- * @description Proposal lifecycle states, derived from `Proposal.state` by
38
- * `getProposalStatus`. Not stored on the ledger.
39
- *
40
- * - `Inactive`: no proposal with that id exists.
41
- * - `Active`: `state` is an expiry in the future.
42
- * - `Executed`: `state` is `executedState()`.
43
- * - `Cancelled`: `state` is `cancelledState()`.
44
- * - `Expired`: `state` is an expiry at or before the current block time.
41
+ * @description Proposal lifecycle status. `Proposal.status` stores `Active`,
42
+ * `Executed`, or `Cancelled`; `getProposalStatus` derives the other two.
43
+ *
44
+ * - `Inactive`: no proposal with that id exists. Never stored.
45
+ * - `Active`: created and not retired. Stored. Executable only while the
46
+ * block time is below `Proposal.expiry`.
47
+ * - `Executed`: `_markExecuted` ran. Stored, terminal.
48
+ * - `Cancelled`: `_cancelProposal` ran. Stored, terminal.
49
+ * - `Expired`: stored `Active` with an `expiry` at or before the current
50
+ * block time. Never stored.
45
51
  */
46
52
  export enum ProposalStatus {
47
53
  Inactive,
@@ -52,69 +58,30 @@ module ProposalManager {
52
58
  }
53
59
 
54
60
  /**
55
- * @description What a `Recipient`'s bytes are and which rail accepts them.
56
- *
57
- * - `ShieldedUser`: a Zswap coin public key. `toShieldedRecipient` only.
58
- * - `UnshieldedUser`: a user address. `toUnshieldedRecipient` only.
59
- * - `Contract`: a contract address. Accepted by both.
60
- *
61
- * @warning `toShieldedRecipient` rejects `UnshieldedUser` and
62
- * `toUnshieldedRecipient` rejects `ShieldedUser`, both at execution rather
63
- * than at creation. That mismatch is the hazard noted in `_createProposal`.
64
- */
65
- export enum RecipientKind {
66
- ShieldedUser,
67
- UnshieldedUser,
68
- Contract
69
- }
70
-
71
- /**
72
- * @description A transfer target.
73
- *
74
- * @warning `address` is raw 32 bytes. `kind` alone decides whether they are
75
- * read as a Zswap coin public key, a user address, or a contract address.
76
- */
77
- export struct Recipient {
78
- kind: RecipientKind,
79
- address: Bytes<32>
80
- }
81
-
82
- /**
83
- * @description A proposal. `state` holds both the lifecycle stage and the
84
- * expiry in a single field:
61
+ * @description A proposal. `status` is the stored lifecycle stage and
62
+ * `expiry` is the deadline, as seconds since the Unix epoch. Reaching a
63
+ * terminal stage changes `status` only, so an executed or cancelled proposal
64
+ * keeps the deadline it was created with.
85
65
  *
86
- * - `executedState()` (1): executed.
87
- * - `cancelledState()` (2): cancelled.
88
- * - any greater value: the expiry, as seconds since the Unix epoch. The
89
- * proposal is active while the block time is below it and expired
90
- * afterwards.
91
- *
92
- * `_createProposal` rejects an `expiry` in the reserved range, so a proposal
93
- * that has not reached a terminal state always has `state` greater than
94
- * `cancelledState()`. Note that `Expired` is NOT terminal: a lapsed proposal
95
- * still carries its original deadline, which is what tells a reader when it
96
- * lapsed.
97
- *
98
- * @warning In the deadline range, `state` marks when a proposal STOPS being
99
- * executable, not when it starts: it is executable while `blockTime < state`
100
- * and inert once `blockTime >= state`.
66
+ * @warning `expiry` marks when a proposal STOPS being executable, not when it
67
+ * starts: an `Active` proposal is executable while `blockTime < expiry` and
68
+ * inert once `blockTime >= expiry`.
101
69
  *
102
70
  * @notice This module implements no execution delay. `expiry` bounds how long
103
71
  * a proposal stays valid; it does not impose a minimum wait before it may be
104
72
  * executed. A consumer that needs a delay must compose one.
105
73
  *
106
- * @notice Off-chain readers decode `state` themselves, applying the sentinel
107
- * checks and the deadline comparison above. `_proposals` is exported ledger
108
- * state and readable without a proof. `executedState` and
109
- * `cancelledState` are pure, so a reader can obtain the sentinel values without
110
- * hardcoding them. A state-reading accessor will be revisited once contract-to-contract
111
- * interactions are available.
74
+ * @notice `_proposals` is exported ledger state and readable without a proof.
75
+ * An off-chain reader takes `status` as stored and, when it is `Active`,
76
+ * compares `expiry` with the current block time. A state-reading accessor
77
+ * will be revisited once contract-to-contract interactions are available.
112
78
  */
113
79
  export struct Proposal {
114
80
  to: Recipient,
115
81
  color: Bytes<32>,
116
82
  amount: Uint<128>,
117
- state: Uint<64>
83
+ expiry: Uint<64>,
84
+ status: ProposalStatus
118
85
  }
119
86
 
120
87
  // ─── State ──────────────────────────────────────────────────────
@@ -136,43 +103,23 @@ module ProposalManager {
136
103
 
137
104
  /**
138
105
  * @description Proposals by id, keyed by the value `_nextProposalId` produced
139
- * at creation. Each entry carries the recipient, token color, amount, and the
140
- * overlaid lifecycle/expiry encoding documented on `Proposal`.
106
+ * at creation. Each entry carries the recipient, token color, amount, expiry,
107
+ * and lifecycle status documented on `Proposal`.
141
108
  * @type {Uint<64>} id - The proposal identifier, issued by `_nextProposalId`.
142
109
  * @type {Proposal} proposal - The proposal record.
143
110
  * @type {Map<id, proposal>}
144
111
  * @type {Map<Uint<64>, Proposal>} _proposals
145
112
  *
146
- * @notice Entries are never deleted. Reaching a terminal state overwrites
147
- * `state` in place so the map only grows, and no circuit here prunes it.
113
+ * @notice Entries are never deleted. Reaching a terminal stage overwrites
114
+ * `status` in place so the map only grows, and no circuit here prunes it.
148
115
  * A consumer that needs cleanup must compose its own.
149
116
  *
150
117
  * @notice Readable off-chain without a proof so integrators read this map
151
- * directly rather than calling a circuit. Decoding `state` is the reader's
152
- * job. See `Proposal`
118
+ * directly rather than calling a circuit. See `Proposal` for how `status`
119
+ * and `expiry` combine.
153
120
  */
154
121
  export ledger _proposals: Map<Uint<64>, Proposal>;
155
122
 
156
- // ─── State Sentinels ────────────────────────────────────────────
157
-
158
- /**
159
- * @description The `Proposal.state` value marking a proposal as executed.
160
- *
161
- * @returns {Uint<64>} 1.
162
- */
163
- export pure circuit executedState(): Uint<64> {
164
- return 1;
165
- }
166
-
167
- /**
168
- * @description The `Proposal.state` value marking a proposal as cancelled.
169
- *
170
- * @returns {Uint<64>} 2.
171
- */
172
- export pure circuit cancelledState(): Uint<64> {
173
- return 2;
174
- }
175
-
176
123
  // ─── Recipient Helpers ──────────────────────────────────────────
177
124
 
178
125
  /**
@@ -278,8 +225,8 @@ module ProposalManager {
278
225
 
279
226
  /**
280
227
  * @description Asserts that a proposal exists and is still executable. The two
281
- * checks are separate: a terminal `state` (executed or cancelled) fails the
282
- * first, and a deadline that has passed fails the second, so the error message
228
+ * checks are separate: a `status` other than `Active` fails the first, and a
229
+ * deadline that has passed fails the second, so the error message
283
230
  * distinguishes the two.
284
231
  *
285
232
  * @notice Expiry is terminal, unlike a conflicting read. A tx rejected
@@ -290,10 +237,10 @@ module ProposalManager {
290
237
  * Requirements:
291
238
  *
292
239
  * - Proposal must exist.
293
- * - Proposal must not be executed or cancelled.
240
+ * - Proposal `status` must be `Active`.
294
241
  * - Proposal must not have expired.
295
242
  *
296
- * @constraints k=9, rows=129
243
+ * @constraints k=7, rows=103
297
244
  *
298
245
  * @param {Uint<64>} id - The proposal ID.
299
246
  *
@@ -302,13 +249,13 @@ module ProposalManager {
302
249
  export circuit assertProposalActive(id: Uint<64>): [] {
303
250
  assertProposalExists(id);
304
251
 
305
- const state = _proposals.lookup(disclose(id)).state;
252
+ const proposal = _proposals.lookup(disclose(id));
306
253
  assert(
307
- state > cancelledState(),
254
+ proposal.status == ProposalStatus.Active,
308
255
  "ProposalManager: proposal not active"
309
256
  );
310
257
  assert(
311
- blockTimeLt(state),
258
+ blockTimeLt(proposal.expiry),
312
259
  "ProposalManager: proposal expired"
313
260
  );
314
261
  }
@@ -341,11 +288,6 @@ module ProposalManager {
341
288
  * reintroducing the immortal proposal this field exists to prevent. Consuming
342
289
  * contracts that need a bounded lifetime must impose their own cap.
343
290
  *
344
- * @notice `expiry` is also rejected if it falls in the sentinel range, keeping
345
- * the two domains `state` overlays disjoint. That check is enforced here rather
346
- * than left to the future check, so disjointness holds for whatever block time
347
- * the executing environment reports.
348
- *
349
291
  * @notice Access control is NOT enforced here.
350
292
  * The consuming contract must gate this behind its own
351
293
  * authorization policy.
@@ -353,10 +295,9 @@ module ProposalManager {
353
295
  * Requirements:
354
296
  *
355
297
  * - `amount` must be greater than 0.
356
- * - `expiry` must be greater than `cancelledState()`.
357
298
  * - `expiry` must be in the future.
358
299
  *
359
- * @constraints k=10, rows=913
300
+ * @constraints k=10, rows=883
360
301
  *
361
302
  * @param {Recipient} to - The recipient (constructed via helper circuits).
362
303
  * @param {Bytes<32>} color - The token color.
@@ -373,10 +314,6 @@ module ProposalManager {
373
314
  expiry: Uint<64>
374
315
  ): Uint<64> {
375
316
  assert(amount > 0, "ProposalManager: zero amount");
376
- assert(
377
- expiry > cancelledState(),
378
- "ProposalManager: expiry in reserved range"
379
- );
380
317
  assert(
381
318
  blockTimeLt(disclose(expiry)),
382
319
  "ProposalManager: expiry not in the future"
@@ -391,7 +328,8 @@ module ProposalManager {
391
328
  to: to,
392
329
  color: color,
393
330
  amount: amount,
394
- state: expiry
331
+ expiry: expiry,
332
+ status: ProposalStatus.Active
395
333
  }));
396
334
 
397
335
  return id;
@@ -420,7 +358,8 @@ module ProposalManager {
420
358
  to: proposal.to,
421
359
  color: proposal.color,
422
360
  amount: proposal.amount,
423
- state: cancelledState()
361
+ expiry: proposal.expiry,
362
+ status: ProposalStatus.Cancelled
424
363
  });
425
364
  }
426
365
 
@@ -447,16 +386,17 @@ module ProposalManager {
447
386
  to: proposal.to,
448
387
  color: proposal.color,
449
388
  amount: proposal.amount,
450
- state: executedState()
389
+ expiry: proposal.expiry,
390
+ status: ProposalStatus.Executed
451
391
  });
452
392
  }
453
393
 
454
394
  // ─── View ───────────────────────────────────────────────────────
455
395
 
456
396
  /**
457
- * @description Returns the full proposal data. The returned `state` is the
458
- * raw encoded field. See `Proposal` for how to read it, and prefer
459
- * `getProposalStatus` when the lifecycle stage is what is wanted.
397
+ * @description Returns the full proposal data. The returned `status` is the
398
+ * stored stage, so an `Active` entry may already be past its `expiry`.
399
+ * Prefer `getProposalStatus` when the effective status is what is wanted.
460
400
  *
461
401
  * @param {Uint<64>} id - The proposal ID.
462
402
  *
@@ -504,9 +444,10 @@ module ProposalManager {
504
444
  }
505
445
 
506
446
  /**
507
- * @description Derives the lifecycle status of a proposal from its `state`
508
- * field and the current block time. Returns `Inactive` for an id that was
509
- * never created rather than failing.
447
+ * @description Derives the effective lifecycle status of a proposal: the
448
+ * stored `status` unless it is `Active`, in which case `expiry` and the
449
+ * current block time decide between `Active` and `Expired`. Returns
450
+ * `Inactive` for an id that was never created rather than failing.
510
451
  *
511
452
  * @constraints k=9, rows=252
512
453
  *
@@ -519,14 +460,11 @@ module ProposalManager {
519
460
  return ProposalStatus.Inactive;
520
461
  }
521
462
 
522
- const state = _proposals.lookup(disclose(id)).state;
523
- if (state == executedState()) {
524
- return ProposalStatus.Executed;
525
- }
526
- if (state == cancelledState()) {
527
- return ProposalStatus.Cancelled;
463
+ const proposal = _proposals.lookup(disclose(id));
464
+ if (proposal.status != ProposalStatus.Active) {
465
+ return proposal.status;
528
466
  }
529
- if (blockTimeLt(state)) {
467
+ if (blockTimeLt(proposal.expiry)) {
530
468
  return ProposalStatus.Active;
531
469
  }
532
470
  return ProposalStatus.Expired;
@@ -0,0 +1,48 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.4.0 (multisig/RecipientTypes.compact)
3
+
4
+ pragma language_version >= 0.26.0;
5
+
6
+ /**
7
+ * @module RecipientTypes
8
+ * @description The transfer-target types shared by the multisig modules.
9
+ *
10
+ * This module holds no ledger state. Importing a module allocates every
11
+ * ledger field it declares, so a contract that needs only the recipient
12
+ * types imports this module rather than `ProposalManager`, which would add
13
+ * a `_nextProposalId` and `_proposals` the contract never writes.
14
+ * `ProposalManager` re-exports both types and provides the typed helper
15
+ * circuits that build and route recipients.
16
+ */
17
+ module RecipientTypes {
18
+ import CompactStandardLibrary;
19
+
20
+ /**
21
+ * @description What a `Recipient`'s bytes are and which rail accepts them.
22
+ *
23
+ * - `ShieldedUser`: a Zswap coin public key. `ProposalManager.toShieldedRecipient` only.
24
+ * - `UnshieldedUser`: a user address. `ProposalManager.toUnshieldedRecipient` only.
25
+ * - `Contract`: a contract address. Accepted by both.
26
+ *
27
+ * @warning `toShieldedRecipient` rejects `UnshieldedUser` and
28
+ * `toUnshieldedRecipient` rejects `ShieldedUser`, both at execution rather
29
+ * than at creation. That mismatch is the hazard noted in
30
+ * `ProposalManager._createProposal`.
31
+ */
32
+ export enum RecipientKind {
33
+ ShieldedUser,
34
+ UnshieldedUser,
35
+ Contract
36
+ }
37
+
38
+ /**
39
+ * @description A transfer target.
40
+ *
41
+ * @warning `address` is raw 32 bytes. `kind` alone decides whether they are
42
+ * read as a Zswap coin public key, a user address, or a contract address.
43
+ */
44
+ export struct Recipient {
45
+ kind: RecipientKind,
46
+ address: Bytes<32>
47
+ }
48
+ }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (multisig/Secp256k1EcdsaMultisig.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (multisig/Secp256k1EcdsaMultisig.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/ShieldedTreasury.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (multisig/ShieldedTreasury.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -82,13 +82,13 @@ module ShieldedTreasury {
82
82
  * The coin is then merged with any existing coin of the same color,
83
83
  * or inserted as a new entry if no coin of that color exists.
84
84
  *
85
- * Zero-value deposits are permitted and are not always a no-op. On a color the
86
- * treasury does not yet hold, a zero-value coin is inserted and occupies that
87
- * color's single slot until a later deposit merges into it; if a coin of that
88
- * color already exists, the zero merges into it via `mergeCoinImmediate` and
89
- * the stored value is unchanged. They may serve as signaling mechanisms when
90
- * events are supported. They are not useful as privacy decoys, this
91
- * treasury's holdings are public (see the module header).
85
+ * @warning Every deposit of a color replaces the coin that `_send` reads for
86
+ * that color, so a deposit that lands first invalidates every `_send` of the
87
+ * same color still waiting to be applied. A consumer that exposes `_deposit`
88
+ * without access control lets any depositor delay authorized sends for as
89
+ * long as they keep depositing; the sends must then be re-proven. No funds are
90
+ * lost. A zero-value coin is rejected, so each deposit costs at least one unit
91
+ * of the token plus the transaction fee.
92
92
  *
93
93
  * @notice Access control is NOT enforced here.
94
94
  * The consuming contract must gate this behind its own
@@ -96,6 +96,7 @@ module ShieldedTreasury {
96
96
  *
97
97
  * Requirements:
98
98
  *
99
+ * - The coin's value must be non-zero.
99
100
  * - Deposit must not cause received total overflow.
100
101
  *
101
102
  * @param {ShieldedCoinInfo} coin - The incoming shielded coin descriptor.
@@ -103,6 +104,8 @@ module ShieldedTreasury {
103
104
  * @returns {[]} Empty tuple.
104
105
  */
105
106
  export circuit _deposit(coin: ShieldedCoinInfo): [] {
107
+ assert(coin.value != 0, "ShieldedTreasury: zero value");
108
+
106
109
  const currentReceived = getReceivedTotal(coin.color);
107
110
  assert(currentReceived <= Utils_UINT128_MAX() - coin.value, "ShieldedTreasury: overflow");
108
111
 
@@ -142,12 +145,16 @@ module ShieldedTreasury {
142
145
  * the retained change coin, and nothing at all when the send leaves no change,
143
146
  * so the sent coin's plaintext is disclosed beyond anything in public state.
144
147
  *
148
+ * @dev A self-addressed coin is auto-received by `sendShielded` but never recorded
149
+ * in _coins so it would be stranded. Therefore, this circuit rejects sends to self.
150
+ *
145
151
  * @notice Access control is NOT enforced here.
146
152
  * The consuming contract must gate this behind its own
147
153
  * authorization policy.
148
154
  *
149
155
  * Requirements:
150
156
  *
157
+ * - `recipient` must not be this contract's own address.
151
158
  * - A coin of the given `color` must exist in the treasury.
152
159
  * - The coin's value must be >= `amount`.
153
160
  * - Send must not cause sent total overflow.
@@ -163,6 +170,11 @@ module ShieldedTreasury {
163
170
  color: Bytes<32>,
164
171
  amount: Uint<128>
165
172
  ): ShieldedSendResult {
173
+ // `kernel.self()` stays outside the `||`: the compiler treats a ledger
174
+ // read behind a witness-conditioned branch as an undeclared disclosure
175
+ const self = kernel.self();
176
+ assert(recipient.is_left || recipient.right != self,
177
+ "ShieldedTreasury: self-send");
166
178
  assert(_coins.member(disclose(color)), "ShieldedTreasury: no balance");
167
179
 
168
180
  const coin = _coins.lookup(disclose(color));
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (multisig/ShieldedTreasuryStateless.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (multisig/ShieldedTreasuryStateless.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -22,10 +22,16 @@ pragma language_version >= 0.26.0;
22
22
  * application time. Sufficiency is likewise enforced by `sendShielded` rather
23
23
  * than by an assert here.
24
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
25
+ * @notice "No ledger state" is not the same as "unobservable". A coin's
26
+ * nullifier is a hash of the coin and this contract's address, and
27
+ * `sendShielded` derives the nonces of the sent coin and of the change from a
28
+ * public hash of the spent coin's nonce. Whoever knows a deposited coin can
29
+ * therefore find the spend that consumes it, recover the amount sent (on a
30
+ * partial spend the change commitment has the value as its only unknown),
31
+ * test candidate recipients against the sent coin's commitment, and repeat
32
+ * from the change coin for every later spend descending from it. `_send`
33
+ * declares the spent coin's disclosure on the consumer's behalf. Consumers
34
+ * that need to make the trade-off themselves
29
35
  * should note it is only declared here for convenience — it could be moved to
30
36
  * the caller. Separately, see `_send`'s warning about its return value, which
31
37
  * carries coin plaintext.
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (multisig/Signer.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (multisig/Signer.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/UnshieldedTreasury.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (multisig/UnshieldedTreasury.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -61,6 +61,14 @@ module UnshieldedTreasury {
61
61
  * operation creates a zero-valued `_balances` entry, which
62
62
  * `getTokenBalance` reports the same as absence.
63
63
  *
64
+ * @warning Every non-zero deposit of a color rewrites the `_balances` entry
65
+ * that `_send` reads for that color, so a deposit that lands first
66
+ * invalidates every `_send` of the same color still waiting to be applied.
67
+ * A consumer that exposes `_deposit` without access control lets any
68
+ * depositor delay authorized sends for as long as they keep depositing; the
69
+ * sends must then be re-proven. No funds are lost. A zero-value deposit
70
+ * rewrites the entry with the same value and does not have this effect.
71
+ *
64
72
  * @notice Access control is NOT enforced here.
65
73
  * The consuming contract must gate this behind its own
66
74
  * authorization policy.
@@ -92,17 +100,16 @@ module UnshieldedTreasury {
92
100
  /**
93
101
  * @description Sends unshielded tokens from the treasury.
94
102
  *
95
- * @warning Recipients are not validated. An unspendable recipient (such as a
96
- * zero address) is accepted and permanently removes the funds from the
97
- * contract. This is not a sanctioned burn — no unshielded equivalent of
98
- * `shieldedBurnAddress` is exposed to contracts — so treat recipient
99
- * validation as the consuming contract's responsibility.
103
+ * @warning Recipients other than this contract are not validated. An
104
+ * unspendable recipient (such as a zero address) is accepted and
105
+ * permanently removes the funds from the contract. This is not a
106
+ * sanctioned burn (no unshielded equivalent of `shieldedBurnAddress` is
107
+ * exposed to contracts), so treat recipient validation as the consuming
108
+ * contract's responsibility.
100
109
  *
101
- * @warning Sending to this contract's own address zeroes the tracked balance
102
- * while the funds stay held and spendable at the protocol level, after which
103
- * `_send` will refuse to touch them. Recovery is only possible outside this
104
- * module: a consumer may reconcile the exported `_balances`, or spend the
105
- * funds through its own `sendUnshielded` call.
110
+ * @dev A self-addressed send is auto-received by `sendUnshielded` but
111
+ * still debited from `_balances`, so the funds would be stranded.
112
+ * Therefore, this circuit rejects sends to self.
106
113
  *
107
114
  * @notice Access control is NOT enforced here.
108
115
  * The consuming contract must gate this behind its own
@@ -110,6 +117,7 @@ module UnshieldedTreasury {
110
117
  *
111
118
  * Requirements:
112
119
  *
120
+ * - `recipient` must not be this contract's own address.
113
121
  * - The amount must be non-zero. The ledger rejects zero-value outputs.
114
122
  * - The treasury must hold sufficient balance for the given token color.
115
123
  *
@@ -124,6 +132,11 @@ module UnshieldedTreasury {
124
132
  color: Bytes<32>,
125
133
  amount: Uint<128>
126
134
  ): [] {
135
+ // `kernel.self()` stays outside the `||`: the compiler treats a ledger
136
+ // read behind a witness-conditioned branch as an undeclared disclosure
137
+ const self = kernel.self();
138
+ assert(!recipient.is_left || recipient.left != self,
139
+ "UnshieldedTreasury: self-send");
127
140
  assert(amount != 0, "UnshieldedTreasury: zero amount");
128
141
 
129
142
  const bal = getTokenBalance(color);
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (multisig/examples/ForwarderPrivateExample.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (multisig/examples/ForwarderPrivateExample.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -54,7 +54,8 @@ export circuit deposit(coin: ShieldedCoinInfo): [] {
54
54
  * @description Spends a previously-deposited shielded coin to the parent,
55
55
  * a coin public key. The caller proves knowledge of `(parent, opSecret)`
56
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`,
57
+ * parent stays hidden from anyone who holds no deposited coin (see the
58
+ * `ForwarderPrivate` module notice). If the input coin's value exceeds `value`,
58
59
  * change is re-emitted back to the contract for future drains.
59
60
  *
60
61
  * The parent is a `ZswapCoinPublicKey`, never a `ContractAddress`: a shielded
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.6 (multisig/examples/ForwarderShieldedExample.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (multisig/examples/ForwarderShieldedExample.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/examples/ForwarderUnshieldedExample.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (multisig/examples/ForwarderUnshieldedExample.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/examples/NativeShieldedTokenIssuerExample.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (multisig/examples/NativeShieldedTokenIssuerExample.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/examples/ShieldedMultiSigV2Example.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0 (multisig/examples/ShieldedMultiSigV2Example.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -42,7 +42,7 @@ export circuit deposit(coin: ShieldedCoinInfo): [] {
42
42
  }
43
43
 
44
44
  export circuit execute(
45
- to: ShieldedMultiSigV2_Proposal_Recipient,
45
+ to: ShieldedMultiSigV2_Recipient,
46
46
  amount: Uint<128>,
47
47
  coin: QualifiedShieldedCoinInfo,
48
48
  pubkeys: Vector<2, Secp256k1Point>,