@openzeppelin/compact-contracts 0.1.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.
- package/README.md +278 -0
- package/access/AccessControl.compact +479 -0
- package/access/Ownable.compact +353 -0
- package/access/ShieldedAccessControl.compact +784 -0
- package/access/ZOwnablePK.compact +304 -0
- package/package.json +54 -0
- package/security/Initializable.compact +60 -0
- package/security/Pausable.compact +89 -0
- package/token/FungibleToken.compact +742 -0
- package/token/MultiToken.compact +728 -0
- package/token/NonFungibleToken.compact +954 -0
- package/utils/Utils.compact +98 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.1.0 (access/Ownable.compact)
|
|
3
|
+
|
|
4
|
+
pragma language_version >= 0.21.0;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module Ownable
|
|
8
|
+
* @description An unshielded Ownable library.
|
|
9
|
+
* This module provides a basic access control mechanism, where there is an owner
|
|
10
|
+
* that can be granted exclusive access to specific circuits.
|
|
11
|
+
* This approach is perfectly reasonable for contracts that have a single administrative user.
|
|
12
|
+
*
|
|
13
|
+
* Authorization is based on a witness-derived identity scheme. The owner proves knowledge
|
|
14
|
+
* of a secret key by injecting it via the `wit_OwnableSK` witness. The module computes
|
|
15
|
+
* an account identifier as `persistentHash(secretKey)` which is a commitment that hides the key
|
|
16
|
+
* while providing a stable, pseudonymous on-chain identity.
|
|
17
|
+
*
|
|
18
|
+
* Because the account identifier is `H(secretKey)` with no per-deployment salt or domain
|
|
19
|
+
* separator, the same secret key produces the same identity across all contracts. This is
|
|
20
|
+
* intentional. It provides a linkable pseudonymous identity analogous to Solidity's
|
|
21
|
+
* `msg.sender`. Users who desire cross-contract unlinkability can use different secret keys
|
|
22
|
+
* per contract at the wallet layer.
|
|
23
|
+
*
|
|
24
|
+
* The initial owner must be set by using the `initialize` circuit during construction.
|
|
25
|
+
* This can later be changed with `transferOwnership`.
|
|
26
|
+
*
|
|
27
|
+
* @notice Ownership can only be transferred to Bytes<32> account identifiers
|
|
28
|
+
* through the main transfer circuits (`transferOwnership` and `_transferOwnership`).
|
|
29
|
+
* In other words, ownership transfers to contract addresses are disallowed through these
|
|
30
|
+
* circuits.
|
|
31
|
+
* This is because Compact currently does not support contract-to-contract calls which means
|
|
32
|
+
* if a contract is granted ownership, the owner contract cannot directly call the protected
|
|
33
|
+
* circuit.
|
|
34
|
+
*
|
|
35
|
+
* @notice This module does offer experimental circuits that allow ownership to be granted
|
|
36
|
+
* to contract addresses (`_unsafeTransferOwnership` and `_unsafeUncheckedTransferOwnership`).
|
|
37
|
+
* Note that the circuit names are very explicit ("unsafe") with these experimental circuits.
|
|
38
|
+
* Until contract-to-contract calls are supported, there is no direct way for a contract
|
|
39
|
+
* to authenticate as an owner.
|
|
40
|
+
*
|
|
41
|
+
* @notice The unsafe circuits are planned to become deprecated once contract-to-contract calls
|
|
42
|
+
* are supported.
|
|
43
|
+
*
|
|
44
|
+
* @dev Migration plan (safe -> C2C-capable ownership transfers)
|
|
45
|
+
*
|
|
46
|
+
* The owner type is the final `Either<Bytes<32>, ContractAddress>` from day one, in both circuit
|
|
47
|
+
* signatures and the `_owner` ledger field. This is deliberate. Midnight's upgrade mechanism, the
|
|
48
|
+
* Contract Maintenance Authority (CMA), can rotate a circuit's verifier key but cannot migrate
|
|
49
|
+
* ledger state (there is no state-rewrite primitive; see the `updatability` tutorial). Fixing the
|
|
50
|
+
* type now makes the future migration a pure verifier-key rotation with zero ledger-state change,
|
|
51
|
+
* which is the only kind of upgrade the CMA supports. Widening the type later would instead force
|
|
52
|
+
* a ledger-layout rewrite, which the CMA cannot perform.
|
|
53
|
+
*
|
|
54
|
+
* When C2C lands, an already-deployed contract migrates without changing its address or ledger
|
|
55
|
+
* state:
|
|
56
|
+
*
|
|
57
|
+
* - Minor release: mark `_unsafeTransferOwnership` (and `_unsafeUncheckedTransferOwnership`)
|
|
58
|
+
* deprecated (and emit a warning if possible); keep their implementation intact so existing
|
|
59
|
+
* callers continue to work.
|
|
60
|
+
* - Major release: ship `transferOwnership` / `_transferOwnership` with the `isContract` guard
|
|
61
|
+
* removed and drop the `_unsafe` variants. Operators adopt the new logic by signing a CMA
|
|
62
|
+
* `MaintenanceUpdate` that rotates the affected verifier keys (`VerifierKeyRemove` +
|
|
63
|
+
* `VerifierKeyInsert` for each guarded circuit, and a `VerifierKeyRemove` for each `_unsafe`
|
|
64
|
+
* variant). The contract address and all ledger state are preserved. Because the CMA allows
|
|
65
|
+
* multiple active VK versions per operation, old and new keys can coexist during a transition
|
|
66
|
+
* window so in-flight proofs still verify. By this point, anyone using the `_unsafe` variants
|
|
67
|
+
* should have migrated to the now C2C-capable circuits.
|
|
68
|
+
*
|
|
69
|
+
* @dev Canonicalization
|
|
70
|
+
* All `Either<Bytes<32>, ContractAddress>` values are canonicalized before being written
|
|
71
|
+
* to the `_owner` ledger field. Canonicalization zeroes out the inactive branch of the Either,
|
|
72
|
+
* ensuring consistent storage regardless of what data the inactive branch carries.
|
|
73
|
+
* The single write point is `_unsafeUncheckedTransferOwnership`, through which all
|
|
74
|
+
* ownership changes flow.
|
|
75
|
+
*
|
|
76
|
+
* @dev Security Considerations:
|
|
77
|
+
* - The `secretKey` must be kept private. Loss of the key prevents the owner
|
|
78
|
+
* from proving ownership. Key exposure allows impersonation.
|
|
79
|
+
* - It is strongly recommended to use cryptographically secure random values for the secret key
|
|
80
|
+
* (e.g., `crypto.getRandomValues()`). Weak or predictable keys can be brute-forced.
|
|
81
|
+
* - The `secretKey` is provided via the `wit_OwnableSK` witness. As with all witnesses,
|
|
82
|
+
* the contract must not assume that the witness implementation matches the developer's code.
|
|
83
|
+
* Any DApp may provide any implementation. The ZK proof system constrains what values
|
|
84
|
+
* can produce valid proofs.
|
|
85
|
+
*/
|
|
86
|
+
module Ownable {
|
|
87
|
+
import CompactStandardLibrary;
|
|
88
|
+
import "../security/Initializable" prefix Initializable_;
|
|
89
|
+
import "../utils/Utils" prefix Utils_;
|
|
90
|
+
|
|
91
|
+
export ledger _owner: Either<Bytes<32>, ContractAddress>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @witness wit_OwnableSK
|
|
95
|
+
* @description Returns the caller's secret key used in deriving the account identifier.
|
|
96
|
+
*
|
|
97
|
+
* The same key produces the same account identifier across all contracts. Users who
|
|
98
|
+
* desire cross-contract unlinkability should use different keys per contract.
|
|
99
|
+
*
|
|
100
|
+
* @returns {Bytes<32>} secretKey - A 32-byte cryptographically secure random value.
|
|
101
|
+
*/
|
|
102
|
+
witness wit_OwnableSK(): Bytes<32>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @description Initializes the contract by setting the `initialOwner`.
|
|
106
|
+
* This must be called in the contract's constructor.
|
|
107
|
+
*
|
|
108
|
+
* @circuitInfo k=10, rows=626
|
|
109
|
+
*
|
|
110
|
+
* Requirements:
|
|
111
|
+
*
|
|
112
|
+
* - Contract is not already initialized.
|
|
113
|
+
* - `initialOwner` is not a ContractAddress.
|
|
114
|
+
* - `initialOwner` is not the zero address.
|
|
115
|
+
*
|
|
116
|
+
* @param {Either<Bytes<32>, ContractAddress>} initialOwner - The initial owner of the contract.
|
|
117
|
+
* @returns {[]} Empty tuple.
|
|
118
|
+
*/
|
|
119
|
+
export circuit initialize(initialOwner: Either<Bytes<32>, ContractAddress>): [] {
|
|
120
|
+
Initializable_initialize();
|
|
121
|
+
assert(!_isTargetZero(initialOwner), "Ownable: invalid initial owner");
|
|
122
|
+
_transferOwnership(initialOwner);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @description Returns the current contract owner.
|
|
127
|
+
*
|
|
128
|
+
* @circuitInfo k=7, rows=76
|
|
129
|
+
*
|
|
130
|
+
* Requirements:
|
|
131
|
+
*
|
|
132
|
+
* - Contract is initialized.
|
|
133
|
+
*
|
|
134
|
+
* @returns {Either<Bytes<32>, ContractAddress>} - The contract owner.
|
|
135
|
+
*/
|
|
136
|
+
export circuit owner(): Either<Bytes<32>, ContractAddress> {
|
|
137
|
+
Initializable_assertInitialized();
|
|
138
|
+
return _owner;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @description Transfers ownership of the contract to `newOwner`.
|
|
143
|
+
*
|
|
144
|
+
* @notice Ownership transfers to contract addresses are currently disallowed until contract-to-contract
|
|
145
|
+
* interactions are supported in Compact.
|
|
146
|
+
* This restriction prevents permanently disabling access to a circuit.
|
|
147
|
+
*
|
|
148
|
+
* @circuitInfo k=13, rows=2959
|
|
149
|
+
*
|
|
150
|
+
* Requirements:
|
|
151
|
+
*
|
|
152
|
+
* - Contract is initialized.
|
|
153
|
+
* - The caller is the current contract owner.
|
|
154
|
+
* - `newOwner` is not a ContractAddress.
|
|
155
|
+
* - `newOwner` is not the zero address.
|
|
156
|
+
*
|
|
157
|
+
* @param {Either<Bytes<32>, ContractAddress>} newOwner - The new owner.
|
|
158
|
+
* @returns {[]} Empty tuple.
|
|
159
|
+
*/
|
|
160
|
+
export circuit transferOwnership(newOwner: Either<Bytes<32>, ContractAddress>): [] {
|
|
161
|
+
Initializable_assertInitialized();
|
|
162
|
+
const isContractAddr = !newOwner.is_left;
|
|
163
|
+
assert(!isContractAddr, "Ownable: unsafe ownership transfer");
|
|
164
|
+
_unsafeTransferOwnership(newOwner);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @description Unsafe variant of `transferOwnership`.
|
|
169
|
+
*
|
|
170
|
+
* @warning Ownership transfers to contract addresses are considered unsafe because contract-to-contract
|
|
171
|
+
* calls are not currently supported.
|
|
172
|
+
* Ownership privileges sent to a contract address may become uncallable.
|
|
173
|
+
* Once contract-to-contract calls are supported, this circuit may be deprecated.
|
|
174
|
+
*
|
|
175
|
+
* @circuitInfo k=13, rows=2956
|
|
176
|
+
*
|
|
177
|
+
* Requirements:
|
|
178
|
+
*
|
|
179
|
+
* - Contract is initialized.
|
|
180
|
+
* - The caller is the current contract owner.
|
|
181
|
+
* - `newOwner` is not the zero address.
|
|
182
|
+
*
|
|
183
|
+
* @param {Either<Bytes<32>, ContractAddress>} newOwner - The new owner.
|
|
184
|
+
* @returns {[]} Empty tuple.
|
|
185
|
+
*/
|
|
186
|
+
export circuit _unsafeTransferOwnership(newOwner: Either<Bytes<32>, ContractAddress>): [] {
|
|
187
|
+
Initializable_assertInitialized();
|
|
188
|
+
assertOnlyOwner();
|
|
189
|
+
assert(!_isTargetZero(newOwner), "Ownable: invalid new owner");
|
|
190
|
+
_unsafeUncheckedTransferOwnership(newOwner);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @description Leaves the contract without an owner.
|
|
195
|
+
* It will not be possible to call `assertOnlyOwner` circuits anymore.
|
|
196
|
+
* Can only be called by the current owner.
|
|
197
|
+
*
|
|
198
|
+
* @circuitInfo k=13, rows=2364
|
|
199
|
+
*
|
|
200
|
+
* Requirements:
|
|
201
|
+
*
|
|
202
|
+
* - Contract is initialized.
|
|
203
|
+
* - The caller is the current contract owner.
|
|
204
|
+
*
|
|
205
|
+
* @returns {[]} Empty tuple.
|
|
206
|
+
*/
|
|
207
|
+
export circuit renounceOwnership(): [] {
|
|
208
|
+
Initializable_assertInitialized();
|
|
209
|
+
assertOnlyOwner();
|
|
210
|
+
|
|
211
|
+
// We use `left` here because `assertOnlyOwner` would return a misleading
|
|
212
|
+
// error msg (c2c is not currently supported)
|
|
213
|
+
const zero = Either<Bytes<32>, ContractAddress> {
|
|
214
|
+
is_left: true, left: default<Bytes<32>>, right: default<ContractAddress>
|
|
215
|
+
};
|
|
216
|
+
_unsafeUncheckedTransferOwnership(zero);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* @description Throws if called by any account other than the owner.
|
|
221
|
+
* Use this to restrict access of specific circuits to the owner.
|
|
222
|
+
*
|
|
223
|
+
* In the case of an external (non-contract) caller, the caller’s identity is
|
|
224
|
+
* derived from the `wit_OwnableSK` witness as `persistentHash(secretKey)`.
|
|
225
|
+
*
|
|
226
|
+
* @circuitInfo k=13, rows=2360
|
|
227
|
+
*
|
|
228
|
+
* Requirements:
|
|
229
|
+
*
|
|
230
|
+
* - Contract is initialized.
|
|
231
|
+
* - The caller is the current contract owner.
|
|
232
|
+
*
|
|
233
|
+
* @returns {[]} Empty tuple.
|
|
234
|
+
*/
|
|
235
|
+
export circuit assertOnlyOwner(): [] {
|
|
236
|
+
Initializable_assertInitialized();
|
|
237
|
+
if (_owner.is_left) {
|
|
238
|
+
assert(_computeAccountId() == _owner.left, "Ownable: caller is not the owner");
|
|
239
|
+
} else {
|
|
240
|
+
assert(false, "Ownable: contract address owner authentication is not yet supported");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* @description Transfers ownership of the contract to `newOwner` without
|
|
246
|
+
* enforcing permission checks on the caller.
|
|
247
|
+
*
|
|
248
|
+
* @notice Ownership transfers to contract addresses are currently disallowed until contract-to-contract
|
|
249
|
+
* interactions are supported in Compact.
|
|
250
|
+
* This restriction prevents circuits from being inadvertently locked in contracts.
|
|
251
|
+
*
|
|
252
|
+
* @circuitInfo k=10, rows=600
|
|
253
|
+
*
|
|
254
|
+
* Requirements:
|
|
255
|
+
*
|
|
256
|
+
* - Contract is initialized.
|
|
257
|
+
* - `newOwner` is not a ContractAddress.
|
|
258
|
+
*
|
|
259
|
+
* @param {Either<Bytes<32>, ContractAddress>} newOwner - The new owner.
|
|
260
|
+
* @returns {[]} Empty tuple.
|
|
261
|
+
*/
|
|
262
|
+
export circuit _transferOwnership(newOwner: Either<Bytes<32>, ContractAddress>): [] {
|
|
263
|
+
Initializable_assertInitialized();
|
|
264
|
+
const isContractAddr = !newOwner.is_left;
|
|
265
|
+
assert(!isContractAddr, "Ownable: unsafe ownership transfer");
|
|
266
|
+
_unsafeUncheckedTransferOwnership(newOwner);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @description Unsafe variant of `_transferOwnership`.
|
|
271
|
+
*
|
|
272
|
+
* @warning Ownership transfers to contract addresses are considered unsafe because contract-to-contract
|
|
273
|
+
* calls are not currently supported.
|
|
274
|
+
* Ownership privileges sent to a contract address may become uncallable.
|
|
275
|
+
* Once contract-to-contract calls are supported, this circuit may be deprecated.
|
|
276
|
+
*
|
|
277
|
+
* @circuitInfo k=10, rows=597
|
|
278
|
+
*
|
|
279
|
+
* Requirements:
|
|
280
|
+
*
|
|
281
|
+
* - Contract is initialized.
|
|
282
|
+
*
|
|
283
|
+
* @param {Either<Bytes<32>, ContractAddress>} newOwner - The new owner.
|
|
284
|
+
* @returns {[]} Empty tuple.
|
|
285
|
+
*/
|
|
286
|
+
export circuit _unsafeUncheckedTransferOwnership(
|
|
287
|
+
newOwner: Either<Bytes<32>, ContractAddress>
|
|
288
|
+
): [] {
|
|
289
|
+
Initializable_assertInitialized();
|
|
290
|
+
const canonAcct = Utils_canonicalize<Bytes<32>, ContractAddress>(newOwner);
|
|
291
|
+
_owner = disclose(canonAcct);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* @description Computes the caller's account identifier from the `wit_OwnableSK` witness.
|
|
296
|
+
*
|
|
297
|
+
* ## ID Derivation
|
|
298
|
+
* `accountId = persistentHash(secretKey)`
|
|
299
|
+
*
|
|
300
|
+
* The result is a 32-byte commitment that uniquely identifies the caller.
|
|
301
|
+
*
|
|
302
|
+
* @returns {Bytes<32>} accountId - The computed account identifier.
|
|
303
|
+
*/
|
|
304
|
+
circuit _computeAccountId(): Bytes<32> {
|
|
305
|
+
return computeAccountId(wit_OwnableSK());
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* @description Computes an account identifier without on-chain state, allowing a user to derive
|
|
310
|
+
* their identity commitment before submitting it in an ownership transfer.
|
|
311
|
+
* This is the off-chain counterpart to {_computeAccountId} and produces an identical result
|
|
312
|
+
* given the same inputs.
|
|
313
|
+
*
|
|
314
|
+
* @warning OpSec: The `secretKey` parameter is a sensitive secret. Mishandling it can
|
|
315
|
+
* permanently compromise the security of this system:
|
|
316
|
+
*
|
|
317
|
+
* - **Never log or persist** the `secretKey` in plaintext — avoid browser devtools,
|
|
318
|
+
* application logs, analytics pipelines, or any observable side-channel.
|
|
319
|
+
* - **Store offline or in secure enclaves** — hardware security modules (HSMs),
|
|
320
|
+
* air-gapped devices, or encrypted vaults are strongly preferred over hot storage.
|
|
321
|
+
* - **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()`
|
|
322
|
+
* or equivalent; weak or predictable keys can be brute-forced to reveal your identity.
|
|
323
|
+
* - **Treat key loss as identity loss** — a lost key cannot be recovered.
|
|
324
|
+
* - **Avoid calling this circuit in untrusted environments** — executing this in an
|
|
325
|
+
* unverified browser extension, compromised runtime, or shared machine may expose
|
|
326
|
+
* the key to a malicious observer.
|
|
327
|
+
*
|
|
328
|
+
* ## ID Derivation
|
|
329
|
+
* `accountId = persistentHash(secretKey)`
|
|
330
|
+
*
|
|
331
|
+
* @param {Bytes<32>} secretKey - A 32-byte cryptographically secure random value.
|
|
332
|
+
*
|
|
333
|
+
* @returns {Bytes<32>} accountId - The computed account identifier.
|
|
334
|
+
*/
|
|
335
|
+
export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> {
|
|
336
|
+
return persistentHash<Vector<1, Bytes<32>>>([secretKey]);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* @description Returns `true` if `target`'s active branch (as indicated by `is_left`)
|
|
341
|
+
* holds the zero value.
|
|
342
|
+
*
|
|
343
|
+
* @param {Either<Bytes<32>, ContractAddress>} target - The value to check.
|
|
344
|
+
* @returns {Boolean} - `true` if the active branch is zero, `false` otherwise.
|
|
345
|
+
*/
|
|
346
|
+
circuit _isTargetZero(target: Either<Bytes<32>, ContractAddress>): Boolean {
|
|
347
|
+
if (target.is_left) {
|
|
348
|
+
return target.left == default<Bytes<32>>;
|
|
349
|
+
} else {
|
|
350
|
+
return target.right == default<ContractAddress>;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|