UNPKG

@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
# 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)