UNPKG

@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
# 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)