@openzeppelin/compact-contracts 0.4.0-alpha.1 → 0.4.0-alpha.3

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 +9 -5
  2. package/access/AccessControl.compact +13 -13
  3. package/access/Ownable.compact +15 -9
  4. package/access/ShieldedAccessControl.compact +25 -1
  5. package/access/ZOwnablePK.compact +8 -8
  6. package/crypto/EcdhMask.compact +1 -1
  7. package/crypto/Ecdsa.compact +4 -4
  8. package/crypto/Eip712.compact +168 -0
  9. package/crypto/ElGamal.compact +44 -3
  10. package/multisig/EcdsaSignerManager.compact +32 -15
  11. package/multisig/ForwarderPrivate.compact +11 -5
  12. package/multisig/ForwarderShielded.compact +2 -2
  13. package/multisig/ForwarderUnshielded.compact +2 -2
  14. package/multisig/ProposalManager.compact +194 -22
  15. package/multisig/ShieldedTreasury.compact +28 -1
  16. package/multisig/ShieldedTreasuryStateless.compact +1 -1
  17. package/multisig/Signer.compact +27 -79
  18. package/multisig/UnshieldedTreasury.compact +9 -1
  19. package/multisig/{presets/forwarder/ForwarderPrivate.compact → examples/ForwarderPrivateExample.compact} +3 -3
  20. package/multisig/{presets/forwarder/ForwarderShielded.compact → examples/ForwarderShieldedExample.compact} +3 -3
  21. package/multisig/{presets/forwarder/ForwarderUnshielded.compact → examples/ForwarderUnshieldedExample.compact} +3 -3
  22. package/multisig/examples/NativeShieldedTokenIssuerExample.compact +143 -0
  23. package/multisig/examples/ShieldedMultiSigV2Example.compact +77 -0
  24. package/multisig/presets/NativeShieldedTokenIssuer.compact +557 -0
  25. package/multisig/presets/ShieldedMultiSigV2.compact +251 -188
  26. package/package.json +15 -11
  27. package/security/Allowlist.compact +5 -5
  28. package/security/Blocklist.compact +5 -5
  29. package/security/Initializable.compact +8 -4
  30. package/security/Pausable.compact +10 -6
  31. package/token/ConfidentialFungibleToken.compact +337 -79
  32. package/token/FungibleToken.compact +35 -21
  33. package/token/MultiToken.compact +15 -15
  34. package/token/NativeShieldedToken.compact +65 -16
  35. package/token/NativeShieldedTokenCore.compact +57 -15
  36. package/token/NativeShieldedTokenFamily.compact +66 -16
  37. package/token/NonFungibleToken.compact +83 -34
  38. package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +11 -6
  39. package/token/extensions/NativeShieldedTokenDerivedNonce.compact +2 -2
  40. package/token/extensions/NativeShieldedTokenFamilyPublicSupply.compact +7 -7
  41. package/token/extensions/NativeShieldedTokenPublicSupply.compact +7 -7
  42. package/token/extensions/NativeShieldedTokenPublicSupplyCore.compact +5 -5
  43. package/utils/EvmAbi.compact +134 -0
  44. package/utils/Utils.compact +11 -1
  45. package/multisig/presets/ShieldedMultiSig.compact +0 -227
  46. package/multisig/presets/ShieldedMultiSigV3.compact +0 -311
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.1 (multisig/EcdsaSignerManager.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.3 (multisig/EcdsaSignerManager.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -13,23 +13,34 @@ pragma language_version >= 0.26.0;
13
13
  * The salt is per-deployment, so the same public key yields a different
14
14
  * commitment in every instance and cannot be correlated across contracts.
15
15
  *
16
- * Wraps `Signer<Bytes<32>>` for the registry and threshold. Consuming
17
- * contracts own the digest: they compute the message hash their signers
18
- * signed, then hand it to `assertApprovals` with the parallel public-key and
19
- * signature vectors.
16
+ * Wraps `Signer<Bytes<32>>` for the registry and threshold and configures it
17
+ * from `initialize`. Consuming contracts own the digest: they compute the
18
+ * message hash their signers signed, then hand it to `assertApprovals` with
19
+ * the parallel public-key and signature vectors.
20
20
  *
21
- * The module's ledger is not exported, so a contract importing it under a
22
- * prefix keeps the salt and the signer registry out of its generated
23
- * `ledger()` reader.
21
+ * @notice Consuming contracts must not import `Signer` alongside this module.
22
+ * A second import instantiates a separate registry that `assertApprovals`
23
+ * never reads.
24
24
  */
25
25
  module EcdsaSignerManager {
26
26
  import CompactStandardLibrary;
27
27
  import "../crypto/Ecdsa" prefix Ecdsa_;
28
28
  import "./Signer"<Bytes<32>> prefix Signer_;
29
29
 
30
+ // Named import/re-export of the registry state only, so a consuming
31
+ // contract's ledger keys read as `_signers` rather than `Signer__signers`.
32
+ // The circuits stay behind the `Signer_` prefix.
33
+ import { _signers, _signerCount, _threshold } from "./Signer"<Bytes<32>>;
34
+ export { _signers, _signerCount, _threshold };
35
+
30
36
  // ─── State ──────────────────────────────────────────────────────────────────
31
37
 
32
- ledger _instanceSalt: Bytes<32>;
38
+ /**
39
+ * @description Deployment-supplied salt for signer commitments. Set at
40
+ * initialization and never changed. The registered commitments must have been
41
+ * derived under this same salt.
42
+ */
43
+ export sealed ledger _instanceSalt: Bytes<32>;
33
44
 
34
45
  // ─── Types ──────────────────────────────────────────────────────────────────
35
46
 
@@ -58,7 +69,8 @@ module EcdsaSignerManager {
58
69
 
59
70
  /**
60
71
  * @description Stores the instance salt and registers the initial signer set.
61
- * Call from the consuming contract's constructor only.
72
+ * Call from the consuming contract's constructor only. Writes the `sealed`
73
+ * salt, so it cannot be reached from an exported circuit.
62
74
  *
63
75
  * Requirements:
64
76
  *
@@ -79,9 +91,14 @@ module EcdsaSignerManager {
79
91
  signerCommitments: Vector<n, Bytes<32>>,
80
92
  threshold: Uint<8>
81
93
  ): [] {
94
+ assert(Signer_getSignerCount() == 0, "EcdsaSignerManager: signers already registered");
82
95
  _instanceSalt = disclose(instanceSalt);
83
96
  assert(threshold <= 2, "EcdsaSignerManager: threshold cannot exceed 2 (assertApprovals verifies 2 signatures)");
84
- Signer_initialize<n>(signerCommitments, threshold);
97
+
98
+ for (const commitment of signerCommitments) {
99
+ Signer__addSigner(commitment);
100
+ }
101
+ Signer__changeThreshold(threshold);
85
102
  }
86
103
 
87
104
  // ─── Commitments ────────────────────────────────────────────────────────────
@@ -125,7 +142,7 @@ module EcdsaSignerManager {
125
142
  * @dev Both entries are asserted, so the count handed to the threshold check
126
143
  * is always 2 and a threshold below 2 admits nothing extra.
127
144
  *
128
- * @circuitInfo k=17, rows=64916
145
+ * @constraints k=17, rows=64916 (zkir-v3)
129
146
  *
130
147
  * Requirements:
131
148
  *
@@ -196,7 +213,7 @@ module EcdsaSignerManager {
196
213
  /**
197
214
  * @description Returns the number of registered signers.
198
215
  *
199
- * @circuitInfo k=6, rows=34
216
+ * @constraints k=6, rows=34 (zkir-v3)
200
217
  *
201
218
  * @returns {Uint<8>} The signer count.
202
219
  */
@@ -207,7 +224,7 @@ module EcdsaSignerManager {
207
224
  /**
208
225
  * @description Returns the approval threshold.
209
226
  *
210
- * @circuitInfo k=6, rows=34
227
+ * @constraints k=6, rows=34 (zkir-v3)
211
228
  *
212
229
  * @returns {Uint<8>} The threshold.
213
230
  */
@@ -218,7 +235,7 @@ module EcdsaSignerManager {
218
235
  /**
219
236
  * @description Returns whether the given commitment is a registered signer.
220
237
  *
221
- * @circuitInfo k=8, rows=129
238
+ * @constraints k=8, rows=129 (zkir-v3)
222
239
  *
223
240
  * @param {Bytes<32>} commitment - The signer commitment to check.
224
241
  * @returns {Boolean} True if registered.
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.1 (multisig/ForwarderPrivate.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.3 (multisig/ForwarderPrivate.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -33,6 +33,12 @@ module ForwarderPrivate {
33
33
 
34
34
  // ─── State ──────────────────────────────────────────────────────
35
35
 
36
+ /**
37
+ * @description Commitment to `(parentAddr, opSecret)`, published in place of
38
+ * the parent itself. A caller proves knowledge of both. The parent key alone
39
+ * is not secret, so `opSecret` supplies the hiding: without it a candidate
40
+ * parent could be hashed and matched against this value.
41
+ */
36
42
  export ledger _parentCommitment: Bytes<32>;
37
43
 
38
44
  /**
@@ -76,7 +82,7 @@ module ForwarderPrivate {
76
82
  * @description Receives a shielded coin into the forwarder's custody.
77
83
  * No ledger write — coins dwell at the contract address until drained.
78
84
  *
79
- * @circuitInfo k=13, rows=6538
85
+ * @constraints k=13, rows=6522
80
86
  *
81
87
  * Requirements:
82
88
  *
@@ -110,7 +116,7 @@ module ForwarderPrivate {
110
116
  * would defeat the private-parent guarantee. A coin public key is encrypted
111
117
  * inside the Zswap output and never appears on the public transcript.
112
118
  *
113
- * @circuitInfo k=15, rows=23937
119
+ * @constraints k=15, rows=23837
114
120
  *
115
121
  * Requirements:
116
122
  *
@@ -121,7 +127,7 @@ module ForwarderPrivate {
121
127
  *
122
128
  * @param {QualifiedShieldedCoinInfo} coin - The coin to spend.
123
129
  * @param {ZswapCoinPublicKey} parent - The parent coin public key. Its 32
124
- * bytes are the preimage to the stored commitment.
130
+ * bytes are one half of the commitment preimage, alongside `opSecret`.
125
131
  * @param {Bytes<32>} opSecret - The operational secret. Never appears
126
132
  * on the public transcript.
127
133
  *
@@ -148,7 +154,7 @@ module ForwarderPrivate {
148
154
  // Reject a zero parent before the commitment gate.
149
155
  assert(!Utils_isKeyZero(parent), "ForwarderPrivate: zero parent");
150
156
 
151
- // Commitment gate — the preimage is the parent key's 32 bytes.
157
+ // Commitment gate. The preimage is the parent key's 32 bytes and `opSecret`.
152
158
  assert(
153
159
  _calculateParentCommitment(parent.bytes, opSecret) == _parentCommitment,
154
160
  "ForwarderPrivate: invalid parent"
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.1 (multisig/ForwarderShielded.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.3 (multisig/ForwarderShielded.compact)
3
3
 
4
4
  pragma language_version >= 0.26.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=24446
106
+ * @constraints k=15, rows=24373
107
107
  *
108
108
  * Requirements:
109
109
  *
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.1 (multisig/ForwarderUnshielded.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.3 (multisig/ForwarderUnshielded.compact)
3
3
 
4
4
  pragma language_version >= 0.26.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=10, rows=565
108
+ * @constraints k=10, rows=563
109
109
  *
110
110
  * Requirements:
111
111
  *
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.1 (multisig/ProposalManager.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.3 (multisig/ProposalManager.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -14,7 +14,7 @@ pragma language_version >= 0.26.0;
14
14
  * 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 status is
17
+ * every proposal's recipient address, token color, amount, and state is
18
18
  * readable off-chain, and `_nextProposalId` reveals how many have been created.
19
19
  * This holds regardless of which treasury a consumer composes: pairing this
20
20
  * module with `ShieldedTreasuryStateless` still publishes the full intent of
@@ -34,41 +34,145 @@ module ProposalManager {
34
34
  // ─── Types ──────────────────────────────────────────────────────
35
35
 
36
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`.
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
45
  */
42
46
  export enum ProposalStatus {
43
47
  Inactive,
44
48
  Active,
45
49
  Executed,
46
- Cancelled
50
+ Cancelled,
51
+ Expired
47
52
  }
48
53
 
54
+ /**
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
+ */
49
65
  export enum RecipientKind {
50
66
  ShieldedUser,
51
67
  UnshieldedUser,
52
68
  Contract
53
69
  }
54
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
+ */
55
77
  export struct Recipient {
56
78
  kind: RecipientKind,
57
79
  address: Bytes<32>
58
80
  }
59
81
 
82
+ /**
83
+ * @description A proposal. `state` holds both the lifecycle stage and the
84
+ * expiry in a single field:
85
+ *
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`.
101
+ *
102
+ * @notice This module implements no execution delay. `expiry` bounds how long
103
+ * a proposal stays valid; it does not impose a minimum wait before it may be
104
+ * executed. A consumer that needs a delay must compose one.
105
+ *
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.
112
+ */
60
113
  export struct Proposal {
61
114
  to: Recipient,
62
115
  color: Bytes<32>,
63
116
  amount: Uint<128>,
64
- status: ProposalStatus
117
+ state: Uint<64>
65
118
  }
66
119
 
67
120
  // ─── State ──────────────────────────────────────────────────────
68
121
 
122
+ /**
123
+ * @description Monotonic source of proposal ids. `_createProposal` increments
124
+ * it and reads the new value back to key `_proposals` so an id is never
125
+ * reused.
126
+ *
127
+ * @notice Reading the counter back is what serializes concurrent creates. The
128
+ * increment on its own would commute, but using the result as a map key turns
129
+ * creation into a pinned read-modify-write. Thus two creates landing in the same
130
+ * block cannot both succeed and the loser is rejected.
131
+ *
132
+ * @notice Readable off-chain without a proof and it discloses how many
133
+ * proposals the contract has ever created.
134
+ */
69
135
  export ledger _nextProposalId: Counter;
136
+
137
+ /**
138
+ * @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`.
141
+ * @type {Uint<64>} id - The proposal identifier, issued by `_nextProposalId`.
142
+ * @type {Proposal} proposal - The proposal record.
143
+ * @type {Map<id, proposal>}
144
+ * @type {Map<Uint<64>, Proposal>} _proposals
145
+ *
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.
148
+ * A consumer that needs cleanup must compose its own.
149
+ *
150
+ * @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`
153
+ */
70
154
  export ledger _proposals: Map<Uint<64>, Proposal>;
71
155
 
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
+
72
176
  // ─── Recipient Helpers ──────────────────────────────────────────
73
177
 
74
178
  /**
@@ -173,12 +277,23 @@ module ProposalManager {
173
277
  }
174
278
 
175
279
  /**
176
- * @description Asserts that a proposal exists and is active.
280
+ * @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
283
+ * distinguishes the two.
284
+ *
285
+ * @notice Expiry is terminal, unlike a conflicting read. A tx rejected
286
+ * for a stale read succeeds once rebuilt against fresh state. Rebuilding
287
+ * cannot help here: block time only moves forward, so an expired proposal
288
+ * stays expired. Check the proposal's status before re-proving.
177
289
  *
178
290
  * Requirements:
179
291
  *
180
292
  * - Proposal must exist.
181
- * - Proposal status must be Active.
293
+ * - Proposal must not be executed or cancelled.
294
+ * - Proposal must not have expired.
295
+ *
296
+ * @constraints k=9, rows=129
182
297
  *
183
298
  * @param {Uint<64>} id - The proposal ID.
184
299
  *
@@ -186,10 +301,16 @@ module ProposalManager {
186
301
  */
187
302
  export circuit assertProposalActive(id: Uint<64>): [] {
188
303
  assertProposalExists(id);
304
+
305
+ const state = _proposals.lookup(disclose(id)).state;
189
306
  assert(
190
- _proposals.lookup(disclose(id)).status == ProposalStatus.Active,
307
+ state > cancelledState(),
191
308
  "ProposalManager: proposal not active"
192
309
  );
310
+ assert(
311
+ blockTimeLt(state),
312
+ "ProposalManager: proposal expired"
313
+ );
193
314
  }
194
315
 
195
316
  // ─── Proposal Lifecycle ─────────────────────────────────────────
@@ -203,12 +324,27 @@ module ProposalManager {
203
324
  * - A `kind` the execution rail cannot accept (an `UnshieldedUser` recipient
204
325
  * on a shielded treasury, say) creates a proposal that passes here and then
205
326
  * reverts in `toShieldedRecipient` at execution. It stays `Active` and
206
- * unexecutable on that rail until `_cancelProposal` retires it.
327
+ * unexecutable on that rail until `_cancelProposal` retires it or its
328
+ * `expiry` lapses.
207
329
  * - A zero or otherwise unspendable `address` is accepted, and funds sent to
208
330
  * it on execution are unrecoverable.
209
331
  *
210
332
  * @notice `_nextProposalId` is incremented and then read back for the map key,
211
- * making this a pinned read-modify-write.
333
+ * making this a pinned read-modify-write. The `increment` alone would commute,
334
+ * but reading it back to key the map reintroduces the conflict, so two
335
+ * proposals created in the same block serialize and the loser is rejected.
336
+ *
337
+ * @notice `expiry` is mandatory and is only checked against the present: a
338
+ * proposal cannot be born expired. There is deliberately NO maximum horizon
339
+ * here, because the module cannot know what a sensible one is for a given
340
+ * deployment. A caller may therefore pass an effectively unreachable expiry,
341
+ * reintroducing the immortal proposal this field exists to prevent. Consuming
342
+ * contracts that need a bounded lifetime must impose their own cap.
343
+ *
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.
212
348
  *
213
349
  * @notice Access control is NOT enforced here.
214
350
  * The consuming contract must gate this behind its own
@@ -217,35 +353,52 @@ module ProposalManager {
217
353
  * Requirements:
218
354
  *
219
355
  * - `amount` must be greater than 0.
356
+ * - `expiry` must be greater than `cancelledState()`.
357
+ * - `expiry` must be in the future.
358
+ *
359
+ * @constraints k=10, rows=913
220
360
  *
221
361
  * @param {Recipient} to - The recipient (constructed via helper circuits).
222
362
  * @param {Bytes<32>} color - The token color.
223
363
  * @param {Uint<128>} amount - The amount to transfer.
364
+ * @param {Uint<64>} expiry - Seconds since the Unix epoch, at and after which
365
+ * the proposal is inert.
224
366
  *
225
367
  * @returns {Uint<64>} The new proposal ID.
226
368
  */
227
369
  export circuit _createProposal(
228
370
  to: Recipient,
229
371
  color: Bytes<32>,
230
- amount: Uint<128>
372
+ amount: Uint<128>,
373
+ expiry: Uint<64>
231
374
  ): Uint<64> {
232
375
  assert(amount > 0, "ProposalManager: zero amount");
376
+ assert(
377
+ expiry > cancelledState(),
378
+ "ProposalManager: expiry in reserved range"
379
+ );
380
+ assert(
381
+ blockTimeLt(disclose(expiry)),
382
+ "ProposalManager: expiry not in the future"
383
+ );
233
384
 
234
385
  _nextProposalId.increment(1);
235
386
  const id = _nextProposalId;
387
+ // Defense in depth
388
+ assert(!_proposals.member(id), "ProposalManager: id already used");
236
389
 
237
390
  _proposals.insert(id, disclose(Proposal {
238
391
  to: to,
239
392
  color: color,
240
393
  amount: amount,
241
- status: ProposalStatus.Active
394
+ state: expiry
242
395
  }));
243
396
 
244
397
  return id;
245
398
  }
246
399
 
247
400
  /**
248
- * @description Cancels a proposal.
401
+ * @description Cancels a proposal, retiring it before its expiry.
249
402
  *
250
403
  * @notice Access control is NOT enforced here.
251
404
  * The consuming contract must gate this behind its own
@@ -267,7 +420,7 @@ module ProposalManager {
267
420
  to: proposal.to,
268
421
  color: proposal.color,
269
422
  amount: proposal.amount,
270
- status: ProposalStatus.Cancelled
423
+ state: cancelledState()
271
424
  });
272
425
  }
273
426
 
@@ -294,14 +447,16 @@ module ProposalManager {
294
447
  to: proposal.to,
295
448
  color: proposal.color,
296
449
  amount: proposal.amount,
297
- status: ProposalStatus.Executed
450
+ state: executedState()
298
451
  });
299
452
  }
300
453
 
301
454
  // ─── View ───────────────────────────────────────────────────────
302
455
 
303
456
  /**
304
- * @description Returns the full proposal data.
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.
305
460
  *
306
461
  * @param {Uint<64>} id - The proposal ID.
307
462
  *
@@ -349,14 +504,31 @@ module ProposalManager {
349
504
  }
350
505
 
351
506
  /**
352
- * @description Returns the status of a proposal.
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.
510
+ *
511
+ * @constraints k=9, rows=252
353
512
  *
354
513
  * @param {Uint<64>} id - The proposal ID.
355
514
  *
356
515
  * @returns {ProposalStatus} The proposal status.
357
516
  */
358
517
  export circuit getProposalStatus(id: Uint<64>): ProposalStatus {
359
- assertProposalExists(id);
360
- return _proposals.lookup(disclose(id)).status;
518
+ if (!_proposals.member(disclose(id))) {
519
+ return ProposalStatus.Inactive;
520
+ }
521
+
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;
528
+ }
529
+ if (blockTimeLt(state)) {
530
+ return ProposalStatus.Active;
531
+ }
532
+ return ProposalStatus.Expired;
361
533
  }
362
534
  }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.1 (multisig/ShieldedTreasury.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.3 (multisig/ShieldedTreasury.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -41,8 +41,35 @@ module ShieldedTreasury {
41
41
 
42
42
  // ─── State ──────────────────────────────────────────────────────
43
43
 
44
+ /**
45
+ * @description The treasury's shielded holdings, one coin per token color.
46
+ * A deposit of an existing color merges into the stored coin rather than
47
+ * adding a second entry.
48
+ * @type {Bytes<32>} color - The token color.
49
+ * @type {QualifiedShieldedCoinInfo} coin - The coin the treasury holds of it.
50
+ * @type {Map<color, coin>}
51
+ * @type {Map<Bytes<32>, QualifiedShieldedCoinInfo>} _coins
52
+ */
44
53
  export ledger _coins: Map<Bytes<32>, QualifiedShieldedCoinInfo>;
54
+ /**
55
+ * @description Lifetime deposits per token color. Only ever increases, so it
56
+ * is a running total rather than a balance. `getTokenBalance` reads `_coins`
57
+ * and is the canonical balance; `getReceivedMinusSent` derives it from the
58
+ * totals and documents when that derivation holds.
59
+ * @type {Bytes<32>} color - The token color.
60
+ * @type {Uint<128>} total - The amount deposited over the contract's life.
61
+ * @type {Map<color, total>}
62
+ * @type {Map<Bytes<32>, Uint<128>>} _received
63
+ */
45
64
  export ledger _received: Map<Bytes<32>, Uint<128>>;
65
+ /**
66
+ * @description Lifetime payouts per token color. The counterpart to
67
+ * `_received`; also only ever increases.
68
+ * @type {Bytes<32>} color - The token color.
69
+ * @type {Uint<128>} total - The amount sent over the contract's life.
70
+ * @type {Map<color, total>}
71
+ * @type {Map<Bytes<32>, Uint<128>>} _sent
72
+ */
46
73
  export ledger _sent: Map<Bytes<32>, Uint<128>>;
47
74
 
48
75
  // ─── Deposit ────────────────────────────────────────────────────
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.1 (multisig/ShieldedTreasuryStateless.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.3 (multisig/ShieldedTreasuryStateless.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5