@openzeppelin/compact-contracts 0.2.0 → 0.3.0-alpha.1
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 +10 -10
- package/access/AccessControl.compact +3 -35
- package/access/Ownable.compact +5 -51
- package/access/ShieldedAccessControl.compact +2 -2
- package/access/ZOwnablePK.compact +2 -2
- package/crypto/EcdhMask.compact +177 -0
- package/crypto/ElGamal.compact +473 -0
- package/multisig/ForwarderPrivate.compact +233 -0
- package/multisig/ForwarderShielded.compact +149 -0
- package/multisig/ForwarderUnshielded.compact +152 -0
- package/multisig/ProposalManager.compact +328 -0
- package/multisig/ShieldedTreasury.compact +204 -0
- package/multisig/ShieldedTreasuryStateless.compact +80 -0
- package/multisig/Signer.compact +312 -0
- package/multisig/SignerManager.compact +205 -0
- package/multisig/UnshieldedTreasury.compact +114 -0
- package/multisig/presets/ShieldedMultiSig.compact +227 -0
- package/multisig/presets/ShieldedMultiSigV2.compact +280 -0
- package/multisig/presets/ShieldedMultiSigV3.compact +368 -0
- package/multisig/presets/forwarder/ForwarderPrivate.compact +125 -0
- package/multisig/presets/forwarder/ForwarderShielded.compact +62 -0
- package/multisig/presets/forwarder/ForwarderUnshielded.compact +64 -0
- package/package.json +32 -16
- package/security/Allowlist.compact +101 -0
- package/security/Blocklist.compact +99 -0
- package/security/Initializable.compact +2 -2
- package/security/Pausable.compact +2 -2
- package/token/ConfidentialFungibleToken.compact +1180 -0
- package/token/FungibleToken.compact +24 -82
- package/token/MultiToken.compact +21 -80
- package/token/NativeShieldedToken.compact +285 -0
- package/token/NativeShieldedTokenCore.compact +343 -0
- package/token/NativeShieldedTokenFamily.compact +287 -0
- package/token/NonFungibleToken.compact +25 -83
- package/token/extensions/ConfidentialFungibleTokenPublicSupply.compact +90 -0
- package/token/extensions/NativeShieldedTokenDerivedNonce.compact +70 -0
- package/token/extensions/NativeShieldedTokenFamilySupply.compact +145 -0
- package/token/extensions/NativeShieldedTokenSupply.compact +142 -0
- package/token/extensions/NativeShieldedTokenSupplyCore.compact +163 -0
- package/utils/Utils.compact +86 -2
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.0.1-alpha.1 (multisig/presets/ShieldedMultiSigV3.compact)
|
|
3
|
+
|
|
4
|
+
pragma language_version >= 0.23.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 `burnAddress()`. 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 ECDSA verification is stubbed. Replace `stubVerifySignature` with
|
|
37
|
+
* `ecdsaVerify`, and `persistentHash` with `keccak256`, once the Compact
|
|
38
|
+
* ECDSA and Keccak primitives are available.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import CompactStandardLibrary;
|
|
42
|
+
|
|
43
|
+
import "../Signer"<Bytes<32>> prefix Signer_;
|
|
44
|
+
import "../../utils/Utils" prefix Utils_;
|
|
45
|
+
// For testing
|
|
46
|
+
export { ZswapCoinPublicKey };
|
|
47
|
+
|
|
48
|
+
// ─── Types ──────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @description Accumulator for fold-based signature verification.
|
|
52
|
+
* Threads the valid count, previous commitment (for duplicate
|
|
53
|
+
* detection), and message hash through each iteration.
|
|
54
|
+
*/
|
|
55
|
+
struct VerificationState {
|
|
56
|
+
validCount: Uint<8>,
|
|
57
|
+
prevCommitment: Bytes<32>,
|
|
58
|
+
msgHash: Bytes<32>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @description Input to persistentHash for computing signer commitments.
|
|
63
|
+
* Combines the ECDSA public key with an instance-specific salt and
|
|
64
|
+
* domain separator to produce a unique, unlinkable commitment.
|
|
65
|
+
*/
|
|
66
|
+
struct SignerCommitmentInput {
|
|
67
|
+
pk: Bytes<64>,
|
|
68
|
+
salt: Bytes<32>,
|
|
69
|
+
domain: Bytes<32>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── State ──────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export ledger _counter: Counter;
|
|
75
|
+
export ledger _coinNonce: Bytes<32>;
|
|
76
|
+
export ledger _instanceSalt: Bytes<32>;
|
|
77
|
+
export sealed ledger _tokenDomain: Bytes<32>;
|
|
78
|
+
|
|
79
|
+
// ─── Constructor ────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @description Deploys the contract with 3 signer commitments and
|
|
83
|
+
* a threshold of 2.
|
|
84
|
+
*
|
|
85
|
+
* Each commitment is computed off-chain as:
|
|
86
|
+
* `persistentHash(SignerCommitmentInput { pk, instanceSalt, domain })`
|
|
87
|
+
* where domain is `pad(32, "multisig:signer:")`.
|
|
88
|
+
*
|
|
89
|
+
* `tokenDomain` is used with `kernel.self()` to derive the token color
|
|
90
|
+
* via `tokenType(_tokenDomain, kernel.self())`. Only coins of this color
|
|
91
|
+
* can be burned through this contract.
|
|
92
|
+
*
|
|
93
|
+
* `initCoinNonce` seeds the `evolveNonce` chain used to produce unique
|
|
94
|
+
* mint nonces. It must be cryptographically random.
|
|
95
|
+
*
|
|
96
|
+
* Requirements:
|
|
97
|
+
*
|
|
98
|
+
* - `signerCommitments` must not contain duplicates.
|
|
99
|
+
* - `instanceSalt` and `initCoinNonce` should be cryptographically random.
|
|
100
|
+
*
|
|
101
|
+
* @param {Bytes<32>} instanceSalt - Random salt for signer commitment derivation.
|
|
102
|
+
* @param {Bytes<32>} initCoinNonce - Initial coin nonce seed.
|
|
103
|
+
* @param {Bytes<32>} tokenDomain - Domain string used to derive this contract's token color.
|
|
104
|
+
* @param {Vector<3, Bytes<32>>} signerCommitments - Hashed signer identities.
|
|
105
|
+
*/
|
|
106
|
+
constructor(
|
|
107
|
+
instanceSalt: Bytes<32>,
|
|
108
|
+
initCoinNonce: Bytes<32>,
|
|
109
|
+
tokenDomain: Bytes<32>,
|
|
110
|
+
signerCommitments: Vector<3, Bytes<32>>,
|
|
111
|
+
) {
|
|
112
|
+
_instanceSalt = disclose(instanceSalt);
|
|
113
|
+
_coinNonce = disclose(initCoinNonce);
|
|
114
|
+
_tokenDomain = disclose(tokenDomain);
|
|
115
|
+
Signer_initialize<3>(signerCommitments, 2);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Mint ───────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @description Mints a new shielded coin addressed to the specified
|
|
122
|
+
* recipient, authorized by threshold signatures.
|
|
123
|
+
*
|
|
124
|
+
* Creates a new UTXO of this contract's token type via `mintShieldedToken`,
|
|
125
|
+
* addressed to the provided recipient. No external coin input is required;
|
|
126
|
+
* supply is produced on-chain.
|
|
127
|
+
*
|
|
128
|
+
* The message hash commits to:
|
|
129
|
+
* - operation domain ("multisig:mint:" for burn op replay protection)
|
|
130
|
+
* - contract address (cross-instance replay protection)
|
|
131
|
+
* - recipient (redirection protection)
|
|
132
|
+
* - counter value (replay protection)
|
|
133
|
+
* - amount
|
|
134
|
+
*
|
|
135
|
+
* Coin nonce uniqueness is guaranteed by `evolveNonce(_counter, _coinNonce)`
|
|
136
|
+
* after the counter has been incremented, binding each mint's nonce to a
|
|
137
|
+
* distinct counter value.
|
|
138
|
+
*
|
|
139
|
+
* @notice Replace `persistentHash` with `keccak256` and `stubVerifySignature`
|
|
140
|
+
* with `ecdsaVerify` once the Compact ECDSA and Keccak primitives are
|
|
141
|
+
* available, to match the custodian's HSM signing format.
|
|
142
|
+
*
|
|
143
|
+
* Requirements:
|
|
144
|
+
*
|
|
145
|
+
* - Both public keys must hash to registered signer commitments.
|
|
146
|
+
* - Both signatures must be valid over the mint message hash.
|
|
147
|
+
* - Signers must not be duplicates.
|
|
148
|
+
* - Threshold must be met.
|
|
149
|
+
*
|
|
150
|
+
* @param {Uint<64>} amount - The token amount to mint.
|
|
151
|
+
* @param {Either<ZswapCoinPublicKey, ContractAddress>} recipient - The address to receive the minted tokens.
|
|
152
|
+
* @param {Vector<2, Bytes<64>>} pubkeys - ECDSA public keys of approving signers.
|
|
153
|
+
* @param {Vector<2, Bytes<64>>} signatures - ECDSA signatures over the mint hash.
|
|
154
|
+
*/
|
|
155
|
+
export circuit mint(
|
|
156
|
+
amount: Uint<64>,
|
|
157
|
+
recipient: Either<ZswapCoinPublicKey, ContractAddress>,
|
|
158
|
+
pubkeys: Vector<2, Bytes<64>>,
|
|
159
|
+
signatures: Vector<2, Bytes<64>>
|
|
160
|
+
): [] {
|
|
161
|
+
const opNonce = _counter;
|
|
162
|
+
_counter.increment(1);
|
|
163
|
+
|
|
164
|
+
// Canonicalize recipient to ensure garbage values don't sully the hash
|
|
165
|
+
const canonRecipient = Utils_canonicalize<ZswapCoinPublicKey, ContractAddress>(recipient);
|
|
166
|
+
const recipientHash = persistentHash<Either<ZswapCoinPublicKey, ContractAddress>>(canonRecipient);
|
|
167
|
+
|
|
168
|
+
const msgHash = persistentHash<Vector<5, Bytes<32>>>([
|
|
169
|
+
pad(32, "multisig:mint:"),
|
|
170
|
+
kernel.self().bytes,
|
|
171
|
+
recipientHash,
|
|
172
|
+
opNonce as Bytes<32>,
|
|
173
|
+
amount as Bytes<32>
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
const initialState = VerificationState {
|
|
177
|
+
validCount: 0 as Uint<8>,
|
|
178
|
+
prevCommitment: pad(32, ""),
|
|
179
|
+
msgHash: msgHash
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const finalState = fold(verifySignature, initialState, pubkeys, signatures);
|
|
183
|
+
// Thresh check not needed as it will always be 2
|
|
184
|
+
// Leaving it as defense-in-depth
|
|
185
|
+
Signer_assertThresholdMet(finalState.validCount);
|
|
186
|
+
|
|
187
|
+
_coinNonce = evolveNonce(_counter, _coinNonce);
|
|
188
|
+
mintShieldedToken(_tokenDomain, disclose(amount), _coinNonce, disclose(canonRecipient));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── Burn ───────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @description Burns a shielded coin of this contract's token type,
|
|
195
|
+
* authorized by threshold signatures.
|
|
196
|
+
*
|
|
197
|
+
* Sends the specified amount of the supplied coin to `burnAddress()` via
|
|
198
|
+
* `sendShielded`. The nullifier is submitted on-chain, permanently marking
|
|
199
|
+
* the UTXO as spent.
|
|
200
|
+
*
|
|
201
|
+
* Change handling is automatic: if `amount < coin.value`, the transaction
|
|
202
|
+
* layer creates a change output addressed back to the contract. The operator
|
|
203
|
+
* discovers this change coin via `nextZswapLocalState.outputs` in the
|
|
204
|
+
* transaction's private result, then discovers its mt_index from the
|
|
205
|
+
* indexer events using the standard flow. No contract-level change logic
|
|
206
|
+
* is required.
|
|
207
|
+
*
|
|
208
|
+
* Only coins of this contract's token type can be burned. The operator
|
|
209
|
+
* supplies the `QualifiedShieldedCoinInfo` from the off-chain UTXO pool.
|
|
210
|
+
*
|
|
211
|
+
* The "multisig:burn:" domain prefix ensures burn signatures cannot be replayed
|
|
212
|
+
* as mint operations for the same parameters.
|
|
213
|
+
*
|
|
214
|
+
* @notice Replace `persistentHash` with `keccak256` and `stubVerifySignature`
|
|
215
|
+
* with `ecdsaVerify` once the Compact ECDSA and Keccak primitives are
|
|
216
|
+
* available, to match the custodian's HSM signing format.
|
|
217
|
+
*
|
|
218
|
+
* Requirements:
|
|
219
|
+
*
|
|
220
|
+
* - Both public keys must hash to registered signer commitments.
|
|
221
|
+
* - Both signatures must be valid over the burn message hash.
|
|
222
|
+
* - Signers must not be duplicates.
|
|
223
|
+
* - Threshold must be met.
|
|
224
|
+
* - coin.color must equal tokenType(_tokenDomain, kernel.self()).
|
|
225
|
+
* - coin.value must be >= amount.
|
|
226
|
+
*
|
|
227
|
+
* @param {QualifiedShieldedCoinInfo} coin - The coin to burn (from operator's UTXO pool).
|
|
228
|
+
* @param {Uint<64>} amount - The token amount to burn.
|
|
229
|
+
* @param {Vector<2, Bytes<64>>} pubkeys - ECDSA public keys of approving signers.
|
|
230
|
+
* @param {Vector<2, Bytes<64>>} signatures - ECDSA signatures over the burn hash.
|
|
231
|
+
*/
|
|
232
|
+
export circuit burn(
|
|
233
|
+
coin: QualifiedShieldedCoinInfo,
|
|
234
|
+
amount: Uint<64>,
|
|
235
|
+
pubkeys: Vector<2, Bytes<64>>,
|
|
236
|
+
signatures: Vector<2, Bytes<64>>
|
|
237
|
+
): [] {
|
|
238
|
+
const opNonce = _counter;
|
|
239
|
+
_counter.increment(1);
|
|
240
|
+
|
|
241
|
+
const msgHash = persistentHash<Vector<4, Bytes<32>>>([
|
|
242
|
+
pad(32, "multisig:burn:"),
|
|
243
|
+
kernel.self().bytes,
|
|
244
|
+
opNonce as Bytes<32>,
|
|
245
|
+
amount as Bytes<32>
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
const initialState = VerificationState {
|
|
249
|
+
validCount: 0 as Uint<8>,
|
|
250
|
+
prevCommitment: pad(32, ""),
|
|
251
|
+
msgHash: msgHash
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const finalState = fold(verifySignature, initialState, pubkeys, signatures);
|
|
255
|
+
// Thresh check not needed as it will always be 2
|
|
256
|
+
// Leaving it as defense-in-depth
|
|
257
|
+
Signer_assertThresholdMet(finalState.validCount);
|
|
258
|
+
|
|
259
|
+
assert(coin.color == tokenType(_tokenDomain, kernel.self()), "Multisig: coin not from this contract");
|
|
260
|
+
assert(coin.value >= amount, "Multisig: insufficient coin value");
|
|
261
|
+
|
|
262
|
+
sendShielded(disclose(coin), shieldedBurnAddress(), disclose(amount));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ─── Signature Verification ─────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* @description Fold callback. Verifies one signer's approval.
|
|
269
|
+
*
|
|
270
|
+
* Computes the signer's commitment from their public key and the instance
|
|
271
|
+
* salt, checks for duplicates against the previous commitment, verifies
|
|
272
|
+
* registry membership, and validates the ECDSA signature.
|
|
273
|
+
*
|
|
274
|
+
* @notice Circuit signatures are fixed at Vector<2, ...>, so this contract
|
|
275
|
+
* supports only a fixed 2-of-3 configuration. Supporting more signatures
|
|
276
|
+
* (e.g. 3-of-N) requires a separate variant with a larger vector and a
|
|
277
|
+
* different duplicate-detection mechanism (sorted commitments or bitmap).
|
|
278
|
+
*
|
|
279
|
+
* @param {VerificationState} state - Accumulator threaded through fold.
|
|
280
|
+
* @param {Bytes<64>} pubkey - The signer's ECDSA public key.
|
|
281
|
+
* @param {Bytes<64>} signature - The signer's signature over msgHash.
|
|
282
|
+
* @returns {VerificationState} Updated accumulator.
|
|
283
|
+
*/
|
|
284
|
+
circuit verifySignature(
|
|
285
|
+
state: VerificationState,
|
|
286
|
+
pubkey: Bytes<64>,
|
|
287
|
+
signature: Bytes<64>
|
|
288
|
+
): VerificationState {
|
|
289
|
+
const commitment = _calculateSignerId(pubkey, _instanceSalt);
|
|
290
|
+
|
|
291
|
+
// Duplicate detection — sufficient for 2 signers only
|
|
292
|
+
assert(commitment != state.prevCommitment, "Multisig: duplicate signer");
|
|
293
|
+
|
|
294
|
+
Signer_assertSigner(commitment);
|
|
295
|
+
|
|
296
|
+
// TODO: Replace with ecdsaVerify + keccak256 when primitives are available
|
|
297
|
+
assert(stubVerifySignature(pubkey, state.msgHash, signature), "Multisig: invalid signature");
|
|
298
|
+
|
|
299
|
+
return VerificationState {
|
|
300
|
+
validCount: state.validCount + 1 as Uint<8>,
|
|
301
|
+
prevCommitment: commitment,
|
|
302
|
+
msgHash: state.msgHash
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* @description Computes a signer commitment from an ECDSA public key.
|
|
308
|
+
*
|
|
309
|
+
* The commitment is persistentHash(pk, salt, domain) where:
|
|
310
|
+
* - pk: the signer's ECDSA public key (64 bytes)
|
|
311
|
+
* - salt: instance-specific random value (prevents cross-contract correlation)
|
|
312
|
+
* - domain: "multisig:signer:" (domain separation)
|
|
313
|
+
*
|
|
314
|
+
* Pure circuit — callable off-chain by the deployer to compute
|
|
315
|
+
* commitments for the constructor.
|
|
316
|
+
*
|
|
317
|
+
* @param {Bytes<64>} pk - The ECDSA public key.
|
|
318
|
+
* @param {Bytes<32>} salt - The instance salt.
|
|
319
|
+
* @returns {Bytes<32>} The signer commitment.
|
|
320
|
+
*/
|
|
321
|
+
export pure circuit _calculateSignerId(
|
|
322
|
+
pk: Bytes<64>,
|
|
323
|
+
salt: Bytes<32>
|
|
324
|
+
): Bytes<32> {
|
|
325
|
+
return persistentHash<SignerCommitmentInput>(SignerCommitmentInput {
|
|
326
|
+
pk: pk,
|
|
327
|
+
salt: salt,
|
|
328
|
+
domain: pad(32, "multisig:signer:")
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* @description Stub for ECDSA signature verification.
|
|
334
|
+
* Always returns true. MUST be replaced before any non-test deployment.
|
|
335
|
+
*/
|
|
336
|
+
circuit stubVerifySignature(
|
|
337
|
+
pubkey: Bytes<64>,
|
|
338
|
+
msgHash: Bytes<32>,
|
|
339
|
+
signature: Bytes<64>
|
|
340
|
+
): Boolean {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ─── View ───────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
export circuit getNonce(): Uint<64> {
|
|
347
|
+
return _counter;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export circuit getTokenDomain(): Bytes<32> {
|
|
351
|
+
return _tokenDomain;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export circuit getTokenType(): Bytes<32> {
|
|
355
|
+
return tokenType(_tokenDomain, kernel.self());
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export circuit getSignerCount(): Uint<8> {
|
|
359
|
+
return Signer_getSignerCount();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export circuit getThreshold(): Uint<8> {
|
|
363
|
+
return Signer_getThreshold();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export circuit isSigner(commitment: Bytes<32>): Boolean {
|
|
367
|
+
return Signer_isSigner(commitment);
|
|
368
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/presets/forwarder/ForwarderPrivate.compact)
|
|
3
|
+
|
|
4
|
+
pragma language_version >= 0.23.0;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title ForwarderPrivate
|
|
8
|
+
* @description Private-parent forwarder. The parent address is hidden
|
|
9
|
+
* behind a `persistentHash` commitment on the ledger. Coins dwell at
|
|
10
|
+
* the contract address after deposit; the operator drains them later
|
|
11
|
+
* by presenting the `(parentAddr, opSecret)` preimage at drain time.
|
|
12
|
+
*
|
|
13
|
+
* Knowledge of the preimage is the sole authorization gate. The
|
|
14
|
+
* operational secret is held off-chain by the deployer; losing it is
|
|
15
|
+
* equivalent to loss of a hot-wallet key. Two forwarders bound to the
|
|
16
|
+
* same parent with different operational secrets produce different
|
|
17
|
+
* commitments and are unlinkable on-chain.
|
|
18
|
+
*
|
|
19
|
+
* @notice Each forwarder is bound to a single parent at deploy. To
|
|
20
|
+
* change the parent, deploy a new forwarder; the old one remains
|
|
21
|
+
* functional for outstanding coins until drained.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import CompactStandardLibrary;
|
|
25
|
+
import "../../ForwarderPrivate" prefix ForwarderPrivate_;
|
|
26
|
+
|
|
27
|
+
export { ShieldedCoinInfo, QualifiedShieldedCoinInfo, ShieldedSendResult, ZswapCoinPublicKey };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @description Deploys the forwarder bound to a specific parent
|
|
31
|
+
* commitment. The deployer computes the commitment off-chain as
|
|
32
|
+
* `calculateParentCommitment(parentAddr, opSecret)` and passes it here.
|
|
33
|
+
*
|
|
34
|
+
* @param {Bytes<32>} parentCommitment - The commitment to the
|
|
35
|
+
* `(parentAddr, opSecret)` pair that the operator will present at drain.
|
|
36
|
+
*/
|
|
37
|
+
constructor(parentCommitment: Bytes<32>) {
|
|
38
|
+
ForwarderPrivate_initialize(parentCommitment);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @description Receives a shielded coin into the forwarder's custody.
|
|
43
|
+
* No ledger write — the coin sits at the contract address until drained.
|
|
44
|
+
*
|
|
45
|
+
* @param {ShieldedCoinInfo} coin - The incoming shielded coin.
|
|
46
|
+
*
|
|
47
|
+
* @returns {[]} Empty tuple.
|
|
48
|
+
*/
|
|
49
|
+
export circuit deposit(coin: ShieldedCoinInfo): [] {
|
|
50
|
+
ForwarderPrivate__deposit(coin);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @description Spends a previously-deposited shielded coin to the parent,
|
|
55
|
+
* a coin public key. The caller proves knowledge of `(parent, opSecret)`
|
|
56
|
+
* matching the stored commitment; the coin is sent as a shielded note, so the
|
|
57
|
+
* parent stays hidden on-chain. If the input coin's value exceeds `value`,
|
|
58
|
+
* change is re-emitted back to the contract for future drains.
|
|
59
|
+
*
|
|
60
|
+
* The parent is a `ZswapCoinPublicKey`, never a `ContractAddress`: a shielded
|
|
61
|
+
* send to a contract publishes that contract's address in cleartext, which
|
|
62
|
+
* would defeat the private-parent guarantee.
|
|
63
|
+
*
|
|
64
|
+
* Requirements:
|
|
65
|
+
*
|
|
66
|
+
* - `parent` must not be the zero key.
|
|
67
|
+
* - `calculateParentCommitment(parent.bytes, opSecret)` must equal the stored
|
|
68
|
+
* `_parentCommitment`.
|
|
69
|
+
* - `coin.value` must be >= `value` (enforced by `sendShielded`).
|
|
70
|
+
*
|
|
71
|
+
* @param {QualifiedShieldedCoinInfo} coin - The coin to spend.
|
|
72
|
+
* @param {ZswapCoinPublicKey} parent - The parent coin public key. Its 32
|
|
73
|
+
* bytes are the preimage to the stored commitment.
|
|
74
|
+
* @param {Bytes<32>} opSecret - The operational secret. Never appears
|
|
75
|
+
* on the public transcript.
|
|
76
|
+
*
|
|
77
|
+
* @warning **Losing the operational secret is permanent fund loss.** It
|
|
78
|
+
* is the sole drain authorization. No rotation, revocation, or recovery
|
|
79
|
+
* path exists. If the operator loses it, every shielded coin accumulated
|
|
80
|
+
* at this contract becomes inaccessible. Back it up offline before the
|
|
81
|
+
* first deposit.
|
|
82
|
+
*
|
|
83
|
+
* @param {Uint<128>} value - The amount to send.
|
|
84
|
+
*
|
|
85
|
+
* @returns {ShieldedSendResult} The result containing the sent coin and
|
|
86
|
+
* any change.
|
|
87
|
+
*/
|
|
88
|
+
export circuit drain(
|
|
89
|
+
coin: QualifiedShieldedCoinInfo,
|
|
90
|
+
parent: ZswapCoinPublicKey,
|
|
91
|
+
opSecret: Bytes<32>,
|
|
92
|
+
value: Uint<128>
|
|
93
|
+
): ShieldedSendResult {
|
|
94
|
+
return ForwarderPrivate__drain(coin, parent, opSecret, value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @description Returns the stored parent commitment.
|
|
99
|
+
*
|
|
100
|
+
* @returns {Bytes<32>} The commitment set at deploy.
|
|
101
|
+
*/
|
|
102
|
+
export circuit getParentCommitment(): Bytes<32> {
|
|
103
|
+
return ForwarderPrivate__parentCommitment;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @description Computes the parent commitment from a `(parentAddr, opSecret)`
|
|
108
|
+
* pair. Pure circuit — used off-chain by the deployer to compute the
|
|
109
|
+
* constructor argument, and inside `drain` for the preimage check.
|
|
110
|
+
*
|
|
111
|
+
* The commitment is domain-tagged
|
|
112
|
+
* (`pad(32, "ForwarderPrivate:commitment")`) to prevent preimage
|
|
113
|
+
* collisions with other `persistentHash` users in the system.
|
|
114
|
+
*
|
|
115
|
+
* @param {Bytes<32>} parentAddr - The parent address.
|
|
116
|
+
* @param {Bytes<32>} opSecret - The operational secret.
|
|
117
|
+
*
|
|
118
|
+
* @returns {Bytes<32>} The commitment `persistentHash([pad(32, "ForwarderPrivate:commitment"), parentAddr, opSecret])`.
|
|
119
|
+
*/
|
|
120
|
+
export pure circuit calculateParentCommitment(
|
|
121
|
+
parentAddr: Bytes<32>,
|
|
122
|
+
opSecret: Bytes<32>
|
|
123
|
+
): Bytes<32> {
|
|
124
|
+
return ForwarderPrivate__calculateParentCommitment(parentAddr, opSecret);
|
|
125
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/presets/forwarder/ForwarderShielded.compact)
|
|
3
|
+
|
|
4
|
+
pragma language_version >= 0.23.0;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title ForwarderShielded
|
|
8
|
+
* @description Public-parent forwarder for shielded coins. Receives a
|
|
9
|
+
* shielded coin and atomically forwards it to the configured parent
|
|
10
|
+
* recipient, a coin public key.
|
|
11
|
+
*
|
|
12
|
+
* The parent recipient is set at deploy time. This preset exposes no
|
|
13
|
+
* setter, so the parent is fixed for the life of the contract. Anyone
|
|
14
|
+
* may call `deposit`; the recipient is fixed, so there is no need for
|
|
15
|
+
* access control.
|
|
16
|
+
*
|
|
17
|
+
* The constructor accepts a `ZswapCoinPublicKey`: an atomic forward can
|
|
18
|
+
* only deliver to a recipient that needs no in-tx claim, and a shielded
|
|
19
|
+
* send to a non-participating contract is rejected. The parent is stored
|
|
20
|
+
* generically (`Either`) on the ledger so a future circuit upgrade can
|
|
21
|
+
* add contract-address support without a state migration.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import CompactStandardLibrary;
|
|
25
|
+
import "../../ForwarderShielded" prefix Forwarder_;
|
|
26
|
+
|
|
27
|
+
export { ZswapCoinPublicKey, ContractAddress, ShieldedCoinInfo, Either };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @description Deploys the forwarder bound to a specific parent
|
|
31
|
+
* recipient.
|
|
32
|
+
*
|
|
33
|
+
* @param {ZswapCoinPublicKey} parent - The coin public key that receives
|
|
34
|
+
* every forwarded coin.
|
|
35
|
+
*/
|
|
36
|
+
constructor(parent: ZswapCoinPublicKey) {
|
|
37
|
+
Forwarder_initialize(parent);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @description Receives a shielded coin and atomically forwards it to
|
|
42
|
+
* the configured parent. The coin is claimed via `receiveShielded` and
|
|
43
|
+
* immediately re-sent via `sendImmediateShielded`.
|
|
44
|
+
*
|
|
45
|
+
* @param {ShieldedCoinInfo} coin - The incoming shielded coin.
|
|
46
|
+
*
|
|
47
|
+
* @returns {[]} Empty tuple.
|
|
48
|
+
*/
|
|
49
|
+
export circuit deposit(coin: ShieldedCoinInfo): [] {
|
|
50
|
+
Forwarder__deposit(coin);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @description Returns the configured parent recipient as stored on the
|
|
55
|
+
* ledger (the generic `Either`; the `left` arm holds the coin public key).
|
|
56
|
+
*
|
|
57
|
+
* @returns {Either<ZswapCoinPublicKey, ContractAddress>} The parent
|
|
58
|
+
* recipient set at deploy.
|
|
59
|
+
*/
|
|
60
|
+
export circuit getParent(): Either<ZswapCoinPublicKey, ContractAddress> {
|
|
61
|
+
return Forwarder__parent;
|
|
62
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha.1 (multisig/presets/forwarder/ForwarderUnshielded.compact)
|
|
3
|
+
|
|
4
|
+
pragma language_version >= 0.23.0;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title ForwarderUnshielded
|
|
8
|
+
* @description Public-parent forwarder for unshielded coins. Receives
|
|
9
|
+
* an unshielded amount of a given color and atomically forwards it to
|
|
10
|
+
* the configured parent recipient, a user address.
|
|
11
|
+
*
|
|
12
|
+
* Unshielded transfers are publicly visible on the chain: depositor,
|
|
13
|
+
* recipient, color, and amount all appear on the public transcript.
|
|
14
|
+
* Use `ForwarderShielded` instead when the deposit kind is shielded.
|
|
15
|
+
*
|
|
16
|
+
* The constructor accepts a `UserAddress`: an atomic forward can only
|
|
17
|
+
* deliver to a recipient that needs no in-tx claim, and an unshielded
|
|
18
|
+
* send to a non-participating contract is rejected. The parent is stored
|
|
19
|
+
* generically (`Either`) on the ledger so a future circuit upgrade can
|
|
20
|
+
* add contract-address support without a state migration.
|
|
21
|
+
*
|
|
22
|
+
* The parent recipient is set at deploy time. This preset exposes no
|
|
23
|
+
* setter, so the parent is fixed for the life of the contract.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import CompactStandardLibrary;
|
|
27
|
+
import "../../ForwarderUnshielded" prefix Forwarder_;
|
|
28
|
+
|
|
29
|
+
export { ContractAddress, UserAddress, Either };
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @description Deploys the forwarder bound to a specific parent
|
|
33
|
+
* recipient.
|
|
34
|
+
*
|
|
35
|
+
* @param {UserAddress} parent - The user address that receives every
|
|
36
|
+
* forwarded amount.
|
|
37
|
+
*/
|
|
38
|
+
constructor(parent: UserAddress) {
|
|
39
|
+
Forwarder_initialize(parent);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @description Receives an unshielded amount of `color` and atomically
|
|
44
|
+
* forwards it to the configured parent.
|
|
45
|
+
*
|
|
46
|
+
* @param {Bytes<32>} color - The token color.
|
|
47
|
+
* @param {Uint<128>} amount - The amount to deposit.
|
|
48
|
+
*
|
|
49
|
+
* @returns {[]} Empty tuple.
|
|
50
|
+
*/
|
|
51
|
+
export circuit deposit(color: Bytes<32>, amount: Uint<128>): [] {
|
|
52
|
+
Forwarder__deposit(color, amount);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @description Returns the configured parent recipient as stored on the
|
|
57
|
+
* ledger (the generic `Either`; the `right` arm holds the user address).
|
|
58
|
+
*
|
|
59
|
+
* @returns {Either<ContractAddress, UserAddress>} The parent recipient
|
|
60
|
+
* set at deploy.
|
|
61
|
+
*/
|
|
62
|
+
export circuit getParent(): Either<ContractAddress, UserAddress> {
|
|
63
|
+
return Forwarder__parent;
|
|
64
|
+
}
|