From 4f59ce53acca346b4435b69f054c9db95f3d5855 Mon Sep 17 00:00:00 2001 From: OtowoSamuel Date: Wed, 25 Feb 2026 21:29:08 +0100 Subject: [PATCH 1/2] issues --- contracts/src/errors.rs | 13 +++ contracts/src/lib.rs | 107 +++++++++++++++++ contracts/src/strategy/interface.rs | 64 +++++++++++ contracts/src/strategy/mod.rs | 6 + contracts/src/strategy/registry.rs | 170 ++++++++++++++++++++++++++++ contracts/src/strategy/routing.rs | 163 ++++++++++++++++++++++++++ contracts/src/strategy/tests.rs | 157 +++++++++++++++++++++++++ 7 files changed, 680 insertions(+) create mode 100644 contracts/src/strategy/interface.rs create mode 100644 contracts/src/strategy/mod.rs create mode 100644 contracts/src/strategy/registry.rs create mode 100644 contracts/src/strategy/routing.rs create mode 100644 contracts/src/strategy/tests.rs diff --git a/contracts/src/errors.rs b/contracts/src/errors.rs index 3d037b03..bbab83b9 100644 --- a/contracts/src/errors.rs +++ b/contracts/src/errors.rs @@ -209,6 +209,16 @@ pub enum SavingsError { /// /// Config initialization can only happen once. ConfigAlreadyInitialized = 91, + + // ========== Strategy Errors (92-99) ========== + /// Returned when a yield strategy is not found in the registry. + StrategyNotFound = 92, + + /// Returned when attempting to register a strategy that already exists. + StrategyAlreadyRegistered = 93, + + /// Returned when attempting to deposit into a disabled strategy. + StrategyDisabled = 94, } #[cfg(test)] @@ -254,6 +264,9 @@ mod tests { SavingsError::InvariantViolation as u32, SavingsError::InvalidFeeBps as u32, SavingsError::ConfigAlreadyInitialized as u32, + SavingsError::StrategyNotFound as u32, + SavingsError::StrategyAlreadyRegistered as u32, + SavingsError::StrategyDisabled as u32, ]; let mut sorted = errors.clone(); diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index 65ba598e..89ddcb50 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -18,6 +18,7 @@ mod lock; pub mod rewards; mod storage_types; +pub mod strategy; mod ttl; mod upgrade; mod users; @@ -34,6 +35,8 @@ pub use crate::storage_types::{ AutoSave, DataKey, GoalSave, GoalSaveView, GroupSave, GroupSaveView, LockSave, LockSaveView, MintPayload, PlanType, SavingsPlan, User, }; +pub use crate::strategy::registry::StrategyInfo; +pub use crate::strategy::routing::{StrategyPosition, StrategyPositionKey}; /// Custom error codes for the contract administration #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -855,6 +858,110 @@ impl NesteraContract { pub fn is_governance_active(env: Env) -> bool { governance::is_governance_active(&env) } + + // ========== Strategy Functions ========== + + /// Registers a new yield strategy (admin/governance only). + pub fn register_strategy( + env: Env, + caller: Address, + strategy_address: Address, + risk_level: u32, + ) -> Result<(), SavingsError> { + strategy::registry::register_strategy(&env, caller, strategy_address, risk_level) + } + + /// Disables a registered yield strategy (admin/governance only). + pub fn disable_strategy( + env: Env, + caller: Address, + strategy_address: Address, + ) -> Result<(), SavingsError> { + strategy::registry::disable_strategy(&env, caller, strategy_address) + } + + /// Returns info about a registered strategy. + pub fn get_strategy( + env: Env, + strategy_address: Address, + ) -> Result { + strategy::registry::get_strategy(&env, strategy_address) + } + + /// Returns all registered strategy addresses. + pub fn get_all_strategies(env: Env) -> Vec
{ + strategy::registry::get_all_strategies(&env) + } + + /// Routes a LockSave deposit to a yield strategy. + pub fn route_lock_to_strategy( + env: Env, + caller: Address, + lock_id: u64, + strategy_address: Address, + amount: i128, + ) -> Result { + caller.require_auth(); + ensure_not_paused(&env)?; + let position_key = StrategyPositionKey::Lock(lock_id); + strategy::routing::route_to_strategy(&env, strategy_address, position_key, amount) + } + + /// Routes a GroupSave pooled deposit to a yield strategy. + pub fn route_group_to_strategy( + env: Env, + caller: Address, + group_id: u64, + strategy_address: Address, + amount: i128, + ) -> Result { + caller.require_auth(); + ensure_not_paused(&env)?; + let position_key = StrategyPositionKey::Group(group_id); + strategy::routing::route_to_strategy(&env, strategy_address, position_key, amount) + } + + /// Returns the strategy position for a lock plan. + pub fn get_lock_strategy_position(env: Env, lock_id: u64) -> Option { + strategy::routing::get_position(&env, StrategyPositionKey::Lock(lock_id)) + } + + /// Returns the strategy position for a group plan. + pub fn get_group_strategy_position(env: Env, group_id: u64) -> Option { + strategy::routing::get_position(&env, StrategyPositionKey::Group(group_id)) + } + + /// Withdraws funds from a lock's strategy position. + pub fn withdraw_lock_strategy( + env: Env, + caller: Address, + lock_id: u64, + to: Address, + ) -> Result { + caller.require_auth(); + ensure_not_paused(&env)?; + strategy::routing::withdraw_from_strategy( + &env, + StrategyPositionKey::Lock(lock_id), + to, + ) + } + + /// Withdraws funds from a group's strategy position. + pub fn withdraw_group_strategy( + env: Env, + caller: Address, + group_id: u64, + to: Address, + ) -> Result { + caller.require_auth(); + ensure_not_paused(&env)?; + strategy::routing::withdraw_from_strategy( + &env, + StrategyPositionKey::Group(group_id), + to, + ) + } } #[cfg(test)] diff --git a/contracts/src/strategy/interface.rs b/contracts/src/strategy/interface.rs new file mode 100644 index 00000000..7a0fd6de --- /dev/null +++ b/contracts/src/strategy/interface.rs @@ -0,0 +1,64 @@ +/// Yield Strategy Interface for Nestera Protocol +/// +/// External strategy contracts must implement this interface to be compatible +/// with the Nestera savings protocol. Strategies are invoked via Soroban +/// cross-contract calls through the strategy registry. +/// +/// # Security Assumptions +/// - Strategy contracts are audited and registered via governance/admin. +/// - All state updates in Nestera happen BEFORE external strategy calls (CEI pattern). +/// - Strategy contracts must not hold user funds beyond what is deposited via `deposit`. +/// - `get_total_balance` must reflect the actual deposited principal + any accrued yield. +/// - `withdraw` must return exactly the requested amount or revert. +/// - `harvest` collects accrued yield and returns the harvested amount. +use soroban_sdk::{contractclient, Address, Env}; + +/// Client interface for external yield strategy contracts. +/// +/// Any contract registered as a yield strategy must expose these entry points. +/// The `contractclient` macro generates a `YieldStrategyClient` that can be used +/// for cross-contract invocation on Soroban. +#[contractclient(name = "YieldStrategyClient")] +pub trait YieldStrategy { + /// Deposits funds into the yield strategy. + /// + /// # Arguments + /// * `env` - The contract environment + /// * `from` - The address depositing (the Nestera contract) + /// * `amount` - The amount of tokens to deposit (must be > 0) + /// + /// # Returns + /// The number of strategy shares minted for this deposit. + fn strategy_deposit(env: Env, from: Address, amount: i128) -> i128; + + /// Withdraws funds from the yield strategy. + /// + /// # Arguments + /// * `env` - The contract environment + /// * `to` - The address to receive withdrawn funds + /// * `amount` - The amount of tokens to withdraw (must be > 0) + /// + /// # Returns + /// The actual amount of tokens returned. + fn strategy_withdraw(env: Env, to: Address, amount: i128) -> i128; + + /// Harvests accrued yield from the strategy. + /// + /// # Arguments + /// * `env` - The contract environment + /// * `to` - The address to receive harvested yield + /// + /// # Returns + /// The amount of yield harvested. + fn strategy_harvest(env: Env, to: Address) -> i128; + + /// Returns the total balance held by this strategy for the caller. + /// + /// # Arguments + /// * `env` - The contract environment + /// * `addr` - The address to query balance for + /// + /// # Returns + /// The total balance (principal + accrued yield) denominated in the deposit token. + fn strategy_balance(env: Env, addr: Address) -> i128; +} diff --git a/contracts/src/strategy/mod.rs b/contracts/src/strategy/mod.rs new file mode 100644 index 00000000..add5523a --- /dev/null +++ b/contracts/src/strategy/mod.rs @@ -0,0 +1,6 @@ +pub mod interface; +pub mod registry; +pub mod routing; + +#[cfg(test)] +mod tests; diff --git a/contracts/src/strategy/registry.rs b/contracts/src/strategy/registry.rs new file mode 100644 index 00000000..5f6ed573 --- /dev/null +++ b/contracts/src/strategy/registry.rs @@ -0,0 +1,170 @@ +use crate::errors::SavingsError; +use crate::governance; +use crate::ttl; +use soroban_sdk::{contracttype, symbol_short, Address, Env, Vec}; + +/// Information about a registered yield strategy. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StrategyInfo { + /// The on-chain address of the strategy contract + pub address: Address, + /// Whether this strategy is currently enabled for deposits + pub enabled: bool, + /// Risk level indicator (0 = lowest risk, 255 = highest risk) + pub risk_level: u32, +} + +/// Storage keys for the strategy registry. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum StrategyKey { + /// Maps a strategy address to its StrategyInfo + Info(Address), + /// List of all registered strategy addresses + AllStrategies, +} + +// ========== Admin / Governance Guard ========== + +/// Ensures the caller is the admin or governance is active. +fn require_admin_or_governance(env: &Env, caller: &Address) -> Result<(), SavingsError> { + caller.require_auth(); + governance::validate_admin_or_governance(env, caller)?; + Ok(()) +} + +// ========== Registry Functions ========== + +/// Registers a new yield strategy. +/// +/// Only callable by admin or governance. The strategy is enabled by default. +/// +/// # Arguments +/// * `env` - The contract environment +/// * `caller` - Admin or governance caller +/// * `strategy_address` - Address of the strategy contract +/// * `risk_level` - Risk classification (0-255) +/// +/// # Errors +/// * `Unauthorized` - If caller is not admin / governance is not active +/// * `StrategyAlreadyRegistered` - If the strategy address is already registered +pub fn register_strategy( + env: &Env, + caller: Address, + strategy_address: Address, + risk_level: u32, +) -> Result<(), SavingsError> { + require_admin_or_governance(env, &caller)?; + + let info_key = StrategyKey::Info(strategy_address.clone()); + + // Prevent duplicate registration + if env.storage().persistent().has(&info_key) { + return Err(SavingsError::StrategyAlreadyRegistered); + } + + let info = StrategyInfo { + address: strategy_address.clone(), + enabled: true, + risk_level, + }; + + // Store strategy info + env.storage().persistent().set(&info_key, &info); + + // Add to the list of all strategies + let list_key = StrategyKey::AllStrategies; + let mut list: Vec
= env + .storage() + .persistent() + .get(&list_key) + .unwrap_or(Vec::new(env)); + list.push_back(strategy_address.clone()); + env.storage().persistent().set(&list_key, &list); + + // Extend TTL + env.storage() + .persistent() + .extend_ttl(&info_key, ttl::LOW_THRESHOLD, ttl::EXTEND_TO); + env.storage() + .persistent() + .extend_ttl(&list_key, ttl::LOW_THRESHOLD, ttl::EXTEND_TO); + + env.events().publish( + (symbol_short!("strat"), symbol_short!("register")), + strategy_address, + ); + + Ok(()) +} + +/// Disables a previously registered strategy. +/// +/// Disabled strategies will not accept new deposits but existing positions +/// can still be withdrawn. +/// +/// # Arguments +/// * `env` - The contract environment +/// * `caller` - Admin or governance caller +/// * `strategy_address` - Address of the strategy to disable +/// +/// # Errors +/// * `Unauthorized` - If caller is not admin / governance is not active +/// * `StrategyNotFound` - If the strategy is not registered +pub fn disable_strategy( + env: &Env, + caller: Address, + strategy_address: Address, +) -> Result<(), SavingsError> { + require_admin_or_governance(env, &caller)?; + + let info_key = StrategyKey::Info(strategy_address.clone()); + let mut info: StrategyInfo = env + .storage() + .persistent() + .get(&info_key) + .ok_or(SavingsError::StrategyNotFound)?; + + info.enabled = false; + env.storage().persistent().set(&info_key, &info); + + env.storage() + .persistent() + .extend_ttl(&info_key, ttl::LOW_THRESHOLD, ttl::EXTEND_TO); + + env.events().publish( + (symbol_short!("strat"), symbol_short!("disable")), + strategy_address, + ); + + Ok(()) +} + +/// Retrieves information about a registered strategy. +/// +/// # Arguments +/// * `env` - The contract environment +/// * `strategy_address` - Address of the strategy to query +/// +/// # Returns +/// `Ok(StrategyInfo)` with the strategy metadata, or `Err(StrategyNotFound)` +pub fn get_strategy( + env: &Env, + strategy_address: Address, +) -> Result { + let info_key = StrategyKey::Info(strategy_address); + env.storage() + .persistent() + .get(&info_key) + .ok_or(SavingsError::StrategyNotFound) +} + +/// Returns the list of all registered strategy addresses. +pub fn get_all_strategies(env: &Env) -> Vec
{ + let list_key = StrategyKey::AllStrategies; + env.storage() + .persistent() + .get(&list_key) + .unwrap_or(Vec::new(env)) +} diff --git a/contracts/src/strategy/routing.rs b/contracts/src/strategy/routing.rs new file mode 100644 index 00000000..f495dafe --- /dev/null +++ b/contracts/src/strategy/routing.rs @@ -0,0 +1,163 @@ +use crate::errors::SavingsError; +use crate::strategy::interface::YieldStrategyClient; +use crate::strategy::registry::{self, StrategyKey}; +use crate::ttl; +use soroban_sdk::{contracttype, symbol_short, Address, Env}; + +/// Tracks a deposit routed to a yield strategy. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StrategyPosition { + /// The strategy contract address + pub strategy: Address, + /// Principal amount deposited into the strategy + pub principal_deposited: i128, + /// Shares received from the strategy + pub strategy_shares: i128, +} + +/// Storage key for strategy positions keyed by (plan_type_tag, plan_id). +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum StrategyPositionKey { + /// Position for a LockSave plan + Lock(u64), + /// Position for a GroupSave plan + Group(u64), +} + +/// Routes eligible deposit funds to a registered yield strategy. +/// +/// Follows the Checks-Effects-Interactions (CEI) pattern: +/// 1. **Checks** – validates strategy exists & is enabled, amount > 0 +/// 2. **Effects** – persists `StrategyPosition` state +/// 3. **Interactions** – calls the external strategy contract +/// +/// If the external strategy call fails, the transaction reverts atomically +/// (Soroban guarantees this), so state is always consistent. +/// +/// # Arguments +/// * `env` - The contract environment +/// * `strategy_address` - Address of the target strategy contract +/// * `position_key` - Storage key for this position (Lock or Group) +/// * `amount` - Amount to deposit into the strategy +/// +/// # Returns +/// The number of strategy shares received. +/// +/// # Errors +/// * `StrategyNotFound` - Strategy not registered +/// * `StrategyDisabled` - Strategy is disabled +/// * `InvalidAmount` - amount <= 0 +pub fn route_to_strategy( + env: &Env, + strategy_address: Address, + position_key: StrategyPositionKey, + amount: i128, +) -> Result { + // --- CHECKS --- + if amount <= 0 { + return Err(SavingsError::InvalidAmount); + } + + let info = registry::get_strategy(env, strategy_address.clone())?; + if !info.enabled { + return Err(SavingsError::StrategyDisabled); + } + + // --- EFFECTS (state update BEFORE external call) --- + // Optimistically record the position; Soroban atomically reverts on failure. + let position = StrategyPosition { + strategy: strategy_address.clone(), + principal_deposited: amount, + strategy_shares: 0, // placeholder, updated after call + }; + env.storage() + .persistent() + .set(&position_key, &position); + + // --- INTERACTIONS (external call) --- + let client = YieldStrategyClient::new(env, &strategy_address); + let shares = client.strategy_deposit(&env.current_contract_address(), &amount); + + // Update shares after successful call + let final_position = StrategyPosition { + strategy: strategy_address.clone(), + principal_deposited: amount, + strategy_shares: shares, + }; + env.storage() + .persistent() + .set(&position_key, &final_position); + + // Extend TTL + env.storage() + .persistent() + .extend_ttl(&position_key, ttl::LOW_THRESHOLD, ttl::EXTEND_TO); + + // Emit event + env.events().publish( + (symbol_short!("strat"), symbol_short!("deposit")), + (strategy_address, amount, shares), + ); + + Ok(shares) +} + +/// Retrieves the strategy position for a plan, if any. +pub fn get_position( + env: &Env, + position_key: StrategyPositionKey, +) -> Option { + env.storage().persistent().get(&position_key) +} + +/// Withdraws funds from a strategy position. +/// +/// # Arguments +/// * `env` - The contract environment +/// * `position_key` - The position to withdraw from +/// * `to` - The recipient address +/// +/// # Returns +/// The amount of tokens received from the strategy. +pub fn withdraw_from_strategy( + env: &Env, + position_key: StrategyPositionKey, + to: Address, +) -> Result { + let position: StrategyPosition = env + .storage() + .persistent() + .get(&position_key) + .ok_or(SavingsError::StrategyNotFound)?; + + if position.principal_deposited == 0 { + return Ok(0); + } + + // Check strategy still exists (may be disabled, but withdrawal still allowed) + let info_key = StrategyKey::Info(position.strategy.clone()); + if !env.storage().persistent().has(&info_key) { + return Err(SavingsError::StrategyNotFound); + } + + // Update state BEFORE external call + let cleared = StrategyPosition { + strategy: position.strategy.clone(), + principal_deposited: 0, + strategy_shares: 0, + }; + env.storage().persistent().set(&position_key, &cleared); + + // External call + let client = YieldStrategyClient::new(env, &position.strategy); + let returned = client.strategy_withdraw(&to, &position.principal_deposited); + + env.events().publish( + (symbol_short!("strat"), symbol_short!("withdraw")), + (position.strategy, returned), + ); + + Ok(returned) +} diff --git a/contracts/src/strategy/tests.rs b/contracts/src/strategy/tests.rs new file mode 100644 index 00000000..b35b9599 --- /dev/null +++ b/contracts/src/strategy/tests.rs @@ -0,0 +1,157 @@ +use crate::strategy::registry::{self, StrategyInfo}; +use crate::strategy::routing::{self, StrategyPositionKey}; +use crate::errors::SavingsError; +use crate::{NesteraContract, NesteraContractClient}; +use soroban_sdk::{ + testutils::Address as _, + Address, BytesN, Env, +}; + +/// Helper: set up an env with an initialized contract and admin. +/// Returns (env, client, admin, contract_id). +fn setup() -> (Env, NesteraContractClient<'static>, Address, Address) { + let env = Env::default(); + let contract_id = env.register(NesteraContract, ()); + let client = NesteraContractClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let admin_pk = BytesN::from_array(&env, &[1u8; 32]); + + env.mock_all_auths(); + client.initialize(&admin, &admin_pk); + + (env, client, admin, contract_id) +} + +// ========== Registry Tests ========== + +#[test] +fn test_register_strategy() { + let (env, client, admin, _) = setup(); + let strategy_addr = Address::generate(&env); + + let result = client.try_register_strategy(&admin, &strategy_addr, &1u32); + assert!(result.is_ok()); + + let info = client.get_strategy(&strategy_addr); + assert_eq!(info.address, strategy_addr); + assert!(info.enabled); + assert_eq!(info.risk_level, 1); +} + +#[test] +fn test_register_duplicate_strategy_fails() { + let (env, client, admin, _) = setup(); + let strategy_addr = Address::generate(&env); + + client.register_strategy(&admin, &strategy_addr, &1u32); + let result = client.try_register_strategy(&admin, &strategy_addr, &2u32); + assert!(result.is_err()); +} + +#[test] +fn test_disable_strategy() { + let (env, client, admin, _) = setup(); + let strategy_addr = Address::generate(&env); + + client.register_strategy(&admin, &strategy_addr, &1u32); + client.disable_strategy(&admin, &strategy_addr); + + let info = client.get_strategy(&strategy_addr); + assert!(!info.enabled); +} + +#[test] +fn test_disable_nonexistent_strategy_fails() { + let (env, client, admin, _) = setup(); + let strategy_addr = Address::generate(&env); + + let result = client.try_disable_strategy(&admin, &strategy_addr); + assert!(result.is_err()); +} + +#[test] +fn test_get_strategy_not_found() { + let (env, client, _admin, _) = setup(); + let strategy_addr = Address::generate(&env); + + let result = client.try_get_strategy(&strategy_addr); + assert!(result.is_err()); +} + +#[test] +fn test_get_all_strategies() { + let (env, client, admin, _) = setup(); + let s1 = Address::generate(&env); + let s2 = Address::generate(&env); + + client.register_strategy(&admin, &s1, &0u32); + client.register_strategy(&admin, &s2, &5u32); + + let all = client.get_all_strategies(); + assert_eq!(all.len(), 2); +} + +#[test] +fn test_register_strategy_unauthorized() { + let (env, client, _admin, _) = setup(); + let non_admin = Address::generate(&env); + let strategy_addr = Address::generate(&env); + + let result = client.try_register_strategy(&non_admin, &strategy_addr, &1u32); + assert!(result.is_err()); +} + +// ========== Routing Unit Tests ========== + +#[test] +fn test_route_to_strategy_invalid_amount() { + let (env, _client, _admin, contract_id) = setup(); + let strategy_addr = Address::generate(&env); + + env.as_contract(&contract_id, || { + let position_key = StrategyPositionKey::Lock(1); + let result = routing::route_to_strategy(&env, strategy_addr.clone(), position_key, 0); + assert_eq!(result, Err(SavingsError::InvalidAmount)); + + let position_key2 = StrategyPositionKey::Lock(2); + let result2 = routing::route_to_strategy(&env, Address::generate(&env), position_key2, -100); + assert_eq!(result2, Err(SavingsError::InvalidAmount)); + }); +} + +#[test] +fn test_route_to_unregistered_strategy_fails() { + let (env, _client, _admin, contract_id) = setup(); + let strategy_addr = Address::generate(&env); + + env.as_contract(&contract_id, || { + let position_key = StrategyPositionKey::Lock(1); + let result = routing::route_to_strategy(&env, strategy_addr.clone(), position_key, 1000); + assert_eq!(result, Err(SavingsError::StrategyNotFound)); + }); +} + +#[test] +fn test_route_to_disabled_strategy_fails() { + let (env, client, admin, contract_id) = setup(); + let strategy_addr = Address::generate(&env); + + client.register_strategy(&admin, &strategy_addr, &1u32); + client.disable_strategy(&admin, &strategy_addr); + + env.as_contract(&contract_id, || { + let position_key = StrategyPositionKey::Lock(1); + let result = routing::route_to_strategy(&env, strategy_addr.clone(), position_key, 1000); + assert_eq!(result, Err(SavingsError::StrategyDisabled)); + }); +} + +#[test] +fn test_get_position_none_when_empty() { + let (env, _client, _admin, contract_id) = setup(); + + env.as_contract(&contract_id, || { + let pos = routing::get_position(&env, StrategyPositionKey::Lock(999)); + assert!(pos.is_none()); + }); +} From 366d6453016edf2351b51891b4c2a1833313826f Mon Sep 17 00:00:00 2001 From: OtowoSamuel Date: Wed, 25 Feb 2026 22:03:04 +0100 Subject: [PATCH 2/2] issues --- contracts/src/governance_events.rs | 10 ++- contracts/src/governance_tests.rs | 116 ++++++++++++++--------------- contracts/src/lib.rs | 17 +---- contracts/src/strategy/registry.rs | 5 +- contracts/src/strategy/routing.rs | 9 +-- contracts/src/strategy/tests.rs | 10 +-- 6 files changed, 74 insertions(+), 93 deletions(-) diff --git a/contracts/src/governance_events.rs b/contracts/src/governance_events.rs index 56fa0e13..cce67fd8 100644 --- a/contracts/src/governance_events.rs +++ b/contracts/src/governance_events.rs @@ -45,7 +45,10 @@ pub fn emit_proposal_created(env: &Env, proposal_id: u64, creator: Address, desc description, }; - env.events().publish((symbol_short!("gov"), symbol_short!("created"), creator), event); + env.events().publish( + (symbol_short!("gov"), symbol_short!("created"), creator), + event, + ); } pub fn emit_vote_cast(env: &Env, proposal_id: u64, voter: Address, vote_type: u32, weight: u128) { @@ -56,7 +59,8 @@ pub fn emit_vote_cast(env: &Env, proposal_id: u64, voter: Address, vote_type: u3 weight, }; - env.events().publish((symbol_short!("gov"), symbol_short!("voted"), voter), event); + env.events() + .publish((symbol_short!("gov"), symbol_short!("voted"), voter), event); } pub fn emit_proposal_queued(env: &Env, proposal_id: u64, queued_at: u64) { @@ -84,4 +88,4 @@ pub fn emit_proposal_canceled(env: &Env, proposal_id: u64, canceled_at: u64) { }; env.events() .publish((symbol_short!("gov"), symbol_short!("canceled")), event); -} \ No newline at end of file +} diff --git a/contracts/src/governance_tests.rs b/contracts/src/governance_tests.rs index b1354790..f5d96b0f 100644 --- a/contracts/src/governance_tests.rs +++ b/contracts/src/governance_tests.rs @@ -2,10 +2,10 @@ mod governance_tests { use crate::governance_events::{ProposalCreated, VoteCast}; - use soroban_sdk::symbol_short; - use soroban_sdk::IntoVal; use crate::rewards::storage_types::RewardsConfig; use crate::{NesteraContract, NesteraContractClient, PlanType}; + use soroban_sdk::symbol_short; + use soroban_sdk::IntoVal; use soroban_sdk::{ testutils::{Address as _, Events}, Address, BytesN, Env, String, Symbol, @@ -171,75 +171,73 @@ mod governance_tests { // NEW TESTS: Governance Event Logging // ──────────────────────────────────────────────────────────────────────────────── -#[test] -fn test_proposal_created_emits_event() { - let (env, client, admin) = setup_contract(); - env.mock_all_auths(); - - client.init_voting_config(&admin, &5000, &604800, &86400, &100, &10_000); - - let creator = Address::generate(&env); - let description = String::from_str(&env, "Test proposal description"); - - let proposal_id = client.create_proposal(&creator, &description); + #[test] + fn test_proposal_created_emits_event() { + let (env, client, admin) = setup_contract(); + env.mock_all_auths(); - let events = env.events().all(); + client.init_voting_config(&admin, &5000, &604800, &86400, &100, &10_000); - let created_event_opt = events.iter().rev().find(|e| { - e.0 == client.address - && e.1 - == ( - symbol_short!("gov"), - symbol_short!("created"), - creator.clone(), - ) - .into_val(&env) - }); + let creator = Address::generate(&env); + let description = String::from_str(&env, "Test proposal description"); - assert!(created_event_opt.is_some(), "ProposalCreated event not emitted"); - let event_data: ProposalCreated = created_event_opt.unwrap().2.clone().into_val(&env); + let proposal_id = client.create_proposal(&creator, &description); - assert_eq!(event_data.proposal_id, proposal_id); - assert_eq!(event_data.creator, creator); - assert_eq!(event_data.description, description); -} + let events = env.events().all(); + + let created_event_opt = events.iter().rev().find(|e| { + e.0 == client.address + && e.1 + == ( + symbol_short!("gov"), + symbol_short!("created"), + creator.clone(), + ) + .into_val(&env) + }); + + assert!( + created_event_opt.is_some(), + "ProposalCreated event not emitted" + ); + let event_data: ProposalCreated = created_event_opt.unwrap().2.clone().into_val(&env); + + assert_eq!(event_data.proposal_id, proposal_id); + assert_eq!(event_data.creator, creator); + assert_eq!(event_data.description, description); + } -#[test] -fn test_vote_cast_emits_event() { - let (env, client, admin) = setup_contract(); - env.mock_all_auths(); + #[test] + fn test_vote_cast_emits_event() { + let (env, client, admin) = setup_contract(); + env.mock_all_auths(); - client.init_voting_config(&admin, &5000, &604800, &86400, &100, &10_000); + client.init_voting_config(&admin, &5000, &604800, &86400, &100, &10_000); - let creator = Address::generate(&env); - let voter = Address::generate(&env); + let creator = Address::generate(&env); + let voter = Address::generate(&env); - client.initialize_user(&voter); - client.create_savings_plan(&voter, &PlanType::Flexi, &10000); + client.initialize_user(&voter); + client.create_savings_plan(&voter, &PlanType::Flexi, &10000); - let proposal_id = client.create_proposal(&creator, &String::from_str(&env, "Vote test")); + let proposal_id = client.create_proposal(&creator, &String::from_str(&env, "Vote test")); - client.vote(&proposal_id, &1, &voter); + client.vote(&proposal_id, &1, &voter); - let events = env.events().all(); + let events = env.events().all(); - let vote_event_opt = events.iter().rev().find(|e| { - e.0 == client.address - && e.1 - == ( - symbol_short!("gov"), - symbol_short!("voted"), - voter.clone(), - ) - .into_val(&env) - }); + let vote_event_opt = events.iter().rev().find(|e| { + e.0 == client.address + && e.1 + == (symbol_short!("gov"), symbol_short!("voted"), voter.clone()).into_val(&env) + }); - assert!(vote_event_opt.is_some(), "VoteCast event not emitted"); - let event_data: VoteCast = vote_event_opt.unwrap().2.clone().into_val(&env); + assert!(vote_event_opt.is_some(), "VoteCast event not emitted"); + let event_data: VoteCast = vote_event_opt.unwrap().2.clone().into_val(&env); - assert_eq!(event_data.proposal_id, proposal_id); - assert_eq!(event_data.voter, voter); - assert_eq!(event_data.vote_type, 1); - assert!(event_data.weight > 0); -} + assert_eq!(event_data.proposal_id, proposal_id); + assert_eq!(event_data.voter, voter); + assert_eq!(event_data.vote_type, 1); + assert!(event_data.weight > 0); + } } diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index 89ddcb50..b628f188 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -881,10 +881,7 @@ impl NesteraContract { } /// Returns info about a registered strategy. - pub fn get_strategy( - env: Env, - strategy_address: Address, - ) -> Result { + pub fn get_strategy(env: Env, strategy_address: Address) -> Result { strategy::registry::get_strategy(&env, strategy_address) } @@ -940,11 +937,7 @@ impl NesteraContract { ) -> Result { caller.require_auth(); ensure_not_paused(&env)?; - strategy::routing::withdraw_from_strategy( - &env, - StrategyPositionKey::Lock(lock_id), - to, - ) + strategy::routing::withdraw_from_strategy(&env, StrategyPositionKey::Lock(lock_id), to) } /// Withdraws funds from a group's strategy position. @@ -956,11 +949,7 @@ impl NesteraContract { ) -> Result { caller.require_auth(); ensure_not_paused(&env)?; - strategy::routing::withdraw_from_strategy( - &env, - StrategyPositionKey::Group(group_id), - to, - ) + strategy::routing::withdraw_from_strategy(&env, StrategyPositionKey::Group(group_id), to) } } diff --git a/contracts/src/strategy/registry.rs b/contracts/src/strategy/registry.rs index 5f6ed573..d9f52724 100644 --- a/contracts/src/strategy/registry.rs +++ b/contracts/src/strategy/registry.rs @@ -149,10 +149,7 @@ pub fn disable_strategy( /// /// # Returns /// `Ok(StrategyInfo)` with the strategy metadata, or `Err(StrategyNotFound)` -pub fn get_strategy( - env: &Env, - strategy_address: Address, -) -> Result { +pub fn get_strategy(env: &Env, strategy_address: Address) -> Result { let info_key = StrategyKey::Info(strategy_address); env.storage() .persistent() diff --git a/contracts/src/strategy/routing.rs b/contracts/src/strategy/routing.rs index f495dafe..d66e275d 100644 --- a/contracts/src/strategy/routing.rs +++ b/contracts/src/strategy/routing.rs @@ -72,9 +72,7 @@ pub fn route_to_strategy( principal_deposited: amount, strategy_shares: 0, // placeholder, updated after call }; - env.storage() - .persistent() - .set(&position_key, &position); + env.storage().persistent().set(&position_key, &position); // --- INTERACTIONS (external call) --- let client = YieldStrategyClient::new(env, &strategy_address); @@ -105,10 +103,7 @@ pub fn route_to_strategy( } /// Retrieves the strategy position for a plan, if any. -pub fn get_position( - env: &Env, - position_key: StrategyPositionKey, -) -> Option { +pub fn get_position(env: &Env, position_key: StrategyPositionKey) -> Option { env.storage().persistent().get(&position_key) } diff --git a/contracts/src/strategy/tests.rs b/contracts/src/strategy/tests.rs index b35b9599..286be8ff 100644 --- a/contracts/src/strategy/tests.rs +++ b/contracts/src/strategy/tests.rs @@ -1,11 +1,8 @@ +use crate::errors::SavingsError; use crate::strategy::registry::{self, StrategyInfo}; use crate::strategy::routing::{self, StrategyPositionKey}; -use crate::errors::SavingsError; use crate::{NesteraContract, NesteraContractClient}; -use soroban_sdk::{ - testutils::Address as _, - Address, BytesN, Env, -}; +use soroban_sdk::{testutils::Address as _, Address, BytesN, Env}; /// Helper: set up an env with an initialized contract and admin. /// Returns (env, client, admin, contract_id). @@ -114,7 +111,8 @@ fn test_route_to_strategy_invalid_amount() { assert_eq!(result, Err(SavingsError::InvalidAmount)); let position_key2 = StrategyPositionKey::Lock(2); - let result2 = routing::route_to_strategy(&env, Address::generate(&env), position_key2, -100); + let result2 = + routing::route_to_strategy(&env, Address::generate(&env), position_key2, -100); assert_eq!(result2, Err(SavingsError::InvalidAmount)); }); }