Address
octCoX8Q…TAUC1r
octCoX8QtTMBFoCgiW7FZCMvdE37sLq5fTV84nfGkTAUC1r
State
OCT balance
0.2OCT
wallet balance
Type
Contract
smart contract on chain
Chain-level
View on Octrascan · devnet ↗
raw txs · nonce · pubkey
Pipoke (no profile)
this wallet has not registered a Pipoke profile
Contract
History
live · 20s · last 0Source · ABI · Bytecode✓ verifiedexpand →
Source · ABI · Bytecode
✓ verified✓ verified
// ============================================================================
// OctraChess — PvP Chess Betting Contract
// ============================================================================
//
// Holds the pot for one or more active chess games at any time.
// Players deposit equal stakes via create_game / join_game.
// The contract pays out via settle (operator-only) or timeout_refund (anyone).
//
// Invariants:
// - No setter for fee/operator/treasury addresses. They are deploy-time fixed.
// - Operator can ONLY call settle(gameId, result). Cannot drain funds or
// redirect fees. Worst-case key compromise affects only games active during
// the compromise window — past games are immutable.
// - timeout_refund is permissionless: if the operator is dead/compromised,
// any caller (including the affected players) can return both stakes after
// SETTLE_GRACE_EPOCHS have passed since the match started.
// - Reentrancy guard on every state-mutating function via nonreentrant.
//
// Status codes:
// 1 = Open — created, awaiting opponent
// 2 = Active — opponent joined, game in progress
// 3 = Settled — winner / draw paid out
// 4 = Refunded — cancelled or timeout-refunded
//
// Result codes (for settle):
// 1 = WhiteWon
// 2 = BlackWon
// 3 = Draw
// ============================================================================
contract OctraChess {
// -- IMMUTABLE CONSTANTS (compile-time, no storage, no setter) -------------
const FEE_BPS: int = 1000 // 10% total fee
const DEV_BPS: int = 500 // 5% to dev treasury
const AIRDROP_BPS: int = 500 // 5% to airdrop treasury
const MIN_STAKE: int = 100000 // 0.1 OCT in ou
const MAX_STAKE: int = 1000000000 // 1000 OCT in ou (alpha cap per §6.5)
// SETTLE_GRACE_EPOCHS — see verification note 1 in spec §12: empirically
// calibrate on devnet so this covers ~2 hours of wall-clock time. Default 720.
const SETTLE_GRACE_EPOCHS: int = 720
// -- STATE (set by constructor, then never modified) ----------------------
state {
operator: address // signs settle()
dev_wallet: address // 5% recipient
airdrop_wallet: address // 5% recipient
next_game_id: int
games: map[int]Game
open_by_creator: map[address]int // address → active "open" game id (0 = none)
active_by_player: map[address]int // address → active "active" game id (0 = none)
}
// -- TYPES ---------------------------------------------------------------
struct Game {
white: address
black: address // zero until joined; joined=true marks valid
stake: int // per-player, in ou
status: int // 1=Open, 2=Active, 3=Settled, 4=Refunded
started_at: int // epoch when status flipped to Active
joined: bool // true once an opponent has joined
}
// -- EVENTS --------------------------------------------------------------
event GameCreated(id: int, white: address, stake: int)
event GameJoined(id: int, black: address)
event GameCancelledOpen(id: int)
event GameSettled(id: int, winner: address, pot: int, dev_cut: int, airdrop_cut: int, winner_cut: int)
event GameSettledDraw(id: int, pot: int, dev_cut: int, airdrop_cut: int, refund_each: int)
event GameRefundedTimeout(id: int)
// -- ERRORS --------------------------------------------------------------
error EBadStake(400, "stake outside MIN/MAX")
error EAlreadyOpen(409, "creator already has open game")
error EAlreadyActive(409, "player already in active game")
error EBadStatus(409, "game not in required status")
error ENotPlayer(403, "caller not player of this game")
error ENotOperator(403, "caller not operator")
error EBadResult(400, "result code must be 1, 2, or 3")
error EWrongStake(400, "join must attach exactly the stake")
error ENotYet(425, "settle grace period not elapsed")
error ESelfPlay(409, "cannot join your own game")
// -- CONSTRUCTOR ---------------------------------------------------------
constructor(op: address, dev: address, airdrop: address) {
assert_address(op)
assert_address(dev)
assert_address(airdrop)
self.operator = op
self.dev_wallet = dev
self.airdrop_wallet = airdrop
self.next_game_id = 1
}
// -- CREATE: player opens a new game and locks their stake --------------
payable fn create_game(stake: int): int {
require(stake >= MIN_STAKE, "stake outside MIN/MAX")
require(stake <= MAX_STAKE, "stake outside MIN/MAX")
require(value == stake, "join must attach exactly the stake")
require(self.open_by_creator[caller] == 0, "creator already has open game")
require(self.active_by_player[caller] == 0, "player already in active game")
let id = self.next_game_id
self.next_game_id = id + 1
self.games[id].white = caller
self.games[id].stake = stake
self.games[id].status = 1
self.games[id].started_at = 0
self.games[id].joined = false
self.open_by_creator[caller] = id
emit GameCreated(id, caller, stake)
return id
}
// -- JOIN: opponent matches an Open game with equal stake ---------------
payable fn join_game(gameId: int): bool {
require(self.games[gameId].status == 1, "game not in required status")
require(caller != self.games[gameId].white, "cannot join your own game")
require(value == self.games[gameId].stake, "join must attach exactly the stake")
require(self.active_by_player[caller] == 0, "player already in active game")
self.games[gameId].black = caller
self.games[gameId].status = 2
self.games[gameId].started_at = epoch
self.games[gameId].joined = true
let white_addr = self.games[gameId].white
self.open_by_creator[white_addr] = 0
self.active_by_player[white_addr] = gameId
self.active_by_player[caller] = gameId
emit GameJoined(gameId, caller)
return true
}
// -- CANCEL OPEN: creator pulls their stake before anyone joined --------
nonreentrant fn cancel_open_game(gameId: int): bool {
require(self.games[gameId].status == 1, "game not in required status")
require(caller == self.games[gameId].white, "caller not player of this game")
let stake = self.games[gameId].stake
self.games[gameId].status = 4
self.open_by_creator[caller] = 0
transfer(caller, stake)
emit GameCancelledOpen(gameId)
return true
}
// -- SETTLE: operator declares result, contract pays out ----------------
// result codes: 1 = WhiteWon, 2 = BlackWon, 3 = Draw
nonreentrant fn settle(gameId: int, result: int): bool {
require(caller == self.operator, "caller not operator")
require(result >= 1, "result code must be 1, 2, or 3")
require(result <= 3, "result code must be 1, 2, or 3")
require(self.games[gameId].status == 2, "game not in required status")
let stake = self.games[gameId].stake
let white_addr = self.games[gameId].white
let black_addr = self.games[gameId].black
let pot = stake * 2
let total_fee = pot * FEE_BPS / 10000
let dev_cut = pot * DEV_BPS / 10000
let airdrop_cut = total_fee - dev_cut // guarantees dev+airdrop == total_fee
let payable_pot = pot - total_fee
self.games[gameId].status = 3
self.active_by_player[white_addr] = 0
self.active_by_player[black_addr] = 0
transfer(self.dev_wallet, dev_cut)
transfer(self.airdrop_wallet, airdrop_cut)
if result == 1 {
transfer(white_addr, payable_pot)
emit GameSettled(gameId, white_addr, pot, dev_cut, airdrop_cut, payable_pot)
} else {
if result == 2 {
transfer(black_addr, payable_pot)
emit GameSettled(gameId, black_addr, pot, dev_cut, airdrop_cut, payable_pot)
} else {
let each = payable_pot / 2
transfer(white_addr, each)
transfer(black_addr, payable_pot - each) // remainder (<=1 ou) goes to black
emit GameSettledDraw(gameId, pot, dev_cut, airdrop_cut, each)
}
}
return true
}
// -- TIMEOUT REFUND: permissionless escape hatch -----------------------
// If operator fails to settle within SETTLE_GRACE_EPOCHS, anyone can
// return both stakes to players, no fee charged.
nonreentrant fn timeout_refund(gameId: int): bool {
require(self.games[gameId].status == 2, "game not in required status")
require(epoch >= self.games[gameId].started_at + SETTLE_GRACE_EPOCHS, "settle grace period not elapsed")
let stake = self.games[gameId].stake
let white_addr = self.games[gameId].white
let black_addr = self.games[gameId].black
self.games[gameId].status = 4
self.active_by_player[white_addr] = 0
self.active_by_player[black_addr] = 0
transfer(white_addr, stake)
transfer(black_addr, stake)
emit GameRefundedTimeout(gameId)
return true
}
// -- VIEWS --------------------------------------------------------------
view fn get_game(gameId: int): (address, address, int, int) {
return (
self.games[gameId].white,
self.games[gameId].black,
self.games[gameId].stake,
self.games[gameId].status
)
}
view fn get_game_timing(gameId: int): (int, int, bool) {
return (
self.games[gameId].started_at,
self.games[gameId].status,
self.games[gameId].joined
)
}
view fn get_operator(): address { return self.operator }
view fn get_dev_wallet(): address { return self.dev_wallet }
view fn get_airdrop_wallet(): address { return self.airdrop_wallet }
view fn get_fee_bps(): int { return FEE_BPS }
view fn get_min_stake(): int { return MIN_STAKE }
view fn get_max_stake(): int { return MAX_STAKE }
view fn get_grace_epochs(): int { return SETTLE_GRACE_EPOCHS }
view fn get_next_game_id(): int { return self.next_game_id }
view fn active_game_of(p: address): int { return self.active_by_player[p] }
view fn open_game_of(p: address): int { return self.open_by_creator[p] }
}