From 93d208a0cc1a36c78c681b941b63588c3e8832c3 Mon Sep 17 00:00:00 2001 From: Kaan Kacar Date: Thu, 11 Jun 2026 19:18:55 +0300 Subject: [PATCH 1/4] Split soroban skill, rebrand Soroban prose, expand zk-proofs with toolchain walkthroughs - Split skills/soroban/SKILL.md (2,578 lines) into a slim entry point plus development.md, testing.md, and security.md; condensed content that LLMs already know and replaced niche detail with doc links - Rebranded prose mentions of Soroban to Stellar smart contracts across skills, README, site, and plugin manifests; crate names, URLs, and official SEP/CAP/tool names are unchanged - Rewrote skills/zk-proofs/SKILL.md around concrete Circom, Noir, and RISC Zero walkthroughs grounded in the official groth16_verifier example, with a curve/proof-system support matrix - Site: copy-skills.mjs now mirrors whole skill directories so companion files are served, and llms.txt indexes them as nested entries; Soroban filter tab renamed to Smart Contracts --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- README.md | 14 +- site/CLAUDE.md | 2 +- site/scripts/copy-skills.mjs | 6 +- site/scripts/generate-llms-txt.mjs | 19 +- site/src/data/skills.ts | 14 +- skills/agentic-payments/SKILL.md | 26 +- skills/assets/SKILL.md | 22 +- skills/dapp/SKILL.md | 12 +- skills/data/SKILL.md | 10 +- skills/soroban/SKILL.md | 2533 +--------------------------- skills/soroban/development.md | 286 ++++ skills/soroban/security.md | 164 ++ skills/soroban/testing.md | 243 +++ skills/standards/SKILL.md | 62 +- skills/zk-proofs/SKILL.md | 353 ++-- 17 files changed, 1080 insertions(+), 2690 deletions(-) create mode 100644 skills/soroban/development.md create mode 100644 skills/soroban/security.md create mode 100644 skills/soroban/testing.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b927793..bc7cf7f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "stellar-dev", "source": "./", - "description": "Skills for Stellar and Soroban development" + "description": "Skills for modern Stellar development" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index a57bf08..76c412b 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "stellar-dev", - "description": "End-to-end Stellar development: Soroban smart contracts (Rust), stellar-sdk (JS/Python/Go), RPC/Horizon APIs, Stellar Assets, wallet integration, testing, security, and ecosystem", + "description": "End-to-end Stellar development: smart contracts (Rust, soroban-sdk), stellar-sdk (JS/Python/Go), RPC/Horizon APIs, Stellar Assets, wallet integration, testing, security, and ecosystem", "version": "1.1.1", "author": { "name": "Stellar Development Foundation" diff --git a/README.md b/README.md index 95b6508..9622aa0 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ Inspired by [solana-foundation/solana-dev-skill](https://github.com/solana-found This skill provides AI assistants with deep knowledge of the current Stellar development ecosystem: -- **Smart Contracts**: Soroban (Rust SDK, WebAssembly) +- **Smart Contracts**: Stellar smart contracts (Rust soroban-sdk, WebAssembly) - **Client SDKs**: stellar-sdk (JavaScript), Python, Go, Rust - **APIs**: Stellar RPC (preferred), Horizon (legacy) - **Assets**: Stellar Assets, Stellar Asset Contract (SAC) - **Wallets**: Freighter, Stellar Wallets Kit, Smart Accounts (passkeys) - **Testing**: Local Quickstart, Testnet, Unit tests -- **Security**: Soroban-specific patterns, audit checklists +- **Security**: Smart contract security patterns, audit checklists - **Ecosystem**: DeFi protocols, developer tools, community projects ## Installing @@ -65,24 +65,24 @@ Copy the `skills/` directory contents to your assistant's skills location. ``` skills/ -├── soroban/SKILL.md # Soroban contracts + testing + security + patterns + pitfalls +├── soroban/ # Stellar smart contracts — SKILL.md entry + development/testing/security files ├── dapp/SKILL.md # Frontend, wallets (Freighter, Wallets Kit), signing, smart accounts ├── assets/SKILL.md # Stellar Assets, trustlines, SAC bridge ├── data/SKILL.md # Stellar RPC (preferred) + Horizon (legacy), indexing ├── agentic-payments/SKILL.md # x402 + MPP (Charge + Channel) for AI/machine payments -├── zk-proofs/SKILL.md # ZK verification, BLS12-381, BN254/Poseidon (status-sensitive) +├── zk-proofs/SKILL.md # ZK verification (BLS12-381 Groth16), Circom/Noir/RISC Zero walkthroughs └── standards/SKILL.md # SEPs, CAPs, ecosystem projects, curated reference links ``` -Each sub-skill is a self-contained Agent Skill with its own frontmatter. Cross-references link related skills (e.g., the `agentic-payments` skill points to `soroban` for the Soroban SACs the protocols call, and to `assets` for USDC). The AI reads only the sub-skills relevant to the task at hand. +Each sub-skill is a self-contained Agent Skill with its own frontmatter. Cross-references link related skills (e.g., the `agentic-payments` skill points to `soroban` for the SACs the protocols call, and to `assets` for USDC). The AI reads only the sub-skills relevant to the task at hand. ## Example Prompts ``` -"Help me write a Soroban smart contract for a token" +"Help me write a Stellar smart contract for a token" "Set up a Next.js app with Freighter wallet connection" "How do I deploy a contract to Stellar Testnet?" -"Create unit tests for my Soroban contract" +"Create unit tests for my smart contract" "Review this contract for security issues" ``` diff --git a/site/CLAUDE.md b/site/CLAUDE.md index e6e05b1..1cd820e 100644 --- a/site/CLAUDE.md +++ b/site/CLAUDE.md @@ -69,7 +69,7 @@ then append to `SKILL_CARD_SOURCES`: ```ts { source: "skills//SKILL.md", - category: "Soroban", // any FilterType value + category: "Smart Contracts", // any FilterType value // Optional overrides — both default to the upstream SKILL.md's // first H1 (title) and frontmatter `description`. title: "Your Skill Title", diff --git a/site/scripts/copy-skills.mjs b/site/scripts/copy-skills.mjs index 947d5a7..31b3444 100644 --- a/site/scripts/copy-skills.mjs +++ b/site/scripts/copy-skills.mjs @@ -89,8 +89,12 @@ for (const source of sources) { missing.push(source); continue; } + // Copy the whole skill directory, not just the SKILL.md: skills may + // split deep-dive content into companion files (e.g. + // skills/soroban/development.md) referenced by relative links, and + // those must be served at the same paths. mkdirSync(dirname(dest), { recursive: true }); - cpSync(src, dest, { dereference: false }); + cpSync(dirname(src), dirname(dest), { recursive: true, dereference: false }); } // Apache-2.0 attribution alongside the content. diff --git a/site/scripts/generate-llms-txt.mjs b/site/scripts/generate-llms-txt.mjs index bd7afd0..3f1b84f 100644 --- a/site/scripts/generate-llms-txt.mjs +++ b/site/scripts/generate-llms-txt.mjs @@ -18,7 +18,7 @@ * this file). The page (src/app/page.tsx) and this script therefore * share one source of truth (no regex parsing, no second copy). */ -import { writeFileSync } from "node:fs"; +import { existsSync, readdirSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -82,10 +82,10 @@ lines.push(""); lines.push("Use Stellar Skills to help when your human asks things like:"); lines.push(""); for (const prompt of [ - "Help me write a Soroban smart contract for a token", + "Help me write a Stellar smart contract for a token", "Set up a Next.js app with Freighter wallet connection", "How do I deploy a contract to Stellar Testnet?", - "Create unit tests for my Soroban contract", + "Create unit tests for my smart contract", "Review this contract for security issues", ]) { lines.push(`- "${prompt}"`); @@ -125,6 +125,19 @@ for (const filter of filters) { for (const c of cards) { if (!c.path) continue; lines.push(`- [${c.title}](${ORIGIN}${c.path}): ${c.description}`); + // Companion markdown files in the same skill directory (e.g. + // skills/soroban/development.md) are part of the skill — index them + // as nested entries so agents can fetch them directly. + const dir = dirname(c.source); + const absDir = join(ROOT, "public", dir); + if (!existsSync(absDir)) continue; + const companions = readdirSync(absDir) + .filter((f) => f.endsWith(".md") && f !== "SKILL.md") + .sort(); + for (const f of companions) { + const meta = readSkillMeta(`${dir}/${f}`); + lines.push(` - [${meta.title ?? f}](${ORIGIN}/${dir}/${f})`); + } } lines.push(""); } diff --git a/site/src/data/skills.ts b/site/src/data/skills.ts index fcd5960..ac96e38 100644 --- a/site/src/data/skills.ts +++ b/site/src/data/skills.ts @@ -17,7 +17,7 @@ type FilterType = | "All" | "Agentic Payments" - | "Soroban" + | "Smart Contracts" | "Frontend" | "Assets" | "APIs" @@ -30,7 +30,7 @@ type FilterType = */ export const FILTERS: readonly FilterType[] = [ "All", - "Soroban", + "Smart Contracts", "Agentic Payments", "Frontend", "Assets", @@ -81,8 +81,8 @@ export type EcosystemCardSource = { export const SKILL_CARD_SOURCES: readonly SkillCardSource[] = [ { source: "skills/soroban/SKILL.md", - category: "Soroban", - title: "Soroban Smart Contracts", + category: "Smart Contracts", + title: "Stellar Smart Contracts", description: "Write, test, secure, and ship Rust smart contracts on Stellar. Covers patterns, pitfalls, and architecture.", }, @@ -98,7 +98,7 @@ export const SKILL_CARD_SOURCES: readonly SkillCardSource[] = [ category: "Assets", title: "Stellar Assets & SAC", description: - "Issue and manage classic Stellar assets and trustlines, with the SAC bridge for Soroban interop.", + "Issue and manage classic Stellar assets and trustlines, with the SAC bridge for smart contract interop.", }, { source: "skills/data/SKILL.md", @@ -119,7 +119,7 @@ export const SKILL_CARD_SOURCES: readonly SkillCardSource[] = [ category: "ZK", title: "ZK Proofs", description: - "Verify Groth16 zero-knowledge proofs on Stellar using BLS12-381, BN254, and Poseidon primitives.", + "Verify Groth16 proofs on-chain via BLS12-381, with Circom, Noir, and RISC Zero toolchain walkthroughs.", }, { source: "skills/standards/SKILL.md", @@ -138,7 +138,7 @@ export const ECOSYSTEM_CARDS: readonly EcosystemCardSource[] = [ { title: "OpenZeppelin Contracts", description: - "Scaffold a Soroban project with OpenZeppelin's audited Stellar contract libraries. Walks through Rust toolchain setup, Stellar CLI install, workspace dependencies, and applying the pausable and ownable macros to your contract.", + "Scaffold a Stellar smart contract project with OpenZeppelin's audited Stellar contract libraries. Walks through Rust toolchain setup, Stellar CLI install, workspace dependencies, and applying the pausable and ownable macros to your contract.", pathLabel: "OpenZeppelin/openzeppelin-skills", copyValue: "https://github.com/OpenZeppelin/openzeppelin-skills/blob/main/skills/setup-stellar-contracts/SKILL.md", diff --git a/skills/agentic-payments/SKILL.md b/skills/agentic-payments/SKILL.md index 782504b..1300009 100644 --- a/skills/agentic-payments/SKILL.md +++ b/skills/agentic-payments/SKILL.md @@ -1,6 +1,6 @@ --- name: agentic-payments -description: Agentic and machine-to-machine payments on Stellar. Covers x402 (HTTP 402 paid APIs via OZ Channels facilitator, fee-sponsored clients) and MPP (Machine Payments Protocol) in both Charge mode (per-request Soroban SAC) and Channel mode (off-chain commits, high-frequency). Defaults to USDC (SEP-41 SAC) on `stellar:testnet`/`stellar:pubnet` (CAIP-2). Use when selling a paid API to AI agents, building an x402 client, or designing a payment-channel architecture for high-frequency agent traffic. +description: Agentic and machine-to-machine payments on Stellar. Covers x402 (HTTP 402 paid APIs via OZ Channels facilitator, fee-sponsored clients) and MPP (Machine Payments Protocol) in both Charge mode (per-request SAC) and Channel mode (off-chain commits, high-frequency). Defaults to USDC (SEP-41 SAC) on `stellar:testnet`/`stellar:pubnet` (CAIP-2). Use when selling a paid API to AI agents, building an x402 client, or designing a payment-channel architecture for high-frequency agent traffic. user-invocable: true argument-hint: "[payment task]" --- @@ -13,7 +13,7 @@ Two complementary protocols for AI-agent and machine-to-machine payments on Stel | | x402 | MPP Charge | MPP Channel | |--|------|------------|-------------| -| Per-request on-chain tx? | Yes (via facilitator) | Yes (Soroban SAC) | No (off-chain commits) | +| Per-request on-chain tx? | Yes (via facilitator) | Yes (SAC) | No (off-chain commits) | | Needs facilitator? | Yes (OZ Channels) | No | No | | Client needs XLM? | No (fees sponsored) | Optional (`feePayer`) | Yes | | Setup complexity | Low | Low | Medium (deploy contract first) | @@ -28,7 +28,7 @@ Two complementary protocols for AI-agent and machine-to-machine payments on Stel All protocols use USDC (SEP-41 SAC) by default; `stellar:testnet` / `stellar:pubnet` CAIP-2 network IDs. ## Related skills -- The Soroban SACs the protocols call → `../soroban/SKILL.md` +- The SACs the protocols call → `../soroban/SKILL.md` - USDC and other classic assets → `../assets/SKILL.md` - Wallets and signing in the buyer client → `../dapp/SKILL.md` - RPC simulation / submission patterns → `../data/SKILL.md` @@ -52,7 +52,7 @@ Trade-off: you depend on OZ Channels (or a self-hosted relayer) for verification ``` Client → GET /resource → Server Client ← 402 Payment Required (payment requirements) ← Server -Client builds Soroban SAC USDC transfer +Client builds SAC USDC transfer Client signs auth entries only (not the full tx envelope) Client → GET /resource + X-PAYMENT header → Server Server → OZ Channels /verify + /settle → Stellar (~5s) @@ -172,7 +172,7 @@ console.log(await res.json()); - `STELLAR_NETWORK` — CAIP-2 network ID; defaults to `stellar:testnet`. Must match the server's network. - `STELLAR_SECRET_KEY` — your S... secret key (needs USDC trustline + balance) -**Browser frontends:** this client uses Node `fetch` and `createEd25519Signer`, both of which run in Node. A vanilla browser cannot sign Soroban auth entries through a typical wallet extension without additional glue. For a browser payer, run the x402 client server-side and expose a thin proxy endpoint to the page, or wire up Wallets-Kit / Freighter with custom auth-entry signing. +**Browser frontends:** this client uses Node `fetch` and `createEd25519Signer`, both of which run in Node. A vanilla browser cannot sign contract auth entries through a typical wallet extension without additional glue. For a browser payer, run the x402 client server-side and expose a thin proxy endpoint to the page, or wire up Wallets-Kit / Freighter with custom auth-entry signing. ## Testnet runbook @@ -288,7 +288,7 @@ USDC on Stellar has two addresses, used in different places. Mixing them up is a | Address | Format | Used for | |---------|--------|----------| | Classic asset issuer | `G...` (32-byte ed25519 public key) | The `issuer` of the classic USDC asset; used when adding a trustline (`new Asset("USDC", G...)`) | -| SAC (Soroban Asset Contract) | `C...` (32-byte contract address) | The Soroban contract the protocol invokes `transfer` on; used in payment requirements | +| SAC (Stellar Asset Contract) | `C...` (32-byte contract address) | The contract the protocol invokes `transfer` on; used in payment requirements | Use the exported constants instead of hard-coding when possible: @@ -315,13 +315,13 @@ Always test on testnet first. To switch a working setup to mainnet, change only ## Key concepts -**Auth entry signing** — On Stellar, x402 clients sign Soroban authorization entries, not full transaction envelopes. The facilitator assembles the complete transaction. This is lighter than EVM/Solana signing, and means clients never need to manage sequence numbers or pay fees. +**Auth entry signing** — On Stellar, x402 clients sign contract authorization entries, not full transaction envelopes. The facilitator assembles the complete transaction. This is lighter than EVM/Solana signing, and means clients never need to manage sequence numbers or pay fees. **Fee sponsorship** — OZ Channels pays all Stellar network fees (~$0.00001/tx). Clients need a funded wallet with USDC but zero XLM. **`exact-v2` scheme** — The Stellar x402 scheme version. Server advertises `scheme: "exact"` + `x402Version: 2`. Don't mix v1 and v2 packages. -**SAC (Stellar Asset Contract)** — USDC on Stellar is a classic asset wrapped in a Soroban contract. x402 payments invoke `transfer` on the SAC. Any SEP-41 token works; USDC is the default. +**SAC (Stellar Asset Contract)** — USDC on Stellar is a classic asset wrapped in a smart contract. x402 payments invoke `transfer` on the SAC. Any SEP-41 token works; USDC is the default. **Ledger expiration** — Auth entries include a `max_ledger` bound. Use `latestLedger + 12` (~1 minute at 5s/ledger). Expired entries fail at settlement. @@ -354,7 +354,7 @@ Always test on testnet first. To switch a working setup to mainnet, change only - Fix: the `payTo` account needs a USDC trustline too. The SAC `transfer` settles the underlying classic asset, which the recipient cannot hold without a trustline. Add `changeTrust` to both accounts during setup. **Trying to sign auth entries from a browser** -- Symptom: bundling errors, or a browser wallet that has no API to sign Soroban auth entries +- Symptom: bundling errors, or a browser wallet that has no API to sign contract auth entries - Fix: run the x402 client server-side (e.g. an Express route the browser calls), or use Wallets-Kit / Freighter with custom auth-entry signing. `@x402/fetch` + `createEd25519Signer` target Node and assume a raw secret key. **Passing a `Keypair` (or a network passphrase) to `createEd25519Signer`** @@ -374,7 +374,7 @@ Always test on testnet first. To switch a working setup to mainnet, change only ## When to use MPP MPP is the right choice when: -- You want **no facilitator dependency** — payments settle directly on Stellar via Soroban SAC transfers +- You want **no facilitator dependency** — payments settle directly on Stellar via SAC transfers - Your AI agent makes **many requests per session** — use channel mode to pay off-chain and settle once - You're building a Stellar-native payment stack without relying on third-party infrastructure @@ -389,7 +389,7 @@ If you need zero-XLM clients or the simplest possible setup, use x402 (Part 1 ab ## Charge mode: per-request payments -Each request triggers a Soroban SAC token transfer settled on-chain. No facilitator. Server can optionally sponsor fees so clients don't need XLM. +Each request triggers a SAC token transfer settled on-chain. No facilitator. Server can optionally sponsor fees so clients don't need XLM. ```bash npm install express @stellar/mpp mppx @stellar/stellar-sdk dotenv @@ -487,7 +487,7 @@ The client deploys a one-way payment channel contract, deposits USDC once, then ### Prerequisites -- Deploy a one-way-channel Soroban contract to get a `C...` contract address +- Deploy a one-way-channel smart contract to get a `C...` contract address - Generate an ed25519 keypair for commitment signing (see [stellar-mpp SDK](https://github.com/stellar/stellar-mpp-sdk)) - Fund the channel with USDC before making requests @@ -615,7 +615,7 @@ npm install @stellar/mpp mppx @stellar/stellar-sdk **Channel: deposit TTL expired** - Symptom: `close()` fails or channel appears drained -- Fix: Soroban contract storage has a TTL. Close the channel before it expires, or extend storage TTL via `bumpContractInstance`. Don't leave channels open indefinitely. +- Fix: Contract storage has a TTL. Close the channel before it expires, or extend storage TTL via `bumpContractInstance`. Don't leave channels open indefinitely. **Charge: client has no XLM for fees** - Symptom: `op_insufficient_balance` or fee errors on client-submitted transactions diff --git a/skills/assets/SKILL.md b/skills/assets/SKILL.md index 7a35d4e..b2c0691 100644 --- a/skills/assets/SKILL.md +++ b/skills/assets/SKILL.md @@ -1,19 +1,19 @@ --- name: assets -description: Stellar Assets (classic) + trustlines + Stellar Asset Contract (SAC) bridge to Soroban. Covers asset issuance, distribution, authorization flags, clawback, regulated assets, trustline management, and the SAC interop layer that exposes classic assets as Soroban tokens. Use when tokenizing real-world assets, issuing stablecoins, managing trustlines, or bridging classic assets to Soroban contracts. +description: Stellar Assets (classic) + trustlines + Stellar Asset Contract (SAC) bridge to smart contracts. Covers asset issuance, distribution, authorization flags, clawback, regulated assets, trustline management, and the SAC interop layer that exposes classic assets as SEP-41 contract tokens. Use when tokenizing real-world assets, issuing stablecoins, managing trustlines, or bridging classic assets to smart contracts. user-invocable: true argument-hint: "[asset task]" --- # Stellar Assets, Trustlines, and SAC -Stellar's native token mechanism: classic asset issuance, trustlines, and the Stellar Asset Contract (SAC) bridge that makes classic assets usable from Soroban. Default to classic assets over custom Soroban tokens unless you need custom logic. +Stellar's native token mechanism: classic asset issuance, trustlines, and the Stellar Asset Contract (SAC) bridge that makes classic assets usable from smart contracts. Default to classic assets over custom contract tokens unless you need custom logic. ## When to use this skill - Issuing a new asset (stablecoin, security token, utility token) - Setting up trustlines from a client or contract - Managing issuer flags (auth required, auth revocable, clawback) -- Bridging a classic asset into a Soroban contract via SAC +- Bridging a classic asset into a smart contract via SAC - Building regulated-asset flows (compliance, KYC, freeze) ## Related skills @@ -30,7 +30,7 @@ Stellar's native token mechanism: classic asset issuance, trustlines, and the St Stellar has two token mechanisms: 1. **Stellar Assets (Classic)**: Built-in, highly efficient, full ecosystem support -2. **Soroban Tokens**: Custom contracts with flexible logic +2. **Contract tokens (SEP-41)**: Custom contracts with flexible logic **Recommendation**: Prefer Stellar Assets unless you need custom token logic. @@ -258,7 +258,7 @@ if (trustline) { ## Stellar Asset Contract (SAC) -SAC provides Soroban interface for Stellar Assets, enabling smart contract interactions. +SAC provides a smart-contract interface for Stellar Assets, enabling smart contract interactions. ### Deploy SAC for Existing Asset @@ -280,7 +280,7 @@ const contractId = asset.contractId(StellarSdk.Networks.TESTNET); // Returns the deterministic SAC contract address ``` -### Using SAC in Soroban Contracts +### Using SAC in Smart Contracts ```rust use soroban_sdk::{token::Client as TokenClient, Address, Env}; @@ -302,7 +302,7 @@ pub fn transfer_asset( ### SAC vs Custom Token Interface -SAC implements the standard Soroban token interface: +SAC implements the standard SEP-41 token interface: - `balance(id: Address) -> i128` - `transfer(from: Address, to: Address, amount: i128)` - `approve(from: Address, spender: Address, amount: i128, expiration_ledger: u32)` @@ -320,7 +320,7 @@ SAC implements the standard Soroban token interface: - Performance critical (classic operations are cheaper) - DEX integration via order book -### Use Soroban Custom Tokens When: +### Use Custom Contract Tokens When: - Complex transfer logic (royalties, fees, restrictions) - Custom authorization schemes - Non-standard token behaviors @@ -328,7 +328,7 @@ SAC implements the standard Soroban token interface: - NFTs or semi-fungible tokens ### Use SAC When: -- Need Stellar Asset in Soroban contract +- Need a Stellar asset inside a smart contract - Building DeFi protocols with existing assets - Bridge between classic and smart contract operations @@ -417,11 +417,11 @@ For fiat on/off ramps: ### SEP-0045 (Web Auth for Contract Accounts) -Extends SEP-10 to support Soroban contract accounts (`C...` addresses) for web authentication. Required for smart wallet / passkey-based anchor integrations. See [SEP-0045](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0045.md). +Extends SEP-10 to support contract accounts (`C...` addresses) for web authentication. Required for smart wallet / passkey-based anchor integrations. See [SEP-0045](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0045.md). ### SEP-0050 (Non-Fungible Tokens) -Standard contract interface for NFTs on Soroban. Reference implementations available in [OpenZeppelin Stellar Contracts](https://github.com/OpenZeppelin/stellar-contracts) with Base, Consecutive, and Enumerable variants. See [SEP-0050](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0050.md). +Standard contract interface for NFTs on Stellar. Reference implementations available in [OpenZeppelin Stellar Contracts](https://github.com/OpenZeppelin/stellar-contracts) with Base, Consecutive, and Enumerable variants. See [SEP-0050](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0050.md). ## Best Practices diff --git a/skills/dapp/SKILL.md b/skills/dapp/SKILL.md index d67f712..e35bca3 100644 --- a/skills/dapp/SKILL.md +++ b/skills/dapp/SKILL.md @@ -1,18 +1,18 @@ --- name: dapp -description: Stellar dApp / frontend development. Covers the JavaScript stellar-sdk (browser + Node.js), Freighter wallet, Stellar Wallets Kit (multi-wallet), Wallet Standard, smart accounts with passkeys, transaction building / signing / submission, Soroban contract invocation from the client, simulation, and error handling. Use when building a React/Next.js/Node.js app that talks to Stellar or Soroban. +description: Stellar dApp / frontend development. Covers the JavaScript stellar-sdk (browser + Node.js), Freighter wallet, Stellar Wallets Kit (multi-wallet), Wallet Standard, smart accounts with passkeys, transaction building / signing / submission, smart contract invocation from the client, simulation, and error handling. Use when building a React/Next.js/Node.js app that talks to Stellar — classic operations or smart contracts. user-invocable: true argument-hint: "[dapp task]" --- # Stellar dApp / Frontend -Client-side development with `@stellar/stellar-sdk`, wallet connection, signing, and submitting transactions. Covers both classic Stellar operations and Soroban contract invocation from the browser or Node.js. +Client-side development with `@stellar/stellar-sdk`, wallet connection, signing, and submitting transactions. Covers both classic Stellar operations and smart contract invocation from the browser or Node.js. ## When to use this skill - Connecting Freighter or other wallets via Stellar Wallets Kit - Building, simulating, signing, and submitting transactions -- Invoking Soroban contracts from a frontend +- Invoking Stellar smart contracts from a frontend - Implementing smart accounts with passkeys - Handling network passphrases (Mainnet / Testnet / local) @@ -263,7 +263,7 @@ export async function buildPaymentTx( } ``` -### Soroban Contract Invocation +### Smart Contract Invocation ```typescript import * as StellarSdk from "@stellar/stellar-sdk"; import { rpc, config } from "@/lib/stellar"; @@ -342,7 +342,7 @@ export async function submitTransaction(signedXdr: string) { config.networkPassphrase ); - // For Soroban transactions, use RPC + // For smart contract transactions, use RPC if (transaction.operations.some(op => op.type === "invokeHostFunction")) { return submitSorobanTransaction(signedXdr); } @@ -652,7 +652,7 @@ const client = new RPChannels.ChannelsClient({ apiKey: "your-api-key", }); -// Submit a Soroban contract call with fee sponsorship +// Submit a smart contract call with fee sponsorship const response = await client.submitSorobanTransaction({ func: contractFunc, auth: contractAuth, diff --git a/skills/data/SKILL.md b/skills/data/SKILL.md index 84ebcf2..cf2e665 100644 --- a/skills/data/SKILL.md +++ b/skills/data/SKILL.md @@ -18,7 +18,7 @@ API access for reading chain state. Stellar RPC is the preferred entry point for ## Related skills - Building transactions to send → `../dapp/SKILL.md` -- Soroban contract simulation and event emission → `../soroban/SKILL.md` +- Smart contract simulation and event emission → `../soroban/SKILL.md` - Asset balance and trustline lookups → `../assets/SKILL.md` - Standards (SEP-7 deeplinks, SEP-10 auth) → `../standards/SKILL.md` @@ -31,7 +31,7 @@ Stellar provides two API paradigms: | API | Status | Use Case | |-----|--------|----------| -| **Stellar RPC** | Preferred | Soroban, real-time state, new projects | +| **Stellar RPC** | Preferred | Smart contracts, real-time state, new projects | | **Horizon** | Legacy-focused | Historical data, legacy applications | **Recommendation**: Use Stellar RPC for all new projects. Use Horizon mainly for historical queries and legacy compatibility paths. @@ -347,7 +347,7 @@ const account = await rpc.getAccount(publicKey); // Horizon (for classic transactions) const result = await horizonServer.submitTransaction(tx); -// RPC (for Soroban transactions) +// RPC (for smart contract transactions) const response = await rpc.sendTransaction(tx); const result = await pollForResult(response.hash); ``` @@ -419,7 +419,7 @@ RPC "Infinite Scroll" is powered by the Stellar data lake — a cloud-based obje For complex queries, event streaming, or custom data pipelines beyond what RPC/Horizon provide: - **Mercury** — Stellar-native indexer with Retroshades, GraphQL API (https://mercurydata.app) -- **SubQuery** — Multi-chain indexer with Stellar/Soroban support, event handlers (https://subquery.network) +- **SubQuery** — Multi-chain indexer with Stellar support, event handlers (https://subquery.network) - **Goldsky** — Real-time data replication pipelines and subgraphs (https://goldsky.com) - **StellarExpert API** — Free, no-auth REST API for assets, accounts, ledger resolution (https://stellar.expert/openapi.html) @@ -481,7 +481,7 @@ export const horizon = new StellarSdk.Horizon.Server(config.horizonUrl); ### Use RPC for: - New application development -- Soroban contract interactions +- Smart contract interactions - Transaction simulation and submission - Real-time account state diff --git a/skills/soroban/SKILL.md b/skills/soroban/SKILL.md index 3c98cb8..b7038d2 100644 --- a/skills/soroban/SKILL.md +++ b/skills/soroban/SKILL.md @@ -1,97 +1,56 @@ --- name: soroban -description: Soroban smart contract development on Stellar (Rust SDK). Covers project setup, contract structure, storage types, authorization, cross-contract calls, events, error handling, testing (unit, integration, fuzz, property, mutation, fork, differential), security patterns and vulnerability classes, advanced architecture patterns (upgrades, factories, governance, DeFi primitives), and common pitfalls. Use when writing, testing, securing, or shipping Soroban contracts. +description: Stellar smart contract development (Rust, soroban-sdk). Entry point with project setup, contract anatomy, and build/deploy workflow, routing to three companion files in this directory — development.md (storage, auth, cross-contract calls, events, errors, upgrades, factories, troubleshooting), testing.md (unit, fuzz, property, fork, mutation, integration), and security.md (vulnerability classes, checklists, tooling, audits). Use when writing, testing, securing, or shipping Stellar smart contracts (formerly branded Soroban). user-invocable: true argument-hint: "[contract task]" --- -# Soroban Smart Contracts +# Stellar Smart Contracts -End-to-end guide for building Soroban contracts: writing them, testing them, securing them, and shipping advanced architectures. This skill bundles five concerns that live and die together — the contract code, the tests, the security posture, the design patterns, and the gotchas. +Guide for building Stellar smart contracts in Rust. Smart contracts on Stellar were formerly branded "Soroban" — the platform name is retired, but the Rust SDK (`soroban-sdk`) and several tool names keep the prefix. + +This file covers setup and the core workflow. The deep dives live alongside it — **read the file that matches the task**: + +| Task | File | +|------|------| +| Storage, auth, cross-contract calls, events, errors, upgrades, factories, troubleshooting | [development.md](development.md) | +| Unit, integration, fuzz, property, fork, and mutation testing | [testing.md](testing.md) | +| Security review, vulnerability classes, checklists, audit prep, tooling | [security.md](security.md) | ## When to use this skill -- Writing a Soroban contract in Rust -- Setting up unit, integration, fuzz, or property tests -- Reviewing a contract for security issues (authorization, reentrancy-adjacent bugs, storage hygiene, TTL, overflow) +- Writing a Stellar smart contract in Rust +- Setting up contract tests (any layer) +- Reviewing a contract for security issues - Architecting upgradeable contracts, factories, governance, or DeFi primitives -- Debugging a Soroban-specific error (auth, storage, archival, resource limits) +- Debugging a contract-specific error (auth, storage, archival, resource limits) ## Related skills - Assets, trustlines, and SAC bridge → `../assets/SKILL.md` - Frontend/wallets that call your contract → `../dapp/SKILL.md` - Chain data queries (RPC/Horizon) → `../data/SKILL.md` -- ZK cryptography (BLS12-381, BN254, Poseidon) → `../zk-proofs/SKILL.md` +- ZK verification (BLS12-381, Groth16, Circom/Noir/RISC Zero) → `../zk-proofs/SKILL.md` - SEP/CAP standards and ecosystem links → `../standards/SKILL.md` ---- - -# Part 1: Contract Development - - -## When to use Soroban -Use Soroban when you need: -- Custom on-chain logic beyond Stellar's built-in operations -- Programmable escrow, lending, or DeFi primitives -- Complex authorization rules -- State management beyond account balances -- Interoperability with Stellar Assets via SAC - -## Quick Navigation -- Initialization and constructors: [Project Setup](#project-setup), [Contract Constructors (Protocol 22+)](#contract-constructors-protocol-22) -- Core implementation patterns: [Core Contract Structure](#core-contract-structure), [Storage Types](#storage-types), [Authorization](#authorization) -- Advanced interactions: [Cross-Contract Calls](#cross-contract-calls), [Events](#events), [Error Handling](#error-handling) -- Delivery workflow: [Building and Deploying](#building-and-deploying), [Unit Testing](#unit-testing), [Best Practices](#best-practices) -- ZK status guidance: [Zero-Knowledge Cryptography (Status-Sensitive)](#zero-knowledge-cryptography-status-sensitive) - -## Alternative Languages +## Platform constraints -Rust is the primary and recommended language for Soroban contracts. Community-maintained alternatives exist but are not recommended for production: -- **AssemblyScript**: [`as-soroban-sdk`](https://github.com/Soneso/as-soroban-sdk) by Soneso — allows TypeScript-like syntax, officially listed on Stellar docs, but may lag behind the latest protocol version -- **Solidity**: [Hyperledger Solang](https://github.com/hyperledger-solang/solang) — SDF-funded, compiles Solidity to Soroban WASM, currently **pre-alpha** ([docs](https://developers.stellar.org/docs/learn/migrate/evm/solidity-support-via-solang)) +Contracts are Rust compiled to WebAssembly, run in a sandboxed host: -## Architecture Overview +- `#![no_std]` required — use `soroban_sdk` types (`String`, `Vec`, `Map`, `Symbol`), not the Rust standard library +- 64KB compiled contract size limit — use the release profile below +- `Symbol` is limited to 32 characters; `symbol_short!()` covers up to 9 +- Storage is rented: every entry has a TTL and can be archived — see [development.md](development.md#storage) +- No `delegatecall`, no classical cross-contract reentrancy — see [security.md](security.md) -### Host-Guest Model -Soroban uses a WebAssembly sandbox with strict separation: -- **Host Environment**: Provides storage, crypto, cross-contract calls -- **Guest Contract**: Your Rust code compiled to WASM -- Contracts reference host objects via handles (not direct memory) +## Project setup -### Key Constraints -- `#![no_std]` required - no Rust standard library -- 64KB contract size limit (use release optimizations) -- Limited heap allocation -- No string type (use `String` from soroban-sdk or `Symbol` for short strings) -- `Symbol` limited to 32 characters (was 10 in earlier versions) - -## Project Setup - -### Initialize a new contract ```bash -stellar contract init my-contract +stellar contract init my-contract # scaffolds a Cargo workspace with contracts/ cd my-contract ``` -This creates: -``` -my-contract/ -├── Cargo.toml -├── src/ -│ └── lib.rs -└── contracts/ - └── hello_world/ - ├── Cargo.toml - └── src/ - └── lib.rs -``` +`Cargo.toml` essentials: -### Cargo.toml configuration ```toml -[package] -name = "my-contract" -version = "0.1.0" -edition = "2021" - [lib] crate-type = ["cdylib"] @@ -110,90 +69,28 @@ debug-assertions = false panic = "abort" codegen-units = 1 lto = true - -[profile.release-with-logs] -inherits = "release" -debug-assertions = true ``` -## Contract Constructors (Protocol 22+) +## Contract anatomy -Use constructors for atomic initialization when protocol support is available. This avoids a separate `initialize` transaction and reduces front-running risk. +One compact example showing state, constructor, auth, TTL, and a typed error: -### Constructor pattern ```rust #![no_std] -use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; +use soroban_sdk::{contract, contracterror, contractimpl, contracttype, Address, Env}; #[contracttype] #[derive(Clone)] pub enum DataKey { Admin, - Value, -} - -#[contract] -pub struct MyContract; - -#[contractimpl] -impl MyContract { - // Runs once at deployment time. - 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); - } -} -``` - -### Deploy with constructor args (CLI) -```bash -stellar contract deploy \ - --wasm target/wasm32-unknown-unknown/release/my_contract.wasm \ - --source alice \ - --network testnet \ - -- \ - --admin alice \ - --initial_value 100 -``` - -### Rules -1. Name must be `__constructor` exactly. -2. Constructor returns `()` (no return value). -3. Runs only at creation time and does not run on upgrade. -4. If constructor fails, deployment fails atomically. - -### Backwards compatibility -If targeting older protocol environments, use guarded `initialize` patterns and prevent re-initialization explicitly. - -## Core Contract Structure - -### Basic Contract -```rust -#![no_std] -use soroban_sdk::{contract, contractimpl, symbol_short, vec, Env, Symbol, Vec}; - -#[contract] -pub struct HelloContract; - -#[contractimpl] -impl HelloContract { - pub fn hello(env: Env, to: Symbol) -> Vec { - vec![&env, symbol_short!("Hello"), to] - } + Counter, } -``` -### Contract with State -```rust -#![no_std] -use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; - -#[contracttype] -#[derive(Clone)] -pub enum DataKey { - Counter, - Admin, - UserBalance(Address), +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum Error { + NotInitialized = 1, } #[contract] @@ -201,23 +98,29 @@ pub struct CounterContract; #[contractimpl] impl CounterContract { - pub fn initialize(env: Env, admin: Address) { - if env.storage().instance().has(&DataKey::Admin) { - panic!("already initialized"); - } + // Runs once, atomically, at deploy time (Protocol 22+). Must be named + // `__constructor` and return (). Does not run again on upgrade. + pub fn __constructor(env: Env, admin: Address) { env.storage().instance().set(&DataKey::Admin, &admin); env.storage().instance().set(&DataKey::Counter, &0u32); } - pub fn increment(env: Env) -> u32 { - let mut count: u32 = env.storage().instance().get(&DataKey::Counter).unwrap_or(0); - count += 1; + pub fn increment(env: Env) -> Result { + let admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .ok_or(Error::NotInitialized)?; + admin.require_auth(); + + let count: u32 = env.storage().instance().get(&DataKey::Counter).unwrap_or(0); + let count = count + 1; env.storage().instance().set(&DataKey::Counter, &count); - // Extend TTL to prevent archival - env.storage().instance().extend_ttl(100, 518400); // threshold, ~30 days + // Extend TTL so contract state is not archived (threshold, extend-to) + env.storage().instance().extend_ttl(100, 518400); - count + Ok(count) } pub fn get_count(env: Env) -> u32 { @@ -226,267 +129,26 @@ impl CounterContract { } ``` -## Storage Types - -Soroban has three storage types with different costs and lifetimes: - -### Instance Storage -- Tied to contract instance lifetime -- Shared across all users -- Best for: admin addresses, global config, counters -```rust -env.storage().instance().set(&key, &value); -env.storage().instance().get(&key); -env.storage().instance().extend_ttl(min_ttl, extend_to); -``` - -### Persistent Storage -- Survives archival (can be restored) -- Per-key TTL management -- Best for: user balances, important state -```rust -env.storage().persistent().set(&key, &value); -env.storage().persistent().get(&key); -env.storage().persistent().extend_ttl(&key, min_ttl, extend_to); -``` - -### Temporary Storage -- Cheapest, automatically deleted when TTL expires -- Cannot be restored after archival -- Best for: caches, temporary flags, session data -```rust -env.storage().temporary().set(&key, &value); -env.storage().temporary().get(&key); -env.storage().temporary().extend_ttl(&key, min_ttl, extend_to); -``` - -### TTL Management -```rust -// Check remaining TTL -let ttl = env.storage().persistent().get_ttl(&key); - -// Extend if below threshold -const MIN_TTL: u32 = 17280; // ~1 day at 5s ledgers -const EXTEND_TO: u32 = 518400; // ~30 days - -if ttl < MIN_TTL { - env.storage().persistent().extend_ttl(&key, MIN_TTL, EXTEND_TO); -} -``` - -## Data Types - -### Primitive Types -```rust -use soroban_sdk::{Address, Bytes, BytesN, Map, String, Symbol, Vec, I128, U256}; - -// Address - account or contract identifier -let addr: Address = env.current_contract_address(); - -// Symbol - short strings (max 32 chars) -let sym: Symbol = symbol_short!("transfer"); - -// String - longer strings -let s: String = String::from_str(&env, "Hello, Stellar!"); - -// Fixed-size bytes -let hash: BytesN<32> = env.crypto().sha256(&bytes); - -// Collections -let v: Vec = vec![&env, 1, 2, 3]; -let m: Map = Map::new(&env); -``` - -### Custom Types -```rust -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct TokenMetadata { - pub name: String, - pub symbol: Symbol, - pub decimals: u32, -} - -#[contracttype] -#[derive(Clone)] -pub enum DataKey { - Admin, - Balance(Address), - Allowance(Address, Address), // (owner, spender) -} -``` - -## Authorization - -### Requiring Authorization -```rust -#[contractimpl] -impl TokenContract { - pub fn transfer(env: Env, from: Address, to: Address, amount: i128) { - // Require 'from' to authorize this call - from.require_auth(); - - // Or require auth for specific arguments - from.require_auth_for_args((&to, amount).into_val(&env)); - - // Transfer logic... - } -} -``` - -### Admin Patterns -```rust -fn require_admin(env: &Env) { - let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); - admin.require_auth(); -} - -pub fn set_admin(env: Env, new_admin: Address) { - require_admin(&env); - env.storage().instance().set(&DataKey::Admin, &new_admin); -} -``` - -## Cross-Contract Calls - -### Calling Another Contract -```rust -use soroban_sdk::{contract, contractimpl, Address, Env}; - -mod token_contract { - soroban_sdk::contractimport!( - file = "../token/target/wasm32-unknown-unknown/release/token.wasm" - ); -} - -#[contract] -pub struct VaultContract; - -#[contractimpl] -impl VaultContract { - pub fn deposit(env: Env, user: Address, token: Address, amount: i128) { - user.require_auth(); - - // Create client for token contract - let token_client = token_contract::Client::new(&env, &token); - - // Call transfer on token contract - token_client.transfer(&user, &env.current_contract_address(), &amount); - - // Update vault state... - } -} -``` - -### Using Stellar Asset Contract (SAC) -```rust -use soroban_sdk::token::Client as TokenClient; - -pub fn transfer_asset(env: Env, from: Address, to: Address, asset: Address, amount: i128) { - from.require_auth(); - - let token = TokenClient::new(&env, &asset); - token.transfer(&from, &to, &amount); -} -``` - -## Events - -### Emitting Events -```rust -use soroban_sdk::{contract, contractevent, contractimpl, Address, Env}; - -#[contractevent(topics = ["transfer"])] -pub struct TransferEvent { - pub from: Address, - pub to: Address, - pub amount: i128, -} - -#[contract] -pub struct TokenContract; - -#[contractimpl] -impl TokenContract { - pub fn transfer(env: Env, from: Address, to: Address, amount: i128) { - // ... transfer logic ... - - // Emit event - TransferEvent { from, to, amount }.publish(&env); - } -} -``` - -## Error Handling +Full patterns (three storage types, auth variants, cross-contract calls, events, custom types): [development.md](development.md). -### Custom Errors -```rust -use soroban_sdk::contracterror; - -#[contracterror] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -#[repr(u32)] -pub enum ContractError { - AlreadyInitialized = 1, - NotInitialized = 2, - InsufficientBalance = 3, - Unauthorized = 4, - InvalidAmount = 5, -} - -// Usage -pub fn transfer(env: Env, from: Address, to: Address, amount: i128) -> Result<(), ContractError> { - if amount <= 0 { - return Err(ContractError::InvalidAmount); - } +## Build, deploy, invoke - let balance: i128 = get_balance(&env, &from); - if balance < amount { - return Err(ContractError::InsufficientBalance); - } - - // ... transfer logic ... - Ok(()) -} -``` - -## Building and Deploying - -### Build Contract ```bash -# Build optimized WASM +# Build optimized WASM → target/wasm32-unknown-unknown/release/*.wasm stellar contract build -# Output: target/wasm32-unknown-unknown/release/my_contract.wasm -``` - -### Deploy to Testnet -```bash -# Generate and fund a new identity +# Create and fund an identity (testnet) stellar keys generate --global alice --network testnet --fund -# Deploy contract +# Deploy (constructor args go after the `--`) stellar contract deploy \ --wasm target/wasm32-unknown-unknown/release/my_contract.wasm \ --source alice \ - --network testnet - -# Returns: CONTRACT_ID (starts with 'C') -``` - -### Initialize Contract -```bash -stellar contract invoke \ - --id CONTRACT_ID \ - --source alice \ --network testnet \ -- \ - initialize \ --admin alice -``` -### Invoke Functions -```bash +# Invoke stellar contract invoke \ --id CONTRACT_ID \ --source alice \ @@ -495,2086 +157,37 @@ stellar contract invoke \ increment ``` -## Unit Testing - -```rust -#![cfg(test)] - -use super::*; -use soroban_sdk::testutils::Address as _; -use soroban_sdk::Env; - -#[test] -fn test_increment() { - let env = Env::default(); - let contract_id = env.register(CounterContract, ()); - let client = CounterContractClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - client.initialize(&admin); - - assert_eq!(client.get_count(), 0); - assert_eq!(client.increment(), 1); - assert_eq!(client.increment(), 2); - assert_eq!(client.get_count(), 2); -} - -#[test] -fn test_transfer_with_auth() { - let env = Env::default(); - env.mock_all_auths(); // Auto-approve all auth requests - - let contract_id = env.register(TokenContract, ()); - let client = TokenContractClient::new(&env, &contract_id); - - let alice = Address::generate(&env); - let bob = Address::generate(&env); - - // Mint tokens to alice - client.mint(&alice, &1000); - - // Transfer from alice to bob - client.transfer(&alice, &bob, &100); - - assert_eq!(client.balance(&alice), 900); - assert_eq!(client.balance(&bob), 100); -} -``` - -## Best Practices - -### Contract Size Optimization -- Use `symbol_short!()` for symbols under 9 chars (more efficient) -- Avoid unnecessary string operations -- Use appropriate storage type for data lifetime -- Consider splitting large contracts - -### Storage Efficiency -- Use compact data structures -- Clean up temporary storage -- Batch storage operations when possible -- Manage TTLs proactively to avoid archival - -### Security -- Always validate inputs -- Use `require_auth()` for sensitive operations -- Check contract ownership in initialization -- Prevent reinitialization attacks -- Validate cross-contract call targets - -### Gas/Resource Optimization -- Minimize storage reads/writes -- Use events for data that doesn't need on-chain queries -- Batch operations where possible -- Profile resource usage with `stellar contract invoke --sim` - -## Zero-Knowledge Cryptography (Status-Sensitive) - -Stellar's ZK cryptography capabilities are evolving. Treat availability as protocol- and network-dependent. - -- [CAP-0059](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0059.md): BLS12-381 primitives -- [CAP-0074](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0074.md): BN254 host functions (proposed) -- [CAP-0075](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0075.md): Poseidon/Poseidon2 host functions (proposed) - -Before implementation, always verify: -1. CAP status in the CAP preamble (`Accepted`/`Implemented` vs draft/awaiting decision) -2. Target network software version and protocol support -3. `soroban-sdk` release support for the target host functions - -### Practical guidance -- Use BLS12-381 features where supported and documented in your target SDK/network. -- For BN254/Poseidon plans, design feature flags and graceful fallbacks until support is active. -- Keep cryptographic assumptions explicit in audits and deployment notes. - -### Example references -- [Groth16 Verifier](https://github.com/stellar/soroban-examples/tree/main/groth16_verifier) -- [Soroban examples repository](https://github.com/stellar/soroban-examples) - -> See [zk-proofs.md](../zk-proofs/SKILL.md) for Groth16 verification patterns, Poseidon usage, Noir/RISC Zero integration, and implementation guidance. - ---- - -# Part 2: Testing Strategy - - -## Quick Navigation -- Strategy overview: [Testing Pyramid](#testing-pyramid) -- Core test layers: [Unit Testing with Soroban SDK](#unit-testing-with-soroban-sdk), [Local Testing with Stellar Quickstart](#local-testing-with-stellar-quickstart), [Testnet Testing](#testnet-testing) -- Integration and CI: [Integration Testing Patterns](#integration-testing-patterns), [Test Configuration](#test-configuration), [CI/CD Configuration](#cicd-configuration) -- Advanced testing: [Fuzz Testing](#fuzz-testing), [Property-Based Testing](#property-based-testing), [Differential Testing with Test Snapshots](#differential-testing-with-test-snapshots), [Fork Testing](#fork-testing), [Mutation Testing](#mutation-testing) -- Performance and readiness: [Resource Profiling](#resource-profiling), [Best Practices](#best-practices) - -## Testing Pyramid - -1. **Unit tests (fast)**: Native Rust tests with `soroban-sdk` testutils -2. **Local integration tests**: Stellar Quickstart Docker -3. **Testnet tests**: Deploy and test on public testnet -4. **Mainnet smoke tests**: Final validation before production - -## Unit Testing with Soroban SDK - -The Soroban SDK provides comprehensive testing utilities that run natively (not in WASM), enabling fast iteration with full debugging support. +To upload WASM without instantiating (e.g. for factories or upgrades), use `stellar contract upload` (the older `stellar contract install` is a deprecated alias). -### Basic Test Setup +## Minimal test ```rust #![cfg(test)] - +use super::*; use soroban_sdk::{testutils::Address as _, Address, Env}; -// Import your contract -use crate::{Contract, ContractClient}; - -#[test] -fn test_basic_functionality() { - // Create test environment - let env = Env::default(); - - // Register contract - let contract_id = env.register(Contract, ()); - - // Create typed client - let client = ContractClient::new(&env, &contract_id); - - // Generate test addresses - let user = Address::generate(&env); - - // Call contract functions - client.initialize(&user); - - // Assert results - assert_eq!(client.get_value(), 0); -} -``` - -### Testing Authorization - -```rust #[test] -fn test_with_auth() { +fn test_increment() { let env = Env::default(); - - // Mock all authorizations automatically env.mock_all_auths(); - - let contract_id = env.register(TokenContract, ()); - let client = TokenContractClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let user1 = Address::generate(&env); - let user2 = Address::generate(&env); - - // Initialize and mint - client.initialize(&admin); - client.mint(&user1, &1000); - - // Transfer (requires auth from user1) - client.transfer(&user1, &user2, &100); - - assert_eq!(client.balance(&user1), 900); - assert_eq!(client.balance(&user2), 100); - - // Verify which auths were required - let auths = env.auths(); - assert_eq!(auths.len(), 1); - // auths[0] contains (address, contract_id, function, args) -} -``` - -### Testing with Specific Auth Requirements - -```rust -#[test] -fn test_specific_auth() { - let env = Env::default(); - let contract_id = env.register(Contract, ()); - let client = ContractClient::new(&env, &contract_id); - - let user = Address::generate(&env); - - // Mock auth only for specific address - env.mock_auths(&[MockAuth { - address: &user, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "transfer", - args: (&user, &other, &100i128).into_val(&env), - sub_invokes: &[], - }, - }]); + let contract_id = env.register(CounterContract, (admin.clone(),)); + let client = CounterContractClient::new(&env, &contract_id); - client.transfer(&user, &other, &100); + assert_eq!(client.increment(), 1); + assert_eq!(client.get_count(), 1); } ``` -### Testing Time-Dependent Logic - -```rust -#[test] -fn test_time_based() { - let env = Env::default(); - let contract_id = env.register(VestingContract, ()); - let client = VestingContractClient::new(&env, &contract_id); - - let beneficiary = Address::generate(&env); - - // Set initial timestamp - env.ledger().set_timestamp(1000); - - client.create_vesting(&beneficiary, &1000, &2000); // unlock at t=2000 - - // Try to claim before unlock - assert!(client.try_claim(&beneficiary).is_err()); - - // Advance time past unlock - env.ledger().set_timestamp(2500); - - // Now claim succeeds - client.claim(&beneficiary); -} -``` - -### Testing Ledger State - -```rust -#[test] -fn test_ledger_manipulation() { - let env = Env::default(); - - // Set ledger sequence - env.ledger().set_sequence_number(1000); - - // Set timestamp - env.ledger().set_timestamp(1704067200); // Jan 1, 2024 - - // Set network passphrase - env.ledger().set_network_id([0u8; 32]); // Custom network ID - - // Get current values - let seq = env.ledger().sequence(); - let ts = env.ledger().timestamp(); -} -``` - -### Testing Events - -```rust -#[test] -fn test_events() { - let env = Env::default(); - let contract_id = env.register(Contract, ()); - let client = ContractClient::new(&env, &contract_id); - - client.do_something(); - - // Get all events - let events = env.events().all(); - - // Check specific event - assert_eq!(events.len(), 1); - - let event = &events[0]; - // event.0 = contract_id - // event.1 = topics (Vec) - // event.2 = data (Val) -} -``` - -### Testing Storage - -```rust -#[test] -fn test_storage_ttl() { - let env = Env::default(); - let contract_id = env.register(Contract, ()); - let client = ContractClient::new(&env, &contract_id); - - client.store_data(); - - // Check TTL - let key = DataKey::MyData; - let ttl = env.as_contract(&contract_id, || { - env.storage().persistent().get_ttl(&key) - }); - - assert!(ttl > 0); -} -``` - -### Testing Cross-Contract Calls - -```rust -#[test] -fn test_cross_contract() { - let env = Env::default(); - - // Register both contracts - let token_id = env.register(token::WASM, ()); - let vault_id = env.register(VaultContract, ()); - - let token_client = token::Client::new(&env, &token_id); - let vault_client = VaultContractClient::new(&env, &vault_id); - - env.mock_all_auths(); - - let user = Address::generate(&env); - - // Setup: mint tokens to user - token_client.mint(&user, &1000); - - // Test: deposit tokens into vault - vault_client.deposit(&user, &token_id, &500); - - assert_eq!(token_client.balance(&user), 500); - assert_eq!(vault_client.balance(&user), 500); -} -``` - -## Local Testing with Stellar Quickstart - -### Start Local Network - -```bash -# Pull and run Stellar Quickstart -docker run --rm -it -p 8000:8000 \ - --name stellar \ - stellar/quickstart:latest \ - --local \ - --enable-soroban-rpc - -# Or use Stellar CLI -stellar container start local -``` - -### Configure for Local Network - -```typescript -import * as StellarSdk from "@stellar/stellar-sdk"; - -const LOCAL_RPC = "http://localhost:8000/soroban/rpc"; -const LOCAL_HORIZON = "http://localhost:8000"; -const LOCAL_PASSPHRASE = "Standalone Network ; February 2017"; - -const rpc = new StellarSdk.rpc.Server(LOCAL_RPC); -const horizon = new StellarSdk.Horizon.Server(LOCAL_HORIZON); -``` - -### Fund Test Accounts (Local) - -```bash -# Using Stellar CLI -stellar keys generate --global test-account --network local --fund - -# Or via friendbot endpoint -curl "http://localhost:8000/friendbot?addr=G..." -``` - -### Deploy and Test Locally - -```bash -# Deploy contract to local network -stellar contract deploy \ - --wasm target/wasm32-unknown-unknown/release/contract.wasm \ - --source test-account \ - --network local - -# Invoke contract -stellar contract invoke \ - --id CONTRACT_ID \ - --source test-account \ - --network local \ - -- \ - function_name \ - --arg value -``` - -## Testnet Testing - -### Network Configuration - -```bash -# Testnet RPC: https://soroban-testnet.stellar.org -# Testnet Horizon: https://horizon-testnet.stellar.org -# Network Passphrase: "Test SDF Network ; September 2015" -# Friendbot: https://friendbot.stellar.org -``` - -### Create and Fund Testnet Account - -```bash -# Generate new identity -stellar keys generate --global my-testnet-key --network testnet - -# Fund via Friendbot -stellar keys fund my-testnet-key --network testnet - -# Or manually -curl "https://friendbot.stellar.org?addr=G..." -``` - -### Deploy to Testnet - -```bash -# Deploy contract -stellar contract deploy \ - --wasm target/wasm32-unknown-unknown/release/contract.wasm \ - --source my-testnet-key \ - --network testnet - -# Upload contract code (separate from deployment) -stellar contract upload \ - --wasm target/wasm32-unknown-unknown/release/contract.wasm \ - --source my-testnet-key \ - --network testnet -``` - -### Testnet Reset Awareness - -**Important**: Testnet resets approximately quarterly: -- All accounts and contracts are deleted -- Plan for re-deployment after resets -- Don't rely on persistent state for test data - -Check reset schedule: https://stellar.org/developers/blog - -## Integration Testing Patterns - -### TypeScript Integration Tests - -```typescript -// tests/integration/contract.test.ts -import * as StellarSdk from "@stellar/stellar-sdk"; - -const RPC_URL = process.env.RPC_URL || "http://localhost:8000/soroban/rpc"; -const NETWORK_PASSPHRASE = process.env.NETWORK_PASSPHRASE || "Standalone Network ; February 2017"; - -describe("Contract Integration Tests", () => { - let rpc: StellarSdk.rpc.Server; - let keypair: StellarSdk.Keypair; - let contractId: string; - - beforeAll(async () => { - rpc = new StellarSdk.rpc.Server(RPC_URL); - keypair = StellarSdk.Keypair.random(); - - // Fund account - await fundAccount(keypair.publicKey()); - - // Deploy contract - contractId = await deployContract(keypair); - }); - - test("should initialize contract", async () => { - const account = await rpc.getAccount(keypair.publicKey()); - const contract = new StellarSdk.Contract(contractId); - - const tx = new StellarSdk.TransactionBuilder(account, { - fee: "100", - networkPassphrase: NETWORK_PASSPHRASE, - }) - .addOperation( - contract.call( - "initialize", - StellarSdk.Address.fromString(keypair.publicKey()).toScVal() - ) - ) - .setTimeout(30) - .build(); - - const simResult = await rpc.simulateTransaction(tx); - const preparedTx = StellarSdk.rpc.assembleTransaction(tx, simResult); - - preparedTx.sign(keypair); - const result = await rpc.sendTransaction(preparedTx.build()); - - expect(result.status).not.toBe("ERROR"); - }); -}); -``` - -### Rust Integration Tests - -```rust -// tests/integration_test.rs -use soroban_sdk::{Env, Address}; -use std::process::Command; - -#[test] -#[ignore] // Run with: cargo test -- --ignored -fn integration_test_with_local_network() { - // Requires local network running - let output = Command::new("stellar") - .args([ - "contract", "invoke", - "--id", "CONTRACT_ID", - "--source", "test-account", - "--network", "local", - "--", - "get_count" - ]) - .output() - .expect("Failed to invoke contract"); - - assert!(output.status.success()); -} -``` - -## Test Configuration - -### Cargo.toml for Tests - -```toml -[dev-dependencies] -soroban-sdk = { version = "25.0.1", features = ["testutils"] } # match [dependencies] version - -[profile.test] -opt-level = 0 -debug = true -``` - -### Running Tests - -```bash -# Run unit tests -cargo test - -# Run with output -cargo test -- --nocapture - -# Run specific test -cargo test test_transfer - -# Run ignored (integration) tests -cargo test -- --ignored -``` - -## CI/CD Configuration - -### GitHub Actions Example - -```yaml -name: Test Soroban Contract - -on: [push, pull_request] - -jobs: - unit-tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - - name: Add WASM target - run: rustup target add wasm32-unknown-unknown - - - name: Run unit tests - run: cargo test - - - name: Build contract - run: cargo build --release --target wasm32-unknown-unknown - - integration-tests: - runs-on: ubuntu-latest - needs: unit-tests - services: - stellar: - image: stellar/quickstart:latest - ports: - - 8000:8000 - options: >- - --health-cmd "curl -f http://localhost:8000 || exit 1" - --health-interval 10s - --health-timeout 5s - --health-retries 10 - - steps: - - uses: actions/checkout@v4 - - - name: Install Stellar CLI - run: | - cargo install stellar-cli --locked - - - name: Deploy and test - run: | - stellar keys generate --global ci-test --network local --fund - stellar contract deploy \ - --wasm target/wasm32-unknown-unknown/release/contract.wasm \ - --source ci-test \ - --network local -``` - -## Best Practices - -### Test Organization -``` -project/ -├── src/ -│ └── lib.rs -├── tests/ -│ ├── common/ -│ │ └── mod.rs # Shared test utilities -│ ├── unit/ -│ │ ├── mod.rs -│ │ └── transfer.rs -│ └── integration/ -│ └── full_flow.rs -└── Cargo.toml -``` - -### Test Utilities Module - -```rust -// tests/common/mod.rs -use soroban_sdk::{testutils::Address as _, Address, Env}; -use crate::{Contract, ContractClient}; - -pub fn setup_contract(env: &Env) -> (Address, ContractClient) { - let contract_id = env.register(Contract, ()); - let client = ContractClient::new(env, &contract_id); - let admin = Address::generate(env); - - env.mock_all_auths(); - client.initialize(&admin); - - (contract_id, client) -} - -pub fn create_funded_user(env: &Env, client: &ContractClient, amount: i128) -> Address { - let user = Address::generate(env); - client.mint(&user, &amount); - user -} -``` - -## Fuzz Testing - -Soroban has first-class fuzz testing via `cargo-fuzz` and the built-in `SorobanArbitrary` trait. All `#[contracttype]` types automatically derive `SorobanArbitrary` when the `"testutils"` feature is active. - -### Setup - -```bash -# Install nightly Rust + cargo-fuzz -rustup install nightly -cargo install --locked cargo-fuzz - -# Initialize fuzz targets -cargo fuzz init -``` - -Update `Cargo.toml` to include both crate types: -```toml -[lib] -crate-type = ["lib", "cdylib"] -``` - -Add to `fuzz/Cargo.toml`: -```toml -[dependencies] -soroban-sdk = { version = "25.0.1", features = ["testutils"] } -``` - -### Writing a Fuzz Target - -```rust -// fuzz/fuzz_targets/fuzz_deposit.rs -#![no_main] - -use libfuzzer_sys::fuzz_target; -use soroban_sdk::{testutils::Address as _, Address, Env}; -use my_contract::{Contract, ContractClient}; - -fuzz_target!(|input: (u64, i128)| { - let (seed, amount) = input; - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register(Contract, ()); - let client = ContractClient::new(&env, &contract_id); - let user = Address::generate(&env); - - // Initialize - client.initialize(&user); - - // Fuzz deposit — should never panic unexpectedly - let _ = client.try_deposit(&user, &amount); -}); -``` - -### Running Fuzz Tests - -```bash -# Run (use --sanitizer=thread on macOS) -cargo +nightly fuzz run fuzz_deposit - -# Generate code coverage -cargo +nightly fuzz coverage fuzz_deposit -``` - -### Soroban Token Fuzzer - -Reusable library for fuzzing token contracts: -- **GitHub**: https://github.com/brson/soroban-token-fuzzer - -### Documentation - -- [Stellar Fuzzing Guide](https://developers.stellar.org/docs/build/guides/testing/fuzzing) -- [Fuzzing Example Contract](https://developers.stellar.org/docs/build/smart-contracts/example-contracts/fuzzing) - -## Property-Based Testing - -Use `proptest` with `SorobanArbitrary` for QuickCheck-style property testing that runs in standard `cargo test`. - -```rust -#[cfg(test)] -mod prop_tests { - use super::*; - use proptest::prelude::*; - use soroban_sdk::{testutils::Address as _, Env}; - - proptest! { - #[test] - fn deposit_then_withdraw_preserves_balance(amount in 1i128..=i128::MAX) { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register(Contract, ()); - let client = ContractClient::new(&env, &contract_id); - let user = Address::generate(&env); - - client.initialize(&user); - client.deposit(&user, &amount); - client.withdraw(&user, &amount); - - prop_assert_eq!(client.balance(&user), 0); - } - } -} -``` - -**Recommended workflow**: Use `cargo-fuzz` interactively to find deep bugs, then convert to `proptest` for regression prevention in CI. - -## Differential Testing with Test Snapshots - -Soroban automatically writes JSON snapshots at the end of every test to `test_snapshots/`, capturing events and final ledger state. Commit these to source control — diffs reveal unintended behavioral changes. - -### Comparing Against Deployed Contracts - -```rust -// Fetch deployed contract for comparison -// $ stellar contract fetch --id C... --out-file deployed.wasm - -mod deployed { - soroban_sdk::contractimport!(file = "deployed.wasm"); -} - -#[test] -fn test_upgrade_compatibility() { - let env = Env::default(); - env.mock_all_auths(); - - // Register both versions - let old_id = env.register(deployed::WASM, ()); - let new_id = env.register(NewContract, ()); - - let old_client = deployed::Client::new(&env, &old_id); - let new_client = NewContractClient::new(&env, &new_id); - - let user = Address::generate(&env); - - // Run identical operations and compare - old_client.initialize(&user); - new_client.initialize(&user); - - assert_eq!(old_client.get_value(), new_client.get_value()); -} -``` - -- **Docs**: [Differential Tests with Test Snapshots](https://developers.stellar.org/docs/build/guides/testing/differential-tests-with-test-snapshots) - -## Fork Testing - -Test against real production state using ledger snapshots: - -```bash -# Create snapshot of deployed contract -stellar snapshot create --address C... --output json --out snapshot.json - -# Optionally at a specific ledger -stellar snapshot create --address C... --ledger 12345678 --output json --out snapshot.json -``` - -```rust -#[test] -fn test_against_mainnet_state() { - let env = Env::from_ledger_snapshot_file("snapshot.json"); - env.mock_all_auths(); - - let contract_id = /* contract address from snapshot */; - let client = ContractClient::new(&env, &contract_id); - - // Test operations against real state - let result = client.get_value(); - assert!(result > 0); -} -``` - -- **Docs**: [Fork Testing](https://developers.stellar.org/docs/build/guides/testing/fork-testing) - -## Mutation Testing - -Use `cargo-mutants` to verify test quality — modifies source code and checks that tests catch the changes. - -```bash -cargo install --locked cargo-mutants -cargo mutants -``` - -**Output interpretation**: -- **CAUGHT**: Tests detected the mutation (good coverage) -- **MISSED**: Tests passed despite mutation (test gap — review `mutants.out/diff/`) - -- **Docs**: [Mutation Testing](https://developers.stellar.org/docs/build/guides/testing/mutation-testing) - -## Resource Profiling - -Soroban uses a multidimensional resource model (CPU instructions, ledger reads/writes, bytes, events, rent). - -### CLI Simulation - -```bash -# Simulate contract invocation to see resource costs -stellar contract invoke \ - --id CONTRACT_ID \ - --source alice \ - --network testnet \ - --sim-only \ - -- \ - function_name --arg value -``` - -### Stellar Plus Profiler (Cheesecake Labs) - -```typescript -import { StellarPlus } from 'stellar-plus'; - -const profilerPlugin = new StellarPlus.Utils.Plugins.sorobanTransaction.profiler(); -// Collects CPU instructions, RAM, ledger reads/writes -// Aggregation: sum, average, standard deviation -// Output: CSV, formatted text tables -``` - -- **Docs**: https://docs.cheesecakelabs.com/stellar-plus/reference/utils/plugins/profiler-plugin - -### Testing Checklist - -- [ ] Unit tests cover all public functions -- [ ] Edge cases tested (zero amounts, max values, empty state) -- [ ] Authorization tested (correct signers required) -- [ ] Error conditions tested (invalid inputs, unauthorized) -- [ ] Events emission verified -- [ ] Storage TTL behavior validated -- [ ] Cross-contract interactions tested -- [ ] Fuzz tests for critical paths (deposits, withdrawals, swaps) -- [ ] Property-based tests for invariants -- [ ] Mutation testing confirms test quality -- [ ] Differential test snapshots committed to source control -- [ ] Integration tests against local network -- [ ] Testnet deployment verified before mainnet - ---- - -# Part 3: Security - - -## Core Principle - -Assume the attacker controls: -- All arguments passed to contract functions -- Transaction ordering and timing -- All accounts except those requiring signatures -- The ability to create contracts that mimic your interface - -## Soroban Security Advantages - -Soroban's architecture prevents certain vulnerability classes by design: - -### No Delegate Call -Unlike Ethereum, Soroban has no `delegatecall` equivalent. Contracts cannot execute arbitrary bytecode in their context, eliminating proxy-based attacks. - -### No Classical Reentrancy -Soroban's synchronous execution model prevents the cross-contract reentrancy that plagues Ethereum. Self-reentrancy is possible but rarely exploitable. - -### Explicit Authorization -Authorization is opt-in via `require_auth()`, making it explicit which operations need signatures. - ---- - -## Vulnerability Categories - -### 1. Missing Authorization Checks - -**Risk**: Anyone can call privileged functions without proper verification. - -**Attack**: Attacker calls admin-only functions, drains funds, or modifies critical state. - -**Vulnerable Code**: -```rust -// BAD: No authorization check -pub fn withdraw(env: Env, to: Address, amount: i128) { - transfer_tokens(&env, &to, amount); -} -``` - -**Secure Code**: -```rust -// GOOD: Requires authorization from admin -pub fn withdraw(env: Env, to: Address, amount: i128) { - let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); - admin.require_auth(); - transfer_tokens(&env, &to, amount); -} -``` - -**Prevention**: Always use `require_auth()` on the caller or an admin address. See Part 1: Contract Development above for full authorization patterns (direct auth, admin helpers, `require_auth_for_args`). - ---- - -### 2. Reinitialization Attacks - -**Risk**: Initialization function can be called multiple times, allowing attacker to overwrite admin or critical state. - -**Attack**: Attacker reinitializes contract to become the admin, then drains assets. - -**Vulnerable Code**: -```rust -// BAD: Can be called multiple times -pub fn initialize(env: Env, admin: Address) { - env.storage().instance().set(&DataKey::Admin, &admin); -} -``` - -**Secure Code**: -```rust -// GOOD: Prevents reinitialization -pub fn initialize(env: Env, admin: Address) { - if env.storage().instance().has(&DataKey::Initialized) { - panic!("already initialized"); - } - env.storage().instance().set(&DataKey::Admin, &admin); - env.storage().instance().set(&DataKey::Initialized, &true); -} - -// Alternative: Check for admin existence -pub fn initialize(env: Env, admin: Address) { - if env.storage().instance().has(&DataKey::Admin) { - panic!("already initialized"); - } - env.storage().instance().set(&DataKey::Admin, &admin); -} -``` - ---- - -### 3. Arbitrary Contract Calls - -**Risk**: Contract calls whatever address is passed as parameter without validation. - -**Attack**: Attacker passes malicious contract that mimics expected interface but behaves differently. - -**Vulnerable Code**: -```rust -// BAD: Calls any contract passed as parameter -pub fn swap(env: Env, token: Address, amount: i128) { - let client = token::Client::new(&env, &token); - client.transfer(...); // Could be malicious contract -} -``` - -**Secure Code**: -```rust -// GOOD: Validate against known allowlist -pub fn swap(env: Env, token: Address, amount: i128) { - let allowed_tokens: Vec
= env.storage() - .instance() - .get(&DataKey::AllowedTokens) - .unwrap(); - - if !allowed_tokens.contains(&token) { - panic!("token not allowed"); - } - - let client = token::Client::new(&env, &token); - client.transfer(...); -} - -// Or validate against Stellar Asset Contract -pub fn swap_sac(env: Env, asset: Address, amount: i128) { - // SACs have known, predictable addresses - // Verify it's a legitimate SAC if needed -} -``` - ---- - -### 4. Integer Overflow/Underflow - -**Risk**: Arithmetic operations overflow or underflow, causing unexpected values. - -**Attack**: Attacker manipulates amounts to cause overflow, bypassing balance checks. - -**Vulnerable Code**: -```rust -// BAD: Unchecked arithmetic -pub fn deposit(env: Env, user: Address, amount: i128) { - let balance: i128 = get_balance(&env, &user); - set_balance(&env, &user, balance + amount); // Can overflow -} -``` - -**Secure Code**: -```rust -// GOOD: Use checked arithmetic -pub fn deposit(env: Env, user: Address, amount: i128) { - let balance: i128 = get_balance(&env, &user); - let new_balance = balance.checked_add(amount) - .expect("overflow"); - set_balance(&env, &user, new_balance); -} - -// Also validate inputs -pub fn deposit(env: Env, user: Address, amount: i128) { - if amount <= 0 { - panic!("invalid amount"); - } - // ... rest of logic -} -``` - ---- - -### 5. Storage Key Collisions - -**Risk**: Different data types share the same storage key, causing data corruption. - -**Attack**: Attacker manipulates one type of data to corrupt another. - -**Vulnerable Code**: -```rust -// BAD: Same prefix for different data -env.storage().persistent().set(&symbol_short!("data"), &user_balance); -env.storage().persistent().set(&symbol_short!("data"), &config); // Overwrites! -``` - -**Secure Code**: -```rust -// GOOD: Use typed enum for keys -#[contracttype] -#[derive(Clone)] -pub enum DataKey { - Admin, - Balance(Address), - Config, - Allowance(Address, Address), -} - -env.storage().persistent().set(&DataKey::Balance(user), &balance); -env.storage().instance().set(&DataKey::Config, &config); -``` - ---- - -### 6. Timing/State Race Conditions - -**Risk**: Contract state changes between check and use. - -**Attack**: In multi-transaction scenarios, attacker exploits gap between validation and action. - -**Prevention**: -```rust -// Use atomic operations where possible -pub fn swap(env: Env, user: Address, amount_in: i128, min_out: i128) { - user.require_auth(); - - // Perform all checks and state changes atomically - let balance = get_balance(&env, &user); - if balance < amount_in { - panic!("insufficient balance"); - } - - let amount_out = calculate_output(amount_in); - if amount_out < min_out { - panic!("slippage exceeded"); - } - - // Update all state together - set_balance(&env, &user, balance - amount_in); - transfer_output(&env, &user, amount_out); -} -``` - ---- - -### 7. TTL/Archival Vulnerabilities - -**Risk**: Critical contract data gets archived, breaking functionality. - -**Attack**: Attacker waits for data to be archived, then exploits the missing state. - -**Prevention**: -```rust -// Extend TTL for critical data -pub fn critical_operation(env: Env) { - // Always extend instance storage - env.storage().instance().extend_ttl( - 100, // threshold - 518400, // extend_to (~30 days) - ); - - // Extend specific persistent keys - env.storage().persistent().extend_ttl( - &DataKey::CriticalData, - 100, - 518400, - ); -} - -// Consider restoration costs in design -// Archived data can be restored, but requires transaction -``` - ---- - -### 8. Cross-Contract Call Validation - -**Risk**: Trusting return values from untrusted contracts. - -**Attack**: Malicious contract returns false data, causing incorrect state updates. - -**Prevention**: -```rust -// Validate all external data -pub fn process_oracle_price(env: Env, oracle: Address, asset: Address) -> i128 { - // Validate oracle is trusted - let trusted_oracles: Vec
= env.storage() - .instance() - .get(&DataKey::TrustedOracles) - .unwrap(); - - if !trusted_oracles.contains(&oracle) { - panic!("untrusted oracle"); - } - - let price: i128 = oracle_client.get_price(&asset); - - // Sanity check the value - if price <= 0 || price > MAX_REASONABLE_PRICE { - panic!("invalid price"); - } - - price -} -``` - ---- - -## Classic Stellar Security - -### Trustline Attacks - -**Risk**: Users create trustlines to malicious assets that look legitimate. - -**Prevention**: -- Verify asset issuer before creating trustlines -- Use well-known asset lists (stellar.toml) -- Display full asset code + issuer in UIs - -### Clawback Awareness - -**Risk**: Assets with clawback enabled can be seized by issuer. - -**Prevention**: -```typescript -// Check if clawback is enabled -const issuerAccount = await server.loadAccount(asset.issuer); -const clawbackEnabled = issuerAccount.flags.auth_clawback_enabled; - -if (clawbackEnabled) { - // Warn user or reject asset -} -``` - -### Account Merge Attacks - -**Risk**: Merged accounts can be recreated with different configuration. - -**Prevention**: -- Validate account state before critical operations -- Don't cache account data long-term - ---- - -## Soroban-Specific Checklist - -### Contract Development -- [ ] All privileged functions require appropriate authorization -- [ ] Initialization can only happen once -- [ ] External contract calls are validated against allowlists -- [ ] All arithmetic uses checked operations -- [ ] Storage keys are typed and collision-free -- [ ] Critical data TTLs are extended proactively -- [ ] Input validation on all public functions -- [ ] Events emitted for auditable state changes - -### Storage Security -- [ ] Sensitive data uses appropriate storage type -- [ ] Instance storage for shared/admin data -- [ ] Persistent storage for user-specific data -- [ ] Temporary storage only for truly temporary data -- [ ] TTL management strategy documented - -### Cross-Contract Calls -- [ ] Called contracts are validated or allowlisted -- [ ] Return values are sanity-checked -- [ ] Failure cases handled gracefully -- [ ] No excessive trust in external state - ---- - -## Client-Side Checklist - -- [ ] Network passphrase validated before signing -- [ ] Transaction simulation before submission -- [ ] Clear display of all operation details -- [ ] Confirmation for high-value transactions -- [ ] Handle all error states gracefully -- [ ] Don't trust client-side validation alone -- [ ] Verify contract addresses against known deployments -- [ ] Check asset trustline status before transfers - ---- - -## Security Review Questions - -1. Can anyone call admin functions without authorization? -2. Can the contract be reinitialized? -3. Are all external contract calls validated? -4. Is arithmetic safe from overflow/underflow? -5. Can storage keys collide? -6. Will critical data survive archival? -7. Are cross-contract return values validated? -8. Can timing attacks exploit state changes? - ---- - -## Bug Bounty Programs - -### Immunefi — Stellar Core (up to $250K) -- **URL**: https://immunefi.com/bug-bounty/stellar/ -- **Scope**: stellar-core, rs-soroban-sdk, rs-soroban-env, soroban-tools (CLI + RPC), js-soroban-client, rs-stellar-xdr, wasmi fork -- **Rewards**: Critical $50K–$250K, High $10K–$50K, Medium $5K, Low $1K -- **Payment**: USD-denominated, paid in XLM. KYC required. -- **Rules**: PoC required. Test on local forks only (no mainnet/testnet). - -### Immunefi — OpenZeppelin on Stellar (up to $25K) -- **URL**: https://immunefi.com/bug-bounty/openzeppelin-stellar/ -- **Scope**: OpenZeppelin Stellar Contracts library -- **Max payout**: $25K per bug, $250K total program cap - -### HackerOne — Web Applications -- **URL**: https://stellar.org/grants-and-funding/bug-bounty -- **Scope**: SDF web applications, production servers, domains -- **Disclosure**: 90-day remediation window before public disclosure - -## Soroban Audit Bank - -SDF's proactive security program with **$3M+ deployed across 43+ audits**. - -- **URL**: https://stellar.org/grants-and-funding/soroban-audit-bank -- **Projects list**: https://stellar.org/audit-bank/projects -- **Eligibility**: SCF-funded projects (financial protocols, infrastructure, high-traction dApps) -- **Co-payment**: 5% upfront (refundable if Critical/High/Medium issues remediated within 20 business days) -- **Follow-up audits**: Triggered at $10M and $100M TVL milestones (includes formal verification and competitive audits) -- **Preparation**: STRIDE threat modeling framework + Audit Readiness Checklist - -### Partner Audit Firms - -| Firm | Specialty | -|------|-----------| -| **OtterSec** | Smart contract audits | -| **Veridise** | Tool-assisted audits, [security checklist](https://veridise.com/blog/audit-insights/building-on-stellar-soroban-grab-this-security-checklist-to-avoid-vulnerabilities/) | -| **Runtime Verification** | Formal methods, [Komet tool](https://runtimeverification.com/blog/introducing-komet-smart-contract-testing-and-verification-tool-for-soroban-created-by-runtime-verification) | -| **CoinFabrik** | Static analysis (Scout), manual audits | -| **QuarksLab** | Security research | -| **Coinspect** | Security audits | -| **Certora** | Formal verification ([Sunbeam Prover](https://docs.certora.com/en/latest/docs/sunbeam/index.html)) | -| **Halborn** | Security assessments | -| **Zellic** | Blockchain + cryptography research | -| **Code4rena** | Competitive audit platform | - -## Security Tooling - -### Static Analysis - -#### Scout Soroban (CoinFabrik) -Open-source vulnerability detector with 23 detectors (critical through enhancement severity). -- **GitHub**: https://github.com/CoinFabrik/scout-soroban -- **Install**: `cargo install cargo-scout-audit` → `cargo scout-audit` -- **Output formats**: HTML, Markdown, JSON, PDF, SARIF (CI/CD integration) -- **VSCode extension**: [Scout Audit](https://marketplace.visualstudio.com/items?itemName=CoinFabrik.scout-audit) -- **Key detectors**: `overflow-check`, `unprotected-update-current-contract-wasm`, `set-contract-storage`, `unrestricted-transfer-from`, `divide-before-multiply`, `dos-unbounded-operation`, `unsafe-unwrap` - -#### OpenZeppelin Security Detectors SDK -Framework for building custom security detectors for Soroban. -- **GitHub**: https://github.com/OpenZeppelin/soroban-security-detectors-sdk -- **Architecture**: `sdk` (core), `detectors` (pre-built), `soroban-scanner` (CLI) -- **Pre-built detectors**: `auth_missing`, `unchecked_ft_transfer`, improper TTL extension, contract panics, unsafe temporary storage -- **Extensible**: Load external detector libraries as shared objects - -### Formal Verification - -#### Certora Sunbeam Prover -Purpose-built formal verification for Soroban — first WASM platform supported by Certora. -- **Docs**: https://docs.certora.com/en/latest/docs/sunbeam/index.html -- **Spec language**: CVLR (Certora Verification Language for Rust) — Rust macros (`cvlr_assert!`, `cvlr_assume!`, `cvlr_satisfy!`) -- **Operates at**: WASM bytecode level (eliminates compiler trust assumptions) -- **Reports**: https://github.com/Certora/SecurityReports -- **Example**: [Blend V1 verification report](https://www.certora.com/reports/blend-smart-contract-verification-report) - -#### Runtime Verification — Komet -Formal verification and testing tool built specifically for Soroban (SCF-funded). -- **Docs**: https://docs.runtimeverification.com/komet -- **Repo**: https://github.com/runtimeverification/komet -- **Spec language**: Rust — property-based tests written in the same language as Soroban contracts -- **Operates at**: WASM bytecode level via [KWasm semantics](https://github.com/runtimeverification/wasm-semantics) (eliminates compiler trust assumptions) -- **Features**: Fuzzing, testing, formal verification -- **Reports**: [RV publications](https://github.com/runtimeverification/publications) -- **Example**: [TokenOps audit and verification report](https://github.com/runtimeverification/publications/blob/main/reports/smart-contracts/TokenOps.pdf) -- **Blog**: [Introducing Komet](https://runtimeverification.com/blog/introducing-komet-smart-contract-testing-and-verification-tool-for-soroban-created-by-runtime-verification) - -### Security Knowledge Base - -#### Soroban Security Portal -Community security knowledge base by Inferara (SCF-funded). -- **URL**: https://sorobansecurity.com -- **Features**: Searchable audit reports, vulnerability database, best practices - -### Security Monitoring (Post-Deployment) - -#### OpenZeppelin Monitor (Stellar alpha) -Open-source contract monitoring with Stellar support. -- **Features**: Self-hosted via Docker, Prometheus + Grafana observability -- **Source**: https://www.openzeppelin.com/news/monitor-and-relayers-are-now-open-source - -## OpenZeppelin Partnership Overview - -Strategic partnership highlights include: -- **40 Auditor Weeks** of dedicated security audits -- **Stellar Contracts library** (audited, production-ready) -- **Relayer** (fee-sponsored transactions, Stellar-native) -- **Monitor** (contract monitoring, Stellar alpha) -- **Security Detectors SDK** (static analysis framework) -- **SEP authorship**: SEP-0049 (Upgradeable Contracts), SEP-0050 (NFTs) -- **Blog**: https://stellar.org/blog/foundation-news/sdf-partners-with-openzeppelin-to-enhance-stellar-smart-contract-development - ---- - -# Part 4: Advanced Patterns - - -## When to use this guide -Use this guide for higher-complexity contract architecture: -- Upgrades and migrations -- Factory/deployer systems -- Governance and timelocks -- DeFi primitives (vaults, pools, oracles) -- Regulated token/compliance workflows -- Resource and storage optimization - -For core contract syntax and day-to-day patterns, refer to the earlier sections in this guide covering contract structure, storage, authorization, cross-contract calls, events, error handling, and testing. - -## Design principles -- Prefer simple state machines over implicit behavior. -- Minimize privileged entrypoints and protect all privileged actions with explicit auth. -- Keep upgrades predictable: version metadata + migration plan + rollback strategy. -- Use idempotent migrations and fail fast on incompatible versions. -- Separate protocol/business logic from governance/admin logic when possible. - -## Upgradeability patterns - -### 1) Explicit upgrade policy -- Decide early whether the contract is mutable or immutable. -- If mutable, implement an `upgrade` entrypoint guarded by admin or governance. -- If immutable, do not expose upgrade capability. - -### 2) Version tracking -Track both runtime and code version: -- Contract metadata (`contractmeta!`) for binary version -- Storage key for migration/application version - -```rust -#![no_std] -use soroban_sdk::{contract, contractimpl, contractmeta, contracttype, Address, BytesN, Env}; - -contractmeta!(key = "binver", val = "1.0.0"); - -#[contracttype] -#[derive(Clone)] -pub enum DataKey { - Admin, - AppVersion, -} - -#[contract] -pub struct Upgradeable; - -#[contractimpl] -impl Upgradeable { - pub fn __constructor(env: Env, admin: Address) { - env.storage().instance().set(&DataKey::Admin, &admin); - env.storage().instance().set(&DataKey::AppVersion, &1u32); - } - - pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) { - let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); - admin.require_auth(); - env.deployer().update_current_contract_wasm(new_wasm_hash); - } -} -``` - -### 3) Migration entrypoint -- Add a dedicated `migrate` function after upgrades. -- Ensure migration is monotonic (`new_version > current_version`). -- Treat migrations as one-way and idempotent. - -## Factory and deployment patterns - -### Factory contract responsibilities -- Authorize who can deploy instances. -- Derive deterministic addresses with salts when needed. -- Emit events for deployments (indexing/ops observability). -- Keep deployment logic separate from instance business logic. - -```rust -#![no_std] -use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, Val, Vec}; - -#[contract] -pub struct Factory; - -#[contractimpl] -impl Factory { - pub fn deploy( - env: Env, - owner: Address, - wasm_hash: BytesN<32>, - salt: BytesN<32>, - constructor_args: Vec, - ) -> Address { - owner.require_auth(); - env.deployer() - .with_address(env.current_contract_address(), salt) - .deploy_v2(wasm_hash, constructor_args) - } -} -``` - -Operational note: -- Keep a registry (or emit canonical deployment events) to avoid orphaned instances. - -## Governance patterns - -### Timelock for sensitive actions -Use a timelock for upgrades and major config changes: -- `propose_*` stores pending action + execute ledger -- `execute_*` enforces delay -- `cancel_*` allows governance abort - -### Multisig and role separation -- Separate roles: proposer, approver, executor. -- Define threshold and signer rotation process. -- Record proposal state in persistent storage and prevent replay. - -Checklist: -- Proposal uniqueness and replay protection -- Expiry semantics -- Clear cancellation path -- Explicit event emission - -## DeFi primitives - -### Vaults -- Track `total_assets` and `total_shares` with careful rounding rules. -- Use conservative math for mint/redeem conversions. -- Enforce pause/emergency controls for admin-level intervention. - -### Pools/AMMs -- Define invariant and fee accounting precisely. -- Protect against stale pricing and manipulation. -- Include slippage checks on all user-facing swaps. - -### Oracle integration -- Require freshness constraints (ledger/time bounds). -- Prefer median/multi-source feeds for critical operations. -- Add circuit breakers for extreme price movement. - -## Compliance-oriented token design - -Common regulated features: -- Allowlist/denylist checks before transfer -- Jurisdiction or investor-class restrictions -- Forced transfer/freeze authority with auditable governance -- Off-chain identity references (never store sensitive PII directly) - -Implementation guidance: -- Keep compliance policy in dedicated modules/entrypoints. -- Emit policy decision events for traceability. -- Treat privileged compliance actions as high-risk operations requiring strong auth. - -## Resource optimization - -### Storage -- Use `instance` for global config. -- Use `persistent` for critical user state. -- Use `temporary` only for disposable data. -- Extend TTL strategically, not on every call. - -### Compute -- Avoid unbounded loops over user-controlled collections. -- Prefer bounded batch operations. -- Reduce cross-contract calls in hot paths. - -### Contract size -- Keep release profile optimized (`opt-level = "z"`, `lto = true`, `panic = "abort"`). -- Split concerns across contracts when near Wasm size limits. - -## Security review checklist for advanced architectures -- Access control is explicit on every privileged path. -- Upgrade and migration are both tested (happy path + failure path). -- Timelock and governance logic is replay-safe. -- External dependency assumptions are documented. -- Emergency controls and incident runbooks are defined. -- Events cover operationally important transitions. - -## Testing strategy for advanced patterns -- Unit tests for role checks, invariants, and edge-case math. -- Integration tests for multi-step governance flows. -- Upgrade tests from old state snapshots to new versions. -- Negative tests for unauthorized and malformed calls. - ---- - -# Part 5: Common Pitfalls - - -## Soroban Contract Issues - -### 1. Contract Size Exceeds 64KB Limit - -**Problem**: Contract won't deploy because WASM exceeds size limit. - -**Symptoms**: -``` -Error: contract exceeds maximum size -``` - -**Solutions**: - -```toml -# Cargo.toml - Use aggressive optimization -[profile.release] -opt-level = "z" # Optimize for size -lto = true # Link-time optimization -codegen-units = 1 # Single codegen unit -panic = "abort" # Smaller panic handling -strip = "symbols" # Remove symbols -``` - -Additional strategies: -- Split large contracts into multiple smaller contracts -- Use `symbol_short!()` for symbols under 9 chars -- Avoid large static data in contract -- Remove debug code and unused functions -- Use `cargo bloat` to identify large dependencies - -```bash -# Check contract size -ls -la target/wasm32-unknown-unknown/release/*.wasm - -# Analyze what's taking space -cargo install cargo-bloat -cargo bloat --release --target wasm32-unknown-unknown -``` - ---- - -### 2. `#![no_std]` Missing - -**Problem**: Compilation fails with std library errors. - -**Symptoms**: -``` -error: cannot find macro `println` in this scope -error[E0433]: failed to resolve: use of undeclared crate or module `std` -``` - -**Solution**: -```rust -// MUST be first line of lib.rs -#![no_std] - -use soroban_sdk::{contract, contractimpl, Env}; - -// Use soroban_sdk equivalents instead of std: -// - soroban_sdk::String instead of std::string::String -// - soroban_sdk::Vec instead of std::vec::Vec -// - soroban_sdk::Map instead of std::collections::HashMap -``` - ---- - -### 3. Storage TTL Not Extended - -**Problem**: Contract data gets archived and becomes inaccessible. - -**Symptoms**: -- Contract calls fail after period of inactivity -- Data appears missing but contract still exists - -**Solution**: -```rust -// Proactively extend TTL in operations that use data -pub fn use_data(env: Env) { - // Extend instance storage - env.storage().instance().extend_ttl( - 50, // If TTL < 50, extend - 518400, // Extend to ~30 days - ); - - // Extend specific persistent keys - env.storage().persistent().extend_ttl( - &DataKey::ImportantData, - 50, - 518400, - ); - - // Now use the data... -} -``` - -> See Part 1: Contract Development above for full TTL management patterns and storage type guidance. - ---- - -### 4. Wrong Storage Type - -**Problem**: Data unexpectedly deleted or costs too high. - -**Symptoms**: -- Temporary data deleted before expected -- Unexpectedly high fees for storage - -**Solution**: -```rust -// Instance: Shared config, survives with contract -env.storage().instance().set(&DataKey::Admin, &admin); - -// Persistent: User data, can be archived but restored -env.storage().persistent().set(&DataKey::Balance(user), &balance); - -// Temporary: Truly temporary, auto-deleted, cheapest -env.storage().temporary().set(&DataKey::Cache(key), &value); -``` - ---- - -### 5. Authorization Not Working - -**Problem**: `require_auth()` not enforcing signatures in tests. - -**Symptoms**: -- Tests pass but transactions fail on network -- Anyone can call protected functions - -**Solution**: -```rust -#[test] -fn test_auth() { - let env = Env::default(); - - // DON'T just mock all auths blindly - // env.mock_all_auths(); // Be careful with this! - - // DO test specific auth requirements with mock_auths() - env.mock_auths(&[MockAuth { - address: &user, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "transfer", - args: (&user, &other, &100i128).into_val(&env), - sub_invokes: &[], - }, - }]); - - client.transfer(&user, &other, &100); - assert!(!env.auths().is_empty()); -} -``` - -> See Part 2: Testing Strategy above for comprehensive auth testing patterns including `mock_all_auths()`, specific auth mocking, and cross-contract auth. - ---- - -## SDK Issues - -### 6. Network Passphrase Mismatch - -**Problem**: Transactions fail with signature errors. - -**Symptoms**: -``` -Error: tx_bad_auth -``` - -**Solution**: -```typescript -import * as StellarSdk from "@stellar/stellar-sdk"; - -// ALWAYS use correct passphrase for network -const PASSPHRASES = { - mainnet: StellarSdk.Networks.PUBLIC, - // "Public Global Stellar Network ; September 2015" - - testnet: StellarSdk.Networks.TESTNET, - // "Test SDF Network ; September 2015" - - local: "Standalone Network ; February 2017", -}; - -// When building transactions -const tx = new StellarSdk.TransactionBuilder(account, { - fee: StellarSdk.BASE_FEE, - networkPassphrase: PASSPHRASES.testnet, // Match your network! -}); -``` - ---- - -### 7. Account Not Funded - -**Problem**: Operations fail because account doesn't exist. - -**Symptoms**: -``` -Error: Account not found -Status: 404 -``` - -**Solution**: -```typescript -// Testnet - use Friendbot -await fetch(`https://friendbot.stellar.org?addr=${publicKey}`); - -// Mainnet - must receive XLM from existing account -const tx = new StellarSdk.TransactionBuilder(funderAccount, { - fee: StellarSdk.BASE_FEE, - networkPassphrase: StellarSdk.Networks.PUBLIC, -}) - .addOperation( - StellarSdk.Operation.createAccount({ - destination: newAccountPublicKey, - startingBalance: "2", // Minimum ~1 XLM for base reserve - }) - ) - .setTimeout(180) - .build(); -``` - ---- - -### 8. Missing Trustline - -**Problem**: Payment fails for non-native assets. - -**Symptoms**: -``` -Error: op_no_trust -``` - -**Solution**: -```typescript -// Check if trustline exists -const account = await server.loadAccount(destination); -const hasTrustline = account.balances.some( - (b) => - b.asset_type !== "native" && - b.asset_code === asset.code && - b.asset_issuer === asset.issuer -); - -if (!hasTrustline) { - // Create trustline first - const trustTx = new StellarSdk.TransactionBuilder(destAccount, { - fee: StellarSdk.BASE_FEE, - networkPassphrase, - }) - .addOperation(StellarSdk.Operation.changeTrust({ asset })) - .setTimeout(180) - .build(); - // Sign and submit... -} -``` - ---- - -### 9. Sequence Number Issues - -**Problem**: Transaction rejected for sequence number. - -**Symptoms**: -``` -Error: tx_bad_seq -``` - -**Causes & Solutions**: - -```typescript -// Cause 1: Stale account data -// Solution: Always load fresh account before building tx -const account = await server.loadAccount(publicKey); - -// Cause 2: Parallel transactions -// Solution: Use sequence number management -class SequenceManager { - private sequence: bigint; - - async getNext(server: Horizon.Server, publicKey: string) { - if (!this.sequence) { - const account = await server.loadAccount(publicKey); - this.sequence = BigInt(account.sequence); - } - this.sequence++; - return this.sequence.toString(); - } -} - -// Cause 3: Transaction timeout without resubmit -// Solution: Rebuild with fresh sequence on timeout -``` - ---- - -### 10. Soroban Transaction Not Simulated - -**Problem**: Soroban transaction fails with resource errors. - -**Symptoms**: -``` -Error: transaction simulation failed -Error: insufficient resources -``` - -**Solution**: -```typescript -// ALWAYS simulate before submitting Soroban transactions -const simulation = await rpc.simulateTransaction(transaction); - -if (StellarSdk.rpc.Api.isSimulationError(simulation)) { - throw new Error(`Simulation failed: ${simulation.error}`); -} - -// Use assembleTransaction to add correct resources -const preparedTx = StellarSdk.rpc.assembleTransaction( - transaction, - simulation -).build(); - -// Now sign and submit preparedTx, not original transaction -``` - ---- - -## Frontend Issues - -### 11. Freighter Not Detected - -**Problem**: Wallet connection fails silently. - -**Symptoms**: -- `isConnected()` returns false -- No wallet prompt appears - -**Solution**: -```typescript -import { isConnected, isAllowed, requestAccess } from "@stellar/freighter-api"; - -async function checkFreighter() { - // Check if extension is installed - const { isConnected: installed, error } = await isConnected(); - if (error || !installed) { - // Prompt user to install - window.open("https://freighter.app", "_blank"); - return; - } - - // Check if this app is already authorized - const { isAllowed: granted } = await isAllowed(); - if (!granted) { - // requestAccess prompts the user and returns { address, error } - const { error: accessError } = await requestAccess(); - if (accessError) throw new Error(accessError.message); - } -} -``` - ---- - -### 12. Network Mismatch with Wallet - -**Problem**: Wallet on different network than app. - -**Symptoms**: -- Transactions fail unexpectedly -- Wrong balances displayed - -**Solution**: -```typescript -import { getNetwork } from "@stellar/freighter-api"; - -async function validateNetwork() { - const { network: walletNetwork, error } = await getNetwork(); - if (error) throw new Error(error.message); - const appNetwork = process.env.NEXT_PUBLIC_STELLAR_NETWORK; - - if (walletNetwork !== appNetwork) { - throw new Error( - `Please switch Freighter to ${appNetwork}. Currently on ${walletNetwork}` - ); - } -} -``` - ---- - -### 13. Transaction Timeout - -**Problem**: Transaction expires before confirmation. - -**Symptoms**: -``` -Error: tx_too_late -``` - -**Solution**: -```typescript -// Set appropriate timeout based on expected confirmation time -const tx = new StellarSdk.TransactionBuilder(account, { - fee: StellarSdk.BASE_FEE, - networkPassphrase, -}) - .addOperation(/* ... */) - .setTimeout(180) // 3 minutes - adjust as needed - .build(); - -// Handle timeout gracefully -async function submitWithRetry(signedXdr: string) { - try { - return await submitTransaction(signedXdr); - } catch (error) { - if (error.response?.data?.extras?.result_codes?.transaction === "tx_too_late") { - // Rebuild with fresh blockhash and retry - const newTx = await rebuildTransaction(signedXdr); - return await submitTransaction(newTx); - } - throw error; - } -} -``` - ---- - -## CLI Issues - -### 14. Identity Not Found - -**Problem**: Stellar CLI can't find saved identity. - -**Symptoms**: -``` -Error: identity "alice" not found -``` - -**Solution**: -```bash -# List existing identities -stellar keys list - -# Generate new identity -stellar keys generate --global alice - -# For testnet with funding -stellar keys generate --global alice --network testnet --fund - -# Specify identity location -stellar keys generate alice --config-dir /custom/path -``` - ---- - -### 15. Contract Invoke Parsing Errors - -**Problem**: CLI can't parse function arguments. - -**Symptoms**: -``` -Error: invalid argument format -``` - -**Solution**: -```bash -# Use correct argument syntax -# Addresses: just the G... or C... string -stellar contract invoke \ - --id CONTRACT_ID \ - --source alice \ - --network testnet \ - -- \ - transfer \ - --from GABC... \ - --to GDEF... \ - --amount 1000 - -# Complex types: use JSON -stellar contract invoke \ - --id CONTRACT_ID \ - -- \ - complex_fn \ - --data '{"field1": "value", "field2": 123}' -``` - ---- - -## General Best Practices +Auth mocking, fuzzing, fork tests, and CI setup: [testing.md](testing.md). -### Debugging Checklist +## Before mainnet -1. **Check network**: Is wallet/CLI on correct network? -2. **Check account**: Is source account funded? -3. **Check sequence**: Is sequence number current? -4. **Check simulation**: Did Soroban tx simulate successfully? -5. **Check trustlines**: For asset transfers, do trustlines exist? -6. **Check TTL**: For contract data, is TTL sufficient? -7. **Check authorization**: Is correct account signing? -8. **Check logs**: What does the error message actually say? +Work through the checklists in [security.md](security.md) — authorization, reinitialization, arithmetic, storage TTLs, and cross-contract validation are the recurring failure modes. -### Error Code Reference +## Documentation -| Code | Meaning | Common Fix | -|------|---------|------------| -| `tx_bad_auth` | Signature invalid | Check network passphrase | -| `tx_bad_seq` | Wrong sequence | Reload account | -| `tx_too_late` | Transaction expired | Rebuild and resubmit | -| `op_no_trust` | Missing trustline | Create trustline first | -| `op_underfunded` | Insufficient balance | Add funds | -| `op_low_reserve` | Below minimum balance | Maintain reserve | +- [Smart contract docs](https://developers.stellar.org/docs/build/smart-contracts) +- [Example contracts](https://github.com/stellar/soroban-examples) +- [soroban-sdk API reference](https://docs.rs/soroban-sdk) +- [Stellar CLI manual](https://developers.stellar.org/docs/tools/cli/stellar-cli) diff --git a/skills/soroban/development.md b/skills/soroban/development.md new file mode 100644 index 0000000..b4fc312 --- /dev/null +++ b/skills/soroban/development.md @@ -0,0 +1,286 @@ +# Development Patterns + +Core and advanced patterns for Stellar smart contracts. For setup and the basic workflow, start at [SKILL.md](SKILL.md); for tests see [testing.md](testing.md); for security review see [security.md](security.md). + +## Architecture + +Contracts run as WebAssembly guests in a sandboxed host. The host provides storage, crypto, and cross-contract calls; guest code references host objects via handles, not direct memory. Practical consequences: + +- `#![no_std]` — use `soroban_sdk` collections and strings +- 64KB compiled size limit (see [Contract size](#contract-size) below) +- Rust is the supported language. Community alternatives (AssemblyScript, Solidity via Solang) exist but are not production-ready — see [migration docs](https://developers.stellar.org/docs/learn/migrate). + +## Storage + +Three storage types with different costs and lifetimes. Choosing wrong is a top source of bugs and fee waste: + +| Type | Lifetime | Use for | +|------|----------|---------| +| `instance()` | Tied to the contract instance, shared TTL | Admin address, global config, small global state | +| `persistent()` | Per-key TTL, archived when expired but **restorable** | User balances, anything that must survive | +| `temporary()` | Per-key TTL, deleted when expired, **not restorable** | Caches, session data, short-lived flags | + +```rust +env.storage().instance().set(&DataKey::Admin, &admin); +env.storage().persistent().set(&DataKey::Balance(user), &balance); +env.storage().temporary().set(&DataKey::Cache(key), &value); +``` + +### TTL management + +Every entry has a TTL (in ledgers, ~5s each) and is archived when it expires. Extend proactively in functions that touch the data: + +```rust +const MIN_TTL: u32 = 17280; // ~1 day +const EXTEND_TO: u32 = 518400; // ~30 days + +// extend_ttl(threshold, extend_to): only extends if TTL < threshold +env.storage().instance().extend_ttl(MIN_TTL, EXTEND_TO); +env.storage().persistent().extend_ttl(&DataKey::Balance(user), MIN_TTL, EXTEND_TO); +``` + +Archived persistent entries can be restored with a `RestoreFootprint` operation, but that costs an extra transaction — design TTL extension into hot paths instead. Details: [state archival docs](https://developers.stellar.org/docs/learn/fundamentals/contract-development/storage/state-archival). + +### Typed storage keys + +Always use a `#[contracttype]` enum for keys — ad-hoc symbols invite collisions: + +```rust +#[contracttype] +#[derive(Clone)] +pub enum DataKey { + Admin, + Balance(Address), + Allowance(Address, Address), // (owner, spender) +} +``` + +## Data types + +```rust +use soroban_sdk::{Address, Bytes, BytesN, Map, String, Symbol, Vec}; + +let addr: Address = env.current_contract_address(); +let sym: Symbol = symbol_short!("transfer"); // ≤9 chars; Symbol max is 32 +let s: String = String::from_str(&env, "hello"); +let hash: BytesN<32> = env.crypto().sha256(&bytes).into(); +let v: Vec = vec![&env, 1, 2, 3]; +let m: Map = Map::new(&env); +``` + +Custom types derive `#[contracttype]`: + +```rust +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TokenMetadata { + pub name: String, + pub symbol: Symbol, + pub decimals: u32, +} +``` + +## Authorization + +Authorization is opt-in and explicit. Call `require_auth()` on every address whose consent the operation needs: + +```rust +pub fn transfer(env: Env, from: Address, to: Address, amount: i128) { + from.require_auth(); + // or bind the auth to specific arguments: + // from.require_auth_for_args((&to, amount).into_val(&env)); + // ... +} +``` + +Admin pattern: + +```rust +fn require_admin(env: &Env) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); + admin.require_auth(); +} +``` + +Authorization semantics (sub-invocations, custom accounts): [auth docs](https://developers.stellar.org/docs/learn/fundamentals/contract-development/authorization). + +## Constructors + +`__constructor` runs once, atomically, at deploy time (Protocol 22+). Prefer it over a separate `initialize` function — it removes the front-running window between deploy and init: + +```rust +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); +} +``` + +Rules: exact name `__constructor`, returns `()`, runs only at creation (not on upgrade), failure aborts the deployment atomically. Pass args at deploy time after the `--` separator (see [SKILL.md](SKILL.md#build-deploy-invoke)). + +If you must support a guarded `initialize` instead (pre-Protocol-22 targets), check-and-set an `Initialized` flag — see [security.md](security.md#2-reinitialization-attacks). + +## Cross-contract calls + +Import another contract's WASM to get a typed client: + +```rust +mod token_contract { + soroban_sdk::contractimport!( + file = "../token/target/wasm32-unknown-unknown/release/token.wasm" + ); +} + +pub fn deposit(env: Env, user: Address, token: Address, amount: i128) { + user.require_auth(); + let token_client = token_contract::Client::new(&env, &token); + token_client.transfer(&user, &env.current_contract_address(), &amount); +} +``` + +For Stellar assets, use the built-in SAC client — every asset has a Stellar Asset Contract: + +```rust +use soroban_sdk::token::Client as TokenClient; + +let token = TokenClient::new(&env, &asset_contract_id); +token.transfer(&from, &to, &amount); +``` + +SAC details and asset interop: `../assets/SKILL.md`. Validate addresses you call — see [security.md](security.md#3-arbitrary-contract-calls). + +## Events + +```rust +use soroban_sdk::contractevent; + +#[contractevent(topics = ["transfer"])] +pub struct TransferEvent { + pub from: Address, + pub to: Address, + pub amount: i128, +} + +// in a contract function: +TransferEvent { from, to, amount }.publish(&env); +``` + +Emit events for every state change you'll want to index or audit — events are much cheaper than storing queryable state. + +## Error handling + +```rust +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum ContractError { + NotInitialized = 1, + InsufficientBalance = 2, + InvalidAmount = 3, +} + +pub fn transfer(env: Env, from: Address, to: Address, amount: i128) -> Result<(), ContractError> { + if amount <= 0 { + return Err(ContractError::InvalidAmount); + } + // ... + Ok(()) +} +``` + +Returning `Result` gives callers (and clients generated for tests) typed errors via `try_` methods. Panics abort with less information — reserve them for invariant violations. + +## Upgradeability + +Decide early whether the contract is mutable. If yes, gate the upgrade behind admin/governance auth and track versions: + +```rust +contractmeta!(key = "binver", val = "1.0.0"); + +pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); + admin.require_auth(); + env.deployer().update_current_contract_wasm(new_wasm_hash); +} +``` + +- Upload the new WASM first (`stellar contract upload`), pass its hash here. +- Add a dedicated `migrate` entrypoint for storage migrations; make it idempotent and monotonic (`new_version > current_version`). +- `__constructor` does not re-run on upgrade. +- SEP-0049 standardizes upgradeable-contract interfaces; OpenZeppelin's [stellar-contracts](https://github.com/OpenZeppelin/stellar-contracts) ships an audited implementation. + +## Factories + +```rust +#[contractimpl] +impl Factory { + pub fn deploy( + env: Env, + owner: Address, + wasm_hash: BytesN<32>, + salt: BytesN<32>, + constructor_args: Vec, + ) -> Address { + owner.require_auth(); + env.deployer() + .with_address(env.current_contract_address(), salt) + .deploy_v2(wasm_hash, constructor_args) + } +} +``` + +- Addresses are deterministic per (deployer, salt) — derive salts intentionally. +- Authorize who may deploy; emit an event per deployment so instances are indexable. +- Keep factory logic separate from instance business logic. + +## Governance + +For sensitive actions (upgrades, config changes), prefer a timelock: `propose_*` stores the pending action plus an execute-after ledger, `execute_*` enforces the delay, `cancel_*` lets governance abort. For multisig, separate proposer/approver/executor roles, store proposal state in persistent storage, and prevent replay (unique proposal IDs, expiry semantics, explicit events). + +## DeFi and compliance patterns + +Condensed design rules — the details are application-specific: + +- **Vaults**: track `total_assets`/`total_shares`, round conservatively on mint/redeem, include pause controls. +- **Pools/AMMs**: define the invariant and fee accounting precisely; slippage-check every user-facing swap. +- **Oracles**: enforce freshness bounds, prefer multi-source/median feeds, add circuit breakers. +- **Regulated tokens**: isolate allowlist/denylist and freeze/forced-transfer logic in dedicated entrypoints with strong auth, emit policy-decision events, never store PII on-chain. + +Worked examples: [soroban-examples](https://github.com/stellar/soroban-examples) (liquidity pool, atomic swap, timelock, single-offer) and the [DeFi tutorials](https://developers.stellar.org/docs/build/apps/guestbook). + +## Contract size + +The 64KB limit is real and the release profile in [SKILL.md](SKILL.md#project-setup) is mandatory. If you still exceed it: + +```bash +ls -la target/wasm32-unknown-unknown/release/*.wasm # check size +cargo install cargo-bloat +cargo bloat --release --target wasm32-unknown-unknown # find heavy deps +``` + +Then: split the contract, drop heavy dependencies, prefer `symbol_short!`, avoid large static data. + +## Resource optimization + +Fees are multidimensional (CPU instructions, ledger reads/writes, bytes, events, rent): + +- Minimize storage reads/writes; batch where possible +- Avoid unbounded loops over user-controlled collections +- Reduce cross-contract calls in hot paths +- Use events instead of storage for data that only needs off-chain visibility +- Profile with `stellar contract invoke ... --sim-only` before optimizing blind + +## Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| `contract exceeds maximum size` | WASM > 64KB | See [Contract size](#contract-size) | +| `cannot find macro println` / `std` errors | Missing `#![no_std]` | Add as first line of `lib.rs`; use SDK types | +| Calls fail after inactivity, data "missing" | Storage TTL expired → archived | Extend TTLs proactively; restore archived entries | +| Temporary data vanished | Wrong storage type | Use `persistent()` for data that must survive | +| `Error: identity "alice" not found` | CLI identity missing | `stellar keys generate --global alice --network testnet --fund` | +| `invalid argument format` on invoke | Wrong CLI arg syntax | Plain strings for addresses; JSON for complex types | +| `transaction simulation failed` | Soroban tx not simulated/assembled | Simulate, then `assembleTransaction` before signing | +| `tx_bad_auth` | Wrong network passphrase or signer | Match passphrase to network; check signing identity | +| `tx_bad_seq` | Stale sequence number | Reload the account before building the tx | + +Client-side issues (wallet detection, trustlines, transaction building from JS) are covered in `../dapp/SKILL.md` and `../data/SKILL.md`. diff --git a/skills/soroban/security.md b/skills/soroban/security.md new file mode 100644 index 0000000..eb3e32d --- /dev/null +++ b/skills/soroban/security.md @@ -0,0 +1,164 @@ +# Security + +Security review guide for Stellar smart contracts. Companion to [SKILL.md](SKILL.md), [development.md](development.md), and [testing.md](testing.md). + +## Threat model + +Assume the attacker controls: + +- All arguments passed to contract functions +- Transaction ordering and timing +- All accounts except those requiring signatures +- The ability to deploy contracts that mimic your interface + +## What the platform rules out + +- **No `delegatecall`** — contracts cannot execute foreign bytecode in their own context; proxy-style hijacks don't exist. +- **No classical reentrancy** — execution is synchronous; the Ethereum-style cross-contract reentrancy class is absent (self-reentrancy is possible but rarely exploitable). +- **Explicit authorization** — `require_auth()` is opt-in, which means *forgetting it* is the failure mode to hunt for. + +## Vulnerability classes + +### 1. Missing authorization + +```rust +// BAD: anyone can drain +pub fn withdraw(env: Env, to: Address, amount: i128) { + transfer_tokens(&env, &to, amount); +} + +// GOOD +pub fn withdraw(env: Env, to: Address, amount: i128) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); + admin.require_auth(); + transfer_tokens(&env, &to, amount); +} +``` + +Every privileged path needs `require_auth()` on the right address. Auth variants: [development.md](development.md#authorization). + +### 2. Reinitialization attacks + +Only relevant if you use a guarded `initialize` instead of a `__constructor` (which can't re-run): + +```rust +// GOOD: refuses second call +pub fn initialize(env: Env, admin: Address) { + if env.storage().instance().has(&DataKey::Admin) { + panic!("already initialized"); + } + env.storage().instance().set(&DataKey::Admin, &admin); +} +``` + +### 3. Arbitrary contract calls + +Calling whatever address a user passes lets an attacker substitute a contract that mimics the interface: + +```rust +// GOOD: allowlist external contracts +pub fn swap(env: Env, token: Address, amount: i128) { + let allowed: Vec
= env.storage().instance().get(&DataKey::AllowedTokens).unwrap(); + if !allowed.contains(&token) { + panic!("token not allowed"); + } + let client = token::Client::new(&env, &token); + // ... +} +``` + +### 4. Integer overflow/underflow + +`overflow-checks = true` in the release profile catches overflows at runtime (panic), but explicit checked math fails cleaner and survives profile mistakes: + +```rust +let new_balance = balance.checked_add(amount).expect("overflow"); +``` + +Also validate sign and range on inputs (`amount <= 0` checks) — `i128` amounts can be negative. + +### 5. Storage key collisions + +Untyped keys can silently overwrite unrelated data. Always use a `#[contracttype]` key enum — see [development.md](development.md#typed-storage-keys). + +### 6. Check-then-act races + +State can change between transactions. Do checks and state changes atomically within one invocation, and take slippage bounds (`min_out`) from the caller: + +```rust +pub fn swap(env: Env, user: Address, amount_in: i128, min_out: i128) { + user.require_auth(); + let amount_out = calculate_output(amount_in); + if amount_out < min_out { + panic!("slippage exceeded"); + } + // update all state in this same invocation +} +``` + +### 7. TTL/archival failures + +If critical state expires, the contract breaks (or worse, behaves as if state never existed). Extend TTLs in hot paths and monitor entry TTLs in production — see [development.md](development.md#ttl-management). + +### 8. Trusting cross-contract return values + +Validate data from external contracts — allowlist oracles, sanity-check magnitudes, enforce freshness: + +```rust +let price: i128 = oracle_client.get_price(&asset); +if price <= 0 || price > MAX_REASONABLE_PRICE { + panic!("invalid price"); +} +``` + +## Classic-side risks (for contracts touching assets) + +- **Trustline spoofing**: display and verify full asset code + issuer; honor curated lists (`stellar.toml`). +- **Clawback**: assets with `auth_clawback_enabled` can be seized by the issuer — check issuer flags before treating balances as final. +- Asset semantics live in `../assets/SKILL.md`. + +## Checklists + +### Contract + +- [ ] All privileged functions require appropriate authorization +- [ ] Initialization can only happen once (or uses `__constructor`) +- [ ] External contract calls validated/allowlisted +- [ ] Arithmetic checked; input signs and ranges validated +- [ ] Storage keys typed and collision-free +- [ ] Critical TTLs extended proactively; archival behavior considered +- [ ] Events emitted for auditable state changes +- [ ] Upgrade path gated, tested (happy + failure), and replay-safe +- [ ] Emergency controls (pause) and incident runbook defined for value-bearing contracts + +### Client-side + +- [ ] Network passphrase validated before signing +- [ ] Transactions simulated before submission +- [ ] Operation details displayed clearly; confirmation for high-value actions +- [ ] Contract addresses verified against known deployments +- [ ] Trustline status checked before transfers + +## Tooling + +**Static analysis** + +- [Scout](https://github.com/CoinFabrik/scout-soroban) (CoinFabrik): `cargo install cargo-scout-audit && cargo scout-audit` — 20+ detectors (missing overflow checks, unprotected WASM update, unrestricted transfers, unsafe unwrap, DoS-unbounded ops). SARIF output for CI; VSCode extension available. +- [Security Detectors SDK](https://github.com/OpenZeppelin/soroban-security-detectors-sdk) (OpenZeppelin): pre-built detectors (`auth_missing`, `unchecked_ft_transfer`, improper TTL extension) plus a framework for writing custom ones. + +**Formal verification** + +- [Certora Sunbeam](https://docs.certora.com/en/latest/docs/sunbeam/index.html): specs as Rust macros (`cvlr_assert!`), operates on WASM bytecode. +- [Komet](https://docs.runtimeverification.com/komet) (Runtime Verification): property tests + formal verification via KWasm semantics. + +**Monitoring**: [OpenZeppelin Monitor](https://www.openzeppelin.com/news/monitor-and-relayers-are-now-open-source) — self-hosted contract monitoring with Stellar support. + +**Knowledge base**: [sorobansecurity.com](https://sorobansecurity.com) — searchable audit reports and vulnerability database. + +## Audits and bounties + +- **[Audit Bank](https://stellar.org/grants-and-funding/soroban-audit-bank)** — SDF-subsidized audits for SCF-funded protocols ($3M+ across 43+ audits to date). Partner firms include OtterSec, Veridise, Runtime Verification, CoinFabrik, Certora, Zellic, Code4rena. Follow-up audits trigger at TVL milestones. +- **[Immunefi — Stellar](https://immunefi.com/bug-bounty/stellar/)** — up to $250K for core/SDK/CLI vulnerabilities (PoC required, local forks only). +- **[Immunefi — OpenZeppelin on Stellar](https://immunefi.com/bug-bounty/openzeppelin-stellar/)** — up to $25K for the audited contracts library. + +Before requesting an audit: run the static analyzers, complete the checklists above, document your threat model and trust assumptions, and have the test suite from [testing.md](testing.md) green — auditors' time is better spent on logic than on lint. diff --git a/skills/soroban/testing.md b/skills/soroban/testing.md new file mode 100644 index 0000000..7164632 --- /dev/null +++ b/skills/soroban/testing.md @@ -0,0 +1,243 @@ +# Testing + +Testing strategy for Stellar smart contracts, from fast native unit tests to mainnet-fork tests. Companion to [SKILL.md](SKILL.md), [development.md](development.md), and [security.md](security.md). + +Layers, fastest first: + +1. **Unit tests** — native Rust with `soroban-sdk` testutils (not WASM, full debugger support) +2. **Local integration** — Stellar Quickstart container +3. **Testnet** — public network rehearsal +4. **Fork tests** — replay against real ledger state + +## Unit testing + +```rust +#![cfg(test)] +use soroban_sdk::{testutils::Address as _, Address, Env}; +use crate::{Contract, ContractClient}; + +#[test] +fn test_basic() { + let env = Env::default(); + let contract_id = env.register(Contract, ()); // () = no constructor args + let client = ContractClient::new(&env, &contract_id); + let user = Address::generate(&env); + + client.initialize(&user); + assert_eq!(client.get_value(), 0); +} +``` + +`env.register` takes constructor args as its second parameter — `env.register(Contract, (admin.clone(),))` for a contract with `__constructor(env, admin)`. + +### Authorization + +`mock_all_auths()` approves everything — convenient, but it can hide missing `require_auth` calls. Always pair it with `env.auths()` assertions, or mock specific auths: + +```rust +#[test] +fn test_auth() { + let env = Env::default(); + let contract_id = env.register(Contract, ()); + let client = ContractClient::new(&env, &contract_id); + let user = Address::generate(&env); + + // Approve only this specific invocation + env.mock_auths(&[MockAuth { + address: &user, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "transfer", + args: (&user, &other, &100i128).into_val(&env), + sub_invokes: &[], + }, + }]); + + client.transfer(&user, &other, &100); + assert!(!env.auths().is_empty()); // verify auth was actually required +} +``` + +### Time and ledger state + +```rust +env.ledger().set_timestamp(1000); // for vesting/timelock logic +env.ledger().set_sequence_number(1000); +// ... act, then advance: +env.ledger().set_timestamp(2500); +``` + +### Events + +```rust +let events = env.events().all(); +assert_eq!(events.len(), 1); +// each event = (contract_id, topics: Vec, data: Val) +``` + +### Storage TTL + +```rust +let ttl = env.as_contract(&contract_id, || { + env.storage().persistent().get_ttl(&DataKey::MyData) +}); +assert!(ttl > 0); +``` + +### Cross-contract + +Register a real dependency from its WASM and test the interaction: + +```rust +mod token { soroban_sdk::contractimport!(file = "token.wasm"); } + +let token_id = env.register(token::WASM, ()); +let vault_id = env.register(VaultContract, ()); +// drive both through their typed clients +``` + +## Local network + +```bash +stellar container start local # or: docker run --rm -it -p 8000:8000 stellar/quickstart:latest --local +stellar keys generate --global test-account --network local --fund +stellar contract deploy --wasm target/wasm32-unknown-unknown/release/contract.wasm \ + --source test-account --network local +``` + +Endpoints: RPC `http://localhost:8000/soroban/rpc`, Horizon `http://localhost:8000`, passphrase `"Standalone Network ; February 2017"`. + +## Testnet + +```bash +stellar keys generate --global my-key --network testnet --fund +stellar contract deploy --wasm ... --source my-key --network testnet +``` + +- RPC: `https://soroban-testnet.stellar.org` · Horizon: `https://horizon-testnet.stellar.org` +- Passphrase: `"Test SDF Network ; September 2015"` · Friendbot: `https://friendbot.stellar.org` +- **Testnet resets quarterly** — everything is deleted. Script your deployments; never treat testnet state as durable. + +## Integration tests + +TypeScript, against local or testnet (the same flow a frontend uses — simulate, assemble, sign, send): + +```typescript +import * as StellarSdk from "@stellar/stellar-sdk"; + +const rpc = new StellarSdk.rpc.Server(process.env.RPC_URL!); +const account = await rpc.getAccount(keypair.publicKey()); +const contract = new StellarSdk.Contract(contractId); + +const tx = new StellarSdk.TransactionBuilder(account, { fee: "100", networkPassphrase }) + .addOperation(contract.call("initialize", StellarSdk.Address.fromString(keypair.publicKey()).toScVal())) + .setTimeout(30) + .build(); + +const sim = await rpc.simulateTransaction(tx); +const prepared = StellarSdk.rpc.assembleTransaction(tx, sim).build(); +prepared.sign(keypair); +const result = await rpc.sendTransaction(prepared); +``` + +In Rust, gate network-dependent tests behind `#[ignore]` and run them with `cargo test -- --ignored` in environments where the network is up. + +## Fuzz testing + +All `#[contracttype]` types implement `SorobanArbitrary` under the `testutils` feature, so fuzzing works out of the box with `cargo-fuzz`: + +```bash +rustup install nightly +cargo install --locked cargo-fuzz +cargo fuzz init +``` + +`Cargo.toml` needs `crate-type = ["lib", "cdylib"]`; add `soroban-sdk = { version = "...", features = ["testutils"] }` to `fuzz/Cargo.toml`. + +```rust +// fuzz/fuzz_targets/fuzz_deposit.rs +#![no_main] +use libfuzzer_sys::fuzz_target; +use soroban_sdk::{testutils::Address as _, Address, Env}; +use my_contract::{Contract, ContractClient}; + +fuzz_target!(|amount: i128| { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(Contract, ()); + let client = ContractClient::new(&env, &contract_id); + let user = Address::generate(&env); + + client.initialize(&user); + let _ = client.try_deposit(&user, &amount); // must never panic unexpectedly +}); +``` + +Run with `cargo +nightly fuzz run fuzz_deposit`. For token contracts there's a reusable harness: [soroban-token-fuzzer](https://github.com/brson/soroban-token-fuzzer). Docs: [fuzzing guide](https://developers.stellar.org/docs/build/guides/testing/fuzzing). + +## Property-based testing + +`proptest` + `SorobanArbitrary` runs in plain `cargo test` — use it to lock in invariants found by fuzzing: + +```rust +proptest! { + #[test] + fn deposit_then_withdraw_preserves_balance(amount in 1i128..=i128::MAX) { + // setup as usual... + client.deposit(&user, &amount); + client.withdraw(&user, &amount); + prop_assert_eq!(client.balance(&user), 0); + } +} +``` + +Workflow: fuzz interactively to find deep bugs → convert findings to proptest regressions for CI. + +## Snapshots, fork tests, mutation tests + +Three techniques worth knowing; each is one command plus a doc link: + +- **Test snapshots**: every test writes a JSON snapshot of events + final ledger state to `test_snapshots/`. Commit them — diffs expose unintended behavioral changes. [Docs](https://developers.stellar.org/docs/build/guides/testing/differential-tests-with-test-snapshots) +- **Fork testing**: `stellar snapshot create --address C... --output json --out snapshot.json`, then `Env::from_ledger_snapshot_file("snapshot.json")` to test against real network state. Also useful for upgrade rehearsals: `stellar contract fetch --id C... --out-file deployed.wasm`, register both old and new versions, compare behavior. [Docs](https://developers.stellar.org/docs/build/guides/testing/fork-testing) +- **Mutation testing**: `cargo install --locked cargo-mutants && cargo mutants` — mutates your source and reports `MISSED` where tests didn't notice. [Docs](https://developers.stellar.org/docs/build/guides/testing/mutation-testing) + +## Resource profiling + +```bash +stellar contract invoke --id CONTRACT_ID --source alice --network testnet \ + --sim-only -- function_name --arg value +``` + +Simulation reports CPU instructions, ledger reads/writes, and fees without submitting. + +## CI + +```yaml +name: Test contracts +on: [push, pull_request] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: rustup target add wasm32-unknown-unknown + - run: cargo test + - run: cargo build --release --target wasm32-unknown-unknown +``` + +For integration jobs, run `stellar/quickstart` as a service container and deploy with the CLI (`cargo install stellar-cli --locked`). + +## Checklist + +- [ ] Unit tests cover all public functions, including error paths +- [ ] Edge cases: zero amounts, max values, empty state +- [ ] Authorization verified with `env.auths()` or specific `mock_auths` +- [ ] Events asserted +- [ ] Storage TTL behavior validated +- [ ] Cross-contract interactions tested against real WASM +- [ ] Fuzz targets for value-moving paths (deposit, withdraw, swap) +- [ ] Property tests for invariants +- [ ] Test snapshots committed +- [ ] Integration test against local network in CI +- [ ] Testnet rehearsal before mainnet diff --git a/skills/standards/SKILL.md b/skills/standards/SKILL.md index 08d4dcd..1f9bdd7 100644 --- a/skills/standards/SKILL.md +++ b/skills/standards/SKILL.md @@ -63,9 +63,9 @@ Treat this file as a routing map, not a source of final governance/status truth. - [SEP-0031](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md): Cross-border payment flow - [SEP-0012](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md): KYC data exchange -## High-value CAPs for Soroban developers +## High-value CAPs for smart contract developers -### Soroban foundations +### Smart contract foundations - [CAP-0046](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046.md): Soroban overview - CAP-0046 subdocuments (`cap-0046-*.md`): runtime, lifecycle, host functions, storage, auth, metering @@ -115,9 +115,9 @@ Use the CAP preamble status fields as the source of truth for implementation rea ## Related docs - Contract implementation details: [`../soroban/SKILL.md`](../soroban/SKILL.md) -- Advanced architecture guidance: [`../soroban/SKILL.md`](../soroban/SKILL.md) +- Advanced architecture guidance: [`../soroban/development.md`](../soroban/development.md) - RPC and data access: [`../data/SKILL.md`](../data/SKILL.md) -- Security considerations: [`../soroban/SKILL.md`](../soroban/SKILL.md#part-3-security) +- Security considerations: [`../soroban/security.md`](../soroban/security.md) --- @@ -145,7 +145,7 @@ Universal liquidity protocol enabling permissionless lending pools. - **Integrations**: Meru, Airtm, Lobstr, DeFindex, Beans #### Slender -First non-custodial lending protocol on Soroban with flash loan support. +First non-custodial lending protocol on Stellar with flash loan support. - **Use Case**: Lending, borrowing, flash loans - **Features**: Pool-based strategy, sTokens, dTokens, utilization caps - **Oracle**: SEP-40 compatible (Reflector) @@ -153,7 +153,7 @@ First non-custodial lending protocol on Soroban with flash loan support. ### DEXs & AMMs #### Soroswap -First DEX and aggregator on Stellar/Soroban. +First DEX and aggregator on Stellar. - **Use Case**: Token swaps, liquidity provision, aggregation - **Website**: https://soroswap.finance - **GitHub (Core)**: https://github.com/soroswap/core @@ -172,7 +172,7 @@ Governance-driven liquidity layer with AMM functionality. - **Docs**: https://docs.aqua.network #### Phoenix Protocol -AMM protocol on Soroban. +AMM protocol on Stellar. - **GitHub**: https://github.com/Phoenix-Protocol-Group - **Use Case**: Token swaps, liquidity pools @@ -203,7 +203,7 @@ SDF's flagship non-custodial browser wallet. - **GitHub**: https://github.com/stellar/freighter - **GitHub (Mobile)**: https://github.com/stellar/freighter-mobile - **API**: https://github.com/stellar/freighter/tree/master/library/freighter-api -- **Features**: Soroban support, mobile apps (iOS/Android), Discover browser +- **Features**: Smart contract support, mobile apps (iOS/Android), Discover browser #### xBull Feature-rich browser wallet with advanced capabilities. @@ -248,7 +248,7 @@ SDK for integrating multiple Stellar wallets. ### Smart Account & Authentication #### Smart Account Kit (Recommended) -Comprehensive TypeScript SDK for OpenZeppelin Smart Accounts on Stellar/Soroban. +Comprehensive TypeScript SDK for OpenZeppelin Smart Accounts on Stellar. - **GitHub**: https://github.com/kalepail/smart-account-kit - **Use Case**: Production smart wallets with passkeys - **Built On**: [OpenZeppelin stellar-contracts](https://github.com/OpenZeppelin/stellar-contracts) @@ -286,7 +286,7 @@ Stellar-native data indexing platform with Retroshades technology. - **Features**: Zephyr VM (serverless Rust execution at ledger close), GraphQL API #### SubQuery -Multi-chain indexer supporting Stellar and Soroban. +Multi-chain indexer supporting Stellar. - **Website**: https://subquery.network - **Quick Start**: https://subquery.network/doc/indexer/quickstart/quickstart_chains/stellar.html - **Features**: Block/transaction/operation/event handlers, multi-threading, 300+ chains @@ -306,7 +306,7 @@ Cloud execution environment for blockchain data processing. ### Contract Libraries #### OpenZeppelin Stellar Contracts -Audited smart contract library for Soroban (track latest release tags before pinning versions). +Audited smart contract library for Stellar (track latest release tags before pinning versions). - **GitHub**: https://github.com/OpenZeppelin/stellar-contracts - **Docs**: https://developers.stellar.org/docs/tools/openzeppelin-contracts - **Contract Wizard**: https://wizard.openzeppelin.com/stellar @@ -316,30 +316,30 @@ Audited smart contract library for Soroban (track latest release tags before pin ### Security Tools #### Scout Soroban (CoinFabrik) -Open-source vulnerability detector with 23 detectors for Soroban contracts. +Open-source vulnerability detector with 23 detectors for Stellar smart contracts. - **GitHub**: https://github.com/CoinFabrik/scout-soroban - **Install**: `cargo install cargo-scout-audit` - **Features**: CLI tool, VSCode extension, SARIF output for CI/CD - **Examples**: https://github.com/CoinFabrik/scout-soroban-examples #### OpenZeppelin Security Detectors SDK -Framework for building custom security detectors for Soroban. +Framework for building custom security detectors for Stellar contracts. - **GitHub**: https://github.com/OpenZeppelin/soroban-security-detectors-sdk - **Detectors**: `auth_missing`, `unchecked_ft_transfer`, improper TTL, contract panics - **Extensible**: Load external detector libraries, CI/CD ready #### Certora Sunbeam Prover -Formal verification for Soroban — first WASM platform supported by Certora. +Formal verification for Stellar smart contracts — first WASM platform supported by Certora. - **Docs**: https://docs.certora.com/en/latest/docs/sunbeam/index.html - **Spec Language**: CVLR (Rust macros) — https://github.com/Certora/cvlr - **Reports**: [Blend V1 verification](https://www.certora.com/reports/blend-smart-contract-verification-report) - **Verifies at**: WASM bytecode level, eliminating compiler trust assumptions #### Runtime Verification — Komet -Formal verification and testing tool designed for Soroban (SCF-funded). +Formal verification and testing tool designed for Stellar smart contracts (SCF-funded). - **Docs**: https://docs.runtimeverification.com/komet - **Repo**: https://github.com/runtimeverification/komet -- **Spec Language**: Rust — property-based tests written in the same language as Soroban contracts +- **Spec Language**: Rust — property-based tests written in the same language as the contracts - **Operates at**: WASM bytecode level via [KWasm semantics](https://github.com/runtimeverification/wasm-semantics) (eliminates compiler trust assumptions) - **Features**: Fuzzing, testing, formal verification - **Reports**: https://github.com/runtimeverification/publications @@ -354,7 +354,7 @@ Community security knowledge base (SCF-funded). ### CLI & SDKs #### Stellar CLI -Official command-line interface for Stellar/Soroban. +Official command-line interface for Stellar. - **Docs**: https://developers.stellar.org/docs/tools/stellar-cli - **Features**: Contract build, deploy, invoke, bindings generation @@ -364,7 +364,7 @@ Official JavaScript/TypeScript SDK. - **npm**: `@stellar/stellar-sdk` #### Soroban Rust SDK -Rust SDK for Soroban contract development. +Rust SDK for smart contract development. - **GitHub**: https://github.com/stellar/rs-soroban-sdk - **Crate**: `soroban-sdk` @@ -445,7 +445,7 @@ Official educational smart contract examples. #### Soroban Example dApp Crowdfunding dApp with Next.js frontend. - **GitHub**: https://github.com/stellar/soroban-example-dapp -- **Learning**: Full-stack Soroban development, Freighter integration +- **Learning**: Full-stack contract development, Freighter integration ### Community Examples @@ -471,7 +471,7 @@ Simple NFT using OpenZeppelin. ## Cross-Chain #### Axelar -Cross-chain gateway and Interchain Token Service for Soroban. +Cross-chain gateway and Interchain Token Service for Stellar. - **GitHub**: https://github.com/axelarnetwork/axelar-amplifier-stellar - **Use Case**: Cross-chain messaging, token bridging, interoperability - **Status**: Active development (verify latest activity before integrating) @@ -488,7 +488,7 @@ Omnichain interoperability protocol with Stellar support. ## Builder Teams & Companies -Notable teams shipping production-level code on Stellar/Soroban. For a broader directory, see [Stellar Ecosystem](https://stellar.org/ecosystem). +Notable teams shipping production-level code on Stellar. For a broader directory, see [Stellar Ecosystem](https://stellar.org/ecosystem). | Team | Website | GitHub | X/Twitter | Notable Projects | |------|---------|--------|-----------|-----------------| @@ -562,7 +562,7 @@ Major companies building on Stellar: ### Stellar Developer Docs - [Stellar Documentation](https://developers.stellar.org/docs) - Primary documentation -- [Build Smart Contracts](https://developers.stellar.org/docs/build/smart-contracts) - Soroban guides +- [Build Smart Contracts](https://developers.stellar.org/docs/build/smart-contracts) - smart contract guides - [Build Apps](https://developers.stellar.org/docs/build/apps) - Client application guides - [Tools & SDKs](https://developers.stellar.org/docs/tools) - Available tooling - [Networks](https://developers.stellar.org/docs/networks) - Network configuration @@ -584,7 +584,7 @@ Major companies building on Stellar: - [Rust SDK (RPC Client)](https://github.com/stellar/rs-stellar-rpc-client) - [SDK Documentation](https://developers.stellar.org/docs/tools/sdks/client-sdks) -### Contract SDK (Soroban Development) +### Contract SDK (Rust) - [Soroban Rust SDK](https://github.com/stellar/rs-soroban-sdk) - `soroban-sdk` - [Soroban SDK Docs](https://docs.rs/soroban-sdk/latest/soroban_sdk/) - Rust docs @@ -640,10 +640,10 @@ For DeFi protocols, wallets, oracles, gaming/NFTs, cross-chain bridges, and buil ## Security -For vulnerability patterns, checklists, and detailed tooling guides, see [Soroban security section](../soroban/SKILL.md). +For vulnerability patterns, checklists, and detailed tooling guides, see [the smart contract security guide](../soroban/security.md). ### Bug Bounty Programs -- [Stellar Bug Bounty (Immunefi)](https://immunefi.com/bug-bounty/stellar/) - Up to $250K, covers core + Soroban +- [Stellar Bug Bounty (Immunefi)](https://immunefi.com/bug-bounty/stellar/) - Up to $250K, covers core + smart contract stack - [OpenZeppelin Stellar Bounty (Immunefi)](https://immunefi.com/bug-bounty/openzeppelin-stellar/) - Up to $25K - [HackerOne VDP](https://stellar.org/grants-and-funding/bug-bounty) - Web application vulnerabilities @@ -659,10 +659,10 @@ For vulnerability patterns, checklists, and detailed tooling guides, see [Soroba ### Formal Verification - [Certora Sunbeam Prover](https://docs.certora.com/en/latest/docs/sunbeam/index.html) - WASM-level formal verification - [CVLR Spec Language](https://github.com/Certora/cvlr) - Certora Verification Language for Rust -- [Runtime Verification Komet](https://runtimeverification.com/blog/introducing-komet-smart-contract-testing-and-verification-tool-for-soroban-created-by-runtime-verification) - Soroban verification tool +- [Runtime Verification Komet](https://runtimeverification.com/blog/introducing-komet-smart-contract-testing-and-verification-tool-for-soroban-created-by-runtime-verification) - contract verification tool ### Security Resources -- [Veridise Security Checklist](https://veridise.com/blog/audit-insights/building-on-stellar-soroban-grab-this-security-checklist-to-avoid-vulnerabilities/) - Soroban-specific +- [Veridise Security Checklist](https://veridise.com/blog/audit-insights/building-on-stellar-soroban-grab-this-security-checklist-to-avoid-vulnerabilities/) - smart-contract security checklist - [Soroban Security Portal](https://sorobansecurity.com) - Community vulnerability database - [CoinFabrik Audit Reports](https://www.coinfabrik.com/smart-contract-audit-reports/) - [Certora Security Reports](https://github.com/Certora/SecurityReports) - Includes Stellar verifications @@ -722,7 +722,7 @@ Always verify CAP status and network support before treating any ZK primitive as ### Data Indexers - [Mercury](https://mercurydata.app) - Stellar-native indexer with Retroshades + GraphQL ([docs](https://docs.mercurydata.app)) -- [SubQuery](https://subquery.network) - Multi-chain indexer with Stellar/Soroban support ([quick start](https://subquery.network/doc/indexer/quickstart/quickstart_chains/stellar.html)) +- [SubQuery](https://subquery.network) - Multi-chain indexer with Stellar support ([quick start](https://subquery.network/doc/indexer/quickstart/quickstart_chains/stellar.html)) - [Goldsky](https://goldsky.com) - Real-time data replication pipelines + subgraphs ([Stellar docs](https://docs.goldsky.com/chains/stellar)) - [Zephyr VM](https://github.com/xycloo/zephyr-vm) - Serverless Rust execution at ledger close @@ -805,7 +805,7 @@ Always verify CAP status and network support before treating any ZK primitive as ### Developer Tools - [Stella AI Bot](https://developers.stellar.org/docs/tools/developer-tools) - AI assistant for Stellar developer questions -- [Soroban Playground](https://soropg.com) - Browser-based Soroban IDE ([GitHub](https://github.com/jamesbachini/Soroban-Playground)) +- [Soroban Playground](https://soropg.com) - Browser-based smart contract IDE ([GitHub](https://github.com/jamesbachini/Soroban-Playground)) ### Blog Posts & Guides - [Composability on Stellar](https://stellar.org/blog/developers/composability-on-stellar-from-concept-to-reality) @@ -835,7 +835,7 @@ Always verify CAP status and network support before treating any ZK primitive as ### Key People to Follow -Builders and contributors actively shaping the Stellar/Soroban ecosystem: +Builders and contributors actively shaping the Stellar ecosystem: | Name | GitHub | X/Twitter | Focus | |------|--------|-----------|-------| @@ -856,7 +856,7 @@ Builders and contributors actively shaping the Stellar/Soroban ecosystem: | Willem Wyndham | [willemneal](https://github.com/willemneal) | [@willemneal](https://x.com/willemneal) | Aha Labs co-founder, Scaffold Stellar, JS contract client | ### Builder Teams & Companies -See Part 2: Stellar Ecosystem above for a table of teams shipping production code on Stellar/Soroban, with GitHub orgs, websites, and Twitter handles. +See Part 2: Stellar Ecosystem above for a table of teams shipping production code on Stellar, with GitHub orgs, websites, and Twitter handles. ### Foundation - [Stellar Development Foundation](https://stellar.org/foundation) diff --git a/skills/zk-proofs/SKILL.md b/skills/zk-proofs/SKILL.md index 041758b..5b301a5 100644 --- a/skills/zk-proofs/SKILL.md +++ b/skills/zk-proofs/SKILL.md @@ -1,167 +1,234 @@ --- name: zk-proofs -description: Zero-knowledge cryptography and privacy patterns on Stellar/Soroban. Covers Groth16 verification, BLS12-381 (CAP-0059, available), BN254 + Poseidon host functions (CAP-0074/0075, status-sensitive), Noir / RISC Zero integration, privacy pools, confidential tokens, Merkle tree commitments, and status-sensitive guidance for protocol/SDK readiness. Use when building privacy-preserving applications or ZK-verifier contracts on Stellar. +description: Zero-knowledge proofs and privacy patterns on Stellar. Covers Groth16 verification in smart contracts via BLS12-381 host functions (CAP-0059, available), the BN254 + Poseidon proposals (CAP-0074/0075, status-sensitive), and concrete toolchain walkthroughs for Circom (on-chain verifiable today), Noir, and RISC Zero (attestation pattern until BN254 lands). Use when building privacy-preserving applications, ZK-verifier contracts, or wiring a proving toolchain to Stellar. user-invocable: true argument-hint: "[zk task]" --- # Zero-Knowledge Proofs & Privacy -Privacy patterns and ZK verification on Stellar/Soroban. Capability is protocol- and SDK-version dependent — always verify CAP status, network version, and `soroban-sdk` host-function support before relying on a primitive. +ZK verification on Stellar. Capability is protocol- and SDK-version dependent — always verify CAP status, network version, and `soroban-sdk` host-function support before relying on a primitive. ## When to use this skill -- Implementing a Groth16 (or other SNARK) verifier as a Soroban contract -- Using BLS12-381 host functions -- Planning for BN254 / Poseidon (currently proposed via CAP-0074/0075) -- Integrating Noir or RISC Zero proofs +- Implementing a Groth16 (or other SNARK) verifier as a Stellar smart contract +- Wiring Circom, Noir, or RISC Zero output to on-chain verification - Building privacy pools, confidential tokens, or Merkle-tree-backed commitments - -## Status-sensitive — always verify -1. CAP status (`Accepted`/`Implemented` vs draft) -2. Target network software + protocol version -3. `soroban-sdk` release support for the target host functions -4. Available feature flags + graceful fallback paths +- Planning for BN254 / Poseidon availability ## Related skills -- Soroban verifier contract patterns + tests → `../soroban/SKILL.md` -- Confidential-token integration with classic assets → `../assets/SKILL.md` -- Off-chain proof verification UI → `../dapp/SKILL.md` +- Contract patterns and deployment → `../soroban/development.md` +- Verifier security review → `../soroban/security.md` - CAPs referenced here → `../standards/SKILL.md` ---- +## What's available — verify before building + +| Primitive | CAP | Status | +|-----------|-----|--------| +| BLS12-381 ops (G1/G2 add, mul, MSM, pairing check, hash-to-curve, Fr arithmetic) | [CAP-0059](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0059.md) | **Available** (Protocol 22+) | +| BN254 host functions | [CAP-0074](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0074.md) | Proposed — check current status | +| Poseidon/Poseidon2 hash | [CAP-0075](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0075.md) | Proposed — check current status | + +Before implementation, always confirm: +1. CAP status in the preamble (`Accepted`/`Implemented` vs draft) +2. Target network protocol version ([software versions](https://developers.stellar.org/docs/networks/software-versions)) +3. `soroban-sdk` release support for the host functions you need + +**The curve decides everything.** BLS12-381 proofs verify natively on-chain today; BN254 proofs (Circom's default, Barretenberg, RISC Zero's Groth16 wrapper) are gated on CAP-0074. + +| Toolchain | Proof system | Curve | On-chain on Stellar | +|-----------|--------------|-------|---------------------| +| Circom + snarkjs (`-p bls12381`) | Groth16 | BLS12-381 | ✅ Today, via CAP-0059 | +| Circom + snarkjs (default) | Groth16 | BN254 | Gated on CAP-0074 | +| Noir + Barretenberg | UltraHonk | BN254 | Not yet — attest off-chain verification | +| RISC Zero (STARK → Groth16 wrap) | Groth16 | BN254 | Gated on CAP-0074 — attest meanwhile | + +## The on-chain verifier (Groth16 over BLS12-381) + +The official [groth16_verifier example](https://github.com/stellar/soroban-examples/tree/main/groth16_verifier) is the canonical implementation — the full contract: + +```rust +#![no_std] +use soroban_sdk::{ + contract, contracterror, contractimpl, contracttype, + crypto::bls12_381::{Fr, G1Affine, G2Affine}, + vec, Env, Vec, +}; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum Groth16Error { + MalformedVerifyingKey = 0, +} + +#[derive(Clone)] +#[contracttype] +pub struct VerificationKey { + pub alpha: G1Affine, + pub beta: G2Affine, + pub gamma: G2Affine, + pub delta: G2Affine, + pub ic: Vec, +} + +#[derive(Clone)] +#[contracttype] +pub struct Proof { + pub a: G1Affine, + pub b: G2Affine, + pub c: G1Affine, +} + +#[contract] +pub struct Groth16Verifier; + +#[contractimpl] +impl Groth16Verifier { + pub fn verify_proof( + env: Env, + vk: VerificationKey, + proof: Proof, + pub_signals: Vec, + ) -> Result { + let bls = env.crypto().bls12_381(); + + // vk_x = ic[0] + sum(pub_signals[i] * ic[i+1]) + if pub_signals.len() + 1 != vk.ic.len() { + return Err(Groth16Error::MalformedVerifyingKey); + } + let mut vk_x = vk.ic.get(0).unwrap(); + for (s, v) in pub_signals.iter().zip(vk.ic.iter().skip(1)) { + let prod = bls.g1_mul(&v, &s); + vk_x = bls.g1_add(&vk_x, &prod); + } + + // e(-A, B) * e(alpha, beta) * e(vk_x, gamma) * e(C, delta) == 1 + let neg_a = -proof.a; + let vp1 = vec![&env, neg_a, vk.alpha, vk_x, proof.c]; + let vp2 = vec![&env, proof.b, vk.beta, vk.gamma, vk.delta]; + + Ok(bls.pairing_check(vp1, vp2)) + } +} +``` + +Point encodings are uncompressed big-endian: `G1Affine` wraps 96 bytes, `G2Affine` 192 bytes, `Fr` 32 bytes. The example's test suite shows the exact conversion from arkworks types (`ark-bls12-381` + `ark-serialize`) — reuse it when building fixtures from your proving toolchain's JSON output. + +In production, wrap this verifier with application logic: fix the `VerificationKey` at deploy time (constructor) instead of taking it as a call argument, and bind proofs to context (see [Pitfalls](#pitfalls)). + +## Walkthrough: Circom → on-chain verification (works today) + +Circom supports BLS12-381 as a target field — this makes it the toolchain that verifies natively on Stellar right now. + +```bash +# 1. Circuit +cat > multiplier.circom <<'EOF' +pragma circom 2.1.6; +template Multiplier() { + signal input a; + signal input b; + signal output c; + c <== a * b; +} +component main = Multiplier(); +EOF + +# 2. Compile for BLS12-381 (NOT the default bn128 — that's gated on CAP-0074) +circom multiplier.circom --r1cs --wasm -p bls12381 + +# 3. Trusted setup (powers of tau on bls12-381, then circuit-specific phase 2) +snarkjs powersoftau new bls12-381 12 pot12_0000.ptau +snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="contrib" -e="random" +snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau +snarkjs groth16 setup multiplier.r1cs pot12_final.ptau multiplier.zkey +snarkjs zkey export verificationkey multiplier.zkey verification_key.json + +# 4. Witness + proof +echo '{"a": 3, "b": 11}' > input.json +node multiplier_js/generate_witness.js multiplier_js/multiplier.wasm input.json witness.wtns +snarkjs groth16 prove multiplier.zkey witness.wtns proof.json public.json + +# 5. Sanity-check off-chain before going on-chain +snarkjs groth16 verify verification_key.json public.json proof.json +``` + +Then convert `proof.json` / `verification_key.json` (decimal-string coordinates) into the contract's types — serialize each point uncompressed big-endian into the 96/192-byte layouts, e.g. via arkworks as in the example's tests — and invoke `verify_proof`. Public signals (`public.json`) become the `Vec` argument; the contract must also validate what those signals *mean* (see [Pitfalls](#pitfalls)). + +For real applications the per-proof flow is: client proves locally (WASM prover or native), submits `(proof, public_signals)` in a contract invocation, contract verifies + applies policy + updates state. + +## Walkthrough: Noir (off-chain verify + attestation, for now) + +Noir's standard backend (Barretenberg) produces UltraHonk proofs over BN254 — neither the proof system nor the curve is on-chain verifiable on Stellar today. + +```bash +# Local proving workflow +nargo new age_check && cd age_check +cat > src/main.nr <<'EOF' +fn main(age: u64, threshold: pub u64) { + assert(age >= threshold); +} +EOF +nargo check +nargo execute witness # writes the witness from Prover.toml inputs +bb prove -b target/age_check.json -w target/witness.gz -o target/proof +bb verify -k target/vk -p target/proof # off-chain verification +``` + +On Stellar, two patterns until the curve/system gap closes: + +1. **Attestation oracle**: a verifier service runs `bb verify` (or the Noir JS verifier) off-chain and submits a signed attestation; the contract `require_auth()`s the attester address and applies policy. The trust assumption (the attester) must be explicit and documented — this is *not* trustless ZK, it's a verifiable-computation oracle. +2. **Switch the proving stack for on-chain parts**: express the on-chain-critical statement as a Circom/Groth16-BLS12-381 circuit (walkthrough above) and keep Noir for off-chain components. + +Track CAP-0074 (BN254): when implemented, BN254 Groth16 verification becomes possible — but UltraHonk would additionally need a verifier implementation in-contract, so Groth16-based paths will land first. + +## Walkthrough: RISC Zero (same gate, clear path) + +RISC Zero proves arbitrary Rust execution (zkVM) and can wrap its STARK receipts into a Groth16 proof over BN254 ("stark-to-snark") — small enough for on-chain verification where BN254 is supported. + +```rust +// Guest (runs inside the zkVM): the computation being proven +use risc0_zkvm::guest::env; + +fn main() { + let input: u64 = env::read(); + let result = expensive_check(input); + env::commit(&result); // becomes part of the public journal +} +``` + +```rust +// Host: produce and verify a receipt locally +let receipt = prover.prove(env, ELF)?.receipt; +receipt.verify(IMAGE_ID)?; // off-chain verification +``` +On Stellar today, use the **attestation pattern** (as with Noir): verify the receipt off-chain — locally or via a proving service — and have an authorized attester submit the journal + attestation to your contract. Once CAP-0074 (BN254) is implemented, the Groth16-wrapped receipt becomes verifiable natively with a BN254 verifier contract mirroring the BLS12-381 one above; the `IMAGE_ID` (which program ran) and journal digest become public inputs. See the [RISC Zero docs](https://dev.risczero.com/api) for the wrapping workflow. -## When to use this guide -Use this guide when the user asks for: -- On-chain ZK proof verification patterns -- Privacy-preserving smart contract architecture -- BN254/Poseidon readiness planning -- Groth16 or PLONK integration strategy -- Cross-chain proof verification design +## Architecture patterns -This guide is intentionally status-aware. ZK capabilities on Stellar evolve with protocol and SDK releases. +- **Verification gateway**: isolate cryptographic checks in a dedicated verifier contract/module — normalize inputs, verify, emit explicit success/failure events. Smaller audit surface, cleaner upgrades. +- **Policy-and-proof split**: `Verifier` (cryptographic validity) → `Policy` (business/compliance rules) → `Application` (state transition). Each independently testable and upgradeable. +- **Capability gating**: enable ZK flows only where required primitives are confirmed available; keep deterministic fallbacks and document the supported network/protocol matrix. -## Source-of-truth checks (required) -Before implementation, always verify: -1. CAP status in `stellar/stellar-protocol` (`Accepted`/`Implemented` vs draft/awaiting decision) -2. Target network protocol/software version -3. `soroban-sdk` support for required cryptographic host functions -4. Availability of production examples matching your proving system +For Merkle-tree commitments (privacy pools, allowlists): until Poseidon (CAP-0075) lands, in-circuit-friendly hashing on-chain is expensive — design trees so the contract only needs root comparisons and membership proofs verified inside the SNARK. -Primary references: -- [CAP-0059](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0059.md) (BLS12-381) -- [CAP-0074](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0074.md) (BN254 proposal) -- [CAP-0075](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0075.md) (Poseidon/Poseidon2 proposal) -- [Stellar protocol/software versions](https://developers.stellar.org/docs/networks/software-versions) -- [RPC providers](https://developers.stellar.org/docs/data/apis/rpc/providers) +## Pitfalls -## Capability model -Treat advanced cryptography as capability-gated: -- Capability A: proof verification primitive support -- Capability B: hash primitive support -- Capability C: SDK ergonomics and bindings -- Capability D: operational cost envelope +- **Verifying the proof but not the statement.** A valid proof only shows *some* witness satisfies the circuit. The contract must validate the public inputs' semantics: who is this proof for, which Merkle root, which action, which amount. +- **Missing anti-replay binding.** Valid proofs can be replayed. Bind a nonce/session/action into the public inputs and persist a replay guard (nullifier set) on-chain. +- **Curve mismatch.** Circom defaults to bn128; on Stellar compile with `-p bls12381` or your proof will be unverifiable on-chain. +- **Trusted-setup hygiene.** Groth16 needs a circuit-specific phase-2 setup; for production use a real multi-party ceremony, not a single-contributor dev setup. +- **Hardcoded protocol assumptions.** Capability-gate; don't assume draft CAPs are live on the target network. -Do not assume all capabilities are present on all networks/environments. +## Testing -## Architecture patterns +- Unit: input domain validation, replay protection, event correctness, malformed/tampered proof rejection (negative paths are the important ones) +- Integration: full prove → submit → verify → state-transition flow against a local network +- Operational: resource costs for realistic proof sizes via simulation (`--sim-only`) — pairing checks are expensive; budget before committing to per-transaction verification + +## References -### 1) Verification gateway -Use a dedicated verifier contract (or module) for cryptographic checks: -- Normalize and validate inputs -- Enforce domain separation for statements -- Verify proof -- Emit explicit success/failure events - -Benefits: -- Smaller audit surface -- Easier upgrades/migrations -- Cleaner operational telemetry - -### 2) Policy-and-proof split -Separate concerns: -- `Verifier`: cryptographic validity only -- `Policy`: business/risk/compliance logic -- `Application`: state transition after verifier + policy pass - -Benefits: -- Better testability -- Safer upgrades -- Clearer incident response - -### 3) Feature flags and graceful fallback -Gate advanced paths by environment support: -- Enable ZK flows only where required primitives are verified available -- Keep deterministic fallback behavior for unsupported environments -- Document supported network/protocol matrix in deployment notes - -## Integration checklist -- [ ] Target network supports required primitives -- [ ] SDK pin supports required APIs -- [ ] Proof statement includes anti-replay binding (nonce/context) -- [ ] Full simulation path is covered (proof + policy + state transition) -- [ ] Negative-path tests exist for malformed/tampered inputs -- [ ] Resource budget checks are documented for realistic proof sizes -- [ ] Security review documents all cryptographic assumptions - -## Common pitfalls - -### Over-trusting proof payload shape -A payload that parses is not equivalent to a valid statement for your application. - -Mitigation: -- Validate public-input semantics and statement domain explicitly. - -### Missing anti-replay controls -Valid proofs can be replayed without context binding. - -Mitigation: -- Bind proofs to session/nonce/action scope and persist replay guards. - -### Monolithic contract design -Combining verifier, policy, and state logic increases audit complexity. - -Mitigation: -- Keep verifier logic isolated and narrow. - -### Hardcoded protocol assumptions -Assuming primitive availability across all networks causes runtime failures. - -Mitigation: -- Capability-gate and verify at deployment time. - -## Testing strategy - -### Unit tests -- Input domain validation -- Replay protection behavior -- Event correctness - -### Integration tests -- End-to-end proof submission flow -- Negative cases: tampered input, stale nonce, unsupported feature path -- Network-configuration differences (local/testnet/mainnet) - -### Operational tests -- Cost/resource envelope under realistic proof sizes -- Load behavior on verifier hot paths -- Upgrade/migration safety tests for verifier changes - -## Security review focus -- Authorization and anti-replay guarantees -- Statement domain separation -- Upgrade controls around verifier/policy modules -- Denial-of-service resistance and bounded workloads -- Event/log coverage for forensic traceability - -## Example starting points -- [Soroban examples](https://github.com/stellar/soroban-examples) -- [Groth16 verifier example](https://github.com/stellar/soroban-examples/tree/main/groth16_verifier) -- [Security guide](../soroban/SKILL.md) -- [Advanced patterns](../soroban/SKILL.md) -- [Standards reference](../standards/SKILL.md) - -## What not to do -- Do not claim specific primitives are production-ready without checking CAP status and network support. -- Do not hardcode draft-spec behavior as guaranteed runtime behavior. -- Do not skip simulation and negative-path testing for verifier flows. +- [groth16_verifier example](https://github.com/stellar/soroban-examples/tree/main/groth16_verifier) — canonical verifier + arkworks test fixtures +- [soroban-examples](https://github.com/stellar/soroban-examples) +- [BLS12-381 SDK docs](https://docs.rs/soroban-sdk/latest/soroban_sdk/crypto/bls12_381/index.html) +- [Circom docs](https://docs.circom.io) · [snarkjs](https://github.com/iden3/snarkjs) · [Noir docs](https://noir-lang.org/docs) · [RISC Zero docs](https://dev.risczero.com) From 95747a327756da8c7dcf616e9d1a38eaec610238 Mon Sep 17 00:00:00 2001 From: Kaan Kacar Date: Fri, 12 Jun 2026 16:23:17 +0300 Subject: [PATCH 2/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- skills/soroban/testing.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/skills/soroban/testing.md b/skills/soroban/testing.md index 7164632..9cf8911 100644 --- a/skills/soroban/testing.md +++ b/skills/soroban/testing.md @@ -11,9 +11,11 @@ Layers, fastest first: ## Unit testing -```rust #![cfg(test)] -use soroban_sdk::{testutils::Address as _, Address, Env}; +use soroban_sdk::{ + testutils::{Address as _, MockAuth, MockAuthInvoke}, + Address, Env, +}; use crate::{Contract, ContractClient}; #[test] From e53d42e559b14cae8cb2aa0f78ea32ae5e088155 Mon Sep 17 00:00:00 2001 From: Kaan Kacar Date: Fri, 12 Jun 2026 16:23:33 +0300 Subject: [PATCH 3/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- skills/soroban/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/soroban/testing.md b/skills/soroban/testing.md index 9cf8911..13687fe 100644 --- a/skills/soroban/testing.md +++ b/skills/soroban/testing.md @@ -43,7 +43,7 @@ fn test_auth() { let contract_id = env.register(Contract, ()); let client = ContractClient::new(&env, &contract_id); let user = Address::generate(&env); - + let other = Address::generate(&env); // Approve only this specific invocation env.mock_auths(&[MockAuth { address: &user, From 0500ebe82ee60831ae9b28d80b7c813f31903c21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:25:49 +0000 Subject: [PATCH 4/4] fix: update --cached check to compare newest mtime across entire skill directory --- site/scripts/copy-skills.mjs | 39 +++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/site/scripts/copy-skills.mjs b/site/scripts/copy-skills.mjs index 31b3444..112bbca 100644 --- a/site/scripts/copy-skills.mjs +++ b/site/scripts/copy-skills.mjs @@ -18,6 +18,7 @@ import { cpSync, existsSync, mkdirSync, + readdirSync, readFileSync, rmSync, statSync, @@ -55,20 +56,40 @@ if (sources.length === 0) { process.exit(1); } -// `--cached` skips the copy when every dest exists AND every dest is at -// least as fresh as its upstream source. Without the mtime check, editing -// a SKILL.md during `pnpm dev` wouldn't show up until the next manual -// `pnpm sync:skills`. +// Returns the newest mtime (ms) among all files under `dir`, or 0 if the +// directory does not exist / is empty. +const newestMtimeInDir = (dir) => { + if (!existsSync(dir)) return 0; + let newest = 0; + const scan = (d) => { + for (const entry of readdirSync(d, { withFileTypes: true })) { + const fullPath = join(d, entry.name); + if (entry.isDirectory()) { + scan(fullPath); + } else { + const mtime = statSync(fullPath).mtimeMs; + if (mtime > newest) newest = mtime; + } + } + }; + scan(dir); + return newest; +}; + +// `--cached` skips the copy when every dest skill directory exists AND its +// newest file is at least as fresh as the newest file in the upstream source +// directory. Comparing directories (not just SKILL.md) ensures that edits to +// companion files like testing.md also trigger a refresh during `pnpm dev`. const isFresh = (source) => { - const dest = join(PUBLIC_DIR, source); - if (!existsSync(dest)) return false; - const src = join(REPO_ROOT, source); - if (!existsSync(src)) { + const srcDir = dirname(join(REPO_ROOT, source)); + const destDir = dirname(join(PUBLIC_DIR, source)); + if (!existsSync(destDir)) return false; + if (!existsSync(srcDir)) { // Upstream missing; --lenient will warn later. Treat as fresh so we // don't trigger a full re-copy just to discover the same missing file. return true; } - return statSync(dest).mtimeMs >= statSync(src).mtimeMs; + return newestMtimeInDir(destDir) >= newestMtimeInDir(srcDir); }; if (cached && sources.every(isFresh)) {