diff --git a/skill/SKILL.md b/skill/SKILL.md index a5040c3..0e4dc55 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -83,6 +83,8 @@ Use this Skill when the user asks for: - Querying chain data or indexing? → [api-rpc-horizon.md](api-rpc-horizon.md) (also see [Data Docs](https://developers.stellar.org/docs/data)) - Security review? → [security.md](security.md) - Hit an error? → [common-pitfalls.md](common-pitfalls.md) +- Advanced patterns (upgrades, factories, DeFi, governance)? → [advanced-patterns.md](advanced-patterns.md) +- SEP/CAP standards reference? → [standards-reference.md](standards-reference.md) ### 2. Pick the right building blocks - Contracts: Soroban Rust SDK + Stellar CLI @@ -122,11 +124,18 @@ When you implement changes, provide: - API access (RPC/Horizon): [api-rpc-horizon.md](api-rpc-horizon.md) - Security checklist: [security.md](security.md) - Common pitfalls: [common-pitfalls.md](common-pitfalls.md) +- Advanced patterns: [advanced-patterns.md](advanced-patterns.md) +- SEP/CAP standards: [standards-reference.md](standards-reference.md) - Ecosystem projects: [ecosystem.md](ecosystem.md) - Reference links: [resources.md](resources.md) ## Keywords stellar, soroban, xlm, smart contracts, rust, wasm, webassembly, rpc, horizon, +freighter, stellar-sdk, soroban-sdk, stellar-cli, trustline, anchor, sep, cap, passkey, +smart wallet, sac, stellar asset contract, defi, token, nft, scaffold stellar, +constructor, upgradeable, factory, governance, multisig, vault, oracle, compliance, +sep-0041, sep-0046, sep-0048, sep-0049, sep-0050, sep-0056, sep-0057, +cap-0046, cap-0051, cap-0058, cap-0059, cap-0074, cap-0075 freighter, stellar-sdk, soroban-sdk, stellar-cli, trustline, anchor, sep, passkey, smart wallet, sac, stellar asset contract, defi, token, nft, scaffold stellar, zero-knowledge, zk, zk-snark, groth16, bn254, poseidon, pairing, privacy, confidential, diff --git a/skill/advanced-patterns.md b/skill/advanced-patterns.md new file mode 100644 index 0000000..5faaad3 --- /dev/null +++ b/skill/advanced-patterns.md @@ -0,0 +1,1006 @@ +# Advanced Soroban Patterns + +## When to use this guide +Use this guide when you need: +- Factory patterns for deploying multiple contracts +- Upgradeable contract patterns (SEP-0049) +- Governance and multi-sig patterns +- DeFi primitives (vaults, oracles, liquidity pools) +- Gas/resource optimization strategies +- Compliance and regulated token patterns + +## Factory Pattern (Deploying from Contracts) +Use `env.deployer()` to programmatically deploy contracts from within another contract. + +```rust +#![no_std] +use soroban_sdk::{contract, contractimpl, symbol_short, Address, BytesN, Env, Symbol, Val, Vec}; + +#[contract] +pub struct Deployer; + +const ADMIN: Symbol = symbol_short!("admin"); + +#[contractimpl] +impl Deployer { + /// Initialize the deployer with an admin + pub fn __constructor(env: Env, admin: Address) { + env.storage().instance().set(&ADMIN, &admin); + } + + /// Deploy a new contract with constructor arguments + pub fn deploy( + env: Env, + wasm_hash: BytesN<32>, + salt: BytesN<32>, + constructor_args: Vec, + ) -> Address { + let admin: Address = env.storage().instance().get(&ADMIN).unwrap(); + admin.require_auth(); + + // Deploy the contract using the uploaded Wasm with given hash + // The contract address is derived from: deployer address + salt + let deployed_address = env + .deployer() + .with_address(env.current_contract_address(), salt) + .deploy_v2(wasm_hash, constructor_args); + + deployed_address + } +} +``` + +### Cross-Contract Communication Pattern +Use `contractimport!` to generate type-safe clients for calling other contracts. + +```rust +#![no_std] +use soroban_sdk::{contract, contractimpl, Address, Env}; + +// Import another contract's WASM to generate client +mod other_contract { + soroban_sdk::contractimport!( + file = "../other/target/wasm32-unknown-unknown/release/other.wasm" + ); +} + +#[contract] +pub struct MyContract; + +#[contractimpl] +impl MyContract { + pub fn call_other(env: Env, other_address: Address, value: i128) -> i128 { + // Create client for the other contract + let client = other_contract::Client::new(&env, &other_address); + + // Call method on other contract + client.some_method(&value) + } +} +``` + +## Upgradeable Contracts (SEP-0049) + +Soroban contracts are **mutable by default** - they can upgrade their WASM bytecode. This differs from Ethereum where upgrades require proxy patterns. + +### Versioning with Contract Metadata +```rust +#![no_std] +use soroban_sdk::{contract, contractimpl, contractmeta, Env}; + +// Store version in contract metadata (SEP-0049 recommended) +contractmeta!(key = "binver", val = "1.2.0"); + +#[contract] +pub struct MyContract; +``` + +### Basic Upgrade Pattern +```rust +use soroban_sdk::{contract, contractimpl, Address, BytesN, Env}; + +#[contract] +pub struct MyContract; + +#[contractimpl] +impl MyContract { + /// Upgrade contract WASM. Only callable by admin. + pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) { + // Verify caller is admin + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); + admin.require_auth(); + + // Update the contract's WASM code + env.deployer().update_current_contract_wasm(new_wasm_hash); + } +} +``` + +### Migration Pattern +Handle storage layout changes after upgrades. + +```rust +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; + +#[contracttype] +pub enum DataKey { + Admin, + Version, + MigrationComplete, + // V2 adds new storage keys + NewFeatureEnabled, +} + +#[contract] +pub struct MyContract; + +#[contractimpl] +impl MyContract { + /// Migrate storage after upgrade. Call once after upgrade. + pub fn migrate(env: Env, new_version: u32) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); + admin.require_auth(); + + let current_version: u32 = env + .storage() + .instance() + .get(&DataKey::Version) + .unwrap_or(1); + + if new_version <= current_version { + panic!("already migrated to this version"); + } + + // Perform migration based on version + if current_version < 2 && new_version >= 2 { + // V1 -> V2 migration + env.storage().instance().set(&DataKey::NewFeatureEnabled, &false); + } + + env.storage().instance().set(&DataKey::Version, &new_version); + } +} +``` + +### Atomic Upgrade + Migrate (Upgrader Contract) +Use a separate contract to perform upgrade and migration atomically (SEP-0049 recommended). + +```rust +use soroban_sdk::{contract, contractimpl, Address, BytesN, Env}; + +mod upgradeable { + soroban_sdk::contractimport!(file = "../target/wasm32-unknown-unknown/release/upgradeable.wasm"); +} + +#[contract] +pub struct Upgrader; + +#[contractimpl] +impl Upgrader { + /// Upgrade and migrate atomically in one transaction + pub fn upgrade_and_migrate( + env: Env, + contract_address: Address, + operator: Address, + new_wasm_hash: BytesN<32>, + new_version: u32, + ) { + operator.require_auth(); + + // Create client using contractimport! + let client = upgradeable::Client::new(&env, &contract_address); + + // Step 1: Upgrade the contract + client.upgrade(&new_wasm_hash); + + // Step 2: Call migrate in same transaction (atomic) + client.migrate(&new_version); + } +} +``` + +### Making Contracts Immutable +To make a contract permanently non-upgradeable, simply don't include an upgrade function. + +```rust +// This contract has NO upgrade function - it's immutable +#[contract] +pub struct ImmutableContract; + +#[contractimpl] +impl ImmutableContract { + pub fn initialize(env: Env, admin: Address) { + // ... initialization logic + // No upgrade() function = permanently immutable + } +} +``` + +### Upgrade Safety Checklist (SEP-0049) +1. **Version tracking**: Store version in metadata (`binver`) and storage +2. **One-time migration**: Ensure `migrate()` can only run once per version +3. **Access control**: Only admin can trigger upgrades +4. **Rollback strategy**: Plan how to fix issues if upgrade goes wrong +5. **No constructor reliance**: Constructor won't run on upgrade +6. **Preserve upgrade capability**: New WASM must include upgrade function (unless intentionally making immutable) +7. **Storage compatibility**: New contract must handle old storage layout + +## Governance Patterns + +### Time-locked Operations +Delay sensitive operations to allow review. + +```rust +use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env}; + +#[contracttype] +#[derive(Clone)] +pub struct PendingUpgrade { + pub wasm_hash: BytesN<32>, + pub execute_after: u64, // ledger sequence + pub proposer: Address, +} + +#[contracttype] +pub enum DataKey { + Admin, + PendingUpgrade, + TimelockDelay, // in ledgers (~5 sec each) +} + +const DEFAULT_DELAY: u64 = 17280; // ~1 day + +#[contract] +pub struct TimelockContract; + +#[contractimpl] +impl TimelockContract { + /// Propose an upgrade (starts timelock) + pub fn propose_upgrade(env: Env, wasm_hash: BytesN<32>) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); + admin.require_auth(); + + let delay: u64 = env + .storage() + .instance() + .get(&DataKey::TimelockDelay) + .unwrap_or(DEFAULT_DELAY); + + let pending = PendingUpgrade { + wasm_hash, + execute_after: env.ledger().sequence() + delay, + proposer: admin, + }; + + env.storage().instance().set(&DataKey::PendingUpgrade, &pending); + } + + /// Execute upgrade after timelock expires + pub fn execute_upgrade(env: Env) { + let pending: PendingUpgrade = env + .storage() + .instance() + .get(&DataKey::PendingUpgrade) + .expect("no pending upgrade"); + + if env.ledger().sequence() < pending.execute_after { + panic!("timelock not expired"); + } + + env.deployer().update_current_contract_wasm(pending.wasm_hash); + env.storage().instance().remove(&DataKey::PendingUpgrade); + } + + /// Cancel pending upgrade + pub fn cancel_upgrade(env: Env) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); + admin.require_auth(); + + env.storage().instance().remove(&DataKey::PendingUpgrade); + } +} +``` + +### Multi-Signature Pattern +Require multiple approvals for sensitive operations. + +```rust +use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env, Map, Vec}; + +#[contracttype] +#[derive(Clone)] +pub struct Proposal { + pub id: u64, + pub action: ProposalAction, + pub approvals: Vec
, + pub executed: bool, +} + +#[contracttype] +#[derive(Clone)] +pub enum ProposalAction { + Upgrade(BytesN<32>), + Transfer { to: Address, amount: i128 }, + AddSigner(Address), + RemoveSigner(Address), + ChangeThreshold(u32), +} + +#[contracttype] +pub enum DataKey { + Signers, + Threshold, + ProposalCount, + Proposal(u64), +} + +#[contract] +pub struct MultisigContract; + +#[contractimpl] +impl MultisigContract { + pub fn initialize(env: Env, signers: Vec
, threshold: u32) { + if env.storage().instance().has(&DataKey::Signers) { + panic!("already initialized"); + } + if threshold as usize > signers.len() || threshold == 0 { + panic!("invalid threshold"); + } + + env.storage().instance().set(&DataKey::Signers, &signers); + env.storage().instance().set(&DataKey::Threshold, &threshold); + env.storage().instance().set(&DataKey::ProposalCount, &0u64); + } + + pub fn propose(env: Env, proposer: Address, action: ProposalAction) -> u64 { + proposer.require_auth(); + Self::require_signer(&env, &proposer); + + let mut count: u64 = env.storage().instance().get(&DataKey::ProposalCount).unwrap(); + count += 1; + + let proposal = Proposal { + id: count, + action, + approvals: Vec::new(&env), + executed: false, + }; + + env.storage().persistent().set(&DataKey::Proposal(count), &proposal); + env.storage().instance().set(&DataKey::ProposalCount, &count); + + count + } + + pub fn approve(env: Env, signer: Address, proposal_id: u64) { + signer.require_auth(); + Self::require_signer(&env, &signer); + + let mut proposal: Proposal = env + .storage() + .persistent() + .get(&DataKey::Proposal(proposal_id)) + .expect("proposal not found"); + + if proposal.executed { + panic!("already executed"); + } + + // Check not already approved by this signer + for approved in proposal.approvals.iter() { + if approved == signer { + panic!("already approved"); + } + } + + proposal.approvals.push_back(signer); + env.storage().persistent().set(&DataKey::Proposal(proposal_id), &proposal); + } + + pub fn execute(env: Env, proposal_id: u64) { + let mut proposal: Proposal = env + .storage() + .persistent() + .get(&DataKey::Proposal(proposal_id)) + .expect("proposal not found"); + + if proposal.executed { + panic!("already executed"); + } + + let threshold: u32 = env.storage().instance().get(&DataKey::Threshold).unwrap(); + if (proposal.approvals.len() as u32) < threshold { + panic!("not enough approvals"); + } + + proposal.executed = true; + env.storage().persistent().set(&DataKey::Proposal(proposal_id), &proposal); + + // Execute the action + match proposal.action { + ProposalAction::Upgrade(wasm_hash) => { + env.deployer().update_current_contract_wasm(wasm_hash); + } + ProposalAction::ChangeThreshold(new_threshold) => { + env.storage().instance().set(&DataKey::Threshold, &new_threshold); + } + // Handle other actions... + _ => {} + } + } + + fn require_signer(env: &Env, addr: &Address) { + let signers: Vec
= env.storage().instance().get(&DataKey::Signers).unwrap(); + let mut is_signer = false; + for s in signers.iter() { + if &s == addr { + is_signer = true; + break; + } + } + if !is_signer { + panic!("not a signer"); + } + } +} +``` + +## DeFi Patterns + +### Vault Pattern (SEP-0056 style) +Tokenized vault for yield-bearing deposits. + +```rust +use soroban_sdk::{contract, contractimpl, contracttype, token, Address, Env}; + +#[contracttype] +pub enum DataKey { + Asset, // underlying asset address + TotalShares, + TotalAssets, + Shares(Address), // user shares +} + +#[contract] +pub struct Vault; + +#[contractimpl] +impl Vault { + pub fn deposit(env: Env, user: Address, assets: i128) -> i128 { + user.require_auth(); + + let asset_addr: Address = env.storage().instance().get(&DataKey::Asset).unwrap(); + let token = token::Client::new(&env, &asset_addr); + + // Transfer assets to vault + token.transfer(&user, &env.current_contract_address(), &assets); + + // Calculate shares to mint + let shares = Self::convert_to_shares(&env, assets); + + // Update state + let mut total_shares: i128 = env.storage().instance().get(&DataKey::TotalShares).unwrap_or(0); + let mut total_assets: i128 = env.storage().instance().get(&DataKey::TotalAssets).unwrap_or(0); + let mut user_shares: i128 = env.storage().persistent().get(&DataKey::Shares(user.clone())).unwrap_or(0); + + total_shares += shares; + total_assets += assets; + user_shares += shares; + + env.storage().instance().set(&DataKey::TotalShares, &total_shares); + env.storage().instance().set(&DataKey::TotalAssets, &total_assets); + env.storage().persistent().set(&DataKey::Shares(user), &user_shares); + + shares + } + + pub fn withdraw(env: Env, user: Address, shares: i128) -> i128 { + user.require_auth(); + + let user_shares: i128 = env.storage().persistent().get(&DataKey::Shares(user.clone())).unwrap_or(0); + if user_shares < shares { + panic!("insufficient shares"); + } + + // Calculate assets to return + let assets = Self::convert_to_assets(&env, shares); + + // Update state + let mut total_shares: i128 = env.storage().instance().get(&DataKey::TotalShares).unwrap(); + let mut total_assets: i128 = env.storage().instance().get(&DataKey::TotalAssets).unwrap(); + + total_shares -= shares; + total_assets -= assets; + + env.storage().instance().set(&DataKey::TotalShares, &total_shares); + env.storage().instance().set(&DataKey::TotalAssets, &total_assets); + env.storage().persistent().set(&DataKey::Shares(user.clone()), &(user_shares - shares)); + + // Transfer assets to user + let asset_addr: Address = env.storage().instance().get(&DataKey::Asset).unwrap(); + let token = token::Client::new(&env, &asset_addr); + token.transfer(&env.current_contract_address(), &user, &assets); + + assets + } + + fn convert_to_shares(env: &Env, assets: i128) -> i128 { + let total_shares: i128 = env.storage().instance().get(&DataKey::TotalShares).unwrap_or(0); + let total_assets: i128 = env.storage().instance().get(&DataKey::TotalAssets).unwrap_or(0); + + if total_shares == 0 || total_assets == 0 { + assets // 1:1 for first deposit + } else { + (assets * total_shares) / total_assets + } + } + + fn convert_to_assets(env: &Env, shares: i128) -> i128 { + let total_shares: i128 = env.storage().instance().get(&DataKey::TotalShares).unwrap_or(0); + let total_assets: i128 = env.storage().instance().get(&DataKey::TotalAssets).unwrap_or(0); + + if total_shares == 0 { + 0 + } else { + (shares * total_assets) / total_shares + } + } +} +``` + +### Oracle Integration Pattern +Consume price feeds from oracle contracts. + +```rust +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, Symbol}; + +#[contracttype] +#[derive(Clone)] +pub struct PriceData { + pub price: i128, // price with decimals + pub timestamp: u64, // Unix timestamp + pub decimals: u32, +} + +// Import oracle contract (requires the compiled WASM) +mod oracle { + soroban_sdk::contractimport!( + file = "../oracle/target/wasm32-unknown-unknown/release/oracle.wasm" + ); +} + +#[contract] +pub struct MyDeFiContract; + +#[contractimpl] +impl MyDeFiContract { + pub fn get_collateral_value(env: Env, oracle_addr: Address, asset: Symbol, amount: i128) -> i128 { + // Create client for oracle contract + let oracle_client = oracle::Client::new(&env, &oracle_addr); + let price_data: PriceData = oracle_client.get_price(&asset); + + // Check price is fresh (within last ~10 minutes) + let max_age: u64 = 600; // 600 seconds = 10 minutes + let current_time: u64 = env.ledger().timestamp(); + if current_time - price_data.timestamp > max_age { + panic!("stale price"); + } + + // Calculate value: amount * price / 10^decimals + let decimals_factor = 10i128.pow(price_data.decimals); + (amount * price_data.price) / decimals_factor + } +} +``` + +### Liquidity Pool (Constant Product AMM) +```rust +use soroban_sdk::{contract, contractimpl, contracttype, token, Address, Env}; + +#[contracttype] +pub enum DataKey { + TokenA, + TokenB, + ReserveA, + ReserveB, + TotalLiquidity, + Liquidity(Address), +} + +const FEE_BPS: i128 = 30; // 0.3% fee + +#[contract] +pub struct LiquidityPool; + +#[contractimpl] +impl LiquidityPool { + /// Swap token A for token B + pub fn swap_a_for_b(env: Env, user: Address, amount_in: i128, min_out: i128) -> i128 { + user.require_auth(); + + let reserve_a: i128 = env.storage().instance().get(&DataKey::ReserveA).unwrap(); + let reserve_b: i128 = env.storage().instance().get(&DataKey::ReserveB).unwrap(); + + // Calculate output with fee: (amount_in * (10000 - fee) * reserve_b) / (reserve_a * 10000 + amount_in * (10000 - fee)) + let amount_in_with_fee = amount_in * (10000 - FEE_BPS); + let numerator = amount_in_with_fee * reserve_b; + let denominator = reserve_a * 10000 + amount_in_with_fee; + let amount_out = numerator / denominator; + + if amount_out < min_out { + panic!("slippage exceeded"); + } + + // Transfer tokens + let token_a: Address = env.storage().instance().get(&DataKey::TokenA).unwrap(); + let token_b: Address = env.storage().instance().get(&DataKey::TokenB).unwrap(); + + token::Client::new(&env, &token_a).transfer(&user, &env.current_contract_address(), &amount_in); + token::Client::new(&env, &token_b).transfer(&env.current_contract_address(), &user, &amount_out); + + // Update reserves + env.storage().instance().set(&DataKey::ReserveA, &(reserve_a + amount_in)); + env.storage().instance().set(&DataKey::ReserveB, &(reserve_b - amount_out)); + + amount_out + } + + /// Add liquidity + pub fn add_liquidity(env: Env, user: Address, amount_a: i128, amount_b: i128) -> i128 { + user.require_auth(); + + let reserve_a: i128 = env.storage().instance().get(&DataKey::ReserveA).unwrap_or(0); + let reserve_b: i128 = env.storage().instance().get(&DataKey::ReserveB).unwrap_or(0); + let total_liquidity: i128 = env.storage().instance().get(&DataKey::TotalLiquidity).unwrap_or(0); + + let liquidity: i128; + if total_liquidity == 0 { + // First deposit - liquidity = sqrt(amount_a * amount_b) + liquidity = Self::sqrt(amount_a * amount_b); + } else { + // Proportional deposit + let liquidity_a = (amount_a * total_liquidity) / reserve_a; + let liquidity_b = (amount_b * total_liquidity) / reserve_b; + liquidity = if liquidity_a < liquidity_b { liquidity_a } else { liquidity_b }; + } + + // Transfer tokens to pool + let token_a: Address = env.storage().instance().get(&DataKey::TokenA).unwrap(); + let token_b: Address = env.storage().instance().get(&DataKey::TokenB).unwrap(); + + token::Client::new(&env, &token_a).transfer(&user, &env.current_contract_address(), &amount_a); + token::Client::new(&env, &token_b).transfer(&user, &env.current_contract_address(), &amount_b); + + // Update state + let user_liquidity: i128 = env.storage().persistent().get(&DataKey::Liquidity(user.clone())).unwrap_or(0); + env.storage().persistent().set(&DataKey::Liquidity(user), &(user_liquidity + liquidity)); + env.storage().instance().set(&DataKey::TotalLiquidity, &(total_liquidity + liquidity)); + env.storage().instance().set(&DataKey::ReserveA, &(reserve_a + amount_a)); + env.storage().instance().set(&DataKey::ReserveB, &(reserve_b + amount_b)); + + liquidity + } + + fn sqrt(n: i128) -> i128 { + if n == 0 { return 0; } + let mut x = n; + let mut y = (x + 1) / 2; + while y < x { + x = y; + y = (x + n / x) / 2; + } + x + } +} +``` + +## Resource Optimization + +### Batching Operations +Combine multiple operations to reduce transaction overhead. + +```rust +use soroban_sdk::{contract, contractimpl, contracttype, token, Address, Env, Vec}; + +#[contracttype] +#[derive(Clone)] +pub struct Transfer { + pub to: Address, + pub amount: i128, +} + +#[contracttype] +pub enum DataKey { + Token, +} + +#[contract] +pub struct BatchContract; + +#[contractimpl] +impl BatchContract { + /// Batch multiple transfers in one call + pub fn batch_transfer(env: Env, from: Address, transfers: Vec) { + from.require_auth(); + + let token_addr: Address = env.storage().instance().get(&DataKey::Token).unwrap(); + let token_client = token::Client::new(&env, &token_addr); + + for transfer in transfers.iter() { + token_client.transfer(&from, &transfer.to, &transfer.amount); + } + } +} +``` + +### Storage Optimization +```rust +// Use compact data structures +#[contracttype] +pub enum DataKey { + // Use u32 IDs instead of full addresses when possible + UserById(u32), + IdByUser(Address), + // Pack related data together + UserData(u32), // contains balance, status, timestamp in one struct +} + +// Use appropriate storage type +impl StorageOptimized { + pub fn set_data(env: Env, key: DataKey, value: SomeData) { + // Temporary: cheap, auto-deleted (use for caches, flags) + env.storage().temporary().set(&DataKey::TempFlag, &true); + + // Instance: shared across contract (use for global config) + env.storage().instance().set(&DataKey::Config, &config); + + // Persistent: per-key TTL (use for user balances, important state) + env.storage().persistent().set(&DataKey::Balance(user), &balance); + } +} +``` + +### Lazy Loading Pattern +Only load data when needed. + +```rust +#[contract] +pub struct LazyContract; + +#[contractimpl] +impl LazyContract { + pub fn process_if_needed(env: Env, user: Address) { + // Check flag first (cheap read) + let needs_processing: bool = env + .storage() + .temporary() + .get(&DataKey::NeedsProcess(user.clone())) + .unwrap_or(false); + + if !needs_processing { + return; // Early exit, no expensive reads + } + + // Only now load full user data (expensive) + let user_data: UserData = env + .storage() + .persistent() + .get(&DataKey::UserData(user.clone())) + .unwrap(); + + // Process... + } +} +``` + +## Compliance Patterns (SEP-0057 style) + +### Transfer Restrictions +Implement whitelisting for regulated tokens. + +```rust +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; + +#[contracttype] +pub enum DataKey { + Admin, + Whitelist(Address), + TransfersPaused, +} + +#[contract] +pub struct RegulatedToken; + +#[contractimpl] +impl RegulatedToken { + /// Transfer with compliance checks + pub fn transfer(env: Env, from: Address, to: Address, amount: i128) { + from.require_auth(); + + // Check not paused + let paused: bool = env + .storage() + .instance() + .get(&DataKey::TransfersPaused) + .unwrap_or(false); + if paused { + panic!("transfers paused"); + } + + // Check both parties are whitelisted + let from_whitelisted: bool = env + .storage() + .persistent() + .get(&DataKey::Whitelist(from.clone())) + .unwrap_or(false); + let to_whitelisted: bool = env + .storage() + .persistent() + .get(&DataKey::Whitelist(to.clone())) + .unwrap_or(false); + + if !from_whitelisted || !to_whitelisted { + panic!("not whitelisted"); + } + + // Perform transfer... + Self::do_transfer(&env, &from, &to, amount); + } + + /// Admin: add address to whitelist + pub fn add_to_whitelist(env: Env, address: Address) { + Self::require_admin(&env); + env.storage().persistent().set(&DataKey::Whitelist(address), &true); + } + + /// Admin: remove address from whitelist + pub fn remove_from_whitelist(env: Env, address: Address) { + Self::require_admin(&env); + env.storage().persistent().set(&DataKey::Whitelist(address), &false); + } + + /// Admin: pause all transfers + pub fn pause(env: Env) { + Self::require_admin(&env); + env.storage().instance().set(&DataKey::TransfersPaused, &true); + } + + /// Admin: unpause transfers + pub fn unpause(env: Env) { + Self::require_admin(&env); + env.storage().instance().set(&DataKey::TransfersPaused, &false); + } + + fn require_admin(env: &Env) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); + admin.require_auth(); + } + + fn do_transfer(env: &Env, from: &Address, to: &Address, amount: i128) { + // Implementation... + } +} +``` + +### Clawback Support +Allow authorized recovery of tokens (for compliance). + +```rust +use soroban_sdk::{contractevent, Address, Env}; + +#[contractevent] +pub struct ClawbackEvent { + pub from: Address, + pub amount: i128, +} + +#[contract] +pub struct ClawbackToken; + +#[contractimpl] +impl ClawbackToken { + /// Clawback tokens from an address (compliance/legal requirement) + pub fn clawback(env: Env, from: Address, amount: i128) { + // Only clawback admin can call + let clawback_admin: Address = env + .storage() + .instance() + .get(&DataKey::ClawbackAdmin) + .unwrap(); + clawback_admin.require_auth(); + + // Reduce balance (no auth from 'from' required) + let mut balance: i128 = env + .storage() + .persistent() + .get(&DataKey::Balance(from.clone())) + .unwrap_or(0); + + if balance < amount { + panic!("insufficient balance for clawback"); + } + + balance -= amount; + env.storage().persistent().set(&DataKey::Balance(from.clone()), &balance); + + // Emit clawback event for audit trail + ClawbackEvent { + from: from.clone(), + amount, + }.publish(&env); + } +} +``` + +## Cross-Contract Call Patterns + +### Standard Pattern: contractimport! +Use `contractimport!` to generate a type-safe client. This is the recommended pattern for all cross-contract calls. + +```rust +use soroban_sdk::{contract, contractimpl, Address, Env}; + +// Import the target contract's WASM to generate client +mod target_contract { + soroban_sdk::contractimport!( + file = "../target/wasm32-unknown-unknown/release/target_contract.wasm" + ); +} + +#[contract] +pub struct CallerContract; + +#[contractimpl] +impl CallerContract { + pub fn call_target(env: Env, target_address: Address, value: i128) -> i128 { + // Create client using the generated Client type + let client = target_contract::Client::new(&env, &target_address); + + // Call method on the target contract - type-safe + client.process_value(&value) + } +} +``` + +### Token Client (SEP-0041) +For calling standard token contracts, use the built-in `token::Client`. + +```rust +use soroban_sdk::{contract, contractimpl, token, Address, Env}; + +#[contract] +pub struct MyContract; + +#[contractimpl] +impl MyContract { + pub fn transfer_tokens(env: Env, token_address: Address, from: Address, to: Address, amount: i128) { + from.require_auth(); + + // Use built-in token client for SEP-0041 compliant tokens + let token_client = token::Client::new(&env, &token_address); + token_client.transfer(&from, &to, &amount); + } + + pub fn get_balance(env: Env, token_address: Address, account: Address) -> i128 { + let token_client = token::Client::new(&env, &token_address); + token_client.balance(&account) + } +} +``` + +### Key Points +- `contractimport!` generates a `Client` type from the WASM file +- The WASM must be available at compile time +- The client provides type-safe method calls +- For tokens, use `token::Client` from the SDK + +## Related Standards + +| Standard | Description | Status | +|----------|-------------|--------| +| [SEP-0046](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0046.md) | Contract Meta | Active | +| [SEP-0048](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0048.md) | Contract Interface Specification | Active | +| [SEP-0049](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0049.md) | Upgradeable Contracts | Draft | +| [SEP-0056](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0056.md) | Tokenized Vault Standard | Draft | +| [SEP-0057](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0057.md) | T-REX (Regulated Tokens) | Draft | +| [CAP-0058](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0058.md) | Constructors for Soroban | Final (Protocol 22) | diff --git a/skill/contracts-soroban.md b/skill/contracts-soroban.md index a386cec..26e4f92 100644 --- a/skill/contracts-soroban.md +++ b/skill/contracts-soroban.md @@ -142,6 +142,59 @@ impl CounterContract { } ``` +## Contract Constructors (CAP-0058, Protocol 22+) + +Constructors provide **atomic initialization** - the contract is guaranteed to be initialized when created, preventing front-running attacks. + +### Defining a Constructor +```rust +#![no_std] +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; + +#[contracttype] +pub enum DataKey { + Admin, + Value, +} + +#[contract] +pub struct MyContract; + +#[contractimpl] +impl MyContract { + /// Constructor - called automatically when contract is deployed. + /// Must return () (void). Cannot be called after deployment. + pub fn __constructor(env: Env, admin: Address, initial_value: u32) { + env.storage().instance().set(&DataKey::Admin, &admin); + env.storage().instance().set(&DataKey::Value, &initial_value); + } + + pub fn get_value(env: Env) -> u32 { + env.storage().instance().get(&DataKey::Value).unwrap() + } +} +``` + +### Constructor Rules +1. Function must be named `__constructor` exactly +2. Must return `void` (no return value) - returning anything else fails deployment +3. Only called once during contract creation - NOT called on upgrades +4. Can take any number of arguments (including zero) +5. Can access storage, emit events, make cross-contract calls +6. If constructor fails, contract is not created (atomic) +7. Contracts without constructors can still be deployed (treated as 0-arg constructor) + +### Constructor vs Initialize Pattern + +| Aspect | Constructor | Initialize Function | +|--------|-------------|---------------------| +| Atomicity | Guaranteed - single transaction | Requires separate tx, can be front-run | +| Re-initialization | Impossible | Must add protection manually | +| Upgrade behavior | Not called on upgrade | Can be called on upgrade (if desired) | +| Protocol support | Protocol 22+ | All versions | + +**Recommendation:** Use constructors for new contracts (Protocol 22+). Use initialize pattern only for backwards compatibility or when re-initialization on upgrade is needed. + ## Storage Types Soroban has three storage types with different costs and lifetimes: @@ -381,7 +434,7 @@ stellar contract build # Generate and fund a new identity stellar keys generate --global alice --network testnet --fund -# Deploy contract +# Deploy contract (without constructor) stellar contract deploy \ --wasm target/wasm32-unknown-unknown/release/my_contract.wasm \ --source alice \ @@ -390,7 +443,19 @@ stellar contract deploy \ # Returns: CONTRACT_ID (starts with 'C') ``` -### Initialize Contract +### Deploy with Constructor Arguments (Protocol 22+) +```bash +# Deploy with constructor arguments +stellar contract deploy \ + --wasm target/wasm32-unknown-unknown/release/my_contract.wasm \ + --source alice \ + --network testnet \ + -- \ + --admin alice \ + --initial_value 100 +``` + +### Initialize Contract (legacy pattern) ```bash stellar contract invoke \ --id CONTRACT_ID \ diff --git a/skill/standards-reference.md b/skill/standards-reference.md new file mode 100644 index 0000000..79a9b33 --- /dev/null +++ b/skill/standards-reference.md @@ -0,0 +1,233 @@ +# Stellar Standards Reference (SEPs & CAPs) + +## When to use this guide +Use this guide when you need: +- Understanding which SEPs apply to your use case +- Protocol-level capabilities from specific CAPs +- Interface specifications for interoperability +- Compliance with ecosystem standards + +## SEPs for Smart Contracts + +### Active/Final SEPs + +| SEP | Title | Description | Use When | +|-----|-------|-------------|----------| +| [SEP-0041](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md) | Soroban Token Interface | Standard token interface (balance, transfer, approve, allowance) | Building fungible tokens on Soroban | +| [SEP-0046](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0046.md) | Contract Meta | Metadata storage in WASM custom sections | Adding version info, build metadata to contracts | +| [SEP-0048](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0048.md) | Contract Interface Specification | XDR schema for contract interfaces (`contractspecv0`) | Auto-generated clients, tooling, block explorers | + +### Draft SEPs (Emerging Standards) + +| SEP | Title | Description | Use When | +|-----|-------|-------------|----------| +| [SEP-0044](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0044.md) | Token Memo Extension | Add memo support to token transfers | Compliance, exchange integration | +| [SEP-0045](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0045.md) | Web Auth for Contract Accounts | SEP-10 authentication for smart wallets | Passkey/smart wallet authentication | +| [SEP-0047](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0047.md) | Contract Interface Discovery | Discover which SEPs a contract implements | Building interoperable tooling | +| [SEP-0049](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0049.md) | Upgradeable Contracts | Guidelines for safe contract upgrades | Planning upgrade strategy | +| [SEP-0050](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0050.md) | Non-Fungible Tokens | NFT standard for Soroban | Building NFT collections | +| [SEP-0055](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0055.md) | Contract Build Verification | Verify contract source matches deployed WASM | Audit, trust verification | +| [SEP-0056](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0056.md) | Tokenized Vault Standard | ERC-4626 style yield-bearing vaults | DeFi yield products | +| [SEP-0057](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0057.md) | T-REX (Regulated Tokens) | Security tokens with compliance features | Regulated securities, KYC tokens | + +--- + +## CAPs for Smart Contracts + +### Soroban Core System (Protocol 20) + +| CAP | Title | Key Concepts | +|-----|-------|--------------| +| [CAP-0046](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046.md) | Soroban Overview | Smart contract system architecture | +| [CAP-0046-01](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-01.md) | WASM Runtime | WebAssembly execution environment | +| [CAP-0046-02](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-02.md) | Contract Lifecycle | Deploy, invoke, update, archival | +| [CAP-0046-03](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-03.md) | Host Functions | Crypto, storage, cross-contract calls | +| [CAP-0046-05](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-05.md) | Smart Contract Data | Storage types, TTL, archival | +| [CAP-0046-06](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-06.md) | Stellar Asset Contract (SAC) | Bridge classic assets to Soroban | +| [CAP-0046-07](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-07.md) | Fee & Resource Model | Compute, storage, bandwidth costs | +| [CAP-0046-08](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-08.md) | Events | Contract event emission | +| [CAP-0046-10](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-10.md) | Budget Metering | Resource limits and metering | +| [CAP-0046-11](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-11.md) | Authorization Framework | `require_auth()`, custom accounts | +| [CAP-0046-12](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-12.md) | State Archival | TTL management, restoration | + +### Protocol 21 Enhancements + +| CAP | Title | Description | +|-----|-------|-------------| +| [CAP-0051](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0051.md) | Secp256r1 Verification | WebAuthn/Passkey signature verification | +| [CAP-0053](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0053.md) | TTL Extension Functions | Separate instance/code TTL extension | +| [CAP-0054](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0054.md) | VM Cost Model | Refined WASM instantiation costs | +| [CAP-0055](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0055.md) | Streamlined Linking | Faster contract loading | +| [CAP-0056](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0056.md) | Module Caching | Intra-transaction WASM caching | + +### Protocol 22 Features + +| CAP | Title | Description | +|-----|-------|-------------| +| [CAP-0058](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0058.md) | Contract Constructors | `__constructor` for atomic initialization | +| [CAP-0059](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0059.md) | BLS12-381 Curve | Cryptographic primitives for ZK proofs | + +### Protocol 23 Features + +| CAP | Title | Description | +|-----|-------|-------------| +| [CAP-0062](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0062.md) | Live State Prioritization | Optimized state access | +| [CAP-0065](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0065.md) | Reusable Module Cache | Cross-transaction WASM caching | +| [CAP-0066](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0066.md) | In-Memory Read Resource | Cheaper state reads | +| [CAP-0067](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md) | Unified Asset Events | Consistent event format for all assets | +| [CAP-0068](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0068.md) | Get Executable for Address | Query contract's WASM hash | +| [CAP-0069](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0069.md) | String/Bytes Conversion | Host functions for type conversion | + +### Protocol 25 "X-Ray" (ZK Cryptography) + +| CAP | Title | Description | +|-----|-------|-------------| +| [CAP-0074](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0074.md) | BN254 Curve | Ethereum-compatible ZK curve (EIP-196/197) | +| [CAP-0075](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0075.md) | Poseidon Hash | ZK-friendly hash functions | + +### Draft/Upcoming CAPs + +| CAP | Title | Description | Status | +|-----|-------|-------------|--------| +| [CAP-0071](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0071.md) | Auth Delegation | Delegate auth to custom accounts | Draft | +| [CAP-0072](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0072.md) | Contract Signers | Add contract as account signer | Draft | +| [CAP-0073](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0073.md) | SAC G-Account Balances | SAC can create classic trustlines | Draft | +| [CAP-0078](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0078.md) | Limited TTL Extensions | Bounded TTL extension functions | Draft | +| [CAP-0079](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0079.md) | Muxed Address Strkey | Convert muxed addresses in contracts | Draft | +| [CAP-0080](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0080.md) | ZK BN254 Utilities | Efficient ZK host functions | Draft | + +--- + +## SEPs for Ecosystem Integration + +### Authentication & Authorization + +| SEP | Title | Description | +|-----|-------|-------------| +| [SEP-0010](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md) | Web Authentication | Challenge-response auth for web apps | +| [SEP-0045](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0045.md) | Web Auth for Contracts | SEP-10 for smart wallet accounts | + +### Asset Metadata & Discovery + +| SEP | Title | Description | +|-----|-------|-------------| +| [SEP-0001](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md) | stellar.toml | Domain-level asset/account metadata | +| [SEP-0047](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0047.md) | Interface Discovery | Discover contract SEP implementations | + +### Anchor Services (Fiat On/Off Ramps) + +| SEP | Title | Description | +|-----|-------|-------------| +| [SEP-0006](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md) | Deposit/Withdrawal API | Programmatic anchor integration | +| [SEP-0024](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md) | Hosted Deposit/Withdrawal | Interactive anchor flows | +| [SEP-0031](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md) | Cross-Border Payments | Direct fiat-to-fiat transfers | + +### Compliance + +| SEP | Title | Description | +|-----|-------|-------------| +| [SEP-0012](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md) | KYC API | Customer verification for anchors | +| [SEP-0057](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0057.md) | T-REX | Regulated token framework | + +--- + +## Quick Reference by Use Case + +### Building a Token +1. **Fungible token**: Implement [SEP-0041](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md) interface +2. **NFT**: Follow [SEP-0050](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0050.md) (Draft) +3. **Regulated token**: Use [SEP-0057](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0057.md) patterns +4. **Bridge classic asset**: Use SAC ([CAP-0046-06](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-06.md)) + +### Building DeFi +1. **Vault/Yield**: Follow [SEP-0056](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0056.md) (Draft) +2. **ZK Privacy**: Use [CAP-0074](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0074.md)/[CAP-0075](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0075.md) primitives +3. **Price feeds**: No standard yet - see oracle integration patterns + +### Building Smart Wallets +1. **Passkey auth**: Use [CAP-0051](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0051.md) (secp256r1) +2. **Web auth**: Implement [SEP-0045](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0045.md) (Draft) +3. **Custom accounts**: Use [CAP-0046-11](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-11.md) auth framework + +### Contract Lifecycle +1. **Metadata**: Use [SEP-0046](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0046.md) `contractmeta!` +2. **Interface spec**: Auto-generated per [SEP-0048](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0048.md) +3. **Upgrades**: Follow [SEP-0049](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0049.md) guidelines +4. **Build verification**: Use [SEP-0055](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0055.md) (Draft) + +--- + +## Implementation Examples + +### SEP-0041 Token Interface +```rust +use soroban_sdk::{contract, contractimpl, Address, Env, String, Symbol}; + +/// SEP-0041 compliant token interface +pub trait TokenInterface { + // Admin + fn initialize(env: Env, admin: Address, decimal: u32, name: String, symbol: String); + + // Getters + fn name(env: Env) -> String; + fn symbol(env: Env) -> Symbol; + fn decimals(env: Env) -> u32; + fn balance(env: Env, id: Address) -> i128; + + // Transfers + fn transfer(env: Env, from: Address, to: Address, amount: i128); + fn transfer_from(env: Env, spender: Address, from: Address, to: Address, amount: i128); + + // Allowances + fn approve(env: Env, from: Address, spender: Address, amount: i128, expiration_ledger: u32); + fn allowance(env: Env, from: Address, spender: Address) -> i128; + + // Supply + fn total_supply(env: Env) -> i128; + + // Admin operations + fn mint(env: Env, to: Address, amount: i128); + fn burn(env: Env, from: Address, amount: i128); +} +``` + +### SEP-0046 Contract Metadata +```rust +use soroban_sdk::contractmeta; + +// Version tracking (SEP-0049 recommended) +contractmeta!(key = "binver", val = "1.0.0"); + +// Custom metadata +contractmeta!(key = "author", val = "MyTeam"); +contractmeta!(key = "repo", val = "https://github.com/myteam/mycontract"); +``` + +**CLI build with metadata:** +```bash +stellar contract build --meta binver=1.0.0 --meta author=MyTeam +``` + +### SEP-0048 Interface Inspection +```bash +# View contract interface from local WASM file +stellar contract info interface --wasm target/wasm32-unknown-unknown/release/my_contract.wasm + +# From deployed contract (by ID) +stellar contract info interface --contract-id CONTRACT_ID --network testnet + +# Output as JSON instead of Rust +stellar contract info interface --wasm my_contract.wasm --output json-formatted +``` + +--- + +## Protocol Version Quick Reference + +| Protocol | Key Features | Mainnet Date | +|----------|--------------|--------------| +| 20 | Soroban launch (smart contracts) | Feb 2024 | +| 21 | Secp256r1 (passkeys), TTL functions | Aug 2024 | +| 22 | Constructors, BLS12-381 | Nov 2024 | +| 23 | State optimization, unified events | Jan 2025 | +| 25 | BN254 + Poseidon (ZK) | Jan 2026 |