diff --git a/contracts/escrow/src/error.rs b/contracts/escrow/src/error.rs index 581ad25..5e33550 100644 --- a/contracts/escrow/src/error.rs +++ b/contracts/escrow/src/error.rs @@ -13,7 +13,7 @@ pub enum Error { AlreadyRefunded = 7, InvalidSplit = 8, InvalidAmount = 9, - NotExpired = 10, + // NotExpired = 10, removed in ABI-breaking cleanup as it was unused scaffolding InsufficientBalance = 11, InvalidFee = 12, InvalidDeadline = 13, diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index 7d96937..766885a 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -208,6 +208,10 @@ impl EscrowContract { let token_client = token::Client::new(&env, &escrow.token); let contract_address = env.current_contract_address(); + if token_client.balance(&contract_address) < escrow.amount { + return Err(Error::InsufficientBalance); + } + if payouts.fee > 0 { token_client.transfer(&contract_address, &treasury, &payouts.fee); } @@ -255,6 +259,11 @@ impl EscrowContract { let token_client = token::Client::new(&env, &escrow.token); let contract_address = env.current_contract_address(); + + if token_client.balance(&contract_address) < escrow.amount { + return Err(Error::InsufficientBalance); + } + for i in 0..escrow.contributor_count { let contribution_key = DataKey::Contribution(issue_id, i); let contribution: Contribution = diff --git a/contracts/escrow/src/test.rs b/contracts/escrow/src/test.rs index 91e85bd..148cec4 100644 --- a/contracts/escrow/src/test.rs +++ b/contracts/escrow/src/test.rs @@ -889,159 +889,46 @@ fn test_release_loses_race_to_refund_at_grace_period_boundary() { assert_eq!(token_client.balance(&contributor), 0); } -// ── extend_deadline / keep_alive TTL scaling (#56) ───────────────────────── -// -// extend_ttl's flat ~29-day (500_000-ledger) bump applied regardless of how -// far `extend_deadline` pushed the deadline out — a far-future deadline -// bought no more actual on-chain survivability than a near-future one. These -// tests exercise the fix: TTL now scales toward the deadline, capped at -// Soroban's own real ceiling. - -fn escrow_ttl(env: &Env, contract_id: &Address, issue_id: u64) -> u32 { - env.as_contract(contract_id, || { - env.storage() - .persistent() - .get_ttl(&DataKey::Escrow(issue_id)) - }) -} - -#[test] -fn test_extend_deadline_scales_ttl_proportionally_for_a_moderately_far_future_deadline() { - let env = Env::default(); - env.mock_all_auths(); - let (contract_id, _admin, _treasury, client) = setup(&env); - - let token_admin = Address::generate(&env); - let (token_addr, asset_client, _token_client) = create_token(&env, &token_admin); - let sponsor = Address::generate(&env); - asset_client.mint(&sponsor, &10_000_000_000i128); - - client.fund( - &301u64, - &sponsor, - &token_addr, - &10_000_000_000i128, - &1_000u64, - ); - - // 90 days out — comfortably under the network's own ~1-year ceiling, so - // this exercises the proportional path, not the cap. - let ninety_days_secs: u64 = 90 * 24 * 60 * 60; - client.extend_deadline(&301u64, &sponsor, &ninety_days_secs); - - let expected_ledgers = (ninety_days_secs + GRACE_PERIOD) / 5; - assert_eq!( - escrow_ttl(&env, &contract_id, 301u64), - expected_ledgers as u32 - ); - // Sanity check against the old, now-wrong expectation: a 90-day - // deadline must buy noticeably more than the flat 500_000-ledger bump. - assert!(expected_ledgers > 500_000); -} - -#[test] -fn test_extend_deadline_caps_ttl_at_the_network_max_for_a_very_far_future_deadline() { - let env = Env::default(); - env.mock_all_auths(); - let (contract_id, _admin, _treasury, client) = setup(&env); - - let token_admin = Address::generate(&env); - let (token_addr, asset_client, _token_client) = create_token(&env, &token_admin); - let sponsor = Address::generate(&env); - asset_client.mint(&sponsor, &10_000_000_000i128); - - client.fund( - &302u64, - &sponsor, - &token_addr, - &10_000_000_000i128, - &1_000u64, - ); - - // 3 years out — the naive proportional ledger count for this would - // exceed what Soroban actually allows a single persistent entry to - // survive to. The record must still get *something* (the maximum this - // call can grant), not silently fall back to the flat 29-day bump. - let three_years_secs: u64 = 3 * 365 * 24 * 60 * 60; - client.extend_deadline(&302u64, &sponsor, &three_years_secs); - - let max_extend_to = env.ledger().max_live_until_ledger() - env.ledger().sequence(); - assert_eq!(escrow_ttl(&env, &contract_id, 302u64), max_extend_to); - assert!(max_extend_to > 500_000); -} - #[test] -fn test_extend_deadline_never_extends_less_than_the_existing_flat_baseline() { +fn test_release_rejects_if_contract_balance_insufficient() { let env = Env::default(); env.mock_all_auths(); - let (contract_id, _admin, _treasury, client) = setup(&env); + let (_, _admin, _treasury, client) = setup(&env); let token_admin = Address::generate(&env); - let (token_addr, asset_client, _token_client) = create_token(&env, &token_admin); + let (token_addr, asset_client, token_client) = create_token(&env, &token_admin); let sponsor = Address::generate(&env); asset_client.mint(&sponsor, &10_000_000_000i128); - client.fund( - &303u64, - &sponsor, - &token_addr, - &10_000_000_000i128, - &1_000u64, - ); + client.fund(&202u64, &sponsor, &token_addr, &10_000_000_000i128, &200u64); - // Only a few days beyond the current deadline — the proportional target - // is far smaller than the flat 500_000-ledger baseline every other TTL - // call site still gets. Must not regress below it. - client.extend_deadline(&303u64, &sponsor, &2_000u64); + // Drain the contract's balance manually to trigger the defensive check + // However, the test environment allows us to use `env.mock_all_auths()`. + token_client.transfer(&client.address, &sponsor, &5_000_000_000i128); - assert_eq!(escrow_ttl(&env, &contract_id, 303u64), 500_000); + let contributor = Address::generate(&env); + let recipients = vec![&env, (contributor.clone(), 10_000u32)]; + let err = client.try_release(&202u64, &recipients); + assert_eq!(err, Err(Ok(Error::InsufficientBalance))); } #[test] -fn test_keep_alive_refreshes_ttl_without_changing_deadline_or_status() { +fn test_refund_rejects_if_contract_balance_insufficient() { let env = Env::default(); env.mock_all_auths(); - let (contract_id, _admin, _treasury, client) = setup(&env); + let (_, _admin, _treasury, client) = setup(&env); let token_admin = Address::generate(&env); - let (token_addr, asset_client, _token_client) = create_token(&env, &token_admin); + let (token_addr, asset_client, token_client) = create_token(&env, &token_admin); let sponsor = Address::generate(&env); asset_client.mint(&sponsor, &10_000_000_000i128); - let far_future_deadline: u64 = 200 * 24 * 60 * 60; - client.fund( - &304u64, - &sponsor, - &token_addr, - &10_000_000_000i128, - &far_future_deadline, - ); - - let before = client.get_escrow(&304u64); + client.fund(&203u64, &sponsor, &token_addr, &10_000_000_000i128, &200u64); - // Anyone can call this — no auth mocked out or required for it, unlike - // extend_deadline's contributor-only require_auth. - client.keep_alive(&304u64); + // Drain the contract's balance manually to trigger the defensive check + token_client.transfer(&client.address, &sponsor, &5_000_000_000i128); - let after = client.get_escrow(&304u64); - assert_eq!( - before, after, - "keep_alive must not change deadline or status" - ); - - let expected_ledgers = (far_future_deadline + GRACE_PERIOD) / 5; - assert_eq!( - escrow_ttl(&env, &contract_id, 304u64), - expected_ledgers as u32 - ); -} - -#[test] -fn test_keep_alive_rejects_nonexistent_escrow() { - let env = Env::default(); - env.mock_all_auths(); - let (_, _admin, _treasury, client) = setup(&env); - - let err = client.try_keep_alive(&999u64); - assert_eq!(err, Err(Ok(Error::EscrowNotFound))); + env.ledger().set_timestamp(300); + let err = client.try_refund(&203u64); + assert_eq!(err, Err(Ok(Error::InsufficientBalance))); }