@lfdecentralizedtrust/zeto-contracts 0.2.2 → 0.5.0

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 (152) hide show
  1. package/README.md +70 -162
  2. package/contracts/factory.sol +30 -34
  3. package/contracts/factory_upgradeable.sol +11 -7
  4. package/contracts/lib/common/util.sol +40 -0
  5. package/contracts/lib/interfaces/ILockableCapability.sol +318 -0
  6. package/contracts/lib/interfaces/{izeto.sol → IZeto.sol} +13 -1
  7. package/contracts/lib/interfaces/{izeto_initializable.sol → IZetoInitializable.sol} +14 -12
  8. package/contracts/lib/interfaces/{izeto_kyc.sol → IZetoKyc.sol} +3 -3
  9. package/contracts/lib/interfaces/IZetoLockableCapability.sol +237 -0
  10. package/contracts/lib/interfaces/IZetoStorage.sol +106 -0
  11. package/contracts/lib/interfaces/{izeto_lockable.sol → IZetoVerifier.sol} +8 -21
  12. package/contracts/lib/registry.sol +74 -26
  13. package/contracts/lib/storage/base.sol +210 -0
  14. package/contracts/lib/storage/nullifier.sol +166 -0
  15. package/contracts/lib/zeto_common.sol +277 -28
  16. package/contracts/lib/zeto_fungible.sol +451 -33
  17. package/contracts/lib/zeto_fungible_base.sol +69 -0
  18. package/contracts/lib/zeto_fungible_burn.sol +83 -53
  19. package/contracts/lib/zeto_fungible_burn_nullifier.sol +115 -72
  20. package/contracts/lib/zeto_fungible_nullifier.sol +167 -0
  21. package/contracts/lib/zeto_lockable.sol +613 -0
  22. package/contracts/lib/zeto_non_fungible.sol +236 -0
  23. package/contracts/lib/zeto_non_fungible_base.sol +61 -0
  24. package/contracts/lib/zeto_non_fungible_nullifier.sol +61 -0
  25. package/contracts/test/escrow1.sol +90 -34
  26. package/contracts/test/escrow2.sol +64 -29
  27. package/contracts/test/qurrency.sol +10 -2
  28. package/contracts/test/smt.sol +6 -1
  29. package/contracts/test/tendecimals.sol +14 -12
  30. package/contracts/verifiers/impl/anon.sol +189 -0
  31. package/contracts/verifiers/impl/anon_batch.sol +301 -0
  32. package/contracts/verifiers/impl/anon_enc.sol +266 -0
  33. package/contracts/verifiers/impl/anon_enc_batch.sol +602 -0
  34. package/contracts/verifiers/impl/anon_enc_nullifier.sol +287 -0
  35. package/contracts/verifiers/impl/anon_enc_nullifier_batch.sol +679 -0
  36. package/contracts/verifiers/impl/anon_enc_nullifier_kyc.sol +294 -0
  37. package/contracts/verifiers/impl/anon_enc_nullifier_kyc_batch.sol +686 -0
  38. package/contracts/verifiers/impl/anon_enc_nullifier_non_repudiation.sol +413 -0
  39. package/contracts/verifiers/impl/anon_enc_nullifier_non_repudiation_batch.sol +1141 -0
  40. package/contracts/verifiers/impl/anon_nullifier_kyc_transfer.sol +217 -0
  41. package/contracts/verifiers/impl/anon_nullifier_kyc_transferLocked.sol +196 -0
  42. package/contracts/verifiers/impl/anon_nullifier_kyc_transferLocked_batch.sol +308 -0
  43. package/contracts/verifiers/impl/anon_nullifier_kyc_transfer_batch.sol +385 -0
  44. package/contracts/verifiers/impl/anon_nullifier_qurrency_transfer.sol +497 -0
  45. package/contracts/verifiers/impl/anon_nullifier_qurrency_transfer_batch.sol +1001 -0
  46. package/contracts/verifiers/{verifier_anon_nullifier_transferLocked.sol → impl/anon_nullifier_transfer.sol} +12 -19
  47. package/contracts/verifiers/{verifier_anon_nullifier_transferLocked_batch.sol → impl/anon_nullifier_transfer_batch.sol} +44 -51
  48. package/contracts/verifiers/impl/burn.sol +182 -0
  49. package/contracts/verifiers/impl/burn_batch.sol +238 -0
  50. package/contracts/verifiers/impl/burn_nullifier.sol +203 -0
  51. package/contracts/verifiers/impl/burn_nullifier_batch.sol +315 -0
  52. package/contracts/verifiers/impl/deposit.sol +182 -0
  53. package/contracts/verifiers/impl/deposit_kyc.sol +189 -0
  54. package/contracts/verifiers/impl/nf_anon.sol +175 -0
  55. package/contracts/verifiers/{verifier_nf_anon_nullifier_transferLocked.sol → impl/nf_anon_nullifier_transfer.sol} +6 -13
  56. package/contracts/verifiers/impl/withdraw.sol +189 -0
  57. package/contracts/verifiers/impl/withdraw_batch.sol +245 -0
  58. package/contracts/verifiers/impl/withdraw_nullifier.sol +210 -0
  59. package/contracts/verifiers/impl/withdraw_nullifier_batch.sol +322 -0
  60. package/contracts/verifiers/verifier_anon.sol +31 -186
  61. package/contracts/verifiers/verifier_anon_batch.sol +31 -298
  62. package/contracts/verifiers/verifier_anon_enc.sol +31 -263
  63. package/contracts/verifiers/verifier_anon_enc_batch.sol +31 -599
  64. package/contracts/verifiers/verifier_anon_enc_nullifier.sol +31 -284
  65. package/contracts/verifiers/verifier_anon_enc_nullifier_batch.sol +33 -676
  66. package/contracts/verifiers/verifier_anon_enc_nullifier_kyc.sol +31 -291
  67. package/contracts/verifiers/verifier_anon_enc_nullifier_kyc_batch.sol +33 -683
  68. package/contracts/verifiers/verifier_anon_enc_nullifier_non_repudiation.sol +33 -410
  69. package/contracts/verifiers/verifier_anon_enc_nullifier_non_repudiation_batch.sol +33 -1138
  70. package/contracts/verifiers/verifier_anon_nullifier_kyc_transfer.sol +33 -214
  71. package/contracts/verifiers/verifier_anon_nullifier_kyc_transferLocked.sol +33 -221
  72. package/contracts/verifiers/verifier_anon_nullifier_kyc_transferLocked_batch.sol +33 -389
  73. package/contracts/verifiers/verifier_anon_nullifier_kyc_transfer_batch.sol +33 -382
  74. package/contracts/verifiers/verifier_anon_nullifier_qurrency_transfer.sol +33 -221
  75. package/contracts/verifiers/verifier_anon_nullifier_qurrency_transfer_batch.sol +33 -389
  76. package/contracts/verifiers/verifier_anon_nullifier_transfer.sol +33 -207
  77. package/contracts/verifiers/verifier_anon_nullifier_transfer_batch.sol +33 -375
  78. package/contracts/verifiers/verifier_burn.sol +31 -179
  79. package/contracts/verifiers/verifier_burn_batch.sol +31 -235
  80. package/contracts/verifiers/verifier_burn_nullifier.sol +31 -200
  81. package/contracts/verifiers/verifier_burn_nullifier_batch.sol +31 -312
  82. package/contracts/verifiers/verifier_deposit.sol +31 -179
  83. package/contracts/verifiers/verifier_deposit_kyc.sol +34 -0
  84. package/contracts/verifiers/verifier_nf_anon.sol +31 -172
  85. package/contracts/verifiers/verifier_nf_anon_nullifier_transfer.sol +33 -179
  86. package/contracts/verifiers/verifier_withdraw.sol +31 -186
  87. package/contracts/verifiers/verifier_withdraw_batch.sol +31 -242
  88. package/contracts/verifiers/verifier_withdraw_nullifier.sol +31 -207
  89. package/contracts/verifiers/verifier_withdraw_nullifier_batch.sol +33 -319
  90. package/contracts/zeto_anon.sol +77 -231
  91. package/contracts/zeto_anon_burnable.sol +56 -12
  92. package/contracts/zeto_anon_enc.sol +93 -190
  93. package/contracts/zeto_anon_enc_nullifier.sol +249 -195
  94. package/contracts/zeto_anon_enc_nullifier_kyc.sol +51 -232
  95. package/contracts/zeto_anon_enc_nullifier_non_repudiation.sol +231 -238
  96. package/contracts/zeto_anon_nullifier.sol +164 -298
  97. package/contracts/zeto_anon_nullifier_burnable.sol +68 -18
  98. package/contracts/zeto_anon_nullifier_kyc.sol +40 -345
  99. package/contracts/zeto_anon_nullifier_qurrency.sol +217 -361
  100. package/contracts/zeto_nf_anon.sol +55 -130
  101. package/contracts/zeto_nf_anon_nullifier.sol +90 -152
  102. package/hardhat.config.ts +9 -3
  103. package/ignition/modules/lib/deps.ts +8 -0
  104. package/ignition/modules/zeto_anon_burnable.ts +8 -7
  105. package/ignition/modules/zeto_anon_enc_nullifier.ts +31 -0
  106. package/ignition/modules/zeto_anon_enc_nullifier_kyc.ts +2 -2
  107. package/ignition/modules/zeto_anon_nullifier.ts +28 -4
  108. package/ignition/modules/zeto_anon_nullifier_burnable.ts +50 -16
  109. package/ignition/modules/zeto_anon_nullifier_kyc.ts +27 -12
  110. package/ignition/modules/zeto_nf_anon_nullifier.ts +14 -10
  111. package/package.json +4 -2
  112. package/scripts/deploy_cloneable.ts +19 -10
  113. package/scripts/deploy_upgradeable.ts +9 -5
  114. package/scripts/tokens/Zeto_Anon.ts +3 -3
  115. package/scripts/tokens/Zeto_AnonBurnable.ts +1 -1
  116. package/scripts/tokens/Zeto_AnonEnc.ts +12 -3
  117. package/scripts/tokens/Zeto_AnonEncNullifier.ts +5 -3
  118. package/scripts/tokens/Zeto_AnonEncNullifierKyc.ts +1 -1
  119. package/scripts/tokens/Zeto_AnonEncNullifierNonRepudiation.ts +1 -1
  120. package/scripts/tokens/Zeto_AnonNullifier.ts +1 -1
  121. package/scripts/tokens/Zeto_AnonNullifierBurnable.ts +1 -1
  122. package/scripts/tokens/Zeto_AnonNullifierKyc.ts +1 -1
  123. package/scripts/tokens/Zeto_AnonNullifierQurrency.ts +1 -1
  124. package/scripts/tokens/Zeto_NfAnon.ts +2 -2
  125. package/scripts/tokens/Zeto_NfAnonNullifier.ts +1 -1
  126. package/test/factory.ts +46 -73
  127. package/test/lib/anon_nullifier_helpers.ts +89 -0
  128. package/test/lib/anon_zeto_helpers.ts +76 -0
  129. package/test/lib/deploy.ts +3 -2
  130. package/test/lib/utils.ts +74 -35
  131. package/test/test/escrow1.ts +185 -58
  132. package/test/test/escrow2.ts +200 -107
  133. package/test/test/qurrency.ts +13 -33
  134. package/test/usdc-shielding.ts +39 -26
  135. package/test/utils.ts +144 -21
  136. package/test/zeto_anon.ts +956 -465
  137. package/test/zeto_anon_enc.ts +881 -142
  138. package/test/zeto_anon_enc_nullifier.ts +847 -38
  139. package/test/zeto_anon_enc_nullifier_kyc.ts +120 -181
  140. package/test/zeto_anon_enc_nullifier_non_repudiation.ts +77 -46
  141. package/test/zeto_anon_nullifier.ts +1212 -953
  142. package/test/zeto_anon_nullifier_kyc.ts +674 -655
  143. package/test/zeto_anon_nullifier_qurrency.ts +452 -306
  144. package/test/zeto_nf_anon.ts +737 -137
  145. package/test/zeto_nf_anon_nullifier.ts +876 -246
  146. package/contracts/lib/zeto_base.sol +0 -293
  147. package/contracts/lib/zeto_fungible_withdraw.sol +0 -132
  148. package/contracts/lib/zeto_fungible_withdraw_nullifier.sol +0 -144
  149. package/contracts/lib/zeto_nullifier.sol +0 -340
  150. package/contracts/zkDvP.sol_ +0 -273
  151. package/test/zkDvP.ts_ +0 -455
  152. /package/contracts/lib/{common.sol → common/common.sol} +0 -0
@@ -0,0 +1,106 @@
1
+ // Copyright © 2025 Kaleido, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+ // Licensed under the Apache License, Version 2.0 (the "License");
6
+ // you may not use this file except in compliance with the License.
7
+ // You may obtain a copy of the License at
8
+ //
9
+ // http://www.apache.org/licenses/LICENSE-2.0
10
+ //
11
+ // Unless required by applicable law or agreed to in writing, software
12
+ // distributed under the License is distributed on an "AS IS" BASIS,
13
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ // See the License for the specific language governing permissions and
15
+ // limitations under the License.
16
+ pragma solidity ^0.8.27;
17
+
18
+ interface IZetoStorage {
19
+ enum UTXOStatus {
20
+ UNKNOWN, // default value for the empty UTXO slots
21
+ UNSPENT,
22
+ SPENT
23
+ }
24
+
25
+ // For the main UTXO storage
26
+
27
+ /**
28
+ * @dev query whether a UTXO is currently spent
29
+ * @param txo The UTXO to check
30
+ * @return bool whether the UTXO is spent
31
+ */
32
+ function spent(uint256 txo) external view returns (UTXOStatus);
33
+
34
+ /**
35
+ * @dev validate the inputs
36
+ * @param inputs The inputs to validate
37
+ * @param inputsLocked Whether the inputs are locked
38
+ */
39
+ function validateInputs(
40
+ uint256[] calldata inputs,
41
+ bool inputsLocked
42
+ ) external view;
43
+
44
+ /**
45
+ * @dev validate the outputs
46
+ * @param outputs The outputs to validate
47
+ */
48
+ function validateOutputs(uint256[] calldata outputs) external view;
49
+
50
+ /**
51
+ * @dev validate the root
52
+ * @param root The root to validate
53
+ * @return bool whether the root is valid
54
+ */
55
+ function validateRoot(uint256 root) external view returns (bool);
56
+
57
+ /**
58
+ * @dev get the root
59
+ * @return uint256 the root
60
+ */
61
+ function getRoot() external view returns (uint256);
62
+
63
+ /**
64
+ * @dev process the inputs
65
+ * @param inputs The inputs to process
66
+ * @param inputsLocked Whether the inputs are locked
67
+ */
68
+ function processInputs(
69
+ uint256[] calldata inputs,
70
+ bool inputsLocked
71
+ ) external;
72
+
73
+ /**
74
+ * @dev process the outputs
75
+ * @param outputs The outputs to process
76
+ */
77
+ function processOutputs(uint256[] calldata outputs) external;
78
+
79
+ /**
80
+ * @dev Register freshly-minted locked UTXOs in the locked-UTXO ledger.
81
+ *
82
+ * The storage layer is intentionally a thin UTXO substrate and
83
+ * does NOT track who is allowed to spend a locked UTXO; that
84
+ * responsibility belongs to the Zeto contract on top (e.g. the
85
+ * lock-id-keyed `_locks[lockId].spender` in
86
+ * `ZetoFungible`-style lockable-capability contracts). Callers
87
+ * that need delegate/spender semantics must maintain that state
88
+ * themselves above this interface.
89
+ *
90
+ * @param lockedOutputs The locked UTXOs to register as UNSPENT
91
+ */
92
+ function processLockedOutputs(uint256[] calldata lockedOutputs) external;
93
+
94
+ /**
95
+ * @dev Query whether a UTXO is currently in the locked-unspent set.
96
+ *
97
+ * The delegate / spender of the lock is intentionally NOT
98
+ * returned here: the storage layer no longer tracks it. To
99
+ * surface the spender alongside the locked-state, expose the
100
+ * combined view at the Zeto layer instead.
101
+ *
102
+ * @param utxo The UTXO to check
103
+ * @return whether the UTXO is currently locked-unspent
104
+ */
105
+ function locked(uint256 utxo) external view returns (bool);
106
+ }
@@ -1,4 +1,4 @@
1
- // Copyright © 2024 Kaleido, Inc.
1
+ // Copyright © 2025 Kaleido, Inc.
2
2
  //
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
  //
@@ -15,24 +15,11 @@
15
15
  // limitations under the License.
16
16
  pragma solidity ^0.8.27;
17
17
 
18
- import {Commonlib} from "../common.sol";
19
-
20
- interface IZetoLockable {
21
- error UTXOAlreadyLocked(uint256 utxo);
22
- error UTXONotLocked(uint256 utxo);
23
- error NotLockDelegate(uint256 utxo, address delegate, address sender);
24
- event UTXOsLocked(
25
- uint256[] inputs,
26
- uint256[] outputs,
27
- uint256[] lockedOutputs,
28
- address indexed delegate,
29
- address indexed submitter,
30
- bytes data
31
- );
32
- event LockDelegateChanged(
33
- uint256[] lockedOutputs,
34
- address indexed oldDelegate,
35
- address indexed newDelegate,
36
- bytes data
37
- );
18
+ interface IGroth16Verifier {
19
+ function verify(
20
+ uint[2] calldata _pA,
21
+ uint[2][2] calldata _pB,
22
+ uint[2] calldata _pC,
23
+ uint[] calldata _pubSignals
24
+ ) external view returns (bool);
38
25
  }
@@ -15,81 +15,129 @@
15
15
  // limitations under the License.
16
16
  pragma solidity ^0.8.27;
17
17
 
18
- import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
18
+ import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
19
19
  import {SmtLib} from "@iden3/contracts/contracts/lib/SmtLib.sol";
20
20
  import {PoseidonHasher} from "@iden3/contracts/contracts/lib/hash/PoseidonHasher.sol";
21
21
  import {PoseidonUnit2L, PoseidonUnit3L} from "@iden3/contracts/contracts/lib/Poseidon.sol";
22
- import {Commonlib} from "./common.sol";
23
- import {IZeto} from "./interfaces/izeto.sol";
24
- import {IZetoKyc} from "./interfaces/izeto_kyc.sol";
25
-
26
- uint256 constant MAX_SMT_DEPTH = 64;
22
+ import {MAX_SMT_DEPTH} from "./interfaces/IZeto.sol";
23
+ import {IZetoKyc} from "./interfaces/IZetoKyc.sol";
27
24
 
28
25
  /// @title A sample on-chain implementation of a KYC registry
26
+ /// @author Kaleido, Inc.
29
27
  /// @dev The registry uses a Sparse Merkle Tree to store the
30
28
  /// Zeto accounts (Babyjubjub keys), so that transaction
31
29
  /// submitters can generate proofs of membership for the
32
30
  /// accounts in a privacy-preserving manner.
33
- /// @author Kaleido, Inc.
34
- abstract contract Registry is OwnableUpgradeable, IZetoKyc {
35
- SmtLib.Data internal _publicKeysTree;
31
+ ///
32
+ /// Mixed into KYC-aware concrete tokens (e.g. {Zeto_AnonNullifierKyc})
33
+ /// so that those tokens can expose `register`/`isRegistered` and
34
+ /// contribute the identities tree's root as an additional public input
35
+ /// to the transfer/deposit ZK circuits via the inheriting token's
36
+ /// {extraInputs}/{extraInputsForDeposit} overrides.
37
+ ///
38
+ /// Inherits {Ownable2StepUpgradeable} (rather than the simpler
39
+ /// {OwnableUpgradeable}) so that the diamond formed when this mixin is
40
+ /// composed with a token that already inherits {ZetoCommon}, which is
41
+ /// itself {Ownable2StepUpgradeable}, resolves cleanly: both branches of
42
+ /// the inheritance graph reach `transferOwnership`/`_transferOwnership`
43
+ /// through the same {Ownable2StepUpgradeable} implementation, so no
44
+ /// leaf-level override is required. The Ownable initialization itself
45
+ /// is performed once by the token's `__ZetoCommon_init`; the Registry
46
+ /// initializer only needs to set up the SMT.
47
+ abstract contract Registry is Ownable2StepUpgradeable, IZetoKyc {
36
48
  using SmtLib for SmtLib.Data;
37
49
 
38
- error AlreadyRegistered(uint256[2]);
50
+ /// @dev Thrown by {register} when the supplied public key is already
51
+ /// a member of the identities tree.
52
+ error AlreadyRegistered(uint256[2] publicKey);
39
53
 
54
+ /// @dev Internal-only so it can only be called from a derived
55
+ /// contract's own `initializer`-guarded entrypoint.
40
56
  function __Registry_init() internal onlyInitializing {
41
- _publicKeysTree.initialize(MAX_SMT_DEPTH);
42
- _publicKeysTree.setHasher(new PoseidonHasher());
57
+ RegistryStorage.Layout storage $ = RegistryStorage.layout();
58
+ $.publicKeysTree.initialize(MAX_SMT_DEPTH);
59
+ $.publicKeysTree.setHasher(new PoseidonHasher());
43
60
  }
44
61
 
62
+ /**
63
+ * @dev Register a new Zeto account.
64
+ * @param publicKey The public Babyjubjub key to register.
65
+ * @param data Opaque off-chain correlation data appended to the
66
+ * {IdentityRegistered} event.
67
+ *
68
+ * Emits an {IdentityRegistered} event.
69
+ */
45
70
  function register(
46
- uint256[2] memory publicKey,
71
+ uint256[2] calldata publicKey,
47
72
  bytes calldata data
48
73
  ) public onlyOwner {
49
74
  _register(publicKey, data);
50
75
  }
51
76
 
52
- /// @dev returns whether the given public key is registered
53
- /// @param publicKey The Babyjubjub public key to check
54
- /// @return bool whether the given public key is included in the registry
77
+ /**
78
+ * @dev Returns whether the given public key is registered.
79
+ * @param publicKey The Babyjubjub public key to check.
80
+ */
55
81
  function isRegistered(
56
- uint256[2] memory publicKey
82
+ uint256[2] calldata publicKey
57
83
  ) public view returns (bool) {
58
84
  uint256 nodeKey = _getIdentitiesLeafNodeKey(publicKey);
59
- SmtLib.Node memory node = _publicKeysTree.getNode(nodeKey);
85
+ SmtLib.Node memory node = RegistryStorage.layout().publicKeysTree.getNode(
86
+ nodeKey
87
+ );
60
88
  return node.nodeType != SmtLib.NodeType.EMPTY;
61
89
  }
62
90
 
91
+ /**
92
+ * @dev Returns the root of the identities tree. This is contributed as
93
+ * an extra public input to the transfer / deposit circuits in
94
+ * KYC-aware token contracts via {extraInputs}.
95
+ */
63
96
  function getIdentitiesRoot() public view returns (uint256) {
64
- return _publicKeysTree.getRoot();
97
+ return RegistryStorage.layout().publicKeysTree.getRoot();
65
98
  }
66
99
 
67
- /// @dev Register a new Zeto account
68
- /// @param publicKey The public Babyjubjub key to register
69
100
  function _register(
70
- uint256[2] memory publicKey,
101
+ uint256[2] calldata publicKey,
71
102
  bytes calldata data
72
103
  ) internal {
73
104
  uint256 nodeHash = _getIdentitiesLeafNodeHash(publicKey);
74
- SmtLib.Node memory node = _publicKeysTree.getNode(nodeHash);
105
+ RegistryStorage.Layout storage $ = RegistryStorage.layout();
106
+ SmtLib.Node memory node = $.publicKeysTree.getNode(nodeHash);
75
107
  if (node.nodeType != SmtLib.NodeType.EMPTY) {
76
108
  revert AlreadyRegistered(publicKey);
77
109
  }
78
- _publicKeysTree.addLeaf(nodeHash, nodeHash);
110
+ $.publicKeysTree.addLeaf(nodeHash, nodeHash);
79
111
  emit IdentityRegistered(publicKey, data);
80
112
  }
81
113
 
82
114
  function _getIdentitiesLeafNodeHash(
83
- uint256[2] memory publicKey
115
+ uint256[2] calldata publicKey
84
116
  ) internal pure returns (uint256) {
85
117
  return PoseidonUnit2L.poseidon(publicKey);
86
118
  }
87
119
 
88
120
  function _getIdentitiesLeafNodeKey(
89
- uint256[2] memory publicKey
121
+ uint256[2] calldata publicKey
90
122
  ) internal pure returns (uint256) {
91
123
  uint256 nodeHash = PoseidonUnit2L.poseidon(publicKey);
92
124
  uint256[3] memory params = [nodeHash, nodeHash, uint256(1)];
93
125
  return PoseidonUnit3L.poseidon(params);
94
126
  }
95
127
  }
128
+
129
+ /// @dev ERC-7201 (`erc7201:zeto.storage.Registry`).
130
+ library RegistryStorage {
131
+ struct Layout {
132
+ SmtLib.Data publicKeysTree;
133
+ }
134
+
135
+ bytes32 private constant STORAGE_LOCATION =
136
+ 0xf79d6fea6eabe5c80b325fcf2331fe08f18a473c50e27c55890676ff6746fd00;
137
+
138
+ function layout() internal pure returns (Layout storage $) {
139
+ assembly {
140
+ $.slot := STORAGE_LOCATION
141
+ }
142
+ }
143
+ }
@@ -0,0 +1,210 @@
1
+ // Copyright © 2025 Kaleido, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+ // Licensed under the Apache License, Version 2.0 (the "License");
6
+ // you may not use this file except in compliance with the License.
7
+ // You may obtain a copy of the License at
8
+ //
9
+ // http://www.apache.org/licenses/LICENSE-2.0
10
+ //
11
+ // Unless required by applicable law or agreed to in writing, software
12
+ // distributed under the License is distributed on an "AS IS" BASIS,
13
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ // See the License for the specific language governing permissions and
15
+ // limitations under the License.
16
+ pragma solidity ^0.8.27;
17
+
18
+ import {IZetoConstants} from "../interfaces/IZeto.sol";
19
+ import {IZetoLockableCapability} from "../interfaces/IZetoLockableCapability.sol";
20
+ import {IZetoStorage} from "../interfaces/IZetoStorage.sol";
21
+ import {Commonlib} from "../common/common.sol";
22
+ import {Util} from "../common/util.sol";
23
+
24
+ contract BaseStorage is IZetoStorage, IZetoConstants {
25
+ /// @dev Thrown when a state-mutating storage call is made by anyone other
26
+ /// than the Zeto contract that deployed this storage instance.
27
+ error StorageUnauthorized(address caller);
28
+
29
+ /// @dev Thrown by view stubs that only have a meaningful implementation
30
+ /// in nullifier-based storage (e.g. {validateRoot}, {getRoot}).
31
+ /// Replaces the previous `revert("Not implemented")` strings so
32
+ /// callers can match a typed error.
33
+ error NotImplemented();
34
+
35
+ /**
36
+ * @dev The Zeto contract that owns this storage instance. Captured at
37
+ * construction time and immutable for the lifetime of the storage
38
+ * contract. All state-mutating entry points are gated to this
39
+ * address; view functions remain unrestricted.
40
+ *
41
+ * This storage contract is created via `new BaseStorage()` (or a
42
+ * subclass) from inside the Zeto contract's initializer, so
43
+ * `msg.sender` at construction is the Zeto proxy address.
44
+ */
45
+ address internal immutable _zeto;
46
+
47
+ // tracks all the regular (unlocked) UTXOs
48
+ mapping(uint256 => UTXOStatus) internal _utxos;
49
+ // used for tracking locked UTXOs. multi-step transaction flows that require counterparties
50
+ // to upload proofs. To protect the party that uploads their proof first,
51
+ // and prevent any other party from utilizing the uploaded proof to execute
52
+ // a transaction, the input UTXOs or nullifiers can be locked and only usable
53
+ // by the same party that did the locking.
54
+ //
55
+ // The "who is allowed to spend a locked UTXO" question is intentionally
56
+ // NOT answered at this layer; the Zeto contract on top owns that state
57
+ // (e.g. `_locks[lockId].spender` in ZetoFungible-style lockable
58
+ // capabilities). This contract only tracks whether the UTXO is locked.
59
+ mapping(uint256 => UTXOStatus) internal _lockedUtxos;
60
+
61
+ /// @dev Reverts unless the caller is the Zeto contract that deployed this
62
+ /// storage instance. Applied to every state-mutating function so the
63
+ /// storage contract cannot be poked directly by external accounts.
64
+ modifier onlyZeto() {
65
+ if (msg.sender != _zeto) {
66
+ revert StorageUnauthorized(msg.sender);
67
+ }
68
+ _;
69
+ }
70
+
71
+ constructor() {
72
+ _zeto = msg.sender;
73
+ }
74
+
75
+ /// @notice Returns the address authorized to call state-mutating
76
+ /// functions on this storage contract.
77
+ function zeto() external view returns (address) {
78
+ return _zeto;
79
+ }
80
+
81
+ function validateInputs(
82
+ uint256[] calldata inputs,
83
+ bool inputsLocked
84
+ ) public view virtual {
85
+ // sort the inputs to detect duplicates
86
+ uint256[] memory sortedInputs = Util.sortCommitments(inputs);
87
+ // Check the inputs are all unspent
88
+ for (uint256 i = 0; i < sortedInputs.length; ++i) {
89
+ if (sortedInputs[i] == 0) {
90
+ // skip the zero inputs
91
+ continue;
92
+ }
93
+ if (i > 0 && sortedInputs[i] == sortedInputs[i - 1]) {
94
+ revert UTXODuplicate(sortedInputs[i]);
95
+ }
96
+ if (
97
+ _lockedUtxos[sortedInputs[i]] == UTXOStatus.UNKNOWN &&
98
+ _utxos[sortedInputs[i]] == UTXOStatus.UNKNOWN
99
+ ) {
100
+ revert UTXONotMinted(sortedInputs[i]);
101
+ }
102
+ if (
103
+ _lockedUtxos[sortedInputs[i]] == UTXOStatus.SPENT ||
104
+ _utxos[sortedInputs[i]] == UTXOStatus.SPENT
105
+ ) {
106
+ revert UTXOAlreadySpent(sortedInputs[i]);
107
+ }
108
+ if (
109
+ !inputsLocked &&
110
+ _lockedUtxos[sortedInputs[i]] == UTXOStatus.UNSPENT
111
+ ) {
112
+ revert IZetoLockableCapability.AlreadyLocked(sortedInputs[i]);
113
+ }
114
+ }
115
+ }
116
+
117
+ function validateOutputs(uint256[] calldata outputs) public view virtual {
118
+ // sort the outputs to detect duplicates
119
+ uint256[] memory sortedOutputs = Util.sortCommitments(outputs);
120
+
121
+ // Check for duplicate outputs
122
+ for (uint256 i = 0; i < sortedOutputs.length; ++i) {
123
+ uint256 output = sortedOutputs[i];
124
+ if (output == 0) {
125
+ // skip the zero outputs
126
+ continue;
127
+ }
128
+ if (i > 0 && output == sortedOutputs[i - 1]) {
129
+ revert UTXODuplicate(output);
130
+ }
131
+ // check the outputs are all new - looking in the locked and unlocked UTXOs
132
+ if (
133
+ _utxos[output] == UTXOStatus.SPENT ||
134
+ _lockedUtxos[output] == UTXOStatus.SPENT
135
+ ) {
136
+ revert UTXOAlreadySpent(output);
137
+ } else if (
138
+ _utxos[output] == UTXOStatus.UNSPENT ||
139
+ _lockedUtxos[output] == UTXOStatus.UNSPENT
140
+ ) {
141
+ revert UTXOAlreadyOwned(output);
142
+ }
143
+ }
144
+ }
145
+
146
+ // Only needed for the nullifier based implementation
147
+ function validateRoot(uint256) public view virtual returns (bool) {
148
+ revert NotImplemented();
149
+ }
150
+
151
+ // Only needed for the nullifier based implementation
152
+ function getRoot() public view virtual returns (uint256) {
153
+ revert NotImplemented();
154
+ }
155
+
156
+ function processInputs(
157
+ uint256[] calldata inputs,
158
+ bool inputsLocked
159
+ ) public virtual onlyZeto {
160
+ mapping(uint256 => UTXOStatus) storage utxos = inputsLocked
161
+ ? _lockedUtxos
162
+ : _utxos;
163
+ // consume the input UTXOs
164
+ for (uint256 i = 0; i < inputs.length; ++i) {
165
+ if (inputs[i] != 0) {
166
+ utxos[inputs[i]] = UTXOStatus.SPENT;
167
+ }
168
+ }
169
+ }
170
+
171
+ function processOutputs(
172
+ uint256[] calldata outputs
173
+ ) public virtual onlyZeto {
174
+ for (uint256 i = 0; i < outputs.length; ++i) {
175
+ if (outputs[i] != 0) {
176
+ _utxos[outputs[i]] = UTXOStatus.UNSPENT;
177
+ }
178
+ }
179
+ }
180
+
181
+ function processLockedOutputs(
182
+ uint256[] calldata lockedOutputs
183
+ ) public virtual onlyZeto {
184
+ // register the locked UTXOs as UNSPENT in the locked-UTXO ledger.
185
+ for (uint256 i = 0; i < lockedOutputs.length; ++i) {
186
+ if (lockedOutputs[i] != 0) {
187
+ _lockedUtxos[lockedOutputs[i]] = UTXOStatus.UNSPENT;
188
+ }
189
+ }
190
+ }
191
+
192
+ function locked(uint256 utxo) public view returns (bool) {
193
+ return _lockedUtxos[utxo] == UTXOStatus.UNSPENT;
194
+ }
195
+
196
+ function spent(uint256 utxo) public view virtual returns (UTXOStatus) {
197
+ if (
198
+ _utxos[utxo] == UTXOStatus.SPENT ||
199
+ _lockedUtxos[utxo] == UTXOStatus.SPENT
200
+ ) {
201
+ return UTXOStatus.SPENT;
202
+ } else if (
203
+ _utxos[utxo] == UTXOStatus.UNSPENT ||
204
+ _lockedUtxos[utxo] == UTXOStatus.UNSPENT
205
+ ) {
206
+ return UTXOStatus.UNSPENT;
207
+ }
208
+ return UTXOStatus.UNKNOWN;
209
+ }
210
+ }
@@ -0,0 +1,166 @@
1
+ // Copyright © 2025 Kaleido, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+ // Licensed under the Apache License, Version 2.0 (the "License");
6
+ // you may not use this file except in compliance with the License.
7
+ // You may obtain a copy of the License at
8
+ //
9
+ // http://www.apache.org/licenses/LICENSE-2.0
10
+ //
11
+ // Unless required by applicable law or agreed to in writing, software
12
+ // distributed under the License is distributed on an "AS IS" BASIS,
13
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ // See the License for the specific language governing permissions and
15
+ // limitations under the License.
16
+ pragma solidity ^0.8.27;
17
+
18
+ import {IZetoConstants, MAX_SMT_DEPTH} from "../interfaces/IZeto.sol";
19
+ import {IZetoStorage} from "../interfaces/IZetoStorage.sol";
20
+ import {Commonlib} from "../common/common.sol";
21
+ import {Util} from "../common/util.sol";
22
+ import {SmtLib} from "@iden3/contracts/contracts/lib/SmtLib.sol";
23
+ import {IHasher} from "@iden3/contracts/contracts/interfaces/IHasher.sol";
24
+ import {PoseidonUnit3L} from "@iden3/contracts/contracts/lib/Poseidon.sol";
25
+ import {PoseidonHasher} from "@iden3/contracts/contracts/lib/hash/PoseidonHasher.sol";
26
+ import {BaseStorage} from "./base.sol";
27
+
28
+ contract NullifierStorage is BaseStorage {
29
+ // used for tracking regular (unlocked) UTXOs
30
+ // locked UTXOs are tracked in the base storage
31
+ SmtLib.Data internal _commitmentsTree;
32
+ using SmtLib for SmtLib.Data;
33
+
34
+ mapping(uint256 => bool) private _nullifiers;
35
+
36
+ error UTXORootNotFound(uint256 root);
37
+
38
+ constructor() {
39
+ _commitmentsTree.initialize(MAX_SMT_DEPTH);
40
+ IHasher hasher = new PoseidonHasher();
41
+ _commitmentsTree.setHasher(hasher);
42
+ }
43
+
44
+ function validateInputs(
45
+ uint256[] calldata inputs,
46
+ bool inputsLocked
47
+ ) public view override {
48
+ if (inputsLocked) {
49
+ // locked inputs are regular UTXOs (rather than nullifiers)
50
+ // and are validated in the base storage
51
+ super.validateInputs(inputs, inputsLocked);
52
+ return;
53
+ }
54
+
55
+ // sort the nullifiers to detect duplicates
56
+ uint256[] memory sortedInputs = Util.sortCommitments(inputs);
57
+
58
+ // Check the inputs are all unspent
59
+ for (uint256 i = 0; i < sortedInputs.length; ++i) {
60
+ if (sortedInputs[i] == 0) {
61
+ // skip the zero inputs
62
+ continue;
63
+ }
64
+ if (i > 0 && sortedInputs[i] == sortedInputs[i - 1]) {
65
+ revert UTXODuplicate(sortedInputs[i]);
66
+ }
67
+ if (_nullifiers[sortedInputs[i]] == true) {
68
+ revert UTXOAlreadySpent(sortedInputs[i]);
69
+ }
70
+ }
71
+ }
72
+
73
+ function validateOutputs(uint256[] calldata outputs) public view override {
74
+ // sort the outputs to detect duplicates
75
+ uint256[] memory sortedOutputs = Util.sortCommitments(outputs);
76
+
77
+ // Check the outputs are all new UTXOs
78
+ for (uint256 i = 0; i < sortedOutputs.length; ++i) {
79
+ if (sortedOutputs[i] == 0) {
80
+ // skip the zero outputs
81
+ continue;
82
+ }
83
+ if (i > 0 && sortedOutputs[i] == sortedOutputs[i - 1]) {
84
+ revert UTXODuplicate(sortedOutputs[i]);
85
+ }
86
+ // check the unlocked commitments tree
87
+ bool existsInTree = exists(sortedOutputs[i]);
88
+ if (existsInTree) {
89
+ revert UTXOAlreadyOwned(sortedOutputs[i]);
90
+ }
91
+ }
92
+ }
93
+
94
+ function validateRoot(uint256 root) public view override returns (bool) {
95
+ // Check if the root has existed before. It does not need to be the latest root.
96
+ // Our SMT is append-only, so if the root has existed before, and the merklet proof
97
+ // is valid, then the leaves still exist in the tree.
98
+ if (!_commitmentsTree.rootExists(root)) {
99
+ revert UTXORootNotFound(root);
100
+ }
101
+
102
+ return true;
103
+ }
104
+
105
+ function getRoot() public view override returns (uint256) {
106
+ return _commitmentsTree.getRoot();
107
+ }
108
+
109
+ function processInputs(
110
+ uint256[] calldata nullifiers,
111
+ bool inputsLocked
112
+ ) public override onlyZeto {
113
+ if (inputsLocked) {
114
+ // locked inputs are regular UTXOs (rather than nullifiers)
115
+ // and are processed in the base storage
116
+ super.processInputs(nullifiers, inputsLocked);
117
+ return;
118
+ }
119
+
120
+ for (uint256 i = 0; i < nullifiers.length; ++i) {
121
+ if (nullifiers[i] != 0) {
122
+ _nullifiers[nullifiers[i]] = true;
123
+ }
124
+ }
125
+ }
126
+
127
+ function processOutputs(
128
+ uint256[] calldata outputs
129
+ ) public override onlyZeto {
130
+ for (uint256 i = 0; i < outputs.length; ++i) {
131
+ if (outputs[i] != 0) {
132
+ _commitmentsTree.addLeaf(outputs[i], outputs[i]);
133
+ }
134
+ }
135
+ }
136
+
137
+ function spent(uint256 utxo) public view override returns (UTXOStatus) {
138
+ // by design, the contract does not know this
139
+ return UTXOStatus.UNKNOWN;
140
+ }
141
+
142
+ // check the existence of a UTXO in either the unlocked or locked commitments storage
143
+ function exists(uint256 utxo) internal view returns (bool) {
144
+ if (everExistedAsUnlocked(utxo)) {
145
+ return true;
146
+ }
147
+ return existsAsLocked(utxo);
148
+ }
149
+
150
+ // check the existence of a UTXO in the commitments tree. we take a shortcut
151
+ // by checking the list of nodes by their node hash, because the commitments
152
+ // tree is append-only, no updates or deletions are allowed. As a result, all
153
+ // nodes in the list are valid leaf nodes, aka there are no orphaned nodes.
154
+ function everExistedAsUnlocked(uint256 utxo) internal view returns (bool) {
155
+ uint256 nodeHash = Util.getLeafNodeHash(utxo, utxo);
156
+ SmtLib.Node memory node = _commitmentsTree.getNode(nodeHash);
157
+ return node.nodeType != SmtLib.NodeType.EMPTY;
158
+ }
159
+
160
+ function existsAsLocked(uint256 utxo) internal view returns (bool) {
161
+ if (super.locked(utxo)) {
162
+ return true;
163
+ }
164
+ return super.spent(utxo) != UTXOStatus.UNKNOWN;
165
+ }
166
+ }