From 891f8bf5c8b10c85c9ec09fb77f824bdf56cd403 Mon Sep 17 00:00:00 2001 From: happyboy24 Date: Wed, 24 Jun 2026 11:45:59 +0100 Subject: [PATCH] check phase commit: --- EVENTS.md | 14 +++ TODO.md | 16 +++ neurowealth-vault/contracts/vault/src/lib.rs | 92 ++++++++++++++- .../contracts/vault/src/tests/mod.rs | 2 + .../vault/src/tests/test_user_strategy.rs | 108 ++++++++++++++++++ 5 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 TODO.md create mode 100644 neurowealth-vault/contracts/vault/src/tests/test_user_strategy.rs diff --git a/EVENTS.md b/EVENTS.md index 66caf7f8..7d67cbc2 100644 --- a/EVENTS.md +++ b/EVENTS.md @@ -216,6 +216,7 @@ pub struct UserDepositCapUpdatedEvent { ### 8c. CapsUpdatedEvent **Topic:** `"caps_upd"` + Emitted when user deposit and TVL caps are updated in a single transaction via `set_caps`. ```rust @@ -227,6 +228,19 @@ pub struct CapsUpdatedEvent { } ``` +### 9. UserStrategyUpdatedEvent + +Emitted when a user updates their on-chain strategy preference. + +```rust +pub struct UserStrategyUpdatedEvent { + pub user: Address, + pub old_strategy: Symbol, + pub new_strategy: Symbol, +} +``` + +**Topics**: `SymbolShort("user_strat")` ### 9. AgentUpdatedEvent **Topic:** `"agent"` diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..bf28c06a --- /dev/null +++ b/TODO.md @@ -0,0 +1,16 @@ +# TODO + +- [ ] Add/confirm per-user strategy storage key and API: + - [x] `DataKey::UserStrategy(Address)` exists + - [x] `set_user_strategy(env, user, strategy)` requires `user.require_auth()` + - [x] `get_user_strategy(env, user) -> Symbol` implemented +- [ ] Event + documentation: + - [x] `UserStrategyUpdatedEvent { user, old_strategy, new_strategy }` exists in contract + - [x] Document `UserStrategyUpdatedEvent` in `EVENTS.md` + - [x] Ensure event topic constant for strategy updates is canonical (avoid using `TOPIC_USER_CAP_UPDATED`) +- [ ] Tests: + - [x] Unit tests cover set/get roundtrip for all strategy symbols + - [x] Tests cover auth enforcement for unauthorized updates + - [x] Tests cover event payload correctness + - [x] Tests cover default strategy on first deposit + diff --git a/neurowealth-vault/contracts/vault/src/lib.rs b/neurowealth-vault/contracts/vault/src/lib.rs index ee568603..8f3f9ceb 100644 --- a/neurowealth-vault/contracts/vault/src/lib.rs +++ b/neurowealth-vault/contracts/vault/src/lib.rs @@ -133,6 +133,8 @@ use soroban_sdk::{ pub enum VaultError { /// Supplied min limit is negative. NegativeMin = 1, + /// Unsupported user strategy symbol. + UnsupportedStrategy = 48, /// Supplied max limit is negative. NegativeMax = 2, /// max must be greater than or equal to min. @@ -311,6 +313,9 @@ pub enum DataKey { /// The address of the Stellar DEX liquidity pool contract used by the /// Balanced/Growth strategies for on-chain liquidity provision. DexPool, + /// Per-user strategy symbol. + /// Controls how the agent should rebalance funds for this user. + UserStrategy(Address), } // ============================================================================ @@ -703,6 +708,15 @@ pub struct RebalanceFailedEvent { pub reason: Symbol, } +/// Emitted when a user updates their on-chain strategy preference. +#[allow(missing_docs)] +#[contracttype] +pub struct UserStrategyUpdatedEvent { + pub user: Address, + pub old_strategy: Symbol, + pub new_strategy: Symbol, +} + #[allow(missing_docs)] #[contracttype] pub struct UserInfo { @@ -771,6 +785,7 @@ pub(crate) const TOPIC_UNPAUSED: Symbol = symbol_short!("unpaused"); pub(crate) const TOPIC_EMERGENCY_PAUSED: Symbol = symbol_short!("emerg"); pub(crate) const TOPIC_TVL_CAP_UPDATED: Symbol = symbol_short!("tvl_cap"); pub(crate) const TOPIC_USER_CAP_UPDATED: Symbol = symbol_short!("user_cap"); +pub(crate) const TOPIC_USER_STRATEGY_UPDATED: Symbol = symbol_short!("user_str"); pub(crate) const TOPIC_LIMITS_UPDATED: Symbol = symbol_short!("l_upd"); pub(crate) const TOPIC_DEPOSIT_LIMITS_UPDATED: Symbol = symbol_short!("dep_lim"); pub(crate) const TOPIC_CAPS_UPDATED: Symbol = symbol_short!("caps_upd"); @@ -1177,10 +1192,17 @@ impl NeuroWealthVault { /// - If amount would exceed the TVL cap. /// - If the USDC transfer fails. /// - If shares to mint rounds down to zero. - pub fn deposit(env: Env, user: Address, amount: i128) { +pub fn deposit(env: Env, user: Address, amount: i128) { Self::require_initialized(&env); user.require_auth(); + // Default per-user strategy on first deposit. + if !env.storage().persistent().has(&DataKey::UserStrategy(user.clone())) { + env.storage() + .persistent() + .set(&DataKey::UserStrategy(user.clone()), &Self::strategy_symbol_balanced(&env)); + } + Self::require_not_paused(&env); Self::require_positive_amount(&env, amount); Self::require_minimum_deposit(&env, amount); @@ -3756,6 +3778,42 @@ impl NeuroWealthVault { /// let human_rate = rate as f64 / 10_000_000.0; // → 1.05 /// let user_assets = user_shares as f64 * human_rate; /// ``` +pub fn set_user_strategy(env: Env, user: Address, strategy: Symbol) { + Self::require_initialized(&env); + user.require_auth(); + + // Validate strategy symbol against allowlist + Self::validate_user_strategy(&env, &strategy); + + let old_strategy: Symbol = env + .storage() + .persistent() + .get(&DataKey::UserStrategy(user.clone())) + .unwrap_or(Self::strategy_symbol_balanced(&env)); + + env.storage() + .persistent() + .set(&DataKey::UserStrategy(user.clone()), &strategy); + + env.events().publish( + (TOPIC_USER_STRATEGY_UPDATED,), + UserStrategyUpdatedEvent { + user, + old_strategy, + new_strategy: strategy, + }, + ); + } + + /// Returns the configured strategy symbol for a user. + pub fn get_user_strategy(env: Env, user: Address) -> Symbol { + Self::require_initialized(&env); + env.storage() + .persistent() + .get(&DataKey::UserStrategy(user)) + .unwrap_or(Self::strategy_symbol_balanced(&env)) + } + pub fn get_exchange_rate(env: Env) -> i128 { Self::require_initialized(&env); @@ -3978,7 +4036,39 @@ impl NeuroWealthVault { } } + #[inline] + fn strategy_symbol_balanced(env: &Env) -> Symbol { + let _ = env; + symbol_short!("balanced") + } + + #[inline] + fn strategy_symbol_growth(env: &Env) -> Symbol { + let _ = env; + symbol_short!("growth") + } + + #[inline] + fn strategy_symbol_defensive(env: &Env) -> Symbol { + let _ = env; + symbol_short!("defens") + } + + fn validate_user_strategy(env: &Env, strategy: &Symbol) { + let supported = [ + Self::strategy_symbol_balanced(env), + Self::strategy_symbol_growth(env), + Self::strategy_symbol_defensive(env), + ]; + Self::require( + env, + supported.iter().any(|s| s == strategy), + VaultError::UnsupportedStrategy, + ); + } + /// Internal helper: convert assets (USDC) to shares using current totals. + /// Uses floor division - safe for deposits (user gets fewer shares, vault benefits). /// /// # Inflation-attack note diff --git a/neurowealth-vault/contracts/vault/src/tests/mod.rs b/neurowealth-vault/contracts/vault/src/tests/mod.rs index 6bd69483..12f7a174 100644 --- a/neurowealth-vault/contracts/vault/src/tests/mod.rs +++ b/neurowealth-vault/contracts/vault/src/tests/mod.rs @@ -31,4 +31,6 @@ mod test_ttl; mod test_update_total_assets_blend; mod test_withdraw; mod test_yield; +mod test_user_strategy; mod utils; + diff --git a/neurowealth-vault/contracts/vault/src/tests/test_user_strategy.rs b/neurowealth-vault/contracts/vault/src/tests/test_user_strategy.rs new file mode 100644 index 00000000..7d15f11e --- /dev/null +++ b/neurowealth-vault/contracts/vault/src/tests/test_user_strategy.rs @@ -0,0 +1,108 @@ +//! Per-user strategy storage and authorization tests + +use super::utils::*; +use crate::{ +NeuroWealthVaultClient, UserStrategyUpdatedEvent, TOPIC_USER_STRATEGY_UPDATED, +}; +use soroban_sdk::{symbol_short, testutils::Address as _, Address, Env, TryFromVal}; + +const STRATEGY_BALANCED: &str = "balanced"; +const STRATEGY_GROWTH: &str = "growth"; +const STRATEGY_DEFENSIVE: &str = "defens"; + +fn supported_symbols(_env: &Env) -> [soroban_sdk::Symbol; 3] { + [ + symbol_short!("balanced"), + symbol_short!("growth"), + symbol_short!("defens"), + ] +} + +#[test] +fn test_set_get_user_strategy_roundtrip_for_all_symbols() { + let env = Env::default(); + env.mock_all_auths(); + + let (contract_id, _agent, _owner, _usdc_token) = setup_vault_with_token(&env); + let client = NeuroWealthVaultClient::new(&env, &contract_id); + + let user = Address::generate(&env); + +for strat in supported_symbols(&env).iter() { + client.set_user_strategy(&user, strat); + let stored = client.get_user_strategy(&user); + assert_eq!(stored, strat.clone(), "stored strategy must match set strategy"); + } +} + +#[test] +fn test_set_user_strategy_requires_user_auth() { + let env = Env::default(); + env.mock_all_auths(); + + let (contract_id, _agent, _owner, _usdc_token) = setup_vault_with_token(&env); + let client = NeuroWealthVaultClient::new(&env, &contract_id); + + let user = Address::generate(&env); + let attacker = Address::generate(&env); + + env.mock_auths(&[]); + + let res = client.try_set_user_strategy(&attacker, &symbol_short!("growth")); + assert!(res.is_err(), "should fail without user authorization"); + + // Ensure attacker did not set + let stored = client.get_user_strategy(&attacker); + assert_eq!(stored, symbol_short!("balanced"), "unknown attacker should read default"); +} + +#[test] +fn test_set_user_strategy_event_emits_correct_old_and_new() { + let env = Env::default(); + env.mock_all_auths(); + + let (contract_id, _agent, _owner, _usdc_token) = setup_vault_with_token(&env); + let client = NeuroWealthVaultClient::new(&env, &contract_id); + + let user = Address::generate(&env); + + // First deposit not strictly required for the event, but default read + // should be "balanced". + let old = client.get_user_strategy(&user); + assert_eq!(old, symbol_short!("balanced")); + + client.set_user_strategy(&user, &symbol_short!("growth")); + + let events = find_events_by_topic(env.events().all(), &env, TOPIC_USER_STRATEGY_UPDATED); + assert_eq!(events.len(), 1, "one strategy update event expected"); + + let (_, _, data) = &events[0]; + let event = UserStrategyUpdatedEvent::try_from_val(&env, data) + .expect("Should decode UserStrategyUpdatedEvent"); + + assert_eq!(event.user, user); + assert_eq!(event.old_strategy, symbol_short!("balanced")); + assert_eq!(event.new_strategy, symbol_short!("growth")); +} + +#[test] +fn test_default_strategy_is_set_on_first_deposit() { + let env = Env::default(); + env.mock_all_auths(); + + let (contract_id, _agent, _owner, usdc_token) = setup_vault_with_token(&env); + let client = NeuroWealthVaultClient::new(&env, &contract_id); + + let user = Address::generate(&env); + let amount = 5_000_000_i128; + + // Ensure user strategy not previously set + let before = client.get_user_strategy(&user); + assert_eq!(before, symbol_short!("balanced")); + + mint_and_deposit(&env, &client, &usdc_token, &user, amount); + + let after = client.get_user_strategy(&user); + assert_eq!(after, symbol_short!("balanced")); +} +