Bionttestnet
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
balance0.2 OCT
version1.0 Rehovot
code hash752648…677317
History
live · 20s · last 0
Source · ABI · Bytecode
✓ verified
expand →
✓ 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] }
}