@btc-vision/btc-runtime 1.10.12 → 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/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/types/bytes-writer-reader.md +76 -0
- 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 +34 -63
- package/runtime/types/SchnorrSignature.ts +44 -0
- package/runtime/utils/lengths.ts +2 -0
package/docs/README.md
CHANGED
|
@@ -285,6 +285,18 @@ npm run build:token
|
|
|
285
285
|
- [Common Mistakes](./contracts/reentrancy-guard.md#common-mistakes)
|
|
286
286
|
- [Testing Reentrancy](./contracts/reentrancy-guard.md#testing-reentrancy)
|
|
287
287
|
|
|
288
|
+
#### [Upgradeable](./contracts/upgradeable.md)
|
|
289
|
+
- [Overview](./contracts/upgradeable.md#overview)
|
|
290
|
+
- [Class Reference](./contracts/upgradeable.md#class-reference)
|
|
291
|
+
- [Properties](./contracts/upgradeable.md#properties)
|
|
292
|
+
- [Methods](./contracts/upgradeable.md#methods)
|
|
293
|
+
- [Events](./contracts/upgradeable.md#events)
|
|
294
|
+
- [Usage Patterns](./contracts/upgradeable.md#usage-patterns)
|
|
295
|
+
- [Security Considerations](./contracts/upgradeable.md#security-considerations)
|
|
296
|
+
- [Upgrade Workflow](./contracts/upgradeable.md#upgrade-workflow)
|
|
297
|
+
- [Combining with Other Base Classes](./contracts/upgradeable.md#combining-with-other-base-classes)
|
|
298
|
+
- [Solidity Comparison](./contracts/upgradeable.md#solidity-comparison)
|
|
299
|
+
|
|
288
300
|
---
|
|
289
301
|
|
|
290
302
|
### Types & Utilities
|
|
@@ -487,14 +499,30 @@ npm run build:token
|
|
|
487
499
|
- [Solidity vs OPNet Comparison](./advanced/bitcoin-scripts.md#solidity-vs-opnet-comparison)
|
|
488
500
|
- [Best Practices](./advanced/bitcoin-scripts.md#best-practices)
|
|
489
501
|
|
|
502
|
+
#### [Contract Upgrades](./advanced/contract-upgrades.md)
|
|
503
|
+
- [Overview](./advanced/contract-upgrades.md#overview)
|
|
504
|
+
- [How It Works](./advanced/contract-upgrades.md#how-it-works)
|
|
505
|
+
- [Basic Usage](./advanced/contract-upgrades.md#basic-usage)
|
|
506
|
+
- [The Timelock Pattern](./advanced/contract-upgrades.md#the-timelock-pattern)
|
|
507
|
+
- [Why Use a Timelock?](./advanced/contract-upgrades.md#why-use-a-timelock)
|
|
508
|
+
- [Using the Upgradeable Base Class](./advanced/contract-upgrades.md#using-the-upgradeable-base-class)
|
|
509
|
+
- [Using the UpgradeablePlugin](./advanced/contract-upgrades.md#using-the-upgradeableplugin)
|
|
510
|
+
- [Storage Compatibility](./advanced/contract-upgrades.md#storage-compatibility)
|
|
511
|
+
- [Security Considerations](./advanced/contract-upgrades.md#security-considerations)
|
|
512
|
+
- [Comparison with Other Platforms](./advanced/contract-upgrades.md#comparison-with-other-platforms)
|
|
513
|
+
- [Complete Example](./advanced/contract-upgrades.md#complete-example)
|
|
514
|
+
- [Upgrade Workflow](./advanced/contract-upgrades.md#upgrade-workflow)
|
|
515
|
+
|
|
490
516
|
#### [Plugins](./advanced/plugins.md)
|
|
491
517
|
- [Overview](./advanced/plugins.md#overview)
|
|
492
518
|
- [Plugin Architecture](./advanced/plugins.md#plugin-architecture)
|
|
493
519
|
- [Creating Plugins](./advanced/plugins.md#creating-plugins)
|
|
494
520
|
- [Lifecycle Hooks](./advanced/plugins.md#lifecycle-hooks)
|
|
495
521
|
- [onDeployment](./advanced/plugins.md#ondeployment)
|
|
522
|
+
- [onUpdate](./advanced/plugins.md#onupdate)
|
|
496
523
|
- [onExecutionStarted](./advanced/plugins.md#onexecutionstarted)
|
|
497
524
|
- [onExecutionCompleted](./advanced/plugins.md#onexecutioncompleted)
|
|
525
|
+
- [execute](./advanced/plugins.md#execute)
|
|
498
526
|
- [Built-in Plugins](./advanced/plugins.md#built-in-plugins)
|
|
499
527
|
- [Solidity vs OPNet Comparison](./advanced/plugins.md#solidity-vs-opnet-comparison)
|
|
500
528
|
- [Best Practices](./advanced/plugins.md#best-practices)
|
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
# Contract Upgrades
|
|
2
|
+
|
|
3
|
+
OPNet provides a native bytecode replacement mechanism that allows contracts to upgrade their execution logic while preserving their address and storage state. This guide covers the upgrade mechanism, security considerations, and the timelock pattern for safe upgrades.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Unlike Ethereum's proxy patterns or Solana's upgrade authority model, OPNet enables contracts to replace their own bytecode through a VM opcode. The mechanism uses an address-based replacement model where new bytecode is deployed to a temporary contract, and the target contract references that address to perform the upgrade.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { Blockchain } from '@btc-vision/btc-runtime/runtime';
|
|
11
|
+
|
|
12
|
+
// Deploy new bytecode as a separate contract first
|
|
13
|
+
// Then update the current contract's bytecode
|
|
14
|
+
Blockchain.updateContractFromExisting(newBytecodeAddress);
|
|
15
|
+
// New bytecode takes effect at the next block
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## How It Works
|
|
19
|
+
|
|
20
|
+
```mermaid
|
|
21
|
+
sequenceDiagram
|
|
22
|
+
participant Owner as Contract Owner
|
|
23
|
+
participant Target as Target Contract
|
|
24
|
+
participant Source as Source Contract<br/>(New Bytecode)
|
|
25
|
+
participant VM as OPNet VM
|
|
26
|
+
|
|
27
|
+
Note over Owner: Step 1: Deploy new bytecode
|
|
28
|
+
Owner->>VM: Deploy new contract with updated code
|
|
29
|
+
VM->>Source: Create contract at new address
|
|
30
|
+
|
|
31
|
+
Note over Owner: Step 2: Call upgrade function
|
|
32
|
+
Owner->>Target: upgrade(sourceAddress)
|
|
33
|
+
Target->>Target: Validate permissions
|
|
34
|
+
Target->>Target: Validate source is a contract
|
|
35
|
+
|
|
36
|
+
Note over Target,VM: Step 3: Execute bytecode replacement
|
|
37
|
+
Target->>VM: updateContractFromExisting(sourceAddress)
|
|
38
|
+
VM->>Source: Read bytecode
|
|
39
|
+
VM->>VM: Schedule replacement for next block
|
|
40
|
+
|
|
41
|
+
Note over VM: Block boundary
|
|
42
|
+
VM->>Target: Replace bytecode
|
|
43
|
+
|
|
44
|
+
Note over Target: Step 4: New code active
|
|
45
|
+
Owner->>Target: Any call now executes new bytecode
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Basic Usage
|
|
49
|
+
|
|
50
|
+
### The updateContractFromExisting Method
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
/**
|
|
54
|
+
* Updates this contract's bytecode from an existing deployed contract.
|
|
55
|
+
* New bytecode takes effect at the next block.
|
|
56
|
+
*
|
|
57
|
+
* @param sourceAddress - Address of the contract containing the new bytecode
|
|
58
|
+
* @param calldata - Optional calldata passed to the VM (default: empty)
|
|
59
|
+
*/
|
|
60
|
+
Blockchain.updateContractFromExisting(
|
|
61
|
+
sourceAddress: Address,
|
|
62
|
+
calldata?: BytesWriter | null
|
|
63
|
+
): void
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Simple Upgrade Function
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import {
|
|
70
|
+
OP_NET,
|
|
71
|
+
Blockchain,
|
|
72
|
+
Address,
|
|
73
|
+
Calldata,
|
|
74
|
+
BytesWriter,
|
|
75
|
+
encodeSelector,
|
|
76
|
+
Revert,
|
|
77
|
+
} from '@btc-vision/btc-runtime/runtime';
|
|
78
|
+
|
|
79
|
+
@final
|
|
80
|
+
export class MyContract extends OP_NET {
|
|
81
|
+
public override execute(method: Selector, calldata: Calldata): BytesWriter {
|
|
82
|
+
switch (method) {
|
|
83
|
+
case encodeSelector('upgrade(address)'):
|
|
84
|
+
return this.upgrade(calldata);
|
|
85
|
+
default:
|
|
86
|
+
return super.execute(method, calldata);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private upgrade(calldata: Calldata): BytesWriter {
|
|
91
|
+
// Only deployer can upgrade
|
|
92
|
+
this.onlyDeployer(Blockchain.tx.sender);
|
|
93
|
+
|
|
94
|
+
const sourceAddress = calldata.readAddress();
|
|
95
|
+
|
|
96
|
+
// Validate source is a deployed contract
|
|
97
|
+
if (!Blockchain.isContract(sourceAddress)) {
|
|
98
|
+
throw new Revert('Source must be a deployed contract');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Perform upgrade - takes effect next block
|
|
102
|
+
Blockchain.updateContractFromExisting(sourceAddress);
|
|
103
|
+
|
|
104
|
+
return new BytesWriter(0);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## The Timelock Pattern
|
|
110
|
+
|
|
111
|
+
Immediate upgrades are risky because users have no time to react to potentially malicious changes. The timelock pattern addresses this by requiring a delay between submitting and applying an upgrade.
|
|
112
|
+
|
|
113
|
+
### Why Use a Timelock?
|
|
114
|
+
|
|
115
|
+
1. **User Protection**: Users can monitor for pending upgrades and exit if they distrust changes
|
|
116
|
+
2. **Attack Prevention**: Prevents instant malicious upgrades
|
|
117
|
+
3. **Transparency**: All pending upgrades are visible on-chain
|
|
118
|
+
|
|
119
|
+
### Timelock Flow
|
|
120
|
+
|
|
121
|
+
```mermaid
|
|
122
|
+
sequenceDiagram
|
|
123
|
+
participant Owner as Contract Owner
|
|
124
|
+
participant Contract as Upgradeable Contract
|
|
125
|
+
participant Users as Users/Indexers
|
|
126
|
+
|
|
127
|
+
Note over Owner: Day 1: Submit upgrade
|
|
128
|
+
Owner->>Contract: submitUpgrade(sourceAddress)
|
|
129
|
+
Contract->>Contract: Store pending address + block
|
|
130
|
+
Contract-->>Users: Emit UpgradeSubmitted event
|
|
131
|
+
|
|
132
|
+
Note over Users: Users can monitor and exit
|
|
133
|
+
Users->>Users: Review pending upgrade
|
|
134
|
+
Users->>Users: Exit if distrustful
|
|
135
|
+
|
|
136
|
+
Note over Owner: Day 2+: Apply after delay
|
|
137
|
+
Owner->>Contract: applyUpgrade(sourceAddress)
|
|
138
|
+
Contract->>Contract: Verify delay elapsed
|
|
139
|
+
Contract->>Contract: Verify address matches
|
|
140
|
+
Contract->>Contract: Execute upgrade
|
|
141
|
+
Contract-->>Users: Emit UpgradeApplied event
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Using the Upgradeable Base Class
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import {
|
|
148
|
+
Upgradeable,
|
|
149
|
+
Calldata,
|
|
150
|
+
BytesWriter,
|
|
151
|
+
encodeSelector,
|
|
152
|
+
Selector,
|
|
153
|
+
ADDRESS_BYTE_LENGTH,
|
|
154
|
+
} from '@btc-vision/btc-runtime/runtime';
|
|
155
|
+
|
|
156
|
+
@final
|
|
157
|
+
export class MyUpgradeableContract extends Upgradeable {
|
|
158
|
+
// Set upgrade delay: 144 blocks = ~24 hours
|
|
159
|
+
protected readonly upgradeDelay: u64 = 144;
|
|
160
|
+
|
|
161
|
+
public override execute(method: Selector, calldata: Calldata): BytesWriter {
|
|
162
|
+
switch (method) {
|
|
163
|
+
case encodeSelector('submitUpgrade'):
|
|
164
|
+
return this.submitUpgrade(calldata.readAddress());
|
|
165
|
+
case encodeSelector('applyUpgrade'): {
|
|
166
|
+
const sourceAddress = calldata.readAddress();
|
|
167
|
+
const remainingLength = calldata.byteLength - ADDRESS_BYTE_LENGTH;
|
|
168
|
+
const updateCalldata = new BytesWriter(remainingLength);
|
|
169
|
+
if (remainingLength > 0) {
|
|
170
|
+
updateCalldata.writeBytes(calldata.readBytes(remainingLength));
|
|
171
|
+
}
|
|
172
|
+
return this.applyUpgrade(sourceAddress, updateCalldata);
|
|
173
|
+
}
|
|
174
|
+
case encodeSelector('cancelUpgrade'):
|
|
175
|
+
return this.cancelUpgrade();
|
|
176
|
+
// ... other methods
|
|
177
|
+
default:
|
|
178
|
+
return super.execute(method, calldata);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Using the UpgradeablePlugin
|
|
185
|
+
|
|
186
|
+
If you don't want to extend a base class (for example, if you're already extending `OP20` or `OP721`), use the `UpgradeablePlugin` instead. The plugin system is fully automatic - just register the plugin in your constructor:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import {
|
|
190
|
+
OP20,
|
|
191
|
+
UpgradeablePlugin,
|
|
192
|
+
} from '@btc-vision/btc-runtime/runtime';
|
|
193
|
+
|
|
194
|
+
@final
|
|
195
|
+
export class MyToken extends OP20 {
|
|
196
|
+
public constructor() {
|
|
197
|
+
super();
|
|
198
|
+
// Register the plugin - 144 blocks = ~24 hours (default)
|
|
199
|
+
this.registerPlugin(new UpgradeablePlugin(144));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// No need to modify execute() - upgrade methods are handled automatically!
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The plugin automatically handles these methods:
|
|
207
|
+
- `submitUpgrade(address)` - Submit upgrade for timelock
|
|
208
|
+
- `applyUpgrade(address)` - Apply upgrade after delay
|
|
209
|
+
- `cancelUpgrade()` - Cancel pending upgrade
|
|
210
|
+
- `pendingUpgrade()` - Get pending upgrade info
|
|
211
|
+
- `upgradeDelay()` - Get configured delay
|
|
212
|
+
|
|
213
|
+
**How it works:** When your contract's `execute()` falls through to `super.execute()`, the base `OP_NET` class automatically checks all registered plugins before throwing "Method not found".
|
|
214
|
+
|
|
215
|
+
### Common Delay Values
|
|
216
|
+
|
|
217
|
+
| Delay | Blocks | Use Case |
|
|
218
|
+
|-------|--------|----------|
|
|
219
|
+
| ~1 hour | 6 | Emergency patches (not recommended for production) |
|
|
220
|
+
| ~24 hours | 144 | Standard upgrades (default) |
|
|
221
|
+
| ~1 week | 1008 | Critical infrastructure |
|
|
222
|
+
| ~1 month | 4320 | Governance-controlled contracts |
|
|
223
|
+
|
|
224
|
+
### Upgrade Events
|
|
225
|
+
|
|
226
|
+
The `Upgradeable` contract emits events for monitoring:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// Emitted when an upgrade is submitted
|
|
230
|
+
class UpgradeSubmittedEvent {
|
|
231
|
+
sourceAddress: Address; // New bytecode contract
|
|
232
|
+
submitBlock: u64; // Block when submitted
|
|
233
|
+
effectiveBlock: u64; // Block when upgrade can be applied
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Emitted when an upgrade is applied
|
|
237
|
+
class UpgradeAppliedEvent {
|
|
238
|
+
sourceAddress: Address; // New bytecode contract
|
|
239
|
+
appliedAtBlock: u64; // Block when applied
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Emitted when a pending upgrade is cancelled
|
|
243
|
+
class UpgradeCancelledEvent {
|
|
244
|
+
sourceAddress: Address; // Cancelled source contract
|
|
245
|
+
cancelledAtBlock: u64; // Block when cancelled
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Storage Compatibility
|
|
250
|
+
|
|
251
|
+
Storage layout compatibility across bytecode versions is the developer's responsibility. The VM does not validate or migrate storage between versions.
|
|
252
|
+
|
|
253
|
+
### What Persists
|
|
254
|
+
|
|
255
|
+
- Contract address (unchanged)
|
|
256
|
+
- All storage slots (unchanged)
|
|
257
|
+
- Contract deployer
|
|
258
|
+
|
|
259
|
+
### What Changes
|
|
260
|
+
|
|
261
|
+
- Execution logic (bytecode)
|
|
262
|
+
|
|
263
|
+
### Storage Layout Rules
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// Version 1
|
|
267
|
+
class MyContractV1 extends OP_NET {
|
|
268
|
+
private balancePointer: u16 = Blockchain.nextPointer; // Pointer 1
|
|
269
|
+
private allowancePointer: u16 = Blockchain.nextPointer; // Pointer 2
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Version 2 - CORRECT: Add new pointers at the end
|
|
273
|
+
class MyContractV2 extends OP_NET {
|
|
274
|
+
private balancePointer: u16 = Blockchain.nextPointer; // Pointer 1 (same)
|
|
275
|
+
private allowancePointer: u16 = Blockchain.nextPointer; // Pointer 2 (same)
|
|
276
|
+
private newFeaturePointer: u16 = Blockchain.nextPointer; // Pointer 3 (new)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Version 2 - WRONG: Changing order breaks storage
|
|
280
|
+
class MyContractV2Bad extends OP_NET {
|
|
281
|
+
private newFeaturePointer: u16 = Blockchain.nextPointer; // Pointer 1 (was balance!)
|
|
282
|
+
private balancePointer: u16 = Blockchain.nextPointer; // Pointer 2 (was allowance!)
|
|
283
|
+
private allowancePointer: u16 = Blockchain.nextPointer; // Pointer 3 (new slot)
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Best Practices
|
|
288
|
+
|
|
289
|
+
1. **Never remove or reorder existing pointers**
|
|
290
|
+
2. **Always add new pointers at the end**
|
|
291
|
+
3. **Document pointer assignments**
|
|
292
|
+
4. **Test upgrades thoroughly on testnet**
|
|
293
|
+
|
|
294
|
+
## The onUpdate Lifecycle Hook
|
|
295
|
+
|
|
296
|
+
When a contract's bytecode is updated via `updateContractFromExisting`, the VM calls the `onUpdate` hook on the **new** bytecode. This allows the new contract version to perform migrations, initialize new storage, or validate the upgrade.
|
|
297
|
+
|
|
298
|
+
### Basic Usage
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
@final
|
|
302
|
+
export class MyContractV2 extends OP_NET {
|
|
303
|
+
// New storage pointer added in V2
|
|
304
|
+
private newFeaturePointer: u16 = Blockchain.nextPointer;
|
|
305
|
+
private _newFeature: StoredU256;
|
|
306
|
+
|
|
307
|
+
public constructor() {
|
|
308
|
+
super();
|
|
309
|
+
this._newFeature = new StoredU256(this.newFeaturePointer, EMPTY_POINTER);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
public override onUpdate(calldata: Calldata): void {
|
|
313
|
+
super.onUpdate(calldata); // Call plugins first
|
|
314
|
+
|
|
315
|
+
// Initialize new storage with default value
|
|
316
|
+
if (this._newFeature.value === u256.Zero) {
|
|
317
|
+
this._newFeature.value = u256.fromU64(100);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Version-Based Migrations
|
|
324
|
+
|
|
325
|
+
For complex migrations, pass a version number in the calldata:
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
public override onUpdate(calldata: Calldata): void {
|
|
329
|
+
super.onUpdate(calldata);
|
|
330
|
+
|
|
331
|
+
// Read migration version from calldata
|
|
332
|
+
const fromVersion = calldata.readU64();
|
|
333
|
+
|
|
334
|
+
if (fromVersion === 1) {
|
|
335
|
+
this.migrateFromV1();
|
|
336
|
+
} else if (fromVersion === 2) {
|
|
337
|
+
this.migrateFromV2();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private migrateFromV1(): void {
|
|
342
|
+
// Migration logic from V1 to V3
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private migrateFromV2(): void {
|
|
346
|
+
// Migration logic from V2 to V3
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Then pass the version when upgrading:
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
// In the upgrade transaction
|
|
354
|
+
const migrationData = new BytesWriter(8);
|
|
355
|
+
migrationData.writeU64(1); // Migrating from version 1
|
|
356
|
+
|
|
357
|
+
Blockchain.updateContractFromExisting(newBytecodeAddress, migrationData);
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Plugin Support
|
|
361
|
+
|
|
362
|
+
Plugins can also implement `onUpdate` to perform their own migration logic:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
class MyPlugin extends Plugin {
|
|
366
|
+
public override onUpdate(calldata: Calldata): void {
|
|
367
|
+
// Plugin-specific migration logic
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Important Notes
|
|
373
|
+
|
|
374
|
+
1. **onUpdate runs on new bytecode**: The hook executes using the new contract's code, not the old one
|
|
375
|
+
2. **Storage is shared**: The new code reads/writes to the same storage slots as the old code
|
|
376
|
+
3. **Call super.onUpdate()**: Always call `super.onUpdate(calldata)` to ensure plugins are notified
|
|
377
|
+
4. **Empty calldata**: If no calldata was passed to `updateContractFromExisting`, an empty reader is provided
|
|
378
|
+
|
|
379
|
+
## Security Considerations
|
|
380
|
+
|
|
381
|
+
### Source Contract Validation
|
|
382
|
+
|
|
383
|
+
Always validate that the source address is an existing deployed contract:
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
if (!Blockchain.isContract(sourceAddress)) {
|
|
387
|
+
throw new Revert('Source must be a deployed contract');
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
This prevents an attacker from:
|
|
392
|
+
1. Submitting a not-yet-deployed address
|
|
393
|
+
2. Deploying malicious bytecode just before applying
|
|
394
|
+
3. Front-running the upgrade
|
|
395
|
+
|
|
396
|
+
### Address Verification at Apply
|
|
397
|
+
|
|
398
|
+
The `applyUpgrade` function requires the address parameter to match the pending upgrade:
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
if (!sourceAddress.equals(this.pendingUpgradeAddress)) {
|
|
402
|
+
throw new Revert('Address does not match pending upgrade');
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
This prevents front-running attacks where an attacker tries to substitute a different contract.
|
|
407
|
+
|
|
408
|
+
### Permission Model
|
|
409
|
+
|
|
410
|
+
The VM does not enforce any specific permission model. Implement appropriate access control:
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
// Simple: Deployer only
|
|
414
|
+
this.onlyDeployer(Blockchain.tx.sender);
|
|
415
|
+
|
|
416
|
+
// Advanced: Multisig or governance
|
|
417
|
+
if (!this.isAuthorizedUpgrader(Blockchain.tx.sender)) {
|
|
418
|
+
throw new Revert('Not authorized');
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Activation Boundary
|
|
423
|
+
|
|
424
|
+
When `updateContractFromExisting` is called:
|
|
425
|
+
- Transactions in the **same block** execute against **old bytecode**
|
|
426
|
+
- Transactions in **subsequent blocks** execute against **new bytecode**
|
|
427
|
+
|
|
428
|
+
This provides a clean transition with no mid-block ambiguity.
|
|
429
|
+
|
|
430
|
+
## Comparison with Other Platforms
|
|
431
|
+
|
|
432
|
+
| Feature | OPNet | Ethereum | Solana |
|
|
433
|
+
|---------|-------|----------|--------|
|
|
434
|
+
| Mechanism | VM opcode | Proxy + delegatecall | Upgrade authority |
|
|
435
|
+
| Delay | Contract-level (recommended) | Contract-level only | None (instant) |
|
|
436
|
+
| Storage | Same address, slots persist | Proxy storage, collision risk | Account data persists |
|
|
437
|
+
| Permission | Contract-defined | Proxy admin | Single authority key |
|
|
438
|
+
| Complexity | Low (single opcode) | High (proxy patterns) | Low (simple authority) |
|
|
439
|
+
|
|
440
|
+
## Complete Example
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
import {
|
|
444
|
+
Upgradeable,
|
|
445
|
+
Blockchain,
|
|
446
|
+
Calldata,
|
|
447
|
+
BytesWriter,
|
|
448
|
+
encodeSelector,
|
|
449
|
+
Selector,
|
|
450
|
+
StoredU256,
|
|
451
|
+
EMPTY_POINTER,
|
|
452
|
+
ADDRESS_BYTE_LENGTH,
|
|
453
|
+
} from '@btc-vision/btc-runtime/runtime';
|
|
454
|
+
|
|
455
|
+
@final
|
|
456
|
+
export class UpgradeableVault extends Upgradeable {
|
|
457
|
+
// 1-week upgrade delay for security
|
|
458
|
+
protected readonly upgradeDelay: u64 = 1008;
|
|
459
|
+
|
|
460
|
+
// Storage pointers - never reorder these
|
|
461
|
+
private totalDepositedPointer: u16 = Blockchain.nextPointer;
|
|
462
|
+
private totalDeposited: StoredU256;
|
|
463
|
+
|
|
464
|
+
public constructor() {
|
|
465
|
+
super();
|
|
466
|
+
this.totalDeposited = new StoredU256(
|
|
467
|
+
this.totalDepositedPointer,
|
|
468
|
+
EMPTY_POINTER
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
public override execute(method: Selector, calldata: Calldata): BytesWriter {
|
|
473
|
+
switch (method) {
|
|
474
|
+
// Upgrade methods
|
|
475
|
+
case encodeSelector('submitUpgrade'):
|
|
476
|
+
return this.submitUpgrade(calldata.readAddress());
|
|
477
|
+
case encodeSelector('applyUpgrade'): {
|
|
478
|
+
const sourceAddress = calldata.readAddress();
|
|
479
|
+
const remainingLength = calldata.byteLength - ADDRESS_BYTE_LENGTH;
|
|
480
|
+
const updateCalldata = new BytesWriter(remainingLength);
|
|
481
|
+
if (remainingLength > 0) {
|
|
482
|
+
updateCalldata.writeBytes(calldata.readBytes(remainingLength));
|
|
483
|
+
}
|
|
484
|
+
return this.applyUpgrade(sourceAddress, updateCalldata);
|
|
485
|
+
}
|
|
486
|
+
case encodeSelector('cancelUpgrade'):
|
|
487
|
+
return this.cancelUpgrade();
|
|
488
|
+
|
|
489
|
+
// View methods for upgrade status
|
|
490
|
+
case encodeSelector('pendingUpgrade'):
|
|
491
|
+
return this.getPendingUpgrade();
|
|
492
|
+
case encodeSelector('upgradeEffectiveBlock'):
|
|
493
|
+
return this.getUpgradeEffectiveBlock();
|
|
494
|
+
|
|
495
|
+
// Business logic
|
|
496
|
+
case encodeSelector('deposit'):
|
|
497
|
+
return this.deposit(calldata);
|
|
498
|
+
|
|
499
|
+
default:
|
|
500
|
+
return super.execute(method, calldata);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private getPendingUpgrade(): BytesWriter {
|
|
505
|
+
const response = new BytesWriter(40);
|
|
506
|
+
response.writeAddress(this.pendingUpgradeAddress);
|
|
507
|
+
response.writeU64(this.pendingUpgradeBlock);
|
|
508
|
+
return response;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private getUpgradeEffectiveBlock(): BytesWriter {
|
|
512
|
+
const response = new BytesWriter(8);
|
|
513
|
+
response.writeU64(this.upgradeEffectiveBlock);
|
|
514
|
+
return response;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private deposit(calldata: Calldata): BytesWriter {
|
|
518
|
+
// Business logic...
|
|
519
|
+
return new BytesWriter(0);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
## Upgrade Workflow
|
|
525
|
+
|
|
526
|
+
1. **Develop and test new version** on testnet
|
|
527
|
+
2. **Deploy new bytecode** as a separate contract
|
|
528
|
+
3. **Submit upgrade** with `submitUpgrade(newAddress)`
|
|
529
|
+
4. **Wait for delay** (users can exit during this period)
|
|
530
|
+
5. **Apply upgrade** with `applyUpgrade(newAddress)`
|
|
531
|
+
6. **Verify** new functionality works correctly
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
**Navigation:**
|
|
536
|
+
- Previous: [Bitcoin Scripts](./bitcoin-scripts.md)
|
|
537
|
+
- Next: [Plugins](./plugins.md)
|