Last updated: 2026-07-29 · Verified against: contracts/escrow/src/errors.rs and contracts/oracle/src/errors.rs
This document is the exhaustive, user-facing reference for every error a caller can receive from the two on-chain Soroban contracts in this repo:
EscrowContract—contracts/escrow/src/errors.rs(50 variants)OracleContract—contracts/oracle/src/errors.rs(21 variants)
Every variant defined in those two files is documented below. If you add, remove, or renumber a variant, update this file in the same PR.
Both contracts use Soroban's #[contracterror] macro. An error is not a
string — it's a small integer (u32) discriminant attached to the function's
Result<T, Error>. When a call fails, the CLI/SDK surfaces it as something
like:
Error(Contract, #4)
#4 is the numeric code from the tables below. Map it back to a name using
this document, then look up the cause and recovery steps.
stellar contract invoke --id $ESCROW_CONTRACT_ID -- deposit \
--match_id 42 --player <ADDRESS>
# ... Error(Contract, #4) ...
# → 4 = Unauthorized (see Escrow table below)- The on-chain error is only the numeric discriminant — no message text,
stack trace, storage contents, or argument values are ever included in the
contract's return value. This is enforced by
#[contracterror]itself, not by application logic, so there is no on-chain string to accidentally leak. - Several variants are intentionally coarse-grained for this reason. For
example,
Unauthorizedis returned both when the contract has never been initialized and when the caller is simply the wrong account — this avoids confirming or denying internal state (e.g. "does this contract have an admin set?") to an unauthenticated caller. - Off-chain consumers (frontend, oracle-service, support tooling) are the place sensitive detail can leak. When mapping these codes to user-facing UI text, do not embed request payloads, private keys, raw RPC responses, or internal match data in the displayed message — surface only the code, name, and the generic recovery guidance from this document.
- Recoverable — the caller (player, admin, or oracle) can take a concrete action — fix input, wait, switch signer, or call a different function — and the same operation will succeed afterward. No funds or state are lost.
- Fatal — the error indicates an invariant violation or a hard arithmetic/storage limit. There is no client-side retry that fixes it; it requires investigation, an admin/dev intervention, or in the worst case means that specific match is stuck (other matches are unaffected).
| Code | Name | Thrown By | Cause | Recovery | Example |
|---|---|---|---|---|---|
| 1 | MatchNotFound |
deposit, submit_result, cancel_match, expire_match, get_match, is_funded, get_depositor_count, get_escrow_balance |
match_id has no stored Match — wrong ID, typo, or wrong contract/network. |
Call get_match_count to confirm the valid ID range, or get_player_matches_paginated to re-fetch a player's real match IDs. Double-check $ESCROW_CONTRACT_ID and --network. |
get_match --match_id 999 on a contract with only 50 matches → #1. |
| 2 | AlreadyFunded |
deposit |
The same player called deposit twice for one match. |
No funds are at risk — the second call is simply rejected. Call get_depositor_count first if unsure whether you've already deposited. |
Player1 deposits, then accidentally retries the same tx after a slow confirmation → #2 on the retry; original deposit is untouched. |
| 3 | NotFunded |
submit_result (incl. via submit_result_with_oracle_record) |
Result submission was attempted before both players deposited. | Wait for both deposits; poll is_funded or get_depositor_count before asking the oracle to submit. |
Oracle submits a result the moment a game finishes, but Player2 never funded the escrow → #3. |
| 4 | Unauthorized |
pause, unpause, add_allowed_token, remove_allowed_token, deposit, submit_result, cancel_match, get_admin, get_oracle, set_match_timeout, propose_admin, accept_admin, update_oracle, transfer_admin |
Caller isn't the required signer (admin/oracle/depositing player) or the contract hasn't been initialized yet (admin/oracle key absent in storage). |
Re-sign with the correct keypair, or call initialize first on a fresh deployment. Use is_initialized to tell the two cases apart safely. |
Calling pause with a non-admin key → #4. Calling get_admin on a contract that was never initialized → also #4. |
| 4 | NotAdmin (sub-case of Unauthorized) |
pause, unpause, add_allowed_token, remove_allowed_token, set_match_timeout, propose_admin, accept_admin, update_oracle, transfer_admin |
The caller is not the configured admin address. Surfaces as Error(Contract, #4). The contract does not use a separate NotAdmin variant — Unauthorized covers all authorization failures to keep callers from probing whether an admin is set. |
Verify the signing key matches the admin returned by get_admin. If the contract is uninitialized, call initialize first (check with is_initialized). To rotate the admin, the current admin must call propose_admin/accept_admin or transfer_admin. |
Calling pause with a non-admin keypair → Error(Contract, #4). |
| 4 | NotOracle (sub-case of Unauthorized) |
submit_result, submit_result_with_oracle_record |
The caller is not the configured oracle address. Surfaces as Error(Contract, #4). Like NotAdmin, the contract returns the same Unauthorized code to avoid leaking internal state to unauthenticated callers. |
Verify the signing key matches the oracle returned by get_oracle. If the oracle address needs updating, the admin must call update_oracle with the correct new address. If the contract is uninitialized, call initialize first. |
Oracle service running with a rotated keypair that no longer matches the on-chain oracle address → Error(Contract, #4) on every submit_result call. |
| 5 | InvalidState |
deposit, submit_result, cancel_match, expire_match |
The match isn't in the lifecycle state the function requires (e.g. depositing into a Completed match, submitting a result for a non-Active match). |
Call get_match and check the state field before retrying the action. |
Calling submit_result on a match already Completed → #5. |
| 7 | AlreadyInitialized |
initialize |
initialize was called a second time. |
No action needed — the contract is already configured. Use get_admin/get_oracle to confirm current config instead of re-initializing. |
Re-running a deploy script that calls initialize unconditionally → #7 on the second run. |
| 9 | ContractPaused |
create_match, deposit, submit_result, submit_result_with_oracle_record |
Admin called pause; these functions are blocked while paused. |
Wait for the admin to call unpause; poll is_paused to know when it's safe to retry. |
create_match during an incident-response pause → #9 until unpause is called. |
| 10 | InvalidAmount |
create_match |
stake_amount <= 0, or stake_amount is below the admin-configured minimum_stake (see set_minimum_stake; defaults to 1). |
Resubmit with a positive stake_amount that meets get_protocol_config().minimum_stake. |
create_match with stake_amount = 0 → #10. set_minimum_stake(50) then create_match with stake_amount = 10 → #10. |
| 13 | DuplicateGameId |
create_match |
game_id was already used by a previous match (each game maps to exactly one escrow match, to prevent oracle replay across matches). |
Use a fresh, unique game_id, or look up the existing match instead of creating a new one. |
Two players try to escrow the same Lichess game URL twice → second create_match gets #13. |
| Code | Name | Thrown By | Cause | Recovery | Example |
|---|---|---|---|---|---|
| 14 | MatchNotExpired |
expire_match |
expire_match was called before current_ledger - created_ledger >= timeout. |
Wait until the configured timeout elapses. Check get_match_timeout and the match's created_ledger (via get_match) to compute the earliest valid ledger. Note: This timeout is the primary safety mechanism protecting players if the oracle goes offline — see FAQ: What happens if the oracle goes offline? |
Calling expire_match one day into a 30-day default timeout → #14. |
| 15 | InvalidGameId |
create_match |
game_id is empty or longer than 64 bytes. |
Pass a valid Lichess (8-char alphanumeric) or Chess.com (numeric) game ID under the 64-byte limit. | create_match with game_id = "" → #15. |
| 16 | InvalidPlayers |
create_match |
player1 == player2, or player2 is the escrow contract's own address. |
Supply two distinct, real player addresses. | create_match where both players are the same wallet → #16. |
| 17 | TokenNotAllowed |
create_match, create_match_with_conversion, create_match_with_referrer |
Either the token allowlist is active (at least one token was ever added) and the supplied token isn't on it, or the token (or, for conversion matches, either token) is on the admin-managed blacklist (add_token_to_blacklist) — checked first and takes precedence over allowlist status. |
Admin must call add_allowed_token for that token, or the caller should pick an already-allowed one via get_allowed_tokens; if blacklisted, use a different token or have admin call remove_token_from_blacklist. |
create_match with an unlisted custom token after the admin enabled allowlisting, or with a token an admin previously blacklisted for fraud (rejected even if it's also on the allowlist) → #17. |
| 18 | InvalidAddress |
initialize, update_oracle |
The oracle/new_oracle address equals the escrow contract's own address. |
Supply a distinct external account or contract address. | initialize called with oracle = <ESCROW_CONTRACT_ID itself> → #18. |
| 19 | MatchAlreadyActive |
cancel_match |
cancel_match was called on a match that's already Active (both players deposited) — voluntary cancellation is pre-activation only. |
Let the match proceed to submit_result, or wait for expire_match eligibility if it stalls. Active matches cannot be cancelled by players. |
A player tries to back out after both stakes are in → #19. |
| 20 | InvalidTimeout |
set_match_timeout |
seconds is outside [86,400, 7,776,000] (1–90 days, wall-clock seconds). |
Pass a timeout within the 1–90 day range, in seconds. Use MIN_MATCH_TIMEOUT_SECONDS = 86,400 (1 day) and MAX_MATCH_TIMEOUT_SECONDS = 7,776,000 (90 days) as bounds. |
set_match_timeout with seconds = 100 (≈1.5 minutes) → #20. |
| 21 | SnapshotNotFound |
submit_result (ledger snapshot verification) |
An internal ledger snapshot required to verify the oracle's result proof is not available — typically when the result is submitted too far in the past (TTL expired) or ledger data was purged. | Resubmit the result sooner after the game finishes. Ensure oracle service processes results within a few hours of completion, not days later. | Oracle attempts to verify a result 1+ months after the game ended → #21 (ledger snapshot purged). |
| Code | Name | Thrown By | Cause | Recovery | Example |
|---|---|---|---|---|---|
| 6 | AlreadyExists |
create_match |
A Match already exists at the storage slot for the next sequential match ID before create_match assigns it. Under normal operation MatchCount is the sole source of the next ID, so this should never trigger. |
Not client-recoverable. Indicates storage/state corruption or a bug in ID assignment — requires admin/dev investigation; in the worst case, a contract migration. | Would only be observed after manual storage tampering or a contract bug — not reachable via the public API in current code. |
| 8 | Overflow |
add_allowed_token (token counter), create_match (match counter), submit_result (stake_amount * 2) |
An arithmetic guard (checked_add/checked_mul) tripped: a counter hit u32/u64::MAX, or stake_amount is large enough that doubling it overflows i128. |
Counter overflow isn't realistically recoverable (would require billions of matches/tokens) short of a contract upgrade. Pot overflow is fatal for that one match only — it must be guarded against at create_match time by capping stake_amount well under i128::MAX / 2; once such a match exists, submit_result will always revert, so the only path forward is cancel_match/expire_match to return the deposits. |
A match created with stake_amount near i128::MAX / 2 will permanently fail submit_result with #8 — recover player funds via expire_match instead. |
| 11 | RollbackWindowExpired | dispute_and_rollback_match | Called after the 24h heartbeat window (ROLLBACK_WINDOW_SECONDS) had already elapsed since last_heartbeat. | Use expire_match/cancel_match instead, or call heartbeat_match before the window lapses next time. | Disputing a match 25h after the last heartbeat → #11. |
| 12 | ReasonTooLong | dispute_and_rollback_match | reason was empty or longer than MAX_REASON_LEN (256 bytes). | Resubmit with a non-empty reason under 256 bytes. | Passing a 300-byte reason string → #12. |
Note on platform: create_match takes platform: Platform, a typed enum (Platform::Lichess / Platform::ChessDotCom). Because it's a typed enum rather than a free-form string, an invalid platform value is rejected by the contract ABI itself (an unrecognized discriminant fails to deserialize) before the call ever reaches contract code — there is no "unknown platform" case for the contract to return a typed error for. The escrow error enum is also already at the XDR-enforced cap of 50 cases (ScSpecUdtErrorEnumV0::cases is a VecM<_, 50>), so no additional error code is available to add for this even if it were reachable.
| 51 | InvalidPlatform | Defined in errors.rs but not reachable via the public API today. platform is a typed Platform enum (Lichess / ChessDotCom) — the ABI rejects any other discriminant before the call reaches contract code, so create_match can never observe an "unknown platform" value to reject. Reserved for forward compatibility if platform ever needs dynamic (string-based) construction. |
These error codes support advanced features including dispute resolution, staking tiers, vesting schedules, and contract upgrades:
| Code | Name | Thrown By | Cause | Recovery | Example |
|---|---|---|---|---|---|
| 22 | VestingNotExpired |
Vesting check functions | Attempting to claim vested payout before the vesting period elapses | Wait until the vesting period expires, then retry the claim | Calling claim_vested_payout before the configured vesting duration has passed → #22. |
| 23 | AlreadyClaimed |
Payout claim functions | A player has already claimed their payout for this match | No action needed — the payout was already received. Check account balance or transaction history to confirm. | Player1 calls claim_vested_payout twice for the same match → #23 on the second call. |
| 24 | DisputeNotFound |
Dispute resolution functions | Attempting to resolve or query a dispute that doesn't exist | Confirm the dispute ID exists via the dispute listing functions | Admin calls resolve_disputed_match with an invalid dispute ID → #24. |
| 25 | PendingResultNotFound |
Dispute functions | No pending result exists for the match yet | Submit a result first before initiating a dispute | Attempting to dispute a match that has no oracle result submitted → #25. |
| 26 | DisputeAlreadyResolved |
Dispute functions | The dispute has already been resolved by the admin | No further action needed — the resolution is final | Attempting to vote or resolve a dispute that's already been settled → #26. |
| 27 | VotingPeriodElapsed |
Oracle voting functions | The voting window for this consensus vote has closed | Wait for the next voting round or submit through a different path | Attempting to vote on an oracle result after the voting period ended → #27. |
| 28 | AlreadyVoted |
Oracle voting functions | The oracle has already cast a vote on this match | No action needed — vote is already recorded. To change a vote, resubmit a different result. | An oracle calls submit_oracle_result with a different winner after already voting → #28 (equivocation). |
| 29 | NotStaker |
Staking/tier functions | Caller is not a registered staker or doesn't meet tier requirements | Register with the staking system or deposit the minimum tier amount | Non-staked player attempts to participate in a tier-restricted match → #29. |
| 30 | VotingPeriodNotElapsed |
Dispute resolution | Attempting to finalize consensus before the voting window closes | Wait for the voting period to fully elapse | Calling finalize_consensus before all oracles have had time to vote → #30. |
| 31 | MatchNotInPendingResult |
Dispute functions | The match is not in the "pending result" state required for disputes | Confirm the match has a submitted result but is not yet finalized | Attempting to dispute a match in Completed state → #31. |
| 32 | DisputePeriodNotElapsed |
Dispute resolution | Attempting to finalize a dispute before the dispute window closes | Wait for the dispute period to expire | Admin tries to resolve a dispute too soon → #32. |
| 33 | DisputeAlreadyRaised |
Dispute creation | A dispute has already been raised for this match | No action needed — one dispute is already in progress. Monitor the resolution process. | Attempting to raise a second dispute for the same match → #33. |
| 34 | InvalidEvidenceHash |
Dispute functions | The evidence hash format is invalid or missing | Provide a valid cryptographic hash of the dispute evidence | Submitting evidence with a malformed or incorrectly-sized hash → #34. |
| 35 | TierStakeNotAllowed |
Match creation with tiers | Stake amount doesn't align with player tier requirements | Adjust stake to match the player's tier bracket, or upgrade tier by staking more | Player in Tier 1 (1–100 XLM) tries to create a match with 500 XLM stake → #35. |
| 36 | NotInitialized |
Read functions on uninitialized contract | Contract has not been initialized yet | Call initialize first (admin must do this after deployment) |
Calling get_admin before initialize on a fresh contract → #36. |
| 37 | InvalidPauseState |
Pause/unpause functions | Attempting to pause an already-paused contract or unpause a running one | Check current pause state via is_paused before calling pause/unpause |
Calling pause when contract is already paused → #37. |
| 38 | InvalidConversionRate |
Rate verification | The conversion rate submitted is not a valid positive number | Supply a positive, non-zero conversion rate | Submitting a swap with conversion rate = 0 → #38. |
| 39 | ConversionRateOutOfBounds |
Swap/token rate functions | The conversion rate exceeds acceptable bounds (typically ±5% of oracle rate) | Resubmit with a rate within tolerance, or wait for oracle to refresh | Submitting a swap rate 10% higher than the oracle rate when only ±5% is allowed → #39. |
| 40 | ConversionRateStalePriceSource |
Rate validation | The price source used for rate validation is stale (too old) | Refresh the price feed from the oracle and retry | Attempting a swap using a price quote older than the configured TTL → #40. |
| 41 | InsufficientBond |
Bond/stake system | Caller hasn't posted the required bond for the operation | Post the minimum bond amount and retry | Attempting a dispute without having posted the bond → #41. |
| 42 | QuorumNotMet |
Consensus functions | Consensus voting hasn't reached the required quorum | Wait for more oracles to vote | Attempting to finalize consensus before enough oracles have voted → #42. |
| 43 | InsufficientHoldingDuration |
Tier/staking functions | Staked tokens don't meet the minimum holding-period requirement | Wait longer, or unstake and re-stake to reset the timer | Player tries to use tier benefits before their stake has been locked for the required time → #43. |
| 44 | OracleSlashFailed |
Internal slashing logic | Attempted to slash an oracle's stake but the operation failed | Not recoverable by caller; indicates a contract state issue requiring investigation | Slashing logic attempted but underlying state became inconsistent → #44 (fatal). |
| 45 | TooManyActiveMatches |
Match creation | The player has exceeded the maximum concurrent active matches | Wait for some existing matches to complete/cancel | Player with 50 active matches tries to create a 51st → #45. |
| 46 | NotStablecoin |
Match creation (stablecoin-only mode) | Token is not a registered stablecoin and stablecoin-only mode is enabled | Use a stablecoin token (e.g., USDC, EURC) or have admin disable stablecoin-only mode | Creating a match with a non-stablecoin token when the contract is in stablecoin-only mode → #46. |
| 47 | UpgradeNotScheduled |
Contract upgrade functions | Attempting to execute an upgrade that hasn't been scheduled | Schedule the upgrade first via the admin upgrade functions | Calling execute_upgrade without a prior schedule_upgrade → #47. |
| 48 | UpgradeReviewPeriodNotElapsed |
Contract upgrade | Attempting to execute an upgrade before the review/delay period expires | Wait for the configured review window to pass | Trying to execute an upgrade 1 hour after scheduling when the minimum is 24 hours → #48. |
| 49 | InvalidVersion |
Contract upgrade | The contract version specified in the upgrade is invalid or doesn't exist | Supply a valid contract version identifier | Scheduling an upgrade to a version that doesn't exist → #49. |
| 50 | UpgradeAlreadyScheduled |
Contract upgrade | An upgrade is already scheduled; only one upgrade can be pending at a time | Execute or cancel the existing scheduled upgrade first | Attempting to schedule a second upgrade while one is already pending → #50. |
All 22 variants are primarily recoverable — the majority represent client-side issues or rate limit exceedances, though a few indicate internal oracle stake/consensus state requiring investigation.
| Code | Name | Thrown By | Cause | Recovery | Example |
|---|---|---|---|---|---|
| 1 | Unauthorized |
submit_result, submit_batch_results, submit_oracle_result, has_result_admin, delete_result, update_admin, pause, unpause, set_oracle_rate_limits, register_oracle_with_stake |
Caller isn't the configured admin, or the contract hasn't been initialized (admin key absent). |
Re-sign with the correct admin keypair, or call initialize first. Use is_initialized to distinguish the two cases. |
submit_result signed by a non-admin oracle service key → #1. |
| 2 | AlreadySubmitted |
submit_result, submit_batch_results, submit_oracle_result |
A result for match_id is already stored — results are immutable once recorded (integrity guard). |
Check has_result/get_result before submitting. If a genuine correction is needed, admin must delete_result first, then resubmit. |
The oracle service retries a submission after a network timeout, not realizing the first attempt actually landed → #2 on the retry (safe — no duplicate result is written). |
| 3 | ResultNotFound |
get_result, delete_result |
No result exists for match_id — never submitted, wrong ID, or the persistent entry's TTL expired and was purged. |
Confirm match_id, check has_result to see if it was ever submitted, or submit the result if it's genuinely missing. |
get_result --match_id 7 before the oracle has reported anything for match 7 → #3. |
| 4 | AlreadyInitialized |
initialize |
initialize was called a second time. |
No action needed — the contract is already configured. | Re-running a deploy script unconditionally → #4 on the second run. |
| 5 | ContractPaused |
submit_result, submit_batch_results, submit_oracle_result, delete_result |
Admin called pause. |
Wait for unpause; poll a paused-status check before retrying. |
Result submission attempted during an incident-response pause → #5. |
| 6 | InvalidGameId |
submit_result, submit_batch_results, submit_oracle_result |
game_id is empty in the submission (or in any batch entry). |
Resubmit with the real platform game ID populated. | A batch entry built from a malformed scrape with game_id = "" → #6. |
| 7 | BatchTooLarge |
submit_batch_results |
entries.len() > 100 (MAX_BATCH_SIZE). |
Split the batch into chunks of ≤100 entries. | Submitting 250 tournament results in one call → #7. |
| 8 | BatchDuplicateEntry |
submit_batch_results |
Two entries in the same batch share a match_id. |
De-duplicate entries client-side — each match_id may appear once per batch. |
A batch builder accidentally includes the same match_id twice after a join bug → #8. |
| 9 | RateLimitExceeded |
submit_result, submit_batch_results, submit_oracle_result (via check_oracle_rate_limit) |
The submission(s) would exceed the oracle's configured hourly or daily sliding-window limit (see set_oracle_rate_limits). |
Check get_oracle_rate_limit_status for remaining quota and window reset timing; wait for the window to roll over, or have the admin raise the limit. |
An oracle service burst-submits 150 results in one hour against the default 100/hour limit → #9 once the limit is hit, with an oracle / alert event already emitted at 80% usage. |
| 10 | InvalidRateLimit |
set_oracle_rate_limits |
hourly_limit > daily_limit when both are non-zero. |
Pass consistent limits (hourly_limit <= daily_limit), or pass 0 for either to fall back to the contract default. |
set_oracle_rate_limits(oracle, 500, 100) → #10. |
| 11 | InsufficientStake |
submit_oracle_result, submit_batch_results (for consensus-based submission) |
The oracle has registered stake but it has been slashed to zero or is below the minimum required for participation. | Re-register with register_oracle_with_stake and deposit sufficient collateral. |
An oracle's stake was slashed due to equivocation or SLA violations, and they attempt to submit without re-staking → #11. |
| 12 | NotRegisteredOracle |
submit_oracle_result (consensus submission path) |
submit_oracle_result was called by an address that has never registered via register_oracle_with_stake. |
Call register_oracle_with_stake first with the required collateral amount. |
A new oracle service instance attempts consensus voting without first registering → #12. |
| 14 | MatchDisputed |
submit_oracle_result |
The match's consensus has deadlocked (no remaining oracle vote can push any candidate result over the threshold) and is awaiting admin resolution via resolve_disputed_match. |
Do not submit further votes; await admin resolution. Once resolved, the final result will be recorded. | During a close vote, no oracle can push any candidate over the threshold, so the match enters disputed state and submit_oracle_result returns #14. |
| 15 | InvalidThreshold |
set_consensus_threshold |
set_consensus_threshold was called with a threshold of 0 (invalid for consensus). |
Pass a threshold ≥ 1 (e.g., 2 for 2-of-N voting). | set_consensus_threshold(oracle, 0) → #15. |
| 16 | MatchNotDisputed |
resolve_disputed_match |
resolve_disputed_match was called for a match that is not in a disputed (deadlocked) consensus state. |
Confirm the match is actually disputed by checking its consensus state. Do not call resolve on a non-disputed match. | Admin tries to resolve a match that finished normally (not disputed) → #16. |
| 17 | OracleDeactivated |
submit_oracle_result, submit_result (SLA enforcement) |
The oracle has been deactivated due to repeated SLA violations (slow response time, low accuracy, or other metrics). | Not immediately recoverable — oracle must contact admin or re-register after a cooldown to restore status. | An oracle's average response time drifted above 5s SLA threshold, triggering automatic deactivation → #17. |
| 18 | OracleNotSlow |
deactivate_slow_oracle |
Attempted to deactivate an oracle that has a good SLA (average response time ≤ 5s). | Do not call deactivation on a well-performing oracle. | Admin tries to deactivate an oracle with 2s average response time → #18. |
| 19 | InvalidAmount |
Rate/stake functions | A stake or fee amount is invalid (typically zero or negative). | Supply a positive amount. | Calling register_oracle_with_stake with stake_amount = 0 → #19. |
| 20 | Overflow |
Arithmetic operations (stake accumulation, voting tallies) | An arithmetic guard tripped: a counter or accumulated amount exceeded numeric bounds. | Not typically recoverable client-side. Indicates a contract state issue; contact admin for investigation. | After thousands of slash+re-stake cycles, the oracle's tally counters overflow → #20 (rare, fatal). |
| 21 | SlippageExceeded |
Rate/swap validation | The price changed beyond acceptable slippage bounds between submission and execution. | Resubmit with a wider slippage tolerance or wait for prices to stabilize. | Submitting a swap with 0.5% max slippage when market moved 1% → #21. |
| 22 | StakeTokenMismatch |
register_oracle_with_stake |
The oracle already has a registration, and this call's token differs from the token backing the existing registration — stake in two different tokens cannot be summed. |
Re-register (top up) using the same token as the original registration, or coordinate with the admin to migrate the stake before switching tokens. | An oracle registered with USDC, then attempts to top up its stake with EURC → #22; the existing USDC-denominated stake is left untouched. |
Use this when you only know the symptom, not the code.
| Symptom | Likely error(s) | First thing to check |
|---|---|---|
| "Transaction failed, can't tell why" | Any | Decode the numeric code from the tx result (Error(Contract, #N)), then look it up above. |
| Deposit/submit/cancel rejected right after deploy | Unauthorized (Escrow #4 / Oracle #1) |
Did you call initialize on this contract yet? is_initialized. |
submit_result rejected — oracle key mismatch |
NotOracle / Unauthorized (Escrow #4) |
Confirm the oracle service key matches get_oracle; if rotated, admin must call update_oracle. |
| Admin call rejected — admin key mismatch | NotAdmin / Unauthorized (Escrow #4 / Oracle #1) |
Confirm you're signing with the key returned by get_admin. Use is_initialized to rule out uninitialized contract. |
| Player can't deposit | MatchNotFound (#1), InvalidState (#5), AlreadyFunded (#2), Unauthorized (#4) |
get_match — confirm the ID exists, state is Pending, and you haven't already deposited. |
| Oracle can't submit a result | ContractPaused (#9 / #5), MatchNotFound (#1), NotFunded (#3), Unauthorized (#4 / #1), RateLimitExceeded (#9 oracle) |
is_paused, is_funded, get_oracle_rate_limit_status. |
| Oracle can't submit a result | ContractPaused (#9 / #5), MatchNotFound (#1), NotFunded (#3), Unauthorized (#4 / #1), RateLimitExceeded (#9 oracle) |
is_paused, is_funded, get_oracle_rate_limit_status. |
create_match rejected |
InvalidAmount (#10), InvalidGameId (#15), DuplicateGameId (#13), InvalidPlayers (#16), TokenNotAllowed (#17) |
Validate stake_amount > 0, game_id format/uniqueness, distinct players, get_allowed_tokens if allowlisting is on, and is_token_blacklisted to verify the token is not banned. TokenNotAllowed (#17) covers both an allowlist miss and an admin-managed blacklist hit (add_token_to_blacklist) — the Error enum is at its 49-variant ceiling (see contracts/escrow/src/errors.rs), so blacklist rejection reuses this code rather than adding a new one. |
| Can't cancel a match | MatchAlreadyActive (#19), InvalidState (#5), Unauthorized (#4) |
get_match — cancellation only works on Pending matches you're a player in. |
expire_match rejected |
MatchNotExpired (#14), InvalidState (#5), MatchNotFound (#1) |
Compare get_match_timeout against the match's created_ledger. |
| Oracle batch submission rejected | BatchTooLarge (#7), BatchDuplicateEntry (#8), InvalidGameId (#6), AlreadySubmitted (#2) |
Validate the batch client-side before sending: size ≤100, unique match_ids, non-empty game_ids. |
| Admin config call rejected | Unauthorized (#4 / #1), InvalidTimeout (Escrow #20), InvalidRateLimit (Oracle #10), InvalidAddress (Escrow #18) |
Confirm you're signing with the current admin key and that the new value is within the documented bounds. |
A match seems permanently stuck on submit_result |
Overflow (Escrow #8, fatal) |
Check stake_amount isn't absurdly large; recover funds via cancel_match/expire_match instead of retrying submit_result. |
This document covers all variants present in source as of 2026-07-29:
- Escrow (
contracts/escrow/src/errors.rs): 50/50 variants documented (including new variants 22–50 for dispute resolution, vesting, tiers, and upgrades). - Oracle (
contracts/oracle/src/errors.rs): 21/21 variants documented (including new variants for oracle staking and SLA enforcement).
If cargo build or a code review surfaces a new variant in either
errors.rs, add a row here in the same PR — this file is expected to stay in
lockstep with the source enums.