@btc-vision/btc-runtime 1.10.11 → 1.11.0-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -224
- package/SECURITY.md +38 -191
- package/docs/README.md +28 -0
- package/docs/advanced/contract-upgrades.md +537 -0
- package/docs/advanced/plugins.md +90 -25
- package/docs/api-reference/blockchain.md +48 -14
- package/docs/api-reference/storage.md +2 -111
- package/docs/contracts/op-net-base.md +22 -0
- package/docs/contracts/upgradeable.md +396 -0
- package/docs/core-concepts/blockchain-environment.md +0 -2
- package/docs/core-concepts/security.md +8 -111
- package/docs/core-concepts/storage-system.md +1 -32
- package/docs/examples/nft-with-reservations.md +8 -238
- package/docs/storage/memory-maps.md +1 -44
- package/docs/storage/stored-arrays.md +1 -65
- package/docs/storage/stored-maps.md +1 -73
- package/docs/storage/stored-primitives.md +2 -49
- package/docs/types/bytes-writer-reader.md +76 -0
- package/docs/types/safe-math.md +2 -45
- package/package.json +5 -5
- package/runtime/buffer/BytesReader.ts +90 -3
- package/runtime/buffer/BytesWriter.ts +81 -3
- package/runtime/contracts/OP721.ts +40 -4
- package/runtime/contracts/OP_NET.ts +83 -11
- package/runtime/contracts/Upgradeable.ts +242 -0
- package/runtime/env/BlockchainEnvironment.ts +124 -27
- package/runtime/env/global.ts +24 -0
- package/runtime/events/upgradeable/UpgradeableEvents.ts +41 -0
- package/runtime/generic/AddressMap.ts +20 -18
- package/runtime/generic/ExtendedAddressMap.ts +147 -0
- package/runtime/generic/MapUint8Array.ts +20 -18
- package/runtime/index.ts +8 -0
- package/runtime/plugins/Plugin.ts +34 -0
- package/runtime/plugins/UpgradeablePlugin.ts +279 -0
- package/runtime/storage/BaseStoredString.ts +1 -1
- package/runtime/storage/arrays/StoredPackedArray.ts +4 -0
- package/runtime/types/ExtendedAddress.ts +36 -24
- package/runtime/types/ExtendedAddressCache.ts +27 -0
- package/runtime/types/SafeMath.ts +109 -18
- package/runtime/types/SchnorrSignature.ts +44 -0
- package/runtime/utils/lengths.ts +2 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# Upgradeable
|
|
2
|
+
|
|
3
|
+
The `Upgradeable` base class provides a secure upgrade mechanism with configurable timelock protection. Contracts extending `Upgradeable` can replace their bytecode while giving users time to assess pending changes.
|
|
4
|
+
|
|
5
|
+
> **Alternative**: If you're already extending another base class (like `OP20` or `OP721`), use the [`UpgradeablePlugin`](../advanced/contract-upgrades.md#using-the-upgradeableplugin) instead. Just call `this.registerPlugin(new UpgradeablePlugin(144))` in your constructor - no other code changes needed!
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import {
|
|
11
|
+
Upgradeable,
|
|
12
|
+
Calldata,
|
|
13
|
+
BytesWriter,
|
|
14
|
+
encodeSelector,
|
|
15
|
+
Selector,
|
|
16
|
+
ADDRESS_BYTE_LENGTH,
|
|
17
|
+
} from '@btc-vision/btc-runtime/runtime';
|
|
18
|
+
|
|
19
|
+
@final
|
|
20
|
+
export class MyContract extends Upgradeable {
|
|
21
|
+
// Set delay: 144 blocks = ~24 hours
|
|
22
|
+
protected readonly upgradeDelay: u64 = 144;
|
|
23
|
+
|
|
24
|
+
public override execute(method: Selector, calldata: Calldata): BytesWriter {
|
|
25
|
+
switch (method) {
|
|
26
|
+
case encodeSelector('submitUpgrade'):
|
|
27
|
+
return this.submitUpgrade(calldata.readAddress());
|
|
28
|
+
case encodeSelector('applyUpgrade'): {
|
|
29
|
+
const sourceAddress = calldata.readAddress();
|
|
30
|
+
const remainingLength = calldata.byteLength - ADDRESS_BYTE_LENGTH;
|
|
31
|
+
const updateCalldata = new BytesWriter(remainingLength);
|
|
32
|
+
if (remainingLength > 0) {
|
|
33
|
+
updateCalldata.writeBytes(calldata.readBytes(remainingLength));
|
|
34
|
+
}
|
|
35
|
+
return this.applyUpgrade(sourceAddress, updateCalldata);
|
|
36
|
+
}
|
|
37
|
+
case encodeSelector('cancelUpgrade'):
|
|
38
|
+
return this.cancelUpgrade();
|
|
39
|
+
default:
|
|
40
|
+
return super.execute(method, calldata);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Class Reference
|
|
47
|
+
|
|
48
|
+
### Properties
|
|
49
|
+
|
|
50
|
+
| Property | Type | Description |
|
|
51
|
+
|----------|------|-------------|
|
|
52
|
+
| `upgradeDelay` | `u64` | Blocks to wait before upgrade can be applied (default: 144 = ~24h) |
|
|
53
|
+
| `pendingUpgradeAddress` | `Address` | Source address of pending upgrade (zero if none) |
|
|
54
|
+
| `pendingUpgradeBlock` | `u64` | Block when upgrade was submitted (0 if none) |
|
|
55
|
+
| `upgradeEffectiveBlock` | `u64` | Block when upgrade can be applied (0 if none) |
|
|
56
|
+
| `hasPendingUpgrade` | `bool` | Whether an upgrade is pending |
|
|
57
|
+
| `canApplyUpgrade` | `bool` | Whether the delay has elapsed |
|
|
58
|
+
|
|
59
|
+
### Methods
|
|
60
|
+
|
|
61
|
+
#### submitUpgrade
|
|
62
|
+
|
|
63
|
+
Submits an upgrade for timelock. Only callable by deployer.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
protected submitUpgrade(sourceAddress: Address): BytesWriter
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Parameters:**
|
|
70
|
+
- `sourceAddress`: The address of the contract containing the new bytecode
|
|
71
|
+
|
|
72
|
+
**Reverts if:**
|
|
73
|
+
- Caller is not the deployer
|
|
74
|
+
- Source is not a deployed contract
|
|
75
|
+
- An upgrade is already pending
|
|
76
|
+
|
|
77
|
+
**Emits:** `UpgradeSubmitted(sourceAddress, submitBlock, effectiveBlock)`
|
|
78
|
+
|
|
79
|
+
#### applyUpgrade
|
|
80
|
+
|
|
81
|
+
Applies a pending upgrade after the delay has passed. Only callable by deployer.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
protected applyUpgrade(sourceAddress: Address, calldata: BytesWriter): BytesWriter
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Parameters:**
|
|
88
|
+
- `sourceAddress`: The source contract address (must match pending)
|
|
89
|
+
- `calldata`: Data passed to the `onUpdate` hook of the new contract
|
|
90
|
+
|
|
91
|
+
**Reverts if:**
|
|
92
|
+
- Caller is not the deployer
|
|
93
|
+
- No upgrade is pending
|
|
94
|
+
- Delay has not elapsed
|
|
95
|
+
- Address does not match pending upgrade
|
|
96
|
+
|
|
97
|
+
**Emits:** `UpgradeApplied(sourceAddress, appliedAtBlock)`
|
|
98
|
+
|
|
99
|
+
#### cancelUpgrade
|
|
100
|
+
|
|
101
|
+
Cancels a pending upgrade. Only callable by deployer.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
protected cancelUpgrade(): BytesWriter
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Reverts if:**
|
|
108
|
+
- Caller is not the deployer
|
|
109
|
+
- No upgrade is pending
|
|
110
|
+
|
|
111
|
+
**Emits:** `UpgradeCancelled(sourceAddress, cancelledAtBlock)`
|
|
112
|
+
|
|
113
|
+
## Events
|
|
114
|
+
|
|
115
|
+
### UpgradeSubmittedEvent
|
|
116
|
+
|
|
117
|
+
Emitted when an upgrade is submitted.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
class UpgradeSubmittedEvent extends NetEvent {
|
|
121
|
+
constructor(
|
|
122
|
+
sourceAddress: Address, // Contract with new bytecode
|
|
123
|
+
submitBlock: u64, // Block when submitted
|
|
124
|
+
effectiveBlock: u64 // Block when can be applied
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### UpgradeAppliedEvent
|
|
130
|
+
|
|
131
|
+
Emitted when an upgrade is applied.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
class UpgradeAppliedEvent extends NetEvent {
|
|
135
|
+
constructor(
|
|
136
|
+
sourceAddress: Address, // Contract with new bytecode
|
|
137
|
+
appliedAtBlock: u64 // Block when applied
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### UpgradeCancelledEvent
|
|
143
|
+
|
|
144
|
+
Emitted when a pending upgrade is cancelled.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
class UpgradeCancelledEvent extends NetEvent {
|
|
148
|
+
constructor(
|
|
149
|
+
sourceAddress: Address, // Cancelled source contract
|
|
150
|
+
cancelledAtBlock: u64 // Block when cancelled
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Usage Patterns
|
|
156
|
+
|
|
157
|
+
### Basic Upgradeable Contract
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
@final
|
|
161
|
+
export class SimpleUpgradeable extends Upgradeable {
|
|
162
|
+
protected readonly upgradeDelay: u64 = 144; // ~1 day
|
|
163
|
+
|
|
164
|
+
public override execute(method: Selector, calldata: Calldata): BytesWriter {
|
|
165
|
+
switch (method) {
|
|
166
|
+
case encodeSelector('submitUpgrade'):
|
|
167
|
+
return this.submitUpgrade(calldata.readAddress());
|
|
168
|
+
case encodeSelector('applyUpgrade'): {
|
|
169
|
+
const sourceAddress = calldata.readAddress();
|
|
170
|
+
const remainingLength = calldata.byteLength - ADDRESS_BYTE_LENGTH;
|
|
171
|
+
const updateCalldata = new BytesWriter(remainingLength);
|
|
172
|
+
if (remainingLength > 0) {
|
|
173
|
+
updateCalldata.writeBytes(calldata.readBytes(remainingLength));
|
|
174
|
+
}
|
|
175
|
+
return this.applyUpgrade(sourceAddress, updateCalldata);
|
|
176
|
+
}
|
|
177
|
+
case encodeSelector('cancelUpgrade'):
|
|
178
|
+
return this.cancelUpgrade();
|
|
179
|
+
default:
|
|
180
|
+
return super.execute(method, calldata);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### With Upgrade Status Views
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
@final
|
|
190
|
+
export class UpgradeableWithViews extends Upgradeable {
|
|
191
|
+
protected readonly upgradeDelay: u64 = 1008; // ~1 week
|
|
192
|
+
|
|
193
|
+
public override execute(method: Selector, calldata: Calldata): BytesWriter {
|
|
194
|
+
switch (method) {
|
|
195
|
+
// Upgrade actions
|
|
196
|
+
case encodeSelector('submitUpgrade'):
|
|
197
|
+
return this.submitUpgrade(calldata.readAddress());
|
|
198
|
+
case encodeSelector('applyUpgrade'): {
|
|
199
|
+
const sourceAddress = calldata.readAddress();
|
|
200
|
+
const remainingLength = calldata.byteLength - ADDRESS_BYTE_LENGTH;
|
|
201
|
+
const updateCalldata = new BytesWriter(remainingLength);
|
|
202
|
+
if (remainingLength > 0) {
|
|
203
|
+
updateCalldata.writeBytes(calldata.readBytes(remainingLength));
|
|
204
|
+
}
|
|
205
|
+
return this.applyUpgrade(sourceAddress, updateCalldata);
|
|
206
|
+
}
|
|
207
|
+
case encodeSelector('cancelUpgrade'):
|
|
208
|
+
return this.cancelUpgrade();
|
|
209
|
+
|
|
210
|
+
// View methods
|
|
211
|
+
case encodeSelector('getPendingUpgrade'):
|
|
212
|
+
return this.getPendingUpgrade();
|
|
213
|
+
case encodeSelector('getUpgradeStatus'):
|
|
214
|
+
return this.getUpgradeStatus();
|
|
215
|
+
|
|
216
|
+
default:
|
|
217
|
+
return super.execute(method, calldata);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private getPendingUpgrade(): BytesWriter {
|
|
222
|
+
const response = new BytesWriter(32);
|
|
223
|
+
response.writeAddress(this.pendingUpgradeAddress);
|
|
224
|
+
return response;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private getUpgradeStatus(): BytesWriter {
|
|
228
|
+
const response = new BytesWriter(17);
|
|
229
|
+
response.writeBoolean(this.hasPendingUpgrade);
|
|
230
|
+
response.writeU64(this.pendingUpgradeBlock);
|
|
231
|
+
response.writeU64(this.upgradeEffectiveBlock);
|
|
232
|
+
return response;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Emergency Upgrades (Not Recommended)
|
|
238
|
+
|
|
239
|
+
For contracts that need faster upgrades (use with caution):
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
@final
|
|
243
|
+
export class QuickUpgradeable extends Upgradeable {
|
|
244
|
+
// Only 6 blocks (~1 hour) - use only for emergencies
|
|
245
|
+
protected readonly upgradeDelay: u64 = 6;
|
|
246
|
+
|
|
247
|
+
// ... rest of implementation
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Security Considerations
|
|
252
|
+
|
|
253
|
+
### 1. Delay Selection
|
|
254
|
+
|
|
255
|
+
Choose an appropriate delay based on your contract's risk profile:
|
|
256
|
+
|
|
257
|
+
| Contract Type | Recommended Delay |
|
|
258
|
+
|---------------|-------------------|
|
|
259
|
+
| Test/Development | 1-6 blocks |
|
|
260
|
+
| Standard DeFi | 144 blocks (~24 hours) |
|
|
261
|
+
| High-value vaults | 1008 blocks (~1 week) |
|
|
262
|
+
| Governance contracts | 4320 blocks (~1 month) |
|
|
263
|
+
|
|
264
|
+
### 2. Source Validation
|
|
265
|
+
|
|
266
|
+
The `submitUpgrade` function validates that the source is a deployed contract. This prevents:
|
|
267
|
+
- Submitting non-existent addresses
|
|
268
|
+
- Last-minute malicious deployments
|
|
269
|
+
|
|
270
|
+
### 3. Address Match Verification
|
|
271
|
+
|
|
272
|
+
The `applyUpgrade` function requires the address to match the pending upgrade. This prevents:
|
|
273
|
+
- Front-running attacks
|
|
274
|
+
- Address substitution attacks
|
|
275
|
+
|
|
276
|
+
### 4. Storage Compatibility
|
|
277
|
+
|
|
278
|
+
When upgrading, ensure storage layout compatibility:
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
// V1 - Original
|
|
282
|
+
class ContractV1 extends Upgradeable {
|
|
283
|
+
private ptr1: u16 = Blockchain.nextPointer; // Pointer 1
|
|
284
|
+
private ptr2: u16 = Blockchain.nextPointer; // Pointer 2
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// V2 - Add new pointers at the END
|
|
288
|
+
class ContractV2 extends Upgradeable {
|
|
289
|
+
private ptr1: u16 = Blockchain.nextPointer; // Pointer 1 (same)
|
|
290
|
+
private ptr2: u16 = Blockchain.nextPointer; // Pointer 2 (same)
|
|
291
|
+
private ptr3: u16 = Blockchain.nextPointer; // Pointer 3 (NEW)
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Upgrade Workflow
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
1. Deploy new bytecode contract
|
|
299
|
+
└─> Returns: newContractAddress
|
|
300
|
+
|
|
301
|
+
2. Submit upgrade
|
|
302
|
+
└─> submitUpgrade(newContractAddress)
|
|
303
|
+
└─> Emits: UpgradeSubmitted
|
|
304
|
+
|
|
305
|
+
3. Wait for delay
|
|
306
|
+
└─> Users can monitor and exit
|
|
307
|
+
|
|
308
|
+
4. Apply upgrade
|
|
309
|
+
└─> applyUpgrade(newContractAddress)
|
|
310
|
+
└─> Emits: UpgradeApplied
|
|
311
|
+
└─> VM calls onUpdate() on new bytecode
|
|
312
|
+
└─> New bytecode active next block
|
|
313
|
+
|
|
314
|
+
5. (Optional) Discard source contract
|
|
315
|
+
└─> Source contract can be abandoned
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### The onUpdate Hook
|
|
319
|
+
|
|
320
|
+
Override `onUpdate` in your new contract version to perform migrations:
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
@final
|
|
324
|
+
export class MyContractV2 extends Upgradeable {
|
|
325
|
+
protected readonly upgradeDelay: u64 = 144;
|
|
326
|
+
|
|
327
|
+
// New storage added in V2
|
|
328
|
+
private newFeaturePointer: u16 = Blockchain.nextPointer;
|
|
329
|
+
private _newFeature: StoredU256;
|
|
330
|
+
|
|
331
|
+
public constructor() {
|
|
332
|
+
super();
|
|
333
|
+
this._newFeature = new StoredU256(this.newFeaturePointer, EMPTY_POINTER);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
public override onUpdate(calldata: Calldata): void {
|
|
337
|
+
super.onUpdate(calldata);
|
|
338
|
+
// Initialize new storage
|
|
339
|
+
this._newFeature.value = u256.fromU64(100);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ... execute() and other methods
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
See [The onUpdate Lifecycle Hook](../advanced/contract-upgrades.md#the-onupdate-lifecycle-hook) for more details.
|
|
347
|
+
|
|
348
|
+
## Combining with Other Base Classes
|
|
349
|
+
|
|
350
|
+
### Upgradeable + ReentrancyGuard
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
// Create a combined base class
|
|
354
|
+
class UpgradeableWithReentrancy extends Upgradeable {
|
|
355
|
+
protected readonly reentrancyLevel: ReentrancyLevel = ReentrancyLevel.STANDARD;
|
|
356
|
+
private _locked: StoredBoolean;
|
|
357
|
+
|
|
358
|
+
protected constructor() {
|
|
359
|
+
super();
|
|
360
|
+
this._locked = new StoredBoolean(Blockchain.nextPointer, false);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
protected nonReentrant(): void {
|
|
364
|
+
if (this._locked.value) {
|
|
365
|
+
throw new Revert('ReentrancyGuard: LOCKED');
|
|
366
|
+
}
|
|
367
|
+
this._locked.value = true;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
protected releaseGuard(): void {
|
|
371
|
+
this._locked.value = false;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
@final
|
|
376
|
+
export class SecureUpgradeableVault extends UpgradeableWithReentrancy {
|
|
377
|
+
// Implementation with both upgrade and reentrancy protection
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Solidity Comparison
|
|
382
|
+
|
|
383
|
+
| Feature | OpenZeppelin UUPS | OPNet Upgradeable |
|
|
384
|
+
|---------|-------------------|-------------------|
|
|
385
|
+
| Upgrade mechanism | delegatecall | Native bytecode replacement |
|
|
386
|
+
| Storage location | Implementation contract | Same contract |
|
|
387
|
+
| Proxy overhead | Yes | No |
|
|
388
|
+
| Timelock | Separate contract (optional) | Built-in |
|
|
389
|
+
| Events | Custom | Built-in |
|
|
390
|
+
| Cancel upgrade | Custom implementation | Built-in |
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
**Navigation:**
|
|
395
|
+
- Previous: [ReentrancyGuard](./reentrancy-guard.md)
|
|
396
|
+
- Next: [Address Type](../types/address.md)
|
|
@@ -587,7 +587,6 @@ classDiagram
|
|
|
587
587
|
+getBlockHash(blockNumber) Uint8Array
|
|
588
588
|
+emit(event) void
|
|
589
589
|
+log(data) void
|
|
590
|
-
+registerPlugin(plugin) void
|
|
591
590
|
}
|
|
592
591
|
|
|
593
592
|
class Block {
|
|
@@ -715,7 +714,6 @@ export class MyContract extends OP_NET {
|
|
|
715
714
|
| `Blockchain.getBlockHash()` | `Uint8Array` | Historical block hash |
|
|
716
715
|
| `Blockchain.emit()` | `void` | Emit event |
|
|
717
716
|
| `Blockchain.log()` | `void` | Debug logging (testing only) |
|
|
718
|
-
| `Blockchain.registerPlugin()` | `void` | Register a plugin |
|
|
719
717
|
|
|
720
718
|
---
|
|
721
719
|
|
|
@@ -76,137 +76,34 @@ See [SafeMath API](../api-reference/safe-math.md) for complete reference.
|
|
|
76
76
|
|
|
77
77
|
## Reentrancy Protection
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
Reentrancy attacks occur when a contract calls back into itself before completing:
|
|
82
|
-
|
|
83
|
-
```
|
|
84
|
-
1. User calls withdraw()
|
|
85
|
-
2. Contract sends funds to User
|
|
86
|
-
3. User's receive function calls withdraw() again
|
|
87
|
-
4. Contract sends funds again (before updating balance)
|
|
88
|
-
5. Repeat until drained
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### Reentrancy Attack Sequence
|
|
92
|
-
|
|
93
|
-
```mermaid
|
|
94
|
-
sequenceDiagram
|
|
95
|
-
participant Attacker
|
|
96
|
-
participant VulnerableContract
|
|
97
|
-
participant AttackerContract
|
|
98
|
-
|
|
99
|
-
Attacker->>VulnerableContract: withdraw()
|
|
100
|
-
activate VulnerableContract
|
|
101
|
-
|
|
102
|
-
Note over VulnerableContract: balance = 100<br/>Check balance
|
|
103
|
-
|
|
104
|
-
VulnerableContract->>AttackerContract: Transfer 100 tokens
|
|
105
|
-
activate AttackerContract
|
|
106
|
-
|
|
107
|
-
Note over AttackerContract: Receive callback triggered
|
|
79
|
+
Reentrancy attacks occur when a contract calls back into itself before completing, allowing attackers to drain funds by repeatedly calling withdraw before the balance is updated.
|
|
108
80
|
|
|
109
|
-
|
|
110
|
-
activate VulnerableContract
|
|
111
|
-
|
|
112
|
-
Note over VulnerableContract: balance still = 100<br/>Check balance [BUG!]
|
|
113
|
-
|
|
114
|
-
VulnerableContract->>AttackerContract: Transfer 100 tokens AGAIN
|
|
115
|
-
deactivate VulnerableContract
|
|
116
|
-
|
|
117
|
-
AttackerContract-->>VulnerableContract: Return
|
|
118
|
-
deactivate AttackerContract
|
|
119
|
-
|
|
120
|
-
Note over VulnerableContract: balance = 0<br/>(Too late!)
|
|
121
|
-
|
|
122
|
-
VulnerableContract-->>Attacker: Complete
|
|
123
|
-
deactivate VulnerableContract
|
|
124
|
-
|
|
125
|
-
Note over Attacker: Stole 200 tokens!<br/>Expected: 100
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### The Solution: ReentrancyGuard
|
|
81
|
+
**Solution:** Extend `ReentrancyGuard` to automatically protect all methods:
|
|
129
82
|
|
|
130
83
|
```typescript
|
|
131
84
|
import { ReentrancyGuard, ReentrancyLevel } from '@btc-vision/btc-runtime/runtime';
|
|
132
85
|
|
|
133
86
|
@final
|
|
134
87
|
export class MyContract extends ReentrancyGuard {
|
|
135
|
-
// Override the reentrancy level (STANDARD is the default)
|
|
136
88
|
protected override readonly reentrancyLevel: ReentrancyLevel = ReentrancyLevel.STANDARD;
|
|
137
89
|
|
|
138
|
-
public constructor() {
|
|
139
|
-
super();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
90
|
@method()
|
|
143
91
|
public withdraw(calldata: Calldata): BytesWriter {
|
|
144
|
-
//
|
|
92
|
+
// Protected automatically - re-entry attempts will revert
|
|
145
93
|
const amount = this.balances.get(Blockchain.tx.sender);
|
|
146
|
-
|
|
147
|
-
// Even if external call tries to re-enter, it will fail
|
|
148
94
|
this.balances.set(Blockchain.tx.sender, u256.Zero);
|
|
149
95
|
// ... transfer funds ...
|
|
150
|
-
|
|
151
96
|
return new BytesWriter(0);
|
|
152
97
|
}
|
|
153
98
|
}
|
|
154
99
|
```
|
|
155
100
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
config:
|
|
161
|
-
theme: dark
|
|
162
|
-
---
|
|
163
|
-
flowchart LR
|
|
164
|
-
Start["Method Called"] --> Check{"Guard Active?"}
|
|
165
|
-
Check -->|Yes| Revert["REVERT"]
|
|
166
|
-
Check -->|No| SetGuard["Set Guard Flag"]
|
|
167
|
-
SetGuard --> Execute["Execute Logic"]
|
|
168
|
-
Execute --> External{"External Call?"}
|
|
169
|
-
External -->|Yes| Call["Call External"]
|
|
170
|
-
Call --> Detect{"Re-enter?"}
|
|
171
|
-
Detect -->|Yes| Block["REVERT: Blocked"]
|
|
172
|
-
Detect -->|No| Clear["Clear Guard"]
|
|
173
|
-
External -->|No| Clear
|
|
174
|
-
Clear --> Success["Transaction Success"]
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### Guard Modes
|
|
178
|
-
|
|
179
|
-
| Mode | Description | Use Case |
|
|
180
|
-
|------|-------------|----------|
|
|
181
|
-
| `ReentrancyLevel.STANDARD` | Uses boolean lock, strict mutual exclusion | Default for most contracts |
|
|
182
|
-
| `ReentrancyLevel.CALLBACK` | Uses depth counter, still blocks reentrancy | Depth tracking use cases |
|
|
183
|
-
|
|
184
|
-
```typescript
|
|
185
|
-
// STANDARD: No re-entry allowed, uses boolean lock (default)
|
|
186
|
-
protected override readonly reentrancyLevel: ReentrancyLevel = ReentrancyLevel.STANDARD;
|
|
187
|
-
|
|
188
|
-
// CALLBACK: No re-entry allowed, uses depth counter for tracking
|
|
189
|
-
protected override readonly reentrancyLevel: ReentrancyLevel = ReentrancyLevel.CALLBACK;
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### Guard Mode Decision Tree
|
|
193
|
-
|
|
194
|
-
```mermaid
|
|
195
|
-
---
|
|
196
|
-
config:
|
|
197
|
-
theme: dark
|
|
198
|
-
---
|
|
199
|
-
flowchart LR
|
|
200
|
-
Start{"External Calls?"}
|
|
201
|
-
Start -->|No| None["No Guard Needed"]
|
|
202
|
-
Start -->|Yes| Tracking{"Need Depth Tracking?"}
|
|
203
|
-
Tracking -->|No| Standard["STANDARD Mode"]
|
|
204
|
-
Tracking -->|Yes| CallbackMode["CALLBACK Mode"]
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
Note: Both modes block reentrancy. STANDARD uses a boolean lock; CALLBACK uses a depth counter.
|
|
101
|
+
| Mode | Description |
|
|
102
|
+
|------|-------------|
|
|
103
|
+
| `ReentrancyLevel.STANDARD` | Boolean lock (default) |
|
|
104
|
+
| `ReentrancyLevel.CALLBACK` | Depth counter for tracking |
|
|
208
105
|
|
|
209
|
-
See [ReentrancyGuard](../contracts/reentrancy-guard.md) for detailed usage.
|
|
106
|
+
Both modes block reentrancy. See [ReentrancyGuard](../contracts/reentrancy-guard.md) for detailed explanation, attack diagrams, and advanced usage.
|
|
210
107
|
|
|
211
108
|
## Access Control
|
|
212
109
|
|
|
@@ -195,38 +195,7 @@ flowchart LR
|
|
|
195
195
|
end
|
|
196
196
|
```
|
|
197
197
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
> **DO NOT USE AssemblyScript's Built-in Map**
|
|
201
|
-
>
|
|
202
|
-
> When creating custom map implementations or extending map functionality, you **MUST** use the Map class from `@btc-vision/btc-runtime/runtime`, NOT the built-in AssemblyScript Map.
|
|
203
|
-
>
|
|
204
|
-
> **Why the AssemblyScript Map is broken for blockchain:**
|
|
205
|
-
> - NOT optimized for blockchain storage patterns
|
|
206
|
-
> - Does NOT handle Uint8Array buffers as keys correctly
|
|
207
|
-
> - Does NOT work properly with Address key comparisons
|
|
208
|
-
> - Will cause silent data corruption or key collisions
|
|
209
|
-
>
|
|
210
|
-
> **CORRECT:**
|
|
211
|
-
> ```typescript
|
|
212
|
-
> import { Map } from '@btc-vision/btc-runtime/runtime';
|
|
213
|
-
>
|
|
214
|
-
> export class MyCustomMap<V> extends Map<Address, V> {
|
|
215
|
-
> // Your implementation
|
|
216
|
-
> }
|
|
217
|
-
> ```
|
|
218
|
-
>
|
|
219
|
-
> **WRONG:**
|
|
220
|
-
> ```typescript
|
|
221
|
-
> // DO NOT DO THIS - will break!
|
|
222
|
-
> const map = new Map<Uint8Array, u256>(); // AssemblyScript Map
|
|
223
|
-
> ```
|
|
224
|
-
>
|
|
225
|
-
> The btc-runtime Map is specifically designed to:
|
|
226
|
-
> - Handle Address and Uint8Array key comparisons correctly
|
|
227
|
-
> - Optimize for blockchain storage access patterns
|
|
228
|
-
> - Support proper serialization for persistent storage
|
|
229
|
-
> - Prevent key collisions with custom equality logic
|
|
198
|
+
> **Important:** Do NOT use AssemblyScript's built-in Map for blockchain storage. See [CRITICAL: Map Implementation Warning](../storage/stored-maps.md#critical-map-implementation-warning) for details.
|
|
230
199
|
|
|
231
200
|
## Pointer Allocation
|
|
232
201
|
|