@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.
- package/LICENSE +190 -0
- package/README.md +258 -137
- package/SECURITY.md +226 -0
- package/docs/README.md +614 -0
- package/docs/advanced/bitcoin-scripts.md +939 -0
- package/docs/advanced/cross-contract-calls.md +579 -0
- package/docs/advanced/plugins.md +1006 -0
- package/docs/advanced/quantum-resistance.md +660 -0
- package/docs/advanced/signature-verification.md +715 -0
- package/docs/api-reference/blockchain.md +729 -0
- package/docs/api-reference/events.md +642 -0
- package/docs/api-reference/op20.md +902 -0
- package/docs/api-reference/op721.md +819 -0
- package/docs/api-reference/safe-math.md +510 -0
- package/docs/api-reference/storage.md +840 -0
- package/docs/contracts/op-net-base.md +786 -0
- package/docs/contracts/op20-token.md +687 -0
- package/docs/contracts/op20s-signatures.md +614 -0
- package/docs/contracts/op721-nft.md +785 -0
- package/docs/contracts/reentrancy-guard.md +787 -0
- package/docs/core-concepts/blockchain-environment.md +724 -0
- package/docs/core-concepts/decorators.md +466 -0
- package/docs/core-concepts/events.md +652 -0
- package/docs/core-concepts/pointers.md +391 -0
- package/docs/core-concepts/security.md +473 -0
- package/docs/core-concepts/storage-system.md +969 -0
- package/docs/examples/basic-token.md +745 -0
- package/docs/examples/nft-with-reservations.md +1440 -0
- package/docs/examples/oracle-integration.md +1212 -0
- package/docs/examples/stablecoin.md +1180 -0
- package/docs/getting-started/first-contract.md +575 -0
- package/docs/getting-started/installation.md +384 -0
- package/docs/getting-started/project-structure.md +630 -0
- package/docs/storage/memory-maps.md +764 -0
- package/docs/storage/stored-arrays.md +778 -0
- package/docs/storage/stored-maps.md +758 -0
- package/docs/storage/stored-primitives.md +655 -0
- package/docs/types/address.md +773 -0
- package/docs/types/bytes-writer-reader.md +938 -0
- package/docs/types/calldata.md +744 -0
- package/docs/types/safe-math.md +446 -0
- package/package.json +52 -27
- package/runtime/memory/MapOfMap.ts +1 -0
- 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)
|