synor/docs/tutorials/03-smart-contracts.md
Gulshan Yadav a6233f285d docs: add developer tutorial series
Create comprehensive step-by-step tutorials:
- Tutorial 1: Getting Started - node setup, wallet creation, transactions
- Tutorial 2: Building a Wallet - React app with state management, encryption
- Tutorial 3: Smart Contracts - Rust WASM contracts, token example, testing
- Tutorial 4: API Guide - JSON-RPC, WebSocket subscriptions, client building

Each tutorial includes working code examples and best practices.
2026-01-10 06:24:51 +05:30

517 lines
11 KiB
Markdown

# Tutorial 3: Smart Contracts on Synor
Learn to build, deploy, and interact with smart contracts on Synor's WebAssembly-based contract system.
## What You'll Learn
- Write a contract in Rust
- Compile to WebAssembly
- Deploy to Synor
- Call contract methods
- Handle state and events
## Prerequisites
- Completed [Tutorial 1: Getting Started](./01-getting-started.md)
- Rust installed (`rustup`)
- `wasm32-unknown-unknown` target
---
## Part 1: Understanding Synor Contracts
### How Contracts Work
```
┌─────────────────────────────────────────────┐
│ Synor Smart Contract │
├─────────────────────────────────────────────┤
│ Written in: Rust (compiled to WASM) │
│ Execution: Deterministic sandbox │
│ State: Key-value storage │
│ Gas: Metered execution │
│ Size limit: 256 KB compiled WASM │
└─────────────────────────────────────────────┘
```
### Contract Capabilities
| Feature | Support |
|---------|---------|
| State storage | ✅ Key-value store |
| Cross-contract calls | ✅ Via host functions |
| Events/Logs | ✅ Emit events |
| Token transfers | ✅ Native SYNOR |
| Cryptography | ✅ Blake3, Ed25519 |
| Time access | ✅ Block timestamp |
| Randomness | ❌ Not yet (determinism) |
---
## Part 2: Setting Up
### Install the Contract SDK
```bash
# Add the WASM target
rustup target add wasm32-unknown-unknown
# Install the Synor contract CLI
cargo install synor-contract-cli
```
### Create a New Contract Project
```bash
synor-contract new my_token
cd my_token
```
This creates:
```
my_token/
├── Cargo.toml
├── src/
│ └── lib.rs
└── tests/
└── integration.rs
```
---
## Part 3: Writing Your First Contract
### A Simple Token Contract
Edit `src/lib.rs`:
```rust
//! A simple fungible token contract.
#![no_std]
use synor_contract_sdk::prelude::*;
// Contract state
#[derive(Default)]
struct Token {
name: String,
symbol: String,
decimals: u8,
total_supply: u128,
balances: Map<Address, u128>,
allowances: Map<(Address, Address), u128>,
}
// Contract implementation
#[synor_contract]
impl Token {
/// Initialize the token contract
#[init]
pub fn new(name: String, symbol: String, decimals: u8, initial_supply: u128) -> Self {
let caller = env::caller();
let mut balances = Map::new();
balances.insert(caller, initial_supply);
emit!(TokenCreated {
name: name.clone(),
symbol: symbol.clone(),
total_supply: initial_supply,
});
Token {
name,
symbol,
decimals,
total_supply: initial_supply,
balances,
allowances: Map::new(),
}
}
/// Get token name
#[view]
pub fn name(&self) -> String {
self.name.clone()
}
/// Get token symbol
#[view]
pub fn symbol(&self) -> String {
self.symbol.clone()
}
/// Get decimals
#[view]
pub fn decimals(&self) -> u8 {
self.decimals
}
/// Get total supply
#[view]
pub fn total_supply(&self) -> u128 {
self.total_supply
}
/// Get balance of address
#[view]
pub fn balance_of(&self, owner: Address) -> u128 {
self.balances.get(&owner).copied().unwrap_or(0)
}
/// Transfer tokens to another address
#[mutate]
pub fn transfer(&mut self, to: Address, amount: u128) -> bool {
let from = env::caller();
let from_balance = self.balance_of(from);
require!(from_balance >= amount, "Insufficient balance");
self.balances.insert(from, from_balance - amount);
let to_balance = self.balance_of(to);
self.balances.insert(to, to_balance + amount);
emit!(Transfer { from, to, amount });
true
}
/// Approve spender to transfer tokens
#[mutate]
pub fn approve(&mut self, spender: Address, amount: u128) -> bool {
let owner = env::caller();
self.allowances.insert((owner, spender), amount);
emit!(Approval { owner, spender, amount });
true
}
/// Get allowance
#[view]
pub fn allowance(&self, owner: Address, spender: Address) -> u128 {
self.allowances.get(&(owner, spender)).copied().unwrap_or(0)
}
/// Transfer tokens on behalf of owner
#[mutate]
pub fn transfer_from(&mut self, from: Address, to: Address, amount: u128) -> bool {
let spender = env::caller();
let allowed = self.allowance(from, spender);
require!(allowed >= amount, "Allowance exceeded");
let from_balance = self.balance_of(from);
require!(from_balance >= amount, "Insufficient balance");
// Update balances
self.balances.insert(from, from_balance - amount);
let to_balance = self.balance_of(to);
self.balances.insert(to, to_balance + amount);
// Update allowance
self.allowances.insert((from, spender), allowed - amount);
emit!(Transfer { from, to, amount });
true
}
/// Mint new tokens (only for demonstration)
#[mutate]
pub fn mint(&mut self, to: Address, amount: u128) {
// In production, add access control!
let balance = self.balance_of(to);
self.balances.insert(to, balance + amount);
self.total_supply += amount;
emit!(Transfer {
from: Address::zero(),
to,
amount
});
}
/// Burn tokens
#[mutate]
pub fn burn(&mut self, amount: u128) {
let caller = env::caller();
let balance = self.balance_of(caller);
require!(balance >= amount, "Insufficient balance");
self.balances.insert(caller, balance - amount);
self.total_supply -= amount;
emit!(Transfer {
from: caller,
to: Address::zero(),
amount
});
}
}
// Events
#[event]
struct TokenCreated {
name: String,
symbol: String,
total_supply: u128,
}
#[event]
struct Transfer {
from: Address,
to: Address,
amount: u128,
}
#[event]
struct Approval {
owner: Address,
spender: Address,
amount: u128,
}
```
---
## Part 4: Building the Contract
### Compile to WASM
```bash
# Build optimized WASM
synor-contract build --release
# Output:
# Compiled: target/wasm32-unknown-unknown/release/my_token.wasm
# Size: 45.2 KB
# Hash: 0x1234...
```
### Verify the Build
```bash
# Check contract info
synor-contract info target/wasm32-unknown-unknown/release/my_token.wasm
# Output:
# Contract: my_token
# Methods: new, name, symbol, decimals, total_supply, balance_of, transfer, approve, allowance, transfer_from, mint, burn
# View methods: name, symbol, decimals, total_supply, balance_of, allowance
# Mutate methods: transfer, approve, transfer_from, mint, burn
```
---
## Part 5: Deploying the Contract
### Using the CLI
```bash
# Deploy to testnet
synor-contract deploy \
--wasm target/wasm32-unknown-unknown/release/my_token.wasm \
--init "new" \
--args '"MyToken","MTK",18,1000000000000000000000' \
--network testnet
# Output:
# Contract deployed!
# Address: tsynor1contract_address...
# Transaction: 0xabc123...
# Gas used: 150000
```
### Using JavaScript
```javascript
import { deployContract } from '@synor/sdk';
import fs from 'fs';
async function deploy(wallet) {
const wasm = fs.readFileSync('./my_token.wasm');
const result = await deployContract(wallet, {
wasm,
method: 'new',
args: ['MyToken', 'MTK', 18, '1000000000000000000000'],
});
console.log('Contract address:', result.contractAddress);
return result.contractAddress;
}
```
---
## Part 6: Interacting with the Contract
### Read Contract State (View Methods)
```javascript
import { callContractView } from '@synor/sdk';
// Get token info
const name = await callContractView(contractAddress, 'name', []);
console.log('Token name:', name); // "MyToken"
// Get balance
const balance = await callContractView(
contractAddress,
'balance_of',
[myAddress]
);
console.log('Balance:', balance);
```
### Write to Contract (Mutate Methods)
```javascript
import { callContractMutate } from '@synor/sdk';
// Transfer tokens
const tx = await callContractMutate(wallet, contractAddress, 'transfer', [
recipientAddress,
'100000000000000000000', // 100 tokens with 18 decimals
]);
console.log('Transfer TX:', tx.id);
```
### Listen to Events
```javascript
const ws = new WebSocket('ws://localhost:17111');
ws.onopen = () => {
ws.send(JSON.stringify({
jsonrpc: '2.0',
method: 'synor_subscribeContractEvents',
params: [contractAddress],
id: 1
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.method === 'synor_contractEvent') {
const { eventName, data } = msg.params;
if (eventName === 'Transfer') {
console.log(`Transfer: ${data.from} -> ${data.to}: ${data.amount}`);
}
}
};
```
---
## Part 7: Testing Your Contract
### Write Integration Tests
```rust
// tests/integration.rs
#[test]
fn test_token_transfer() {
let mut contract = Token::new(
"TestToken".to_string(),
"TT".to_string(),
18,
1_000_000 * 10u128.pow(18),
);
let alice = Address::from_hex("0x1234...").unwrap();
let bob = Address::from_hex("0x5678...").unwrap();
// Alice transfers to Bob
env::set_caller(alice);
assert!(contract.transfer(bob, 100 * 10u128.pow(18)));
// Check balances
assert_eq!(contract.balance_of(bob), 100 * 10u128.pow(18));
}
#[test]
fn test_insufficient_balance() {
let contract = Token::new(...);
env::set_caller(bob); // Bob has no tokens
assert!(!contract.transfer(alice, 100));
}
```
### Run Tests
```bash
cargo test
```
---
## Part 8: Best Practices
### Security Checklist
- [ ] Add access control (owner, admin roles)
- [ ] Validate all inputs
- [ ] Check for overflow (use checked_* operations)
- [ ] Test edge cases
- [ ] Audit before mainnet
### Gas Optimization
```rust
// Prefer: Direct storage access
let balance = self.balances.get(&addr);
// Avoid: Multiple reads
let b1 = self.balance_of(addr);
let b2 = self.balance_of(addr); // Wasteful!
// Prefer: Batch updates
self.balances.insert(from, from_balance - amount);
self.balances.insert(to, to_balance + amount);
```
### Error Handling
```rust
// Use require! for validation
require!(amount > 0, "Amount must be positive");
require!(balance >= amount, "Insufficient balance");
// Return Result for recoverable errors
pub fn safe_transfer(&mut self, to: Address, amount: u128) -> Result<(), &'static str> {
if amount == 0 {
return Err("Amount must be positive");
}
// ...
Ok(())
}
```
---
## Example Contracts
For more examples, see:
- **NFT Contract:** `contracts/examples/nft/`
- **DEX Contract:** `contracts/examples/dex/`
- **Multisig:** `contracts/examples/multisig/`
- **DAO Governance:** `contracts/examples/dao/`
---
## What's Next
- [Tutorial 4: Working with the API](./04-api-guide.md)
- [Host Functions Reference](../contracts/HOST_FUNCTIONS.md)
- [Contract SDK Documentation](../contracts/SDK_REFERENCE.md)
---
*Next: [Working with the API](./04-api-guide.md)*