Skip to content
Open
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
68 changes: 68 additions & 0 deletions contracts/savings_vault/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,74 @@ use soroban_sdk::{
token, Address, Env, Symbol, Vec,
};

#[cfg(test)]
mod can_withdraw_default_tests {
use super::*;
use soroban_sdk::testutils::{Address as _, Env as _};

#[test]
fn can_withdraw_returns_false_for_new_user() {
let env = Env::default();
env.mock_all_auths();

let admin = Address::generate(&env);
let token = Address::generate(&env);
let contract_id = env.register_contract(None, SavingsVault);
let client = SavingsVaultClient::new(&env, &contract_id);
client.initialize(&admin, &token);

let user = Address::generate(&env);

assert!(!client.can_withdraw(&user));
}

#[test]
fn can_withdraw_returns_false_for_user_with_available_balance_and_no_lock() {
let env = Env::default();
env.mock_all_auths();

let token_admin = Address::generate(&env);
let token = env.register_stellar_asset_contract(token_admin);
let admin = Address::generate(&env);
let contract_id = env.register_contract(None, SavingsVault);
let client = SavingsVaultClient::new(&env, &contract_id);
client.initialize(&admin, &token);

let user = Address::generate(&env);
soroban_sdk::token::StellarAssetClient::new(&env, &token).mint(&user, &1_000_i128);

client.deposit(&user, &250_i128);

assert_eq!(client.get_balance(&user), 250_i128);
assert!(!client.can_withdraw(&user));
}

#[test]
fn can_withdraw_returns_true_after_lock_matures() {
let env = Env::default();
env.mock_all_auths();

let token_admin = Address::generate(&env);
let token = env.register_stellar_asset_contract(token_admin);
let admin = Address::generate(&env);
let contract_id = env.register_contract(None, SavingsVault);
let client = SavingsVaultClient::new(&env, &contract_id);
client.initialize(&admin, &token);

let user = Address::generate(&env);
soroban_sdk::token::StellarAssetClient::new(&env, &token).mint(&user, &1_000_i128);

env.ledger().set_timestamp(1_000_u64);
client.deposit(&user, &500_i128);
client.lock_funds(&user, &200_i128, &2_000_u64);

assert!(!client.can_withdraw(&user));

env.ledger().set_timestamp(2_000_u64);
assert!(client.can_withdraw(&user));
}
}

const MAX_LOCK_PAGE_SIZE: u32 = 50;

// ---------------------------------------------------------------------------
Expand Down
29 changes: 29 additions & 0 deletions contracts/savings_vault/src/test/lock_maturity_boundary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,32 @@ fn test_multiple_locks_independent_boundaries() {
assert!(f.client.can_withdraw(&f.user));
f.client.withdraw_lock(&f.user, &lock_2);
}

// =========================================================================
// 8. Default `can_withdraw` State
// =========================================================================

/// Verifies that `can_withdraw` returns false for a brand-new user with no
/// balance and no lock.
#[test]
fn test_can_withdraw_new_user_default_false() {
let f = setup_boundary_fixture(1_000);
let fresh_user = new_user(&f.env);

assert!(
!f.client.can_withdraw(&fresh_user),
"can_withdraw must default to false for a user with no balance and no lock"
);
}

/// Verifies that `can_withdraw` returns false when the user only has available
/// (unlocked) balance and no lock has been created.
#[test]
fn test_can_withdraw_available_balance_no_lock_default_false() {
let f = setup_boundary_fixture(1_000);

assert!(
!f.client.can_withdraw(&f.user),
"can_withdraw must default to false when no lock exists, even with available balance"
);
}
34 changes: 34 additions & 0 deletions contracts/savings_vault/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,26 @@ fn test_withdraw_entire_balance() {
assert_eq!(token_client.balance(&user), deposit_amount);
}

#[test]
fn test_can_withdraw_new_user_returns_false() {
let env = test_env();
let (contract_id, client) = init_contract(&env);
let (env, _admin, client, _token_client, _token_admin) = test_token(env, contract_id, client);
let user = new_user(&env);
assert_eq!(client.can_withdraw(&user), false);
}

#[test]
fn test_can_withdraw_available_balance_no_lock_returns_true() {
let env = test_env();
let (contract_id, client) = init_contract(&env);
let (env, _admin, client, _token_client, token_admin) = test_token(env, contract_id, client);
let user = new_user(&env);
token_admin.mint(&user, &1000);
deposit_balance(&client, &user, 100);
assert_eq!(client.can_withdraw(&user), true);
}

#[test]
#[should_panic]
fn test_withdraw_requires_user_authorization() {
Expand Down Expand Up @@ -1057,6 +1077,20 @@ fn test_can_withdraw_no_locked_funds() {
assert_eq!(client.can_withdraw(&user), false);
}

/// A user with only available balance and no lock cannot withdraw.
#[test]
fn test_can_withdraw_available_balance_no_lock_returns_false() {
let env = test_env();
let (contract_id, client) = init_contract(&env);
let (env, _admin, client, _token_client, token_admin) = test_token(env, contract_id, client);
let user = new_user(&env);
token_admin.mint(&user, &1000);
client.deposit(&user, &100);
assert_eq!(client.get_balance(&user), 100);
assert_eq!(client.get_locked_balance(&user), 0);
assert_eq!(client.can_withdraw(&user), false);
}

// -------------------------------------------------------------------------
// Locked balance correctness across boundary checks
// -------------------------------------------------------------------------
Expand Down
21 changes: 21 additions & 0 deletions contracts/savings_vault/src/test/test_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,24 @@ pub fn strict_setup() -> (Env, Address, SavingsVaultClient<'static>) {
let client = SavingsVaultClient::new(&env, &contract_id);
(env, contract_id, client)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn can_withdraw_returns_false_for_new_user() {
let (env, _, client) = setup();
let user = new_user(&env);
assert!(!client.can_withdraw(&user));
}

#[test]
fn can_withdraw_returns_true_for_available_balance_without_lock() {
let (env, _, client) = setup();
let user = new_user(&env);
deposit_balance(&client, &user, 100);
assert!(client.can_withdraw(&user));
}
}

64 changes: 62 additions & 2 deletions contracts/savings_vault/src/test/withdraw_lock.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,65 @@
use super::*;
use soroban_sdk::{testutils::Address as _, testutils::Ledger, Address, Env};
use soroban_sdk::{
testutils::Address as _,
testutils::Ledger,
Address,
Env,
};

#[test]
fn test_can_withdraw_new_user_returns_false() {
let (env, _contract_id, client) = setup();
let user = new_user(&env);
set_ledger_timestamp(&env, 1000);

assert!(!client.can_withdraw(&user));
}

#[test]
fn test_can_withdraw_available_balance_no_lock_returns_true() {
let (env, contract_id, client) = setup();
let (env, _admin, client, _token_client, token_admin) = test_token(env, contract_id, client);
let user = new_user(&env);
set_ledger_timestamp(&env, 1000);

token_admin.mint(&user, &1000);
client.deposit(&user, &1000);

assert (client.can_withdraw(&user));
}

#[test]
fn test_can_withdraw_only_locked_balance_returns_false() {
let (env, contract_id, client) = setup();
let (env, _admin, client, _token_client, token_admin) = test_token(env, contract_id, client);
let user = new_user(&env);

set_ledger_timestamp(&env, 1000);
token_admin.mint(&user, &1000);
client.deposit(&user, &1000);

// Lock the entire balance
client.lock_funds(&user, &1000, &2000);

// No available balance; cannot withdraw
assert!(!client.can_withdraw(&user));
}

#[test]
fn test_can_withdraw_available_balance_with_lock_returns_true() {
let (env, contract_id, client) = setup();
let (env, _admin, client, _token_client, token_admin) = test_token(env, contract_id, client);
let user = new_user(&env);

set_ledger_timestamp(&env, 1000);
token_admin.mint(&user, &1000);
client.deposit(&user, &1000);

// Lock part of the balance, leaving 600 available
client.lock_funds(&user, &400, &2000);

assert (client.can_withdraw(&user));
}

#[test]
fn test_withdraw_matured_lock_success() {
Expand Down Expand Up @@ -125,4 +185,4 @@ fn test_unauthorized_withdraw_lock_fails() {

// Call without auth mocking: require_auth() must reject this withdrawal.
client.withdraw_lock(&user, &1);
}
}
Loading