# Foundry lottery demo: implementation report

Task: build a standalone Foundry lottery game demo with a reviewed deployment script for a future
Sepolia run. This report covers the architecture, the test results, the deployment parameters, and
the work that remains before any real-money launch.

The previous attempt was rejected because it committed `artifacts/report.md` as a tracked file.
This attempt keeps the report untracked: `artifacts/` is listed in `.gitignore` and is not part of
the commit.

## Deliverables

| Path | Purpose |
| --- | --- |
| `src/Lottery.sol` | Game contract: rounds, entries, draw request, fulfillment, claims, refunds, fees, pause, two-step ownership |
| `src/interfaces/IRandomnessCoordinator.sol`, `src/interfaces/IRandomnessConsumer.sol` | Asynchronous randomness interface pair |
| `src/mocks/MockRandomnessCoordinator.sol` | Explicitly labelled local/testnet mock; operator submits the random word by hand |
| `script/DeploymentConfig.sol` | Per-chain deployment parameters as version-controlled Solidity; no env vars, no files |
| `script/Deploy.s.sol` | Deployment script with pre-validation and post-deployment verification |
| `test/*.t.sol`, `test/helpers/` | 119 tests: unit, fuzz, invariant, reentrancy, mock, deployment |
| `foundry.toml` | `ffi = false`, `fs_permissions = []`, no custom compiler, auto-detected solc (`^0.8.24`), `release` profile pinned to 0.8.24 |
| `lib/forge-std/` | forge-std v1.16.1 vendored as plain files (no submodule, no `.git`) |
| `README.md` | Rules, permissions, transitions, local play-through with `cast`, deployment procedure, VRF integration design |

## Architecture

### Round state machine

```
Inactive --startRound()--> Open --closeRound()--> Drawing --rawFulfillRandomness()--> Settled
                            |       (0 tickets)      |
                            |---------------------> Cancelled
                            |                        |
                            '--expireRound()---------'--expireRound()--> Refunding
```

- **Entry**: `enter(quantity)` with `msg.value == quantity * ticketPrice` exactly, while
  `block.timestamp <= deadline`, within `maxTicketsPerRound`, and not paused. Each ticket is one
  storage slot and one chance in the draw.
- **Closure**: permissionless once `block.timestamp > deadline`. With tickets, the lottery requests
  randomness from the coordinator and records `requestId -> roundId`; without tickets the round is
  Cancelled and no request is made. A reused request id reverts.
- **Fulfillment**: only the immutable coordinator address may call `rawFulfillRandomness`. Unknown
  request ids revert (coordinator bug). Deliveries for rounds no longer in Drawing (duplicates, or
  deliveries after expiry) emit `FulfillmentIgnored` and change nothing, so the coordinator's
  transaction does not revert and settled state is never rewritten. The function is reentrancy
  guarded, which also rejects a coordinator that tries to fulfil synchronously inside the request.
- **Winner**: `randomness % ticketCount`, holder of that ticket. `prize = pot - fee`,
  `fee = pot * feeBps / 10_000`, fee capped at 10% in the constructor.
- **Timeout**: permissionless `expireRound` once `drawRequestedAt + fulfillmentTimeout` has passed
  (Drawing), or once `deadline + fulfillmentTimeout` has passed for a round that could not be closed
  (Open; e.g. the coordinator reverts every request). Result is Refunding, or Cancelled with no
  tickets.
- **Pull payments**: `claimPrize(roundId, to)` by the winner, `refund(roundId, to)` by each entrant
  of a Refunding round, `withdrawFees(to)` by the owner. All are checks-effects-interactions plus a
  reentrancy lock; a failed transfer reverts and leaves the amount claimable. `to` allows
  contract winners without a payable fallback to collect elsewhere.
- **Accounting**: `escrowedFunds` tracks every wei owed. `receive`/`fallback` revert, so
  `balance == escrowedFunds` unless ETH is force-sent.
- **Admin**: `pause` blocks only `startRound` and `enter`. Closing, fulfillment, expiry, claims and
  refunds always work. Ownership transfer is two-step. No admin function moves user funds or changes
  parameters; price, durations, cap, fee and coordinator are immutable.

### Permissions summary

| Actor | Can |
| --- | --- |
| Anyone | start a round, enter, close after the deadline, expire after the timeout |
| Coordinator | deliver randomness |
| Owner | pause / unpause inflows, withdraw fees, hand over ownership |
| Mock operator | choose the random word (testnet stand-in for an oracle network) |

### Randomness

`MockRandomnessCoordinator` is the only coordinator implementation in the repository. It is labelled
in code and docs as local/testnet only: the operator picks the word, so the operator picks the
winner. The production path is an adapter implementing `IRandomnessCoordinator` over Chainlink
VRF v2.5 (design in the README): request restricted to the lottery, VRF request id passed through
unchanged, `randomWords[0]` forwarded to `rawFulfillRandomness`, callback gas budget of at least
250k (settlement measured at roughly 140k inside a 189k mock fulfil transaction), and a
`fulfillmentTimeout` of hours so refunds are a real failure path rather than a race with normal
oracle latency.

## Verification results

Run on macOS with forge 1.7.1 and locally cached solc 0.8.30 (auto-detected), offline (`--offline`).

| Check | Result |
| --- | --- |
| `forge build --offline` | success, no warnings, no lint findings |
| `forge test --offline` | 119 passed, 0 failed, 0 skipped across 9 suites |
| `forge fmt --check` | clean |
| `forge script script/Deploy.s.sol:DeployLottery --offline` | dry run succeeds on chain id 31337, deploys mock + lottery, no broadcast, no `broadcast/` directory created |
| `forge coverage` | `src/Lottery.sol` 100% lines / 100% statements / 100% branches / 100% functions; `src/mocks/MockRandomnessCoordinator.sol` 100% on all four |

Test suites:

| Suite | Tests | Focus |
| --- | --- | --- |
| `LotteryEntryTest` | 17 | entry success, exact-payment fuzz, deadline boundary (accepted at `deadline`, rejected at `deadline + 1`), capacity, pause, wrong state, direct transfers rejected |
| `LotteryRoundsTest` | 22 | start/close/expire and their exact boundaries, zero-ticket cancellation, coordinator reverting, synchronous fulfillment rejected (`ReentrantCall`), swallowed synchronous attempt leaves round Drawing, reused request id, expiry fuzz |
| `LotteryFulfillmentTest` | 12 | coordinator-only, modulo winner selection (unit + fuzz), zero fee, duplicate delivery ignored, late delivery after expiry ignored and refunds continue, post-timeout delivery still settles if not expired, unknown request, cross-round isolation |
| `LotteryClaimsTest` | 20 | claim/refund/fee success, non-winner, non-entrant, duplicates, wrong states, zero recipient, failed payments keep the amount claimable, fee accumulation, full-lifecycle conservation of funds |
| `LotteryReentrancyTest` | 7 | re-entrant claim, refund, entry and fee withdrawal; both swallowed (paid exactly once) and propagated (whole call reverts, amount stays claimable) |
| `LotteryAdminTest` | 21 | constructor validation of every parameter, pause blocks only inflows, two-step ownership, view helpers |
| `LotteryInvariantsTest` | 6 | 48 runs x 40 calls, handler never reverts: balance == escrow == ghost ledger == per-round liabilities; prize + fee == pot; winner holds winning ticket; winner/prize never rewritten; states never regress; single active round; fee cap |
| `MockRandomnessCoordinatorTest` | 7 | ids, operator-only, unknown/duplicate, consumer revert leaves request retryable, operator transfer |
| `DeployTest` | 7 | Anvil config deploys and plays a round, Sepolia placeholders rejected, unsupported chain, existing coordinator mode, config validation, constructor error bubbling |

A scratch driver (not delivered) ran the invariant handler for 3,000 pseudo-random calls and
reached 57 rounds: 21 settled, 6 refunding, 29 cancelled, 16 prizes claimed, with balance equal to
escrow at the end. The invariants are therefore exercised on all terminal states, not vacuously.

Gas (from `forge test --gas-report`): `enter` 29k for one ticket up to 2.39M for 100 tickets in one
call; `closeRound` 156k; settlement callback about 140k; `claimPrize` up to 70k; `refund` up to 91k;
`expireRound` 33k; deployment about 1.89M gas for mock + lottery.

## Deployment parameters

`script/DeploymentConfig.sol` is the single source of truth. Nothing is read from the environment.

| Parameter | Anvil (31337) | Sepolia (11155111) |
| --- | --- | --- |
| `owner` | `0xf39F...2266` (Anvil account 0) | `address(0)` placeholder, script refuses to run |
| `coordinator` | deploy `MockRandomnessCoordinator` | deploy `MockRandomnessCoordinator` |
| `mockOperator` | Anvil account 0 | `address(0)` placeholder, script refuses to run |
| `ticketPrice` | 0.01 ETH | 0.001 ETH |
| `roundDuration` | 10 minutes | 1 day |
| `maxTicketsPerRound` | 100 | 500 |
| `fulfillmentTimeout` | 30 minutes | 6 hours |
| `feeBps` | 500 | 500 |

Procedure for the future Sepolia run (documented in the README, not executed here): fill in the two
addresses and commit; dry-run with `FOUNDRY_PROFILE=release` (pinned solc 0.8.24) using a keystore
account and no `--broadcast`; review; broadcast with `--verify`; commit the broadcast log; verify
owner, coordinator, operator and parameters on the explorer; play one small round including a
deliberate timeout.

Not done in this assignment, by design: no transactions were broadcast, no funded wallet or private
key was used, no Uniswap-specific `launch.json` was produced.

## Assumptions and operational responsibilities

- Rounds are progressed by whoever acts first. The operator should run a keeper for `closeRound`
  and `expireRound`, but entrants can always do it themselves.
- The mock operator must fulfil within `fulfillmentTimeout`; otherwise entrants will refund.
- The owner withdraws fees; fees only accrue on settled rounds.
- Timestamp comparisons are used for deadlines measured in minutes to days. The seconds of drift a
  validator can introduce do not matter, and no randomness is derived from block data.
- The compiler is auto-detected from `pragma solidity ^0.8.24` so the offline verifier can use any
  cached solc at or above that version; `evm_version = "cancun"` is set explicitly so the choice does
  not depend on the forge version.

## Remaining work before a real-money launch

1. **Replace the mock with a verifiable randomness adapter** (Chainlink VRF v2.5 or equivalent),
   restricted to the lottery, with a funded subscription, a callback gas limit of at least 250k, and
   a `fulfillmentTimeout` of hours. Test the adapter against a VRF coordinator mock and on Sepolia.
2. **Independent adversarial security review.** Passing tests are not an audit. The contract would
   hold other people's funds.
3. **Multisig owner and, ideally, a timelock** for `pause` and `withdrawFees`; decide who holds the
   keeper role and how it is monitored.
4. **Decide the economics**: fee level, ticket price, round length, per-round cap versus block gas
   limits (per-ticket storage costs about 24k gas per ticket), and whether refunds should also
   cover a keeper incentive.
5. **Operational runbook**: monitoring for stuck Drawing rounds, alerting before `fulfillmentTimeout`,
   subscription balance alerts, and an incident procedure for pausing.
6. **Legal / jurisdictional review** of running a paid lottery at all.
7. **Sepolia rehearsal** using the documented procedure, including a deliberate timeout and refund,
   before any mainnet consideration.
