Skip to content

Latest commit

 

History

History
902 lines (701 loc) · 23.5 KB

File metadata and controls

902 lines (701 loc) · 23.5 KB

OP20 API Reference

The OP20 class implements the fungible token standard, equivalent to ERC20 on Ethereum.

Import

import { OP20, OP20InitParameters } from '@btc-vision/btc-runtime/runtime';

OP20 Architecture

classDiagram
    class OP20 {
        <<abstract>>
        +_totalSupply: StoredU256
        #_maxSupply: StoredU256
        #_decimals: StoredU256
        #_name: StoredString
        #_symbol: StoredString
        #_icon: StoredString
        #balanceOfMap: AddressMemoryMap
        #allowanceMap: MapOfMap~u256~
        #_nonceMap: AddressMemoryMap
        +instantiate(params, skipDeployerVerification) void
        +name(calldata) BytesWriter
        +symbol(calldata) BytesWriter
        +icon(calldata) BytesWriter
        +decimals(calldata) BytesWriter
        +totalSupply(calldata) BytesWriter
        +maximumSupply(calldata) BytesWriter
        +balanceOf(calldata) BytesWriter
        +allowance(calldata) BytesWriter
        +nonceOf(calldata) BytesWriter
        +domainSeparator(calldata) BytesWriter
        +metadata(calldata) BytesWriter
        +transfer(calldata) BytesWriter
        +transferFrom(calldata) BytesWriter
        +safeTransfer(calldata) BytesWriter
        +safeTransferFrom(calldata) BytesWriter
        +increaseAllowance(calldata) BytesWriter
        +decreaseAllowance(calldata) BytesWriter
        +increaseAllowanceBySignature(calldata) BytesWriter
        +decreaseAllowanceBySignature(calldata) BytesWriter
        +burn(calldata) BytesWriter
        #_mint(to, amount) void
        #_burn(from, amount) void
        #_transfer(from, to, amount) void
        #_balanceOf(owner) u256
        #_allowance(owner, spender) u256
        #_increaseAllowance(owner, spender, amount) void
        #_decreaseAllowance(owner, spender, amount) void
        #_spendAllowance(owner, spender, amount) void
        #_safeTransfer(from, to, amount, data) void
    }

    class ReentrancyGuard {
        <<abstract>>
        #reentrancyLevel: ReentrancyLevel
        #isSelectorExcluded(selector) bool
    }

    class OP_NET {
        <<abstract>>
        +address: Address
        +contractDeployer: Address
        #emitEvent(event) void
        #onlyDeployer(caller) void
        Note: @method decorator handles routing
    }

    class MyToken {
        +constructor()
        +onDeployment(calldata) void
        +mint(calldata) BytesWriter
        Note: @method decorator handles routing
    }

    OP_NET <|-- ReentrancyGuard
    ReentrancyGuard <|-- OP20
    OP20 <|-- MyToken

    note for OP20 "Fungible token standard\nwith safe math and\nreentrancy protection"
    note for MyToken "Your implementation\nextends OP20"
Loading

Class Definition

@final
export class MyToken extends OP20 {
    public constructor() {
        super();
    }

    public override onDeployment(calldata: Calldata): void {
        this.instantiate(new OP20InitParameters(
            maxSupply,
            decimals,
            name,
            symbol,
            icon
        ));
    }
}

Initialization

OP20InitParameters

class OP20InitParameters {
    constructor(
        maxSupply: u256,
        decimals: u8,
        name: string,
        symbol: string,
        icon: string = ''
    )
}
Parameter Type Description
maxSupply u256 Maximum token supply (use u256.Max for unlimited)
decimals u8 Token decimals (max 32)
name string Token name
symbol string Token symbol
icon string Token icon URL (optional, default empty string)

instantiate

Initializes the OP20 token. Must be called in onDeployment.

public instantiate(params: OP20InitParameters, skipDeployerVerification: boolean = false): void
public override onDeployment(calldata: Calldata): void {
    this.instantiate(new OP20InitParameters(
        u256.fromU64(1000000),  // Max supply: 1M
        18,                     // 18 decimals
        'My Token',             // Name
        'MTK',                  // Symbol
        ''                      // Icon URL (optional)
    ));
}

Solidity Comparison:

Solidity (ERC20) OP_NET (OP20)
constructor(string name, string symbol) onDeployment(calldata) + instantiate()

View Methods

All view methods in OP20 take Calldata and return BytesWriter. The @method decorator handles ABI encoding/decoding.

name

Returns the token name.

@method()
@returns({ name: 'name', type: ABIDataTypes.STRING })
public name(calldata: Calldata): BytesWriter

symbol

Returns the token symbol.

@method()
@returns({ name: 'symbol', type: ABIDataTypes.STRING })
public symbol(calldata: Calldata): BytesWriter

icon

Returns the token icon URL.

@method()
@returns({ name: 'icon', type: ABIDataTypes.STRING })
public icon(calldata: Calldata): BytesWriter

decimals

Returns the number of decimals.

@method()
@returns({ name: 'decimals', type: ABIDataTypes.UINT8 })
public decimals(calldata: Calldata): BytesWriter

nonceOf

Returns the current nonce for an address (used for signature verification).

@method({ name: 'owner', type: ABIDataTypes.ADDRESS })
@returns({ name: 'nonce', type: ABIDataTypes.UINT256 })
public nonceOf(calldata: Calldata): BytesWriter

domainSeparator

Returns the EIP-712 domain separator for signature verification.

@method()
@returns({ name: 'domainSeparator', type: ABIDataTypes.BYTES32 })
public domainSeparator(calldata: Calldata): BytesWriter

metadata

Returns all token metadata in a single call (name, symbol, icon, decimals, totalSupply, domainSeparator).

@method()
@returns(
    { name: 'name', type: ABIDataTypes.STRING },
    { name: 'symbol', type: ABIDataTypes.STRING },
    { name: 'icon', type: ABIDataTypes.STRING },
    { name: 'decimals', type: ABIDataTypes.UINT8 },
    { name: 'totalSupply', type: ABIDataTypes.UINT256 },
    { name: 'domainSeparator', type: ABIDataTypes.BYTES32 },
)
public metadata(calldata: Calldata): BytesWriter

totalSupply

Returns current total supply.

@method()
@returns({ name: 'totalSupply', type: ABIDataTypes.UINT256 })
public totalSupply(calldata: Calldata): BytesWriter

maximumSupply

Returns maximum possible supply.

@method()
@returns({ name: 'maximumSupply', type: ABIDataTypes.UINT256 })
public maximumSupply(calldata: Calldata): BytesWriter

balanceOf

Returns balance of an address.

@method({ name: 'owner', type: ABIDataTypes.ADDRESS })
@returns({ name: 'balance', type: ABIDataTypes.UINT256 })
public balanceOf(calldata: Calldata): BytesWriter

Internal helper (for use within your contract):

protected _balanceOf(owner: Address): u256

allowance

Returns spending allowance.

@method(
    { name: 'owner', type: ABIDataTypes.ADDRESS },
    { name: 'spender', type: ABIDataTypes.ADDRESS },
)
@returns({ name: 'remaining', type: ABIDataTypes.UINT256 })
public allowance(calldata: Calldata): BytesWriter

Internal helper (for use within your contract):

protected _allowance(owner: Address, spender: Address): u256

Solidity Comparison:

Solidity (ERC20) OP_NET (OP20)
function name() view returns (string) name(calldata): BytesWriter
function balanceOf(address) view returns (uint256) balanceOf(calldata): BytesWriter
function allowance(address, address) view returns (uint256) allowance(calldata): BytesWriter

Transfer Methods

transfer (Calldata)

Transfers tokens from caller to recipient.

public transfer(calldata: Calldata): BytesWriter

Calldata format:

Field Type Size
to Address 32 bytes
amount u256 32 bytes

Returns: Boolean (success)

The following diagram shows the complete transfer validation and execution flow:

flowchart LR
    subgraph "Validation"
        Start([Transfer Request]) --> CheckFrom{from == zero?}
        CheckFrom -->|Yes| Revert1[Revert: Invalid sender]
        CheckFrom -->|No| CheckTo{to == zero?}
        CheckTo -->|Yes| Revert2[Revert: Invalid receiver]
        CheckTo -->|No| GetBal[Get balance of from]
    end

    subgraph "Balance Check"
        GetBal --> CheckBal{balance >= amount?}
        CheckBal -->|No| Revert3[Revert: Insufficient balance]
        CheckBal -->|Yes| SubBal[balance from -= amount]
    end

    subgraph "Transfer Execution"
        SubBal --> AddBal[balance to += amount]
        AddBal --> Emit[Emit TransferredEvent]
        Emit --> Success([Complete])
    end
Loading

transferFrom (Calldata)

Transfers tokens using allowance.

public transferFrom(calldata: Calldata): BytesWriter

Calldata format:

Field Type Size
from Address 32 bytes
to Address 32 bytes
amount u256 32 bytes

Returns: Boolean (success)

The following sequence diagram illustrates both standard transfer and transferFrom with allowance:

sequenceDiagram
    participant User as 👤 User
    participant Contract as OP20 Contract
    participant BalMap as Balance Map
    participant Events as Event System

    Note over User,Events: Standard Transfer Flow

    User->>Contract: transfer(to, amount)
    Contract->>Contract: _transfer(tx.sender, to, amount)

    Contract->>Contract: Validate from != zero
    Contract->>Contract: Validate to != zero

    Contract->>BalMap: Get balance[from]
    BalMap->>Contract: Return balance

    Contract->>Contract: Check balance >= amount

    Contract->>BalMap: Set balance[from] -= amount
    Contract->>BalMap: Set balance[to] += amount

    Contract->>Events: Emit TransferredEvent(operator, from, to, amount)

    Contract->>User: Return success

    Note over User,Events: TransferFrom with Allowance

    User->>Contract: transferFrom(from, to, amount)
    Contract->>Contract: _spendAllowance(from, tx.sender, amount)

    alt Allowance is u256.Max
        Contract->>Contract: Skip allowance deduction
    else Normal allowance
        Contract->>Contract: Check allowance >= amount
        Contract->>Contract: Deduct from allowance
    end

    Contract->>Contract: _transfer(from, to, amount)
    Contract->>Events: Emit TransferredEvent
    Contract->>User: Return success
Loading

Solidity Comparison:

Solidity (ERC20) OP_NET (OP20)
function transfer(address to, uint256 amount) returns (bool) transfer(calldata): BytesWriter
function transferFrom(address from, address to, uint256 amount) returns (bool) transferFrom(calldata): BytesWriter

Approval Methods

increaseAllowance (Calldata)

Increases the allowance granted to a spender.

public increaseAllowance(calldata: Calldata): BytesWriter

Calldata format:

Field Type Size
spender Address 32 bytes
amount u256 32 bytes

Returns: Empty BytesWriter (emits ApprovedEvent)

Note: If overflow would occur, sets allowance to u256.Max (unlimited).

decreaseAllowance (Calldata)

Decreases the allowance granted to a spender.

public decreaseAllowance(calldata: Calldata): BytesWriter

Calldata format:

Field Type Size
spender Address 32 bytes
amount u256 32 bytes

Returns: Empty BytesWriter (emits ApprovedEvent)

Note: If underflow would occur, sets allowance to zero.

increaseAllowanceBySignature (Calldata)

Increases allowance using an EIP-712 typed signature (gasless approval).

public increaseAllowanceBySignature(calldata: Calldata): BytesWriter

Calldata format:

Field Type Size
owner bytes32 32 bytes
ownerTweakedPublicKey bytes32 32 bytes
spender Address 32 bytes
amount u256 32 bytes
deadline u64 8 bytes
signature bytes variable

decreaseAllowanceBySignature (Calldata)

Decreases allowance using an EIP-712 typed signature.

public decreaseAllowanceBySignature(calldata: Calldata): BytesWriter

Calldata format:

Field Type Size
owner bytes32 32 bytes
ownerTweakedPublicKey bytes32 32 bytes
spender Address 32 bytes
amount u256 32 bytes
deadline u64 8 bytes
signature bytes variable

The following diagram shows the allowance management flow including both direct and signature-based methods:

flowchart LR
    subgraph "Allowance Method Selection"
        A[User Allows Spender] --> B{Choose<br/>Method}
        B -->|Direct| C[increaseAllowance]
        B -->|Off-chain| D[increaseAllowanceBySignature]
    end

    subgraph "Direct Allowance Path"
        C --> E[Get allowanceMap owner]
        E --> F[Get current for spender]
        F --> G[new = current + amount]
        G --> H{Overflow?}
        H -->|Yes| I[Set to u256.Max<br/>unlimited]
        H -->|No| J[Set new allowance]
    end

    subgraph "Signature Path"
        D --> L[Verify EIP-712<br/>signature]
        L --> M{Valid?}
        M -->|No| N[Revert:<br/>Invalid signature]
        M -->|Yes| O[Increment nonce]
        O --> E
    end

    I --> K[Emit ApprovedEvent]
    J --> K
Loading

The complete allowance lifecycle from approval to spending:

sequenceDiagram
    participant Owner
    participant Contract as OP20
    participant AllowMap as Allowance Map
    participant Spender

    Note over Owner,Spender: Standard Approval Flow

    Owner->>Contract: increaseAllowance(spender, 1000)
    Contract->>AllowMap: Get allowanceMap[owner]
    AllowMap->>Contract: Return owner's map
    Contract->>Contract: Get current allowance for spender
    Contract->>Contract: newAllowance = current + 1000
    Contract->>AllowMap: Set allowance[owner][spender] = newAllowance
    Contract->>Contract: Emit ApprovedEvent
    Contract->>Owner: Success

    Note over Owner,Spender: Spender Uses Allowance

    Spender->>Contract: transferFrom(owner, recipient, 500)
    Contract->>AllowMap: Get allowance[owner][spender]
    AllowMap->>Contract: Returns 1000

    alt Allowance is u256.Max
        Contract->>Contract: Skip deduction (infinite approval)
    else Normal allowance
        Contract->>Contract: Check 1000 >= 500
        Contract->>AllowMap: Set allowance to 1000 - 500 = 500
    end

    Contract->>Contract: Execute transfer
    Contract->>Spender: Success
Loading

Solidity Comparison:

Solidity (ERC20) OP_NET (OP20)
function approve(address, uint256) returns (bool) N/A (use increaseAllowance/decreaseAllowance)
function increaseAllowance(address, uint256) returns (bool) increaseAllowance(calldata): BytesWriter
function decreaseAllowance(address, uint256) returns (bool) decreaseAllowance(calldata): BytesWriter

Protected Methods

For internal contract use:

_mint

Mints new tokens.

protected _mint(to: Address, amount: u256): void
@method(
    { name: 'to', type: ABIDataTypes.ADDRESS },
    { name: 'amount', type: ABIDataTypes.UINT256 },
)
@returns({ name: 'success', type: ABIDataTypes.BOOL })
@emit('Mint')
public mint(calldata: Calldata): BytesWriter {
    this.onlyDeployer(Blockchain.tx.sender);
    const to: Address = calldata.readAddress();
    const amount: u256 = calldata.readU256();
    this._mint(to, amount);
    return new BytesWriter(0);
}

_burn

Burns tokens.

protected _burn(from: Address, amount: u256): void
@method({ name: 'amount', type: ABIDataTypes.UINT256 })
@returns({ name: 'success', type: ABIDataTypes.BOOL })
@emit('Burn')
public burn(calldata: Calldata): BytesWriter {
    const amount: u256 = calldata.readU256();
    this._burn(Blockchain.tx.sender, amount);
    return new BytesWriter(0);
}

_transfer

Internal transfer.

protected _transfer(from: Address, to: Address, amount: u256): void

_increaseAllowance

Internal method to increase allowance with overflow protection.

protected _increaseAllowance(owner: Address, spender: Address, amount: u256): void

_decreaseAllowance

Internal method to decrease allowance with underflow protection.

protected _decreaseAllowance(owner: Address, spender: Address, amount: u256): void

_spendAllowance

Decrements allowance.

protected _spendAllowance(owner: Address, spender: Address, amount: u256): void

Solidity Comparison:

Solidity (ERC20) OP_NET (OP20)
function _mint(address, uint256) internal _mint(Address, u256): void
function _burn(address, uint256) internal _burn(Address, u256): void
function _transfer(address, address, uint256) internal _transfer(Address, Address, u256): void

Safe Transfer Methods

safeTransfer

Transfer with recipient callback.

public safeTransfer(calldata: Calldata): BytesWriter

Calldata format:

Field Type Size
to Address 32 bytes
amount u256 32 bytes
data bytes variable (with length prefix)

Calls onOP20Received on recipient if it's a contract.

safeTransferFrom

TransferFrom with callback.

public safeTransferFrom(calldata: Calldata): BytesWriter

Calldata format:

Field Type Size
from Address 32 bytes
to Address 32 bytes
amount u256 32 bytes
data bytes variable (with length prefix)

The following diagram shows the safe transfer flow with recipient callback verification:

sequenceDiagram
    participant User as 👤 User
    participant Token as OP20 Contract
    participant Target as Recipient Contract
    participant Guard as ReentrancyGuard

    Note over User,Guard: Safe Transfer Flow

    User->>Token: safeTransfer(to, amount, data)
    Token->>Guard: Check reentrancy depth
    Guard->>Token: OK (depth allowed for callback)

    Token->>Token: _transfer(from, to, amount)
    Token->>Token: Update balances
    Token->>Token: Emit TransferredEvent

    Token->>Token: Check if recipient is contract
    alt Recipient is Contract
        Token->>Token: Build onOP20Received calldata
        Token->>Target: call(onOP20Received(operator, from, amount, data))

        alt Target implements onOP20Received
            Target->>Target: Process receipt
            Target->>Token: Return ON_OP20_RECEIVED_SELECTOR

            Token->>Token: Verify return value
            alt Return value matches selector
                Token->>User: Success
            else Invalid return value
                Token->>User: Revert: Transfer rejected
            end
        else Target doesn't implement
            Target->>Token: Error or wrong selector
            Token->>User: Revert: Transfer rejected
        end
    else Recipient is EOA
        Token->>User: Success (no callback needed)
    end
Loading

Events

TransferredEvent

Emitted on transfers (transfer, transferFrom, safeTransfer, safeTransferFrom).

class TransferredEvent extends NetEvent {
    constructor(
        operator: Address,  // Address that initiated the transfer
        from: Address,      // Address tokens transferred from
        to: Address,        // Address tokens transferred to
        amount: u256        // Amount transferred
    )
}

ApprovedEvent

Emitted on allowance changes (increaseAllowance, decreaseAllowance).

class ApprovedEvent extends NetEvent {
    constructor(
        owner: Address,
        spender: Address,
        amount: u256
    )
}

MintedEvent

Emitted on minting.

class MintedEvent extends NetEvent {
    constructor(
        to: Address,
        amount: u256
    )
}

BurnedEvent

Emitted on burning.

class BurnedEvent extends NetEvent {
    constructor(
        from: Address,
        amount: u256
    )
}

Solidity Comparison:

Solidity (ERC20) OP_NET (OP20)
event Transfer(address indexed from, address indexed to, uint256 value) TransferredEvent(operator, from, to, amount)
event Approval(address indexed owner, address indexed spender, uint256 value) ApprovedEvent(owner, spender, amount)
emit Transfer(from, to, amount) emitEvent(new TransferredEvent(operator, from, to, amount))

Storage Layout

OP20 uses 7 storage pointers:

Pointer Purpose
0 Nonce mapping (for signatures)
1 Max supply
2 Decimals
3 String pointer (shared for name/symbol/icon)
4 Total supply
5 Allowances mapping
6 Balances mapping

Method Selectors

Selector Method
name Returns name
symbol Returns symbol
icon Returns icon URL
decimals Returns decimals
totalSupply Returns total supply
maximumSupply Returns max supply
balanceOf Returns balance
allowance Returns allowance
nonceOf Returns nonce for address
domainSeparator Returns EIP-712 domain separator
metadata Returns all metadata
transfer Transfer tokens
transferFrom Transfer with allowance
safeTransfer Safe transfer with callback
safeTransferFrom Safe transferFrom with callback
increaseAllowance Increase spender allowance
decreaseAllowance Decrease spender allowance
increaseAllowanceBySignature Gasless allowance increase
decreaseAllowanceBySignature Gasless allowance decrease
burn Burn tokens from sender

Complete Example

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

@final
export class MyToken extends OP20 {
    public constructor() {
        super();
    }

    public override onDeployment(calldata: Calldata): void {
        const maxSupply = calldata.readU256();
        const decimals = calldata.readU8();
        const name = calldata.readString();
        const symbol = calldata.readString();
        const initialMint = calldata.readU256();

        this.instantiate(new OP20InitParameters(
            maxSupply,
            decimals,
            name,
            symbol,
            ''  // icon (optional)
        ));

        if (!initialMint.isZero()) {
            this._mint(Blockchain.tx.origin, initialMint);
        }
    }

    @method(
        { name: 'to', type: ABIDataTypes.ADDRESS },
        { name: 'amount', type: ABIDataTypes.UINT256 },
    )
    @returns({ name: 'success', type: ABIDataTypes.BOOL })
    @emit('Minted')
    public mint(calldata: Calldata): BytesWriter {
        this.onlyDeployer(Blockchain.tx.sender);

        const to = calldata.readAddress();
        const amount = calldata.readU256();

        this._mint(to, amount);
        // Note: _mint already emits MintedEvent internally

        return new BytesWriter(0);
    }
}

Solidity Comparison Summary

Solidity (ERC20) OP_NET (OP20)
constructor(...) onDeployment(calldata)
function name() name(): string
function balanceOf(address) balanceOf(Address): u256
_mint(address, uint256) _mint(Address, u256)
emit Transfer(...) emitEvent(new TransferredEvent(...))

Navigation: