From bb50d89797ec95cc543f098b57e7efe949131267 Mon Sep 17 00:00:00 2001 From: belloaliyu11 Date: Tue, 25 Aug 2026 10:16:53 +0100 Subject: [PATCH 1/5] test: add escrow security coverage --- .github/workflows/ci.yml | 14 +++ contracts/contracts/escrow/src/test.rs | 141 +++++++++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65998a0..b3566e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,3 +39,17 @@ jobs: # during the build. No real DB connection is made at build time. DATABASE_URL: ${{ secrets.DATABASE_URL || 'postgres://build:build@localhost/build' }} JWT_SECRET: ${{ secrets.JWT_SECRET || 'build-time-placeholder-secret-32chars!!' }} + + contracts: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Run Soroban Contract Tests + working-directory: contracts + run: cargo test --workspace diff --git a/contracts/contracts/escrow/src/test.rs b/contracts/contracts/escrow/src/test.rs index 86fdeab..fa05762 100644 --- a/contracts/contracts/escrow/src/test.rs +++ b/contracts/contracts/escrow/src/test.rs @@ -51,6 +51,37 @@ fn setup_test() -> TestSetup { } } +fn milestone(env: &Env, id: u32, amount: i128) -> Milestone { + Milestone { + id, + deadline: 0, + amount, + status: MilestoneStatus::Pending, + description: String::from_str(env, "Security milestone"), + client_approved: false, + freelancer_approved: false, + } +} + +fn initialize_single_milestone(setup: &TestSetup, amount: i128) { + let milestones = vec![&setup.env, milestone(&setup.env, 1, amount)]; + setup.escrow_client.initialize( + &setup.admin, + &setup.client, + &setup.freelancer, + &setup.arbiter, + &setup.token_address, + &milestones, + ); +} + +fn fully_approve_single_milestone(setup: &TestSetup) { + setup.escrow_client.fund(); + setup.escrow_client.submit_milestone(&1); + setup.escrow_client.approve(&1); + setup.escrow_client.freelancer_confirm(&1); +} + #[test] fn test_happy_path() { let setup = setup_test(); @@ -439,3 +470,113 @@ fn test_dispute_clears_approvals() { assert_eq!(escrow.has_client_approval(&1), false); assert_eq!(escrow.has_freelancer_approval(&1), false); } + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #7)")] +fn test_unauthorized_refund_fails() { + let setup = setup_test(); + initialize_single_milestone(&setup, 150); + setup.escrow_client.fund(); + + setup.escrow_client.refund(&1, &setup.client); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #7)")] +fn test_unauthorized_dispute_fails() { + let setup = setup_test(); + initialize_single_milestone(&setup, 150); + setup.escrow_client.fund(); + + let stranger = Address::generate(&setup.env); + setup.escrow_client.dispute(&1, &stranger); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #5)")] +fn test_submit_invalid_milestone_fails() { + let setup = setup_test(); + initialize_single_milestone(&setup, 150); + setup.escrow_client.fund(); + + setup.escrow_client.submit_milestone(&99); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #5)")] +fn test_release_invalid_milestone_fails() { + let setup = setup_test(); + initialize_single_milestone(&setup, 150); + fully_approve_single_milestone(&setup); + + setup.escrow_client.release(&99, &setup.client); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #6)")] +fn test_double_release_replay_fails() { + let setup = setup_test(); + initialize_single_milestone(&setup, 150); + fully_approve_single_milestone(&setup); + setup.escrow_client.release(&1, &setup.client); + + setup.escrow_client.release(&1, &setup.client); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #6)")] +fn test_double_refund_replay_fails() { + let setup = setup_test(); + initialize_single_milestone(&setup, 150); + setup.escrow_client.fund(); + setup.escrow_client.refund(&1, &setup.freelancer); + + setup.escrow_client.refund(&1, &setup.freelancer); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #6)")] +fn test_dispute_after_release_fails() { + let setup = setup_test(); + initialize_single_milestone(&setup, 150); + fully_approve_single_milestone(&setup); + setup.escrow_client.release(&1, &setup.freelancer); + + setup.escrow_client.dispute(&1, &setup.client); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #6)")] +fn test_refund_after_release_fails() { + let setup = setup_test(); + initialize_single_milestone(&setup, 150); + fully_approve_single_milestone(&setup); + setup.escrow_client.release(&1, &setup.freelancer); + + setup.escrow_client.refund(&1, &setup.freelancer); +} + +#[test] +fn test_successful_security_events_are_emitted() { + let setup = setup_test(); + let env = setup.env.clone(); + let initial_event_count = env.events().all().len(); + + initialize_single_milestone(&setup, 150); + assert_eq!(env.events().all().len(), initial_event_count + 1); + + setup.escrow_client.fund(); + assert_eq!(env.events().all().len(), initial_event_count + 2); + + setup.escrow_client.submit_milestone(&1); + assert_eq!(env.events().all().len(), initial_event_count + 3); + + setup.escrow_client.approve(&1); + assert_eq!(env.events().all().len(), initial_event_count + 4); + + setup.escrow_client.freelancer_confirm(&1); + assert_eq!(env.events().all().len(), initial_event_count + 5); + + setup.escrow_client.release(&1, &setup.client); + assert_eq!(env.events().all().len(), initial_event_count + 6); +} From 44e36149bf3b620e6a003f441ad91dba488cc45c Mon Sep 17 00:00:00 2001 From: belloaliyu11 <308107046+belloaliyu11@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:39:09 +0100 Subject: [PATCH 2/5] fix: import soroban events test utility --- contracts/contracts/escrow/src/test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/contracts/escrow/src/test.rs b/contracts/contracts/escrow/src/test.rs index fa05762..958f2a8 100644 --- a/contracts/contracts/escrow/src/test.rs +++ b/contracts/contracts/escrow/src/test.rs @@ -2,7 +2,7 @@ use super::*; use soroban_sdk::{ - testutils::Address as _, + testutils::{Address as _, Events as _}, vec, Address, Env, String, }; From fe50ab34a31b91531f270fd4054d36bffc5edc6f Mon Sep 17 00:00:00 2001 From: belloaliyu11 <308107046+belloaliyu11@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:44:43 +0100 Subject: [PATCH 3/5] fix: update contracts for soroban sdk 25 --- contracts/contracts/dispute/src/lib.rs | 3 +-- contracts/contracts/dispute/src/test.rs | 2 +- contracts/contracts/escrow/src/lib.rs | 4 ++-- contracts/contracts/escrow/src/test.rs | 14 +++++++------- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/contracts/contracts/dispute/src/lib.rs b/contracts/contracts/dispute/src/lib.rs index ab48a03..af5664b 100644 --- a/contracts/contracts/dispute/src/lib.rs +++ b/contracts/contracts/dispute/src/lib.rs @@ -253,8 +253,7 @@ impl DisputeContract { Ok(()) } - pub fn resolve(env: Env, dispute_id: u32) -> Result { - let caller = env.caller(); + pub fn resolve(env: Env, caller: Address, dispute_id: u32) -> Result { caller.require_auth(); let mut dispute: Dispute = env diff --git a/contracts/contracts/dispute/src/test.rs b/contracts/contracts/dispute/src/test.rs index 6e117f4..6e4f24d 100644 --- a/contracts/contracts/dispute/src/test.rs +++ b/contracts/contracts/dispute/src/test.rs @@ -108,7 +108,7 @@ fn test_voting_and_resolving() { // Fast forward to after deadline env.ledger().set_timestamp(1101); - let is_resolved_in_favor = client.resolve(&dispute_id); + let is_resolved_in_favor = client.resolve(&caller, &dispute_id); assert_eq!(is_resolved_in_favor, true); // votes_for > votes_against let dispute_final = client.get_dispute(&dispute_id); diff --git a/contracts/contracts/escrow/src/lib.rs b/contracts/contracts/escrow/src/lib.rs index b5f6a5a..0980aad 100644 --- a/contracts/contracts/escrow/src/lib.rs +++ b/contracts/contracts/escrow/src/lib.rs @@ -545,8 +545,8 @@ impl EscrowContract { Ok(()) } - pub fn auto_expire(env: Env, milestone_id: u32) -> Result<(), Error> { - let caller = env.caller(); + pub fn auto_expire(env: Env, caller: Address, milestone_id: u32) -> Result<(), Error> { + caller.require_auth(); let mut milestone: Milestone = env.storage().instance().get(&DataKey::Milestone(milestone_id)).ok_or(Error::MilestoneNotFound)?; if milestone.deadline == 0 { diff --git a/contracts/contracts/escrow/src/test.rs b/contracts/contracts/escrow/src/test.rs index 4856094..40ef83c 100644 --- a/contracts/contracts/escrow/src/test.rs +++ b/contracts/contracts/escrow/src/test.rs @@ -551,23 +551,23 @@ fn test_refund_after_release_fails() { fn test_successful_security_events_are_emitted() { let setup = setup_test(); let env = setup.env.clone(); - let initial_event_count = env.events().all().len(); + let initial_event_count = env.events().all().events().len(); initialize_single_milestone(&setup, 150); - assert_eq!(env.events().all().len(), initial_event_count + 1); + assert_eq!(env.events().all().events().len(), initial_event_count + 1); setup.escrow_client.fund(); - assert_eq!(env.events().all().len(), initial_event_count + 2); + assert_eq!(env.events().all().events().len(), initial_event_count + 2); setup.escrow_client.submit_milestone(&1); - assert_eq!(env.events().all().len(), initial_event_count + 3); + assert_eq!(env.events().all().events().len(), initial_event_count + 3); setup.escrow_client.approve(&1); - assert_eq!(env.events().all().len(), initial_event_count + 4); + assert_eq!(env.events().all().events().len(), initial_event_count + 4); setup.escrow_client.freelancer_confirm(&1); - assert_eq!(env.events().all().len(), initial_event_count + 5); + assert_eq!(env.events().all().events().len(), initial_event_count + 5); setup.escrow_client.release(&1, &setup.client); - assert_eq!(env.events().all().len(), initial_event_count + 6); + assert_eq!(env.events().all().events().len(), initial_event_count + 6); } From c31f683fc2a8c1cfd7e58ef023c741ab3e702e62 Mon Sep 17 00:00:00 2001 From: belloaliyu11 <308107046+belloaliyu11@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:25:13 +0100 Subject: [PATCH 4/5] test: align escrow security expectations --- contracts/contracts/escrow/src/test.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/contracts/contracts/escrow/src/test.rs b/contracts/contracts/escrow/src/test.rs index 40ef83c..53f92e0 100644 --- a/contracts/contracts/escrow/src/test.rs +++ b/contracts/contracts/escrow/src/test.rs @@ -377,7 +377,7 @@ fn test_release_without_approval_fails() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #10)")] +#[should_panic(expected = "HostError: Error(Contract, #6)")] fn test_double_client_approval_fails() { let setup = setup_test(); let escrow = setup.escrow_client; @@ -398,7 +398,7 @@ fn test_double_client_approval_fails() { escrow.fund(); escrow.submit_milestone(&1); escrow.approve(&1); - // Double approval should fail with AlreadyApproved (error code 10) + // Once approved, the milestone is no longer in Submitted state. escrow.approve(&1); } @@ -531,7 +531,7 @@ fn test_dispute_after_release_fails() { let setup = setup_test(); initialize_single_milestone(&setup, 150); fully_approve_single_milestone(&setup); - setup.escrow_client.release(&1, &setup.freelancer); + setup.escrow_client.release(&1, &setup.client); setup.escrow_client.dispute(&1, &setup.client); } @@ -542,7 +542,7 @@ fn test_refund_after_release_fails() { let setup = setup_test(); initialize_single_milestone(&setup, 150); fully_approve_single_milestone(&setup); - setup.escrow_client.release(&1, &setup.freelancer); + setup.escrow_client.release(&1, &setup.client); setup.escrow_client.refund(&1, &setup.freelancer); } @@ -551,23 +551,22 @@ fn test_refund_after_release_fails() { fn test_successful_security_events_are_emitted() { let setup = setup_test(); let env = setup.env.clone(); - let initial_event_count = env.events().all().events().len(); initialize_single_milestone(&setup, 150); - assert_eq!(env.events().all().events().len(), initial_event_count + 1); + assert_eq!(env.events().all().events().len(), 1); setup.escrow_client.fund(); - assert_eq!(env.events().all().events().len(), initial_event_count + 2); + assert_eq!(env.events().all().events().len(), 1); setup.escrow_client.submit_milestone(&1); - assert_eq!(env.events().all().events().len(), initial_event_count + 3); + assert_eq!(env.events().all().events().len(), 1); setup.escrow_client.approve(&1); - assert_eq!(env.events().all().events().len(), initial_event_count + 4); + assert_eq!(env.events().all().events().len(), 1); setup.escrow_client.freelancer_confirm(&1); - assert_eq!(env.events().all().events().len(), initial_event_count + 5); + assert_eq!(env.events().all().events().len(), 1); setup.escrow_client.release(&1, &setup.client); - assert_eq!(env.events().all().events().len(), initial_event_count + 6); + assert_eq!(env.events().all().events().len(), 1); } From 3aa2f4e9d98a7e52adabb5987a806c1e76645358 Mon Sep 17 00:00:00 2001 From: belloaliyu11 <308107046+belloaliyu11@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:27:33 +0100 Subject: [PATCH 5/5] test: filter escrow event assertions --- contracts/contracts/escrow/src/test.rs | 54 +++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/contracts/contracts/escrow/src/test.rs b/contracts/contracts/escrow/src/test.rs index 53f92e0..e525e3c 100644 --- a/contracts/contracts/escrow/src/test.rs +++ b/contracts/contracts/escrow/src/test.rs @@ -553,20 +553,62 @@ fn test_successful_security_events_are_emitted() { let env = setup.env.clone(); initialize_single_milestone(&setup, 150); - assert_eq!(env.events().all().events().len(), 1); + assert_eq!( + env.events() + .all() + .filter_by_contract(&setup.escrow_client.address) + .events() + .len(), + 1 + ); setup.escrow_client.fund(); - assert_eq!(env.events().all().events().len(), 1); + assert_eq!( + env.events() + .all() + .filter_by_contract(&setup.escrow_client.address) + .events() + .len(), + 1 + ); setup.escrow_client.submit_milestone(&1); - assert_eq!(env.events().all().events().len(), 1); + assert_eq!( + env.events() + .all() + .filter_by_contract(&setup.escrow_client.address) + .events() + .len(), + 1 + ); setup.escrow_client.approve(&1); - assert_eq!(env.events().all().events().len(), 1); + assert_eq!( + env.events() + .all() + .filter_by_contract(&setup.escrow_client.address) + .events() + .len(), + 1 + ); setup.escrow_client.freelancer_confirm(&1); - assert_eq!(env.events().all().events().len(), 1); + assert_eq!( + env.events() + .all() + .filter_by_contract(&setup.escrow_client.address) + .events() + .len(), + 1 + ); setup.escrow_client.release(&1, &setup.client); - assert_eq!(env.events().all().events().len(), 1); + assert_eq!( + env.events() + .all() + .filter_by_contract(&setup.escrow_client.address) + .events() + .len(), + 1 + ); }