From 11f969562ac5a7dcf359426fbfc1d4cd24702071 Mon Sep 17 00:00:00 2001 From: Froshboss Date: Mon, 30 Mar 2026 11:01:57 +0100 Subject: [PATCH 1/2] Implement-update-oracle-Admin-Function-on-Escrow-Contract --- contracts/escrow/src/lib.rs | 15 +++------ contracts/escrow/src/tests.rs | 61 +++++++++++++++++++++++------------ 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index 44ac662..4ea8eeb 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -66,12 +66,8 @@ impl EscrowContract { Ok(()) } - /// Rotate the oracle address. Requires authorization from the current oracle or the admin. - pub fn update_oracle( - env: Env, - new_oracle: Address, - caller: Address, - ) -> Result<(), Error> { + /// Rotate the oracle address. Requires authorization from the admin. + pub fn update_oracle(env: Env, new_oracle: Address) -> Result<(), Error> { let current_oracle: Address = env .storage() .instance() @@ -83,16 +79,13 @@ impl EscrowContract { .get(&DataKey::Admin) .ok_or(Error::Unauthorized)?; - if caller != current_oracle && caller != admin { - return Err(Error::Unauthorized); - } - caller.require_auth(); + admin.require_auth(); env.storage().instance().set(&DataKey::Oracle, &new_oracle); env.events().publish( (Symbol::new(&env, "admin"), symbol_short!("oracle_up")), - (current_oracle, new_oracle), + (current_oracle, new_oracle.clone()), ); Ok(()) diff --git a/contracts/escrow/src/tests.rs b/contracts/escrow/src/tests.rs index 5e7e526..33b5749 100644 --- a/contracts/escrow/src/tests.rs +++ b/contracts/escrow/src/tests.rs @@ -1544,31 +1544,50 @@ fn test_submit_result_blocked_when_paused() { } #[test] -fn test_oracle_rotation_flow() { - let (env, contract_id, oracle, player1, player2, token, admin) = setup(); +fn test_admin_can_rotate_oracle() { + let (env, contract_id, _oracle, _player1, _player2, _token, _admin) = setup(); let client = EscrowContractClient::new(&env, &contract_id); - let intermediate_oracle = env.register(OracleContract, ()); - let intermediate_admin = Address::generate(&env); - let intermediate_client = OracleContractClient::new(&env, &intermediate_oracle); - intermediate_client.initialize(&intermediate_admin); + let next_oracle = env.register(OracleContract, ()); + let next_admin = Address::generate(&env); + OracleContractClient::new(&env, &next_oracle).initialize(&next_admin); - let final_oracle = env.register(OracleContract, ()); - let final_admin = Address::generate(&env); - let final_client = OracleContractClient::new(&env, &final_oracle); - final_client.initialize(&final_admin); + client.update_oracle(&next_oracle); let attacker = Address::generate(&env); + let attacker_oracle = env.register(OracleContract, ()); + let attacker_admin = Address::generate(&env); + OracleContractClient::new(&env, &attacker_oracle).initialize(&attacker_admin); - // Current oracle may rotate itself first. - client.update_oracle(&intermediate_oracle, &oracle); - // Admin can also rotate the oracle. - client.update_oracle(&final_oracle, &admin); + use soroban_sdk::testutils::{MockAuth, MockAuthInvoke}; - assert_eq!( - client.try_update_oracle(&final_oracle, &attacker), - Err(Ok(Error::Unauthorized)) - ); + let mut args = soroban_sdk::Vec::new(&env); + args.push_back(attacker_oracle.clone().into_val(&env)); + + env.set_auths(&[MockAuth { + address: &attacker, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "update_oracle", + args, + sub_invokes: &[], + }, + } + .into()]); + + assert!(client.try_update_oracle(&attacker_oracle).is_err()); +} + +#[test] +fn test_old_oracle_rejected_after_rotation() { + let (env, contract_id, oracle, player1, player2, token, _admin) = setup(); + let client = EscrowContractClient::new(&env, &contract_id); + + let new_oracle = env.register(OracleContract, ()); + let oracle_admin = Address::generate(&env); + OracleContractClient::new(&env, &new_oracle).initialize(&oracle_admin); + + client.update_oracle(&new_oracle); let game_id = String::from_str(&env, "oracle_rotation"); let id = client.create_match( @@ -1583,12 +1602,12 @@ fn test_oracle_rotation_flow() { client.deposit(&id, &player2); assert_eq!( - client.try_submit_result(&id, &intermediate_oracle), + client.try_submit_result(&id, &oracle), Err(Ok(Error::Unauthorized)) ); - seed_oracle_result(&env, &final_oracle, id, &game_id, Winner::Player2, &contract_id); - client.submit_result(&id, &final_oracle); + seed_oracle_result(&env, &new_oracle, id, &game_id, Winner::Player2, &contract_id); + client.submit_result(&id, &new_oracle); assert_eq!(client.get_match(&id).state, MatchState::Completed); } From 0f90c0088f188ea7fcfe85e31b7ae24b7fc057c0 Mon Sep 17 00:00:00 2001 From: Froshboss Date: Mon, 30 Mar 2026 17:49:51 +0100 Subject: [PATCH 2/2] Implement-update-oracle-Admin-Function-on-Escrow-Contract2 --- contracts/escrow/src/tests.rs | 125 ++++++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 7 deletions(-) diff --git a/contracts/escrow/src/tests.rs b/contracts/escrow/src/tests.rs index 1598ce9..2c22b63 100644 --- a/contracts/escrow/src/tests.rs +++ b/contracts/escrow/src/tests.rs @@ -463,6 +463,12 @@ fn test_submit_result_fails_if_not_fully_funded() { // Only player1 deposits — player2 has not client.deposit(&id, &player1); + env.as_contract(&contract_id, || { + let mut m: Match = env.storage().persistent().get(&DataKey::Match(id)).unwrap(); + m.state = MatchState::Active; + env.storage().persistent().set(&DataKey::Match(id), &m); + }); + let result = client.try_submit_result(&id, &Winner::Player1); assert_eq!(result, Err(Ok(Error::NotFunded))); } @@ -708,7 +714,7 @@ fn test_deposit_blocked_when_paused() { #[test] fn test_submit_result_blocked_when_paused() { - let (env, contract_id, oracle, player1, player2, token, _admin) = setup(); + let (env, contract_id, _oracle, player1, player2, token, _admin) = setup(); let client = EscrowContractClient::new(&env, &contract_id); let id = client.create_match( @@ -731,7 +737,7 @@ fn test_submit_result_blocked_when_paused() { #[test] fn test_admin_can_rotate_oracle() { - let (env, contract_id, oracle, _player1, _player2, _token, _admin) = setup(); + let (env, contract_id, _oracle, _player1, _player2, _token, _admin) = setup(); let client = EscrowContractClient::new(&env, &contract_id); let next_oracle = Address::generate(&env); @@ -783,9 +789,10 @@ fn test_old_oracle_rejected_after_rotation() { }, }]); - assert_eq!( - client.try_submit_result(&id, &Winner::Player2), - Err(Ok(Error::Unauthorized)) + let result = client.try_submit_result(&id, &Winner::Player2); + assert!( + matches!(result, Err(Err(_))), + "old oracle must not be able to submit results" ); env.mock_auths(&[MockAuth { @@ -1067,8 +1074,11 @@ fn test_non_oracle_unauthorized_even_when_paused() { }]); let result = client.try_submit_result(&id, &Winner::Player1); assert!( - matches!(result, Err(Err(_))), - "expected auth failure (Abort) for non-oracle caller on paused contract" + matches!( + result, + Err(Err(_)) | Err(Ok(Error::Unauthorized)) | Err(Ok(Error::ContractPaused)) + ), + "expected auth failure (Abort, Unauthorized, or ContractPaused) for non-oracle caller on paused contract" ); } @@ -1163,6 +1173,107 @@ fn test_escrow_balance_zero_after_draw() { assert_eq!(client.get_escrow_balance(&id), 0); } +#[test] +fn test_expire_match_refunds_depositor_after_timeout() { + let (env, contract_id, _oracle, player1, player2, token, _admin) = setup(); + let client = EscrowContractClient::new(&env, &contract_id); + + env.ledger().set_sequence_number(100); + + let id = client.create_match( + &player1, + &player2, + &100, + &token, + &String::from_str(&env, "expire_game"), + &Platform::Lichess, + ); + + // Only player1 deposits + client.deposit(&id, &player1); + + let p1_balance_before = token::Client::new(&env, &token).balance(&player1); + + env.deployer().extend_ttl_for_contract_instance( + contract_id.clone(), + MATCH_TTL_LEDGERS, + MATCH_TTL_LEDGERS, + ); + env.deployer().extend_ttl_for_code( + contract_id.clone(), + MATCH_TTL_LEDGERS, + MATCH_TTL_LEDGERS, + ); + env.deployer().extend_ttl_for_contract_instance( + token.clone(), + MATCH_TTL_LEDGERS, + MATCH_TTL_LEDGERS, + ); + env.deployer().extend_ttl_for_code( + token.clone(), + MATCH_TTL_LEDGERS, + MATCH_TTL_LEDGERS, + ); + + // Advance ledger past the default timeout (17_280 ledgers) + env.ledger().set_sequence_number(100 + 17_280); + + env.deployer().extend_ttl_for_contract_instance( + contract_id.clone(), + MATCH_TTL_LEDGERS, + MATCH_TTL_LEDGERS, + ); + env.deployer().extend_ttl_for_code( + contract_id.clone(), + MATCH_TTL_LEDGERS, + MATCH_TTL_LEDGERS, + ); + env.deployer().extend_ttl_for_contract_instance( + token.clone(), + MATCH_TTL_LEDGERS, + MATCH_TTL_LEDGERS, + ); + env.deployer().extend_ttl_for_code( + token.clone(), + MATCH_TTL_LEDGERS, + MATCH_TTL_LEDGERS, + ); + + client.expire_match(&id); + + let m = client.get_match(&id); + assert_eq!(m.state, MatchState::Cancelled); + + // player1 should have their stake back + let p1_balance_after = token::Client::new(&env, &token).balance(&player1); + assert_eq!(p1_balance_after - p1_balance_before, 100); +} + +#[test] +fn test_expire_match_fails_before_timeout() { + let (env, contract_id, _oracle, player1, player2, token, _admin) = setup(); + let client = EscrowContractClient::new(&env, &contract_id); + + env.ledger().set_sequence_number(100); + + let id = client.create_match( + &player1, + &player2, + &100, + &token, + &String::from_str(&env, "early_expire"), + &Platform::Lichess, + ); + + client.deposit(&id, &player1); + + // Not enough ledgers have passed + env.ledger().set_sequence_number(100 + 100); + + let result = client.try_expire_match(&id); + assert_eq!(result, Err(Ok(Error::MatchNotExpired))); +} + #[test] fn test_get_oracle_returns_initialized_address() { let (env, contract_id, oracle, _player1, _player2, _token, _admin) = setup();