Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contracts/escrow/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ pub enum Error {
ContractPaused = 9,
InvalidAmount = 10,
MatchAlreadyActive = 11,
MatchNotExpired = 12,
}
57 changes: 57 additions & 0 deletions contracts/escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)]
Expand Down
57 changes: 57 additions & 0 deletions contracts/escrow/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -946,16 +988,31 @@ 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,
);

let m = client.get_match(&id);
assert_eq!(m.player1, player1);
assert_eq!(m.player2, player2);
>>>>>>> main
}
1 change: 1 addition & 0 deletions contracts/escrow/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ pub enum DataKey {
Oracle,
Admin,
Paused,
MatchTimeout,
}
Loading