diff --git a/Cargo.lock b/Cargo.lock index 302e454..7c2bd63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -548,6 +548,13 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bfcf67fea2815c2fc3b90873fae90957be12ff417335dfadc7f52927feb03b2" +[[package]] +name = "escrow-contract" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "ethnum" version = "1.5.2" diff --git a/Cargo.toml b/Cargo.toml index 33c7ecf..3fdc859 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["soroban", "transfer_state_machine"] +members = ["soroban", "transfer_state_machine", "escrow_contract"] resolver = "2" [profile.release] diff --git a/README.md b/README.md index ef8e1aa..aca8f6c 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,12 @@ locking/escrow, operator rotation, and transfer state tracking. ## Workspace layout -This is a Cargo workspace with two members: +This is a Cargo workspace with the following members: | Crate | Description | | --- | --- | -| `soroban/` | Core Soroban contracts (access control, trusted sources, escrow, thresholds) | +| `soroban/` | Core Soroban contracts (access control, trusted sources, thresholds) | +| `escrow_contract/` | Time-locked escrow contract for bridge transfers | | `transfer_state_machine/` | Transfer state-machine contract logic | | `harness/` | Test harness and integration helpers | diff --git a/escrow_contract/Cargo.toml b/escrow_contract/Cargo.toml new file mode 100644 index 0000000..b6f7063 --- /dev/null +++ b/escrow_contract/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "escrow-contract" +version = "0.1.0" +edition = "2021" +description = "Soroban time-locked escrow contract for Swipely bridge transfers" +license = "MIT" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +soroban-sdk = { version = "22.0.10" } + +[dev-dependencies] +soroban-sdk = { version = "22.0.10", features = ["testutils"] } + +[[test]] +name = "escrow_contract_test" +path = "tests/escrow_contract.test.rs" diff --git a/soroban/src/escrow_contract.rs b/escrow_contract/src/lib.rs similarity index 99% rename from soroban/src/escrow_contract.rs rename to escrow_contract/src/lib.rs index 61c65cc..980b153 100644 --- a/soroban/src/escrow_contract.rs +++ b/escrow_contract/src/lib.rs @@ -1,3 +1,5 @@ +#![no_std] + use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, String, Symbol, Vec, diff --git a/soroban/tests/escrow_contract.test.rs b/escrow_contract/tests/escrow_contract.test.rs similarity index 98% rename from soroban/tests/escrow_contract.test.rs rename to escrow_contract/tests/escrow_contract.test.rs index c852499..7e87684 100644 --- a/soroban/tests/escrow_contract.test.rs +++ b/escrow_contract/tests/escrow_contract.test.rs @@ -1,7 +1,7 @@ #![cfg(test)] use soroban_sdk::{testutils::{Address as _, Ledger}, Address, Env, String, contract, contractimpl, symbol_short, Vec}; -use bridge_watch_contracts::escrow_contract::{TimeLockedEscrowContract, TimeLockedEscrowContractClient}; +use escrow_contract::{TimeLockedEscrowContract, TimeLockedEscrowContractClient}; #[contract] pub struct MockBridgeVerifier; diff --git a/soroban/Cargo.toml b/soroban/Cargo.toml index 858960b..083a6f8 100644 --- a/soroban/Cargo.toml +++ b/soroban/Cargo.toml @@ -25,10 +25,6 @@ path = "tests/asset_locking.test.rs" name = "source_trust_test" path = "tests/source_trust.test.rs" -[[test]] -name = "escrow_contract_test" -path = "tests/escrow_contract.test.rs" - [[test]] name = "operator_rotation_test" path = "tests/operator_rotation.test.rs" diff --git a/soroban/src/asset_deprecation.rs b/soroban/src/asset_deprecation.rs index f546b20..a7a9faa 100644 --- a/soroban/src/asset_deprecation.rs +++ b/soroban/src/asset_deprecation.rs @@ -188,7 +188,7 @@ impl AssetDeprecationContract { } /// Get deprecation configuration for an asset - pub fn get_deprecation_config( + fn get_deprecation_config( env: &Env, asset_code: &String, ) -> Result { @@ -199,7 +199,7 @@ impl AssetDeprecationContract { } /// Guard function to check if write operations are allowed - pub fn check_write_allowed( + fn check_write_allowed( env: &Env, asset_code: &String, ) -> Result<(), DeprecationError> { diff --git a/soroban/src/batch_query.rs b/soroban/src/batch_query.rs index 993e652..d867cba 100644 --- a/soroban/src/batch_query.rs +++ b/soroban/src/batch_query.rs @@ -3,6 +3,7 @@ //! Allow querying multiple assets or bridges in one call to reduce overhead. //! Provides deterministic output with comprehensive error handling and size limits. +use alloc::string::ToString; use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, symbol_short, Env, String, Vec, }; @@ -39,7 +40,7 @@ pub enum QueryResult { /// Successful query with data Success(String), /// Query failed with error message - Error(String), + Failure(String), } /// Batch query response with deterministic ordering @@ -131,7 +132,7 @@ impl BatchQueryContract { } Err(e) => { let error_msg = String::from_str(&env, e); - results.push_back(QueryResult::Error(error_msg)); + results.push_back(QueryResult::Failure(error_msg)); error_count += 1; } } @@ -172,7 +173,7 @@ impl BatchQueryContract { } Err(e) => { let error_msg = String::from_str(&env, e); - results.push_back(QueryResult::Error(error_msg)); + results.push_back(QueryResult::Failure(error_msg)); error_count += 1; } } @@ -266,25 +267,31 @@ impl BatchQueryContract { fn serialize_asset_data(env: &Env, data: &AssetData) -> String { // Simple JSON-like serialization - let mut json = String::from_str( + String::from_str( env, - &format!( + &alloc::format!( "{{\"asset_code\":\"{}\",\"name\":\"{}\",\"symbol\":\"{}\",\"issuer\":\"{}\",\"status\":\"{}\"}}", - data.asset_code, data.name, data.symbol, data.issuer, data.status + data.asset_code.to_string(), + data.name.to_string(), + data.symbol.to_string(), + data.issuer.to_string(), + data.status.to_string() ), - ); - json + ) } fn serialize_bridge_data(env: &Env, data: &BridgeData) -> String { // Simple JSON-like serialization - let mut json = String::from_str( + String::from_str( env, - &format!( + &alloc::format!( "{{\"bridge_id\":\"{}\",\"name\":\"{}\",\"source_chain\":\"{}\",\"dest_chain\":\"{}\",\"status\":\"{}\"}}", - data.bridge_id, data.name, data.source_chain, data.dest_chain, data.status + data.bridge_id.to_string(), + data.name.to_string(), + data.source_chain.to_string(), + data.dest_chain.to_string(), + data.status.to_string() ), - ); - json + ) } } diff --git a/soroban/src/lib.rs b/soroban/src/lib.rs index 48d49a9..f85a1f8 100644 --- a/soroban/src/lib.rs +++ b/soroban/src/lib.rs @@ -1,6 +1,13 @@ #![no_std] #![allow(clippy::too_many_arguments)] +// Only needed by batch_query's test-only JSON serialization helpers below; +// the wasm release build has no global allocator wired up (soroban-sdk's +// bump allocator is opt-in via its "alloc" feature, which isn't enabled +// here), so this must stay test-only rather than being declared unconditionally. +#[cfg(test)] +extern crate alloc; + // governance and insurance_pool are standalone contracts — only compiled for // tests (native target) to avoid Wasm symbol conflicts with BridgeWatchContract. pub mod acl; @@ -15,7 +22,6 @@ pub mod batch_query; #[cfg(test)] pub mod circuit_breaker; pub mod emergency_fund_recovery; -pub mod escrow_contract; #[cfg(test)] pub mod governance; #[cfg(test)] @@ -1287,13 +1293,41 @@ impl BridgeWatchContract { bridge_uptime_score: u32, ) { Self::check_permission(&env, &caller, AdminRole::HealthSubmitter); + Self::submit_health_internal( + env, + caller, + asset_code, + health_score, + liquidity_score, + price_stability_score, + bridge_uptime_score, + ); + } + /// Core of `submit_health`, without the `check_permission()` call. + /// + /// `submit_health_signed` already checks the caller's permission (and + /// thus calls `require_auth()`) before verifying the signature; calling + /// `submit_health` from there would require_auth() the same address + /// again within the same invocation, which soroban rejects ("frame is + /// already authorized"). + fn submit_health_internal( + env: Env, + caller: Address, + asset_code: String, + health_score: u32, + liquidity_score: u32, + price_stability_score: u32, + bridge_uptime_score: u32, + ) { // Check if asset is locked Self::assert_asset_not_locked(&env, &asset_code); - // Check if caller is a trusted source (if any sources are registered) + // Check if caller is a trusted source (if any sources are registered). + // The admin is exempt, same as the role check in check_permission(). + let admin: Address = env.storage().instance().get(&keys::ADMIN).unwrap(); let active_sources = source_trust::get_active_trusted_sources(&env); - if !active_sources.is_empty() { + if caller != admin && !active_sources.is_empty() { // If sources are registered, enforce trust requirement source_trust::require_trusted_source(&env, &caller); } @@ -1344,7 +1378,13 @@ impl BridgeWatchContract { /// the same ledger timestamp. A `health_up` event is emitted per asset. pub fn submit_health_batch(env: Env, caller: Address, records: Vec) { Self::check_permission(&env, &caller, AdminRole::HealthSubmitter); + Self::submit_health_batch_internal(env, caller, records); + } + /// Core of `submit_health_batch`, without the `check_permission()` call — + /// see `submit_health_internal` for why `submit_health_batch_signed` + /// needs this. + fn submit_health_batch_internal(env: Env, caller: Address, records: Vec) { if records.len() > 20 { panic!("batch size exceeds the maximum of 20 records"); } @@ -1411,13 +1451,26 @@ impl BridgeWatchContract { source: String, ) { Self::check_permission(&env, &caller, AdminRole::PriceSubmitter); + Self::submit_price_internal(env, caller, asset_code, price, source); + } + /// Core of `submit_price`, without the `check_permission()` call — see + /// `submit_health_internal` for why `submit_price_signed` needs this. + fn submit_price_internal( + env: Env, + caller: Address, + asset_code: String, + price: i128, + source: String, + ) { // Check if asset is locked Self::assert_asset_not_locked(&env, &asset_code); - // Check if caller is a trusted source (if any sources are registered) + // Check if caller is a trusted source (if any sources are registered). + // The admin is exempt, same as the role check in check_permission(). + let admin: Address = env.storage().instance().get(&keys::ADMIN).unwrap(); let active_sources = source_trust::get_active_trusted_sources(&env); - if !active_sources.is_empty() { + if caller != admin && !active_sources.is_empty() { // If sources are registered, enforce trust requirement source_trust::require_trusted_source(&env, &caller); } @@ -1559,6 +1612,19 @@ impl BridgeWatchContract { panic!("signature has expired"); } + // Nonce replay must be checked before the signature cache below: + // the cache is keyed only by message payload, so resubmitting the + // exact same signed payload (a textbook replay attack) would + // otherwise short-circuit straight past replay detection. + let last_nonce = env + .storage() + .persistent() + .get::<_, u64>(&ConfigDataKey::SignerNonce(signature.signer_id.clone())) + .unwrap_or(0); + if signature.nonce <= last_nonce { + panic!("nonce replay detected"); + } + let payload_hash: BytesN<32> = env.crypto().sha256(&message).into(); if env .storage() @@ -1566,18 +1632,13 @@ impl BridgeWatchContract { .get::<_, bool>(&ConfigDataKey::SigCache(payload_hash.clone())) .unwrap_or(false) { + env.storage().persistent().set( + &ConfigDataKey::SignerNonce(signature.signer_id.clone()), + &signature.nonce, + ); return true; } - let last_nonce = env - .storage() - .persistent() - .get::<_, u64>(&ConfigDataKey::SignerNonce(signature.signer_id.clone())) - .unwrap_or(0); - if signature.nonce <= last_nonce { - panic!("nonce replay detected"); - } - let mut data = Bytes::new(&env); data.append(&message); @@ -1672,7 +1733,7 @@ impl BridgeWatchContract { ); Self::verify_signature(env.clone(), message, signature); - Self::submit_health( + Self::submit_health_internal( env, caller, asset_code, @@ -1704,7 +1765,7 @@ impl BridgeWatchContract { Self::verify_signature(env.clone(), message, signature); - Self::submit_price(env, caller, asset_code, price, source); + Self::submit_price_internal(env, caller, asset_code, price, source); } /// Submit a batch of health records with multi-sig support. @@ -1731,7 +1792,7 @@ impl BridgeWatchContract { Self::verify_multi_sig(env.clone(), batch_message, signatures); - Self::submit_health_batch(env, caller, records); + Self::submit_health_batch_internal(env, caller, records); } /// Build canonical health payload bytes for signature coverage. @@ -1855,6 +1916,7 @@ impl BridgeWatchContract { /// `AssetManager`. pub fn pause_asset(env: Env, caller: Address, asset_code: String) { Self::check_permission(&env, &caller, AdminRole::AssetManager); + Self::assert_asset_not_locked(&env, &asset_code); let mut status = Self::load_asset_health(&env, &asset_code); if !status.active { panic!("cannot pause a deregistered asset"); @@ -1877,6 +1939,7 @@ impl BridgeWatchContract { /// `AssetManager`. pub fn unpause_asset(env: Env, caller: Address, asset_code: String) { Self::check_permission(&env, &caller, AdminRole::AssetManager); + Self::assert_asset_not_locked(&env, &asset_code); let mut status = Self::load_asset_health(&env, &asset_code); if !status.active { panic!("cannot unpause a deregistered asset"); @@ -4450,6 +4513,24 @@ impl BridgeWatchContract { description: String, ) { caller.require_auth(); + Self::set_config_internal(env, caller, category, name, value, description); + } + + /// Core of `set_config`, without the `require_auth()` call. + /// + /// `set_config_bulk` already authorizes the caller once up front; calling + /// `set_config` per item would require_auth() the same address again + /// within the same invocation, which soroban rejects ("frame is already + /// authorized"). This lets both entrypoints share the same validation and + /// storage logic while only authorizing once each. + fn set_config_internal( + env: Env, + caller: Address, + category: ConfigCategory, + name: String, + value: i128, + description: String, + ) { Self::assert_not_globally_paused(&env); Self::check_no_pending_transfer(&env); @@ -4602,7 +4683,7 @@ impl BridgeWatchContract { let keys: Vec<(ConfigCategory, String)> = env .storage() .instance() - .get(&keys::CONFIG_KEYS) + .get(&DataKey::ConfigKeys) .unwrap_or_else(|| Vec::new(&env)); let mut entries: Vec = Vec::new(&env); @@ -4673,10 +4754,11 @@ impl BridgeWatchContract { panic!("config: bulk update list must contain at most 20 items"); } - // Apply each update — uses the same logic as set_config() + // Apply each update — uses the same logic as set_config(), minus the + // auth check (already done once above; see set_config_internal). for i in 0..updates.len() { let u = updates.get(i).unwrap(); - Self::set_config( + Self::set_config_internal( env.clone(), caller.clone(), u.category, @@ -4741,7 +4823,7 @@ impl BridgeWatchContract { .get::<_, ConfigEntry>(&key) .is_none() { - Self::set_config( + Self::set_config_internal( env.clone(), caller.clone(), cat, @@ -9961,6 +10043,11 @@ mod tests { let new_hash = BytesN::from_array(&env, &[7u8; 32]); let proposal_id = client.propose_upgrade(&admin, &new_hash, &false, &None, &None); + // Check events right after the call that emits them — interleaving + // further client calls (even read-only ones) before inspecting + // env.events() can leave earlier events out of the recorded set. + assert_has_event(&env, &client.address, symbol_short!("up_prop")); + assert_eq!(proposal_id, 1); let pending = client.get_pending_upgrade().unwrap(); assert_eq!(pending.proposal_id, 1); @@ -9969,8 +10056,6 @@ mod tests { assert_eq!(pending.required_approvals, 1); assert_eq!(pending.approvals.len(), 1); assert_eq!(pending.approvals.get(0).unwrap(), admin); - - assert_has_event(&env, &client.address, symbol_short!("up_prop")); } #[test] @@ -10013,7 +10098,10 @@ mod tests { let new_hash = BytesN::from_array(&env, &[10u8; 32]); client.propose_upgrade(&admin, &new_hash, &true, &None, &None); client.approve_upgrade(&super_admin, &1); + assert_has_event(&env, &client.address, symbol_short!("up_appr")); + client.execute_upgrade(&admin, &1); + assert_has_event(&env, &client.address, symbol_short!("up_exec")); assert!(client.get_pending_upgrade().is_none()); assert_eq!(client.get_contract_version(), 2); @@ -10025,9 +10113,6 @@ mod tests { assert_eq!(record.proposal_id, 1); assert!(record.emergency); assert!(!record.is_rollback); - - assert_has_event(&env, &client.address, symbol_short!("up_appr")); - assert_has_event(&env, &client.address, symbol_short!("up_exec")); } #[test] @@ -10038,9 +10123,9 @@ mod tests { let new_hash = BytesN::from_array(&env, &[11u8; 32]); client.propose_upgrade(&admin, &new_hash, &false, &None, &None); client.cancel_upgrade(&admin, &1, &String::from_str(&env, "no longer needed")); + assert_has_event(&env, &client.address, symbol_short!("up_cncl")); assert!(client.get_pending_upgrade().is_none()); - assert_has_event(&env, &client.address, symbol_short!("up_cncl")); } #[test] diff --git a/soroban/src/liquidity_pool.rs b/soroban/src/liquidity_pool.rs index f96318b..ff7ba4f 100644 --- a/soroban/src/liquidity_pool.rs +++ b/soroban/src/liquidity_pool.rs @@ -789,12 +789,19 @@ fn isqrt(n: i128) -> i128 { #[cfg(test)] mod tests { use super::*; - use soroban_sdk::{testutils::Ledger as _, Env, String}; + use soroban_sdk::{contract, contractimpl, testutils::Ledger as _, Address, Env, String}; - fn setup() -> Env { + #[contract] + struct TestContext; + + #[contractimpl] + impl TestContext {} + + fn setup() -> (Env, Address) { let env = Env::default(); env.mock_all_auths(); - env + let contract_id = env.register_contract(None, TestContext); + (env, contract_id) } // ── Math utility tests ───────────────────────────────────────────────── @@ -820,437 +827,471 @@ mod tests { #[test] fn test_record_pool_state_creates_snapshot() { - let env = setup(); - env.ledger().set_timestamp(1000); - - let pool_id = String::from_str(&env, "USDC_XLM"); - let reserve_a = 1_000 * PRECISION; - let reserve_b = 5_000 * PRECISION; - let total_shares = 2_236 * PRECISION; // sqrt(1000 * 5000) - let volume = 100 * PRECISION; - let fees = 10 * PRECISION; - - record_pool_state( - &env, - pool_id.clone(), - reserve_a, - reserve_b, - total_shares, - volume, - fees, - PoolType::Amm, - ); + let (env, contract_id) = setup(); + env.as_contract(&contract_id, || { + env.ledger().set_timestamp(1000); - let snapshots = get_pool_history(&env, pool_id, 0, 2000); - assert_eq!(snapshots.len(), 1); + let pool_id = String::from_str(&env, "USDC_XLM"); + let reserve_a = 1_000 * PRECISION; + let reserve_b = 5_000 * PRECISION; + let total_shares = 2_236 * PRECISION; // sqrt(1000 * 5000) + let volume = 100 * PRECISION; + let fees = 10 * PRECISION; - let snap = snapshots.get(0).unwrap(); - assert_eq!(snap.reserve_a, reserve_a); - assert_eq!(snap.reserve_b, reserve_b); - assert_eq!(snap.total_shares, total_shares); - assert_eq!(snap.volume, volume); - assert_eq!(snap.fees_collected, fees); - assert_eq!(snap.pool_type, PoolType::Amm); - } + record_pool_state( + &env, + pool_id.clone(), + reserve_a, + reserve_b, + total_shares, + volume, + fees, + PoolType::Amm, + ); - #[test] - fn test_record_pool_state_calculates_price() { - let env = setup(); - let pool_id = String::from_str(&env, "EUR_XLM"); - let reserve_a = 100 * PRECISION; - let reserve_b = 500 * PRECISION; // price = 5 - - record_pool_state( - &env, - pool_id.clone(), - reserve_a, - reserve_b, - 100 * PRECISION, - 0, - 0, - PoolType::Sdex, - ); + let snapshots = get_pool_history(&env, pool_id, 0, 2000); + assert_eq!(snapshots.len(), 1); - let snapshots = get_pool_history(&env, pool_id, 0, u64::MAX); - let snap = snapshots.get(0).unwrap(); - assert_eq!(snap.price, 5 * PRECISION); + let snap = snapshots.get(0).unwrap(); + assert_eq!(snap.reserve_a, reserve_a); + assert_eq!(snap.reserve_b, reserve_b); + assert_eq!(snap.total_shares, total_shares); + assert_eq!(snap.volume, volume); + assert_eq!(snap.fees_collected, fees); + assert_eq!(snap.pool_type, PoolType::Amm); + }); } #[test] - fn test_record_multiple_snapshots() { - let env = setup(); - let pool_id = String::from_str(&env, "TEST_POOL"); + fn test_record_pool_state_calculates_price() { + let (env, contract_id) = setup(); + env.as_contract(&contract_id, || { + let pool_id = String::from_str(&env, "EUR_XLM"); + let reserve_a = 100 * PRECISION; + let reserve_b = 500 * PRECISION; // price = 5 - for i in 0..5 { - env.ledger().set_timestamp(1000 + i * 3600); record_pool_state( &env, pool_id.clone(), - (1000 + i * 100) as i128 * PRECISION, - (5000 + i * 500) as i128 * PRECISION, - 2000 * PRECISION, + reserve_a, + reserve_b, 100 * PRECISION, - 10 * PRECISION, - PoolType::Amm, + 0, + 0, + PoolType::Sdex, ); - } - let snapshots = get_pool_history(&env, pool_id.clone(), 0, u64::MAX); - assert_eq!(snapshots.len(), 5); + let snapshots = get_pool_history(&env, pool_id, 0, u64::MAX); + let snap = snapshots.get(0).unwrap(); + assert_eq!(snap.price, 5 * PRECISION); + }); + } + + #[test] + fn test_record_multiple_snapshots() { + let (env, contract_id) = setup(); + env.as_contract(&contract_id, || { + let pool_id = String::from_str(&env, "TEST_POOL"); + + for i in 0..5 { + env.ledger().set_timestamp(1000 + i * 3600); + record_pool_state( + &env, + pool_id.clone(), + (1000 + i * 100) as i128 * PRECISION, + (5000 + i * 500) as i128 * PRECISION, + 2000 * PRECISION, + 100 * PRECISION, + 10 * PRECISION, + PoolType::Amm, + ); + } + + let snapshots = get_pool_history(&env, pool_id.clone(), 0, u64::MAX); + assert_eq!(snapshots.len(), 5); + }); } #[test] fn test_record_pool_state_registers_pool() { - let env = setup(); - let pool_id = String::from_str(&env, "NEW_POOL"); - - record_pool_state( - &env, - pool_id.clone(), - 1000 * PRECISION, - 5000 * PRECISION, - 2000 * PRECISION, - 0, - 0, - PoolType::Amm, - ); + let (env, contract_id) = setup(); + env.as_contract(&contract_id, || { + let pool_id = String::from_str(&env, "NEW_POOL"); + + record_pool_state( + &env, + pool_id.clone(), + 1000 * PRECISION, + 5000 * PRECISION, + 2000 * PRECISION, + 0, + 0, + PoolType::Amm, + ); - let pools = get_registered_pools(&env); - assert!(pools.len() >= 1); - let found = pools.iter().find(|p| p == &pool_id); - assert!(found.is_some()); + let pools = get_registered_pools(&env); + assert!(pools.len() >= 1); + let found = pools.iter().find(|p| p == &pool_id); + assert!(found.is_some()); + }); } // ── Pool metrics calculation tests ───────────────────────────────────── #[test] fn test_calculate_pool_metrics_no_snapshots() { - let env = setup(); - let pool_id = String::from_str(&env, "EMPTY_POOL"); - - let metrics = calculate_pool_metrics(&env, pool_id, 3600); - assert_eq!(metrics.total_volume, 0); - assert_eq!(metrics.avg_depth, 0); - assert_eq!(metrics.total_fees, 0); - assert_eq!(metrics.data_points, 0); + let (env, contract_id) = setup(); + env.as_contract(&contract_id, || { + let pool_id = String::from_str(&env, "EMPTY_POOL"); + + let metrics = calculate_pool_metrics(&env, pool_id, 3600); + assert_eq!(metrics.total_volume, 0); + assert_eq!(metrics.avg_depth, 0); + assert_eq!(metrics.total_fees, 0); + assert_eq!(metrics.data_points, 0); + }); } #[test] fn test_calculate_pool_metrics_single_snapshot() { - let env = setup(); - env.ledger().set_timestamp(5000); - - let pool_id = String::from_str(&env, "SINGLE_POOL"); - let reserve_a = 1000 * PRECISION; - let reserve_b = 5000 * PRECISION; - let volume = 100 * PRECISION; - let fees = 10 * PRECISION; - - record_pool_state( - &env, - pool_id.clone(), - reserve_a, - reserve_b, - 2000 * PRECISION, - volume, - fees, - PoolType::Amm, - ); + let (env, contract_id) = setup(); + env.as_contract(&contract_id, || { + env.ledger().set_timestamp(5000); + + let pool_id = String::from_str(&env, "SINGLE_POOL"); + let reserve_a = 1000 * PRECISION; + let reserve_b = 5000 * PRECISION; + let volume = 100 * PRECISION; + let fees = 10 * PRECISION; + + record_pool_state( + &env, + pool_id.clone(), + reserve_a, + reserve_b, + 2000 * PRECISION, + volume, + fees, + PoolType::Amm, + ); - let metrics = calculate_pool_metrics(&env, pool_id, 10000); - assert_eq!(metrics.total_volume, volume); - assert_eq!(metrics.total_fees, fees); - assert_eq!(metrics.data_points, 1); - assert!(metrics.avg_depth > 0); + let metrics = calculate_pool_metrics(&env, pool_id, 10000); + assert_eq!(metrics.total_volume, volume); + assert_eq!(metrics.total_fees, fees); + assert_eq!(metrics.data_points, 1); + assert!(metrics.avg_depth > 0); + }); } #[test] fn test_calculate_pool_metrics_window_filtering() { - let env = setup(); - let pool_id = String::from_str(&env, "WINDOW_POOL"); - - // Record snapshot at t=1000 - env.ledger().set_timestamp(1000); - record_pool_state( - &env, - pool_id.clone(), - 1000 * PRECISION, - 5000 * PRECISION, - 2000 * PRECISION, - 100 * PRECISION, - 10 * PRECISION, - PoolType::Amm, - ); + let (env, contract_id) = setup(); + env.as_contract(&contract_id, || { + let pool_id = String::from_str(&env, "WINDOW_POOL"); - // Record snapshot at t=5000 - env.ledger().set_timestamp(5000); - record_pool_state( - &env, - pool_id.clone(), - 1200 * PRECISION, - 6000 * PRECISION, - 2400 * PRECISION, - 150 * PRECISION, - 15 * PRECISION, - PoolType::Amm, - ); + // Record snapshot at t=1000 + env.ledger().set_timestamp(1000); + record_pool_state( + &env, + pool_id.clone(), + 1000 * PRECISION, + 5000 * PRECISION, + 2000 * PRECISION, + 100 * PRECISION, + 10 * PRECISION, + PoolType::Amm, + ); + + // Record snapshot at t=5000 + env.ledger().set_timestamp(5000); + record_pool_state( + &env, + pool_id.clone(), + 1200 * PRECISION, + 6000 * PRECISION, + 2400 * PRECISION, + 150 * PRECISION, + 15 * PRECISION, + PoolType::Amm, + ); - // Query with narrow window (1000-2000) should return 1 snapshot - let metrics_narrow = calculate_pool_metrics(&env, pool_id.clone(), 1000); - assert_eq!(metrics_narrow.data_points, 1); + // Query with narrow window (1000-2000) should return 1 snapshot + let metrics_narrow = calculate_pool_metrics(&env, pool_id.clone(), 1000); + assert_eq!(metrics_narrow.data_points, 1); - // Query with wide window should return 2 snapshots - let metrics_wide = calculate_pool_metrics(&env, pool_id, 10000); - assert_eq!(metrics_wide.data_points, 2); + // Query with wide window should return 2 snapshots + let metrics_wide = calculate_pool_metrics(&env, pool_id, 10000); + assert_eq!(metrics_wide.data_points, 2); + }); } #[test] fn test_calculate_pool_metrics_accumulates_volume_and_fees() { - let env = setup(); - env.ledger().set_timestamp(1000); - - let pool_id = String::from_str(&env, "ACCUM_POOL"); - - record_pool_state( - &env, - pool_id.clone(), - 1000 * PRECISION, - 5000 * PRECISION, - 2000 * PRECISION, - 100 * PRECISION, - 10 * PRECISION, - PoolType::Amm, - ); + let (env, contract_id) = setup(); + env.as_contract(&contract_id, || { + env.ledger().set_timestamp(1000); - env.ledger().set_timestamp(2000); - record_pool_state( - &env, - pool_id.clone(), - 1100 * PRECISION, - 5500 * PRECISION, - 2200 * PRECISION, - 50 * PRECISION, - 5 * PRECISION, - PoolType::Amm, - ); + let pool_id = String::from_str(&env, "ACCUM_POOL"); - let metrics = calculate_pool_metrics(&env, pool_id, 10000); - assert_eq!(metrics.total_volume, 150 * PRECISION); - assert_eq!(metrics.total_fees, 15 * PRECISION); + record_pool_state( + &env, + pool_id.clone(), + 1000 * PRECISION, + 5000 * PRECISION, + 2000 * PRECISION, + 100 * PRECISION, + 10 * PRECISION, + PoolType::Amm, + ); + + env.ledger().set_timestamp(2000); + record_pool_state( + &env, + pool_id.clone(), + 1100 * PRECISION, + 5500 * PRECISION, + 2200 * PRECISION, + 50 * PRECISION, + 5 * PRECISION, + PoolType::Amm, + ); + + let metrics = calculate_pool_metrics(&env, pool_id, 10000); + assert_eq!(metrics.total_volume, 150 * PRECISION); + assert_eq!(metrics.total_fees, 15 * PRECISION); + }); } // ── Liquidity depth tests ────────────────────────────────────────────── #[test] fn test_get_liquidity_depth_no_snapshots() { - let env = setup(); - let pool_id = String::from_str(&env, "EMPTY"); - let depth = get_liquidity_depth(&env, pool_id); - assert_eq!(depth.depth, 0); + let (env, contract_id) = setup(); + env.as_contract(&contract_id, || { + let pool_id = String::from_str(&env, "EMPTY"); + let depth = get_liquidity_depth(&env, pool_id); + assert_eq!(depth.total_value_locked, 0); + }); } #[test] fn test_get_liquidity_depth_single_snapshot() { - let env = setup(); - env.ledger().set_timestamp(1000); - - let pool_id = String::from_str(&env, "DEPTH_POOL"); - let reserve_a = 1000 * PRECISION; - let reserve_b = 5000 * PRECISION; - - record_pool_state( - &env, - pool_id.clone(), - reserve_a, - reserve_b, - 2000 * PRECISION, - 0, - 0, - PoolType::Amm, - ); + let (env, contract_id) = setup(); + env.as_contract(&contract_id, || { + env.ledger().set_timestamp(1000); + + let pool_id = String::from_str(&env, "DEPTH_POOL"); + let reserve_a = 1000 * PRECISION; + let reserve_b = 5000 * PRECISION; - let depth = get_liquidity_depth(&env, pool_id); - let avg = (reserve_a + reserve_b) / 2; - assert_eq!(depth.depth, avg); + record_pool_state( + &env, + pool_id.clone(), + reserve_a, + reserve_b, + 2000 * PRECISION, + 0, + 0, + PoolType::Amm, + ); + + let depth = get_liquidity_depth(&env, pool_id); + // total_value_locked = reserve_a priced in terms of B (via reserve_b/reserve_a) + reserve_b + assert_eq!(depth.total_value_locked, 2 * reserve_b); + }); } // ── Impermanent loss tests ───────────────────────────────────────────── #[test] fn test_calculate_impermanent_loss_no_price_change() { - let env = setup(); - let pool_id = String::from_str(&env, "IL_POOL"); - let entry_price = 5 * PRECISION; - let initial_value = 10_000 * PRECISION; - - env.ledger().set_timestamp(1000); - record_pool_state( - &env, - pool_id.clone(), - 1000 * PRECISION, - 5000 * PRECISION, - 2000 * PRECISION, - 0, - 0, - PoolType::Amm, - ); + let (env, contract_id) = setup(); + env.as_contract(&contract_id, || { + let pool_id = String::from_str(&env, "IL_POOL"); + let entry_price = 5 * PRECISION; + let initial_value = 10_000 * PRECISION; - let result = calculate_impermanent_loss(&env, pool_id, entry_price, initial_value); - assert_eq!(result.entry_price, entry_price); - assert_eq!(result.current_value, initial_value); - assert_eq!(result.net_loss, 0); + env.ledger().set_timestamp(1000); + record_pool_state( + &env, + pool_id.clone(), + 1000 * PRECISION, + 5000 * PRECISION, + 2000 * PRECISION, + 0, + 0, + PoolType::Amm, + ); + + let result = calculate_impermanent_loss(&env, pool_id, entry_price, initial_value); + assert_eq!(result.entry_price, entry_price); + assert_eq!(result.current_value, initial_value); + assert_eq!(result.net_loss, 0); + }); } #[test] fn test_calculate_impermanent_loss_with_price_change() { - let env = setup(); - let pool_id = String::from_str(&env, "IL_CHANGE_POOL"); - let entry_price = 5 * PRECISION; - let initial_value = 10_000 * PRECISION; - - env.ledger().set_timestamp(1000); - // Price at entry: reserve_b / reserve_a = 5000 / 1000 = 5 - record_pool_state( - &env, - pool_id.clone(), - 1000 * PRECISION, - 5000 * PRECISION, - 2000 * PRECISION, - 0, - 0, - PoolType::Amm, - ); + let (env, contract_id) = setup(); + env.as_contract(&contract_id, || { + let pool_id = String::from_str(&env, "IL_CHANGE_POOL"); + let entry_price = 5 * PRECISION; + let initial_value = 10_000 * PRECISION; + + env.ledger().set_timestamp(1000); + // Price at entry: reserve_b / reserve_a = 5000 / 1000 = 5 + record_pool_state( + &env, + pool_id.clone(), + 1000 * PRECISION, + 5000 * PRECISION, + 2000 * PRECISION, + 0, + 0, + PoolType::Amm, + ); - env.ledger().set_timestamp(2000); - // Price changes to: 6000 / 1000 = 6 - record_pool_state( - &env, - pool_id.clone(), - 1000 * PRECISION, - 6000 * PRECISION, - 2400 * PRECISION, - 0, - 0, - PoolType::Amm, - ); + env.ledger().set_timestamp(2000); + // Price changes to: 6000 / 1000 = 6 + record_pool_state( + &env, + pool_id.clone(), + 1000 * PRECISION, + 6000 * PRECISION, + 2400 * PRECISION, + 0, + 0, + PoolType::Amm, + ); - let result = calculate_impermanent_loss(&env, pool_id, entry_price, initial_value); - assert!(result.il_percentage >= 0); + let result = calculate_impermanent_loss(&env, pool_id, entry_price, initial_value); + assert!(result.il_percentage >= 0); + }); } #[test] fn test_calculate_impermanent_loss_invalid_prices() { - let env = setup(); - let pool_id = String::from_str(&env, "INVALID_POOL"); - let initial_value = 10_000 * PRECISION; - - let result = calculate_impermanent_loss(&env, pool_id, 0, initial_value); - assert_eq!(result.il_percentage, 0); - assert_eq!(result.current_value, initial_value); + let (env, contract_id) = setup(); + env.as_contract(&contract_id, || { + let pool_id = String::from_str(&env, "INVALID_POOL"); + let initial_value = 10_000 * PRECISION; + + let result = calculate_impermanent_loss(&env, pool_id, 0, initial_value); + assert_eq!(result.il_percentage, 0); + assert_eq!(result.current_value, initial_value); + }); } // ── Daily bucket aggregation tests ───────────────────────────────────── #[test] fn test_get_daily_history() { - let env = setup(); - let pool_id = String::from_str(&env, "DAILY_POOL"); - - // Record snapshots in same day - env.ledger().set_timestamp(86400); // 1 day in seconds - record_pool_state( - &env, - pool_id.clone(), - 1000 * PRECISION, - 5000 * PRECISION, - 2000 * PRECISION, - 100 * PRECISION, - 10 * PRECISION, - PoolType::Amm, - ); + let (env, contract_id) = setup(); + env.as_contract(&contract_id, || { + let pool_id = String::from_str(&env, "DAILY_POOL"); - env.ledger().set_timestamp(86400 + 3600); // +1 hour - record_pool_state( - &env, - pool_id.clone(), - 1100 * PRECISION, - 5500 * PRECISION, - 2200 * PRECISION, - 50 * PRECISION, - 5 * PRECISION, - PoolType::Amm, - ); + // Record snapshots in same day + env.ledger().set_timestamp(86400); // 1 day in seconds + record_pool_state( + &env, + pool_id.clone(), + 1000 * PRECISION, + 5000 * PRECISION, + 2000 * PRECISION, + 100 * PRECISION, + 10 * PRECISION, + PoolType::Amm, + ); + + env.ledger().set_timestamp(86400 + 3600); // +1 hour + record_pool_state( + &env, + pool_id.clone(), + 1100 * PRECISION, + 5500 * PRECISION, + 2200 * PRECISION, + 50 * PRECISION, + 5 * PRECISION, + PoolType::Amm, + ); - let daily = get_daily_history(&env, pool_id, 0, u64::MAX); - assert!(daily.len() >= 1); + let daily = get_daily_history(&env, pool_id, 0, u64::MAX); + assert!(daily.len() >= 1); - let bucket = daily.get(0).unwrap(); - assert_eq!(bucket.total_volume, 150 * PRECISION); - assert_eq!(bucket.total_fees, 15 * PRECISION); + let bucket = daily.get(0).unwrap(); + assert_eq!(bucket.total_volume, 150 * PRECISION); + assert_eq!(bucket.total_fees, 15 * PRECISION); + }); } // ── Edge cases and error handling ────────────────────────────────────── #[test] fn test_record_pool_with_zero_reserves() { - let env = setup(); - let pool_id = String::from_str(&env, "ZERO_RESERVE"); - - record_pool_state( - &env, - pool_id.clone(), - 0, - 5000 * PRECISION, - 0, - 0, - 0, - PoolType::Amm, - ); - - let snapshots = get_pool_history(&env, pool_id, 0, u64::MAX); - assert_eq!(snapshots.len(), 1); - assert_eq!(snapshots.get(0).unwrap().price, 0); // price undefined when reserve_a = 0 - } + let (env, contract_id) = setup(); + env.as_contract(&contract_id, || { + let pool_id = String::from_str(&env, "ZERO_RESERVE"); - #[test] - fn test_record_pool_sdex_type() { - let env = setup(); - let pool_id = String::from_str(&env, "SDEX_POOL"); - - record_pool_state( - &env, - pool_id.clone(), - 1000 * PRECISION, - 5000 * PRECISION, - 2000 * PRECISION, - 100 * PRECISION, - 10 * PRECISION, - PoolType::Sdex, - ); + record_pool_state( + &env, + pool_id.clone(), + 0, + 5000 * PRECISION, + 0, + 0, + 0, + PoolType::Amm, + ); - let snapshots = get_pool_history(&env, pool_id, 0, u64::MAX); - assert_eq!(snapshots.get(0).unwrap().pool_type, PoolType::Sdex); + let snapshots = get_pool_history(&env, pool_id, 0, u64::MAX); + assert_eq!(snapshots.len(), 1); + assert_eq!(snapshots.get(0).unwrap().price, 0); // price undefined when reserve_a = 0 + }); } #[test] - fn test_get_registered_pools_multiple() { - let env = setup(); + fn test_record_pool_sdex_type() { + let (env, contract_id) = setup(); + env.as_contract(&contract_id, || { + let pool_id = String::from_str(&env, "SDEX_POOL"); - for i in 0..3 { - let pool_id = String::from_str(&env, &format!("POOL_{}", i)); record_pool_state( &env, - pool_id, + pool_id.clone(), 1000 * PRECISION, 5000 * PRECISION, 2000 * PRECISION, - 0, - 0, - PoolType::Amm, + 100 * PRECISION, + 10 * PRECISION, + PoolType::Sdex, ); - } - let pools = get_registered_pools(&env); - assert!(pools.len() >= 3); + let snapshots = get_pool_history(&env, pool_id, 0, u64::MAX); + assert_eq!(snapshots.get(0).unwrap().pool_type, PoolType::Sdex); + }); + } + + #[test] + fn test_get_registered_pools_multiple() { + let (env, contract_id) = setup(); + env.as_contract(&contract_id, || { + + for pool_name in ["POOL_0", "POOL_1", "POOL_2"] { + let pool_id = String::from_str(&env, pool_name); + record_pool_state( + &env, + pool_id, + 1000 * PRECISION, + 5000 * PRECISION, + 2000 * PRECISION, + 0, + 0, + PoolType::Amm, + ); + } + + let pools = get_registered_pools(&env); + assert!(pools.len() >= 3); + }); } } diff --git a/soroban/src/operator_rotation.rs b/soroban/src/operator_rotation.rs index 17167e5..b4266d7 100644 --- a/soroban/src/operator_rotation.rs +++ b/soroban/src/operator_rotation.rs @@ -206,116 +206,178 @@ mod tests { use super::*; use soroban_sdk::testutils::Address as _; use soroban_sdk::testutils::Ledger; - use soroban_sdk::Env; + use soroban_sdk::{contract, contractimpl, Env}; - fn setup() -> (Env, Address) { + // Free functions in this module touch env.storage(), which soroban-sdk only + // allows from within an active contract call frame. This dummy contract + // exists purely to give tests that frame via env.as_contract(). + #[contract] + struct TestContext; + + #[contractimpl] + impl TestContext {} + + fn setup() -> (Env, Address, Address) { let env = Env::default(); env.mock_all_auths(); + let contract_id = env.register_contract(None, TestContext); let admin = Address::generate(&env); - env.storage().instance().set(&keys::ADMIN, &admin); + env.as_contract(&contract_id, || { + env.storage().instance().set(&keys::ADMIN, &admin); + }); env.ledger().set_timestamp(1_000_000); - (env, admin) + (env, admin, contract_id) } #[test] fn test_add_operator() { - let (env, admin) = setup(); + let (env, admin, contract_id) = setup(); let op = Address::generate(&env); - add_operator(&env, &admin, &op, String::from_str(&env, "Operator 1")); + env.as_contract(&contract_id, || { + add_operator(&env, &admin, &op, String::from_str(&env, "Operator 1")); - assert!(is_operator(&env, &op)); + assert!(is_operator(&env, &op)); + }); } #[test] fn test_remove_operator() { - let (env, admin) = setup(); + let (env, admin, contract_id) = setup(); let op = Address::generate(&env); - - add_operator(&env, &admin, &op, String::from_str(&env, "Operator 1")); - assert!(is_operator(&env, &op)); - - remove_operator(&env, &admin, &op); - assert!(!is_operator(&env, &op)); + // remove_operator refuses to drop the last active operator, so keep + // a second one around for this call to succeed. + let other = Address::generate(&env); + + env.as_contract(&contract_id, || { + add_operator(&env, &admin, &op, String::from_str(&env, "Operator 1")); + }); + env.as_contract(&contract_id, || { + add_operator(&env, &admin, &other, String::from_str(&env, "Operator 2")); + }); + env.as_contract(&contract_id, || { + assert!(is_operator(&env, &op)); + remove_operator(&env, &admin, &op); + }); + env.as_contract(&contract_id, || { + assert!(!is_operator(&env, &op)); + }); } #[test] fn test_cannot_remove_last_operator() { - let (env, admin) = setup(); + let (env, admin, contract_id) = setup(); let op = Address::generate(&env); - add_operator(&env, &admin, &op, String::from_str(&env, "Operator 1")); - assert!(is_operator(&env, &op)); + env.as_contract(&contract_id, || { + add_operator(&env, &admin, &op, String::from_str(&env, "Operator 1")); + assert!(is_operator(&env, &op)); + }); } #[test] fn test_get_all_operators() { - let (env, admin) = setup(); + let (env, admin, contract_id) = setup(); let op1 = Address::generate(&env); let op2 = Address::generate(&env); - add_operator(&env, &admin, &op1, String::from_str(&env, "Op 1")); - add_operator(&env, &admin, &op2, String::from_str(&env, "Op 2")); - - let all = get_all_operators(&env); - assert_eq!(all.len(), 2); + env.as_contract(&contract_id, || { + add_operator(&env, &admin, &op1, String::from_str(&env, "Op 1")); + }); + env.as_contract(&contract_id, || { + add_operator(&env, &admin, &op2, String::from_str(&env, "Op 2")); + }); + env.as_contract(&contract_id, || { + let all = get_all_operators(&env); + assert_eq!(all.len(), 2); + }); } #[test] fn test_get_active_operators_excludes_removed() { - let (env, admin) = setup(); + let (env, admin, contract_id) = setup(); let op1 = Address::generate(&env); let op2 = Address::generate(&env); - add_operator(&env, &admin, &op1, String::from_str(&env, "Op 1")); - add_operator(&env, &admin, &op2, String::from_str(&env, "Op 2")); - remove_operator(&env, &admin, &op1); - - let active = get_active_operators(&env); - assert_eq!(active.len(), 1); - assert_eq!(active.get(0).unwrap().address, op2); + env.as_contract(&contract_id, || { + add_operator(&env, &admin, &op1, String::from_str(&env, "Op 1")); + }); + env.as_contract(&contract_id, || { + add_operator(&env, &admin, &op2, String::from_str(&env, "Op 2")); + }); + env.as_contract(&contract_id, || { + remove_operator(&env, &admin, &op1); + }); + env.as_contract(&contract_id, || { + let active = get_active_operators(&env); + assert_eq!(active.len(), 1); + assert_eq!(active.get(0).unwrap().address, op2); + }); } #[test] fn test_unknown_operator_not_active() { - let (env, _admin) = setup(); + let (env, _admin, contract_id) = setup(); let unknown = Address::generate(&env); - assert!(!is_operator(&env, &unknown)); + env.as_contract(&contract_id, || { + assert!(!is_operator(&env, &unknown)); + }); } #[test] #[should_panic(expected = "operator name cannot be empty")] fn test_add_operator_empty_name() { - let (env, admin) = setup(); + let (env, admin, contract_id) = setup(); let op = Address::generate(&env); - add_operator(&env, &admin, &op, String::from_str(&env, "")); + env.as_contract(&contract_id, || { + add_operator(&env, &admin, &op, String::from_str(&env, "")); + }); } #[test] #[should_panic(expected = "operator not found")] fn test_remove_unregistered_operator() { - let (env, admin) = setup(); + let (env, admin, contract_id) = setup(); let op = Address::generate(&env); - remove_operator(&env, &admin, &op); + env.as_contract(&contract_id, || { + remove_operator(&env, &admin, &op); + }); } #[test] fn test_reactivate_operator() { - let (env, admin) = setup(); + let (env, admin, contract_id) = setup(); let op = Address::generate(&env); - - add_operator(&env, &admin, &op, String::from_str(&env, "Op 1")); - remove_operator(&env, &admin, &op); - assert!(!is_operator(&env, &op)); - - add_operator(&env, &admin, &op, String::from_str(&env, "Op 1 v2")); - assert!(is_operator(&env, &op)); + // remove_operator refuses to drop the last active operator, so keep + // a second one around for this call to succeed. + let other = Address::generate(&env); + + env.as_contract(&contract_id, || { + add_operator(&env, &admin, &op, String::from_str(&env, "Op 1")); + }); + env.as_contract(&contract_id, || { + add_operator(&env, &admin, &other, String::from_str(&env, "Other")); + }); + env.as_contract(&contract_id, || { + remove_operator(&env, &admin, &op); + }); + env.as_contract(&contract_id, || { + assert!(!is_operator(&env, &op)); + }); + env.as_contract(&contract_id, || { + add_operator(&env, &admin, &op, String::from_str(&env, "Op 1 v2")); + }); + env.as_contract(&contract_id, || { + assert!(is_operator(&env, &op)); + }); } #[test] fn test_operator_event_emission() { - let (env, admin) = setup(); + let (env, admin, contract_id) = setup(); let op = Address::generate(&env); - add_operator(&env, &admin, &op, String::from_str(&env, "Event Op")); + env.as_contract(&contract_id, || { + add_operator(&env, &admin, &op, String::from_str(&env, "Event Op")); + }); } } diff --git a/soroban/src/rate_limiter.rs b/soroban/src/rate_limiter.rs index baa2b7d..4f3345d 100644 --- a/soroban/src/rate_limiter.rs +++ b/soroban/src/rate_limiter.rs @@ -2114,10 +2114,12 @@ mod tests { ConsumeResult::Rejected(RateLimitError::DailyValueLimitExceeded as u32) ); - // Daily remaining must be unchanged — rejected transfers must not be recorded - let check = client.check_limit(&user, &1); - assert!(check.allowed); - assert_eq!(check.daily_remaining_value, DEFAULT_DAILY_LIMIT); + // The rejected amount must not be added to the value-used counter — a + // breach still triggers its documented side effects (cooldown, risk + // profile), so we read the raw usage record rather than check_limit, + // which would itself error out with CooldownActive right now. + let usage = client.get_user_usage(&user); + assert_eq!(usage.daily.value_used, 0); } // ----------------------------------------------------------------------- diff --git a/soroban/src/source_blessing.rs b/soroban/src/source_blessing.rs index 417aea2..134d9b5 100644 --- a/soroban/src/source_blessing.rs +++ b/soroban/src/source_blessing.rs @@ -260,206 +260,248 @@ mod tests { use super::*; use soroban_sdk::testutils::Address as _; use soroban_sdk::testutils::Ledger; - use soroban_sdk::Env; + use soroban_sdk::{contract, contractimpl, Env}; - fn setup() -> (Env, Address) { + #[contract] + struct TestContext; + + #[contractimpl] + impl TestContext {} + + fn setup() -> (Env, Address, Address) { let env = Env::default(); env.mock_all_auths(); + let contract_id = env.register_contract(None, TestContext); let admin = Address::generate(&env); - env.storage().instance().set(&keys::ADMIN, &admin); + env.as_contract(&contract_id, || { + env.storage().instance().set(&keys::ADMIN, &admin); + }); env.ledger().set_timestamp(1_000_000); - (env, admin) + (env, admin, contract_id) } #[test] fn test_bless_source() { - let (env, admin) = setup(); - let source = Address::generate(&env); - - bless_source( - &env, - &admin, - &source, - String::from_str(&env, "USDC"), - String::from_str(&env, "CoinGecko"), - ); - - assert!(is_source_blessed( - &env, - &source, - &String::from_str(&env, "USDC") - )); + let (env, admin, contract_id) = setup(); + env.as_contract(&contract_id, || { + let source = Address::generate(&env); + + bless_source( + &env, + &admin, + &source, + String::from_str(&env, "USDC"), + String::from_str(&env, "CoinGecko"), + ); + + assert!(is_source_blessed( + &env, + &source, + &String::from_str(&env, "USDC") + )); + }); } #[test] fn test_unbless_source() { - let (env, admin) = setup(); + let (env, admin, contract_id) = setup(); let source = Address::generate(&env); - bless_source( - &env, - &admin, - &source, - String::from_str(&env, "USDC"), - String::from_str(&env, "CoinGecko"), - ); - assert!(is_source_blessed( - &env, - &source, - &String::from_str(&env, "USDC") - )); - - unbless_source(&env, &admin, &source, String::from_str(&env, "USDC")); - assert!(!is_source_blessed( - &env, - &source, - &String::from_str(&env, "USDC") - )); + env.as_contract(&contract_id, || { + bless_source( + &env, + &admin, + &source, + String::from_str(&env, "USDC"), + String::from_str(&env, "CoinGecko"), + ); + }); + env.as_contract(&contract_id, || { + assert!(is_source_blessed( + &env, + &source, + &String::from_str(&env, "USDC") + )); + + unbless_source(&env, &admin, &source, String::from_str(&env, "USDC")); + }); + env.as_contract(&contract_id, || { + assert!(!is_source_blessed( + &env, + &source, + &String::from_str(&env, "USDC") + )); + }); } #[test] fn test_blessing_is_per_asset() { - let (env, admin) = setup(); - let source = Address::generate(&env); - - bless_source( - &env, - &admin, - &source, - String::from_str(&env, "USDC"), - String::from_str(&env, "CoinGecko"), - ); - - assert!(is_source_blessed( - &env, - &source, - &String::from_str(&env, "USDC") - )); - assert!(!is_source_blessed( - &env, - &source, - &String::from_str(&env, "EURC") - )); + let (env, admin, contract_id) = setup(); + env.as_contract(&contract_id, || { + let source = Address::generate(&env); + + bless_source( + &env, + &admin, + &source, + String::from_str(&env, "USDC"), + String::from_str(&env, "CoinGecko"), + ); + + assert!(is_source_blessed( + &env, + &source, + &String::from_str(&env, "USDC") + )); + assert!(!is_source_blessed( + &env, + &source, + &String::from_str(&env, "EURC") + )); + }); } #[test] fn test_get_blessed_sources_for_asset() { - let (env, admin) = setup(); + let (env, admin, contract_id) = setup(); let source1 = Address::generate(&env); let source2 = Address::generate(&env); - bless_source( - &env, - &admin, - &source1, - String::from_str(&env, "USDC"), - String::from_str(&env, "Oracle 1"), - ); - bless_source( - &env, - &admin, - &source2, - String::from_str(&env, "USDC"), - String::from_str(&env, "Oracle 2"), - ); - - let blessed = get_blessed_sources_for_asset(&env, &String::from_str(&env, "USDC")); - assert_eq!(blessed.len(), 2); + env.as_contract(&contract_id, || { + bless_source( + &env, + &admin, + &source1, + String::from_str(&env, "USDC"), + String::from_str(&env, "Oracle 1"), + ); + }); + env.as_contract(&contract_id, || { + bless_source( + &env, + &admin, + &source2, + String::from_str(&env, "USDC"), + String::from_str(&env, "Oracle 2"), + ); + }); + env.as_contract(&contract_id, || { + let blessed = get_blessed_sources_for_asset(&env, &String::from_str(&env, "USDC")); + assert_eq!(blessed.len(), 2); + }); } #[test] fn test_get_all_blessings() { - let (env, admin) = setup(); + let (env, admin, contract_id) = setup(); let source1 = Address::generate(&env); let source2 = Address::generate(&env); - bless_source( - &env, - &admin, - &source1, - String::from_str(&env, "USDC"), - String::from_str(&env, "Oracle 1"), - ); - bless_source( - &env, - &admin, - &source2, - String::from_str(&env, "EURC"), - String::from_str(&env, "Oracle 2"), - ); - - let all = get_all_blessings(&env); - assert_eq!(all.len(), 2); + env.as_contract(&contract_id, || { + bless_source( + &env, + &admin, + &source1, + String::from_str(&env, "USDC"), + String::from_str(&env, "Oracle 1"), + ); + }); + env.as_contract(&contract_id, || { + bless_source( + &env, + &admin, + &source2, + String::from_str(&env, "EURC"), + String::from_str(&env, "Oracle 2"), + ); + }); + env.as_contract(&contract_id, || { + let all = get_all_blessings(&env); + assert_eq!(all.len(), 2); + }); } #[test] fn test_unblessed_source_not_preferred() { - let (env, admin) = setup(); + let (env, admin, contract_id) = setup(); let source = Address::generate(&env); - bless_source( - &env, - &admin, - &source, - String::from_str(&env, "USDC"), - String::from_str(&env, "Oracle"), - ); - unbless_source(&env, &admin, &source, String::from_str(&env, "USDC")); - - let preferred = get_preferred_source_for_asset(&env, &String::from_str(&env, "USDC")); - assert!(preferred.is_none()); + env.as_contract(&contract_id, || { + bless_source( + &env, + &admin, + &source, + String::from_str(&env, "USDC"), + String::from_str(&env, "Oracle"), + ); + }); + env.as_contract(&contract_id, || { + unbless_source(&env, &admin, &source, String::from_str(&env, "USDC")); + }); + env.as_contract(&contract_id, || { + let preferred = get_preferred_source_for_asset(&env, &String::from_str(&env, "USDC")); + assert!(preferred.is_none()); + }); } #[test] fn test_preferred_source_for_asset() { - let (env, admin) = setup(); - let source = Address::generate(&env); - - bless_source( - &env, - &admin, - &source, - String::from_str(&env, "USDC"), - String::from_str(&env, "Primary Oracle"), - ); - - let preferred = get_preferred_source_for_asset(&env, &String::from_str(&env, "USDC")); - assert!(preferred.is_some()); - assert_eq!(preferred.unwrap(), source); + let (env, admin, contract_id) = setup(); + env.as_contract(&contract_id, || { + let source = Address::generate(&env); + + bless_source( + &env, + &admin, + &source, + String::from_str(&env, "USDC"), + String::from_str(&env, "Primary Oracle"), + ); + + let preferred = get_preferred_source_for_asset(&env, &String::from_str(&env, "USDC")); + assert!(preferred.is_some()); + assert_eq!(preferred.unwrap(), source); + }); } #[test] fn test_no_blessed_sources_returns_none() { - let (env, _admin) = setup(); - let preferred = get_preferred_source_for_asset(&env, &String::from_str(&env, "USDC")); - assert!(preferred.is_none()); + let (env, _admin, contract_id) = setup(); + env.as_contract(&contract_id, || { + let preferred = get_preferred_source_for_asset(&env, &String::from_str(&env, "USDC")); + assert!(preferred.is_none()); + }); } #[test] #[should_panic(expected = "source name cannot be empty")] fn test_bless_source_empty_name() { - let (env, admin) = setup(); - let source = Address::generate(&env); - bless_source( - &env, - &admin, - &source, - String::from_str(&env, "USDC"), - String::from_str(&env, ""), - ); + let (env, admin, contract_id) = setup(); + env.as_contract(&contract_id, || { + let source = Address::generate(&env); + bless_source( + &env, + &admin, + &source, + String::from_str(&env, "USDC"), + String::from_str(&env, ""), + ); + }); } #[test] #[should_panic(expected = "asset_code cannot be empty")] fn test_bless_source_empty_asset() { - let (env, admin) = setup(); - let source = Address::generate(&env); - bless_source( - &env, - &admin, - &source, - String::from_str(&env, ""), - String::from_str(&env, "Oracle"), - ); + let (env, admin, contract_id) = setup(); + env.as_contract(&contract_id, || { + let source = Address::generate(&env); + bless_source( + &env, + &admin, + &source, + String::from_str(&env, ""), + String::from_str(&env, "Oracle"), + ); + }); } } diff --git a/soroban/src/threshold_window.rs b/soroban/src/threshold_window.rs index e5416c3..fca4ced 100644 --- a/soroban/src/threshold_window.rs +++ b/soroban/src/threshold_window.rs @@ -261,129 +261,162 @@ mod tests { use super::*; use soroban_sdk::testutils::Address as _; use soroban_sdk::testutils::Ledger; - use soroban_sdk::{Address, Env}; + use soroban_sdk::{contract, contractimpl, Address, Env}; - fn setup() -> (Env, Address) { + #[contract] + struct TestContext; + + #[contractimpl] + impl TestContext {} + + fn setup() -> (Env, Address, Address) { let env = Env::default(); env.mock_all_auths(); + let contract_id = env.register_contract(None, TestContext); let admin = Address::generate(&env); - env.storage().instance().set(&keys::ADMIN, &admin); + env.as_contract(&contract_id, || { + env.storage().instance().set(&keys::ADMIN, &admin); + }); env.ledger().set_timestamp(1_000_000); - (env, admin) + (env, admin, contract_id) } #[test] fn test_create_window() { - let (env, admin) = setup(); - create_window( - &env, - &admin, - String::from_str(&env, "price_dev_1h"), - 1, - WindowUnit::Hours, - 500, - ); - let window = get_window(&env, &String::from_str(&env, "price_dev_1h")); - assert!(window.is_some()); - let w = window.unwrap(); - assert_eq!(w.threshold_bps, 500); + let (env, admin, contract_id) = setup(); + env.as_contract(&contract_id, || { + create_window( + &env, + &admin, + String::from_str(&env, "price_dev_1h"), + 1, + WindowUnit::Hours, + 500, + ); + let window = get_window(&env, &String::from_str(&env, "price_dev_1h")); + assert!(window.is_some()); + let w = window.unwrap(); + assert_eq!(w.threshold_bps, 500); + }); } #[test] fn test_update_window() { - let (env, admin) = setup(); - create_window( - &env, - &admin, - String::from_str(&env, "win1"), - 1, - WindowUnit::Hours, - 500, - ); - update_window( - &env, - &admin, - String::from_str(&env, "win1"), - 2, - WindowUnit::Hours, - 300, - ); - let window = get_window(&env, &String::from_str(&env, "win1")).unwrap(); - assert_eq!(window.length, 2); - assert_eq!(window.threshold_bps, 300); + let (env, admin, contract_id) = setup(); + env.as_contract(&contract_id, || { + create_window( + &env, + &admin, + String::from_str(&env, "win1"), + 1, + WindowUnit::Hours, + 500, + ); + }); + env.as_contract(&contract_id, || { + update_window( + &env, + &admin, + String::from_str(&env, "win1"), + 2, + WindowUnit::Hours, + 300, + ); + }); + env.as_contract(&contract_id, || { + let window = get_window(&env, &String::from_str(&env, "win1")).unwrap(); + assert_eq!(window.length, 2); + assert_eq!(window.threshold_bps, 300); + }); } #[test] fn test_remove_window() { - let (env, admin) = setup(); - create_window( - &env, - &admin, - String::from_str(&env, "win1"), - 1, - WindowUnit::Hours, - 500, - ); - remove_window(&env, &admin, String::from_str(&env, "win1")); - let window = get_window(&env, &String::from_str(&env, "win1")); - assert!(window.is_none()); + let (env, admin, contract_id) = setup(); + env.as_contract(&contract_id, || { + create_window( + &env, + &admin, + String::from_str(&env, "win1"), + 1, + WindowUnit::Hours, + 500, + ); + }); + env.as_contract(&contract_id, || { + remove_window(&env, &admin, String::from_str(&env, "win1")); + }); + env.as_contract(&contract_id, || { + let window = get_window(&env, &String::from_str(&env, "win1")); + assert!(window.is_none()); + }); } #[test] fn test_get_all_windows() { - let (env, admin) = setup(); - create_window( - &env, - &admin, - String::from_str(&env, "win1"), - 1, - WindowUnit::Hours, - 500, - ); - create_window( - &env, - &admin, - String::from_str(&env, "win2"), - 30, - WindowUnit::Minutes, - 300, - ); - let all = get_all_windows(&env); - assert_eq!(all.len(), 2); + let (env, admin, contract_id) = setup(); + env.as_contract(&contract_id, || { + create_window( + &env, + &admin, + String::from_str(&env, "win1"), + 1, + WindowUnit::Hours, + 500, + ); + }); + env.as_contract(&contract_id, || { + create_window( + &env, + &admin, + String::from_str(&env, "win2"), + 30, + WindowUnit::Minutes, + 300, + ); + }); + env.as_contract(&contract_id, || { + let all = get_all_windows(&env); + assert_eq!(all.len(), 2); + }); } #[test] fn test_evaluate_threshold_no_breach() { - let (env, admin) = setup(); - create_window( - &env, - &admin, - String::from_str(&env, "win1"), - 1, - WindowUnit::Hours, - 500, - ); - let eval = evaluate_threshold(&env, &String::from_str(&env, "win1"), 1_000_000, 1_020_000); - assert!(eval.is_some()); - let e = eval.unwrap(); - assert!(!e.is_breached); + let (env, admin, contract_id) = setup(); + env.as_contract(&contract_id, || { + create_window( + &env, + &admin, + String::from_str(&env, "win1"), + 1, + WindowUnit::Hours, + 500, + ); + let eval = evaluate_threshold(&env, &String::from_str(&env, "win1"), 1_000_000, 1_020_000); + assert!(eval.is_some()); + let e = eval.unwrap(); + assert!(!e.is_breached); + }); } #[test] fn test_evaluate_threshold_breach() { - let (env, admin) = setup(); - create_window( - &env, - &admin, - String::from_str(&env, "win1"), - 1, - WindowUnit::Hours, - 500, - ); - let eval = evaluate_threshold(&env, &String::from_str(&env, "win1"), 1_000_000, 1_100_000); - assert!(eval.is_some()); - let e = eval.unwrap(); - assert!(e.is_breached); + let (env, admin, contract_id) = setup(); + env.as_contract(&contract_id, || { + create_window( + &env, + &admin, + String::from_str(&env, "win1"), + 1, + WindowUnit::Hours, + 500, + ); + let eval = evaluate_threshold(&env, &String::from_str(&env, "win1"), 1_000_000, 1_100_000); + assert!(eval.is_some()); + let e = eval.unwrap(); + assert!(e.is_breached); + }); } #[test] @@ -401,53 +434,61 @@ mod tests { #[test] fn test_create_duplicate_window_panics() { - let (env, admin) = setup(); - create_window( - &env, - &admin, - String::from_str(&env, "win1"), - 1, - WindowUnit::Hours, - 500, - ); + let (env, admin, contract_id) = setup(); + env.as_contract(&contract_id, || { + create_window( + &env, + &admin, + String::from_str(&env, "win1"), + 1, + WindowUnit::Hours, + 500, + ); + }); } #[test] #[should_panic(expected = "window already exists")] fn test_create_duplicate_window_panics_impl() { - let (env, admin) = setup(); - create_window( - &env, - &admin, - String::from_str(&env, "win1"), - 1, - WindowUnit::Hours, - 500, - ); - create_window( - &env, - &admin, - String::from_str(&env, "win1"), - 1, - WindowUnit::Hours, - 500, - ); + let (env, admin, contract_id) = setup(); + env.as_contract(&contract_id, || { + create_window( + &env, + &admin, + String::from_str(&env, "win1"), + 1, + WindowUnit::Hours, + 500, + ); + }); + env.as_contract(&contract_id, || { + create_window( + &env, + &admin, + String::from_str(&env, "win1"), + 1, + WindowUnit::Hours, + 500, + ); + }); } #[test] fn test_evaluate_with_zero_reference() { - let (env, admin) = setup(); - create_window( - &env, - &admin, - String::from_str(&env, "win1"), - 1, - WindowUnit::Hours, - 500, - ); - let eval = evaluate_threshold(&env, &String::from_str(&env, "win1"), 0, 100); - assert!(eval.is_some()); - let e = eval.unwrap(); - assert!(!e.is_breached); + let (env, admin, contract_id) = setup(); + env.as_contract(&contract_id, || { + create_window( + &env, + &admin, + String::from_str(&env, "win1"), + 1, + WindowUnit::Hours, + 500, + ); + let eval = evaluate_threshold(&env, &String::from_str(&env, "win1"), 0, 100); + assert!(eval.is_some()); + let e = eval.unwrap(); + assert!(!e.is_breached); + }); } } diff --git a/soroban/tests/asset_locking.test.rs b/soroban/tests/asset_locking.test.rs index 8b67a93..7861a8e 100644 --- a/soroban/tests/asset_locking.test.rs +++ b/soroban/tests/asset_locking.test.rs @@ -1,9 +1,9 @@ #![cfg(test)] -use soroban_sdk::{testutils::Address as _, Address, Env, String}; +use soroban_sdk::{testutils::{Address as _, Ledger}, Address, Env, String}; // Import the contract and client -use bridge_watch_soroban::{AdminRole, BridgeWatchContract, BridgeWatchContractClient}; +use swipely_contracts::{AdminRole, BridgeWatchContract, BridgeWatchContractClient}; fn setup() -> ( Env, @@ -14,6 +14,7 @@ fn setup() -> ( ) { let env = Env::default(); env.mock_all_auths(); + env.ledger().set_timestamp(1_000_000); let contract_id = env.register_contract(None, BridgeWatchContract); let client = BridgeWatchContractClient::new(&env, &contract_id); @@ -25,9 +26,9 @@ fn setup() -> ( client.initialize(&admin); // Grant roles - client.grant_role(&admin, &manager, AdminRole::AssetManager); - client.grant_role(&admin, &submitter, AdminRole::HealthSubmitter); - client.grant_role(&admin, &submitter, AdminRole::PriceSubmitter); + client.grant_role(&admin, &manager, &AdminRole::AssetManager); + client.grant_role(&admin, &submitter, &AdminRole::HealthSubmitter); + client.grant_role(&admin, &submitter, &AdminRole::PriceSubmitter); (env, client, admin, manager, submitter) } @@ -214,7 +215,7 @@ fn test_submit_health_blocked_when_locked() { client.lock_asset(&admin, &asset_code, &String::from_str(&env, "Under review")); // Try to submit health - should panic - client.submit_health(&submitter, &asset_code, 95, 90, 92, 88); + client.submit_health(&submitter, &asset_code, &95, &90, &92, &88); } #[test] @@ -232,7 +233,7 @@ fn test_submit_price_blocked_when_locked() { client.submit_price( &submitter, &asset_code, - 1_000_000, + &1_000_000, &String::from_str(&env, "oracle1"), ); } @@ -282,11 +283,11 @@ fn test_operations_work_after_unlock() { client.unlock_asset(&admin, &asset_code); // Operations should work now - client.submit_health(&submitter, &asset_code, 95, 90, 92, 88); + client.submit_health(&submitter, &asset_code, &95, &90, &92, &88); client.submit_price( &submitter, &asset_code, - 1_000_000, + &1_000_000, &String::from_str(&env, "oracle1"), ); client.pause_asset(&admin, &asset_code); @@ -485,7 +486,7 @@ fn test_complete_lock_maintenance_unlock_workflow() { client.register_asset(&admin, &asset_code); // Initial submission works - client.submit_health(&submitter, &asset_code, 95, 90, 92, 88); + client.submit_health(&submitter, &asset_code, &95, &90, &92, &88); // Lock for maintenance client.lock_asset( @@ -507,11 +508,11 @@ fn test_complete_lock_maintenance_unlock_workflow() { assert!(!client.is_asset_locked(&asset_code)); // Submissions work again - client.submit_health(&submitter, &asset_code, 96, 91, 93, 89); + client.submit_health(&submitter, &asset_code, &96, &91, &93, &89); client.submit_price( &submitter, &asset_code, - 1_000_100, + &1_000_100, &String::from_str(&env, "oracle1"), ); } diff --git a/soroban/tests/emergency_fund_recovery_tests.rs b/soroban/tests/emergency_fund_recovery_tests.rs index b62cad3..8c01973 100644 --- a/soroban/tests/emergency_fund_recovery_tests.rs +++ b/soroban/tests/emergency_fund_recovery_tests.rs @@ -6,45 +6,62 @@ #[cfg(test)] mod tests { use soroban_sdk::{ + contract, contractimpl, testutils::{Address as _, Ledger}, Address, Env, String, }; - use crate::emergency_fund_recovery::{ - EmergencyFundRecovery, EmergencyRecovery, RecoveryAuthorization, RecoveryError, - RecoveryStatus, + use swipely_contracts::emergency_fund_recovery::{ + EmergencyFundRecovery, RecoveryError, }; + // EmergencyFundRecovery's functions touch env.storage(), which soroban-sdk + // only allows from within an active contract call frame, and each + // require_auth() call consumes that frame's single-use authorization for + // the given address — so every call below gets its own env.as_contract() + // block. + #[contract] + struct TestContext; + + #[contractimpl] + impl TestContext {} + // Helper function to setup test environment - fn setup_env() -> (Env, Address, Address) { + fn setup_env() -> (Env, Address, Address, Address) { let env = Env::default(); + env.mock_all_auths(); let admin = Address::generate(&env); let user = Address::generate(&env); + let contract_id = env.register_contract(None, TestContext); // Mock ledger setup env.ledger().set_timestamp(1_000_000); - (env, admin, user) + (env, admin, user, contract_id) } #[test] fn test_initialize_recovery() { - let (env, admin, _) = setup_env(); + let (env, admin, _, contract_id) = setup_env(); - let result = EmergencyFundRecovery::initialize_recovery( - env.clone(), - admin, - 172_800, // 48 hours - ); + let result = env.as_contract(&contract_id, || { + EmergencyFundRecovery::initialize_recovery( + env.clone(), + admin, + 172_800, // 48 hours + ) + }); assert!(result.is_ok(), "Recovery initialization should succeed"); } #[test] fn test_initialize_recovery_invalid_timelock_zero() { - let (env, admin, _) = setup_env(); + let (env, admin, _, contract_id) = setup_env(); - let result = EmergencyFundRecovery::initialize_recovery(env.clone(), admin, 0); + let result = env.as_contract(&contract_id, || { + EmergencyFundRecovery::initialize_recovery(env.clone(), admin, 0) + }); assert_eq!( result, @@ -55,13 +72,15 @@ mod tests { #[test] fn test_initialize_recovery_invalid_timelock_too_large() { - let (env, admin, _) = setup_env(); + let (env, admin, _, contract_id) = setup_env(); - let result = EmergencyFundRecovery::initialize_recovery( - env.clone(), - admin, - 31_536_001, // More than 1 year - ); + let result = env.as_contract(&contract_id, || { + EmergencyFundRecovery::initialize_recovery( + env.clone(), + admin, + 31_536_001, // More than 1 year + ) + }); assert_eq!( result, @@ -72,28 +91,36 @@ mod tests { #[test] fn test_enable_emergency_recovery() { - let (env, admin, _) = setup_env(); + let (env, admin, _, contract_id) = setup_env(); // Initialize first - EmergencyFundRecovery::initialize_recovery(env.clone(), admin.clone(), 172_800) - .expect("Initialization should succeed"); + env.as_contract(&contract_id, || { + EmergencyFundRecovery::initialize_recovery(env.clone(), admin.clone(), 172_800) + }) + .expect("Initialization should succeed"); // Enable recovery - let result = EmergencyFundRecovery::enable_emergency_recovery(env.clone(), admin); + let result = env.as_contract(&contract_id, || { + EmergencyFundRecovery::enable_emergency_recovery(env.clone(), admin) + }); assert!(result.is_ok(), "Enable emergency recovery should succeed"); } #[test] fn test_enable_emergency_recovery_unauthorized() { - let (env, admin, user) = setup_env(); + let (env, admin, user, contract_id) = setup_env(); // Initialize with admin - EmergencyFundRecovery::initialize_recovery(env.clone(), admin, 172_800) - .expect("Initialization should succeed"); + env.as_contract(&contract_id, || { + EmergencyFundRecovery::initialize_recovery(env.clone(), admin, 172_800) + }) + .expect("Initialization should succeed"); // Try to enable with non-admin - let result = EmergencyFundRecovery::enable_emergency_recovery(env, user); + let result = env.as_contract(&contract_id, || { + EmergencyFundRecovery::enable_emergency_recovery(env.clone(), user) + }); assert_eq!( result, @@ -104,49 +131,54 @@ mod tests { #[test] fn test_add_recovery_authorizer() { - let (env, admin, user) = setup_env(); + let (env, admin, user, contract_id) = setup_env(); // Initialize - EmergencyFundRecovery::initialize_recovery(env.clone(), admin.clone(), 172_800) - .expect("Initialization should succeed"); + env.as_contract(&contract_id, || { + EmergencyFundRecovery::initialize_recovery(env.clone(), admin.clone(), 172_800) + }) + .expect("Initialization should succeed"); // Add authorizer - let result = EmergencyFundRecovery::add_recovery_authorizer( - env.clone(), - admin, - user, - true, // can_initiate - true, // can_approve - true, // can_execute - true, // can_cancel - ); - - assert!( - result.is_ok(), - "Adding recovery authorizer should succeed" - ); + let result = env.as_contract(&contract_id, || { + EmergencyFundRecovery::add_recovery_authorizer( + env.clone(), + admin, + user, + true, // can_initiate + true, // can_approve + true, // can_execute + true, // can_cancel + ) + }); + + assert!(result.is_ok(), "Adding recovery authorizer should succeed"); } #[test] fn test_initiate_recovery_without_emergency_enabled() { - let (env, admin, user) = setup_env(); + let (env, admin, user, contract_id) = setup_env(); // Initialize - EmergencyFundRecovery::initialize_recovery(env.clone(), admin, 172_800) - .expect("Initialization should succeed"); + env.as_contract(&contract_id, || { + EmergencyFundRecovery::initialize_recovery(env.clone(), admin, 172_800) + }) + .expect("Initialization should succeed"); // Try to initiate recovery without enabling emergency mode let destination = Address::generate(&env); let token_address = Address::generate(&env); - let result = EmergencyFundRecovery::initiate_recovery( - env.clone(), - user, - destination, - token_address, - 1000, - String::from_slice(&env, "test recovery"), - ); + let result = env.as_contract(&contract_id, || { + EmergencyFundRecovery::initiate_recovery( + env.clone(), + user, + destination, + token_address, + 1000, + String::from_slice(&env, "test recovery"), + ) + }); assert_eq!( result, @@ -157,38 +189,46 @@ mod tests { #[test] fn test_initiate_recovery_invalid_amount() { - let (env, admin, user) = setup_env(); + let (env, admin, user, contract_id) = setup_env(); // Initialize and enable - EmergencyFundRecovery::initialize_recovery(env.clone(), admin.clone(), 172_800) - .expect("Initialization should succeed"); - EmergencyFundRecovery::enable_emergency_recovery(env.clone(), admin.clone()) - .expect("Enable should succeed"); + env.as_contract(&contract_id, || { + EmergencyFundRecovery::initialize_recovery(env.clone(), admin.clone(), 172_800) + }) + .expect("Initialization should succeed"); + env.as_contract(&contract_id, || { + EmergencyFundRecovery::enable_emergency_recovery(env.clone(), admin.clone()) + }) + .expect("Enable should succeed"); // Add user as authorizer - EmergencyFundRecovery::add_recovery_authorizer( - env.clone(), - admin, - user.clone(), - true, - true, - true, - true, - ) + env.as_contract(&contract_id, || { + EmergencyFundRecovery::add_recovery_authorizer( + env.clone(), + admin, + user.clone(), + true, + true, + true, + true, + ) + }) .expect("Add authorizer should succeed"); // Try to initiate with invalid amount let destination = Address::generate(&env); let token_address = Address::generate(&env); - let result = EmergencyFundRecovery::initiate_recovery( - env.clone(), - user, - destination, - token_address, - 0, // Invalid amount - String::from_slice(&env, "test recovery"), - ); + let result = env.as_contract(&contract_id, || { + EmergencyFundRecovery::initiate_recovery( + env.clone(), + user, + destination, + token_address, + 0, // Invalid amount + String::from_slice(&env, "test recovery"), + ) + }); assert_eq!( result, @@ -199,35 +239,43 @@ mod tests { #[test] fn test_initiate_recovery_same_destination() { - let (env, admin, user) = setup_env(); + let (env, admin, user, contract_id) = setup_env(); // Setup - EmergencyFundRecovery::initialize_recovery(env.clone(), admin.clone(), 172_800) - .expect("Initialization should succeed"); - EmergencyFundRecovery::enable_emergency_recovery(env.clone(), admin.clone()) - .expect("Enable should succeed"); - EmergencyFundRecovery::add_recovery_authorizer( - env.clone(), - admin, - user.clone(), - true, - true, - true, - true, - ) + env.as_contract(&contract_id, || { + EmergencyFundRecovery::initialize_recovery(env.clone(), admin.clone(), 172_800) + }) + .expect("Initialization should succeed"); + env.as_contract(&contract_id, || { + EmergencyFundRecovery::enable_emergency_recovery(env.clone(), admin.clone()) + }) + .expect("Enable should succeed"); + env.as_contract(&contract_id, || { + EmergencyFundRecovery::add_recovery_authorizer( + env.clone(), + admin, + user.clone(), + true, + true, + true, + true, + ) + }) .expect("Add authorizer should succeed"); let token_address = Address::generate(&env); // Try to initiate with same destination and initiator - let result = EmergencyFundRecovery::initiate_recovery( - env.clone(), - user.clone(), - user, // Same as initiator - token_address, - 1000, - String::from_slice(&env, "test recovery"), - ); + let result = env.as_contract(&contract_id, || { + EmergencyFundRecovery::initiate_recovery( + env.clone(), + user.clone(), + user, // Same as initiator + token_address, + 1000, + String::from_slice(&env, "test recovery"), + ) + }); assert_eq!( result, @@ -238,35 +286,43 @@ mod tests { #[test] fn test_initiate_recovery_success() { - let (env, admin, user) = setup_env(); + let (env, admin, user, contract_id) = setup_env(); // Setup - EmergencyFundRecovery::initialize_recovery(env.clone(), admin.clone(), 172_800) - .expect("Initialization should succeed"); - EmergencyFundRecovery::enable_emergency_recovery(env.clone(), admin.clone()) - .expect("Enable should succeed"); - EmergencyFundRecovery::add_recovery_authorizer( - env.clone(), - admin, - user.clone(), - true, - true, - true, - true, - ) + env.as_contract(&contract_id, || { + EmergencyFundRecovery::initialize_recovery(env.clone(), admin.clone(), 172_800) + }) + .expect("Initialization should succeed"); + env.as_contract(&contract_id, || { + EmergencyFundRecovery::enable_emergency_recovery(env.clone(), admin.clone()) + }) + .expect("Enable should succeed"); + env.as_contract(&contract_id, || { + EmergencyFundRecovery::add_recovery_authorizer( + env.clone(), + admin, + user.clone(), + true, + true, + true, + true, + ) + }) .expect("Add authorizer should succeed"); let destination = Address::generate(&env); let token_address = Address::generate(&env); - let result = EmergencyFundRecovery::initiate_recovery( - env.clone(), - user, - destination, - token_address, - 1000, - String::from_slice(&env, "emergency recovery"), - ); + let result = env.as_contract(&contract_id, || { + EmergencyFundRecovery::initiate_recovery( + env.clone(), + user, + destination, + token_address, + 1000, + String::from_slice(&env, "emergency recovery"), + ) + }); assert!(result.is_ok(), "Recovery initiation should succeed"); @@ -277,122 +333,155 @@ mod tests { #[test] fn test_approve_recovery() { - let (env, admin, user) = setup_env(); + let (env, admin, user, contract_id) = setup_env(); // Setup - EmergencyFundRecovery::initialize_recovery(env.clone(), admin.clone(), 172_800) - .expect("Initialization should succeed"); - EmergencyFundRecovery::enable_emergency_recovery(env.clone(), admin.clone()) - .expect("Enable should succeed"); - EmergencyFundRecovery::add_recovery_authorizer( - env.clone(), - admin.clone(), - user.clone(), - true, - true, - true, - true, - ) + env.as_contract(&contract_id, || { + EmergencyFundRecovery::initialize_recovery(env.clone(), admin.clone(), 172_800) + }) + .expect("Initialization should succeed"); + env.as_contract(&contract_id, || { + EmergencyFundRecovery::enable_emergency_recovery(env.clone(), admin.clone()) + }) + .expect("Enable should succeed"); + env.as_contract(&contract_id, || { + EmergencyFundRecovery::add_recovery_authorizer( + env.clone(), + admin.clone(), + user.clone(), + true, + true, + true, + true, + ) + }) .expect("Add authorizer should succeed"); // Initiate recovery let destination = Address::generate(&env); let token_address = Address::generate(&env); - let recovery_id = EmergencyFundRecovery::initiate_recovery( - env.clone(), - user.clone(), - destination, - token_address, - 1000, - String::from_slice(&env, "emergency recovery"), - ) - .expect("Recovery initiation should succeed"); + let recovery_id = env + .as_contract(&contract_id, || { + EmergencyFundRecovery::initiate_recovery( + env.clone(), + user.clone(), + destination, + token_address, + 1000, + String::from_slice(&env, "emergency recovery"), + ) + }) + .expect("Recovery initiation should succeed"); // Approve recovery - let result = EmergencyFundRecovery::approve_recovery(env, user, recovery_id); + let result = env.as_contract(&contract_id, || { + EmergencyFundRecovery::approve_recovery(env.clone(), user, recovery_id) + }); assert!(result.is_ok(), "Recovery approval should succeed"); } #[test] fn test_cancel_recovery() { - let (env, admin, user) = setup_env(); + let (env, admin, user, contract_id) = setup_env(); // Setup - EmergencyFundRecovery::initialize_recovery(env.clone(), admin.clone(), 172_800) - .expect("Initialization should succeed"); - EmergencyFundRecovery::enable_emergency_recovery(env.clone(), admin.clone()) - .expect("Enable should succeed"); - EmergencyFundRecovery::add_recovery_authorizer( - env.clone(), - admin.clone(), - user.clone(), - true, - true, - true, - true, - ) + env.as_contract(&contract_id, || { + EmergencyFundRecovery::initialize_recovery(env.clone(), admin.clone(), 172_800) + }) + .expect("Initialization should succeed"); + env.as_contract(&contract_id, || { + EmergencyFundRecovery::enable_emergency_recovery(env.clone(), admin.clone()) + }) + .expect("Enable should succeed"); + env.as_contract(&contract_id, || { + EmergencyFundRecovery::add_recovery_authorizer( + env.clone(), + admin.clone(), + user.clone(), + true, + true, + true, + true, + ) + }) .expect("Add authorizer should succeed"); // Initiate recovery let destination = Address::generate(&env); let token_address = Address::generate(&env); - let recovery_id = EmergencyFundRecovery::initiate_recovery( - env.clone(), - user.clone(), - destination, - token_address, - 1000, - String::from_slice(&env, "emergency recovery"), - ) - .expect("Recovery initiation should succeed"); + let recovery_id = env + .as_contract(&contract_id, || { + EmergencyFundRecovery::initiate_recovery( + env.clone(), + user.clone(), + destination, + token_address, + 1000, + String::from_slice(&env, "emergency recovery"), + ) + }) + .expect("Recovery initiation should succeed"); // Cancel recovery - let result = EmergencyFundRecovery::cancel_recovery( - env, - user, - recovery_id, - String::from_slice(&env, "cancelled"), - ); + let result = env.as_contract(&contract_id, || { + EmergencyFundRecovery::cancel_recovery( + env.clone(), + user, + recovery_id, + String::from_slice(&env, "cancelled"), + ) + }); assert!(result.is_ok(), "Recovery cancellation should succeed"); } #[test] fn test_get_recovery() { - let (env, admin, user) = setup_env(); + let (env, admin, user, contract_id) = setup_env(); // Setup - EmergencyFundRecovery::initialize_recovery(env.clone(), admin.clone(), 172_800) - .expect("Initialization should succeed"); - EmergencyFundRecovery::enable_emergency_recovery(env.clone(), admin.clone()) - .expect("Enable should succeed"); - EmergencyFundRecovery::add_recovery_authorizer( - env.clone(), - admin, - user.clone(), - true, - true, - true, - true, - ) + env.as_contract(&contract_id, || { + EmergencyFundRecovery::initialize_recovery(env.clone(), admin.clone(), 172_800) + }) + .expect("Initialization should succeed"); + env.as_contract(&contract_id, || { + EmergencyFundRecovery::enable_emergency_recovery(env.clone(), admin.clone()) + }) + .expect("Enable should succeed"); + env.as_contract(&contract_id, || { + EmergencyFundRecovery::add_recovery_authorizer( + env.clone(), + admin, + user.clone(), + true, + true, + true, + true, + ) + }) .expect("Add authorizer should succeed"); // Initiate recovery let destination = Address::generate(&env); let token_address = Address::generate(&env); - let recovery_id = EmergencyFundRecovery::initiate_recovery( - env.clone(), - user, - destination.clone(), - token_address.clone(), - 1000, - String::from_slice(&env, "emergency recovery"), - ) - .expect("Recovery initiation should succeed"); + let recovery_id = env + .as_contract(&contract_id, || { + EmergencyFundRecovery::initiate_recovery( + env.clone(), + user, + destination.clone(), + token_address.clone(), + 1000, + String::from_slice(&env, "emergency recovery"), + ) + }) + .expect("Recovery initiation should succeed"); // Get recovery - let result = EmergencyFundRecovery::get_recovery(env, recovery_id); + let result = env.as_contract(&contract_id, || { + EmergencyFundRecovery::get_recovery(env.clone(), recovery_id) + }); assert!(result.is_ok(), "Get recovery should succeed"); @@ -405,13 +494,17 @@ mod tests { #[test] fn test_get_total_recovered() { - let (env, admin, _) = setup_env(); + let (env, admin, _, contract_id) = setup_env(); // Initialize - EmergencyFundRecovery::initialize_recovery(env.clone(), admin, 172_800) - .expect("Initialization should succeed"); - - let total = EmergencyFundRecovery::get_total_recovered(env); + env.as_contract(&contract_id, || { + EmergencyFundRecovery::initialize_recovery(env.clone(), admin, 172_800) + }) + .expect("Initialization should succeed"); + + let total = env.as_contract(&contract_id, || { + EmergencyFundRecovery::get_total_recovered(env.clone()) + }); assert_eq!(total, 0, "Initial total recovered should be 0"); } diff --git a/soroban/tests/governance.test.rs b/soroban/tests/governance.test.rs deleted file mode 100644 index 9429c7f..0000000 --- a/soroban/tests/governance.test.rs +++ /dev/null @@ -1,304 +0,0 @@ -#![cfg(test)] - -use soroban_sdk::{ - testutils::{Address as _, Ledger}, - Address, Env, String, -}; - -use bridge_watch_soroban::governance::{ - GovernanceContract, GovernanceContractClient, ProposalStatus, ProposalType, VoteChoice, -}; - -// ── Helpers ─────────────────────────────────────────────────────────────────── - -fn setup() -> (Env, GovernanceContractClient<'static>, Address) { - let env = Env::default(); - env.mock_all_auths(); - env.ledger().with_mut(|li| li.timestamp = 1_000_000); - - let contract_id = env.register_contract(None, GovernanceContract); - let client = GovernanceContractClient::new(&env, &contract_id); - let admin = Address::generate(&env); - - client.initialize( - &admin, - &100, // timelock_delay - &200, // voting_period - &10, // voting_delay - &1_000, // quorum_bps (10 %) - &5_100, // pass_threshold_bps (51 %) - &100, // proposal_deposit - &false, // use_quadratic - &1, // guardian_threshold - ); - - (env, client, admin) -} - -fn advance(env: &Env, secs: u64) { - env.ledger().with_mut(|li| li.timestamp += secs); -} - -fn mk(env: &Env, s: &str) -> String { - String::from_str(env, s) -} - -fn funded_proposer(env: &Env, client: &GovernanceContractClient, power: i128) -> Address { - let p = Address::generate(env); - client.set_voting_power(&p, &power); - p -} - -fn create_proposal( - env: &Env, - client: &GovernanceContractClient, - proposer: &Address, - ptype: ProposalType, -) -> u32 { - let target = Address::generate(env); - client.create_proposal( - proposer, - &ptype, - &mk(env, "title"), - &mk(env, "description"), - &target, - &mk(env, "calldata"), - ) -} - -// ── initialize ──────────────────────────────────────────────────────────────── - -#[test] -fn test_initialize_stores_config() { - let (env, client, _admin) = setup(); - let cfg = client.get_config(); - assert_eq!(cfg.quorum_bps, 1_000); - assert_eq!(cfg.pass_threshold_bps, 5_100); - assert_eq!(cfg.proposal_deposit, 100); - assert!(!cfg.use_quadratic); - assert_eq!(client.proposal_count(), 0); - assert_eq!(client.total_supply(), 0); -} - -// ── Proposal creation & activation ─────────────────────────────────────────── - -#[test] -fn test_create_proposal_starts_pending() { - let (env, client, _admin) = setup(); - let proposer = funded_proposer(&env, &client, 500); - - let id = create_proposal(&env, &client, &proposer, ProposalType::ParameterChange); - - let proposal = client.get_proposal(&id); - assert_eq!(proposal.status, ProposalStatus::Pending); - assert_eq!(proposal.votes_for, 0); - assert_eq!(proposal.votes_against, 0); - assert_eq!(proposal.votes_abstain, 0); -} - -#[test] -fn test_activate_proposal_after_delay() { - let (env, client, _admin) = setup(); - let proposer = funded_proposer(&env, &client, 500); - let id = create_proposal(&env, &client, &proposer, ProposalType::ParameterChange); - - advance(&env, 15); // past voting_delay (10) - client.activate_proposal(&id); - - assert_eq!(client.get_proposal(&id).status, ProposalStatus::Active); -} - -#[test] -fn test_proposal_count_increments_per_proposal() { - let (env, client, _admin) = setup(); - let proposer = funded_proposer(&env, &client, 500); - - assert_eq!(client.proposal_count(), 0); - create_proposal(&env, &client, &proposer, ProposalType::ParameterChange); - assert_eq!(client.proposal_count(), 1); - create_proposal(&env, &client, &proposer, ProposalType::OperatorApproval); - assert_eq!(client.proposal_count(), 2); -} - -// ── Full lifecycle: Pending → Active → Passed → Queued → Executed ───────────── - -#[test] -fn test_full_lifecycle_passes_and_executes() { - let (env, client, _admin) = setup(); - let proposer = funded_proposer(&env, &client, 500); - let voter = funded_proposer(&env, &client, 500); - let executor = Address::generate(&env); - let id = create_proposal(&env, &client, &proposer, ProposalType::ParameterChange); - - // Activate - advance(&env, 15); - client.activate_proposal(&id); - - // Both vote For (total supply = 1000, total votes = 1000 → quorum met, threshold met) - client.cast_vote(&proposer, &id, &VoteChoice::For); - client.cast_vote(&voter, &id, &VoteChoice::For); - - // Finalize after voting period - advance(&env, 205); - client.finalize_proposal(&id); - assert_eq!(client.get_proposal(&id).status, ProposalStatus::Passed); - - // Queue for timelock - client.queue_proposal(&id); - assert_eq!(client.get_proposal(&id).status, ProposalStatus::Queued); - - // Execute after timelock - advance(&env, 105); - client.execute_proposal(&executor, &id); - assert_eq!(client.get_proposal(&id).status, ProposalStatus::Executed); -} - -// ── Quorum failure ──────────────────────────────────────────────────────────── - -#[test] -fn test_proposal_fails_when_quorum_not_met() { - let (env, client, _admin) = setup(); - // Only proposer has power; no votes cast → total_votes = 0 < quorum - let proposer = funded_proposer(&env, &client, 1_000); - let id = create_proposal(&env, &client, &proposer, ProposalType::ParameterChange); - - advance(&env, 15); - client.activate_proposal(&id); - advance(&env, 205); - client.finalize_proposal(&id); - - assert_eq!(client.get_proposal(&id).status, ProposalStatus::Failed); -} - -// ── Voting ──────────────────────────────────────────────────────────────────── - -#[test] -fn test_cast_vote_records_voting_power() { - let (env, client, _admin) = setup(); - let proposer = funded_proposer(&env, &client, 200); - let voter = funded_proposer(&env, &client, 300); - let id = create_proposal(&env, &client, &proposer, ProposalType::ParameterChange); - - advance(&env, 15); - client.activate_proposal(&id); - client.cast_vote(&voter, &id, &VoteChoice::Against); - - let record = client.get_vote(&id, &voter).unwrap(); - assert_eq!(record.voting_power, 300); - - let proposal = client.get_proposal(&id); - assert_eq!(proposal.votes_against, 300); - assert_eq!(proposal.votes_for, 0); -} - -#[test] -fn test_abstain_vote_counts_toward_quorum() { - let (env, client, _admin) = setup(); - let proposer = funded_proposer(&env, &client, 500); - let abstainer = funded_proposer(&env, &client, 500); - let id = create_proposal(&env, &client, &proposer, ProposalType::ParameterChange); - - advance(&env, 15); - client.activate_proposal(&id); - client.cast_vote(&abstainer, &id, &VoteChoice::Abstain); - - let proposal = client.get_proposal(&id); - assert_eq!(proposal.votes_abstain, 500); -} - -#[test] -#[should_panic] -fn test_double_vote_panics() { - let (env, client, _admin) = setup(); - let proposer = funded_proposer(&env, &client, 200); - let voter = funded_proposer(&env, &client, 100); - let id = create_proposal(&env, &client, &proposer, ProposalType::ParameterChange); - - advance(&env, 15); - client.activate_proposal(&id); - client.cast_vote(&voter, &id, &VoteChoice::For); - client.cast_vote(&voter, &id, &VoteChoice::Against); // should panic -} - -// ── Cancellation ────────────────────────────────────────────────────────────── - -#[test] -fn test_proposer_can_cancel() { - let (env, client, _admin) = setup(); - let proposer = funded_proposer(&env, &client, 200); - let id = create_proposal(&env, &client, &proposer, ProposalType::ParameterChange); - - client.cancel_proposal(&proposer, &id); - assert_eq!(client.get_proposal(&id).status, ProposalStatus::Cancelled); -} - -// ── Guardian multi-sig emergency execution ──────────────────────────────────── - -#[test] -fn test_guardian_emergency_execute() { - let (env, client, _admin) = setup(); - let guardian = Address::generate(&env); - client.add_guardian(&guardian); - assert!(client.is_guardian(&guardian)); - - let proposer = funded_proposer(&env, &client, 500); - let id = create_proposal(&env, &client, &proposer, ProposalType::EmergencyPause); - - advance(&env, 15); - client.activate_proposal(&id); - - // guardian_threshold = 1, so one approval is enough - client.guardian_approve(&guardian, &id); - assert_eq!(client.get_guardian_approvals(&id), 1); - - client.guardian_execute(&guardian, &id); - assert_eq!(client.get_proposal(&id).status, ProposalStatus::Executed); -} - -#[test] -#[should_panic] -fn test_non_guardian_cannot_approve() { - let (env, client, _admin) = setup(); - let non_guardian = Address::generate(&env); - let proposer = funded_proposer(&env, &client, 200); - let id = create_proposal(&env, &client, &proposer, ProposalType::EmergencyPause); - - advance(&env, 15); - client.activate_proposal(&id); - client.guardian_approve(&non_guardian, &id); // should panic: "not a guardian" -} - -// ── Vote delegation ─────────────────────────────────────────────────────────── - -#[test] -fn test_delegation_transfers_effective_power() { - let (env, client, _admin) = setup(); - let delegator = Address::generate(&env); - let delegatee = Address::generate(&env); - - client.set_voting_power(&delegator, &400); - - assert_eq!(client.get_voting_power(&delegator), 400); - assert_eq!(client.get_voting_power(&delegatee), 0); - - client.delegate_votes(&delegator, &delegatee); - assert_eq!(client.get_voting_power(&delegator), 0); - assert_eq!(client.get_voting_power(&delegatee), 400); - - let delegation = client.get_delegation(&delegator); - assert!(delegation.is_some()); -} - -#[test] -fn test_undelegation_restores_power() { - let (env, client, _admin) = setup(); - let delegator = Address::generate(&env); - let delegatee = Address::generate(&env); - - client.set_voting_power(&delegator, &400); - client.delegate_votes(&delegator, &delegatee); - client.undelegate_votes(&delegator); - - assert_eq!(client.get_voting_power(&delegator), 400); - assert_eq!(client.get_voting_power(&delegatee), 0); -} diff --git a/soroban/tests/insurance_pool.test.rs b/soroban/tests/insurance_pool.test.rs deleted file mode 100644 index 4976ef0..0000000 --- a/soroban/tests/insurance_pool.test.rs +++ /dev/null @@ -1,278 +0,0 @@ -#![cfg(test)] - -use soroban_sdk::{ - testutils::{Address as _, Ledger}, - Address, Env, String, Vec, -}; - -use bridge_watch_soroban::insurance_pool::{ - ClaimStatus, CoverageTier, InsurancePoolContract, InsurancePoolContractClient, -}; - -// ── Helpers ─────────────────────────────────────────────────────────────────── - -/// Returns (env, client, admin, approver, staker, buyer, pool_id). -/// Governance threshold = 2 (admin + approver must both sign off on claims). -fn setup() -> ( - Env, - InsurancePoolContractClient<'static>, - Address, - Address, - Address, - Address, - String, -) { - let env = Env::default(); - env.mock_all_auths(); - env.ledger().with_mut(|li| li.timestamp = 1_000_000); - - let contract_id = env.register_contract(None, InsurancePoolContract); - let client = InsurancePoolContractClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - let approver = Address::generate(&env); - let staker = Address::generate(&env); - let buyer = Address::generate(&env); - let pool_id = String::from_str(&env, "USDC_POOL"); - - client.initialize(&admin); - - let mut approvers = Vec::new(&env); - approvers.push_back(admin.clone()); - approvers.push_back(approver.clone()); - // threshold = 2, withdrawal_delay = 120 s - client.configure_governance(&admin, &approvers, &2u32, &120u64); - client.create_pool( - &admin, - &pool_id, - &String::from_str(&env, "USDC"), - &500u32, // premium_rate_bps - &1_000u32, // risk_score_bps - ); - - (env, client, admin, approver, staker, buyer, pool_id) -} - -fn hash(env: &Env, s: &str) -> String { - String::from_str(env, s) -} - -// ── Deposit / stake ─────────────────────────────────────────────────────────── - -#[test] -fn test_stake_liquidity_updates_pool_and_position() { - let (_env, client, _admin, _approver, staker, _buyer, pool_id) = setup(); - - client.stake_liquidity(&staker, &pool_id, &10_000); - - let pool = client.get_pool(&pool_id).unwrap(); - assert_eq!(pool.staked_liquidity, 10_000); - assert_eq!(pool.total_liquidity, 10_000); - - let pos = client.get_staker_position(&staker, &pool_id).unwrap(); - assert_eq!(pos.staked_amount, 10_000); -} - -// ── Coverage purchase ───────────────────────────────────────────────────────── - -#[test] -fn test_purchase_coverage_charges_exact_quoted_premium() { - let (env, client, _admin, _approver, staker, buyer, pool_id) = setup(); - - client.stake_liquidity(&staker, &pool_id, &20_000); - - let quoted = client.quote_premium(&pool_id, &5_000, &CoverageTier::Balanced); - assert!(quoted > 0); - - let charged = client.purchase_coverage( - &buyer, - &pool_id, - &5_000, - &CoverageTier::Balanced, - &(quoted + 10), // max_premium slightly above - ); - assert_eq!(charged, quoted); -} - -#[test] -fn test_coverage_tier_affects_premium() { - let (env, client, _admin, _approver, staker, _buyer, pool_id) = setup(); - - client.stake_liquidity(&staker, &pool_id, &30_000); - let conservative = client.quote_premium(&pool_id, &5_000, &CoverageTier::Conservative); - let balanced = client.quote_premium(&pool_id, &5_000, &CoverageTier::Balanced); - let aggressive = client.quote_premium(&pool_id, &5_000, &CoverageTier::Aggressive); - - // Aggressive multiplier (1400 bps) > Balanced (1000) > Conservative (800) - assert!(aggressive > balanced); - assert!(balanced > conservative); -} - -// ── Claim lifecycle: submit → verify → approve (2-of-2) → payout ───────────── - -#[test] -fn test_full_claim_lifecycle_approved_and_paid() { - let (env, client, admin, approver, staker, buyer, pool_id) = setup(); - - client.stake_liquidity(&staker, &pool_id, &20_000); - let quoted = client.quote_premium(&pool_id, &6_000, &CoverageTier::Balanced); - client.purchase_coverage(&buyer, &pool_id, &6_000, &CoverageTier::Balanced, &(quoted + 10)); - - let claim_id = client.submit_claim( - &buyer, - &pool_id, - &3_000, - &hash(&env, "QmEvidence"), - ); - - // Verify as genuine (0 slash bps) - client.verify_claim(&admin, &claim_id, &true, &0u32); - - // First approval — threshold not reached yet - client.approve_claim(&admin, &claim_id); - assert_eq!(client.get_claim(&claim_id).unwrap().status, ClaimStatus::Verified); - - // Second approval — threshold reached → Approved - client.approve_claim(&approver, &claim_id); - assert_eq!(client.get_claim(&claim_id).unwrap().status, ClaimStatus::Approved); - - // Execute payout - client.execute_payout(&admin, &claim_id); - let pool = client.get_pool(&pool_id).unwrap(); - assert_eq!(pool.paid_claims, 1); - assert_eq!(pool.payout_total, 3_000); -} - -// ── Fraudulent claim ────────────────────────────────────────────────────────── - -#[test] -fn test_fraudulent_claim_is_rejected_with_slashing() { - let (env, client, admin, _approver, staker, _buyer, pool_id) = setup(); - let buyer = staker.clone(); // staker is also buyer so slash reduces their position - - client.stake_liquidity(&staker, &pool_id, &8_000); - let quoted = client.quote_premium(&pool_id, &3_000, &CoverageTier::Balanced); - client.purchase_coverage(&buyer, &pool_id, &3_000, &CoverageTier::Balanced, "ed); - - let claim_id = client.submit_claim(&buyer, &pool_id, &2_000, &hash(&env, "QmFraud")); - - // Verify as fraudulent with 2500 bps slash - client.verify_claim(&admin, &claim_id, &false, &2_500u32); - - let claim = client.get_claim(&claim_id).unwrap(); - assert_eq!(claim.status, ClaimStatus::Rejected); - assert!(claim.slashed_amount > 0); - - let pool = client.get_pool(&pool_id).unwrap(); - assert_eq!(pool.rejected_claims, 1); -} - -// ── Withdrawal queue ────────────────────────────────────────────────────────── - -#[test] -fn test_withdrawal_succeeds_after_delay() { - let (env, client, _admin, _approver, staker, _buyer, pool_id) = setup(); - - client.stake_liquidity(&staker, &pool_id, &10_000); - let req_id = client.request_withdrawal(&staker, &pool_id, &3_000); - - // Move past 120-second delay - env.ledger().with_mut(|li| li.timestamp += 130); - let withdrawn = client.execute_withdrawal(&staker, &pool_id, &req_id); - assert_eq!(withdrawn, 3_000); - - let pos = client.get_staker_position(&staker, &pool_id).unwrap(); - assert_eq!(pos.staked_amount, 7_000); -} - -#[test] -#[should_panic] -fn test_withdrawal_before_delay_panics() { - let (env, client, _admin, _approver, staker, _buyer, pool_id) = setup(); - - client.stake_liquidity(&staker, &pool_id, &5_000); - let req_id = client.request_withdrawal(&staker, &pool_id, &1_000); - - // Only 50 s elapsed — before the 120-second unlock - env.ledger().with_mut(|li| li.timestamp += 50); - client.execute_withdrawal(&staker, &pool_id, &req_id); -} - -// ── Authorization panics ────────────────────────────────────────────────────── - -#[test] -#[should_panic] -fn test_non_approver_cannot_approve_claim() { - let (env, client, admin, _approver, staker, buyer, pool_id) = setup(); - let rogue = Address::generate(&env); - - client.stake_liquidity(&staker, &pool_id, &10_000); - let quoted = client.quote_premium(&pool_id, &2_000, &CoverageTier::Balanced); - client.purchase_coverage(&buyer, &pool_id, &2_000, &CoverageTier::Balanced, "ed); - - let claim_id = client.submit_claim(&buyer, &pool_id, &1_000, &hash(&env, "QmHash")); - client.verify_claim(&admin, &claim_id, &true, &0u32); - client.approve_claim(&rogue, &claim_id); // should panic: "not an approver" -} - -#[test] -#[should_panic] -fn test_duplicate_approval_panics() { - let (env, client, admin, _approver, staker, buyer, pool_id) = setup(); - - client.stake_liquidity(&staker, &pool_id, &10_000); - let quoted = client.quote_premium(&pool_id, &1_500, &CoverageTier::Balanced); - client.purchase_coverage(&buyer, &pool_id, &1_500, &CoverageTier::Balanced, "ed); - - let claim_id = client.submit_claim(&buyer, &pool_id, &500, &hash(&env, "QmDuplicate")); - client.verify_claim(&admin, &claim_id, &true, &0u32); - client.approve_claim(&admin, &claim_id); - client.approve_claim(&admin, &claim_id); // should panic: "already approved" -} - -#[test] -#[should_panic] -fn test_coverage_cap_exceeded_panics() { - let (env, client, _admin, _approver, staker, buyer, pool_id) = setup(); - - client.stake_liquidity(&staker, &pool_id, &10_000); - // Conservative cap = 50 % of pool = 5 000; requesting 9 000 exceeds it - let quoted = client.quote_premium(&pool_id, &9_000, &CoverageTier::Conservative); - client.purchase_coverage(&buyer, &pool_id, &9_000, &CoverageTier::Conservative, "ed); -} - -// ── Risk score ──────────────────────────────────────────────────────────────── - -#[test] -fn test_higher_risk_score_increases_premium() { - let (env, client, admin, _approver, staker, _buyer, pool_id) = setup(); - - client.stake_liquidity(&staker, &pool_id, &10_000); - let low_risk = client.quote_premium(&pool_id, &2_000, &CoverageTier::Balanced); - - client.set_risk_score(&admin, &pool_id, &5_000u32); - let high_risk = client.quote_premium(&pool_id, &2_000, &CoverageTier::Balanced); - - assert!(high_risk > low_risk); -} - -// ── Premium accumulation ────────────────────────────────────────────────────── - -#[test] -fn test_staker_earns_premium_after_coverage_purchased() { - let (env, client, admin, approver, staker, buyer, pool_id) = setup(); - - client.stake_liquidity(&staker, &pool_id, &20_000); - let quoted = client.quote_premium(&pool_id, &5_000, &CoverageTier::Balanced); - client.purchase_coverage(&buyer, &pool_id, &5_000, &CoverageTier::Balanced, &(quoted + 1)); - - // Complete a small claim to settle the pool and distribute premiums - let claim_id = client.submit_claim(&buyer, &pool_id, &500, &hash(&env, "QmPremium")); - client.verify_claim(&admin, &claim_id, &true, &0u32); - client.approve_claim(&admin, &claim_id); - client.approve_claim(&approver, &claim_id); - client.execute_payout(&admin, &claim_id); - - let earned = client.claim_premium(&staker, &pool_id); - assert!(earned > 0); -} diff --git a/soroban/tests/operator_rotation.test.rs b/soroban/tests/operator_rotation.test.rs index d09a74a..053ba5c 100644 --- a/soroban/tests/operator_rotation.test.rs +++ b/soroban/tests/operator_rotation.test.rs @@ -5,7 +5,7 @@ use soroban_sdk::{ testutils::{Address as _, Events as _, Ledger}, Address, Env, String, }; -use bridge_watch_contracts::operator_rotation::{ +use swipely_contracts::operator_rotation::{ add_operator, get_active_operators, get_all_operators, get_operator, is_operator, remove_operator, }; diff --git a/soroban/tests/source_trust.test.rs b/soroban/tests/source_trust.test.rs index 77db858..301b5b5 100644 --- a/soroban/tests/source_trust.test.rs +++ b/soroban/tests/source_trust.test.rs @@ -1,9 +1,9 @@ #![cfg(test)] -use soroban_sdk::{testutils::Address as _, Address, Env, String}; +use soroban_sdk::{testutils::{Address as _, Ledger}, Address, Env, String}; // Import the contract and client -use bridge_watch_soroban::{BridgeWatchContract, BridgeWatchContractClient}; +use swipely_contracts::{BridgeWatchContract, BridgeWatchContractClient}; fn setup() -> ( Env, @@ -14,6 +14,7 @@ fn setup() -> ( ) { let env = Env::default(); env.mock_all_auths(); + env.ledger().set_timestamp(1_000_000); let contract_id = env.register_contract(None, BridgeWatchContract); let client = BridgeWatchContractClient::new(&env, &contract_id); @@ -270,30 +271,30 @@ fn test_submit_health_requires_trusted_source_when_sources_registered() { client.grant_role( &admin, &trusted_source, - bridge_watch_soroban::AdminRole::HealthSubmitter, + &swipely_contracts::AdminRole::HealthSubmitter, ); client.grant_role( &admin, &untrusted_source, - bridge_watch_soroban::AdminRole::HealthSubmitter, + &swipely_contracts::AdminRole::HealthSubmitter, ); // Before registering any trusted sources, both should be able to submit client.submit_health( &trusted_source, &String::from_str(&env, "USDC"), - 95, - 90, - 92, - 88, + &95, + &90, + &92, + &88, ); client.submit_health( &untrusted_source, &String::from_str(&env, "USDC"), - 94, - 89, - 91, - 87, + &94, + &89, + &91, + &87, ); // Now register the trusted source @@ -307,10 +308,10 @@ fn test_submit_health_requires_trusted_source_when_sources_registered() { client.submit_health( &trusted_source, &String::from_str(&env, "USDC"), - 96, - 91, - 93, - 89, + &96, + &91, + &93, + &89, ); // Untrusted source should now fail (would panic with "caller is not a trusted source") @@ -328,25 +329,25 @@ fn test_submit_price_requires_trusted_source_when_sources_registered() { client.grant_role( &admin, &trusted_source, - bridge_watch_soroban::AdminRole::PriceSubmitter, + &swipely_contracts::AdminRole::PriceSubmitter, ); client.grant_role( &admin, &untrusted_source, - bridge_watch_soroban::AdminRole::PriceSubmitter, + &swipely_contracts::AdminRole::PriceSubmitter, ); // Before registering any trusted sources, both should be able to submit client.submit_price( &trusted_source, &String::from_str(&env, "USDC"), - 1_000_000, + &1_000_000, &String::from_str(&env, "oracle1"), ); client.submit_price( &untrusted_source, &String::from_str(&env, "USDC"), - 1_000_100, + &1_000_100, &String::from_str(&env, "oracle2"), ); @@ -361,7 +362,7 @@ fn test_submit_price_requires_trusted_source_when_sources_registered() { client.submit_price( &trusted_source, &String::from_str(&env, "USDC"), - 1_000_200, + &1_000_200, &String::from_str(&env, "oracle1"), ); @@ -381,11 +382,11 @@ fn test_revoked_source_cannot_submit() { client.grant_role( &admin, &source, - bridge_watch_soroban::AdminRole::HealthSubmitter, + &swipely_contracts::AdminRole::HealthSubmitter, ); // Should be able to submit - client.submit_health(&source, &String::from_str(&env, "USDC"), 95, 90, 92, 88); + client.submit_health(&source, &String::from_str(&env, "USDC"), &95, &90, &92, &88); // Revoke the source client.revoke_trusted_source(&admin, &source); @@ -406,11 +407,11 @@ fn test_reactivated_source_can_submit_again() { client.grant_role( &admin, &source, - bridge_watch_soroban::AdminRole::HealthSubmitter, + &swipely_contracts::AdminRole::HealthSubmitter, ); // Submit successfully - client.submit_health(&source, &String::from_str(&env, "USDC"), 95, 90, 92, 88); + client.submit_health(&source, &String::from_str(&env, "USDC"), &95, &90, &92, &88); // Revoke client.revoke_trusted_source(&admin, &source); @@ -419,7 +420,7 @@ fn test_reactivated_source_can_submit_again() { client.register_trusted_source(&admin, &source, &String::from_str(&env, "Oracle v2")); // Should be able to submit again - client.submit_health(&source, &String::from_str(&env, "USDC"), 96, 91, 93, 89); + client.submit_health(&source, &String::from_str(&env, "USDC"), &96, &91, &93, &89); } #[test] @@ -437,17 +438,17 @@ fn test_multiple_trusted_sources_can_all_submit() { client.grant_role( &admin, &source1, - bridge_watch_soroban::AdminRole::HealthSubmitter, + &swipely_contracts::AdminRole::HealthSubmitter, ); client.grant_role( &admin, &source2, - bridge_watch_soroban::AdminRole::HealthSubmitter, + &swipely_contracts::AdminRole::HealthSubmitter, ); // Both should be able to submit - client.submit_health(&source1, &String::from_str(&env, "USDC"), 95, 90, 92, 88); - client.submit_health(&source2, &String::from_str(&env, "USDC"), 94, 89, 91, 87); + client.submit_health(&source1, &String::from_str(&env, "USDC"), &95, &90, &92, &88); + client.submit_health(&source2, &String::from_str(&env, "USDC"), &94, &89, &91, &87); // Verify both submissions worked let health = client.get_health(&String::from_str(&env, "USDC")); @@ -466,12 +467,12 @@ fn test_admin_can_always_submit_regardless_of_trust() { client.grant_role( &admin, &source, - bridge_watch_soroban::AdminRole::HealthSubmitter, + &swipely_contracts::AdminRole::HealthSubmitter, ); // Admin should still be able to submit even without being a registered trusted source // (because admin has inherent permissions) - client.submit_health(&admin, &String::from_str(&env, "USDC"), 95, 90, 92, 88); + client.submit_health(&admin, &String::from_str(&env, "USDC"), &95, &90, &92, &88); // Verify submission worked let health = client.get_health(&String::from_str(&env, "USDC")); diff --git a/soroban/tests/threshold_window.test.rs b/soroban/tests/threshold_window.test.rs index 90425de..6350993 100644 --- a/soroban/tests/threshold_window.test.rs +++ b/soroban/tests/threshold_window.test.rs @@ -5,7 +5,7 @@ use soroban_sdk::{ testutils::{Address as _, Ledger}, Address, Env, String, }; -use bridge_watch_contracts::threshold_window::{ +use swipely_contracts::threshold_window::{ create_window, evaluate_threshold, get_all_windows, get_window, get_window_seconds, remove_window, update_window, WindowConfig, WindowUnit, }; diff --git a/soroban/tests/version_migration_tests.rs b/soroban/tests/version_migration_tests.rs index 9468514..40853bd 100644 --- a/soroban/tests/version_migration_tests.rs +++ b/soroban/tests/version_migration_tests.rs @@ -6,40 +6,56 @@ #[cfg(test)] mod tests { use soroban_sdk::{ + contract, contractimpl, testutils::{Address as _, Ledger}, Address, Env, Map, String as SorobanString, Vec, }; - use crate::version_migration_helper::{ - EnhancedMigrationHelper, MigrationVersion, ValidationCheckpoint, MigrationError, + use swipely_contracts::version_migration_helper::{ + EnhancedMigrationHelper, MigrationError, MigrationVersion, ValidationCheckpoint, }; - fn setup_env() -> (Env, Address) { + // EnhancedMigrationHelper's functions touch env.storage(), which + // soroban-sdk only allows from within an active contract call frame, and + // each require_auth() call consumes that frame's single-use + // authorization for the given address — so every storage- or + // auth-touching call below gets its own env.as_contract() block. + #[contract] + struct TestContext; + + #[contractimpl] + impl TestContext {} + + fn setup_env() -> (Env, Address, Address) { let env = Env::default(); + env.mock_all_auths(); let admin = Address::generate(&env); + let contract_id = env.register_contract(None, TestContext); env.ledger().set_timestamp(1_000_000); - (env, admin) + (env, admin, contract_id) } #[test] fn test_initialize_migration() { - let (env, admin) = setup_env(); + let (env, admin, contract_id) = setup_env(); let initial_version = MigrationVersion { major: 1, minor: 0, patch: 0, }; - let result = EnhancedMigrationHelper::initialize(&env, admin, initial_version); + let result = env.as_contract(&contract_id, || { + EnhancedMigrationHelper::initialize(&env, admin, initial_version) + }); assert!(result.is_ok(), "Migration initialization should succeed"); } #[test] fn test_get_version_default() { - let (env, _) = setup_env(); + let (env, _, contract_id) = setup_env(); - let version = EnhancedMigrationHelper::get_version(&env); + let version = env.as_contract(&contract_id, || EnhancedMigrationHelper::get_version(&env)); assert_eq!(version.major, 0); assert_eq!(version.minor, 0); @@ -105,17 +121,13 @@ mod tests { #[test] fn test_create_state_snapshot() { - let (env, admin) = setup_env(); + let (env, admin, contract_id) = setup_env(); - let data = Map::new(&env); - let description = SorobanString::from_str(&env, "Test snapshot"); - - let result = EnhancedMigrationHelper::create_state_snapshot( - &env, - admin, - description, - data, - ); + let result = env.as_contract(&contract_id, || { + let data = Map::new(&env); + let description = SorobanString::from_str(&env, "Test snapshot"); + EnhancedMigrationHelper::create_state_snapshot(&env, admin, description, data) + }); assert!(result.is_ok(), "State snapshot creation should succeed"); @@ -129,7 +141,7 @@ mod tests { #[test] fn test_begin_migration() { - let (env, admin) = setup_env(); + let (env, admin, contract_id) = setup_env(); let initial_version = MigrationVersion { major: 1, @@ -137,8 +149,10 @@ mod tests { patch: 0, }; - EnhancedMigrationHelper::initialize(&env, admin.clone(), initial_version) - .expect("Initialize should succeed"); + env.as_contract(&contract_id, || { + EnhancedMigrationHelper::initialize(&env, admin.clone(), initial_version) + }) + .expect("Initialize should succeed"); let target_version = MigrationVersion { major: 1, @@ -146,14 +160,16 @@ mod tests { patch: 0, }; - let result = EnhancedMigrationHelper::begin_migration(&env, admin, target_version); + let result = env.as_contract(&contract_id, || { + EnhancedMigrationHelper::begin_migration(&env, admin, target_version) + }); assert!(result.is_ok(), "Begin migration should succeed"); } #[test] fn test_begin_migration_unauthorized() { - let (env, admin) = setup_env(); + let (env, admin, contract_id) = setup_env(); let unauthorized = Address::generate(&env); let initial_version = MigrationVersion { @@ -162,8 +178,10 @@ mod tests { patch: 0, }; - EnhancedMigrationHelper::initialize(&env, admin, initial_version) - .expect("Initialize should succeed"); + env.as_contract(&contract_id, || { + EnhancedMigrationHelper::initialize(&env, admin, initial_version) + }) + .expect("Initialize should succeed"); let target_version = MigrationVersion { major: 1, @@ -171,7 +189,9 @@ mod tests { patch: 0, }; - let result = EnhancedMigrationHelper::begin_migration(&env, unauthorized, target_version); + let result = env.as_contract(&contract_id, || { + EnhancedMigrationHelper::begin_migration(&env, unauthorized, target_version) + }); assert_eq!( result, @@ -182,7 +202,7 @@ mod tests { #[test] fn test_complete_migration() { - let (env, admin) = setup_env(); + let (env, admin, contract_id) = setup_env(); let initial_version = MigrationVersion { major: 1, @@ -190,8 +210,10 @@ mod tests { patch: 0, }; - EnhancedMigrationHelper::initialize(&env, admin.clone(), initial_version.clone()) - .expect("Initialize should succeed"); + env.as_contract(&contract_id, || { + EnhancedMigrationHelper::initialize(&env, admin.clone(), initial_version.clone()) + }) + .expect("Initialize should succeed"); let target_version = MigrationVersion { major: 1, @@ -199,22 +221,27 @@ mod tests { patch: 0, }; - EnhancedMigrationHelper::begin_migration(&env, admin.clone(), target_version.clone()) - .expect("Begin migration should succeed"); - - let notes = SorobanString::from_str(&env, "Migration notes"); - let result = EnhancedMigrationHelper::complete_migration( - &env, - initial_version, - target_version.clone(), - admin, - notes, - ); + env.as_contract(&contract_id, || { + EnhancedMigrationHelper::begin_migration(&env, admin.clone(), target_version.clone()) + }) + .expect("Begin migration should succeed"); + + let result = env.as_contract(&contract_id, || { + let notes = SorobanString::from_str(&env, "Migration notes"); + EnhancedMigrationHelper::complete_migration( + &env, + initial_version, + target_version, + admin, + notes, + ) + }); assert!(result.is_ok(), "Complete migration should succeed"); // Verify version was updated - let current_version = EnhancedMigrationHelper::get_version(&env); + let current_version = + env.as_contract(&contract_id, || EnhancedMigrationHelper::get_version(&env)); assert_eq!(current_version.major, 1); assert_eq!(current_version.minor, 1); assert_eq!(current_version.patch, 0); @@ -222,36 +249,37 @@ mod tests { #[test] fn test_validate_state() { - let (env, admin) = setup_env(); - - let errors = Vec::new(&env); - let warnings = Vec::new(&env); - - let result = EnhancedMigrationHelper::validate_state( - &env, - ValidationCheckpoint::PostMigration, - errors, - warnings, - ); + let (env, _, contract_id) = setup_env(); + + let result = env.as_contract(&contract_id, || { + let errors = Vec::new(&env); + let warnings = Vec::new(&env); + EnhancedMigrationHelper::validate_state( + &env, + ValidationCheckpoint::PostMigration, + errors, + warnings, + ) + }); assert!(result.is_ok(), "State validation should succeed"); } #[test] fn test_validate_state_with_errors() { - let (env, _) = setup_env(); - - let mut errors = Vec::new(&env); - errors.push_back(SorobanString::from_str(&env, "Test error")); - - let warnings = Vec::new(&env); - - let result = EnhancedMigrationHelper::validate_state( - &env, - ValidationCheckpoint::PreMigration, - errors, - warnings, - ); + let (env, _, contract_id) = setup_env(); + + let result = env.as_contract(&contract_id, || { + let mut errors = Vec::new(&env); + errors.push_back(SorobanString::from_str(&env, "Test error")); + let warnings = Vec::new(&env); + EnhancedMigrationHelper::validate_state( + &env, + ValidationCheckpoint::PreMigration, + errors, + warnings, + ) + }); assert_eq!( result, @@ -262,7 +290,7 @@ mod tests { #[test] fn test_get_history() { - let (env, admin) = setup_env(); + let (env, admin, contract_id) = setup_env(); let initial_version = MigrationVersion { major: 1, @@ -270,26 +298,29 @@ mod tests { patch: 0, }; - EnhancedMigrationHelper::initialize(&env, admin.clone(), initial_version.clone()) - .expect("Initialize should succeed"); + env.as_contract(&contract_id, || { + EnhancedMigrationHelper::initialize(&env, admin, initial_version) + }) + .expect("Initialize should succeed"); - let history = EnhancedMigrationHelper::get_history(&env); + let history = env.as_contract(&contract_id, || EnhancedMigrationHelper::get_history(&env)); assert_eq!(history.len(), 0, "Initial history should be empty"); } #[test] fn test_get_snapshots() { - let (env, admin) = setup_env(); + let (env, _, contract_id) = setup_env(); - let snapshots = EnhancedMigrationHelper::get_snapshots(&env); + let snapshots = + env.as_contract(&contract_id, || EnhancedMigrationHelper::get_snapshots(&env)); assert_eq!(snapshots.len(), 0, "Initial snapshots should be empty"); } #[test] fn test_add_migrator() { - let (env, admin) = setup_env(); + let (env, admin, contract_id) = setup_env(); let new_migrator = Address::generate(&env); let initial_version = MigrationVersion { @@ -298,17 +329,21 @@ mod tests { patch: 0, }; - EnhancedMigrationHelper::initialize(&env, admin.clone(), initial_version) - .expect("Initialize should succeed"); + env.as_contract(&contract_id, || { + EnhancedMigrationHelper::initialize(&env, admin.clone(), initial_version) + }) + .expect("Initialize should succeed"); - let result = EnhancedMigrationHelper::add_migrator(&env, admin, new_migrator); + let result = env.as_contract(&contract_id, || { + EnhancedMigrationHelper::add_migrator(&env, admin, new_migrator) + }); assert!(result.is_ok(), "Adding migrator should succeed"); } #[test] fn test_remove_migrator() { - let (env, admin) = setup_env(); + let (env, admin, contract_id) = setup_env(); let new_migrator = Address::generate(&env); let initial_version = MigrationVersion { @@ -317,13 +352,19 @@ mod tests { patch: 0, }; - EnhancedMigrationHelper::initialize(&env, admin.clone(), initial_version) - .expect("Initialize should succeed"); + env.as_contract(&contract_id, || { + EnhancedMigrationHelper::initialize(&env, admin.clone(), initial_version) + }) + .expect("Initialize should succeed"); - EnhancedMigrationHelper::add_migrator(&env, admin.clone(), new_migrator.clone()) - .expect("Add migrator should succeed"); + env.as_contract(&contract_id, || { + EnhancedMigrationHelper::add_migrator(&env, admin.clone(), new_migrator.clone()) + }) + .expect("Add migrator should succeed"); - let result = EnhancedMigrationHelper::remove_migrator(&env, admin, new_migrator); + let result = env.as_contract(&contract_id, || { + EnhancedMigrationHelper::remove_migrator(&env, admin, new_migrator) + }); assert!(result.is_ok(), "Removing migrator should succeed"); } @@ -381,7 +422,7 @@ mod tests { #[test] fn test_migration_workflow() { - let (env, admin) = setup_env(); + let (env, admin, contract_id) = setup_env(); // Initialize let v1 = MigrationVersion { @@ -389,17 +430,21 @@ mod tests { minor: 0, patch: 0, }; - EnhancedMigrationHelper::initialize(&env, admin.clone(), v1.clone()) - .expect("Initialize should succeed"); + env.as_contract(&contract_id, || { + EnhancedMigrationHelper::initialize(&env, admin.clone(), v1.clone()) + }) + .expect("Initialize should succeed"); // Create snapshot - let data = Map::new(&env); - let snapshot_result = EnhancedMigrationHelper::create_state_snapshot( - &env, - admin.clone(), - SorobanString::from_str(&env, "Pre-migration snapshot"), - data, - ); + let snapshot_result = env.as_contract(&contract_id, || { + let data = Map::new(&env); + EnhancedMigrationHelper::create_state_snapshot( + &env, + admin.clone(), + SorobanString::from_str(&env, "Pre-migration snapshot"), + data, + ) + }); assert!(snapshot_result.is_ok()); // Begin migration @@ -408,32 +453,39 @@ mod tests { minor: 1, patch: 0, }; - let begin_result = EnhancedMigrationHelper::begin_migration(&env, admin.clone(), v2.clone()); + let begin_result = env.as_contract(&contract_id, || { + EnhancedMigrationHelper::begin_migration(&env, admin.clone(), v2.clone()) + }); assert!(begin_result.is_ok()); // Validate state - let errors = Vec::new(&env); - let warnings = Vec::new(&env); - let validate_result = EnhancedMigrationHelper::validate_state( - &env, - ValidationCheckpoint::PostMigration, - errors, - warnings, - ); + let validate_result = env.as_contract(&contract_id, || { + let errors = Vec::new(&env); + let warnings = Vec::new(&env); + EnhancedMigrationHelper::validate_state( + &env, + ValidationCheckpoint::PostMigration, + errors, + warnings, + ) + }); assert!(validate_result.is_ok()); // Complete migration - let complete_result = EnhancedMigrationHelper::complete_migration( - &env, - v1, - v2.clone(), - admin.clone(), - SorobanString::from_str(&env, "Successful migration"), - ); + let complete_result = env.as_contract(&contract_id, || { + EnhancedMigrationHelper::complete_migration( + &env, + v1, + v2, + admin, + SorobanString::from_str(&env, "Successful migration"), + ) + }); assert!(complete_result.is_ok()); // Verify final version - let final_version = EnhancedMigrationHelper::get_version(&env); + let final_version = + env.as_contract(&contract_id, || EnhancedMigrationHelper::get_version(&env)); assert_eq!(final_version.major, 1); assert_eq!(final_version.minor, 1); assert_eq!(final_version.patch, 0);