@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,242 @@
|
|
|
1
|
+
import { u256 } from '@btc-vision/as-bignum/assembly';
|
|
2
|
+
import { Blockchain } from '../env';
|
|
3
|
+
import { OP_NET } from './OP_NET';
|
|
4
|
+
import { StoredAddress } from '../storage/StoredAddress';
|
|
5
|
+
import { StoredU256 } from '../storage/StoredU256';
|
|
6
|
+
import { Address } from '../types/Address';
|
|
7
|
+
import { Revert } from '../types/Revert';
|
|
8
|
+
import { BytesWriter } from '../buffer/BytesWriter';
|
|
9
|
+
import { EMPTY_POINTER } from '../math/bytes';
|
|
10
|
+
import {
|
|
11
|
+
UpgradeSubmittedEvent,
|
|
12
|
+
UpgradeAppliedEvent,
|
|
13
|
+
UpgradeCancelledEvent,
|
|
14
|
+
} from '../events/upgradeable/UpgradeableEvents';
|
|
15
|
+
|
|
16
|
+
const pendingUpgradeAddressPointer: u16 = Blockchain.nextPointer;
|
|
17
|
+
const pendingUpgradeBlockPointer: u16 = Blockchain.nextPointer;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Upgradeable - Base contract for upgradeable contracts with timelock protection.
|
|
21
|
+
*
|
|
22
|
+
* This contract provides a secure upgrade mechanism with a configurable delay period.
|
|
23
|
+
* The pattern prevents instant malicious upgrades by requiring:
|
|
24
|
+
* 1. submitUpgrade() - Submit the source contract address, starts the timelock
|
|
25
|
+
* 2. Wait for the delay period to pass
|
|
26
|
+
* 3. applyUpgrade() - Apply the upgrade after the delay
|
|
27
|
+
*
|
|
28
|
+
* Users can monitor for UpgradeSubmitted events and exit if they distrust pending changes.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* @final
|
|
33
|
+
* export class MyUpgradeableContract extends Upgradeable {
|
|
34
|
+
* // Set a 24-hour delay (144 blocks at 10 min/block)
|
|
35
|
+
* protected readonly upgradeDelay: u64 = 144;
|
|
36
|
+
*
|
|
37
|
+
* public override execute(method: Selector, calldata: Calldata): BytesWriter {
|
|
38
|
+
* switch (method) {
|
|
39
|
+
* case encodeSelector('submitUpgrade'):
|
|
40
|
+
* return this.submitUpgrade(calldata.readAddress());
|
|
41
|
+
* case encodeSelector('applyUpgrade'):
|
|
42
|
+
* const sourceAddress = calldata.readAddress();
|
|
43
|
+
* const updateCalldata = new BytesWriter(calldata.byteLength - ADDRESS_BYTE_LENGTH);
|
|
44
|
+
* // Copy remaining calldata for onUpdate
|
|
45
|
+
* return this.applyUpgrade(sourceAddress, updateCalldata);
|
|
46
|
+
* case encodeSelector('cancelUpgrade'):
|
|
47
|
+
* return this.cancelUpgrade();
|
|
48
|
+
* default:
|
|
49
|
+
* return super.execute(method, calldata);
|
|
50
|
+
* }
|
|
51
|
+
* }
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export class Upgradeable extends OP_NET {
|
|
56
|
+
/**
|
|
57
|
+
* The pending upgrade source address.
|
|
58
|
+
* Zero address means no pending upgrade.
|
|
59
|
+
*/
|
|
60
|
+
protected readonly _pendingUpgradeAddress: StoredAddress;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The block number when the upgrade was submitted.
|
|
64
|
+
* Stored as u256, used as u64.
|
|
65
|
+
*/
|
|
66
|
+
protected readonly _pendingUpgradeBlock: StoredU256;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The number of blocks to wait before an upgrade can be applied.
|
|
70
|
+
* Override this in derived contracts to set the delay.
|
|
71
|
+
*
|
|
72
|
+
* Common values:
|
|
73
|
+
* - 6 blocks = ~1 hour
|
|
74
|
+
* - 144 blocks = ~24 hours
|
|
75
|
+
* - 1008 blocks = ~1 week
|
|
76
|
+
*/
|
|
77
|
+
protected readonly upgradeDelay: u64 = 144; // ~24 hours default
|
|
78
|
+
|
|
79
|
+
protected constructor() {
|
|
80
|
+
super();
|
|
81
|
+
this._pendingUpgradeAddress = new StoredAddress(pendingUpgradeAddressPointer);
|
|
82
|
+
this._pendingUpgradeBlock = new StoredU256(pendingUpgradeBlockPointer, EMPTY_POINTER);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Returns the pending upgrade source address.
|
|
87
|
+
* Returns zero address if no upgrade is pending.
|
|
88
|
+
*/
|
|
89
|
+
public get pendingUpgradeAddress(): Address {
|
|
90
|
+
return this._pendingUpgradeAddress.value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Returns the block number when the pending upgrade was submitted.
|
|
95
|
+
* Returns 0 if no upgrade is pending.
|
|
96
|
+
*/
|
|
97
|
+
public get pendingUpgradeBlock(): u64 {
|
|
98
|
+
return this._pendingUpgradeBlock.value.lo1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Returns the block number when the pending upgrade can be applied.
|
|
103
|
+
* Returns 0 if no upgrade is pending.
|
|
104
|
+
*/
|
|
105
|
+
public get upgradeEffectiveBlock(): u64 {
|
|
106
|
+
const submitBlock = this.pendingUpgradeBlock;
|
|
107
|
+
if (submitBlock === 0) return 0;
|
|
108
|
+
return submitBlock + this.upgradeDelay;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Returns true if there is a pending upgrade.
|
|
113
|
+
*/
|
|
114
|
+
public get hasPendingUpgrade(): bool {
|
|
115
|
+
return this.pendingUpgradeBlock !== 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Returns true if the pending upgrade can be applied (delay has passed).
|
|
120
|
+
*/
|
|
121
|
+
public get canApplyUpgrade(): bool {
|
|
122
|
+
if (!this.hasPendingUpgrade) return false;
|
|
123
|
+
return Blockchain.block.number >= this.upgradeEffectiveBlock;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Submits an upgrade for timelock.
|
|
128
|
+
*
|
|
129
|
+
* The source address must be a deployed contract containing the new bytecode.
|
|
130
|
+
* After submission, the upgrade can only be applied after upgradeDelay blocks.
|
|
131
|
+
*
|
|
132
|
+
* Emits UpgradeSubmitted event.
|
|
133
|
+
*
|
|
134
|
+
* @param sourceAddress - The source contract address containing new bytecode
|
|
135
|
+
* @returns Empty response
|
|
136
|
+
* @throws If caller is not deployer
|
|
137
|
+
* @throws If source is not a deployed contract
|
|
138
|
+
* @throws If an upgrade is already pending
|
|
139
|
+
*/
|
|
140
|
+
protected submitUpgrade(sourceAddress: Address): BytesWriter {
|
|
141
|
+
this.onlyDeployer(Blockchain.tx.sender);
|
|
142
|
+
|
|
143
|
+
// Check no pending upgrade
|
|
144
|
+
if (this.hasPendingUpgrade) {
|
|
145
|
+
throw new Revert('Upgrade already pending. Cancel first.');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Validate source is a deployed contract
|
|
149
|
+
if (!Blockchain.isContract(sourceAddress)) {
|
|
150
|
+
throw new Revert('Source must be a deployed contract');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Store pending upgrade
|
|
154
|
+
const currentBlock = Blockchain.block.number;
|
|
155
|
+
this._pendingUpgradeAddress.value = sourceAddress;
|
|
156
|
+
this._pendingUpgradeBlock.value = u256.fromU64(currentBlock);
|
|
157
|
+
|
|
158
|
+
// Emit event
|
|
159
|
+
const effectiveBlock = currentBlock + this.upgradeDelay;
|
|
160
|
+
Blockchain.emit(new UpgradeSubmittedEvent(sourceAddress, currentBlock, effectiveBlock));
|
|
161
|
+
|
|
162
|
+
return new BytesWriter(0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Applies a pending upgrade after the timelock period has passed.
|
|
167
|
+
*
|
|
168
|
+
* The provided address must match the pending upgrade address as an
|
|
169
|
+
* additional security measure against front-running attacks.
|
|
170
|
+
*
|
|
171
|
+
* Emits UpgradeApplied event before the upgrade (new bytecode takes effect next block).
|
|
172
|
+
*
|
|
173
|
+
* @param sourceAddress - The source contract address (must match pending)
|
|
174
|
+
* @param calldata - The calldata to pass to onUpdate method of the new contract
|
|
175
|
+
* @returns Empty response
|
|
176
|
+
* @throws If caller is not deployer
|
|
177
|
+
* @throws If no upgrade is pending
|
|
178
|
+
* @throws If delay has not passed
|
|
179
|
+
* @throws If provided address does not match pending
|
|
180
|
+
*/
|
|
181
|
+
protected applyUpgrade(sourceAddress: Address, calldata: BytesWriter): BytesWriter {
|
|
182
|
+
this.onlyDeployer(Blockchain.tx.sender);
|
|
183
|
+
|
|
184
|
+
// Check pending upgrade exists
|
|
185
|
+
if (!this.hasPendingUpgrade) {
|
|
186
|
+
throw new Revert('No pending upgrade');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check delay has passed
|
|
190
|
+
if (!this.canApplyUpgrade) {
|
|
191
|
+
throw new Revert('Upgrade delay not elapsed');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Verify address matches pending
|
|
195
|
+
if (!sourceAddress.equals(this._pendingUpgradeAddress.value)) {
|
|
196
|
+
throw new Revert('Address does not match pending upgrade');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Clear pending state before upgrade
|
|
200
|
+
this._pendingUpgradeAddress.value = Address.zero();
|
|
201
|
+
this._pendingUpgradeBlock.value = u256.Zero;
|
|
202
|
+
|
|
203
|
+
// Emit event
|
|
204
|
+
Blockchain.emit(new UpgradeAppliedEvent(sourceAddress, Blockchain.block.number));
|
|
205
|
+
|
|
206
|
+
// Perform upgrade - new bytecode takes effect next block
|
|
207
|
+
Blockchain.updateContractFromExisting(sourceAddress, calldata);
|
|
208
|
+
|
|
209
|
+
return new BytesWriter(0);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Cancels a pending upgrade.
|
|
214
|
+
*
|
|
215
|
+
* Can only be called by the deployer. Clears the pending upgrade state.
|
|
216
|
+
*
|
|
217
|
+
* Emits UpgradeCancelled event.
|
|
218
|
+
*
|
|
219
|
+
* @returns Empty response
|
|
220
|
+
* @throws If caller is not deployer
|
|
221
|
+
* @throws If no upgrade is pending
|
|
222
|
+
*/
|
|
223
|
+
protected cancelUpgrade(): BytesWriter {
|
|
224
|
+
this.onlyDeployer(Blockchain.tx.sender);
|
|
225
|
+
|
|
226
|
+
// Check pending upgrade exists
|
|
227
|
+
if (!this.hasPendingUpgrade) {
|
|
228
|
+
throw new Revert('No pending upgrade');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const pendingAddress = this._pendingUpgradeAddress.value;
|
|
232
|
+
|
|
233
|
+
// Clear pending state
|
|
234
|
+
this._pendingUpgradeAddress.value = Address.zero();
|
|
235
|
+
this._pendingUpgradeBlock.value = u256.Zero;
|
|
236
|
+
|
|
237
|
+
// Emit event
|
|
238
|
+
Blockchain.emit(new UpgradeCancelledEvent(pendingAddress, Blockchain.block.number));
|
|
239
|
+
|
|
240
|
+
return new BytesWriter(0);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -22,12 +22,12 @@ import {
|
|
|
22
22
|
storePointer,
|
|
23
23
|
tLoadPointer,
|
|
24
24
|
tStorePointer,
|
|
25
|
+
updateFromAddress,
|
|
25
26
|
validateBitcoinAddress,
|
|
26
27
|
verifySignature,
|
|
27
28
|
} from './global';
|
|
28
29
|
import { eqUint, MapUint8Array } from '../generic/MapUint8Array';
|
|
29
30
|
import { EMPTY_BUFFER } from '../math/bytes';
|
|
30
|
-
import { Plugin } from '../plugins/Plugin';
|
|
31
31
|
import { Calldata } from '../types';
|
|
32
32
|
import { Revert } from '../types/Revert';
|
|
33
33
|
import { Selector } from '../math/abi';
|
|
@@ -75,7 +75,6 @@ export class BlockchainEnvironment {
|
|
|
75
75
|
private storage: MapUint8Array = new MapUint8Array();
|
|
76
76
|
private transientStorage: MapUint8Array = new MapUint8Array();
|
|
77
77
|
private _selfContract: Potential<OP_NET> = null;
|
|
78
|
-
private _plugins: Plugin[] = [];
|
|
79
78
|
private _network: Networks = Networks.Unknown;
|
|
80
79
|
|
|
81
80
|
/**
|
|
@@ -268,32 +267,29 @@ export class BlockchainEnvironment {
|
|
|
268
267
|
}
|
|
269
268
|
|
|
270
269
|
/**
|
|
271
|
-
*
|
|
270
|
+
* Handles contract deployment initialization.
|
|
272
271
|
*
|
|
273
|
-
* @param
|
|
272
|
+
* @param calldata - Deployment parameters
|
|
274
273
|
*
|
|
275
274
|
* @remarks
|
|
276
|
-
*
|
|
275
|
+
* Called once during deployment. Delegates to the contract's onDeployment
|
|
276
|
+
* which handles plugin notification.
|
|
277
277
|
*/
|
|
278
|
-
public
|
|
279
|
-
this.
|
|
278
|
+
public onDeployment(calldata: Calldata): void {
|
|
279
|
+
this.contract.onDeployment(calldata);
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
/**
|
|
283
|
-
* Handles contract
|
|
283
|
+
* Handles contract bytecode update.
|
|
284
284
|
*
|
|
285
|
-
* @param calldata -
|
|
285
|
+
* @param calldata - Update parameters passed to updateContractFromExisting
|
|
286
286
|
*
|
|
287
287
|
* @remarks
|
|
288
|
-
* Called
|
|
288
|
+
* Called when the contract's bytecode is updated. Delegates to the contract's
|
|
289
|
+
* onUpdate which handles plugin notification and migration logic.
|
|
289
290
|
*/
|
|
290
|
-
public
|
|
291
|
-
|
|
292
|
-
for (let i: i32 = 0; i < len; i++) {
|
|
293
|
-
// Unchecked access for speed
|
|
294
|
-
unchecked(this._plugins[i].onDeployment(calldata));
|
|
295
|
-
}
|
|
296
|
-
this.contract.onDeployment(calldata);
|
|
291
|
+
public onUpdate(calldata: Calldata): void {
|
|
292
|
+
this.contract.onUpdate(calldata);
|
|
297
293
|
}
|
|
298
294
|
|
|
299
295
|
/**
|
|
@@ -303,13 +299,9 @@ export class BlockchainEnvironment {
|
|
|
303
299
|
* @param calldata - Method parameters
|
|
304
300
|
*
|
|
305
301
|
* @remarks
|
|
306
|
-
*
|
|
302
|
+
* Delegates to the contract's onExecutionStarted which handles plugin notification.
|
|
307
303
|
*/
|
|
308
304
|
public onExecutionStarted(selector: Selector, calldata: Calldata): void {
|
|
309
|
-
const len = this._plugins.length;
|
|
310
|
-
for (let i: i32 = 0; i < len; i++) {
|
|
311
|
-
unchecked(this._plugins[i].onExecutionStarted(selector, calldata));
|
|
312
|
-
}
|
|
313
305
|
this.contract.onExecutionStarted(selector, calldata);
|
|
314
306
|
}
|
|
315
307
|
|
|
@@ -320,13 +312,9 @@ export class BlockchainEnvironment {
|
|
|
320
312
|
* @param calldata - Method parameters that were passed
|
|
321
313
|
*
|
|
322
314
|
* @remarks
|
|
323
|
-
*
|
|
315
|
+
* Delegates to the contract's onExecutionCompleted which handles plugin notification.
|
|
324
316
|
*/
|
|
325
317
|
public onExecutionCompleted(selector: Selector, calldata: Calldata): void {
|
|
326
|
-
const len = this._plugins.length;
|
|
327
|
-
for (let i: i32 = 0; i < len; i++) {
|
|
328
|
-
unchecked(this._plugins[i].onExecutionCompleted(selector, calldata));
|
|
329
|
-
}
|
|
330
318
|
this.contract.onExecutionCompleted(selector, calldata);
|
|
331
319
|
}
|
|
332
320
|
|
|
@@ -591,6 +579,115 @@ export class BlockchainEnvironment {
|
|
|
591
579
|
return contractAddressReader.readAddress();
|
|
592
580
|
}
|
|
593
581
|
|
|
582
|
+
/**
|
|
583
|
+
* Updates this contract's bytecode from an existing deployed contract.
|
|
584
|
+
*
|
|
585
|
+
* This method triggers a bytecode replacement where the calling contract's execution
|
|
586
|
+
* logic is replaced with the bytecode from the source contract. The new bytecode
|
|
587
|
+
* takes effect at the next block.
|
|
588
|
+
*
|
|
589
|
+
* @param sourceAddress - Address of the contract containing the new bytecode
|
|
590
|
+
* @param [calldata] - Optional parameters passed to the new bytecode's onUpdate method
|
|
591
|
+
* @throws {Revert} When the source address is invalid or the update fails
|
|
592
|
+
*
|
|
593
|
+
* @warning This is a privileged operation with significant implications:
|
|
594
|
+
* - Storage layout compatibility is entirely the developer's responsibility
|
|
595
|
+
* - The contract address and all storage slots persist unchanged
|
|
596
|
+
* - Only the execution logic changes
|
|
597
|
+
* - The source contract must be an already-deployed contract
|
|
598
|
+
*
|
|
599
|
+
* @remarks
|
|
600
|
+
* Contracts should implement their own permission checks and optional timelock
|
|
601
|
+
* patterns before calling this method. A recommended pattern is:
|
|
602
|
+
*
|
|
603
|
+
* 1. `submitUpdate(address)` - Logs source address and block number, validates
|
|
604
|
+
* that the address is an existing deployed contract
|
|
605
|
+
* 2. `applyUpdate(address)` - Can only be called after a configured delay,
|
|
606
|
+
* verifies address matches the submitted one, then calls this method
|
|
607
|
+
*
|
|
608
|
+
* This pattern gives users time to assess pending changes and exit if needed.
|
|
609
|
+
*
|
|
610
|
+
* @example
|
|
611
|
+
* ```typescript
|
|
612
|
+
* // Simple immediate update (not recommended for production)
|
|
613
|
+
* public upgrade(calldata: Calldata): BytesWriter {
|
|
614
|
+
* this.onlyDeployer(Blockchain.tx.sender);
|
|
615
|
+
* const newBytecodeAddress = calldata.readAddress();
|
|
616
|
+
* Blockchain.updateContractFromExisting(newBytecodeAddress);
|
|
617
|
+
* return new BytesWriter(0);
|
|
618
|
+
* }
|
|
619
|
+
* ```
|
|
620
|
+
*
|
|
621
|
+
* @example
|
|
622
|
+
* ```typescript
|
|
623
|
+
* // Timelock pattern (recommended)
|
|
624
|
+
* private pendingUpdatePointer: u16 = Blockchain.nextPointer;
|
|
625
|
+
* private pendingUpdate: StoredAddress = new StoredAddress(this.pendingUpdatePointer);
|
|
626
|
+
*
|
|
627
|
+
* private pendingUpdateBlockPointer: u16 = Blockchain.nextPointer;
|
|
628
|
+
* private pendingUpdateBlock: StoredU64 = new StoredU64(this.pendingUpdateBlockPointer);
|
|
629
|
+
*
|
|
630
|
+
* private readonly UPDATE_DELAY: u64 = 144; // ~1 day in blocks
|
|
631
|
+
*
|
|
632
|
+
* public submitUpdate(calldata: Calldata): BytesWriter {
|
|
633
|
+
* this.onlyDeployer(Blockchain.tx.sender);
|
|
634
|
+
* const sourceAddress = calldata.readAddress();
|
|
635
|
+
*
|
|
636
|
+
* // Validate source is an existing contract
|
|
637
|
+
* if (!Blockchain.isContract(sourceAddress)) {
|
|
638
|
+
* throw new Revert('Source must be a deployed contract');
|
|
639
|
+
* }
|
|
640
|
+
*
|
|
641
|
+
* this.pendingUpdate.value = sourceAddress;
|
|
642
|
+
* this.pendingUpdateBlock.value = Blockchain.block.number;
|
|
643
|
+
* return new BytesWriter(0);
|
|
644
|
+
* }
|
|
645
|
+
*
|
|
646
|
+
* public applyUpdate(calldata: Calldata): BytesWriter {
|
|
647
|
+
* this.onlyDeployer(Blockchain.tx.sender);
|
|
648
|
+
* const sourceAddress = calldata.readAddress();
|
|
649
|
+
*
|
|
650
|
+
* // Verify address matches pending update
|
|
651
|
+
* if (!sourceAddress.equals(this.pendingUpdate.value)) {
|
|
652
|
+
* throw new Revert('Address does not match pending update');
|
|
653
|
+
* }
|
|
654
|
+
*
|
|
655
|
+
* // Verify delay has passed
|
|
656
|
+
* const submitBlock = this.pendingUpdateBlock.value;
|
|
657
|
+
* if (Blockchain.block.number < submitBlock + this.UPDATE_DELAY) {
|
|
658
|
+
* throw new Revert('Update delay not elapsed');
|
|
659
|
+
* }
|
|
660
|
+
*
|
|
661
|
+
* Blockchain.updateContractFromExisting(sourceAddress);
|
|
662
|
+
* return new BytesWriter(0);
|
|
663
|
+
* }
|
|
664
|
+
* ```
|
|
665
|
+
*/
|
|
666
|
+
public updateContractFromExisting(
|
|
667
|
+
sourceAddress: Address,
|
|
668
|
+
calldata: BytesWriter | null = null,
|
|
669
|
+
): void {
|
|
670
|
+
if (!sourceAddress) {
|
|
671
|
+
throw new Revert('Source address is required');
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (!this.isContract(sourceAddress)) {
|
|
675
|
+
throw new Revert('Source address must be a deployed contract');
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const callDataBuffer = calldata ? calldata.getBuffer().buffer : new ArrayBuffer(0);
|
|
679
|
+
|
|
680
|
+
const status = updateFromAddress(
|
|
681
|
+
sourceAddress.buffer,
|
|
682
|
+
callDataBuffer,
|
|
683
|
+
callDataBuffer.byteLength,
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
if (status !== 0) {
|
|
687
|
+
throw new Revert('Failed to update contract bytecode');
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
594
691
|
/**
|
|
595
692
|
* Reads a value from persistent storage.
|
|
596
693
|
*
|
package/runtime/env/global.ts
CHANGED
|
@@ -65,6 +65,30 @@ export declare function tStorePointer(key: ArrayBuffer, value: ArrayBuffer): voi
|
|
|
65
65
|
@external('env', 'deployFromAddress')
|
|
66
66
|
export declare function deployFromAddress(originAddress: ArrayBuffer, salt: ArrayBuffer, calldata: ArrayBuffer, calldataLength: u32, resultAddress: ArrayBuffer): u32;
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Updates the calling contract's bytecode from an existing contract.
|
|
70
|
+
*
|
|
71
|
+
* This VM opcode enables bytecode replacement where a contract can replace its own
|
|
72
|
+
* execution logic by referencing another deployed contract containing the new WASM bytecode.
|
|
73
|
+
* The new bytecode takes effect at the next block.
|
|
74
|
+
*
|
|
75
|
+
* @param {ArrayBuffer} sourceAddress - The address of the contract containing the new bytecode.
|
|
76
|
+
* @param {ArrayBuffer} calldata - The calldata for the update (passed to onUpdate if implemented).
|
|
77
|
+
* @param {u32} calldataLength - The length of the calldata.
|
|
78
|
+
* @returns {u32} - Status code (0 = success, non-zero = failure).
|
|
79
|
+
*
|
|
80
|
+
* @remarks
|
|
81
|
+
* - The source contract must be an already-deployed contract
|
|
82
|
+
* - Storage layout compatibility is the developer's responsibility
|
|
83
|
+
* - The contract address and all storage slots persist unchanged
|
|
84
|
+
* - Only the execution logic changes
|
|
85
|
+
*
|
|
86
|
+
* @warning This is a privileged operation. Contracts should implement their own
|
|
87
|
+
* permission checks and optional timelock patterns before calling this.
|
|
88
|
+
*/
|
|
89
|
+
@external('env', 'updateFromAddress')
|
|
90
|
+
export declare function updateFromAddress(sourceAddress: ArrayBuffer, calldata: ArrayBuffer, calldataLength: u32): u32;
|
|
91
|
+
|
|
68
92
|
/**
|
|
69
93
|
* Calls a contract.
|
|
70
94
|
* @param {ArrayBuffer} address - The address of the contract to call.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { NetEvent } from '../NetEvent';
|
|
2
|
+
import { BytesWriter } from '../../buffer/BytesWriter';
|
|
3
|
+
import { Address } from '../../types/Address';
|
|
4
|
+
import { ADDRESS_BYTE_LENGTH } from '../../utils';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Event emitted when an upgrade is submitted for timelock.
|
|
8
|
+
*/
|
|
9
|
+
export class UpgradeSubmittedEvent extends NetEvent {
|
|
10
|
+
constructor(sourceAddress: Address, submitBlock: u64, effectiveBlock: u64) {
|
|
11
|
+
const data = new BytesWriter(ADDRESS_BYTE_LENGTH + 16);
|
|
12
|
+
data.writeAddress(sourceAddress);
|
|
13
|
+
data.writeU64(submitBlock);
|
|
14
|
+
data.writeU64(effectiveBlock);
|
|
15
|
+
super('UpgradeSubmitted', data);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Event emitted when an upgrade is applied.
|
|
21
|
+
*/
|
|
22
|
+
export class UpgradeAppliedEvent extends NetEvent {
|
|
23
|
+
constructor(sourceAddress: Address, appliedAtBlock: u64) {
|
|
24
|
+
const data = new BytesWriter(ADDRESS_BYTE_LENGTH + 8);
|
|
25
|
+
data.writeAddress(sourceAddress);
|
|
26
|
+
data.writeU64(appliedAtBlock);
|
|
27
|
+
super('UpgradeApplied', data);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Event emitted when a pending upgrade is cancelled.
|
|
33
|
+
*/
|
|
34
|
+
export class UpgradeCancelledEvent extends NetEvent {
|
|
35
|
+
constructor(sourceAddress: Address, cancelledAtBlock: u64) {
|
|
36
|
+
const data = new BytesWriter(ADDRESS_BYTE_LENGTH + 8);
|
|
37
|
+
data.writeAddress(sourceAddress);
|
|
38
|
+
data.writeU64(cancelledAtBlock);
|
|
39
|
+
super('UpgradeCancelled', data);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -26,23 +26,7 @@ export class AddressMap<V> implements IMap<Address, V> {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
public set(key: Address, value: V): this {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// Check Cache (Fastest)
|
|
32
|
-
if (this._lastIndex !== -1) {
|
|
33
|
-
const cachedKey = unchecked(this._keys[this._lastIndex]);
|
|
34
|
-
// Check length first, then full content equality
|
|
35
|
-
if (cachedKey.length === key.length) {
|
|
36
|
-
if (memory.compare(cachedKey.dataStart, key.dataStart, key.length) === 0) {
|
|
37
|
-
index = this._lastIndex;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Full Scan if cache missed
|
|
43
|
-
if (index === -1) {
|
|
44
|
-
index = this.indexOf(key);
|
|
45
|
-
}
|
|
29
|
+
const index = this.indexOf(key);
|
|
46
30
|
|
|
47
31
|
if (index === -1) {
|
|
48
32
|
this._keys.push(key);
|
|
@@ -50,7 +34,7 @@ export class AddressMap<V> implements IMap<Address, V> {
|
|
|
50
34
|
this._lastIndex = this._keys.length - 1;
|
|
51
35
|
} else {
|
|
52
36
|
unchecked((this._values[index] = value));
|
|
53
|
-
// Cache is already pointing to this index (from indexOf
|
|
37
|
+
// Cache is already pointing to this index (from indexOf)
|
|
54
38
|
this._lastIndex = index;
|
|
55
39
|
}
|
|
56
40
|
|
|
@@ -63,6 +47,10 @@ export class AddressMap<V> implements IMap<Address, V> {
|
|
|
63
47
|
* Uses a "Prefix Filter" to skip expensive memory comparisons.
|
|
64
48
|
*/
|
|
65
49
|
public indexOf(pointerHash: Address): i32 {
|
|
50
|
+
if (this.isLastIndex(pointerHash)) {
|
|
51
|
+
return this._lastIndex;
|
|
52
|
+
}
|
|
53
|
+
|
|
66
54
|
const len = this._keys.length;
|
|
67
55
|
if (len === 0) return -1;
|
|
68
56
|
|
|
@@ -111,6 +99,20 @@ export class AddressMap<V> implements IMap<Address, V> {
|
|
|
111
99
|
return -1;
|
|
112
100
|
}
|
|
113
101
|
|
|
102
|
+
private isLastIndex(key: Uint8Array): bool {
|
|
103
|
+
if (this._lastIndex !== -1) {
|
|
104
|
+
const cachedKey = unchecked(this._keys[this._lastIndex]);
|
|
105
|
+
// Check length first, then full content equality
|
|
106
|
+
if (cachedKey.length === key.length) {
|
|
107
|
+
if (memory.compare(cachedKey.dataStart, key.dataStart, key.length) === 0) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
114
116
|
@inline
|
|
115
117
|
public has(key: Address): bool {
|
|
116
118
|
return this.indexOf(key) !== -1;
|