# 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, 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)*