@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
1,072 lines (864 loc) • 31.6 kB
Markdown
# Plugins
Plugins extend contract functionality through lifecycle hooks. They allow modular features that can be shared across contracts.
## Overview
```typescript
import { Plugin, Blockchain, Calldata, Selector, BytesWriter } from '@btc-vision/btc-runtime/runtime';
// Create a plugin by extending Plugin class
class MyPlugin extends Plugin {
public override onDeployment(calldata: Calldata): void {
// Called during contract deployment
}
public override onUpdate(calldata: Calldata): void {
// Called when contract bytecode is updated
}
public override onExecutionStarted(selector: Selector, calldata: Calldata): void {
// Called before each method execution
}
public override onExecutionCompleted(selector: Selector, calldata: Calldata): void {
// Called after each successful method execution
}
public override execute(method: Selector, calldata: Calldata): BytesWriter | null {
// Handle method selectors - return BytesWriter if handled, null if not
return null;
}
}
// Register with contract
this.registerPlugin(new MyPlugin());
```
## Plugin Lifecycle
Plugins are initialized when the contract is deployed and can intercept method calls or handle them directly:
```mermaid
---
config:
theme: dark
---
sequenceDiagram
participant Deployer as 👤 Deployer
participant Contract as Contract
participant Plugin as Plugin
participant Blockchain as OP_NET Runtime
Note over Deployer,Blockchain: Contract Deployment
Deployer->>Contract: constructor()
Contract->>Plugin: new Plugin()
Contract->>Contract: registerPlugin(plugin)
Deployer->>Contract: deploy(calldata)
Blockchain->>Plugin: onDeployment(calldata)
Note over Plugin: Initialize plugin state
Plugin-->>Blockchain: complete
Blockchain->>Contract: onDeployment(calldata)
Note over Contract: Initialize contract state
Contract-->>Blockchain: complete
Note over Deployer,Blockchain: Method Execution
Deployer->>Contract: method(calldata)
Blockchain->>Plugin: onExecutionStarted(selector, calldata)
Note over Plugin: Pre-execution checks<br/>(access control, pausing, etc.)
Plugin-->>Blockchain: continue
alt Plugin handles selector
Blockchain->>Plugin: execute(selector, calldata)
Plugin-->>Blockchain: BytesWriter result
else Contract handles selector
Blockchain->>Contract: method executes
Note over Contract: Core business logic
Contract-->>Blockchain: result
end
Blockchain->>Plugin: onExecutionCompleted(selector, calldata)
Note over Plugin: Post-execution tasks<br/>(metrics, logging, etc.)
Plugin-->>Blockchain: complete
Blockchain-->>Deployer: return result
```
## Plugin Interface
The base Plugin class that all plugins extend:
```mermaid
---
config:
theme: dark
---
classDiagram
class Plugin {
<<base>>
+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|null
}
class RoleBasedAccessPlugin {
-rolesPointer: u16
-roles: AddressMemoryMap
+hasRole(account: Address, role: u256) bool
+grantRole(account: Address, role: u256) void
+revokeRole(account: Address, role: u256) void
}
class PausablePlugin {
-pausedPointer: u16
-_paused: StoredBoolean
+paused: bool
+pause() void
+unpause() void
}
class FeeCollectorPlugin {
-feeRecipientPointer: u16
-feePercentPointer: u16
+calculateFee(amount: u256) u256
+setFeeRecipient(recipient: Address) void
+setFeePercent(percent: u256) void
}
Plugin <|-- RoleBasedAccessPlugin
Plugin <|-- PausablePlugin
Plugin <|-- FeeCollectorPlugin
```
### Base Plugin Class
The `Plugin` class provides lifecycle hooks and method handling:
```typescript
export class Plugin {
// Called once during contract deployment, before contract's onDeployment
public onDeployment(_calldata: Calldata): void {}
// Called when contract bytecode is updated via updateContractFromExisting
public onUpdate(_calldata: Calldata): void {}
// Called before each method execution
public onExecutionStarted(_selector: Selector, _calldata: Calldata): void {}
// Called after each successful method execution
public onExecutionCompleted(_selector: Selector, _calldata: Calldata): void {}
// Handle method selectors - return BytesWriter if handled, null if not
public execute(_method: Selector, _calldata: Calldata): BytesWriter | null {
return null;
}
}
```
### Implementing a Plugin
```typescript
import {
Plugin,
Calldata,
Selector,
Blockchain,
BytesWriter,
Revert,
encodeSelector
} from '-vision/btc-runtime/runtime';
// Simple logging plugin using lifecycle hooks
class LoggingPlugin extends Plugin {
public override onDeployment(calldata: Calldata): void {
// Log deployment
}
public override onUpdate(calldata: Calldata): void {
// Handle contract updates
}
public override onExecutionStarted(selector: Selector, calldata: Calldata): void {
// Log method call before execution
}
public override onExecutionCompleted(selector: Selector, calldata: Calldata): void {
// Log method completion
}
}
// Plugin that handles method selectors (like UpdatablePlugin)
class MethodHandlerPlugin extends Plugin {
public override execute(method: Selector, calldata: Calldata): BytesWriter | null {
switch (method) {
case encodeSelector('myPluginMethod()'):
return this.myPluginMethod();
default:
return null; // Not handled by this plugin
}
}
private myPluginMethod(): BytesWriter {
// Method implementation
return new BytesWriter(0);
}
}
```
## Registration
### In Contract Constructor
```typescript
import { OP_NET, Blockchain } from '@btc-vision/btc-runtime/runtime';
export class MyContract extends OP_NET {
private loggingPlugin: LoggingPlugin;
public constructor() {
super();
// Create and register plugins
this.loggingPlugin = new LoggingPlugin();
this.registerPlugin(this.loggingPlugin);
}
}
```
### Conditional Registration in onDeployment
```typescript
public override onDeployment(calldata: Calldata): void {
const enableLogging = calldata.readBoolean();
if (enableLogging) {
this.registerPlugin(new LoggingPlugin());
}
// Continue with normal deployment
super.onDeployment(calldata);
}
```
## Role-Based Access Control
This diagram shows how the RBAC plugin intercepts calls and checks permissions:
```mermaid
---
config:
theme: dark
---
flowchart LR
subgraph OP_NET["OP_NET Plugin-Based Access Control"]
A["👤 User calls method"] --> B["onExecutionStarted"]
B --> C{"Method requires role?"}
C -->|"No"| D["Allow execution"]
C -->|"Yes"| E["Get required role for selector"]
E --> F["Load user's roles from storage"]
F --> G{"User has role?<br/>Bitwise AND check"}
G -->|"Yes"| H["Allow execution"]
G -->|"No"| I["Revert: Missing required role"]
D --> J["Method executes"]
H --> J
subgraph RoleBits["Role Enum - Powers of 2"]
R1["Role.ADMIN = 1"]
R2["Role.MINTER = 2"]
R3["Role.PAUSER = 4"]
R4["Role.OPERATOR = 8"]
end
subgraph BitwiseOps["Bitwise Operations"]
B1["Grant: currentRoles OR role"]
B2["Check: currentRoles AND role != 0"]
B3["Revoke: currentRoles AND NOT role"]
end
end
```
### Access Control Plugin Implementation
Use an enum with bit flags for role management (powers of 2):
```typescript
import { u256 } from '@btc-vision/as-bignum/assembly';
import {
Plugin,
Calldata,
Selector,
Blockchain,
Address,
Revert,
SafeMath,
StoredU256,
AddressMemoryMap,
encodeSelector
} from '-vision/btc-runtime/runtime';
// Define method selectors (sha256 first 4 bytes of method signature)
const MINT_SELECTOR: u32 = 0x40c10f19; // mint(address,uint256)
const PAUSE_SELECTOR: u32 = 0x8456cb59; // pause()
const UNPAUSE_SELECTOR: u32 = 0x3f4ba83a; // unpause()
const SET_OPERATOR_SELECTOR: u32 = 0xb3ab15fb; // setOperator(address)
// Role enum (bit flags - must be powers of 2)
enum Role {
ADMIN = 1, // 2^0
MINTER = 2, // 2^1
PAUSER = 4, // 2^2
OPERATOR = 8 // 2^3
}
class RoleBasedAccessPlugin extends Plugin {
private rolesPointer: u16;
private roles: AddressMemoryMap;
public constructor(pointer: u16) {
super();
this.rolesPointer = pointer;
this.roles = new AddressMemoryMap(this.rolesPointer);
}
public override onDeployment(calldata: Calldata): void {
// Grant admin role to deployer
const deployer = Blockchain.tx.origin;
this.grantRole(deployer, u256.fromU64(Role.ADMIN));
}
public override onExecutionStarted(selector: Selector, calldata: Calldata): void {
const requiredRole = this.getRequiredRole(selector);
if (!requiredRole.isZero()) {
const sender = Blockchain.tx.sender;
if (!this.hasRole(sender, requiredRole)) {
throw new Revert('Missing required role');
}
}
}
public override onExecutionCompleted(selector: Selector, calldata: Calldata): void {
// No-op for this plugin
}
public hasRole(account: Address, role: u256): bool {
const userRoles = this.roles.get(account);
// Check if role bit is set using bitwise AND
return !SafeMath.and(userRoles, role).isZero();
}
public grantRole(account: Address, role: u256): void {
const currentRoles = this.roles.get(account);
// Set role bit using bitwise OR
this.roles.set(account, SafeMath.or(currentRoles, role));
}
public revokeRole(account: Address, role: u256): void {
const currentRoles = this.roles.get(account);
// Clear role bit using AND with inverted role
const invertedRole = SafeMath.xor(role, u256.Max);
this.roles.set(account, SafeMath.and(currentRoles, invertedRole));
}
private getRequiredRole(selector: Selector): u256 {
// Map methods to required roles
switch (selector) {
case MINT_SELECTOR:
return u256.fromU64(Role.MINTER);
case PAUSE_SELECTOR:
case UNPAUSE_SELECTOR:
return u256.fromU64(Role.PAUSER);
case SET_OPERATOR_SELECTOR:
return u256.fromU64(Role.ADMIN);
default:
return u256.Zero; // No role required
}
}
}
```
## Pausable Plugin
```typescript
import {
Plugin,
Calldata,
Selector,
Blockchain,
Revert,
StoredBoolean,
encodeSelector
} from '-vision/btc-runtime/runtime';
// Define method selectors (sha256 first 4 bytes of method signature)
const BALANCE_OF_SELECTOR: u32 = 0x70a08231; // balanceOf(address)
const TOTAL_SUPPLY_SELECTOR: u32 = 0x18160ddd; // totalSupply()
const NAME_SELECTOR: u32 = 0x06fdde03; // name()
const SYMBOL_SELECTOR: u32 = 0x95d89b41; // symbol()
const PAUSE_SELECTOR: u32 = 0x8456cb59; // pause()
const UNPAUSE_SELECTOR: u32 = 0x3f4ba83a; // unpause()
class PausablePlugin extends Plugin {
private pausedPointer: u16;
private _paused: StoredBoolean;
public constructor(pointer: u16) {
super();
this.pausedPointer = pointer;
this._paused = new StoredBoolean(this.pausedPointer, false);
}
public override onDeployment(calldata: Calldata): void {
// Start unpaused by default
}
public override onExecutionStarted(selector: Selector, calldata: Calldata): void {
// Skip pause check for view methods
if (this.isViewMethod(selector)) return;
// Skip pause check for admin methods
if (this.isAdminMethod(selector)) return;
if (this._paused.value) {
throw new Revert('Contract is paused');
}
}
public override onExecutionCompleted(selector: Selector, calldata: Calldata): void {
// No-op for this plugin
}
public get paused(): bool {
return this._paused.value;
}
public pause(): void {
this._paused.value = true;
}
public unpause(): void {
this._paused.value = false;
}
private isViewMethod(selector: Selector): bool {
switch (selector) {
case BALANCE_OF_SELECTOR:
case TOTAL_SUPPLY_SELECTOR:
case NAME_SELECTOR:
case SYMBOL_SELECTOR:
return true;
default:
return false;
}
}
private isAdminMethod(selector: Selector): bool {
switch (selector) {
case PAUSE_SELECTOR:
case UNPAUSE_SELECTOR:
return true;
default:
return false;
}
}
}
```
## Fee Collector Plugin
```typescript
import { u256 } from '@btc-vision/as-bignum/assembly';
import {
Plugin,
Calldata,
Selector,
Address,
SafeMath,
StoredAddress,
StoredU256,
EMPTY_POINTER
} from '-vision/btc-runtime/runtime';
class FeeCollectorPlugin extends Plugin {
private feeRecipientPointer: u16;
private feePercentPointer: u16;
private _feeRecipient: StoredAddress;
private _feePercent: StoredU256; // Basis points (100 = 1%)
public constructor(recipientPointer: u16, percentPointer: u16) {
super();
this.feeRecipientPointer = recipientPointer;
this.feePercentPointer = percentPointer;
this._feeRecipient = new StoredAddress(this.feeRecipientPointer, Address.zero());
this._feePercent = new StoredU256(this.feePercentPointer, EMPTY_POINTER);
}
public override onDeployment(calldata: Calldata): void {
// Fee configuration set separately
}
public override onExecutionStarted(selector: Selector, calldata: Calldata): void {
// No pre-execution logic needed
}
public override onExecutionCompleted(selector: Selector, calldata: Calldata): void {
// Could track fees collected per method
}
public setFeeRecipient(recipient: Address): void {
this._feeRecipient.value = recipient;
}
public setFeePercent(percent: u256): void {
this._feePercent.value = percent;
}
public calculateFee(amount: u256): u256 {
return SafeMath.div(
SafeMath.mul(amount, this._feePercent.value),
u256.fromU64(10000) // Basis points denominator
);
}
public get feeRecipient(): Address {
return this._feeRecipient.value;
}
public get feePercent(): u256 {
return this._feePercent.value;
}
}
```
## Plugin Communication
### Using Plugins in Contracts
```typescript
import { u256 } from '@btc-vision/as-bignum/assembly';
import {
OP_NET,
Blockchain,
Calldata,
BytesWriter,
Address,
SafeMath,
ABIDataTypes
} from '-vision/btc-runtime/runtime';
export class MyContract extends OP_NET {
private feePlugin: FeeCollectorPlugin;
public constructor() {
super();
// Use nextPointer for storage allocation
this.feePlugin = new FeeCollectorPlugin(
Blockchain.nextPointer,
Blockchain.nextPointer
);
this.registerPlugin(this.feePlugin);
}
(
{ name: 'to', type: ABIDataTypes.ADDRESS },
{ name: 'amount', type: ABIDataTypes.UINT256 },
)
public transfer(calldata: Calldata): BytesWriter {
const to = calldata.readAddress();
const amount = calldata.readU256();
// Use plugin to calculate fee
const fee = this.feePlugin.calculateFee(amount);
const netAmount = SafeMath.sub(amount, fee);
// Transfer net amount to recipient
this._transfer(Blockchain.tx.sender, to, netAmount);
// Transfer fee to collector
if (!fee.isZero()) {
this._transfer(Blockchain.tx.sender, this.feePlugin.feeRecipient, fee);
}
return new BytesWriter(0);
}
}
```
## Lifecycle Hooks
### Execution Order
```
Deployment:
1. Plugin.onDeployment (for each registered plugin, in order)
2. Contract.onDeployment
Update (when bytecode is updated):
1. Plugin.onUpdate (for each registered plugin, in order)
2. Contract.onUpdate
Method execution:
1. Plugin.onExecutionStarted (for each registered plugin, in order)
2. Contract.onExecutionStarted
3. Plugin.execute (check plugins first, return if handled)
4. Contract method executes (if no plugin handled it)
5. Plugin.onExecutionCompleted (for each registered plugin, in order)
6. Contract.onExecutionCompleted
```
### Metrics Plugin Example
```typescript
import {
Plugin,
Calldata,
Selector,
StoredU256,
SafeMath,
EMPTY_POINTER
} from '-vision/btc-runtime/runtime';
import { u256 } from '@btc-vision/as-bignum/assembly';
class MetricsPlugin extends Plugin {
private totalCallsPointer: u16;
private _totalCalls: StoredU256;
public constructor(pointer: u16) {
super();
this.totalCallsPointer = pointer;
this._totalCalls = new StoredU256(this.totalCallsPointer, EMPTY_POINTER);
}
public override onDeployment(calldata: Calldata): void {
// Initialize metrics
}
public override onExecutionStarted(selector: Selector, calldata: Calldata): void {
// Increment call counter
this._totalCalls.value = SafeMath.add(this._totalCalls.value, u256.One);
}
public override onExecutionCompleted(selector: Selector, calldata: Calldata): void {
// Could track successful completions separately
}
public get totalCalls(): u256 {
return this._totalCalls.value;
}
}
```
## Solidity vs OP_NET: Plugin System Comparison
OP_NET plugins provide a more flexible and powerful alternative to Solidity's inheritance and modifier patterns. They enable cross-cutting concerns without tight coupling.
### Feature Comparison Table
| Feature | Solidity/EVM | OP_NET | OP_NET Advantage |
|---------|--------------|-------|-----------------|
| **Code Reuse Pattern** | Inheritance | Composition (Plugins) | Flexible, no diamond problem |
| **Pre-Execution Hooks** | Modifiers | `onExecutionStarted` | Centralized, selector-aware |
| **Post-Execution Hooks** | Limited (manual) | `onExecutionCompleted` | Automatic after success |
| **Deployment Hooks** | Constructor only | `onDeployment` per plugin | Modular initialization |
| **Method Interception** | Modifier per function | Selector-based routing | One plugin, all methods |
| **Multiple Behaviors** | Multiple inheritance | Multiple plugins | No conflicts |
| **Dynamic Registration** | Not possible | Runtime registration | Conditional features |
| **Execution Order** | Modifier stack | Plugin registration order | Explicit control |
### Pattern Mapping Table
| Solidity Pattern | OP_NET Plugin Equivalent | Improvement |
|-----------------|------------------------|-------------|
| OpenZeppelin `Ownable` | `RoleBasedAccessPlugin` with ADMIN role | Multi-role support |
| OpenZeppelin `Pausable` | `PausablePlugin` | Selector-specific pausing |
| OpenZeppelin `AccessControl` | `RoleBasedAccessPlugin` | Bit-flag roles, efficient storage |
| `nonReentrant` modifier | Reentrancy guard in `onExecutionStarted` | Global protection |
| Function modifiers | `onExecutionStarted` hook | Centralized logic |
| `Initializable` | `onDeployment` hook | Per-plugin initialization |
| OpenZeppelin `ERC20Snapshot` | Metrics plugin pattern | Flexible tracking |
### Capability Matrix
| Capability | Solidity | OP_NET |
|------------|:--------:|:-----:|
| Pre-execution hooks | Modifiers (per-function) | Plugins (centralized) |
| Post-execution hooks | Manual | Built-in |
| Deployment initialization | Constructor | Per-plugin onDeployment |
| Selector-based routing | Manual switch | Built-in selector parameter |
| Dynamic feature toggles | Storage + modifiers | Conditional plugin registration |
| Cross-cutting logging | Manual in each function | Single metrics plugin |
| Role-based access | Multiple modifiers | Single plugin, bitwise roles |
| Pausable with exceptions | Complex modifiers | Selector whitelist |
### Inheritance vs Composition
```mermaid
---
config:
theme: dark
---
flowchart TB
subgraph Solidity["Solidity - Inheritance Chain"]
S1["Contract"] --> S2["Ownable"]
S1 --> S3["Pausable"]
S1 --> S4["AccessControl"]
S2 --> S5["Context"]
S3 --> S5
S4 --> S5
S6["Diamond Problem Risk"]
end
subgraph OP_NET["OP_NET - Plugin Composition"]
O1["Contract"] --> O2["OP_NET base"]
O3["AccessPlugin"] -.->|"registered"| O1
O4["PausablePlugin"] -.->|"registered"| O1
O5["FeePlugin"] -.->|"registered"| O1
O6["No Inheritance Conflicts"]
end
```
### Code Complexity Comparison
| Aspect | Solidity | OP_NET |
|--------|----------|-------|
| Adding access control | Import + inherit + add modifiers | Register plugin |
| Adding pausable | Import + inherit + add modifiers | Register plugin |
| Combining both | Multiple inheritance + modifier stacking | Register both plugins |
| Method-specific rules | Separate modifier per method | Selector switch in plugin |
| Adding new role | Modify contract, redeploy | Update plugin role mapping |
### Access Control Comparison
#### Solidity: OpenZeppelin Inheritance
```solidity
// Solidity with OpenZeppelin - Inheritance-based
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract MyContract is Ownable, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
// Must add modifier to EVERY protected function
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
// ...
}
function pause() external onlyRole(PAUSER_ROLE) {
// ...
}
function adminOnly() external onlyOwner {
// ...
}
// Limitations:
// - Must add modifier to each function
// - Role checks scattered across contract
// - String-based role hashes (inefficient)
// - Complex inheritance hierarchy
}
```
#### OP_NET: Plugin-Based Composition
```typescript
// OP_NET with Plugin - Composition-based
export class MyContract extends OP_NET {
private accessPlugin: RoleBasedAccessPlugin;
public constructor() {
super();
// Single plugin handles ALL access control
this.accessPlugin = new RoleBasedAccessPlugin(Blockchain.nextPointer);
this.registerPlugin(this.accessPlugin);
}
()
public mint(_calldata: Calldata): BytesWriter {
// Plugin automatically checks MINTER role
// based on selector mapping - no modifier needed!
return new BytesWriter(0);
}
()
public pause(_calldata: Calldata): BytesWriter {
// Plugin automatically checks PAUSER role
return new BytesWriter(0);
}
()
public adminOnly(_calldata: Calldata): BytesWriter {
// Plugin automatically checks ADMIN role
return new BytesWriter(0);
}
// Advantages:
// - No modifiers on individual functions
// - Centralized access control logic
// - Bitwise roles (efficient storage)
// - Role-to-selector mapping in one place
}
```
### Pausable Comparison
#### Solidity: Modifier Per Function
```solidity
// Solidity with OpenZeppelin
import "@openzeppelin/contracts/security/Pausable.sol";
contract MyContract is Pausable {
// Must add whenNotPaused to EVERY pausable function
function transfer(address to, uint256 amount) external whenNotPaused {
// ...
}
function approve(address spender, uint256 amount) external whenNotPaused {
// ...
}
// View functions don't need modifier (manual decision)
function balanceOf(address owner) external view returns (uint256) {
// ...
}
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
// Limitations:
// - Must remember to add modifier to each function
// - Easy to forget on new functions
// - No automatic view function detection
}
```
#### OP_NET: Automatic Selector-Based Pausing
```typescript
// OP_NET with Plugin
export class MyContract extends OP_NET {
private pausablePlugin: PausablePlugin;
public constructor() {
super();
this.pausablePlugin = new PausablePlugin(Blockchain.nextPointer);
this.registerPlugin(this.pausablePlugin);
}
(ABIDataTypes.UINT256)
public transfer(calldata: Calldata): BytesWriter {
// Plugin automatically checks pause status
// No modifier needed!
const amount = calldata.readU256();
return new BytesWriter(0);
}
(ABIDataTypes.ADDRESS)
public balanceOf(calldata: Calldata): BytesWriter {
// Plugin knows this is a view method (via selector)
// Automatically skips pause check
return new BytesWriter(32);
}
()
public pause(_calldata: Calldata): BytesWriter {
// Admin methods also whitelisted automatically
this.pausablePlugin.pause();
return new BytesWriter(0);
}
// Advantages:
// - No modifiers needed on individual functions
// - View methods automatically detected via selector
// - Admin methods automatically whitelisted
// - Cannot forget to protect a function
}
```
### Multiple Plugins Example
```typescript
// OP_NET - Composing multiple plugins (no inheritance conflicts)
export class CompleteContract extends OP_NET {
private accessPlugin: RoleBasedAccessPlugin;
private pausablePlugin: PausablePlugin;
private feePlugin: FeeCollectorPlugin;
private metricsPlugin: MetricsPlugin;
public constructor() {
super();
// Order matters - security checks first!
this.accessPlugin = new RoleBasedAccessPlugin(Blockchain.nextPointer);
this.registerPlugin(this.accessPlugin); // 1. Check permissions
this.pausablePlugin = new PausablePlugin(Blockchain.nextPointer);
this.registerPlugin(this.pausablePlugin); // 2. Check pause status
this.feePlugin = new FeeCollectorPlugin(
Blockchain.nextPointer,
Blockchain.nextPointer
);
this.registerPlugin(this.feePlugin); // 3. Fee calculations
this.metricsPlugin = new MetricsPlugin(Blockchain.nextPointer);
this.registerPlugin(this.metricsPlugin); // 4. Track metrics
}
// All plugins execute their hooks automatically
// Execution order: access -> pausable -> fee -> metrics -> method
}
```
### Lifecycle Hook Comparison
| Lifecycle Event | Solidity | OP_NET |
|-----------------|----------|-------|
| Contract deployment | Single constructor | `onDeployment` per plugin + contract |
| Contract update | Proxy reinitialize | `onUpdate` per plugin + contract |
| Before method call | Modifiers (manual per function) | `onExecutionStarted` (automatic) |
| Method handling | Function dispatch | `execute` returns BytesWriter or null |
| After method call | No built-in hook | `onExecutionCompleted` (automatic) |
| Error handling | try/catch (limited) | Revert in any hook |
### Why OP_NET Plugins?
| Solidity Limitation | OP_NET Solution |
|---------------------|----------------|
| Modifier on every function | Centralized selector-based routing |
| Multiple inheritance complexity | Simple composition |
| Diamond problem risk | No inheritance conflicts |
| String-based role hashes | Efficient bitwise roles |
| Manual post-execution hooks | Built-in `onExecutionCompleted` |
| Constructor-only initialization | Per-plugin `onDeployment` |
| Static feature set | Dynamic plugin registration |
| Scattered access logic | Centralized in plugins |
## Best Practices
### 1. Single Responsibility
```typescript
// Good: Focused plugins
class PausablePlugin extends Plugin { }
class AccessControlPlugin extends Plugin { }
class FeeCollectorPlugin extends Plugin { }
// Bad: Monolithic plugin
class EverythingPlugin extends Plugin {
// Handles pausing, access control, fees, logging...
}
```
### 2. Use `public override` for Hook Methods
```typescript
class MyPlugin extends Plugin {
// Always use 'public override' when overriding Plugin methods
public override onDeployment(calldata: Calldata): void {
// Implementation
}
public override onUpdate(calldata: Calldata): void {
// Implementation
}
public override onExecutionStarted(selector: Selector, calldata: Calldata): void {
// Implementation
}
public override onExecutionCompleted(selector: Selector, calldata: Calldata): void {
// Implementation
}
public override execute(method: Selector, calldata: Calldata): BytesWriter | null {
// Return BytesWriter if handled, null if not
return null;
}
}
```
### 3. Use Proper Storage
```typescript
// Good: Use StoredU256, StoredBoolean, etc. for persistent state
class MyPlugin extends Plugin {
private pointer: u16;
private _value: StoredU256;
public constructor(pointer: u16) {
super();
this.pointer = pointer;
this._value = new StoredU256(this.pointer, EMPTY_POINTER);
}
}
// Bad: Regular class fields don't persist across calls
class BadPlugin extends Plugin {
private value: u256 = u256.Zero; // This won't persist!
}
```
### 4. Use Role Enum with Bit Flags
```typescript
// Good: Use enum with bit flags for roles (powers of 2)
enum Role {
ADMIN = 1, // 2^0
MINTER = 2, // 2^1
PAUSER = 4 // 2^2
}
// Convert enum to u256 for storage/comparison
const adminRole: u256 = u256.fromU64(Role.ADMIN);
// Check role with bitwise AND
const hasRole = !SafeMath.and(userRoles, u256.fromU64(Role.MINTER)).isZero();
// Bad: String-based roles
// const roles = new Map<string, Set<Address>>(); // Don't do this!
```
### 5. Handle Errors Gracefully
```typescript
public override onExecutionStarted(selector: Selector, calldata: Calldata): void {
// Throw Revert for access control failures
if (!this.hasPermission(Blockchain.tx.sender, selector)) {
throw new Revert('Access denied');
}
}
```
### 6. Order Plugins Correctly
```typescript
// Security checks should come first
this.registerPlugin(this.accessControl); // Check permissions first
this.registerPlugin(this.pausable); // Then pausable
this.registerPlugin(this.metrics); // Metrics last
```
---
**Navigation:**
- Previous: [Bitcoin Scripts](./bitcoin-scripts.md)
- Next: [Basic Token Example](../examples/basic-token.md)