@openzeppelin/compact-contracts 0.3.0-alpha → 0.3.0-alpha.2
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 +7 -7
- package/access/AccessControl.compact +1 -1
- package/access/Ownable.compact +1 -1
- package/access/ShieldedAccessControl.compact +1 -1
- package/access/ZOwnablePK.compact +1 -1
- package/crypto/EcdhMask.compact +177 -0
- package/crypto/ElGamal.compact +1 -1
- package/multisig/ForwarderPrivate.compact +16 -12
- package/multisig/ForwarderShielded.compact +2 -2
- package/multisig/ForwarderUnshielded.compact +2 -2
- package/multisig/ProposalManager.compact +35 -1
- package/multisig/ShieldedTreasury.compact +46 -13
- package/multisig/ShieldedTreasuryStateless.compact +56 -23
- package/multisig/Signer.compact +45 -41
- package/multisig/UnshieldedTreasury.compact +44 -17
- package/multisig/presets/ShieldedMultiSig.compact +10 -19
- package/multisig/presets/ShieldedMultiSigV2.compact +2 -2
- package/multisig/presets/ShieldedMultiSigV3.compact +2 -2
- package/multisig/presets/forwarder/ForwarderPrivate.compact +1 -1
- package/multisig/presets/forwarder/ForwarderShielded.compact +1 -1
- package/multisig/presets/forwarder/ForwarderUnshielded.compact +1 -1
- package/package.json +32 -20
- package/security/Allowlist.compact +1 -1
- package/security/Blocklist.compact +1 -1
- package/security/Initializable.compact +1 -1
- package/security/Pausable.compact +1 -1
- package/token/ConfidentialFungibleToken.compact +1208 -0
- package/token/FungibleToken.compact +1 -1
- package/token/MultiToken.compact +1 -1
- package/token/NativeShieldedToken.compact +2 -2
- package/token/NativeShieldedTokenCore.compact +1 -1
- package/token/NativeShieldedTokenFamily.compact +2 -2
- package/token/NonFungibleToken.compact +1 -1
- package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +90 -0
- package/token/extensions/NativeShieldedTokenDerivedNonce.compact +70 -0
- package/token/extensions/NativeShieldedTokenFamilyPublicSupply.compact +145 -0
- package/token/extensions/NativeShieldedTokenPublicSupply.compact +142 -0
- package/token/extensions/NativeShieldedTokenPublicSupplyCore.compact +163 -0
- package/utils/Utils.compact +1 -1
- package/multisig/SignerManager.compact +0 -205
package/README.md
CHANGED
|
@@ -214,7 +214,7 @@ git clone git@github.com:OpenZeppelin/compact-contracts.git
|
|
|
214
214
|
```bash
|
|
215
215
|
nvm install && \
|
|
216
216
|
yarn && \
|
|
217
|
-
yarn
|
|
217
|
+
yarn compile
|
|
218
218
|
```
|
|
219
219
|
|
|
220
220
|
### Run tests
|
|
@@ -226,8 +226,8 @@ yarn test
|
|
|
226
226
|
### Check/apply Biome formatter
|
|
227
227
|
|
|
228
228
|
```bash
|
|
229
|
-
yarn
|
|
230
|
-
yarn
|
|
229
|
+
yarn lint
|
|
230
|
+
yarn lint:fix
|
|
231
231
|
```
|
|
232
232
|
|
|
233
233
|
### Advanced
|
|
@@ -235,8 +235,8 @@ yarn fmt-and-lint:fix
|
|
|
235
235
|
#### Targeted compilation
|
|
236
236
|
|
|
237
237
|
```bash
|
|
238
|
-
yarn
|
|
239
|
-
yarn
|
|
238
|
+
yarn compile:access
|
|
239
|
+
yarn compile:archive
|
|
240
240
|
...
|
|
241
241
|
```
|
|
242
242
|
|
|
@@ -246,10 +246,10 @@ ZK key generation is slow and usually unnecessary during development.
|
|
|
246
246
|
|
|
247
247
|
```bash
|
|
248
248
|
# Full compilation with skip-zk (use environment variable)
|
|
249
|
-
SKIP_ZK=true yarn
|
|
249
|
+
SKIP_ZK=true yarn compile
|
|
250
250
|
|
|
251
251
|
# Access compilation with skip-zk (this compiles security first as a dependency)
|
|
252
|
-
SKIP_ZK=true yarn
|
|
252
|
+
SKIP_ZK=true yarn compile:access
|
|
253
253
|
```
|
|
254
254
|
|
|
255
255
|
#### Clean environment
|
package/access/Ownable.compact
CHANGED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha.2 (crypto/EcdhMask.compact)
|
|
3
|
+
|
|
4
|
+
pragma language_version >= 0.23.0;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module EcdhMask
|
|
8
|
+
* @description Stateless ECDH hybrid encryption of a single field element to a
|
|
9
|
+
* Jubjub public key. Delivers a value to whoever holds the secret behind a
|
|
10
|
+
* Jubjub public key WITHOUT encoding it in the exponent, so recovery needs no
|
|
11
|
+
* discrete-log search and the value is unbounded within `Uint<128>`. This is the
|
|
12
|
+
* direct-decrypt counterpart to the homomorphic `crypto/ElGamal`: use ElGamal to
|
|
13
|
+
* accumulate ciphertexts, use this to hand a recipient the plaintext.
|
|
14
|
+
*
|
|
15
|
+
* Construction (recipient key `pk = g^ek`, ephemeral scalar `e` fresh per call):
|
|
16
|
+
* E = g^e (ephemeral public key)
|
|
17
|
+
* S = pk^e = g^{ek*e} (ECDH shared secret point)
|
|
18
|
+
* mask = KDF(S) (domain-separated Field, see `kdf`)
|
|
19
|
+
* ct = value + mask (field one-time-pad)
|
|
20
|
+
* Recipient recovers: S = E^ek, mask = KDF(S), value = ct - mask.
|
|
21
|
+
*
|
|
22
|
+
* @dev Freshness and secrecy precondition (CRITICAL). `e` MUST be both unique per
|
|
23
|
+
* call to the same recipient AND secret (unpredictable to observers), even across
|
|
24
|
+
* circuit invocations that reuse a randomness seed.
|
|
25
|
+
*
|
|
26
|
+
* Uniqueness: the mask is a one-time pad, so a repeated `e` repeats `S` and
|
|
27
|
+
* `mask`, and reusing a pad is catastrophic (subtracting two ciphertexts recovers
|
|
28
|
+
* both values).
|
|
29
|
+
*
|
|
30
|
+
* Secrecy: `recipientPk` is public, so anyone who learns `e` recomputes
|
|
31
|
+
* `S = recipientPk^e`, hence the mask, and opens the ciphertext. A unique but
|
|
32
|
+
* PREDICTABLE `e` (a plain counter, a public nonce) therefore breaks
|
|
33
|
+
* confidentiality completely. `e` must be high-entropy and secret, exactly like
|
|
34
|
+
* an ElGamal encryption randomness (see `crypto/ElGamal`), not merely distinct.
|
|
35
|
+
*
|
|
36
|
+
* The caller supplies `e` and owns both properties, e.g. by expanding a secret
|
|
37
|
+
* per-invocation seed bound to a per-recipient-unique nonce (a monotonic index).
|
|
38
|
+
* This is stronger than exponential ElGamal's uniqueness requirement (there,
|
|
39
|
+
* reuse merely leaks plaintext equality); do not treat `e` as "any randomness."
|
|
40
|
+
*
|
|
41
|
+
* @dev Weak inputs. Two degenerate inputs zero the shared secret and expose the
|
|
42
|
+
* value: `recipientPk = identity` gives `S = identity` for any `e`, and `e = 0`
|
|
43
|
+
* gives `S = identity` for any key. Both yield `mask = KDF(identity, domain)`, a
|
|
44
|
+
* value any observer can recompute, so `ct = value + mask` opens trivially. The
|
|
45
|
+
* identity is a valid subgroup point, so the runtime subgroup guarantee does NOT
|
|
46
|
+
* catch it; `encrypt` therefore asserts `recipientPk != identity` and
|
|
47
|
+
* `ephemeralPk (= g^e) != identity`. Guarding the ephemeral POINT rather than the
|
|
48
|
+
* scalar subsumes `e != 0` and stays correct regardless of how the runtime treats
|
|
49
|
+
* an out-of-range scalar: `g^e = identity` captures both `e = 0` and any
|
|
50
|
+
* `e ≡ 0 (mod ℓ)`. (Guarding `e != 0` directly would instead lean on the runtime
|
|
51
|
+
* faulting `ecMul` for scalars `≥ ℓ`; the point guard removes that dependency.)
|
|
52
|
+
* These are the only weak inputs (for a subgroup key, `S = identity` iff the
|
|
53
|
+
* ephemeral point is the identity).
|
|
54
|
+
*
|
|
55
|
+
* @dev Integrity. The pad is malleable and NO authentication is added here. This
|
|
56
|
+
* is safe in the intended on-chain use, where the ciphertext is computed
|
|
57
|
+
* in-circuit from a value the same circuit commits to elsewhere (a producer
|
|
58
|
+
* cannot desync the two) and is immutable once written (no in-transit attacker).
|
|
59
|
+
* A corrupted ciphertext could only fail the recipient's own later proof, never
|
|
60
|
+
* enable theft. Consumers with a different threat model (mutable storage,
|
|
61
|
+
* in-transit tampering) MUST add their own authentication.
|
|
62
|
+
*
|
|
63
|
+
* @dev Security. This is hashed ElGamal (ECIES without a MAC) over Jubjub and
|
|
64
|
+
* inherits its guarantees: IND-CPA under the Oracle Diffie-Hellman assumption in
|
|
65
|
+
* the random-oracle model (the `persistentHash` KDF keyed by the secret point
|
|
66
|
+
* `S`). It is NOT IND-CCA: the additive pad is malleable (see Integrity), which
|
|
67
|
+
* is acceptable here only because the on-chain ciphertext is bound in-circuit to
|
|
68
|
+
* the value it commits to elsewhere. Hiding carries a large margin: `kdf` returns
|
|
69
|
+
* a `degradeToTransient` output in `[0, 2^248)` while `value < 2^128`, so even
|
|
70
|
+
* treating the KDF output as uniform the value is hidden with about `2^-120`
|
|
71
|
+
* statistical slack, leaving only the assumption that the hash of the DH secret
|
|
72
|
+
* is PRF-like. Confidentiality relies on the runtime constraining every
|
|
73
|
+
* `JubjubPoint` into the prime-order subgroup (see `crypto/ElGamal`), so `S`
|
|
74
|
+
* cannot be a low-order point that leaks `ek`. Binding the ephemeral `E` into the
|
|
75
|
+
* KDF is deliberately omitted: for a fixed `recipientPk`, `E = g^e` and
|
|
76
|
+
* `S = pk^e` are both determined by `e`, so `KDF(S)` already varies with `E` and
|
|
77
|
+
* folding it in buys nothing.
|
|
78
|
+
*
|
|
79
|
+
* @dev Shared key with `crypto/ElGamal` (in scope). A consumer MAY use the same
|
|
80
|
+
* recipient Jubjub key for both these memos (hashed ElGamal) and exponential-
|
|
81
|
+
* ElGamal balance ciphertexts, as the confidential token does. This joint use is
|
|
82
|
+
* safe: the two schemes share only the public key, this KDF is domain-separated
|
|
83
|
+
* and keyed by the secret DH point, and neither exposes a decryption oracle, so
|
|
84
|
+
* under CPA / ODH a ciphertext of one scheme does not help attack the other.
|
|
85
|
+
*
|
|
86
|
+
* @dev No ledger state, no witnesses. Every circuit is pure and takes its keys,
|
|
87
|
+
* value, and randomness as explicit arguments. `decrypt` is exposed as a pure
|
|
88
|
+
* circuit for wallet/off-chain use (recovering a received value needs no proof);
|
|
89
|
+
* the on-chain path only ever calls `encrypt`.
|
|
90
|
+
*/
|
|
91
|
+
module EcdhMask {
|
|
92
|
+
import CompactStandardLibrary;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @description An ECDH one-time-pad ciphertext: the ephemeral public key and
|
|
96
|
+
* the field-masked value.
|
|
97
|
+
*/
|
|
98
|
+
export struct Ciphertext {
|
|
99
|
+
ephemeralPk: JubjubPoint;
|
|
100
|
+
ct: Field;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @description Derives the field mask from the ECDH shared secret point,
|
|
105
|
+
* domain-separated by a caller-supplied `domain` tag. Hashes the point to
|
|
106
|
+
* `Bytes<32>`, then re-hashes it with the domain tag and truncates into the
|
|
107
|
+
* field via `degradeToTransient`. The consumer chooses `domain` (so this
|
|
108
|
+
* module is not tied to any one protocol) and MUST use the same value on
|
|
109
|
+
* encrypt and decrypt.
|
|
110
|
+
*
|
|
111
|
+
* @dev Uses `persistentHash` deliberately: the recipient reproduces this mask
|
|
112
|
+
* to decrypt, possibly across a platform upgrade, so it must be upgrade-stable.
|
|
113
|
+
*
|
|
114
|
+
* @param sShared - The ECDH shared secret point (`pk^e` for the sender,
|
|
115
|
+
* `E^ek` for the recipient; equal by construction).
|
|
116
|
+
* @param domain - The consumer's domain-separation tag.
|
|
117
|
+
* @return The field mask.
|
|
118
|
+
*/
|
|
119
|
+
export pure circuit kdf(sShared: JubjubPoint, domain: Bytes<32>): Field {
|
|
120
|
+
const pointHash = persistentHash<JubjubPoint>(sShared);
|
|
121
|
+
return degradeToTransient(
|
|
122
|
+
persistentHash<Vector<2, Bytes<32>>>([pointHash, domain])
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @description Encrypts `value` to `recipientPk` under ephemeral scalar `e`.
|
|
128
|
+
*
|
|
129
|
+
* @notice `e` MUST be fresh per call (see the module freshness precondition).
|
|
130
|
+
*
|
|
131
|
+
* Requirements:
|
|
132
|
+
*
|
|
133
|
+
* - `recipientPk` is not the identity point.
|
|
134
|
+
* - `e` is non-zero, a valid Jubjub scalar (`< ℓ`), secret, and fresh per call.
|
|
135
|
+
*
|
|
136
|
+
* @param recipientPk - The recipient's Jubjub public key (`g^ek`).
|
|
137
|
+
* @param value - The value to deliver (any `Uint<128>`; no `2^48` bound).
|
|
138
|
+
* @param e - A fresh ephemeral scalar.
|
|
139
|
+
* @param domain - The consumer's domain-separation tag (must match decrypt).
|
|
140
|
+
* @return The ciphertext `{ ephemeralPk = g^e, ct = value + KDF(pk^e) }`.
|
|
141
|
+
*/
|
|
142
|
+
export pure circuit encrypt(
|
|
143
|
+
recipientPk: JubjubPoint,
|
|
144
|
+
value: Uint<128>,
|
|
145
|
+
e: Field,
|
|
146
|
+
domain: Bytes<32>
|
|
147
|
+
): Ciphertext {
|
|
148
|
+
const identity = ecMulGenerator(0 as Field);
|
|
149
|
+
assert(recipientPk != identity, "EcdhMask: identity pk");
|
|
150
|
+
const ephemeralPk = ecMulGenerator(e);
|
|
151
|
+
// Guard the ephemeral POINT, not the scalar: `ephemeralPk == identity`
|
|
152
|
+
// captures `e == 0` and any `e` that reduces to 0 mod l, so the guard is
|
|
153
|
+
// correct regardless of how the runtime treats an out-of-range scalar. This
|
|
154
|
+
// subsumes `e != 0` (see the weak-inputs note).
|
|
155
|
+
assert(ephemeralPk != identity, "EcdhMask: zero ephemeral");
|
|
156
|
+
const sShared = ecMul(recipientPk, e);
|
|
157
|
+
const mask = kdf(sShared, domain);
|
|
158
|
+
return Ciphertext { ephemeralPk: ephemeralPk, ct: (value as Field) + mask };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @description Recovers the value from a ciphertext using the recipient's
|
|
163
|
+
* secret scalar. Pure and off-chain (no proof needed to read a received value).
|
|
164
|
+
*
|
|
165
|
+
* @param ciphertext - The ciphertext to decrypt.
|
|
166
|
+
* @param ekScalar - The recipient's secret scalar (`crypto/ElGamal`'s
|
|
167
|
+
* `secretToScalar(EK)`).
|
|
168
|
+
* @param domain - The consumer's domain-separation tag (must match encrypt).
|
|
169
|
+
* @return The recovered value as a `Field` (equal to the original `value`,
|
|
170
|
+
* which is `< 2^128`).
|
|
171
|
+
*/
|
|
172
|
+
export pure circuit decrypt(ciphertext: Ciphertext, ekScalar: Field, domain: Bytes<32>): Field {
|
|
173
|
+
const sShared = ecMul(ciphertext.ephemeralPk, ekScalar);
|
|
174
|
+
const mask = kdf(sShared, domain);
|
|
175
|
+
return ciphertext.ct - mask;
|
|
176
|
+
}
|
|
177
|
+
}
|
package/crypto/ElGamal.compact
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/ForwarderPrivate.compact)
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha.2 (multisig/ForwarderPrivate.compact)
|
|
3
3
|
|
|
4
4
|
pragma language_version >= 0.23.0;
|
|
5
5
|
|
|
@@ -97,8 +97,12 @@ module ForwarderPrivate {
|
|
|
97
97
|
* @description Spends a previously-deposited shielded coin to the parent,
|
|
98
98
|
* a coin public key. The caller proves knowledge of `(parent, opSecret)`
|
|
99
99
|
* matching the stored commitment; the coin is sent as a plain shielded note,
|
|
100
|
-
* so the parent stays hidden on-chain. If `coin.value` exceeds `value`,
|
|
101
|
-
*
|
|
100
|
+
* so the parent stays hidden on-chain. If `coin.value` exceeds `value`,
|
|
101
|
+
* `sendShielded` routes the change back to this contract as a self-owned
|
|
102
|
+
* output; `_drain` returns it untouched via `result.change`. The
|
|
103
|
+
* implementing contract chooses what to do with it: leave it dwelling at
|
|
104
|
+
* the contract for a future drain, or spend it onward to a different
|
|
105
|
+
* recipient in the same transaction via `sendImmediateShielded`.
|
|
102
106
|
*
|
|
103
107
|
* The parent is a `ZswapCoinPublicKey`, never a `ContractAddress`: a shielded
|
|
104
108
|
* send to a contract publishes that contract's address in cleartext on the
|
|
@@ -106,7 +110,7 @@ module ForwarderPrivate {
|
|
|
106
110
|
* would defeat the private-parent guarantee. A coin public key is encrypted
|
|
107
111
|
* inside the Zswap output and never appears on the public transcript.
|
|
108
112
|
*
|
|
109
|
-
* @circuitInfo k=
|
|
113
|
+
* @circuitInfo k=15, rows=23937
|
|
110
114
|
*
|
|
111
115
|
* Requirements:
|
|
112
116
|
*
|
|
@@ -160,14 +164,14 @@ module ForwarderPrivate {
|
|
|
160
164
|
disclose(value)
|
|
161
165
|
);
|
|
162
166
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
// `sendShielded` already emits any change as a fresh output owned by this
|
|
168
|
+
// contract (dwelling at the contract address for future drains). `_drain`
|
|
169
|
+
// returns it untouched: it must not re-spend the change here and then hand
|
|
170
|
+
// that same (now-nullified) coin back via `result.change` — that is the
|
|
171
|
+
// double spend a node rejects on the next drain. A caller is free to spend
|
|
172
|
+
// the returned change onward itself (e.g. route it to another recipient);
|
|
173
|
+
// the rule is only that the change must never be re-spent AND still treated
|
|
174
|
+
// as held.
|
|
171
175
|
return result;
|
|
172
176
|
}
|
|
173
177
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/ForwarderShielded.compact)
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha.2 (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=
|
|
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 (multisig/ForwarderUnshielded.compact)
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha.2 (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=
|
|
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 (multisig/ProposalManager.compact)
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha.2 (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 (multisig/ShieldedTreasury.compact)
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha.2 (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
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
|
@@ -85,8 +102,18 @@ module ShieldedTreasury {
|
|
|
85
102
|
* @description Sends shielded tokens from the treasury.
|
|
86
103
|
*
|
|
87
104
|
* Looks up the stored coin by color, verifies sufficient value,
|
|
88
|
-
* and executes the shielded send.
|
|
89
|
-
*
|
|
105
|
+
* and executes the shielded send. `sendShielded` already routes any
|
|
106
|
+
* change back to this contract as a self-owned output. Unlike the
|
|
107
|
+
* stateless variant, this treasury RETAINS the change: it is recorded in
|
|
108
|
+
* the UTXO map for the next spend. The returned `result.change` therefore
|
|
109
|
+
* belongs to the treasury and must not be re-spent by the caller (doing
|
|
110
|
+
* so would double-spend the recorded coin).
|
|
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.
|
|
90
117
|
*
|
|
91
118
|
* @notice Access control is NOT enforced here.
|
|
92
119
|
* The consuming contract must gate this behind its own
|
|
@@ -119,14 +146,13 @@ module ShieldedTreasury {
|
|
|
119
146
|
|
|
120
147
|
const result = sendShielded(coin, disclose(recipient), disclose(amount));
|
|
121
148
|
|
|
149
|
+
// `sendShielded` spends the stored coin (revealing its nullifier) and,
|
|
150
|
+
// when there is change, emits it as a fresh output owned by this
|
|
151
|
+
// contract. That change coin is already spendable. Do NOT re-spend it
|
|
152
|
+
// with `sendImmediateShielded`, which would reveal its nullifier in this
|
|
153
|
+
// same transaction and leave the recorded coin permanently double-spent.
|
|
122
154
|
if (disclose(result.change.is_some)) {
|
|
123
|
-
|
|
124
|
-
sendImmediateShielded(
|
|
125
|
-
changeCoin,
|
|
126
|
-
Utils_selfAsRecipient(),
|
|
127
|
-
changeCoin.value
|
|
128
|
-
);
|
|
129
|
-
_coins.insertCoin(disclose(color), changeCoin, Utils_selfAsRecipient());
|
|
155
|
+
_coins.insertCoin(disclose(color), result.change.value, Utils_selfAsRecipient());
|
|
130
156
|
} else {
|
|
131
157
|
_coins.remove(disclose(color));
|
|
132
158
|
}
|
|
@@ -191,6 +217,13 @@ module ShieldedTreasury {
|
|
|
191
217
|
* and cumulative sent totals for a color. Should equal
|
|
192
218
|
* `getTokenBalance` if accounting is consistent.
|
|
193
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
|
+
*
|
|
194
227
|
* @param {Bytes<32>} color - The token color.
|
|
195
228
|
*
|
|
196
229
|
* @returns {Uint<128>} Received minus sent.
|
|
@@ -1,26 +1,41 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/ShieldedTreasuryStateless.compact)
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha.2 (multisig/ShieldedTreasuryStateless.compact)
|
|
3
3
|
|
|
4
4
|
pragma language_version >= 0.23.0;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* @module
|
|
8
|
-
* @description Manages shielded (private) token deposits
|
|
9
|
-
*
|
|
7
|
+
* @module ShieldedTreasuryStateless
|
|
8
|
+
* @description Manages shielded (private) token deposits and transfers for
|
|
9
|
+
* multisig governance contracts, without any ledger state.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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;
|
|
23
|
-
import { selfAsRecipient } from "../utils/Utils" prefix Utils_;
|
|
24
39
|
|
|
25
40
|
// ─── Deposit ────────────────────────────────────────────────────
|
|
26
41
|
|
|
@@ -38,10 +53,27 @@ module ShieldedTreasuryStateless {
|
|
|
38
53
|
*
|
|
39
54
|
* The coin is supplied by the caller rather than looked up from
|
|
40
55
|
* on-chain state; the token color is derived from `coin.color`.
|
|
41
|
-
* Executes the shielded send and
|
|
42
|
-
*
|
|
56
|
+
* Executes the shielded send and returns the result untouched.
|
|
57
|
+
* `sendShielded` already routes any change back to this contract as a
|
|
58
|
+
* self-owned output, so `result.change` is a live, spendable coin. The
|
|
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
|
|
61
|
+
* different recipient in the same transaction via `sendImmediateShielded`.
|
|
62
|
+
* (Re-spending change is only meaningful when it goes somewhere other than
|
|
63
|
+
* self — `sendShielded` already routes it to self.) No balance or
|
|
43
64
|
* received/sent accounting is tracked on the ledger.
|
|
44
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
|
+
*
|
|
45
77
|
* @notice Access control is NOT enforced here.
|
|
46
78
|
* The consuming contract must gate this behind its own
|
|
47
79
|
* authorization policy.
|
|
@@ -63,14 +95,15 @@ module ShieldedTreasuryStateless {
|
|
|
63
95
|
): ShieldedSendResult {
|
|
64
96
|
const result = sendShielded(disclose(coin), disclose(recipient), disclose(amount));
|
|
65
97
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
98
|
+
// `sendShielded` already emits any change as a fresh, self-owned output.
|
|
99
|
+
// `_send` returns it untouched: it must not re-spend the change here and
|
|
100
|
+
// then hand that same (now-nullified) coin back via `result.change`. That
|
|
101
|
+
// is the double spend a node rejects on the next spend. The implementing
|
|
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.
|
|
74
107
|
return result;
|
|
75
108
|
}
|
|
76
109
|
}
|