diff --git a/.gitignore b/.gitignore
index 47657e0..49ed1a2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,4 @@ Cargo.lock
/target/
/target_local/
.md-*
-hidden
\ No newline at end of file
+hidden
diff --git a/contracts/revenue_pool/src/lib.rs b/contracts/revenue_pool/src/lib.rs
index 26f4e57..76564df 100644
--- a/contracts/revenue_pool/src/lib.rs
+++ b/contracts/revenue_pool/src/lib.rs
@@ -20,7 +20,8 @@ impl RevenuePool {
/// * `usdc_token` – Stellar USDC (or wrapped USDC) token contract address.
pub fn init(env: Env, admin: Address, usdc_token: Address) {
admin.require_auth();
- if env.storage().instance().has(&Symbol::new(&env, ADMIN_KEY)) {
+ let inst = env.storage().instance();
+ if inst.has(&Symbol::new(&env, ADMIN_KEY)) {
panic!("revenue pool already initialized");
}
let inst = env.storage().instance();
@@ -36,7 +37,7 @@ impl RevenuePool {
env.storage()
.instance()
.get(&Symbol::new(&env, ADMIN_KEY))
- .unwrap_or_else(|| panic!("revenue pool not initialized"))
+ .expect("revenue pool not initialized")
}
/// Replace the current admin. Only the existing admin may call this.
@@ -156,7 +157,7 @@ impl RevenuePool {
.storage()
.instance()
.get(&Symbol::new(&env, USDC_KEY))
- .unwrap_or_else(|| panic!("revenue pool not initialized"));
+ .expect("revenue pool not initialized");
let usdc = token::Client::new(&env, &usdc_address);
usdc.balance(&env.current_contract_address())
}
diff --git a/contracts/revenue_pool/src/test.rs b/contracts/revenue_pool/src/test.rs
index 05d6d51..a3d9429 100644
--- a/contracts/revenue_pool/src/test.rs
+++ b/contracts/revenue_pool/src/test.rs
@@ -185,6 +185,48 @@ fn distribute_negative_panics() {
assert!(result.is_err());
}
+#[test]
+fn receive_payment_from_non_vault() {
+ let env = Env::default();
+ env.mock_all_auths();
+ let admin = Address::generate(&env);
+ let (_, client) = create_pool(&env);
+ let (usdc, _, _) = create_usdc(&env, &admin);
+
+ client.init(&admin, &usdc);
+ client.receive_payment(&admin, &250, &false);
+
+ let events = env.events().all();
+ assert!(!events.is_empty());
+
+ client.init(&admin, &usdc);
+ client.set_admin(&attacker, &new_admin);
+}
+
+#[test]
+#[should_panic(expected = "revenue pool not initialized")]
+fn balance_before_init_panics() {
+ let env = Env::default();
+ let (_, client) = create_pool(&env);
+ client.balance();
+}
+
+#[test]
+fn distribute_negative_panics() {
+ let env = Env::default();
+ env.mock_all_auths();
+ let admin = Address::generate(&env);
+ let developer = Address::generate(&env);
+ let (_, client) = create_pool(&env);
+ let (usdc, _, _) = create_usdc(&env, &admin);
+
+ client.init(&admin, &usdc);
+ let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+ client.distribute(&admin, &developer, &-1);
+ }));
+ assert!(result.is_err());
+}
+
#[test]
fn receive_payment_from_non_vault() {
let env = Env::default();
@@ -260,18 +302,43 @@ fn batch_distribute_success() {
assert_eq!(client.balance(), 500);
}
+/// Full lifecycle test: init, get_admin, balance, distribute, receive_payment, set_admin.
#[test]
-#[should_panic(expected = "unauthorized: caller is not admin")]
-fn batch_distribute_unauthorized_panics() {
+fn full_lifecycle() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
+ let new_admin = Address::generate(&env);
+ let developer = Address::generate(&env);
let attacker = Address::generate(&env);
let dev = Address::generate(&env);
let (pool_addr, client) = create_pool(&env);
- let (usdc_address, _, usdc_admin) = create_usdc(&env, &admin);
+ let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &admin);
+ // Init
client.init(&admin, &usdc_address);
+ assert_eq!(client.get_admin(), admin);
+
+ // Fund and check balance
+ fund_pool(&usdc_admin, &pool_addr, 1000);
+ assert_eq!(client.balance(), 1000);
+
+ // Distribute
+ client.distribute(&admin, &developer, &400);
+ assert_eq!(usdc_client.balance(&developer), 400);
+ assert_eq!(client.balance(), 600);
+
+ // Receive payment event
+ client.receive_payment(&admin, &100, &true);
+
+ // Set admin
+ client.set_admin(&admin, &new_admin);
+ assert_eq!(client.get_admin(), new_admin);
+
+ // New admin can distribute
+ client.distribute(&new_admin, &developer, &100);
+ assert_eq!(usdc_client.balance(&developer), 500);
+ assert_eq!(client.balance(), 500);
fund_pool(&usdc_admin, &pool_addr, 500);
let mut payments: Vec<(Address, i128)> = Vec::new(&env);
@@ -314,3 +381,76 @@ fn batch_distribute_insufficient_balance_panics() {
payments.push_back((dev.clone(), 100_i128));
client.batch_distribute(&admin, &payments);
}
+
+#[test]
+fn batch_distribute_success() {
+ let env = Env::default();
+ env.mock_all_auths();
+ let admin = Address::generate(&env);
+ let dev1 = Address::generate(&env);
+ let dev2 = Address::generate(&env);
+ let (pool_addr, client) = create_pool(&env);
+ let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &admin);
+
+ client.init(&admin, &usdc_address);
+ fund_pool(&usdc_admin, &pool_addr, 1_000);
+
+ let payments = soroban_sdk::vec![&env, (dev1.clone(), 300_i128), (dev2.clone(), 200_i128)];
+ client.batch_distribute(&admin, &payments);
+
+ assert_eq!(usdc_client.balance(&dev1), 300);
+ assert_eq!(usdc_client.balance(&dev2), 200);
+ assert_eq!(usdc_client.balance(&pool_addr), 500);
+}
+
+#[test]
+#[should_panic(expected = "unauthorized: caller is not admin")]
+fn batch_distribute_unauthorized_panics() {
+ let env = Env::default();
+ env.mock_all_auths();
+ let admin = Address::generate(&env);
+ let attacker = Address::generate(&env);
+ let dev = Address::generate(&env);
+ let (pool_addr, client) = create_pool(&env);
+ let (usdc_address, _, usdc_admin) = create_usdc(&env, &admin);
+
+ client.init(&admin, &usdc_address);
+ fund_pool(&usdc_admin, &pool_addr, 500);
+
+ let payments = soroban_sdk::vec![&env, (dev.clone(), 100_i128)];
+ client.batch_distribute(&attacker, &payments);
+}
+
+#[test]
+#[should_panic(expected = "amount must be positive")]
+fn batch_distribute_zero_amount_panics() {
+ let env = Env::default();
+ env.mock_all_auths();
+ let admin = Address::generate(&env);
+ let dev = Address::generate(&env);
+ let (pool_addr, client) = create_pool(&env);
+ let (usdc_address, _, usdc_admin) = create_usdc(&env, &admin);
+
+ client.init(&admin, &usdc_address);
+ fund_pool(&usdc_admin, &pool_addr, 500);
+
+ let payments = soroban_sdk::vec![&env, (dev.clone(), 0_i128)];
+ client.batch_distribute(&admin, &payments);
+}
+
+#[test]
+#[should_panic(expected = "insufficient USDC balance")]
+fn batch_distribute_insufficient_balance_panics() {
+ let env = Env::default();
+ env.mock_all_auths();
+ let admin = Address::generate(&env);
+ let dev = Address::generate(&env);
+ let (pool_addr, client) = create_pool(&env);
+ let (usdc_address, _, usdc_admin) = create_usdc(&env, &admin);
+
+ client.init(&admin, &usdc_address);
+ fund_pool(&usdc_admin, &pool_addr, 100);
+
+ let payments = soroban_sdk::vec![&env, (dev.clone(), 200_i128)];
+ client.batch_distribute(&admin, &payments);
+}
diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs
index 9b6b9c1..e23e912 100644
--- a/contracts/vault/src/test.rs
+++ b/contracts/vault/src/test.rs
@@ -1114,6 +1114,8 @@ fn withdraw_reduces_balance() {
#[test]
fn withdraw_insufficient_balance_fails() {
let env = Env::default();
+ env.mock_all_auths();
+
let owner = Address::generate(&env);
let contract_id = env.register(CalloraVault {}, ());
let client = CalloraVaultClient::new(&env, &contract_id);
@@ -1195,6 +1197,67 @@ fn withdraw_to_reduces_balance() {
#[test]
fn withdraw_to_insufficient_balance_fails() {
let env = Env::default();
+ let owner = Address::generate(&env);
+ let recipient = Address::generate(&env);
+ let contract_id = env.register(CalloraVault {}, ());
+ let client = CalloraVaultClient::new(&env, &contract_id);
+ let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner);
+
+ env.mock_all_auths();
+ fund_vault(&usdc_admin, &contract_id, 100);
+ client.init(&owner, &usdc_token, &Some(100), &None, &None, &None);
+
+ let result = client.try_withdraw_to(&recipient, &500);
+ assert!(result.is_err(), "expected error for insufficient balance");
+}
+
+#[test]
+fn deposit_below_minimum_fails() {
+ let env = Env::default();
+ let owner = Address::generate(&env);
+ let depositor = Address::generate(&env);
+ let contract_id = env.register(CalloraVault {}, ());
+ let client = CalloraVaultClient::new(&env, &contract_id);
+ let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner);
+
+ env.mock_all_auths();
+ fund_vault(&usdc_admin, &contract_id, 100);
+ client.init(&owner, &usdc_token, &Some(100), &Some(50), &None, &None);
+
+ fund_vault(&usdc_admin, &depositor, 30);
+ let usdc_client = token::Client::new(&env, &usdc_token);
+ usdc_client.approve(&depositor, &contract_id, &30, &1000);
+ let result = client.try_deposit(&depositor, &30);
+ assert!(result.is_err(), "expected error for deposit below minimum");
+}
+
+#[test]
+fn deposit_at_minimum_succeeds() {
+ let env = Env::default();
+ let owner = Address::generate(&env);
+ let depositor = Address::generate(&env);
+ let contract_id = env.register(CalloraVault {}, ());
+ let client = CalloraVaultClient::new(&env, &contract_id);
+ let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner);
+
+ env.mock_all_auths();
+ fund_vault(&usdc_admin, &contract_id, 100);
+ client.init(&owner, &usdc_token, &Some(100), &Some(50), &None, &None);
+
+ fund_vault(&usdc_admin, &depositor, 50);
+ let usdc_client = token::Client::new(&env, &usdc_token);
+ usdc_client.approve(&depositor, &contract_id, &50, &1000);
+ let new_balance = client.deposit(&depositor, &50);
+ assert_eq!(new_balance, 150);
+}
+
+#[test]
+fn double_init_fails() {
+ let env = Env::default();
+ let owner = Address::generate(&env);
+ let contract_id = env.register(CalloraVault {}, ());
+ let client = CalloraVaultClient::new(&env, &contract_id);
+ let (usdc_token, _, usdc_admin) = create_usdc(&env, &owner);
let owner = Address::generate(&env);
let new_owner = Address::generate(&env);
diff --git a/coverage/cobertura.xml b/coverage/cobertura.xml
index 3a7a6b4..7642a09 100644
--- a/coverage/cobertura.xml
+++ b/coverage/cobertura.xml
@@ -1 +1,2 @@
-