@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,1180 @@
1
+ # Stablecoin Example
2
+
3
+ A production-ready stablecoin implementation with role-based access control, pausability, blacklist functionality, and minter allowances.
4
+
5
+ ## Overview
6
+
7
+ This example demonstrates:
8
+ - Role-based access control (Admin, Minter, Pauser, Blacklister)
9
+ - Pausable transfers
10
+ - Address blacklisting
11
+ - Minter allowances with supply caps
12
+ - Decorators for ABI generation
13
+ - Detailed event logging
14
+
15
+ ## Role-Based Access Control
16
+
17
+ Roles use bit flags (powers of 2) for efficient storage and checking:
18
+
19
+ ```mermaid
20
+ ---
21
+ config:
22
+ theme: dark
23
+ ---
24
+ graph LR
25
+ subgraph "Role Enum - Powers of 2"
26
+ A["Role.ADMIN = 1<br/>(2^0 = 0001)"]
27
+ B["Role.MINTER = 2<br/>(2^1 = 0010)"]
28
+ C["Role.PAUSER = 4<br/>(2^2 = 0100)"]
29
+ D["Role.BLACKLISTER = 8<br/>(2^3 = 1000)"]
30
+ end
31
+
32
+ subgraph "Example: Account with Admin + Minter"
33
+ E["Roles = 0011 (binary)<br/>= 1 | 2 = 3"]
34
+ end
35
+
36
+ subgraph "Bitwise Operations"
37
+ F["Grant Role: SafeMath.or(currentRoles, role)"]
38
+ G["Revoke Role: SafeMath.and(currentRoles, SafeMath.xor(role, u256.Max))"]
39
+ H["Check Role: !SafeMath.and(roles, role).isZero()"]
40
+ end
41
+
42
+ A --> E
43
+ B --> E
44
+ E --> F
45
+ E --> G
46
+ E --> H
47
+ ```
48
+
49
+ ### Role Implementation
50
+
51
+ ```typescript
52
+ // Define roles as enum with bit flags
53
+ enum Role {
54
+ ADMIN = 1, // 2^0
55
+ MINTER = 2, // 2^1
56
+ PAUSER = 4, // 2^2
57
+ BLACKLISTER = 8 // 2^3
58
+ }
59
+
60
+ // Check role before action
61
+ private onlyRole(role: u256): void {
62
+ if (!this.hasRole(Blockchain.tx.sender, role)) {
63
+ throw new Revert('AccessControl: missing role');
64
+ }
65
+ }
66
+
67
+ public hasRole(account: Address, role: u256): bool {
68
+ const roles = this._roles.get(account);
69
+ return !SafeMath.and(roles, role).isZero();
70
+ }
71
+ ```
72
+
73
+ **Solidity Comparison:**
74
+
75
+ ```solidity
76
+ // Solidity - OpenZeppelin AccessControl
77
+ bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
78
+
79
+ function hasRole(bytes32 role, address account) public view returns (bool) {
80
+ return _roles[role].members[account];
81
+ }
82
+
83
+ modifier onlyRole(bytes32 role) {
84
+ require(hasRole(role, msg.sender), "AccessControl: missing role");
85
+ _;
86
+ }
87
+
88
+ // OPNet - Bit flags for efficiency
89
+ enum Role { ADMIN = 1, MINTER = 2, PAUSER = 4, BLACKLISTER = 8 }
90
+
91
+ public hasRole(account: Address, role: u256): bool {
92
+ const roles = this._roles.get(account);
93
+ return !SafeMath.and(roles, role).isZero();
94
+ }
95
+ ```
96
+
97
+ ### Grant/Revoke Flow
98
+
99
+ ```mermaid
100
+ ---
101
+ config:
102
+ theme: dark
103
+ ---
104
+ flowchart LR
105
+ A["👤 User submits grantRole TX"] --> B{Has ADMIN role?}
106
+ B -->|No| C[Revert: Missing role]
107
+ B -->|Yes| D[Read current roles]
108
+ D --> E[OR roles together]
109
+ E --> F[Write to storage]
110
+ F --> G[Emit event]
111
+ G --> H[TX Success]
112
+ ```
113
+
114
+ ### Role Hierarchy
115
+
116
+ ```
117
+ Admin (Role.ADMIN = 1)
118
+ - Can grant/revoke all roles
119
+ - Can update master minter
120
+ - Has emergency powers
121
+
122
+ Master Minter
123
+ - Configure minter allowances
124
+ - Add new minters
125
+ - Remove minters
126
+
127
+ Minter (Role.MINTER = 2)
128
+ - Mint up to allowance
129
+ - Burn own tokens
130
+
131
+ Pauser (Role.PAUSER = 4)
132
+ - Pause all transfers
133
+ - Unpause
134
+
135
+ Blacklister (Role.BLACKLISTER = 8)
136
+ - Add addresses to blacklist
137
+ - Remove from blacklist
138
+ ```
139
+
140
+ ## Pausable Functionality
141
+
142
+ The contract can be paused to block all transfers:
143
+
144
+ ```mermaid
145
+ ---
146
+ config:
147
+ theme: dark
148
+ ---
149
+ stateDiagram-v2
150
+ [*] --> Normal: Deploy Contract
151
+ Normal --> Paused: pause() TX by Role.PAUSER
152
+ Paused --> Normal: unpause() TX by Role.PAUSER
153
+
154
+ state Normal {
155
+ [*] --> AllowTransfers
156
+ AllowTransfers --> AllowMinting
157
+ AllowMinting --> AllowBurning
158
+ }
159
+
160
+ state Paused {
161
+ [*] --> BlockTransfers
162
+ BlockTransfers --> BlockMinting
163
+ BlockMinting --> BlockBurning
164
+ }
165
+
166
+ note right of Normal
167
+ All token operations allowed
168
+ transfers, mints, burns work
169
+ Storage updates permitted
170
+ end note
171
+
172
+ note right of Paused
173
+ Only view functions work
174
+ All state-changing ops revert
175
+ Users must wait for unpause
176
+ end note
177
+ ```
178
+
179
+ ### Pausable Implementation
180
+
181
+ ```typescript
182
+ private whenNotPaused(): void {
183
+ if (this._paused.value) {
184
+ throw new Revert('Pausable: paused');
185
+ }
186
+ }
187
+
188
+ @method()
189
+ @emit('Paused')
190
+ public pause(_calldata: Calldata): BytesWriter {
191
+ this.onlyRole(u256.fromU64(Role.PAUSER));
192
+ this._paused.value = true;
193
+ this.emitEvent(new Paused(Blockchain.tx.sender));
194
+ return new BytesWriter(0);
195
+ }
196
+ ```
197
+
198
+ **Solidity Comparison:**
199
+
200
+ ```solidity
201
+ // Solidity - OpenZeppelin Pausable
202
+ modifier whenNotPaused() {
203
+ require(!paused(), "Pausable: paused");
204
+ _;
205
+ }
206
+
207
+ function pause() external onlyRole(PAUSER_ROLE) {
208
+ _pause();
209
+ }
210
+
211
+ // OPNet
212
+ private whenNotPaused(): void {
213
+ if (this._paused.value) {
214
+ throw new Revert('Pausable: paused');
215
+ }
216
+ }
217
+ ```
218
+
219
+ ## Blacklist System
220
+
221
+ Addresses can be blacklisted to prevent transfers. Each address exists in one of two states:
222
+
223
+ ```mermaid
224
+ ---
225
+ config:
226
+ theme: dark
227
+ ---
228
+ stateDiagram-v2
229
+ [*] --> Active: Address First Used
230
+ Active --> Blacklisted: blacklist(address) by BLACKLISTER
231
+ Blacklisted --> Active: unBlacklist(address) by BLACKLISTER
232
+
233
+ state Active {
234
+ [*] --> CanTransfer
235
+ CanTransfer --> CanReceive
236
+ CanReceive --> CanMint
237
+ }
238
+
239
+ state Blacklisted {
240
+ [*] --> TransferBlocked
241
+ TransferBlocked --> ReceiveBlocked
242
+ ReceiveBlocked --> MintBlocked
243
+ }
244
+
245
+ note right of Active
246
+ Address can:
247
+ - Send tokens
248
+ - Receive tokens
249
+ - Be minted to
250
+ end note
251
+
252
+ note right of Blacklisted
253
+ Address cannot:
254
+ - Send tokens (reverts)
255
+ - Receive tokens (reverts)
256
+ - Be minted to (reverts)
257
+ Balance is frozen
258
+ end note
259
+ ```
260
+
261
+ ```mermaid
262
+ ---
263
+ config:
264
+ theme: dark
265
+ ---
266
+ flowchart LR
267
+ A["👤 User submits transfer TX"] --> B{Contract paused?}
268
+ B -->|Yes| C[Revert: Paused]
269
+ B -->|No| D{Blacklisted?}
270
+ D -->|Yes| E[Revert: Blacklisted]
271
+ D -->|No| F{Sufficient balance?}
272
+ F -->|No| G[Revert: Insufficient balance]
273
+ F -->|Yes| H[Update balances]
274
+ H --> I[Emit event]
275
+ I --> J[TX Success]
276
+ ```
277
+
278
+ ### Blacklist Implementation
279
+
280
+ ```typescript
281
+ private notBlacklisted(account: Address): void {
282
+ // AddressMemoryMap.get() returns u256; non-zero means blacklisted
283
+ if (!this._blacklist.get(account).isZero()) {
284
+ throw new Revert('Blacklisted');
285
+ }
286
+ }
287
+
288
+ public override transfer(calldata: Calldata): BytesWriter {
289
+ this.whenNotPaused();
290
+ this.notBlacklisted(Blockchain.tx.sender);
291
+
292
+ const to = calldata.readAddress();
293
+ this.notBlacklisted(to);
294
+
295
+ // Continue with transfer...
296
+ }
297
+ ```
298
+
299
+ ## Minter Allowance System
300
+
301
+ Each minter has a limited supply they can mint. The master minter configures allowances for each minter:
302
+
303
+ ```mermaid
304
+ sequenceDiagram
305
+ participant MasterMinter as 👤 Master Minter
306
+ participant BTC as Bitcoin Network
307
+ participant Contract as Contract Execution
308
+ participant Minter as 👤 Minter
309
+ participant Storage as Storage Layer
310
+
311
+ MasterMinter->>BTC: Submit configureMinter(minter, allowance) TX
312
+ BTC->>Contract: Execute configureMinter
313
+ Contract->>Contract: Check caller is master minter
314
+ Contract->>Storage: Write Role.MINTER if needed
315
+ Contract->>Storage: Write minter allowance
316
+ Storage-->>Contract: Success
317
+ Contract-->>BTC: Success
318
+ BTC-->>MasterMinter: TX Confirmed
319
+
320
+ Note over Minter,Storage: Later: Minter wants to mint
321
+
322
+ Minter->>BTC: Submit mint(to, amount) TX
323
+ BTC->>Contract: Execute mint
324
+ Contract->>Contract: Check Role.MINTER
325
+ Contract->>Contract: Check not paused
326
+ Contract->>Contract: Check blacklist (to + minter)
327
+ Contract->>Storage: Read minter allowance
328
+ Storage-->>Contract: currentAllowance
329
+
330
+ alt amount > allowance
331
+ Contract-->>BTC: Revert: Allowance exceeded
332
+ BTC-->>Minter: TX Failed
333
+ else amount <= allowance
334
+ Contract->>Storage: Write allowance -= amount
335
+ Contract->>Storage: Write _mint(to, amount)
336
+ Contract->>Contract: Emit Mint event
337
+ Contract-->>BTC: Success
338
+ BTC-->>Minter: TX Confirmed
339
+ end
340
+
341
+ Note over Contract,Storage: Allowance tracks remaining mint capacity
342
+ ```
343
+
344
+ ### Minter Allowance Implementation
345
+
346
+ ```typescript
347
+ @method(
348
+ { name: 'minter', type: ABIDataTypes.ADDRESS },
349
+ { name: 'allowance', type: ABIDataTypes.UINT256 },
350
+ )
351
+ public configureMinter(calldata: Calldata): BytesWriter {
352
+ this.onlyMasterMinter();
353
+ const minter = calldata.readAddress();
354
+ const allowance = calldata.readU256(); // Max they can mint
355
+ // ...
356
+ }
357
+ ```
358
+
359
+ ## Complete Implementation
360
+
361
+ ```typescript
362
+ import { u256 } from '@btc-vision/as-bignum/assembly';
363
+ import {
364
+ OP20,
365
+ OP20InitParameters,
366
+ Blockchain,
367
+ Address,
368
+ Calldata,
369
+ BytesWriter,
370
+ SafeMath,
371
+ Revert,
372
+ NetEvent,
373
+ StoredBoolean,
374
+ StoredAddress,
375
+ AddressMemoryMap,
376
+ ABIDataTypes,
377
+ } from '@btc-vision/btc-runtime/runtime';
378
+
379
+ // Role enum - MUST be powers of 2 for bitwise operations
380
+ enum Role {
381
+ ADMIN = 1, // 2^0
382
+ MINTER = 2, // 2^1
383
+ PAUSER = 4, // 2^2
384
+ BLACKLISTER = 8 // 2^3
385
+ }
386
+
387
+ // Custom events
388
+ class RoleGranted extends NetEvent {
389
+ public constructor(
390
+ public readonly role: u256,
391
+ public readonly account: Address,
392
+ public readonly sender: Address
393
+ ) {
394
+ super('RoleGranted');
395
+ }
396
+
397
+ protected override encodeData(writer: BytesWriter): void {
398
+ writer.writeU256(this.role);
399
+ writer.writeAddress(this.account);
400
+ writer.writeAddress(this.sender);
401
+ }
402
+ }
403
+
404
+ class RoleRevoked extends NetEvent {
405
+ public constructor(
406
+ public readonly role: u256,
407
+ public readonly account: Address,
408
+ public readonly sender: Address
409
+ ) {
410
+ super('RoleRevoked');
411
+ }
412
+
413
+ protected override encodeData(writer: BytesWriter): void {
414
+ writer.writeU256(this.role);
415
+ writer.writeAddress(this.account);
416
+ writer.writeAddress(this.sender);
417
+ }
418
+ }
419
+
420
+ class Blacklisted extends NetEvent {
421
+ public constructor(public readonly account: Address) {
422
+ super('Blacklisted');
423
+ }
424
+
425
+ protected override encodeData(writer: BytesWriter): void {
426
+ writer.writeAddress(this.account);
427
+ }
428
+ }
429
+
430
+ class UnBlacklisted extends NetEvent {
431
+ public constructor(public readonly account: Address) {
432
+ super('UnBlacklisted');
433
+ }
434
+
435
+ protected override encodeData(writer: BytesWriter): void {
436
+ writer.writeAddress(this.account);
437
+ }
438
+ }
439
+
440
+ class Paused extends NetEvent {
441
+ public constructor(public readonly account: Address) {
442
+ super('Paused');
443
+ }
444
+
445
+ protected override encodeData(writer: BytesWriter): void {
446
+ writer.writeAddress(this.account);
447
+ }
448
+ }
449
+
450
+ class Unpaused extends NetEvent {
451
+ public constructor(public readonly account: Address) {
452
+ super('Unpaused');
453
+ }
454
+
455
+ protected override encodeData(writer: BytesWriter): void {
456
+ writer.writeAddress(this.account);
457
+ }
458
+ }
459
+
460
+ @final
461
+ export class Stablecoin extends OP20 {
462
+ // Access control storage
463
+ private rolesPointer: u16 = Blockchain.nextPointer;
464
+ private masterMinterPointer: u16 = Blockchain.nextPointer;
465
+
466
+ // Pausable storage
467
+ private pausedPointer: u16 = Blockchain.nextPointer;
468
+
469
+ // Blacklist storage
470
+ private blacklistPointer: u16 = Blockchain.nextPointer;
471
+
472
+ // Minter allowances
473
+ private minterAllowancePointer: u16 = Blockchain.nextPointer;
474
+
475
+ // Stored values
476
+ private _roles: AddressMemoryMap;
477
+ private _masterMinter: StoredAddress;
478
+ private _paused: StoredBoolean;
479
+ private _blacklist: AddressMemoryMap;
480
+ private _minterAllowance: AddressMemoryMap;
481
+
482
+ public constructor() {
483
+ super();
484
+
485
+ this._roles = new AddressMemoryMap(this.rolesPointer);
486
+ this._masterMinter = new StoredAddress(this.masterMinterPointer, Address.zero());
487
+ this._paused = new StoredBoolean(this.pausedPointer, false);
488
+ this._blacklist = new AddressMemoryMap(this.blacklistPointer);
489
+ this._minterAllowance = new AddressMemoryMap(this.minterAllowancePointer);
490
+ }
491
+
492
+ public override onDeployment(calldata: Calldata): void {
493
+ const name = calldata.readString();
494
+ const symbol = calldata.readString();
495
+ const admin = calldata.readAddress();
496
+ const masterMinter = calldata.readAddress();
497
+
498
+ // Initialize as stablecoin (no max supply, 6 decimals typical for USD)
499
+ this.instantiate(new OP20InitParameters(
500
+ u256.Max, // No max supply
501
+ 6, // USDC-style decimals
502
+ name,
503
+ symbol
504
+ ));
505
+
506
+ // Set up initial roles
507
+ this._grantRole(admin, u256.fromU64(Role.ADMIN));
508
+ this._grantRole(admin, u256.fromU64(Role.PAUSER));
509
+ this._grantRole(admin, u256.fromU64(Role.BLACKLISTER));
510
+ this._masterMinter.value = masterMinter;
511
+ }
512
+
513
+ // ============ MODIFIERS ============
514
+
515
+ private onlyRole(role: u256): void {
516
+ if (!this.hasRole(Blockchain.tx.sender, role)) {
517
+ throw new Revert('AccessControl: missing role');
518
+ }
519
+ }
520
+
521
+ private whenNotPaused(): void {
522
+ if (this._paused.value) {
523
+ throw new Revert('Pausable: paused');
524
+ }
525
+ }
526
+
527
+ private notBlacklisted(account: Address): void {
528
+ // AddressMemoryMap.get() returns u256; non-zero means blacklisted
529
+ if (!this._blacklist.get(account).isZero()) {
530
+ throw new Revert('Blacklisted');
531
+ }
532
+ }
533
+
534
+ private onlyMasterMinter(): void {
535
+ if (!Blockchain.tx.sender.equals(this._masterMinter.value)) {
536
+ throw new Revert('Caller is not master minter');
537
+ }
538
+ }
539
+
540
+ // ============ ROLE MANAGEMENT ============
541
+
542
+ private _grantRole(account: Address, role: u256): void {
543
+ const currentRoles = this._roles.get(account);
544
+ // Use SafeMath.or for bitwise OR on u256
545
+ const newRoles = SafeMath.or(currentRoles, role);
546
+ this._roles.set(account, newRoles);
547
+
548
+ this.emitEvent(new RoleGranted(role, account, Blockchain.tx.sender));
549
+ }
550
+
551
+ private _revokeRole(account: Address, role: u256): void {
552
+ const currentRoles = this._roles.get(account);
553
+ // Use SafeMath.xor to invert, then SafeMath.and to clear bits
554
+ const invertedRole = SafeMath.xor(role, u256.Max);
555
+ const newRoles = SafeMath.and(currentRoles, invertedRole);
556
+ this._roles.set(account, newRoles);
557
+
558
+ this.emitEvent(new RoleRevoked(role, account, Blockchain.tx.sender));
559
+ }
560
+
561
+ public hasRole(account: Address, role: u256): bool {
562
+ const roles = this._roles.get(account);
563
+ // Use SafeMath.and for bitwise AND on u256
564
+ return !SafeMath.and(roles, role).isZero();
565
+ }
566
+
567
+ @method(
568
+ { name: 'account', type: ABIDataTypes.ADDRESS },
569
+ { name: 'role', type: ABIDataTypes.UINT256 },
570
+ )
571
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
572
+ @emit('RoleGranted')
573
+ public grantRole(calldata: Calldata): BytesWriter {
574
+ this.onlyRole(u256.fromU64(Role.ADMIN));
575
+
576
+ const account = calldata.readAddress();
577
+ const role = calldata.readU256();
578
+
579
+ this._grantRole(account, role);
580
+
581
+ return new BytesWriter(0);
582
+ }
583
+
584
+ @method(
585
+ { name: 'account', type: ABIDataTypes.ADDRESS },
586
+ { name: 'role', type: ABIDataTypes.UINT256 },
587
+ )
588
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
589
+ @emit('RoleRevoked')
590
+ public revokeRole(calldata: Calldata): BytesWriter {
591
+ this.onlyRole(u256.fromU64(Role.ADMIN));
592
+
593
+ const account = calldata.readAddress();
594
+ const role = calldata.readU256();
595
+
596
+ this._revokeRole(account, role);
597
+
598
+ return new BytesWriter(0);
599
+ }
600
+
601
+ // ============ MINTING ============
602
+
603
+ @method(
604
+ { name: 'minter', type: ABIDataTypes.ADDRESS },
605
+ { name: 'allowance', type: ABIDataTypes.UINT256 },
606
+ )
607
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
608
+ @emit('MinterConfigured')
609
+ public configureMinter(calldata: Calldata): BytesWriter {
610
+ this.onlyMasterMinter();
611
+
612
+ const minter = calldata.readAddress();
613
+ const allowance = calldata.readU256();
614
+
615
+ // Grant minter role if new
616
+ if (!this.hasRole(minter, u256.fromU64(Role.MINTER))) {
617
+ this._grantRole(minter, u256.fromU64(Role.MINTER));
618
+ }
619
+
620
+ // Set allowance
621
+ this._minterAllowance.set(minter, allowance);
622
+
623
+ return new BytesWriter(0);
624
+ }
625
+
626
+ @method({ name: 'minter', type: ABIDataTypes.ADDRESS })
627
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
628
+ @emit('MinterRemoved')
629
+ public removeMinter(calldata: Calldata): BytesWriter {
630
+ this.onlyMasterMinter();
631
+
632
+ const minter = calldata.readAddress();
633
+
634
+ this._revokeRole(minter, u256.fromU64(Role.MINTER));
635
+ this._minterAllowance.set(minter, u256.Zero);
636
+
637
+ return new BytesWriter(0);
638
+ }
639
+
640
+ @method(
641
+ { name: 'to', type: ABIDataTypes.ADDRESS },
642
+ { name: 'amount', type: ABIDataTypes.UINT256 },
643
+ )
644
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
645
+ @emit('Mint')
646
+ public mint(calldata: Calldata): BytesWriter {
647
+ this.onlyRole(u256.fromU64(Role.MINTER));
648
+ this.whenNotPaused();
649
+
650
+ const to = calldata.readAddress();
651
+ const amount = calldata.readU256();
652
+ const minter = Blockchain.tx.sender;
653
+
654
+ // Check blacklist
655
+ this.notBlacklisted(to);
656
+ this.notBlacklisted(minter);
657
+
658
+ // Check and update allowance
659
+ const allowance = this._minterAllowance.get(minter);
660
+ if (allowance < amount) {
661
+ throw new Revert('Minter allowance exceeded');
662
+ }
663
+ this._minterAllowance.set(minter, SafeMath.sub(allowance, amount));
664
+
665
+ // Mint
666
+ this._mint(to, amount);
667
+
668
+ return new BytesWriter(0);
669
+ }
670
+
671
+ @method({ name: 'amount', type: ABIDataTypes.UINT256 })
672
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
673
+ @emit('Burn')
674
+ public burn(calldata: Calldata): BytesWriter {
675
+ this.onlyRole(u256.fromU64(Role.MINTER));
676
+ this.whenNotPaused();
677
+
678
+ const amount = calldata.readU256();
679
+ const burner = Blockchain.tx.sender;
680
+
681
+ this.notBlacklisted(burner);
682
+
683
+ this._burn(burner, amount);
684
+
685
+ return new BytesWriter(0);
686
+ }
687
+
688
+ // ============ PAUSABLE ============
689
+
690
+ @method()
691
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
692
+ @emit('Paused')
693
+ public pause(_calldata: Calldata): BytesWriter {
694
+ this.onlyRole(u256.fromU64(Role.PAUSER));
695
+
696
+ this._paused.value = true;
697
+ this.emitEvent(new Paused(Blockchain.tx.sender));
698
+
699
+ return new BytesWriter(0);
700
+ }
701
+
702
+ @method()
703
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
704
+ @emit('Unpaused')
705
+ public unpause(_calldata: Calldata): BytesWriter {
706
+ this.onlyRole(u256.fromU64(Role.PAUSER));
707
+
708
+ this._paused.value = false;
709
+ this.emitEvent(new Unpaused(Blockchain.tx.sender));
710
+
711
+ return new BytesWriter(0);
712
+ }
713
+
714
+ // ============ BLACKLIST ============
715
+
716
+ @method({ name: 'account', type: ABIDataTypes.ADDRESS })
717
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
718
+ @emit('Blacklisted')
719
+ public blacklist(calldata: Calldata): BytesWriter {
720
+ this.onlyRole(u256.fromU64(Role.BLACKLISTER));
721
+
722
+ const account = calldata.readAddress();
723
+ // AddressMemoryMap stores u256; use u256.One for true
724
+ this._blacklist.set(account, u256.One);
725
+
726
+ this.emitEvent(new Blacklisted(account));
727
+
728
+ return new BytesWriter(0);
729
+ }
730
+
731
+ @method({ name: 'account', type: ABIDataTypes.ADDRESS })
732
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
733
+ @emit('UnBlacklisted')
734
+ public unBlacklist(calldata: Calldata): BytesWriter {
735
+ this.onlyRole(u256.fromU64(Role.BLACKLISTER));
736
+
737
+ const account = calldata.readAddress();
738
+ // AddressMemoryMap stores u256; use u256.Zero for false
739
+ this._blacklist.set(account, u256.Zero);
740
+
741
+ this.emitEvent(new UnBlacklisted(account));
742
+
743
+ return new BytesWriter(0);
744
+ }
745
+
746
+ // ============ OVERRIDE TRANSFERS ============
747
+
748
+ @method(
749
+ { name: 'to', type: ABIDataTypes.ADDRESS },
750
+ { name: 'amount', type: ABIDataTypes.UINT256 },
751
+ )
752
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
753
+ @emit('Transfer')
754
+ public override transfer(calldata: Calldata): BytesWriter {
755
+ this.whenNotPaused();
756
+ this.notBlacklisted(Blockchain.tx.sender);
757
+
758
+ const to = calldata.readAddress();
759
+ this.notBlacklisted(to);
760
+
761
+ // Re-read to pass to parent
762
+ const fullCalldata = new Calldata(calldata.buffer);
763
+ return super.transfer(fullCalldata);
764
+ }
765
+
766
+ @method(
767
+ { name: 'from', type: ABIDataTypes.ADDRESS },
768
+ { name: 'to', type: ABIDataTypes.ADDRESS },
769
+ { name: 'amount', type: ABIDataTypes.UINT256 },
770
+ )
771
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
772
+ @emit('Transfer')
773
+ public override transferFrom(calldata: Calldata): BytesWriter {
774
+ this.whenNotPaused();
775
+
776
+ const from = calldata.readAddress();
777
+ const to = calldata.readAddress();
778
+
779
+ this.notBlacklisted(Blockchain.tx.sender);
780
+ this.notBlacklisted(from);
781
+ this.notBlacklisted(to);
782
+
783
+ // Re-read to pass to parent
784
+ const fullCalldata = new Calldata(calldata.buffer);
785
+ return super.transferFrom(fullCalldata);
786
+ }
787
+
788
+ // ============ VIEW FUNCTIONS ============
789
+
790
+ @method()
791
+ @returns({ name: 'paused', type: ABIDataTypes.BOOL })
792
+ public isPaused(_calldata: Calldata): BytesWriter {
793
+ const writer = new BytesWriter(1);
794
+ writer.writeBoolean(this._paused.value);
795
+ return writer;
796
+ }
797
+
798
+ @method({ name: 'account', type: ABIDataTypes.ADDRESS })
799
+ @returns({ name: 'blacklisted', type: ABIDataTypes.BOOL })
800
+ public isBlacklisted(calldata: Calldata): BytesWriter {
801
+ const account = calldata.readAddress();
802
+
803
+ const writer = new BytesWriter(1);
804
+ // AddressMemoryMap.get() returns u256; convert to boolean
805
+ writer.writeBoolean(!this._blacklist.get(account).isZero());
806
+ return writer;
807
+ }
808
+
809
+ @method(
810
+ { name: 'account', type: ABIDataTypes.ADDRESS },
811
+ { name: 'role', type: ABIDataTypes.UINT256 },
812
+ )
813
+ @returns({ name: 'hasRole', type: ABIDataTypes.BOOL })
814
+ public checkHasRole(calldata: Calldata): BytesWriter {
815
+ const account = calldata.readAddress();
816
+ const role = calldata.readU256();
817
+
818
+ const writer = new BytesWriter(1);
819
+ writer.writeBoolean(this.hasRole(account, role));
820
+ return writer;
821
+ }
822
+
823
+ @method({ name: 'minter', type: ABIDataTypes.ADDRESS })
824
+ @returns({ name: 'allowance', type: ABIDataTypes.UINT256 })
825
+ public minterAllowance(calldata: Calldata): BytesWriter {
826
+ const minter = calldata.readAddress();
827
+
828
+ const writer = new BytesWriter(32);
829
+ writer.writeU256(this._minterAllowance.get(minter));
830
+ return writer;
831
+ }
832
+
833
+ @method()
834
+ @returns({ name: 'masterMinter', type: ABIDataTypes.ADDRESS })
835
+ public getMasterMinter(_calldata: Calldata): BytesWriter {
836
+ const writer = new BytesWriter(32);
837
+ writer.writeAddress(this._masterMinter.value);
838
+ return writer;
839
+ }
840
+ }
841
+ ```
842
+
843
+ ## Key Patterns Summary
844
+
845
+ ### Bitwise Operations on u256
846
+
847
+ Use `SafeMath` methods for bitwise operations:
848
+
849
+ ```typescript
850
+ // Grant role (OR)
851
+ const newRoles = SafeMath.or(currentRoles, role);
852
+
853
+ // Revoke role (AND with inverted mask)
854
+ const invertedRole = SafeMath.xor(role, u256.Max);
855
+ const newRoles = SafeMath.and(currentRoles, invertedRole);
856
+
857
+ // Check role (AND)
858
+ const hasRole = !SafeMath.and(roles, role).isZero();
859
+ ```
860
+
861
+ ## Solidity Equivalent
862
+
863
+ For developers familiar with Solidity, here is the equivalent implementation using OpenZeppelin's ERC20Pausable and AccessControl:
864
+
865
+ ```solidity
866
+ // SPDX-License-Identifier: MIT
867
+ pragma solidity ^0.8.20;
868
+
869
+ import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
870
+ import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
871
+ import "@openzeppelin/contracts/access/AccessControl.sol";
872
+
873
+ contract Stablecoin is ERC20, ERC20Pausable, AccessControl {
874
+ bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
875
+ bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
876
+ bytes32 public constant BLACKLISTER_ROLE = keccak256("BLACKLISTER_ROLE");
877
+
878
+ address public masterMinter;
879
+ mapping(address => bool) public blacklisted;
880
+ mapping(address => uint256) public minterAllowance;
881
+
882
+ event Blacklisted(address indexed account);
883
+ event UnBlacklisted(address indexed account);
884
+ event MinterConfigured(address indexed minter, uint256 allowance);
885
+ event MinterRemoved(address indexed minter);
886
+
887
+ modifier notBlacklisted(address account) {
888
+ require(!blacklisted[account], "Blacklisted");
889
+ _;
890
+ }
891
+
892
+ constructor(
893
+ string memory name,
894
+ string memory symbol,
895
+ address admin,
896
+ address _masterMinter
897
+ ) ERC20(name, symbol) {
898
+ _grantRole(DEFAULT_ADMIN_ROLE, admin);
899
+ _grantRole(PAUSER_ROLE, admin);
900
+ _grantRole(BLACKLISTER_ROLE, admin);
901
+ masterMinter = _masterMinter;
902
+ }
903
+
904
+ function decimals() public pure override returns (uint8) {
905
+ return 6; // USDC-style
906
+ }
907
+
908
+ // ============ MINTING ============
909
+
910
+ function configureMinter(address minter, uint256 allowance) external {
911
+ require(msg.sender == masterMinter, "Caller is not master minter");
912
+ if (!hasRole(MINTER_ROLE, minter)) {
913
+ _grantRole(MINTER_ROLE, minter);
914
+ }
915
+ minterAllowance[minter] = allowance;
916
+ emit MinterConfigured(minter, allowance);
917
+ }
918
+
919
+ function removeMinter(address minter) external {
920
+ require(msg.sender == masterMinter, "Caller is not master minter");
921
+ _revokeRole(MINTER_ROLE, minter);
922
+ minterAllowance[minter] = 0;
923
+ emit MinterRemoved(minter);
924
+ }
925
+
926
+ function mint(address to, uint256 amount)
927
+ external
928
+ onlyRole(MINTER_ROLE)
929
+ whenNotPaused
930
+ notBlacklisted(msg.sender)
931
+ notBlacklisted(to)
932
+ {
933
+ require(minterAllowance[msg.sender] >= amount, "Minter allowance exceeded");
934
+ minterAllowance[msg.sender] -= amount;
935
+ _mint(to, amount);
936
+ }
937
+
938
+ function burn(uint256 amount)
939
+ external
940
+ onlyRole(MINTER_ROLE)
941
+ whenNotPaused
942
+ notBlacklisted(msg.sender)
943
+ {
944
+ _burn(msg.sender, amount);
945
+ }
946
+
947
+ // ============ PAUSABLE ============
948
+
949
+ function pause() external onlyRole(PAUSER_ROLE) {
950
+ _pause();
951
+ }
952
+
953
+ function unpause() external onlyRole(PAUSER_ROLE) {
954
+ _unpause();
955
+ }
956
+
957
+ // ============ BLACKLIST ============
958
+
959
+ function blacklist(address account) external onlyRole(BLACKLISTER_ROLE) {
960
+ blacklisted[account] = true;
961
+ emit Blacklisted(account);
962
+ }
963
+
964
+ function unBlacklist(address account) external onlyRole(BLACKLISTER_ROLE) {
965
+ blacklisted[account] = false;
966
+ emit UnBlacklisted(account);
967
+ }
968
+
969
+ // ============ TRANSFER OVERRIDES ============
970
+
971
+ function _update(address from, address to, uint256 value)
972
+ internal
973
+ override(ERC20, ERC20Pausable)
974
+ notBlacklisted(from)
975
+ notBlacklisted(to)
976
+ {
977
+ super._update(from, to, value);
978
+ }
979
+ }
980
+ ```
981
+
982
+ ## Solidity vs OPNet Comparison
983
+
984
+ ### Key Differences Table
985
+
986
+ | Aspect | Solidity (OpenZeppelin) | OPNet |
987
+ |--------|------------------------|-------|
988
+ | **Access Control** | `AccessControl` with `bytes32` role hashes | Bit flags in `u256` with enum |
989
+ | **Role Definition** | `keccak256("MINTER_ROLE")` | `enum Role { MINTER = 2 }` (powers of 2) |
990
+ | **Role Check** | `hasRole(MINTER_ROLE, account)` | `!SafeMath.and(roles, role).isZero()` |
991
+ | **Role Grant** | `_grantRole(role, account)` | `SafeMath.or(currentRoles, role)` |
992
+ | **Pausable** | `ERC20Pausable` extension | Manual `_paused: StoredBoolean` |
993
+ | **Modifiers** | `whenNotPaused`, `onlyRole()` | Inline function calls |
994
+ | **Blacklist** | `mapping(address => bool)` | `AddressMemoryMap` |
995
+ | **Multiple Inheritance** | `is ERC20, ERC20Pausable, AccessControl` | Single `extends OP20` |
996
+ | **Decimals** | Override `decimals()` function | Set in `OP20InitParameters` |
997
+
998
+ ### Role System Comparison
999
+
1000
+ **Solidity (OpenZeppelin AccessControl):**
1001
+ ```solidity
1002
+ // Roles as keccak256 hashes
1003
+ bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
1004
+ bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
1005
+
1006
+ // Each role is a separate mapping
1007
+ mapping(bytes32 role => mapping(address account => bool)) private _roles;
1008
+
1009
+ // Check role
1010
+ function hasRole(bytes32 role, address account) public view returns (bool) {
1011
+ return _roles[role][account];
1012
+ }
1013
+
1014
+ // Grant role
1015
+ function grantRole(bytes32 role, address account) public onlyRole(getRoleAdmin(role)) {
1016
+ _grantRole(role, account);
1017
+ }
1018
+ ```
1019
+
1020
+ **OPNet (Bit Flag System):**
1021
+ ```typescript
1022
+ // Roles as bit flags (powers of 2)
1023
+ enum Role {
1024
+ ADMIN = 1, // 0001
1025
+ MINTER = 2, // 0010
1026
+ PAUSER = 4, // 0100
1027
+ BLACKLISTER = 8 // 1000
1028
+ }
1029
+
1030
+ // All roles stored in single u256 per address
1031
+ private _roles: AddressMemoryMap; // address -> u256 (combined roles)
1032
+
1033
+ // Check role using bitwise AND
1034
+ public hasRole(account: Address, role: u256): bool {
1035
+ const roles = this._roles.get(account);
1036
+ return !SafeMath.and(roles, role).isZero();
1037
+ }
1038
+
1039
+ // Grant role using bitwise OR
1040
+ private _grantRole(account: Address, role: u256): void {
1041
+ const currentRoles = this._roles.get(account);
1042
+ const newRoles = SafeMath.or(currentRoles, role);
1043
+ this._roles.set(account, newRoles);
1044
+ }
1045
+ ```
1046
+
1047
+ ### Pausable Pattern Comparison
1048
+
1049
+ **Solidity (OpenZeppelin ERC20Pausable):**
1050
+ ```solidity
1051
+ import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
1052
+
1053
+ contract MyToken is ERC20Pausable {
1054
+ function pause() external onlyRole(PAUSER_ROLE) {
1055
+ _pause(); // Built-in from Pausable
1056
+ }
1057
+
1058
+ function unpause() external onlyRole(PAUSER_ROLE) {
1059
+ _unpause(); // Built-in from Pausable
1060
+ }
1061
+
1062
+ // Transfers automatically checked via _update override
1063
+ }
1064
+ ```
1065
+
1066
+ **OPNet (Manual Implementation):**
1067
+ ```typescript
1068
+ private _paused: StoredBoolean;
1069
+
1070
+ private whenNotPaused(): void {
1071
+ if (this._paused.value) {
1072
+ throw new Revert('Pausable: paused');
1073
+ }
1074
+ }
1075
+
1076
+ @method()
1077
+ @emit('Paused')
1078
+ public pause(_calldata: Calldata): BytesWriter {
1079
+ this.onlyRole(u256.fromU64(Role.PAUSER));
1080
+ this._paused.value = true;
1081
+ this.emitEvent(new Paused(Blockchain.tx.sender));
1082
+ return new BytesWriter(0);
1083
+ }
1084
+
1085
+ // Must manually call whenNotPaused() in each method
1086
+ public override transfer(calldata: Calldata): BytesWriter {
1087
+ this.whenNotPaused();
1088
+ // ... rest of transfer logic
1089
+ }
1090
+ ```
1091
+
1092
+ ### Blacklist Pattern Comparison
1093
+
1094
+ **Solidity:**
1095
+ ```solidity
1096
+ mapping(address => bool) public blacklisted;
1097
+
1098
+ modifier notBlacklisted(address account) {
1099
+ require(!blacklisted[account], "Blacklisted");
1100
+ _;
1101
+ }
1102
+
1103
+ function blacklist(address account) external onlyRole(BLACKLISTER_ROLE) {
1104
+ blacklisted[account] = true;
1105
+ emit Blacklisted(account);
1106
+ }
1107
+ ```
1108
+
1109
+ **OPNet:**
1110
+ ```typescript
1111
+ private _blacklist: AddressMemoryMap;
1112
+
1113
+ private notBlacklisted(account: Address): void {
1114
+ // AddressMemoryMap.get() returns u256; non-zero means blacklisted
1115
+ if (!this._blacklist.get(account).isZero()) {
1116
+ throw new Revert('Blacklisted');
1117
+ }
1118
+ }
1119
+
1120
+ @method({ name: 'account', type: ABIDataTypes.ADDRESS })
1121
+ @emit('Blacklisted')
1122
+ public blacklist(calldata: Calldata): BytesWriter {
1123
+ this.onlyRole(u256.fromU64(Role.BLACKLISTER));
1124
+ const account = calldata.readAddress();
1125
+ // AddressMemoryMap stores u256; use u256.One for true
1126
+ this._blacklist.set(account, u256.One);
1127
+ this.emitEvent(new Blacklisted(account));
1128
+ return new BytesWriter(0);
1129
+ }
1130
+ ```
1131
+
1132
+ ### Advantages of OPNet Approach
1133
+
1134
+ | Feature | Benefit |
1135
+ |---------|---------|
1136
+ | **Efficient Role Storage** | Single u256 per address stores all roles efficiently |
1137
+ | **No Role Admin Complexity** | Simpler role hierarchy without OpenZeppelin's role admin system |
1138
+ | **Explicit Control Flow** | Manual checks make security-critical code paths visible |
1139
+ | **Bitcoin Security** | Inherits Bitcoin's proven consensus and security model |
1140
+ | **No Diamond Problem** | Single inheritance avoids Solidity's multiple inheritance issues |
1141
+ | **Custom Minter Allowance** | Built-in per-minter supply caps (like USDC) |
1142
+
1143
+ ### Minter Allowance Pattern (USDC-style)
1144
+
1145
+ Both implementations support minter allowances, but OPNet makes this a first-class feature:
1146
+
1147
+ **Solidity:**
1148
+ ```solidity
1149
+ mapping(address => uint256) public minterAllowance;
1150
+
1151
+ function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
1152
+ require(minterAllowance[msg.sender] >= amount, "Minter allowance exceeded");
1153
+ minterAllowance[msg.sender] -= amount;
1154
+ _mint(to, amount);
1155
+ }
1156
+ ```
1157
+
1158
+ **OPNet:**
1159
+ ```typescript
1160
+ private _minterAllowance: AddressMemoryMap;
1161
+
1162
+ @method(...)
1163
+ public mint(calldata: Calldata): BytesWriter {
1164
+ this.onlyRole(u256.fromU64(Role.MINTER));
1165
+ // ... validation
1166
+ const allowance = this._minterAllowance.get(minter);
1167
+ if (allowance < amount) {
1168
+ throw new Revert('Minter allowance exceeded');
1169
+ }
1170
+ this._minterAllowance.set(minter, SafeMath.sub(allowance, amount));
1171
+ this._mint(to, amount);
1172
+ return new BytesWriter(0);
1173
+ }
1174
+ ```
1175
+
1176
+ ---
1177
+
1178
+ **Navigation:**
1179
+ - Previous: [NFT with Reservations](./nft-with-reservations.md)
1180
+ - Next: [Oracle Integration](./oracle-integration.md)