@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
903 lines (701 loc) • 24.1 kB
Markdown
# OP20 API Reference
The `OP20` class implements the fungible token standard, equivalent to ERC20 on Ethereum.
## Import
```typescript
import { OP20, OP20InitParameters } from '@btc-vision/btc-runtime/runtime';
```
## OP20 Architecture
```mermaid
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"
```
## Class Definition
```typescript
@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
```typescript
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`.
```typescript
public instantiate(params: OP20InitParameters, skipDeployerVerification: boolean = false): void
```
```typescript
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.
```typescript
@method()
@returns({ name: 'name', type: ABIDataTypes.STRING })
public name(calldata: Calldata): BytesWriter
```
### symbol
Returns the token symbol.
```typescript
@method()
@returns({ name: 'symbol', type: ABIDataTypes.STRING })
public symbol(calldata: Calldata): BytesWriter
```
### icon
Returns the token icon URL.
```typescript
@method()
@returns({ name: 'icon', type: ABIDataTypes.STRING })
public icon(calldata: Calldata): BytesWriter
```
### decimals
Returns the number of decimals.
```typescript
@method()
@returns({ name: 'decimals', type: ABIDataTypes.UINT8 })
public decimals(calldata: Calldata): BytesWriter
```
### nonceOf
Returns the current nonce for an address (used for signature verification).
```typescript
@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.
```typescript
@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).
```typescript
@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.
```typescript
@method()
@returns({ name: 'totalSupply', type: ABIDataTypes.UINT256 })
public totalSupply(calldata: Calldata): BytesWriter
```
### maximumSupply
Returns maximum possible supply.
```typescript
@method()
@returns({ name: 'maximumSupply', type: ABIDataTypes.UINT256 })
public maximumSupply(calldata: Calldata): BytesWriter
```
### balanceOf
Returns balance of an address.
```typescript
@method({ name: 'owner', type: ABIDataTypes.ADDRESS })
@returns({ name: 'balance', type: ABIDataTypes.UINT256 })
public balanceOf(calldata: Calldata): BytesWriter
```
Internal helper (for use within your contract):
```typescript
protected _balanceOf(owner: Address): u256
```
### allowance
Returns spending allowance.
```typescript
@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):
```typescript
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.
```typescript
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:
```mermaid
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
```
### transferFrom (Calldata)
Transfers tokens using allowance.
```typescript
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:
```mermaid
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
```
**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.
```typescript
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.
```typescript
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).
```typescript
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.
```typescript
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:
```mermaid
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
```
The complete allowance lifecycle from approval to spending:
```mermaid
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
```
**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.
```typescript
protected _mint(to: Address, amount: u256): void
```
```typescript
@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.
```typescript
protected _burn(from: Address, amount: u256): void
```
```typescript
@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.
```typescript
protected _transfer(from: Address, to: Address, amount: u256): void
```
### _increaseAllowance
Internal method to increase allowance with overflow protection.
```typescript
protected _increaseAllowance(owner: Address, spender: Address, amount: u256): void
```
### _decreaseAllowance
Internal method to decrease allowance with underflow protection.
```typescript
protected _decreaseAllowance(owner: Address, spender: Address, amount: u256): void
```
### _spendAllowance
Decrements allowance.
```typescript
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.
```typescript
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.
```typescript
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:
```mermaid
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
```
## Events
### TransferredEvent
Emitted on transfers (transfer, transferFrom, safeTransfer, safeTransferFrom).
```typescript
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).
```typescript
class ApprovedEvent extends NetEvent {
constructor(
owner: Address,
spender: Address,
amount: u256
)
}
```
### MintedEvent
Emitted on minting.
```typescript
class MintedEvent extends NetEvent {
constructor(
to: Address,
amount: u256
)
}
```
### BurnedEvent
Emitted on burning.
```typescript
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
```typescript
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:**
- Previous: [Blockchain API](./blockchain.md)
- Next: [OP721 API](./op721.md)