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
252 changes: 119 additions & 133 deletions escrow/src/lib.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion escrow/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ pub fn deploy_with_id(env: &Env) -> (Address, LiquifactEscrowClient<'_>) {

pub fn setup(env: &Env) -> (LiquifactEscrowClient<'_>, Address, Address) {
let mut ledger_info = env.ledger().get();
ledger_info.timestamp = 12345;
ledger_info.timestamp = 0;
ledger_info.sequence_number = 100;
env.ledger().set(ledger_info);
env.mock_all_auths();
Expand Down
216 changes: 206 additions & 10 deletions escrow/src/tests/admin.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::*;
use crate::{
AdminAcceptedEvent, AdminProposalCancelled, AdminProposedEvent, EscrowCloseSnapshot,
FundingTargetUpdated, RegistryRefRebound,
FundingTargetUpdated, RegistryRefRebound, DEFAULT_MATURITY_MAX_HORIZON_SECS,
};

use soroban_sdk::Event;
Expand Down Expand Up @@ -460,12 +460,13 @@ fn test_admin_handover_lifecycle() {
// 2. Accept admin (verifying the events)
let contract_id = client.address.clone();
let updated = client.accept_admin();
let accept_events = env.events().all();
assert_eq!(updated.admin, new_admin.clone());
assert_eq!(client.get_pending_admin(), None);

// Verify AdminAcceptedEvent
assert_eq!(
env.events().all().events().last().unwrap().clone(),
accept_events.events().last().unwrap().clone(),
crate::AdminAcceptedEvent {
name: symbol_short!("adm_acc"),
invoice_id: client.get_escrow().invoice_id,
Expand Down Expand Up @@ -1071,7 +1072,7 @@ fn test_update_funding_target_equal_to_funded_amount_succeeds() {
let updated = client.update_funding_target(&4_000i128);
assert_eq!(updated.funding_target, 4_000i128);
assert_eq!(updated.funded_amount, 4_000i128);
assert_eq!(updated.status, 0);
assert_eq!(updated.status, 1);
}

/// Passing a negative value must panic with "Target must be strictly positive".
Expand Down Expand Up @@ -1595,11 +1596,12 @@ fn test_rotate_beneficiary_success_dual_auth() {
let contract_id = client.address.clone();

let updated = client.rotate_beneficiary(&new_sme);
let rotate_events = env.events().all();
assert_eq!(updated.sme_address, new_sme);
assert_eq!(client.get_escrow().sme_address, new_sme);

assert_eq!(
env.events().all().events().last().unwrap().clone(),
rotate_events.events().last().unwrap().clone(),
crate::BeneficiaryRotated {
name: symbol_short!("ben_rot"),
invoice_id: client.get_escrow().invoice_id,
Expand Down Expand Up @@ -1801,7 +1803,6 @@ fn test_rebind_registry_ref_sets_and_clears() {
let (client, admin, sme) = setup(&env);

let contract_id = client.address.clone();
let invoice_id = client.get_escrow().invoice_id.clone();

let reg1 = Address::generate(&env);
let reg2 = Address::generate(&env);
Expand All @@ -1827,6 +1828,8 @@ fn test_rebind_registry_ref_sets_and_clears() {
&None,
);

let invoice_id = client.get_escrow().invoice_id.clone();

// Set to reg1
client.rebind_registry_ref(&Some(reg1.clone()));
assert_eq!(client.get_registry_ref(), Some(reg1.clone()));
Expand All @@ -1837,12 +1840,13 @@ fn test_rebind_registry_ref_sets_and_clears() {

// Clear to None
client.rebind_registry_ref(&None);
let clear_events = env.events().all();
assert_eq!(client.get_registry_ref(), None);

// Event sanity: last event should be clear (registry == None)
let last = env.events().all().events().last().unwrap().clone();
let last = clear_events.events().last().unwrap().clone();
let expected = crate::RegistryRefRebound {
name: symbol_short!("reg_rebind"),
name: Symbol::new(&env, "reg_rebind"),
invoice_id: invoice_id.clone(),
registry: None,
}
Expand All @@ -1856,12 +1860,13 @@ fn test_rebind_registry_ref_sets_and_clears() {

// Clear using clear_registry_ref
client.clear_registry_ref();
let clear2_events = env.events().all();
assert_eq!(client.get_registry_ref(), None);

// Event sanity: last event should be clear (registry == None) from clear_registry_ref
let last = env.events().all().events().last().unwrap().clone();
let last = clear2_events.events().last().unwrap().clone();
let expected = crate::RegistryRefRebound {
name: symbol_short!("reg_rebind"),
name: Symbol::new(&env, "reg_rebind"),
invoice_id,
registry: None,
}
Expand Down Expand Up @@ -2002,6 +2007,198 @@ fn test_error_code_uniqueness() {
}
}

// ── update_maturity_max_horizon: bounds, admin gate, event emission ──────────

/// `update_maturity_max_horizon` must require admin authorization; a caller
/// without the admin credential must panic.
#[test]
#[should_panic]
fn test_update_maturity_max_horizon_unauthorized() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let sme = Address::generate(&env);
let client = deploy(&env);
client.init(
&admin,
&soroban_sdk::String::from_str(&env, "HORI_U"),
&sme,
&1_000i128,
&500i64,
&0u64,
&Address::generate(&env),
&None,
&Address::generate(&env),
&None,
&None,
&None,
&None,
&None,
&None,
&None,
&None,
);
env.mock_auths(&[]);
client.update_maturity_max_horizon(&3_600u64);
}

/// `update_maturity_max_horizon` must emit a `MaturityMaxHorizonUpdated` event
/// (topic `"mtry_max"`) carrying the previous horizon and the new value.
#[test]
fn test_update_maturity_max_horizon_emits_event() {
use soroban_sdk::testutils::Events as _;

let env = Env::default();
let (client, admin, sme) = setup(&env);
let contract_id = client.address.clone();
client.init(
&admin,
&soroban_sdk::String::from_str(&env, "HORI_E"),
&sme,
&1_000i128,
&500i64,
&0u64,
&Address::generate(&env),
&None,
&Address::generate(&env),
&None,
&None,
&None,
&None,
&None,
&None,
&None,
&None,
);

let new_horizon = 3_600u64;
client.update_maturity_max_horizon(&new_horizon);

assert_eq!(
env.events().all().events().last().unwrap().clone(),
crate::MaturityMaxHorizonUpdated {
name: symbol_short!("mtry_max"),
invoice_id: client.get_escrow().invoice_id,
old_horizon: DEFAULT_MATURITY_MAX_HORIZON_SECS,
new_horizon,
}
.to_xdr(&env, &contract_id)
);
}

/// When no explicit horizon has been stored, `get_maturity_max_horizon` must
/// fall back to `DEFAULT_MATURITY_MAX_HORIZON_SECS`.
#[test]
fn test_update_maturity_max_horizon_default_fallback() {
let env = Env::default();
let (client, admin, sme) = setup(&env);
default_init(&client, &env, &admin, &sme);
assert_eq!(
client.get_maturity_max_horizon(),
DEFAULT_MATURITY_MAX_HORIZON_SECS,
);
}

/// Lowering the horizon must NOT retroactively invalidate an already-set
/// maturity. A subsequent `update_maturity` call targeting `now + new_horizon + 1`
/// must be rejected; the stored maturity must remain intact throughout.
#[test]
fn test_lowered_horizon_existing_maturity_untouched_and_far_update_rejected() {
let env = Env::default();
let (client, admin, sme) = setup(&env);
let (token, treasury) = free_addresses(&env);
env.ledger().set_timestamp(1_000);

client.init(
&admin,
&soroban_sdk::String::from_str(&env, "HORI_R"),
&sme,
&1_000i128,
&500i64,
&2_000u64, // maturity within the default 5-year horizon
&token,
&None,
&treasury,
&None,
&None,
&None,
&None,
&None,
&None,
&None,
&None,
);

assert_eq!(client.get_escrow().maturity, 2_000u64);

// Lower the horizon to 500 s: new ceiling = now(1_000) + 500 = 1_500.
// The existing maturity (2_000) is beyond that ceiling but must survive unchanged.
client.update_maturity_max_horizon(&500u64);
assert_eq!(
client.get_escrow().maturity,
2_000u64,
"lowering the horizon must not retroactively invalidate an already-set maturity"
);

// A subsequent update to 1_501 (one second beyond the new ceiling) must fail.
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
client.update_maturity(&1_501u64);
}))
.is_err(),
"update_maturity beyond the new horizon must be rejected"
);

// The stored maturity must remain 2_000 after the failed update attempt.
assert_eq!(
client.get_escrow().maturity,
2_000u64,
"a rejected update_maturity call must not alter the stored maturity"
);
}

/// After lowering the horizon, an `update_maturity` to a ledger time within
/// `[now, now + new_horizon]` must still succeed.
#[test]
fn test_lowered_horizon_allows_within_horizon_maturity_update() {
let env = Env::default();
let (client, admin, sme) = setup(&env);
let (token, treasury) = free_addresses(&env);
env.ledger().set_timestamp(1_000);

client.init(
&admin,
&soroban_sdk::String::from_str(&env, "HORI_W"),
&sme,
&1_000i128,
&500i64,
&0u64,
&token,
&None,
&treasury,
&None,
&None,
&None,
&None,
&None,
&None,
&None,
&None,
);

// Lower the horizon to 2 hours.
client.update_maturity_max_horizon(&7_200u64);
assert_eq!(client.get_maturity_max_horizon(), 7_200u64);

// 1 hour from now (3_600 s) is within the new 2-hour horizon.
let near_maturity = 1_000u64 + 3_600u64;
let updated = client.update_maturity(&near_maturity);
assert_eq!(
updated.maturity, near_maturity,
"maturity within the new horizon must be accepted"
);
}

/// Verify key-rotation recovery flow:
/// 1. Old admin sets a legal hold.
/// 2. Old admin initiates handover to new admin.
Expand Down Expand Up @@ -2055,4 +2252,3 @@ fn test_post_handover_admin_can_clear_hold_set_by_old_admin() {
client.clear_legal_hold();
assert!(!client.get_legal_hold());
}

8 changes: 6 additions & 2 deletions escrow/src/tests/auth_matrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ fn setup_inited(
&None,
&None,
&None,
&None);
&None,
);
(client, admin, sme, treasury, token)
}

Expand All @@ -60,7 +61,10 @@ macro_rules! assert_no_auth_panics {
($env:expr, $call:expr) => {{
$env.mock_auths(&[]);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| $call));
assert!(result.is_err(), "expected panic with no auth, but call succeeded");
assert!(
result.is_err(),
"expected panic with no auth, but call succeeded"
);
}};
}

Expand Down
Loading
Loading