# Kitty contracts: system map (audit 2026-11-08) Commit `4c4f99124887c75435649249ed7a84a9ede9f848` on `main`. solc 0.8.26, via_ir, OpenZeppelin 5.1.0. ## Scope | File | nSLOC | Role | |---|---|---| | src/KittyRound.sol | 351 | One group buy: pledges, target, step buys, claims | | src/KittyFactory.sol | 74 | Deploys rounds, validates config, router/base allowlists | | src/TokenRegistry.sol | 24 | Curated token -> ticker list | | src/interfaces.sol | 23 | V2 factory/pair/router + registry interfaces | | src/dex/KittyDex.sol | 165 | Testnet V2-shaped AMM (light review) | | src/test-tokens/TestToken.sol | 24 | Testnet faucet token (light review) | ## Actors and trust | Actor | Powers | Trusted? | |---|---|---| | Factory owner (Ownable2Step, single key) | `setRouter`, `setBase` for new rounds | Trusted for new rounds only; promised no power over live rounds | | Registry owner (Ownable2Step, separate contract, same key on testnet) | `verify`, `revoke` tokens/tickers | Trusted curator; registry is read live by rounds (see REG-1) | | Round admin (creator, `msg.sender` of `createRound`) | `proposeTarget`, `executeStep(minOut)`, `finalize` early, `cancel` while Open | **Not trusted**: product promises admin can only buy the agreed target | | Members | `pledge`, `withdraw` (Open only), `veto`, `claim`, `claimRefund` | Untrusted, includes outsiders who pledge only to vote | | Anyone | `cancel` after expiry/veto/minRaise miss, `finalize` after `executeDeadline` | Untrusted | | Router + its `factory()` | Executes the swap, quotes price, reports reserves | Trusted once allowlisted | | Target token | `symbol()`, `decimals()`, `balanceOf`, transfers | Untrusted unless GUARD_ADDRESS/REGISTRY | ## Asset flows - In: `pledge` -> base `transferFrom(member -> round)` (balance-diff credited). - Out before first buy: `withdraw` -> base to member. - Spend: `executeStep` -> `forceApprove(router, amountIn)`, `swapExactTokensForTokens(amountIn, minOut, [base,target], round, now)`. - Out after end: `claim` -> target `bought*pledge/startTotal` + base `(startTotal-spent)*pledge/startTotal`; `claimRefund` base only. Cancelled: full pledge back. - No sweep or admin transfer path exists. Donations and rounding dust stay in the round. ## State ledgers | State | Writers | |---|---| | `pledged[m]`, `totalPledged`, `memberCount` | pledge, withdraw (Open only) | | `vetoWeight[n]`, `hasVetoed[n][m]` | veto, pledge, withdraw | | `target`, `targetSetAt`, `proposalNonce` | constructor (ADDRESS), proposeTarget | | `phase` | executeStep (Open->Executing->Finalized), finalize, cancel/veto (Open->Cancelled) | | `startTotal`, `spent`, `bought`, `stepsDone`, `lastStepAt` | executeStep | | `claimedTokens[m]`, `claimedRefund[m]` | _claim | ## Promises table | # | Promise | Holds? | |---|---|---| | P1 | Admin can only spend the pot by buying the agreed target via allowlisted router, path [base,target] | Route yes; **price no** (KR-1); "agreed target" weak in TICKER-only with reviewWindow 0 (KR-2) | | P2 | Step schedule fixed at creation, cooldown between steps | Yes (immutable `_stepBps`, `stepCooldown`) | | P3 | GUARD_TICKER: symbol() == ticker | Yes as specified; no protection against impostors on its own (KR-2) | | P4 | GUARD_ADDRESS: target fixed at creation | Yes | | P5 | GUARD_REGISTRY: target on curated registry (+ ticker match) | Yes, but registry is live, not snapshotted (REG-1) | | P6 | Review window after proposal | Yes when > 0; factory allows 0 (KR-2) | | P7 | Member veto by pledged weight | Weight is unsnapshotted live pledge; flash-pledge cancels (KR-3) | | P8 | maxPrice ceiling | Yes, robust (measured on actual fill); optional, default 0 (KR-1) | | P9 | minLiquidity (base reserve) | Spot read, manipulable in the same tx (KR-1) | | P10 | maxSlippageBps floor vs router quote | Floor is relative to in-tx spot quote; no protection vs the caller (KR-1) | | P11 | minRaise / maxRaise / walletCap | Yes; walletCap is per address (sybil) | | P12 | Pledge and execute deadlines | Yes; execution may start before pledgeDeadline (KR-7) | | P13 | Withdraw any time before first buy | Yes | | P14 | Same average price for all; pro-rata tokens + refund | Yes (fuzz + invariants; rounding down) | | P15 | Honeypot target cannot trap refund | Yes (`claimRefund`) | | P16 | Factory owner: no power over existing rounds | Factory yes; registry owner no (REG-1) | | P17 | Admin never custodies funds | Yes (swap `to` = round) | ## Invariants 1. `base.balanceOf(round) >= totalPledged` while Open. 2. `sum(pledged) == totalPledged`; frozen once Executing. 3. `startTotal == totalPledged` at the first step; `spent <= startTotal`. 4. `sum(stepAmounts) == startTotal` (last step takes remainder). 5. `sum(claimed tokens) <= bought`, `sum(claimed refunds) <= startTotal - spent`. 6. Each member's average price == `spent/bought` (rounding down only). 7. `vetoWeight[n] == sum(pledged[m] for m with hasVetoed[n][m])`. 8. No base leaves the round except to members or to the router inside executeStep. 9. Phase is monotone: Open -> {Executing -> Finalized | Cancelled}. 10. Only the admin can move base into the router; amount per step fixed by schedule. 11. The tokens bought per base spent are within maxSlippageBps of the **fair** price (fails: KR-1). 12. A target bought must have been visible to members for >= reviewWindow while they could still withdraw (holds, but window can be 0: KR-2). 13. Veto outcome reflects members who were in the round when the target was proposed (fails: KR-3). 14. After creation, no third party can change which targets a round can buy or stop a running round (fails: REG-1). 15. Claim refund never depends on target transfer working. ## Target chains Testnet chain 31390 (anvil-style keys, see INT-1). Production chain unknown; promises reference USDC and a real V2 router. Public mempool assumed (sandwich-able). USDC is blacklistable/pausable/upgradeable. ## Protocol types (register) launchpad-token (group buy / custodial pool), amm-dex integration, governance-ve (veto vote). ## External dependencies V2 router `getAmountsOut`/`swapExactTokensForTokens`/`factory()`; V2 pair `getReserves`/`token0`; TokenRegistry; base (USDC); target ERC20 (any, possibly malicious in TICKER/REGISTRY-without-snapshot modes).