Legend: 🆕 New (unassigned)
Status: Open — unassigned
Labels: testing
Priority: Medium
Estimated Time: 15 minutes
Description:
There is no test verifying that player1's token balance is reduced by exactly stake_amount after calling deposit. The existing test_deposit_and_activate only checks is_funded and get_escrow_balance, not the player's actual balance.
Tasks:
- Create match, record player1 balance before deposit
- Call
depositfor player1 - Assert player1 balance decreased by
stake_amount
Status: Open — unassigned
Labels: testing
Priority: Medium
Estimated Time: 15 minutes
Description:
No test verifies that player2's token balance is reduced by exactly stake_amount after calling deposit. Mirrors the gap identified for player1 in #65.
Tasks:
- Create match, record player2 balance before deposit
- Call
depositfor player2 - Assert player2 balance decreased by
stake_amount
Status: Open — unassigned
Labels: testing
Priority: Medium
Estimated Time: 15 minutes
Description:
After both players deposit, the contract address should hold exactly 2 * stake_amount tokens. No test currently reads the contract's token balance directly to confirm this.
Tasks:
- Create match with
stake_amount = 100 - Both players deposit
- Use
TokenClient::balance(&contract_id)to assert balance is200
Status: Open — unassigned
Labels: enhancement
Priority: Low
Estimated Time: 15 minutes
Description: There is no public getter for the oracle address stored in the escrow contract. Frontends and integrators cannot verify which oracle is trusted without reading raw storage.
Tasks:
- Add
get_oracle(env: Env) -> Addressread function returningDataKey::Oracle - Add test asserting it returns the address passed to
initialize
Status: Open — unassigned
Labels: testing
Priority: Low
Estimated Time: 15 minutes
Description:
test_create_match only asserts the returned id and state. No test verifies that get_match returns the correct stake_amount and token address that were passed to create_match.
Tasks:
- Create match with a specific
stake_amountandtoken - Call
get_match - Assert
m.stake_amountandm.tokenmatch the inputs
Status: Open — unassigned
Labels: testing
Priority: Low
Estimated Time: 15 minutes
Description:
No test asserts that get_match returns the correct player1 and player2 addresses after creation.
Tasks:
- Create match with known player addresses
- Call
get_match - Assert
m.player1andm.player2match the inputs
Status: Open — unassigned
Labels: testing
Priority: Low
Estimated Time: 15 minutes
Description:
No test verifies that get_match returns the correct game_id string and platform enum value after creation.
Tasks:
- Create match with
game_id = "testgame1"andPlatform::ChessDotCom - Call
get_match - Assert
m.game_idandm.platformmatch the inputs
Status: Open — unassigned
Labels: testing
Priority: Medium
Estimated Time: 30 minutes
Description: No test verifies that two separate pairs of players can run concurrent matches without interfering with each other's state, deposits, or payouts.
Tasks:
- Create match A (player1 vs player2) and match B (player3 vs player4)
- Both matches go through full deposit and payout lifecycle independently
- Assert each match's state and balances are correct and isolated
Status: Open — unassigned
Labels: testing
Priority: Medium
Estimated Time: 30 minutes
Description:
No test verifies that deposit is blocked when the contract is paused. test_admin_pause_blocks_create_match only covers create_match.
Tasks:
- Create a match before pausing
- Admin calls
pause() - Call
depositon the existing match - Assert
Error::ContractPaused
Status: Open — unassigned
Labels: testing
Priority: Low
Estimated Time: 15 minutes
Description:
OracleContract::get_result returns Error::ResultNotFound for unknown IDs, but there is no test confirming this behaviour.
Tasks:
- Call
get_result(9999)on a fresh oracle contract - Assert
Error::ResultNotFoundis returned
Status: Open — unassigned
Labels: enhancement, security
Priority: High
Estimated Time: 1 hour
Description:
The escrow contract has pause() and unpause() admin functions for emergency stops, but the oracle contract has no equivalent. A compromised oracle admin key could continue submitting fraudulent results with no way to halt the oracle independently.
Tasks:
- Add
DataKey::Pausedto oracle contract - Add
pause()andunpause()admin-only functions - Guard
submit_resultwith a paused check returningError::ContractPaused - Add tests for pause/unpause on oracle
Status: Open — unassigned
Labels: bug
Priority: Medium
Estimated Time: 30 minutes
Description:
OracleContract::get_result reads from persistent storage but does not call extend_ttl. If a result entry is near expiry and is read without extension, it may expire before the next access, causing ResultNotFound errors for valid results.
Tasks:
- Add
env.storage().persistent().extend_ttl(&DataKey::Result(match_id), MATCH_TTL_LEDGERS, MATCH_TTL_LEDGERS)insideget_result - Add test verifying TTL is extended after a
get_resultcall
Status: Open — unassigned
Labels: bug
Priority: High
Estimated Time: 30 minutes
Description:
cancel_match checks m.state != MatchState::Pending and returns InvalidState, which correctly blocks cancellation of Active matches. However, there is no dedicated test confirming this guard works, and the error message gives no indication of why cancellation was rejected.
Tasks:
- Add explicit test: fund both players (match becomes Active), call
cancel_match, assertError::InvalidState - Consider adding a descriptive error variant
Error::MatchAlreadyActive
Status: Open — unassigned
Labels: bug, security
Priority: High
Estimated Time: 30 minutes
Description:
submit_result retrieves the oracle address and calls oracle.require_auth(), but the auth check is placed after the paused check. A non-oracle caller can observe whether the contract is paused before being rejected, leaking contract state. Auth must be the first check.
Tasks:
- Reorder
submit_resultsooracle.require_auth()is called before the paused check - Add test confirming a non-oracle address receives
Unauthorizedeven on a paused contract
Status: Open — unassigned
Labels: testing, Stellar Wave
Priority: Medium
Estimated Time: 30 minutes
Description:
Verify that calling deposit on a paused contract returns Error::ContractPaused. Currently only create_match is tested under pause conditions.
Tasks:
- Create a match (before pausing)
- Admin calls
pause() - Call
depositon the created match - Assert
Error::ContractPausedis returned
Status: Open — unassigned
Labels: testing
Priority: High
Estimated Time: 1 hour
Description:
There is no single test that exercises the complete match lifecycle: create_match → deposit (both players) → submit_result → verify final balances and match state. Individual unit tests exist but no end-to-end integration test.
Tasks:
- Write
test_full_match_lifecyclecovering all steps - Assert match state transitions:
Pending→Active→Completed - Assert final token balances for winner and loser
- Assert
get_escrow_balancereturns0after payout
Status: Open — unassigned
Labels: bug
Priority: Medium
Estimated Time: 30 minutes
Description:
initialize accepts any Address as the oracle without checking it is a valid non-zero address. Passing a zero/default address would silently set a useless oracle, permanently blocking submit_result.
Tasks:
- Add a check that the oracle address is not the contract's own address or a known invalid address
- Document the expected oracle address format
- Add test for invalid oracle address rejection
Status: Open — unassigned
Labels: enhancement, Stellar Wave
Priority: Low
Estimated Time: 15 minutes
Description: There is no public getter for the oracle address stored in the escrow contract. Frontends and integrators cannot verify which oracle is trusted without reading raw storage.
Tasks:
- Add
pub fn get_oracle(env: Env) -> Result<Address, Error>returningDataKey::Oracle - Return
Error::Unauthorizedif not initialized - Add test asserting it returns the address set at
initialize
Status: Open — unassigned
Labels: enhancement, Stellar Wave
Priority: Low
Estimated Time: 15 minutes
Description:
There is no public getter for the paused state of the escrow contract. Frontends cannot check whether the contract is paused before attempting transactions, leading to confusing ContractPaused errors.
Tasks:
- Add
pub fn is_paused(env: Env) -> boolreturningDataKey::Paused - Add test asserting
is_pausedreturnsfalseinitially,trueafterpause(), andfalseafterunpause()
Status: Open — unassigned
Labels: enhancement, Stellar Wave
Priority: High
Estimated Time: 1 hour
Description:
The oracle address is set once at initialize and cannot be changed. If the oracle service is compromised or needs to be rotated, there is no way to update it without redeploying the entire escrow contract.
Tasks:
- Add
pub fn update_oracle(env: Env, new_oracle: Address) -> Result<(), Error>requiring admin auth - Update
DataKey::Oraclewith the new address - Add test for oracle rotation
- Add test that old oracle address is rejected after rotation
Status: Open — unassigned
Labels: enhancement, security, Stellar Wave
Priority: High
Estimated Time: 1 hour
Description: The oracle contract has no emergency pause mechanism. If the oracle admin key is compromised, fraudulent results can be submitted indefinitely with no way to halt the oracle independently of the escrow contract.
Tasks:
- Add
DataKey::Pausedto oracleDataKeyenum - Add
pub fn pause(env: Env) -> Result<(), Error>andpub fn unpause(env: Env) -> Result<(), Error>requiring admin auth - Guard
submit_resultwith paused check - Add tests for pause/unpause behaviour on oracle
Status: Open — unassigned
Labels: enhancement, security, Stellar Wave
Priority: High
Estimated Time: 2 hours
Description:
Neither the escrow nor oracle contract has a safe admin transfer mechanism. A direct set_admin(new_admin) call risks permanently locking admin access if the wrong address is provided. A two-step transfer (propose → accept) prevents accidental lockout.
Tasks:
- Add
DataKey::PendingAdminto both contracts - Add
propose_admin(new_admin: Address)requiring current admin auth - Add
accept_admin()requiring new admin auth - Add tests for successful transfer and rejection of unaccepted proposals
Status: Open — unassigned
Labels: bug
Priority: Medium
Estimated Time: 30 minutes
Description:
There is no check that player1 != player2. A single address can create a match against itself, deposit twice, and receive the full pot back, wasting ledger resources and potentially gaming any future reward systems.
Tasks:
- Add
if player1 == player2 { return Err(Error::InvalidPlayers) }guard increate_match - Add
InvalidPlayerserror variant toerrors.rs - Add test asserting self-match creation returns
Error::InvalidPlayers
Status: Open — unassigned
Labels: bug, security
Priority: High
Estimated Time: 1 hour
Description:
The same game_id can be used to create multiple matches. An attacker could create duplicate matches for the same game and collect payouts multiple times if the oracle submits results for the same game ID.
Tasks:
- Track used
game_idvalues in aDataKey::GameId(String)persistent entry - Reject
create_matchifgame_idalready exists, returningError::AlreadyExists - Add test for duplicate game ID rejection
Status: Open — unassigned
Labels: bug, enhancement
Priority: Medium
Estimated Time: 2 hours
Description: If player1 creates a match and deposits but player2 never deposits, player1's funds are locked indefinitely. There is no expiry or timeout that allows player1 to reclaim their stake automatically.
Tasks:
- Add
created_ledger: u32field toMatchstruct, set viaenv.ledger().sequence() - Add
expire_match(match_id: u64)function that allows cancellation after a configurable ledger timeout - Add
DataKey::MatchTimeoutfor the configurable timeout value - Add test for expiry-based cancellation
Status: Open — unassigned
Labels: bug, security
Priority: High
Estimated Time: 1 hour
Description:
create_match accepts any Address as the token parameter with no validation. A malicious actor could pass a fake token contract that behaves unexpectedly during transfer calls, potentially draining the contract.
Tasks:
- Add
DataKey::AllowedToken(Address)and an admin functionadd_allowed_token(token: Address)to manage the allowlist - Reject
create_matchif token is not on the allowlist, returningError::InvalidToken - Add
InvalidTokenerror variant - Add tests for allowed and disallowed tokens
Status: Open — unassigned
Labels: enhancement
Priority: Low
Estimated Time: 2 hours
Description: There is no index mapping a player address to their match IDs. Frontends must scan all match IDs to find a player's matches, which is impractical at scale.
Tasks:
- Add
DataKey::PlayerMatches(Address)storing aVec<u64>of match IDs - Update
create_matchto append the new match ID to both players' index entries - Add
get_player_matches(env: Env, player: Address) -> Vec<u64>read function - Add test asserting the index is populated correctly after match creation
Status: Open — unassigned
Labels: bug, security
Priority: High
Estimated Time: 1 hour
Description:
The oracle contract's admin address is set once at initialize and cannot be changed. If the oracle service key is compromised, there is no recovery path short of redeploying the contract.
Tasks:
- Add
update_admin(env: Env, new_admin: Address) -> Result<(), Error>requiring current admin auth - Update
DataKey::Adminwith the new address - Add test for admin rotation
- Add test that old admin cannot call
submit_resultafter rotation
Status: Open — unassigned
Labels: bug, security
Priority: High
Estimated Time: 30 minutes
Description:
submit_result checks m.state == Active which implies both players deposited. However, if a state inconsistency bug exists, the contract could attempt to transfer more tokens than it holds, causing a panic rather than a graceful error.
Tasks:
- Add explicit
is_fundedcheck before computingpot - Return
Error::NotFundedif the match is not fully funded - Add defensive test for this scenario
Status: Open — unassigned
Labels: enhancement
Priority: Medium
Estimated Time: 1 hour
Description:
The Match struct stores no creation ledger sequence number. Without this, there is no way to implement timeouts, sort matches by age, or detect stale pending matches.
Tasks:
- Add
created_ledger: u32field toMatch, populated viaenv.ledger().sequence() - Add test asserting
created_ledgeris non-zero aftercreate_match - Use this field as the basis for future timeout logic (see #89)
Status: Open — unassigned
Labels: enhancement
Priority: Low
Estimated Time: 2 hours
Description:
There is no on-chain index of all match IDs. Frontends must iterate from 0 to MatchCount and call get_match for each, which is expensive and fragile if any match has expired from storage.
Tasks:
- Add
DataKey::ActiveMatchesstoring aVec<u64> - Append on
create_match, remove oncancel_matchandsubmit_result - Add
get_active_matches(env: Env) -> Vec<u64>read function - Add test asserting the index is updated correctly through the match lifecycle
Status: Open — unassigned
Labels: testing
Priority: Low
Estimated Time: 15 minutes
Description:
No test explicitly verifies the has_result function returns false before a result is submitted and true after. The existing test_submit_and_get_result only calls has_result after submission.
Tasks:
- Assert
has_result(0)returnsfalseon a fresh contract - Submit a result for match
0 - Assert
has_result(0)returnstrue
Status: Open — unassigned
Labels: testing
Priority: Medium
Estimated Time: 30 minutes
Description:
test_submit_result_emits_event in the oracle tests verifies the event is emitted but only checks Player1Wins. No test covers Player2Wins or Draw result events.
Tasks:
- Add test for
MatchResult::Player2Winsevent emission - Add test for
MatchResult::Drawevent emission - Assert
match_idandresultin event data are correct for each case
Status: Open — unassigned
Labels: testing
Priority: Medium
Estimated Time: 15 minutes
Description:
test_duplicate_submit_fails uses #[should_panic] without asserting the specific error code. A try_submit_result variant should assert Error::AlreadySubmitted explicitly.
Tasks:
- Call
try_submit_resulttwice for the samematch_id - Assert the second call returns
Err(Ok(Error::AlreadySubmitted))
Status: Open — unassigned
Labels: bug
Priority: Low
Estimated Time: 30 minutes
Description:
When the second player deposits and the match transitions from Pending to Active, no match_activated event is emitted. Frontends cannot detect when a match is ready to start without polling get_match.
Tasks:
- Emit a
("match", "activated")event insidedepositwhenm.statetransitions toActive - Include
match_idin event data - Add test asserting the event is emitted only on the second deposit
Status: Open — unassigned
Labels: testing
Priority: Low
Estimated Time: 15 minutes
Description:
No test verifies that MatchCount increments sequentially and that each new match receives the correct ID. The existing test_create_match only creates one match and asserts id == 0.
Tasks:
- Create 5 matches in sequence
- Assert returned IDs are
0,1,2,3,4 - Assert
get_match(4)returns the last created match correctly
Status: Open — unassigned
Labels: testing, security
Priority: High
Estimated Time: 15 minutes
Description:
test_admin_pause_blocks_create_match verifies pause works but there is no test confirming that a non-admin address cannot call unpause() to re-enable a paused contract.
Tasks:
- Admin calls
pause() - Call
unpause()from a non-admin address - Assert auth failure (panic or
Error::Unauthorized)
Status: Open — unassigned
Labels: testing
Priority: Medium
Estimated Time: 15 minutes
Description:
No test explicitly verifies that cancel_match returns Error::InvalidState when called on a Completed match. The guard exists in code but is untested for this specific state.
Tasks:
- Create match, both players deposit, call
submit_resultto complete it - Call
try_cancel_matchon the completed match - Assert
Err(Ok(Error::InvalidState))
Status: Open — unassigned
Labels: testing
Priority: Low
Estimated Time: 15 minutes
Description:
test_draw_refund verifies player balances are restored but does not assert that get_escrow_balance returns 0 after the draw payout, leaving a gap in escrow drain verification.
Tasks:
- Complete a match with
Winner::Draw - Assert
get_escrow_balance(match_id)returns0
Status: Open — unassigned
Labels: testing
Priority: Medium
Estimated Time: 15 minutes
Description:
test_payout_winner only tests Winner::Player1. There is no test verifying that Winner::Player2 correctly transfers the full pot to player2 and leaves player1 with their post-deposit balance.
Tasks:
- Create match with
stake_amount = 100, both players start with1000 - Both players deposit
- Call
submit_resultwithWinner::Player2 - Assert player2 balance is
1100and player1 balance is900