@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
809 lines (631 loc) • 23.1 kB
Markdown
# OP_NET Base Contract
`OP_NET` is the base class for all OP_NET smart contracts. It implements the `IBTC` interface and provides the foundational structure for contract lifecycle, method dispatching, event emission, and access control.
## Overview
```typescript
import { OP_NET, Calldata, BytesWriter, ABIDataTypes } from '@btc-vision/btc-runtime/runtime';
@final
export class MyContract extends OP_NET {
public constructor() {
super();
}
public override onDeployment(calldata: Calldata): void {
// One-time initialization
}
@method({ name: 'param', type: ABIDataTypes.UINT256 })
@returns({ name: 'result', type: ABIDataTypes.UINT256 })
public myMethod(calldata: Calldata): BytesWriter {
// Method implementation - routing is AUTOMATIC via @method decorator
return new BytesWriter(0);
}
}
```
**Note:** Method routing is handled AUTOMATICALLY by the runtime via `@method` decorators. You do NOT need to override the `execute` method - the decorator system handles selector generation and call routing.
**Solidity Comparison:**
```solidity
// Solidity: Automatic method routing via ABI
contract MyContract {
constructor() {
// Runs once at deployment
}
function myMethod() public returns (bytes memory) {
// Method implementation
}
}
// OP_NET: AUTOMATIC method routing via @method decorators
// - Constructor runs on EVERY call
// - Routing is automatic via decorator system
// - One-time init in onDeployment()
```
## Contract Lifecycle
### Inheritance Hierarchy
OP_NET contracts follow a clear inheritance pattern:
```mermaid
classDiagram
class OP_NET {
Base Contract
implements IBTC
+address Address (getter)
+contractDeployer Address (getter)
+onDeployment(_calldata: Calldata) void
+onUpdate(_calldata: Calldata) void
+onExecutionStarted(_selector: Selector, _calldata: Calldata) void
+onExecutionCompleted(_selector: Selector, _calldata: Calldata) void
+execute(method: Selector, _calldata: Calldata) BytesWriter
#emitEvent(event: NetEvent) void
#onlyDeployer(caller: Address) void
#isSelf(address: Address) boolean
#_buildDomainSeparator() Uint8Array
Note: @method decorator handles routing
}
class MyContract {
Custom Contract
-balancesPointer: u16
-balances: AddressMemoryMap
+constructor()
+onDeployment(calldata: Calldata) void
+myMethod(calldata: Calldata) BytesWriter
Note: @method decorator handles routing
}
class ReentrancyGuard {
Reentrancy Protection
extends OP_NET
#_locked: StoredBoolean
#_reentrancyDepth: StoredU256
+nonReentrantBefore() void
+nonReentrantAfter() void
}
class OP20 {
Fungible Token Standard
extends ReentrancyGuard
-_totalSupply: StoredU256
-balanceOfMap: AddressMemoryMap
+transfer(calldata: Calldata) BytesWriter
+approve(calldata: Calldata) BytesWriter
}
class OP721 {
NFT Standard
extends ReentrancyGuard
-_owners: AddressMemoryMap
-_balances: AddressMemoryMap
+transferFrom(calldata: Calldata) BytesWriter
+mint(to: Address, tokenId: u256) void
}
OP_NET <|-- MyContract : extends
OP_NET <|-- ReentrancyGuard : extends
ReentrancyGuard <|-- OP20 : extends
ReentrancyGuard <|-- OP721 : extends
```
### Deployment and Execution Flow
The following diagram shows how contracts are deployed and executed on OP_NET:
```mermaid
---
config:
theme: dark
---
sequenceDiagram
participant User as User
participant Bitcoin as Bitcoin L1
participant WASM as WASM Runtime
participant Contract as Contract
participant Storage as Storage
Note over User,Storage: Deployment Phase (Once)
User->>Bitcoin: Submit deployment TX
Bitcoin->>WASM: Create contract
WASM->>Contract: constructor()
Contract->>Contract: Initialize storage pointers
Contract->>Contract: Create storage map instances
WASM->>Contract: onDeployment(calldata)
Contract->>Contract: Read deployment calldata
Contract->>Storage: Set initial state
Contract->>WASM: Emit deployment events
Note over Contract: Contract Ready
Note over User,Storage: Execution Phase (Every Transaction)
User->>Bitcoin: Submit transaction
Bitcoin->>WASM: Route to contract
WASM->>Contract: constructor() runs AGAIN
Contract->>Contract: Re-initialize storage maps
WASM->>Contract: onExecutionStarted(selector, calldata)
Contract->>Contract: Read method selector from TX
WASM->>Contract: execute(method, calldata)
alt Selector matches
Contract->>Contract: Call method handler
Contract->>Contract: Read calldata parameters
Contract->>Contract: Validate inputs
alt Valid inputs
Contract->>Storage: Read current state
Contract->>Contract: Execute business logic
Contract->>Storage: Write updated state
Contract->>WASM: emitEvent()
else Invalid inputs
Contract->>WASM: Revert transaction
end
else No match
Contract->>Contract: super.execute(parent)
alt Parent has method
Contract->>Storage: Execute parent method
Contract->>WASM: emitEvent()
else No handler
Contract->>WASM: Revert: Unknown selector
end
end
WASM->>Contract: onExecutionCompleted(selector, calldata)
Contract->>WASM: Return BytesWriter result
WASM->>Bitcoin: Commit state changes
Bitcoin->>User: Transaction complete
```
### 1. Construction
The constructor runs on **every** contract interaction:
```typescript
public constructor() {
super(); // Always call parent constructor
// Initialize storage maps (these run every time)
this.balances = new AddressMemoryMap(this.balancesPointer);
// DON'T do one-time initialization here!
}
```
**Key Difference from Solidity:**
| Solidity | OP_NET |
|----------|-------|
| Constructor runs once at deployment | Constructor runs every call |
| Initialize state in constructor | Initialize state in `onDeployment` |
### 2. Deployment (onDeployment)
Runs exactly **once** when the contract is first deployed:
```typescript
public override onDeployment(calldata: Calldata): void {
// Read deployment parameters
const initialSupply = calldata.readU256();
const tokenName = calldata.readString();
// Set initial state
this._totalSupply.value = initialSupply;
this._name.value = tokenName;
// Mint initial tokens
this._mint(Blockchain.tx.origin, initialSupply);
}
```
**Solidity Comparison:**
```solidity
// Solidity: One-time init in constructor
constructor(uint256 initialSupply, string memory tokenName) {
_totalSupply = initialSupply;
_name = tokenName;
_mint(msg.sender, initialSupply);
}
// OP_NET: One-time init in onDeployment()
// Constructor runs every call, onDeployment runs once
```
### 2b. Update (onUpdate)
Runs when the contract's bytecode is updated via `updateContractFromExisting`:
```typescript
public override onUpdate(calldata: Calldata): void {
super.onUpdate(calldata); // Notify plugins
// Read migration parameters
const fromVersion = calldata.readU64();
// Perform migration based on version
if (fromVersion === 1) {
// Initialize new storage added in this version
this._newFeature.value = u256.fromU64(100);
}
}
```
> **Note:** This hook is called on the **new** bytecode, not the old one. See [Contract Updates](../advanced/updatable#the-onupdate-lifecycle-hook) for details.
### 3. Method Execution
Methods are automatically routed via `@method` decorators:
```typescript
@method(
{ name: 'to', type: ABIDataTypes.ADDRESS },
{ name: 'amount', type: ABIDataTypes.UINT256 },
)
@returns({ name: 'success', type: ABIDataTypes.BOOL })
public transfer(calldata: Calldata): BytesWriter {
const to = calldata.readAddress();
const amount = calldata.readU256();
// ... implementation
return new BytesWriter(1);
}
@method({ name: 'spender', type: ABIDataTypes.ADDRESS }, { name: 'amount', type: ABIDataTypes.UINT256 })
public approve(calldata: Calldata): BytesWriter {
// ... implementation
return new BytesWriter(0);
}
@method({ name: 'account', type: ABIDataTypes.ADDRESS })
@returns({ name: 'balance', type: ABIDataTypes.UINT256 })
public balanceOf(calldata: Calldata): BytesWriter {
const account = calldata.readAddress();
// ... implementation
const writer = new BytesWriter(32);
writer.writeU256(balance);
return writer;
}
```
**Note:** The runtime automatically generates selectors and routes calls based on `@method` decorators. You do NOT need to override the `execute` method.
### Transaction Sequence
The following sequence diagram shows the complete flow of a transaction through the system:
```mermaid
sequenceDiagram
participant User as 👤 User Wallet
participant Blockchain as Bitcoin L1
participant TxPool as Transaction Pool
participant VM as WASM Runtime
participant Contract as OP_NET Contract
participant Storage as Storage Pointers
participant EventLog as Event Log
User->>TxPool: Submit signed transaction
Note over User,TxPool: Contains: contract address,<br/>method selector, calldata
TxPool->>Blockchain: Transaction confirmed
Blockchain->>VM: Route to contract address
VM->>Contract: Instantiate contract instance
activate Contract
Contract->>Contract: constructor()
Note over Contract: Runs EVERY call<br/>Initialize storage maps
Contract->>Storage: Allocate storage pointers
Storage-->>Contract: Pointer addresses
VM->>Contract: onExecutionStarted(selector, calldata)
Note over Contract: Pre-execution hook<br/>Can add logging/validation
VM->>Contract: execute(selector, calldata)
Contract->>Contract: switch(selector)
Note over Contract: Method routing logic
alt Known Method Selector
Contract->>Contract: methodHandler(calldata)
Contract->>Contract: calldata.readAddress()
Contract->>Contract: calldata.readU256()
Note over Contract: Parse parameters
Contract->>Storage: Read current state
Storage-->>Contract: Current values
Contract->>Contract: Business logic
Note over Contract: SafeMath operations,<br/>validations, state changes
Contract->>Storage: Write updated state
Note over Storage: Persistent storage<br/>committed on success
Contract->>EventLog: emitEvent(TransferEvent)
Note over EventLog: Events for indexing<br/>off-chain systems
else Unknown Method
Contract->>Contract: super.execute(selector, calldata)
alt Parent Has Method
Note over Contract: OP_NET parent<br/>or OP20/OP721 parent
Contract->>Storage: Parent method logic
Contract->>EventLog: Parent method events
else No Handler
Contract->>VM: throw Revert Unknown method
VM->>User: Transaction reverted
Note over User: No state changes,<br/>fees still consumed
end
end
Contract->>Contract: onExecutionCompleted(selector, calldata)
Note over Contract: Post-execution hook<br/>Cleanup, final checks
Contract->>VM: Return BytesWriter
deactivate Contract
VM->>Blockchain: Commit state changes
Blockchain->>User: Transaction receipt
Note over User: Success with events<br/>or revert with error
```
## Method Selectors
Methods are identified by selectors (4-byte identifiers). The `@method` decorator automatically generates and registers selectors:
```typescript
// Selectors are generated AUTOMATICALLY from @method decorators
@method(
{ name: 'to', type: ABIDataTypes.ADDRESS },
{ name: 'amount', type: ABIDataTypes.UINT256 },
)
public transfer(calldata: Calldata): BytesWriter {
// Runtime automatically routes calls to this method
return new BytesWriter(0);
}
```
### Solidity Comparison
```solidity
// Solidity: Automatic selector generation
function transfer(address to, uint256 amount) public { }
// Selector: keccak256("transfer(address,uint256)")[:4]
// OP_NET: ALSO automatic via @method decorator
// @method({ name: 'to', type: ABIDataTypes.ADDRESS }, ...)
// public transfer(calldata: Calldata): BytesWriter { }
```
**Note:** Both Solidity and OP_NET handle selector generation automatically. In OP_NET, use `@method` decorators and the runtime handles routing.
### Built-in Methods
The base `OP_NET` class provides a built-in `deployer()` method that returns the contract deployer address:
```typescript
// Built-in method handled by OP_NET.execute()
// Selector: encodeSelector('deployer()')
// Returns: Address (the contract deployer)
```
This method is automatically available on all contracts that extend `OP_NET`. When called, it returns the `contractDeployer` address.
## Access Control
### onlyDeployer
Restrict function access to the contract deployer:
```typescript
@method({ name: 'parameter', type: ABIDataTypes.UINT256 })
public adminFunction(calldata: Calldata): BytesWriter {
this.onlyDeployer(Blockchain.tx.sender);
// Only deployer reaches here
return new BytesWriter(0);
}
```
**Solidity Comparison:**
```solidity
// Solidity: Using OpenZeppelin Ownable
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyContract is Ownable {
function adminFunction(uint256 parameter) public onlyOwner {
// Only owner reaches here
}
}
// OP_NET: Built-in onlyDeployer check
// this.onlyDeployer(Blockchain.tx.sender);
```
### Custom Access Control
```typescript
private adminPointer: u16 = Blockchain.nextPointer;
private admin: StoredAddress = new StoredAddress(this.adminPointer, Address.zero());
private onlyAdmin(): void {
if (!Blockchain.tx.sender.equals(this.admin.value)) {
throw new Revert('Caller is not admin');
}
}
@method({ name: 'value', type: ABIDataTypes.UINT256 })
@returns({ name: 'success', type: ABIDataTypes.BOOL })
public setParameter(calldata: Calldata): BytesWriter {
this.onlyAdmin();
// ...
}
```
**Solidity Comparison:**
```solidity
// Solidity: Custom access control
address private admin;
modifier onlyAdmin() {
require(msg.sender == admin, "Caller is not admin");
_;
}
function setParameter(uint256 value) public onlyAdmin {
// ...
}
// OP_NET: Similar pattern but with explicit method call
// this.onlyAdmin(); at start of method
```
## Event Emission
Emit events to notify off-chain systems:
```typescript
import { NetEvent, TransferEvent } from '@btc-vision/btc-runtime/runtime';
// Using built-in events
this.emitEvent(new TransferEvent(from, to, amount));
// Using custom events
this.emitEvent(new MyCustomEvent(data1, data2));
```
**Solidity Comparison:**
```solidity
// Solidity: Emit keyword
event Transfer(address indexed from, address indexed to, uint256 value);
emit Transfer(from, to, amount);
// OP_NET: emitEvent method
this.emitEvent(new TransferEvent(from, to, amount));
```
## Protected Helper Methods
The `OP_NET` base class provides several protected helper methods:
### isSelf
Checks if a given address is the contract's own address:
```typescript
protected isSelf(address: Address): boolean {
return this.address === address;
}
// Usage example
if (this.isSelf(targetAddress)) {
// Handle self-call case
}
```
### _buildDomainSeparator
A method stub for building EIP-712 style domain separators. Must be overridden in derived classes that need signature verification:
```typescript
protected _buildDomainSeparator(): Uint8Array {
// Override in derived class to provide domain separator
throw new Error('Method not implemented.');
}
```
## Storage Patterns
### Pointer Allocation
```typescript
export class MyContract extends OP_NET {
// Allocate storage pointers at class level
private counterPointer: u16 = Blockchain.nextPointer;
private ownerPointer: u16 = Blockchain.nextPointer;
private dataPointer: u16 = Blockchain.nextPointer;
// Create storage instances
private counter: StoredU256 = new StoredU256(this.counterPointer, EMPTY_POINTER);
private owner: StoredAddress = new StoredAddress(this.ownerPointer, Address.zero());
}
```
**Solidity Comparison:**
```solidity
// Solidity: Automatic storage slot allocation
contract MyContract {
uint256 private counter; // slot 0
address private owner; // slot 1
bytes private data; // slot 2
}
// OP_NET: Explicit pointer allocation
// private counterPointer: u16 = Blockchain.nextPointer;
// private counter: StoredU256 = new StoredU256(this.counterPointer, EMPTY_POINTER);
```
### Storage Maps
```typescript
export class MyContract extends OP_NET {
private balancesPointer: u16 = Blockchain.nextPointer;
private balances: AddressMemoryMap;
public constructor() {
super();
// Initialize maps in constructor (runs every time, but that's OK)
this.balances = new AddressMemoryMap(this.balancesPointer);
}
}
```
**Solidity Comparison:**
```solidity
// Solidity: mapping declaration
mapping(address => uint256) private balances;
// OP_NET: AddressMemoryMap with pointer
// private balancesPointer: u16 = Blockchain.nextPointer;
// this.balances = new AddressMemoryMap(this.balancesPointer);
```
## Complete Example
```typescript
import { u256 } from '@btc-vision/as-bignum/assembly';
import {
OP_NET,
Blockchain,
Address,
Calldata,
BytesWriter,
StoredU256,
AddressMemoryMap,
SafeMath,
Revert,
ABIDataTypes,
} from '@btc-vision/btc-runtime/runtime';
@final
export class SimpleToken extends OP_NET {
// Storage pointers
private totalSupplyPointer: u16 = Blockchain.nextPointer;
private balancesPointer: u16 = Blockchain.nextPointer;
// Storage
private _totalSupply: StoredU256 = new StoredU256(this.totalSupplyPointer, EMPTY_POINTER);
private balances: AddressMemoryMap;
public constructor() {
super();
this.balances = new AddressMemoryMap(this.balancesPointer);
}
public override onDeployment(calldata: Calldata): void {
const initialSupply = calldata.readU256();
this._totalSupply.value = initialSupply;
this.balances.set(Blockchain.tx.origin, initialSupply);
}
@method(
{ name: 'to', type: ABIDataTypes.ADDRESS },
{ name: 'amount', type: ABIDataTypes.UINT256 },
)
@returns({ name: 'success', type: ABIDataTypes.BOOL })
@emit('Transfer')
public transfer(calldata: Calldata): BytesWriter {
const to = calldata.readAddress();
const amount = calldata.readU256();
const from = Blockchain.tx.sender;
// Validation
if (to.equals(Address.zero())) {
throw new Revert('Cannot transfer to zero address');
}
// Get balances
const fromBalance = this.balances.get(from);
if (fromBalance < amount) {
throw new Revert('Insufficient balance');
}
// Update balances
this.balances.set(from, SafeMath.sub(fromBalance, amount));
this.balances.set(to, SafeMath.add(this.balances.get(to), amount));
return new BytesWriter(0);
}
@method({ name: 'account', type: ABIDataTypes.ADDRESS })
@returns({ name: 'balance', type: ABIDataTypes.UINT256 })
public balanceOf(calldata: Calldata): BytesWriter {
const address = calldata.readAddress();
const balance = this.balances.get(address);
const writer = new BytesWriter(32);
writer.writeU256(balance);
return writer;
}
@method()
@returns({ name: 'supply', type: ABIDataTypes.UINT256 })
public totalSupply(_calldata: Calldata): BytesWriter {
const writer = new BytesWriter(32);
writer.writeU256(this._totalSupply.value);
return writer;
}
}
```
**Note:** Method routing is handled AUTOMATICALLY via `@method` decorators. No `execute` override is needed.
## Inheritance
### Extending OP_NET
```typescript
// Direct extension
export class MyContract extends OP_NET { }
// Extend with reentrancy protection
export class MySecureContract extends ReentrancyGuard { }
// Extend with additional features (OP20/OP721 extend ReentrancyGuard which extends OP_NET)
export class MyToken extends OP20 { } // OP20 extends ReentrancyGuard extends OP_NET
export class MyNFT extends OP721 { } // OP721 extends ReentrancyGuard extends OP_NET
```
### Adding Functionality
```typescript
// Create a base class with shared functionality
export abstract class Pausable extends OP_NET {
private pausedPointer: u16 = Blockchain.nextPointer;
protected _paused: StoredBoolean = new StoredBoolean(this.pausedPointer, false);
protected whenNotPaused(): void {
if (this._paused.value) {
throw new Revert('Contract is paused');
}
}
}
// Use in your contract
export class MyToken extends Pausable {
@method(
{ name: 'to', type: ABIDataTypes.ADDRESS },
{ name: 'amount', type: ABIDataTypes.UINT256 },
)
@returns({ name: 'success', type: ABIDataTypes.BOOL })
@emit('Transfer')
public transfer(calldata: Calldata): BytesWriter {
this.whenNotPaused();
// ...
}
}
```
**Solidity Comparison:**
```solidity
// Solidity: OpenZeppelin Pausable
import "@openzeppelin/contracts/security/Pausable.sol";
contract MyToken is ERC20, Pausable {
function transfer(address to, uint256 amount) public whenNotPaused {
// ...
}
}
// OP_NET: Custom Pausable base class
// export abstract class Pausable extends OP_NET { ... }
// this.whenNotPaused(); at start of method
```
## Best Practices
### 1. Always Use @final
```typescript
@final // Prevents further inheritance, enables optimizations
export class MyContract extends OP_NET { }
```
### 2. Call super() in Constructor
```typescript
public constructor() {
super(); // Always first!
// Then your initialization...
}
```
### 3. Use @method Decorators for Public Methods
```typescript
// CORRECT: Use @method decorator for automatic routing
@method({ name: 'to', type: ABIDataTypes.ADDRESS }, { name: 'amount', type: ABIDataTypes.UINT256 })
@returns({ name: 'success', type: ABIDataTypes.BOOL })
@emit('Transfer')
public transfer(calldata: Calldata): BytesWriter {
// Implementation...
}
// DO NOT manually override execute() - routing is automatic
```
### 4. Document Your Methods
```typescript
/**
* Transfers tokens from sender to recipient.
* @param calldata Contains: to (Address), amount (u256)
* @returns Empty BytesWriter on success
* @throws Revert if insufficient balance or zero address
*/
private transfer(calldata: Calldata): BytesWriter {
// ...
}
```
---
**Navigation:**
- Previous: [Security](../core-concepts/security.md)
- Next: [OP20 Token](./op20-token.md)