@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.
- package/README.md +9 -5
- package/access/AccessControl.compact +13 -13
- package/access/Ownable.compact +15 -9
- package/access/ShieldedAccessControl.compact +25 -1
- package/access/ZOwnablePK.compact +8 -8
- package/crypto/EcdhMask.compact +1 -1
- package/crypto/Ecdsa.compact +4 -4
- package/crypto/Eip712.compact +168 -0
- package/crypto/ElGamal.compact +44 -3
- package/multisig/EcdsaSignerManager.compact +32 -15
- package/multisig/ForwarderPrivate.compact +11 -5
- package/multisig/ForwarderShielded.compact +2 -2
- package/multisig/ForwarderUnshielded.compact +2 -2
- package/multisig/ProposalManager.compact +194 -22
- package/multisig/ShieldedTreasury.compact +28 -1
- package/multisig/ShieldedTreasuryStateless.compact +1 -1
- package/multisig/Signer.compact +27 -79
- package/multisig/UnshieldedTreasury.compact +9 -1
- package/multisig/{presets/forwarder/ForwarderPrivate.compact → examples/ForwarderPrivateExample.compact} +3 -3
- package/multisig/{presets/forwarder/ForwarderShielded.compact → examples/ForwarderShieldedExample.compact} +3 -3
- package/multisig/{presets/forwarder/ForwarderUnshielded.compact → examples/ForwarderUnshieldedExample.compact} +3 -3
- package/multisig/examples/NativeShieldedTokenIssuerExample.compact +143 -0
- package/multisig/examples/ShieldedMultiSigV2Example.compact +77 -0
- package/multisig/presets/NativeShieldedTokenIssuer.compact +557 -0
- package/multisig/presets/ShieldedMultiSigV2.compact +251 -188
- package/package.json +15 -11
- package/security/Allowlist.compact +5 -5
- package/security/Blocklist.compact +5 -5
- package/security/Initializable.compact +8 -4
- package/security/Pausable.compact +10 -6
- package/token/ConfidentialFungibleToken.compact +337 -79
- package/token/FungibleToken.compact +35 -21
- package/token/MultiToken.compact +15 -15
- package/token/NativeShieldedToken.compact +65 -16
- package/token/NativeShieldedTokenCore.compact +57 -15
- package/token/NativeShieldedTokenFamily.compact +66 -16
- package/token/NonFungibleToken.compact +83 -34
- package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +11 -6
- package/token/extensions/NativeShieldedTokenDerivedNonce.compact +2 -2
- package/token/extensions/NativeShieldedTokenFamilyPublicSupply.compact +7 -7
- package/token/extensions/NativeShieldedTokenPublicSupply.compact +7 -7
- package/token/extensions/NativeShieldedTokenPublicSupplyCore.compact +5 -5
- package/utils/EvmAbi.compact +134 -0
- package/utils/Utils.compact +11 -1
- package/multisig/presets/ShieldedMultiSig.compact +0 -227
- 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.
|
|
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
|
|
17
|
-
* contracts own the digest: they compute the
|
|
18
|
-
* signed, then hand it to `assertApprovals` with
|
|
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
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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.
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
* @
|
|
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.
|
|
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
|
-
* @
|
|
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.
|
|
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
|
|
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
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
360
|
-
|
|
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.
|
|
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 ────────────────────────────────────────────────────
|