@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
576 lines (451 loc) • 14.4 kB
Markdown
# Your First Contract
This tutorial guides you through creating a complete OP20 token contract from scratch. By the end, you'll understand the core concepts of OP_NET smart contract development.
## What We're Building
A simple fungible token (like an ERC20 on Ethereum) with:
- Fixed maximum supply
- Minting capability (deployer only)
- Transfer functionality
- Balance queries
## Step 1: Create the Contract File
Create `src/token/MyToken.ts`:
```typescript
import { u256 } from '@btc-vision/as-bignum/assembly';
import {
Blockchain,
BytesWriter,
Calldata,
OP20,
OP20InitParameters,
} from '-vision/btc-runtime/runtime';
export class MyToken extends OP20 {
public constructor() {
super();
}
public override onDeployment(_calldata: Calldata): void {
const maxSupply: u256 = u256.fromString('1000000000000000000000000');
const decimals: u8 = 18;
const name: string = 'MyToken';
const symbol: string = 'MTK';
this.instantiate(new OP20InitParameters(maxSupply, decimals, name, symbol));
this._mint(Blockchain.tx.origin, maxSupply);
}
(
{ name: 'address', type: ABIDataTypes.ADDRESS },
{ name: 'amount', type: ABIDataTypes.UINT256 }
)
('Minted')
public mint(calldata: Calldata): BytesWriter {
this.onlyDeployer(Blockchain.tx.sender);
this._mint(calldata.readAddress(), calldata.readU256());
return new BytesWriter(0);
}
}
```
Let's break this down piece by piece.
## Contract Lifecycle Overview
This diagram illustrates the complete lifecycle of an OP_NET smart contract from deployment to execution:
```mermaid
---
config:
theme: dark
---
flowchart LR
A["Deployment"] --> B["onDeployment()"]
B --> C["Initialize Parameters"]
C --> D["Contract Active"]
D --> E{"Transaction"}
E --> F["execute()"]
F -->|"transfer()"| G["Transfer Tokens"]
F -->|"mint()"| H["Mint Tokens"]
F -->|"balanceOf()"| I["Query Balance"]
F -->|"approve()"| J["Approve Spender"]
G --> K["Complete"]
H --> K
I --> K
J --> K
K --> E
```
## Step 2: Understanding the Code
### Token Contract Architecture
This diagram shows how your MyToken contract inherits functionality from the OP20 base class:
```mermaid
---
config:
theme: dark
---
classDiagram
class OP_NET {
+Address address
+Address contractDeployer
+onDeployment(calldata)
+execute(selector, calldata)
+onlyDeployer(caller)
+emitEvent(event)
}
class ReentrancyGuard {
-u8 reentrancyStatus
+nonReentrant()
+protected()
}
class OP20 {
+StoredU256 _totalSupply
+StoredString _name
+StoredString _symbol
+u8 _decimals
+u256 _maxSupply
+transfer(to, amount)
+approve(spender, amount)
+balanceOf(address)
+_mint(to, amount)
+_burn(from, amount)
}
class MyToken {
+onDeployment(calldata)
+mint(calldata)
}
OP_NET <|-- ReentrancyGuard
ReentrancyGuard <|-- OP20
OP20 <|-- MyToken
note for MyToken "Custom implementation:\n- Deployment logic\n- Additional mint function"
note for OP20 "Built-in methods:\n- transfer\n- approve\n- balanceOf\n- totalSupply"
note for OP_NET "Base contract:\n- Access control\n- Event system\n- Execution router"
```
### The Class Declaration
```typescript
export class MyToken extends OP20 {
```
| Component | Meaning |
|-----------|---------|
| `` | AssemblyScript decorator - prevents inheritance |
| `export` | Makes the class accessible outside the file |
| `extends OP20` | Inherits from the fungible token standard |
**Solidity equivalent:**
```solidity
contract MyToken is ERC20 {
```
### The Constructor
```typescript
public constructor() {
super();
}
```
**IMPORTANT:** In OP_NET, the constructor runs on **every** contract interaction, not just deployment. This is different from Solidity!
```typescript
// OP_NET // Solidity
public constructor() { // constructor() {
super(); // // Runs ONCE at deployment
// Runs EVERY time! // }
}
```
Never put initialization logic in the constructor. Use `onDeployment` instead.
### The Deployment Hook
```typescript
public override onDeployment(_calldata: Calldata): void {
const maxSupply: u256 = u256.fromString('1000000000000000000000000');
const decimals: u8 = 18;
const name: string = 'MyToken';
const symbol: string = 'MTK';
this.instantiate(new OP20InitParameters(maxSupply, decimals, name, symbol));
this._mint(Blockchain.tx.origin, maxSupply);
}
```
This method runs **once** when the contract is first deployed. It's the equivalent of Solidity's `constructor()`.
| Parameter | Value | Meaning |
|-----------|-------|---------|
| `maxSupply` | 1,000,000 (with 18 decimals) | Maximum tokens that can ever exist |
| `decimals` | 18 | Decimal places (like ETH/wei) |
| `name` | "MyToken" | Human-readable name |
| `symbol` | "MTK" | Ticker symbol |
**Solidity equivalent:**
```solidity
constructor() ERC20("MyToken", "MTK") {
_mint(msg.sender, 1000000 * 10**18);
}
```
### The Mint Function
```typescript
(
{ name: 'address', type: ABIDataTypes.ADDRESS },
{ name: 'amount', type: ABIDataTypes.UINT256 }
)
('Minted')
public mint(calldata: Calldata): BytesWriter {
this.onlyDeployer(Blockchain.tx.sender);
this._mint(calldata.readAddress(), calldata.readU256());
return new BytesWriter(0);
}
```
### Mint Operation Data Flow
This sequence diagram shows what happens when the mint function is called:
```mermaid
---
config:
theme: dark
---
sequenceDiagram
participant User as 👤 User
participant Blockchain as Blockchain
participant MyToken as MyToken
participant OP20 as OP20
participant Storage as Storage
User->>Blockchain: Call mint(to, amount)
Blockchain->>MyToken: constructor()
Note over MyToken: Runs on EVERY call
Blockchain->>MyToken: execute(selector, calldata)
MyToken->>MyToken: mint(calldata)
MyToken->>Blockchain: Check tx.sender
Blockchain-->>MyToken: sender address
MyToken->>MyToken: onlyDeployer(sender)
alt sender != deployer
MyToken-->>User: Revert("Only deployer")
end
MyToken->>MyToken: readAddress() from calldata
MyToken->>MyToken: readU256() from calldata
MyToken->>OP20: _mint(to, amount)
OP20->>Storage: Read current balance[to]
Storage-->>OP20: currentBalance
OP20->>OP20: newBalance = currentBalance + amount
OP20->>Storage: Write balance[to] = newBalance
OP20->>Storage: Read totalSupply
Storage-->>OP20: currentSupply
OP20->>OP20: newSupply = currentSupply + amount
OP20->>Storage: Write totalSupply = newSupply
OP20->>Blockchain: emit(MintEvent)
OP20-->>MyToken: Success
MyToken->>MyToken: new BytesWriter(0)
MyToken-->>Blockchain: Empty response
Blockchain-->>User: Transaction success
Note over Storage: All changes persisted<br/>to blockchain state
```
Breaking this down:
| Line | Purpose |
|------|---------|
| `(...)` | Declares method parameters for ABI generation |
| `('Minted')` | Declares event emission for ABI documentation |
| `onlyDeployer(...)` | Access control - only the deployer can call |
| `calldata.readAddress()` | Parse the recipient address from input |
| `calldata.readU256()` | Parse the amount from input |
| `_mint(to, amount)` | Internal mint function from OP20 |
| `return new BytesWriter(0)` | Return empty response |
**Solidity equivalent:**
```solidity
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
```
## Step 3: Understanding Types
### u256 - Big Numbers
OP_NET uses `u256` for large numbers (like balances):
```typescript
import { u256 } from '@btc-vision/as-bignum/assembly';
// Creating u256 values
const a = u256.fromU64(100); // From small number
const b = u256.fromU64(1_000_000); // From u64
const c = u256.fromString('99999999999999'); // From string (large numbers)
// NEVER use floating point!
// const bad = u256.fromU64(1.5); // WRONG! - No floating point!
```
**Why not native numbers?**
| JavaScript/TypeScript | AssemblyScript/OP_NET |
|----------------------|----------------------|
| `number` (64-bit float) | Non-deterministic! |
| `BigInt` | Not supported in WASM |
| N/A | `u256` (deterministic) |
### Address
Addresses are 32 bytes in OP_NET:
```typescript
import { Address, Blockchain } from '@btc-vision/btc-runtime/runtime';
// Get the current sender
const sender: Address = Blockchain.tx.sender;
// Zero address (like address(0) in Solidity)
const zero = Address.zero();
// Compare addresses
if (sender.equals(zero)) {
throw new Revert('Cannot be zero address');
}
```
### Calldata
Input parsing uses `Calldata`:
```typescript
public myMethod(calldata: Calldata): BytesWriter {
// Read parameters in order
const address = calldata.readAddress(); // 32 bytes
const amount = calldata.readU256(); // 32 bytes
const flag = calldata.readBoolean(); // 1 byte
const data = calldata.readBytes(); // Variable length
// ...
}
```
## Step 4: Inherited OP20 Methods
By extending `OP20`, your token automatically gets these methods:
| Method | Description | Selector |
|--------|-------------|----------|
| `transfer(to, amount)` | Transfer tokens | Built-in |
| `transferFrom(from, to, amount)` | Transfer with approval | Built-in |
| `approve(spender, amount)` | Approve spender | Built-in |
| `balanceOf(address)` | Get balance | Built-in |
| `allowance(owner, spender)` | Get allowance | Built-in |
| `totalSupply()` | Total supply | Built-in |
| `name()` | Token name | Built-in |
| `symbol()` | Token symbol | Built-in |
| `decimals()` | Decimal places | Built-in |
## Step 5: Building the Contract
Add to your `package.json`:
```json
{
"scripts": {
"build:token": "asc src/token/index.ts --target token --measure --uncheckedBehavior never"
}
}
```
Create `src/token/index.ts`:
```typescript
import { Blockchain } from '@btc-vision/btc-runtime/runtime';
import { revertOnError } from '@btc-vision/btc-runtime/runtime/abort/abort';
import { MyToken } from './MyToken';
// DO NOT TOUCH TO THIS.
Blockchain.contract = () => {
// ONLY CHANGE THE CONTRACT CLASS NAME.
// DO NOT ADD CUSTOM LOGIC HERE.
return new MyToken();
};
// VERY IMPORTANT
export * from '-vision/btc-runtime/runtime/exports';
// VERY IMPORTANT
export function abort(message: string, fileName: string, line: u32, column: u32): void {
revertOnError(message, fileName, line, column);
}
```
Build:
```bash
npm run build:token
```
## Solidity Comparison
Here's a side-by-side comparison of the complete contract:
<table>
<tr>
<th>OP_NET (AssemblyScript)</th>
<th>Solidity</th>
</tr>
<tr>
<td>
```typescript
import { u256 } from '@btc-vision/as-bignum/assembly';
import {
Blockchain,
BytesWriter,
Calldata,
OP20,
OP20InitParameters,
} from '-vision/btc-runtime/runtime';
export class MyToken extends OP20 {
public constructor() {
super();
}
public override onDeployment(_: Calldata): void {
const maxSupply = u256.fromString('1000000000000000000000000');
this.instantiate(new OP20InitParameters(
maxSupply,
18,
'MyToken',
'MTK'
));
this._mint(Blockchain.tx.origin, maxSupply);
}
(
{ name: 'address', type: ABIDataTypes.ADDRESS },
{ name: 'amount', type: ABIDataTypes.UINT256 }
)
('Minted')
public mint(calldata: Calldata): BytesWriter {
this.onlyDeployer(Blockchain.tx.sender);
this._mint(
calldata.readAddress(),
calldata.readU256()
);
return new BytesWriter(0);
}
}
```
</td>
<td>
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
uint256 public constant MAX_SUPPLY =
1000000 * 10**18;
constructor()
ERC20("MyToken", "MTK")
Ownable(msg.sender)
{
_mint(msg.sender, MAX_SUPPLY);
}
function mint(
address to,
uint256 amount
) external onlyOwner {
_mint(to, amount);
}
}
```
</td>
</tr>
</table>
## Common Patterns
### Access Control
```typescript
// Only deployer can call
({ name: 'param', type: ABIDataTypes.UINT256 })
({ name: 'result', type: ABIDataTypes.UINT256 })
public adminFunction(calldata: Calldata): BytesWriter {
this.onlyDeployer(Blockchain.tx.sender);
const param = calldata.readU256();
// ... perform admin logic
const result = new BytesWriter(32);
result.writeU256(param);
return result;
}
```
### Error Handling
```typescript
import { Revert, Address, BytesWriter, Calldata } from '@btc-vision/btc-runtime/runtime';
(
{ name: 'to', type: ABIDataTypes.ADDRESS },
{ name: 'amount', type: ABIDataTypes.UINT256 }
)
('Transferred')
public transfer(calldata: Calldata): BytesWriter {
const to = calldata.readAddress();
const amount = calldata.readU256();
if (to.equals(Address.zero())) {
throw new Revert('Cannot transfer to zero address');
}
// ...
return new BytesWriter(0);
}
```
### Reading Storage
```typescript
import { SafeMath } from '@btc-vision/btc-runtime/runtime';
// In OP20, balances are managed automatically
const balance: u256 = this.balanceOf(address);
// When performing u256 operations, always use SafeMath
const newBalance = SafeMath.add(balance, amount);
const result = SafeMath.sub(balance, amount);
```
## Next Steps
Now that you've created your first contract:
1. [Understand the project structure](./project-structure.md)
2. [Learn about the blockchain environment](../core-concepts/blockchain-environment.md)
3. [Explore storage in depth](../core-concepts/storage-system.md)
4. [See more examples](../examples/basic-token.md)
---
**Navigation:**
- Previous: [Installation](./installation.md)
- Next: [Project Structure](./project-structure.md)