Skip to content

Latest commit

 

History

History
1071 lines (864 loc) · 30.9 KB

File metadata and controls

1071 lines (864 loc) · 30.9 KB

Plugins

Plugins extend contract functionality through lifecycle hooks. They allow modular features that can be shared across contracts.

Overview

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:

---
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
Loading

Plugin Interface

The base Plugin class that all plugins extend:

---
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
Loading

Base Plugin Class

The Plugin class provides lifecycle hooks and method handling:

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

import {
    Plugin,
    Calldata,
    Selector,
    Blockchain,
    BytesWriter,
    Revert,
    encodeSelector
} from '@btc-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

import { OP_NET, Blockchain } from '@btc-vision/btc-runtime/runtime';

@final
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

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:

---
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
Loading

Access Control Plugin Implementation

Use an enum with bit flags for role management (powers of 2):

import { u256 } from '@btc-vision/as-bignum/assembly';
import {
    Plugin,
    Calldata,
    Selector,
    Blockchain,
    Address,
    Revert,
    SafeMath,
    StoredU256,
    AddressMemoryMap,
    encodeSelector
} from '@btc-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

import {
    Plugin,
    Calldata,
    Selector,
    Blockchain,
    Revert,
    StoredBoolean,
    encodeSelector
} from '@btc-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

import { u256 } from '@btc-vision/as-bignum/assembly';
import {
    Plugin,
    Calldata,
    Selector,
    Address,
    SafeMath,
    StoredAddress,
    StoredU256,
    EMPTY_POINTER
} from '@btc-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

import { u256 } from '@btc-vision/as-bignum/assembly';
import {
    OP_NET,
    Blockchain,
    Calldata,
    BytesWriter,
    Address,
    SafeMath,
    ABIDataTypes
} from '@btc-vision/btc-runtime/runtime';

@final
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);
    }

    @method(
        { 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

import {
    Plugin,
    Calldata,
    Selector,
    StoredU256,
    SafeMath,
    EMPTY_POINTER
} from '@btc-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

---
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
Loading

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 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

// OP_NET with Plugin - Composition-based
@final
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);
    }

    @method()
    public mint(_calldata: Calldata): BytesWriter {
        // Plugin automatically checks MINTER role
        // based on selector mapping - no modifier needed!
        return new BytesWriter(0);
    }

    @method()
    public pause(_calldata: Calldata): BytesWriter {
        // Plugin automatically checks PAUSER role
        return new BytesWriter(0);
    }

    @method()
    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 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

// OP_NET with Plugin
@final
export class MyContract extends OP_NET {
    private pausablePlugin: PausablePlugin;

    public constructor() {
        super();
        this.pausablePlugin = new PausablePlugin(Blockchain.nextPointer);
        this.registerPlugin(this.pausablePlugin);
    }

    @method(ABIDataTypes.UINT256)
    public transfer(calldata: Calldata): BytesWriter {
        // Plugin automatically checks pause status
        // No modifier needed!
        const amount = calldata.readU256();
        return new BytesWriter(0);
    }

    @method(ABIDataTypes.ADDRESS)
    public balanceOf(calldata: Calldata): BytesWriter {
        // Plugin knows this is a view method (via selector)
        // Automatically skips pause check
        return new BytesWriter(32);
    }

    @method()
    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

// OP_NET - Composing multiple plugins (no inheritance conflicts)
@final
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

// 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

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

// 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

// 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

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

// 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: