@openzeppelin/compact-contracts 0.5.0-alpha.1 → 0.5.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.
Files changed (51) hide show
  1. package/README.md +9 -4
  2. package/access/AccessControl.compact +8 -9
  3. package/access/Ownable.compact +19 -20
  4. package/access/ShieldedAccessControl.compact +1 -1
  5. package/access/ZOwnablePK.compact +3 -3
  6. package/crypto/Ecdh.compact +1 -1
  7. package/crypto/EcdhMask.compact +61 -22
  8. package/crypto/Ecdsa.compact +1 -1
  9. package/crypto/Eip712.compact +1 -1
  10. package/crypto/ElGamal.compact +13 -13
  11. package/math/UintN.compact +82 -0
  12. package/multisig/ForwarderPrivate.compact +1 -1
  13. package/multisig/ForwarderShielded.compact +1 -1
  14. package/multisig/ForwarderUnshielded.compact +1 -1
  15. package/multisig/ProposalManager.compact +3 -3
  16. package/multisig/Secp256k1EcdsaMultisig.compact +1 -1
  17. package/multisig/ShieldedTreasury.compact +1 -1
  18. package/multisig/ShieldedTreasuryStateless.compact +5 -11
  19. package/multisig/Signer.compact +1 -1
  20. package/multisig/UnshieldedTreasury.compact +1 -1
  21. package/multisig/examples/ForwarderPrivateExample.compact +1 -1
  22. package/multisig/examples/ForwarderShieldedExample.compact +1 -1
  23. package/multisig/examples/ForwarderUnshieldedExample.compact +1 -1
  24. package/multisig/examples/{ShieldedMultiSigV2Example.compact → NativeShieldedStatelessTreasuryExample.compact} +23 -23
  25. package/multisig/examples/NativeShieldedTokenIssuerExample.compact +1 -1
  26. package/multisig/presets/NativeShieldedStatelessTreasury.compact +443 -0
  27. package/multisig/presets/NativeShieldedTokenIssuer.compact +1 -1
  28. package/package.json +11 -11
  29. package/security/Allowlist.compact +1 -1
  30. package/security/Blocklist.compact +1 -1
  31. package/security/Initializable.compact +1 -1
  32. package/security/Pausable.compact +1 -1
  33. package/token/ConfidentialFungibleToken.compact +1 -1
  34. package/token/ConfidentialNoteFungibleToken.compact +1 -1
  35. package/token/FungibleToken.compact +34 -30
  36. package/token/MultiToken.compact +25 -21
  37. package/token/NativeShieldedToken.compact +2 -2
  38. package/token/NativeShieldedTokenCore.compact +1 -1
  39. package/token/NativeShieldedTokenFamily.compact +4 -4
  40. package/token/NonFungibleToken.compact +26 -22
  41. package/token/examples/ZOwnableNativeShieldedTokenExample.compact +1 -1
  42. package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +1 -1
  43. package/token/extensions/ConfidentialNoteFungibleTokenDelivery.compact +274 -0
  44. package/token/extensions/NativeShieldedTokenDerivedNonce.compact +1 -1
  45. package/token/extensions/NativeShieldedTokenFamilyPublicSupply.compact +1 -1
  46. package/token/extensions/NativeShieldedTokenPublicSupply.compact +1 -1
  47. package/token/extensions/NativeShieldedTokenPublicSupplyCore.compact +1 -1
  48. package/token/presets/ZOwnableNativeShieldedToken.compact +1 -1
  49. package/utils/EvmAbi.compact +1 -1
  50. package/utils/Utils.compact +1 -1
  51. package/multisig/presets/ShieldedMultiSigV2.compact +0 -289
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (multisig/UnshieldedTreasury.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (multisig/UnshieldedTreasury.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (multisig/examples/ForwarderPrivateExample.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (multisig/examples/ForwarderPrivateExample.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (multisig/examples/ForwarderShieldedExample.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (multisig/examples/ForwarderShieldedExample.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (multisig/examples/ForwarderUnshieldedExample.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (multisig/examples/ForwarderUnshieldedExample.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,11 +1,11 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (multisig/examples/ShieldedMultiSigV2Example.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (multisig/examples/NativeShieldedStatelessTreasuryExample.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
6
6
  /**
7
- * @title ShieldedMultiSigV2Example
8
- * @description Deployable reference contract for the `ShieldedMultiSigV2`
7
+ * @title NativeShieldedStatelessTreasuryExample
8
+ * @description Deployable reference contract for the `NativeShieldedStatelessTreasury`
9
9
  * preset: a 2-of-3 wallet whose spends are authorized by ECDSA signatures
10
10
  * collected off-chain and verified in a single transaction.
11
11
  *
@@ -15,14 +15,14 @@ pragma language_version >= 0.26.0;
15
15
 
16
16
  import CompactStandardLibrary;
17
17
 
18
- import "../presets/ShieldedMultiSigV2"<2> prefix ShieldedMultiSigV2_;
18
+ import "../presets/NativeShieldedStatelessTreasury"<2> prefix NativeShieldedStatelessTreasury_;
19
19
 
20
20
  export { ShieldedCoinInfo, QualifiedShieldedCoinInfo, ShieldedSendResult };
21
21
 
22
- // Surface the preset's domain separator unprefixed so `ledger()` readers see
23
- // the key name the preset published.
24
- import { _domainSeparator } from "../presets/ShieldedMultiSigV2"<2>;
25
- export { _domainSeparator };
22
+ // Surface the preset's replay counter and domain separator unprefixed so
23
+ // `ledger()` readers see the key names the preset published.
24
+ import { _nonce, _domainSeparator } from "../presets/NativeShieldedStatelessTreasury"<2>;
25
+ export { _nonce, _domainSeparator };
26
26
 
27
27
  /**
28
28
  * @description Deploys the wallet with 3 signer commitments.
@@ -34,42 +34,42 @@ constructor(
34
34
  instanceSalt: Bytes<32>,
35
35
  signerCommitments: Vector<3, Bytes<32>>
36
36
  ) {
37
- ShieldedMultiSigV2_initialize<3>(instanceSalt, signerCommitments);
37
+ NativeShieldedStatelessTreasury_initialize<3>(instanceSalt, signerCommitments);
38
+ }
39
+
40
+ export pure circuit _calculateSignerId(
41
+ pk: Secp256k1Point,
42
+ salt: Bytes<32>
43
+ ): Bytes<32> {
44
+ return NativeShieldedStatelessTreasury__calculateSignerId(pk, salt);
38
45
  }
39
46
 
40
47
  export circuit deposit(coin: ShieldedCoinInfo): [] {
41
- return ShieldedMultiSigV2_deposit(coin);
48
+ return NativeShieldedStatelessTreasury_deposit(coin);
42
49
  }
43
50
 
44
51
  export circuit execute(
45
- to: ShieldedMultiSigV2_Proposal_Recipient,
52
+ to: NativeShieldedStatelessTreasury_Proposal_Recipient,
46
53
  amount: Uint<128>,
47
54
  coin: QualifiedShieldedCoinInfo,
48
55
  pubkeys: Vector<2, Secp256k1Point>,
49
56
  signatures: Vector<2, Secp256k1EcdsaSignature>
50
57
  ): ShieldedSendResult {
51
- return ShieldedMultiSigV2_execute(to, amount, coin, pubkeys, signatures);
52
- }
53
-
54
- export pure circuit _calculateSignerId(
55
- pk: Secp256k1Point,
56
- salt: Bytes<32>
57
- ): Bytes<32> {
58
- return ShieldedMultiSigV2__calculateSignerId(pk, salt);
58
+ return NativeShieldedStatelessTreasury_execute(to, amount, coin, pubkeys, signatures);
59
59
  }
60
60
 
61
61
  export circuit getNonce(): Uint<64> {
62
- return ShieldedMultiSigV2_getNonce();
62
+ return NativeShieldedStatelessTreasury_getNonce();
63
63
  }
64
64
 
65
65
  export circuit getSignerCount(): Uint<8> {
66
- return ShieldedMultiSigV2_getSignerCount();
66
+ return NativeShieldedStatelessTreasury_getSignerCount();
67
67
  }
68
68
 
69
69
  export circuit getThreshold(): Uint<8> {
70
- return ShieldedMultiSigV2_getThreshold();
70
+ return NativeShieldedStatelessTreasury_getThreshold();
71
71
  }
72
72
 
73
73
  export circuit isSigner(commitment: Bytes<32>): Boolean {
74
- return ShieldedMultiSigV2_isSigner(commitment);
74
+ return NativeShieldedStatelessTreasury_isSigner(commitment);
75
75
  }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (multisig/examples/NativeShieldedTokenIssuerExample.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (multisig/examples/NativeShieldedTokenIssuerExample.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -0,0 +1,443 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (multisig/presets/NativeShieldedStatelessTreasury.compact)
3
+
4
+ pragma language_version >= 0.26.0;
5
+
6
+ /**
7
+ * @module NativeShieldedStatelessTreasury
8
+ * @description
9
+ * A threshold ECDSA multisig treasury for shielded coins. Anyone may
10
+ * deposit; value leaves only through `execute`, a single tx carrying `t`
11
+ * registered signers' approvals. There is no proposal lifecycle: signatures
12
+ * are collected off-chain and verified in that one tx.
13
+ *
14
+ * Composes `Secp256k1EcdsaMultisig` (signer commitments, approval checks),
15
+ * `ShieldedTreasuryStateless` as `Treasury_` (deposit and send, no coin
16
+ * state on the ledger) and `ProposalManager` as `Proposal_` for its
17
+ * `Recipient` and `RecipientKind` types alone.
18
+ *
19
+ * @dev Notation:
20
+ * - `H<T>(...)`: `persistentHash<T>` (SHA-256) over the Compact encoding of
21
+ * `T`, a `Bytes<32>` digest.
22
+ * - `keccak(...)`: `keccak256` over the concatenation of its 32-byte words.
23
+ * - `DS`: `_domainSeparator`, the EIP-712 domain separator fixed at
24
+ * construction.
25
+ * - `self`: `kernel.self().bytes`, this contract's address.
26
+ * - `nonce`: the `_nonce` value read at the start of `execute`, the
27
+ * signature replay counter.
28
+ * - `cm`: a signer commitment, the registry key.
29
+ *
30
+ * @dev Derivations:
31
+ * - Domain separator: `DS = Eip712_domainSeparator(
32
+ * keccak("NativeShieldedStatelessTreasury"), keccak("1"), instanceSalt)`,
33
+ * the EIP-712 domain `(name, version, salt)`. `chainId` and
34
+ * `verifyingContract` are absent: no network id is available in-circuit
35
+ * and a 32-byte address does not fit `address`. `self` inside the struct
36
+ * separates deployments instead.
37
+ * - Execute digest: `keccak(0x1901 ‖ DS ‖ keccak([EXECUTE_TYPEHASH, self,
38
+ * uint64Word(nonce), uint8Word(to.kind), to.address, coin.color,
39
+ * uint128Word(amount)]))`.
40
+ * - Type hash: `EXECUTE_TYPEHASH = keccak("Execute(bytes32 contractAddress,
41
+ * uint256 nonce,uint8 recipientKind,bytes32 recipient,bytes32 coinColor,
42
+ * uint256 amount)")`, written as a literal word.
43
+ * - Signer commitment: `cm = H<SignerCommitmentInput>({ pkX, pkY, salt,
44
+ * domain: pad(32, "Secp256k1EcdsaMultisig:signer") })`, `pkX` / `pkY`
45
+ * the affine coordinates as `Bytes<32>`. `_calculateSignerId` is the
46
+ * exported pure circuit.
47
+ * - Encoding: `H` hashes the Compact encoding, not a byte concatenation.
48
+ * Digest integers are big-endian ABI words (`utils/EvmAbi`), so an EVM
49
+ * signer's typed-data encoder reproduces them.
50
+ * `contracts/src/multisig/test/EcdsaTestUtils.ts` pins the digest through
51
+ * ethers (`executeMsgHash`).
52
+ *
53
+ * @notice Implementing contracts register the signer set from their
54
+ * constructor by calling this module's `initialize`, and MUST NOT import
55
+ * `Signer` or `Secp256k1EcdsaMultisig` directly.
56
+ *
57
+ * @dev Parameterization:
58
+ * `t` is the approval width and the threshold, fixed at import
59
+ * (`"../presets/NativeShieldedStatelessTreasury"<t>`): `execute` verifies
60
+ * exactly `t` distinct signers. The implementing contract fixes the signer
61
+ * count `n` at `initialize`. Import one width per contract: two widths share
62
+ * one signer registry, see `Secp256k1EcdsaMultisig`.
63
+ * `NativeShieldedStatelessTreasuryExample` fixes 2-of-3. The `@constraints`
64
+ * tag on `execute` is measured at `t = 2`.
65
+ *
66
+ * @dev Compatibility:
67
+ * The wire format the signers implement is the EIP-712 domain
68
+ * (`NativeShieldedStatelessTreasury`, version `1`, salt), the `Execute` type
69
+ * string, the `Secp256k1EcdsaMultisig:signer` commitment domain (owned by
70
+ * `Secp256k1EcdsaMultisig`), the `cm` preimage struct, and the `Vector<t>` /
71
+ * `Vector<n>` shapes the implementing contract fixes. Changing any of them
72
+ * breaks every existing signer setup. The test suite pins them through
73
+ * `EcdsaTestUtils.ts`.
74
+ *
75
+ * @notice Privacy:
76
+ * - Public, per tx:
77
+ * - Entry point: `deposit` or `execute`.
78
+ * - Approvers: the `t` signer commitments (`disclose(commitment)` in
79
+ * `Signer_assertSigner`), so WHICH signers approved.
80
+ * - Deposit: the coin commitment as a receive claim, with this contract
81
+ * named in the Zswap output (`disclose(coin)` in `Treasury__deposit`).
82
+ * - Execute: the spent coin's nullifier and the sent and change coin
83
+ * commitments. The returned `ShieldedSendResult` stays with the caller.
84
+ * - Public, as state:
85
+ * - Cells: `_nonce`, `_domainSeparator`, and the `Secp256k1EcdsaMultisig`
86
+ * cells (`_signers`, `_signerCount`, `_threshold`, `_instanceSalt`)
87
+ * behind its prefix. `_nonce` and `_domainSeparator` are exported, so an
88
+ * implementing contract can surface them in its `ledger()` reader, as
89
+ * the example does; the rest are read through the view circuits.
90
+ * - Absent: no coin map, no balances, no received or sent totals.
91
+ * - Private:
92
+ * - Signer material: public keys and signatures are verified in-circuit,
93
+ * never disclosed.
94
+ * - Operation data: the amount and the recipient.
95
+ * - Signer identity: keys sit behind `cm` with a public salt, so an observer
96
+ * holding a candidate key can test membership; one without cannot
97
+ * correlate a signer across deployments.
98
+ * - Auditability: no balance is stored, but a coin commitment is a hash of
99
+ * the coin's data, so whoever knows a held coin can link its receive to
100
+ * its later spend. The change coin's nonce derives from the spent coin's,
101
+ * so that observer can follow the change too.
102
+ *
103
+ * @notice Security:
104
+ * - Digest binding: the `Execute` struct binds its type hash, `self`,
105
+ * `nonce`, the recipient kind and address, `coin.color` and `amount` under
106
+ * `DS`, so a signature authorizes one spend on one instance at one nonce,
107
+ * and is neither replayable nor redirectable. Removing the `self` word
108
+ * removes cross-instance replay protection, since `DS` cannot carry the
109
+ * address.
110
+ * - Low-s: verification goes through `Ecdsa_secp256k1EcdsaVerifyLowS`, so a
111
+ * high-s encoding is an invalid signature.
112
+ * - Distinct signers: `assertApprovals` takes exactly `t` keys and rejects a
113
+ * repeated commitment, so no set of fewer than `t` registered signers
114
+ * moves value. `initialize` rejects `n < t`.
115
+ * - Recipient kind: `execute` rejects any recipient but a `ShieldedUser`. A
116
+ * contract-addressed coin would be unclaimable, making the transaction
117
+ * malformed.
118
+ * - Open deposits: `deposit` has no access control, by design. Only
119
+ * `execute` moves value out.
120
+ * - No coin bookkeeping: with no coin registry, a double spend or a coin the
121
+ * contract does not own is rejected by the protocol at application time
122
+ * rather than by an assert here. Sufficiency (`coin.value >= amount`) is
123
+ * likewise left to `sendShielded`.
124
+ * - Immutable signer set: no signer reconfiguration circuit is exported, so
125
+ * the commitments and the threshold are fixed. Losing more than `n - t`
126
+ * keys freezes spending.
127
+ * - Atomicity: a failed assert rejects the whole tx before any state moves;
128
+ * the nonce does not advance and the sender pays no fee.
129
+ *
130
+ * @notice Concurrency:
131
+ * - Replay nonce: `execute` reads `_nonce` into the digest. The read is
132
+ * pinned, so any two executes in one block conflict: at most one lands per
133
+ * block, and the losing signers re-sign at the next nonce.
134
+ * - `getNonce`: submitted as a tx it pins `_nonce` and conflicts with a
135
+ * same-block `execute`.
136
+ * - Deposits: `deposit` writes no ledger state, so it commutes with
137
+ * everything, a same-block `execute` included.
138
+ * - Views: the signer views read only state written by `initialize`, so they
139
+ * commute with everything.
140
+ *
141
+ * @notice Scope:
142
+ * Deposit and send only. No mint, no burn, no unshielded rail, and no
143
+ * on-chain proposal: `ProposalManager` is imported for `Proposal_Recipient`
144
+ * and `Proposal_RecipientKind` only, while its `_proposals` map and
145
+ * `_nextProposalId` are allocated by the import and never written. `execute`
146
+ * checks `to.kind` itself rather than calling `toShieldedRecipient`. Coins
147
+ * the treasury holds are ordinary Zswap coins it neither tracks nor
148
+ * restricts.
149
+ *
150
+ * @notice Client-side duties:
151
+ * - Before deploy: pick a random `instanceSalt`, derive each `cm` with
152
+ * `_calculateSignerId(pk, salt)`, pass them as `signerCommitments`.
153
+ * - Per operation: read `getNonce()`, rebuild the digest as
154
+ * `EcdsaTestUtils.ts` does, collect `t` low-s signatures off-chain, submit
155
+ * keys and signatures in matching `Vector<t>` positions.
156
+ * - Coin tracking: keep the treasury's coins off-chain, recovering each
157
+ * coin's `mt_index` from the indexer's `ZswapOutput` / ledger events to
158
+ * build the `QualifiedShieldedCoinInfo` that `execute` takes.
159
+ * - Change coin: persist the one `execute` returns. `sendShielded` emits it
160
+ * as a fresh self-owned output, and it replaces the spent coin as the
161
+ * treasury's holding.
162
+ *
163
+ * @notice Scalability:
164
+ * - Throughput: one `execute` per block (see Concurrency). Deposits write no
165
+ * ledger state, so they do not contend.
166
+ * - State: constant-size, one counter, the domain separator and an
167
+ * `n`-entry signer set. Nothing grows per deposit or spend.
168
+ * - Coins: no merge circuit, so the off-chain pool gains one coin per
169
+ * deposit and `execute` spends exactly one of them. A spend larger than
170
+ * the largest single coin is not expressible.
171
+ * - Amounts: `execute` takes `Uint<128>`.
172
+ *
173
+ * @dev Composition:
174
+ * - On-chain balances: swap `ShieldedTreasuryStateless` for
175
+ * `ShieldedTreasury` to get one merged UTXO per color and cumulative
176
+ * totals, both published.
177
+ * - Unshielded rail: pair `Proposal_toUnshieldedRecipient` with
178
+ * `UnshieldedTreasury`; this preset wires only the shielded one.
179
+ * - Other thresholds: import at another `t` (see Parameterization).
180
+ * - Proposal lifecycle: wire in the rest of `ProposalManager` for on-chain
181
+ * proposals, at the cost its own `@warning` names: every proposal's
182
+ * recipient, color and amount becomes public.
183
+ *
184
+ * @notice Requires ZKIR v3: `keccak256` is unavailable on the v2 backend.
185
+ */
186
+ module NativeShieldedStatelessTreasury<#t> {
187
+ import CompactStandardLibrary;
188
+
189
+ import "../Secp256k1EcdsaMultisig"<t> prefix Secp256k1EcdsaMultisig_;
190
+ import "../../crypto/Eip712" prefix Eip712_;
191
+ import "../../utils/EvmAbi" prefix EvmAbi_;
192
+ import "../ProposalManager" prefix Proposal_;
193
+ import "../ShieldedTreasuryStateless" prefix Treasury_;
194
+
195
+ // Implementing contracts need this for the re-exported `execute` signature.
196
+ // Importing `ProposalManager` again instead would give them a second,
197
+ // unwritten copy of its state.
198
+ export { Proposal_Recipient };
199
+
200
+ /**
201
+ * @description Signature replay counter. `execute` reads it into its
202
+ * digest and increments it; `getNonce` returns the value the next digest
203
+ * binds to. The read is pinned, so at most one `execute` lands per block.
204
+ * Exported so the implementing contract's `ledger()` reader can surface it.
205
+ */
206
+ export ledger _nonce: Counter;
207
+
208
+ /**
209
+ * @description The EIP-712 domain separator every `execute` digest is built
210
+ * under, derived from `(name, version, instanceSalt)`. Set once at
211
+ * construction.
212
+ */
213
+ export sealed ledger _domainSeparator: Bytes<32>;
214
+
215
+ /**
216
+ * @description Registers the signer set at threshold `t` and fixes the
217
+ * EIP-712 domain separator.
218
+ *
219
+ * @notice Call from the implementing contract's constructor only. It writes
220
+ * the `sealed` `_domainSeparator`, so implementing contracts MUST NOT expose
221
+ * `initialize` as a post-deploy circuit.
222
+ *
223
+ * The EIP-712 domain fixed here is `(name, version, salt)`. `chainId` and
224
+ * `verifyingContract` are absent: no network identifier is available
225
+ * in-circuit, and a 32-byte Midnight address does not fit
226
+ * `verifyingContract`'s `address` type.
227
+ *
228
+ * Replay protection comes from `execute` binding `kernel.self()` in its
229
+ * struct rather than from the domain. The ledger randomizes a contract's
230
+ * address at deployment, so no two deployments share one on any network.
231
+ * A signature for one cannot authorize another.
232
+ *
233
+ * `instanceSalt` also keys the signer commitments, so a fresh value per
234
+ * deployment keeps one public key from producing the same commitment in two
235
+ * deployments which would link the signer set across them.
236
+ *
237
+ * Requirements:
238
+ *
239
+ * - `instanceSalt` should be cryptographically random.
240
+ * - `signerCommitments` must not contain duplicates.
241
+ * - `n` must be at least `t`.
242
+ *
243
+ * @param {Bytes<32>} instanceSalt - Per-deployment salt for commitment derivation.
244
+ * @param {Vector<n, Bytes<32>>} signerCommitments - Hashed signer identities.
245
+ * @returns {[]} Empty tuple.
246
+ */
247
+ export circuit initialize<#n>(
248
+ instanceSalt: Bytes<32>,
249
+ signerCommitments: Vector<n, Bytes<32>>
250
+ ): [] {
251
+ Secp256k1EcdsaMultisig_initialize<n>(instanceSalt, signerCommitments);
252
+
253
+ const hashedName = keccak256<Bytes<31>>(pad(31, "NativeShieldedStatelessTreasury"));
254
+ const hashedVersion = keccak256<Bytes<1>>(pad(1, "1"));
255
+ _domainSeparator = Eip712_domainSeparator(
256
+ hashedName,
257
+ hashedVersion,
258
+ disclose(instanceSalt)
259
+ );
260
+ }
261
+
262
+ /**
263
+ * @description `keccak256` of the `Execute` type string:
264
+ * `Execute(bytes32 contractAddress,uint256 nonce,uint8 recipientKind,bytes32 recipient,bytes32 coinColor,uint256 amount)`
265
+ *
266
+ * @dev Written as a literal word. `compactc` emits a full keccak
267
+ * permutation even for a constant argument, so hashing the type string
268
+ * in-circuit would add ~4.2k rows to every `execute`. The spec checks this
269
+ * word against the type string above.
270
+ *
271
+ * @returns {Bytes<32>} The `Execute` type hash.
272
+ */
273
+ pure circuit EXECUTE_TYPEHASH(): Bytes<32> {
274
+ const typeHash: Vector<32, Uint<8>> = [
275
+ 0x8e, 0x36, 0xc9, 0x78, 0xf2, 0x00, 0x15, 0xe8,
276
+ 0x1d, 0x7a, 0x53, 0xef, 0xc3, 0x2e, 0x2b, 0x08,
277
+ 0xf1, 0x24, 0x97, 0xa5, 0x05, 0x7c, 0x70, 0x20,
278
+ 0x7f, 0x8b, 0xc4, 0xb7, 0xc6, 0xcf, 0x5e, 0x15
279
+ ];
280
+
281
+ return typeHash as Bytes<32>;
282
+ }
283
+
284
+ /**
285
+ * @description Computes a signer commitment from a secp256k1 public key,
286
+ * as `persistentHash(pkX, pkY, salt, "Secp256k1EcdsaMultisig:signer")`.
287
+ *
288
+ * Pure, so the deployer can derive constructor commitments off-chain.
289
+ *
290
+ * Requirements:
291
+ *
292
+ * - `pk` must not be the secp256k1 identity point, which has no coordinates.
293
+ *
294
+ * @param {Secp256k1Point} pk - The secp256k1 public key.
295
+ * @param {Bytes<32>} salt - The instance salt.
296
+ *
297
+ * @returns {Bytes<32>} The signer commitment.
298
+ */
299
+ export pure circuit _calculateSignerId(
300
+ pk: Secp256k1Point,
301
+ salt: Bytes<32>
302
+ ): Bytes<32> {
303
+ return Secp256k1EcdsaMultisig_calculateSignerId(pk, salt);
304
+ }
305
+
306
+ /**
307
+ * @description Receives a shielded coin into the multisig treasury.
308
+ *
309
+ * No access control which allows anyone to deposit. The coin is claimed at the
310
+ * protocol level through receiveShielded. No coin data is stored on the
311
+ * public ledger, preserving full balance privacy.
312
+ *
313
+ * The operator discovers the coin's Merkle tree index by subscribing
314
+ * to ZswapOutput events via the indexer, filtering by contract address,
315
+ * and extracting mt_index. Combined with the known ShieldedCoinInfo,
316
+ * this produces the QualifiedShieldedCoinInfo needed for spending.
317
+ *
318
+ * @constraints k=13, rows=5898 (zkir-v3)
319
+ *
320
+ * @param {ShieldedCoinInfo} coin - The incoming shielded coin.
321
+ */
322
+ export circuit deposit(coin: ShieldedCoinInfo): [] {
323
+ Treasury__deposit(coin);
324
+ }
325
+
326
+ /**
327
+ * @description Executes a shielded send authorized by threshold signatures.
328
+ *
329
+ * The circuit reads the current nonce from the ledger, increments it,
330
+ * then reconstructs the EIP-712 digest that signers must have signed
331
+ * off-chain: the `Execute` struct (contract address, nonce, recipient kind,
332
+ * recipient, coin color, amount) hashed under this deployment's domain
333
+ * separator. The digest is bound to the contract instance and the full recipient (kind and
334
+ * address), so a signature authorizes exactly one operation on one instance.
335
+ *
336
+ * Approvals are checked by `Secp256k1EcdsaMultisig`, which derives each
337
+ * signer's commitment from their public key and the instance salt and
338
+ * rejects duplicates. Constraints below are measured at `t = 2`.
339
+ *
340
+ * @notice `to` must be a `ShieldedUser`. A contract-addressed coin would be
341
+ * unclaimable, making the transaction malformed.
342
+ *
343
+ * @constraints k=17, rows=98969 (zkir-v3)
344
+ *
345
+ * Requirements:
346
+ *
347
+ * - No two entries may be the same signer.
348
+ * - Every public key must hash to a registered signer commitment.
349
+ * - Every signature must be valid over the message hash and use the low-s
350
+ * encoding.
351
+ * - `to.kind` must be `ShieldedUser`.
352
+ * - Coin value must be >= amount.
353
+ *
354
+ * @param {Proposal_Recipient} to - The recipient.
355
+ * @param {Uint<128>} amount - The amount to send.
356
+ * @param {QualifiedShieldedCoinInfo} coin - The coin to spend (from operator's pool).
357
+ * @param {Vector<t, Secp256k1Point>} pubkeys - Public keys of approving signers.
358
+ * @param {Vector<t, Secp256k1EcdsaSignature>} signatures - ECDSA signatures over the operation.
359
+ *
360
+ * @returns {ShieldedSendResult} The send result including any change.
361
+ */
362
+ export circuit execute(
363
+ to: Proposal_Recipient,
364
+ amount: Uint<128>,
365
+ coin: QualifiedShieldedCoinInfo,
366
+ pubkeys: Vector<t, Secp256k1Point>,
367
+ signatures: Vector<t, Secp256k1EcdsaSignature>
368
+ ): ShieldedSendResult {
369
+ // Increment nonce
370
+ const currentNonce = _nonce;
371
+ _nonce.increment(1);
372
+
373
+ // Construct message hash
374
+ const msgHash = Eip712_hashTypedData(_domainSeparator, keccak256<Vector<7, Bytes<32>>>([
375
+ EXECUTE_TYPEHASH(),
376
+ // Binds the digest to this deployment. The domain cannot carry the
377
+ // address, so removing this word removes cross-instance replay
378
+ // protection.
379
+ kernel.self().bytes,
380
+ EvmAbi_uint64Word(currentNonce),
381
+ EvmAbi_uint8Word(to.kind as Uint<8>),
382
+ to.address,
383
+ coin.color,
384
+ EvmAbi_uint128Word(amount)
385
+ ]));
386
+
387
+ Secp256k1EcdsaMultisig_assertApprovals(msgHash, pubkeys, signatures);
388
+
389
+ assert(
390
+ to.kind == Proposal_RecipientKind.ShieldedUser,
391
+ "NativeShieldedStatelessTreasury: recipient must be a coin public key"
392
+ );
393
+ const recipient = left<ZswapCoinPublicKey, ContractAddress>(
394
+ ZswapCoinPublicKey { bytes: to.address }
395
+ );
396
+ return Treasury__send(coin, recipient, amount);
397
+ }
398
+
399
+ /**
400
+ * @description Returns the nonce the next `execute` digest will bind to.
401
+ *
402
+ * @constraints k=6, rows=34 (zkir-v3)
403
+ *
404
+ * @returns {Uint<64>} The current nonce.
405
+ */
406
+ export circuit getNonce(): Uint<64> {
407
+ return _nonce;
408
+ }
409
+
410
+ /**
411
+ * @description Returns the number of registered signers.
412
+ *
413
+ * @constraints k=6, rows=34 (zkir-v3)
414
+ *
415
+ * @returns {Uint<8>} The signer count.
416
+ */
417
+ export circuit getSignerCount(): Uint<8> {
418
+ return Secp256k1EcdsaMultisig_getSignerCount();
419
+ }
420
+
421
+ /**
422
+ * @description Returns the approval threshold.
423
+ *
424
+ * @constraints k=6, rows=34 (zkir-v3)
425
+ *
426
+ * @returns {Uint<8>} The threshold.
427
+ */
428
+ export circuit getThreshold(): Uint<8> {
429
+ return Secp256k1EcdsaMultisig_getThreshold();
430
+ }
431
+
432
+ /**
433
+ * @description Returns whether the given commitment is a registered signer.
434
+ *
435
+ * @constraints k=8, rows=129 (zkir-v3)
436
+ *
437
+ * @param {Bytes<32>} commitment - The signer commitment to check.
438
+ * @returns {Boolean} True if registered.
439
+ */
440
+ export circuit isSigner(commitment: Bytes<32>): Boolean {
441
+ return Secp256k1EcdsaMultisig_isSigner(commitment);
442
+ }
443
+ }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (multisig/presets/NativeShieldedTokenIssuer.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (multisig/presets/NativeShieldedTokenIssuer.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openzeppelin/compact-contracts",
3
- "version": "0.5.0-alpha.1",
3
+ "version": "0.5.0-alpha.2",
4
4
  "description": "OpenZeppelin Compact contract library",
5
5
  "keywords": [
6
6
  "openzeppelin",
@@ -24,15 +24,15 @@
24
24
  "#test-utils/*": "./test-utils/*"
25
25
  },
26
26
  "scripts": {
27
- "compile": "compact-compiler --exclude '*/archive/*' --exclude '*/crypto/*' --exclude '*/multisig/*'",
28
- "compile:access": "compact-compiler --dir access",
29
- "compile:archive": "compact-compiler --dir archive",
30
- "compile:crypto": "compact-compiler --dir crypto --feature-zkir-v3 --exclude 'MockElGamal.compact' --exclude 'MockEcdhMask.compact' --exclude 'MockCurveOps.compact' && compact-compiler --dir crypto/test/mocks --exclude 'MockEcdsa.compact' --exclude 'MockEip712.compact'",
31
- "compile:multisig": "compact-compiler --dir multisig --feature-zkir-v3",
32
- "compile:security": "compact-compiler --dir security",
33
- "compile:token": "compact-compiler --dir token",
34
- "compile:utils": "compact-compiler --dir utils",
35
- "compile:integration": "compact-compiler --src test/integration/_mocks",
27
+ "compile:access": "rm -rf artifacts/access && compact-compiler +0.34.0 --hierarchical --dir access",
28
+ "compile:archive": "rm -rf artifacts/archive && compact-compiler +0.34.0 --hierarchical --dir archive",
29
+ "compile:crypto": "rm -rf artifacts/crypto && compact-compiler +0.34.0 --hierarchical --dir crypto --feature-zkir-v3 --exclude 'MockElGamal.compact' --exclude 'MockEcdhMask.compact' --exclude 'MockCurveOps.compact' && compact-compiler +0.34.0 --hierarchical --dir crypto/test/mocks --exclude 'MockEcdsa.compact' --exclude 'MockEip712.compact'",
30
+ "compile:math": "rm -rf artifacts/math && compact-compiler +0.34.0 --hierarchical --dir math",
31
+ "compile:multisig": "rm -rf artifacts/multisig && compact-compiler +0.34.0 --hierarchical --dir multisig --feature-zkir-v3",
32
+ "compile:security": "rm -rf artifacts/security && compact-compiler +0.34.0 --hierarchical --dir security",
33
+ "compile:token": "rm -rf artifacts/token && compact-compiler +0.34.0 --hierarchical --dir token",
34
+ "compile:utils": "rm -rf artifacts/utils && compact-compiler +0.34.0 --hierarchical --dir utils",
35
+ "compile:integration": "rm -rf artifacts/integration && compact-compiler +0.34.0 --hierarchical --src test/integration/_mocks --out artifacts/integration",
36
36
  "build": "compact-builder --hierarchical --out dist --clean-dist --feature-zkir-v3 --exclude '*/archive/*' --exclude 'Mock*' --exclude '*.mock.compact' --copy package.json --copy ../README.md && find dist -type d -empty -delete",
37
37
  "test": "vitest run --project unit",
38
38
  "test:coverage": "vitest run --project unit --coverage",
@@ -71,7 +71,7 @@
71
71
  "@vitest/coverage-v8": "^5.0.0",
72
72
  "ethers": "^6.17.0",
73
73
  "fast-check": "^4.10.1",
74
- "pino": "^9.7.0",
74
+ "pino": "^10.3.1",
75
75
  "ts-node": "^10.9.2",
76
76
  "typescript": "^6.0.3",
77
77
  "vite": "^8.3.0",
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (security/Allowlist.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (security/Allowlist.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (security/Blocklist.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (security/Blocklist.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (security/Initializable.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (security/Initializable.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (security/Pausable.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (security/Pausable.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.5.0-alpha.1 (token/ConfidentialFungibleToken.compact)
2
+ // OpenZeppelin Compact Contracts v0.5.0-alpha.2 (token/ConfidentialFungibleToken.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5