# DrawManager

`contracts/src/DrawManager.sol` — the multi-tenant draw engine. One deployment
serves every registered token: it escrows pots, runs commit-reveal draws with
**tiered winners**, verifies per-slot winning claims by merkle proof, pays
prizes in the winner's **choice of allowlisted stock token** (or raw ETH), and
skims the protocol rake.

Inherits OpenZeppelin `Ownable` and `ReentrancyGuard`; implements `IEntropyConsumer`
(the DiceEntropy oracle callback surface).

## Tiers & prize slots

Each round pays a schedule of tiers; the default is **50% to 1 grand winner,
30% split across 5, 20% split across 20** — 26 prize slots. Slots enumerate
tiers in order (slot 0 = the grand winner). Key properties:

* **Independent draws.** Slot `i`'s winning ticket is derived from
  `keccak256(abi.encode(seed, oracleRandom, i))`, mapped uniformly onto
  `[0, totalWeight)` by rejection sampling (no modulo bias). The same address
  may win multiple slots — deliberately not deduplicated: more weight means
  proportionally more expected prizes, not a capped one-win.
* **Frozen at commit.** The owner sets per-token schedules with `setTiers`
  (shares must sum to exactly 10 000 bps, ≤ 8 tiers, ≤ 64 total winners);
  a committed round carries its own frozen copy, so a schedule change never
  affects an open round.
* **Exact distribution.** Tier shares and per-slot splits truncate; every wei
  of truncation dust is added to slot 0, so slot prizes always sum exactly to
  the pot.
* **Tickets are not stored.** Reveal stores the seed; any slot's ticket is
  re-derived on demand (`slotTicket`) — fixing the seed fixes all 26 at once.

## Constants

| Name | Value | Meaning |
|---|---|---|
| `REVEAL_WINDOW_BLOCKS` | `200_000` | Blocks after commit in which the keeper must reveal (≈ a day on sub-second blocks); afterwards the round is voidable by anyone |
| `CLAIM_WINDOW_BLOCKS` | `6_200_000` | Blocks after commit before `sweepUnclaimed` may roll unclaimed slots into the next pot (≥ ~30 days after the latest possible reveal) |
| `MAX_RAKE_BPS` | `2_000` | Hard cap on any token's rake (20%) |
| `MAX_TIERS` | `8` | Tiers per schedule; 8 packed tiers fill one storage word |
| `MAX_WINNERS` | `64` | Total winners per round; keeps the claim bitmap in one `uint64` |
| `DEFAULT_TIERS_PACKED` | — | The 50/30/20 × 1/5/20 default schedule, packed |

## Storage

| Variable | Type | Meaning |
|---|---|---|
| `treasury` | `address` | Receives the rake at commit |
| `prizeBuyer` | `PrizeBuyer` | Swapper used for stock-token prizes |
| `entropy` | `IEntropy` | External commit-reveal randomness oracle (DiceEntropy); unset disables the second entropy source |
| `entropyProvider` | `address` | Oracle provider randomness is requested from |
| `entropyGasLimit` | `uint32` | Callback gas budget bought per request (default `150_000`) |
| `entropyBalance` | `uint256` | ETH reserved for oracle fees — tracked separately so a draw can never be funded out of a pot, or vice versa |
| `registrars` | `mapping(address => bool)` | Addresses allowed to register tokens |
| `allowedPrizeTokens` | `mapping(address => bool)` | Stock tokens a winner may choose at claim time; `address(0)` (raw ETH) is always allowed and never listed |
| `tokenConfig` | `mapping(address => TokenConfig)` | Per-token settings |
| `tokenTiersPacked` | `mapping(address => uint256)` | Per-token tier schedule override; zero = default. Read only at commit |
| `pendingPot` | `mapping(address => uint256)` | ETH accrued for a token, not yet committed to a round |
| `currentRoundId` | `mapping(address => uint256)` | Latest round id per token (starts at 1 on first commit) |
| `rounds` | internal mapping | Round records — read via `getRound` (the flattened auto-getter of the 14-field struct exceeds the stack budget) |
| `entropyRefs` | `mapping(uint64 => EntropyRef)` | Reverse index from an oracle request id to its `(token, roundId)` |

### `TokenConfig`

```solidity
struct TokenConfig {
    address vault;  // revenue forwarder allowed to fund this token's pot
    address keeper; // operator allowed to commit/reveal
    uint16 rakeBps; // skimmed to treasury at commit
    bool registered;
}
```

### `Round`

```solidity
struct Round {
    RoundStatus status;     // None | Committed | Revealed | Settled | Voided
    uint64 commitBlock;     // starts the reveal & claim windows
    uint64 entropySequence; // oracle request id; 0 = no oracle at commit
    uint16 winnerCount;     // total prize slots across all tiers, frozen at commit
    uint16 claimedCount;    // slots claimed so far
    uint64 claimedBitmap;   // bit i set = slot i claimed
    bytes32 weightsRoot;    // merkle root over depositor-weight leaves
    bytes32 seedCommitment; // keccak256(abi.encodePacked(seed))
    bytes32 seed;           // keeper seed; 0 until reveal
    bytes32 oracleRandom;   // delivered by the oracle callback; 0 until then
    uint256 tiersPacked;    // tier schedule frozen at commit
    uint256 totalWeight;    // ticket-space size
    uint256 pot;            // ETH escrowed for this round (post-rake)
    uint256 potRemaining;   // ETH still escrowed (decreases per claim/sweep)
}
```

## Functions

### Registry (owner / registrar)

| Function | Access | Notes |
|---|---|---|
| `setRegistrar(address, bool)` | owner | Grants/revokes token-registration rights |
| `setTreasury(address)` | owner | |
| `setPrizeBuyer(PrizeBuyer)` | owner | Required for stock payouts (`PrizeBuyerNotSet` otherwise) |
| `setEntropy(entropy, provider, gasLimit)` | owner | Rounds bind their source at commit, so changing it never weakens an open round |
| `setPrizeTokenAllowed(stockToken, bool)` | owner | Manages the claim-time stock allowlist; `address(0)` cannot be listed |
| `setTiers(token, Tier[])` | owner | Schedule for **future** rounds; validated (sum = 10 000 bps, counts ≥ 1, ≤ 8 tiers, ≤ 64 winners) |
| `registerToken(token, vault, keeper, rakeBps)` | owner or registrar | One-shot per token (`AlreadyRegistered`); rake ≤ 20% |
| `updateTokenConfig(token, keeper, rakeBps)` | owner | Vault address is immutable post-registration |

### Oracle fee budget

| Function | Access | Notes |
|---|---|---|
| `fundEntropy()` | anyone, payable | Tops up the ETH used to pay oracle request fees |
| `withdrawEntropy(to, amount)` | owner | Recovers unspent fee budget; cannot exceed `entropyBalance` |
| `receive()` | — | Bare transfers (e.g. oracle fee refunds) are credited to `entropyBalance` |

### Funding

```solidity
function fundPot(address token) external payable
```

Open to anyone. Called by the keeper's revenue conversions and the
PonsFeeCollector; also how sponsors top up a pot. Reverts for unregistered
tokens. Emits `PotFunded`.

### Draw flow (keeper unless noted)

```solidity
function commitDraw(address token, bytes32 weightsRoot, uint256 totalWeight, bytes32 seedCommitment)
    external nonReentrant returns (uint256 roundId)
```

Opens a round: locks `pendingPot` (must be > 0), skims `rakeBps` to the
treasury, records the weights root, total weight, and seed commitment,
**freezes the token's tier schedule into the round**, and — if an oracle is
configured — buys the independent half of the entropy in the same transaction
(fee paid from `entropyBalance`; an underfunded budget reverts with
`EntropyUnderfunded`). Emits `DrawCommitted` and, when an oracle is set,
`EntropyRequested`.

```solidity
function revealDraw(address token, uint256 roundId, bytes32 seed) external
```

Within the reveal window only. Verifies the seed against the commitment; if
the round bought oracle entropy, the oracle must have delivered
(`EntropyPending` otherwise). Stores the seed — fixing every slot's ticket at
once. Emits `DrawRevealed`.

```solidity
function claimPrize(Claim calldata c) external nonReentrant

struct Claim {
    address token;
    uint256 roundId;
    uint256 slot;         // prize slot in [0, winnerCount); slot 0 = grand
    address account;      // the winning leaf's account
    uint256 cumStart;     // the leaf's ticket-range start
    uint256 weight;       // the leaf's weight (range length)
    bytes32[] proof;      // merkle proof of the leaf
    address stockToken;   // payout choice: address(0) = ETH, else allowlisted stock
    uint24 poolFee;       // PrizeBuyer swap fee tier (ignored for ETH)
    uint256 minAmountOut; // PrizeBuyer slippage floor (ignored for ETH)
}
```

**Anyone** may settle a slot with its winning leaf (keeper settles on winners'
behalf; a winner can always self-claim and pick their own stock). Verifies the
merkle proof, re-derives the slot's ticket and checks
`cumStart <= ticket < cumStart + weight`, checks the claim bitmap
(`SlotAlreadyClaimed`), validates the stock choice against the allowlist, then
pays that slot's prize — raw ETH or
`prizeBuyer.buyFor{value: prize}(stockToken, account, poolFee, minAmountOut)`.
The round flips to `Settled` when the last slot is claimed. Emits
`PrizeClaimed`.

```solidity
function sweepUnclaimed(address token, uint256 roundId) external
```

Owner or the token's keeper, only after `CLAIM_WINDOW_BLOCKS` lapses on a
`Revealed` round. Rolls the unclaimed remainder (`potRemaining`) into
`pendingPot` for the next round and settles the round. Emits `UnclaimedSwept`.

```solidity
function voidRound(address token, uint256 roundId) external nonReentrant
```

**Anyone**, only after the reveal window lapses on a `Committed` round. Rolls
the full pot — every tier's slots, none claimable pre-reveal — back into
`pendingPot`, and makes a best-effort `refundRequest` to the oracle for an
undelivered random number (a rejected refund never blocks the void). Emits
`RoundVoided` and, on a successful refund, `EntropyRefunded`.

### Views

* `getRound(token, roundId) → Round`
* `getTiers(token) → Tier[]` — the schedule future rounds would commit with
* `getRoundTiers(token, roundId) → Tier[]` — the schedule frozen into a round
* `slotTicket(token, roundId, slot) → uint256` — a revealed round's winning ticket for a slot
* `slotPrize(token, roundId, slot) → uint256` — a slot's ETH prize (slot 0 includes dust)
* `isSlotClaimed(token, roundId, slot) → bool`
* public mappings: `tokenConfig`, `tokenTiersPacked`, `allowedPrizeTokens`, `pendingPot`, `currentRoundId`

## Events

```solidity
event RegistrarSet(address indexed registrar, bool allowed);
event TokenRegistered(address indexed token, address vault, address keeper, uint16 rakeBps);
event TokenConfigUpdated(address indexed token, address keeper, uint16 rakeBps);
event TiersSet(address indexed token, Tier[] tiers);
event PrizeTokenAllowed(address indexed stockToken, bool allowed);
event PotFunded(address indexed token, address indexed from, uint256 amount);
event DrawCommitted(address indexed token, uint256 indexed roundId, bytes32 weightsRoot, uint256 totalWeight, uint256 pot, uint256 rake, uint256 tiersPacked);
event DrawRevealed(address indexed token, uint256 indexed roundId, bytes32 seed, uint16 winnerCount);
event EntropySet(address indexed entropy, address indexed provider, uint32 gasLimit);
event EntropyFunded(address indexed from, uint256 amount);
event EntropyWithdrawn(address indexed to, uint256 amount);
event EntropyRequested(address indexed token, uint256 indexed roundId, uint64 indexed sequenceNumber, uint256 fee);
event EntropyDelivered(address indexed token, uint256 indexed roundId, uint64 indexed sequenceNumber);
event EntropyRefunded(address indexed token, uint256 indexed roundId, uint64 indexed sequenceNumber);
event PrizeClaimed(address indexed token, uint256 indexed roundId, address indexed winner, uint256 slot, uint256 prizeEth, address stockToken);
event UnclaimedSwept(address indexed token, uint256 indexed roundId, uint256 rolledOver, uint256 unclaimedSlots);
event RoundVoided(address indexed token, uint256 indexed roundId, uint256 rolledOver);
```

## Errors

`NotRegistrar` · `NotKeeper` · `NotAuthorized` · `TokenNotRegistered` ·
`AlreadyRegistered` · `RakeTooHigh` · `InvalidTiers` · `BadRoundStatus` ·
`EmptyPot` · `ZeroWeight` · `SeedMismatch` · `RevealWindowOpen` ·
`RevealWindowClosed` · `ClaimWindowOpen` · `BadSlot` · `SlotAlreadyClaimed` ·
`PrizeTokenNotAllowed(stockToken)` · `PrizeBuyerNotSet` · `NotWinningLeaf` ·
`InvalidProof` · `EthTransferFailed` · `NotEntropyOracle` ·
`EntropyUnderfunded(required, available)` · `EntropyPending`
