@openzeppelin/compact-contracts 0.3.0-alpha → 0.3.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +7 -7
  2. package/access/AccessControl.compact +1 -1
  3. package/access/Ownable.compact +1 -1
  4. package/access/ShieldedAccessControl.compact +1 -1
  5. package/access/ZOwnablePK.compact +1 -1
  6. package/crypto/EcdhMask.compact +177 -0
  7. package/crypto/ElGamal.compact +1 -1
  8. package/multisig/ForwarderPrivate.compact +15 -11
  9. package/multisig/ForwarderShielded.compact +1 -1
  10. package/multisig/ForwarderUnshielded.compact +1 -1
  11. package/multisig/ProposalManager.compact +1 -1
  12. package/multisig/ShieldedTreasury.compact +13 -10
  13. package/multisig/ShieldedTreasuryStateless.compact +16 -12
  14. package/multisig/Signer.compact +1 -1
  15. package/multisig/SignerManager.compact +1 -1
  16. package/multisig/UnshieldedTreasury.compact +1 -1
  17. package/multisig/presets/ShieldedMultiSig.compact +7 -16
  18. package/multisig/presets/ShieldedMultiSigV2.compact +1 -1
  19. package/multisig/presets/forwarder/ForwarderPrivate.compact +1 -1
  20. package/multisig/presets/forwarder/ForwarderShielded.compact +1 -1
  21. package/multisig/presets/forwarder/ForwarderUnshielded.compact +1 -1
  22. package/package.json +31 -19
  23. package/security/Allowlist.compact +1 -1
  24. package/security/Blocklist.compact +1 -1
  25. package/security/Initializable.compact +1 -1
  26. package/security/Pausable.compact +1 -1
  27. package/token/ConfidentialFungibleToken.compact +1180 -0
  28. package/token/FungibleToken.compact +1 -1
  29. package/token/MultiToken.compact +1 -1
  30. package/token/NativeShieldedToken.compact +1 -1
  31. package/token/NativeShieldedTokenCore.compact +1 -1
  32. package/token/NativeShieldedTokenFamily.compact +1 -1
  33. package/token/NonFungibleToken.compact +1 -1
  34. package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +90 -0
  35. package/token/extensions/NativeShieldedTokenDerivedNonce.compact +70 -0
  36. package/token/extensions/NativeShieldedTokenFamilySupply.compact +145 -0
  37. package/token/extensions/NativeShieldedTokenSupply.compact +142 -0
  38. package/token/extensions/NativeShieldedTokenSupplyCore.compact +163 -0
  39. package/utils/Utils.compact +1 -1
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 compact
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 fmt-and-lint
230
- yarn fmt-and-lint:fix
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 compact:access
239
- yarn compact:archive
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 compact
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 compact:access
252
+ SKIP_ZK=true yarn compile:access
253
253
  ```
254
254
 
255
255
  #### Clean environment
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (access/AccessControl.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (access/AccessControl.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (access/Ownable.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (access/Ownable.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (access/ShieldedAccessControl.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (access/ShieldedAccessControl.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (access/ZOwnablePK.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (access/ZOwnablePK.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -0,0 +1,177 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (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
+ }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (crypto/ElGamal.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (crypto/ElGamal.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/ForwarderPrivate.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (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`, the
101
- * change is re-emitted back to the contract for future drains.
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
@@ -160,14 +164,14 @@ module ForwarderPrivate {
160
164
  disclose(value)
161
165
  );
162
166
 
163
- if (disclose(result.change.is_some)) {
164
- sendImmediateShielded(
165
- disclose(result.change.value),
166
- Utils_selfAsRecipient(),
167
- disclose(result.change.value.value)
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.1 (multisig/ForwarderShielded.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/ForwarderUnshielded.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/ForwarderUnshielded.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/ProposalManager.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/ProposalManager.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/ShieldedTreasury.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/ShieldedTreasury.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -85,8 +85,12 @@ module ShieldedTreasury {
85
85
  * @description Sends shielded tokens from the treasury.
86
86
  *
87
87
  * Looks up the stored coin by color, verifies sufficient value,
88
- * and executes the shielded send. If the send produces change,
89
- * it is sent back to the contract via `sendImmediateShielded`.
88
+ * and executes the shielded send. `sendShielded` already routes any
89
+ * change back to this contract as a self-owned output. Unlike the
90
+ * stateless variant, this treasury RETAINS the change: it is recorded in
91
+ * the UTXO map for the next spend. The returned `result.change` therefore
92
+ * belongs to the treasury and must not be re-spent by the caller (doing
93
+ * so would double-spend the recorded coin).
90
94
  *
91
95
  * @notice Access control is NOT enforced here.
92
96
  * The consuming contract must gate this behind its own
@@ -119,14 +123,13 @@ module ShieldedTreasury {
119
123
 
120
124
  const result = sendShielded(coin, disclose(recipient), disclose(amount));
121
125
 
126
+ // `sendShielded` spends the stored coin (revealing its nullifier) and,
127
+ // when there is change, emits it as a fresh output owned by this
128
+ // contract. That change coin is already spendable. Do NOT re-spend it
129
+ // with `sendImmediateShielded`, which would reveal its nullifier in this
130
+ // same transaction and leave the recorded coin permanently double-spent.
122
131
  if (disclose(result.change.is_some)) {
123
- const changeCoin = result.change.value;
124
- sendImmediateShielded(
125
- changeCoin,
126
- Utils_selfAsRecipient(),
127
- changeCoin.value
128
- );
129
- _coins.insertCoin(disclose(color), changeCoin, Utils_selfAsRecipient());
132
+ _coins.insertCoin(disclose(color), result.change.value, Utils_selfAsRecipient());
130
133
  } else {
131
134
  _coins.remove(disclose(color));
132
135
  }
@@ -1,5 +1,5 @@
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.1 (multisig/ShieldedTreasuryStateless.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -20,7 +20,6 @@ pragma language_version >= 0.23.0;
20
20
  */
21
21
  module ShieldedTreasuryStateless {
22
22
  import CompactStandardLibrary;
23
- import { selfAsRecipient } from "../utils/Utils" prefix Utils_;
24
23
 
25
24
  // ─── Deposit ────────────────────────────────────────────────────
26
25
 
@@ -38,8 +37,14 @@ module ShieldedTreasuryStateless {
38
37
  *
39
38
  * The coin is supplied by the caller rather than looked up from
40
39
  * on-chain state; the token color is derived from `coin.color`.
41
- * Executes the shielded send and, if change is produced, returns it
42
- * to the contract via `sendImmediateShielded`. No balance or
40
+ * Executes the shielded send and returns the result untouched.
41
+ * `sendShielded` already routes any change back to this contract as a
42
+ * self-owned output, so `result.change` is a live, spendable coin. The
43
+ * implementing contract chooses what to do with it: persist it and pass
44
+ * it back as the `coin` for a later spend, or spend it onward to a
45
+ * different recipient in the same transaction via `sendImmediateShielded`.
46
+ * (Re-spending change is only meaningful when it goes somewhere other than
47
+ * self — `sendShielded` already routes it to self.) No balance or
43
48
  * received/sent accounting is tracked on the ledger.
44
49
  *
45
50
  * @notice Access control is NOT enforced here.
@@ -63,14 +68,13 @@ module ShieldedTreasuryStateless {
63
68
  ): ShieldedSendResult {
64
69
  const result = sendShielded(disclose(coin), disclose(recipient), disclose(amount));
65
70
 
66
- if (disclose(result.change.is_some)) {
67
- sendImmediateShielded(
68
- disclose(result.change.value),
69
- Utils_selfAsRecipient(),
70
- disclose(result.change.value.value)
71
- );
72
- }
73
-
71
+ // `sendShielded` already emits any change as a fresh, self-owned output.
72
+ // `_send` returns it untouched: it must not re-spend the change here and
73
+ // then hand that same (now-nullified) coin back via `result.change`. That
74
+ // is the double spend a node rejects on the next spend. The implementing
75
+ // contract decides what to do with the returned change: persist it and
76
+ // spend it later, or route it onward itself. The rule is only that the
77
+ // change must never be re-spent AND still treated as held.
74
78
  return result;
75
79
  }
76
80
  }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/Signer.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/Signer.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/SignerManager.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/SignerManager.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/UnshieldedTreasury.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/UnshieldedTreasury.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/presets/ShieldedMultiSig.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/presets/ShieldedMultiSig.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -187,17 +187,12 @@ export circuit getProposal(id: Uint<64>): Proposal_Proposal {
187
187
  return Proposal_getProposal(id);
188
188
  }
189
189
 
190
- export circuit getProposalRecipient(id: Uint<64>): Proposal_Recipient {
191
- return Proposal_getProposalRecipient(id);
192
- }
193
-
194
- export circuit getProposalAmount(id: Uint<64>): Uint<128> {
195
- return Proposal_getProposalAmount(id);
196
- }
197
-
198
- export circuit getProposalColor(id: Uint<64>): Bytes<32> {
199
- return Proposal_getProposalColor(id);
200
- }
190
+ // NOTE: the per-field proposal getters (getProposalRecipient / getProposalAmount
191
+ // / getProposalColor) and getReceivedMinusSent were dropped to bring the deploy
192
+ // transaction under the block-weight limit (the full 19-circuit deploy is
193
+ // rejected with "Transaction would exhaust the block limits"). They are all
194
+ // redundant: read the proposal fields via `getProposal(id).to / .amount / .color`,
195
+ // and the net balance via `getReceivedTotal(color) - getSentTotal(color)`.
201
196
 
202
197
  export circuit getProposalStatus(id: Uint<64>): Proposal_ProposalStatus {
203
198
  return Proposal_getProposalStatus(id);
@@ -217,10 +212,6 @@ export circuit getSentTotal(color: Bytes<32>): Uint<128> {
217
212
  return Treasury_getSentTotal(color);
218
213
  }
219
214
 
220
- export circuit getReceivedMinusSent(color: Bytes<32>): Uint<128> {
221
- return Treasury_getReceivedMinusSent(color);
222
- }
223
-
224
215
  // ISignerManager
225
216
 
226
217
  export circuit getSignerCount(): Uint<8> {
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/presets/ShieldedMultiSigV2.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/presets/ShieldedMultiSigV2.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/presets/forwarder/ForwarderPrivate.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/presets/forwarder/ForwarderPrivate.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/presets/forwarder/ForwarderShielded.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/presets/forwarder/ForwarderShielded.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/presets/forwarder/ForwarderUnshielded.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/presets/forwarder/ForwarderUnshielded.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openzeppelin/compact-contracts",
3
- "version": "0.3.0-alpha",
3
+ "version": "0.3.0-alpha.1",
4
4
  "description": "OpenZeppelin Compact contract library",
5
5
  "keywords": [
6
6
  "openzeppelin",
@@ -21,23 +21,24 @@
21
21
  "homepage": "https://docs.openzeppelin.com/contracts-compact/",
22
22
  "type": "module",
23
23
  "imports": {
24
- "#test-utils/address.js": "./test-utils/address.js"
24
+ "#test-utils/*": "./test-utils/*"
25
25
  },
26
26
  "scripts": {
27
- "compact": "compact-compiler --exclude '*/archive/*'",
28
- "compact:access": "compact-compiler --dir access",
29
- "compact:archive": "compact-compiler --dir archive",
30
- "compact:crypto": "compact-compiler --dir crypto",
31
- "compact:multisig": "compact-compiler --dir multisig",
32
- "compact:security": "compact-compiler --dir security",
33
- "compact:token": "compact-compiler --dir token",
34
- "compact:utils": "compact-compiler --dir utils",
27
+ "compile": "compact-compiler --exclude '*/archive/*'",
28
+ "compile:access": "compact-compiler --dir access",
29
+ "compile:archive": "compact-compiler --dir archive",
30
+ "compile:crypto": "compact-compiler --dir crypto",
31
+ "compile:multisig": "compact-compiler --dir multisig",
32
+ "compile:security": "compact-compiler --dir security",
33
+ "compile:token": "compact-compiler --dir token",
34
+ "compile:utils": "compact-compiler --dir utils",
35
+ "compile:integration": "SKIP_ZK=true compact-compiler --src test/integration/_mocks",
35
36
  "build": "compact-builder --hierarchical --out dist --clean-dist --exclude '*/archive/*' --exclude 'Mock*' --exclude '*.mock.compact' --copy package.json --copy ../README.md && find dist -type d -empty -delete",
36
- "test": "SKIP_ZK=true yarn run compact && vitest run",
37
- "test:coverage": "SKIP_ZK=true yarn run compact && vitest run --coverage",
38
- "test:live": "yarn run compact && MIDNIGHT_BACKEND=live vitest run --config vitest.live.config.ts",
39
- "compact:integration": "SKIP_ZK=true compact compile test/integration/_mocks/SharedInitCollision.compact artifacts/SharedInitCollision && SKIP_ZK=true compact compile test/integration/_mocks/ComposedTokens.compact artifacts/ComposedTokens",
40
- "test:integration": "yarn run compact:integration && vitest run --config vitest.integration.config.ts",
37
+ "test": "vitest run --project unit",
38
+ "test:coverage": "vitest run --project unit --coverage",
39
+ "test:integration": "vitest run --project integration",
40
+ "test:harness": "vitest run --project harness",
41
+ "test:harness:live": "MIDNIGHT_BACKEND=live vitest run --project harness-live",
41
42
  "types": "tsc -p tsconfig.json --noEmit",
42
43
  "clean": "git clean -fXd"
43
44
  },
@@ -48,13 +49,24 @@
48
49
  "@openzeppelin/compact-cli": "^0.0.2"
49
50
  },
50
51
  "devDependencies": {
52
+ "@midnight-ntwrk/compact-js": "2.5.1",
53
+ "@midnight-ntwrk/compact-runtime": "0.16.0",
54
+ "@midnight-ntwrk/ledger-v8": "8.1.0",
55
+ "@midnight-ntwrk/midnight-js-contracts": "4.1.1",
56
+ "@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.1.1",
57
+ "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.1.1",
58
+ "@midnight-ntwrk/midnight-js-network-id": "4.1.1",
59
+ "@midnight-ntwrk/midnight-js-node-zk-config-provider": "4.1.1",
60
+ "@midnight-ntwrk/midnight-js-protocol": "4.1.1",
61
+ "@midnight-ntwrk/midnight-js-types": "4.1.1",
62
+ "@midnight-ntwrk/testkit-js": "4.1.1",
51
63
  "@openzeppelin/compact-simulator": "^0.2.0",
52
64
  "@tsconfig/node24": "^24.0.4",
53
- "@types/node": "25.9.3",
54
- "@vitest/coverage-v8": "^4.1.9",
55
- "fast-check": "^4.8.0",
65
+ "@types/node": "26.1.1",
66
+ "@vitest/coverage-v8": "^4.1.10",
67
+ "fast-check": "^4.9.0",
56
68
  "ts-node": "^10.9.2",
57
69
  "typescript": "^6.0.3",
58
- "vitest": "^4.1.9"
70
+ "vitest": "^4.1.10"
59
71
  }
60
72
  }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (security/Allowlist.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (security/Allowlist.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (security/Blocklist.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (security/Blocklist.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (security/Initializable.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (security/Initializable.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-alpha (security/Pausable.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha.1 (security/Pausable.compact)
3
3
 
4
4
  pragma language_version >= 0.23.0;
5
5