@openzeppelin/compact-contracts 0.4.0-alpha.1 → 0.4.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -5
- package/access/AccessControl.compact +13 -13
- package/access/Ownable.compact +15 -9
- package/access/ShieldedAccessControl.compact +25 -1
- package/access/ZOwnablePK.compact +8 -8
- package/crypto/EcdhMask.compact +1 -1
- package/crypto/Ecdsa.compact +4 -4
- package/crypto/Eip712.compact +168 -0
- package/crypto/ElGamal.compact +1 -1
- package/multisig/EcdsaSignerManager.compact +32 -15
- package/multisig/ForwarderPrivate.compact +11 -5
- package/multisig/ForwarderShielded.compact +2 -2
- package/multisig/ForwarderUnshielded.compact +2 -2
- package/multisig/ProposalManager.compact +194 -22
- package/multisig/ShieldedTreasury.compact +28 -1
- package/multisig/ShieldedTreasuryStateless.compact +1 -1
- package/multisig/Signer.compact +27 -79
- package/multisig/UnshieldedTreasury.compact +9 -1
- package/multisig/{presets/forwarder/ForwarderPrivate.compact → examples/ForwarderPrivateExample.compact} +3 -3
- package/multisig/{presets/forwarder/ForwarderShielded.compact → examples/ForwarderShieldedExample.compact} +3 -3
- package/multisig/{presets/forwarder/ForwarderUnshielded.compact → examples/ForwarderUnshieldedExample.compact} +3 -3
- package/multisig/examples/NativeShieldedTokenIssuerExample.compact +143 -0
- package/multisig/examples/ShieldedMultiSigV2Example.compact +77 -0
- package/multisig/presets/NativeShieldedTokenIssuer.compact +542 -0
- package/multisig/presets/ShieldedMultiSigV2.compact +251 -188
- package/package.json +15 -11
- package/security/Allowlist.compact +5 -5
- package/security/Blocklist.compact +5 -5
- package/security/Initializable.compact +8 -4
- package/security/Pausable.compact +10 -6
- package/token/ConfidentialFungibleToken.compact +80 -21
- package/token/FungibleToken.compact +35 -21
- package/token/MultiToken.compact +15 -15
- package/token/NativeShieldedToken.compact +9 -9
- package/token/NativeShieldedTokenCore.compact +19 -9
- package/token/NativeShieldedTokenFamily.compact +9 -9
- package/token/NonFungibleToken.compact +32 -27
- package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +11 -6
- package/token/extensions/NativeShieldedTokenDerivedNonce.compact +2 -2
- package/token/extensions/NativeShieldedTokenFamilyPublicSupply.compact +5 -5
- package/token/extensions/NativeShieldedTokenPublicSupply.compact +5 -5
- package/token/extensions/NativeShieldedTokenPublicSupplyCore.compact +5 -5
- package/utils/EvmAbi.compact +134 -0
- package/utils/Utils.compact +1 -1
- package/multisig/presets/ShieldedMultiSig.compact +0 -227
- package/multisig/presets/ShieldedMultiSigV3.compact +0 -311
|
@@ -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
|
+
}
|
package/utils/Utils.compact
CHANGED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// OpenZeppelin Compact Contracts v0.4.0-alpha.1 (multisig/presets/ShieldedMultiSig.compact)
|
|
3
|
-
|
|
4
|
-
pragma language_version >= 0.26.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
|
-
}
|
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// OpenZeppelin Compact Contracts v0.4.0-alpha.1 (multisig/presets/ShieldedMultiSigV3.compact)
|
|
3
|
-
|
|
4
|
-
pragma language_version >= 0.26.0;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @title ShieldedMultiSigV3
|
|
8
|
-
* @description Privacy-preserving multisig token contract for tokenised
|
|
9
|
-
* deposits. Both minting and burning require threshold ECDSA
|
|
10
|
-
* authorization. No single party can unilaterally create or destroy tokens.
|
|
11
|
-
*
|
|
12
|
-
* Designed with the following features:
|
|
13
|
-
* - Supply only enters through authorized mint (no open deposit)
|
|
14
|
-
* - Supply only exits through authorized burn (no arbitrary transfers)
|
|
15
|
-
* - Non-transferability enforced by absence of transfer/execute circuits
|
|
16
|
-
* - Change coins from partial burns handled by the transaction layer
|
|
17
|
-
* - Operator discovers coins via ZswapOutput events from the indexer
|
|
18
|
-
*
|
|
19
|
-
* Uses Midnight's native shielded token primitives:
|
|
20
|
-
*
|
|
21
|
-
* - `mint` creates a new UTXO via `mintShieldedToken`, addressed to the
|
|
22
|
-
* contract. No external coin input; supply is produced on-chain.
|
|
23
|
-
* - `burn` consumes a UTXO via `sendShielded` to `shieldedBurnAddress()`. Only
|
|
24
|
-
* coins of this contract's token type can be burned. Change is handled
|
|
25
|
-
* automatically by the transaction layer.
|
|
26
|
-
*
|
|
27
|
-
* Signer identities are stored as commitments: hashes of ECDSA public keys
|
|
28
|
-
* combined with an instance salt and domain separator. A counter
|
|
29
|
-
* provides replay protection, and also feeds `evolveNonce` to produce
|
|
30
|
-
* unique coin nonces on each mint.
|
|
31
|
-
*
|
|
32
|
-
* Operation domain prefixes ("multisig:mint:" / "multisig:burn:") in the message
|
|
33
|
-
* hash prevent signatures for one operation type from being replayed as
|
|
34
|
-
* the other.
|
|
35
|
-
*
|
|
36
|
-
* @notice Threshold authorization is enforced with secp256k1 ECDSA
|
|
37
|
-
* (`secp256k1EcdsaVerifyLowS`) over `persistentHash` digests, via
|
|
38
|
-
* `EcdsaSignerManager`, so a high-s signature is rejected. The approval
|
|
39
|
-
* vectors are fixed at `Vector<2, ...>`, so this contract is 2-of-3 only.
|
|
40
|
-
*
|
|
41
|
-
* TODO: switch every digest to `keccak256` to match the custodian's HSM
|
|
42
|
-
* signing format (#827).
|
|
43
|
-
*/
|
|
44
|
-
|
|
45
|
-
import CompactStandardLibrary;
|
|
46
|
-
|
|
47
|
-
import "../EcdsaSignerManager" prefix EcdsaSignerManager_;
|
|
48
|
-
import "../../utils/Utils" prefix Utils_;
|
|
49
|
-
// For testing
|
|
50
|
-
export { ZswapCoinPublicKey };
|
|
51
|
-
|
|
52
|
-
// ─── State ──────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
export ledger _counter: Counter;
|
|
55
|
-
export ledger _coinNonce: Bytes<32>;
|
|
56
|
-
export sealed ledger _tokenDomain: Bytes<32>;
|
|
57
|
-
|
|
58
|
-
// ─── Constructor ────────────────────────────────────────────────
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @description Deploys the contract with 3 signer commitments and
|
|
62
|
-
* a threshold of 2.
|
|
63
|
-
*
|
|
64
|
-
* Each commitment is `persistentHash(pkX, pkY, instanceSalt,
|
|
65
|
-
* "multisig:signer:")`. Use the exported `_calculateSignerId` circuit to
|
|
66
|
-
* derive them consistently with in-circuit verification.
|
|
67
|
-
*
|
|
68
|
-
* `tokenDomain` is used with `kernel.self()` to derive the token color
|
|
69
|
-
* via `tokenType(_tokenDomain, kernel.self())`. Only coins of this color
|
|
70
|
-
* can be burned through this contract.
|
|
71
|
-
*
|
|
72
|
-
* `initCoinNonce` seeds the `evolveNonce` chain used to produce unique
|
|
73
|
-
* mint nonces. It must be cryptographically random.
|
|
74
|
-
*
|
|
75
|
-
* Requirements:
|
|
76
|
-
*
|
|
77
|
-
* - `signerCommitments` must not contain duplicates.
|
|
78
|
-
* - `instanceSalt` and `initCoinNonce` should be cryptographically random.
|
|
79
|
-
*
|
|
80
|
-
* @param {Bytes<32>} instanceSalt - Random salt for signer commitment derivation.
|
|
81
|
-
* @param {Bytes<32>} initCoinNonce - Initial coin nonce seed.
|
|
82
|
-
* @param {Bytes<32>} tokenDomain - Domain string used to derive this contract's token color.
|
|
83
|
-
* @param {Vector<3, Bytes<32>>} signerCommitments - Hashed signer identities.
|
|
84
|
-
*/
|
|
85
|
-
constructor(
|
|
86
|
-
instanceSalt: Bytes<32>,
|
|
87
|
-
initCoinNonce: Bytes<32>,
|
|
88
|
-
tokenDomain: Bytes<32>,
|
|
89
|
-
signerCommitments: Vector<3, Bytes<32>>,
|
|
90
|
-
) {
|
|
91
|
-
_coinNonce = disclose(initCoinNonce);
|
|
92
|
-
_tokenDomain = disclose(tokenDomain);
|
|
93
|
-
EcdsaSignerManager_initialize<3>(instanceSalt, signerCommitments, 2);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ─── Mint ───────────────────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* @description Mints a new shielded coin addressed to the specified
|
|
100
|
-
* recipient, authorized by threshold signatures.
|
|
101
|
-
*
|
|
102
|
-
* Creates a new UTXO of this contract's token type via `mintShieldedToken`,
|
|
103
|
-
* addressed to the provided recipient. No external coin input is required;
|
|
104
|
-
* supply is produced on-chain.
|
|
105
|
-
*
|
|
106
|
-
* The message hash commits to:
|
|
107
|
-
* - operation domain ("multisig:mint:" for burn op replay protection)
|
|
108
|
-
* - contract address (cross-instance replay protection)
|
|
109
|
-
* - recipient (redirection protection)
|
|
110
|
-
* - counter value (replay protection)
|
|
111
|
-
* - amount
|
|
112
|
-
*
|
|
113
|
-
* Coin nonce uniqueness is guaranteed by `evolveNonce(_counter, _coinNonce)`
|
|
114
|
-
* after the counter has been incremented, binding each mint's nonce to a
|
|
115
|
-
* distinct counter value.
|
|
116
|
-
*
|
|
117
|
-
* @circuitInfo k=17, rows=84506
|
|
118
|
-
*
|
|
119
|
-
* Requirements:
|
|
120
|
-
*
|
|
121
|
-
* - Both public keys must hash to registered signer commitments.
|
|
122
|
-
* - Both signatures must be valid over the mint message hash.
|
|
123
|
-
* - Signers must not be duplicates.
|
|
124
|
-
* - Threshold must be met.
|
|
125
|
-
*
|
|
126
|
-
* @param {Uint<64>} amount - The token amount to mint.
|
|
127
|
-
* @param {Either<ZswapCoinPublicKey, ContractAddress>} recipient - The address to receive the minted tokens.
|
|
128
|
-
* @param {Vector<2, Secp256k1Point>} pubkeys - Public keys of approving signers.
|
|
129
|
-
* @param {Vector<2, Secp256k1EcdsaSignature>} signatures - ECDSA signatures over the mint hash.
|
|
130
|
-
*/
|
|
131
|
-
export circuit mint(
|
|
132
|
-
amount: Uint<64>,
|
|
133
|
-
recipient: Either<ZswapCoinPublicKey, ContractAddress>,
|
|
134
|
-
pubkeys: Vector<2, Secp256k1Point>,
|
|
135
|
-
signatures: Vector<2, Secp256k1EcdsaSignature>
|
|
136
|
-
): [] {
|
|
137
|
-
const opNonce = _counter;
|
|
138
|
-
_counter.increment(1);
|
|
139
|
-
|
|
140
|
-
// Canonicalize recipient to ensure garbage values don't sully the hash
|
|
141
|
-
const canonRecipient = Utils_canonicalize<ZswapCoinPublicKey, ContractAddress>(recipient);
|
|
142
|
-
const recipientHash = persistentHash<Either<ZswapCoinPublicKey, ContractAddress>>(canonRecipient);
|
|
143
|
-
|
|
144
|
-
const msgHash = persistentHash<Vector<5, Bytes<32>>>([
|
|
145
|
-
pad(32, "multisig:mint:"),
|
|
146
|
-
kernel.self().bytes,
|
|
147
|
-
recipientHash,
|
|
148
|
-
opNonce as Bytes<32>,
|
|
149
|
-
amount as Bytes<32>
|
|
150
|
-
]);
|
|
151
|
-
|
|
152
|
-
EcdsaSignerManager_assertApprovals(msgHash, pubkeys, signatures);
|
|
153
|
-
|
|
154
|
-
_coinNonce = evolveNonce(_counter, _coinNonce);
|
|
155
|
-
mintShieldedToken(_tokenDomain, disclose(amount), _coinNonce, disclose(canonRecipient));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ─── Burn ───────────────────────────────────────────────────────
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* @description Burns a shielded coin of this contract's token type,
|
|
162
|
-
* authorized by threshold signatures.
|
|
163
|
-
*
|
|
164
|
-
* Sends the specified amount of the supplied coin to `shieldedBurnAddress()` via
|
|
165
|
-
* `sendShielded`. The nullifier is submitted on-chain, permanently marking
|
|
166
|
-
* the UTXO as spent.
|
|
167
|
-
*
|
|
168
|
-
* Change handling is automatic: if `amount < coin.value`, the transaction
|
|
169
|
-
* layer creates a change output addressed back to the contract. The operator
|
|
170
|
-
* discovers this change coin via `nextZswapLocalState.outputs` in the
|
|
171
|
-
* transaction's private result, then discovers its mt_index from the
|
|
172
|
-
* indexer events using the standard flow. No contract-level change logic
|
|
173
|
-
* is required.
|
|
174
|
-
*
|
|
175
|
-
* Only coins of this contract's token type can be burned. The operator
|
|
176
|
-
* supplies the `QualifiedShieldedCoinInfo` from the off-chain UTXO pool.
|
|
177
|
-
*
|
|
178
|
-
* The "multisig:burn:" domain prefix ensures burn signatures cannot be replayed
|
|
179
|
-
* as mint operations for the same parameters.
|
|
180
|
-
*
|
|
181
|
-
* @circuitInfo k=17, rows=92170
|
|
182
|
-
*
|
|
183
|
-
* Requirements:
|
|
184
|
-
*
|
|
185
|
-
* - Both public keys must hash to registered signer commitments.
|
|
186
|
-
* - Both signatures must be valid over the burn message hash.
|
|
187
|
-
* - Signers must not be duplicates.
|
|
188
|
-
* - Threshold must be met.
|
|
189
|
-
* - coin.color must equal tokenType(_tokenDomain, kernel.self()).
|
|
190
|
-
* - coin.value must be >= amount.
|
|
191
|
-
*
|
|
192
|
-
* @param {QualifiedShieldedCoinInfo} coin - The coin to burn (from operator's UTXO pool).
|
|
193
|
-
* @param {Uint<64>} amount - The token amount to burn.
|
|
194
|
-
* @param {Vector<2, Secp256k1Point>} pubkeys - Public keys of approving signers.
|
|
195
|
-
* @param {Vector<2, Secp256k1EcdsaSignature>} signatures - ECDSA signatures over the burn hash.
|
|
196
|
-
*/
|
|
197
|
-
export circuit burn(
|
|
198
|
-
coin: QualifiedShieldedCoinInfo,
|
|
199
|
-
amount: Uint<64>,
|
|
200
|
-
pubkeys: Vector<2, Secp256k1Point>,
|
|
201
|
-
signatures: Vector<2, Secp256k1EcdsaSignature>
|
|
202
|
-
): [] {
|
|
203
|
-
const opNonce = _counter;
|
|
204
|
-
_counter.increment(1);
|
|
205
|
-
|
|
206
|
-
const msgHash = persistentHash<Vector<4, Bytes<32>>>([
|
|
207
|
-
pad(32, "multisig:burn:"),
|
|
208
|
-
kernel.self().bytes,
|
|
209
|
-
opNonce as Bytes<32>,
|
|
210
|
-
amount as Bytes<32>
|
|
211
|
-
]);
|
|
212
|
-
|
|
213
|
-
EcdsaSignerManager_assertApprovals(msgHash, pubkeys, signatures);
|
|
214
|
-
|
|
215
|
-
assert(coin.color == tokenType(_tokenDomain, kernel.self()), "Multisig: coin not from this contract");
|
|
216
|
-
assert(coin.value >= amount, "Multisig: insufficient coin value");
|
|
217
|
-
|
|
218
|
-
sendShielded(disclose(coin), shieldedBurnAddress(), disclose(amount));
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ─── Commitments ────────────────────────────────────────────────
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* @description Computes a signer commitment from a secp256k1 public key,
|
|
225
|
-
* as `persistentHash(pkX, pkY, salt, "multisig:signer:")`.
|
|
226
|
-
*
|
|
227
|
-
* Pure, so the deployer can derive constructor commitments off-chain.
|
|
228
|
-
*
|
|
229
|
-
* Requirements:
|
|
230
|
-
*
|
|
231
|
-
* - `pk` must not be the secp256k1 identity point, which has no coordinates.
|
|
232
|
-
*
|
|
233
|
-
* @param {Secp256k1Point} pk - The secp256k1 public key.
|
|
234
|
-
* @param {Bytes<32>} salt - The instance salt.
|
|
235
|
-
* @returns {Bytes<32>} The signer commitment.
|
|
236
|
-
*/
|
|
237
|
-
export pure circuit _calculateSignerId(
|
|
238
|
-
pk: Secp256k1Point,
|
|
239
|
-
salt: Bytes<32>
|
|
240
|
-
): Bytes<32> {
|
|
241
|
-
return EcdsaSignerManager_calculateSignerId(pk, salt);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// ─── View ───────────────────────────────────────────────────────
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* @description Returns the counter the next mint or burn digest will bind to.
|
|
248
|
-
*
|
|
249
|
-
* @circuitInfo k=6, rows=34
|
|
250
|
-
*
|
|
251
|
-
* @returns {Uint<64>} The current counter.
|
|
252
|
-
*/
|
|
253
|
-
export circuit getNonce(): Uint<64> {
|
|
254
|
-
return _counter;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* @description Returns the domain separator this contract's token type derives from.
|
|
259
|
-
*
|
|
260
|
-
* @circuitInfo k=6, rows=57
|
|
261
|
-
*
|
|
262
|
-
* @returns {Bytes<32>} The token domain.
|
|
263
|
-
*/
|
|
264
|
-
export circuit getTokenDomain(): Bytes<32> {
|
|
265
|
-
return _tokenDomain;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* @description Returns this contract's token type.
|
|
270
|
-
*
|
|
271
|
-
* @circuitInfo k=13, rows=3836
|
|
272
|
-
*
|
|
273
|
-
* @returns {Bytes<32>} The token type.
|
|
274
|
-
*/
|
|
275
|
-
export circuit getTokenType(): Bytes<32> {
|
|
276
|
-
return tokenType(_tokenDomain, kernel.self());
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* @description Returns the number of registered signers.
|
|
281
|
-
*
|
|
282
|
-
* @circuitInfo k=6, rows=34
|
|
283
|
-
*
|
|
284
|
-
* @returns {Uint<8>} The signer count.
|
|
285
|
-
*/
|
|
286
|
-
export circuit getSignerCount(): Uint<8> {
|
|
287
|
-
return EcdsaSignerManager_getSignerCount();
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* @description Returns the approval threshold.
|
|
292
|
-
*
|
|
293
|
-
* @circuitInfo k=6, rows=34
|
|
294
|
-
*
|
|
295
|
-
* @returns {Uint<8>} The threshold.
|
|
296
|
-
*/
|
|
297
|
-
export circuit getThreshold(): Uint<8> {
|
|
298
|
-
return EcdsaSignerManager_getThreshold();
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* @description Returns whether the given commitment is a registered signer.
|
|
303
|
-
*
|
|
304
|
-
* @circuitInfo k=8, rows=129
|
|
305
|
-
*
|
|
306
|
-
* @param {Bytes<32>} commitment - The signer commitment to check.
|
|
307
|
-
* @returns {Boolean} True if registered.
|
|
308
|
-
*/
|
|
309
|
-
export circuit isSigner(commitment: Bytes<32>): Boolean {
|
|
310
|
-
return EcdsaSignerManager_isSigner(commitment);
|
|
311
|
-
}
|