diff --git a/contracts/escrow/src/errors.rs b/contracts/escrow/src/errors.rs index fb67ccb..c63e0cc 100644 --- a/contracts/escrow/src/errors.rs +++ b/contracts/escrow/src/errors.rs @@ -14,4 +14,5 @@ pub enum Error { ContractPaused = 9, InvalidAmount = 10, MatchAlreadyActive = 11, + MatchNotExpired = 12, } diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index 27af3a2..426cf58 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -10,6 +10,9 @@ use types::{DataKey, Match, MatchState, Platform, Winner}; /// ~30 days at 5s/ledger. Used as both the TTL threshold and the extend-to value. const MATCH_TTL_LEDGERS: u32 = 518_400; +/// Default match expiry timeout (~24 hours at 5s/ledger). +const DEFAULT_MATCH_TIMEOUT_LEDGERS: u32 = 17_280; + #[contract] pub struct EscrowContract; @@ -333,6 +336,60 @@ impl EscrowContract { let deposited = m.player1_deposited as i128 + m.player2_deposited as i128; Ok(deposited * m.stake_amount) } + + /// Cancel a Pending match that has exceeded the configurable ledger timeout, + /// refunding any deposited stakes. Anyone may call this once the timeout elapses. + pub fn expire_match(env: Env, match_id: u64) -> Result<(), Error> { + let mut m: Match = env + .storage() + .persistent() + .get(&DataKey::Match(match_id)) + .ok_or(Error::MatchNotFound)?; + + if m.state != MatchState::Pending { + return Err(Error::InvalidState); + } + + let timeout: u32 = env + .storage() + .instance() + .get(&DataKey::MatchTimeout) + .unwrap_or(DEFAULT_MATCH_TIMEOUT_LEDGERS); + + let elapsed = env + .ledger() + .sequence() + .saturating_sub(m.created_ledger); + + if elapsed < timeout { + return Err(Error::MatchNotExpired); + } + + let client = token::Client::new(&env, &m.token); + if m.player1_deposited { + client.transfer(&env.current_contract_address(), &m.player1, &m.stake_amount); + } + if m.player2_deposited { + client.transfer(&env.current_contract_address(), &m.player2, &m.stake_amount); + } + + m.state = MatchState::Cancelled; + env.storage() + .persistent() + .set(&DataKey::Match(match_id), &m); + env.storage().persistent().extend_ttl( + &DataKey::Match(match_id), + MATCH_TTL_LEDGERS, + MATCH_TTL_LEDGERS, + ); + + env.events().publish( + (Symbol::new(&env, "match"), symbol_short!("expired")), + match_id, + ); + + Ok(()) + } } #[cfg(test)] diff --git a/contracts/escrow/src/tests.rs b/contracts/escrow/src/tests.rs index e3b3eb2..5c3b3a4 100644 --- a/contracts/escrow/src/tests.rs +++ b/contracts/escrow/src/tests.rs @@ -935,6 +935,48 @@ fn test_escrow_balance_zero_after_draw() { } #[test] +<<<<<<< fix/match-expiry-timeout +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); + + // Advance ledger past the default timeout (17_280 ledgers) + env.ledger().set_sequence_number(100 + 17_280); + + 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); + +======= fn test_get_oracle_returns_initialized_address() { let (env, contract_id, oracle, _player1, _player2, _token, _admin) = setup(); let client = EscrowContractClient::new(&env, &contract_id); @@ -946,11 +988,25 @@ fn test_get_match_returns_correct_players() { let (env, contract_id, _oracle, player1, player2, token, _admin) = setup(); let client = EscrowContractClient::new(&env, &contract_id); +>>>>>>> main let id = client.create_match( &player1, &player2, &100, &token, +<<<<<<< fix/match-expiry-timeout + &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))); +======= &String::from_str(&env, "players_test"), &Platform::Lichess, ); @@ -958,4 +1014,5 @@ fn test_get_match_returns_correct_players() { let m = client.get_match(&id); assert_eq!(m.player1, player1); assert_eq!(m.player2, player2); +>>>>>>> main } diff --git a/contracts/escrow/src/types.rs b/contracts/escrow/src/types.rs index ca40751..c8f8c41 100644 --- a/contracts/escrow/src/types.rs +++ b/contracts/escrow/src/types.rs @@ -47,4 +47,5 @@ pub enum DataKey { Oracle, Admin, Paused, + MatchTimeout, }