Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,9 @@ jobs:
- name: Verify DataKey docs match lib.rs
run: bash scripts/check-data-key-docs.sh

- name: Verify every test_*.rs file is registered in tests/mod.rs
run: bash scripts/check-test-registration.sh

changes:
name: Detect changes
runs-on: ubuntu-latest
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ This changelog is tied to the vault contract `Version` storage value. Each relea

## [Unreleased]
<!-- Add entries below. Format: `- Short description (Issue #N).` -->
- **Deprecate `set_limits` (Issue #991; no behavior change):** `set_limits` is
marked `#[deprecated]` in favor of `set_caps` (or `set_tvl_cap`/
`set_user_deposit_cap`/`set_deposit_limits` individually), which use
clearer parameter names and a consistent `panic_with_error!` convention
instead of `set_limits`'s `Result`-returning style. See `DEPRECATION.md`
for the migration path. `set_limits` itself is unchanged and still
functions; only tests that exercise its own access control still call it.
- **WhatsApp bot hardening (Issues #943, #944, #945, #946; no contract change):**
OTP challenges and sessions are persisted in Postgres (`whatsapp_otps`,
`whatsapp_sessions`) with atomic, replica-safe operations; custodial wallet
Expand Down
780 changes: 387 additions & 393 deletions neurowealth-vault/contracts/vault/src/lib.rs

Large diffs are not rendered by default.

16 changes: 11 additions & 5 deletions neurowealth-vault/contracts/vault/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod test_access_control;
mod test_adversarial_agent_simulation;
mod test_agent_compromise_scenarios;
mod test_agent_timelock;
mod test_allowance_hygiene;
mod test_approval_ttl;
Expand All @@ -15,6 +16,7 @@ mod test_blend_integration;
mod test_budget;
mod test_checked_arithmetic;
mod test_circuit_breaker;
mod test_decrease_cap_proptest;
// Pre-existing compile failures (stale APIs / missing traits); not harvest coverage.
// mod test_concurrent_timelocks;
mod test_deposit;
Expand All @@ -30,6 +32,7 @@ mod test_exchange_rate;
mod test_fuzz_deposit_withdraw;
mod test_get_user_strategy_unset;
mod test_harvest;
mod test_holding_period_coverage;
mod test_index_pruning;
mod test_inflation_attack;
mod test_initialize;
Expand All @@ -40,6 +43,7 @@ mod test_multi_asset;
mod test_multi_protocol;
mod test_multi_user_concurrent;
mod test_no_privilege_escalation_proptest;
mod test_ownership;
mod test_pause;
mod test_pool_config_idempotent;
mod test_pool_rotation_with_deployed_funds;
Expand All @@ -64,6 +68,7 @@ mod test_standby_agent;
mod test_storage_griefing_analysis;
mod test_strategy_switch_low_liquidity;
mod test_total_assets_cap;
mod test_total_shares_invariant_proptest;
mod test_transfer_delta_sanity;
mod test_ttl;
mod test_ttl_extension_failures;
Expand All @@ -88,15 +93,16 @@ mod test_performance_fee;
// ANSI-escape-corrupted and `#`-less `#[test]` attributes, bare `assert_eq(`
// calls, and a stale `setup_vault()` call shape rewritten against the real
// fixture API in `utils.rs`. Both now test real, existing entry points.
// `test_user_apy` likewise calls the real, existing `get_user_apy` entry
// point and is registered below.
mod test_insurance_fund;
mod test_pause_queue_breaker_matrix_proptest;
mod test_user_apy;
mod test_withdrawal_queue;

// `test_user_apy` and `test_min_withdraw`/`test_min_withdrawal` stay disabled:
// they call `get_user_deposit_timestamp`/`get_user_realized_apy` and
// `test_min_withdraw`/`test_min_withdrawal` stay disabled: they call
// `set_min_withdrawal`/`get_min_withdrawal`/`VaultError::BelowMinimumWithdrawal`,
// none of which exist anywhere in the contract. Orphaned tests for features
// that were never implemented, not harvested for coverage.
// mod test_user_apy;
// none of which exist anywhere in the contract. Orphaned tests for a feature
// that was never implemented, not harvested for coverage.
// mod test_min_withdraw;
// mod test_min_withdrawal;
100 changes: 64 additions & 36 deletions neurowealth-vault/contracts/vault/src/tests/test_access_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
use super::utils::*;
use crate::{AgentUpdatedEvent, TOPIC_AGENT_UPDATED};
use soroban_sdk::{
symbol_short, testutils::Address as _, testutils::Ledger as _, Address, BytesN, Env, TryFromVal,
symbol_short,
testutils::{Address as _, Ledger as _},
Address, BytesN, Env, TryFromVal,
};

/// Keeps storage entries alive across the large ledger jumps these tests make.
/// Keeps storage entries alive across the large ledger jumps some of these
/// tests make (mirrors the helper in `test_share_locking.rs`).
fn keep_storage_alive(env: &Env) {
env.ledger().with_mut(|li| {
li.min_persistent_entry_ttl = 1_000_000;
Expand Down Expand Up @@ -116,17 +119,16 @@ fn test_owner_can_update_agent() {
let (contract_id, old_agent, _owner, _usdc_token) = setup_vault_with_token(&env);
let client = NeuroWealthVaultClient::new(&env, &contract_id);

// update_agent only PROPOSES the change (step 1 of 2, issue #317); the
// active agent is unchanged until confirm_agent_update() is called after
// the timelock elapses.
// `update_agent` only proposes the change; it takes effect after the
// timelock elapses and `confirm_agent_update` is called (#317).
let new_agent = Address::generate(&env);
client.update_agent(&new_agent);

assert_eq!(
client.get_agent(),
old_agent,
"agent must not change until confirmed"
"active agent must not change until the timelock is confirmed"
);

let (pending_addr, expiry) = client
.get_pending_agent_update()
.expect("pending agent update should be recorded");
Expand All @@ -149,12 +151,11 @@ fn test_update_agent_emits_event() {
let (contract_id, old_agent, _owner, _usdc_token) = setup_vault_with_token(&env);
let client = NeuroWealthVaultClient::new(&env, &contract_id);

// update_agent only proposes (step 1 of 2, issue #317); TOPIC_AGENT_UPDATED /
// AgentUpdatedEvent is only emitted (for backward compatibility) by
// confirm_agent_update() once the timelock has elapsed.
// `TOPIC_AGENT_UPDATED` is the backward-compatible event emitted by
// `confirm_agent_update` once the timelock elapses, not by the
// `update_agent` proposal step itself (#317).
let new_agent = Address::generate(&env);
client.update_agent(&new_agent);

let (_, expiry) = client.get_pending_agent_update().unwrap();
env.ledger().set_sequence_number(expiry);
client.confirm_agent_update();
Expand Down Expand Up @@ -454,11 +455,11 @@ fn test_agent_can_rebalance() {
let env = Env::default();
env.mock_all_auths();

let (contract_id, _agent, _owner, _usdc_token) = setup_vault_with_token(&env);
let (contract_id, agent, _owner, _usdc_token) = setup_vault_with_token(&env);
let client = NeuroWealthVaultClient::new(&env, &contract_id);

// "none" protocol is always safe — no external pool required
client.rebalance(&_agent, &symbol_short!("none"), &500_i128, &0_i128);
client.rebalance(&agent, &symbol_short!("none"), &500_i128, &0_i128);

assert_eq!(
client.get_current_protocol(),
Expand Down Expand Up @@ -608,17 +609,18 @@ fn test_pending_owner_cannot_cancel_ownership_transfer() {

assert_eq!(client.get_pending_owner().unwrap(), pending_owner);

// The pending owner tries to cancel — should fail with CallerIsNotOwner
// `cancel_ownership_transfer` has no explicit caller parameter — it
// enforces ownership via `require_is_owner`'s `owner.require_auth()` on
// the stored owner address, which `mock_all_auths()` would trivially
// satisfy regardless of who "calls" it. Revoke all auth so the guard can
// be observed rejecting a non-owner caller (mirrors
// `test_non_owner_cannot_set_deposit_limits`).
env.mock_auths(&[]);
let result = client.try_cancel_ownership_transfer();
assert!(
result.is_err(),
"pending owner should not be able to cancel ownership transfer"
);
assert_eq!(
result,
Err(Ok(soroban_sdk::Error::from_contract_error(34))),
"should fail with CallerIsNotOwner error"
);
}

/// Test that an uninvolved third party cannot cancel an ownership transfer (Issue #573).
Expand All @@ -634,18 +636,17 @@ fn test_third_party_cannot_cancel_ownership_transfer() {
let pending_owner = Address::generate(&env);
client.transfer_ownership(&pending_owner);

// An uninvolved third party tries to cancel — should fail with CallerIsNotOwner
// An uninvolved third party tries to cancel — should fail with
// CallerIsNotOwner. See the comment in
// `test_pending_owner_cannot_cancel_ownership_transfer`: revoke all auth
// since `cancel_ownership_transfer` has no explicit caller parameter.
let third_party = Address::generate(&env);
env.mock_auths(&[]);
let result = client.try_cancel_ownership_transfer();
assert!(
result.is_err(),
"third party should not be able to cancel ownership transfer"
);
assert_eq!(
result,
Err(Ok(soroban_sdk::Error::from_contract_error(34))),
"should fail with CallerIsNotOwner error"
);
}

#[test]
Expand Down Expand Up @@ -834,11 +835,11 @@ fn test_rebalance_blocked_while_paused() {
let env = Env::default();
env.mock_all_auths();

let (contract_id, _agent, owner, _usdc_token) = setup_vault_with_token(&env);
let (contract_id, agent, owner, _usdc_token) = setup_vault_with_token(&env);
let client = NeuroWealthVaultClient::new(&env, &contract_id);

client.pause(&owner);
client.rebalance(&_agent, &symbol_short!("none"), &500_i128, &0_i128);
client.rebalance(&agent, &symbol_short!("none"), &500_i128, &0_i128);
}

// ============================================================================
Expand Down Expand Up @@ -1019,9 +1020,16 @@ fn test_agent_cannot_set_tvl_cap_during_normal_state() {
let env = Env::default();
env.mock_all_auths();

let (contract_id, agent, _owner, _usdc_token) = setup_vault_with_token(&env);
let (contract_id, _agent, _owner, _usdc_token) = setup_vault_with_token(&env);
let client = NeuroWealthVaultClient::new(&env, &contract_id);

// `set_tvl_cap` takes no explicit caller parameter — it enforces
// ownership via `require_is_owner`'s `owner.require_auth()` on the
// stored owner address, which `mock_all_auths()` would trivially
// satisfy regardless of who "calls" it. Revoke all auth so the guard
// can be observed rejecting a non-owner caller (mirrors
// `test_non_owner_cannot_set_tvl_cap`).
env.mock_auths(&[]);
let result = client.try_set_tvl_cap(&50_000_000_000_i128);
assert!(
result.is_err(),
Expand All @@ -1034,9 +1042,12 @@ fn test_agent_cannot_set_user_deposit_cap_during_normal_state() {
let env = Env::default();
env.mock_all_auths();

let (contract_id, agent, _owner, _usdc_token) = setup_vault_with_token(&env);
let (contract_id, _agent, _owner, _usdc_token) = setup_vault_with_token(&env);
let client = NeuroWealthVaultClient::new(&env, &contract_id);

// See the comment in `test_agent_cannot_set_tvl_cap_during_normal_state`:
// `set_user_deposit_cap` has no explicit caller parameter either.
env.mock_auths(&[]);
let result = client.try_set_user_deposit_cap(&10_000_000_000_i128);
assert!(
result.is_err(),
Expand All @@ -1049,9 +1060,12 @@ fn test_agent_cannot_set_deposit_limits_during_normal_state() {
let env = Env::default();
env.mock_all_auths();

let (contract_id, agent, _owner, _usdc_token) = setup_vault_with_token(&env);
let (contract_id, _agent, _owner, _usdc_token) = setup_vault_with_token(&env);
let client = NeuroWealthVaultClient::new(&env, &contract_id);

// See the comment in `test_agent_cannot_set_tvl_cap_during_normal_state`:
// `set_deposit_limits` has no explicit caller parameter either.
env.mock_auths(&[]);
let result = client.try_set_deposit_limits(&2_000_000_i128, &50_000_000_000_i128);
assert!(
result.is_err(),
Expand All @@ -1077,9 +1091,12 @@ fn test_agent_cannot_set_rebalance_cooldown_during_normal_state() {
let env = Env::default();
env.mock_all_auths();

let (contract_id, agent, _owner, _usdc_token) = setup_vault_with_token(&env);
let (contract_id, _agent, _owner, _usdc_token) = setup_vault_with_token(&env);
let client = NeuroWealthVaultClient::new(&env, &contract_id);

// See the comment in `test_agent_cannot_set_tvl_cap_during_normal_state`:
// `set_rebalance_cooldown` has no explicit caller parameter either.
env.mock_auths(&[]);
let result = client.try_set_rebalance_cooldown(&10_u32);
assert!(
result.is_err(),
Expand All @@ -1092,9 +1109,12 @@ fn test_agent_cannot_set_approval_ttl_during_normal_state() {
let env = Env::default();
env.mock_all_auths();

let (contract_id, agent, _owner, _usdc_token) = setup_vault_with_token(&env);
let (contract_id, _agent, _owner, _usdc_token) = setup_vault_with_token(&env);
let client = NeuroWealthVaultClient::new(&env, &contract_id);

// See the comment in `test_agent_cannot_set_tvl_cap_during_normal_state`:
// `set_approval_ttl` has no explicit caller parameter either.
env.mock_auths(&[]);
let result = client.try_set_approval_ttl(&2_000_u32);
assert!(
result.is_err(),
Expand Down Expand Up @@ -1160,13 +1180,17 @@ fn test_agent_cannot_set_tvl_cap_during_pending_agent_timelock() {
let env = Env::default();
env.mock_all_auths();

let (contract_id, agent, _owner, _usdc_token) = setup_vault_with_token(&env);
let (contract_id, _agent, _owner, _usdc_token) = setup_vault_with_token(&env);
let client = NeuroWealthVaultClient::new(&env, &contract_id);

let new_agent = Address::generate(&env);
client.update_agent(&new_agent);

// During pending timelock, agent cannot set TVL cap.
// `set_tvl_cap` has no explicit caller parameter (see the comment in
// `test_agent_cannot_set_tvl_cap_during_normal_state`), so revoke all
// auth to observe the owner-only guard reject a non-owner caller even
// while an agent update is pending.
env.mock_auths(&[]);
let result = client.try_set_tvl_cap(&50_000_000_000_i128);
assert!(
result.is_err(),
Expand All @@ -1179,13 +1203,17 @@ fn test_agent_cannot_set_deposit_limits_during_pending_agent_timelock() {
let env = Env::default();
env.mock_all_auths();

let (contract_id, agent, _owner, _usdc_token) = setup_vault_with_token(&env);
let (contract_id, _agent, _owner, _usdc_token) = setup_vault_with_token(&env);
let client = NeuroWealthVaultClient::new(&env, &contract_id);

let new_agent = Address::generate(&env);
client.update_agent(&new_agent);

// During pending timelock, agent cannot set deposit limits.
// `set_deposit_limits` has no explicit caller parameter (see the comment
// in `test_agent_cannot_set_tvl_cap_during_normal_state`), so revoke all
// auth to observe the owner-only guard reject a non-owner caller even
// while an agent update is pending.
env.mock_auths(&[]);
let result = client.try_set_deposit_limits(&2_000_000_i128, &50_000_000_000_i128);
assert!(
result.is_err(),
Expand Down
Loading