@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
688 lines (546 loc) • 19.8 kB
Markdown
# OP20 Token Standard
OP20 is OP_NET's fungible token standard, equivalent to Ethereum's ERC20. It provides a complete implementation for creating tokens with transfer, approval, and balance tracking capabilities.
## Overview
```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 {
this.instantiate(new OP20InitParameters(
u256.fromString('1000000000000000000000000'), // maxSupply: 1M tokens
18, // decimals
'MyToken', // name
'MTK', // symbol
'https://example.com/icon.png' // icon (optional)
));
// Mint initial supply to deployer
this._mint(Blockchain.tx.origin, this._maxSupply.value);
}
}
```
## ERC20 vs OP20 Comparison
| Feature | ERC20 (Solidity) | OP20 (OP_NET) |
|---------|------------------|--------------|
| Language | Solidity | AssemblyScript |
| Runtime | EVM | WASM |
| Integer Type | `uint256` | `u256` |
| Max Decimals | 18 (convention) | 32 (hard limit) |
| Max Supply | Unlimited | Enforced at instantiation |
| Approval Pattern | `approve()` + `transferFrom()` | `increaseAllowance()`/`decreaseAllowance()` + `transferFrom()` |
| Unlimited Approval | Decremented on transfer | Optimized - not decremented |
| Events | Solidity events | `emitEvent()` system |
| Inheritance | Multiple inheritance | Single inheritance |
## Initialization
### OP20InitParameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `maxSupply` | `u256` | Maximum token supply (cannot be exceeded) |
| `decimals` | `u8` | Decimal places (max 32) |
| `name` | `string` | Token name |
| `symbol` | `string` | Token symbol |
| `icon` | `string` | Token icon URL (optional, defaults to empty string) |
```typescript
const params = new OP20InitParameters(
u256.fromString('1000000000000000000000000000'), // 1 billion with 18 decimals
18,
'My Token',
'MTK',
'https://example.com/icon.png' // optional
);
this.instantiate(params);
```
### Decimal Limit
**IMPORTANT:** Decimals cannot exceed 32.
```typescript
// Valid
const params = new OP20InitParameters(maxSupply, 18, name, symbol);
// Invalid - will throw
const params = new OP20InitParameters(maxSupply, 33, name, symbol);
```
## Transfer Flow
The following diagram illustrates how a token transfer is processed:
```mermaid
config:
theme: dark
flowchart LR
A[👤 User signs TX] --> B[Submit to blockchain]
B --> C[Contract.transfer called]
C --> D{Valid recipient?}
D -->|No| E[Revert]
D -->|Yes| F{Sufficient balance?}
F -->|No| G[Revert]
F -->|Yes| H[Subtract from sender]
H --> I[Add to recipient]
I --> J[Emit TransferredEvent]
J --> K[Return success]
```
### Detailed Transfer Sequence
```mermaid
sequenceDiagram
participant User as 👤 User Wallet
participant Blockchain as Bitcoin L1
participant VM as WASM Runtime
participant OP20 as OP20 Contract
participant Calldata as Calldata Reader
participant Storage as Storage Pointers
participant BalanceMap as balanceOfMap<br/>(Pointer 5)
participant TotalSupply as _totalSupply<br/>(Pointer 4)
participant EventLog as Event Log System
User->>Blockchain: Submit transfer(to, amount) TX
Blockchain->>VM: Execute transaction
VM->>OP20: Call transfer method
activate OP20
OP20->>Calldata: readAddress()
Calldata-->>OP20: to address
OP20->>Calldata: readU256()
Calldata-->>OP20: amount
OP20->>OP20: Get sender = Blockchain.tx.sender
Note over OP20: sender is msg.sender equivalent
OP20->>OP20: Validate to != Address.zero()
alt to is zero address
OP20->>VM: Revert('Cannot transfer to zero address')
VM->>User: Transaction failed
else Valid recipient
OP20->>OP20: _transfer(sender, to, amount)
OP20->>BalanceMap: get(sender)
BalanceMap->>Storage: Read from storage slot
Storage-->>BalanceMap: Raw balance data
BalanceMap-->>OP20: senderBalance: u256
alt Insufficient balance
OP20->>VM: Revert('Insufficient balance')
VM->>User: Transaction failed
else Sufficient balance
OP20->>OP20: SafeMath.sub(senderBalance, amount)
Note over OP20: Underflow protection
OP20->>BalanceMap: set(sender, newSenderBalance)
BalanceMap->>Storage: Write to storage slot
Note over Storage: Persistent state change
OP20->>BalanceMap: get(to)
BalanceMap->>Storage: Read recipient balance
Storage-->>BalanceMap: Recipient balance
BalanceMap-->>OP20: recipientBalance: u256
OP20->>OP20: SafeMath.add(recipientBalance, amount)
Note over OP20: Overflow protection
OP20->>BalanceMap: set(to, newRecipientBalance)
BalanceMap->>Storage: Write updated balance
Note over Storage: Both balances now updated
OP20->>OP20: Create TransferredEvent(operator, from, to, amount)
OP20->>EventLog: emitEvent(transferEvent)
Note over EventLog: Indexed for off-chain queries
OP20->>VM: Return BytesWriter(0)
deactivate OP20
VM->>Blockchain: Commit state changes
Blockchain->>User: Transaction success + receipt
Note over User: Balance updated,<br/>event emitted
end
end
```
## Token Lifecycle
```mermaid
stateDiagram-v2
[*] --> Undeployed
Undeployed --> Deployed: onDeployment(params)
state Deployed {
[*] --> ZeroSupply
ZeroSupply --> HasSupply: _mint()
HasSupply --> HasSupply: transfer()
HasSupply --> HasSupply: increaseAllowance()
HasSupply --> HasSupply: transferFrom()
HasSupply --> LowerSupply: _burn()
LowerSupply --> HasSupply: _mint()
LowerSupply --> ZeroSupply: _burn() all
state "Total Supply Management" as Supply {
[*] --> BelowMax
BelowMax --> BelowMax: _mint() within limit
BelowMax --> AtMax: _mint() to maxSupply
AtMax --> BelowMax: _burn()
BelowMax --> [*]: totalSupply = 0
}
}
```
## Built-in Methods
OP20 provides these methods automatically:
### Query Methods
| Method | Returns | Description |
|--------|---------|-------------|
| `name()` | `string` | Token name |
| `symbol()` | `string` | Token symbol |
| `icon()` | `string` | Token icon URL |
| `decimals()` | `u8` | Decimal places |
| `totalSupply()` | `u256` | Current total supply |
| `maximumSupply()` | `u256` | Maximum possible supply |
| `balanceOf(owner)` | `u256` | Balance of address |
| `allowance(owner, spender)` | `u256` | Approved amount |
| `nonceOf(owner)` | `u256` | Nonce for signature verification |
| `domainSeparator()` | `bytes32` | EIP-712 domain separator |
| `metadata()` | `multiple` | All token metadata in one call |
### Transfer Methods
| Method | Description |
|--------|-------------|
| `transfer(to, amount)` | Transfer tokens from sender |
| `transferFrom(from, to, amount)` | Transfer using approval |
| `safeTransfer(to, amount, data)` | Transfer with recipient callback |
| `safeTransferFrom(from, to, amount, data)` | TransferFrom with recipient callback |
### Approval Methods
| Method | Description |
|--------|-------------|
| `increaseAllowance(spender, amount)` | Increase approval |
| `decreaseAllowance(spender, amount)` | Decrease approval |
| `increaseAllowanceBySignature(...)` | Gasless approval increase via signature |
| `decreaseAllowanceBySignature(...)` | Gasless approval decrease via signature |
### Other Methods
| Method | Description |
|--------|-------------|
| `burn(amount)` | Burn tokens from sender's balance |
## Approval Flow
The following diagram shows how the approval and transferFrom pattern works:
```mermaid
config:
theme: dark
flowchart LR
A[👤 User increases allowance] --> B[Set allowance in storage]
B --> C[Emit ApprovedEvent]
C --> D[Spender calls transferFrom]
D --> E{Sufficient allowance?}
E -->|No| F[Revert]
E -->|Yes| G{Unlimited approval?}
G -->|Yes| H[Skip allowance update]
G -->|No| I[Decrease allowance]
H --> J[Execute transfer]
I --> J
J --> K[Update balances]
K --> L[Emit TransferredEvent]
```
## Solidity Comparison
<table>
<tr>
<th>ERC20 (Solidity)</th>
<th>OP20 (OP_NET)</th>
</tr>
<tr>
<td>
```solidity
contract MyToken is ERC20 {
constructor()
ERC20("MyToken", "MTK")
{
_mint(msg.sender, 1000000 * 10**18);
}
}
```
</td>
<td>
```typescript
@final
export class MyToken extends OP20 {
constructor() {
super();
}
public override onDeployment(_: Calldata): void {
this.instantiate(new OP20InitParameters(
u256.fromString('1000000000000000000000000'),
18, 'MyToken', 'MTK', ''
));
this._mint(Blockchain.tx.origin, this._maxSupply.value);
}
}
```
</td>
</tr>
</table>
## Storage Layout
OP20 uses the following storage pointers internally:
| Pointer | Storage | Description |
|---------|---------|-------------|
| 0 | `nonceMap` | Address -> nonce mapping (for signatures) |
| 1 | `maxSupply` | Maximum token supply |
| 2 | `decimals` | Decimal places |
| 3 | `stringPointer` | Shared pointer for name (sub 0), symbol (sub 1), icon (sub 2) |
| 4 | `totalSupply` | Current total supply |
| 5 | `allowanceMap` | Owner -> spender -> amount mapping |
| 6 | `balanceOfMap` | Address -> balance mapping |
**Note:** Your contract's pointers start after OP20's internal pointers (pointer 7+).
## Extending OP20
### Adding Custom Methods
```typescript
@final
export class MyToken extends OP20 {
public constructor() {
super();
}
public override onDeployment(calldata: Calldata): void {
this.instantiate(new OP20InitParameters(
u256.fromString('1000000000000000000000000'),
18, 'MyToken', 'MTK'
));
}
// Custom mint function (OP20 does not have a built-in public mint)
@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);
}
// Note: OP20 already has a public burn(amount) method built-in
// You can override it if you need custom behavior:
// public override burn(calldata: Calldata): BytesWriter { ... }
}
```
### Internal Methods
OP20 provides protected methods for extending functionality:
| Method | Description |
|--------|-------------|
| `_mint(to, amount)` | Mint new tokens |
| `_burn(from, amount)` | Burn tokens |
| `_transfer(from, to, amount)` | Internal transfer |
| `_balanceOf(owner)` | Get balance of address |
| `_allowance(owner, spender)` | Get allowance amount |
| `_increaseAllowance(owner, spender, amount)` | Increase allowance with overflow protection |
| `_decreaseAllowance(owner, spender, amount)` | Decrease allowance with underflow protection |
| `_spendAllowance(owner, spender, amount)` | Spend from allowance (for transferFrom) |
| `_safeTransfer(from, to, amount, data)` | Transfer with receiver callback |
```typescript
// Minting tokens
this._mint(recipient, amount);
// Burning tokens
this._burn(holder, amount);
// Internal transfer (no sender checks)
this._transfer(from, to, amount);
```
## Events
OP20 emits these events automatically:
### TransferredEvent
```typescript
// Emitted on transfer(), transferFrom(), safeTransfer(), safeTransferFrom()
TransferredEvent(operator: Address, from: Address, to: Address, amount: u256)
// operator: the address that initiated the transfer (Blockchain.tx.sender)
// from: the address tokens are transferred from
// to: the address tokens are transferred to
// amount: the number of tokens transferred
```
### ApprovedEvent
```typescript
// Emitted on increaseAllowance(), decreaseAllowance()
ApprovedEvent(owner: Address, spender: Address, amount: u256)
```
### MintedEvent
```typescript
// Emitted when new tokens are minted via _mint()
MintedEvent(to: Address, amount: u256)
```
### BurnedEvent
```typescript
// Emitted when tokens are burned via burn() or _burn()
BurnedEvent(from: Address, amount: u256)
```
## Approval Patterns
The following state diagram shows how an allowance transitions between different states:
```mermaid
config:
theme: dark
stateDiagram-v2
[*] --> NoAllowance
NoAllowance --> LimitedAllowance: increaseAllowance(amount)
NoAllowance --> UnlimitedAllowance: increaseAllowance(u256.Max)
LimitedAllowance --> LimitedAllowance: increaseAllowance(delta)
LimitedAllowance --> LimitedAllowance: decreaseAllowance(delta)
LimitedAllowance --> LimitedAllowance: transferFrom (decrements)
LimitedAllowance --> NoAllowance: transferFrom (exhausted)
LimitedAllowance --> NoAllowance: decreaseAllowance(all)
LimitedAllowance --> UnlimitedAllowance: increaseAllowance (overflow)
UnlimitedAllowance --> UnlimitedAllowance: transferFrom (no change)
UnlimitedAllowance --> LimitedAllowance: decreaseAllowance(amount)
UnlimitedAllowance --> NoAllowance: decreaseAllowance(all)
```
### Standard Approval
```typescript
// User increases allowance for spender
increaseAllowance(spender, 1000);
// Spender can transfer up to 1000 tokens
transferFrom(user, recipient, 500); // Allowance now 500
transferFrom(user, recipient, 500); // Allowance now 0
```
### Unlimited Approval
```typescript
// Increase allowance to maximum - overflows to u256.Max
increaseAllowance(spender, u256.Max);
// Transfers don't reduce unlimited allowance
transferFrom(user, recipient, 1000); // Allowance still u256.Max
```
**Note:** OP20 optimizes unlimited approvals (u256.Max) - they're not decremented on transfer.
### Increase/Decrease Pattern
```typescript
// Safe pattern using increase/decrease (prevents front-running)
increaseAllowance(spender, 100); // Add 100 to current allowance
decreaseAllowance(spender, 50); // Remove 50 from current allowance
// Note: If decrease amount > current allowance, it sets to zero (no underflow)
// If increase would overflow, it sets to u256.Max (unlimited)
```
## Edge Cases
The following state diagram shows how token balances transition for an individual address:
```mermaid
config:
theme: dark
stateDiagram-v2
[*] --> ZeroBalance
ZeroBalance --> HasBalance: receive tokens
HasBalance --> HasBalance: transfer (partial)
HasBalance --> HasBalance: receive more
HasBalance --> ZeroBalance: transfer (all)
HasBalance --> ZeroBalance: burn (all)
note right of HasBalance
Balance can increase via:
- _mint()
- transfer()
- transferFrom()
end note
```
### Zero Address
```typescript
// Transfer to zero address reverts
transfer(Address.zero(), amount); // Throws: "Cannot transfer to zero address"
// Minting to zero address reverts
_mint(Address.zero(), amount); // Throws
// Burning from zero address reverts
_burn(Address.zero(), amount); // Throws
```
### Overflow Protection
```typescript
// Minting beyond maxSupply reverts
_mint(to, amount); // Throws if totalSupply + amount > maxSupply
// All arithmetic uses SafeMath
// Overflow/underflow automatically reverts
```
### Self-Approval
```typescript
// Approving yourself is valid but pointless
increaseAllowance(Blockchain.tx.sender, amount); // Works, but why?
```
## Complete Token Example
```typescript
import { u256 } from '@btc-vision/as-bignum/assembly';
import {
OP20,
OP20InitParameters,
Blockchain,
Address,
Calldata,
BytesWriter,
Selector,
SafeMath,
Revert,
StoredBoolean,
AddressMemoryMap,
ABIDataTypes,
} from '@btc-vision/btc-runtime/runtime';
@final
export class AdvancedToken extends OP20 {
// Additional storage
private pausedPointer: u16 = Blockchain.nextPointer;
private blacklistPointer: u16 = Blockchain.nextPointer;
private _paused: StoredBoolean = new StoredBoolean(this.pausedPointer, false);
private _blacklist: AddressMemoryMap;
public constructor() {
super();
this._blacklist = new AddressMemoryMap(this.blacklistPointer);
}
public override onDeployment(calldata: Calldata): void {
const maxSupply = calldata.readU256();
const decimals = calldata.readU8();
const name = calldata.readString();
const symbol = calldata.readString();
this.instantiate(new OP20InitParameters(maxSupply, decimals, name, symbol));
}
// Override transfer to add checks
public override transfer(calldata: Calldata): BytesWriter {
this.whenNotPaused();
this.checkBlacklist(Blockchain.tx.sender);
return super.transfer(calldata);
}
// Admin: Mint tokens
@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);
this._mint(calldata.readAddress(), calldata.readU256());
// Note: _mint() already emits MintedEvent internally
return new BytesWriter(0);
}
// Admin: Pause/unpause
@method()
@returns({ name: 'success', type: ABIDataTypes.BOOL })
public pause(_calldata: Calldata): BytesWriter {
this.onlyDeployer(Blockchain.tx.sender);
this._paused.value = true;
return new BytesWriter(0);
}
@method()
@returns({ name: 'success', type: ABIDataTypes.BOOL })
public unpause(_calldata: Calldata): BytesWriter {
this.onlyDeployer(Blockchain.tx.sender);
this._paused.value = false;
return new BytesWriter(0);
}
// Admin: Blacklist management
@method({ name: 'address', type: ABIDataTypes.ADDRESS })
@returns({ name: 'success', type: ABIDataTypes.BOOL })
public blacklist(calldata: Calldata): BytesWriter {
this.onlyDeployer(Blockchain.tx.sender);
this._blacklist.set(calldata.readAddress(), true);
return new BytesWriter(0);
}
// Internal helpers
private whenNotPaused(): void {
if (this._paused.value) {
throw new Revert('Token is paused');
}
}
private checkBlacklist(address: Address): void {
if (this._blacklist.get(address)) {
throw new Revert('Address is blacklisted');
}
}
}
```
## Best Practices
1. **Always call `instantiate()` in `onDeployment`**
2. **Use SafeMath for any custom arithmetic**
3. **Emit events for custom state changes**
4. **Validate all inputs before processing**
5. **Use `_mint`/`_burn` for supply changes**
6. **Override `transfer` carefully (call `super`)**
**Navigation:**
- Previous: [OP_NET Base](./op-net-base.md)
- Next: [OP20S Signatures](./op20s-signatures.md)