diff --git a/contracts/revenue_pool/src/lib.rs b/contracts/revenue_pool/src/lib.rs index 2e840cd..26f4e57 100644 --- a/contracts/revenue_pool/src/lib.rs +++ b/contracts/revenue_pool/src/lib.rs @@ -23,12 +23,9 @@ impl RevenuePool { if env.storage().instance().has(&Symbol::new(&env, ADMIN_KEY)) { panic!("revenue pool already initialized"); } - env.storage() - .instance() - .set(&Symbol::new(&env, ADMIN_KEY), &admin); - env.storage() - .instance() - .set(&Symbol::new(&env, USDC_KEY), &usdc_token); + let inst = env.storage().instance(); + inst.set(&Symbol::new(&env, ADMIN_KEY), &admin); + inst.set(&Symbol::new(&env, USDC_KEY), &usdc_token); env.events() .publish((Symbol::new(&env, "init"), admin), usdc_token); @@ -49,9 +46,8 @@ impl RevenuePool { if caller != current { panic!("unauthorized: caller is not admin"); } - env.storage() - .instance() - .set(&Symbol::new(&env, ADMIN_KEY), &new_admin); + let inst = env.storage().instance(); + inst.set(&Symbol::new(&env, ADMIN_KEY), &new_admin); } /// Placeholder: record that payment was received (e.g. from vault). @@ -96,7 +92,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); let contract_address = env.current_contract_address(); @@ -138,7 +134,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); let contract_address = env.current_contract_address(); diff --git a/contracts/revenue_pool/src/test.rs b/contracts/revenue_pool/src/test.rs index 8327701..05d6d51 100644 --- a/contracts/revenue_pool/src/test.rs +++ b/contracts/revenue_pool/src/test.rs @@ -2,7 +2,7 @@ extern crate std; use super::*; use soroban_sdk::testutils::{Address as _, Events as _}; -use soroban_sdk::{token, vec}; +use soroban_sdk::token; fn create_usdc<'a>( env: &'a Env, @@ -147,59 +147,117 @@ fn receive_payment_emits_event() { } #[test] -fn batch_distribute_success() { +#[should_panic(expected = "unauthorized: caller is not admin")] +fn set_admin_unauthorized_panics() { 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); + let attacker = Address::generate(&env); + let new_admin = Address::generate(&env); + let (_, client) = create_pool(&env); + let (usdc, _, _) = create_usdc(&env, &admin); - client.init(&admin, &usdc_address); - fund_pool(&usdc_admin, &pool_addr, 1_000); + client.init(&admin, &usdc); + client.set_admin(&attacker, &new_admin); +} - let payments = vec![&env, (dev1.clone(), 300), (dev2.clone(), 500)]; - client.batch_distribute(&admin, &payments); +#[test] +#[should_panic(expected = "revenue pool not initialized")] +fn balance_before_init_panics() { + let env = Env::default(); + let (_, client) = create_pool(&env); + client.balance(); +} - assert_eq!(usdc_client.balance(&pool_addr), 200); - assert_eq!(usdc_client.balance(&dev1), 300); - assert_eq!(usdc_client.balance(&dev2), 500); +#[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] -#[should_panic(expected = "amount must be positive")] -fn batch_distribute_zero_panics() { +fn receive_payment_from_non_vault() { let env = Env::default(); env.mock_all_auths(); let admin = Address::generate(&env); - let dev1 = Address::generate(&env); - let dev2 = Address::generate(&env); let (_, client) = create_pool(&env); - let (usdc_address, _, _) = create_usdc(&env, &admin); + 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()); +} + +/// Full lifecycle test: init, get_admin, balance, distribute, receive_payment, set_admin. +#[test] +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 (pool_addr, client) = create_pool(&env); + let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &admin); + // Init client.init(&admin, &usdc_address); + assert_eq!(client.get_admin(), admin); - let payments = vec![&env, (dev1.clone(), 300), (dev2.clone(), 0)]; - client.batch_distribute(&admin, &payments); + // 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); } #[test] -#[should_panic(expected = "insufficient USDC balance")] -fn batch_distribute_insufficient_balance_panics() { +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_admin) = create_usdc(&env, &admin); + let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &admin); client.init(&admin, &usdc_address); - fund_pool(&usdc_admin, &pool_addr, 400); + fund_pool(&usdc_admin, &pool_addr, 1000); - let payments = vec![&env, (dev1.clone(), 300), (dev2.clone(), 200)]; + let mut payments: Vec<(Address, i128)> = Vec::new(&env); + payments.push_back((dev1.clone(), 300_i128)); + payments.push_back((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!(client.balance(), 500); } #[test] @@ -209,44 +267,50 @@ fn batch_distribute_unauthorized_panics() { env.mock_all_auths(); let admin = Address::generate(&env); let attacker = Address::generate(&env); - let dev1 = 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, 1000); + fund_pool(&usdc_admin, &pool_addr, 500); - let payments = vec![&env, (dev1.clone(), 300)]; + let mut payments: Vec<(Address, i128)> = Vec::new(&env); + payments.push_back((dev.clone(), 100_i128)); client.batch_distribute(&attacker, &payments); } #[test] -fn get_admin_before_init_fails() { +#[should_panic(expected = "amount must be positive")] +fn batch_distribute_zero_amount_panics() { let env = Env::default(); - let (_, client) = create_pool(&env); - let result = client.try_get_admin(); - assert!(result.is_err(), "expected error when pool not initialized"); -} + 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); -#[test] -fn balance_before_init_fails() { - let env = Env::default(); - let (_, client) = create_pool(&env); - let result = client.try_balance(); - assert!(result.is_err(), "expected error when pool not initialized"); + client.init(&admin, &usdc_address); + fund_pool(&usdc_admin, &pool_addr, 500); + + let mut payments: Vec<(Address, i128)> = Vec::new(&env); + payments.push_back((dev.clone(), 0_i128)); + client.batch_distribute(&admin, &payments); } #[test] -fn set_admin_unauthorized_fails() { +#[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 intruder = Address::generate(&env); - let new_admin = Address::generate(&env); - let (_, client) = create_pool(&env); - let (usdc, _, _) = create_usdc(&env, &admin); + 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); - let result = client.try_set_admin(&intruder, &new_admin); - assert!(result.is_err(), "expected error for unauthorized set_admin"); + client.init(&admin, &usdc_address); + fund_pool(&usdc_admin, &pool_addr, 50); + + let mut payments: Vec<(Address, i128)> = Vec::new(&env); + payments.push_back((dev.clone(), 100_i128)); + client.batch_distribute(&admin, &payments); } diff --git a/contracts/vault/src/lib.rs b/contracts/vault/src/lib.rs index 7e3d59a..97502b8 100644 --- a/contracts/vault/src/lib.rs +++ b/contracts/vault/src/lib.rs @@ -37,8 +37,6 @@ pub struct DeductItem { pub amount: i128, pub request_id: Option, } -use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, Symbol}; -use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, Symbol, Vec}; #[contracttype] #[derive(Clone)] @@ -71,23 +69,6 @@ pub const DEFAULT_MAX_DEDUCT: i128 = i128::MAX; pub struct DistributeEvent { pub to: Address, pub amount: i128, -/// Maximum allowed length for metadata strings (IPFS CID or URI). -/// IPFS CIDv1 (base32) is typically ~59 chars, CIDv0 is 46 chars. -/// HTTPS URIs can vary, but we cap at 256 chars to prevent storage abuse. -/// This limit balances flexibility with storage cost constraints. -pub const MAX_METADATA_LENGTH: u32 = 256; - -#[contracttype] -pub enum StorageKey { - Meta, - AllowedDepositor, - /// Offering metadata: maps offering_id (String) -> metadata (String) - /// The metadata string typically contains an IPFS CID (e.g., "QmXxx..." or "bafyxxx...") - /// or an HTTPS URI (e.g., "https://example.com/metadata/offering123.json") - OfferingMetadata(String), - AllowedDepositors, - ApiPrice(Symbol), - Paused, } #[contract] @@ -118,16 +99,6 @@ impl CalloraVault { ) -> VaultMeta { owner.require_auth(); if env.storage().instance().has(&Symbol::new(&env, META_KEY)) { - /// # Security Note - /// The `owner` address is required to authorize the initialization transaction via `owner.require_auth()`. - /// This prevents unauthorized parties from initializing the vault with a "zero" or unauthenticated owner. - /// - /// # Panics - /// - If the vault is already initialized - /// - If `initial_balance` is negative - pub fn init(env: Env, owner: Address, initial_balance: Option) -> VaultMeta { - owner.require_auth(); - if env.storage().instance().has(&StorageKey::Meta) { panic!("vault already initialized"); } let balance = initial_balance.unwrap_or(0); @@ -140,7 +111,6 @@ impl CalloraVault { authorized_caller, min_deposit: min_deposit_val, }; - // Persist metadata under both the literal key and the constant for safety. let inst = env.storage().instance(); inst.set(&Symbol::new(&env, "meta"), &meta); inst.set(&Symbol::new(&env, META_KEY), &meta); @@ -164,7 +134,7 @@ impl CalloraVault { env.storage() .instance() .get(&Symbol::new(&env, ADMIN_KEY)) - .unwrap_or_else(|| panic!("vault not initialized")) + .expect("vault not initialized") } /// Replace the current admin. Only the existing admin may call this. @@ -173,12 +143,6 @@ impl CalloraVault { let current_admin = Self::get_admin(env.clone()); if caller != current_admin { panic!("unauthorized: caller is not admin"); - /// Check if the caller is authorized to deposit (owner or allowed depositor). - fn is_authorized_depositor(env: Env, caller: Address) -> bool { - let meta = Self::get_meta(env.clone()); - // Owner is always authorized - if caller == meta.owner { - return true; } let inst = env.storage().instance(); inst.set(&Symbol::new(&env, ADMIN_KEY), &new_admin); @@ -188,6 +152,8 @@ impl CalloraVault { let allowed: Vec
= env .storage() .instance() + .get(&Symbol::new(&env, MAX_DEDUCT_KEY)) + .expect("vault not initialized") .get(&StorageKey::AllowedDepositors) .unwrap_or(Vec::new(&env)); allowed.contains(&caller) @@ -199,6 +165,7 @@ impl CalloraVault { assert!(caller == meta.owner, "unauthorized: owner only"); } + /// Distribute accumulated USDC to a single developer address. /// Get vault metadata (owner and balance). /// /// # Panics @@ -210,36 +177,31 @@ impl CalloraVault { /// # Events /// Emits topic `("distribute", to)` with data `amount` on success. pub fn distribute(env: Env, caller: Address, to: Address, amount: i128) { - // 1. Require on-chain signature from caller. caller.require_auth(); - // 2. Only the registered admin may distribute. let admin = Self::get_admin(env.clone()); if caller != admin { panic!("unauthorized: caller is not admin"); } - // 3. Amount must be positive. if amount <= 0 { panic!("amount must be positive"); } - // 4. Load the USDC token address. - let usdc_opt: Option
= env.storage().instance().get(&Symbol::new(&env, USDC_KEY)); - let usdc_address: Address = usdc_opt.unwrap_or_else(|| panic!("vault not initialized")); - + let usdc_address: Address = env + .storage() + .instance() + .get(&Symbol::new(&env, USDC_KEY)) + .expect("vault not initialized"); let usdc = token::Client::new(&env, &usdc_address); - // 5. Check vault has enough USDC. let vault_balance = usdc.balance(&env.current_contract_address()); if vault_balance < amount { panic!("insufficient USDC balance"); } - // 6. Transfer USDC from vault to developer. usdc.transfer(&env.current_contract_address(), &to, &amount); - // 7. Emit distribute event. env.events() .publish((Symbol::new(&env, "distribute"), to), amount); } @@ -248,6 +210,8 @@ impl CalloraVault { pub fn get_meta(env: Env) -> VaultMeta { env.storage() .instance() + .get(&Symbol::new(&env, META_KEY)) + .expect("vault not initialized") .get(&StorageKey::Meta) .unwrap_or_else(|| panic!("vault not initialized")) } @@ -301,6 +265,8 @@ impl CalloraVault { /// Emits a "deposit" event with amount and new balance. pub fn deposit(env: Env, amount: i128) -> i128 { /// Deposit: user transfers USDC to the contract; contract increases internal balance. + pub fn deposit(env: Env, from: Address, amount: i128) -> i128 { + from.require_auth(); /// Caller must have authorized the transfer (token transfer_from). Supports multiple depositors. /// Emits a "deposit" event with the depositor address and amount. pub fn deposit(env: Env, caller: Address, amount: i128) -> i128 { @@ -353,6 +319,7 @@ impl CalloraVault { meta.balance } + /// Deduct balance for an API call. /// Return current balance. pub fn balance(env: Env) -> i128 { Self::get_meta(env).balance @@ -374,34 +341,6 @@ impl CalloraVault { let max_deduct = Self::get_max_deduct(env.clone()); assert!(amount > 0, "amount must be positive"); assert!(amount <= max_deduct, "deduct amount exceeds max_deduct"); - /// Pause the vault. Only the owner may call this. - pub fn pause(env: Env, caller: Address) { - caller.require_auth(); - Self::require_owner(env.clone(), caller); - env.storage().instance().set(&StorageKey::Paused, &true); - } - - /// Unpause the vault. Only the owner may call this. - pub fn unpause(env: Env, caller: Address) { - caller.require_auth(); - Self::require_owner(env.clone(), caller); - env.storage().instance().set(&StorageKey::Paused, &false); - } - - /// Return whether the vault is currently paused. - pub fn paused(env: Env) -> bool { - env.storage() - .instance() - .get(&StorageKey::Paused) - .unwrap_or(false) - } - - /// Deduct balance for an API call. Only owner/authorized caller in production. - /// Panics if the vault is paused. - pub fn deduct(env: Env, caller: Address, amount: i128) -> i128 { - caller.require_auth(); - Self::require_owner(env.clone(), caller); - assert!(!Self::paused(env.clone()), "vault is paused"); let mut meta = Self::get_meta(env.clone()); @@ -466,10 +405,8 @@ impl CalloraVault { let mut total_amount = 0i128; for item in items.iter() { assert!(item.amount > 0, "amount must be positive"); - assert!( - item.amount <= max_deduct, - "deduct amount exceeds max_deduct" - ); + let within_limit = item.amount <= max_deduct; + assert!(within_limit, "deduct amount exceeds max_deduct"); assert!(running >= item.amount, "insufficient balance"); running -= item.amount; total_amount += item.amount; @@ -502,172 +439,8 @@ impl CalloraVault { meta.balance } - /// Withdraw from vault. Callable only by the vault owner; reduces balance and transfers USDC to owner. + /// Withdraw from vault. Callable only by the vault owner. pub fn withdraw(env: Env, amount: i128) -> i128 { - /// Set the price per API call (in smallest USDC units) for a given API ID. - /// Callable by the owner or allowed depositor (backend/admin). - pub fn set_price(env: Env, caller: Address, api_id: Symbol, price: i128) { - caller.require_auth(); - - assert!( - Self::is_authorized_depositor(env.clone(), caller.clone()), - "unauthorized: only owner or allowed depositor can set price" - ); - - env.storage() - .instance() - .set(&StorageKey::ApiPrice(api_id), &price); - } - - /// Get the configured price per API call (in smallest USDC units) for a given API ID. - /// Returns `None` if no price has been set for this API. - pub fn get_price(env: Env, api_id: Symbol) -> Option { - env.storage() - .instance() - .get::(&StorageKey::ApiPrice(api_id)) - } - - /// Return current balance. - pub fn balance(env: Env) -> i128 { - Self::get_meta(env).balance - } - - // ======================================================================== - // Offering Metadata Management - // ======================================================================== - - /// Set metadata for an offering. Only the owner (issuer) can set metadata. - /// - /// # Parameters - /// - `caller`: Must be the vault owner (authenticated via require_auth) - /// - `offering_id`: Unique identifier for the offering (e.g., "offering-001") - /// - `metadata`: Off-chain metadata reference (IPFS CID or HTTPS URI) - /// - /// # Metadata Format - /// The metadata string should contain: - /// - IPFS CID (v0): e.g., "QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco" - /// - IPFS CID (v1): e.g., "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" - /// - HTTPS URI: e.g., "https://example.com/metadata/offering123.json" - /// - /// # Off-chain Usage Pattern - /// Clients should: - /// 1. Call `get_metadata(offering_id)` to retrieve the reference - /// 2. If IPFS CID: Fetch from IPFS gateway (e.g., https://ipfs.io/ipfs/{CID}) - /// 3. If HTTPS URI: Fetch directly via HTTP GET - /// 4. Parse the JSON metadata (expected fields: name, description, image, etc.) - /// - /// # Storage Limits - /// - Maximum metadata length: 256 characters - /// - Exceeding this limit will cause a panic - /// - /// # Events - /// Emits a "metadata_set" event with topics: (metadata_set, offering_id, caller) - /// and data: metadata string - /// - /// # Errors - /// - Panics if caller is not the owner - /// - Panics if metadata exceeds MAX_METADATA_LENGTH - /// - Panics if offering_id already has metadata (use update_metadata instead) - pub fn set_metadata( - env: Env, - caller: Address, - offering_id: String, - metadata: String, - ) -> String { - caller.require_auth(); - Self::require_owner(&env, &caller); - - // Validate metadata length - let metadata_len = metadata.len(); - assert!( - metadata_len <= MAX_METADATA_LENGTH, - "metadata exceeds maximum length of {} characters", - MAX_METADATA_LENGTH - ); - - // Check if metadata already exists - let key = StorageKey::OfferingMetadata(offering_id.clone()); - assert!( - !env.storage().instance().has(&key), - "metadata already exists for this offering; use update_metadata to modify" - ); - - // Store metadata - env.storage().instance().set(&key, &metadata); - - // Emit event: topics = (metadata_set, offering_id, caller), data = metadata - env.events().publish( - (Symbol::new(&env, "metadata_set"), offering_id, caller), - metadata.clone(), - ); - - metadata - } - - /// Update existing metadata for an offering. Only the owner (issuer) can update. - /// - /// # Parameters - /// - `caller`: Must be the vault owner (authenticated via require_auth) - /// - `offering_id`: Unique identifier for the offering - /// - `metadata`: New off-chain metadata reference (IPFS CID or HTTPS URI) - /// - /// # Events - /// Emits a "metadata_updated" event with topics: (metadata_updated, offering_id, caller) - /// and data: (old_metadata, new_metadata) tuple - /// - /// # Errors - /// - Panics if caller is not the owner - /// - Panics if metadata exceeds MAX_METADATA_LENGTH - /// - Panics if offering_id has no existing metadata (use set_metadata first) - pub fn update_metadata( - env: Env, - caller: Address, - offering_id: String, - metadata: String, - ) -> String { - caller.require_auth(); - Self::require_owner(&env, &caller); - - // Validate metadata length - let metadata_len = metadata.len(); - assert!( - metadata_len <= MAX_METADATA_LENGTH, - "metadata exceeds maximum length of {} characters", - MAX_METADATA_LENGTH - ); - - // Check if metadata exists - let key = StorageKey::OfferingMetadata(offering_id.clone()); - let old_metadata: String = env.storage().instance().get(&key).unwrap_or_else(|| { - panic!("no metadata exists for this offering; use set_metadata first") - }); - - // Update metadata - env.storage().instance().set(&key, &metadata); - - // Emit event: topics = (metadata_updated, offering_id, caller), data = (old, new) - env.events().publish( - (Symbol::new(&env, "metadata_updated"), offering_id, caller), - (old_metadata, metadata.clone()), - ); - - metadata - } - - /// Get metadata for an offering. Returns None if no metadata is set. - /// - /// # Parameters - /// - `offering_id`: Unique identifier for the offering - /// - /// # Returns - /// - `Some(metadata)` if metadata exists - /// - `None` if no metadata has been set for this offering - pub fn get_metadata(env: Env, offering_id: String) -> Option { - let key = StorageKey::OfferingMetadata(offering_id); - env.storage().instance().get(&key) - } - - pub fn transfer_ownership(env: Env, new_owner: Address) { let mut meta = Self::get_meta(env.clone()); meta.owner.require_auth(); assert!(amount > 0, "amount must be positive"); @@ -693,7 +466,7 @@ impl CalloraVault { meta.balance } - /// Withdraw from vault to a designated address. Owner-only; transfers USDC to `to`. + /// Withdraw from vault to a designated address. Owner-only. pub fn withdraw_to(env: Env, to: Address, amount: i128) -> i128 { let mut meta = Self::get_meta(env.clone()); meta.owner.require_auth(); diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index 59ec4d9..9b6b9c1 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -1,5 +1,3 @@ -//! Vault contract unit tests (deposits, access control, API pricing). - extern crate std; use super::*; @@ -155,8 +153,6 @@ fn init_with_balance_emits_event() { env.mock_all_auths(); fund_vault(&usdc_admin, &contract_id, 1000); - env.mock_all_auths(); - // Call init directly inside as_contract so events are captured let events = env.as_contract(&contract_id, || { CalloraVault::init(env.clone(), owner.clone(), Some(1000), None); CalloraVault::init( @@ -229,9 +225,6 @@ fn get_meta_returns_owner_and_balance() { assert_eq!(client.balance(), 300); env.mock_all_auths(); client.deduct(&owner, &50); - env.mock_all_auths(); - client.init(&owner, &Some(100)); - env.mock_all_auths(); fund_vault(&usdc_admin, &contract_id, 500); client.init(&owner, &usdc_token, &Some(500), &None, &None, &None); @@ -333,7 +326,7 @@ fn allowed_depositor_can_deposit() { fn deduct_reduces_balance() { let env = Env::default(); let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault, ()); + let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); let (usdc, _, usdc_admin) = create_usdc(&env, &owner); let caller = Address::generate(&env); @@ -414,12 +407,11 @@ fn owner_can_clear_allowed_depositor() { fn deduct_with_request_id() { let env = Env::default(); let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault, ()); + let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); let (usdc, _, usdc_admin) = create_usdc(&env, &owner); let caller = Address::generate(&env); - // Initialize vault with initial balance env.mock_all_auths(); fund_vault(&usdc_admin, &contract_id, 1000); client.init(&owner, &usdc, &Some(1000), &None, &None, &None); @@ -437,8 +429,6 @@ fn deduct_insufficient_balance_fails() { let client = CalloraVaultClient::new(&env, &contract_id); let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); let caller = Address::generate(&env); - env.mock_all_auths(); - client.init(&owner, &Some(100)); env.mock_all_auths(); fund_vault(&usdc_admin, &contract_id, 10); @@ -452,15 +442,11 @@ fn deduct_insufficient_balance_fails() { fn deduct_exact_balance_succeeds() { let env = Env::default(); let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault, ()); + let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); let caller = Address::generate(&env); - env.mock_all_auths(); - client.init(&owner, &Some(100)); - - // Try to deposit as unauthorized address (should panic) env.mock_all_auths(); fund_vault(&usdc_admin, &contract_id, 75); client.init(&owner, &usdc_token, &Some(75), &None, &None, &None); @@ -475,14 +461,10 @@ fn deduct_event_contains_request_id() { let env = Env::default(); let owner = Address::generate(&env); let caller = Address::generate(&env); - let contract_id = env.register(CalloraVault, ()); + 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(); - client.init(&owner, &Some(100)); - - // Owner sets allowed depositor env.mock_all_auths(); fund_vault(&usdc_admin, &contract_id, 500); client.init(&owner, &usdc_token, &Some(500), &None, &None, &None); @@ -538,8 +520,6 @@ fn batch_deduct_events_contain_request_ids() { 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(); - client.init(&owner, &Some(100)); // Set depositor client.set_allowed_depositor(&owner, &Some(depositor.clone())); @@ -699,7 +679,6 @@ fn set_admin_updates_admin() { let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); - client.init(&owner, &Some(100)); client.init(&owner, &Some(500), &None); env.mock_all_auths(); @@ -809,7 +788,6 @@ fn distribute_zero_amount_fails() { let developer = Address::generate(&env); let (vault_address, client) = create_vault(&env); let (usdc, _, usdc_admin_client) = create_usdc(&env, &admin); - client.init(&owner, &Some(100)); env.mock_all_auths(); @@ -1195,11 +1173,6 @@ fn test_transfer_ownership_not_owner() { let result = client.try_withdraw(&0); assert!(result.is_err(), "expected error for zero amount"); } - env.mock_all_auths(); - - // transfer ownership via client - // Owner authorizes transfer (require_auth in contract) - client.transfer_ownership(&new_owner); #[test] fn withdraw_to_reduces_balance() { @@ -1264,33 +1237,6 @@ fn deposit_below_minimum_fails() { let owner = Address::generate(&env); let depositor = Address::generate(&env); let contract_id = env.register(CalloraVault {}, ()); -fn allowed_depositor_can_set_price() { - 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); - - client.init(&owner, &Some(100)); - - let api_id = Symbol::new(&env, "backend_api"); - - env.mock_all_auths(); - client.set_allowed_depositor(&owner, &Some(depositor.clone())); - - client.set_price(&depositor, &api_id, &25); - - let price = client.get_price(&api_id); - assert_eq!(price, Some(25)); -} - -#[test] -#[should_panic(expected = "unauthorized: only owner or allowed depositor can set price")] -fn unauthorized_cannot_set_price() { - let env = Env::default(); - let owner = Address::generate(&env); - let unauthorized = 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); @@ -1429,7 +1375,6 @@ fn double_init_fails() { let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); - client.init(&owner, &Some(100)); env.mock_all_auths(); fund_vault(&usdc_admin, &contract_id, 100); @@ -1447,12 +1392,6 @@ fn init_insufficient_usdc_balance_fails() { let client = CalloraVaultClient::new(&env, &contract_id); let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner); - env.mock_all_auths(); - // Initialize with balance of 100 - client.init(&owner, &Some(100)); - assert_eq!(client.balance(), 100); - - // Mock the owner as the invoker env.mock_all_auths(); fund_vault(&usdc_admin, &contract_id, 50); @@ -1462,14 +1401,6 @@ fn init_insufficient_usdc_balance_fails() { "expected error when initial_balance exceeds contract USDC" ); } - env.mock_all_auths(); - - // This should panic because new_owner is the same as current owner - client.transfer_ownership(&owner); - // Attempt to deduct more than balance, which should panic - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - client.deduct(&owner, &101); - })); #[test] fn init_with_zero_max_deduct_fails() { @@ -1487,245 +1418,6 @@ fn init_with_zero_max_deduct_fails() { #[test] fn init_with_revenue_pool_and_get_revenue_pool() { -#[should_panic(expected = "unauthorized: owner only")] -fn unauthorized_cannot_update_metadata() { - let env = Env::default(); - let owner = Address::generate(&env); - let unauthorized = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - client.init(&owner, &Some(100)); - - env.mock_all_auths(); - - let offering_id = String::from_str(&env, "offering-006"); - let metadata = String::from_str(&env, "QmInitial"); - - // Owner sets metadata - client.set_metadata(&owner, &offering_id, &metadata); - - // Unauthorized user tries to update (should panic) - let new_metadata = String::from_str(&env, "QmUnauthorized"); - client.update_metadata(&unauthorized, &offering_id, &new_metadata); -} - -#[test] -fn empty_metadata_is_allowed() { - let env = Env::default(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - client.init(&owner, &Some(100)); - - env.mock_all_auths(); - - let offering_id = String::from_str(&env, "offering-007"); - let empty_metadata = String::from_str(&env, ""); - - // Empty string should be allowed - client.set_metadata(&owner, &offering_id, &empty_metadata); - - let retrieved = client.get_metadata(&offering_id); - assert_eq!(retrieved, Some(empty_metadata)); -} - -#[test] -#[should_panic(expected = "metadata exceeds maximum length")] -fn oversized_metadata_is_rejected() { - let env = Env::default(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - client.init(&owner, &Some(100)); - - env.mock_all_auths(); - - let offering_id = String::from_str(&env, "offering-008"); - - // Create a string that exceeds MAX_METADATA_LENGTH (256 chars) - let oversized = "a".repeat(257); - let oversized_metadata = String::from_str(&env, &oversized); - - // Should panic due to length constraint - client.set_metadata(&owner, &offering_id, &oversized_metadata); -} - -#[test] -#[should_panic(expected = "metadata exceeds maximum length")] -fn oversized_update_is_rejected() { - let env = Env::default(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - client.init(&owner, &Some(100)); - - env.mock_all_auths(); - - let offering_id = String::from_str(&env, "offering-009"); - let initial_metadata = String::from_str(&env, "QmInitial"); - - // Set initial metadata - client.set_metadata(&owner, &offering_id, &initial_metadata); - - // Try to update with oversized metadata - let oversized = "b".repeat(257); - let oversized_metadata = String::from_str(&env, &oversized); - - // Should panic due to length constraint - client.update_metadata(&owner, &offering_id, &oversized_metadata); -} - -#[test] -fn repeated_updates_to_same_offering() { - let env = Env::default(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - client.init(&owner, &Some(100)); - - env.mock_all_auths(); - - let offering_id = String::from_str(&env, "offering-010"); - - // Set initial metadata - let metadata1 = String::from_str(&env, "QmVersion1"); - client.set_metadata(&owner, &offering_id, &metadata1); - assert_eq!(client.get_metadata(&offering_id), Some(metadata1)); - - // Update multiple times - let metadata2 = String::from_str(&env, "QmVersion2"); - client.update_metadata(&owner, &offering_id, &metadata2); - assert_eq!(client.get_metadata(&offering_id), Some(metadata2)); - - let metadata3 = String::from_str(&env, "QmVersion3"); - client.update_metadata(&owner, &offering_id, &metadata3); - assert_eq!(client.get_metadata(&offering_id), Some(metadata3)); - - let metadata4 = String::from_str(&env, "QmVersion4"); - client.update_metadata(&owner, &offering_id, &metadata4); - assert_eq!(client.get_metadata(&offering_id), Some(metadata4)); -} - -#[test] -#[should_panic(expected = "metadata already exists for this offering")] -fn cannot_set_metadata_twice() { - let env = Env::default(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - client.init(&owner, &Some(100)); - - env.mock_all_auths(); - - let offering_id = String::from_str(&env, "offering-011"); - let metadata1 = String::from_str(&env, "QmFirst"); - let metadata2 = String::from_str(&env, "QmSecond"); - - // Set metadata - client.set_metadata(&owner, &offering_id, &metadata1); - - // Try to set again (should panic) - client.set_metadata(&owner, &offering_id, &metadata2); -} - -#[test] -#[should_panic(expected = "no metadata exists for this offering")] -fn cannot_update_nonexistent_metadata() { - let env = Env::default(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - client.init(&owner, &Some(100)); - - env.mock_all_auths(); - - let offering_id = String::from_str(&env, "offering-012"); - let metadata = String::from_str(&env, "QmNonexistent"); - - // Try to update without setting first (should panic) - client.update_metadata(&owner, &offering_id, &metadata); -} - -#[test] -fn get_nonexistent_metadata_returns_none() { - let env = Env::default(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - client.init(&owner, &Some(100)); - - let offering_id = String::from_str(&env, "offering-nonexistent"); - - // Should return None for nonexistent metadata - let retrieved = client.get_metadata(&offering_id); - assert_eq!(retrieved, None); -} - -#[test] -fn metadata_at_max_length_is_accepted() { - let env = Env::default(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - client.init(&owner, &Some(100)); - - env.mock_all_auths(); - - let offering_id = String::from_str(&env, "offering-013"); - - // Create a string exactly at MAX_METADATA_LENGTH (256 chars) - let max_length = "x".repeat(256); - let max_metadata = String::from_str(&env, &max_length); - - // Should succeed - client.set_metadata(&owner, &offering_id, &max_metadata); - - let retrieved = client.get_metadata(&offering_id); - assert_eq!(retrieved, Some(max_metadata)); -} - -#[test] -fn multiple_offerings_can_have_metadata() { - let env = Env::default(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault {}, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - client.init(&owner, &Some(100)); - - env.mock_all_auths(); - - // Set metadata for multiple offerings - let offering1 = String::from_str(&env, "offering-A"); - let metadata1 = String::from_str(&env, "QmMetadataA"); - client.set_metadata(&owner, &offering1, &metadata1); - - let offering2 = String::from_str(&env, "offering-B"); - let metadata2 = String::from_str(&env, "QmMetadataB"); - client.set_metadata(&owner, &offering2, &metadata2); - - let offering3 = String::from_str(&env, "offering-C"); - let metadata3 = String::from_str(&env, "QmMetadataC"); - client.set_metadata(&owner, &offering3, &metadata3); - - // Verify all metadata is stored independently - assert_eq!(client.get_metadata(&offering1), Some(metadata1)); - assert_eq!(client.get_metadata(&offering2), Some(metadata2)); - assert_eq!(client.get_metadata(&offering3), Some(metadata3)); -} - -#[test] -#[should_panic] -fn test_transfer_ownership_not_owner() { let env = Env::default(); let owner = Address::generate(&env); let revenue_pool = Address::generate(&env); @@ -1760,11 +1452,6 @@ fn get_revenue_pool_returns_none_when_not_set() { let retrieved_pool = client.get_revenue_pool(); assert_eq!(retrieved_pool, None); - client.init(&owner, &Some(100)); - - // No auth for owner — transfer_ownership requires current owner to authorize - env.mock_auths(&[]); - client.transfer_ownership(&new_owner); } #[test] @@ -1783,7 +1470,6 @@ fn get_max_deduct_returns_configured_value() { } /// Fuzz test: random deposit/deduct sequence asserting balance >= 0 and matches expected. -/// Run with: cargo test --package callora-vault fuzz_deposit_and_deduct -- --nocapture #[test] fn fuzz_deposit_and_deduct() { use rand::Rng; @@ -1803,7 +1489,6 @@ fn fuzz_deposit_and_deduct() { let initial_balance: i128 = 1_000; fund_vault(&usdc_admin, &vault_address, initial_balance); - // Pre-fund owner for deposits in the loop usdc_admin.mint(&owner, &250_000); usdc_client.approve(&owner, &vault_address, &250_000, &10_000); vault.init( @@ -1878,7 +1563,6 @@ fn fuzz_deposit_and_deduct_seeded() { let (vault_address, vault) = create_vault(&env); let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &owner); - // Pre-fund owner for deposits in the loop usdc_admin.mint(&owner, &5_000_000); usdc_client.approve(&owner, &vault_address, &5_000_000, &10_000); vault.init(&owner, &usdc_address, &Some(0), &None, &None, &None); @@ -1948,9 +1632,8 @@ fn batch_deduct_all_succeed() { #[should_panic(expected = "insufficient balance")] fn batch_deduct_all_revert() { let env = Env::default(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault, ()); + let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); let (usdc_address, _, usdc_admin) = create_usdc(&env, &owner); @@ -1982,7 +1665,7 @@ fn batch_deduct_all_revert() { fn init_twice_panics_on_reinit() { let env = Env::default(); let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault, ()); + let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); env.mock_all_auths(); @@ -2020,27 +1703,11 @@ fn init_twice_panics_on_reinit() { assert_eq!(client.balance(), 25); } -#[test] -#[should_panic(expected = "vault is paused")] -fn test_deduct_when_paused_panics() { - let env = Env::default(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault, ()); - let client = CalloraVaultClient::new(&env, &contract_id); - - client.init(&owner, &Some(500)); - env.mock_all_auths(); - client.pause(&owner); - client.deduct(&owner, &100); -} - #[test] fn owner_unchanged_after_deposit_and_deduct() { let env = Env::default(); - env.mock_all_auths(); - let owner = Address::generate(&env); - let contract_id = env.register(CalloraVault, ()); + let contract_id = env.register(CalloraVault {}, ()); let client = CalloraVaultClient::new(&env, &contract_id); env.mock_all_auths(); @@ -2049,7 +1716,6 @@ fn owner_unchanged_after_deposit_and_deduct() { env.mock_all_auths(); fund_vault(&usdc_admin, &contract_id, 100); - // Fund owner for the deposit call usdc_admin.mint(&owner, &50); usdc_client.approve(&owner, &contract_id, &50, &10_000); client.init(&owner, &usdc_address, &Some(100), &None, &None, &None); diff --git a/coverage/cobertura.xml b/coverage/cobertura.xml index 19af539..3a7a6b4 100644 --- a/coverage/cobertura.xml +++ b/coverage/cobertura.xml @@ -1 +1 @@ -/home/jeffersonyouashi/Documents/DRIPS/Callora-Contracts \ No newline at end of file +/home/jeffersonyouashi/Documents/DRIPS/Callora-Contracts \ No newline at end of file diff --git a/coverage/tarpaulin-report.html b/coverage/tarpaulin-report.html index 0cb9653..1b37319 100644 --- a/coverage/tarpaulin-report.html +++ b/coverage/tarpaulin-report.html @@ -193,8 +193,8 @@