From c91735e9fe06d299112b05d609c8a1199e9a2198 Mon Sep 17 00:00:00 2001 From: Brooks Student Portal Date: Sat, 27 Jun 2026 11:31:21 -0700 Subject: [PATCH] add admin config controls --- contracts/inheritance-contract/src/lib.rs | 144 +++++++++- contracts/inheritance-contract/src/test.rs | 310 ++++++++++++++++++++- 2 files changed, 452 insertions(+), 2 deletions(-) diff --git a/contracts/inheritance-contract/src/lib.rs b/contracts/inheritance-contract/src/lib.rs index 0760c51c2..29910af51 100644 --- a/contracts/inheritance-contract/src/lib.rs +++ b/contracts/inheritance-contract/src/lib.rs @@ -1,5 +1,7 @@ #![no_std] -use soroban_sdk::{contract, contracterror, contractimpl, contracttype, Address, Env, String, Vec}; +use soroban_sdk::{ + contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, String, Vec, +}; const MAX_BENEFICIARIES: u32 = 100; const PLAN_TTL_THRESHOLD: u32 = 500; @@ -18,6 +20,11 @@ pub enum Error { NegativeAmount = 6, InsufficientBalance = 7, TooManyBeneficiaries = 8, + AlreadyInitialized = 9, + AdminNotSet = 10, + InvalidYieldRate = 11, + TokenAlreadySupported = 12, + TokenNotSupported = 13, } #[contracttype] @@ -55,6 +62,8 @@ pub enum DataKey { #[derive(Clone, Debug, Eq, PartialEq)] pub enum InstanceDataKey { Admin, + GlobalYieldRate, + SupportedToken(Address), } #[contract] @@ -72,6 +81,27 @@ impl InheritanceContract { .temporary() .extend_ttl(key, TEMP_TTL_LEEWAY, TEMP_TTL_THRESHOLD); } + + fn extend_instance_ttl(env: &Env) { + env.storage() + .instance() + .extend_ttl(PLAN_TTL_LEEWAY, PLAN_TTL_THRESHOLD); + } + + /// Read the configured admin address, or error if the contract is uninitialized. + fn read_admin(env: &Env) -> Result { + env.storage() + .instance() + .get(&InstanceDataKey::Admin) + .ok_or(Error::AdminNotSet) + } + + /// Require the stored admin's signature for governance operations. + fn require_admin(env: &Env) -> Result { + let admin = Self::read_admin(env)?; + admin.require_auth(); + Ok(admin) + } } #[contractimpl] @@ -265,6 +295,118 @@ impl InheritanceContract { Ok(()) } + + /// Initialize the contract with the governing admin address. + /// Can only be called once; subsequent calls return `AlreadyInitialized`. + pub fn initialize(env: Env, admin: Address) -> Result<(), Error> { + if env.storage().instance().has(&InstanceDataKey::Admin) { + return Err(Error::AlreadyInitialized); + } + + env.storage() + .instance() + .set(&InstanceDataKey::Admin, &admin); + Self::extend_instance_ttl(&env); + + env.events() + .publish((symbol_short!("init"), admin.clone()), admin); + + Ok(()) + } + + /// Update the global base yield rate (in basis points). + /// Restricted to the admin signature. Rate must be <= 10000 bps (100%). + pub fn set_global_yield_rate(env: Env, rate_bps: u32) -> Result<(), Error> { + Self::require_admin(&env)?; + + if rate_bps > 10000 { + return Err(Error::InvalidYieldRate); + } + + env.storage() + .instance() + .set(&InstanceDataKey::GlobalYieldRate, &rate_bps); + Self::extend_instance_ttl(&env); + + env.events() + .publish((symbol_short!("yield_set"),), rate_bps); + + Ok(()) + } + + /// Retrieve the global base yield rate in basis points (defaults to 0 if unset). + pub fn get_global_yield_rate(env: Env) -> u32 { + env.storage() + .instance() + .get(&InstanceDataKey::GlobalYieldRate) + .unwrap_or(0) + } + + /// Register a supported asset (USDC, XLM, etc.) in the token registry. + /// Restricted to the admin signature. + pub fn add_supported_token(env: Env, token: Address) -> Result<(), Error> { + Self::require_admin(&env)?; + + let key = InstanceDataKey::SupportedToken(token.clone()); + if env.storage().instance().has(&key) { + return Err(Error::TokenAlreadySupported); + } + + env.storage().instance().set(&key, &true); + Self::extend_instance_ttl(&env); + + env.events() + .publish((symbol_short!("tkn_add"), token.clone()), token); + + Ok(()) + } + + /// Remove an asset from the supported token registry. + /// Restricted to the admin signature. + pub fn remove_supported_token(env: Env, token: Address) -> Result<(), Error> { + Self::require_admin(&env)?; + + let key = InstanceDataKey::SupportedToken(token.clone()); + if !env.storage().instance().has(&key) { + return Err(Error::TokenNotSupported); + } + + env.storage().instance().remove(&key); + Self::extend_instance_ttl(&env); + + env.events() + .publish((symbol_short!("tkn_rm"), token.clone()), token); + + Ok(()) + } + + /// Check whether a token is registered as a supported asset. + pub fn is_supported_token(env: Env, token: Address) -> bool { + env.storage() + .instance() + .has(&InstanceDataKey::SupportedToken(token)) + } + + /// Transfer admin/governance control to a new address. + /// Restricted to the current admin signature. + pub fn set_admin(env: Env, new_admin: Address) -> Result<(), Error> { + let previous_admin = Self::require_admin(&env)?; + + env.storage() + .instance() + .set(&InstanceDataKey::Admin, &new_admin); + Self::extend_instance_ttl(&env); + + env.events() + .publish((symbol_short!("admin_set"), previous_admin), new_admin); + + Ok(()) + } + + /// Retrieve the current admin address. + pub fn get_admin(env: Env) -> Result { + Self::read_admin(&env) + } } #[cfg(test)] diff --git a/contracts/inheritance-contract/src/test.rs b/contracts/inheritance-contract/src/test.rs index 3765cc18b..1d1734ba9 100644 --- a/contracts/inheritance-contract/src/test.rs +++ b/contracts/inheritance-contract/src/test.rs @@ -3,7 +3,7 @@ use super::*; use soroban_sdk::testutils::Address as _; use soroban_sdk::testutils::Ledger; -use soroban_sdk::{Address, Env, String, Vec}; +use soroban_sdk::{symbol_short, vec, Address, Env, IntoVal, String, Vec}; #[test] fn test_contract_compilation() { @@ -539,3 +539,311 @@ fn test_trigger_payout_no_plan() { let result = client.try_trigger_payout(&owner); assert_eq!(result, Err(Ok(Error::PlanNotFound))); } + +// --------------------------------------------------------------------------- +// Admin / governance configuration tests +// --------------------------------------------------------------------------- + +#[test] +fn test_initialize_sets_admin() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + client.initialize(&admin); + + assert_eq!(client.get_admin(), admin.clone()); + assert_eq!( + env.events().all(), + vec![ + &env, + ( + contract_id, + (symbol_short!("init"), admin.clone()).into_val(&env), + admin.into_val(&env) + ) + ] + ); +} + +#[test] +fn test_initialize_only_once() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + client.initialize(&admin); + + let result = client.try_initialize(&Address::generate(&env)); + assert_eq!(result, Err(Ok(Error::AlreadyInitialized))); +} + +#[test] +fn test_set_global_yield_rate_success() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + client.initialize(&admin); + + // Default is 0 before being set. + assert_eq!(client.get_global_yield_rate(), 0); + + client.set_global_yield_rate(&750); + assert_eq!(client.get_global_yield_rate(), 750); + + // Can be updated again. + client.set_global_yield_rate(&1200); + assert_eq!(client.get_global_yield_rate(), 1200); + + assert_eq!( + env.events().all(), + vec![ + &env, + ( + contract_id.clone(), + (symbol_short!("init"), admin.clone()).into_val(&env), + admin.into_val(&env) + ), + ( + contract_id.clone(), + (symbol_short!("yield_set"),).into_val(&env), + 750_u32.into_val(&env) + ), + ( + contract_id, + (symbol_short!("yield_set"),).into_val(&env), + 1200_u32.into_val(&env) + ) + ] + ); +} + +#[test] +fn test_set_global_yield_rate_invalid() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + client.initialize(&admin); + + // 10001 bps > 100% is rejected. + let result = client.try_set_global_yield_rate(&10001); + assert_eq!(result, Err(Ok(Error::InvalidYieldRate))); + + // Boundary value 10000 (100%) is accepted. + client.set_global_yield_rate(&10000); + assert_eq!(client.get_global_yield_rate(), 10000); +} + +#[test] +fn test_set_global_yield_rate_requires_admin() { + let env = Env::default(); + + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + + env.mock_all_auths(); + client.initialize(&admin); + + // No mocked auth: admin signature is required and missing. + env.set_auths(&[]); + let result = client.try_set_global_yield_rate(&500); + assert!(result.is_err()); +} + +#[test] +fn test_set_global_yield_rate_uninitialized() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let result = client.try_set_global_yield_rate(&500); + assert_eq!(result, Err(Ok(Error::AdminNotSet))); +} + +#[test] +fn test_add_and_remove_supported_token() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + client.initialize(&admin); + + let usdc = Address::generate(&env); + let xlm = Address::generate(&env); + + assert!(!client.is_supported_token(&usdc)); + + client.add_supported_token(&usdc); + client.add_supported_token(&xlm); + + assert!(client.is_supported_token(&usdc)); + assert!(client.is_supported_token(&xlm)); + + client.remove_supported_token(&usdc); + assert!(!client.is_supported_token(&usdc)); + assert!(client.is_supported_token(&xlm)); + + assert_eq!( + env.events().all(), + vec![ + &env, + ( + contract_id.clone(), + (symbol_short!("init"), admin.clone()).into_val(&env), + admin.into_val(&env) + ), + ( + contract_id.clone(), + (symbol_short!("tkn_add"), usdc.clone()).into_val(&env), + usdc.clone().into_val(&env) + ), + ( + contract_id.clone(), + (symbol_short!("tkn_add"), xlm.clone()).into_val(&env), + xlm.into_val(&env) + ), + ( + contract_id, + (symbol_short!("tkn_rm"), usdc.clone()).into_val(&env), + usdc.into_val(&env) + ) + ] + ); +} + +#[test] +fn test_add_supported_token_duplicate() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + client.initialize(&admin); + + let usdc = Address::generate(&env); + client.add_supported_token(&usdc); + + let result = client.try_add_supported_token(&usdc); + assert_eq!(result, Err(Ok(Error::TokenAlreadySupported))); +} + +#[test] +fn test_remove_supported_token_not_registered() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + client.initialize(&admin); + + let result = client.try_remove_supported_token(&Address::generate(&env)); + assert_eq!(result, Err(Ok(Error::TokenNotSupported))); +} + +#[test] +fn test_add_supported_token_requires_admin() { + let env = Env::default(); + + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + + env.mock_all_auths(); + client.initialize(&admin); + + env.set_auths(&[]); + let result = client.try_add_supported_token(&Address::generate(&env)); + assert!(result.is_err()); +} + +#[test] +fn test_set_admin_transfers_control() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let new_admin = Address::generate(&env); + client.initialize(&admin); + + client.set_admin(&new_admin); + assert_eq!(client.get_admin(), new_admin.clone()); + + assert_eq!( + env.events().all(), + vec![ + &env, + ( + contract_id.clone(), + (symbol_short!("init"), admin.clone()).into_val(&env), + admin.clone().into_val(&env) + ), + ( + contract_id, + (symbol_short!("admin_set"), admin).into_val(&env), + new_admin.clone().into_val(&env) + ) + ] + ); + + // The new admin can perform governance operations. + client.set_global_yield_rate(&300); + assert_eq!(client.get_global_yield_rate(), 300); +} + +#[test] +fn test_set_admin_requires_admin() { + let env = Env::default(); + + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + + env.mock_all_auths(); + client.initialize(&admin); + + env.set_auths(&[]); + let result = client.try_set_admin(&Address::generate(&env)); + assert!(result.is_err()); +} + +#[test] +fn test_get_admin_uninitialized() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let result = client.try_get_admin(); + assert_eq!(result, Err(Ok(Error::AdminNotSet))); +}