@openzeppelin/compact-contracts 0.3.0-rc.1 → 0.4.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +14 -4
  2. package/access/AccessControl.compact +14 -14
  3. package/access/Ownable.compact +16 -10
  4. package/access/ShieldedAccessControl.compact +26 -2
  5. package/access/ZOwnablePK.compact +9 -9
  6. package/crypto/EcdhMask.compact +5 -5
  7. package/crypto/Ecdsa.compact +111 -0
  8. package/crypto/Eip712.compact +168 -0
  9. package/crypto/ElGamal.compact +33 -107
  10. package/multisig/EcdsaSignerManager.compact +246 -0
  11. package/multisig/ForwarderPrivate.compact +12 -6
  12. package/multisig/ForwarderShielded.compact +3 -3
  13. package/multisig/ForwarderUnshielded.compact +3 -3
  14. package/multisig/ProposalManager.compact +195 -23
  15. package/multisig/ShieldedTreasury.compact +29 -2
  16. package/multisig/ShieldedTreasuryStateless.compact +2 -2
  17. package/multisig/Signer.compact +28 -80
  18. package/multisig/UnshieldedTreasury.compact +10 -2
  19. package/multisig/{presets/forwarder/ForwarderPrivate.compact → examples/ForwarderPrivateExample.compact} +4 -4
  20. package/multisig/{presets/forwarder/ForwarderShielded.compact → examples/ForwarderShieldedExample.compact} +4 -4
  21. package/multisig/{presets/forwarder/ForwarderUnshielded.compact → examples/ForwarderUnshieldedExample.compact} +4 -4
  22. package/multisig/examples/NativeShieldedTokenIssuerExample.compact +143 -0
  23. package/multisig/examples/ShieldedMultiSigV2Example.compact +77 -0
  24. package/multisig/presets/NativeShieldedTokenIssuer.compact +542 -0
  25. package/multisig/presets/ShieldedMultiSigV2.compact +267 -259
  26. package/package.json +31 -25
  27. package/security/Allowlist.compact +6 -6
  28. package/security/Blocklist.compact +6 -6
  29. package/security/Initializable.compact +9 -5
  30. package/security/Pausable.compact +11 -7
  31. package/token/ConfidentialFungibleToken.compact +143 -270
  32. package/token/FungibleToken.compact +36 -22
  33. package/token/MultiToken.compact +16 -16
  34. package/token/NativeShieldedToken.compact +17 -66
  35. package/token/NativeShieldedTokenCore.compact +26 -48
  36. package/token/NativeShieldedTokenFamily.compact +17 -67
  37. package/token/NonFungibleToken.compact +40 -79
  38. package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +12 -7
  39. package/token/extensions/NativeShieldedTokenDerivedNonce.compact +3 -3
  40. package/token/extensions/NativeShieldedTokenFamilyPublicSupply.compact +8 -8
  41. package/token/extensions/NativeShieldedTokenPublicSupply.compact +8 -8
  42. package/token/extensions/NativeShieldedTokenPublicSupplyCore.compact +6 -6
  43. package/utils/EvmAbi.compact +134 -0
  44. package/utils/Utils.compact +2 -12
  45. package/multisig/presets/ShieldedMultiSig.compact +0 -227
  46. package/multisig/presets/ShieldedMultiSigV3.compact +0 -368
@@ -0,0 +1,134 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.2 (utils/EvmAbi.compact)
3
+
4
+ pragma language_version >= 0.26.0;
5
+
6
+ /**
7
+ * @module EvmAbi
8
+ * @description Ethereum ABI word encoding, for messages that must hash
9
+ * identically in Compact and on the EVM.
10
+ *
11
+ * A contract verifying signatures produced by EVM signing infrastructure has
12
+ * to hash the exact bytes the signer hashed. `keccak256<T>(value)` hashes
13
+ * Compact's binary representation of `T`, which coincides with
14
+ * `abi.encode` for some types and diverges for others:
15
+ *
16
+ * - `Bytes<n>` is emitted as exactly `n` bytes. So `Bytes<32>` already *is* an
17
+ * EVM ABI word, and `keccak256<Vector<n, Bytes<32>>>(words)` is byte-identical
18
+ * to Solidity's `keccak256(abi.encode(word0, ..., wordN))`. Narrower types
19
+ * are emitted at their own width rather than padded to 32, which is what
20
+ * makes `abi.encodePacked`-style layouts expressible too.
21
+ * - Integers cast with `as Bytes<n>` are emitted **little-endian**, and a
22
+ * `Uint<64>` occupies 8 bytes rather than a full word. `abi.encode(uint256)`
23
+ * is a big-endian 32-byte word, so an integer cast directly into a message
24
+ * will not match what an EVM signer produced.
25
+ * - `Boolean` is emitted as a single byte, where `abi.encode(bool)` is a full
26
+ * word holding 0 or 1 in its least significant byte.
27
+ *
28
+ * This module supplies the conversions that diverge, so a message can be
29
+ * assembled as a `Vector<n, Bytes<32>>` of EVM ABI words and hashed with
30
+ * `keccak256` to reproduce an EVM digest exactly.
31
+ *
32
+ * @notice These are encoding primitives only. The choice of signing envelope
33
+ * (a raw digest, EIP-191, or EIP-712 typed data) belongs to the contract that
34
+ * owns the message.
35
+ *
36
+ * @notice The module itself needs no `keccak256`, so it compiles on either
37
+ * ZKIR backend and `compile:utils` can keep emitting v2. Every `@constraints`
38
+ * below is a v3 figure, and tagged as such.
39
+ */
40
+ module EvmAbi {
41
+ import CompactStandardLibrary;
42
+
43
+ /**
44
+ * @description Encodes a `Uint<8>` as an `abi.encode(uint8)` word: the
45
+ * value in the least significant byte, zero elsewhere.
46
+ *
47
+ * @dev No reversal or decomposition is needed -- a `Uint<8>` is already a
48
+ * single byte so this is the cheapest of the integer encoders. It is
49
+ * also how a Compact `enum` should reach a digest, matching Solidity,
50
+ * where an enum ABI-encodes as its `uint8` ordinal: `uint8Word(kind as Uint<8>)`.
51
+ *
52
+ * @constraints k=6, rows=55 (zkir-v3)
53
+ *
54
+ * @param {Uint<8>} value - The value to encode.
55
+ * @returns {Bytes<32>} The value as an EVM ABI word.
56
+ */
57
+ export pure circuit uint8Word(value: Uint<8>): Bytes<32> {
58
+ const word: Vector<32, Uint<8>> = [
59
+ 0, 0, 0, 0, 0, 0, 0, 0,
60
+ 0, 0, 0, 0, 0, 0, 0, 0,
61
+ 0, 0, 0, 0, 0, 0, 0, 0,
62
+ 0, 0, 0, 0, 0, 0, 0, value
63
+ ];
64
+
65
+ return word as Bytes<32>;
66
+ }
67
+
68
+ /**
69
+ * @description Encodes a `Uint<64>` as an `abi.encode(uint256)` word: the
70
+ * value big-endian in the low 8 bytes, zero-padded on the left.
71
+ *
72
+ * @dev The `Uint<64> as Bytes<8>` cast yields the value little-endian, so
73
+ * the bytes are reversed into place. Only 8 bytes are decomposed rather
74
+ * than 32; the wider cast costs roughly four times as many constraints for
75
+ * an identical result.
76
+ *
77
+ * @constraints k=11, rows=1348 (zkir-v3)
78
+ *
79
+ * @param {Uint<64>} value - The value to encode.
80
+ * @returns {Bytes<32>} The value as a big-endian EVM ABI word.
81
+ */
82
+ export pure circuit uint64Word(value: Uint<64>): Bytes<32> {
83
+ const le = (value as Bytes<8>) as Vector<8, Uint<8>>;
84
+
85
+ const word: Vector<32, Uint<8>> = [
86
+ 0, 0, 0, 0, 0, 0, 0, 0,
87
+ 0, 0, 0, 0, 0, 0, 0, 0,
88
+ 0, 0, 0, 0, 0, 0, 0, 0,
89
+ le[7], le[6], le[5], le[4], le[3], le[2], le[1], le[0]
90
+ ];
91
+
92
+ return word as Bytes<32>;
93
+ }
94
+
95
+ /**
96
+ * @description Encodes a `Uint<128>` as an `abi.encode(uint256)` word: the
97
+ * value big-endian in the low 16 bytes, zero-padded on the left.
98
+ *
99
+ * @dev Decomposes 16 bytes rather than 32, for the reason given on
100
+ * `uint64Word`. Costs roughly twice `uint64Word`, so prefer the narrower
101
+ * encoder when the value genuinely fits in 64 bits. The width here is
102
+ * the value's, not the word's.
103
+ *
104
+ * @constraints k=12, rows=2503 (zkir-v3)
105
+ *
106
+ * @param {Uint<128>} value - The value to encode.
107
+ * @returns {Bytes<32>} The value as a big-endian EVM ABI word.
108
+ */
109
+ export pure circuit uint128Word(value: Uint<128>): Bytes<32> {
110
+ const le = (value as Bytes<16>) as Vector<16, Uint<8>>;
111
+
112
+ const word: Vector<32, Uint<8>> = [
113
+ 0, 0, 0, 0, 0, 0, 0, 0,
114
+ 0, 0, 0, 0, 0, 0, 0, 0,
115
+ le[15], le[14], le[13], le[12], le[11], le[10], le[9], le[8],
116
+ le[7], le[6], le[5], le[4], le[3], le[2], le[1], le[0]
117
+ ];
118
+
119
+ return word as Bytes<32>;
120
+ }
121
+
122
+ /**
123
+ * @description Encodes a `Boolean` as an `abi.encode(bool)` word: zero
124
+ * everywhere except the least significant byte, which is 1 for true.
125
+ *
126
+ * @constraints k=6, rows=54 (zkir-v3)
127
+ *
128
+ * @param {Boolean} value - The value to encode.
129
+ * @returns {Bytes<32>} The value as an EVM ABI word.
130
+ */
131
+ export pure circuit boolWord(value: Boolean): Bytes<32> {
132
+ return uint8Word(value ? 1 as Uint<8> : 0 as Uint<8>);
133
+ }
134
+ }
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-rc.1 (utils/Utils.compact)
2
+ // OpenZeppelin Compact Contracts v0.4.0-alpha.2 (utils/Utils.compact)
3
3
 
4
- pragma language_version >= 0.23.0;
4
+ pragma language_version >= 0.26.0;
5
5
 
6
6
  /**
7
7
  * @module Utils.
@@ -150,16 +150,6 @@ module Utils {
150
150
  * ## ID Derivation
151
151
  * `accountId = persistentHash(secretKey)`
152
152
  *
153
- * @dev The absence of a domain-separation tag is DELIBERATE. This identifier
154
- * is global by design: the same key material yields the same identity in
155
- * every module deriving one, so a user who wishes to carry one identity
156
- * across modules can. Modules wanting a per-deployment, unlinkable identity
157
- * should not use this circuit.
158
- *
159
- * @dev NOT FOR PRIVATE DERIVATION. This returns an identifier, not a secret,
160
- * and the construction is untagged. A circuit deriving a PRIVATE value from a
161
- * secret MUST use its own domain-separation tag.
162
- *
163
153
  * @param {Bytes<32>} secretKey - A 32-byte cryptographically secure random value.
164
154
  *
165
155
  * @returns {Bytes<32>} accountId - The computed account identifier.
@@ -1,227 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Compact Contracts v0.3.0-rc.1 (multisig/presets/ShieldedMultiSig.compact)
3
-
4
- pragma language_version >= 0.23.0;
5
-
6
- /**
7
- * @module ShieldedMultiSig
8
- * @description A shielded multisig preset composing `Signer`,
9
- * `ProposalManager`, and `ShieldedTreasury`. Signers approve proposals that
10
- * transfer shielded tokens out of the treasury once the configured threshold
11
- * is met.
12
- *
13
- * @notice Signer identity uses `Either<ZswapCoinPublicKey, ContractAddress>`
14
- * in state for forward compatibility. In the current protocol, only
15
- * `left(ZswapCoinPublicKey)` callers can authenticate — `getCaller()` resolves
16
- * via `ownPublicKey()` and has no way to produce a right-variant today.
17
- * Registering contract-address signers is permitted but those signers cannot
18
- * exercise governance (create/approve/revoke) until contract-to-contract calls
19
- * are supported. Choose your signer set accordingly.
20
- *
21
- * @notice The state shape is deliberately kept broad so that, once
22
- * contract-to-contract calls are supported, `getCaller()` can be swapped via
23
- * a CMA (Contract Maintenance Authorities) circuit upgrade without a state
24
- * migration. Existing deployments would then gain working contract-signer
25
- * authentication.
26
- */
27
-
28
- import CompactStandardLibrary;
29
-
30
- import "../ProposalManager" prefix Proposal_;
31
- import "../ShieldedTreasury" prefix Treasury_;
32
- import "../Signer"<Either<ZswapCoinPublicKey, ContractAddress>> prefix Signer_;
33
-
34
- // ─── State ───────────────────────────────────────────────────────────────
35
-
36
- export ledger _proposalApprovals: Map<Uint<64>, Map<Either<ZswapCoinPublicKey, ContractAddress>, Boolean>>;
37
- export ledger _approvalCount: Map<Uint<64>, Uint<8>>;
38
-
39
- // ─── Constructor ─────────────────────────────────────────────────────────
40
-
41
- constructor(
42
- signers: Vector<3, Either<ZswapCoinPublicKey, ContractAddress>>,
43
- thresh: Uint<8>
44
- ) {
45
- Signer_initialize<3>(signers, thresh);
46
- }
47
-
48
- // ─── Deposit ─────────────────────────────────────────────────────────────
49
-
50
- export circuit deposit(coin: ShieldedCoinInfo): [] {
51
- Treasury__deposit(coin);
52
- }
53
-
54
- // ─── Proposals ───────────────────────────────────────────────────────────
55
-
56
- export circuit createShieldedProposal(
57
- to: Proposal_Recipient,
58
- color: Bytes<32>,
59
- amount: Uint<128>
60
- ): Uint<64> {
61
- const callerPK = getCaller();
62
- Signer_assertSigner(callerPK);
63
-
64
- assert(
65
- to.kind == Proposal_RecipientKind.ShieldedUser
66
- || to.kind == Proposal_RecipientKind.Contract,
67
- "ShieldedMultiSig: recipient must be a shielded user or contract"
68
- );
69
-
70
- return Proposal__createProposal(to, color, amount);
71
- }
72
-
73
- export circuit approveProposal(id: Uint<64>): [] {
74
- // Check if active
75
- Proposal_assertProposalActive(id);
76
-
77
- // Check signer
78
- const callerPK = getCaller();
79
- Signer_assertSigner(callerPK);
80
-
81
- // Check if already approved
82
- assert(!isProposalApprovedBySigner(id, callerPK), "Multisig: already approved");
83
-
84
- // Approve
85
- _approveProposal(id, callerPK);
86
- }
87
-
88
- export circuit revokeApproval(id: Uint<64>): [] {
89
- // Check if active
90
- Proposal_assertProposalActive(id);
91
-
92
- // Check signer
93
- const callerPK = getCaller();
94
- Signer_assertSigner(callerPK);
95
-
96
- // Check has approved
97
- assert(isProposalApprovedBySigner(id, callerPK), "Multisig: not approved");
98
-
99
- // Revoke
100
- _revokeApproval(id, callerPK);
101
- }
102
-
103
- export circuit executeShieldedProposal(
104
- id: Uint<64>,
105
- ): ShieldedSendResult {
106
- // Check if active
107
- Proposal_assertProposalActive(id);
108
-
109
- // Check threshold
110
- const approvalCount = getApprovalCount(id);
111
- Signer_assertThresholdMet(approvalCount);
112
-
113
- // Transfer
114
- const { to, color, amount } = Proposal_getProposal(id);
115
- const result = Treasury__send(
116
- Proposal_toShieldedRecipient(to),
117
- color,
118
- amount,
119
- );
120
-
121
- // Finish lifecycle
122
- Proposal__markExecuted(id);
123
- return result;
124
- }
125
-
126
- // ─── Internal ───────────────────────────────────────────────────────────
127
-
128
- circuit _approveProposal(id: Uint<64>, signer: Either<ZswapCoinPublicKey, ContractAddress>): [] {
129
- if (!_proposalApprovals.member(disclose(id))) {
130
- _proposalApprovals.insert(disclose(id), default<Map<Either<ZswapCoinPublicKey, ContractAddress>, Boolean>>);
131
- }
132
-
133
- _proposalApprovals.lookup(disclose(id)).insert(disclose(signer), disclose(true));
134
-
135
- const newCount = getApprovalCount(id) + 1 as Uint<8>;
136
- _approvalCount.insert(disclose(id), disclose(newCount));
137
- }
138
-
139
- circuit _revokeApproval(id: Uint<64>, signer: Either<ZswapCoinPublicKey, ContractAddress>): [] {
140
- _proposalApprovals.lookup(disclose(id)).remove(disclose(signer));
141
-
142
- const newCount = getApprovalCount(id) - 1 as Uint<8>;
143
- _approvalCount.insert(disclose(id), disclose(newCount));
144
- }
145
-
146
- /**
147
- * @description Returns the caller identity used for signer authentication.
148
- *
149
- * @warning Currently resolves callers via `ownPublicKey()` only, so any signer
150
- * registered as a `right(ContractAddress)` variant cannot authenticate through
151
- * this circuit today. Ledger fields keep the `Either<ZswapCoinPublicKey, ContractAddress>`
152
- * shape so that, once contract-to-contract calls are supported, `getCaller()`
153
- * can be replaced via a CMA (Contract Maintenance Authorities) circuit upgrade
154
- * to detect the contract-call context and return the appropriate variant —
155
- * without a state migration.
156
- *
157
- * @returns {Either<ZswapCoinPublicKey, ContractAddress>} The caller wrapped as a left-variant.
158
- */
159
- circuit getCaller(): Either<ZswapCoinPublicKey, ContractAddress> {
160
- return left<ZswapCoinPublicKey, ContractAddress>(ownPublicKey());
161
- }
162
-
163
- // ─── View ───────────────────────────────────────────────────────────────
164
-
165
- export circuit isProposalApprovedBySigner(
166
- id: Uint<64>,
167
- signer: Either<ZswapCoinPublicKey, ContractAddress>
168
- ): Boolean {
169
- if (!_proposalApprovals.member(disclose(id)) || !_proposalApprovals.lookup(disclose(id)).member(disclose(signer))) {
170
- return false;
171
- }
172
-
173
- return _proposalApprovals.lookup(disclose(id)).lookup(disclose(signer));
174
- }
175
-
176
- export circuit getApprovalCount(id: Uint<64>): Uint<8> {
177
- if (!_approvalCount.member(disclose(id))) {
178
- return 0;
179
- }
180
-
181
- return _approvalCount.lookup(disclose(id));
182
- }
183
-
184
- // IProposalManager
185
-
186
- export circuit getProposal(id: Uint<64>): Proposal_Proposal {
187
- return Proposal_getProposal(id);
188
- }
189
-
190
- // NOTE: the per-field proposal getters (getProposalRecipient / getProposalAmount
191
- // / getProposalColor) and getReceivedMinusSent were dropped to bring the deploy
192
- // transaction under the block-weight limit (the full 19-circuit deploy is
193
- // rejected with "Transaction would exhaust the block limits"). They are all
194
- // redundant: read the proposal fields via `getProposal(id).to / .amount / .color`,
195
- // and the net balance via `getReceivedTotal(color) - getSentTotal(color)`.
196
-
197
- export circuit getProposalStatus(id: Uint<64>): Proposal_ProposalStatus {
198
- return Proposal_getProposalStatus(id);
199
- }
200
-
201
- // IShieldedTreasury
202
-
203
- export circuit getTokenBalance(color: Bytes<32>): Uint<128> {
204
- return Treasury_getTokenBalance(color);
205
- }
206
-
207
- export circuit getReceivedTotal(color: Bytes<32>): Uint<128> {
208
- return Treasury_getReceivedTotal(color);
209
- }
210
-
211
- export circuit getSentTotal(color: Bytes<32>): Uint<128> {
212
- return Treasury_getSentTotal(color);
213
- }
214
-
215
- // ISigner
216
-
217
- export circuit getSignerCount(): Uint<8> {
218
- return Signer_getSignerCount();
219
- }
220
-
221
- export circuit getThreshold(): Uint<8> {
222
- return Signer_getThreshold();
223
- }
224
-
225
- export circuit isSigner(account: Either<ZswapCoinPublicKey, ContractAddress>): Boolean {
226
- return Signer_isSigner(account);
227
- }