From a388669854c53052a383cfb23082f76c17b48e52 Mon Sep 17 00:00:00 2001 From: James Akolo Date: Mon, 27 Jul 2026 16:49:41 +0100 Subject: [PATCH 1/4] test: add a test for a two-solver race on the same intent_id (#70) --- intent_settlement/src/test.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/intent_settlement/src/test.rs b/intent_settlement/src/test.rs index 74e0521..9a34053 100644 --- a/intent_settlement/src/test.rs +++ b/intent_settlement/src/test.rs @@ -721,6 +721,34 @@ fn cannot_accept_already_accepted_intent() { assert_eq!(res, Err(Ok(Error::IntentNotOpen.into()))); } +#[test] +fn two_solver_race_on_same_intent_id() { + let ctx = setup(); + let c = ctx.client(); + + // Register two solvers + ctx.register_solver(); + let solver2 = Address::generate(&ctx.env); + ctx.bond_admin().mint(&solver2, &BOND); + c.register_solver(&solver2, &BOND); + + // Submit an intent + let id = ctx.submit(); + + // First solver accepts successfully + c.accept_intent(&ctx.solver, &id); + let solver1_record = c.get_solver(&ctx.solver).unwrap(); + assert_eq!(solver1_record.active_intents, 1); + + // Second solver tries to accept the same intent — should fail + let res = c.try_accept_intent(&solver2, &id); + assert_eq!(res, Err(Ok(Error::IntentNotOpen.into()))); + + // Verify second solver's active_intents was never incremented + let solver2_record = c.get_solver(&solver2).unwrap(); + assert_eq!(solver2_record.active_intents, 0); +} + // ─── Fill guards ──────────────────────────────────────────────────────────────── #[test] From a79a001082a9985cad02c6eeebcc6fb0c76694f4 Mon Sep 17 00:00:00 2001 From: James Akolo Date: Mon, 27 Jul 2026 16:50:03 +0100 Subject: [PATCH 2/4] test: add a boundary test for withdraw_bond leaving exactly MIN_BOND remaining (#69) --- intent_settlement/src/test.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/intent_settlement/src/test.rs b/intent_settlement/src/test.rs index 9a34053..80b7719 100644 --- a/intent_settlement/src/test.rs +++ b/intent_settlement/src/test.rs @@ -358,6 +358,35 @@ fn withdraw_bond_below_min_bond_fails() { assert_eq!(res, Err(Ok(Error::SolverBondTooLow.into()))); } +#[test] +fn withdraw_bond_leaving_exactly_min_bond_succeeds() { + let ctx = setup(); + let c = ctx.client(); + ctx.register_solver(); + + // Withdraw exactly the amount that leaves MIN_BOND remaining + let withdraw_amount = BOND - MIN_BOND; + c.withdraw_bond(&ctx.solver, &withdraw_amount); + + let record = c.get_solver(&ctx.solver).unwrap(); + assert_eq!(record.bond_amount, MIN_BOND); + assert!(record.is_active); + assert_eq!(ctx.bond().balance(&ctx.solver), withdraw_amount); + assert_eq!(ctx.bond().balance(&ctx.contract_id), MIN_BOND); +} + +#[test] +fn withdraw_bond_below_exact_min_bond_fails() { + let ctx = setup(); + let c = ctx.client(); + ctx.register_solver(); + + // Attempt to leave one unit less than MIN_BOND + let too_much = BOND - MIN_BOND + 1; + let res = c.try_withdraw_bond(&ctx.solver, &too_much); + assert_eq!(res, Err(Ok(Error::SolverBondTooLow.into()))); +} + #[test] fn withdraw_bond_more_than_balance_fails() { let ctx = setup(); From 437befbfb49486b9d95c2eabab33884876e63260 Mon Sep 17 00:00:00 2001 From: James Akolo Date: Mon, 27 Jul 2026 16:50:25 +0100 Subject: [PATCH 3/4] test: add tests for storage TTL extension behavior across ledger advancement (#68) --- intent_settlement/src/test.rs | 113 ++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/intent_settlement/src/test.rs b/intent_settlement/src/test.rs index 80b7719..4ad989d 100644 --- a/intent_settlement/src/test.rs +++ b/intent_settlement/src/test.rs @@ -1037,6 +1037,119 @@ fn state_changing_calls_extend_instance_ttl() { assert!(instance_ttl >= crate::INSTANCE_TTL_EXTEND_TO - 1); } +#[test] +fn persistent_intent_ttl_extended_near_threshold() { + use soroban_sdk::testutils::storage::Persistent as _; + + let ctx = setup(); + let id = ctx.submit(); + + // Get the initial TTL after submission + let initial_ttl = ctx.env.as_contract(&ctx.contract_id, || { + ctx.env + .storage() + .persistent() + .get_ttl(&crate::DataKey::Intent(id.clone())) + }); + + // Simulate advancing ledger close to the threshold + let mut new_ttl = initial_ttl; + while new_ttl > crate::PERSISTENT_TTL_THRESHOLD + 100 { + ctx.pass_time(1000); // Each pass reduces TTL + new_ttl = ctx.env.as_contract(&ctx.contract_id, || { + ctx.env + .storage() + .persistent() + .get_ttl(&crate::DataKey::Intent(id.clone())) + }); + } + + // Now call a function that touches the intent (accept_intent) + ctx.register_solver(); + ctx.client().accept_intent(&ctx.solver, &id); + + // TTL should be extended to near PERSISTENT_TTL_EXTEND_TO + let extended_ttl = ctx.env.as_contract(&ctx.contract_id, || { + ctx.env + .storage() + .persistent() + .get_ttl(&crate::DataKey::Intent(id)) + }); + + assert!(extended_ttl >= crate::PERSISTENT_TTL_EXTEND_TO - 1); +} + +#[test] +fn persistent_solver_ttl_extended_near_threshold() { + use soroban_sdk::testutils::storage::Persistent as _; + + let ctx = setup(); + ctx.register_solver(); + + let initial_ttl = ctx.env.as_contract(&ctx.contract_id, || { + ctx.env + .storage() + .persistent() + .get_ttl(&crate::DataKey::Solver(ctx.solver.clone())) + }); + + // Simulate advancing ledger close to the threshold + let mut new_ttl = initial_ttl; + while new_ttl > crate::PERSISTENT_TTL_THRESHOLD + 100 { + ctx.pass_time(1000); + new_ttl = ctx.env.as_contract(&ctx.contract_id, || { + ctx.env + .storage() + .persistent() + .get_ttl(&crate::DataKey::Solver(ctx.solver.clone())) + }); + } + + // Call a function that touches the solver record (accept_intent) + let id = ctx.submit(); + ctx.client().accept_intent(&ctx.solver, &id); + + // TTL should be extended to near PERSISTENT_TTL_EXTEND_TO + let extended_ttl = ctx.env.as_contract(&ctx.contract_id, || { + ctx.env + .storage() + .persistent() + .get_ttl(&crate::DataKey::Solver(ctx.solver.clone())) + }); + + assert!(extended_ttl >= crate::PERSISTENT_TTL_EXTEND_TO - 1); +} + +#[test] +fn instance_ttl_extended_near_threshold() { + use soroban_sdk::testutils::storage::Instance as _; + + let ctx = setup(); + + let initial_ttl = ctx.env.as_contract(&ctx.contract_id, || { + ctx.env.storage().instance().get_ttl() + }); + + // Simulate advancing ledger close to the threshold + let mut new_ttl = initial_ttl; + while new_ttl > crate::INSTANCE_TTL_THRESHOLD + 100 { + ctx.pass_time(1000); + new_ttl = ctx.env.as_contract(&ctx.contract_id, || { + ctx.env.storage().instance().get_ttl() + }); + } + + // Call a state-changing function (register_solver touches instance) + ctx.register_solver(); + + // TTL should be extended to near INSTANCE_TTL_EXTEND_TO + let extended_ttl = ctx.env.as_contract(&ctx.contract_id, || { + ctx.env.storage().instance().get_ttl() + }); + + assert!(extended_ttl >= crate::INSTANCE_TTL_EXTEND_TO - 1); +} + // ─── Views ────────────────────────────────────────────────────────────────────── #[test] From eba6a8e7ee5ac65bcdc65681821b146174bebbad Mon Sep 17 00:00:00 2001 From: James Akolo Date: Mon, 27 Jul 2026 16:50:52 +0100 Subject: [PATCH 4/4] test: add negative tests confirming every error variant is reachable and correctly triggered (#67) --- intent_settlement/src/test.rs | 66 +++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/intent_settlement/src/test.rs b/intent_settlement/src/test.rs index 4ad989d..0618396 100644 --- a/intent_settlement/src/test.rs +++ b/intent_settlement/src/test.rs @@ -1164,3 +1164,69 @@ fn get_bond_token_returns_configured_token() { let ctx = setup(); assert_eq!(ctx.client().get_bond_token(), Some(ctx.bond_token.clone())); } + +// ─── Error variant coverage ───────────────────────────────────────────────────── + +#[test] +fn intent_already_filled_error_when_fill_called_twice() { + let ctx = setup(); + let c = ctx.client(); + ctx.register_solver(); + let id = ctx.submit(); + c.accept_intent(&ctx.solver, &id); + + // Fill the intent once + let fee = FILL * 5 / 10_000; + ctx.dst_admin().mint(&ctx.solver, &(FILL + fee)); + c.fill_intent(&ctx.solver, &id, &FILL); + + // Attempt to fill again with fresh funds + ctx.dst_admin().mint(&ctx.solver, &(FILL + fee)); + let res = c.try_fill_intent(&ctx.solver, &id, &FILL); + assert_eq!(res, Err(Ok(Error::IntentAlreadyFilled.into()))); +} + +#[test] +fn not_initialized_error_when_accessing_admin_before_init() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, IntentSettlement); + let c = IntentSettlementClient::new(&env, &contract_id); + + // Attempting to access admin before initialize should fail + let res = c.try_get_admin(); + assert_eq!(res, Err(Ok(Error::NotInitialized.into()))); +} + +#[test] +fn solver_inactive_error_when_accepting_after_slash_deactivation() { + let ctx = setup(); + let c = ctx.client(); + + let thin_bond = MIN_BOND + MIN_BOND / 10; + ctx.bond_admin().mint(&ctx.solver, &thin_bond); + c.register_solver(&ctx.solver, &thin_bond); + + let id = ctx.submit(); + c.accept_intent(&ctx.solver, &id); + ctx.pass_time(FILL_WINDOW + 1); + c.slash_solver(&id); + + // Solver is now inactive; try to accept another intent + let id2 = ctx.submit(); + let res = c.try_accept_intent(&ctx.solver, &id2); + assert_eq!(res, Err(Ok(Error::SolverInactive.into()))); +} + +#[test] +fn fill_window_expired_error_when_slash_called_too_early() { + let ctx = setup(); + ctx.register_solver(); + let id = ctx.submit(); + ctx.client().accept_intent(&ctx.solver, &id); + + // Still within the fill window, so slash should fail with FillWindowExpired + // (meaning "the window isn't expired yet, you can't slash") + let res = ctx.client().try_slash_solver(&id); + assert_eq!(res, Err(Ok(Error::FillWindowExpired.into()))); +}