From 2be1af9eb45695397945740ffc998d8b5e5c7003 Mon Sep 17 00:00:00 2001 From: Anyia David Date: Sat, 22 Aug 2026 11:10:13 +0100 Subject: [PATCH 1/9] =?UTF-8?q?test:=20add=20test=5Fset=5Femergency=5Fstop?= =?UTF-8?q?=5Funauthorized=20=E2=80=94=20non-admin=20call=20is=20rejected?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/src/test.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/contracts/src/test.rs b/contracts/src/test.rs index 82c3e9d..c9fd250 100644 --- a/contracts/src/test.rs +++ b/contracts/src/test.rs @@ -487,6 +487,49 @@ fn test_concurrent_portfolio_creation() { assert_eq!(portfolio_b.user, user_b); } +#[test] +#[should_panic] +fn test_set_emergency_stop_unauthorized() { + let env = Env::default(); + // Do NOT use mock_all_auths — we want to control who is authorized + let contract_id = env.register_contract(None, PortfolioRebalancer); + let client = PortfolioRebalancerClient::new(&env, &contract_id); + let reflector_id = env.register_contract(None, reflector_contract::MockReflector); + let admin = Address::generate(&env); + let non_admin = Address::generate(&env); + + // Only authorize the admin for initialize + env.mock_auths(&[ + soroban_sdk::testutils::MockAuth { + address: &admin, + invoke: &soroban_sdk::testutils::MockAuthInvoke { + contract: &contract_id, + fn_name: "initialize", + args: (&admin, &reflector_id).into_val(&env), + sub_invokes: &[], + }, + }, + ]); + client.initialize(&admin, &reflector_id); + + // Now authorize only non_admin for the set_emergency_stop call. + // Since non_admin is NOT the admin, require_auth() on admin should fail. + env.mock_auths(&[ + soroban_sdk::testutils::MockAuth { + address: &non_admin, + invoke: &soroban_sdk::testutils::MockAuthInvoke { + contract: &contract_id, + fn_name: "set_emergency_stop", + args: (true,).into_val(&env), + sub_invokes: &[], + }, + }, + ]); + + // This should panic because admin.require_auth() fails when only non_admin is authorized + client.set_emergency_stop(&true); +} + #[test] fn test_same_user_two_portfolios() { let env = Env::default(); From 03146fe1b2072be919e6a4a0bd6ef6e7917d469e Mon Sep 17 00:00:00 2001 From: Anyia David Date: Sat, 22 Aug 2026 11:10:32 +0100 Subject: [PATCH 2/9] =?UTF-8?q?test:=20add=20test=5Fset=5Femergency=5Fstop?= =?UTF-8?q?=5Fblocks=5Fdeposit=20=E2=80=94=20deposit=20rejected=20when=20s?= =?UTF-8?q?top=20enabled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/src/test.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/contracts/src/test.rs b/contracts/src/test.rs index c9fd250..0a74395 100644 --- a/contracts/src/test.rs +++ b/contracts/src/test.rs @@ -487,6 +487,32 @@ fn test_concurrent_portfolio_creation() { assert_eq!(portfolio_b.user, user_b); } +#[test] +fn test_set_emergency_stop_blocks_deposit() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, PortfolioRebalancer); + let client = PortfolioRebalancerClient::new(&env, &contract_id); + let reflector_id = env.register_contract(None, reflector_contract::MockReflector); + let admin = Address::generate(&env); + let user = Address::generate(&env); + client.initialize(&admin, &reflector_id); + + // Create portfolio before enabling emergency stop + let mut allocations = Map::new(&env); + let asset = Address::generate(&env); + allocations.set(asset.clone(), 100); + let pid = client.create_portfolio(&user, &allocations, &5); + + // Admin enables emergency stop + client.set_emergency_stop(&true); + + // User tries to deposit — should be blocked + let result = client.try_deposit(&pid, &asset, &100); + assert_eq!(result, Err(Ok(Error::EmergencyStop))); +} + #[test] #[should_panic] fn test_set_emergency_stop_unauthorized() { From 1934658efb4d6bb563f452d7d6211979185a5a0e Mon Sep 17 00:00:00 2001 From: Anyia David Date: Sat, 22 Aug 2026 11:10:49 +0100 Subject: [PATCH 3/9] =?UTF-8?q?test:=20add=20test=5Fset=5Femergency=5Fstop?= =?UTF-8?q?=5Fblocks=5Frebalance=20=E2=80=94=20rebalance=20rejected=20when?= =?UTF-8?q?=20stop=20enabled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/src/test.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/contracts/src/test.rs b/contracts/src/test.rs index 0a74395..f58d2b6 100644 --- a/contracts/src/test.rs +++ b/contracts/src/test.rs @@ -487,6 +487,35 @@ fn test_concurrent_portfolio_creation() { assert_eq!(portfolio_b.user, user_b); } +#[test] +fn test_set_emergency_stop_blocks_rebalance() { + let env = Env::default(); + env.mock_all_auths(); + + env.ledger().with_mut(|li| { + li.timestamp = 10000; + }); + + let contract_id = env.register_contract(None, PortfolioRebalancer); + let client = PortfolioRebalancerClient::new(&env, &contract_id); + let reflector_id = env.register_contract(None, reflector_contract::MockReflector); + let admin = Address::generate(&env); + let user = Address::generate(&env); + client.initialize(&admin, &reflector_id); + + let mut allocations = Map::new(&env); + let asset = Address::generate(&env); + allocations.set(asset, 100); + let pid = client.create_portfolio(&user, &allocations, &5); + + // Admin enables emergency stop + client.set_emergency_stop(&true); + + // Try to rebalance — should be blocked + let result = client.try_execute_rebalance(&pid); + assert_eq!(result, Err(Ok(Error::EmergencyStop))); +} + #[test] fn test_set_emergency_stop_blocks_deposit() { let env = Env::default(); From 33cfb9f340071e087c856756dca603db1a076f7f Mon Sep 17 00:00:00 2001 From: Anyia David Date: Sat, 22 Aug 2026 11:11:03 +0100 Subject: [PATCH 4/9] =?UTF-8?q?test:=20add=20test=5Fget=5Fportfolio=5Fnot?= =?UTF-8?q?=5Ffound=20=E2=80=94=20non-existent=20ID=20panics=20on=20unwrap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/src/test.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/contracts/src/test.rs b/contracts/src/test.rs index f58d2b6..429e5c7 100644 --- a/contracts/src/test.rs +++ b/contracts/src/test.rs @@ -487,6 +487,22 @@ fn test_concurrent_portfolio_creation() { assert_eq!(portfolio_b.user, user_b); } +#[test] +#[should_panic] +fn test_get_portfolio_not_found() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, PortfolioRebalancer); + let client = PortfolioRebalancerClient::new(&env, &contract_id); + let reflector_id = env.register_contract(None, reflector_contract::MockReflector); + let admin = Address::generate(&env); + client.initialize(&admin, &reflector_id); + + // Query a portfolio ID that was never created — should panic (unwrap on None) + client.get_portfolio(&9999); +} + #[test] fn test_set_emergency_stop_blocks_rebalance() { let env = Env::default(); From e8b7dad23055d0afa614c132cd540be60f224d1f Mon Sep 17 00:00:00 2001 From: Anyia David Date: Sat, 22 Aug 2026 11:11:18 +0100 Subject: [PATCH 5/9] =?UTF-8?q?test:=20add=20test=5Fcreate=5Fportfolio=5Fe?= =?UTF-8?q?mpty=5Fallocations=20=E2=80=94=20empty=20map=20returns=20Invali?= =?UTF-8?q?dAllocation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/src/test.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/contracts/src/test.rs b/contracts/src/test.rs index 429e5c7..0be6013 100644 --- a/contracts/src/test.rs +++ b/contracts/src/test.rs @@ -487,6 +487,25 @@ fn test_concurrent_portfolio_creation() { assert_eq!(portfolio_b.user, user_b); } +#[test] +fn test_create_portfolio_empty_allocations() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, PortfolioRebalancer); + let client = PortfolioRebalancerClient::new(&env, &contract_id); + let reflector_id = env.register_contract(None, reflector_contract::MockReflector); + let admin = Address::generate(&env); + let user = Address::generate(&env); + client.initialize(&admin, &reflector_id); + + // Empty allocations map — sum is 0, not 100 + let empty_allocations = Map::new(&env); + + let result = client.try_create_portfolio(&user, &empty_allocations, &5); + assert_eq!(result, Err(Ok(Error::InvalidAllocation))); +} + #[test] #[should_panic] fn test_get_portfolio_not_found() { From 70c8ba6f24c116a66bacf428e7673c742f3c2ecf Mon Sep 17 00:00:00 2001 From: Anyia David Date: Sat, 22 Aug 2026 11:11:42 +0100 Subject: [PATCH 6/9] =?UTF-8?q?test:=20add=20test=5Fdeposit=5Fwrong=5Fport?= =?UTF-8?q?folio=20=E2=80=94=20non-owner=20deposit=20is=20rejected?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/src/test.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/contracts/src/test.rs b/contracts/src/test.rs index 0be6013..5b9703d 100644 --- a/contracts/src/test.rs +++ b/contracts/src/test.rs @@ -487,6 +487,43 @@ fn test_concurrent_portfolio_creation() { assert_eq!(portfolio_b.user, user_b); } +#[test] +#[should_panic] +fn test_deposit_wrong_portfolio() { + let env = Env::default(); + // Use mock_all_auths for setup (initialize + create_portfolio) + env.mock_all_auths(); + + let contract_id = env.register_contract(None, PortfolioRebalancer); + let client = PortfolioRebalancerClient::new(&env, &contract_id); + let reflector_id = env.register_contract(None, reflector_contract::MockReflector); + let admin = Address::generate(&env); + let owner = Address::generate(&env); + let intruder = Address::generate(&env); + client.initialize(&admin, &reflector_id); + + let mut allocations = Map::new(&env); + let asset = Address::generate(&env); + allocations.set(asset.clone(), 100); + let pid = client.create_portfolio(&owner, &allocations, &5); + + // Switch to targeted auth: only authorize intruder (not the portfolio owner) + env.mock_auths(&[ + soroban_sdk::testutils::MockAuth { + address: &intruder, + invoke: &soroban_sdk::testutils::MockAuthInvoke { + contract: &contract_id, + fn_name: "deposit", + args: (pid, asset.clone(), 100i128).into_val(&env), + sub_invokes: &[], + }, + }, + ]); + + // Intruder tries to deposit into owner's portfolio — should panic on require_auth + client.deposit(&pid, &asset, &100); +} + #[test] fn test_create_portfolio_empty_allocations() { let env = Env::default(); From 6effde1d8e9759cdd7816eb4c184efb4a3f37772 Mon Sep 17 00:00:00 2001 From: Anyia David Date: Sat, 22 Aug 2026 11:12:09 +0100 Subject: [PATCH 7/9] =?UTF-8?q?test:=20add=20test=5Fexecute=5Frebalance=5F?= =?UTF-8?q?not=5Fowner=20=E2=80=94=20non-owner=20rebalance=20is=20rejected?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/src/test.rs | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/contracts/src/test.rs b/contracts/src/test.rs index 5b9703d..d1a432d 100644 --- a/contracts/src/test.rs +++ b/contracts/src/test.rs @@ -487,6 +487,51 @@ fn test_concurrent_portfolio_creation() { assert_eq!(portfolio_b.user, user_b); } +#[test] +#[should_panic] +fn test_execute_rebalance_not_owner() { + let env = Env::default(); + env.mock_all_auths(); + + env.ledger().with_mut(|li| { + li.timestamp = 10000; + }); + + let contract_id = env.register_contract(None, PortfolioRebalancer); + let client = PortfolioRebalancerClient::new(&env, &contract_id); + let reflector_id = env.register_contract(None, reflector_contract::MockReflector); + let admin = Address::generate(&env); + let owner = Address::generate(&env); + let intruder = Address::generate(&env); + client.initialize(&admin, &reflector_id); + + let mut allocations = Map::new(&env); + let asset = Address::generate(&env); + allocations.set(asset, 100); + let pid = client.create_portfolio(&owner, &allocations, &5); + + // Advance past cooldown + env.ledger().with_mut(|li| { + li.timestamp = 20000; + }); + + // Switch to targeted auth: only authorize intruder (not the portfolio owner) + env.mock_auths(&[ + soroban_sdk::testutils::MockAuth { + address: &intruder, + invoke: &soroban_sdk::testutils::MockAuthInvoke { + contract: &contract_id, + fn_name: "execute_rebalance", + args: (pid,).into_val(&env), + sub_invokes: &[], + }, + }, + ]); + + // Intruder tries to rebalance owner's portfolio — should panic on require_auth + client.execute_rebalance(&pid); +} + #[test] #[should_panic] fn test_deposit_wrong_portfolio() { From ad32fe1ddb77dbaabe7da7bc30eb88b7bf206030 Mon Sep 17 00:00:00 2001 From: Anyia David Date: Sat, 22 Aug 2026 12:44:57 +0100 Subject: [PATCH 8/9] fix: add IntoVal import and fix Cargo.lock dependency conflict for ed25519-dalek --- contracts/src/test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/test.rs b/contracts/src/test.rs index d1a432d..6c1da28 100644 --- a/contracts/src/test.rs +++ b/contracts/src/test.rs @@ -1,7 +1,7 @@ #![cfg(test)] use super::*; -use soroban_sdk::{testutils::{Address as _, Ledger}, Address, Env, Map, Vec, Symbol, Val}; +use soroban_sdk::{testutils::{Address as _, Ledger}, Address, Env, IntoVal, Map, Vec, Symbol, Val}; // Mock Reflector Contract mod reflector_contract { From 90414fb68167cf7669d3a3af31ab526c7308339c Mon Sep 17 00:00:00 2001 From: Anyia David Date: Sat, 22 Aug 2026 15:18:25 +0100 Subject: [PATCH 9/9] test: use try_get_portfolio with error assertion instead of should_panic Addresses review feedback: #[should_panic] passes even if the contract returns an error instead of panicking (wrong failure mode). Using try_get_portfolio and asserting is_err() is more precise. --- contracts/src/test.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/src/test.rs b/contracts/src/test.rs index 6c1da28..aa64fc3 100644 --- a/contracts/src/test.rs +++ b/contracts/src/test.rs @@ -589,7 +589,6 @@ fn test_create_portfolio_empty_allocations() { } #[test] -#[should_panic] fn test_get_portfolio_not_found() { let env = Env::default(); env.mock_all_auths(); @@ -600,8 +599,9 @@ fn test_get_portfolio_not_found() { let admin = Address::generate(&env); client.initialize(&admin, &reflector_id); - // Query a portfolio ID that was never created — should panic (unwrap on None) - client.get_portfolio(&9999); + // Query a portfolio ID that was never created — try_ catches the panic as an error + let result = client.try_get_portfolio(&9999); + assert!(result.is_err()); } #[test]