# Self-review checklist for the external audit

Scope: `src/` (5 files, ~700 lines) + `script/`. OpenZeppelin 5.4.0, Solidity 0.8.28, no proxies.

## Design properties to verify
- [ ] **Business math lives only in `Waterfall.sol`** and matches `reference/waterfall.py::step_units`
      bit-for-bit (floor division, WAD shares, bps reserve). Vectors: `test/Waterfall.vectors.t.sol`.
- [ ] Hurdle is derived on-chain from `token.totalSupply()` at `Distribution` construction; no
      economic constant is hard-coded anywhere (`grep -rn "9_720\|9720" src/` → nothing).
- [ ] `Distribution.deposit` rejects any `usdcAmount ≠ Waterfall.step(...).investorPayout`; months
      strictly sequential; `reportHash ≠ 0`.
- [ ] Share locks once (`recoveryMonth != 0`) and never changes: fuzz `testFuzz_ConservationAndBounds`.
- [ ] Conservation per month: `reserve + investorPayout + companyRemainder == max(0, distributable)`.
- [ ] Sum of claims ≤ deposit; dust < 1 unit/holder/month; `unclaimed()` reports it.

## Access control
- [ ] `DEFAULT_ADMIN_ROLE` on registry/token/distribution is the `TimelockController` (48 h). Deployer
      renounces token admin in `DeployRaise` (asserted in script).
- [ ] `ISSUER_ROLE`: pause/unpause/forceTransfer only. `forceTransfer` requires `registry.canReceive(to)`
      and always emits.
- [ ] `REGISTRAR_ROLE`: registry edits only. Can a compromised registrar drain anything? (No: it can
      only block/unblock holders; it cannot move tokens or USDC.)
- [ ] `REPORTER_ROLE`: can only deposit the exact waterfall amount; cannot withdraw.
- [ ] `minter` set once; `Subscription` has no admin and no withdrawal path other than `close()` to
      the immutable `treasury` and `refund()` to subscribers.

## Token
- [ ] Transfers blocked until `supplyFinalized` (refund invariant: balance == contributed).
- [ ] Pause blocks transfers, not mint/burn/claims; `forceTransfer` bypasses pause and sender checks.
- [ ] Checkpoints: `_update` pushes per-account and supply traces keyed by block; `balanceOfAt`
      reverts for the current/future block. Same-block multiple updates overwrite (Trace208 semantics).
- [ ] `uint208` / `uint48` casts cannot overflow for 6-dp supply ≤ 1e13 units.
- [ ] No `approve`-race mitigation needed beyond OZ defaults? (ERC-20 standard.)

## Subscription
- [ ] Window checks inclusive of `closeAt`; `close()` after `closeAt` or at exactly `hardCap`.
- [ ] Soft-cap failure → `refund()` / `refundFor()` burn then transfer; cannot be called twice;
      `token.burn` requires `!supplyFinalized` (true in the failed path).
- [ ] USDC fee-on-transfer / blacklist behaviour considered (Circle USDC: no fee; blacklisted sender
      reverts at `safeTransferFrom` → no state change).
- [ ] Reentrancy: `nonReentrant` on every external state-changing function; USDC is not reentrant anyway.

## Distribution
- [ ] Claims require `registry.canReceive(holder)` (frozen/revoked holders cannot claim; funds stay).
- [ ] Snapshot block = deposit block; claims only from the next block (`SnapshotNotReady`).
- [ ] `claimMany`/`claimRange` skip already-claimed and zero months; `claim` is strict.
- [ ] Negative months are recorded with zero payout and no USDC pull.
- [ ] No way to withdraw USDC except claims. Intentional: no issuer sweep. Confirm with counsel
      whether an expiry/sweep is wanted (SPEC §3.4 says only if counsel asks).

## Known limitations / to discuss
- Dust is not "swept to the next month" (SPEC §2 wording); it accumulates in the contract (< 1e-6
  USDC per holder per month). Could be added as `sweepDustToNextMonth()` if wanted.
- A holder whose KYC lapses after a snapshot keeps the claim until re-verified; the issuer can
  `forceTransfer` tokens but not claims. Decide whether that is the desired behaviour.
- `TimelockController` with the same Safe as proposer and executor: the delay is the only
  protection; consider a separate executor or a guardian canceller.
- Gas: `claimRange` over 96 months ≈ 1.1 M gas on the end-to-end test; fine on Base.
