@btc-vision/btc-runtime 1.10.8 → 1.10.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +258 -137
  3. package/SECURITY.md +226 -0
  4. package/docs/README.md +614 -0
  5. package/docs/advanced/bitcoin-scripts.md +939 -0
  6. package/docs/advanced/cross-contract-calls.md +579 -0
  7. package/docs/advanced/plugins.md +1006 -0
  8. package/docs/advanced/quantum-resistance.md +660 -0
  9. package/docs/advanced/signature-verification.md +715 -0
  10. package/docs/api-reference/blockchain.md +729 -0
  11. package/docs/api-reference/events.md +642 -0
  12. package/docs/api-reference/op20.md +902 -0
  13. package/docs/api-reference/op721.md +819 -0
  14. package/docs/api-reference/safe-math.md +510 -0
  15. package/docs/api-reference/storage.md +840 -0
  16. package/docs/contracts/op-net-base.md +786 -0
  17. package/docs/contracts/op20-token.md +687 -0
  18. package/docs/contracts/op20s-signatures.md +614 -0
  19. package/docs/contracts/op721-nft.md +785 -0
  20. package/docs/contracts/reentrancy-guard.md +787 -0
  21. package/docs/core-concepts/blockchain-environment.md +724 -0
  22. package/docs/core-concepts/decorators.md +466 -0
  23. package/docs/core-concepts/events.md +652 -0
  24. package/docs/core-concepts/pointers.md +391 -0
  25. package/docs/core-concepts/security.md +473 -0
  26. package/docs/core-concepts/storage-system.md +969 -0
  27. package/docs/examples/basic-token.md +745 -0
  28. package/docs/examples/nft-with-reservations.md +1440 -0
  29. package/docs/examples/oracle-integration.md +1212 -0
  30. package/docs/examples/stablecoin.md +1180 -0
  31. package/docs/getting-started/first-contract.md +575 -0
  32. package/docs/getting-started/installation.md +384 -0
  33. package/docs/getting-started/project-structure.md +630 -0
  34. package/docs/storage/memory-maps.md +764 -0
  35. package/docs/storage/stored-arrays.md +778 -0
  36. package/docs/storage/stored-maps.md +758 -0
  37. package/docs/storage/stored-primitives.md +655 -0
  38. package/docs/types/address.md +773 -0
  39. package/docs/types/bytes-writer-reader.md +938 -0
  40. package/docs/types/calldata.md +744 -0
  41. package/docs/types/safe-math.md +446 -0
  42. package/package.json +52 -27
  43. package/runtime/memory/MapOfMap.ts +1 -0
  44. package/LICENSE.md +0 -21
@@ -0,0 +1,1212 @@
1
+ # Oracle Integration Example
2
+
3
+ A multi-oracle price aggregation system that demonstrates how to build reliable price feeds for DeFi applications.
4
+
5
+ ## Overview
6
+
7
+ This example demonstrates:
8
+ - Multiple oracle sources
9
+ - Price aggregation strategies
10
+ - Stale data protection
11
+ - Deviation thresholds
12
+ - Admin controls for oracle management
13
+ - Decorators for ABI generation
14
+
15
+ ## Multi-Oracle Price Aggregation
16
+
17
+ The oracle system collects prices from multiple sources and aggregates them using a median calculation:
18
+
19
+ ```mermaid
20
+ ---
21
+ config:
22
+ theme: dark
23
+ ---
24
+ flowchart LR
25
+ A["👤 User: Oracles submit prices"] --> B[Collect prices]
26
+ B --> C{Fresh prices?}
27
+ C -->|Stale| D[Reject]
28
+ C -->|Fresh| E{Enough oracles?}
29
+ E -->|No| F[Skip update]
30
+ E -->|Yes| G[Calculate median]
31
+ G --> H{Within deviation?}
32
+ H -->|No| I[Reject]
33
+ H -->|Yes| J[Update price]
34
+ J --> K[Write to storage]
35
+ K --> L[Emit event]
36
+ ```
37
+
38
+ ## Median Calculation Process
39
+
40
+ The median is calculated by sorting all submitted prices and taking the middle value:
41
+
42
+ ```mermaid
43
+ ---
44
+ config:
45
+ theme: dark
46
+ ---
47
+ flowchart LR
48
+ subgraph "Input Data"
49
+ A["Raw Prices:<br/>[$50,200, $50,000, $50,100]"]
50
+ end
51
+
52
+ subgraph "Processing"
53
+ B[Bubble Sort Algorithm]
54
+ C["Sorted:<br/>[$50,000, $50,100, $50,200]"]
55
+ D{Array Length}
56
+ E["Take Middle Value<br/>prices[length/2]"]
57
+ F["Average Two Middle Values<br/>(prices[mid-1] + prices[mid]) / 2"]
58
+ end
59
+
60
+ subgraph "Result"
61
+ G["Final Median Price:<br/>$50,100"]
62
+ end
63
+
64
+ A --> B
65
+ B --> C
66
+ C --> D
67
+ D -->|Odd Length| E
68
+ D -->|Even Length| F
69
+ E --> G
70
+ F --> G
71
+ ```
72
+
73
+ ### Median Implementation
74
+
75
+ ```typescript
76
+ private calculateMedian(prices: u256[]): u256 {
77
+ const len = prices.length;
78
+
79
+ // Simple bubble sort for small arrays
80
+ for (let i = 0; i < len; i++) {
81
+ for (let j = i + 1; j < len; j++) {
82
+ if (prices[j] < prices[i]) {
83
+ const temp = prices[i];
84
+ prices[i] = prices[j];
85
+ prices[j] = temp;
86
+ }
87
+ }
88
+ }
89
+
90
+ const mid = len / 2;
91
+ if (len % 2 == 0) {
92
+ // Average of two middle values
93
+ return SafeMath.div(
94
+ SafeMath.add(prices[mid - 1], prices[mid]),
95
+ u256.fromU64(2)
96
+ );
97
+ } else {
98
+ return prices[mid];
99
+ }
100
+ }
101
+ ```
102
+
103
+ **Solidity Comparison:**
104
+
105
+ ```solidity
106
+ // Solidity - Chainlink-style aggregator
107
+ function latestRoundData() external view returns (
108
+ uint80 roundId,
109
+ int256 answer,
110
+ uint256 startedAt,
111
+ uint256 updatedAt,
112
+ uint80 answeredInRound
113
+ );
114
+
115
+ // OPNet - Custom multi-oracle aggregation
116
+ @method({ name: 'asset', type: ABIDataTypes.ADDRESS })
117
+ @returns(
118
+ { name: 'price', type: ABIDataTypes.UINT256 },
119
+ { name: 'timestamp', type: ABIDataTypes.UINT64 },
120
+ )
121
+ public getPrice(calldata: Calldata): BytesWriter { }
122
+ ```
123
+
124
+ ## Deviation Check
125
+
126
+ Price updates are validated against deviation thresholds to prevent manipulation:
127
+
128
+ ```mermaid
129
+ ---
130
+ config:
131
+ theme: dark
132
+ ---
133
+ flowchart LR
134
+ A["👤 User: Oracle submits price"] --> B{Current price exists?}
135
+ B -->|No| C[Accept first price]
136
+ B -->|Yes| D[Calculate bounds]
137
+ D --> E{Within bounds?}
138
+ E -->|No| F[Reject: Too much change]
139
+ E -->|Yes| G[Accept price]
140
+ G --> H[Write to storage]
141
+ ```
142
+
143
+ ### Deviation Implementation
144
+
145
+ ```typescript
146
+ private withinDeviation(oldPrice: u256, newPrice: u256): bool {
147
+ const maxDev = this._maxDeviation.value;
148
+ const basisPoints = u256.fromU64(10000);
149
+
150
+ // Calculate allowed deviation
151
+ const maxChange = SafeMath.div(SafeMath.mul(oldPrice, maxDev), basisPoints);
152
+
153
+ // Check if new price is within range
154
+ const lowerBound = SafeMath.sub(oldPrice, maxChange);
155
+ const upperBound = SafeMath.add(oldPrice, maxChange);
156
+
157
+ return newPrice >= lowerBound && newPrice <= upperBound;
158
+ }
159
+ ```
160
+
161
+ ## Price Submission and Retrieval
162
+
163
+ The complete flow shows how multiple oracles submit prices and how consumers retrieve the aggregated price:
164
+
165
+ ```mermaid
166
+ sequenceDiagram
167
+ participant Oracle1 as 👤 Oracle 1
168
+ participant Oracle2 as 👤 Oracle 2
169
+ participant Oracle3 as 👤 Oracle 3
170
+ participant BTC as Bitcoin Network
171
+ participant Contract as Contract Execution
172
+ participant Storage as Storage Layer
173
+ participant Consumer as 👤 DeFi App
174
+
175
+ Note over Oracle1,Oracle3: Multiple oracles submit prices
176
+
177
+ Oracle1->>BTC: Submit submitPrice(BTC, $50,100) TX
178
+ BTC->>Contract: Execute submitPrice
179
+ Contract->>Contract: Verify oracle authorized
180
+ Contract->>Storage: Write oracle1 price + timestamp
181
+ Contract->>Contract: tryUpdatePrice(BTC)
182
+ Contract->>Storage: Read all oracle prices
183
+ Contract->>Contract: Filter stale prices
184
+ Note over Contract: Only 1 oracle, skip update (need 3)
185
+ Contract-->>BTC: Success
186
+ BTC-->>Oracle1: TX Confirmed
187
+
188
+ Oracle2->>BTC: Submit submitPrice(BTC, $50,000) TX
189
+ BTC->>Contract: Execute submitPrice
190
+ Contract->>Contract: Verify oracle authorized
191
+ Contract->>Storage: Write oracle2 price + timestamp
192
+ Contract->>Contract: tryUpdatePrice(BTC)
193
+ Contract->>Storage: Read all oracle prices
194
+ Note over Contract: Only 2 oracles, skip update (need 3)
195
+ Contract-->>BTC: Success
196
+ BTC-->>Oracle2: TX Confirmed
197
+
198
+ Oracle3->>BTC: Submit submitPrice(BTC, $50,200) TX
199
+ BTC->>Contract: Execute submitPrice
200
+ Contract->>Contract: Verify oracle authorized
201
+ Contract->>Storage: Write oracle3 price + timestamp
202
+ Contract->>Contract: tryUpdatePrice(BTC)
203
+ Contract->>Storage: Read all oracle prices
204
+ Contract->>Contract: Calculate median: $50,100
205
+ Contract->>Contract: Check deviation bounds
206
+ Contract->>Storage: Write aggregated price
207
+ Contract->>Contract: Emit PriceUpdated($50,100)
208
+ Contract-->>BTC: Success
209
+ BTC-->>Oracle3: TX Confirmed
210
+
211
+ Note over Consumer,Storage: Later: DeFi app needs price
212
+
213
+ Consumer->>BTC: Submit getPrice(BTC) TX
214
+ BTC->>Contract: Execute getPrice
215
+ Contract->>Storage: Read price + timestamp
216
+ Storage-->>Contract: $50,100, timestamp
217
+ Contract->>Contract: Check staleness
218
+ alt Price is stale
219
+ Contract-->>BTC: Revert: Price is stale
220
+ BTC-->>Consumer: TX Failed
221
+ else Price is fresh
222
+ Contract-->>BTC: Return ($50,100, timestamp)
223
+ BTC-->>Consumer: TX Success
224
+ end
225
+
226
+ Note over Contract,Storage: minOracles = 3, maxDeviation = 5%, maxStaleness = 1 hour
227
+ ```
228
+
229
+ ## Oracle Management
230
+
231
+ Oracles can be added and removed by the contract deployer:
232
+
233
+ ```mermaid
234
+ stateDiagram-v2
235
+ [*] --> OracleAdded: addOracle() TX
236
+ OracleAdded --> Active: Oracle can submit prices
237
+ Active --> OracleRemoved: removeOracle() TX
238
+ OracleRemoved --> [*]
239
+
240
+ state Active {
241
+ [*] --> Waiting
242
+ Waiting --> PriceSubmitted: submitPrice() TX
243
+ PriceSubmitted --> Waiting
244
+ }
245
+
246
+ note right of OracleAdded
247
+ Only deployer can add
248
+ Duplicate check performed
249
+ Emit OracleAdded event
250
+ end note
251
+
252
+ note left of OracleRemoved
253
+ Only deployer can remove
254
+ Must maintain minOracles
255
+ Emit OracleRemoved event
256
+ end note
257
+
258
+ note right of PriceSubmitted
259
+ Price stored with timestamp
260
+ Triggers aggregation attempt
261
+ Individual oracle data tracked
262
+ end note
263
+ ```
264
+
265
+ ## Complete Implementation
266
+
267
+ ```typescript
268
+ import { u256 } from '@btc-vision/as-bignum/assembly';
269
+ import {
270
+ OP_NET,
271
+ Blockchain,
272
+ Address,
273
+ Calldata,
274
+ BytesWriter,
275
+ SafeMath,
276
+ Revert,
277
+ NetEvent,
278
+ StoredU256,
279
+ StoredU64,
280
+ StoredU8,
281
+ StoredAddressArray,
282
+ AddressMemoryMap,
283
+ ABIDataTypes,
284
+ sha256,
285
+ encodePointer,
286
+ } from '@btc-vision/btc-runtime/runtime';
287
+
288
+ // Events
289
+ class PriceUpdated extends NetEvent {
290
+ public constructor(
291
+ public readonly asset: Address,
292
+ public readonly price: u256,
293
+ public readonly timestamp: u64
294
+ ) {
295
+ super('PriceUpdated');
296
+ }
297
+
298
+ protected override encodeData(writer: BytesWriter): void {
299
+ writer.writeAddress(this.asset);
300
+ writer.writeU256(this.price);
301
+ writer.writeU64(this.timestamp);
302
+ }
303
+ }
304
+
305
+ class OracleAdded extends NetEvent {
306
+ public constructor(public readonly oracle: Address) {
307
+ super('OracleAdded');
308
+ }
309
+
310
+ protected override encodeData(writer: BytesWriter): void {
311
+ writer.writeAddress(this.oracle);
312
+ }
313
+ }
314
+
315
+ class OracleRemoved extends NetEvent {
316
+ public constructor(public readonly oracle: Address) {
317
+ super('OracleRemoved');
318
+ }
319
+
320
+ protected override encodeData(writer: BytesWriter): void {
321
+ writer.writeAddress(this.oracle);
322
+ }
323
+ }
324
+
325
+ @final
326
+ export class MultiOracle extends OP_NET {
327
+ // Oracle list
328
+ private oraclesPointer: u16 = Blockchain.nextPointer;
329
+ private oracles: StoredAddressArray;
330
+
331
+ // Configuration
332
+ private minOraclesPointer: u16 = Blockchain.nextPointer;
333
+ private maxDeviationPointer: u16 = Blockchain.nextPointer;
334
+ private maxStalenessPointer: u16 = Blockchain.nextPointer;
335
+
336
+ private _minOracles: StoredU8;
337
+ private _maxDeviation: StoredU256; // In basis points (100 = 1%)
338
+ private _maxStaleness: StoredU64; // In seconds
339
+
340
+ // Price data per asset
341
+ private pricesPointer: u16 = Blockchain.nextPointer;
342
+ private timestampsPointer: u16 = Blockchain.nextPointer;
343
+
344
+ private _prices: AddressMemoryMap;
345
+ private _timestamps: AddressMemoryMap;
346
+
347
+ // Individual oracle submissions
348
+ private oraclePricesPointer: u16 = Blockchain.nextPointer;
349
+ private oracleTimestampsPointer: u16 = Blockchain.nextPointer;
350
+
351
+ public constructor() {
352
+ super();
353
+
354
+ this.oracles = new StoredAddressArray(this.oraclesPointer);
355
+ this._minOracles = new StoredU8(this.minOraclesPointer, 1);
356
+ this._maxDeviation = new StoredU256(this.maxDeviationPointer, EMPTY_POINTER);
357
+ this._maxStaleness = new StoredU64(this.maxStalenessPointer, 3600); // 1 hour
358
+
359
+ this._prices = new AddressMemoryMap(this.pricesPointer);
360
+ this._timestamps = new AddressMemoryMap(this.timestampsPointer);
361
+ }
362
+
363
+ public override onDeployment(calldata: Calldata): void {
364
+ const minOracles = calldata.readU8();
365
+ const maxDeviation = calldata.readU256();
366
+ const maxStaleness = calldata.readU64();
367
+ const initialOracles = calldata.readAddressArray();
368
+
369
+ this._minOracles.value = minOracles;
370
+ this._maxDeviation.value = maxDeviation;
371
+ this._maxStaleness.value = maxStaleness;
372
+
373
+ // Add initial oracles
374
+ for (let i = 0; i < initialOracles.length; i++) {
375
+ this.oracles.push(initialOracles[i]);
376
+ this.emitEvent(new OracleAdded(initialOracles[i]));
377
+ }
378
+ }
379
+
380
+ // ============ ORACLE MANAGEMENT ============
381
+
382
+ @method({ name: 'oracle', type: ABIDataTypes.ADDRESS })
383
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
384
+ @emit('OracleAdded')
385
+ public addOracle(calldata: Calldata): BytesWriter {
386
+ this.onlyDeployer(Blockchain.tx.sender);
387
+
388
+ const oracle = calldata.readAddress();
389
+
390
+ // Check not already added
391
+ const length = this.oracles.length;
392
+ for (let i: u64 = 0; i < length; i++) {
393
+ if (this.oracles.get(i).equals(oracle)) {
394
+ throw new Revert('Oracle already exists');
395
+ }
396
+ }
397
+
398
+ this.oracles.push(oracle);
399
+ this.emitEvent(new OracleAdded(oracle));
400
+
401
+ return new BytesWriter(0);
402
+ }
403
+
404
+ @method({ name: 'oracle', type: ABIDataTypes.ADDRESS })
405
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
406
+ @emit('OracleRemoved')
407
+ public removeOracle(calldata: Calldata): BytesWriter {
408
+ this.onlyDeployer(Blockchain.tx.sender);
409
+
410
+ const oracle = calldata.readAddress();
411
+
412
+ // Find and remove
413
+ let found = false;
414
+ const length = this.oracles.length;
415
+
416
+ for (let i: u64 = 0; i < length; i++) {
417
+ if (this.oracles.get(i).equals(oracle)) {
418
+ // Swap with last and pop
419
+ if (i < length - 1) {
420
+ this.oracles.set(i, this.oracles.get(length - 1));
421
+ }
422
+ this.oracles.pop();
423
+ found = true;
424
+ break;
425
+ }
426
+ }
427
+
428
+ if (!found) {
429
+ throw new Revert('Oracle not found');
430
+ }
431
+
432
+ // Ensure minimum oracles remain
433
+ if (this.oracles.length < u64(this._minOracles.value)) {
434
+ throw new Revert('Would go below minimum oracles');
435
+ }
436
+
437
+ this.emitEvent(new OracleRemoved(oracle));
438
+
439
+ return new BytesWriter(0);
440
+ }
441
+
442
+ // ============ PRICE SUBMISSION ============
443
+
444
+ /**
445
+ * Submit price update from authorized oracle.
446
+ */
447
+ @method(
448
+ { name: 'asset', type: ABIDataTypes.ADDRESS },
449
+ { name: 'price', type: ABIDataTypes.UINT256 },
450
+ )
451
+ @returns({ name: 'success', type: ABIDataTypes.BOOL })
452
+ @emit('PriceUpdated')
453
+ public submitPrice(calldata: Calldata): BytesWriter {
454
+ const oracle = Blockchain.tx.sender;
455
+
456
+ // Verify sender is authorized oracle
457
+ if (!this.isOracle(oracle)) {
458
+ throw new Revert('Not authorized oracle');
459
+ }
460
+
461
+ const asset = calldata.readAddress();
462
+ const price = calldata.readU256();
463
+
464
+ // Store individual oracle submission
465
+ const key = this.oracleAssetKey(oracle, asset);
466
+ this.setOraclePrice(key, price);
467
+ this.setOracleTimestamp(key, Blockchain.block.medianTime);
468
+
469
+ // Try to update aggregated price
470
+ this.tryUpdatePrice(asset);
471
+
472
+ return new BytesWriter(0);
473
+ }
474
+
475
+ /**
476
+ * Attempt to aggregate and update the asset price.
477
+ */
478
+ private tryUpdatePrice(asset: Address): void {
479
+ const prices: u256[] = [];
480
+ const now = Blockchain.block.medianTime;
481
+ const maxStale = this._maxStaleness.value;
482
+
483
+ // Collect valid prices from all oracles
484
+ const oracleCount = this.oracles.length;
485
+ for (let i: u64 = 0; i < oracleCount; i++) {
486
+ const oracle = this.oracles.get(i);
487
+ const key = this.oracleAssetKey(oracle, asset);
488
+
489
+ const price = this.getOraclePrice(key);
490
+ const timestamp = this.getOracleTimestamp(key);
491
+
492
+ // Skip stale or unset prices
493
+ if (price.isZero()) continue;
494
+ if (now - timestamp > maxStale) continue;
495
+
496
+ prices.push(price);
497
+ }
498
+
499
+ // Check minimum oracles
500
+ if (u32(prices.length) < u32(this._minOracles.value)) {
501
+ return; // Not enough fresh prices
502
+ }
503
+
504
+ // Calculate median price
505
+ const medianPrice = this.calculateMedian(prices);
506
+
507
+ // Check deviation from current price
508
+ const currentPrice = this._prices.get(asset);
509
+ if (!currentPrice.isZero()) {
510
+ if (!this.withinDeviation(currentPrice, medianPrice)) {
511
+ // Price moved too much - might be manipulation
512
+ // In production, you might want different handling
513
+ return;
514
+ }
515
+ }
516
+
517
+ // Update price
518
+ this._prices.set(asset, medianPrice);
519
+ // AddressMemoryMap stores u256; convert timestamp to u256
520
+ this._timestamps.set(asset, u256.fromU64(now));
521
+
522
+ this.emitEvent(new PriceUpdated(asset, medianPrice, now));
523
+ }
524
+
525
+ // ============ PRICE READING ============
526
+
527
+ /**
528
+ * Get the latest price for an asset.
529
+ */
530
+ @method({ name: 'asset', type: ABIDataTypes.ADDRESS })
531
+ @returns(
532
+ { name: 'price', type: ABIDataTypes.UINT256 },
533
+ { name: 'timestamp', type: ABIDataTypes.UINT64 },
534
+ )
535
+ public getPrice(calldata: Calldata): BytesWriter {
536
+ const asset = calldata.readAddress();
537
+
538
+ const price = this._prices.get(asset);
539
+ // AddressMemoryMap returns u256; convert to u64 for timestamp
540
+ const timestamp: u64 = this._timestamps.get(asset).toU64();
541
+
542
+ // Check for stale price
543
+ const now = Blockchain.block.medianTime;
544
+ if (now - timestamp > this._maxStaleness.value) {
545
+ throw new Revert('Price is stale');
546
+ }
547
+
548
+ if (price.isZero()) {
549
+ throw new Revert('Price not available');
550
+ }
551
+
552
+ const writer = new BytesWriter(40);
553
+ writer.writeU256(price);
554
+ writer.writeU64(timestamp);
555
+ return writer;
556
+ }
557
+
558
+ /**
559
+ * Get price without staleness check (for reference).
560
+ */
561
+ @method({ name: 'asset', type: ABIDataTypes.ADDRESS })
562
+ @returns(
563
+ { name: 'price', type: ABIDataTypes.UINT256 },
564
+ { name: 'timestamp', type: ABIDataTypes.UINT64 },
565
+ )
566
+ public getLatestPrice(calldata: Calldata): BytesWriter {
567
+ const asset = calldata.readAddress();
568
+
569
+ const writer = new BytesWriter(40);
570
+ writer.writeU256(this._prices.get(asset));
571
+ // AddressMemoryMap returns u256; convert to u64 for timestamp
572
+ writer.writeU64(this._timestamps.get(asset).toU64());
573
+ return writer;
574
+ }
575
+
576
+ // ============ HELPERS ============
577
+
578
+ private isOracle(addr: Address): bool {
579
+ const length = this.oracles.length;
580
+ for (let i: u64 = 0; i < length; i++) {
581
+ if (this.oracles.get(i).equals(addr)) {
582
+ return true;
583
+ }
584
+ }
585
+ return false;
586
+ }
587
+
588
+ private oracleAssetKey(oracle: Address, asset: Address): u256 {
589
+ const combined = new Uint8Array(64);
590
+ combined.set(oracle, 0);
591
+ combined.set(asset, 32);
592
+ return u256.fromBytes(sha256(combined));
593
+ }
594
+
595
+ private calculateMedian(prices: u256[]): u256 {
596
+ const len = prices.length;
597
+
598
+ // Simple bubble sort for small arrays
599
+ for (let i = 0; i < len; i++) {
600
+ for (let j = i + 1; j < len; j++) {
601
+ if (prices[j] < prices[i]) {
602
+ const temp = prices[i];
603
+ prices[i] = prices[j];
604
+ prices[j] = temp;
605
+ }
606
+ }
607
+ }
608
+
609
+ const mid = len / 2;
610
+ if (len % 2 == 0) {
611
+ // Average of two middle values
612
+ return SafeMath.div(
613
+ SafeMath.add(prices[mid - 1], prices[mid]),
614
+ u256.fromU64(2)
615
+ );
616
+ } else {
617
+ return prices[mid];
618
+ }
619
+ }
620
+
621
+ private withinDeviation(oldPrice: u256, newPrice: u256): bool {
622
+ const maxDev = this._maxDeviation.value;
623
+ const basisPoints = u256.fromU64(10000);
624
+
625
+ // Calculate allowed deviation
626
+ const maxChange = SafeMath.div(SafeMath.mul(oldPrice, maxDev), basisPoints);
627
+
628
+ // Check if new price is within range
629
+ const lowerBound = SafeMath.sub(oldPrice, maxChange);
630
+ const upperBound = SafeMath.add(oldPrice, maxChange);
631
+
632
+ return newPrice >= lowerBound && newPrice <= upperBound;
633
+ }
634
+
635
+ // Storage helpers using encodePointer for proper storage keys
636
+ private setOraclePrice(key: u256, price: u256): void {
637
+ const keyBytes = key.toUint8Array(true).subarray(2, 32); // Use 30 bytes
638
+ const pointerHash = encodePointer(this.oraclePricesPointer, keyBytes);
639
+ Blockchain.setStorageAt(pointerHash, price.toUint8Array(true));
640
+ }
641
+
642
+ private getOraclePrice(key: u256): u256 {
643
+ const keyBytes = key.toUint8Array(true).subarray(2, 32); // Use 30 bytes
644
+ const pointerHash = encodePointer(this.oraclePricesPointer, keyBytes);
645
+ const stored = Blockchain.getStorageAt(pointerHash);
646
+ return u256.fromBytes(stored, true);
647
+ }
648
+
649
+ private setOracleTimestamp(key: u256, timestamp: u64): void {
650
+ const keyBytes = key.toUint8Array(true).subarray(2, 32); // Use 30 bytes
651
+ const pointerHash = encodePointer(this.oracleTimestampsPointer, keyBytes);
652
+ Blockchain.setStorageAt(pointerHash, u256.fromU64(timestamp).toUint8Array(true));
653
+ }
654
+
655
+ private getOracleTimestamp(key: u256): u64 {
656
+ const keyBytes = key.toUint8Array(true).subarray(2, 32); // Use 30 bytes
657
+ const pointerHash = encodePointer(this.oracleTimestampsPointer, keyBytes);
658
+ const stored = Blockchain.getStorageAt(pointerHash);
659
+ return u256.fromBytes(stored, true).toU64();
660
+ }
661
+
662
+ // ============ VIEW FUNCTIONS ============
663
+
664
+ @method()
665
+ @returns({ name: 'oracles', type: ABIDataTypes.ADDRESS_ARRAY })
666
+ public getOracles(_calldata: Calldata): BytesWriter {
667
+ const count = this.oracles.length;
668
+ // Use u32 cast for length - arrays in AssemblyScript are i32 indexed
669
+ const countU32 = u32(count);
670
+ const writer = new BytesWriter(4 + 32 * i32(count));
671
+
672
+ writer.writeU32(countU32);
673
+ for (let i: u64 = 0; i < count; i++) {
674
+ writer.writeAddress(this.oracles.get(i));
675
+ }
676
+
677
+ return writer;
678
+ }
679
+
680
+ @method()
681
+ @returns(
682
+ { name: 'minOracles', type: ABIDataTypes.UINT8 },
683
+ { name: 'maxDeviation', type: ABIDataTypes.UINT256 },
684
+ { name: 'maxStaleness', type: ABIDataTypes.UINT64 },
685
+ )
686
+ public getConfig(_calldata: Calldata): BytesWriter {
687
+ const writer = new BytesWriter(48);
688
+
689
+ writer.writeU8(this._minOracles.value);
690
+ writer.writeU256(this._maxDeviation.value);
691
+ writer.writeU64(this._maxStaleness.value);
692
+
693
+ return writer;
694
+ }
695
+
696
+ @method({ name: 'oracle', type: ABIDataTypes.ADDRESS })
697
+ @returns({ name: 'isOracle', type: ABIDataTypes.BOOL })
698
+ public checkIsOracle(calldata: Calldata): BytesWriter {
699
+ const oracle = calldata.readAddress();
700
+
701
+ const writer = new BytesWriter(1);
702
+ writer.writeBoolean(this.isOracle(oracle));
703
+ return writer;
704
+ }
705
+ }
706
+ ```
707
+
708
+ ## Key Concepts
709
+
710
+ ### Multi-Oracle Aggregation
711
+
712
+ ```
713
+ Oracle 1 -> Price: 100.5
714
+ Oracle 2 -> Price: 100.3 -> Median: 100.4 -> Aggregated Price
715
+ Oracle 3 -> Price: 100.4
716
+ ```
717
+
718
+ ### Staleness Protection
719
+
720
+ ```typescript
721
+ // Check for stale price
722
+ const now = Blockchain.block.medianTime;
723
+ if (now - timestamp > this._maxStaleness.value) {
724
+ throw new Revert('Price is stale');
725
+ }
726
+ ```
727
+
728
+ ### Timestamp Storage
729
+
730
+ AddressMemoryMap stores and returns u256 values. Convert timestamps as needed:
731
+
732
+ ```typescript
733
+ // AddressMemoryMap stores u256 values
734
+ private _timestamps: AddressMemoryMap;
735
+ this._timestamps = new AddressMemoryMap(this.timestampsPointer);
736
+
737
+ // Store timestamp as u256
738
+ this._timestamps.set(asset, u256.fromU64(Blockchain.block.medianTime));
739
+
740
+ // Retrieve timestamp and convert to u64
741
+ const timestamp: u64 = this._timestamps.get(asset).toU64();
742
+ ```
743
+
744
+ ## Usage
745
+
746
+ ### Deploy
747
+
748
+ ```typescript
749
+ const writer = new BytesWriter(128);
750
+ writer.writeU8(3); // minOracles
751
+ writer.writeU256(u256.fromU64(500)); // maxDeviation (5%)
752
+ writer.writeU64(3600); // maxStaleness (1 hour)
753
+ writer.writeAddressArray([oracle1, oracle2, oracle3]);
754
+ ```
755
+
756
+ ### Submit Price (Oracle)
757
+
758
+ ```typescript
759
+ const SUBMIT_PRICE_SELECTOR: u32 = 0x8d6cc56d; // submitPrice(address,uint256)
760
+
761
+ const writer = new BytesWriter(68);
762
+ writer.writeSelector(SUBMIT_PRICE_SELECTOR);
763
+ writer.writeAddress(btcAsset);
764
+ writer.writeU256(u256.fromU64(50000_000000)); // $50,000 with 6 decimals
765
+ ```
766
+
767
+ ### Read Price
768
+
769
+ ```typescript
770
+ const GET_PRICE_SELECTOR: u32 = 0x41976e09; // getPrice(address)
771
+
772
+ const writer = new BytesWriter(36);
773
+ writer.writeSelector(GET_PRICE_SELECTOR);
774
+ writer.writeAddress(btcAsset);
775
+
776
+ const result = contract.call(oracle, writer.getBuffer(), true);
777
+ // Returns: price (u256), timestamp (u64)
778
+ ```
779
+
780
+ ## Best Practices
781
+
782
+ ### 1. Use Proper Type Casts
783
+
784
+ ```typescript
785
+ // Converting u64 array length to u32 for BytesWriter
786
+ const count = this.oracles.length; // u64
787
+ const countU32 = u32(count);
788
+ const writer = new BytesWriter(4 + 32 * i32(countU32));
789
+ ```
790
+
791
+ ### 2. Handle Timestamps Properly
792
+
793
+ ```typescript
794
+ // AddressMemoryMap stores u256 values
795
+ private _maxStaleness: StoredU64;
796
+ private _timestamps: AddressMemoryMap;
797
+
798
+ // Store timestamp as u256
799
+ this._timestamps.set(asset, u256.fromU64(Blockchain.block.medianTime));
800
+
801
+ // Retrieve and convert to u64
802
+ const timestamp: u64 = this._timestamps.get(asset).toU64();
803
+ ```
804
+
805
+ ### 3. Add Decorators for ABI Generation
806
+
807
+ ```typescript
808
+ @method({ name: 'asset', type: ABIDataTypes.ADDRESS })
809
+ @returns(
810
+ { name: 'price', type: ABIDataTypes.UINT256 },
811
+ { name: 'timestamp', type: ABIDataTypes.UINT64 },
812
+ )
813
+ public getPrice(calldata: Calldata): BytesWriter { }
814
+ ```
815
+
816
+ ## Solidity Equivalent
817
+
818
+ For developers familiar with Solidity, here is an equivalent Chainlink-style oracle aggregator implementation:
819
+
820
+ ```solidity
821
+ // SPDX-License-Identifier: MIT
822
+ pragma solidity ^0.8.20;
823
+
824
+ import "@openzeppelin/contracts/access/Ownable.sol";
825
+
826
+ interface AggregatorV3Interface {
827
+ function latestRoundData() external view returns (
828
+ uint80 roundId,
829
+ int256 answer,
830
+ uint256 startedAt,
831
+ uint256 updatedAt,
832
+ uint80 answeredInRound
833
+ );
834
+ }
835
+
836
+ contract MultiOracle is Ownable {
837
+ struct PriceData {
838
+ uint256 price;
839
+ uint64 timestamp;
840
+ }
841
+
842
+ address[] public oracles;
843
+ mapping(address => bool) public isOracle;
844
+
845
+ uint8 public minOracles;
846
+ uint256 public maxDeviation; // In basis points (100 = 1%)
847
+ uint64 public maxStaleness; // In seconds
848
+
849
+ // Aggregated prices per asset
850
+ mapping(address => PriceData) public prices;
851
+
852
+ // Individual oracle submissions
853
+ mapping(bytes32 => PriceData) private oracleSubmissions;
854
+
855
+ event PriceUpdated(address indexed asset, uint256 price, uint64 timestamp);
856
+ event OracleAdded(address indexed oracle);
857
+ event OracleRemoved(address indexed oracle);
858
+
859
+ constructor(
860
+ uint8 _minOracles,
861
+ uint256 _maxDeviation,
862
+ uint64 _maxStaleness,
863
+ address[] memory _initialOracles
864
+ ) Ownable(msg.sender) {
865
+ minOracles = _minOracles;
866
+ maxDeviation = _maxDeviation;
867
+ maxStaleness = _maxStaleness;
868
+
869
+ for (uint i = 0; i < _initialOracles.length; i++) {
870
+ oracles.push(_initialOracles[i]);
871
+ isOracle[_initialOracles[i]] = true;
872
+ emit OracleAdded(_initialOracles[i]);
873
+ }
874
+ }
875
+
876
+ function addOracle(address oracle) external onlyOwner {
877
+ require(!isOracle[oracle], "Oracle already exists");
878
+ oracles.push(oracle);
879
+ isOracle[oracle] = true;
880
+ emit OracleAdded(oracle);
881
+ }
882
+
883
+ function removeOracle(address oracle) external onlyOwner {
884
+ require(isOracle[oracle], "Oracle not found");
885
+ require(oracles.length > minOracles, "Would go below minimum oracles");
886
+
887
+ // Find and remove
888
+ for (uint i = 0; i < oracles.length; i++) {
889
+ if (oracles[i] == oracle) {
890
+ oracles[i] = oracles[oracles.length - 1];
891
+ oracles.pop();
892
+ break;
893
+ }
894
+ }
895
+ isOracle[oracle] = false;
896
+ emit OracleRemoved(oracle);
897
+ }
898
+
899
+ function submitPrice(address asset, uint256 price) external {
900
+ require(isOracle[msg.sender], "Not authorized oracle");
901
+
902
+ bytes32 key = keccak256(abi.encodePacked(msg.sender, asset));
903
+ oracleSubmissions[key] = PriceData(price, uint64(block.timestamp));
904
+
905
+ _tryUpdatePrice(asset);
906
+ }
907
+
908
+ function _tryUpdatePrice(address asset) internal {
909
+ uint256[] memory validPrices = new uint256[](oracles.length);
910
+ uint256 validCount = 0;
911
+
912
+ for (uint i = 0; i < oracles.length; i++) {
913
+ bytes32 key = keccak256(abi.encodePacked(oracles[i], asset));
914
+ PriceData memory data = oracleSubmissions[key];
915
+
916
+ // Skip stale or unset prices
917
+ if (data.price == 0) continue;
918
+ if (block.timestamp - data.timestamp > maxStaleness) continue;
919
+
920
+ validPrices[validCount] = data.price;
921
+ validCount++;
922
+ }
923
+
924
+ if (validCount < minOracles) return;
925
+
926
+ uint256 medianPrice = _calculateMedian(validPrices, validCount);
927
+
928
+ // Check deviation
929
+ PriceData memory current = prices[asset];
930
+ if (current.price != 0 && !_withinDeviation(current.price, medianPrice)) {
931
+ return;
932
+ }
933
+
934
+ prices[asset] = PriceData(medianPrice, uint64(block.timestamp));
935
+ emit PriceUpdated(asset, medianPrice, uint64(block.timestamp));
936
+ }
937
+
938
+ function _calculateMedian(uint256[] memory arr, uint256 len) internal pure returns (uint256) {
939
+ // Simple bubble sort
940
+ for (uint i = 0; i < len; i++) {
941
+ for (uint j = i + 1; j < len; j++) {
942
+ if (arr[j] < arr[i]) {
943
+ (arr[i], arr[j]) = (arr[j], arr[i]);
944
+ }
945
+ }
946
+ }
947
+
948
+ uint256 mid = len / 2;
949
+ if (len % 2 == 0) {
950
+ return (arr[mid - 1] + arr[mid]) / 2;
951
+ }
952
+ return arr[mid];
953
+ }
954
+
955
+ function _withinDeviation(uint256 oldPrice, uint256 newPrice) internal view returns (bool) {
956
+ uint256 maxChange = (oldPrice * maxDeviation) / 10000;
957
+ uint256 lowerBound = oldPrice - maxChange;
958
+ uint256 upperBound = oldPrice + maxChange;
959
+ return newPrice >= lowerBound && newPrice <= upperBound;
960
+ }
961
+
962
+ function getPrice(address asset) external view returns (uint256 price, uint64 timestamp) {
963
+ PriceData memory data = prices[asset];
964
+ require(data.price != 0, "Price not available");
965
+ require(block.timestamp - data.timestamp <= maxStaleness, "Price is stale");
966
+ return (data.price, data.timestamp);
967
+ }
968
+
969
+ function getLatestPrice(address asset) external view returns (uint256 price, uint64 timestamp) {
970
+ PriceData memory data = prices[asset];
971
+ return (data.price, data.timestamp);
972
+ }
973
+
974
+ function getOracles() external view returns (address[] memory) {
975
+ return oracles;
976
+ }
977
+ }
978
+ ```
979
+
980
+ ## Solidity vs OPNet Comparison
981
+
982
+ ### Key Differences Table
983
+
984
+ | Aspect | Solidity (Chainlink-style) | OPNet |
985
+ |--------|---------------------------|-------|
986
+ | **Oracle Interface** | `AggregatorV3Interface` with rounds | Custom multi-oracle aggregation |
987
+ | **Price Storage** | `mapping(address => PriceData)` | `AddressMemoryMap` |
988
+ | **Timestamp Source** | `block.timestamp` | `Blockchain.block.medianTime` |
989
+ | **Key Generation** | `keccak256(abi.encodePacked(...))` | `sha256(combined)` |
990
+ | **Array Handling** | Dynamic arrays with `.push()/.pop()` | `StoredAddressArray` |
991
+ | **Sorting** | In-memory array manipulation | Same pattern, u256 comparisons |
992
+ | **Staleness Check** | `block.timestamp - data.timestamp` | `now - timestamp > maxStaleness` |
993
+ | **Return Format** | Multiple return values | `BytesWriter` serialization |
994
+
995
+ ### Chainlink vs OPNet Oracle Pattern
996
+
997
+ **Chainlink (Solidity) - Consumer Pattern:**
998
+ ```solidity
999
+ import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
1000
+
1001
+ contract PriceConsumer {
1002
+ AggregatorV3Interface internal priceFeed;
1003
+
1004
+ constructor() {
1005
+ // ETH/USD on Ethereum mainnet
1006
+ priceFeed = AggregatorV3Interface(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419);
1007
+ }
1008
+
1009
+ function getLatestPrice() public view returns (int256) {
1010
+ (
1011
+ uint80 roundId,
1012
+ int256 answer,
1013
+ uint256 startedAt,
1014
+ uint256 updatedAt,
1015
+ uint80 answeredInRound
1016
+ ) = priceFeed.latestRoundData();
1017
+
1018
+ require(updatedAt > block.timestamp - 3600, "Stale price");
1019
+ return answer;
1020
+ }
1021
+ }
1022
+ ```
1023
+
1024
+ **OPNet - Self-Contained Oracle:**
1025
+ ```typescript
1026
+ @method({ name: 'asset', type: ABIDataTypes.ADDRESS })
1027
+ @returns(
1028
+ { name: 'price', type: ABIDataTypes.UINT256 },
1029
+ { name: 'timestamp', type: ABIDataTypes.UINT64 },
1030
+ )
1031
+ public getPrice(calldata: Calldata): BytesWriter {
1032
+ const asset = calldata.readAddress();
1033
+ const price = this._prices.get(asset);
1034
+ // AddressMemoryMap returns u256; convert to u64 for timestamp
1035
+ const timestamp: u64 = this._timestamps.get(asset).toU64();
1036
+
1037
+ const now = Blockchain.block.medianTime;
1038
+ if (now - timestamp > this._maxStaleness.value) {
1039
+ throw new Revert('Price is stale');
1040
+ }
1041
+
1042
+ const writer = new BytesWriter(40);
1043
+ writer.writeU256(price);
1044
+ writer.writeU64(timestamp);
1045
+ return writer;
1046
+ }
1047
+ ```
1048
+
1049
+ ### Price Aggregation Comparison
1050
+
1051
+ **Solidity:**
1052
+ ```solidity
1053
+ function _calculateMedian(uint256[] memory arr, uint256 len) internal pure returns (uint256) {
1054
+ // Bubble sort
1055
+ for (uint i = 0; i < len; i++) {
1056
+ for (uint j = i + 1; j < len; j++) {
1057
+ if (arr[j] < arr[i]) {
1058
+ (arr[i], arr[j]) = (arr[j], arr[i]);
1059
+ }
1060
+ }
1061
+ }
1062
+
1063
+ uint256 mid = len / 2;
1064
+ if (len % 2 == 0) {
1065
+ return (arr[mid - 1] + arr[mid]) / 2;
1066
+ }
1067
+ return arr[mid];
1068
+ }
1069
+ ```
1070
+
1071
+ **OPNet:**
1072
+ ```typescript
1073
+ private calculateMedian(prices: u256[]): u256 {
1074
+ const len = prices.length;
1075
+
1076
+ // Bubble sort for small arrays
1077
+ for (let i = 0; i < len; i++) {
1078
+ for (let j = i + 1; j < len; j++) {
1079
+ if (prices[j] < prices[i]) {
1080
+ const temp = prices[i];
1081
+ prices[i] = prices[j];
1082
+ prices[j] = temp;
1083
+ }
1084
+ }
1085
+ }
1086
+
1087
+ const mid = len / 2;
1088
+ if (len % 2 == 0) {
1089
+ return SafeMath.div(
1090
+ SafeMath.add(prices[mid - 1], prices[mid]),
1091
+ u256.fromU64(2)
1092
+ );
1093
+ }
1094
+ return prices[mid];
1095
+ }
1096
+ ```
1097
+
1098
+ ### Storage Key Generation Comparison
1099
+
1100
+ **Solidity:**
1101
+ ```solidity
1102
+ // Uses keccak256 for storage key
1103
+ bytes32 key = keccak256(abi.encodePacked(oracle, asset));
1104
+ oracleSubmissions[key] = PriceData(price, timestamp);
1105
+ ```
1106
+
1107
+ **OPNet:**
1108
+ ```typescript
1109
+ // Uses sha256 for storage key
1110
+ private oracleAssetKey(oracle: Address, asset: Address): u256 {
1111
+ const combined = new Uint8Array(64);
1112
+ combined.set(oracle, 0);
1113
+ combined.set(asset, 32);
1114
+ return u256.fromBytes(sha256(combined));
1115
+ }
1116
+
1117
+ // Uses encodePointer for proper storage addressing
1118
+ private setOraclePrice(key: u256, price: u256): void {
1119
+ const keyBytes = key.toUint8Array(true).subarray(2, 32);
1120
+ const pointerHash = encodePointer(this.oraclePricesPointer, keyBytes);
1121
+ Blockchain.setStorageAt(pointerHash, price.toUint8Array(true));
1122
+ }
1123
+ ```
1124
+
1125
+ ### Advantages of OPNet Approach
1126
+
1127
+ | Feature | Benefit |
1128
+ |---------|---------|
1129
+ | **Self-Contained Oracle** | No external dependencies like Chainlink feeds |
1130
+ | **Multi-Oracle Aggregation** | Built-in median calculation from multiple sources |
1131
+ | **Bitcoin Timestamp** | Uses `medianTime` for manipulation resistance |
1132
+ | **Flexible Configuration** | Runtime-configurable min oracles, deviation, staleness |
1133
+ | **Native u256 Math** | First-class 256-bit integer support |
1134
+ | **Explicit Storage** | Direct control over storage layout with pointers |
1135
+ | **No External Calls** | All oracle logic contained within single contract |
1136
+
1137
+ ### Deviation Check Comparison
1138
+
1139
+ **Solidity:**
1140
+ ```solidity
1141
+ function _withinDeviation(uint256 oldPrice, uint256 newPrice) internal view returns (bool) {
1142
+ uint256 maxChange = (oldPrice * maxDeviation) / 10000;
1143
+ uint256 lowerBound = oldPrice - maxChange;
1144
+ uint256 upperBound = oldPrice + maxChange;
1145
+ return newPrice >= lowerBound && newPrice <= upperBound;
1146
+ }
1147
+ ```
1148
+
1149
+ **OPNet:**
1150
+ ```typescript
1151
+ private withinDeviation(oldPrice: u256, newPrice: u256): bool {
1152
+ const maxDev = this._maxDeviation.value;
1153
+ const basisPoints = u256.fromU64(10000);
1154
+
1155
+ const maxChange = SafeMath.div(SafeMath.mul(oldPrice, maxDev), basisPoints);
1156
+ const lowerBound = SafeMath.sub(oldPrice, maxChange);
1157
+ const upperBound = SafeMath.add(oldPrice, maxChange);
1158
+
1159
+ return newPrice >= lowerBound && newPrice <= upperBound;
1160
+ }
1161
+ ```
1162
+
1163
+ ### Oracle Authorization Comparison
1164
+
1165
+ **Solidity:**
1166
+ ```solidity
1167
+ address[] public oracles;
1168
+ mapping(address => bool) public isOracle;
1169
+
1170
+ function submitPrice(address asset, uint256 price) external {
1171
+ require(isOracle[msg.sender], "Not authorized oracle");
1172
+ // ...
1173
+ }
1174
+ ```
1175
+
1176
+ **OPNet:**
1177
+ ```typescript
1178
+ private oracles: StoredAddressArray;
1179
+
1180
+ private isOracle(addr: Address): bool {
1181
+ const length = this.oracles.length;
1182
+ for (let i: u64 = 0; i < length; i++) {
1183
+ if (this.oracles.get(i).equals(addr)) {
1184
+ return true;
1185
+ }
1186
+ }
1187
+ return false;
1188
+ }
1189
+
1190
+ public submitPrice(calldata: Calldata): BytesWriter {
1191
+ if (!this.isOracle(Blockchain.tx.sender)) {
1192
+ throw new Revert('Not authorized oracle');
1193
+ }
1194
+ // ...
1195
+ }
1196
+ ```
1197
+
1198
+ ### When to Choose Each Approach
1199
+
1200
+ | Use Case | Recommended Approach |
1201
+ |----------|---------------------|
1202
+ | **Need Chainlink feeds** | Solidity with AggregatorV3Interface |
1203
+ | **Custom oracle network** | OPNet multi-oracle aggregation |
1204
+ | **Bitcoin-native DeFi** | OPNet with Bitcoin timestamp |
1205
+ | **Existing EVM infrastructure** | Solidity |
1206
+ | **New protocol on Bitcoin** | OPNet |
1207
+
1208
+ ---
1209
+
1210
+ **Navigation:**
1211
+ - Previous: [Stablecoin](./stablecoin.md)
1212
+ - Next: [API Reference - Blockchain](../api-reference/blockchain.md)