From cab98aee0930584fe6bf946d33366f01a1fc0a46 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Sun, 24 May 2026 09:30:00 +0100 Subject: [PATCH 01/38] build: add Makefile with build, test, lint, deploy, and wasm-size targets --- Makefile | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..499dc2c --- /dev/null +++ b/Makefile @@ -0,0 +1,71 @@ +NETWORK ?= testnet +SOURCE ?= deployer +WASM_DIR := contracts/target/wasm32v1-none/release + +.PHONY: build test test-verbose lint fmt clean deploy check-tools + +## Build all contracts for release (WASM) +build: + @echo "Building contracts..." + cd contracts && cargo build --target wasm32v1-none --release --quiet + @echo "Build complete. WASMs in $(WASM_DIR)/" + +## Build with debug assertions (for detailed error messages during dev) +build-debug: + cd contracts && cargo build --target wasm32v1-none --profile release-with-logs + +## Run all contract tests +test: + cd contracts && cargo test --quiet 2>&1 | tail -20 + +## Run tests with full output +test-verbose: + cd contracts && cargo test -- --nocapture + +## Run tests for a specific contract +test-oracle: + cd contracts && cargo test -p parashield-oracle-verifier -- --nocapture + +test-policy: + cd contracts && cargo test -p parashield-policy-engine -- --nocapture + +test-claims: + cd contracts && cargo test -p parashield-claims-processor -- --nocapture + +test-pool: + cd contracts && cargo test -p parashield-risk-pool -- --nocapture + +test-dao: + cd contracts && cargo test -p parashield-governance-dao -- --nocapture + +## Run Clippy linter +lint: + cd contracts && cargo clippy --all-targets -- -D warnings + +## Format Rust source +fmt: + cd contracts && cargo fmt --all + +## Check formatting without modifying files +fmt-check: + cd contracts && cargo fmt --all -- --check + +## Remove build artifacts +clean: + cd contracts && cargo clean + +## Deploy to testnet (requires stellar CLI and funded deployer key) +deploy: + ./scripts/deploy_testnet.sh + +## Check required tools are installed +check-tools: + @which stellar > /dev/null || (echo "Error: stellar CLI not found. Install from https://github.com/stellar/stellar-cli" && exit 1) + @which cargo > /dev/null || (echo "Error: cargo not found. Install Rust from https://rustup.rs" && exit 1) + @rustup target list --installed | grep -q wasm32v1-none || (echo "Adding wasm32v1-none target..." && rustup target add wasm32v1-none) + @echo "All required tools found." + +## Print sizes of compiled WASMs +wasm-sizes: build + @echo "Contract WASM sizes:" + @ls -lh $(WASM_DIR)/*.wasm 2>/dev/null || echo "No WASMs built yet. Run 'make build' first." From 1cb8247258f0caa9ddde9b1a0ca705aceedb1862 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Sun, 24 May 2026 14:00:00 +0100 Subject: [PATCH 02/38] docs: add ARCHITECTURE.md covering contract map, data flow, oracle keys, and risk pool economics --- ARCHITECTURE.md | 127 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 ARCHITECTURE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..a9eb00b --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,127 @@ +# Parashield Protocol Architecture + +## Overview + +Parashield is a decentralised parametric insurance protocol built on Stellar Soroban. +Unlike traditional insurance, claims are settled automatically by smart contracts +when a real-world trigger condition is confirmed by an oracle network. +No adjuster. No form. No delay. + +## Contract Map + +``` +┌──────────────────────────────────────────────────────────┐ +│ User / DApp │ +└────────────────────┬──────────────────────────────────────┘ + │ buy_policy / submit_claim + ▼ +┌──────────────────────────────────────────────────────────┐ +│ Policy Engine (policy-engine) │ +│ - Products catalogue (admin-defined) │ +│ - Policy lifecycle: Active → Claimed / Expired │ +│ - Holds USDC escrow until payout or expiry │ +└──────┬──────────────────────────────────┬────────────────┘ + │ get_policy / pay_claim / │ get_contract_balance + │ expire_policy │ + ▼ ▼ +┌─────────────────────┐ ┌─────────────────────────┐ +│ Claims Processor │ │ Risk Pool │ +│ (claims-processor) │ │ (risk-pool) │ +│ │ │ - LP deposits USDC │ +│ - Evaluates oracle │ │ - Pool tokens (shares) │ +│ - Calls pay_claim │ │ - Yield from premiums │ +│ or expire_policy │ │ - Locks coverage cap. │ +└──────┬──────────────┘ └─────────────────────────┘ + │ verify_trigger + ▼ +┌──────────────────────────────────────────────────────────┐ +│ Oracle Verifier (oracle-verifier) │ +│ - Multiple oracles submit signed observations │ +│ - Confidence-weighted median aggregation │ +│ - verify_trigger: returns bool for trigger condition │ +└──────────────────────────────────────────────────────────┘ + + Admin / Token Holders + │ + ▼ + ┌────────────────────┐ + │ Governance DAO │ + │ (governance-dao) │ + │ - Proposals │ + │ - Token voting │ + │ - Protocol params │ + └────────────────────┘ +``` + +## Data Flow: Parametric Payout + +``` +1. Admin creates InsuranceProduct (oracle key, threshold, comparison) +2. User calls buy_policy(product_id, coverage_amount, duration_days, oracle_key) + - Premium = coverage * premium_rate_bps / 10_000 + - USDC premium transferred from user to Policy Engine + - Policy record created with status = Active +3. Oracle(s) submit data via oracle-verifier.submit_data() periodically +4. Keeper calls claims-processor.auto_process(policy_id) + - Claims Processor calls oracle-verifier.verify_trigger(condition) + - If trigger met: Policy Engine.pay_claim() → USDC → policyholder + - If trigger not met AND policy expired: Policy Engine.expire_policy() +``` + +## Fixed-Point Math + +All monetary values use 7-decimal fixed point matching Stellar's native precision: + +| Display value | On-chain representation | +|---------------|------------------------| +| 1 USDC | 10_000_000 | +| 50.5 mm rain | 505_000_000 | +| 120 min delay | 1_200_000_000 | + +## Oracle Key Format + +Oracle keys follow a structured naming convention (max 9 chars = Soroban Symbol): + +| Data type | Key format | Example | +|-------------|-------------------------------|-------------| +| Rainfall | `{loc}{yyyymm}` | `kis2606` | +| Temperature | `tmp{loc}{mm}` | `tmpkis06` | +| Flight | `fl{flight}{dd}` | `flkq10015` | +| Wind speed | `wnd{loc}{mm}` | `wndmom06` | +| DeFi event | `defi{proto}` | `defiave` | + +## Risk Pool Economics (v2) + +``` +Premium flow: + 80% → Risk Pool (LP yield) + 10% → Protocol Treasury (governance-controlled) + 10% → Backstop Fund (solvency reserve) + +Utilization rate = total_active_coverage / total_deposited_liquidity + +Target APY ranges: + Low-risk pools (crop, flight): 8–15% + Medium-risk (disaster): 15–25% + High-risk (DeFi exploit): 25–40% +``` + +## Governance DAO (v2) + +SHIELD token holders govern protocol parameters: + +- Add / remove insurance products +- Adjust premium rates and trigger thresholds +- Register / deregister oracle sources +- Allocate protocol treasury funds +- Emergency pause individual contracts + +**Proposal lifecycle:** Draft → Active (7-day voting) → Passed (≥10% quorum, simple majority) → Executed (2-day timelock) + +## Security Notes + +- Admin keys should transition to Governance DAO after protocol launch +- Oracle submissions are bounded by registered oracle set (not open) +- Policy Engine holds USDC in escrow: no admin withdrawal function +- Claims Processor is the only address authorized to call `pay_claim` / `expire_policy` +- All monetary arithmetic uses checked arithmetic (Soroban default with overflow-checks = true) From b7f28915975744df7abcbe2248c97c5ff1aaeef7 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Mon, 25 May 2026 09:00:00 +0100 Subject: [PATCH 03/38] chore: add version 0.2.0, authors, description, keywords, and license to all crate manifests --- contracts/claims-processor/Cargo.toml | 21 +++++++++++++-------- contracts/governance-dao/Cargo.toml | 13 +++++++++---- contracts/oracle-verifier/Cargo.toml | 13 +++++++++---- contracts/policy-engine/Cargo.toml | 13 +++++++++---- contracts/risk-pool/Cargo.toml | 13 +++++++++---- 5 files changed, 49 insertions(+), 24 deletions(-) diff --git a/contracts/claims-processor/Cargo.toml b/contracts/claims-processor/Cargo.toml index 9442a55..c9c7092 100644 --- a/contracts/claims-processor/Cargo.toml +++ b/contracts/claims-processor/Cargo.toml @@ -1,8 +1,13 @@ [package] -name = "parashield-claims-processor" -version = "0.1.0" -edition = "2021" -publish = false +name = "parashield-claims-processor" +version = "0.2.0" +edition = "2021" +description = "Automatic claim evaluation and settlement engine for Parashield Protocol" +authors = ["Parashield Protocol "] +keywords = ["soroban", "stellar", "claims", "insurance", "defi"] +categories = ["cryptography::cryptocurrencies"] +license = "MIT" +publish = false [lib] crate-type = ["cdylib", "rlib"] @@ -11,11 +16,11 @@ crate-type = ["cdylib", "rlib"] testutils = ["soroban-sdk/testutils"] [dependencies] -soroban-sdk = { workspace = true } -parashield-policy-engine = { path = "../policy-engine" } +soroban-sdk = { workspace = true } +parashield-policy-engine = { path = "../policy-engine" } parashield-oracle-verifier = { path = "../oracle-verifier" } [dev-dependencies] -soroban-sdk = { workspace = true, features = ["testutils"] } -parashield-policy-engine = { path = "../policy-engine", features = ["testutils"] } +soroban-sdk = { workspace = true, features = ["testutils"] } +parashield-policy-engine = { path = "../policy-engine", features = ["testutils"] } parashield-oracle-verifier = { path = "../oracle-verifier", features = ["testutils"] } diff --git a/contracts/governance-dao/Cargo.toml b/contracts/governance-dao/Cargo.toml index 9187a85..e8c4947 100644 --- a/contracts/governance-dao/Cargo.toml +++ b/contracts/governance-dao/Cargo.toml @@ -1,8 +1,13 @@ [package] -name = "parashield-governance-dao" -version = "0.1.0" -edition = "2021" -publish = false +name = "parashield-governance-dao" +version = "0.2.0" +edition = "2021" +description = "Token-weighted governance DAO for Parashield Protocol parameter management" +authors = ["Parashield Protocol "] +keywords = ["soroban", "stellar", "governance", "dao", "defi"] +categories = ["cryptography::cryptocurrencies"] +license = "MIT" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contracts/oracle-verifier/Cargo.toml b/contracts/oracle-verifier/Cargo.toml index eaff56e..6abeb75 100644 --- a/contracts/oracle-verifier/Cargo.toml +++ b/contracts/oracle-verifier/Cargo.toml @@ -1,8 +1,13 @@ [package] -name = "parashield-oracle-verifier" -version = "0.1.0" -edition = "2021" -publish = false +name = "parashield-oracle-verifier" +version = "0.2.0" +edition = "2021" +description = "Multi-oracle data aggregation and trigger verification for Parashield Protocol" +authors = ["Parashield Protocol "] +keywords = ["soroban", "stellar", "oracle", "insurance", "defi"] +categories = ["cryptography::cryptocurrencies"] +license = "MIT" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contracts/policy-engine/Cargo.toml b/contracts/policy-engine/Cargo.toml index 7ba51f2..a251a69 100644 --- a/contracts/policy-engine/Cargo.toml +++ b/contracts/policy-engine/Cargo.toml @@ -1,8 +1,13 @@ [package] -name = "parashield-policy-engine" -version = "0.1.0" -edition = "2021" -publish = false +name = "parashield-policy-engine" +version = "0.2.0" +edition = "2021" +description = "Insurance product catalogue and policy lifecycle management for Parashield Protocol" +authors = ["Parashield Protocol "] +keywords = ["soroban", "stellar", "insurance", "policy", "defi"] +categories = ["cryptography::cryptocurrencies"] +license = "MIT" +publish = false [lib] crate-type = ["cdylib", "rlib"] diff --git a/contracts/risk-pool/Cargo.toml b/contracts/risk-pool/Cargo.toml index c14dea4..380fcd0 100644 --- a/contracts/risk-pool/Cargo.toml +++ b/contracts/risk-pool/Cargo.toml @@ -1,8 +1,13 @@ [package] -name = "parashield-risk-pool" -version = "0.1.0" -edition = "2021" -publish = false +name = "parashield-risk-pool" +version = "0.2.0" +edition = "2021" +description = "Liquidity provider risk pools with yield distribution for Parashield Protocol" +authors = ["Parashield Protocol "] +keywords = ["soroban", "stellar", "liquidity", "yield", "defi"] +categories = ["cryptography::cryptocurrencies"] +license = "MIT" +publish = false [lib] crate-type = ["cdylib", "rlib"] From d318797c039cf4cae661d4e2b8198c8c1351130f Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Mon, 25 May 2026 09:15:00 +0100 Subject: [PATCH 04/38] =?UTF-8?q?feat(risk-pool):=20add=20types=20?= =?UTF-8?q?=E2=80=94=20LpPosition,=20CapitalLock,=20PoolStats,=20PoolStatu?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/risk-pool/src/types.rs | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 contracts/risk-pool/src/types.rs diff --git a/contracts/risk-pool/src/types.rs b/contracts/risk-pool/src/types.rs new file mode 100644 index 0000000..b6271c2 --- /dev/null +++ b/contracts/risk-pool/src/types.rs @@ -0,0 +1,49 @@ +use soroban_sdk::{contracttype, Address, Symbol}; + +/// Status of a risk pool. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PoolStatus { + Active, + Paused, + /// No new deposits; existing LPs can withdraw + WindingDown, +} + +/// A liquidity provider's position in the pool. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LpPosition { + pub provider: Address, + /// Amount of USDC deposited (7-decimal stroops) + pub deposited: i128, + /// Pool-share tokens held (7-decimal, proportional to ownership) + pub shares: i128, + /// Total accumulated premium yield already claimed by this LP + pub yield_claimed: i128, + pub deposited_at: u64, + pub last_yield_claim: u64, +} + +/// A capital lock placed on the pool when a policy is active. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CapitalLock { + pub policy_id: u128, + pub amount: i128, + pub locked_at: u64, + pub released: bool, +} + +/// Aggregate pool stats exposed via queries. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PoolStats { + /// Category: "crop" | "flight" | "disaster" | "defi" + pub category: Symbol, + pub total_deposited: i128, + pub total_locked: i128, + pub total_shares: i128, + pub accumulated_premium: i128, + pub status: PoolStatus, +} From 4a82e4361539f85636d2c6a52e29cb847545b977 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Mon, 25 May 2026 14:30:00 +0100 Subject: [PATCH 05/38] feat(risk-pool): implement initialize, deposit with proportional share minting --- contracts/risk-pool/src/lib.rs | 311 +++++++++++++++++++++++++++++---- 1 file changed, 281 insertions(+), 30 deletions(-) diff --git a/contracts/risk-pool/src/lib.rs b/contracts/risk-pool/src/lib.rs index b00896c..4cac67e 100644 --- a/contracts/risk-pool/src/lib.rs +++ b/contracts/risk-pool/src/lib.rs @@ -1,56 +1,307 @@ -//! Parashield Risk Pool — v2 (not yet implemented) +//! Parashield Risk Pool //! -//! Liquidity providers deposit USDC into categorised risk pools. -//! Pool tokens (Stellar assets on the built-in DEX) represent LP shares. -//! Premiums flow in as yield; approved claims reduce pool balance. -//! -//! Full design: see ARCHITECTURE.md § Risk Pool +//! Liquidity providers deposit USDC into category-specific risk pools. +//! Pool-share tokens represent proportional ownership. //! //! Economics //! ────────── -//! Premium flow: 80% → pool (LP yield), 10% → protocol treasury, 10% → backstop fund -//! Claims flow: settled from pool balance; LP share value decreases proportionally -//! Target APY: 8-15% for low-risk pools, 20-40% for high-risk +//! - Premium flow: 80% pool yield, 10% protocol treasury, 10% backstop fund +//! - Claims flow: coverage settled from pool balance; LP share value decreases +//! - Utilization rate = total_locked / total_deposited +//! - Target APY: 8-40% depending on risk category +//! +//! v2 — full implementation; Risk Pool is now deployable and testable. #![no_std] -use soroban_sdk::{contract, contractimpl, Address, Env}; +use soroban_sdk::{ + contract, contractimpl, contracttype, contracterror, panic_with_error, + token, Address, Env, Symbol, Vec, +}; + +pub mod types; +pub use types::*; + +const PREMIUM_LP_BPS: i128 = 8_000; // 80% of premium to LP pool +const PREMIUM_TREAS_BPS: i128 = 1_000; // 10% to treasury + +#[contracttype] +enum StorageKey { + Initialized, + Admin, + Treasury, + UsdcToken, + Category, + TotalDeposited, + TotalLocked, + TotalShares, + AccumulatedPremium, + Status, + LpPosition(Address), + LpList, + Lock(u128), +} + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum Error { + AlreadyInitialized = 1, + NotInitialized = 2, + Unauthorized = 3, + InsufficientFunds = 4, + ZeroAmount = 5, + PoolNotActive = 6, + NoShares = 7, + AlreadyLocked = 8, + LockNotFound = 9, + AlreadyReleased = 10, + Undercollateralized = 11, +} #[contract] pub struct RiskPool; #[contractimpl] impl RiskPool { - pub fn initialize(_env: Env, _admin: Address) { - unimplemented!("RiskPool is scheduled for v2. See ARCHITECTURE.md for design.") + + pub fn initialize( + env: Env, + admin: Address, + usdc_token: Address, + treasury: Address, + category: Symbol, + ) { + if env.storage().instance().has(&StorageKey::Initialized) { + panic_with_error!(&env, Error::AlreadyInitialized); + } + admin.require_auth(); + env.storage().instance().set(&StorageKey::Initialized, &true); + env.storage().instance().set(&StorageKey::Admin, &admin); + env.storage().instance().set(&StorageKey::UsdcToken, &usdc_token); + env.storage().instance().set(&StorageKey::Treasury, &treasury); + env.storage().instance().set(&StorageKey::Category, &category); + env.storage().instance().set(&StorageKey::TotalDeposited, &0i128); + env.storage().instance().set(&StorageKey::TotalLocked, &0i128); + env.storage().instance().set(&StorageKey::TotalShares, &0i128); + env.storage().instance().set(&StorageKey::AccumulatedPremium, &0i128); + env.storage().instance().set(&StorageKey::Status, &PoolStatus::Active); + env.storage().instance().set(&StorageKey::LpList, &Vec::
::new(&env)); } - /// Deposit USDC into the pool and receive pool-share tokens. - pub fn deposit(_env: Env, _provider: Address, _amount: i128) -> u128 { - unimplemented!() + // ── Deposits ────────────────────────────────────────────────────────────── + + pub fn deposit(env: Env, provider: Address, amount: i128) -> i128 { + provider.require_auth(); + if amount <= 0 { panic_with_error!(&env, Error::ZeroAmount); } + Self::assert_active(&env); + + let total_deposited: i128 = env.storage().instance() + .get(&StorageKey::TotalDeposited).unwrap_or(0); + let total_shares: i128 = env.storage().instance() + .get(&StorageKey::TotalShares).unwrap_or(0); + + let new_shares = if total_deposited == 0 || total_shares == 0 { + amount // 1 share = 1 USDC at initialization + } else { + amount * total_shares / total_deposited + }; + + let usdc: Address = env.storage().instance().get(&StorageKey::UsdcToken).unwrap(); + token::Client::new(&env, &usdc) + .transfer(&provider, &env.current_contract_address(), &amount); + + let now = env.ledger().timestamp(); + let lp_key = StorageKey::LpPosition(provider.clone()); + let position: LpPosition = match env.storage().persistent().get(&lp_key) { + Some(mut pos) => { + pos.deposited += amount; + pos.shares += new_shares; + pos + } + None => { + let mut lp_list: Vec
= env.storage().instance() + .get(&StorageKey::LpList).unwrap_or_else(|| Vec::new(&env)); + lp_list.push_back(provider.clone()); + env.storage().instance().set(&StorageKey::LpList, &lp_list); + LpPosition { + provider: provider.clone(), + deposited: amount, + shares: new_shares, + yield_claimed: 0, + deposited_at: now, + last_yield_claim: now, + } + } + }; + env.storage().persistent().set(&lp_key, &position); + env.storage().instance().set(&StorageKey::TotalDeposited, &(total_deposited + amount)); + env.storage().instance().set(&StorageKey::TotalShares, &(total_shares + new_shares)); + + new_shares } - /// Burn pool-share tokens and withdraw USDC. - pub fn withdraw(_env: Env, _provider: Address, _shares: u128) -> i128 { - unimplemented!() + pub fn withdraw(env: Env, provider: Address, shares: i128) -> i128 { + provider.require_auth(); + if shares <= 0 { panic_with_error!(&env, Error::ZeroAmount); } + + let lp_key = StorageKey::LpPosition(provider.clone()); + let mut position: LpPosition = env.storage().persistent() + .get(&lp_key) + .unwrap_or_else(|| panic_with_error!(&env, Error::NoShares)); + if position.shares < shares { panic_with_error!(&env, Error::InsufficientFunds); } + + let total_deposited: i128 = env.storage().instance().get(&StorageKey::TotalDeposited).unwrap_or(0); + let total_shares: i128 = env.storage().instance().get(&StorageKey::TotalShares).unwrap_or(0); + let total_locked: i128 = env.storage().instance().get(&StorageKey::TotalLocked).unwrap_or(0); + + let amount = shares * total_deposited / total_shares; + let available = total_deposited - total_locked; + if amount > available { panic_with_error!(&env, Error::Undercollateralized); } + + let usdc: Address = env.storage().instance().get(&StorageKey::UsdcToken).unwrap(); + token::Client::new(&env, &usdc) + .transfer(&env.current_contract_address(), &provider, &amount); + + position.deposited = position.deposited.saturating_sub(amount); + position.shares -= shares; + env.storage().persistent().set(&lp_key, &position); + env.storage().instance().set(&StorageKey::TotalDeposited, &(total_deposited - amount)); + env.storage().instance().set(&StorageKey::TotalShares, &(total_shares - shares)); + + amount } - /// Harvest accumulated premium yield. - pub fn claim_yield(_env: Env, _provider: Address) -> i128 { - unimplemented!() + // ── Premium and yield ───────────────────────────────────────────────────── + + pub fn receive_premium(env: Env, caller: Address, amount: i128) { + caller.require_auth(); + if amount <= 0 { return; } + let usdc: Address = env.storage().instance().get(&StorageKey::UsdcToken).unwrap(); + token::Client::new(&env, &usdc) + .transfer(&caller, &env.current_contract_address(), &amount); + + let lp_share = amount * PREMIUM_LP_BPS / 10_000; + let treas_share = amount * PREMIUM_TREAS_BPS / 10_000; + + let treasury: Address = env.storage().instance().get(&StorageKey::Treasury).unwrap(); + token::Client::new(&env, &usdc) + .transfer(&env.current_contract_address(), &treasury, &treas_share); + + let acc: i128 = env.storage().instance() + .get(&StorageKey::AccumulatedPremium).unwrap_or(0); + env.storage().instance().set(&StorageKey::AccumulatedPremium, &(acc + lp_share)); } - /// Called by Policy Engine when a policy is created — locks capital. - pub fn lock_for_policy(_env: Env, _caller: Address, _policy_id: u128, _amount: i128) { - unimplemented!() + pub fn claim_yield(env: Env, provider: Address) -> i128 { + provider.require_auth(); + let lp_key = StorageKey::LpPosition(provider.clone()); + let mut position: LpPosition = env.storage().persistent() + .get(&lp_key) + .unwrap_or_else(|| panic_with_error!(&env, Error::NoShares)); + + let total_shares: i128 = env.storage().instance().get(&StorageKey::TotalShares).unwrap_or(0); + let accumulated: i128 = env.storage().instance().get(&StorageKey::AccumulatedPremium).unwrap_or(0); + if total_shares == 0 { return 0; } + + let entitled = accumulated * position.shares / total_shares; + let claimable = entitled.saturating_sub(position.yield_claimed); + if claimable == 0 { return 0; } + + let usdc: Address = env.storage().instance().get(&StorageKey::UsdcToken).unwrap(); + token::Client::new(&env, &usdc) + .transfer(&env.current_contract_address(), &provider, &claimable); + + position.yield_claimed += claimable; + position.last_yield_claim = env.ledger().timestamp(); + env.storage().persistent().set(&lp_key, &position); + + claimable + } + + // ── Capital locks ───────────────────────────────────────────────────────── + + pub fn lock_for_policy(env: Env, caller: Address, policy_id: u128, amount: i128) { + caller.require_auth(); + let total_deposited: i128 = env.storage().instance().get(&StorageKey::TotalDeposited).unwrap_or(0); + let total_locked: i128 = env.storage().instance().get(&StorageKey::TotalLocked).unwrap_or(0); + if total_deposited - total_locked < amount { panic_with_error!(&env, Error::Undercollateralized); } + if env.storage().persistent().has(&StorageKey::Lock(policy_id)) { panic_with_error!(&env, Error::AlreadyLocked); } + + env.storage().persistent().set(&StorageKey::Lock(policy_id), &CapitalLock { + policy_id, + amount, + locked_at: env.ledger().timestamp(), + released: false, + }); + env.storage().instance().set(&StorageKey::TotalLocked, &(total_locked + amount)); + } + + pub fn release_for_claim(env: Env, caller: Address, policy_id: u128) { + caller.require_auth(); + let mut lock: CapitalLock = env.storage().persistent() + .get(&StorageKey::Lock(policy_id)) + .unwrap_or_else(|| panic_with_error!(&env, Error::LockNotFound)); + if lock.released { panic_with_error!(&env, Error::AlreadyReleased); } + lock.released = true; + env.storage().persistent().set(&StorageKey::Lock(policy_id), &lock); + let total_locked: i128 = env.storage().instance().get(&StorageKey::TotalLocked).unwrap_or(0); + env.storage().instance().set(&StorageKey::TotalLocked, &(total_locked.saturating_sub(lock.amount))); + } + + // ── Queries ─────────────────────────────────────────────────────────────── + + pub fn get_stats(env: Env) -> PoolStats { + PoolStats { + category: env.storage().instance().get(&StorageKey::Category).unwrap(), + total_deposited: env.storage().instance().get(&StorageKey::TotalDeposited).unwrap_or(0), + total_locked: env.storage().instance().get(&StorageKey::TotalLocked).unwrap_or(0), + total_shares: env.storage().instance().get(&StorageKey::TotalShares).unwrap_or(0), + accumulated_premium: env.storage().instance().get(&StorageKey::AccumulatedPremium).unwrap_or(0), + status: env.storage().instance().get(&StorageKey::Status).unwrap_or(PoolStatus::Active), + } } - /// Called by Claims Processor after payout — reduces locked capital. - pub fn release_for_claim(_env: Env, _caller: Address, _policy_id: u128, _amount: i128) { - unimplemented!() + pub fn get_position(env: Env, provider: Address) -> Option { + env.storage().persistent().get(&StorageKey::LpPosition(provider)) } - /// Called when a policy expires with no claim — returns locked capital to pool. - pub fn release_expired(_env: Env, _caller: Address, _policy_id: u128) { - unimplemented!() + pub fn get_utilization_rate(env: Env) -> u32 { + let deposited: i128 = env.storage().instance().get(&StorageKey::TotalDeposited).unwrap_or(0); + let locked: i128 = env.storage().instance().get(&StorageKey::TotalLocked).unwrap_or(0); + if deposited == 0 { return 0; } + (locked * 10_000 / deposited) as u32 + } + + pub fn get_admin(env: Env) -> Address { + env.storage().instance().get(&StorageKey::Admin) + .unwrap_or_else(|| panic_with_error!(&env, Error::NotInitialized)) + } + + // ── Admin ───────────────────────────────────────────────────────────────── + + pub fn pause(env: Env, admin: Address) { + Self::require_admin(&env, &admin); + env.storage().instance().set(&StorageKey::Status, &PoolStatus::Paused); + } + + pub fn resume(env: Env, admin: Address) { + Self::require_admin(&env, &admin); + env.storage().instance().set(&StorageKey::Status, &PoolStatus::Active); + } + + fn require_admin(env: &Env, caller: &Address) { + let admin: Address = env.storage().instance().get(&StorageKey::Admin) + .unwrap_or_else(|| panic_with_error!(env, Error::NotInitialized)); + if *caller != admin { panic_with_error!(env, Error::Unauthorized); } + caller.require_auth(); + } + + fn assert_active(env: &Env) { + let status: PoolStatus = env.storage().instance() + .get(&StorageKey::Status).unwrap_or(PoolStatus::Active); + if status != PoolStatus::Active { panic_with_error!(env, Error::PoolNotActive); } } } + +#[cfg(test)] +mod test; From cf3323c46d6d5771497f1612d8325c289718bd26 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Tue, 26 May 2026 10:00:00 +0100 Subject: [PATCH 06/38] test(risk-pool): add deposit, withdraw, yield, lock, and pause test suite --- contracts/risk-pool/src/test.rs | 227 ++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 contracts/risk-pool/src/test.rs diff --git a/contracts/risk-pool/src/test.rs b/contracts/risk-pool/src/test.rs new file mode 100644 index 0000000..14826a5 --- /dev/null +++ b/contracts/risk-pool/src/test.rs @@ -0,0 +1,227 @@ +#![cfg(test)] + +extern crate std; + +use soroban_sdk::{ + testutils::{Address as _, Ledger}, + token, Address, Env, Symbol, +}; + +use crate::{Error, RiskPool, RiskPoolClient}; + +// ── helpers ─────────────────────────────────────────────────────────────────── + +fn setup() -> (Env, RiskPoolClient<'static>, Address, Address, Address, Address) { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + let lp1 = Address::generate(&env); + + let usdc_id = env.register_stellar_asset_contract_v2(admin.clone()).address(); + let pool_id = env.register(RiskPool, ()); + let pool = RiskPoolClient::new(&env, &pool_id); + + let usdc_admin_client = token::StellarAssetClient::new(&env, &usdc_id); + usdc_admin_client.mint(&lp1, &1_000_000_000_0000000i128); + + pool.initialize( + &admin, + &usdc_id, + &treasury, + &Symbol::new(&env, "crop"), + ); + + (env, pool, usdc_id, admin, treasury, lp1) +} + +fn ledger_ts(env: &Env) -> u64 { + env.ledger().timestamp() +} + +// ── initialization ──────────────────────────────────────────────────────────── + +#[test] +fn initialize_sets_state() { + let (_, pool, _, _, _, _) = setup(); + let stats = pool.get_stats(); + assert_eq!(stats.total_deposited, 0); + assert_eq!(stats.total_shares, 0); + assert_eq!(stats.total_locked, 0); +} + +#[test] +#[should_panic(expected = "Error(Contract, #1)")] +fn cannot_initialize_twice() { + let (env, pool, usdc, admin, treasury, _) = setup(); + pool.initialize(&admin, &usdc, &treasury, &Symbol::new(&env, "crop")); +} + +// ── deposits ────────────────────────────────────────────────────────────────── + +#[test] +fn first_deposit_mints_one_to_one_shares() { + let (_, pool, _, _, _, lp1) = setup(); + let shares = pool.deposit(&lp1, &500_000_0000000i128); + assert_eq!(shares, 500_000_0000000i128); + + let stats = pool.get_stats(); + assert_eq!(stats.total_deposited, 500_000_0000000i128); + assert_eq!(stats.total_shares, 500_000_0000000i128); +} + +#[test] +fn second_deposit_proportional_shares() { + let (env, pool, usdc_id, admin, _, lp1) = setup(); + let lp2 = Address::generate(&env); + token::StellarAssetClient::new(&env, &usdc_id).mint(&lp2, &500_000_0000000i128); + + pool.deposit(&lp1, &500_000_0000000i128); + let shares2 = pool.deposit(&lp2, &250_000_0000000i128); + // shares2 should be half of lp1's shares + assert_eq!(shares2, 250_000_0000000i128); +} + +#[test] +fn utilization_zero_before_locks() { + let (_, pool, _, _, _, lp1) = setup(); + pool.deposit(&lp1, &1_000_0000000i128); + assert_eq!(pool.get_utilization_rate(), 0); +} + +// ── withdrawals ─────────────────────────────────────────────────────────────── + +#[test] +fn withdraw_full_position() { + let (_, pool, _, _, _, lp1) = setup(); + let amount = 400_0000000i128; + let shares = pool.deposit(&lp1, &amount); + let returned = pool.withdraw(&lp1, &shares); + assert_eq!(returned, amount); + + let stats = pool.get_stats(); + assert_eq!(stats.total_deposited, 0); +} + +#[test] +#[should_panic(expected = "Error(Contract, #7)")] +fn withdraw_without_position_fails() { + let (env, pool, _, _, _, _) = setup(); + let stranger = Address::generate(&env); + pool.withdraw(&stranger, &1_0000000i128); +} + +#[test] +#[should_panic(expected = "Error(Contract, #11)")] +fn withdraw_locked_capital_fails() { + let (env, pool, _, admin, _, lp1) = setup(); + let amount = 100_0000000i128; + let shares = pool.deposit(&lp1, &amount); + + pool.lock_for_policy(&admin, &1u128, &amount); // lock all capital + pool.withdraw(&lp1, &shares); // should fail +} + +// ── premium routing ──────────────────────────────────────────────────────────── + +#[test] +fn receive_premium_adds_lp_share() { + let (_, pool, _, _, _, lp1) = setup(); + pool.deposit(&lp1, &1_000_0000000i128); + + let before = pool.get_stats().accumulated_premium; + pool.receive_premium(&lp1, &100_0000000i128); + let after = pool.get_stats().accumulated_premium; + // 80% goes to LP accumulated + assert!(after > before); + assert_eq!(after - before, 80_0000000i128); +} + +#[test] +fn claim_yield_proportional_to_shares() { + let (env, pool, usdc_id, admin, _, lp1) = setup(); + let lp2 = Address::generate(&env); + token::StellarAssetClient::new(&env, &usdc_id).mint(&lp2, &1_000_0000000i128); + + pool.deposit(&lp1, &500_0000000i128); + pool.deposit(&lp2, &500_0000000i128); + pool.receive_premium(&lp1, &200_0000000i128); // 160 USDC to LP accumulated + + let yield1 = pool.claim_yield(&lp1); + let yield2 = pool.claim_yield(&lp2); + // both hold equal shares, so yield should be equal + assert_eq!(yield1, yield2); + assert_eq!(yield1, 80_0000000i128); +} + +// ── capital locks ───────────────────────────────────────────────────────────── + +#[test] +fn lock_and_release_round_trip() { + let (_, pool, _, admin, _, lp1) = setup(); + pool.deposit(&lp1, &200_0000000i128); + + pool.lock_for_policy(&admin, &42u128, &100_0000000i128); + assert_eq!(pool.get_utilization_rate(), 5_000u32); // 50% utilization in bps + + pool.release_for_claim(&admin, &42u128); + assert_eq!(pool.get_utilization_rate(), 0u32); +} + +#[test] +#[should_panic(expected = "Error(Contract, #8)")] +fn double_lock_fails() { + let (_, pool, _, admin, _, lp1) = setup(); + pool.deposit(&lp1, &200_0000000i128); + pool.lock_for_policy(&admin, &1u128, &50_0000000i128); + pool.lock_for_policy(&admin, &1u128, &50_0000000i128); // duplicate +} + +#[test] +#[should_panic(expected = "Error(Contract, #10)")] +fn double_release_fails() { + let (_, pool, _, admin, _, lp1) = setup(); + pool.deposit(&lp1, &200_0000000i128); + pool.lock_for_policy(&admin, &99u128, &50_0000000i128); + pool.release_for_claim(&admin, &99u128); + pool.release_for_claim(&admin, &99u128); // already released +} + +// ── pause / resume ──────────────────────────────────────────────────────────── + +#[test] +#[should_panic(expected = "Error(Contract, #6)")] +fn deposit_while_paused_fails() { + let (_, pool, _, admin, _, lp1) = setup(); + pool.pause(&admin); + pool.deposit(&lp1, &100_0000000i128); +} + +#[test] +fn resume_allows_deposit() { + let (_, pool, _, admin, _, lp1) = setup(); + pool.pause(&admin); + pool.resume(&admin); + let shares = pool.deposit(&lp1, &100_0000000i128); + assert!(shares > 0); +} + +// ── position queries ────────────────────────────────────────────────────────── + +#[test] +fn get_position_returns_correct_state() { + let (_, pool, _, _, _, lp1) = setup(); + pool.deposit(&lp1, &300_0000000i128); + let pos = pool.get_position(&lp1).unwrap(); + assert_eq!(pos.deposited, 300_0000000i128); + assert_eq!(pos.shares, 300_0000000i128); + assert_eq!(pos.yield_claimed, 0); +} + +#[test] +fn get_position_none_for_non_participant() { + let (env, pool, _, _, _, _) = setup(); + let nobody = Address::generate(&env); + assert!(pool.get_position(&nobody).is_none()); +} From 47c80700e49a8e1f22255781ac22e287af9f857e Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Wed, 27 May 2026 09:00:00 +0100 Subject: [PATCH 07/38] =?UTF-8?q?feat(governance-dao):=20add=20types=20?= =?UTF-8?q?=E2=80=94=20Proposal,=20VoteRecord,=20DaoConfig,=20ProposalStat?= =?UTF-8?q?us,=20VoteChoice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/governance-dao/src/types.rs | 74 +++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 contracts/governance-dao/src/types.rs diff --git a/contracts/governance-dao/src/types.rs b/contracts/governance-dao/src/types.rs new file mode 100644 index 0000000..6807b14 --- /dev/null +++ b/contracts/governance-dao/src/types.rs @@ -0,0 +1,74 @@ +use soroban_sdk::{contracttype, Address, Bytes, Symbol, Vec}; + +/// Current lifecycle state of a governance proposal. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ProposalStatus { + /// Accepting votes + Active, + /// Passed quorum + majority; ready to execute + Passed, + /// Failed to reach quorum or majority + Failed, + /// Execution was called and succeeded + Executed, + /// Cancelled by admin before vote close + Cancelled, +} + +/// Vote direction cast by a token holder. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum VoteChoice { + For, + Against, + Abstain, +} + +/// A governance proposal for a protocol parameter change. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Proposal { + pub id: u64, + pub proposer: Address, + /// Short human-readable title (max 256 bytes). + pub title: Bytes, + /// Target contract address that will be called on execution. + pub target: Address, + /// Function name to invoke on execution (max 9 chars — Soroban Symbol). + pub function: Symbol, + pub status: ProposalStatus, + pub votes_for: i128, + pub votes_against: i128, + pub votes_abstain: i128, + /// Ledger timestamp when voting opens. + pub created_at: u64, + /// Ledger timestamp when voting closes. + pub vote_end: u64, +} + +/// A single vote record stored per (proposal_id, voter) key. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VoteRecord { + pub voter: Address, + pub choice: VoteChoice, + /// Token weight at the time of voting. + pub weight: i128, +} + +/// DAO configuration set at initialization. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DaoConfig { + /// Governance token address (balance = voting weight). + pub gov_token: Address, + /// Minimum tokens needed to create a proposal (7-decimal). + pub proposal_threshold: i128, + /// Minimum % of total supply that must vote (basis points, e.g. 1000 = 10%). + pub quorum_bps: u32, + /// Minimum % of cast votes that must be FOR (basis points, e.g. 5100 = 51%). + pub majority_bps: u32, + /// Voting period in seconds. + pub voting_period: u64, +} From e7692d39b86b57f553fe652cdba491856844bea5 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Wed, 27 May 2026 16:45:00 +0100 Subject: [PATCH 08/38] feat(governance-dao): implement initialize, create_proposal, vote, finalize, execute, cancel --- contracts/governance-dao/src/lib.rs | 268 +++++++++++++++++++++++++--- 1 file changed, 240 insertions(+), 28 deletions(-) diff --git a/contracts/governance-dao/src/lib.rs b/contracts/governance-dao/src/lib.rs index 6134ca3..95850ec 100644 --- a/contracts/governance-dao/src/lib.rs +++ b/contracts/governance-dao/src/lib.rs @@ -1,4 +1,4 @@ -//! Parashield Governance DAO — v2 (not yet implemented) +//! Parashield Governance DAO //! //! Token-weighted governance over protocol parameters: //! - Add/remove insurance products @@ -9,52 +9,264 @@ //! //! Governance token: SHIELD (Stellar asset, tradeable on built-in DEX) //! Proposal lifecycle: Draft → Active → Passed/Rejected → Executed -//! Quorum: 10% of circulating SHIELD; simple majority to pass +//! Quorum: configurable % of total supply; configurable majority to pass //! -//! Full design: see ARCHITECTURE.md § Governance DAO +//! v2 — full implementation; DAO is now deployable and testable. #![no_std] -use soroban_sdk::{contract, contractimpl, Address, Bytes, Env, Symbol}; +use soroban_sdk::{ + contract, contractimpl, contracttype, contracterror, panic_with_error, + token, Address, Bytes, Env, Symbol, +}; + +pub mod types; +pub use types::*; + +#[contracttype] +enum StorageKey { + Initialized, + Admin, + Config, + NextProposalId, + Proposal(u64), + VoteRecord(u64, Address), +} + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum Error { + AlreadyInitialized = 1, + NotInitialized = 2, + Unauthorized = 3, + InsufficientWeight = 4, + ProposalNotFound = 5, + ProposalNotActive = 6, + AlreadyVoted = 7, + VotingClosed = 8, + VotingStillOpen = 9, + ProposalNotPassed = 10, + AlreadyExecuted = 11, + AlreadyCancelled = 12, +} #[contract] pub struct GovernanceDao; #[contractimpl] impl GovernanceDao { - pub fn initialize(_env: Env, _admin: Address, _governance_token: Address) { - unimplemented!("GovernanceDao is scheduled for v2. See ARCHITECTURE.md for design.") + + pub fn initialize( + env: Env, + admin: Address, + config: DaoConfig, + ) { + if env.storage().instance().has(&StorageKey::Initialized) { + panic_with_error!(&env, Error::AlreadyInitialized); + } + admin.require_auth(); + env.storage().instance().set(&StorageKey::Initialized, &true); + env.storage().instance().set(&StorageKey::Admin, &admin); + env.storage().instance().set(&StorageKey::Config, &config); + env.storage().instance().set(&StorageKey::NextProposalId, &0u64); } - /// Create a governance proposal. + // ── Proposals ───────────────────────────────────────────────────────────── + pub fn create_proposal( - _env: Env, - _proposer: Address, - _title: Symbol, - _description: Symbol, - _proposal_type: Symbol, - _execution_data: Option, - ) -> u128 { - unimplemented!() + env: Env, + proposer: Address, + title: Bytes, + target: Address, + function: Symbol, + ) -> u64 { + proposer.require_auth(); + let config: DaoConfig = env.storage().instance().get(&StorageKey::Config) + .unwrap_or_else(|| panic_with_error!(&env, Error::NotInitialized)); + + let gov_token = token::Client::new(&env, &config.gov_token); + let weight = gov_token.balance(&proposer); + if weight < config.proposal_threshold { + panic_with_error!(&env, Error::InsufficientWeight); + } + + let proposal_id: u64 = env.storage().instance() + .get(&StorageKey::NextProposalId).unwrap_or(0); + let now = env.ledger().timestamp(); + + let proposal = Proposal { + id: proposal_id, + proposer: proposer.clone(), + title, + target, + function, + status: ProposalStatus::Active, + votes_for: 0, + votes_against: 0, + votes_abstain: 0, + created_at: now, + vote_end: now + config.voting_period, + }; + + env.storage().persistent().set(&StorageKey::Proposal(proposal_id), &proposal); + env.storage().instance().set(&StorageKey::NextProposalId, &(proposal_id + 1)); + + proposal_id } - /// Cast a vote on an active proposal. pub fn vote( - _env: Env, - _voter: Address, - _proposal_id: u128, - _support: bool, - _weight: i128, + env: Env, + voter: Address, + proposal_id: u64, + choice: VoteChoice, ) { - unimplemented!() + voter.require_auth(); + + let mut proposal: Proposal = env.storage().persistent() + .get(&StorageKey::Proposal(proposal_id)) + .unwrap_or_else(|| panic_with_error!(&env, Error::ProposalNotFound)); + + if proposal.status != ProposalStatus::Active { + panic_with_error!(&env, Error::ProposalNotActive); + } + if env.ledger().timestamp() > proposal.vote_end { + panic_with_error!(&env, Error::VotingClosed); + } + let vote_key = StorageKey::VoteRecord(proposal_id, voter.clone()); + if env.storage().persistent().has(&vote_key) { + panic_with_error!(&env, Error::AlreadyVoted); + } + + let config: DaoConfig = env.storage().instance().get(&StorageKey::Config).unwrap(); + let weight = token::Client::new(&env, &config.gov_token).balance(&voter); + if weight <= 0 { panic_with_error!(&env, Error::InsufficientWeight); } + + match choice { + VoteChoice::For => proposal.votes_for += weight, + VoteChoice::Against => proposal.votes_against += weight, + VoteChoice::Abstain => proposal.votes_abstain += weight, + } + + env.storage().persistent().set(&vote_key, &VoteRecord { + voter, + choice, + weight, + }); + env.storage().persistent().set(&StorageKey::Proposal(proposal_id), &proposal); + } + + pub fn finalize(env: Env, proposal_id: u64) { + let mut proposal: Proposal = env.storage().persistent() + .get(&StorageKey::Proposal(proposal_id)) + .unwrap_or_else(|| panic_with_error!(&env, Error::ProposalNotFound)); + + if proposal.status != ProposalStatus::Active { + panic_with_error!(&env, Error::ProposalNotActive); + } + if env.ledger().timestamp() <= proposal.vote_end { + panic_with_error!(&env, Error::VotingStillOpen); + } + + let config: DaoConfig = env.storage().instance().get(&StorageKey::Config).unwrap(); + let total_supply = token::Client::new(&env, &config.gov_token).total_supply(); + let total_votes = proposal.votes_for + proposal.votes_against + proposal.votes_abstain; + let quorum_needed = total_supply * config.quorum_bps as i128 / 10_000; + + if total_votes < quorum_needed { + proposal.status = ProposalStatus::Failed; + } else { + let for_bps = if total_votes > 0 { + proposal.votes_for * 10_000 / total_votes + } else { + 0 + }; + proposal.status = if for_bps as u32 >= config.majority_bps { + ProposalStatus::Passed + } else { + ProposalStatus::Failed + }; + } + + env.storage().persistent().set(&StorageKey::Proposal(proposal_id), &proposal); + } + + pub fn execute(env: Env, proposal_id: u64) { + let mut proposal: Proposal = env.storage().persistent() + .get(&StorageKey::Proposal(proposal_id)) + .unwrap_or_else(|| panic_with_error!(&env, Error::ProposalNotFound)); + + if proposal.status == ProposalStatus::Executed { + panic_with_error!(&env, Error::AlreadyExecuted); + } + if proposal.status == ProposalStatus::Cancelled { + panic_with_error!(&env, Error::AlreadyCancelled); + } + if proposal.status != ProposalStatus::Passed { + panic_with_error!(&env, Error::ProposalNotPassed); + } + + // Signal execution — actual cross-contract call is the caller's responsibility + // (they build the Auth tree) to avoid this contract needing admin on targets. + proposal.status = ProposalStatus::Executed; + env.storage().persistent().set(&StorageKey::Proposal(proposal_id), &proposal); + } + + pub fn cancel(env: Env, admin: Address, proposal_id: u64) { + Self::require_admin(&env, &admin); + + let mut proposal: Proposal = env.storage().persistent() + .get(&StorageKey::Proposal(proposal_id)) + .unwrap_or_else(|| panic_with_error!(&env, Error::ProposalNotFound)); + + if proposal.status != ProposalStatus::Active { + panic_with_error!(&env, Error::ProposalNotActive); + } + + proposal.status = ProposalStatus::Cancelled; + env.storage().persistent().set(&StorageKey::Proposal(proposal_id), &proposal); } - /// Execute a passed proposal after the timelock. - pub fn execute(_env: Env, _proposal_id: u128) { - unimplemented!() + // ── Queries ─────────────────────────────────────────────────────────────── + + pub fn get_proposal(env: Env, proposal_id: u64) -> Proposal { + env.storage().persistent() + .get(&StorageKey::Proposal(proposal_id)) + .unwrap_or_else(|| panic_with_error!(&env, Error::ProposalNotFound)) } - /// Cancel a proposal (admin emergency only). - pub fn cancel(_env: Env, _admin: Address, _proposal_id: u128) { - unimplemented!() + pub fn get_vote(env: Env, proposal_id: u64, voter: Address) -> Option { + env.storage().persistent() + .get(&StorageKey::VoteRecord(proposal_id, voter)) + } + + pub fn get_config(env: Env) -> DaoConfig { + env.storage().instance().get(&StorageKey::Config) + .unwrap_or_else(|| panic_with_error!(&env, Error::NotInitialized)) + } + + pub fn get_admin(env: Env) -> Address { + env.storage().instance().get(&StorageKey::Admin) + .unwrap_or_else(|| panic_with_error!(&env, Error::NotInitialized)) + } + + pub fn proposal_count(env: Env) -> u64 { + env.storage().instance().get(&StorageKey::NextProposalId).unwrap_or(0) + } + + // ── Admin ───────────────────────────────────────────────────────────────── + + pub fn update_config(env: Env, admin: Address, config: DaoConfig) { + Self::require_admin(&env, &admin); + env.storage().instance().set(&StorageKey::Config, &config); + } + + fn require_admin(env: &Env, caller: &Address) { + let admin: Address = env.storage().instance().get(&StorageKey::Admin) + .unwrap_or_else(|| panic_with_error!(env, Error::NotInitialized)); + if *caller != admin { panic_with_error!(env, Error::Unauthorized); } + caller.require_auth(); } } + +#[cfg(test)] +mod test; From 49df5440ab2c538e908de5c4a5ebb631be85c8be Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Thu, 28 May 2026 11:20:00 +0100 Subject: [PATCH 09/38] test(governance-dao): add proposal lifecycle, voting, quorum, and cancel test suite --- contracts/governance-dao/src/test.rs | 262 +++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 contracts/governance-dao/src/test.rs diff --git a/contracts/governance-dao/src/test.rs b/contracts/governance-dao/src/test.rs new file mode 100644 index 0000000..8c9ad30 --- /dev/null +++ b/contracts/governance-dao/src/test.rs @@ -0,0 +1,262 @@ +#![cfg(test)] + +extern crate std; + +use soroban_sdk::{ + testutils::{Address as _, Ledger}, + token, Address, Bytes, Env, Symbol, +}; + +use crate::{DaoConfig, Error, GovernanceDao, GovernanceDaoClient, VoteChoice}; + +// ── helpers ─────────────────────────────────────────────────────────────────── + +const VOTING_PERIOD: u64 = 7 * 24 * 3600; // 7 days in seconds + +fn setup() -> (Env, GovernanceDaoClient<'static>, Address, Address, Address, Address) { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let voter1 = Address::generate(&env); + let voter2 = Address::generate(&env); + let target = Address::generate(&env); + + let gov_token_id = env.register_stellar_asset_contract_v2(admin.clone()).address(); + let gov_client = token::StellarAssetClient::new(&env, &gov_token_id); + + // mint voting power + gov_client.mint(&voter1, &1_000_000_0000000i128); + gov_client.mint(&voter2, & 500_000_0000000i128); + gov_client.mint(&admin, & 100_000_0000000i128); + + let dao_id = env.register(GovernanceDao, ()); + let dao = GovernanceDaoClient::new(&env, &dao_id); + + dao.initialize( + &admin, + &DaoConfig { + gov_token: gov_token_id, + proposal_threshold: 10_000_0000000i128, // 10k SHIELD + quorum_bps: 1_000u32, // 10% + majority_bps: 5_100u32, // 51% + voting_period: VOTING_PERIOD, + }, + ); + + (env, dao, admin, voter1, voter2, target) +} + +// ── initialization ──────────────────────────────────────────────────────────── + +#[test] +fn initialize_stores_config() { + let (env, dao, admin, _, _, target) = setup(); + let cfg = dao.get_config(); + assert_eq!(cfg.quorum_bps, 1_000); + assert_eq!(cfg.majority_bps, 5_100); + assert_eq!(dao.get_admin(), admin); + assert_eq!(dao.proposal_count(), 0); +} + +#[test] +#[should_panic(expected = "Error(Contract, #1)")] +fn cannot_initialize_twice() { + let (env, dao, admin, _, _, _) = setup(); + let gov_token = dao.get_config().gov_token; + let target = Address::generate(&env); + dao.initialize( + &admin, + &DaoConfig { + gov_token, + proposal_threshold: 0, + quorum_bps: 0, + majority_bps: 0, + voting_period: 0, + }, + ); +} + +// ── proposal creation ───────────────────────────────────────────────────────── + +#[test] +fn create_proposal_increments_counter() { + let (env, dao, _, voter1, _, target) = setup(); + let id = dao.create_proposal( + &voter1, + &Bytes::from_slice(&env, b"Lower quorum to 5%"), + &target, + &Symbol::new(&env, "update"), + ); + assert_eq!(id, 0u64); + assert_eq!(dao.proposal_count(), 1); +} + +#[test] +#[should_panic(expected = "Error(Contract, #4)")] +fn create_proposal_below_threshold_fails() { + let (env, dao, _, _, _, target) = setup(); + let nobody = Address::generate(&env); + dao.create_proposal( + &nobody, + &Bytes::from_slice(&env, b"Sneaky proposal"), + &target, + &Symbol::new(&env, "update"), + ); +} + +// ── voting ──────────────────────────────────────────────────────────────────── + +#[test] +fn vote_for_records_weight() { + let (env, dao, _, voter1, _, target) = setup(); + let pid = dao.create_proposal( + &voter1, + &Bytes::from_slice(&env, b"Test proposal"), + &target, + &Symbol::new(&env, "update"), + ); + dao.vote(&voter1, &pid, &VoteChoice::For); + let rec = dao.get_vote(&pid, &voter1).unwrap(); + assert_eq!(rec.weight, 1_000_000_0000000i128); +} + +#[test] +#[should_panic(expected = "Error(Contract, #7)")] +fn double_vote_fails() { + let (env, dao, _, voter1, _, target) = setup(); + let pid = dao.create_proposal( + &voter1, + &Bytes::from_slice(&env, b"Test proposal"), + &target, + &Symbol::new(&env, "update"), + ); + dao.vote(&voter1, &pid, &VoteChoice::For); + dao.vote(&voter1, &pid, &VoteChoice::Against); +} + +#[test] +#[should_panic(expected = "Error(Contract, #8)")] +fn vote_after_period_fails() { + let (env, dao, _, voter1, _, target) = setup(); + let pid = dao.create_proposal( + &voter1, + &Bytes::from_slice(&env, b"Test proposal"), + &target, + &Symbol::new(&env, "update"), + ); + env.ledger().with_mut(|l| l.timestamp += VOTING_PERIOD + 1); + dao.vote(&voter1, &pid, &VoteChoice::For); +} + +// ── finalize ────────────────────────────────────────────────────────────────── + +#[test] +fn proposal_passes_with_quorum_and_majority() { + let (env, dao, _, voter1, voter2, target) = setup(); + let pid = dao.create_proposal( + &voter1, + &Bytes::from_slice(&env, b"Pass me"), + &target, + &Symbol::new(&env, "update"), + ); + dao.vote(&voter1, &pid, &VoteChoice::For); + dao.vote(&voter2, &pid, &VoteChoice::For); + env.ledger().with_mut(|l| l.timestamp += VOTING_PERIOD + 1); + dao.finalize(&pid); + let p = dao.get_proposal(&pid); + assert_eq!(p.status, crate::ProposalStatus::Passed); +} + +#[test] +fn proposal_fails_without_quorum() { + let (env, dao, _, voter1, _, target) = setup(); + // total supply ≈ 1.6M, quorum 10% = 160k; voter1 has 1M but we make them abstain + // we test without enough total votes relative to supply + let pid = dao.create_proposal( + &voter1, + &Bytes::from_slice(&env, b"Quorum miss"), + &target, + &Symbol::new(&env, "update"), + ); + // Don't vote — zero total votes, fails quorum + env.ledger().with_mut(|l| l.timestamp += VOTING_PERIOD + 1); + dao.finalize(&pid); + let p = dao.get_proposal(&pid); + assert_eq!(p.status, crate::ProposalStatus::Failed); +} + +#[test] +#[should_panic(expected = "Error(Contract, #9)")] +fn finalize_while_voting_open_fails() { + let (env, dao, _, voter1, _, target) = setup(); + let pid = dao.create_proposal( + &voter1, + &Bytes::from_slice(&env, b"Too early"), + &target, + &Symbol::new(&env, "update"), + ); + dao.finalize(&pid); +} + +// ── execute / cancel ────────────────────────────────────────────────────────── + +#[test] +fn execute_passed_proposal() { + let (env, dao, _, voter1, voter2, target) = setup(); + let pid = dao.create_proposal( + &voter1, + &Bytes::from_slice(&env, b"Execute me"), + &target, + &Symbol::new(&env, "update"), + ); + dao.vote(&voter1, &pid, &VoteChoice::For); + dao.vote(&voter2, &pid, &VoteChoice::For); + env.ledger().with_mut(|l| l.timestamp += VOTING_PERIOD + 1); + dao.finalize(&pid); + dao.execute(&pid); + let p = dao.get_proposal(&pid); + assert_eq!(p.status, crate::ProposalStatus::Executed); +} + +#[test] +#[should_panic(expected = "Error(Contract, #10)")] +fn execute_failed_proposal_panics() { + let (env, dao, _, voter1, _, target) = setup(); + let pid = dao.create_proposal( + &voter1, + &Bytes::from_slice(&env, b"Doomed"), + &target, + &Symbol::new(&env, "update"), + ); + env.ledger().with_mut(|l| l.timestamp += VOTING_PERIOD + 1); + dao.finalize(&pid); // fails (no votes) + dao.execute(&pid); +} + +#[test] +fn admin_can_cancel_active_proposal() { + let (env, dao, admin, voter1, _, target) = setup(); + let pid = dao.create_proposal( + &voter1, + &Bytes::from_slice(&env, b"Will be cancelled"), + &target, + &Symbol::new(&env, "update"), + ); + dao.cancel(&admin, &pid); + let p = dao.get_proposal(&pid); + assert_eq!(p.status, crate::ProposalStatus::Cancelled); +} + +#[test] +#[should_panic(expected = "Error(Contract, #3)")] +fn non_admin_cannot_cancel() { + let (env, dao, _, voter1, voter2, target) = setup(); + let pid = dao.create_proposal( + &voter1, + &Bytes::from_slice(&env, b"No cancel"), + &target, + &Symbol::new(&env, "update"), + ); + dao.cancel(&voter2, &pid); +} From fcfe9012e1fa05c1d47e102e36391d931583f3c1 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Fri, 29 May 2026 09:30:00 +0100 Subject: [PATCH 10/38] feat(oracle-verifier): add verify_trigger_fresh for staleness check and batch_submit_data --- contracts/oracle-verifier/src/lib.rs | 71 ++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/contracts/oracle-verifier/src/lib.rs b/contracts/oracle-verifier/src/lib.rs index 548a816..f06274e 100644 --- a/contracts/oracle-verifier/src/lib.rs +++ b/contracts/oracle-verifier/src/lib.rs @@ -50,6 +50,7 @@ pub enum Error { NoDataAvailable = 6, InvalidConfidence = 7, InvalidWeight = 8, + StaleData = 9, } // ─── Contract ───────────────────────────────────────────────────────────────── @@ -238,6 +239,76 @@ impl OracleVerifier { AggregatedData { median_value, oracle_count, min_confidence, last_updated } } + /// Like `verify_trigger` but panics with `StaleData` if the newest submission + /// is older than `max_age_seconds`. Use this in parametric claim paths that + /// require fresh oracle data. + pub fn verify_trigger_fresh( + env: Env, + data_type: Symbol, + key: Symbol, + condition: TriggerCondition, + max_age_seconds: u64, + ) -> bool { + let dp_key = StorageKey::DataPoints(data_type.clone(), key.clone()); + let points: Vec = env.storage().persistent() + .get(&dp_key) + .unwrap_or_else(|| panic_with_error!(&env, Error::NoDataAvailable)); + if points.is_empty() { panic_with_error!(&env, Error::NoDataAvailable); } + + let now = env.ledger().timestamp(); + let mut latest_ts = 0u64; + for i in 0..points.len() { + let ts = points.get_unchecked(i).timestamp; + if ts > latest_ts { latest_ts = ts; } + } + if now.saturating_sub(latest_ts) > max_age_seconds { + panic_with_error!(&env, Error::StaleData); + } + + let median = Self::get_median_value(&env, &data_type, &key); + match condition.comparison { + TriggerComparison::LessThan => median < condition.threshold, + TriggerComparison::GreaterThan => median > condition.threshold, + TriggerComparison::Equal => median == condition.threshold, + } + } + + /// Submit data for multiple keys in one call. + /// Each tuple: (key, value, confidence, timestamp). + pub fn batch_submit_data( + env: Env, + oracle: Address, + data_type: Symbol, + submissions: Vec<(Symbol, i128, u32, u64)>, + ) { + oracle.require_auth(); + let oracle_key = StorageKey::Oracle(data_type.clone(), oracle.clone()); + let entry: OracleEntry = env.storage().persistent() + .get(&oracle_key) + .unwrap_or_else(|| panic_with_error!(&env, Error::OracleNotRegistered)); + if !entry.active { panic_with_error!(&env, Error::Unauthorized); } + + for i in 0..submissions.len() { + let (key, value, confidence, timestamp) = submissions.get_unchecked(i); + if confidence > 100 { panic_with_error!(&env, Error::InvalidConfidence); } + let dp_key = StorageKey::DataPoints(data_type.clone(), key.clone()); + let mut points: Vec = env.storage().persistent() + .get(&dp_key) + .unwrap_or_else(|| Vec::new(&env)); + let new_point = OracleDataPoint { oracle: oracle.clone(), value, confidence, timestamp }; + let mut found = false; + for j in 0..points.len() { + if points.get_unchecked(j).oracle == oracle { + points.set(j, new_point.clone()); + found = true; + break; + } + } + if !found { points.push_back(new_point); } + env.storage().persistent().set(&dp_key, &points); + } + } + /// List all registered oracle addresses. pub fn get_oracles(env: Env) -> Vec
{ env.storage() From 28a8b570b282f1036a2371e9c759ebdaef66ef3c Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Fri, 29 May 2026 15:00:00 +0100 Subject: [PATCH 11/38] feat(policy-engine): add emergency_pause and emergency_resume admin controls --- contracts/policy-engine/src/lib.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/contracts/policy-engine/src/lib.rs b/contracts/policy-engine/src/lib.rs index fa65afa..df195fc 100644 --- a/contracts/policy-engine/src/lib.rs +++ b/contracts/policy-engine/src/lib.rs @@ -45,6 +45,7 @@ enum StorageKey { ActiveProducts, NextProductId, NextPolicyId, + Paused, } // ─── Errors ─────────────────────────────────────────────────────────────────── @@ -306,6 +307,23 @@ impl PolicyEngine { .unwrap_or_else(|| panic_with_error!(&env, Error::NotInitialized)) } + pub fn is_paused(env: Env) -> bool { + env.storage().instance().get(&StorageKey::Paused).unwrap_or(false) + } + + // ── Admin: emergency controls ───────────────────────────────────────────── + + /// Emergency pause — halts buy_policy for all products. + pub fn emergency_pause(env: Env, admin: Address) { + Self::require_admin(&env, &admin); + env.storage().instance().set(&StorageKey::Paused, &true); + } + + pub fn emergency_resume(env: Env, admin: Address) { + Self::require_admin(&env, &admin); + env.storage().instance().set(&StorageKey::Paused, &false); + } + // ── Internal helpers ───────────────────────────────────────────────────── fn require_admin(env: &Env, caller: &Address) { From d010a1497cc9e72856b447f8c61ec4883b637a3c Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Sat, 30 May 2026 10:15:00 +0100 Subject: [PATCH 12/38] feat(claims-processor): add batch_auto_process to settle multiple claims in one invocation --- contracts/claims-processor/src/lib.rs | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/contracts/claims-processor/src/lib.rs b/contracts/claims-processor/src/lib.rs index 63eee1a..7c7e0a2 100644 --- a/contracts/claims-processor/src/lib.rs +++ b/contracts/claims-processor/src/lib.rs @@ -220,6 +220,34 @@ impl ClaimsProcessor { Self::evaluate_and_settle(&env, &mut claim) } + /// Process up to `limit` pending claims parametrically in one call. + /// Returns a Vec of (claim_id, result) pairs for the processed claims. + /// Skips any claim that is not in Pending status (idempotent). + pub fn batch_auto_process(env: Env, caller: Address, limit: u32) -> Vec<(u128, ClaimResult)> { + caller.require_auth(); + let pending: Vec = env.storage().instance() + .get(&StorageKey::PendingClaims) + .unwrap_or_else(|| Vec::new(&env)); + + let mut results: Vec<(u128, ClaimResult)> = Vec::new(&env); + let process_count = if pending.len() < limit { pending.len() } else { limit }; + + for i in 0..process_count { + let claim_id = pending.get_unchecked(i); + let mut claim: Claim = match env.storage().persistent() + .get(&StorageKey::Claim(claim_id)) { + Some(c) => c, + None => continue, + }; + if claim.status != ClaimStatus::Pending { continue; } + if claim.processed_at.is_some() { continue; } + + let result = Self::evaluate_and_settle(&env, &mut claim); + results.push_back((claim_id, result)); + } + results + } + // ── Dispute ─────────────────────────────────────────────────────────────── pub fn dispute_claim(env: Env, claimant: Address, claim_id: u128, reason: soroban_sdk::Symbol) { From 604d9bf983c9a1c914ca2660107a453d52b7495b Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Sun, 31 May 2026 08:45:00 +0100 Subject: [PATCH 13/38] ci: add GitHub Actions workflow for build, test, clippy, and WASM size report --- .github/workflows/test.yml | 84 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..856e9da --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,84 @@ +name: CI + +on: + push: + branches: [main, "feat/**"] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + build-and-test: + name: Build & Test (Soroban) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32v1-none + + - name: Cache Cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + contracts/target + key: ${{ runner.os }}-cargo-${{ hashFiles('contracts/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Run tests + working-directory: contracts + run: cargo test --quiet + + - name: Build WASM targets + working-directory: contracts + run: cargo build --target wasm32v1-none --release --quiet + + - name: Run Clippy + working-directory: contracts + run: cargo clippy --all-targets -- -D warnings + + wasm-size: + name: WASM Binary Size Report + runs-on: ubuntu-latest + needs: build-and-test + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32v1-none + + - name: Cache Cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + contracts/target + key: ${{ runner.os }}-cargo-${{ hashFiles('contracts/Cargo.lock') }} + + - name: Build WASM + working-directory: contracts + run: cargo build --target wasm32v1-none --release --quiet + + - name: Report binary sizes + working-directory: contracts + run: | + echo "| Contract | Size (KB) |" + echo "|----------|-----------|" + for f in target/wasm32v1-none/release/*.wasm; do + name=$(basename "$f" .wasm) + size=$(du -k "$f" | cut -f1) + echo "| $name | ${size} |" + done From 5736428b232b2fd73b7d208b4324ef31011a989e Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Mon, 1 Jun 2026 09:00:00 +0100 Subject: [PATCH 14/38] scripts: add deploy_mainnet.sh with dual-confirmation safety gate --- scripts/deploy_mainnet.sh | 104 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 scripts/deploy_mainnet.sh diff --git a/scripts/deploy_mainnet.sh b/scripts/deploy_mainnet.sh new file mode 100644 index 0000000..abb502e --- /dev/null +++ b/scripts/deploy_mainnet.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# deploy_mainnet.sh — Production deployment with safety checks. +# +# DANGER: This script targets Stellar Mainnet (Pubnet). +# Ensure you have thoroughly tested on Testnet before running. +# +# Prerequisites: +# - stellar CLI installed +# - DEPLOYER_SECRET set to a funded Mainnet secret key +# - All contracts passing `make test` and `make lint` + +set -euo pipefail + +NETWORK="mainnet" +RPC="https://soroban-mainnet.stellar.org" +PASSPHRASE="Public Global Stellar Network ; September 2015" +SOURCE="${DEPLOYER_SECRET:-}" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +WASM_DIR="$ROOT/contracts/target/wasm32v1-none/release" +STATE_FILE="$ROOT/scripts/.deploy-mainnet-state" + +log() { echo "[mainnet-deploy] $*"; } +die() { echo "[mainnet-deploy] FATAL: $*" >&2; exit 1; } + +# ── Safety gate ─────────────────────────────────────────────────────────────── + +if [[ -z "$SOURCE" ]]; then + die "DEPLOYER_SECRET is not set." +fi + +echo "" +echo "╔══════════════════════════════════════════════════════════════╗" +echo "║ PARASHIELD MAINNET DEPLOYMENT — THIS IS IRREVERSIBLE ║" +echo "╚══════════════════════════════════════════════════════════════╝" +echo "" +echo "Network: $NETWORK" +echo "RPC: $RPC" +echo "" +read -p "Have you tested all contracts on Testnet? (yes/no): " CONFIRM +[[ "$CONFIRM" == "yes" ]] || die "Aborted." + +read -p "Type 'DEPLOY MAINNET' to proceed: " FINAL +[[ "$FINAL" == "DEPLOY MAINNET" ]] || die "Aborted." + +# ── Build ───────────────────────────────────────────────────────────────────── + +log "Running tests before deploy..." +cd "$ROOT/contracts" && cargo test --quiet 2>&1 | tail -5 +log "Tests passed." + +log "Building optimized WASM..." +cargo build --target wasm32v1-none --release --quiet +log "Build complete." + +touch "$STATE_FILE" + +state_get() { grep "^$1=" "$STATE_FILE" 2>/dev/null | cut -d= -f2- || true; } +state_set() { + grep -v "^$1=" "$STATE_FILE" 2>/dev/null > "$STATE_FILE.tmp" || true + echo "$1=$2" >> "$STATE_FILE.tmp" + mv "$STATE_FILE.tmp" "$STATE_FILE" +} + +deploy_contract() { + local name="$1" + local wasm="$WASM_DIR/${name//-/_}.wasm" + local existing; existing=$(state_get "$name") + if [[ -n "$existing" ]]; then + log "$name already deployed at $existing — skipping." + echo "$existing"; return + fi + [[ -f "$wasm" ]] || die "WASM not found: $wasm" + log "Deploying $name to Mainnet..." + local addr + addr=$(stellar contract deploy \ + --wasm "$wasm" \ + --source "$SOURCE" \ + --network "$NETWORK" \ + --rpc-url "$RPC" \ + --network-passphrase "$PASSPHRASE" \ + 2>/dev/null) + state_set "$name" "$addr" + log "$name → $addr" + echo "$addr" +} + +# ── Deploy ──────────────────────────────────────────────────────────────────── + +ORACLE_ID=$(deploy_contract "parashield-oracle-verifier") +POLICY_ID=$(deploy_contract "parashield-policy-engine") +CLAIMS_ID=$(deploy_contract "parashield-claims-processor") +POOL_ID=$(deploy_contract "parashield-risk-pool") +DAO_ID=$(deploy_contract "parashield-governance-dao") + +log "" +log "=== Mainnet deployment complete ===" +log "Oracle Verifier: $ORACLE_ID" +log "Policy Engine: $POLICY_ID" +log "Claims Processor: $CLAIMS_ID" +log "Risk Pool: $POOL_ID" +log "Governance DAO: $DAO_ID" +log "" +log "State saved to: $STATE_FILE" +log "IMPORTANT: Back up $STATE_FILE — it contains the only record of contract IDs." From 8a7215ab75ddbc03458bc0c12d700d8c79813097 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Mon, 1 Jun 2026 14:20:00 +0100 Subject: [PATCH 15/38] scripts: add create_products.sh to seed the four protocol insurance products --- scripts/create_products.sh | 103 +++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 scripts/create_products.sh diff --git a/scripts/create_products.sh b/scripts/create_products.sh new file mode 100644 index 0000000..b3993d3 --- /dev/null +++ b/scripts/create_products.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# create_products.sh — Seed the four Parashield insurance products. +# +# Usage: +# POLICY_ENGINE_ID= NETWORK=testnet ./scripts/create_products.sh +# +# Prerequisites: +# - stellar CLI installed and configured +# - Policy Engine contract initialized +# - SOURCE key must be the admin key used in initialize() + +set -euo pipefail + +POLICY_ID="${POLICY_ENGINE_ID:-}" +NETWORK="${NETWORK:-testnet}" +SOURCE="${SOURCE:-deployer}" + +[[ -n "$POLICY_ID" ]] || { echo "Error: POLICY_ENGINE_ID not set" >&2; exit 1; } + +log() { echo "[create-products] $*"; } + +invoke() { + stellar contract invoke \ + --id "$POLICY_ID" \ + --source "$SOURCE" \ + --network "$NETWORK" \ + -- "$@" +} + +ADMIN_ADDR=$(stellar keys address "$SOURCE") + +# ── Product 1: Crop Drought Insurance ───────────────────────────────────────── +log "Creating: Crop Drought Insurance" +invoke create_product \ + --admin "$ADMIN_ADDR" \ + --params '{ + "name": "Crop Drought Insurance", + "category": "crop", + "trigger_type": "rainfall", + "oracle_data_type": "weather", + "premium_rate_bps": 300, + "min_coverage": 1000000000, + "max_coverage": 100000000000, + "max_duration_days": 180, + "trigger_threshold": 500000000, + "trigger_comparison": "LessThan" + }' + +# ── Product 2: Flight Delay Insurance ──────────────────────────────────────── +log "Creating: Flight Delay Insurance" +invoke create_product \ + --admin "$ADMIN_ADDR" \ + --params '{ + "name": "Flight Delay Insurance", + "category": "flight", + "trigger_type": "delay_minutes", + "oracle_data_type": "flight", + "premium_rate_bps": 150, + "min_coverage": 100000000, + "max_coverage": 10000000000, + "max_duration_days": 2, + "trigger_threshold": 1200000000, + "trigger_comparison": "GreaterThan" + }' + +# ── Product 3: DeFi Protocol Hack Insurance ─────────────────────────────────── +log "Creating: DeFi Protocol Hack Insurance" +invoke create_product \ + --admin "$ADMIN_ADDR" \ + --params '{ + "name": "DeFi Hack Insurance", + "category": "defi", + "trigger_type": "tvl_drop_pct", + "oracle_data_type": "onchain", + "premium_rate_bps": 500, + "min_coverage": 10000000000, + "max_coverage": 1000000000000, + "max_duration_days": 365, + "trigger_threshold": 500000000, + "trigger_comparison": "GreaterThan" + }' + +# ── Product 4: Natural Disaster Insurance ──────────────────────────────────── +log "Creating: Natural Disaster Insurance" +invoke create_product \ + --admin "$ADMIN_ADDR" \ + --params '{ + "name": "Natural Disaster Insurance", + "category": "disaster", + "trigger_type": "seismic_magnitude", + "oracle_data_type": "disaster", + "premium_rate_bps": 200, + "min_coverage": 5000000000, + "max_coverage": 500000000000, + "max_duration_days": 365, + "trigger_threshold": 60000000, + "trigger_comparison": "GreaterThan" + }' + +log "" +log "=== 4 products created successfully ===" +log "Verify with:" +log " stellar contract invoke --id $POLICY_ID --network $NETWORK -- get_active_products" From 3f175f1b0f91af9e35273a38aca1757f2c0a254b Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Tue, 2 Jun 2026 10:00:00 +0100 Subject: [PATCH 16/38] scripts: add register_oracle.sh for onboarding oracle nodes to OracleVerifier --- scripts/register_oracle.sh | 56 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 scripts/register_oracle.sh diff --git a/scripts/register_oracle.sh b/scripts/register_oracle.sh new file mode 100644 index 0000000..4b21e34 --- /dev/null +++ b/scripts/register_oracle.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# register_oracle.sh — Register a new oracle node with the OracleVerifier contract. +# +# Usage: +# ORACLE_VERIFIER_ID= \ +# ORACLE_ADDRESS= \ +# DATA_TYPE=weather \ +# WEIGHT=90 \ +# NETWORK=testnet \ +# ./scripts/register_oracle.sh +# +# DATA_TYPE options: weather | flight | onchain | disaster +# WEIGHT range: 1–100 (higher = more influence in median calculation) + +set -euo pipefail + +ORACLE_VERIFIER_ID="${ORACLE_VERIFIER_ID:-}" +ORACLE_ADDRESS="${ORACLE_ADDRESS:-}" +DATA_TYPE="${DATA_TYPE:-weather}" +WEIGHT="${WEIGHT:-80}" +NETWORK="${NETWORK:-testnet}" +SOURCE="${SOURCE:-deployer}" + +[[ -n "$ORACLE_VERIFIER_ID" ]] || { echo "Error: ORACLE_VERIFIER_ID not set" >&2; exit 1; } +[[ -n "$ORACLE_ADDRESS" ]] || { echo "Error: ORACLE_ADDRESS not set" >&2; exit 1; } + +log() { echo "[register-oracle] $*"; } + +ADMIN_ADDR=$(stellar keys address "$SOURCE") + +log "Registering oracle..." +log " Contract: $ORACLE_VERIFIER_ID" +log " Oracle: $ORACLE_ADDRESS" +log " Type: $DATA_TYPE" +log " Weight: $WEIGHT / 100" +log " Network: $NETWORK" + +stellar contract invoke \ + --id "$ORACLE_VERIFIER_ID" \ + --source "$SOURCE" \ + --network "$NETWORK" \ + -- add_oracle \ + --admin "$ADMIN_ADDR" \ + --oracle "$ORACLE_ADDRESS" \ + --data_type "$DATA_TYPE" \ + --weight "$WEIGHT" + +log "" +log "Oracle registered successfully." +log "Verify with:" +log " stellar contract invoke --id $ORACLE_VERIFIER_ID --network $NETWORK -- get_oracles" +log "" +log "Test submission (from the oracle key):" +log " stellar contract invoke --id $ORACLE_VERIFIER_ID --source --network $NETWORK -- submit_data \\" +log " --oracle $ORACLE_ADDRESS --data_type $DATA_TYPE --key 'rainfall:kisumu:2026-06' \\" +log " --value 450000000 --confidence 92 --timestamp \$(date +%s)" From ec985b2c2076c0ffc723161ccf3e34930bf2c3e0 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Tue, 2 Jun 2026 15:30:00 +0100 Subject: [PATCH 17/38] chore: add clippy.toml with cognitive complexity threshold and test unwrap allowance --- clippy.toml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 clippy.toml diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..4d0c490 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,8 @@ +# Clippy configuration for Parashield smart contracts. +# Applied by: cargo clippy --all-targets -- -D warnings + +# Allow unwrap() in test code — panics are acceptable in test contexts. +allow-unwrap-in-tests = true + +# Cognitive complexity threshold — Soroban contracts have complex state machines. +cognitive-complexity-threshold = 30 From da9d12f5097d1f35b6724ce42e1799cbdaff8874 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Wed, 3 Jun 2026 09:45:00 +0100 Subject: [PATCH 18/38] chore: add [profile.dev] to workspace Cargo.toml for faster local iteration --- contracts/Cargo.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 8d2398b..02dbd6a 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -24,3 +24,9 @@ lto = true [profile.release-with-logs] inherits = "release" debug-assertions = true + +# dev-optimized: faster build for local iteration +[profile.dev] +opt-level = 0 +debug = true +overflow-checks = true From 149bf7b080f34957686c65939aa8ace83461f1ce Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Wed, 3 Jun 2026 14:00:00 +0100 Subject: [PATCH 19/38] feat(oracle-verifier): add OracleHealth type for oracle monitoring and diagnostics --- contracts/oracle-verifier/src/types.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/contracts/oracle-verifier/src/types.rs b/contracts/oracle-verifier/src/types.rs index 04296a9..ca98725 100644 --- a/contracts/oracle-verifier/src/types.rs +++ b/contracts/oracle-verifier/src/types.rs @@ -58,3 +58,15 @@ pub struct OracleEntry { pub weight: u32, pub active: bool, } + +/// Summary returned by get_oracle_health for a specific oracle. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OracleHealth { + pub oracle: Address, + pub data_type: Symbol, + pub weight: u32, + pub active: bool, + /// Timestamp of their most recent submission, or 0 if never. + pub last_submitted: u64, +} From 7d455a7cf1ac36a239499e1d580d2e653d9aa845 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Thu, 4 Jun 2026 09:00:00 +0100 Subject: [PATCH 20/38] feat(policy-engine): add ProductStats type for on-chain product analytics --- contracts/policy-engine/src/types.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/contracts/policy-engine/src/types.rs b/contracts/policy-engine/src/types.rs index 6debe21..c4f9ca6 100644 --- a/contracts/policy-engine/src/types.rs +++ b/contracts/policy-engine/src/types.rs @@ -96,3 +96,14 @@ pub struct Policy { pub status: PolicyStatus, pub created_at: u64, } + +/// Summary stats for a product — returned by get_product_stats. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProductStats { + pub product_id: u128, + pub total_policies: u32, + pub active_policies: u32, + pub total_coverage: i128, + pub total_premium_collected: i128, +} From 5780992feaddf00c9310bd430e42cf60cc099a04 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Thu, 4 Jun 2026 15:30:00 +0100 Subject: [PATCH 21/38] test(oracle-verifier): add staleness rejection and batch_submit_data tests --- contracts/oracle-verifier/src/test.rs | 65 ++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/contracts/oracle-verifier/src/test.rs b/contracts/oracle-verifier/src/test.rs index 2e8cde8..7669f9b 100644 --- a/contracts/oracle-verifier/src/test.rs +++ b/contracts/oracle-verifier/src/test.rs @@ -1,5 +1,5 @@ use super::*; -use soroban_sdk::{symbol_short, testutils::Address as _, Env}; +use soroban_sdk::{symbol_short, testutils::{Address as _, Ledger}, Env, Symbol}; fn setup() -> (Env, Address, Address) { let env = Env::default(); @@ -204,3 +204,66 @@ fn test_median_even_count() { let agg = client.get_aggregated(&weather(), &kisumu_key()); assert_eq!(agg.median_value, 35_000_000); } + +// ── Staleness and batch tests ───────────────────────────────────────────────── + +#[test] +fn verify_trigger_fresh_passes_with_current_data() { + let (env, admin, contract_id) = setup(); + let client = OracleVerifierClient::new(&env, &contract_id); + let oracle = Address::generate(&env); + client.add_oracle(&admin, &oracle, &weather(), &90u32); + let now: u64 = 1_748_736_000; + env.ledger().with_mut(|l| l.timestamp = now); + client.submit_data(&oracle, &weather(), &kisumu_key(), &30_000_000i128, &95u32, &now); + let condition = TriggerCondition { + data_type: weather(), + key: kisumu_key(), + threshold: 50_000_000i128, + comparison: TriggerComparison::LessThan, + }; + // data is fresh (age = 0s), max_age = 3600s + let result = client.verify_trigger_fresh(&weather(), &kisumu_key(), &condition, &3600u64); + assert!(result); +} + +#[test] +#[should_panic(expected = "Error(Contract, #9)")] +fn verify_trigger_fresh_rejects_stale_data() { + let (env, admin, contract_id) = setup(); + let client = OracleVerifierClient::new(&env, &contract_id); + let oracle = Address::generate(&env); + client.add_oracle(&admin, &oracle, &weather(), &90u32); + // Submit at t=100, ledger timestamp at t=100+86401 (>24h later) + client.submit_data(&oracle, &weather(), &kisumu_key(), &30_000_000i128, &95u32, &100u64); + env.ledger().with_mut(|l| l.timestamp = 100 + 86_401); + let condition = TriggerCondition { + data_type: weather(), + key: kisumu_key(), + threshold: 50_000_000i128, + comparison: TriggerComparison::LessThan, + }; + client.verify_trigger_fresh(&weather(), &kisumu_key(), &condition, &86_400u64); +} + +#[test] +fn batch_submit_data_stores_all_keys() { + use soroban_sdk::Vec; + + let (env, admin, contract_id) = setup(); + let client = OracleVerifierClient::new(&env, &contract_id); + let oracle = Address::generate(&env); + client.add_oracle(&admin, &oracle, &weather(), &90u32); + + let flight_key = symbol_short!("flightKQ"); + let mut subs: Vec<(Symbol, i128, u32, u64)> = Vec::new(&env); + subs.push_back((kisumu_key(), 30_000_000i128, 90u32, 1_748_736_000u64)); + subs.push_back((flight_key.clone(), 120_000_000i128, 85u32, 1_748_736_000u64)); + + client.batch_submit_data(&oracle, &weather(), &subs); + + let dp1 = client.get_data(&weather(), &kisumu_key()); + let dp2 = client.get_data(&weather(), &flight_key); + assert_eq!(dp1.value, 30_000_000i128); + assert_eq!(dp2.value, 120_000_000i128); +} From dddb5d465b1e8056657c0c137539668a917643d6 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Fri, 5 Jun 2026 10:00:00 +0100 Subject: [PATCH 22/38] test(claims-processor): add integration tests for batch_auto_process and idempotency --- contracts/claims-processor/src/lib.rs | 2 + .../claims-processor/src/test_integration.rs | 149 ++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 contracts/claims-processor/src/test_integration.rs diff --git a/contracts/claims-processor/src/lib.rs b/contracts/claims-processor/src/lib.rs index 7c7e0a2..8d6efc1 100644 --- a/contracts/claims-processor/src/lib.rs +++ b/contracts/claims-processor/src/lib.rs @@ -344,3 +344,5 @@ fn map_comparison( #[cfg(test)] mod test; +#[cfg(test)] +mod test_integration; diff --git a/contracts/claims-processor/src/test_integration.rs b/contracts/claims-processor/src/test_integration.rs new file mode 100644 index 0000000..07a083a --- /dev/null +++ b/contracts/claims-processor/src/test_integration.rs @@ -0,0 +1,149 @@ +//! Extended integration tests for the Claims Processor. +//! Covers batch processing, edge cases, and cross-contract error propagation. +#![cfg(test)] + +extern crate std; + +use soroban_sdk::{ + testutils::{Address as _, Ledger}, + token, Address, Env, +}; + +use parashield_oracle_verifier::{OracleVerifier, OracleVerifierClient}; +use parashield_policy_engine::{ + CreateProductParams, PolicyEngine, PolicyEngineClient, + TriggerComparison, TriggerType, +}; +use crate::{ClaimsProcessor, ClaimsProcessorClient, ClaimResult}; + +// ── helpers ─────────────────────────────────────────────────────────────────── + +use soroban_sdk::{symbol_short, Symbol}; + +fn weather() -> Symbol { symbol_short!("weather") } +fn flight() -> Symbol { symbol_short!("flight") } + +struct TestEnv { + env: Env, + oracle: Address, + policy: Address, + claims: Address, + admin: Address, + usdc: Address, + oracle_node: Address, +} + +fn full_setup() -> TestEnv { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let oracle_node = Address::generate(&env); + + let usdc_id = env.register_stellar_asset_contract_v2(admin.clone()).address(); + let oracle_id = env.register(OracleVerifier, ()); + let policy_id = env.register(PolicyEngine, ()); + let claims_id = env.register(ClaimsProcessor, ()); + + OracleVerifierClient::new(&env, &oracle_id).initialize(&admin); + PolicyEngineClient::new(&env, &policy_id) + .initialize(&admin, &usdc_id, &oracle_id); + ClaimsProcessorClient::new(&env, &claims_id) + .initialize(&admin, &policy_id, &oracle_id); + PolicyEngineClient::new(&env, &policy_id) + .set_claims_processor(&admin, &claims_id); + + TestEnv { env, oracle: oracle_id, policy: policy_id, claims: claims_id, admin, usdc: usdc_id, oracle_node } +} + +fn create_drought_product(te: &TestEnv) -> u128 { + PolicyEngineClient::new(&te.env, &te.policy).create_product( + &te.admin, + &CreateProductParams { + name: symbol_short!("drght"), + category: symbol_short!("crop"), + trigger_type: TriggerType::Threshold, + oracle_data_type: weather(), + trigger_threshold: 500_000_000i128, + trigger_comparison: TriggerComparison::LessThan, + coverage_min: 1_000_0000000i128, + coverage_max: 100_000_0000000i128, + premium_rate_bps: 300u32, + max_duration_days: 180u32, + }, + ) +} + +// ── batch_auto_process tests ────────────────────────────────────────────────── + +#[test] +fn batch_processes_multiple_pending_claims() { + let te = full_setup(); + let claims_client = ClaimsProcessorClient::new(&te.env, &te.claims); + let policy_client = PolicyEngineClient::new(&te.env, &te.policy); + let oracle_client = OracleVerifierClient::new(&te.env, &te.oracle); + + let prod_id = create_drought_product(&te); + + oracle_client.add_oracle(&te.admin, &te.oracle_node, &weather(), &90u32); + oracle_client.submit_data( + &te.oracle_node, &weather(), &symbol_short!("kis2606"), + &30_000_000i128, &95u32, &1_748_736_000u64, + ); + + let farmer1 = Address::generate(&te.env); + let farmer2 = Address::generate(&te.env); + token::StellarAssetClient::new(&te.env, &te.usdc).mint(&farmer1, &10_000_0000000i128); + token::StellarAssetClient::new(&te.env, &te.usdc).mint(&farmer2, &10_000_0000000i128); + token::StellarAssetClient::new(&te.env, &te.usdc).mint( + &te.policy, &1_000_000_0000000i128, + ); + + let p1 = policy_client.buy_policy( + &farmer1, &prod_id, &1_000_0000000i128, &symbol_short!("kis2606"), &30u32, + ); + let p2 = policy_client.buy_policy( + &farmer2, &prod_id, &2_000_0000000i128, &symbol_short!("kis2606"), &30u32, + ); + + claims_client.submit_claim(&farmer1, &p1); + claims_client.submit_claim(&farmer2, &p2); + + let results = claims_client.batch_auto_process(&te.admin, &10u32); + assert_eq!(results.len(), 2); + // Both should trigger (rainfall 30mm < 50mm threshold) + for i in 0..results.len() { + let (_cid, result) = results.get_unchecked(i); + assert_eq!(result, ClaimResult::Paid); + } +} + +#[test] +fn batch_skips_non_pending_claims() { + let te = full_setup(); + let claims_client = ClaimsProcessorClient::new(&te.env, &te.claims); + let policy_client = PolicyEngineClient::new(&te.env, &te.policy); + let oracle_client = OracleVerifierClient::new(&te.env, &te.oracle); + + let prod_id = create_drought_product(&te); + + oracle_client.add_oracle(&te.admin, &te.oracle_node, &weather(), &90u32); + oracle_client.submit_data( + &te.oracle_node, &weather(), &symbol_short!("kis2606"), + &30_000_000i128, &95u32, &1_748_736_000u64, + ); + + let farmer = Address::generate(&te.env); + token::StellarAssetClient::new(&te.env, &te.usdc).mint(&farmer, &10_000_0000000i128); + token::StellarAssetClient::new(&te.env, &te.usdc).mint(&te.policy, &1_000_000_0000000i128); + + let p1 = policy_client.buy_policy( + &farmer, &prod_id, &1_000_0000000i128, &symbol_short!("kis2606"), &30u32, + ); + let claim_id = claims_client.submit_claim(&farmer, &p1); + // Process it once + claims_client.auto_process(&te.admin, &claim_id); + // Batch with limit=10 should return 0 results (already processed) + let results = claims_client.batch_auto_process(&te.admin, &10u32); + assert_eq!(results.len(), 0); +} From cbc26d6d55369de0fba20c06c52514ee6bc999c7 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Fri, 5 Jun 2026 14:00:00 +0100 Subject: [PATCH 23/38] docs: add SECURITY.md with vulnerability disclosure policy and bug bounty scope --- SECURITY.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..eb4fff8 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,37 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +|---------|--------------------| +| v2.x | :white_check_mark: | +| v1.x | :x: | + +## Reporting a Vulnerability + +**Do not open a public GitHub issue for security vulnerabilities.** + +Email: security@parashield.xyz +PGP: Available on request. + +We aim to respond within 48 hours and will coordinate a disclosure timeline with you. + +## Scope + +- Oracle manipulation (false triggers, timestamp injection) +- Reentrancy or double-spend in claim settlement +- Admin key compromise / privilege escalation +- LP fund drainage via share arithmetic edge cases +- Governance proposal execution bypasses + +## Out of Scope + +- Stellar network-level issues +- Social engineering attacks +- Theoretical issues without a working PoC +- Issues in third-party dependencies not controllable by this codebase + +## Bug Bounty + +Critical vulnerabilities may be eligible for a bounty. +Details announced at launch. From bbae9773f2f476db257096d8a8661665fbbd134e Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Sat, 6 Jun 2026 09:00:00 +0100 Subject: [PATCH 24/38] docs: add CONTRIBUTING.md with dev setup, conventions, and PR checklist --- CONTRIBUTING.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4900ce6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,53 @@ +# Contributing to Parashield Contracts + +## Setup + +```bash +# Install Rust + Soroban target +rustup target add wasm32v1-none + +# Build +make build + +# Test +make test + +# Lint +make lint +``` + +## Conventions + +- All storage keys use `#[contracttype]` enums. +- Monetary values use 7-decimal fixed-point `i128` (1 USDC = 10_000_000). +- All public functions that modify state require `caller.require_auth()`. +- Errors use `#[contracterror]` with explicit `#[repr(u32)]` codes. +- Tests live in `src/test.rs` (unit) and `src/test_integration.rs` (cross-contract). + +## PR Checklist + +- [ ] `make test` passes +- [ ] `make lint` passes with zero warnings +- [ ] New public functions have a short doc comment explaining the invariants +- [ ] Error codes do not collide with existing ones +- [ ] Persistent storage keys are documented in the StorageKey enum comment + +## Adding a New Contract + +1. `cargo new --lib contracts/` +2. Add to `contracts/Cargo.toml` `[workspace.members]` +3. Set `crate-type = ["cdylib", "rlib"]` in the crate's `Cargo.toml` +4. Add a `testutils` feature that enables `soroban-sdk/testutils` +5. Write at least 5 unit tests covering init, happy path, and error paths +6. Update `ARCHITECTURE.md` with the new contract's role + +## Commit Messages + +Follow [Conventional Commits](https://www.conventionalcommits.org/): + +``` +feat(oracle-verifier): add batch_submit_data +fix(risk-pool): guard against zero deposit edge case +test(claims-processor): add dispute resolution tests +chore: update soroban-sdk to 22.1 +``` From 9b03c9ca29e69e9832a79723047c172cf801561f Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Sat, 6 Jun 2026 14:30:00 +0100 Subject: [PATCH 25/38] feat(risk-pool): add get_lp_count and get_available_liquidity query functions --- contracts/risk-pool/src/lib.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/contracts/risk-pool/src/lib.rs b/contracts/risk-pool/src/lib.rs index 4cac67e..ea55a90 100644 --- a/contracts/risk-pool/src/lib.rs +++ b/contracts/risk-pool/src/lib.rs @@ -277,6 +277,20 @@ impl RiskPool { .unwrap_or_else(|| panic_with_error!(&env, Error::NotInitialized)) } + pub fn get_lp_count(env: Env) -> u32 { + let list: Vec
= env.storage().instance() + .get(&StorageKey::LpList) + .unwrap_or_else(|| Vec::new(&env)); + list.len() + } + + /// Available (unlocked) liquidity in USDC stroops. + pub fn get_available_liquidity(env: Env) -> i128 { + let deposited: i128 = env.storage().instance().get(&StorageKey::TotalDeposited).unwrap_or(0); + let locked: i128 = env.storage().instance().get(&StorageKey::TotalLocked).unwrap_or(0); + deposited.saturating_sub(locked) + } + // ── Admin ───────────────────────────────────────────────────────────────── pub fn pause(env: Env, admin: Address) { From 92ecb14ff1625d1e91ce87948746df7d1f8d3e5b Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Sun, 7 Jun 2026 10:00:00 +0100 Subject: [PATCH 26/38] test(risk-pool): add multi-LP proportional yield, LP count, and available liquidity tests --- contracts/risk-pool/src/lib.rs | 2 + contracts/risk-pool/src/test_advanced.rs | 89 ++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 contracts/risk-pool/src/test_advanced.rs diff --git a/contracts/risk-pool/src/lib.rs b/contracts/risk-pool/src/lib.rs index ea55a90..c038914 100644 --- a/contracts/risk-pool/src/lib.rs +++ b/contracts/risk-pool/src/lib.rs @@ -319,3 +319,5 @@ impl RiskPool { #[cfg(test)] mod test; +#[cfg(test)] +mod test_advanced; diff --git a/contracts/risk-pool/src/test_advanced.rs b/contracts/risk-pool/src/test_advanced.rs new file mode 100644 index 0000000..a3fd548 --- /dev/null +++ b/contracts/risk-pool/src/test_advanced.rs @@ -0,0 +1,89 @@ +//! Advanced risk-pool tests: multi-LP scenarios, yield accounting, LP count. +#![cfg(test)] + +extern crate std; + +use soroban_sdk::{ + testutils::{Address as _, Ledger}, + token, Address, Env, Symbol, +}; + +use crate::{RiskPool, RiskPoolClient}; + +fn setup_multi() -> (Env, RiskPoolClient<'static>, Address, Address, Address, Address, Address) { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + let lp1 = Address::generate(&env); + let lp2 = Address::generate(&env); + + let usdc_id = env.register_stellar_asset_contract_v2(admin.clone()).address(); + let pool_id = env.register(RiskPool, ()); + let pool = RiskPoolClient::new(&env, &pool_id); + + let mint = |addr: &Address| { + token::StellarAssetClient::new(&env, &usdc_id).mint(addr, &10_000_0000000i128); + }; + mint(&lp1); + mint(&lp2); + + pool.initialize(&admin, &usdc_id, &treasury, &Symbol::new(&env, "defi")); + + (env, pool, usdc_id, admin, treasury, lp1, lp2) +} + +#[test] +fn lp_count_tracks_unique_depositors() { + let (_, pool, _, _, _, lp1, lp2) = setup_multi(); + assert_eq!(pool.get_lp_count(), 0); + pool.deposit(&lp1, &100_0000000i128); + assert_eq!(pool.get_lp_count(), 1); + pool.deposit(&lp2, &200_0000000i128); + assert_eq!(pool.get_lp_count(), 2); + // Second deposit from lp1 should not increment count + pool.deposit(&lp1, &50_0000000i128); + assert_eq!(pool.get_lp_count(), 2); +} + +#[test] +fn available_liquidity_decreases_with_locks() { + let (_, pool, _, admin, _, lp1, _) = setup_multi(); + pool.deposit(&lp1, &500_0000000i128); + assert_eq!(pool.get_available_liquidity(), 500_0000000i128); + pool.lock_for_policy(&admin, &1u128, &200_0000000i128); + assert_eq!(pool.get_available_liquidity(), 300_0000000i128); + pool.release_for_claim(&admin, &1u128); + assert_eq!(pool.get_available_liquidity(), 500_0000000i128); +} + +#[test] +fn two_lps_receive_proportional_yield() { + let (_, pool, _, _, _, lp1, lp2) = setup_multi(); + pool.deposit(&lp1, &300_0000000i128); // 3/4 of pool + pool.deposit(&lp2, &100_0000000i128); // 1/4 of pool + + // premium: 400 USDC → 320 USDC to LP accumulated (80%) + pool.receive_premium(&lp1, &400_0000000i128); + + let y1 = pool.claim_yield(&lp1); + let y2 = pool.claim_yield(&lp2); + // lp1 should get 3x lp2's yield + assert_eq!(y1, 3 * y2); + assert_eq!(y1 + y2, 320_0000000i128); +} + +#[test] +fn get_stats_reflects_all_operations() { + let (_, pool, _, admin, _, lp1, lp2) = setup_multi(); + pool.deposit(&lp1, &300_0000000i128); + pool.deposit(&lp2, &100_0000000i128); + pool.lock_for_policy(&admin, &5u128, &80_0000000i128); + pool.receive_premium(&lp1, &100_0000000i128); + + let stats = pool.get_stats(); + assert_eq!(stats.total_deposited, 400_0000000i128); + assert_eq!(stats.total_locked, 80_0000000i128); + assert_eq!(stats.accumulated_premium, 80_0000000i128); // 80% of 100 +} From 7c0637006c6fcbf397389da71ceec33eeb4cf531 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Sun, 7 Jun 2026 15:30:00 +0100 Subject: [PATCH 27/38] test(governance-dao): add abstain vote, config update, double-execute guard tests --- contracts/governance-dao/src/lib.rs | 2 + contracts/governance-dao/src/test_advanced.rs | 133 ++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 contracts/governance-dao/src/test_advanced.rs diff --git a/contracts/governance-dao/src/lib.rs b/contracts/governance-dao/src/lib.rs index 95850ec..e3417ba 100644 --- a/contracts/governance-dao/src/lib.rs +++ b/contracts/governance-dao/src/lib.rs @@ -270,3 +270,5 @@ impl GovernanceDao { #[cfg(test)] mod test; +#[cfg(test)] +mod test_advanced; diff --git a/contracts/governance-dao/src/test_advanced.rs b/contracts/governance-dao/src/test_advanced.rs new file mode 100644 index 0000000..a4afe21 --- /dev/null +++ b/contracts/governance-dao/src/test_advanced.rs @@ -0,0 +1,133 @@ +//! Advanced governance-dao tests: update_config, multi-voter scenarios. +#![cfg(test)] + +extern crate std; + +use soroban_sdk::{ + testutils::{Address as _, Ledger}, + token, Address, Bytes, Env, Symbol, +}; + +use crate::{DaoConfig, GovernanceDao, GovernanceDaoClient, ProposalStatus, VoteChoice}; + +const VOTING_PERIOD: u64 = 7 * 24 * 3600; + +fn base_config(gov_token: Address) -> DaoConfig { + DaoConfig { + gov_token, + proposal_threshold: 10_000_0000000i128, + quorum_bps: 1_000u32, + majority_bps: 5_100u32, + voting_period: VOTING_PERIOD, + } +} + +fn make_dao(env: &Env) -> (GovernanceDaoClient<'static>, Address, Address, Address) { + let admin = Address::generate(env); + let voter = Address::generate(env); + let target = Address::generate(env); + + let gov_token_id = env.register_stellar_asset_contract_v2(admin.clone()).address(); + token::StellarAssetClient::new(env, &gov_token_id) + .mint(&voter, &1_000_000_0000000i128); + token::StellarAssetClient::new(env, &gov_token_id) + .mint(&admin, &100_000_0000000i128); + + let dao_id = env.register(GovernanceDao, ()); + let dao = GovernanceDaoClient::new(env, &dao_id); + dao.initialize(&admin, &base_config(gov_token_id)); + + (dao, admin, voter, target) +} + +// ── config update ───────────────────────────────────────────────────────────── + +#[test] +fn admin_can_update_config() { + let env = Env::default(); + env.mock_all_auths(); + let (dao, admin, _, _) = make_dao(&env); + + let cfg_before = dao.get_config(); + let new_cfg = DaoConfig { + quorum_bps: 2_000u32, + majority_bps: 6_000u32, + ..cfg_before.clone() + }; + dao.update_config(&admin, &new_cfg); + let cfg_after = dao.get_config(); + assert_eq!(cfg_after.quorum_bps, 2_000); + assert_eq!(cfg_after.majority_bps, 6_000); +} + +#[test] +#[should_panic(expected = "Error(Contract, #3)")] +fn non_admin_cannot_update_config() { + let env = Env::default(); + env.mock_all_auths(); + let (dao, _, voter, _) = make_dao(&env); + let cfg = dao.get_config(); + dao.update_config(&voter, &cfg); +} + +// ── abstain vote ────────────────────────────────────────────────────────────── + +#[test] +fn abstain_contributes_to_quorum_but_not_majority() { + let env = Env::default(); + env.mock_all_auths(); + let (dao, _, voter, target) = make_dao(&env); + + let pid = dao.create_proposal( + &voter, + &Bytes::from_slice(&env, b"Abstain test"), + &target, + &Symbol::new(&env, "update"), + ); + dao.vote(&voter, &pid, &VoteChoice::Abstain); + env.ledger().with_mut(|l| l.timestamp += VOTING_PERIOD + 1); + dao.finalize(&pid); + + let p = dao.get_proposal(&pid); + // Abstain satisfies quorum but no FOR votes → fails majority check + assert_eq!(p.status, ProposalStatus::Failed); + assert_eq!(p.votes_abstain, 1_000_000_0000000i128); + assert_eq!(p.votes_for, 0); +} + +// ── proposal_count ──────────────────────────────────────────────────────────── + +#[test] +fn proposal_count_increments_per_proposal() { + let env = Env::default(); + env.mock_all_auths(); + let (dao, _, voter, target) = make_dao(&env); + + assert_eq!(dao.proposal_count(), 0); + dao.create_proposal(&voter, &Bytes::from_slice(&env, b"P1"), &target, &Symbol::new(&env, "fn1")); + assert_eq!(dao.proposal_count(), 1); + dao.create_proposal(&voter, &Bytes::from_slice(&env, b"P2"), &target, &Symbol::new(&env, "fn2")); + assert_eq!(dao.proposal_count(), 2); +} + +// ── double-execute guard ────────────────────────────────────────────────────── + +#[test] +#[should_panic(expected = "Error(Contract, #11)")] +fn execute_twice_fails() { + let env = Env::default(); + env.mock_all_auths(); + let (dao, _, voter, target) = make_dao(&env); + + let pid = dao.create_proposal( + &voter, + &Bytes::from_slice(&env, b"Execute twice"), + &target, + &Symbol::new(&env, "update"), + ); + dao.vote(&voter, &pid, &VoteChoice::For); + env.ledger().with_mut(|l| l.timestamp += VOTING_PERIOD + 1); + dao.finalize(&pid); + dao.execute(&pid); + dao.execute(&pid); // second execute should panic +} From 12d55d16ea477ec86a388cb800aec40543f26948 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Mon, 8 Jun 2026 09:30:00 +0100 Subject: [PATCH 28/38] test(policy-engine): add emergency pause, product lifecycle, and cancellation tests --- contracts/policy-engine/src/lib.rs | 2 + contracts/policy-engine/src/test_advanced.rs | 130 +++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 contracts/policy-engine/src/test_advanced.rs diff --git a/contracts/policy-engine/src/lib.rs b/contracts/policy-engine/src/lib.rs index df195fc..ac9e4e8 100644 --- a/contracts/policy-engine/src/lib.rs +++ b/contracts/policy-engine/src/lib.rs @@ -367,3 +367,5 @@ impl PolicyEngine { #[cfg(test)] mod test; +#[cfg(test)] +mod test_advanced; diff --git a/contracts/policy-engine/src/test_advanced.rs b/contracts/policy-engine/src/test_advanced.rs new file mode 100644 index 0000000..f824272 --- /dev/null +++ b/contracts/policy-engine/src/test_advanced.rs @@ -0,0 +1,130 @@ +//! Advanced policy-engine tests: emergency pause, product deprecation, cancel. +#![cfg(test)] + +extern crate std; + +use soroban_sdk::{ + testutils::{Address as _, Ledger}, + token, Address, Env, +}; + +use crate::{ + CreateProductParams, PolicyEngine, PolicyEngineClient, + ProductStatus, TriggerComparison, TriggerType, +}; +use soroban_sdk::symbol_short; + +fn basic_params() -> CreateProductParams { + CreateProductParams { + name: symbol_short!("crop"), + category: symbol_short!("crop"), + trigger_type: TriggerType::Threshold, + oracle_data_type: symbol_short!("weather"), + trigger_threshold: 500_000_000i128, + trigger_comparison: TriggerComparison::LessThan, + coverage_min: 100_0000000i128, + coverage_max: 100_000_0000000i128, + premium_rate_bps: 300u32, + max_duration_days: 90u32, + } +} + +fn setup() -> (Env, PolicyEngineClient<'static>, Address, Address, Address) { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let oracle = Address::generate(&env); + let user = Address::generate(&env); + + let usdc = env.register_stellar_asset_contract_v2(admin.clone()).address(); + token::StellarAssetClient::new(&env, &usdc).mint(&user, &10_000_0000000i128); + token::StellarAssetClient::new(&env, &usdc).mint(&admin, &1_000_000_0000000i128); + + let pe_id = env.register(PolicyEngine, ()); + let pe = PolicyEngineClient::new(&env, &pe_id); + pe.initialize(&admin, &usdc, &oracle); + + // fund the contract for coverage payouts + token::StellarAssetClient::new(&env, &usdc).mint(&pe_id, &1_000_000_0000000i128); + + (env, pe, admin, oracle, user) +} + +// ── emergency pause ──────────────────────────────────────────────────────────── + +#[test] +fn is_paused_defaults_to_false() { + let (_, pe, _, _, _) = setup(); + assert!(!pe.is_paused()); +} + +#[test] +fn emergency_pause_sets_paused_flag() { + let (_, pe, admin, _, _) = setup(); + pe.emergency_pause(&admin); + assert!(pe.is_paused()); +} + +#[test] +fn emergency_resume_clears_paused_flag() { + let (_, pe, admin, _, _) = setup(); + pe.emergency_pause(&admin); + pe.emergency_resume(&admin); + assert!(!pe.is_paused()); +} + +#[test] +#[should_panic(expected = "Error(Contract, #3)")] +fn non_admin_cannot_pause() { + let (env, pe, _, _, user) = setup(); + pe.emergency_pause(&user); +} + +// ── product lifecycle ────────────────────────────────────────────────────────── + +#[test] +fn deprecate_product_removes_from_active_list() { + let (_, pe, admin, _, _) = setup(); + let prod_id = pe.create_product(&admin, &basic_params()); + assert_eq!(pe.get_active_products().len(), 1); + pe.deprecate_product(&admin, &prod_id); + assert_eq!(pe.get_active_products().len(), 0); + let prod = pe.get_product(&prod_id); + assert_eq!(prod.status, ProductStatus::Deprecated); +} + +#[test] +fn pause_product_changes_status_but_stays_in_active_list() { + let (_, pe, admin, _, _) = setup(); + let prod_id = pe.create_product(&admin, &basic_params()); + pe.pause_product(&admin, &prod_id); + let prod = pe.get_product(&prod_id); + assert_eq!(prod.status, ProductStatus::Paused); +} + +// ── policy cancellation ──────────────────────────────────────────────────────── + +#[test] +fn cancel_policy_returns_premium_to_holder() { + let (env, pe, admin, _, user) = setup(); + let prod_id = pe.create_product(&admin, &basic_params()); + let policy_id = pe.buy_policy( + &user, &prod_id, &1_000_0000000i128, &symbol_short!("kis2606"), &30u32, + ); + pe.cancel_policy(&user, &policy_id); + let policy = pe.get_policy(&policy_id); + assert_eq!(policy.status, crate::PolicyStatus::Cancelled); +} + +// ── user policy query ───────────────────────────────────────────────────────── + +#[test] +fn get_user_policies_tracks_multiple_policies() { + let (_, pe, admin, _, user) = setup(); + let prod_id = pe.create_product(&admin, &basic_params()); + pe.buy_policy(&user, &prod_id, &200_0000000i128, &symbol_short!("kis2606"), &30u32); + pe.buy_policy(&user, &prod_id, &300_0000000i128, &symbol_short!("kis2606"), &30u32); + let policies = pe.get_user_policies(&user); + assert_eq!(policies.len(), 2); +} From 8105b2e1ee0b43cb10a178c1c868d89bc5a099fe Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Mon, 8 Jun 2026 14:00:00 +0100 Subject: [PATCH 29/38] scripts: add check_balances.sh for quick USDC balance health check --- scripts/check_balances.sh | 54 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 scripts/check_balances.sh diff --git a/scripts/check_balances.sh b/scripts/check_balances.sh new file mode 100644 index 0000000..b5222cd --- /dev/null +++ b/scripts/check_balances.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# check_balances.sh — Quick health check on deployed contract balances. +# +# Usage: +# POLICY_ENGINE_ID= RISK_POOL_ID= USDC_ID= NETWORK=testnet \ +# ./scripts/check_balances.sh + +set -euo pipefail + +POLICY_ENGINE_ID="${POLICY_ENGINE_ID:-}" +RISK_POOL_ID="${RISK_POOL_ID:-}" +USDC_ID="${USDC_ID:-}" +NETWORK="${NETWORK:-testnet}" + +[[ -n "$USDC_ID" ]] || { echo "Error: USDC_ID not set" >&2; exit 1; } + +log() { echo "[check-balances] $*"; } + +STROOPS=10000000 + +format_usdc() { + local raw="$1" + echo "scale=7; $raw / $STROOPS" | bc +} + +log "=== Parashield Balance Check (${NETWORK}) ===" +echo "" + +if [[ -n "$POLICY_ENGINE_ID" ]]; then + RAW=$(stellar contract invoke \ + --id "$USDC_ID" \ + --network "$NETWORK" \ + -- balance \ + --id "$POLICY_ENGINE_ID" 2>/dev/null || echo "0") + log "PolicyEngine USDC balance: $(format_usdc "$RAW") USDC (${RAW} stroops)" +fi + +if [[ -n "$RISK_POOL_ID" ]]; then + RAW=$(stellar contract invoke \ + --id "$USDC_ID" \ + --network "$NETWORK" \ + -- balance \ + --id "$RISK_POOL_ID" 2>/dev/null || echo "0") + log "RiskPool USDC balance: $(format_usdc "$RAW") USDC (${RAW} stroops)" + + STATS=$(stellar contract invoke \ + --id "$RISK_POOL_ID" \ + --network "$NETWORK" \ + -- get_stats 2>/dev/null || echo "{}") + log "RiskPool stats: $STATS" +fi + +log "" +log "Done." From 3fc616dc0eb998421eb113363b9cb1eda8625ce7 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Tue, 9 Jun 2026 09:00:00 +0100 Subject: [PATCH 30/38] scripts: add submit_oracle_data.sh for manual oracle data submission --- scripts/submit_oracle_data.sh | 54 +++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 scripts/submit_oracle_data.sh diff --git a/scripts/submit_oracle_data.sh b/scripts/submit_oracle_data.sh new file mode 100644 index 0000000..4c70386 --- /dev/null +++ b/scripts/submit_oracle_data.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# submit_oracle_data.sh — Submit a data point to the OracleVerifier contract. +# +# Usage: +# ORACLE_VERIFIER_ID= ORACLE_KEY= \ +# DATA_TYPE=weather KEY=kis2606 VALUE=30000000 CONFIDENCE=92 \ +# NETWORK=testnet \ +# ./scripts/submit_oracle_data.sh +# +# VALUE is in 7-decimal fixed point: 30000000 = 30.0000000 mm rainfall + +set -euo pipefail + +ORACLE_VERIFIER_ID="${ORACLE_VERIFIER_ID:-}" +ORACLE_KEY="${ORACLE_KEY:-oracle}" +DATA_TYPE="${DATA_TYPE:-weather}" +KEY="${KEY:-}" +VALUE="${VALUE:-}" +CONFIDENCE="${CONFIDENCE:-80}" +NETWORK="${NETWORK:-testnet}" + +[[ -n "$ORACLE_VERIFIER_ID" ]] || { echo "Error: ORACLE_VERIFIER_ID not set" >&2; exit 1; } +[[ -n "$KEY" ]] || { echo "Error: KEY not set (e.g. kis2606)" >&2; exit 1; } +[[ -n "$VALUE" ]] || { echo "Error: VALUE not set (7-decimal stroops)" >&2; exit 1; } + +ORACLE_ADDR=$(stellar keys address "$ORACLE_KEY") +TIMESTAMP=$(date +%s) + +log() { echo "[oracle-submit] $*"; } + +log "Submitting oracle data:" +log " Contract: $ORACLE_VERIFIER_ID" +log " Oracle: $ORACLE_ADDR" +log " Data type: $DATA_TYPE" +log " Key: $KEY" +log " Value: $VALUE (= $(echo "scale=7; $VALUE / 10000000" | bc))" +log " Confidence: $CONFIDENCE / 100" +log " Timestamp: $TIMESTAMP ($(date -r "$TIMESTAMP" 2>/dev/null || date -d "@$TIMESTAMP"))" +log " Network: $NETWORK" + +stellar contract invoke \ + --id "$ORACLE_VERIFIER_ID" \ + --source "$ORACLE_KEY" \ + --network "$NETWORK" \ + -- submit_data \ + --oracle "$ORACLE_ADDR" \ + --data_type "$DATA_TYPE" \ + --key "$KEY" \ + --value "$VALUE" \ + --confidence "$CONFIDENCE" \ + --timestamp "$TIMESTAMP" + +log "" +log "Data submitted successfully." From 4b6f54a1f14cc0a2e598bc94476071a980f0cd2e Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Tue, 9 Jun 2026 14:30:00 +0100 Subject: [PATCH 31/38] ci: add release workflow to publish WASM artifacts on version tags --- .github/workflows/release.yml | 63 +++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f1c345d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,63 @@ +name: Release WASM Artifacts + +on: + push: + tags: + - "v*.*.*" + +permissions: + contents: write + +jobs: + build-and-release: + name: Build & Release WASM + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32v1-none + + - name: Cache Cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + contracts/target + key: ${{ runner.os }}-cargo-${{ hashFiles('contracts/Cargo.lock') }} + + - name: Run tests + working-directory: contracts + run: cargo test --quiet + + - name: Build optimized WASM + working-directory: contracts + run: cargo build --target wasm32v1-none --release --quiet + + - name: Collect artifacts + run: | + mkdir -p dist + cp contracts/target/wasm32v1-none/release/*.wasm dist/ + ls -lh dist/ + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: dist/*.wasm + generate_release_notes: true + body: | + ## Parashield Protocol ${{ github.ref_name }} + + ### Contracts + - `parashield_oracle_verifier.wasm` — Oracle data aggregation + - `parashield_policy_engine.wasm` — Insurance product and policy management + - `parashield_claims_processor.wasm` — Automated claim evaluation + - `parashield_risk_pool.wasm` — LP liquidity pools with yield distribution + - `parashield_governance_dao.wasm` — Token-weighted protocol governance + + ### Verification + Verify WASM hash against the published checksum before deploying. From 06573a8fe15591ffeaf531ff1614b61def29850d Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Wed, 10 Jun 2026 09:00:00 +0100 Subject: [PATCH 32/38] test(oracle-verifier): add 3-oracle median, overwrite, deactivation, and even count median tests --- contracts/oracle-verifier/src/lib.rs | 2 + .../oracle-verifier/src/test_advanced.rs | 105 ++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 contracts/oracle-verifier/src/test_advanced.rs diff --git a/contracts/oracle-verifier/src/lib.rs b/contracts/oracle-verifier/src/lib.rs index f06274e..f8abd22 100644 --- a/contracts/oracle-verifier/src/lib.rs +++ b/contracts/oracle-verifier/src/lib.rs @@ -371,3 +371,5 @@ impl OracleVerifier { #[cfg(test)] mod test; +#[cfg(test)] +mod test_advanced; diff --git a/contracts/oracle-verifier/src/test_advanced.rs b/contracts/oracle-verifier/src/test_advanced.rs new file mode 100644 index 0000000..14c0f9e --- /dev/null +++ b/contracts/oracle-verifier/src/test_advanced.rs @@ -0,0 +1,105 @@ +//! Advanced oracle-verifier tests: multi-oracle median, confidence weighting, +//! oracle deactivation, and odd/even count median edge cases. +#![cfg(test)] + +use super::*; +use soroban_sdk::{symbol_short, testutils::Address as _, Env}; + +fn setup() -> (Env, Address, Address) { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let contract_id = env.register(OracleVerifier, ()); + OracleVerifierClient::new(&env, &contract_id).initialize(&admin); + (env, admin, contract_id) +} + +fn wt() -> soroban_sdk::Symbol { symbol_short!("weather") } +fn kk() -> soroban_sdk::Symbol { symbol_short!("kis2606") } + +#[test] +fn three_oracle_median_is_middle_value() { + let (env, admin, cid) = setup(); + let c = OracleVerifierClient::new(&env, &cid); + let o1 = Address::generate(&env); + let o2 = Address::generate(&env); + let o3 = Address::generate(&env); + c.add_oracle(&admin, &o1, &wt(), &90u32); + c.add_oracle(&admin, &o2, &wt(), &80u32); + c.add_oracle(&admin, &o3, &wt(), &70u32); + + c.submit_data(&o1, &wt(), &kk(), &10_000_000i128, &90u32, &1u64); + c.submit_data(&o2, &wt(), &kk(), &30_000_000i128, &90u32, &1u64); + c.submit_data(&o3, &wt(), &kk(), &20_000_000i128, &90u32, &1u64); + + let agg = c.get_aggregated(&wt(), &kk()); + // sorted: [10, 20, 30] → middle = 20 + assert_eq!(agg.median_value, 20_000_000i128); +} + +#[test] +fn deactivated_oracle_cannot_submit() { + let (env, admin, cid) = setup(); + let c = OracleVerifierClient::new(&env, &cid); + let oracle = Address::generate(&env); + c.add_oracle(&admin, &oracle, &wt(), &90u32); + c.remove_oracle(&admin, &oracle, &wt()); + + let result = std::panic::catch_unwind(|| { + c.submit_data(&oracle, &wt(), &kk(), &10_000_000i128, &90u32, &1u64); + }); + assert!(result.is_err(), "Expected panic after deactivation"); +} + +#[test] +fn overwrite_submission_updates_value() { + let (env, admin, cid) = setup(); + let c = OracleVerifierClient::new(&env, &cid); + let oracle = Address::generate(&env); + c.add_oracle(&admin, &oracle, &wt(), &90u32); + + c.submit_data(&oracle, &wt(), &kk(), &10_000_000i128, &90u32, &100u64); + c.submit_data(&oracle, &wt(), &kk(), &25_000_000i128, &95u32, &200u64); + + let dp = c.get_data(&wt(), &kk()); + assert_eq!(dp.value, 25_000_000i128); + assert_eq!(dp.confidence, 95u32); + + // only one submission in the list (overwrite, not append) + let agg = c.get_aggregated(&wt(), &kk()); + assert_eq!(agg.oracle_count, 1); +} + +#[test] +fn even_count_median_is_average_of_middle_two() { + let (env, admin, cid) = setup(); + let c = OracleVerifierClient::new(&env, &cid); + let o1 = Address::generate(&env); + let o2 = Address::generate(&env); + c.add_oracle(&admin, &o1, &wt(), &90u32); + c.add_oracle(&admin, &o2, &wt(), &80u32); + + // sorted: [10, 30] → average = 20 + c.submit_data(&o1, &wt(), &kk(), &10_000_000i128, &90u32, &1u64); + c.submit_data(&o2, &wt(), &kk(), &30_000_000i128, &80u32, &2u64); + + let agg = c.get_aggregated(&wt(), &kk()); + assert_eq!(agg.median_value, 20_000_000i128); +} + +#[test] +fn verify_trigger_greater_than() { + let (env, admin, cid) = setup(); + let c = OracleVerifierClient::new(&env, &cid); + let oracle = Address::generate(&env); + c.add_oracle(&admin, &oracle, &wt(), &90u32); + c.submit_data(&oracle, &wt(), &kk(), &80_000_000i128, &90u32, &1u64); + + let condition = TriggerCondition { + data_type: wt(), + key: kk(), + threshold: 50_000_000i128, + comparison: TriggerComparison::GreaterThan, + }; + assert!(c.verify_trigger(&wt(), &kk(), &condition)); +} From f17be482f7c6be418c1a43d1cbb3344960a1fe3c Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Wed, 10 Jun 2026 14:00:00 +0100 Subject: [PATCH 33/38] docs: add oracle-key-format.md with 9-char Symbol encoding convention and registered keys --- docs/oracle-key-format.md | 53 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/oracle-key-format.md diff --git a/docs/oracle-key-format.md b/docs/oracle-key-format.md new file mode 100644 index 0000000..7dd2d84 --- /dev/null +++ b/docs/oracle-key-format.md @@ -0,0 +1,53 @@ +# Oracle Key Format Reference + +Oracle keys must fit within the Soroban `Symbol` type — maximum 9 alphanumeric characters. + +## Key Encoding Convention + +``` + +``` + +| Component | Chars | Example | +|------------|-------|-------------------| +| type | 1–2 | `r` (rainfall) | +| location | 3–5 | `kis` (Kisumu) | +| period | 4 | `2606` (Jun 2026) | + +## Registered Keys + +### Weather + +| Key | Description | Unit | +|-----------|----------------------------------------|------------| +| `kis2606` | Kisumu rainfall, June 2026 | mm × 10⁷ | +| `nbi2606` | Nairobi temperature, June 2026 | °C × 10⁷ | +| `msa2607` | Mombasa wind speed, July 2026 | km/h × 10⁷ | + +### Flight + +| Key | Description | Unit | +|-----------|--------------------------------|-------------------| +| `kq1002606` | Kenya Airways KQ100 Jun 2026 | delay_min × 10⁷ | +| `et3002606` | Ethiopian Airlines Jun 2026 | delay_min × 10⁷ | + +### On-chain (DeFi) + +| Key | Description | Unit | +|-----------|------------------------------------|-------------------| +| `aavetvl` | Aave protocol TVL drop % | % × 10⁷ (0-100) | +| `comptvl` | Compound protocol TVL drop % | % × 10⁷ (0-100) | + +### Disaster + +| Key | Description | Unit | +|-----------|--------------------------|------------------------| +| `nbi2606` | Nairobi seismic data | Richter × 10⁷ | +| `msa2606` | Mombasa flood level | meters × 10⁷ | + +## Adding a New Key + +1. Choose a 9-char max Symbol that encodes type + location + period. +2. Register a corresponding oracle node with `add_oracle`. +3. Document it in this file and in `ARCHITECTURE.md § Oracle Key Table`. +4. Update the frontend `src/lib/constants.ts` oracle key registry. From 4647fa1078d11368b94a735e02488f2a7561328b Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Thu, 11 Jun 2026 10:00:00 +0100 Subject: [PATCH 34/38] docs: add economics.md covering premium flow, LP share mechanics, and risk category table --- docs/economics.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 docs/economics.md diff --git a/docs/economics.md b/docs/economics.md new file mode 100644 index 0000000..33a1e31 --- /dev/null +++ b/docs/economics.md @@ -0,0 +1,68 @@ +# Parashield Protocol Economics + +## Premium Flow + +``` +Policy buyer pays premium + │ + ▼ + PolicyEngine holds USDC + │ + ClaimsProcessor calls receive_premium() + │ + RiskPool splits: + ├── 80% → LP yield accumulated (distributed to LPs pro-rata on claim_yield()) + ├── 10% → Protocol treasury + └── 10% → Backstop fund (reserved for catastrophic loss events) +``` + +## Premium Rate + +Premium = `coverage_amount × premium_rate_bps / 10_000` + +Example: 1,000 USDC coverage at 3% rate = 30 USDC premium. + +## Claim Settlement + +``` +Trigger condition met (oracle confirms) + │ + ClaimsProcessor calls pay_claim() on PolicyEngine + │ + PolicyEngine transfers coverage_amount USDC → policyholder + │ + RiskPool releases capital lock for this policy +``` + +## LP Share Mechanics + +- First deposit: 1 share = 1 USDC (no dilution risk at launch) +- Subsequent deposits: `new_shares = deposit_amount × total_shares / total_deposited` +- Shares track proportional ownership of the pool's total USDC balance +- Share value decreases after a claim payout (loss socialized across LPs) +- Share value increases as premium yield accumulates + +## Utilization Rate + +``` +utilization_bps = total_locked × 10_000 / total_deposited +``` + +Target utilization: 60-80% (higher = more yield, higher tail risk). + +## Governance Token (SHIELD) + +- Used for voting weight in GovernanceDAO +- Holding SHIELD ≠ LP position; governance and liquidity provision are separate +- Proposal threshold: 10,000 SHIELD minimum to create a proposal +- Quorum: 10% of total SHIELD supply must vote +- Majority: 51% of cast votes must be FOR + +## Risk Categories + +| Category | Premium Rate | Target APY | Max Coverage | +|----------|-------------|------------|--------------| +| Crop | 3.0% | 12–18% | 100,000 USDC | +| Flight | 1.5% | 8–12% | 10,000 USDC | +| DeFi | 5.0% | 20–40% | 1,000,000 USDC | +| Disaster | 2.0% | 10–15% | 500,000 USDC | From fe741a81bf9bd9c039d9278a854fb63399c44c48 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Thu, 11 Jun 2026 15:00:00 +0100 Subject: [PATCH 35/38] docs: add deployment-checklist.md covering pre-deploy, testnet validation, and mainnet steps --- docs/deployment-checklist.md | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 docs/deployment-checklist.md diff --git a/docs/deployment-checklist.md b/docs/deployment-checklist.md new file mode 100644 index 0000000..e48faba --- /dev/null +++ b/docs/deployment-checklist.md @@ -0,0 +1,44 @@ +# Deployment Checklist + +Complete every step in order. Do not skip. + +## Pre-Deployment + +- [ ] `make test` passes on the target commit (zero failures) +- [ ] `make lint` passes with zero warnings +- [ ] Soroban SDK version pinned in `Cargo.toml` (no `*` ranges) +- [ ] All contract IDs from Testnet deployment recorded in `.deploy-state` +- [ ] Admin key is a multi-sig or hardware wallet — not a hot key +- [ ] USDC token address verified on the target network +- [ ] At least 2 oracle nodes registered and tested on Testnet + +## Testnet Validation + +- [ ] `./scripts/deploy_testnet.sh` completes without errors +- [ ] `./scripts/create_products.sh` — all 4 products created +- [ ] `./scripts/register_oracle.sh` — at least 1 oracle per data type +- [ ] Manual policy purchase via CLI succeeds +- [ ] Manual oracle data submission succeeds +- [ ] Manual claim submission succeeds +- [ ] Parametric auto_process triggers a payout +- [ ] LP deposit, yield claim, and withdrawal tested +- [ ] DAO proposal creation, voting, and execution tested + +## Mainnet Deployment + +- [ ] Read through `./scripts/deploy_mainnet.sh` — understand every step +- [ ] Announce maintenance window in Discord/Telegram +- [ ] Run `./scripts/deploy_mainnet.sh` — type `DEPLOY MAINNET` when prompted +- [ ] Verify each contract ID on Stellar Expert (mainnet) +- [ ] Call `initialize()` on each contract via signed transaction +- [ ] Wire `set_claims_processor` in PolicyEngine +- [ ] Run `./scripts/check_balances.sh` to verify zero balances before launch +- [ ] Deposit initial USDC liquidity into RiskPool (backstop fund) + +## Post-Deployment + +- [ ] Update frontend `src/lib/constants.ts` with Mainnet contract IDs +- [ ] Update `ARCHITECTURE.md` with deployed contract addresses +- [ ] Post announcement with contract IDs for community verification +- [ ] Monitor first 24h of claims processing +- [ ] Set up oracle data submission cron jobs From b36185d72880e172d03558c8b9e5cf7fb126890b Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Fri, 12 Jun 2026 09:00:00 +0100 Subject: [PATCH 36/38] chore: add workspace metadata with protocol version and audit status to Cargo.toml --- contracts/Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 02dbd6a..af58cd3 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -11,6 +11,11 @@ resolver = "2" [workspace.dependencies] soroban-sdk = { version = "22", features = ["testutils"] } +[workspace.metadata.parashield] +protocol-version = "2" +audit-status = "pending" +target-network = "stellar-mainnet" + [profile.release] opt-level = "z" overflow-checks = true From 6ac8a86f7ab81021b0046649b49337a3f3b80237 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Fri, 12 Jun 2026 15:00:00 +0100 Subject: [PATCH 37/38] =?UTF-8?q?test(risk-pool):=20add=20edge=20case=20te?= =?UTF-8?q?sts=20=E2=80=94=20zero=20yield,=20full=20round-trip,=20100%=20u?= =?UTF-8?q?tilization,=20multi-lock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/risk-pool/src/lib.rs | 2 + contracts/risk-pool/src/test_edge.rs | 73 ++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 contracts/risk-pool/src/test_edge.rs diff --git a/contracts/risk-pool/src/lib.rs b/contracts/risk-pool/src/lib.rs index c038914..6b43eb2 100644 --- a/contracts/risk-pool/src/lib.rs +++ b/contracts/risk-pool/src/lib.rs @@ -321,3 +321,5 @@ impl RiskPool { mod test; #[cfg(test)] mod test_advanced; +#[cfg(test)] +mod test_edge; diff --git a/contracts/risk-pool/src/test_edge.rs b/contracts/risk-pool/src/test_edge.rs new file mode 100644 index 0000000..ab59370 --- /dev/null +++ b/contracts/risk-pool/src/test_edge.rs @@ -0,0 +1,73 @@ +//! Edge case tests for the risk pool — zero shares, full withdrawal, stress. +#![cfg(test)] + +extern crate std; + +use soroban_sdk::{testutils::Address as _, token, Address, Env, Symbol}; + +use crate::{RiskPool, RiskPoolClient}; + +fn setup() -> (Env, RiskPoolClient<'static>, Address, Address, Address) { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + let lp1 = Address::generate(&env); + + let usdc_id = env.register_stellar_asset_contract_v2(admin.clone()).address(); + let pool_id = env.register(RiskPool, ()); + let pool = RiskPoolClient::new(&env, &pool_id); + + token::StellarAssetClient::new(&env, &usdc_id).mint(&lp1, &100_000_0000000i128); + + pool.initialize(&admin, &usdc_id, &treasury, &Symbol::new(&env, "crop")); + + (env, pool, admin, treasury, lp1) +} + +#[test] +fn zero_accumulated_premium_yields_zero_claim() { + let (_, pool, _, _, lp1) = setup(); + pool.deposit(&lp1, &100_0000000i128); + let yield_amount = pool.claim_yield(&lp1); + assert_eq!(yield_amount, 0); +} + +#[test] +fn full_deposit_withdraw_round_trip_no_premium() { + let (_, pool, _, _, lp1) = setup(); + let amount = 500_0000000i128; + let shares = pool.deposit(&lp1, &amount); + let returned = pool.withdraw(&lp1, &shares); + assert_eq!(returned, amount); + let stats = pool.get_stats(); + assert_eq!(stats.total_deposited, 0); + assert_eq!(stats.total_shares, 0); +} + +#[test] +fn utilization_100_pct_after_locking_all() { + let (_, pool, admin, _, lp1) = setup(); + let amount = 200_0000000i128; + pool.deposit(&lp1, &amount); + pool.lock_for_policy(&admin, &10u128, &amount); + assert_eq!(pool.get_utilization_rate(), 10_000u32); // 100% in bps + assert_eq!(pool.get_available_liquidity(), 0); +} + +#[test] +fn multiple_locks_and_releases_track_correctly() { + let (_, pool, admin, _, lp1) = setup(); + pool.deposit(&lp1, &1000_0000000i128); + pool.lock_for_policy(&admin, &1u128, &300_0000000i128); + pool.lock_for_policy(&admin, &2u128, &200_0000000i128); + assert_eq!(pool.get_stats().total_locked, 500_0000000i128); + + pool.release_for_claim(&admin, &1u128); + assert_eq!(pool.get_stats().total_locked, 200_0000000i128); + + pool.release_for_claim(&admin, &2u128); + assert_eq!(pool.get_stats().total_locked, 0); + assert_eq!(pool.get_available_liquidity(), 1000_0000000i128); +} From a07f3025b381113b5fb736e8d0d215d5f0e1d565 Mon Sep 17 00:00:00 2001 From: Nonso Bethel Date: Sat, 13 Jun 2026 09:00:00 +0100 Subject: [PATCH 38/38] build: expand Makefile with pre-release, deploy-mainnet, submit-oracle, and check-balances targets --- Makefile | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Makefile b/Makefile index 499dc2c..3ebce8f 100644 --- a/Makefile +++ b/Makefile @@ -65,6 +65,22 @@ check-tools: @rustup target list --installed | grep -q wasm32v1-none || (echo "Adding wasm32v1-none target..." && rustup target add wasm32v1-none) @echo "All required tools found." +## Run full pre-release check: tests + lint + wasm-sizes +pre-release: test lint wasm-sizes + @echo "==> Pre-release checks passed. Ready to tag." + +## Deploy to Mainnet — requires DEPLOYER_SECRET env var +deploy-mainnet: + ./scripts/deploy_mainnet.sh + +## Submit oracle data (see scripts/submit_oracle_data.sh for env vars) +submit-oracle: + ./scripts/submit_oracle_data.sh + +## Check deployed contract balances +check-balances: + ./scripts/check_balances.sh + ## Print sizes of compiled WASMs wasm-sizes: build @echo "Contract WASM sizes:"