@btc-vision/btc-runtime 1.10.8 → 1.10.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +258 -137
  3. package/SECURITY.md +226 -0
  4. package/docs/README.md +614 -0
  5. package/docs/advanced/bitcoin-scripts.md +939 -0
  6. package/docs/advanced/cross-contract-calls.md +579 -0
  7. package/docs/advanced/plugins.md +1006 -0
  8. package/docs/advanced/quantum-resistance.md +660 -0
  9. package/docs/advanced/signature-verification.md +715 -0
  10. package/docs/api-reference/blockchain.md +729 -0
  11. package/docs/api-reference/events.md +642 -0
  12. package/docs/api-reference/op20.md +902 -0
  13. package/docs/api-reference/op721.md +819 -0
  14. package/docs/api-reference/safe-math.md +510 -0
  15. package/docs/api-reference/storage.md +840 -0
  16. package/docs/contracts/op-net-base.md +786 -0
  17. package/docs/contracts/op20-token.md +687 -0
  18. package/docs/contracts/op20s-signatures.md +614 -0
  19. package/docs/contracts/op721-nft.md +785 -0
  20. package/docs/contracts/reentrancy-guard.md +787 -0
  21. package/docs/core-concepts/blockchain-environment.md +724 -0
  22. package/docs/core-concepts/decorators.md +466 -0
  23. package/docs/core-concepts/events.md +652 -0
  24. package/docs/core-concepts/pointers.md +391 -0
  25. package/docs/core-concepts/security.md +473 -0
  26. package/docs/core-concepts/storage-system.md +969 -0
  27. package/docs/examples/basic-token.md +745 -0
  28. package/docs/examples/nft-with-reservations.md +1440 -0
  29. package/docs/examples/oracle-integration.md +1212 -0
  30. package/docs/examples/stablecoin.md +1180 -0
  31. package/docs/getting-started/first-contract.md +575 -0
  32. package/docs/getting-started/installation.md +384 -0
  33. package/docs/getting-started/project-structure.md +630 -0
  34. package/docs/storage/memory-maps.md +764 -0
  35. package/docs/storage/stored-arrays.md +778 -0
  36. package/docs/storage/stored-maps.md +758 -0
  37. package/docs/storage/stored-primitives.md +655 -0
  38. package/docs/types/address.md +773 -0
  39. package/docs/types/bytes-writer-reader.md +938 -0
  40. package/docs/types/calldata.md +744 -0
  41. package/docs/types/safe-math.md +446 -0
  42. package/package.json +52 -27
  43. package/runtime/memory/MapOfMap.ts +1 -0
  44. package/LICENSE.md +0 -21
@@ -0,0 +1,614 @@
1
+ # OP20S - Signature-Based Approvals
2
+
3
+ OP20S extends OP20 with signature-based approval mechanisms, enabling off-chain approvals and enhanced security through cryptographic signatures.
4
+
5
+ ## Overview
6
+
7
+ OP20S adds:
8
+ - **Permit-style approvals** - Approve via signature instead of transaction
9
+ - **Nonce management** - Replay attack protection
10
+ - **Deadline enforcement** - Time-limited signatures
11
+ - **ML-DSA support** - Quantum-resistant signatures
12
+
13
+ ```typescript
14
+ import {
15
+ OP20S,
16
+ OP20InitParameters,
17
+ Calldata,
18
+ BytesWriter,
19
+ ABIDataTypes,
20
+ } from '@btc-vision/btc-runtime/runtime';
21
+ import { u256 } from '@btc-vision/as-bignum/assembly';
22
+
23
+ @final
24
+ export class MyToken extends OP20S {
25
+ public constructor() {
26
+ super();
27
+ }
28
+
29
+ public override onDeployment(_calldata: Calldata): void {
30
+ this.instantiate(new OP20InitParameters(
31
+ u256.fromString('1000000000000000000000000'),
32
+ 18,
33
+ 'MyToken',
34
+ 'MTK'
35
+ ));
36
+ }
37
+ }
38
+ ```
39
+
40
+ ## ERC20Permit vs OP20S Comparison
41
+
42
+ | Feature | ERC20Permit (EIP-2612) | OP20S (OPNet) |
43
+ |---------|------------------------|---------------|
44
+ | Language | Solidity | AssemblyScript |
45
+ | Signature Type | ECDSA (v, r, s) | Schnorr or ML-DSA |
46
+ | Domain Separator | EIP-712 | EIP-712 style |
47
+ | Quantum Resistance | No | Yes (ML-DSA option) |
48
+ | Signature Parameter | Three params (v, r, s) | Single bytes param |
49
+ | Nonce Type | `uint256` | `u256` |
50
+
51
+ ## Why Signature-Based Approvals?
52
+
53
+ ### Traditional Approval Flow
54
+
55
+ ```
56
+ 1. User signs APPROVE transaction
57
+ 2. User submits APPROVE TX to blockchain
58
+ 3. Contract updates allowance
59
+ 4. User signs TRANSFER_FROM transaction (or protocol does)
60
+ 5. Transfer executes
61
+
62
+ Total: 2 transactions required from user
63
+ ```
64
+
65
+ ### Signature-Based Flow
66
+
67
+ ```
68
+ 1. User signs approval MESSAGE (off-chain, no TX needed)
69
+ 2. Protocol submits signature with action
70
+ 3. Contract verifies signature and executes
71
+
72
+ Total: 1 transaction, user signs off-chain only
73
+ ```
74
+
75
+ ## Permit Flow
76
+
77
+ The following diagram shows the high-level permit flow:
78
+
79
+ ```mermaid
80
+ ---
81
+ config:
82
+ theme: dark
83
+ ---
84
+ flowchart LR
85
+ A[👤 User signs off-chain] --> B[Send signature to relayer]
86
+ B --> C[Relayer submits permit TX]
87
+ C --> D{Deadline valid?}
88
+ D -->|No| E[Revert]
89
+ D -->|Yes| F{Nonce correct?}
90
+ F -->|No| G[Revert]
91
+ F -->|Yes| H{Signature valid?}
92
+ H -->|No| I[Revert]
93
+ H -->|Yes| J[Increment nonce]
94
+ J --> K[Set allowance]
95
+ K --> L[Emit ApprovalEvent]
96
+ ```
97
+
98
+ ## Detailed Permit Sequence
99
+
100
+ The following sequence diagram shows the complete permit process from off-chain signing to on-chain execution:
101
+
102
+ ```mermaid
103
+ sequenceDiagram
104
+ participant User as 👤 User Wallet<br/>(Has Private Key)
105
+ participant OffChain as Off-Chain Signer<br/>(Browser/App)
106
+ participant Relayer as Relayer/Protocol<br/>(Submits TX)
107
+ participant Blockchain as Bitcoin L1
108
+ participant VM as WASM Runtime
109
+ participant OP20S as OP20S Contract
110
+ participant Storage as Storage Pointers
111
+ participant NonceMap as nonces Map<br/>(Prevents Replay)
112
+ participant AllowanceMap as allowance Map<br/>(Pointer 6)
113
+ participant DomainSep as Domain Separator<br/>(EIP-712)
114
+ participant EventLog as Event Log
115
+
116
+ Note over User,OffChain: Phase 1: Off-Chain Signing (No TX needed)
117
+
118
+ User->>OffChain: Request permit for spender
119
+ OffChain->>OP20S: Query nonce(owner) view call
120
+ Note over OffChain,OP20S: Read-only view call
121
+
122
+ OP20S->>NonceMap: get(owner)
123
+ NonceMap->>Storage: Read current nonce
124
+ Storage-->>NonceMap: nonce value
125
+ NonceMap-->>OP20S: currentNonce
126
+ OP20S-->>OffChain: Return nonce
127
+
128
+ OffChain->>DomainSep: Get DOMAIN_SEPARATOR()
129
+ DomainSep-->>OffChain: Domain hash
130
+ Note over DomainSep: Includes: name, version,<br/>chainId, contract address
131
+
132
+ OffChain->>OffChain: Build permit struct
133
+ Note over OffChain: owner, spender, value,<br/>nonce, deadline
134
+
135
+ OffChain->>OffChain: Hash permit data
136
+ Note over OffChain: permitHash = keccak256(<br/>abi.encode(typeHash, permit))
137
+
138
+ OffChain->>OffChain: Build EIP-712 message
139
+ Note over OffChain: message = keccak256(<br/>0x1901 + domainSep + permitHash)
140
+
141
+ User->>OffChain: Sign message with private key
142
+ Note over User: Schnorr or ML-DSA signature
143
+
144
+ OffChain-->>User: signature bytes
145
+
146
+ Note over User,Blockchain: Phase 2: On-Chain Submission (Relayer Pays)
147
+
148
+ User->>Relayer: Send signature + permit params
149
+ Note over User,Relayer: User sends NO on-chain TX
150
+
151
+ Relayer->>Blockchain: Submit permit() transaction
152
+ Note over Relayer: Relayer submits TX to chain
153
+
154
+ Blockchain->>VM: Execute transaction
155
+ VM->>OP20S: permit(owner, spender, value, deadline, sig)
156
+ activate OP20S
157
+
158
+ OP20S->>OP20S: Read calldata parameters
159
+
160
+ OP20S->>OP20S: Check Blockchain.block.medianTime
161
+
162
+ alt Deadline passed
163
+ OP20S->>VM: Revert('Permit expired')
164
+ VM->>Relayer: Transaction failed
165
+ Relayer->>User: Permit expired
166
+ else Valid deadline
167
+ OP20S->>NonceMap: get(owner)
168
+ NonceMap->>Storage: Read expected nonce
169
+ Storage-->>NonceMap: expectedNonce
170
+ NonceMap-->>OP20S: nonce value
171
+
172
+ OP20S->>DomainSep: getDomainSeparator()
173
+ DomainSep-->>OP20S: domain hash
174
+
175
+ OP20S->>OP20S: buildPermitMessage()
176
+ Note over OP20S: Reconstruct same message<br/>user signed off-chain
177
+
178
+ OP20S->>OP20S: verifySignature(owner, message, sig)
179
+ Note over OP20S: Recover signer from signature
180
+
181
+ alt Signature invalid OR signer != owner
182
+ OP20S->>VM: Revert('Invalid signature')
183
+ VM->>Relayer: Transaction failed
184
+ else Signature valid
185
+ OP20S->>OP20S: SafeMath.add(nonce, 1)
186
+ OP20S->>NonceMap: set(owner, nonce + 1)
187
+ NonceMap->>Storage: Write incremented nonce
188
+ Note over Storage: Replay protection:<br/>Old signatures now invalid
189
+
190
+ OP20S->>OP20S: _approve(owner, spender, value)
191
+
192
+ OP20S->>AllowanceMap: set(owner->spender, value)
193
+ AllowanceMap->>Storage: Write allowance
194
+ Note over Storage: Approval stored
195
+
196
+ OP20S->>OP20S: Create ApprovalEvent
197
+ OP20S->>EventLog: emit ApprovalEvent(owner, spender, value)
198
+
199
+ OP20S->>VM: Return success
200
+ deactivate OP20S
201
+
202
+ VM->>Blockchain: Commit state changes
203
+ Blockchain->>Relayer: Transaction success
204
+ Relayer->>User: Permit approved!
205
+ Note over User: Approved without<br/>submitting any TX!
206
+ end
207
+ end
208
+ ```
209
+
210
+ ## Message Construction
211
+
212
+ The following diagram shows how the permit message is constructed for signing:
213
+
214
+ ```mermaid
215
+ ---
216
+ config:
217
+ theme: dark
218
+ ---
219
+ flowchart LR
220
+ A[Domain separator] --> B[Permit struct]
221
+ B --> C[Combine with EIP-712 prefix]
222
+ C --> D[Hash message]
223
+ D --> E[👤 User signs with private key]
224
+ E --> F[Submit signature on-chain]
225
+ F --> G[Verify signature]
226
+ G --> H{Valid?}
227
+ H -->|No| I[Revert]
228
+ H -->|Yes| J[Process permit]
229
+ ```
230
+
231
+ ## Nonce Management
232
+
233
+ Each address has a nonce that increments with each signature use to prevent replay attacks:
234
+
235
+ ```mermaid
236
+ stateDiagram-v2
237
+ [*] --> NeverUsed: 👤 User account created
238
+
239
+ state NeverUsed {
240
+ [*] --> Nonce0
241
+ }
242
+
243
+ NeverUsed --> FirstPermit: permit() with nonce=0
244
+
245
+ state FirstPermit {
246
+ Nonce0 --> Nonce1: Signature verified<br/>Nonce incremented
247
+ }
248
+
249
+ FirstPermit --> SubsequentPermits
250
+
251
+ state SubsequentPermits {
252
+ Nonce1 --> Nonce2: permit() nonce=1
253
+ Nonce2 --> Nonce3: permit() nonce=2
254
+ Nonce3 --> NoncePlus: permit() nonce=3
255
+ NoncePlus --> NoncePlus: More permits
256
+ }
257
+
258
+ state "Replay Attack Blocked" as Blocked {
259
+ [*] --> AttemptOldNonce
260
+ AttemptOldNonce --> Reverted: Old signature submitted
261
+ }
262
+
263
+ SubsequentPermits --> Blocked: Try to reuse old signature
264
+ Blocked --> SubsequentPermits: Continue with correct nonce
265
+ ```
266
+
267
+ ## Permit Method
268
+
269
+ ### Usage
270
+
271
+ ```typescript
272
+ // Off-chain: User signs permit data
273
+ const permitData = {
274
+ owner: userAddress,
275
+ spender: protocolAddress,
276
+ value: amount,
277
+ nonce: await contract.nonces(userAddress),
278
+ deadline: Math.floor(Date.now() / 1000) + 3600 // 1 hour
279
+ };
280
+
281
+ const signature = signPermit(permitData, userPrivateKey);
282
+
283
+ // On-chain: Protocol submits permit
284
+ contract.permit(owner, spender, value, deadline, signature);
285
+ ```
286
+
287
+ ### Parameters
288
+
289
+ | Parameter | Type | Description |
290
+ |-----------|------|-------------|
291
+ | `owner` | `Address` | Token owner granting approval |
292
+ | `spender` | `Address` | Address being approved |
293
+ | `value` | `u256` | Amount to approve |
294
+ | `deadline` | `u64` | Signature expiration timestamp |
295
+ | `signature` | `bytes` | Cryptographic signature |
296
+
297
+ ### Signature Verification
298
+
299
+ The permit verifies:
300
+ 1. **Signature validity** - Matches owner's public key
301
+ 2. **Deadline** - Current time <= deadline
302
+ 3. **Nonce** - Matches expected nonce for owner
303
+ 4. **Domain** - Correct contract and chain
304
+
305
+ ## Nonces
306
+
307
+ ```typescript
308
+ // Get current nonce for address
309
+ const nonce: u256 = contract.nonces(address);
310
+
311
+ // Nonce auto-increments after successful permit
312
+ // This prevents signature replay
313
+ ```
314
+
315
+ ### Replay Protection
316
+
317
+ ```typescript
318
+ // Signature for nonce 0 used
319
+ contract.permit(..., nonce=0, signature0); // Success, nonce now 1
320
+
321
+ // Same signature replayed
322
+ contract.permit(..., nonce=0, signature0); // FAILS - nonce mismatch
323
+ ```
324
+
325
+ ## Solidity Comparison (EIP-2612)
326
+
327
+ <table>
328
+ <tr>
329
+ <th>ERC20Permit (Solidity)</th>
330
+ <th>OP20S (OPNet)</th>
331
+ </tr>
332
+ <tr>
333
+ <td>
334
+
335
+ ```solidity
336
+ contract MyToken is ERC20Permit {
337
+ constructor()
338
+ ERC20("MyToken", "MTK")
339
+ ERC20Permit("MyToken")
340
+ { }
341
+ }
342
+
343
+ // Usage
344
+ token.permit(
345
+ owner,
346
+ spender,
347
+ value,
348
+ deadline,
349
+ v, r, s // ECDSA signature components
350
+ );
351
+ ```
352
+
353
+ </td>
354
+ <td>
355
+
356
+ ```typescript
357
+ @final
358
+ export class MyToken extends OP20S {
359
+ constructor() {
360
+ super();
361
+ }
362
+
363
+ public override onDeployment(_: Calldata): void {
364
+ this.instantiate(new OP20InitParameters(
365
+ maxSupply, 18, 'MyToken', 'MTK'
366
+ ));
367
+ }
368
+ }
369
+
370
+ // Usage
371
+ token.permit(
372
+ owner,
373
+ spender,
374
+ value,
375
+ deadline,
376
+ signature // Schnorr or ML-DSA
377
+ );
378
+ ```
379
+
380
+ </td>
381
+ </tr>
382
+ </table>
383
+
384
+ ## Domain Separator
385
+
386
+ OP20S uses EIP-712 style domain separation:
387
+
388
+ ```typescript
389
+ // Domain includes:
390
+ // - Contract name
391
+ // - Contract version
392
+ // - Chain ID
393
+ // - Contract address
394
+
395
+ // This prevents cross-chain and cross-contract replay
396
+ ```
397
+
398
+ ## Quantum Resistance
399
+
400
+ OP20S supports ML-DSA (Dilithium) signatures for quantum resistance:
401
+
402
+ ```typescript
403
+ // Traditional Schnorr signature
404
+ contract.permit(owner, spender, value, deadline, schnorrSignature);
405
+
406
+ // ML-DSA quantum-resistant signature
407
+ contract.permitQuantum(owner, spender, value, deadline, mldsaSignature);
408
+ ```
409
+
410
+ ### Extended Address
411
+
412
+ For quantum-safe operations, use `ExtendedAddress`:
413
+
414
+ ```typescript
415
+ import { ExtendedAddress } from '@btc-vision/btc-runtime/runtime';
416
+
417
+ // Extended address with both key types
418
+ const extAddress = new ExtendedAddress(
419
+ traditionalPubKey, // Schnorr
420
+ mldsaPubKey // ML-DSA
421
+ );
422
+ ```
423
+
424
+ See [Quantum Resistance](../advanced/quantum-resistance.md) for details.
425
+
426
+ ## Implementation Details
427
+
428
+ ### Permit Verification
429
+
430
+ ```typescript
431
+ @method(
432
+ { name: 'owner', type: ABIDataTypes.ADDRESS },
433
+ { name: 'spender', type: ABIDataTypes.ADDRESS },
434
+ { name: 'value', type: ABIDataTypes.UINT256 },
435
+ { name: 'deadline', type: ABIDataTypes.UINT64 },
436
+ { name: 'signature', type: ABIDataTypes.BYTES },
437
+ )
438
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
439
+ @emit('Approval')
440
+ public permit(calldata: Calldata): BytesWriter {
441
+ const owner = calldata.readAddress();
442
+ const spender = calldata.readAddress();
443
+ const value = calldata.readU256();
444
+ const deadline = calldata.readU64();
445
+ const signature = calldata.readBytes();
446
+
447
+ // Check deadline
448
+ if (Blockchain.block.medianTime > deadline) {
449
+ throw new Revert('Permit expired');
450
+ }
451
+
452
+ // Get and increment nonce
453
+ const nonce = this.nonces.get(owner);
454
+ this.nonces.set(owner, SafeMath.add(nonce, u256.One));
455
+
456
+ // Verify signature
457
+ const message = this.buildPermitMessage(owner, spender, value, nonce, deadline);
458
+ if (!this.verifySignature(owner, message, signature)) {
459
+ throw new Revert('Invalid signature');
460
+ }
461
+
462
+ // Set approval
463
+ this._approve(owner, spender, value);
464
+
465
+ return new BytesWriter(0);
466
+ }
467
+ ```
468
+
469
+ ### Message Format
470
+
471
+ ```typescript
472
+ // Permit message structure
473
+ struct Permit {
474
+ address owner;
475
+ address spender;
476
+ uint256 value;
477
+ uint256 nonce;
478
+ uint256 deadline;
479
+ }
480
+
481
+ // Hashed with domain separator
482
+ messageHash = SHA256(domainSeparator || SHA256(permitTypeHash || encode(permit)))
483
+ ```
484
+
485
+ ## Additional Methods
486
+
487
+ OP20S adds these methods to OP20:
488
+
489
+ | Method | Description |
490
+ |--------|-------------|
491
+ | `permit(...)` | Approve via signature |
492
+ | `nonces(address)` | Get nonce for address |
493
+ | `DOMAIN_SEPARATOR()` | Get domain separator |
494
+
495
+ ## Use Cases
496
+
497
+ ### 1. Off-Chain Approvals
498
+
499
+ ```typescript
500
+ // User signs permit off-chain
501
+ const sig = await user.signPermit(spender, amount, deadline);
502
+
503
+ // Protocol submits on user's behalf
504
+ await contract.permit(user, spender, amount, deadline, sig);
505
+ await contract.transferFrom(user, recipient, amount);
506
+
507
+ // User submitted no on-chain TX!
508
+ ```
509
+
510
+ ### 2. Single-Transaction Approve+Transfer
511
+
512
+ ```typescript
513
+ // Protocol contract
514
+ @method(
515
+ { name: 'owner', type: ABIDataTypes.ADDRESS },
516
+ { name: 'amount', type: ABIDataTypes.UINT256 },
517
+ { name: 'deadline', type: ABIDataTypes.UINT64 },
518
+ { name: 'signature', type: ABIDataTypes.BYTES },
519
+ )
520
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
521
+ @emit('Deposit')
522
+ public depositWithPermit(calldata: Calldata): BytesWriter {
523
+ // Read permit params
524
+ const owner = calldata.readAddress();
525
+ const amount = calldata.readU256();
526
+ const deadline = calldata.readU64();
527
+ const signature = calldata.readBytes();
528
+
529
+ // Execute permit
530
+ token.permit(owner, this.address, amount, deadline, signature);
531
+
532
+ // Now transfer in same transaction
533
+ token.transferFrom(owner, this.address, amount);
534
+
535
+ return new BytesWriter(0);
536
+ }
537
+ ```
538
+
539
+ ### 3. Meta-Transactions
540
+
541
+ ```typescript
542
+ // Relayer submits on behalf of user
543
+ @method(
544
+ { name: 'user', type: ABIDataTypes.ADDRESS },
545
+ { name: 'permitSig', type: ABIDataTypes.BYTES },
546
+ { name: 'actionSig', type: ABIDataTypes.BYTES },
547
+ )
548
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
549
+ public executeMetaTx(calldata: Calldata): BytesWriter {
550
+ const user = calldata.readAddress();
551
+ const permitSig = calldata.readBytes();
552
+ const actionSig = calldata.readBytes();
553
+
554
+ // Verify permit
555
+ this.permit(user, ...permitSig);
556
+
557
+ // Execute action
558
+ this.executeAction(user, actionSig);
559
+
560
+ return new BytesWriter(0);
561
+ }
562
+ ```
563
+
564
+ ## Security Considerations
565
+
566
+ ### Deadline Selection
567
+
568
+ ```typescript
569
+ // Too short: User might not complete in time
570
+ const deadline = now + 60; // 1 minute - risky
571
+
572
+ // Reasonable: Enough time, limited exposure
573
+ const deadline = now + 3600; // 1 hour - good
574
+
575
+ // Too long: Extended attack window
576
+ const deadline = now + 86400 * 365; // 1 year - bad
577
+ ```
578
+
579
+ ### Signature Storage
580
+
581
+ ```typescript
582
+ // NEVER store signatures on-chain unnecessarily
583
+ // They become public and could be analyzed
584
+
585
+ // DO process and discard
586
+ const sig = calldata.readBytes();
587
+ verifyAndProcess(sig);
588
+ // sig is gone after transaction
589
+ ```
590
+
591
+ ### Front-Running Protection
592
+
593
+ ```typescript
594
+ // Permit followed by transfer in same tx = safe
595
+ permit(owner, spender, value, deadline, sig);
596
+ transferFrom(owner, recipient, value); // Same tx
597
+
598
+ // Separate transactions = front-running risk
599
+ // Attacker could see permit and race to use allowance
600
+ ```
601
+
602
+ ## Best Practices
603
+
604
+ 1. **Set reasonable deadlines** - Not too short, not too long
605
+ 2. **Process permits atomically** - Permit + action in same transaction
606
+ 3. **Monitor nonces** - Track expected nonces off-chain
607
+ 4. **Verify domains** - Ensure signatures are for correct contract
608
+ 5. **Consider quantum safety** - Use ML-DSA for high-value applications
609
+
610
+ ---
611
+
612
+ **Navigation:**
613
+ - Previous: [OP20 Token](./op20-token.md)
614
+ - Next: [OP721 NFT](./op721-nft.md)