@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,787 @@
1
+ # ReentrancyGuard
2
+
3
+ ReentrancyGuard protects your contracts from reentrancy attacks, one of the most common smart contract vulnerabilities. It prevents a contract from being called back into itself before the first call completes.
4
+
5
+ ## Overview
6
+
7
+ ```typescript
8
+ import {
9
+ ReentrancyGuard,
10
+ ReentrancyLevel,
11
+ } from '@btc-vision/btc-runtime/runtime';
12
+
13
+ @final
14
+ export class MyContract extends ReentrancyGuard {
15
+ protected readonly reentrancyLevel: ReentrancyLevel = ReentrancyLevel.STANDARD;
16
+
17
+ public constructor() {
18
+ super();
19
+ }
20
+
21
+ @method()
22
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
23
+ public withdraw(calldata: Calldata): BytesWriter {
24
+ // Protected automatically by ReentrancyGuard
25
+ const amount = this.balances.get(Blockchain.tx.sender);
26
+ this.balances.set(Blockchain.tx.sender, u256.Zero);
27
+ this.sendFunds(Blockchain.tx.sender, amount);
28
+
29
+ return new BytesWriter(0);
30
+ }
31
+ }
32
+ ```
33
+
34
+ ## OpenZeppelin vs OPNet ReentrancyGuard
35
+
36
+ | Feature | OpenZeppelin (Solidity) | OPNet ReentrancyGuard |
37
+ |---------|-------------------------|----------------------|
38
+ | Protection Scope | Per-function (`nonReentrant` modifier) | All methods by default |
39
+ | Opt-in/Opt-out | Opt-in per function | Opt-out via `isSelectorExcluded` |
40
+ | Lock Type | Boolean lock | Boolean lock (STANDARD) / Depth counter (CALLBACK) |
41
+ | Callback Support | No (always blocks) | No (both modes block reentry) |
42
+ | Storage | Persistent storage | Persistent storage |
43
+
44
+ ## What is Reentrancy?
45
+
46
+ ### The Attack
47
+
48
+ ```typescript
49
+ // Vulnerable contract
50
+ public withdraw(): void {
51
+ const balance = balances.get(sender);
52
+
53
+ // 1. External call BEFORE state update
54
+ sendFunds(sender, balance);
55
+ // Attacker's receive function calls withdraw() again
56
+ // balance is still the original amount!
57
+
58
+ // 2. State update happens too late
59
+ balances.set(sender, u256.Zero);
60
+ }
61
+ ```
62
+
63
+ Attack flow:
64
+ ```
65
+ 1. Attacker calls withdraw()
66
+ 2. Contract sends funds to attacker
67
+ 3. Attacker's receive function calls withdraw() again
68
+ 4. Balance hasn't been updated yet, so attacker withdraws again
69
+ 5. Repeat until contract is drained
70
+ ```
71
+
72
+ ### The Defense
73
+
74
+ ReentrancyGuard prevents this by locking the contract during execution:
75
+
76
+ ```typescript
77
+ // Protected contract
78
+ public withdraw(): void {
79
+ // ReentrancyGuard: Check and set lock
80
+ // If already locked, transaction reverts
81
+
82
+ const balance = balances.get(sender);
83
+ balances.set(sender, u256.Zero); // State update
84
+ sendFunds(sender, balance); // External call
85
+
86
+ // ReentrancyGuard: Release lock
87
+ }
88
+ ```
89
+
90
+ ## Guard Mechanism
91
+
92
+ The following diagram shows how the guard checks and manages reentrancy depth:
93
+
94
+ ```mermaid
95
+ ---
96
+ config:
97
+ theme: dark
98
+ ---
99
+ flowchart LR
100
+ A[👤 User submits TX] --> B[onExecutionStarted]
101
+ B --> C{Check lock/depth}
102
+ C -->|STANDARD: locked| D[Revert]
103
+ C -->|CALLBACK: depth >= 1| E[Revert]
104
+ C -->|Valid| F[Set lock/Increment depth]
105
+ F --> G[Execute method]
106
+ G --> H{External call?}
107
+ H -->|Yes| I{Callback attempt?}
108
+ I -->|Yes| C
109
+ I -->|No| J[Complete]
110
+ H -->|No| J
111
+ J --> K[onExecutionCompleted]
112
+ K --> L[Decrement depth]
113
+ ```
114
+
115
+ ## Vulnerable Contract Attack
116
+
117
+ The following sequence diagram shows how a reentrancy attack works against an unprotected contract:
118
+
119
+ ```mermaid
120
+ sequenceDiagram
121
+ participant Attacker as Attacker Wallet
122
+ participant Blockchain as Bitcoin L1
123
+ participant VM as WASM Runtime
124
+ participant Vulnerable as Vulnerable Contract<br/>(NO ReentrancyGuard)
125
+ participant Storage as Storage Pointers
126
+ participant MaliciousContract as Attacker's Contract<br/>(Malicious Receiver)
127
+
128
+ Note over Vulnerable: VULNERABLE: No reentrancy protection
129
+
130
+ Attacker->>Blockchain: Submit withdraw() TX
131
+ Blockchain->>VM: Execute transaction
132
+ VM->>Vulnerable: Call withdraw()
133
+ activate Vulnerable
134
+
135
+ Vulnerable->>Storage: Read balances[attacker]
136
+ Storage-->>Vulnerable: balance = 100 BTC
137
+ Note over Vulnerable: Step 1: Read balance
138
+
139
+ Note over Vulnerable: CRITICAL ERROR:<br/>External call BEFORE state update!
140
+
141
+ Vulnerable->>MaliciousContract: send(100 BTC)
142
+ activate MaliciousContract
143
+ Note over MaliciousContract: Receive callback triggered
144
+
145
+ MaliciousContract->>Blockchain: Call withdraw() AGAIN
146
+ Blockchain->>VM: Execute nested call
147
+ VM->>Vulnerable: withdraw() RE-ENTRY
148
+ activate Vulnerable
149
+
150
+ Vulnerable->>Storage: Read balances[attacker]
151
+ Storage-->>Vulnerable: balance = 100 BTC
152
+ Note over Vulnerable: STILL 100!<br/>Not updated yet!
153
+
154
+ Vulnerable->>MaliciousContract: send(100 BTC) AGAIN
155
+ Note over MaliciousContract: Received 100 BTC (2nd time)
156
+
157
+ Vulnerable->>Storage: balances[attacker] = 0
158
+ Note over Storage: Update too late (nested call)
159
+
160
+ deactivate Vulnerable
161
+
162
+ Note over MaliciousContract: Attack successful!<br/>Received 200 BTC total
163
+
164
+ deactivate MaliciousContract
165
+
166
+ Vulnerable->>Storage: balances[attacker] = 0
167
+ Note over Storage: Update happens (original call)<br/>but damage already done!
168
+
169
+ Vulnerable->>VM: Return success
170
+ deactivate Vulnerable
171
+
172
+ VM->>Blockchain: Commit state
173
+ Blockchain->>Attacker: Transaction complete
174
+ Note over Attacker: Stole 100 BTC<br/>by exploiting reentrancy!
175
+ ```
176
+
177
+ ## Protected Contract Defense
178
+
179
+ The following sequence diagram shows how ReentrancyGuard blocks the same attack:
180
+
181
+ ```mermaid
182
+ sequenceDiagram
183
+ participant Attacker as Attacker Wallet
184
+ participant Blockchain as Bitcoin L1
185
+ participant VM as WASM Runtime
186
+ participant Protected as Protected Contract<br/>(WITH ReentrancyGuard)
187
+ participant Guard as ReentrancyGuard<br/>Persistent Storage
188
+ participant Storage as Storage Pointers
189
+ participant MaliciousContract as Attacker's Contract
190
+
191
+ Note over Protected: PROTECTED: ReentrancyLevel.STANDARD
192
+
193
+ Attacker->>Blockchain: Submit withdraw() TX
194
+ Blockchain->>VM: Execute transaction
195
+ VM->>Protected: Call withdraw()
196
+ activate Protected
197
+
198
+ Protected->>Protected: onExecutionStarted hook
199
+ Protected->>Guard: nonReentrantBefore()
200
+ Guard->>Guard: Read _locked
201
+ Note over Guard: _locked = false (unlocked)
202
+
203
+ Guard->>Guard: Set _locked = true
204
+ Note over Guard: LOCK ACQUIRED
205
+
206
+ Protected->>Storage: Read balances[attacker]
207
+ Storage-->>Protected: balance = 100 BTC
208
+
209
+ Protected->>Storage: balances[attacker] = 0
210
+ Note over Storage: STATE UPDATED FIRST!<br/>Checks-Effects-Interactions pattern
211
+
212
+ Protected->>MaliciousContract: send(100 BTC)
213
+ activate MaliciousContract
214
+ Note over MaliciousContract: Receive callback triggered
215
+
216
+ MaliciousContract->>Blockchain: Call withdraw() AGAIN
217
+ Blockchain->>VM: Execute nested call attempt
218
+ VM->>Protected: withdraw() RE-ENTRY ATTEMPT
219
+ activate Protected
220
+
221
+ Protected->>Protected: onExecutionStarted hook
222
+ Protected->>Guard: nonReentrantBefore()
223
+ Guard->>Guard: Read _locked
224
+ Note over Guard: _locked = true (LOCKED!)
225
+
226
+ alt Lock check fails
227
+ Guard->>Protected: REVERT('ReentrancyGuard: LOCKED')
228
+ Protected->>VM: Revert transaction
229
+ deactivate Protected
230
+ VM->>MaliciousContract: Call reverted
231
+ Note over MaliciousContract: ATTACK BLOCKED!<br/>No funds stolen
232
+ end
233
+
234
+ deactivate MaliciousContract
235
+
236
+ Protected->>Protected: Continue execution
237
+ Protected->>Protected: onExecutionCompleted hook
238
+ Protected->>Guard: nonReentrantAfter()
239
+ Guard->>Guard: Set _locked = false
240
+ Note over Guard: LOCK RELEASED
241
+
242
+ Protected->>VM: Return success
243
+ deactivate Protected
244
+
245
+ VM->>Blockchain: Commit state changes
246
+ Note over Storage: Only 1 withdrawal processed
247
+
248
+ Blockchain->>Attacker: Transaction success
249
+ Note over Attacker: Received only 100 BTC<br/>Attack prevented!
250
+ ```
251
+
252
+ ## Choosing a Guard Mode
253
+
254
+ Use this decision diagram to select the appropriate reentrancy level:
255
+
256
+ ```mermaid
257
+ ---
258
+ config:
259
+ theme: dark
260
+ ---
261
+ flowchart LR
262
+ A{External calls?} -->|No| B[No guard needed]
263
+ A -->|Yes| C{Need depth tracking?}
264
+ C -->|No| D[Use STANDARD mode]
265
+ C -->|Yes| E[Use CALLBACK mode]
266
+ ```
267
+
268
+ Note: Both modes block reentrancy. STANDARD uses a boolean lock; CALLBACK uses a depth counter.
269
+
270
+ ## Guard Modes
271
+
272
+ The following state diagram shows how the reentrancy lock transitions between states:
273
+
274
+ ```mermaid
275
+ ---
276
+ config:
277
+ theme: dark
278
+ ---
279
+ stateDiagram-v2
280
+ [*] --> Unlocked: Contract idle
281
+
282
+ state "STANDARD Mode" as Standard {
283
+ Unlocked --> Locked: onExecutionStarted
284
+ Locked --> Unlocked: onExecutionCompleted
285
+ Locked --> Reverted: Reentry attempt
286
+ Reverted --> [*]: Transaction fails
287
+ }
288
+
289
+ state "CALLBACK Mode" as Callback {
290
+ [*] --> Depth0
291
+ Depth0 --> Depth1: First call
292
+ Depth1 --> Reverted2: Any reentry attempt
293
+ Reverted2 --> [*]: Transaction fails
294
+ Depth1 --> Depth0: First call completes
295
+ }
296
+ ```
297
+
298
+ ### STANDARD Mode
299
+
300
+ Strict mutual exclusion - no re-entry allowed at all.
301
+
302
+ ```typescript
303
+ @final
304
+ export class SecureVault extends ReentrancyGuard {
305
+ protected readonly reentrancyLevel: ReentrancyLevel = ReentrancyLevel.STANDARD;
306
+
307
+ public constructor() {
308
+ super();
309
+ }
310
+
311
+ @method({ name: 'amount', type: ABIDataTypes.UINT256 })
312
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
313
+ public deposit(calldata: Calldata): BytesWriter {
314
+ // Cannot be re-entered
315
+ // ...
316
+ }
317
+
318
+ @method()
319
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
320
+ public withdraw(calldata: Calldata): BytesWriter {
321
+ // Cannot be re-entered
322
+ // deposit() also blocked while this runs
323
+ // ...
324
+ }
325
+ }
326
+ ```
327
+
328
+ **Use STANDARD when:**
329
+ - Handling funds/assets
330
+ - Complex multi-step operations
331
+ - Any operation where re-entry could cause issues
332
+
333
+ ### CALLBACK Mode
334
+
335
+ Uses depth tracking instead of a simple boolean lock. Currently configured to reject any reentry (depth >= 1 triggers revert).
336
+
337
+ ```typescript
338
+ @final
339
+ export class TokenWithCallbacks extends ReentrancyGuard {
340
+ protected readonly reentrancyLevel: ReentrancyLevel = ReentrancyLevel.CALLBACK;
341
+
342
+ public constructor() {
343
+ super();
344
+ }
345
+
346
+ @method(
347
+ { name: 'from', type: ABIDataTypes.ADDRESS },
348
+ { name: 'to', type: ABIDataTypes.ADDRESS },
349
+ { name: 'tokenId', type: ABIDataTypes.UINT256 },
350
+ )
351
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
352
+ @emit('Transfer')
353
+ public safeTransfer(calldata: Calldata): BytesWriter {
354
+ // Transfer token
355
+ this._transfer(from, to, tokenId);
356
+
357
+ // Notify receiver (might call back)
358
+ this.onTokenReceived(to, from, tokenId);
359
+ // Note: With current implementation, any reentry is rejected
360
+
361
+ return new BytesWriter(0);
362
+ }
363
+ }
364
+ ```
365
+
366
+ **Use CALLBACK when:**
367
+ - You need depth-based tracking instead of a simple boolean lock
368
+ - You want differentiated error messages (Max depth exceeded vs LOCKED)
369
+ - Note: Current implementation rejects any reentry at depth >= 1
370
+
371
+ ## How It Works
372
+
373
+ ### Internal State
374
+
375
+ The reentrancy guard uses a boolean lock and depth counter stored in storage:
376
+
377
+ ```mermaid
378
+ ---
379
+ config:
380
+ theme: dark
381
+ ---
382
+ stateDiagram-v2
383
+ [*] --> depth_0: Transaction starts
384
+
385
+ depth_0 --> depth_1: Method entry (lock acquired)
386
+
387
+ state depth_1 {
388
+ [*] --> Executing
389
+ Executing --> ExternalCall: Call other contract
390
+ ExternalCall --> CallbackCheck: Contract calls back
391
+ }
392
+
393
+ depth_1 --> depth_0: Method exit (lock released)
394
+ depth_1 --> Revert: Method exit (error)
395
+
396
+ state "Callback Handling" as CallbackHandling {
397
+ CallbackCheck --> Blocked: Both modes block reentry
398
+ Blocked --> Revert
399
+ }
400
+
401
+ depth_0 --> [*]: Transaction ends
402
+ Revert --> [*]: Transaction reverted
403
+ ```
404
+
405
+ ```typescript
406
+ // ReentrancyGuard uses storage for the lock state
407
+ // _locked: StoredBoolean - tracks if guard is engaged
408
+ // _reentrancyDepth: StoredU256 - tracks call depth in CALLBACK mode
409
+
410
+ // The ReentrancyLevel enum (defined in ReentrancyGuard.ts):
411
+ enum ReentrancyLevel {
412
+ STANDARD = 0, // Strict single entry, uses boolean lock
413
+ CALLBACK = 1 // Uses depth counter (still blocks reentrancy at depth >= 1)
414
+ }
415
+ ```
416
+
417
+ ### STANDARD Mode Logic
418
+
419
+ ```typescript
420
+ // On method entry (nonReentrantBefore):
421
+ if (this._locked.value) {
422
+ throw new Revert('ReentrancyGuard: LOCKED');
423
+ }
424
+ this._locked.value = true;
425
+
426
+ // ... execute method ...
427
+
428
+ // On method exit (nonReentrantAfter):
429
+ this._locked.value = false;
430
+ ```
431
+
432
+ ### CALLBACK Mode Logic
433
+
434
+ ```typescript
435
+ // On method entry (nonReentrantBefore):
436
+ const currentDepth = this._reentrancyDepth.value;
437
+
438
+ // Maximum depth of 1 (original call only, rejects any callback reentry)
439
+ if (currentDepth >= u256.One) {
440
+ throw new Revert('ReentrancyGuard: Max depth exceeded');
441
+ }
442
+
443
+ this._reentrancyDepth.value = SafeMath.add(currentDepth, u256.One);
444
+
445
+ // Use locked flag for first entry
446
+ if (currentDepth.isZero()) {
447
+ this._locked.value = true;
448
+ }
449
+
450
+ // On method exit (nonReentrantAfter):
451
+ const currentDepth = this._reentrancyDepth.value;
452
+ if (currentDepth.isZero()) {
453
+ throw new Revert('ReentrancyGuard: Depth underflow');
454
+ }
455
+
456
+ const newDepth = SafeMath.sub(currentDepth, u256.One);
457
+ this._reentrancyDepth.value = newDepth;
458
+
459
+ // Clear locked flag when fully exited
460
+ if (newDepth.isZero()) {
461
+ this._locked.value = false;
462
+ }
463
+ ```
464
+
465
+ ## Usage Patterns
466
+
467
+ ### Basic Protection
468
+
469
+ ```typescript
470
+ @final
471
+ export class ProtectedContract extends ReentrancyGuard {
472
+ protected readonly reentrancyLevel: ReentrancyLevel = ReentrancyLevel.STANDARD;
473
+
474
+ public constructor() {
475
+ super();
476
+ }
477
+
478
+ @method()
479
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
480
+ public sensitiveOperation(calldata: Calldata): BytesWriter {
481
+ // All public methods are automatically protected
482
+ // No additional code needed
483
+ return new BytesWriter(0);
484
+ }
485
+ }
486
+ ```
487
+
488
+ ### Combined with Other Bases
489
+
490
+ ```typescript
491
+ // ReentrancyGuard with OP20
492
+ @final
493
+ export class SecureToken extends OP20 {
494
+ // OP20 doesn't extend ReentrancyGuard
495
+ // You need to implement protection manually
496
+
497
+ private locked: bool = false;
498
+
499
+ private nonReentrant(): void {
500
+ if (this.locked) {
501
+ throw new Revert('Reentrant call');
502
+ }
503
+ this.locked = true;
504
+ }
505
+
506
+ private releaseGuard(): void {
507
+ this.locked = false;
508
+ }
509
+
510
+ @method()
511
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
512
+ public customWithdraw(calldata: Calldata): BytesWriter {
513
+ this.nonReentrant();
514
+ try {
515
+ // ... operation ...
516
+ return new BytesWriter(0);
517
+ } finally {
518
+ this.releaseGuard();
519
+ }
520
+ }
521
+ }
522
+ ```
523
+
524
+ ### Excluded Methods
525
+
526
+ The base `ReentrancyGuard` automatically excludes standard token receiver callbacks from reentrancy checks:
527
+
528
+ ```typescript
529
+ // Built-in exclusions in ReentrancyGuard base class:
530
+ // - ON_OP20_RECEIVED_SELECTOR
531
+ // - ON_OP721_RECEIVED_SELECTOR
532
+ // - ON_OP1155_RECEIVED_MAGIC
533
+ // - ON_OP1155_BATCH_RECEIVED_MAGIC
534
+ ```
535
+
536
+ You can override `isSelectorExcluded` to add custom exclusions:
537
+
538
+ ```typescript
539
+ @final
540
+ export class MyContract extends ReentrancyGuard {
541
+ protected readonly reentrancyLevel: ReentrancyLevel = ReentrancyLevel.STANDARD;
542
+
543
+ public constructor() {
544
+ super();
545
+ }
546
+
547
+ // Override to exclude specific selectors
548
+ protected override isSelectorExcluded(selector: Selector): boolean {
549
+ // Define selectors for view functions
550
+ const BALANCE_OF_SELECTOR: u32 = encodeSelector('balanceOf');
551
+ const TOTAL_SUPPLY_SELECTOR: u32 = encodeSelector('totalSupply');
552
+
553
+ // View functions don't need protection
554
+ if (selector === BALANCE_OF_SELECTOR) return true;
555
+ if (selector === TOTAL_SUPPLY_SELECTOR) return true;
556
+
557
+ return super.isSelectorExcluded(selector);
558
+ }
559
+ }
560
+ ```
561
+
562
+ ## Solidity Comparison
563
+
564
+ <table>
565
+ <tr>
566
+ <th>OpenZeppelin ReentrancyGuard</th>
567
+ <th>OPNet ReentrancyGuard</th>
568
+ </tr>
569
+ <tr>
570
+ <td>
571
+
572
+ ```solidity
573
+ import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
574
+
575
+ contract MyContract is ReentrancyGuard {
576
+ function withdraw() external nonReentrant {
577
+ // Protected
578
+ }
579
+
580
+ function deposit() external {
581
+ // NOT protected (no modifier)
582
+ }
583
+ }
584
+ ```
585
+
586
+ </td>
587
+ <td>
588
+
589
+ ```typescript
590
+ @final
591
+ export class MyContract extends ReentrancyGuard {
592
+ protected readonly reentrancyLevel: ReentrancyLevel = ReentrancyLevel.STANDARD;
593
+
594
+ public constructor() {
595
+ super();
596
+ }
597
+
598
+ @method()
599
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
600
+ public withdraw(calldata: Calldata): BytesWriter {
601
+ // Protected automatically
602
+ }
603
+
604
+ @method({ name: 'amount', type: ABIDataTypes.UINT256 })
605
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
606
+ public deposit(calldata: Calldata): BytesWriter {
607
+ // Also protected automatically
608
+ }
609
+ }
610
+ ```
611
+
612
+ </td>
613
+ </tr>
614
+ </table>
615
+
616
+ Key differences:
617
+ - Solidity: Explicit `nonReentrant` modifier per function
618
+ - OPNet: All methods protected by default (opt-out via `isSelectorExcluded`)
619
+
620
+ ## Best Practices
621
+
622
+ ### 1. Use STANDARD Mode by Default
623
+
624
+ ```typescript
625
+ // Default to strictest protection
626
+ protected readonly reentrancyLevel: ReentrancyLevel = ReentrancyLevel.STANDARD;
627
+
628
+ // Only use CALLBACK when specifically needed
629
+ protected readonly reentrancyLevel: ReentrancyLevel = ReentrancyLevel.CALLBACK;
630
+ ```
631
+
632
+ ### 2. Follow Checks-Effects-Interactions Pattern
633
+
634
+ Even with ReentrancyGuard, use this pattern:
635
+
636
+ ```typescript
637
+ @method({ name: 'amount', type: ABIDataTypes.UINT256 })
638
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
639
+ public withdraw(calldata: Calldata): BytesWriter {
640
+ const amount = calldata.readU256();
641
+
642
+ // 1. CHECKS - Validate inputs
643
+ if (amount.isZero()) {
644
+ throw new Revert('Amount is zero');
645
+ }
646
+
647
+ const balance = this.balances.get(Blockchain.tx.sender);
648
+ if (balance < amount) {
649
+ throw new Revert('Insufficient balance');
650
+ }
651
+
652
+ // 2. EFFECTS - Update state
653
+ this.balances.set(Blockchain.tx.sender, SafeMath.sub(balance, amount));
654
+
655
+ // 3. INTERACTIONS - External calls last
656
+ this.sendFunds(Blockchain.tx.sender, amount);
657
+
658
+ return new BytesWriter(0);
659
+ }
660
+ ```
661
+
662
+ ### 3. Protect All State-Changing Functions
663
+
664
+ ```typescript
665
+ // View functions can be excluded
666
+ protected override isSelectorExcluded(selector: Selector): boolean {
667
+ // Define selectors for read-only functions
668
+ const BALANCE_OF_SELECTOR: u32 = encodeSelector('balanceOf');
669
+ const NAME_SELECTOR: u32 = encodeSelector('name');
670
+ const SYMBOL_SELECTOR: u32 = encodeSelector('symbol');
671
+
672
+ // Only exclude read-only functions
673
+ if (selector === BALANCE_OF_SELECTOR) return true;
674
+ if (selector === NAME_SELECTOR) return true;
675
+ if (selector === SYMBOL_SELECTOR) return true;
676
+
677
+ // All state-changing functions stay protected
678
+ return false;
679
+ }
680
+ ```
681
+
682
+ ### 4. Be Careful with External Calls
683
+
684
+ ```typescript
685
+ // Both STANDARD and CALLBACK modes block reentrancy
686
+ // Always update state before making external calls
687
+
688
+ @method(
689
+ { name: 'from', type: ABIDataTypes.ADDRESS },
690
+ { name: 'to', type: ABIDataTypes.ADDRESS },
691
+ { name: 'tokenId', type: ABIDataTypes.UINT256 },
692
+ )
693
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
694
+ @emit('Transfer')
695
+ public safeTransfer(calldata: Calldata): BytesWriter {
696
+ // Update state BEFORE external call
697
+ this._transfer(from, to, tokenId);
698
+
699
+ // External call - if it tries to re-enter, ReentrancyGuard blocks it
700
+ this.notifyReceiver(to, from, tokenId);
701
+
702
+ return new BytesWriter(0);
703
+ }
704
+ ```
705
+
706
+ ## Common Mistakes
707
+
708
+ ### 1. Forgetting External Calls
709
+
710
+ ```typescript
711
+ // WRONG: Hidden external call
712
+ public process(): void {
713
+ oracle.updatePrice(); // This could call back!
714
+ // ...
715
+ }
716
+
717
+ // CORRECT: Aware of all external interactions
718
+ public process(): void {
719
+ // ReentrancyGuard protects this
720
+ oracle.updatePrice();
721
+ // Even if oracle calls back, it will revert
722
+ }
723
+ ```
724
+
725
+ ### 2. State Before Guard
726
+
727
+ ```typescript
728
+ // WRONG: State read before protection takes effect
729
+ public getValue(): u256 {
730
+ const value = storage.get(key); // Reads before guard
731
+ return value;
732
+ }
733
+
734
+ // In OPNet, the guard is checked at method entry,
735
+ // so this isn't an issue - just be aware of it
736
+ ```
737
+
738
+ ### 3. Over-Exclusion
739
+
740
+ ```typescript
741
+ // WRONG: Excluding too many functions
742
+ protected override isSelectorExcluded(selector: Selector): boolean {
743
+ const TRANSFER_SELECTOR: u32 = encodeSelector('transfer');
744
+
745
+ // DON'T exclude state-changing functions!
746
+ if (selector === TRANSFER_SELECTOR) return true; // DANGEROUS
747
+ return false;
748
+ }
749
+ ```
750
+
751
+ ## Testing Reentrancy
752
+
753
+ ```typescript
754
+ // Test contract that attempts reentrancy
755
+ @final
756
+ export class AttackerContract extends OP_NET {
757
+ private targetContract: Address;
758
+ private attackCount: u32 = 0;
759
+
760
+ @method({ name: 'target', type: ABIDataTypes.ADDRESS })
761
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
762
+ public attack(calldata: Calldata): BytesWriter {
763
+ this.targetContract = calldata.readAddress();
764
+
765
+ // Call target
766
+ Blockchain.call(this.targetContract, encodeWithdraw(), true);
767
+
768
+ return new BytesWriter(0);
769
+ }
770
+
771
+ // Called when receiving funds
772
+ public onReceive(): void {
773
+ if (this.attackCount < 10) {
774
+ this.attackCount++;
775
+ // Try to re-enter
776
+ Blockchain.call(this.targetContract, encodeWithdraw(), false);
777
+ // With ReentrancyGuard, this will fail
778
+ }
779
+ }
780
+ }
781
+ ```
782
+
783
+ ---
784
+
785
+ **Navigation:**
786
+ - Previous: [OP721 NFT](./op721-nft.md)
787
+ - Next: [Address Type](../types/address.md)