@openzeppelin/compact-contracts 0.4.0-alpha.1 → 0.4.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 (46) hide show
  1. package/README.md +9 -5
  2. package/access/AccessControl.compact +13 -13
  3. package/access/Ownable.compact +15 -9
  4. package/access/ShieldedAccessControl.compact +25 -1
  5. package/access/ZOwnablePK.compact +8 -8
  6. package/crypto/EcdhMask.compact +1 -1
  7. package/crypto/Ecdsa.compact +4 -4
  8. package/crypto/Eip712.compact +168 -0
  9. package/crypto/ElGamal.compact +1 -1
  10. package/multisig/EcdsaSignerManager.compact +32 -15
  11. package/multisig/ForwarderPrivate.compact +11 -5
  12. package/multisig/ForwarderShielded.compact +2 -2
  13. package/multisig/ForwarderUnshielded.compact +2 -2
  14. package/multisig/ProposalManager.compact +194 -22
  15. package/multisig/ShieldedTreasury.compact +28 -1
  16. package/multisig/ShieldedTreasuryStateless.compact +1 -1
  17. package/multisig/Signer.compact +27 -79
  18. package/multisig/UnshieldedTreasury.compact +9 -1
  19. package/multisig/{presets/forwarder/ForwarderPrivate.compact → examples/ForwarderPrivateExample.compact} +3 -3
  20. package/multisig/{presets/forwarder/ForwarderShielded.compact → examples/ForwarderShieldedExample.compact} +3 -3
  21. package/multisig/{presets/forwarder/ForwarderUnshielded.compact → examples/ForwarderUnshieldedExample.compact} +3 -3
  22. package/multisig/examples/NativeShieldedTokenIssuerExample.compact +143 -0
  23. package/multisig/examples/ShieldedMultiSigV2Example.compact +77 -0
  24. package/multisig/presets/NativeShieldedTokenIssuer.compact +542 -0
  25. package/multisig/presets/ShieldedMultiSigV2.compact +251 -188
  26. package/package.json +15 -11
  27. package/security/Allowlist.compact +5 -5
  28. package/security/Blocklist.compact +5 -5
  29. package/security/Initializable.compact +8 -4
  30. package/security/Pausable.compact +10 -6
  31. package/token/ConfidentialFungibleToken.compact +80 -21
  32. package/token/FungibleToken.compact +35 -21
  33. package/token/MultiToken.compact +15 -15
  34. package/token/NativeShieldedToken.compact +9 -9
  35. package/token/NativeShieldedTokenCore.compact +19 -9
  36. package/token/NativeShieldedTokenFamily.compact +9 -9
  37. package/token/NonFungibleToken.compact +32 -27
  38. package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +11 -6
  39. package/token/extensions/NativeShieldedTokenDerivedNonce.compact +2 -2
  40. package/token/extensions/NativeShieldedTokenFamilyPublicSupply.compact +5 -5
  41. package/token/extensions/NativeShieldedTokenPublicSupply.compact +5 -5
  42. package/token/extensions/NativeShieldedTokenPublicSupplyCore.compact +5 -5
  43. package/utils/EvmAbi.compact +134 -0
  44. package/utils/Utils.compact +1 -1
  45. package/multisig/presets/ShieldedMultiSig.compact +0 -227
  46. package/multisig/presets/ShieldedMultiSigV3.compact +0 -311
@@ -0,0 +1,542 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.2 (multisig/presets/NativeShieldedTokenIssuer.compact)
3
+
4
+ pragma language_version >= 0.26.0;
5
+
6
+ /**
7
+ * @module NativeShieldedTokenIssuer
8
+ * @description
9
+ * A 2-of-3 ECDSA multisig that mints and burns one native shielded token,
10
+ * for tokenized deposits. Value enters only through `mint` and leaves the
11
+ * contract only through `burn`; each is a single tx carrying two registered
12
+ * signers' approvals, so no single party creates or destroys tokens.
13
+ *
14
+ * Composes `EcdsaSignerManager` (signer commitments, approval checks),
15
+ * `NativeShieldedToken` as `NST_` (one token; domain, `name`, `symbol` and
16
+ * `decimals` fixed at construction) and `NativeShieldedTokenDerivedNonce` as
17
+ * `NSTDerivedNonce_` (coin nonces derived from a counter).
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
+ * - `opNonce`: the `_counter` value read at the start of `mint` / `burn`. It
27
+ * is the signature replay counter and is separate from the
28
+ * `NativeShieldedTokenDerivedNonce` counter that feeds coin nonces.
29
+ * - `cm`: a signer commitment, the registry key.
30
+ * - `color`: `tokenType(_domain, self)`, this token's coin color.
31
+ *
32
+ * @dev Derivations:
33
+ * - Domain separator: `DS = Eip712_domainSeparator(
34
+ * keccak("NativeShieldedTokenIssuer"), keccak("1"), instanceSalt)`, the
35
+ * EIP-712 domain `(name, version, salt)`. `chainId` and
36
+ * `verifyingContract` are absent: no network id is available in-circuit
37
+ * and a 32-byte address does not fit `address`. `self` inside each struct
38
+ * separates deployments instead.
39
+ * - Mint digest: `keccak(0x1901 ‖ DS ‖ keccak([MINT_TYPEHASH, self,
40
+ * recipientWord, boolWord(isContract), uint64Word(opNonce),
41
+ * uint64Word(amount)]))`, with `canonRecipient =
42
+ * Utils_canonicalize(recipient)` (unused `Either` arm zeroed),
43
+ * `recipientWord` its active arm and `isContract = !canonRecipient.is_left`.
44
+ * - Burn digest: `keccak(0x1901 ‖ DS ‖ keccak([BURN_TYPEHASH, self,
45
+ * uint64Word(opNonce), uint64Word(amount)]))`.
46
+ * - Type hashes: `MINT_TYPEHASH = keccak("Mint(bytes32 contractAddress,
47
+ * bytes32 recipient,bool isContract,uint256 nonce,uint256 amount)")` and
48
+ * `BURN_TYPEHASH = keccak("Burn(bytes32 contractAddress,uint256 nonce,
49
+ * uint256 amount)")`, written as literal words.
50
+ * - Signer commitment: `cm = H<SignerCommitmentInput>({ pkX, pkY, salt,
51
+ * domain: pad(32, "multisig:signer:") })`, `pkX` / `pkY` the affine
52
+ * coordinates as `Bytes<32>`. `_calculateSignerId` is the exported pure
53
+ * circuit.
54
+ * - Coin nonce: `evolveNonce(counter, pad(32, "NativeShieldedToken:nonce"))`
55
+ * over the derived-nonce counter after its increment.
56
+ * - Encoding: `H` hashes the Compact encoding, not a byte concatenation.
57
+ * Digest integers are big-endian ABI words (`utils/EvmAbi`), so an EVM
58
+ * signer's typed-data encoder reproduces them.
59
+ * `contracts/src/multisig/test/EcdsaTestUtils.ts` pins the digests
60
+ * through ethers (`mintMsgHash`, `burnMsgHash`).
61
+ *
62
+ * @notice Implementing contracts register the signer set from their
63
+ * constructor by calling this module's `initialize`, and MUST NOT import
64
+ * `EcdsaSignerManager` directly.
65
+ *
66
+ * @dev Parameterization:
67
+ * None: the signer set is hardcoded to 3 and the threshold to 2.
68
+ *
69
+ * @dev Compatibility:
70
+ * The wire format the custodian's signer implements is the EIP-712 domain
71
+ * (`NativeShieldedTokenIssuer`, version `1`, salt), the `Mint` / `Burn`
72
+ * type strings, the `multisig:signer:` commitment domain (generic prefix,
73
+ * owned by `EcdsaSignerManager`), the `cm` preimage struct, and the
74
+ * `Vector<2>` / `Vector<3>` shapes. Changing any of them breaks every
75
+ * existing signer setup. The test suite pins them through
76
+ * `EcdsaTestUtils.ts`.
77
+ *
78
+ * @notice Privacy:
79
+ * - Public, per tx:
80
+ * - Entry point: mint vs burn.
81
+ * - Approvers: the two signer commitments (`disclose(commitment)` in
82
+ * `Signer_assertSigner`), so WHICH two of the three signers approved.
83
+ * - Mint: `amount`, as the `shieldedMints` effect under the token domain,
84
+ * and the coin commitment as a spend claim. Recipient and nonce are not
85
+ * published. The nonce is `evolveNonce` of the public
86
+ * `_derivedNonceCounter`, so a candidate recipient can be tested
87
+ * against the commitment. A mint to this contract adds the same
88
+ * commitment as a receive claim and names the contract in the Zswap
89
+ * output.
90
+ * - Burn: the spent coin's nullifier, the burn-output commitment and, on a
91
+ * partial burn, the change commitment as both spend and receive claim.
92
+ * `amount`, the coin's value and `mt_index` are not published. Every
93
+ * coin this contract holds was minted to it, so its nullifier is
94
+ * computable and a burn links to its mint; the amount is protected by
95
+ * the commitment hash alone.
96
+ * - Public, as state:
97
+ * - Cells: `_counter`, `_domainSeparator`, `_derivedNonceCounter`,
98
+ * `_signers`, `_signerCount`, `_threshold`, `_instanceSalt`, `_domain`,
99
+ * `_name` / `_symbol` / `_decimals`, `_isInitialized`. All are exported, so an implementing
100
+ * contract can surface them in its `ledger()` reader.
101
+ * - Absent: no supply, no balances, no coin registry.
102
+ * - Private:
103
+ * - Signer material: public keys and signatures are verified in-circuit,
104
+ * never disclosed.
105
+ * - Operation data: burn amounts. Mint recipients, up to the commitment
106
+ * test above.
107
+ * - Signer identity: keys sit behind `cm` with a public salt, so an observer
108
+ * holding a candidate key can test membership; one without cannot
109
+ * correlate a signer across deployments.
110
+ * - Counterparty: a mint recipient learns nothing beyond the coin handed over
111
+ * out of band; every observer learns the amount of every mint and can
112
+ * test a candidate recipient against the commitment.
113
+ * - Auditability: mint amounts are public, burn amounts are not. No
114
+ * on-chain total is kept. `NativeShieldedTokenPublicSupply` adds one and
115
+ * makes burn amounts public.
116
+ *
117
+ * @notice Security:
118
+ * - Digest binding: each struct binds its type hash, `self`, `opNonce` and
119
+ * `amount` (`mint` also the recipient word and its kind) under `DS`, so a
120
+ * signature authorizes one operation on one instance at one nonce, and is
121
+ * neither replayable as the other operation, nor under another domain,
122
+ * nor redirectable. Removing the `self` word removes cross-instance
123
+ * replay protection, since `DS` cannot carry the address.
124
+ * - Low-s: verification goes through `Ecdsa_secp256k1EcdsaVerifyLowS`, so a
125
+ * high-s encoding is an invalid signature.
126
+ * - Distinct signers: `assertApprovals` asserts both entries and rejects a
127
+ * repeated commitment, so two distinct registered signers are always
128
+ * required. The threshold `2` is the ceiling `EcdsaSignerManager` accepts.
129
+ * - Canonical recipient: `Utils_canonicalize` zeroes the unused `Either` arm
130
+ * before hashing and minting, so a recipient with both arms populated
131
+ * cannot yield two digests for one destination.
132
+ * - Immutable signer set: no signer reconfiguration circuit is exported, so
133
+ * the three commitments and the threshold are fixed. Losing two of the
134
+ * three keys freezes the contract.
135
+ * - Griefing: the derived-nonce mint is the only mint path and it is
136
+ * threshold-gated, so `NativeShieldedTokenDerivedNonce`'s
137
+ * collision-griefing precondition (a caller-nonce mint at another trust
138
+ * level) is absent.
139
+ * - Atomicity: a failed assert rejects the whole tx before any state moves;
140
+ * neither counter advances and the sender pays no fee.
141
+ *
142
+ * @notice Concurrency:
143
+ * - Replay counter: `mint` and `burn` both read `_counter` into the digest.
144
+ * The read is pinned, so any two of them in one block conflict: at most one
145
+ * mint-or-burn lands per block, and the losing signer pair re-signs at the
146
+ * next nonce.
147
+ * - Derived-nonce counter: `mint` also pins it (`evolveNonce` consumes it);
148
+ * the `_counter` conflict already covers that pair.
149
+ * - `getNonce`: submitted as a tx it pins `_counter` and conflicts with a
150
+ * same-block mint or burn.
151
+ * - Views: metadata, `tokenColor` and the signer views read only state
152
+ * written by `initialize`, so they commute with everything.
153
+ *
154
+ * @notice Scope:
155
+ * Mint and burn only. No send, transfer or execute circuit exists, so
156
+ * contract-held coins leave only through `burn`. No deposit or receive
157
+ * circuit exists either: `burn` spends a coin the contract already holds,
158
+ * supplied by the operator as `QualifiedShieldedCoinInfo`. Coins minted to
159
+ * users are ordinary Zswap coins the contract neither tracks nor restricts.
160
+ * No supply accounting.
161
+ *
162
+ * @notice Client-side duties:
163
+ * - Before deploy: pick a random `instanceSalt`, derive each `cm` with
164
+ * `_calculateSignerId(pk, salt)`, pass the three as `signerCommitments`.
165
+ * - Per operation: read `getNonce()`, rebuild the digest as
166
+ * `EcdsaTestUtils.ts` does, collect two low-s signatures off-chain, submit
167
+ * keys and signatures in matching `Vector<2>` positions.
168
+ * - Coin delivery: hand the `ShieldedCoinInfo` returned by `mint` to the
169
+ * recipient out of band; contract-initiated mints create no ciphertext, so
170
+ * a wallet cannot find the coin by scanning.
171
+ * - Coin tracking: keep contract-held coins off-chain, recovering each
172
+ * coin's `mt_index` from the indexer's `ZswapOutput` / ledger events to
173
+ * build the `QualifiedShieldedCoinInfo` that `burn` takes.
174
+ * - Change coin: persist the one `burn` returns. It replaces the spent coin
175
+ * as the contract's holding and is not otherwise recoverable.
176
+ *
177
+ * @notice Scalability:
178
+ * - Throughput: one mint-or-burn per block (see Concurrency).
179
+ * - State: constant-size, two counters and a three-entry signer set. Nothing
180
+ * grows per operation.
181
+ * - Amounts: `mint` caps at `Uint<64>` by `mintShieldedToken`; `burn` takes
182
+ * `Uint<64>` by its signature although `NST__burnFromSelf` accepts
183
+ * `Uint<128>`.
184
+ *
185
+ * @dev Composition:
186
+ * - Recipient privacy: pass a caller-supplied secret random nonce to
187
+ * `NST__mint` instead of `NSTDerivedNonce__deriveNonce()`.
188
+ * - Deposits: a deposit path needs a `receiveShielded` circuit; none is
189
+ * included.
190
+ *
191
+ * @notice Requires ZKIR v3: `keccak256` is unavailable on the v2 backend.
192
+ */
193
+ module NativeShieldedTokenIssuer {
194
+ import CompactStandardLibrary;
195
+
196
+ import "../EcdsaSignerManager" prefix EcdsaSignerManager_;
197
+ import "../../crypto/Eip712" prefix Eip712_;
198
+ import "../../utils/Utils" prefix Utils_;
199
+ import "../../utils/EvmAbi" prefix EvmAbi_;
200
+ import "../../token/NativeShieldedToken" prefix NST_;
201
+ import "../../token/extensions/NativeShieldedTokenDerivedNonce" prefix NSTDerivedNonce_;
202
+
203
+ // Named import/re-export of the composed modules' state only, so an
204
+ // implementing contract can surface every ledger field under its own key.
205
+ // The circuits stay behind the prefixes above. The derived-nonce counter is
206
+ // aliased because this module's replay counter already owns `_counter`.
207
+ import { _instanceSalt, _signers, _signerCount, _threshold } from "../EcdsaSignerManager";
208
+ import { _domain, _name, _symbol, _decimals, _isInitialized } from "../../token/NativeShieldedToken";
209
+ import { _counter as _derivedNonceCounter } from "../../token/extensions/NativeShieldedTokenDerivedNonce";
210
+ export { _instanceSalt, _signers, _signerCount, _threshold };
211
+ export { _domain, _name, _symbol, _decimals, _isInitialized };
212
+ export { _derivedNonceCounter };
213
+
214
+ /**
215
+ * @description Signature replay counter. `mint` and `burn` read it into
216
+ * their digest and increment it; `getNonce` returns the value the next
217
+ * digest binds to. The read is pinned, so at most one mint-or-burn lands
218
+ * per block.
219
+ */
220
+ export ledger _counter: Counter;
221
+
222
+ /**
223
+ * @description The EIP-712 domain separator every operation digest is
224
+ * built under, derived from `(name, version, instanceSalt)`. Set once at
225
+ * construction.
226
+ */
227
+ export sealed ledger _domainSeparator: Bytes<32>;
228
+
229
+ /**
230
+ * @description Registers the signer set, initializes the token metadata
231
+ * and domain, and fixes the EIP-712 domain separator. The threshold is
232
+ * fixed at 2.
233
+ *
234
+ * @notice Call from the implementing contract's constructor only. It writes
235
+ * the token module's `sealed` domain and `_domainSeparator`, so
236
+ * implementing contracts MUST NOT expose it as a post-deploy circuit.
237
+ *
238
+ * `instanceSalt` is both the EIP-712 domain salt and the signer-commitment
239
+ * salt, so a fresh value per deployment keeps one public key from
240
+ * producing the same commitment in two deployments.
241
+ *
242
+ * The token color is `tokenType(tokenDomain, kernel.self())`. Only coins
243
+ * of this color can be burned through this contract.
244
+ *
245
+ * Requirements:
246
+ *
247
+ * - `instanceSalt` should be cryptographically random.
248
+ * - `signerCommitments` must not contain duplicates.
249
+ *
250
+ * @param {Bytes<32>} instanceSalt - Per-deployment salt for commitment derivation.
251
+ * @param {Bytes<32>} tokenDomain - Domain separator for this contract's token color.
252
+ * @param {Opaque<"string">} name_ - The token name.
253
+ * @param {Opaque<"string">} symbol_ - The token symbol.
254
+ * @param {Uint<8>} decimals_ - The token decimals.
255
+ * @param {Vector<3, Bytes<32>>} signerCommitments - Hashed signer identities.
256
+ * @returns {[]} Empty tuple.
257
+ */
258
+ export circuit initialize(
259
+ instanceSalt: Bytes<32>,
260
+ tokenDomain: Bytes<32>,
261
+ name_: Opaque<"string">,
262
+ symbol_: Opaque<"string">,
263
+ decimals_: Uint<8>,
264
+ signerCommitments: Vector<3, Bytes<32>>,
265
+ ): [] {
266
+ EcdsaSignerManager_initialize<3>(instanceSalt, signerCommitments, 2);
267
+ NST_initialize(tokenDomain, name_, symbol_, decimals_);
268
+
269
+ const hashedName = keccak256<Bytes<25>>(pad(25, "NativeShieldedTokenIssuer"));
270
+ const hashedVersion = keccak256<Bytes<1>>(pad(1, "1"));
271
+ _domainSeparator = Eip712_domainSeparator(
272
+ hashedName,
273
+ hashedVersion,
274
+ disclose(instanceSalt)
275
+ );
276
+ }
277
+
278
+ /**
279
+ * @description `keccak256` of the `Mint` type string:
280
+ * `Mint(bytes32 contractAddress,bytes32 recipient,bool isContract,uint256 nonce,uint256 amount)`
281
+ *
282
+ * @dev Written as a literal word; see `BURN_TYPEHASH`. The spec checks this
283
+ * word against the type string above.
284
+ *
285
+ * @returns {Bytes<32>} The `Mint` type hash.
286
+ */
287
+ pure circuit MINT_TYPEHASH(): Bytes<32> {
288
+ const typeHash: Vector<32, Uint<8>> = [
289
+ 0x28, 0xd4, 0xc8, 0x40, 0xbc, 0x95, 0xfe, 0x08,
290
+ 0x4e, 0xa5, 0xe3, 0x20, 0x94, 0x43, 0xba, 0x67,
291
+ 0x21, 0xb7, 0x17, 0x09, 0x71, 0xee, 0x05, 0xef,
292
+ 0xaf, 0x0a, 0x98, 0x51, 0xee, 0x5f, 0xd1, 0xf5
293
+ ];
294
+
295
+ return typeHash as Bytes<32>;
296
+ }
297
+
298
+ /**
299
+ * @description `keccak256` of the `Burn` type string:
300
+ * `Burn(bytes32 contractAddress,uint256 nonce,uint256 amount)`
301
+ *
302
+ * @dev Written as a literal word. `compactc` emits a full keccak
303
+ * permutation even for a constant argument, so hashing the type string
304
+ * in-circuit would add ~4.2k rows to every operation. The spec checks this
305
+ * word against the type string above.
306
+ *
307
+ * @returns {Bytes<32>} The `Burn` type hash.
308
+ */
309
+ pure circuit BURN_TYPEHASH(): Bytes<32> {
310
+ const typeHash: Vector<32, Uint<8>> = [
311
+ 0xdf, 0x62, 0xd6, 0x7d, 0x39, 0x08, 0x06, 0xa6,
312
+ 0xb6, 0x3f, 0xbd, 0x9f, 0xb5, 0xb8, 0x90, 0x4e,
313
+ 0xf8, 0x6a, 0x4d, 0x67, 0xf3, 0x82, 0x4a, 0x3d,
314
+ 0x85, 0x5f, 0x6e, 0xef, 0xb8, 0xe7, 0x98, 0x09
315
+ ];
316
+
317
+ return typeHash as Bytes<32>;
318
+ }
319
+
320
+ /**
321
+ * @description Computes a signer commitment from a secp256k1 public key,
322
+ * as `persistentHash(pkX, pkY, salt, "multisig:signer:")`.
323
+ *
324
+ * Pure, so the deployer can derive constructor commitments off-chain.
325
+ *
326
+ * Requirements:
327
+ *
328
+ * - `pk` must not be the secp256k1 identity point, which has no coordinates.
329
+ *
330
+ * @param {Secp256k1Point} pk - The secp256k1 public key.
331
+ * @param {Bytes<32>} salt - The instance salt.
332
+ * @returns {Bytes<32>} The signer commitment.
333
+ */
334
+ export pure circuit _calculateSignerId(
335
+ pk: Secp256k1Point,
336
+ salt: Bytes<32>
337
+ ): Bytes<32> {
338
+ return EcdsaSignerManager_calculateSignerId(pk, salt);
339
+ }
340
+
341
+ /**
342
+ * @description Mints a new shielded coin of this contract's token to
343
+ * `recipient`, authorized by threshold signatures.
344
+ *
345
+ * The coin nonce comes from `NativeShieldedTokenDerivedNonce`, so anyone
346
+ * can test a candidate recipient against the coin commitment.
347
+ *
348
+ * @notice The returned coin info is the only copy available to the
349
+ * recipient; deliver it out of band.
350
+ *
351
+ * @constraints k=17, rows=89952 (zkir-v3)
352
+ *
353
+ * Requirements:
354
+ *
355
+ * - Both public keys must hash to registered signer commitments.
356
+ * - Both signatures must be valid over the mint message hash.
357
+ * - Signers must not be duplicates.
358
+ * - Threshold must be met.
359
+ * - `recipient` is not zero.
360
+ *
361
+ * @param {Uint<64>} amount - The token amount to mint.
362
+ * @param {Either<ZswapCoinPublicKey, ContractAddress>} recipient - The address to receive the minted tokens.
363
+ * @param {Vector<2, Secp256k1Point>} pubkeys - Public keys of approving signers.
364
+ * @param {Vector<2, Secp256k1EcdsaSignature>} signatures - ECDSA signatures over the mint hash.
365
+ * @returns {ShieldedCoinInfo} The minted coin (nonce, color, value).
366
+ */
367
+ export circuit mint(
368
+ amount: Uint<64>,
369
+ recipient: Either<ZswapCoinPublicKey, ContractAddress>,
370
+ pubkeys: Vector<2, Secp256k1Point>,
371
+ signatures: Vector<2, Secp256k1EcdsaSignature>
372
+ ): ShieldedCoinInfo {
373
+ const opNonce = _counter;
374
+ _counter.increment(1);
375
+
376
+ // Canonicalize recipient to ensure garbage values don't sully the hash
377
+ const canonRecipient = Utils_canonicalize<ZswapCoinPublicKey, ContractAddress>(recipient);
378
+
379
+ // Only the active arm reaches the digest, so a crafted value carrying data
380
+ // in its unused arm cannot alter what was signed.
381
+ const recipientWord = canonRecipient.is_left
382
+ ? canonRecipient.left.bytes
383
+ : canonRecipient.right.bytes;
384
+
385
+ const msgHash = Eip712_hashTypedData(_domainSeparator, keccak256<Vector<6, Bytes<32>>>([
386
+ MINT_TYPEHASH(),
387
+ // Binds the digest to this deployment. The domain cannot carry the
388
+ // address, so removing this word removes cross-instance replay
389
+ // protection.
390
+ kernel.self().bytes,
391
+ recipientWord,
392
+ EvmAbi_boolWord(!canonRecipient.is_left),
393
+ EvmAbi_uint64Word(opNonce),
394
+ EvmAbi_uint64Word(amount)
395
+ ]));
396
+
397
+ EcdsaSignerManager_assertApprovals(msgHash, pubkeys, signatures);
398
+
399
+ return NST__mint(canonRecipient, amount, NSTDerivedNonce__deriveNonce());
400
+ }
401
+
402
+ /**
403
+ * @description Burns `amount` from a coin this contract holds, authorized
404
+ * by threshold signatures.
405
+ *
406
+ * The operator supplies the `QualifiedShieldedCoinInfo` from the off-chain
407
+ * UTXO pool. On a partial burn the change is auto-received by the contract
408
+ * and returned; it replaces `coin` as the contract's holding, so the
409
+ * operator must persist it.
410
+ *
411
+ * @constraints k=17, rows=97232 (zkir-v3)
412
+ *
413
+ * Requirements:
414
+ *
415
+ * - Both public keys must hash to registered signer commitments.
416
+ * - Both signatures must be valid over the burn message hash.
417
+ * - Signers must not be duplicates.
418
+ * - Threshold must be met.
419
+ * - `coin.color` is this contract's token color.
420
+ * - `amount` is less than or equal to `coin.value`.
421
+ *
422
+ * @param {QualifiedShieldedCoinInfo} coin - The contract-held coin to burn from.
423
+ * @param {Uint<64>} amount - The token amount to burn.
424
+ * @param {Vector<2, Secp256k1Point>} pubkeys - Public keys of approving signers.
425
+ * @param {Vector<2, Secp256k1EcdsaSignature>} signatures - ECDSA signatures over the burn hash.
426
+ * @returns {Maybe<ShieldedCoinInfo>} The change coin retained by the
427
+ * contract, or `none` if the coin was burned in full.
428
+ */
429
+ export circuit burn(
430
+ coin: QualifiedShieldedCoinInfo,
431
+ amount: Uint<64>,
432
+ pubkeys: Vector<2, Secp256k1Point>,
433
+ signatures: Vector<2, Secp256k1EcdsaSignature>
434
+ ): Maybe<ShieldedCoinInfo> {
435
+ const opNonce = _counter;
436
+ _counter.increment(1);
437
+
438
+ const msgHash = Eip712_hashTypedData(_domainSeparator, keccak256<Vector<4, Bytes<32>>>([
439
+ BURN_TYPEHASH(),
440
+ // Binds the digest to this deployment. The domain cannot carry the
441
+ // address, so removing this word removes cross-instance replay
442
+ // protection.
443
+ kernel.self().bytes,
444
+ EvmAbi_uint64Word(opNonce),
445
+ EvmAbi_uint64Word(amount)
446
+ ]));
447
+
448
+ EcdsaSignerManager_assertApprovals(msgHash, pubkeys, signatures);
449
+
450
+ return NST__burnFromSelf(coin, amount);
451
+ }
452
+
453
+ /**
454
+ * @description Returns the counter the next mint or burn digest will bind to.
455
+ *
456
+ * @constraints k=6, rows=34 (zkir-v3)
457
+ *
458
+ * @returns {Uint<64>} The current counter.
459
+ */
460
+ export circuit getNonce(): Uint<64> {
461
+ return _counter;
462
+ }
463
+
464
+ /**
465
+ * @description Returns the token name.
466
+ *
467
+ * @constraints k=6, rows=45 (zkir-v3)
468
+ *
469
+ * @returns {Opaque<"string">} The token name.
470
+ */
471
+ export circuit name(): Opaque<"string"> {
472
+ return NST_name();
473
+ }
474
+
475
+ /**
476
+ * @description Returns the token symbol.
477
+ *
478
+ * @constraints k=6, rows=45 (zkir-v3)
479
+ *
480
+ * @returns {Opaque<"string">} The token symbol.
481
+ */
482
+ export circuit symbol(): Opaque<"string"> {
483
+ return NST_symbol();
484
+ }
485
+
486
+ /**
487
+ * @description Returns the token decimals.
488
+ *
489
+ * @constraints k=6, rows=45 (zkir-v3)
490
+ *
491
+ * @returns {Uint<8>} The decimals value.
492
+ */
493
+ export circuit decimals(): Uint<8> {
494
+ return NST_decimals();
495
+ }
496
+
497
+ /**
498
+ * @description Returns this contract's token color,
499
+ * `tokenType(tokenDomain, kernel.self())`.
500
+ *
501
+ * @constraints k=13, rows=3847 (zkir-v3)
502
+ *
503
+ * @returns {Bytes<32>} The coin color.
504
+ */
505
+ export circuit tokenColor(): Bytes<32> {
506
+ return NST_tokenColor();
507
+ }
508
+
509
+ /**
510
+ * @description Returns the number of registered signers.
511
+ *
512
+ * @constraints k=6, rows=34 (zkir-v3)
513
+ *
514
+ * @returns {Uint<8>} The signer count.
515
+ */
516
+ export circuit getSignerCount(): Uint<8> {
517
+ return EcdsaSignerManager_getSignerCount();
518
+ }
519
+
520
+ /**
521
+ * @description Returns the approval threshold.
522
+ *
523
+ * @constraints k=6, rows=34 (zkir-v3)
524
+ *
525
+ * @returns {Uint<8>} The threshold.
526
+ */
527
+ export circuit getThreshold(): Uint<8> {
528
+ return EcdsaSignerManager_getThreshold();
529
+ }
530
+
531
+ /**
532
+ * @description Returns whether the given commitment is a registered signer.
533
+ *
534
+ * @constraints k=8, rows=129 (zkir-v3)
535
+ *
536
+ * @param {Bytes<32>} commitment - The signer commitment to check.
537
+ * @returns {Boolean} True if registered.
538
+ */
539
+ export circuit isSigner(commitment: Bytes<32>): Boolean {
540
+ return EcdsaSignerManager_isSigner(commitment);
541
+ }
542
+ }