# Draw Lifecycle

Each registered token runs an independent sequence of rounds. A round moves through
a strict state machine enforced by [`DrawManager`](/contracts/draw-manager):

```mermaid
stateDiagram-v2
    [*] --> None
    None --> Committed: commitDraw (keeper) — freezes tiers, buys oracle entropy
    Committed --> Committed: _entropyCallback (oracle delivers)
    Committed --> Revealed: revealDraw (keeper, within window, oracle delivered)
    Committed --> Voided: voidRound (anyone, after window)
    Revealed --> Revealed: claimPrize per slot (anyone with a winning proof)
    Revealed --> Settled: last slot claimed
    Revealed --> Settled: sweepUnclaimed (owner/keeper, after claim window)
    Voided --> [*]: pot rolls into pendingPot
    Settled --> [*]
```

## 1. Accrue

All week, the keeper converts revenue streams to ETH and forwards them via
`fundPot(token)`, growing `pendingPot[token]`: the 80% yield skim from the
[PrizeVault](/contracts/prize-vault), $POT trading fees collected from the
Pons pool, and any treasury top-ups (the first six draws are floored at
$500). Anyone can also call `fundPot` directly — sponsoring a pot is
permissionless.

## 2. Commit — `commitDraw(token, weightsRoot, totalWeight, seedCommitment)`

Called by the token's **keeper**. In one transaction:

1. A new `roundId` is opened (`currentRoundId[token]` increments).
2. The full `pendingPot` is locked into the round; an empty pot reverts (`EmptyPot`).
3. **Rake is skimmed to the treasury**: `rake = gross * rakeBps / 10_000`
   (default 6%, hard-capped at 20%) — funds ops and the pot floor guarantee.
4. The **holder-weights merkle root** is published. Leaves are
   `keccak256(abi.encode(account, cumStart, weight))`, sorted by `cumStart` and tiling
   the interval `[0, totalWeight)` — every holder owns a contiguous ticket range whose
   size is their weight (see [Eligibility & Odds](/protocol/eligibility)).
5. A **seed commitment** is published: `keccak256(abi.encodePacked(seed))` for a secret
   seed the keeper drew in advance.
   5b. The token's **tier schedule is frozen into the round** (default: 50% × 1
   grand / 30% ÷ 5 / 20% ÷ 20 = 26 prize slots). Owner schedule changes only
   ever apply to rounds committed afterwards.
6. In the same transaction, the contract **buys the independent half of the entropy**
   from the DiceEntropy oracle (`_requestEntropy`): it pays the oracle fee from a
   dedicated `entropyBalance` and records the request's sequence number on the round.
   At this moment the keeper's seed is committed but secret — the oracle operator
   cannot see it, and the keeper cannot see the oracle's answer.

The commit block number is recorded; it starts the reveal window.

## 2b. Oracle delivery — `_entropyCallback(sequenceNumber, provider, randomNumber)`

The oracle reveals in a later transaction, invoking the callback on the DrawManager.
Only the configured entropy contract may call it; the random number is stored on the
round (`oracleRandom`) and `EntropyDelivered` is emitted. Late or unknown deliveries
(a voided round, a duplicate) are **accepted silently and ignored** so the oracle's
reveal transaction never reverts on Stockpot's account.

## 3. Reveal — `revealDraw(token, roundId, seed)`

The keeper must reveal within `REVEAL_WINDOW_BLOCKS = 200_000` blocks of the commit
(Robinhood Chain has sub-second blocks, so this is a time budget of roughly a day).
The contract checks `keccak256(abi.encodePacked(seed)) == seedCommitment` and — if the
round bought oracle entropy — that the oracle has delivered (`EntropyPending`
otherwise: revealing without it would hand the keeper sole control of the outcome).
Storing the seed fixes **every prize slot's ticket at once**; slot `i`'s ticket
is re-derived on demand rather than stored:

```solidity
ticket_i = _uniformTicket(uint256(keccak256(abi.encode(seed, oracleRandom, i))), round.totalWeight);
```

`_uniformTicket` maps the 256-bit entropy onto `[0, totalWeight)` by
**rejection sampling**: entropy values in the small biased band are re-hashed,
so every ticket is exactly equally likely — no modulo bias, for any weight.
Slots are drawn independently; the same address may win several.

The entropy combines contributions from **two mutually distrusting parties**:

* the **keeper's seed**, hash-committed before the oracle's answer existed;
* the **oracle's random number**, generated by an operator independent of the keeper
  who never saw the seed.

Neither party learns the other's contribution before committing to its own, so biasing
a draw requires both to collude. The reveal-block hash is deliberately **not** an
input: the keeper chooses the reveal transaction and could see the hash in advance, so
it would grant grinding power rather than remove it. See
[Security & Trust Model](/protocol/security).

## 4a. Claim — `claimPrize(Claim)` (per prize slot)

Each of the round's prize slots is claimed independently. Callable by
**anyone** holding the slot's winning leaf — in practice the keeper settles on
winners' behalf, but a winner can always self-claim and pick their own stock.
The `Claim` struct carries the slot index, the leaf `(account, cumStart,
weight)`, its merkle proof, and the payout choice. The contract verifies:

1. the merkle proof against `weightsRoot`;
2. that the slot's re-derived ticket falls inside the leaf's range:
   `cumStart <= ticket_slot < cumStart + weight`;
3. that the slot is unclaimed (per-round bitmap);
4. that the chosen `stockToken` is allowlisted (or `address(0)` for ETH).

Then the slot pays out:

* **ETH** (`stockToken == address(0)`): the slot's prize is sent as raw ETH.
* **Stock of choice**: the slot's prize is forwarded to
  [`PrizeBuyer`](/contracts/prize-buyer), which swaps into the chosen
  allowlisted stock token and delivers **directly to the winner**. `poolFee`
  selects the pool tier; `minAmountOut` is the slippage floor (the keeper
  quotes it off-chain; a self-claiming winner sets their own tolerance).

Slot prizes come from the frozen tier schedule; integer-division dust goes to
slot 0, so all slots together pay out the pot exactly. The round flips to
`Settled` when its last slot is claimed.

## 4b. Sweep — `sweepUnclaimed(token, roundId)`

If winners never show, the owner or the token's keeper may — only after
`CLAIM_WINDOW_BLOCKS = 6_200_000` blocks from commit (≥ ~30 days past the
latest possible reveal) — roll a revealed round's unclaimed remainder into
`pendingPot` for the next round and settle it. Claimed slots are untouched.

## 4c. Void — `voidRound(token, roundId)`

If the keeper fails to reveal inside the window, **anyone** can void the round.
The pot rolls back into `pendingPot[token]` and funds the next round — a stalling
keeper forfeits the round but can never strand the money. The same applies if the
oracle never delivers: the reveal stays blocked (`EntropyPending`), the window lapses,
and the round voids — withholding by either party only costs that party the draw.

Voiding also makes a best-effort `refundRequest` to the oracle for an undelivered
random number; a rejected refund never blocks the void.

## Rake math

At commit, with `rakeBps` from the token's config:

| | |
|---|---|
| Gross pot | `pendingPot[token]` at commit time |
| Rake to treasury | `gross * rakeBps / 10_000` |
| Round pot (escrowed) | `gross - rake` |
| Hard cap | `MAX_RAKE_BPS = 2_000` (20%) |
| Default | `DEFAULT_RAKE_BPS = 600` (6%) |

## Liveness properties

* A committed round can always terminate: either the keeper reveals in time, or the
  window lapses and anyone voids it.
* Voided and rolled-over funds are never stuck — they re-enter `pendingPot` and are
  committed into the next round.
* A revealed round's slot prizes stay escrowed for their winners for at least
  the claim window (~30 days past the latest possible reveal); per-slot
  settlement is permissionless. After the window, unclaimed funds roll into
  the next pot via `sweepUnclaimed` — money never strands.
