diff --git a/.gitignore b/.gitignore index 47657e0..49ed1a2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ Cargo.lock /target/ /target_local/ .md-* -hidden \ No newline at end of file +hidden diff --git a/contracts/revenue_pool/src/lib.rs b/contracts/revenue_pool/src/lib.rs index 26f4e57..76564df 100644 --- a/contracts/revenue_pool/src/lib.rs +++ b/contracts/revenue_pool/src/lib.rs @@ -20,7 +20,8 @@ impl RevenuePool { /// * `usdc_token` – Stellar USDC (or wrapped USDC) token contract address. pub fn init(env: Env, admin: Address, usdc_token: Address) { admin.require_auth(); - if env.storage().instance().has(&Symbol::new(&env, ADMIN_KEY)) { + let inst = env.storage().instance(); + if inst.has(&Symbol::new(&env, ADMIN_KEY)) { panic!("revenue pool already initialized"); } let inst = env.storage().instance(); @@ -36,7 +37,7 @@ impl RevenuePool { env.storage() .instance() .get(&Symbol::new(&env, ADMIN_KEY)) - .unwrap_or_else(|| panic!("revenue pool not initialized")) + .expect("revenue pool not initialized") } /// Replace the current admin. Only the existing admin may call this. @@ -156,7 +157,7 @@ impl RevenuePool { .storage() .instance() .get(&Symbol::new(&env, USDC_KEY)) - .unwrap_or_else(|| panic!("revenue pool not initialized")); + .expect("revenue pool not initialized"); let usdc = token::Client::new(&env, &usdc_address); usdc.balance(&env.current_contract_address()) } diff --git a/contracts/revenue_pool/src/test.rs b/contracts/revenue_pool/src/test.rs index 05d6d51..a3d9429 100644 --- a/contracts/revenue_pool/src/test.rs +++ b/contracts/revenue_pool/src/test.rs @@ -185,6 +185,48 @@ fn distribute_negative_panics() { assert!(result.is_err()); } +#[test] +fn receive_payment_from_non_vault() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let (_, client) = create_pool(&env); + let (usdc, _, _) = create_usdc(&env, &admin); + + client.init(&admin, &usdc); + client.receive_payment(&admin, &250, &false); + + let events = env.events().all(); + assert!(!events.is_empty()); + + client.init(&admin, &usdc); + client.set_admin(&attacker, &new_admin); +} + +#[test] +#[should_panic(expected = "revenue pool not initialized")] +fn balance_before_init_panics() { + let env = Env::default(); + let (_, client) = create_pool(&env); + client.balance(); +} + +#[test] +fn distribute_negative_panics() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let developer = Address::generate(&env); + let (_, client) = create_pool(&env); + let (usdc, _, _) = create_usdc(&env, &admin); + + client.init(&admin, &usdc); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + client.distribute(&admin, &developer, &-1); + })); + assert!(result.is_err()); +} + #[test] fn receive_payment_from_non_vault() { let env = Env::default(); @@ -260,18 +302,43 @@ fn batch_distribute_success() { assert_eq!(client.balance(), 500); } +/// Full lifecycle test: init, get_admin, balance, distribute, receive_payment, set_admin. #[test] -#[should_panic(expected = "unauthorized: caller is not admin")] -fn batch_distribute_unauthorized_panics() { +fn full_lifecycle() { let env = Env::default(); env.mock_all_auths(); let admin = Address::generate(&env); + let new_admin = Address::generate(&env); + let developer = Address::generate(&env); let attacker = Address::generate(&env); let dev = Address::generate(&env); let (pool_addr, client) = create_pool(&env); - let (usdc_address, _, usdc_admin) = create_usdc(&env, &admin); + let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &admin); + // Init client.init(&admin, &usdc_address); + assert_eq!(client.get_admin(), admin); + + // Fund and check balance + fund_pool(&usdc_admin, &pool_addr, 1000); + assert_eq!(client.balance(), 1000); + + // Distribute + client.distribute(&admin, &developer, &400); + assert_eq!(usdc_client.balance(&developer), 400); + assert_eq!(client.balance(), 600); + + // Receive payment event + client.receive_payment(&admin, &100, &true); + + // Set admin + client.set_admin(&admin, &new_admin); + assert_eq!(client.get_admin(), new_admin); + + // New admin can distribute + client.distribute(&new_admin, &developer, &100); + assert_eq!(usdc_client.balance(&developer), 500); + assert_eq!(client.balance(), 500); fund_pool(&usdc_admin, &pool_addr, 500); let mut payments: Vec<(Address, i128)> = Vec::new(&env); @@ -314,3 +381,76 @@ fn batch_distribute_insufficient_balance_panics() { payments.push_back((dev.clone(), 100_i128)); client.batch_distribute(&admin, &payments); } + +#[test] +fn batch_distribute_success() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let dev1 = Address::generate(&env); + let dev2 = Address::generate(&env); + let (pool_addr, client) = create_pool(&env); + let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &admin); + + client.init(&admin, &usdc_address); + fund_pool(&usdc_admin, &pool_addr, 1_000); + + let payments = soroban_sdk::vec![&env, (dev1.clone(), 300_i128), (dev2.clone(), 200_i128)]; + client.batch_distribute(&admin, &payments); + + assert_eq!(usdc_client.balance(&dev1), 300); + assert_eq!(usdc_client.balance(&dev2), 200); + assert_eq!(usdc_client.balance(&pool_addr), 500); +} + +#[test] +#[should_panic(expected = "unauthorized: caller is not admin")] +fn batch_distribute_unauthorized_panics() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let attacker = Address::generate(&env); + let dev = Address::generate(&env); + let (pool_addr, client) = create_pool(&env); + let (usdc_address, _, usdc_admin) = create_usdc(&env, &admin); + + client.init(&admin, &usdc_address); + fund_pool(&usdc_admin, &pool_addr, 500); + + let payments = soroban_sdk::vec![&env, (dev.clone(), 100_i128)]; + client.batch_distribute(&attacker, &payments); +} + +#[test] +#[should_panic(expected = "amount must be positive")] +fn batch_distribute_zero_amount_panics() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let dev = Address::generate(&env); + let (pool_addr, client) = create_pool(&env); + let (usdc_address, _, usdc_admin) = create_usdc(&env, &admin); + + client.init(&admin, &usdc_address); + fund_pool(&usdc_admin, &pool_addr, 500); + + let payments = soroban_sdk::vec![&env, (dev.clone(), 0_i128)]; + client.batch_distribute(&admin, &payments); +} + +#[test] +#[should_panic(expected = "insufficient USDC balance")] +fn batch_distribute_insufficient_balance_panics() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let dev = Address::generate(&env); + let (pool_addr, client) = create_pool(&env); + let (usdc_address, _, usdc_admin) = create_usdc(&env, &admin); + + client.init(&admin, &usdc_address); + fund_pool(&usdc_admin, &pool_addr, 100); + + let payments = soroban_sdk::vec![&env, (dev.clone(), 200_i128)]; + client.batch_distribute(&admin, &payments); +} diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index 9b6b9c1..e23e912 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -1114,6 +1114,8 @@ fn withdraw_reduces_balance() { #[test] fn withdraw_insufficient_balance_fails() { let env = Env::default(); + env.mock_all_auths(); + let owner = Address::generate(&env); let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); @@ -1195,6 +1197,67 @@ fn withdraw_to_reduces_balance() { #[test] fn withdraw_to_insufficient_balance_fails() { let env = Env::default(); + let owner = Address::generate(&env); + let recipient = Address::generate(&env); + let contract_id = env.register(CalloraVault {}, ()); + let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); + + env.mock_all_auths(); + fund_vault(&usdc_admin, &contract_id, 100); + client.init(&owner, &usdc_token, &Some(100), &None, &None, &None); + + let result = client.try_withdraw_to(&recipient, &500); + assert!(result.is_err(), "expected error for insufficient balance"); +} + +#[test] +fn deposit_below_minimum_fails() { + let env = Env::default(); + let owner = Address::generate(&env); + let depositor = Address::generate(&env); + let contract_id = env.register(CalloraVault {}, ()); + let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); + + env.mock_all_auths(); + fund_vault(&usdc_admin, &contract_id, 100); + client.init(&owner, &usdc_token, &Some(100), &Some(50), &None, &None); + + fund_vault(&usdc_admin, &depositor, 30); + let usdc_client = token::Client::new(&env, &usdc_token); + usdc_client.approve(&depositor, &contract_id, &30, &1000); + let result = client.try_deposit(&depositor, &30); + assert!(result.is_err(), "expected error for deposit below minimum"); +} + +#[test] +fn deposit_at_minimum_succeeds() { + let env = Env::default(); + let owner = Address::generate(&env); + let depositor = Address::generate(&env); + let contract_id = env.register(CalloraVault {}, ()); + let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); + + env.mock_all_auths(); + fund_vault(&usdc_admin, &contract_id, 100); + client.init(&owner, &usdc_token, &Some(100), &Some(50), &None, &None); + + fund_vault(&usdc_admin, &depositor, 50); + let usdc_client = token::Client::new(&env, &usdc_token); + usdc_client.approve(&depositor, &contract_id, &50, &1000); + let new_balance = client.deposit(&depositor, &50); + assert_eq!(new_balance, 150); +} + +#[test] +fn double_init_fails() { + let env = Env::default(); + let owner = Address::generate(&env); + let contract_id = env.register(CalloraVault {}, ()); + let client = CalloraVaultClient::new(&env, &contract_id); + let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); let owner = Address::generate(&env); let new_owner = Address::generate(&env); diff --git a/coverage/cobertura.xml b/coverage/cobertura.xml index 3a7a6b4..7642a09 100644 --- a/coverage/cobertura.xml +++ b/coverage/cobertura.xml @@ -1 +1,2 @@ -/home/jeffersonyouashi/Documents/DRIPS/Callora-Contracts \ No newline at end of file +/home/jeffersonyouashi/Documents/DRIPS/Callora-Contracts +/home/jeffersonyouashi/Documents/DRIPS/Callora-Contracts diff --git a/coverage/tarpaulin-report.html b/coverage/tarpaulin-report.html index 1b37319..dc56e2b 100644 --- a/coverage/tarpaulin-report.html +++ b/coverage/tarpaulin-report.html @@ -193,6 +193,8 @@
diff --git a/scripts/coverage.sh b/scripts/coverage.sh index a1328c4..4681a3f 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -4,90 +4,46 @@ # Generate a test coverage report for the Callora Contracts workspace using # cargo-tarpaulin and enforce a minimum of 95 % line coverage. # -# Usage -# ----- -# ./scripts/coverage.sh # run from the workspace root +# Usage: +# ./scripts/coverage.sh # -# First-time setup -# ---------------- -# The script installs cargo-tarpaulin automatically if it is not found. -# You only need a working Rust / Cargo toolchain (stable). +# Prerequisites: +# cargo install cargo-tarpaulin # -# Output -# ------ -# coverage/tarpaulin-report.html – interactive per-file report -# coverage/cobertura.xml – Cobertura XML (consumed by CI) -# Stdout summary printed at end of run +# The script reads tarpaulin.toml for configuration (fail-under, output format, +# timeout, etc.). set -euo pipefail -# --------------------------------------------------------------------------- -# Configuration — keep in sync with tarpaulin.toml -# --------------------------------------------------------------------------- -MINIMUM_COVERAGE=95 -COVERAGE_DIR="coverage" -TARPAULIN_VERSION="0.31" # minimum version; any newer release also works +# ── Colours ────────────────────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +CYAN='\033[0;36m' +NC='\033[0m' # No Colour + +# ── Pre-flight checks ─────────────────────────────────────────────────────── +if ! command -v cargo-tarpaulin &>/dev/null; then + echo -e "${RED}ERROR:${NC} cargo-tarpaulin is not installed." + echo " Install it with: cargo install cargo-tarpaulin" + exit 1 +fi -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- -info() { echo -e " \033[1;34m[INFO]\033[0m $*"; } -success() { echo -e " \033[1;32m[PASS]\033[0m $*"; } -error() { echo -e " \033[1;31m[FAIL]\033[0m $*" >&2; } +TARPAULIN_VERSION=$(cargo tarpaulin --version 2>&1 || true) +echo -e " ${CYAN}[INFO]${NC} Using ${TARPAULIN_VERSION}" +echo -e " ${CYAN}[INFO]${NC} Running tests with coverage instrumentation..." -# --------------------------------------------------------------------------- -# Make sure we run from the workspace root (directory containing Cargo.toml) -# --------------------------------------------------------------------------- -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "${SCRIPT_DIR}/.." +# ── Run tarpaulin ──────────────────────────────────────────────────────────── +# All flags come from tarpaulin.toml at the workspace root. +cargo tarpaulin -if [[ ! -f "Cargo.toml" ]]; then - error "Could not locate workspace Cargo.toml. Run this script from the repo root." - exit 1 -fi +STATUS=$? -# --------------------------------------------------------------------------- -# Install cargo-tarpaulin if missing -# --------------------------------------------------------------------------- -if ! cargo tarpaulin --version &>/dev/null 2>&1; then - info "cargo-tarpaulin not found — installing (this happens once)..." - cargo install cargo-tarpaulin --version "^${TARPAULIN_VERSION}" --locked - success "cargo-tarpaulin installed." +if [ $STATUS -eq 0 ]; then + echo "" + echo -e " ${GREEN}[OK]${NC} Coverage threshold met." else - INSTALLED=$(cargo tarpaulin --version 2>&1 | head -1) - info "Using ${INSTALLED}" + echo "" + echo -e " ${RED}[FAIL]${NC} Coverage below threshold — see report above." fi -# --------------------------------------------------------------------------- -# Prepare output directory -# --------------------------------------------------------------------------- -mkdir -p "${COVERAGE_DIR}" - -# --------------------------------------------------------------------------- -# Run coverage -# tarpaulin.toml in the workspace root carries the full configuration; -# flags below match it so the script can also be run without the config file. -# --------------------------------------------------------------------------- -info "Running tests with coverage instrumentation..." -echo "" - -cargo tarpaulin \ - --config tarpaulin.toml - -echo "" - -# --------------------------------------------------------------------------- -# Friendly reminder of where to find the reports -# --------------------------------------------------------------------------- -success "Coverage run complete." -echo "" -echo " Reports written to ./${COVERAGE_DIR}/" -echo " HTML → ./${COVERAGE_DIR}/tarpaulin-report.html" -echo " XML → ./${COVERAGE_DIR}/cobertura.xml" -echo "" -echo " Open the HTML report in a browser:" -echo " xdg-open ./${COVERAGE_DIR}/tarpaulin-report.html # Linux" -echo " open ./${COVERAGE_DIR}/tarpaulin-report.html # macOS" -echo "" -echo " Minimum enforced: ${MINIMUM_COVERAGE}%" -echo " (non-zero exit from tarpaulin means coverage fell below the threshold)" +exit $STATUS