@openzeppelin/compact-contracts 0.1.0 → 0.3.0-alpha

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 (33) hide show
  1. package/README.md +15 -16
  2. package/access/AccessControl.compact +3 -35
  3. package/access/Ownable.compact +47 -60
  4. package/access/ShieldedAccessControl.compact +43 -10
  5. package/access/ZOwnablePK.compact +45 -10
  6. package/crypto/ElGamal.compact +473 -0
  7. package/multisig/ForwarderPrivate.compact +229 -0
  8. package/multisig/ForwarderShielded.compact +149 -0
  9. package/multisig/ForwarderUnshielded.compact +152 -0
  10. package/multisig/ProposalManager.compact +328 -0
  11. package/multisig/ShieldedTreasury.compact +201 -0
  12. package/multisig/ShieldedTreasuryStateless.compact +76 -0
  13. package/multisig/Signer.compact +312 -0
  14. package/multisig/SignerManager.compact +205 -0
  15. package/multisig/UnshieldedTreasury.compact +114 -0
  16. package/multisig/presets/ShieldedMultiSig.compact +236 -0
  17. package/multisig/presets/ShieldedMultiSigV2.compact +280 -0
  18. package/multisig/presets/ShieldedMultiSigV3.compact +368 -0
  19. package/multisig/presets/forwarder/ForwarderPrivate.compact +125 -0
  20. package/multisig/presets/forwarder/ForwarderShielded.compact +62 -0
  21. package/multisig/presets/forwarder/ForwarderUnshielded.compact +64 -0
  22. package/package.json +12 -6
  23. package/security/Allowlist.compact +101 -0
  24. package/security/Blocklist.compact +99 -0
  25. package/security/Initializable.compact +18 -2
  26. package/security/Pausable.compact +2 -2
  27. package/token/FungibleToken.compact +79 -103
  28. package/token/MultiToken.compact +67 -93
  29. package/token/NativeShieldedToken.compact +285 -0
  30. package/token/NativeShieldedTokenCore.compact +343 -0
  31. package/token/NativeShieldedTokenFamily.compact +287 -0
  32. package/token/NonFungibleToken.compact +85 -110
  33. package/utils/Utils.compact +86 -2
@@ -0,0 +1,101 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha (security/Allowlist.compact)
3
+
4
+ pragma language_version >= 0.23.0;
5
+
6
+ /**
7
+ * @module Allowlist
8
+ * @description A minimal, reusable permit-list keyed by an abstract `Bytes<32>`
9
+ * account identifier. It answers a single question: is this account a member?
10
+ *
11
+ * This module is a pure membership MECHANISM, not a policy. It holds no notion
12
+ * of "when" the list is enforced and no access control. The mutators (`_allow`,
13
+ * `_disallow`) are deliberately UNGATED composable primitives: a composing
14
+ * contract gates them behind its access-control module (the same way a token
15
+ * gates `_mint` / `_burn`), and decides when to call `assertAllowed` (for
16
+ * example, only while an "eligibility required" flag is set). Keeping the
17
+ * enforce-or-not decision in the composing layer leaves this module general
18
+ * enough to back KYC allowlists, permissioned mints, membership gates, and the
19
+ * like.
20
+ *
21
+ * @notice This is the PUBLIC (transparent) allowlist: membership status is
22
+ * publicly readable and the approved set is publicly enumerable. That is the
23
+ * right posture for transparent or public-graph assets. A graph-private asset
24
+ * should instead use a shielded allowlist (a public allowlist would re-expose
25
+ * the candidate set and bound the anonymity set).
26
+ */
27
+ module Allowlist {
28
+ import CompactStandardLibrary;
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // State
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /** @description Approved accounts. Absent means not allowed. */
35
+ export ledger _allowed: Set<Bytes<32>>;
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Views
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * @description Returns whether `account` is currently allowed.
43
+ *
44
+ * @circuitInfo k=9, rows=305
45
+ *
46
+ * @param {Bytes<32>} account - The account to query.
47
+ * @return {Boolean} - True if `account` is a member of the allowlist.
48
+ */
49
+ export circuit isAllowed(account: Bytes<32>): Boolean {
50
+ return _allowed.member(disclose(account));
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Check (the eligibility seam)
55
+ // ---------------------------------------------------------------------------
56
+
57
+ /**
58
+ * @description Asserts that `account` is allowed. The composing contract
59
+ * decides when this gate applies.
60
+ *
61
+ * @circuitInfo k=9, rows=305
62
+ *
63
+ * Requirements:
64
+ *
65
+ * - `account` is a member of the allowlist.
66
+ *
67
+ * @param {Bytes<32>} account - The account that must be allowed.
68
+ * @return {[]} - Empty tuple.
69
+ */
70
+ export circuit assertAllowed(account: Bytes<32>): [] {
71
+ assert(isAllowed(account), "Allowlist: account not allowed");
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Mutators (UNGATED - the composing contract is responsible for gating these)
76
+ // ---------------------------------------------------------------------------
77
+
78
+ /**
79
+ * @description Adds `account` to the allowlist.
80
+ *
81
+ * @circuitInfo k=9, rows=303
82
+ *
83
+ * @param {Bytes<32>} account - The account to allow.
84
+ * @return {[]} - Empty tuple.
85
+ */
86
+ export circuit _allow(account: Bytes<32>): [] {
87
+ _allowed.insert(disclose(account));
88
+ }
89
+
90
+ /**
91
+ * @description Removes `account` from the allowlist.
92
+ *
93
+ * @circuitInfo k=9, rows=303
94
+ *
95
+ * @param {Bytes<32>} account - The account to disallow.
96
+ * @return {[]} - Empty tuple.
97
+ */
98
+ export circuit _disallow(account: Bytes<32>): [] {
99
+ _allowed.remove(disclose(account));
100
+ }
101
+ }
@@ -0,0 +1,99 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha (security/Blocklist.compact)
3
+
4
+ pragma language_version >= 0.23.0;
5
+
6
+ /**
7
+ * @module Blocklist
8
+ * @description A minimal, reusable deny-list keyed by an abstract `Bytes<32>`
9
+ * account identifier. It answers a single question: is this account blocked?
10
+ *
11
+ * This module is a pure membership MECHANISM, not a policy. It holds no notion
12
+ * of "when" the list is enforced and no access control. The mutators (`_block`,
13
+ * `_unblock`) are deliberately UNGATED composable primitives: a composing
14
+ * contract gates them behind its access-control module (the same way a token
15
+ * gates `_mint` / `_burn`), and decides when to call `assertNotBlocked`. Keeping
16
+ * the enforce-or-not decision in the composing layer leaves this module general
17
+ * enough to back sanctions lists, account freezes, abuse denials, and the like.
18
+ *
19
+ * @notice This is the PUBLIC (transparent) blocklist: membership status is
20
+ * publicly readable and the blocked set is publicly enumerable. That is the
21
+ * right posture for transparent or public-graph assets. A graph-private asset
22
+ * should instead use a shielded blocklist (a public blocklist would re-expose
23
+ * the candidate set and bound the anonymity set).
24
+ */
25
+ module Blocklist {
26
+ import CompactStandardLibrary;
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // State
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /** @description Blocked accounts. Absent means not blocked. */
33
+ export ledger _blocked: Set<Bytes<32>>;
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Views
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /**
40
+ * @description Returns whether `account` is currently blocked.
41
+ *
42
+ * @circuitInfo k=9, rows=305
43
+ *
44
+ * @param {Bytes<32>} account - The account to query.
45
+ * @return {Boolean} - True if `account` is a member of the blocklist.
46
+ */
47
+ export circuit isBlocked(account: Bytes<32>): Boolean {
48
+ return _blocked.member(disclose(account));
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Check (the eligibility seam)
53
+ // ---------------------------------------------------------------------------
54
+
55
+ /**
56
+ * @description Asserts that `account` is not blocked. The composing contract
57
+ * decides when this gate applies.
58
+ *
59
+ * @circuitInfo k=9, rows=308
60
+ *
61
+ * Requirements:
62
+ *
63
+ * - `account` is not a member of the blocklist.
64
+ *
65
+ * @param {Bytes<32>} account - The account that must not be blocked.
66
+ * @return {[]} - Empty tuple.
67
+ */
68
+ export circuit assertNotBlocked(account: Bytes<32>): [] {
69
+ assert(!isBlocked(account), "Blocklist: account blocked");
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Mutators (UNGATED - the composing contract is responsible for gating these)
74
+ // ---------------------------------------------------------------------------
75
+
76
+ /**
77
+ * @description Adds `account` to the blocklist.
78
+ *
79
+ * @circuitInfo k=9, rows=303
80
+ *
81
+ * @param {Bytes<32>} account - The account to block.
82
+ * @return {[]} - Empty tuple.
83
+ */
84
+ export circuit _block(account: Bytes<32>): [] {
85
+ _blocked.insert(disclose(account));
86
+ }
87
+
88
+ /**
89
+ * @description Removes `account` from the blocklist.
90
+ *
91
+ * @circuitInfo k=9, rows=303
92
+ *
93
+ * @param {Bytes<32>} account - The account to unblock.
94
+ * @return {[]} - Empty tuple.
95
+ */
96
+ export circuit _unblock(account: Bytes<32>): [] {
97
+ _blocked.remove(disclose(account));
98
+ }
99
+ }
@@ -1,11 +1,27 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.1.0 (security/Initializable.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha (security/Initializable.compact)
3
3
 
4
- pragma language_version >= 0.21.0;
4
+ pragma language_version >= 0.23.0;
5
5
 
6
6
  /**
7
7
  * @module Initializable
8
8
  * @description Initializable provides a simple mechanism that mimics the functionality of a constructor.
9
+ *
10
+ * @notice ⚠️ TECHNICAL WARNING: only use this module at the top contract layer, and
11
+ * import it into at most one module per contract.
12
+ *
13
+ * The Compact compiler deduplicates a shared transitive dependency's ledger state
14
+ * inconsistently depending on directory layout
15
+ * (https://github.com/LFDT-Minokawa/compact/issues/270, reproduced on compiler
16
+ * 0.29.0–0.31.0). When two modules in the SAME directory both import this module,
17
+ * their `_isInitialized` flags collapse into a SINGLE shared ledger slot: calling
18
+ * `initialize()` on one marks the other as initialized, and the other can never be
19
+ * initialized independently. Modules in different directories get separate slots.
20
+ * The result is unpredictable and a security hazard for composed contracts.
21
+ *
22
+ * For this reason the OpenZeppelin modules no longer use this module: each tracks
23
+ * its own `_isInitialized` ledger flag inline. This module is kept for direct,
24
+ * single-import use and can be reintroduced across modules once #270 is resolved.
9
25
  */
10
26
  module Initializable {
11
27
  import CompactStandardLibrary;
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.1.0 (security/Pausable.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha (security/Pausable.compact)
3
3
 
4
- pragma language_version >= 0.21.0;
4
+ pragma language_version >= 0.23.0;
5
5
 
6
6
  /**
7
7
  * @module Pausable
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.1.0 (token/FungibleToken.compact)
2
+ // OpenZeppelin Compact Contracts v0.3.0-alpha (token/FungibleToken.compact)
3
3
 
4
- pragma language_version >= 0.21.0;
4
+ pragma language_version >= 0.23.0;
5
5
 
6
6
  /**
7
7
  * @module FungibleToken
@@ -64,9 +64,15 @@ pragma language_version >= 0.21.0;
64
64
  */
65
65
  module FungibleToken {
66
66
  import CompactStandardLibrary;
67
- import "../security/Initializable" prefix Initializable_;
68
67
  import "../utils/Utils" prefix Utils_;
69
68
 
69
+ /**
70
+ * @description Initialization flag, tracked per-module to avoid the compiler's
71
+ * shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the
72
+ * Initializable module for rationale.
73
+ */
74
+ export ledger _isInitialized: Boolean;
75
+
70
76
  /**
71
77
  * @description Mapping from account addresses to their token balances.
72
78
  * @type {Either<Bytes<32>, ContractAddress>} account - The account address.
@@ -75,6 +81,7 @@ module FungibleToken {
75
81
  * @type {Map<Either<Bytes<32>, ContractAddress>, Uint<128>>} _balances
76
82
  */
77
83
  export ledger _balances: Map<Either<Bytes<32>, ContractAddress>, Uint<128>>;
84
+
78
85
  /**
79
86
  * @description Mapping from owner accounts to spender accounts and their allowances.
80
87
  * @type {Either<Bytes<32>, ContractAddress>} account - The owner account address.
@@ -104,19 +111,6 @@ module FungibleToken {
104
111
  */
105
112
  witness wit_FungibleTokenSK(): Bytes<32>;
106
113
 
107
- /**
108
- * @description Returns a canonical zero Either value for Bytes<32> and ContractAddress.
109
- * This circuit returns the left variant (Bytes<32>) to avoid misleading contract-to-contract
110
- * error messages.
111
- *
112
- * @return {Either<Bytes<32>, ContractAddress>} - The zero value.
113
- */
114
- export pure circuit ZERO(): Either<Bytes<32>, ContractAddress> {
115
- return Either<Bytes<32>, ContractAddress> {
116
- is_left: true, left: default<Bytes<32>>, right: default<ContractAddress>
117
- };
118
- }
119
-
120
114
  /**
121
115
  * @description Initializes the contract by setting the name, symbol, and decimals.
122
116
  * @dev This MUST be called in the implementing contract's constructor. Failure to do so
@@ -134,12 +128,39 @@ module FungibleToken {
134
128
  symbol_: Opaque<"string">,
135
129
  decimals_: Uint<8>
136
130
  ): [] {
137
- Initializable_initialize();
131
+ assertNotInitialized();
132
+ _isInitialized = true;
138
133
  _name = disclose(name_);
139
134
  _symbol = disclose(symbol_);
140
135
  _decimals = disclose(decimals_);
141
136
  }
142
137
 
138
+ /**
139
+ * @description Asserts that the contract has been initialized, throwing an error if not.
140
+ *
141
+ * Requirements:
142
+ *
143
+ * - Contract must be initialized.
144
+ *
145
+ * @return {[]} - Empty tuple.
146
+ */
147
+ circuit assertInitialized(): [] {
148
+ assert(_isInitialized, "FungibleToken: contract not initialized");
149
+ }
150
+
151
+ /**
152
+ * @description Asserts that the contract has not been initialized, throwing an error if it has.
153
+ *
154
+ * Requirements:
155
+ *
156
+ * - Contract must not be initialized.
157
+ *
158
+ * @return {[]} - Empty tuple.
159
+ */
160
+ circuit assertNotInitialized(): [] {
161
+ assert(!_isInitialized, "FungibleToken: contract already initialized");
162
+ }
163
+
143
164
  /**
144
165
  * @description Returns the token name.
145
166
  *
@@ -152,7 +173,7 @@ module FungibleToken {
152
173
  * @return {Opaque<"string">} - The token name.
153
174
  */
154
175
  export circuit name(): Opaque<"string"> {
155
- Initializable_assertInitialized();
176
+ assertInitialized();
156
177
  return _name;
157
178
  }
158
179
 
@@ -168,7 +189,7 @@ module FungibleToken {
168
189
  * @return {Opaque<"string">} - The token name.
169
190
  */
170
191
  export circuit symbol(): Opaque<"string"> {
171
- Initializable_assertInitialized();
192
+ assertInitialized();
172
193
  return _symbol;
173
194
  }
174
195
 
@@ -184,7 +205,7 @@ module FungibleToken {
184
205
  * @return {Uint<8>} - The account's token balance.
185
206
  */
186
207
  export circuit decimals(): Uint<8> {
187
- Initializable_assertInitialized();
208
+ assertInitialized();
188
209
  return _decimals;
189
210
  }
190
211
 
@@ -200,7 +221,7 @@ module FungibleToken {
200
221
  * @return {Uint<128>} - The total supply of tokens.
201
222
  */
202
223
  export circuit totalSupply(): Uint<128> {
203
- Initializable_assertInitialized();
224
+ assertInitialized();
204
225
  return _totalSupply;
205
226
  }
206
227
 
@@ -219,7 +240,7 @@ module FungibleToken {
219
240
  * @return {Uint<128>} - The account's token balance.
220
241
  */
221
242
  export circuit balanceOf(account: Either<Bytes<32>, ContractAddress>): Uint<128> {
222
- Initializable_assertInitialized();
243
+ assertInitialized();
223
244
  const canonAcct = Utils_canonicalize<Bytes<32>, ContractAddress>(account);
224
245
 
225
246
  if (!_balances.member(disclose(canonAcct))) {
@@ -253,7 +274,7 @@ module FungibleToken {
253
274
  to: Either<Bytes<32>, ContractAddress>,
254
275
  value: Uint<128>
255
276
  ): Boolean {
256
- Initializable_assertInitialized();
277
+ assertInitialized();
257
278
  const isContractAddr = !to.is_left;
258
279
  assert(!isContractAddr, "FungibleToken: unsafe transfer");
259
280
 
@@ -283,7 +304,7 @@ module FungibleToken {
283
304
  to: Either<Bytes<32>, ContractAddress>,
284
305
  value: Uint<128>
285
306
  ): Boolean {
286
- Initializable_assertInitialized();
307
+ assertInitialized();
287
308
  const owner = left<Bytes<32>, ContractAddress>(_computeAccountId());
288
309
  _unsafeUncheckedTransfer(owner, to, value);
289
310
  return true;
@@ -309,7 +330,7 @@ module FungibleToken {
309
330
  owner: Either<Bytes<32>, ContractAddress>,
310
331
  spender: Either<Bytes<32>, ContractAddress>
311
332
  ): Uint<128> {
312
- Initializable_assertInitialized();
333
+ assertInitialized();
313
334
  const canonOwner = Utils_canonicalize<Bytes<32>, ContractAddress>(owner);
314
335
  const canonSpender = Utils_canonicalize<Bytes<32>, ContractAddress>(spender);
315
336
 
@@ -337,7 +358,7 @@ module FungibleToken {
337
358
  export circuit approve(spender: Either<Bytes<32>, ContractAddress>,
338
359
  value: Uint<128>
339
360
  ): Boolean {
340
- Initializable_assertInitialized();
361
+ assertInitialized();
341
362
 
342
363
  const owner = left<Bytes<32>, ContractAddress>(_computeAccountId());
343
364
  _approve(owner, spender, value);
@@ -373,7 +394,7 @@ module FungibleToken {
373
394
  to: Either<Bytes<32>, ContractAddress>,
374
395
  value: Uint<128>
375
396
  ): Boolean {
376
- Initializable_assertInitialized();
397
+ assertInitialized();
377
398
  const isContractAddr = !to.is_left;
378
399
  assert(!isContractAddr, "FungibleToken: unsafe transfer");
379
400
  return _unsafeTransferFrom(fromAddress, to, value);
@@ -406,7 +427,7 @@ module FungibleToken {
406
427
  to: Either<Bytes<32>, ContractAddress>,
407
428
  value: Uint<128>
408
429
  ): Boolean {
409
- Initializable_assertInitialized();
430
+ assertInitialized();
410
431
 
411
432
  const spender = left<Bytes<32>, ContractAddress>(_computeAccountId());
412
433
  _spendAllowance(fromAddress, spender, value);
@@ -443,7 +464,7 @@ module FungibleToken {
443
464
  to: Either<Bytes<32>, ContractAddress>,
444
465
  value: Uint<128>
445
466
  ): [] {
446
- Initializable_assertInitialized();
467
+ assertInitialized();
447
468
  const isContractAddr = !to.is_left;
448
469
  assert(!isContractAddr, "FungibleToken: unsafe transfer");
449
470
  _unsafeUncheckedTransfer(fromAddress, to, value);
@@ -474,9 +495,9 @@ module FungibleToken {
474
495
  to: Either<Bytes<32>, ContractAddress>,
475
496
  value: Uint<128>
476
497
  ): [] {
477
- Initializable_assertInitialized();
478
- assert(!_isTargetZero(fromAddress), "FungibleToken: invalid sender");
479
- assert(!_isTargetZero(to), "FungibleToken: invalid receiver");
498
+ assertInitialized();
499
+ assert(!Utils_isTargetZero(fromAddress), "FungibleToken: invalid sender");
500
+ assert(!Utils_isTargetZero(to), "FungibleToken: invalid receiver");
480
501
 
481
502
  _update(fromAddress, to, value);
482
503
  }
@@ -501,11 +522,11 @@ module FungibleToken {
501
522
  to: Either<Bytes<32>, ContractAddress>,
502
523
  value: Uint<128>
503
524
  ): [] {
504
- Initializable_assertInitialized();
525
+ assertInitialized();
505
526
  const canonFrom = Utils_canonicalize<Bytes<32>, ContractAddress>(fromAddress);
506
527
  const canonTo = Utils_canonicalize<Bytes<32>, ContractAddress>(to);
507
528
 
508
- if (_isTargetZero(disclose(canonFrom))) {
529
+ if (Utils_isTargetZero(disclose(canonFrom))) {
509
530
  // Mint
510
531
  const MAX_UINT128 = 340282366920938463463374607431768211455;
511
532
  assert(MAX_UINT128 - _totalSupply >= value, "FungibleToken: arithmetic overflow");
@@ -517,7 +538,7 @@ module FungibleToken {
517
538
  _balances.insert(disclose(canonFrom), disclose(fromBal - value as Uint<128>));
518
539
  }
519
540
 
520
- if (_isTargetZero(disclose(canonTo))) {
541
+ if (Utils_isTargetZero(disclose(canonTo))) {
521
542
  // Burn
522
543
  _totalSupply = disclose(_totalSupply - value as Uint<128>);
523
544
  } else {
@@ -547,7 +568,7 @@ module FungibleToken {
547
568
  * @return {[]} - Empty tuple.
548
569
  */
549
570
  export circuit _mint(account: Either<Bytes<32>, ContractAddress>, value: Uint<128>): [] {
550
- Initializable_assertInitialized();
571
+ assertInitialized();
551
572
  const isContractAddr = !account.is_left;
552
573
  assert(!isContractAddr, "FungibleToken: unsafe transfer");
553
574
  _unsafeMint(account, value);
@@ -575,9 +596,9 @@ module FungibleToken {
575
596
  account: Either<Bytes<32>, ContractAddress>,
576
597
  value: Uint<128>
577
598
  ): [] {
578
- Initializable_assertInitialized();
579
- assert(!_isTargetZero(account), "FungibleToken: invalid receiver");
580
- _update(ZERO(), account, value);
599
+ assertInitialized();
600
+ assert(!Utils_isTargetZero(account), "FungibleToken: invalid receiver");
601
+ _update(Utils_zeroAccount(), account, value);
581
602
  }
582
603
 
583
604
  /**
@@ -597,9 +618,9 @@ module FungibleToken {
597
618
  * @return {[]} - Empty tuple.
598
619
  */
599
620
  export circuit _burn(account: Either<Bytes<32>, ContractAddress>, value: Uint<128>): [] {
600
- Initializable_assertInitialized();
601
- assert(!_isTargetZero(account), "FungibleToken: invalid sender");
602
- _update(account, ZERO(), value);
621
+ assertInitialized();
622
+ assert(!Utils_isTargetZero(account), "FungibleToken: invalid sender");
623
+ _update(account, Utils_zeroAccount(), value);
603
624
  }
604
625
 
605
626
  /**
@@ -625,12 +646,12 @@ module FungibleToken {
625
646
  spender: Either<Bytes<32>, ContractAddress>,
626
647
  value: Uint<128>
627
648
  ): [] {
628
- Initializable_assertInitialized();
649
+ assertInitialized();
629
650
  const canonOwner = Utils_canonicalize<Bytes<32>, ContractAddress>(owner);
630
651
  const canonSpender = Utils_canonicalize<Bytes<32>, ContractAddress>(spender);
631
652
 
632
- assert(!_isTargetZero(canonOwner), "FungibleToken: invalid owner");
633
- assert(!_isTargetZero(canonSpender), "FungibleToken: invalid spender");
653
+ assert(!Utils_isTargetZero(canonOwner), "FungibleToken: invalid owner");
654
+ assert(!Utils_isTargetZero(canonSpender), "FungibleToken: invalid spender");
634
655
 
635
656
  if (!_allowances.member(disclose(canonOwner))) {
636
657
  // If owner doesn't exist, create and insert a new sub-map directly
@@ -663,20 +684,21 @@ module FungibleToken {
663
684
  spender: Either<Bytes<32>, ContractAddress>,
664
685
  value: Uint<128>
665
686
  ): [] {
666
- Initializable_assertInitialized();
687
+ assertInitialized();
667
688
  const canonOwner = Utils_canonicalize<Bytes<32>, ContractAddress>(owner);
668
689
  const canonSpender = Utils_canonicalize<Bytes<32>, ContractAddress>(spender);
669
690
 
670
- assert((_allowances.member(disclose(canonOwner)) &&
671
- _allowances.lookup(canonOwner).member(disclose(canonSpender))),
672
- "FungibleToken: insufficient allowance"
673
- );
674
-
675
- const currentAllowance = _allowances.lookup(canonOwner).lookup(disclose(canonSpender));
676
- const MAX_UINT128 = 340282366920938463463374607431768211455;
677
- if (currentAllowance < MAX_UINT128) {
678
- assert(currentAllowance >= value, "FungibleToken: insufficient allowance");
679
- _approve(canonOwner, canonSpender, currentAllowance - value as Uint<128>);
691
+ // A missing allowance entry is treated as a zero allowance, mirroring
692
+ // `allowance`, and a zero-value spend is a no-op. Guarding on `value`
693
+ // avoids reverting (outside an assertion) on a missing entry and avoids
694
+ // writing a spurious zero entry for a zero-value spend.
695
+ if (disclose(value > 0)) {
696
+ const currentAllowance = allowance(canonOwner, canonSpender);
697
+ const MAX_UINT128 = 340282366920938463463374607431768211455;
698
+ if (currentAllowance < MAX_UINT128) {
699
+ assert(currentAllowance >= value, "FungibleToken: insufficient allowance");
700
+ _approve(canonOwner, canonSpender, currentAllowance - value as Uint<128>);
701
+ }
680
702
  }
681
703
  }
682
704
 
@@ -691,52 +713,6 @@ module FungibleToken {
691
713
  * @returns {Bytes<32>} accountId - The computed account identifier.
692
714
  */
693
715
  circuit _computeAccountId(): Bytes<32> {
694
- return computeAccountId(wit_FungibleTokenSK());
695
- }
696
-
697
- /**
698
- * @description Computes an account identifier without on-chain state, allowing a user to derive
699
- * their identity commitment before submitting a token operation.
700
- * This is the off-chain counterpart to {_computeAccountId} and produces an identical result
701
- * given the same inputs.
702
- *
703
- * @warning OpSec: The `secretKey` parameter is a sensitive secret. Mishandling it can
704
- * permanently compromise the security of this system:
705
- *
706
- * - **Never log or persist** the `secretKey` in plaintext — avoid browser devtools,
707
- * application logs, analytics pipelines, or any observable side-channel.
708
- * - **Store offline or in secure enclaves** — hardware security modules (HSMs),
709
- * air-gapped devices, or encrypted vaults are strongly preferred over hot storage.
710
- * - **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()`
711
- * or equivalent; weak or predictable keys can be brute-forced to reveal your identity.
712
- * - **Treat key loss as identity loss** — a lost key cannot be recovered.
713
- * - **Avoid calling this circuit in untrusted environments** — executing this in an
714
- * unverified browser extension, compromised runtime, or shared machine may expose
715
- * the key to a malicious observer.
716
- *
717
- * ## ID Derivation
718
- * `accountId = persistentHash(secretKey)`
719
- *
720
- * @param {Bytes<32>} secretKey - A 32-byte cryptographically secure random value.
721
- *
722
- * @returns {Bytes<32>} accountId - The computed account identifier.
723
- */
724
- export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> {
725
- return persistentHash<Vector<1, Bytes<32>>>([secretKey]);
726
- }
727
-
728
- /**
729
- * @description Returns `true` if `target`'s active branch (as indicated by `is_left`)
730
- * holds the zero value.
731
- *
732
- * @param {Either<Bytes<32>, ContractAddress>} target - The value to check.
733
- * @returns {Boolean} - `true` if the active branch is zero, `false` otherwise.
734
- */
735
- circuit _isTargetZero(target: Either<Bytes<32>, ContractAddress>): Boolean {
736
- if (target.is_left) {
737
- return target.left == default<Bytes<32>>;
738
- } else {
739
- return target.right == default<ContractAddress>;
740
- }
716
+ return Utils_computeAccountId(wit_FungibleTokenSK());
741
717
  }
742
718
  }