@openzeppelin/compact-contracts 0.4.0-alpha.3 → 0.4.0-alpha.5

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 (40) hide show
  1. package/access/AccessControl.compact +1 -1
  2. package/access/Ownable.compact +1 -1
  3. package/access/ShieldedAccessControl.compact +1 -1
  4. package/access/ZOwnablePK.compact +1 -1
  5. package/crypto/EcdhMask.compact +1 -1
  6. package/crypto/Ecdsa.compact +1 -1
  7. package/crypto/Eip712.compact +1 -1
  8. package/crypto/ElGamal.compact +1 -1
  9. package/multisig/EcdsaSignerManager.compact +9 -7
  10. package/multisig/ForwarderPrivate.compact +1 -1
  11. package/multisig/ForwarderShielded.compact +14 -5
  12. package/multisig/ForwarderUnshielded.compact +1 -1
  13. package/multisig/ProposalManager.compact +1 -1
  14. package/multisig/ShieldedTreasury.compact +1 -1
  15. package/multisig/ShieldedTreasuryStateless.compact +1 -1
  16. package/multisig/Signer.compact +1 -1
  17. package/multisig/UnshieldedTreasury.compact +5 -3
  18. package/multisig/examples/ForwarderPrivateExample.compact +1 -1
  19. package/multisig/examples/ForwarderShieldedExample.compact +8 -5
  20. package/multisig/examples/ForwarderUnshieldedExample.compact +1 -1
  21. package/multisig/examples/NativeShieldedTokenIssuerExample.compact +29 -7
  22. package/multisig/examples/ShieldedMultiSigV2Example.compact +2 -2
  23. package/multisig/presets/NativeShieldedTokenIssuer.compact +278 -123
  24. package/multisig/presets/ShieldedMultiSigV2.compact +16 -7
  25. package/package.json +6 -4
  26. package/security/Allowlist.compact +1 -1
  27. package/security/Blocklist.compact +1 -1
  28. package/security/Initializable.compact +1 -1
  29. package/security/Pausable.compact +1 -1
  30. package/token/ConfidentialFungibleToken.compact +17 -1
  31. package/token/FungibleToken.compact +1 -1
  32. package/token/MultiToken.compact +1 -1
  33. package/token/NativeShieldedToken.compact +2 -2
  34. package/token/NativeShieldedTokenCore.compact +11 -18
  35. package/token/NativeShieldedTokenFamily.compact +2 -2
  36. package/token/NonFungibleToken.compact +1 -1
  37. package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +1 -1
  38. package/token/extensions/NativeShieldedTokenDerivedNonce.compact +1 -1
  39. package/utils/EvmAbi.compact +1 -1
  40. package/utils/Utils.compact +1 -1
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.4.0-alpha.3 (multisig/presets/NativeShieldedTokenIssuer.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.5 (multisig/presets/NativeShieldedTokenIssuer.compact)
3
3
 
4
4
  pragma language_version >= 0.26.0;
5
5
 
@@ -7,8 +7,10 @@ pragma language_version >= 0.26.0;
7
7
  * @module NativeShieldedTokenIssuer
8
8
  * @description
9
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
10
+ * for tokenized deposits. Value enters only through `mint` (to a holder's
11
+ * coin public key) or `mintToSelf` (a coin this contract holds) and leaves
12
+ * only through `burn` (a holder's coin, paid into the tx) or `burnFromSelf`
13
+ * (a coin this contract holds); each is a single tx carrying two registered
12
14
  * signers' approvals, so no single party creates or destroys tokens.
13
15
  *
14
16
  * Composes `EcdsaSignerManager` (signer commitments, approval checks),
@@ -23,8 +25,9 @@ pragma language_version >= 0.26.0;
23
25
  * - `DS`: `_domainSeparator`, the EIP-712 domain separator fixed at
24
26
  * construction.
25
27
  * - `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
+ * - `opNonce`: the `_counter` value read at the start of every operation
29
+ * (`mint`, `mintToSelf`, `burn`, `burnFromSelf`). It is the signature
30
+ * replay counter and is separate from the
28
31
  * `NativeShieldedTokenDerivedNonce` counter that feeds coin nonces.
29
32
  * - `cm`: a signer commitment, the registry key.
30
33
  * - `color`: `tokenType(_domain, self)`, this token's coin color.
@@ -37,16 +40,23 @@ pragma language_version >= 0.26.0;
37
40
  * and a 32-byte address does not fit `address`. `self` inside each struct
38
41
  * separates deployments instead.
39
42
  * - 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`.
43
+ * recipient.bytes, uint64Word(opNonce), uint64Word(amount)]))`.
44
+ * - MintToSelf digest: `keccak(0x1901 ‖ DS ‖ keccak([MINT_TO_SELF_TYPEHASH,
45
+ * self, uint64Word(opNonce), uint64Word(amount)]))`.
44
46
  * - Burn digest: `keccak(0x1901 ‖ DS ‖ keccak([BURN_TYPEHASH, self,
45
- * uint64Word(opNonce), uint64Word(amount)]))`.
47
+ * refundTo.bytes, uint64Word(opNonce), uint128Word(amount)]))`.
48
+ * - BurnFromSelf digest: `keccak(0x1901 ‖ DS ‖ keccak([
49
+ * BURN_FROM_SELF_TYPEHASH, self, uint64Word(opNonce), uint128Word(amount),
50
+ * coin.nonce, uint128Word(coin.value)]))`. The last two words name the
51
+ * holding, so approvals authorize one coin rather than an amount.
46
52
  * - 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.
53
+ * bytes32 recipient,uint256 nonce,uint256 amount)")`,
54
+ * `MINT_TO_SELF_TYPEHASH = keccak("MintToSelf(bytes32 contractAddress,
55
+ * uint256 nonce,uint256 amount)")`, `BURN_TYPEHASH = keccak("Burn(bytes32 contractAddress,bytes32 refundTo,
56
+ * uint256 nonce,uint256 amount)")` and `BURN_FROM_SELF_TYPEHASH =
57
+ * keccak("BurnFromSelf(bytes32 contractAddress,uint256 nonce,
58
+ * uint256 amount,bytes32 coinNonce,uint256 coinValue)")`, written as
59
+ * literal words.
50
60
  * - Signer commitment: `cm = H<SignerCommitmentInput>({ pkX, pkY, salt,
51
61
  * domain: pad(32, "multisig:signer:") })`, `pkX` / `pkY` the affine
52
62
  * coordinates as `Bytes<32>`. `_calculateSignerId` is the exported pure
@@ -57,7 +67,8 @@ pragma language_version >= 0.26.0;
57
67
  * Digest integers are big-endian ABI words (`utils/EvmAbi`), so an EVM
58
68
  * signer's typed-data encoder reproduces them.
59
69
  * `contracts/src/multisig/test/EcdsaTestUtils.ts` pins the digests
60
- * through ethers (`mintMsgHash`, `burnMsgHash`).
70
+ * through ethers (`mintMsgHash`, `mintToSelfMsgHash`, `burnMsgHash`,
71
+ * `burnFromSelfMsgHash`).
61
72
  *
62
73
  * @notice Implementing contracts register the signer set from their
63
74
  * constructor by calling this module's `initialize`, and MUST NOT import
@@ -68,8 +79,8 @@ pragma language_version >= 0.26.0;
68
79
  *
69
80
  * @dev Compatibility:
70
81
  * 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,
82
+ * (`NativeShieldedTokenIssuer`, version `1`, salt), the `Mint` /
83
+ * `MintToSelf` / `Burn` / `BurnFromSelf` type strings, the `multisig:signer:` commitment domain (generic prefix,
73
84
  * owned by `EcdsaSignerManager`), the `cm` preimage struct, and the
74
85
  * `Vector<2>` / `Vector<3>` shapes. Changing any of them breaks every
75
86
  * existing signer setup. The test suite pins them through
@@ -77,22 +88,30 @@ pragma language_version >= 0.26.0;
77
88
  *
78
89
  * @notice Privacy:
79
90
  * - Public, per tx:
80
- * - Entry point: mint vs burn.
91
+ * - Entry point: which of the four operations ran.
81
92
  * - Approvers: the two signer commitments (`disclose(commitment)` in
82
93
  * `Signer_assertSigner`), so WHICH two of the three signers approved.
83
94
  * - Mint: `amount`, as the `shieldedMints` effect under the token domain,
84
95
  * and the coin commitment as a spend claim. Recipient and nonce are not
85
96
  * published. The nonce is `evolveNonce` of the public
86
97
  * `_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.
98
+ * against the commitment.
99
+ * - MintToSelf: as `mint`, plus the same commitment as a receive claim and
100
+ * the contract named in the Zswap output.
101
+ * - Burn: the holder's coin commitment as a receive claim, the burn-output
102
+ * commitment and, on a partial burn, the refund commitment. `amount`,
103
+ * `coin.value` and `refundTo` stay hidden only while `coin` carries a
104
+ * nonce the observer does not know. A coin passed straight from `mint`
105
+ * has a derived nonce, so the receive claim links the burn to its mint
106
+ * and exposes all three by trial: `coin.value` from the mint, `amount`
107
+ * over `[0, value]` against the burn output, `refundTo` against the
108
+ * refund commitment.
109
+ * - BurnFromSelf: the spent coin's nullifier, the burn-output commitment
110
+ * and, on a partial burn, the change commitment as both spend and
111
+ * receive claim. `amount`, the coin's value and `mt_index` are not
112
+ * published, but every held coin has a derived nonce, so its nullifier
113
+ * links the burn to its mint and `amount` is recoverable by trial over
114
+ * `[0, value]`.
96
115
  * - Public, as state:
97
116
  * - Cells: `_counter`, `_domainSeparator`, `_derivedNonceCounter`,
98
117
  * `_signers`, `_signerCount`, `_threshold`, `_instanceSalt`, `_domain`,
@@ -102,37 +121,41 @@ pragma language_version >= 0.26.0;
102
121
  * - Private:
103
122
  * - Signer material: public keys and signatures are verified in-circuit,
104
123
  * never disclosed.
105
- * - Operation data: burn amounts. Mint recipients, up to the commitment
106
- * test above.
124
+ * - Operation data: `burn` amounts and `refundTo`, for a coin whose nonce
125
+ * is secret. Mint recipients, up to the commitment test above.
107
126
  * - Signer identity: keys sit behind `cm` with a public salt, so an observer
108
127
  * holding a candidate key can test membership; one without cannot
109
128
  * correlate a signer across deployments.
110
129
  * - Counterparty: a mint recipient learns nothing beyond the coin handed over
111
130
  * out of band; every observer learns the amount of every mint and can
112
131
  * test a candidate recipient against the commitment.
113
- * - Auditability: mint amounts are public, burn amounts are not. No
132
+ * - Auditability: mint amounts are public; `burnFromSelf` amounts are
133
+ * recoverable by trial, `burn` amounts only for a linkable coin. No
114
134
  * on-chain total is kept. `NativeShieldedTokenPublicSupply` adds one and
115
135
  * makes burn amounts public.
116
136
  *
117
137
  * @notice Security:
118
138
  * - 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
139
+ * `amount` (`mint` also `recipient`, `burn` also `refundTo`,
140
+ * `burnFromSelf` also `coin.nonce` and `coin.value`) under `DS`, so a
120
141
  * signature authorizes one operation on one instance at one nonce, and is
121
- * neither replayable as the other operation, nor under another domain,
142
+ * neither replayable as another operation, nor under another domain,
122
143
  * nor redirectable. Removing the `self` word removes cross-instance
123
- * replay protection, since `DS` cannot carry the address.
144
+ * replay protection, since `DS` cannot carry the address. Only
145
+ * `burnFromSelf` binds its coin, because only there does the submitter
146
+ * pick it from the contract's own holdings. `Burn` needs no such word. Its
147
+ * coin is paid into the tx by whoever submits it so replaying a
148
+ * signature against a different coin destroys the submitter's own funds and
149
+ * still refunds the bound `refundTo`.
124
150
  * - Low-s: verification goes through `Ecdsa_secp256k1EcdsaVerifyLowS`, so a
125
151
  * high-s encoding is an invalid signature.
126
152
  * - Distinct signers: `assertApprovals` asserts both entries and rejects a
127
153
  * 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.
154
+ * required. The threshold `2` is the only value `EcdsaSignerManager` accepts.
132
155
  * - Immutable signer set: no signer reconfiguration circuit is exported, so
133
156
  * the three commitments and the threshold are fixed. Losing two of the
134
157
  * three keys freezes the contract.
135
- * - Griefing: the derived-nonce mint is the only mint path and it is
158
+ * - Griefing: the derived-nonce mints are the only mint paths and both are
136
159
  * threshold-gated, so `NativeShieldedTokenDerivedNonce`'s
137
160
  * collision-griefing precondition (a caller-nonce mint at another trust
138
161
  * level) is absent.
@@ -140,24 +163,24 @@ pragma language_version >= 0.26.0;
140
163
  * neither counter advances and the sender pays no fee.
141
164
  *
142
165
  * @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.
166
+ * - Replay counter: `mint`, `mintToSelf`, `burn` and `burnFromSelf` read
167
+ * `_counter` into the digest. The read is pinned, so any two of them in one block
168
+ * conflict: at most one operation lands per block, and the losing signer
169
+ * pair re-signs at the next nonce.
170
+ * - Derived-nonce counter: both mints also pin it (`evolveNonce` consumes
171
+ * it); the `_counter` conflict already covers those pairs.
172
+ * - `getNonce`: submitted as a tx it pins `_counter` and conflicts with any
173
+ * same-block operation.
151
174
  * - Views: metadata, `tokenColor` and the signer views read only state
152
175
  * written by `initialize`, so they commute with everything.
153
176
  *
154
177
  * @notice Scope:
155
178
  * 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.
179
+ * contract-held coins (from `mintToSelf` or `burnFromSelf` change) leave only
180
+ * through `burnFromSelf`, supplied by the operator as
181
+ * `QualifiedShieldedCoinInfo`. Coins minted to users are ordinary
182
+ * Zswap coins the contract neither tracks nor restricts; a holder redeems by
183
+ * paying the coin into a `burn` tx. No supply accounting.
161
184
  *
162
185
  * @notice Client-side duties:
163
186
  * - Before deploy: pick a random `instanceSalt`, derive each `cm` with
@@ -165,28 +188,40 @@ pragma language_version >= 0.26.0;
165
188
  * - Per operation: read `getNonce()`, rebuild the digest as
166
189
  * `EcdsaTestUtils.ts` does, collect two low-s signatures off-chain, submit
167
190
  * 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.
191
+ * - `mint`: hand the returned `ShieldedCoinInfo` to the recipient out of
192
+ * band; contract-initiated mints create no ciphertext, so a wallet cannot
193
+ * find the coin by scanning.
194
+ * - `mintToSelf`: persist the returned coin as a contract holding, as for
195
+ * `burnFromSelf` change.
196
+ * - `burn`: the holder's wallet funds `coin` into the tx; hand the returned
197
+ * refund coin to `refundTo` out of band. For an unlinkable burn the holder
198
+ * re-nonces the coin first (a wallet self-transfer yields a fresh secret
199
+ * nonce); the minted coin as-is is valid but linkable. The contract cannot
200
+ * enforce this.
201
+ * - `burnFromSelf` coin selection: pick the coin before collecting
202
+ * signatures, not after. Its `nonce` and `value` are in the digest, so
203
+ * signers approve one holding and the submitted coin must be that one.
204
+ * - `burnFromSelf` coin tracking: keep contract-held coins off-chain,
205
+ * recovering each coin's `mt_index` from the indexer's `ZswapOutput` /
206
+ * ledger events to build the `QualifiedShieldedCoinInfo` it takes.
207
+ * - `burnFromSelf` change coin: it replaces the spent coin as the contract's
208
+ * holding. Either persist the coin the circuit returns, or rebuild it from
209
+ * the signed `coinNonce` and `coinValue`, which determine it, so recovery
210
+ * doesn't need to rest on the submitter alone. Rebuilding leans on the protocol's
211
+ * nonce derivation which this module neither defines nor pins.
176
212
  *
177
213
  * @notice Scalability:
178
- * - Throughput: one mint-or-burn per block (see Concurrency).
214
+ * - Throughput: one operation per block (see Concurrency).
179
215
  * - State: constant-size, two counters and a three-entry signer set. Nothing
180
216
  * 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>`.
217
+ * - Amounts: mints cap at `Uint<64>` by `mintShieldedToken`. Burns take
218
+ * `Uint<128>`, the coin value range.
184
219
  *
185
220
  * @dev Composition:
186
221
  * - 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.
222
+ * `NST__mint` / `NST__mintToSelf` instead of `NSTDerivedNonce__deriveNonce()`.
223
+ * - Deposits: no deposit-and-hold path; `burn` receives and spends the
224
+ * holder's coin in one call.
190
225
  *
191
226
  * @notice Requires ZKIR v3: `keccak256` is unavailable on the v2 backend.
192
227
  */
@@ -195,7 +230,6 @@ module NativeShieldedTokenIssuer {
195
230
 
196
231
  import "../EcdsaSignerManager" prefix EcdsaSignerManager_;
197
232
  import "../../crypto/Eip712" prefix Eip712_;
198
- import "../../utils/Utils" prefix Utils_;
199
233
  import "../../utils/EvmAbi" prefix EvmAbi_;
200
234
  import "../../token/NativeShieldedToken" prefix NST_;
201
235
  import "../../token/extensions/NativeShieldedTokenDerivedNonce" prefix NSTDerivedNonce_;
@@ -212,10 +246,11 @@ module NativeShieldedTokenIssuer {
212
246
  export { _derivedNonceCounter };
213
247
 
214
248
  /**
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.
249
+ * @description Signature replay counter. Every operation (`mint`,
250
+ * `mintToSelf`, `burn`, `burnFromSelf`) reads it into its digest and
251
+ * increments it; `getNonce` returns the value
252
+ * the next digest binds to. The read is pinned, so at most one operation
253
+ * lands per block.
219
254
  */
220
255
  export ledger _counter: Counter;
221
256
 
@@ -277,19 +312,39 @@ module NativeShieldedTokenIssuer {
277
312
 
278
313
  /**
279
314
  * @description `keccak256` of the `Mint` type string:
280
- * `Mint(bytes32 contractAddress,bytes32 recipient,bool isContract,uint256 nonce,uint256 amount)`
315
+ * `Mint(bytes32 contractAddress,bytes32 recipient,uint256 nonce,uint256 amount)`
281
316
  *
282
- * @dev Written as a literal word; see `BURN_TYPEHASH`. The spec checks this
317
+ * @dev Written as a literal word; see `BURN_FROM_SELF_TYPEHASH`. The spec checks this
283
318
  * word against the type string above.
284
319
  *
285
320
  * @returns {Bytes<32>} The `Mint` type hash.
286
321
  */
287
322
  pure circuit MINT_TYPEHASH(): Bytes<32> {
288
323
  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
324
+ 0x65, 0x2d, 0x4d, 0xad, 0x8f, 0xb2, 0x6c, 0x5c,
325
+ 0x50, 0xc9, 0x87, 0xd4, 0x19, 0xdc, 0x93, 0x53,
326
+ 0x14, 0xa0, 0x1e, 0xd4, 0x5d, 0xb1, 0x59, 0xf2,
327
+ 0x30, 0xad, 0xc1, 0xa7, 0xe9, 0x02, 0x91, 0x1c
328
+ ];
329
+
330
+ return typeHash as Bytes<32>;
331
+ }
332
+
333
+ /**
334
+ * @description `keccak256` of the `MintToSelf` type string:
335
+ * `MintToSelf(bytes32 contractAddress,uint256 nonce,uint256 amount)`
336
+ *
337
+ * @dev Written as a literal word; see `BURN_FROM_SELF_TYPEHASH`. The spec
338
+ * checks this word against the type string above.
339
+ *
340
+ * @returns {Bytes<32>} The `MintToSelf` type hash.
341
+ */
342
+ pure circuit MINT_TO_SELF_TYPEHASH(): Bytes<32> {
343
+ const typeHash: Vector<32, Uint<8>> = [
344
+ 0xf8, 0xb3, 0x1e, 0x4e, 0x0f, 0x2d, 0x10, 0x33,
345
+ 0x82, 0xb4, 0xb3, 0x72, 0xac, 0x65, 0xa7, 0xad,
346
+ 0xcd, 0x03, 0x89, 0x39, 0x8d, 0x24, 0x6c, 0x5e,
347
+ 0xc2, 0xc9, 0x09, 0x38, 0x53, 0xdb, 0x49, 0x91
293
348
  ];
294
349
 
295
350
  return typeHash as Bytes<32>;
@@ -297,21 +352,41 @@ module NativeShieldedTokenIssuer {
297
352
 
298
353
  /**
299
354
  * @description `keccak256` of the `Burn` type string:
300
- * `Burn(bytes32 contractAddress,uint256 nonce,uint256 amount)`
355
+ * `Burn(bytes32 contractAddress,bytes32 refundTo,uint256 nonce,uint256 amount)`
356
+ *
357
+ * @dev Written as a literal word; see `BURN_FROM_SELF_TYPEHASH`. The spec
358
+ * checks this word against the type string above.
359
+ *
360
+ * @returns {Bytes<32>} The `Burn` type hash.
361
+ */
362
+ pure circuit BURN_TYPEHASH(): Bytes<32> {
363
+ const typeHash: Vector<32, Uint<8>> = [
364
+ 0x96, 0x8e, 0x03, 0xa5, 0xba, 0x91, 0xc8, 0xdd,
365
+ 0x27, 0x56, 0x79, 0x5a, 0xc6, 0x57, 0x52, 0x13,
366
+ 0x06, 0xe0, 0xd9, 0x7c, 0xe8, 0x1e, 0xbf, 0x9b,
367
+ 0x68, 0x24, 0x45, 0xa2, 0x86, 0xfc, 0xeb, 0xef
368
+ ];
369
+
370
+ return typeHash as Bytes<32>;
371
+ }
372
+
373
+ /**
374
+ * @description `keccak256` of the `BurnFromSelf` type string:
375
+ * `BurnFromSelf(bytes32 contractAddress,uint256 nonce,uint256 amount,bytes32 coinNonce,uint256 coinValue)`
301
376
  *
302
377
  * @dev Written as a literal word. `compactc` emits a full keccak
303
378
  * permutation even for a constant argument, so hashing the type string
304
379
  * in-circuit would add ~4.2k rows to every operation. The spec checks this
305
380
  * word against the type string above.
306
381
  *
307
- * @returns {Bytes<32>} The `Burn` type hash.
382
+ * @returns {Bytes<32>} The `BurnFromSelf` type hash.
308
383
  */
309
- pure circuit BURN_TYPEHASH(): Bytes<32> {
384
+ pure circuit BURN_FROM_SELF_TYPEHASH(): Bytes<32> {
310
385
  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
386
+ 0x50, 0xa1, 0xf2, 0x88, 0xba, 0xb8, 0x77, 0x4b,
387
+ 0x1f, 0x55, 0x86, 0x31, 0xc7, 0x75, 0xda, 0x49,
388
+ 0xec, 0x20, 0x4e, 0x07, 0xcf, 0x88, 0x53, 0x9b,
389
+ 0xca, 0x8e, 0xb9, 0x8c, 0xac, 0xf4, 0x5b, 0xe9
315
390
  ];
316
391
 
317
392
  return typeHash as Bytes<32>;
@@ -348,7 +423,7 @@ module NativeShieldedTokenIssuer {
348
423
  * @notice The returned coin info is the only copy available to the
349
424
  * recipient; deliver it out of band.
350
425
  *
351
- * @constraints k=17, rows=105173 (zkir-v3)
426
+ * @constraints k=17, rows=89815 (zkir-v3)
352
427
  *
353
428
  * Requirements:
354
429
  *
@@ -357,61 +432,135 @@ module NativeShieldedTokenIssuer {
357
432
  * - Signers must not be duplicates.
358
433
  * - Threshold must be met.
359
434
  * - `recipient` is not zero.
360
- * - A contract `recipient` is this contract's own address.
361
435
  *
362
436
  * @param {Uint<64>} amount - The token amount to mint.
363
- * @param {Either<ZswapCoinPublicKey, ContractAddress>} recipient - The address to receive the minted tokens.
437
+ * @param {ZswapCoinPublicKey} recipient - The coin public key to receive the minted tokens.
364
438
  * @param {Vector<2, Secp256k1Point>} pubkeys - Public keys of approving signers.
365
439
  * @param {Vector<2, Secp256k1EcdsaSignature>} signatures - ECDSA signatures over the mint hash.
366
440
  * @returns {ShieldedCoinInfo} The minted coin (nonce, color, value).
367
441
  */
368
442
  export circuit mint(
369
443
  amount: Uint<64>,
370
- recipient: Either<ZswapCoinPublicKey, ContractAddress>,
444
+ recipient: ZswapCoinPublicKey,
371
445
  pubkeys: Vector<2, Secp256k1Point>,
372
446
  signatures: Vector<2, Secp256k1EcdsaSignature>
373
447
  ): ShieldedCoinInfo {
374
448
  const opNonce = _counter;
375
449
  _counter.increment(1);
376
450
 
377
- // Canonicalize recipient to ensure garbage values don't sully the hash
378
- const canonRecipient = Utils_canonicalize<ZswapCoinPublicKey, ContractAddress>(recipient);
451
+ const msgHash = Eip712_hashTypedData(_domainSeparator, keccak256<Vector<5, Bytes<32>>>([
452
+ MINT_TYPEHASH(),
453
+ // Binds the digest to this deployment. The domain cannot carry the
454
+ // address, so removing this word removes cross-instance replay
455
+ // protection.
456
+ kernel.self().bytes,
457
+ recipient.bytes,
458
+ EvmAbi_uint64Word(opNonce),
459
+ EvmAbi_uint64Word(amount)
460
+ ]));
461
+
462
+ EcdsaSignerManager_assertApprovals(msgHash, pubkeys, signatures);
379
463
 
380
- // Only the active arm reaches the digest, so a crafted value carrying data
381
- // in its unused arm cannot alter what was signed.
382
- const recipientWord = canonRecipient.is_left
383
- ? canonRecipient.left.bytes
384
- : canonRecipient.right.bytes;
464
+ return NST__mint(recipient, amount, NSTDerivedNonce__deriveNonce());
465
+ }
385
466
 
386
- const msgHash = Eip712_hashTypedData(_domainSeparator, keccak256<Vector<6, Bytes<32>>>([
387
- MINT_TYPEHASH(),
467
+ /**
468
+ * @description Mints a new shielded coin of this contract's token to the
469
+ * contract itself, claimed in the same call, authorized by threshold
470
+ * signatures.
471
+ *
472
+ * @notice The returned coin info is the contract's only record of the
473
+ * holding; persist it for `burnFromSelf`.
474
+ *
475
+ * @constraints k=17, rows=85594 (zkir-v3)
476
+ *
477
+ * Requirements:
478
+ *
479
+ * - Both public keys must hash to registered signer commitments.
480
+ * - Both signatures must be valid over the mint-to-self message hash.
481
+ * - Signers must not be duplicates.
482
+ * - Threshold must be met.
483
+ *
484
+ * @param {Uint<64>} amount - The token amount to mint.
485
+ * @param {Vector<2, Secp256k1Point>} pubkeys - Public keys of approving signers.
486
+ * @param {Vector<2, Secp256k1EcdsaSignature>} signatures - ECDSA signatures over the mint-to-self hash.
487
+ * @returns {ShieldedCoinInfo} The minted coin (nonce, color, value).
488
+ */
489
+ export circuit mintToSelf(
490
+ amount: Uint<64>,
491
+ pubkeys: Vector<2, Secp256k1Point>,
492
+ signatures: Vector<2, Secp256k1EcdsaSignature>
493
+ ): ShieldedCoinInfo {
494
+ const opNonce = _counter;
495
+ _counter.increment(1);
496
+
497
+ const msgHash = Eip712_hashTypedData(_domainSeparator, keccak256<Vector<4, Bytes<32>>>([
498
+ MINT_TO_SELF_TYPEHASH(),
388
499
  // Binds the digest to this deployment. The domain cannot carry the
389
500
  // address, so removing this word removes cross-instance replay
390
501
  // protection.
391
502
  kernel.self().bytes,
392
- recipientWord,
393
- EvmAbi_boolWord(!canonRecipient.is_left),
394
503
  EvmAbi_uint64Word(opNonce),
395
504
  EvmAbi_uint64Word(amount)
396
505
  ]));
397
506
 
398
507
  EcdsaSignerManager_assertApprovals(msgHash, pubkeys, signatures);
399
508
 
400
- // `NST__mint` takes a coin public key only; the contract arm is limited to
401
- // this contract, since a mint to any other contract leaves an unclaimed
402
- // output.
403
- // The recipient is public (it is already in the signed digest), so the
404
- // arm may gate which mint runs.
405
- const target = disclose(canonRecipient);
406
- const self = kernel.self();
407
- assert(target.is_left || target.right == self,
408
- "NativeShieldedToken: recipient contract must be self");
409
- const nonce = NSTDerivedNonce__deriveNonce();
410
- if (target.is_left) {
411
- return NST__mint(target.left, amount, nonce);
412
- } else {
413
- return NST__mintToSelf(amount, nonce);
414
- }
509
+ return NST__mintToSelf(amount, NSTDerivedNonce__deriveNonce());
510
+ }
511
+
512
+ /**
513
+ * @description Burns `amount` from a holder's coin, authorized by threshold
514
+ * signatures. The holder's transaction pays `coin` in; the contract
515
+ * receives it and burns it in the same call. Any change goes to `refundTo`.
516
+ *
517
+ * @notice The returned refund coin info is the only copy available to
518
+ * `refundTo`; deliver it out of band.
519
+ *
520
+ * @constraints k=17, rows=119826 (zkir-v3)
521
+ *
522
+ * Requirements:
523
+ *
524
+ * - Both public keys must hash to registered signer commitments.
525
+ * - Both signatures must be valid over the burn message hash.
526
+ * - Signers must not be duplicates.
527
+ * - Threshold must be met.
528
+ * - `coin.color` is this contract's token color.
529
+ * - `amount` is less than or equal to `coin.value`.
530
+ * - `refundTo` is not zero.
531
+ *
532
+ * @param {ShieldedCoinInfo} coin - The holder's coin to burn from.
533
+ * @param {Uint<128>} amount - The token amount to burn.
534
+ * @param {ZswapCoinPublicKey} refundTo - Receives `coin.value - amount`.
535
+ * @param {Vector<2, Secp256k1Point>} pubkeys - Public keys of approving signers.
536
+ * @param {Vector<2, Secp256k1EcdsaSignature>} signatures - ECDSA signatures over the burn hash.
537
+ * @returns {Maybe<ShieldedCoinInfo>} The refund coin created for `refundTo`,
538
+ * or `none` if the coin was burned in full.
539
+ */
540
+ export circuit burn(
541
+ coin: ShieldedCoinInfo,
542
+ amount: Uint<128>,
543
+ refundTo: ZswapCoinPublicKey,
544
+ pubkeys: Vector<2, Secp256k1Point>,
545
+ signatures: Vector<2, Secp256k1EcdsaSignature>
546
+ ): Maybe<ShieldedCoinInfo> {
547
+ const opNonce = _counter;
548
+ _counter.increment(1);
549
+
550
+ const msgHash = Eip712_hashTypedData(_domainSeparator, keccak256<Vector<5, Bytes<32>>>([
551
+ BURN_TYPEHASH(),
552
+ // Binds the digest to this deployment. The domain cannot carry the
553
+ // address, so removing this word removes cross-instance replay
554
+ // protection.
555
+ kernel.self().bytes,
556
+ refundTo.bytes,
557
+ EvmAbi_uint64Word(opNonce),
558
+ EvmAbi_uint128Word(amount)
559
+ ]));
560
+
561
+ EcdsaSignerManager_assertApprovals(msgHash, pubkeys, signatures);
562
+
563
+ return NST__burn(coin, amount, refundTo);
415
564
  }
416
565
 
417
566
  /**
@@ -423,41 +572,47 @@ module NativeShieldedTokenIssuer {
423
572
  * and returned; it replaces `coin` as the contract's holding, so the
424
573
  * operator must persist it.
425
574
  *
426
- * @constraints k=17, rows=97232 (zkir-v3)
575
+ * The digest binds `coin.nonce` and `coin.value`, so approvals authorize one
576
+ * specific holding rather than an amount.
577
+ *
578
+ * @constraints k=17, rows=104981 (zkir-v3)
427
579
  *
428
580
  * Requirements:
429
581
  *
430
582
  * - Both public keys must hash to registered signer commitments.
431
- * - Both signatures must be valid over the burn message hash.
583
+ * - Both signatures must be valid over the burn-from-self message hash.
432
584
  * - Signers must not be duplicates.
433
585
  * - Threshold must be met.
434
586
  * - `coin.color` is this contract's token color.
435
587
  * - `amount` is less than or equal to `coin.value`.
588
+ * - `coin.nonce` and `coin.value` match the approved coin.
436
589
  *
437
590
  * @param {QualifiedShieldedCoinInfo} coin - The contract-held coin to burn from.
438
- * @param {Uint<64>} amount - The token amount to burn.
591
+ * @param {Uint<128>} amount - The token amount to burn.
439
592
  * @param {Vector<2, Secp256k1Point>} pubkeys - Public keys of approving signers.
440
- * @param {Vector<2, Secp256k1EcdsaSignature>} signatures - ECDSA signatures over the burn hash.
593
+ * @param {Vector<2, Secp256k1EcdsaSignature>} signatures - ECDSA signatures over the burn-from-self hash.
441
594
  * @returns {Maybe<ShieldedCoinInfo>} The change coin retained by the
442
595
  * contract, or `none` if the coin was burned in full.
443
596
  */
444
- export circuit burn(
597
+ export circuit burnFromSelf(
445
598
  coin: QualifiedShieldedCoinInfo,
446
- amount: Uint<64>,
599
+ amount: Uint<128>,
447
600
  pubkeys: Vector<2, Secp256k1Point>,
448
601
  signatures: Vector<2, Secp256k1EcdsaSignature>
449
602
  ): Maybe<ShieldedCoinInfo> {
450
603
  const opNonce = _counter;
451
604
  _counter.increment(1);
452
605
 
453
- const msgHash = Eip712_hashTypedData(_domainSeparator, keccak256<Vector<4, Bytes<32>>>([
454
- BURN_TYPEHASH(),
606
+ const msgHash = Eip712_hashTypedData(_domainSeparator, keccak256<Vector<6, Bytes<32>>>([
607
+ BURN_FROM_SELF_TYPEHASH(),
455
608
  // Binds the digest to this deployment. The domain cannot carry the
456
609
  // address, so removing this word removes cross-instance replay
457
610
  // protection.
458
611
  kernel.self().bytes,
459
612
  EvmAbi_uint64Word(opNonce),
460
- EvmAbi_uint64Word(amount)
613
+ EvmAbi_uint128Word(amount),
614
+ coin.nonce,
615
+ EvmAbi_uint128Word(coin.value)
461
616
  ]));
462
617
 
463
618
  EcdsaSignerManager_assertApprovals(msgHash, pubkeys, signatures);
@@ -466,7 +621,7 @@ module NativeShieldedTokenIssuer {
466
621
  }
467
622
 
468
623
  /**
469
- * @description Returns the counter the next mint or burn digest will bind to.
624
+ * @description Returns the counter the next operation's digest will bind to.
470
625
  *
471
626
  * @constraints k=6, rows=34 (zkir-v3)
472
627
  *