# Smart contract constraints (`IPvpGameV2` + protocol ledger)

PvP games wager players against each other into a shared pot — **no house liquidity is at risk**, so
there is no `quoteCaps` / `quoteRiskParams` / reserved-profit machinery (contrast `@chain/casino-sdk`).
The protocol's job is escrow, the contribution ledger, the protocol fee, and settlement transfers.
**Turn order is enforced by the game, not the protocol** (see "Turn order & skipping").

API v2 separates **protocol-owned money/accounting** from **game-owned policy**. The protocol does
not assume a fixed buy-in, a unique seat per address, a creator privilege, a player cap, or a timeout
cancellation policy.

The canonical interface lives at [`../simulator/contracts/IPvpGameV2.sol`](../simulator/contracts/IPvpGameV2.sol).

***

## Game interface

Your game **must** implement `IPvpGameV2`. All hooks are **`view`** (stateless policy). The protocol
records contributions, claims, and phase; games read a bounded `LobbyContext` plus ledger views
through `msg.sender` as `IPvpLobbyLedgerV2`.

| Hook             | Responsibility                                                                                                  |
| ---------------- | --------------------------------------------------------------------------------------------------------------- |
| `onLobbyOpen`    | Validate asset/config/open data. Optionally assign a non-zero `lobbyKey` unique within this game's namespace.   |
| `onEntry`        | Accept or reject the caller's explicit stake (including zero) and return a `positionId`. Revert to reject.      |
| `onLobbyStart`   | Validate the actor and readiness; return the first `PvpStepResult` (state, phase, optional randomness request). |
| `onPlayerAction` | Apply `actionData` for the forwarded actor. The protocol does **not** check turn order.                         |
| `onRandomness`   | Consume fulfilled randomness; may return the turn to the same actor via `gameState`.                            |
| `canCancel`      | Own every normal and recovery cancellation permission. The protocol adds no fill/action timeout.                |
| `getClaim`       | For claimable settlement, quote one stable `claimId` → recipient + amount from final state.                     |

There is **no** `quoteLobby`, **no** protocol `canStart` boolean separate from `onLobbyStart`, and
**no** vault identity in the participant model — the submitting address is the participant.

### Unbiased d6 from `bytes32` randomness (MUST)

If you map facet bytes to faces `1..6` via a **byte walk**, use rejection sampling (`byte < 252`
then `(byte % 6) + 1`). **Do not** use raw `(randomness[i] % 6) + 1`. For many dice per fulfillment,
you may instead use `keccak256(abi.encode(randomness, uniqueSalt))` then `word % 6`. See
[`RANDOMNESS_DICE.md`](./RANDOMNESS_DICE.md).

***

## Protocol responsibilities

* Whitelist the game and the lobby's single immutable ERC20 escrow asset.
* Store lobby lifecycle state, the game-defined `lobbyKey`, opaque `config`, and opaque `gameState`.
* Enforce non-zero lobby-key uniqueness within the game contract's namespace.
* Transfer only the explicit stake authorized by the entering address.
* Append every accepted entry to the contribution ledger and maintain participant/position aggregates.
* Expose indexed ledger reads to games and paginated reads to host applications.
* Request randomness and validate phase transitions.
* Deduct the protocol fee once at resolution:
  `fee = pot * protocolFeeBps / 10_000`, `distributable = pot - fee`.
* Derive the settlement mode from the recipients returned at resolution: exactly **one** recipient
  is paid the whole distributable pot immediately (a failed transfer is parked for `claimPayout`);
  zero or several recipients settle via claims.
* Track collected claim IDs and cap claimable transfers at the remaining distributable pot.
* On cancellation, make each participant's **full aggregate stake** independently refundable.

`protocolFeeBps` is a protocol-level governance constant (not game-controlled). The game only decides
who receives the distributable pot and how.

The fee is split inside the protocol (game developer, bonus reserve, room operator, team). The
operator share goes to the frontend that opened the lobby; operator registration is not open yet,
so today it resolves to the protocol's own operator. None of this changes `distributable`. See
"Operators" in `CHAIN_WTF_PVP_GAMES.md`.

***

## Game responsibilities (money-safety checklist)

* **Phase-gate entries** when your arrays or rules assume a closed set of participants after start
  (Bones reverts unless `ctx.phase == WAITING_FOR_PLAYERS`). The protocol does not invent seats.
* **Bound player-submitted numbers** that feed sums or multiplications (a score-submission game
  should cap scores, e.g. at `type(uint64).max`) so resolution cannot overflow under Solidity 0.8
  checked math and freeze the pot.
* **Return recipients deliberately**: exactly one recipient means that address is paid the whole
  distributable pot immediately; return several (or none, when not enumerable) for claimable
  distributions.
* **Assign rounding dust deliberately** in claimable splits so claim quotes sum exactly to the
  distributable pot (the Bones draw split gives the dust to seat 0).
* **Derive lobby keys** from everything that should make a lobby unique (e.g. Jackpot:
  `keccak256(abi.encode(token, config))`). Do not key only on a timestamp field that another opener
  can squat.
* **Treat contribution IDs as dense** `0 .. contributionCount - 1` in append order when walking or
  binary-searching the ledger (documented on `getContribution`).

***

## Entry and position accounting

Each accepted entry becomes one ledger record:

| Field            | Meaning                                                                      |
| ---------------- | ---------------------------------------------------------------------------- |
| `contributionId` | Dense per-lobby 0-based index in append order (`0 .. contributionCount - 1`) |
| `participant`    | Entering address                                                             |
| `amount`         | Accepted stake for this entry                                                |
| `positionId`     | Game-defined position key                                                    |
| `cumulativePot`  | Pot total after this entry (enables weighted ticket search)                  |
| `contributedAt`  | Timestamp                                                                    |

Returning the **same** `positionId` aggregates repeated entries into one position. Returning
**different** IDs keeps them independent.

Examples:

* **Bones** — rejects `ParticipantContext.joined == true`; `positionId` is address-derived; fixed
  exact stake; exactly two seats.
* **Jackpot** — accepts repeats; same address-derived `positionId`; minimum stake; time window.

`IPvpLobbyLedgerV2.getContribution(lobbyId, contributionId)` therefore supports ordered index walks
and binary search over cumulative pots (see `JackpotGame`).

***

## Settlement invariants (enforced by the protocol)

When a step returns `nextPhase == RESOLVED`, the protocol validates settlement before moving funds.
A violation reverts and the lobby stays unresolved.

### Immediate settlement (derived: exactly one recipient)

* The game returns `recipients` with a single non-zero address, which receives the whole
  distributable pot (`distributable = pot - fee`).
* The payout transfer is attempted without reverting: if it fails (e.g. a blacklisted recipient),
  the payout is parked on the lobby and the winner collects it later via the permissionless
  `claimPayout(lobbyId)`.

Reference pattern (Jackpot): draw a stake-weighted ticket, return the drawn participant as the only
recipient.

### Claimable settlement (derived: zero or several recipients)

* No payout loop at resolution; the returned recipients (if enumerated) only select the mode and
  must contain no zero addresses.
* Anyone may submit a game-defined `claimId`; the protocol calls `getClaim`, records the ID, and
  transfers to the returned recipient.
* The caller **cannot** redirect payment. Multiple claims may share a recipient.
* Total paid across all claims is capped at the remaining distributable pot.

### Cancellation

`CANCELLED` is **not** a game result. It always means **full refunds** of each participant's
aggregate stake, claimed independently via the host API / protocol refund path — never a penalty or
forfeiture. Who may cancel is entirely `canCancel`.

***

## Protocol orchestration notes

* **Whitelist**: game and stake token must be governance-whitelisted so pot accounting stays exact
  against standard ERC20s.
* **No stake on open**: opening records config and optional lobby key only. Value moves on
  **entry**.
* **Turns are not protocol-enforced.** Submit forwards the actor to `onPlayerAction` and lets the
  game decide validity. Intentional — see "Turn order & skipping".
* **Randomness from any step**: `onLobbyStart`, `onPlayerAction`, or `onRandomness` may return
  `requestRandomnessNow = true` with `nextPhase = WAITING_RANDOMNESS`.
* **Phases**: invalid transitions revert. You cannot resolve from a terminal phase or request
  randomness inconsistently.
* **View safety**: hooks must be deterministic and view-safe for a given context (the protocol may
  simulate).
* **Reentrancy**: value-moving entrypoints are non-reentrant and finalize pot/phase before
  transfers, layered on the token whitelist.

Exact selector names and error codes live on the deployed facet ABI; treat verified bytecode as
authoritative if this document drifts.

***

## Turn order & skipping (game-enforced)

Because the protocol does not enforce turns, **the game owns all turn logic** in `onPlayerAction`:

* A game with turns tracks its current actor (and any sub-turn structure) in `gameState`, and a normal
  move must `require(actor == currentActor)` itself.
* This is what lets an action trigger randomness and then route the **same** player to act again
  (`decide → WAITING_RANDOMNESS → onRandomness keeps currentActor → that player decides again`) — a
  shape a fixed protocol-level turn-guard could not express.

**Skipping a slacking player** falls straight out of this and needs **no protocol support**:

1. When the game hands a turn to a player, it writes a deadline into `gameState`
   (`deadline = block.timestamp + timeBank`).
2. It defines a `SKIP` action whose handler checks `block.timestamp > deadline` (and ignores the
   caller), then forfeits the no-show — they simply receive a **0 amount** at resolution (or are
   dropped from the active set) — and advances the turn.
3. Since the protocol forwards **any** caller to `onPlayerAction`, **anyone** (another player, or a
   keeper bot) can send the `SKIP` tx once the deadline passes; the laggard cannot stall the game.

```solidity
// inside onPlayerAction
(uint8 kind) = abi.decode(actionData, (uint8));
if (kind == SKIP) {
  require(block.timestamp > s.deadline, "not expired"); // caller is irrelevant
  // forfeit s.currentActor (0 amount), advance turn, set s.deadline = block.timestamp + timeBank
} else {
  require(actor == s.currentActor, "not your turn");
  require(block.timestamp <= s.deadline, "timed out"); // optional hard cutoff
  // apply move, advance turn, reset deadline (and/or requestRandomnessNow = true)
}
```

The guest decodes the deadline from `raw.gameState` (its own state) to render a countdown and a
"Skip" button. A game that does **not** want permissionless callers may additionally require
membership for non-skip actions — but the skip path should stay open to keep the game live.

Skips alone make an abandoned match slow to finish: every missed move costs the present player a
transaction. Pair them with a forfeit, preferably through the liveness standard below.

### Liveness standard (optional)

[`../simulator/contracts/PvpLiveness.sol`](../simulator/contracts/PvpLiveness.sol) standardizes what a lobby is blocked on
and how anyone unblocks it, so a host, keeper or guest can drive skip / forfeit / resign / advance on
any game without knowing its action encoding. It is not a protocol rule: games may skip it or handle
timeouts their own way, and the protocol never calls it.

**Detect**: `supportsInterface(0xae5c86f9)` (ERC-165 id of `IPvpLiveness`) returns `true`. Treat a
revert or `false` as unsupported.

**Read**: `liveness(uint8 phase, bytes config, bytes gameState)` takes the values every lobby read
model carries and returns `supported` (a capability bitmask) plus the pending `waits`. Each wait is
one thing the lobby is blocked on:

| Field       | Meaning                                                                            |
| ----------- | ---------------------------------------------------------------------------------- |
| `kind`      | `PLAYER_ACTION`, `PLAYER_REVEAL`, `TIMED_ADVANCE` or `CHALLENGE_WINDOW`            |
| `actor`     | The player who owes the step; zero for a wait nobody personally owes               |
| `subject`   | Game-defined handle (round id, claim id, …); zero when the game does not need one  |
| `dueAt`     | `SKIP` (or `ADVANCE`, when `actor` is zero) is accepted once past it; 0 = no path  |
| `forfeitAt` | `FORFEIT` of `actor` is accepted once past it; 0 = this wait cannot cost the match |

Turn-based games publish one wait at a time. Simultaneous games publish one per player who still
owes the step, which is what makes "everyone is late" distinguishable from "one player is late".

**Act**: submit `abi.encodePacked(tag, bytes32 subject)` as `actionData` through
`submitLobbyAction`. A zero subject means "whichever wait qualifies".

| Action    | Tag          | Subject        | Who                                        | Effect                                                                    |
| --------- | ------------ | -------------- | ------------------------------------------ | ------------------------------------------------------------------------- |
| `SKIP`    | `0x021033a8` | wait `subject` | Anyone, after `dueAt`                      | The game plays or passes the missing input; the match continues           |
| `FORFEIT` | `0xe04af859` | target player  | Anyone, after `forfeitAt`                  | That `actor` loses; in a two-seat game the other seat takes the pot       |
| `RESIGN`  | `0x1fbe2408` | unused         | A player still in contention               | The caller loses as if forfeited                                          |
| `ADVANCE` | `0x46c0809d` | wait `subject` | Anyone, after an actor-less wait's `dueAt` | The game closes the round, settles the challenge, or ends a stalled match |

Each tag is `bytes4(keccak256('PvpLiveness.<ACTION>'))`. The payload is 36 bytes, never a whole
number of ABI words, so it cannot collide with a game's own actions; games using packed encodings
must avoid 36-byte payloads. The facet only accepts actions in `IN_PROGRESS` /
`WAITING_PLAYER_ACTION`, so none of them work while randomness is pending.

**Implement**: inherit `PvpLiveness`, implement `_livenessCapabilities()` and
`_pendingWaits(config, gameState)`, and at the top of `onPlayerAction`:

```solidity
(LivenessAction action, bytes32 subject) = _livenessAction(actionData);
if (action != LivenessAction.NONE) {
  // waits built from the state you already decoded; reverts unless the action is due
  PvpWait memory wait = _requireLivenessAction(action, subject, _waitsOf(s));
  if (action == LivenessAction.SKIP) return _skip(s);
  if (action == LivenessAction.FORFEIT) return _concede(s, _seatOf(s, wait.actor));
  if (action == LivenessAction.RESIGN) return _concede(s, _seatOf(s, actor));
  return _advance(s); // actor-less wait: close the round, or end a stalled match
}
```

TypeScript hosts and guests get the tags, the ABI, `encodeLivenessAction` and
`getDueWaits(status, nowSeconds)` from `@chain/pvp-sdk/liveness`.

**Reference games**: `BonesGame` (turn-based: one `PLAYER_ACTION` wait, skip then forfeit) and
`RockPaperScissorsGame` (simultaneous: a wait per player who owes a commit or a reveal, no skip
path, and an actor-less `TIMED_ADVANCE` while both seats are still silent so an empty window ends as
a no-contest instead of punishing whoever is named first).

***

## Cancellation and refunds

Unlike v1, the protocol does **not** define default fill / action / randomness timeout constants that
auto-cancel lobbies. Recovery is game policy via `canCancel`:

| Concern                  | v2 approach                                                                                    |
| ------------------------ | ---------------------------------------------------------------------------------------------- |
| Lobby never starts       | Game may allow the administrator, opener, or anyone to cancel while waiting                    |
| Mid-game abandon / stall | Liveness skip, then forfeit against the idle player; optional `canCancel` for full unwind      |
| Stuck randomness         | Protocol/provider operational concern; games should not strand funds on unfulfillable requests |
| Who gets money on cancel | Always full refund of each participant's aggregate stake — never a partial penalty             |

There is no protocol `leaveLobby` seat model. Leaving before start is either a game that allows
cancel/refund patterns or simply not entering again for single-entry games.

***

## Reference implementations

SDK reference games:

| Contract                | Path                                                                                                                     | Demonstrates                                                                                                               |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `PvpLiveness`           | [`../simulator/contracts/PvpLiveness.sol`](../simulator/contracts/PvpLiveness.sol)                                       | Optional base for the liveness standard: detection, wait view, reserved skip / forfeit / resign / advance payloads         |
| `BonesGame`             | [`../simulator/contracts/examples/BonesGame.sol`](../simulator/contracts/examples/BonesGame.sol)                         | Turn-based: fixed stake, two seats, per-roll VRF, one liveness wait (skip then forfeit), immediate winner / claimable draw |
| `RockPaperScissorsGame` | [`../simulator/contracts/examples/RockPaperScissorsGame.sol`](../simulator/contracts/examples/RockPaperScissorsGame.sol) | Simultaneous: commit–reveal windows, a liveness wait per player who owes a step, no-contest when both miss one             |
| `JackpotGame`           | [`../simulator/contracts/examples/JackpotGame.sol`](../simulator/contracts/examples/JackpotGame.sol)                     | Variable/repeat stakes, lobby keys, weighted ticket via ledger binary search                                               |

Full protocol facets, when deployed in this monorepo, live under `packages/contracts`. Treat the
deployed facet's verified ABI as authoritative if this doc drifts.

***

## Related docs

* [`CHAIN_WTF_PVP_GAMES.md`](./CHAIN_WTF_PVP_GAMES.md) — bridge, snapshot, manifest, frontend patterns
* [`RANDOMNESS_DICE.md`](./RANDOMNESS_DICE.md) — rejection sampling for d6
* [`VISUAL_AND_UX.md`](./VISUAL_AND_UX.md) — iframe UX expectations
