@ballkidz/defifa 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/ADMINISTRATION.md +3 -3
  2. package/ARCHITECTURE.md +2 -0
  3. package/AUDIT_INSTRUCTIONS.md +422 -0
  4. package/CRYPTO_ECON.md +5 -5
  5. package/README.md +1 -1
  6. package/RISKS.md +38 -335
  7. package/SKILLS.md +1 -1
  8. package/USER_JOURNEYS.md +691 -0
  9. package/package.json +7 -7
  10. package/script/Deploy.s.sol +14 -3
  11. package/script/helpers/DefifaDeploymentLib.sol +13 -15
  12. package/src/DefifaDeployer.sol +221 -192
  13. package/src/DefifaGovernor.sol +286 -276
  14. package/src/DefifaHook.sol +68 -34
  15. package/src/DefifaProjectOwner.sol +27 -4
  16. package/src/DefifaTokenUriResolver.sol +136 -134
  17. package/src/enums/DefifaGamePhase.sol +1 -1
  18. package/src/enums/DefifaScorecardState.sol +1 -1
  19. package/src/interfaces/IDefifaDeployer.sol +52 -50
  20. package/src/interfaces/IDefifaGamePhaseReporter.sol +2 -2
  21. package/src/interfaces/IDefifaGamePotReporter.sol +1 -1
  22. package/src/interfaces/IDefifaGovernor.sol +53 -54
  23. package/src/interfaces/IDefifaHook.sol +104 -103
  24. package/src/interfaces/IDefifaTokenUriResolver.sol +2 -2
  25. package/src/libraries/DefifaFontImporter.sol +11 -9
  26. package/src/libraries/DefifaHookLib.sol +66 -53
  27. package/src/structs/DefifaAttestations.sol +1 -1
  28. package/src/structs/DefifaDelegation.sol +1 -1
  29. package/src/structs/DefifaLaunchProjectData.sol +4 -4
  30. package/src/structs/DefifaOpsData.sol +1 -1
  31. package/src/structs/DefifaScorecard.sol +1 -1
  32. package/src/structs/DefifaTierCashOutWeight.sol +1 -1
  33. package/src/structs/DefifaTierParams.sol +2 -1
  34. package/test/DefifaAdversarialQuorum.t.sol +602 -0
  35. package/test/DefifaAuditLowGuards.t.sol +304 -0
  36. package/test/DefifaFeeAccounting.t.sol +37 -16
  37. package/test/DefifaGovernor.t.sol +43 -19
  38. package/test/DefifaHookRegressions.t.sol +14 -12
  39. package/test/DefifaMintCostInvariant.t.sol +31 -12
  40. package/test/DefifaNoContest.t.sol +34 -16
  41. package/test/DefifaSecurity.t.sol +46 -28
  42. package/test/DefifaUSDC.t.sol +45 -36
  43. package/test/Fork.t.sol +43 -43
  44. package/test/SVG.t.sol +2 -2
  45. package/test/TestAuditGaps.sol +982 -0
  46. package/test/TestQALastMile.t.sol +511 -0
  47. package/test/regression/FulfillmentBlocksRatification.t.sol +36 -30
  48. package/test/regression/GracePeriodBypass.t.sol +15 -10
package/ADMINISTRATION.md CHANGED
@@ -23,7 +23,7 @@ Admin privileges and their scope in defifa-collection-deployer-v6.
23
23
  | Function | Required Role | Permission Check | What It Does |
24
24
  |----------|--------------|-----------------|-------------|
25
25
  | `launchGameWith()` | Anyone | None (permissionless) | Creates a new JB project, clones DefifaHook, initializes governor, configures rulesets with MINT/REFUND/SCORING phases. Game parameters are immutable after this call. |
26
- | `fulfillCommitmentsOf()` | Anyone | Guarded by `fulfilledCommitmentsOf[gameId] != 0` reentrancy check; requires `cashOutWeightIsSet` on the hook | Sends fee payouts (Defifa 5% + NANA 2.5% + user splits) via `sendPayoutsOf`, then queues the final COMPLETE ruleset. Can only execute once per game. |
26
+ | `fulfillCommitmentsOf()` | Anyone | Guarded by `fulfilledCommitmentsOf[gameId] != 0` reentrancy check; requires `cashOutWeightIsSet` on the hook | Sends fee payouts (Defifa 5% + NANA 2.5% + user splits) via try-catch `sendPayoutsOf`, then queues the final COMPLETE ruleset. If payout fails, emits `CommitmentPayoutFailed` and sets sentinel. Can only execute once per game. |
27
27
  | `triggerNoContestFor()` | Anyone | Requires `currentGamePhaseOf(gameId) == NO_CONTEST` and `!noContestTriggeredFor[gameId]` | Queues a new ruleset without payout limits so surplus equals balance, enabling full refunds. Can only execute once per game. |
28
28
 
29
29
  ### DefifaGovernor
@@ -33,7 +33,7 @@ Admin privileges and their scope in defifa-collection-deployer-v6.
33
33
  | `initializeGame()` | DefifaDeployer (owner) | `onlyOwner` (line 294) | Sets attestation start time and grace period for a game. Enforces minimum 1-day grace period. Called automatically during `launchGameWith()`. |
34
34
  | `submitScorecardFor()` | Anyone | Must be in SCORING phase; no ratified scorecard yet; no duplicate scorecard hash; weighted tiers must have nonzero supply | Submits a scorecard for attestation. Sets `attestationsBegin` and `gracePeriodEnds` timestamps. |
35
35
  | `attestToScorecardFrom()` | Any NFT holder | Must be in SCORING phase; scorecard must be ACTIVE or SUCCEEDED; caller cannot have already attested | Records attestation weight based on tier holdings at the scorecard's `attestationsBegin` timestamp. |
36
- | `ratifyScorecardFrom()` | Anyone | Scorecard must be in SUCCEEDED state (quorum met + grace period elapsed); no scorecard already ratified | Executes the scorecard via low-level call to `setTierCashOutWeightsTo` on the hook, then tries `fulfillCommitmentsOf`. |
36
+ | `ratifyScorecardFrom()` | Anyone | Scorecard must be in SUCCEEDED state (quorum met + grace period elapsed); no scorecard already ratified | Executes the scorecard via low-level call to `setTierCashOutWeightsTo` on the hook, then calls `fulfillCommitmentsOf`. |
37
37
 
38
38
  ### DefifaHook
39
39
 
@@ -75,7 +75,7 @@ COUNTDOWN --> MINT --> REFUND (optional) --> SCORING --> COMPLETE or NO_CONTEST
75
75
  2. NFT holders attest based on their per-tier voting weight (`attestToScorecardFrom`)
76
76
  3. Once quorum (50% of minted tiers' attestation power) is met and grace period passes, anyone ratifies (`ratifyScorecardFrom`)
77
77
  4. The governor calls `setTierCashOutWeightsTo` on the hook via low-level call
78
- 5. `fulfillCommitmentsOf` sends fee payouts and queues the final ruleset
78
+ 5. `fulfillCommitmentsOf` sends fee payouts (try-catch) and queues the final ruleset
79
79
 
80
80
  **No single entity controls scoring.** The process requires collective attestation from NFT holders across tiers.
81
81
 
package/ARCHITECTURE.md CHANGED
@@ -75,6 +75,8 @@ Attestor → DefifaGovernor.attestToScorecard(proposalId)
75
75
  - `@bananapus/721-hook-v6` — NFT tier system
76
76
  - `@bananapus/address-registry-v6` — Deterministic deploys
77
77
  - `@bananapus/permission-ids-v6` — Permission constants
78
+ - `@croptop/core-v6` — Croptop integration
79
+ - `@rev-net/core-v6` — Revnet integration
78
80
  - `@openzeppelin/contracts` — Checkpoints, Ownable, Clones
79
81
  - `@prb/math` — mulDiv
80
82
  - `scripty.sol` — On-chain scripting for SVG
@@ -0,0 +1,422 @@
1
+ # defifa-collection-deployer-v6 -- Audit Instructions
2
+
3
+ Prediction game platform built on Juicebox V6. Players buy NFT tiers representing outcomes, a governance process scores the outcomes, and winners claim treasury funds proportional to their tier's score.
4
+
5
+ ---
6
+
7
+ ## Architecture
8
+
9
+ Five contracts, one library. Total ~2,800 lines of production Solidity.
10
+
11
+ ```
12
+ DefifaDeployer.sol (906 lines) -- Game factory. Owns all game JB projects. Manages lifecycle rulesets, fee splits, fulfillment, no-contest.
13
+ DefifaHook.sol (1082 lines) -- Pay/cashout hook. NFT minting, burning, attestation delegation, fee token distribution, cash-out weight logic.
14
+ DefifaGovernor.sol (514 lines) -- Scorecard governance. Submit, attest, ratify scorecards. Singleton across all games.
15
+ DefifaHookLib.sol (368 lines) -- Pure/view helpers. Weight validation, cash-out math, attestation computation, token claiming.
16
+ DefifaProjectOwner.sol (67 lines) -- Permanent holder of the Defifa project NFT. Grants SET_SPLIT_GROUPS permission.
17
+ DefifaTokenUriResolver.sol (313 lines) -- On-chain SVG metadata for game NFTs.
18
+ ```
19
+
20
+ ### Contract Relationships
21
+
22
+ ```
23
+ DefifaDeployer
24
+ ├── creates JB projects via CONTROLLER.launchProjectFor()
25
+ ├── clones DefifaHook via Clones.cloneDeterministic()
26
+ ├── initializes DefifaGovernor.initializeGame() for each game
27
+ ├── implements IDefifaGamePhaseReporter (phase state machine)
28
+ ├── implements IDefifaGamePotReporter (treasury balance queries)
29
+ ├── fulfillCommitmentsOf() -- sends fee payouts, queues final ruleset
30
+ └── triggerNoContestFor() -- queues refund ruleset for NO_CONTEST games
31
+
32
+ DefifaHook (clone, one per game)
33
+ ├── extends JB721Hook (ERC-721 with Juicebox terminal integration)
34
+ ├── extends Ownable (owner = DefifaGovernor)
35
+ ├── afterPayRecordedWith() -- mints NFTs on payment
36
+ ├── beforeCashOutRecordedWith() -- calculates reclaim amounts
37
+ ├── afterCashOutRecordedWith() -- burns NFTs, distributes fee tokens
38
+ ├── setTierCashOutWeightsTo() -- onlyOwner, called by governor
39
+ ├── attestation delegation system (checkpoints, per-tier, per-account)
40
+ └── delegates to DefifaHookLib for computation
41
+
42
+ DefifaGovernor (singleton, shared across all games)
43
+ ├── extends Ownable (owner = DefifaDeployer)
44
+ ├── submitScorecardFor() -- anyone during SCORING
45
+ ├── attestToScorecardFrom() -- NFT holders during SCORING
46
+ ├── ratifyScorecardFrom() -- anyone when SUCCEEDED
47
+ │ ├── calls DefifaHook.setTierCashOutWeightsTo() via low-level call
48
+ │ └── calls DefifaDeployer.fulfillCommitmentsOf() (internal try-catch on sendPayoutsOf)
49
+ └── quorum() -- 50% of minted tiers' max attestation power
50
+ ```
51
+
52
+ ### Dependencies
53
+
54
+ | Dependency | Used For |
55
+ |-----------|---------|
56
+ | `@bananapus/core-v6` | JBController, JBMultiTerminal, JBTerminalStore, JBRulesets, JBDirectory, JBPrices |
57
+ | `@bananapus/721-hook-v6` | JB721Hook, JB721TiersHookStore, tier management, NFT minting/burning |
58
+ | `@bananapus/address-registry-v6` | Deterministic hook deployment tracking |
59
+ | `@bananapus/permission-ids-v6` | SET_SPLIT_GROUPS permission constant |
60
+ | `@openzeppelin/contracts` | Ownable, Clones, Checkpoints.Trace208, SafeERC20, IERC721Receiver |
61
+ | `@prb/math` | mulDiv for precise fee and weight calculations |
62
+ | `scripty.sol` / `typeface` | On-chain SVG font rendering |
63
+
64
+ ---
65
+
66
+ ## Game Lifecycle
67
+
68
+ ### Phase State Machine
69
+
70
+ Phases are determined by Juicebox ruleset cycle numbers, safety mechanism checks, and scorecard ratification status. The state machine is in `DefifaDeployer.currentGamePhaseOf()` (line 221-257).
71
+
72
+ ```
73
+ COUNTDOWN (cycleNumber == 0)
74
+
75
+
76
+ MINT (cycleNumber == 1)
77
+ │ Players buy NFTs. Delegation available. Refunds allowed at mint price.
78
+ │ Reserved minting paused (pauseMintPendingReserves: true).
79
+
80
+ REFUND (cycleNumber == 2, if refundPeriodDuration != 0)
81
+ │ No new payments (pausePay: true). Refunds at mint price.
82
+ │ Reserved minting paused.
83
+
84
+ SCORING (cycleNumber >= 2/3, duration == 0)
85
+ │ Checks applied IN THIS ORDER:
86
+ │ 1. cashOutWeightIsSet? → COMPLETE (ratified scorecard is final)
87
+ │ 2. noContestTriggeredFor? → NO_CONTEST
88
+ │ 3. minParticipation check: balance < threshold? → NO_CONTEST
89
+ │ 4. scorecardTimeout check: block.timestamp > start + timeout? → NO_CONTEST
90
+ │ 5. Otherwise → SCORING
91
+
92
+ COMPLETE (cashOutWeightIsSet == true)
93
+ │ Winners cash out at scored weights. Fee tokens distributed.
94
+
95
+ NO_CONTEST (safety mechanism triggered)
96
+ │ Requires triggerNoContestFor() before cash-outs work.
97
+ │ Full refund at mint price.
98
+ ```
99
+
100
+ ### Ruleset Configuration per Phase
101
+
102
+ | Phase | pausePay | cashOutTaxRate | ownerMustSendPayouts | payoutLimits | fundAccessLimitGroups |
103
+ |-------|----------|---------------|---------------------|-------------|----------------------|
104
+ | MINT | false | 0 | false | none | none |
105
+ | REFUND | true | 0 | false | none | none |
106
+ | SCORING | true | 0 | true | uint224.max | yes (fee splits) |
107
+ | COMPLETE (post-fulfill) | true | 0 | true | none | none |
108
+ | NO_CONTEST (post-trigger) | true | 0 | true | none | none |
109
+
110
+ ---
111
+
112
+ ## Key Flows
113
+
114
+ ### Payment and Minting (DefifaHook.afterPayRecordedWith → _processPayment)
115
+
116
+ 1. Verify caller is a project terminal, currency matches `pricingCurrency`.
117
+ 2. Decode metadata: `(address _attestationDelegate, uint16[] _tierIdsToMint)`.
118
+ 3. Compute attestation units per unique tier via `DefifaHookLib.computeAttestationUnits()`.
119
+ 4. For each unique tier: set delegation if needed, transfer attestation units from address(0) to payer.
120
+ 5. Call `_mintAll()`: `store.recordMint()`, increment `_totalMintCost += amount`, mint ERC-721s.
121
+ 6. Revert if `leftoverAmount != 0` (exact pricing enforced, `DefifaHook_Overspending`).
122
+
123
+ ### Cash-Out (DefifaHook.beforeCashOutRecordedWith + afterCashOutRecordedWith)
124
+
125
+ **Before (view, returns reclaim params):**
126
+ 1. Decode token IDs from metadata.
127
+ 2. Compute cumulative mint price via `DefifaHookLib.computeCumulativeMintPrice()`.
128
+ 3. Compute `cashOutCount` based on game phase:
129
+ - MINT/REFUND/NO_CONTEST: `cashOutCount = cumulativeMintPrice` (full refund).
130
+ - SCORING/COMPLETE: `cashOutCount = mulDiv(surplus + amountRedeemed, cumulativeCashOutWeight, TOTAL_CASHOUT_WEIGHT)`.
131
+ 4. Return `totalSupply = surplus.value` (the surplus IS the total supply for Juicebox's bonding curve).
132
+
133
+ **After (state-changing, burns tokens):**
134
+ 1. Verify caller is a project terminal.
135
+ 2. For each token: verify ownership, burn it, increment `tokensRedeemedFrom[tierId]` if COMPLETE.
136
+ 3. Call `store.recordBurn()`.
137
+ 4. If COMPLETE: increment `amountRedeemed`, call `_claimTokensFor()` to distribute fee tokens.
138
+ 5. Revert with `DefifaHook_NothingToClaim` if reclaimed amount is 0 AND no fee tokens were distributed.
139
+ 6. Decrement `_totalMintCost -= cumulativeMintPrice`.
140
+
141
+ ### Scorecard Governance
142
+
143
+ **Submit (DefifaGovernor.submitScorecardFor):**
144
+ 1. Require SCORING phase, game initialized, no ratified scorecard.
145
+ 2. Validate: no weight on tiers with zero supply.
146
+ 3. Hash scorecard: `keccak256(abi.encode(dataHook, abi.encodeWithSelector(setTierCashOutWeightsTo.selector, tierWeights)))`.
147
+ 4. Store `attestationsBegin = max(block.timestamp, attestationStartTime)`.
148
+ 5. Store `gracePeriodEnds = attestationsBegin + attestationGracePeriod`.
149
+
150
+ **Attest (DefifaGovernor.attestToScorecardFrom):**
151
+ 1. Require SCORING phase, scorecard ACTIVE or SUCCEEDED.
152
+ 2. Prevent double attestation per account per scorecard.
153
+ 3. Compute weight via `getAttestationWeight()` at `attestationsBegin` timestamp.
154
+ 4. Increment `_scorecardAttestationsOf[gameId][scorecardId].count += weight`.
155
+
156
+ **Ratify (DefifaGovernor.ratifyScorecardFrom):**
157
+ 1. Require no prior ratification, scorecard in SUCCEEDED state.
158
+ 2. Store `ratifiedScorecardIdOf[gameId] = scorecardId`.
159
+ 3. Execute scorecard via low-level call: `dataHook.call(abi.encodeWithSelector(setTierCashOutWeightsTo.selector, tierWeights))`.
160
+ 4. Direct call: `IDefifaDeployer(owner).fulfillCommitmentsOf(gameId)`.
161
+
162
+ ### Commitment Fulfillment (DefifaDeployer.fulfillCommitmentsOf)
163
+
164
+ 1. Guard: `fulfilledCommitmentsOf[gameId] != 0` → return (idempotent).
165
+ 2. Require `cashOutWeightIsSet == true`.
166
+ 3. Compute `feeAmount = mulDiv(pot, _commitmentPercentOf[gameId], SPLITS_TOTAL_PERCENT)`.
167
+ 4. Store `fulfilledCommitmentsOf[gameId] = max(feeAmount, 1)` (reentrancy guard).
168
+ 5. Try-catch: `terminal.sendPayoutsOf(gameId, token, feeAmount, ..., minTokensPaidOut: 0)`. On failure, reset to sentinel (1) and emit `CommitmentPayoutFailed`.
169
+ 6. Queue final ruleset: no payout limits, no fund access constraints, surplus = entire balance.
170
+
171
+ ### No-Contest Trigger (DefifaDeployer.triggerNoContestFor)
172
+
173
+ 1. Require `currentGamePhaseOf(gameId) == NO_CONTEST`.
174
+ 2. Require `!noContestTriggeredFor[gameId]`.
175
+ 3. Set `noContestTriggeredFor[gameId] = true`.
176
+ 4. Queue ruleset: no `fundAccessLimitGroups`, making balance = surplus for full refunds.
177
+
178
+ ---
179
+
180
+ ## Attestation and Governance Mechanics
181
+
182
+ ### Attestation Power Calculation (DefifaGovernor.getAttestationWeight)
183
+
184
+ Per-tier attestation power for an account:
185
+ ```
186
+ tierPower = MAX_ATTESTATION_POWER_TIER * (account's attestation units / tier's total attestation units)
187
+ ```
188
+
189
+ Where:
190
+ - `MAX_ATTESTATION_POWER_TIER = 1,000,000,000` (1e9)
191
+ - Account's units come from `getPastTierAttestationUnitsOf()` (checkpoint at `attestationsBegin` timestamp)
192
+ - Total tier units from `getPastTierTotalAttestationUnitsOf()`
193
+
194
+ Total attestation power = sum of per-tier powers across all tiers.
195
+
196
+ ### Quorum Calculation (DefifaGovernor.quorum)
197
+
198
+ ```
199
+ quorum = (number_of_minted_tiers * MAX_ATTESTATION_POWER_TIER) / 2
200
+ ```
201
+
202
+ A tier is "minted" if `currentSupplyOfTier(tierId) != 0` (live supply, reads current state, not snapshotted).
203
+
204
+ ### Scorecard State Machine (DefifaGovernor.stateOf)
205
+
206
+ ```
207
+ If ratifiedScorecardIdOf[gameId] != 0:
208
+ This scorecard == ratified? → RATIFIED
209
+ Otherwise → DEFEATED
210
+ If attestationsBegin > block.timestamp → PENDING
211
+ If gracePeriodEnds > block.timestamp → ACTIVE
212
+ If quorum <= attestation count → SUCCEEDED
213
+ Otherwise → ACTIVE
214
+ ```
215
+
216
+ ### Cash-Out Weight Validation (DefifaHookLib.validateAndBuildWeights)
217
+
218
+ 1. Tier IDs must be in strictly ascending order (prevents duplicates).
219
+ 2. Each tier must be in category 0.
220
+ 3. Each tier must exist (id <= maxTierId).
221
+ 4. Cumulative weight must equal exactly `TOTAL_CASHOUT_WEIGHT` (1e18).
222
+ 5. Stored as `uint256[128]` array indexed by `tierId - 1`.
223
+
224
+ ### Per-Token Cash-Out Weight (DefifaHookLib.computeCashOutWeight)
225
+
226
+ ```
227
+ totalTokensForCashoutInTier = initialSupply - remainingSupply - (burnedTokens - tokensRedeemedFrom[tierId])
228
+ perTokenWeight = tierWeight / totalTokensForCashoutInTier
229
+ ```
230
+
231
+ Integer division rounds down. Maximum dust loss: 1 wei per tier per game (128 wei max across all tiers).
232
+
233
+ ---
234
+
235
+ ## Fee Structure
236
+
237
+ ### Constants
238
+
239
+ ```
240
+ DEFIFA_FEE_DIVISOR = 20 → 5.0% to Defifa project
241
+ BASE_PROTOCOL_FEE_DIVISOR = 40 → 2.5% to NANA/base protocol project
242
+ Total platform fees: 7.5% of the pot
243
+ ```
244
+
245
+ ### Split Normalization (_buildSplits)
246
+
247
+ 1. Compute absolute percents: `nanaPercent = SPLITS_TOTAL_PERCENT / 40`, `defifaPercent = SPLITS_TOTAL_PERCENT / 20`.
248
+ 2. Add any user-defined splits.
249
+ 3. Sum total absolute percent; revert if > SPLITS_TOTAL_PERCENT.
250
+ 4. Normalize each split: `normalizedPercent = mulDiv(absolutePercent, SPLITS_TOTAL_PERCENT, totalAbsolute)`.
251
+ 5. NANA split placed last, absorbs rounding remainder: `SPLITS_TOTAL_PERCENT - normalizedTotal`.
252
+ 6. Store `_commitmentPercentOf[gameId] = totalAbsolutePercent` for fulfillment calculation.
253
+
254
+ ### Fee Token Distribution (_claimTokensFor via DefifaHookLib.claimTokensFor)
255
+
256
+ During COMPLETE cash-outs, the hook distributes `$DEFIFA` and `$NANA` tokens proportionally:
257
+ ```
258
+ defifaAmount = mulDiv(defifaBalance, shareToBeneficiary, outOfTotal)
259
+ baseProtocolAmount = mulDiv(baseProtocolBalance, shareToBeneficiary, outOfTotal)
260
+ ```
261
+
262
+ Where `shareToBeneficiary = cumulativeMintPrice` of burned tokens and `outOfTotal = _totalMintCost`.
263
+
264
+ ---
265
+
266
+ ## Priority Audit Areas
267
+
268
+ ### P0 -- Critical (Fund Safety)
269
+
270
+ 1. **Cash-out weight arithmetic**: Verify `computeCashOutWeight()` and `computeCashOutCount()` in `DefifaHookLib` cannot overflow or return inflated values. The `_weight / _totalTokensForCashoutInTier` division is the core economic calculation. Confirm `tokensRedeemedFrom` tracking is correct: incremented ONLY during COMPLETE cash-outs (line 656), NOT during MINT/REFUND refunds.
271
+
272
+ 2. **`_totalMintCost` integrity**: This variable is the denominator for fee token distribution. It is incremented on paid mint (`_mintAll`, line 869), reserved mint (`mintReservesFor`, line 571), and decremented on cash-out (`afterCashOutRecordedWith`, line 685). Verify no path exists where `_totalMintCost` underflows or becomes inconsistent with actual live token count.
273
+
274
+ 3. **Fulfillment reentrancy guard**: `fulfilledCommitmentsOf[gameId]` is set to `max(feeAmount, 1)` BEFORE external calls to `sendPayoutsOf` and `queueRulesetsOf` (DefifaDeployer lines 325-382). Verify this guard prevents double fulfillment via reentrancy through the terminal.
275
+
276
+ 4. **Scorecard execution via low-level call**: `ratifyScorecardFrom` calls `_metadata.dataHook.call(_calldata)` (DefifaGovernor line 402). The `_calldata` is `abi.encodeWithSelector(setTierCashOutWeightsTo.selector, tierWeights)`. Verify that the hash-based proposal system prevents any calldata that does not match the submitted scorecard from being executed.
277
+
278
+ 5. **Fee accounting during fulfillment**: `fulfillCommitmentsOf` computes `feeAmount = mulDiv(pot, _commitmentPercentOf[gameId], SPLITS_TOTAL_PERCENT)` and sends this amount as payouts via try-catch. On success, `fulfilledCommitmentsOf` retains the fee amount; on failure, it resets to sentinel (1) and the fee stays in the pot. Verify that `currentGamePotOf` correctly subtracts `fulfilledCommitmentsOf` and that the sentinel value (1 wei) does not cause meaningful accounting error.
279
+
280
+ ### P1 -- High (Governance Integrity)
281
+
282
+ 6. **Quorum manipulation via live supply**: `quorum()` reads `currentSupplyOfTier()` at call time (not snapshotted). Verify that burning tokens during SCORING is prevented by `DefifaHook_NothingToClaim` (cash-out weights not set yet). Check if any other burn path exists that could reduce quorum after attestations have begun.
283
+
284
+ 7. **Attestation snapshotting**: Attestation weight is computed at the `attestationsBegin` timestamp via `getPastTierAttestationUnitsOf()`. Verify that the `Checkpoints.Trace208.upperLookup()` correctly captures the state at that exact timestamp, and that minting or transferring NFTs after `attestationsBegin` does not retroactively affect attestation power.
285
+
286
+ 8. **Double attestation prevention**: `_attestations.hasAttested[msg.sender]` (DefifaGovernor line 354) prevents double voting. But verify that an attacker cannot attest, transfer NFTs to another address, and have that address attest with the same attestation power (the snapshot at `attestationsBegin` should prevent this, but verify the checkpoint resolution).
287
+
288
+ 9. **Grace period anchoring**: `gracePeriodEnds = attestationsBegin + attestationGracePeriod` (DefifaGovernor line 477). Verify that early scorecard submission (before `attestationStartTime`) correctly delays the grace period start, preventing instant ratification.
289
+
290
+ ### P2 -- Medium (Access Control and State Transitions)
291
+
292
+ 10. **Hook ownership chain**: DefifaDeployer creates the hook clone, calls `initialize()`, then `transferOwnership(GOVERNOR)` (line 568). Verify that no window exists between `initialize()` and `transferOwnership()` where an attacker could call `setTierCashOutWeightsTo()` (requires `onlyOwner`).
293
+
294
+ 11. **Phase check ordering in `currentGamePhaseOf()`**: The function checks `cashOutWeightIsSet` BEFORE `noContestTriggeredFor` (lines 233-236). Verify this ordering is correct: a ratified scorecard should always take priority over no-contest.
295
+
296
+ 12. **Clone initialization guard**: `DefifaHook.initialize()` checks `address(this) == CODE_ORIGIN` (line 486, prevents initializing the implementation) and `address(store) != address(0)` (line 489, prevents re-initialization). Verify these guards are sufficient against proxy/clone attacks.
297
+
298
+ 13. **Delegation lockdown**: `setTierDelegateTo` and `setTierDelegatesTo` require `MINT` phase (DefifaHook lines 740, 751). Verify that auto-delegation on transfer (`_transferTierAttestationUnits`, lines 1027-1031) correctly handles the case where a recipient already has a delegate set.
299
+
300
+ ### P3 -- Low (Edge Cases and Rounding)
301
+
302
+ 14. **Integer division dust**: `computeCashOutWeight()` returns `_weight / _totalTokensForCashoutInTier`. The maximum loss is 1 wei per tier. With 128 max tiers, at most 128 wei locked per game. Verify this bound is correct.
303
+
304
+ 15. **`uint208` overflow in checkpoints**: Attestation units use `Checkpoints.Trace208`. Maximum per tier: `tier.votingUnits * tier.initialSupply`. With `initialSupply = 999_999_999` and typical voting units, verify this cannot overflow `uint208`.
305
+
306
+ 16. **Token URI resolver interaction**: `DefifaTokenUriResolver.tokenUriOf()` calls `gamePotReporter.currentGamePotOf()` and `hook.cashOutWeightOf()`. Verify that a malicious URI resolver cannot cause state changes or excessive gas consumption.
307
+
308
+ ---
309
+
310
+ ## Invariants
311
+
312
+ These properties should hold for all games in all states. The test suite validates most of them.
313
+
314
+ ### Fund Conservation
315
+ - `totalCashOuts + remainingSurplus + fulfilledCommitments == originalPot` (within N wei where N = total user count)
316
+ - `amountRedeemed` (DefifaHook) only increases during COMPLETE cash-outs, never during MINT/REFUND/NO_CONTEST
317
+ - `fulfilledCommitmentsOf[gameId]` is set exactly once (idempotent guard)
318
+
319
+ ### Token Accounting
320
+ - `_totalMintCost == sum(tier.price * liveTokenCount[tier])` for all tiers at all times
321
+ - `tokensRedeemedFrom[tierId]` only incremented during COMPLETE phase cash-outs
322
+ - `_totalMintCost` decremented by exactly `cumulativeMintPrice` on each cash-out
323
+
324
+ ### Scorecard Integrity
325
+ - `sum(tierWeights[i].cashOutWeight) == TOTAL_CASHOUT_WEIGHT` (exactly 1e18) for any ratified scorecard
326
+ - Tier IDs in scorecard are strictly ascending (no duplicates)
327
+ - Only tiers in category 0 can receive cash-out weight
328
+ - Only tiers with `currentSupply > 0` can receive nonzero weight at submission time
329
+
330
+ ### Governance
331
+ - Each account can attest to a given scorecard at most once
332
+ - Attestation power is snapshotted at `attestationsBegin` (not live)
333
+ - Quorum threshold: 50% of minted tiers' total max attestation power (live at call time)
334
+ - Only one scorecard can be ratified per game
335
+ - Minimum grace period: 1 day (enforced in `initializeGame`, line 303)
336
+
337
+ ### Phase Transitions
338
+ - A ratified scorecard (`cashOutWeightIsSet == true`) always produces COMPLETE, regardless of other conditions
339
+ - NO_CONTEST is only reachable from SCORING (never from MINT or REFUND)
340
+ - `triggerNoContestFor` can be called exactly once per game
341
+ - Phase progression is monotonic: COUNTDOWN -> MINT -> REFUND -> SCORING -> COMPLETE/NO_CONTEST
342
+
343
+ ### Attestation Units
344
+ - Sum of all delegate attestation units for a tier == total tier attestation units (conservation on transfer)
345
+ - Auto-delegation on transfer prevents units from being lost to `address(0)`
346
+ - Delegation changes only allowed during MINT phase (except auto-delegation on transfer)
347
+
348
+ ---
349
+
350
+ ## Testing
351
+
352
+ ### Test Files (14 files, ~100 test functions)
353
+
354
+ | File | Focus |
355
+ |------|-------|
356
+ | `DefifaGovernor.t.sol` | Core lifecycle: minting, refunding, scoring, cash-out. Fuzz tests on tier counts and distributions. |
357
+ | `DefifaSecurity.t.sol` | Fund conservation (fuzz), high-volume 32 tiers, winner-take-all, extreme weights, quorum manipulation, delegation lockdown, reserved minter fee tokens. |
358
+ | `DefifaNoContest.t.sol` | Both NO_CONTEST triggers: minParticipation threshold and scorecardTimeout. Trigger/refund/idempotency. |
359
+ | `DefifaFeeAccounting.t.sol` | Fee split normalization, rounding loss bounds, cash-out after fees, user splits. |
360
+ | `DefifaMintCostInvariant.t.sol` | Stateful fuzz: `_totalMintCost` invariant across random mints and refunds. |
361
+ | `DefifaHookRegressions.t.sol` | Audit finding M-5: attestation unit conservation on transfer to undelegated recipients. |
362
+ | `DefifaAuditLowGuards.t.sol` | Input validation: double initialization, uint48 overflow, zero-address delegation. |
363
+ | `Fork.t.sol` | Mainnet fork tests: full lifecycle, edge cases, all revert conditions, scorecard state machine. ~50 tests. |
364
+ | `regression/FulfillmentBlocksRatification.t.sol` | Fulfillment failure does not block ratification (try-catch behavior). |
365
+ | `regression/GracePeriodBypass.t.sol` | Grace period extends from attestation start, not submission time. |
366
+ | `DefifaUSDC.t.sol` | ERC-20 (USDC) game variant. |
367
+ | `SVG.t.sol` | Token URI resolver SVG rendering. |
368
+
369
+ ### Running Tests
370
+
371
+ ```bash
372
+ forge test --match-path "test/*.t.sol" -vvv
373
+ forge test --match-path "test/regression/*.t.sol" -vvv
374
+ ```
375
+
376
+ For invariant tests:
377
+ ```bash
378
+ forge test --match-contract DefifaMintCostInvariant -vvv
379
+ ```
380
+
381
+ ### Known Test Gaps
382
+
383
+ | Area | Current Coverage | Risk |
384
+ |------|-----------------|------|
385
+ | ERC-20 token games (non-ETH) | Single USDC test file | LOW |
386
+ | Games with >32 tiers | Fuzz caps at 12, one test at 32 | LOW |
387
+ | Concurrent multi-game governor | Tests use single game per governor | MEDIUM |
388
+ | Adversarial token URI resolver | No malicious resolver test | LOW |
389
+ | Clone address collision | No explicit collision test | LOW |
390
+
391
+ ---
392
+
393
+ ## Constants Reference
394
+
395
+ | Constant | Value | Location |
396
+ |----------|-------|---------|
397
+ | `TOTAL_CASHOUT_WEIGHT` | 1e18 | DefifaHookLib line 28 |
398
+ | `MAX_ATTESTATION_POWER_TIER` | 1e9 | DefifaGovernor line 64 |
399
+ | `DEFIFA_FEE_DIVISOR` | 20 (5%) | DefifaDeployer line 111 |
400
+ | `BASE_PROTOCOL_FEE_DIVISOR` | 40 (2.5%) | DefifaDeployer line 107 |
401
+ | `SPLITS_TOTAL_PERCENT` | 1e9 | JBConstants |
402
+ | `initialSupply` per tier | 999,999,999 | DefifaDeployer line 491 |
403
+ | Max tiers per game | 128 | DefifaHook `uint256[128]` (line 76) |
404
+ | Min grace period | 1 day | DefifaGovernor line 303 |
405
+ | Compiler | Solidity 0.8.26 | All files |
406
+
407
+ ---
408
+
409
+ ## Entry Points for Review
410
+
411
+ Start with the money: follow ETH from payment to cash-out.
412
+
413
+ 1. `DefifaHook._processPayment()` (line 929) -- where tokens enter
414
+ 2. `DefifaHook.beforeCashOutRecordedWith()` (line 253) -- reclaim calculation
415
+ 3. `DefifaHook.afterCashOutRecordedWith()` (line 605) -- where tokens leave
416
+ 4. `DefifaDeployer.fulfillCommitmentsOf()` (line 296) -- fee distribution
417
+ 5. `DefifaGovernor.ratifyScorecardFrom()` (line 372) -- scorecard execution
418
+ 6. `DefifaHookLib.validateAndBuildWeights()` (line 35) -- weight validation
419
+ 7. `DefifaHookLib.computeCashOutWeight()` (line 95) -- per-token value
420
+ 8. `DefifaDeployer._buildSplits()` (line 826) -- fee normalization
421
+ 9. `DefifaDeployer.currentGamePhaseOf()` (line 221) -- phase state machine
422
+ 10. `DefifaDeployer.triggerNoContestFor()` (line 586) -- no-contest safety valve
package/CRYPTO_ECON.md CHANGED
@@ -707,7 +707,7 @@ We identify five distinct scenarios in which game funds could become permanently
707
707
 
708
708
  **Scenario D: Attestation power in dead addresses.** If >50% of game pieces are transferred to contracts that cannot call `attestToScorecardFrom()`, the exercisable attestation power drops below quorum permanently. This is distinct from Scenario C because the delegation may be correct but the delegatees are inaccessible.
709
709
 
710
- **Scenario E: Split target reverts on ratification.** `ratifyScorecardFrom()` calls `fulfillCommitmentsOf()`, which calls `sendPayoutsOf()`. If a split target is a reverting contract, the entire ratification transaction fails. The scorecard reached SUCCEEDED state in the governor, but the on-chain execution of `setTierCashOutWeightsTo` never completes. The game is stuck in SCORING despite having a governance-approved outcome.
710
+ **Scenario E: Split target reverts on ratification.** `ratifyScorecardFrom()` calls `fulfillCommitmentsOf()`, which calls `sendPayoutsOf()`. If a split target is a reverting contract, `sendPayoutsOf` is caught by the internal try-catch in `fulfillCommitmentsOf`. The `CommitmentPayoutFailed` event is emitted, `fulfilledCommitmentsOf` is set to the sentinel value 1, and the final ruleset is still queued. Players can cash out immediately — the fee amount stays in the pot, slightly benefiting cash-out recipients. This is no longer a stuck-funds scenario.
711
711
 
712
712
  | Scenario | Funds stuck? | Delegate resolves? | Automated resolution? |
713
713
  |:---------|:------------:|:------------------:|:---------------------:|
@@ -715,7 +715,7 @@ We identify five distinct scenarios in which game funds could become permanently
715
715
  | B: Quorum unreachable | Yes | Yes, if has power | No |
716
716
  | C: Dead delegate | Yes | No | No |
717
717
  | D: Dead attestation holders | Yes | No | No |
718
- | E: Split target reverts | Yes | No | No |
718
+ | E: Split target reverts | No | N/A | Yes (try-catch) |
719
719
 
720
720
  Note: the case where *all* minters refund during REFUND is not a deadlock — the treasury balance drops to zero and there are no funds to recover.
721
721
 
@@ -900,13 +900,13 @@ _terminal.sendPayoutsOf({
900
900
  token: _token,
901
901
  amount: _pot,
902
902
  currency: ...,
903
- minTokensPaidOut: _pot
903
+ minTokensPaidOut: 0
904
904
  });
905
905
  ```
906
906
 
907
- The split structure routes fees (~10%) to protocol projects and returns the remainder (~90%) back to the game treasury via `addToBalanceOf`. If `sendPayoutsOf` interprets `minTokensPaidOut` as the minimum that permanently leaves the project (rather than the total processed), this transaction will revert because only ~10% actually leaves. This would permanently block fee extraction and game completion, trapping all funds.
907
+ The split structure routes fees (~10%) to protocol projects and returns the remainder (~90%) back to the game treasury via `addToBalanceOf`. `minTokensPaidOut` is set to 0 to avoid reverts from partial payouts. Additionally, the entire `sendPayoutsOf` call is wrapped in a try-catch: if the payout fails for any reason, `CommitmentPayoutFailed` is emitted, `fulfilledCommitmentsOf` is reset to the sentinel value 1, and the final ruleset is still queued. The fee amount stays in the pot, slightly benefiting cash-out recipients.
908
908
 
909
- **Recommended fix.** Set `minTokensPaidOut` to 0 or to the expected fee amount (i.e., `_pot / baseProtocolFeeDivisor + _pot / defifaFeeDivisor`). This preserves the economic intent while removing the revert risk.
909
+ **Status:** Resolved. `minTokensPaidOut` set to 0 and try-catch ensures the final ruleset is always queued.
910
910
 
911
911
  ---
912
912
 
package/README.md CHANGED
@@ -97,7 +97,7 @@ Fees are taken as payouts during commitment fulfillment. The remaining surplus i
97
97
  ## Install
98
98
 
99
99
  ```bash
100
- npm install @ballkidz/defifa-collection-deployer
100
+ npm install @ballkidz/defifa
101
101
  ```
102
102
 
103
103
  ## Develop