@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,728 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.1.0 (token/MultiToken.compact)
|
|
3
|
+
|
|
4
|
+
pragma language_version >= 0.21.0;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module MultiToken
|
|
8
|
+
* @description An unshielded MultiToken library. This library is inspired by
|
|
9
|
+
* ERC1155. Many aspects of the EIP-1155 spec cannot be implemented in Compact;
|
|
10
|
+
* therefore, the MultiToken module should be treated as an approximation of
|
|
11
|
+
* the ERC1155 standard and not necessarily a compliant implementation.
|
|
12
|
+
*
|
|
13
|
+
* Authorization is based on a witness-derived identity scheme. Each caller proves knowledge
|
|
14
|
+
* of a secret key by injecting it via the `wit_MultiTokenSK` 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
|
+
* @notice One notable difference regarding this implementation and the EIP1155 spec
|
|
25
|
+
* consists of the token size. Uint<128> is used as the token size because Uint<256>
|
|
26
|
+
* cannot be supported. This is true for both token IDs and for amounts.
|
|
27
|
+
* This is due to encoding limits on the midnight circuit backend:
|
|
28
|
+
* https://github.com/midnightntwrk/compactc/issues/929
|
|
29
|
+
*
|
|
30
|
+
* @notice Some features defined in the EIP1155 spec are NOT included.
|
|
31
|
+
* Such features include:
|
|
32
|
+
*
|
|
33
|
+
* 1. Batch mint, burn, transfer - Without support for dynamic arrays,
|
|
34
|
+
* batching transfers is difficult to do without a hacky solution.
|
|
35
|
+
* For instance, we could change the `to` and `fromAddress` parameters to be
|
|
36
|
+
* vectors. This would change the signature and would be both difficult
|
|
37
|
+
* to use and easy to misuse.
|
|
38
|
+
*
|
|
39
|
+
* 2. Querying batched balances - This can be somewhat supported.
|
|
40
|
+
* The issue, without dynamic arrays, is that the module circuit must use
|
|
41
|
+
* Vector<n> for accounts and ids; therefore, the implementing contract must
|
|
42
|
+
* explicitly define the number of balances to query in the circuit i.e.
|
|
43
|
+
*
|
|
44
|
+
* balanceOfBatch_10(
|
|
45
|
+
* accounts: Vector<10, Either<Bytes<32>, ContractAddress>>,
|
|
46
|
+
* ids: Vector<10, Uint<128>>
|
|
47
|
+
* ): Vector<10, Uint<128>>
|
|
48
|
+
*
|
|
49
|
+
* Since this module does not offer mint or transfer batching,
|
|
50
|
+
* balance batching is also not included at this time.
|
|
51
|
+
*
|
|
52
|
+
* 3. Introspection - Compact currently cannot support contract-to-contract
|
|
53
|
+
* queries for introspection. ERC165 (or an equivalent thereof) is NOT
|
|
54
|
+
* included in the contract.
|
|
55
|
+
*
|
|
56
|
+
* 4. Safe transfers - The lack of an introspection mechanism means
|
|
57
|
+
* safe transfers of any kind can not be supported.
|
|
58
|
+
* BE AWARE: Tokens sent to a contract address MAY be lost forever.
|
|
59
|
+
*
|
|
60
|
+
* Due to the vast incompatibilities with the EIP1155 spec, it is our
|
|
61
|
+
* opinion that this implementation should not be called ERC1155 at this time
|
|
62
|
+
* as this would be both very confusing and misleading. This may change as more
|
|
63
|
+
* features become available. The list of missing features is as follows:
|
|
64
|
+
*
|
|
65
|
+
* - Full uint256 support.
|
|
66
|
+
* - Events.
|
|
67
|
+
* - Dynamic arrays.
|
|
68
|
+
* - Introspection.
|
|
69
|
+
* - Contract-to-contract calls for acceptance callback.
|
|
70
|
+
*
|
|
71
|
+
* @dev Canonicalization
|
|
72
|
+
* All `Either<Bytes<32>, ContractAddress>` values are canonicalized before use as map keys
|
|
73
|
+
* or in ledger writes. Canonicalization zeroes out the inactive branch of the Either,
|
|
74
|
+
* ensuring that two values with the same active branch always resolve to the same map key
|
|
75
|
+
* regardless of what data the inactive branch carries. Write paths are canonicalized in
|
|
76
|
+
* `_update` (for `_balances`) and `_setApprovalForAll` (for `_operatorApprovals`).
|
|
77
|
+
* Read paths are canonicalized in `balanceOf` and `isApprovedForAll`.
|
|
78
|
+
* `_unsafeTransferFrom` canonicalizes `fromAddress` before comparing against the caller identity.
|
|
79
|
+
*
|
|
80
|
+
* @dev Security Considerations:
|
|
81
|
+
* - The `secretKey` must be kept private. Loss of the key prevents token holders
|
|
82
|
+
* from proving ownership or authorization. Key exposure allows impersonation.
|
|
83
|
+
* - It is strongly recommended to use cryptographically secure random values for the secret key
|
|
84
|
+
* (e.g., `crypto.getRandomValues()`). Weak or predictable keys can be brute-forced.
|
|
85
|
+
* - The `secretKey` is provided via the `wit_MultiTokenSK` witness. As with all witnesses,
|
|
86
|
+
* the contract must not assume that the witness implementation matches the developer's code.
|
|
87
|
+
* Any DApp may provide any implementation. The ZK proof system constrains what values
|
|
88
|
+
* can produce valid proofs.
|
|
89
|
+
*
|
|
90
|
+
* @dev Migration plan (safe -> C2C-capable transfers)
|
|
91
|
+
*
|
|
92
|
+
* The main circuits (`transferFrom`, `_transfer`, `_mint`) reject `ContractAddress` recipients via
|
|
93
|
+
* an `isContract` guard, because a contract that receives tokens currently cannot move them back
|
|
94
|
+
* out. The `_unsafe` variants (`_unsafeTransferFrom`, `_unsafeTransfer`, `_unsafeMint`) skip the
|
|
95
|
+
* guard for callers who need contract recipients (accepting that such tokens are stranded until
|
|
96
|
+
* C2C lands).
|
|
97
|
+
*
|
|
98
|
+
* The recipient type is the final `Either<Bytes<32>, ContractAddress>` from day one, in both
|
|
99
|
+
* circuit signatures and ledger maps. This is deliberate. Midnight's upgrade mechanism, the
|
|
100
|
+
* Contract Maintenance Authority (CMA), can rotate a circuit's verifier key but cannot migrate
|
|
101
|
+
* ledger state (there is no state-rewrite primitive; see the `updatability` tutorial). Fixing the
|
|
102
|
+
* types now makes the future migration a pure verifier-key rotation with zero ledger-state change,
|
|
103
|
+
* which is the only kind of upgrade the CMA supports. Widening the types later would instead force
|
|
104
|
+
* a ledger-layout rewrite, which the CMA cannot perform.
|
|
105
|
+
*
|
|
106
|
+
* When C2C lands, an already-deployed token migrates without changing its address or ledger state:
|
|
107
|
+
*
|
|
108
|
+
* - Minor release: mark the `_unsafe` variants deprecated (and emit a warning if possible); keep
|
|
109
|
+
* their implementation intact so existing callers continue to work.
|
|
110
|
+
* - Major release: ship the main circuits with the `isContract` guard removed and drop the
|
|
111
|
+
* `_unsafe` variants. Operators adopt the new logic by signing a CMA `MaintenanceUpdate` that
|
|
112
|
+
* rotates the affected verifier keys (`VerifierKeyRemove` + `VerifierKeyInsert` for each guarded
|
|
113
|
+
* circuit, and a `VerifierKeyRemove` for each `_unsafe` variant). The contract address and all
|
|
114
|
+
* ledger state are preserved. Because the CMA allows multiple active VK versions per operation,
|
|
115
|
+
* old and new keys can coexist during a transition window so in-flight proofs still verify. By
|
|
116
|
+
* this point, anyone using the `_unsafe` variants should have migrated to the now C2C-capable
|
|
117
|
+
* circuits.
|
|
118
|
+
*
|
|
119
|
+
* @notice Further discussion and consideration required:
|
|
120
|
+
*
|
|
121
|
+
* - Consider changing the underscore in the internal methods to `unsafe` or
|
|
122
|
+
* adopting dot notation for prefixing imports.
|
|
123
|
+
* - Revise logic once contract-to-contract interactions are available on midnight.
|
|
124
|
+
*/
|
|
125
|
+
module MultiToken {
|
|
126
|
+
import CompactStandardLibrary;
|
|
127
|
+
import "../security/Initializable" prefix Initializable_;
|
|
128
|
+
import "../utils/Utils" prefix Utils_;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @description Mapping from token ID to account balances.
|
|
132
|
+
* @type {Uint<128>} id - The token identifier.
|
|
133
|
+
* @type {Either<Bytes<32>, ContractAddress>} account - The account address.
|
|
134
|
+
* @type {Uint<128>} balance - The balance of the account for the token.
|
|
135
|
+
* @type {Map<id, Map<account, balance>>}
|
|
136
|
+
* @type {Map<Uint<128>, Map<Either<Bytes<32>, ContractAddress>, Uint<128>>>} _balances
|
|
137
|
+
*/
|
|
138
|
+
export ledger _balances: Map<Uint<128>,
|
|
139
|
+
Map<Either<Bytes<32>, ContractAddress>, Uint<128>>>;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @description Mapping from account to operator approvals.
|
|
143
|
+
* @type {Either<Bytes<32>, ContractAddress>} account - The account address.
|
|
144
|
+
* @type {Either<Bytes<32>, ContractAddress>} operator - The operator address.
|
|
145
|
+
* @type {Boolean} approved - The approval status of the operator for the account.
|
|
146
|
+
* @type {Map<account, Map<operator, approved>>}
|
|
147
|
+
* @type {Map<Either<Bytes<32>, ContractAddress>, Map<Either<Bytes<32>, ContractAddress>, Boolean>>}
|
|
148
|
+
*/
|
|
149
|
+
export ledger _operatorApprovals: Map<Either<Bytes<32>, ContractAddress>,
|
|
150
|
+
Map<Either<Bytes<32>, ContractAddress>, Boolean>>;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @description Base URI for computing token URIs.
|
|
154
|
+
* Used as the URI for all token types by relying on ID substitution, e.g. https://token-cdn-domain/{id}.json
|
|
155
|
+
* @type {Opaque<"string">} _uri - The base URI for all token URIs.
|
|
156
|
+
*/
|
|
157
|
+
export ledger _uri: Opaque<"string">;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @witness wit_MultiTokenSK
|
|
161
|
+
* @description Returns the caller's secret key used in deriving the account identifier.
|
|
162
|
+
*
|
|
163
|
+
* The same key produces the same account identifier across all contracts. Users who
|
|
164
|
+
* desire cross-contract unlinkability should use different keys per contract.
|
|
165
|
+
*
|
|
166
|
+
* @returns {Bytes<32>} secretKey - A 32-byte cryptographically secure random value.
|
|
167
|
+
*/
|
|
168
|
+
witness wit_MultiTokenSK(): Bytes<32>;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @description Returns a canonical zero Either value (left variant with zero Bytes<32>).
|
|
172
|
+
* Used as the zero value for mint/burn operations in `_update`, where a zero
|
|
173
|
+
* `fromAddress` signals a mint and a zero `to` signals a burn.
|
|
174
|
+
*
|
|
175
|
+
* The left variant is chosen so that `_isTargetZero` checks against `default<Bytes<32>>`,
|
|
176
|
+
* which is consistent with how `_update` dispatches on the zero check. A right variant
|
|
177
|
+
* with `default<ContractAddress>` would also pass `_isTargetZero`, but using the left
|
|
178
|
+
* variant avoids triggering contract address guards in circuits like `_mint` that check
|
|
179
|
+
* `!to.is_left` before delegating to `_unsafeMint`.
|
|
180
|
+
*
|
|
181
|
+
* @return {Either<Bytes<32>, ContractAddress>} - The zero value.
|
|
182
|
+
*/
|
|
183
|
+
export pure circuit ZERO(): Either<Bytes<32>, ContractAddress> {
|
|
184
|
+
return Either<Bytes<32>, ContractAddress> {
|
|
185
|
+
is_left: true, left: default<Bytes<32>>, right: default<ContractAddress>
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @description Initializes the contract by setting the base URI for all tokens.
|
|
191
|
+
*
|
|
192
|
+
* @circuitInfo k=6, rows=33
|
|
193
|
+
*
|
|
194
|
+
* Requirements:
|
|
195
|
+
*
|
|
196
|
+
* - Must be called in the contract's constructor.
|
|
197
|
+
*
|
|
198
|
+
* @param {Opaque<"string">} uri_ - The base URI for all token URIs.
|
|
199
|
+
* @return {[]} - Empty tuple.
|
|
200
|
+
*/
|
|
201
|
+
export circuit initialize(uri_: Opaque<"string">): [] {
|
|
202
|
+
Initializable_initialize();
|
|
203
|
+
_setURI(uri_);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @description This implementation returns the same URI for *all* token types. It relies
|
|
208
|
+
* on the token type ID substitution mechanism defined in the EIP:
|
|
209
|
+
* https://eips.ethereum.org/EIPS/eip-1155#metadata.
|
|
210
|
+
* Clients calling this function must replace the `\{id\}` substring with the
|
|
211
|
+
* actual token type ID.
|
|
212
|
+
*
|
|
213
|
+
* @circuitInfo k=8, rows=176
|
|
214
|
+
*
|
|
215
|
+
* Requirements:
|
|
216
|
+
*
|
|
217
|
+
* - Contract is initialized.
|
|
218
|
+
*
|
|
219
|
+
* @param {Uint<128>} id - The token identifier to query.
|
|
220
|
+
* @return {Opaque<"string">} - The base URI for all tokens.
|
|
221
|
+
*/
|
|
222
|
+
export circuit uri(id: Uint<128>): Opaque<"string"> {
|
|
223
|
+
Initializable_assertInitialized();
|
|
224
|
+
return _uri;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @description Returns the amount of `id` tokens owned by `account`.
|
|
229
|
+
*
|
|
230
|
+
* @circuitInfo k=10, rows=875
|
|
231
|
+
*
|
|
232
|
+
* Requirements:
|
|
233
|
+
*
|
|
234
|
+
* - Contract is initialized.
|
|
235
|
+
*
|
|
236
|
+
* @param {Either<Bytes<32>, ContractAddress>} account - The account balance to query.
|
|
237
|
+
* @param {Uint<128>} id - The token identifier to query.
|
|
238
|
+
* @return {Uint<128>} - The quantity of `id` tokens that `account` owns.
|
|
239
|
+
*/
|
|
240
|
+
export circuit balanceOf(
|
|
241
|
+
account: Either<Bytes<32>, ContractAddress>,
|
|
242
|
+
id: Uint<128>
|
|
243
|
+
): Uint<128> {
|
|
244
|
+
Initializable_assertInitialized();
|
|
245
|
+
const canonAcct = Utils_canonicalize<Bytes<32>, ContractAddress>(account);
|
|
246
|
+
|
|
247
|
+
if (!_balances.member(disclose(id)) || !_balances.lookup(id).member(disclose(canonAcct))) {
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
return _balances.lookup(id).lookup(disclose(canonAcct));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @description Enables or disables approval for `operator` to manage all of the caller's assets.
|
|
255
|
+
*
|
|
256
|
+
* In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_MultiTokenSK`
|
|
257
|
+
* witness as `persistentHash(secretKey)`.
|
|
258
|
+
*
|
|
259
|
+
* @circuitInfo k=13, rows=2944
|
|
260
|
+
*
|
|
261
|
+
* Requirements:
|
|
262
|
+
*
|
|
263
|
+
* - Contract is initialized.
|
|
264
|
+
* - `operator` is not zero.
|
|
265
|
+
*
|
|
266
|
+
* @param {Either<Bytes<32>, ContractAddress>} operator - The account whose approval is set
|
|
267
|
+
* for the caller's assets.
|
|
268
|
+
* @param {Boolean} approved - The boolean value determining if the operator may or may not handle the
|
|
269
|
+
* caller's assets.
|
|
270
|
+
* @return {[]} - Empty tuple.
|
|
271
|
+
*/
|
|
272
|
+
export circuit setApprovalForAll(
|
|
273
|
+
operator: Either<Bytes<32>, ContractAddress>,
|
|
274
|
+
approved: Boolean
|
|
275
|
+
): [] {
|
|
276
|
+
Initializable_assertInitialized();
|
|
277
|
+
const caller = left<Bytes<32>, ContractAddress>(_computeAccountId());
|
|
278
|
+
_setApprovalForAll(caller, operator, approved);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @description Queries if `operator` is an authorized operator for `account`.
|
|
283
|
+
*
|
|
284
|
+
* @circuitInfo k=11, rows=1343
|
|
285
|
+
*
|
|
286
|
+
* Requirements:
|
|
287
|
+
*
|
|
288
|
+
* - Contract is initialized.
|
|
289
|
+
*
|
|
290
|
+
* @param {Either<Bytes<32>, ContractAddress>} account - The queried possessor of assets.
|
|
291
|
+
* @param {Either<Bytes<32>, ContractAddress>} operator - The queried handler of `account`'s assets.
|
|
292
|
+
* @return {Boolean} - Whether or not `operator` has permission to handle `account`'s assets.
|
|
293
|
+
*/
|
|
294
|
+
export circuit isApprovedForAll(
|
|
295
|
+
account: Either<Bytes<32>, ContractAddress>,
|
|
296
|
+
operator: Either<Bytes<32>, ContractAddress>
|
|
297
|
+
): Boolean {
|
|
298
|
+
Initializable_assertInitialized();
|
|
299
|
+
const canonAcct = Utils_canonicalize<Bytes<32>, ContractAddress>(account);
|
|
300
|
+
const canonOp = Utils_canonicalize<Bytes<32>, ContractAddress>(operator);
|
|
301
|
+
|
|
302
|
+
if (!_operatorApprovals.member(disclose(canonAcct)) ||
|
|
303
|
+
!_operatorApprovals.lookup(canonAcct).member(disclose(canonOp))) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return _operatorApprovals.lookup(canonAcct).lookup(disclose(canonOp));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* @description Transfers ownership of `value` amount of `id` tokens from `fromAddress` to `to`.
|
|
312
|
+
* The caller must be `fromAddress` or approved to transfer on their behalf.
|
|
313
|
+
*
|
|
314
|
+
* In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_MultiTokenSK`
|
|
315
|
+
* witness as `persistentHash(secretKey)`.
|
|
316
|
+
*
|
|
317
|
+
* @notice Transfers to contract addresses are currently disallowed until contract-to-contract
|
|
318
|
+
* interactions are supported in Compact. This restriction prevents assets from
|
|
319
|
+
* being inadvertently locked in contracts that cannot currently handle token receipt.
|
|
320
|
+
*
|
|
321
|
+
* @circuitInfo k=13, rows=4926
|
|
322
|
+
*
|
|
323
|
+
* Requirements:
|
|
324
|
+
*
|
|
325
|
+
* - Contract is initialized.
|
|
326
|
+
* - `to` is not a ContractAddress.
|
|
327
|
+
* - `to` is not zero.
|
|
328
|
+
* - `fromAddress` is not zero.
|
|
329
|
+
* - Caller must be `fromAddress` or approved via `setApprovalForAll`.
|
|
330
|
+
* - `fromAddress` must have an `id` balance of at least `value`.
|
|
331
|
+
*
|
|
332
|
+
* @param {Either<Bytes<32>, ContractAddress>} fromAddress - The owner from which the transfer originates.
|
|
333
|
+
* @param {Either<Bytes<32>, ContractAddress>} to - The recipient of the transferred assets.
|
|
334
|
+
* @param {Uint<128>} id - The unique identifier of the asset type.
|
|
335
|
+
* @param {Uint<128>} value - The quantity of `id` tokens to transfer.
|
|
336
|
+
* @return {[]} - Empty tuple.
|
|
337
|
+
*/
|
|
338
|
+
export circuit transferFrom(
|
|
339
|
+
fromAddress: Either<Bytes<32>, ContractAddress>,
|
|
340
|
+
to: Either<Bytes<32>, ContractAddress>,
|
|
341
|
+
id: Uint<128>,
|
|
342
|
+
value: Uint<128>
|
|
343
|
+
): [] {
|
|
344
|
+
const isContractAddr = !to.is_left;
|
|
345
|
+
assert(!isContractAddr, "MultiToken: unsafe transfer");
|
|
346
|
+
_unsafeTransferFrom(fromAddress, to, id, value);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* @description Transfers ownership of `value` amount of `id` tokens from `fromAddress` to `to`.
|
|
351
|
+
* Does not impose restrictions on the caller, making it suitable for composition
|
|
352
|
+
* in higher-level contract logic.
|
|
353
|
+
*
|
|
354
|
+
* @notice Transfers to contract addresses are currently disallowed until contract-to-contract
|
|
355
|
+
* interactions are supported in Compact. This restriction prevents assets from
|
|
356
|
+
* being inadvertently locked in contracts that cannot currently handle token receipt.
|
|
357
|
+
*
|
|
358
|
+
* @circuitInfo k=12, rows=2450
|
|
359
|
+
*
|
|
360
|
+
* Requirements:
|
|
361
|
+
*
|
|
362
|
+
* - Contract is initialized.
|
|
363
|
+
* - `to` is not a ContractAddress.
|
|
364
|
+
* - `to` is not zero.
|
|
365
|
+
* - `fromAddress` is not zero.
|
|
366
|
+
* - `fromAddress` must have an `id` balance of at least `value`.
|
|
367
|
+
*
|
|
368
|
+
* @param {Either<Bytes<32>, ContractAddress>} fromAddress - The owner from which the transfer originates.
|
|
369
|
+
* @param {Either<Bytes<32>, ContractAddress>} to - The recipient of the transferred assets.
|
|
370
|
+
* @param {Uint<128>} id - The unique identifier of the asset type.
|
|
371
|
+
* @param {Uint<128>} value - The quantity of `id` tokens to transfer.
|
|
372
|
+
* @return {[]} - Empty tuple.
|
|
373
|
+
*/
|
|
374
|
+
export circuit _transfer(
|
|
375
|
+
fromAddress: Either<Bytes<32>, ContractAddress>,
|
|
376
|
+
to: Either<Bytes<32>, ContractAddress>,
|
|
377
|
+
id: Uint<128>,
|
|
378
|
+
value: Uint<128>
|
|
379
|
+
): [] {
|
|
380
|
+
const isContractAddr = !to.is_left;
|
|
381
|
+
assert(!isContractAddr, "MultiToken: unsafe transfer");
|
|
382
|
+
_unsafeTransfer(fromAddress, to, id, value);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* @description Transfers a value amount of tokens of type id from fromAddress to to.
|
|
387
|
+
* This circuit will mint (or burn) if `fromAddress` (or `to`) is zero.
|
|
388
|
+
*
|
|
389
|
+
* @notice Both `fromAddress` and `to` are canonicalized internally before use as map keys.
|
|
390
|
+
*
|
|
391
|
+
* Requirements:
|
|
392
|
+
*
|
|
393
|
+
* - Contract is initialized.
|
|
394
|
+
* - If `fromAddress` is not zero, the balance of `id` of `fromAddress` must be >= `value`.
|
|
395
|
+
*
|
|
396
|
+
* @param {Either<Bytes<32>, ContractAddress>} fromAddress - The origin of the transfer.
|
|
397
|
+
* @param {Either<Bytes<32>, ContractAddress>} to - The destination of the transfer.
|
|
398
|
+
* @param {Uint<128>} id - The unique identifier of the asset type.
|
|
399
|
+
* @param {Uint<128>} value - The quantity of `id` tokens to transfer.
|
|
400
|
+
* @return {[]} - Empty tuple.
|
|
401
|
+
*/
|
|
402
|
+
circuit _update(fromAddress: Either<Bytes<32>, ContractAddress>,
|
|
403
|
+
to: Either<Bytes<32>, ContractAddress>,
|
|
404
|
+
id: Uint<128>,
|
|
405
|
+
value: Uint<128>
|
|
406
|
+
): [] {
|
|
407
|
+
Initializable_assertInitialized();
|
|
408
|
+
const canonFrom = Utils_canonicalize<Bytes<32>, ContractAddress>(fromAddress);
|
|
409
|
+
const canonTo = Utils_canonicalize<Bytes<32>, ContractAddress>(to);
|
|
410
|
+
|
|
411
|
+
if (!_isTargetZero(disclose(canonFrom))) {
|
|
412
|
+
const fromBalance = balanceOf(canonFrom, id);
|
|
413
|
+
assert(fromBalance >= value, "MultiToken: insufficient balance");
|
|
414
|
+
const newBalance = fromBalance - value;
|
|
415
|
+
_balances.lookup(id).insert(disclose(canonFrom), disclose(newBalance));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!_isTargetZero(disclose(canonTo))) {
|
|
419
|
+
if (!_balances.member(disclose(id))) {
|
|
420
|
+
_balances.insert(
|
|
421
|
+
disclose(id),
|
|
422
|
+
default<Map<Either<Bytes<32>, ContractAddress>, Uint<128>>>
|
|
423
|
+
);
|
|
424
|
+
_balances.lookup(id).insert(disclose(canonTo), disclose(value as Uint<128>));
|
|
425
|
+
} else {
|
|
426
|
+
const toBalance = balanceOf(canonTo, id);
|
|
427
|
+
const MAX_UINT128 = 340282366920938463463374607431768211455;
|
|
428
|
+
assert(MAX_UINT128 - toBalance >= value, "MultiToken: arithmetic overflow");
|
|
429
|
+
_balances.lookup(id).insert(disclose(canonTo), disclose(toBalance + value as Uint<128>));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* @description Unsafe variant of `transferFrom` which allows transfers to contract addresses.
|
|
436
|
+
* The caller must be `fromAddress` or approved to transfer on their behalf.
|
|
437
|
+
*
|
|
438
|
+
* In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_MultiTokenSK`
|
|
439
|
+
* witness as `persistentHash(secretKey)`.
|
|
440
|
+
*
|
|
441
|
+
* @warning Transfers to contract addresses are considered unsafe because contract-to-contract
|
|
442
|
+
* calls are not currently supported. Tokens sent to a contract address may become irretrievable.
|
|
443
|
+
* Once contract-to-contract calls are supported, this circuit may be deprecated.
|
|
444
|
+
*
|
|
445
|
+
* @circuitInfo k=13, rows=4925
|
|
446
|
+
*
|
|
447
|
+
* Requirements:
|
|
448
|
+
*
|
|
449
|
+
* - Contract is initialized.
|
|
450
|
+
* - `to` is not zero.
|
|
451
|
+
* - `fromAddress` is not zero.
|
|
452
|
+
* - Caller must be `fromAddress` or approved via `setApprovalForAll`.
|
|
453
|
+
* - `fromAddress` must have an `id` balance of at least `value`.
|
|
454
|
+
*
|
|
455
|
+
* @param {Either<Bytes<32>, ContractAddress>} fromAddress - The owner from which the transfer originates.
|
|
456
|
+
* @param {Either<Bytes<32>, ContractAddress>} to - The recipient of the transferred assets.
|
|
457
|
+
* @param {Uint<128>} id - The unique identifier of the asset type.
|
|
458
|
+
* @param {Uint<128>} value - The quantity of `id` tokens to transfer.
|
|
459
|
+
* @return {[]} - Empty tuple.
|
|
460
|
+
*/
|
|
461
|
+
export circuit _unsafeTransferFrom(
|
|
462
|
+
fromAddress: Either<Bytes<32>, ContractAddress>,
|
|
463
|
+
to: Either<Bytes<32>, ContractAddress>,
|
|
464
|
+
id: Uint<128>,
|
|
465
|
+
value: Uint<128>
|
|
466
|
+
): [] {
|
|
467
|
+
Initializable_assertInitialized();
|
|
468
|
+
|
|
469
|
+
const caller = left<Bytes<32>, ContractAddress>(_computeAccountId());
|
|
470
|
+
const canonFrom = Utils_canonicalize<Bytes<32>, ContractAddress>(fromAddress);
|
|
471
|
+
if (disclose(canonFrom) != disclose(caller)) {
|
|
472
|
+
assert(isApprovedForAll(canonFrom, caller), "MultiToken: unauthorized operator");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
_unsafeTransfer(canonFrom, to, id, value);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* @description Unsafe variant of `_transfer` which allows transfers to contract addresses.
|
|
480
|
+
* Does not impose restrictions on the caller, making it suitable as a low-level
|
|
481
|
+
* building block for advanced contract logic.
|
|
482
|
+
*
|
|
483
|
+
* @warning Transfers to contract addresses are considered unsafe because contract-to-contract
|
|
484
|
+
* calls are not currently supported. Tokens sent to a contract address may become irretrievable.
|
|
485
|
+
* Once contract-to-contract calls are supported, this circuit may be deprecated.
|
|
486
|
+
*
|
|
487
|
+
* @circuitInfo k=12, rows=2449
|
|
488
|
+
*
|
|
489
|
+
* Requirements:
|
|
490
|
+
*
|
|
491
|
+
* - Contract is initialized.
|
|
492
|
+
* - `fromAddress` is not zero.
|
|
493
|
+
* - `to` is not zero.
|
|
494
|
+
* - `fromAddress` must have an `id` balance of at least `value`.
|
|
495
|
+
*
|
|
496
|
+
* @param {Either<Bytes<32>, ContractAddress>} fromAddress - The owner from which the transfer originates.
|
|
497
|
+
* @param {Either<Bytes<32>, ContractAddress>} to - The recipient of the transferred assets.
|
|
498
|
+
* @param {Uint<128>} id - The unique identifier of the asset type.
|
|
499
|
+
* @param {Uint<128>} value - The quantity of `id` tokens to transfer.
|
|
500
|
+
* @return {[]} - Empty tuple.
|
|
501
|
+
*/
|
|
502
|
+
export circuit _unsafeTransfer(
|
|
503
|
+
fromAddress: Either<Bytes<32>, ContractAddress>,
|
|
504
|
+
to: Either<Bytes<32>, ContractAddress>,
|
|
505
|
+
id: Uint<128>,
|
|
506
|
+
value: Uint<128>
|
|
507
|
+
): [] {
|
|
508
|
+
Initializable_assertInitialized();
|
|
509
|
+
|
|
510
|
+
assert(!_isTargetZero(fromAddress), "MultiToken: invalid sender");
|
|
511
|
+
assert(!_isTargetZero(to), "MultiToken: invalid receiver");
|
|
512
|
+
_update(fromAddress, to, id, value);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* @description Sets a new URI for all token types, by relying on the token type ID
|
|
517
|
+
* substitution mechanism defined in the MultiToken standard.
|
|
518
|
+
* See https://eips.ethereum.org/EIPS/eip-1155#metadata.
|
|
519
|
+
*
|
|
520
|
+
* @notice By this mechanism, any occurrence of the `\{id\}` substring in either the
|
|
521
|
+
* URI or any of the values in the JSON file at said URI will be replaced by
|
|
522
|
+
* clients with the token type ID.
|
|
523
|
+
*
|
|
524
|
+
* For example, the `https://token-cdn-domain/\{id\}.json` URI would be
|
|
525
|
+
* interpreted by clients as
|
|
526
|
+
* `https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json`
|
|
527
|
+
* for token type ID 0x4cce0.
|
|
528
|
+
*
|
|
529
|
+
* @circuitInfo k=6, rows=28
|
|
530
|
+
*
|
|
531
|
+
* Requirements:
|
|
532
|
+
*
|
|
533
|
+
* - Contract is initialized.
|
|
534
|
+
*
|
|
535
|
+
* @param {Opaque<"string">} newURI - The new base URI for all tokens.
|
|
536
|
+
* @return {[]} - Empty tuple.
|
|
537
|
+
*/
|
|
538
|
+
export circuit _setURI(newURI: Opaque<"string">): [] {
|
|
539
|
+
Initializable_assertInitialized();
|
|
540
|
+
_uri = disclose(newURI);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* @description Creates a `value` amount of tokens of type `id`, and assigns them to `to`.
|
|
545
|
+
*
|
|
546
|
+
* @notice Transfers to contract addresses are currently disallowed until contract-to-contract
|
|
547
|
+
* interactions are supported in Compact. This restriction prevents assets from
|
|
548
|
+
* being inadvertently locked in contracts that cannot currently handle token receipt.
|
|
549
|
+
*
|
|
550
|
+
* @circuitInfo k=11, rows=1531
|
|
551
|
+
*
|
|
552
|
+
* Requirements:
|
|
553
|
+
*
|
|
554
|
+
* - Contract is initialized.
|
|
555
|
+
* - `to` is not zero.
|
|
556
|
+
* - `to` is not a ContractAddress.
|
|
557
|
+
*
|
|
558
|
+
* @param {Either<Bytes<32>, ContractAddress>} to - The recipient of the minted tokens.
|
|
559
|
+
* @param {Uint<128>} id - The unique identifier for the token type.
|
|
560
|
+
* @param {Uint<128>} value - The quantity of `id` tokens that are minted to `to`.
|
|
561
|
+
* @return {[]} - Empty tuple.
|
|
562
|
+
*/
|
|
563
|
+
export circuit _mint(to: Either<Bytes<32>, ContractAddress>,
|
|
564
|
+
id: Uint<128>,
|
|
565
|
+
value: Uint<128>
|
|
566
|
+
): [] {
|
|
567
|
+
const isContractAddr = !to.is_left;
|
|
568
|
+
assert(!isContractAddr, "MultiToken: unsafe transfer");
|
|
569
|
+
_unsafeMint(to, id, value);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* @description Unsafe variant of `_mint` which allows transfers to contract addresses.
|
|
574
|
+
*
|
|
575
|
+
* @warning Transfers to contract addresses are considered unsafe because contract-to-contract
|
|
576
|
+
* calls are not currently supported. Tokens sent to a contract address may become irretrievable.
|
|
577
|
+
* Once contract-to-contract calls are supported, this circuit may be deprecated.
|
|
578
|
+
*
|
|
579
|
+
* @circuitInfo k=11, rows=1530
|
|
580
|
+
*
|
|
581
|
+
* Requirements:
|
|
582
|
+
*
|
|
583
|
+
* - Contract is initialized.
|
|
584
|
+
* - `to` is not zero.
|
|
585
|
+
*
|
|
586
|
+
* @param {Either<Bytes<32>, ContractAddress>} to - The recipient of the minted tokens.
|
|
587
|
+
* @param {Uint<128>} id - The unique identifier for the token type.
|
|
588
|
+
* @param {Uint<128>} value - The quantity of `id` tokens that are minted to `to`.
|
|
589
|
+
* @return {[]} - Empty tuple.
|
|
590
|
+
*/
|
|
591
|
+
export circuit _unsafeMint(
|
|
592
|
+
to: Either<Bytes<32>, ContractAddress>,
|
|
593
|
+
id: Uint<128>,
|
|
594
|
+
value: Uint<128>
|
|
595
|
+
): [] {
|
|
596
|
+
Initializable_assertInitialized();
|
|
597
|
+
assert(!_isTargetZero(to), "MultiToken: invalid receiver");
|
|
598
|
+
_update(ZERO(), to, id, value);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* @description Destroys a `value` amount of tokens of type `id` from `fromAddress`.
|
|
603
|
+
*
|
|
604
|
+
* @circuitInfo k=11, rows=1222
|
|
605
|
+
*
|
|
606
|
+
* Requirements:
|
|
607
|
+
*
|
|
608
|
+
* - Contract is initialized.
|
|
609
|
+
* - `fromAddress` is not zero.
|
|
610
|
+
* - `fromAddress` must have an `id` balance of at least `value`.
|
|
611
|
+
*
|
|
612
|
+
* @param {Either<Bytes<32>, ContractAddress>} fromAddress - The owner whose tokens will be destroyed.
|
|
613
|
+
* @param {Uint<128>} id - The unique identifier of the token type.
|
|
614
|
+
* @param {Uint<128>} value - The quantity of `id` tokens that will be destroyed from `fromAddress`.
|
|
615
|
+
* @return {[]} - Empty tuple.
|
|
616
|
+
*/
|
|
617
|
+
export circuit _burn(fromAddress: Either<Bytes<32>, ContractAddress>,
|
|
618
|
+
id: Uint<128>,
|
|
619
|
+
value: Uint<128>
|
|
620
|
+
): [] {
|
|
621
|
+
Initializable_assertInitialized();
|
|
622
|
+
assert(!_isTargetZero(fromAddress), "MultiToken: invalid sender");
|
|
623
|
+
_update(fromAddress, ZERO(), id, value);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* @description Enables or disables approval for `operator` to manage all of `owner`'s assets.
|
|
628
|
+
*
|
|
629
|
+
* @notice This circuit does not check for access permissions but can be useful as a building block
|
|
630
|
+
* for more complex contract logic.
|
|
631
|
+
*
|
|
632
|
+
* @circuitInfo k=11, rows=1279
|
|
633
|
+
*
|
|
634
|
+
* Requirements:
|
|
635
|
+
*
|
|
636
|
+
* - Contract is initialized.
|
|
637
|
+
* - `owner` is not zero.
|
|
638
|
+
* - `operator` is not zero.
|
|
639
|
+
*
|
|
640
|
+
* @param {Either<Bytes<32>, ContractAddress>} owner - The account whose assets are managed.
|
|
641
|
+
* @param {Either<Bytes<32>, ContractAddress>} operator - The account whose approval is set for
|
|
642
|
+
* the `owner`'s assets.
|
|
643
|
+
* @param {Boolean} approved - The boolean value determining if the operator may or may not handle the
|
|
644
|
+
* `owner`'s assets.
|
|
645
|
+
* @return {[]} - Empty tuple.
|
|
646
|
+
*/
|
|
647
|
+
export circuit _setApprovalForAll(
|
|
648
|
+
owner: Either<Bytes<32>, ContractAddress>,
|
|
649
|
+
operator: Either<Bytes<32>, ContractAddress>,
|
|
650
|
+
approved: Boolean
|
|
651
|
+
): [] {
|
|
652
|
+
Initializable_assertInitialized();
|
|
653
|
+
const canonOwner = Utils_canonicalize<Bytes<32>, ContractAddress>(owner);
|
|
654
|
+
assert(!_isTargetZero(canonOwner), "MultiToken: invalid owner");
|
|
655
|
+
|
|
656
|
+
const canonOp = Utils_canonicalize<Bytes<32>, ContractAddress>(operator);
|
|
657
|
+
assert(!_isTargetZero(canonOp), "MultiToken: invalid operator");
|
|
658
|
+
|
|
659
|
+
if (!_operatorApprovals.member(disclose(canonOwner))) {
|
|
660
|
+
_operatorApprovals.insert(
|
|
661
|
+
disclose(canonOwner),
|
|
662
|
+
default<Map<Either<Bytes<32>, ContractAddress>, Boolean>>
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
_operatorApprovals.lookup(canonOwner).insert(disclose(canonOp), disclose(approved));
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* @description Computes the caller's account identifier from the `wit_MultiTokenSK` witness.
|
|
671
|
+
*
|
|
672
|
+
* ## ID Derivation
|
|
673
|
+
* `accountId = persistentHash(secretKey)`
|
|
674
|
+
*
|
|
675
|
+
* The result is a 32-byte commitment that uniquely identifies the caller.
|
|
676
|
+
*
|
|
677
|
+
* @returns {Bytes<32>} accountId - The computed account identifier.
|
|
678
|
+
*/
|
|
679
|
+
circuit _computeAccountId(): Bytes<32> {
|
|
680
|
+
return computeAccountId(wit_MultiTokenSK());
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* @description Computes an account identifier without on-chain state, allowing a user to derive
|
|
685
|
+
* their identity commitment before submitting it in a token operation.
|
|
686
|
+
* This is the off-chain counterpart to {_computeAccountId} and produces an identical result
|
|
687
|
+
* given the same inputs.
|
|
688
|
+
*
|
|
689
|
+
* @warning OpSec: The `secretKey` parameter is a sensitive secret. Mishandling it can
|
|
690
|
+
* permanently compromise the security of this system:
|
|
691
|
+
*
|
|
692
|
+
* - **Never log or persist** the `secretKey` in plaintext — avoid browser devtools,
|
|
693
|
+
* application logs, analytics pipelines, or any observable side-channel.
|
|
694
|
+
* - **Store offline or in secure enclaves** — hardware security modules (HSMs),
|
|
695
|
+
* air-gapped devices, or encrypted vaults are strongly preferred over hot storage.
|
|
696
|
+
* - **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()`
|
|
697
|
+
* or equivalent; weak or predictable keys can be brute-forced to reveal your identity.
|
|
698
|
+
* - **Treat key loss as identity loss** — a lost key cannot be recovered.
|
|
699
|
+
* - **Avoid calling this circuit in untrusted environments** — executing this in an
|
|
700
|
+
* unverified browser extension, compromised runtime, or shared machine may expose
|
|
701
|
+
* the key to a malicious observer.
|
|
702
|
+
*
|
|
703
|
+
* ## ID Derivation
|
|
704
|
+
* `accountId = persistentHash(secretKey)`
|
|
705
|
+
*
|
|
706
|
+
* @param {Bytes<32>} secretKey - A 32-byte cryptographically secure random value.
|
|
707
|
+
*
|
|
708
|
+
* @returns {Bytes<32>} accountId - The computed account identifier.
|
|
709
|
+
*/
|
|
710
|
+
export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> {
|
|
711
|
+
return persistentHash<Vector<1, Bytes<32>>>([secretKey]);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* @description Returns `true` if `target`'s active branch (as indicated by `is_left`)
|
|
716
|
+
* holds the zero value.
|
|
717
|
+
*
|
|
718
|
+
* @param {Either<Bytes<32>, ContractAddress>} target - The value to check.
|
|
719
|
+
* @returns {Boolean} - `true` if the active branch is zero, `false` otherwise.
|
|
720
|
+
*/
|
|
721
|
+
circuit _isTargetZero(target: Either<Bytes<32>, ContractAddress>): Boolean {
|
|
722
|
+
if (target.is_left) {
|
|
723
|
+
return target.left == default<Bytes<32>>;
|
|
724
|
+
} else {
|
|
725
|
+
return target.right == default<ContractAddress>;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|