@ballkidz/defifa 0.0.6 → 0.0.8

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 (47) hide show
  1. package/ADMINISTRATION.md +3 -3
  2. package/AUDIT_INSTRUCTIONS.md +422 -0
  3. package/CRYPTO_ECON.md +5 -5
  4. package/RISKS.md +38 -335
  5. package/SKILLS.md +1 -1
  6. package/STYLE_GUIDE.md +14 -1
  7. package/USER_JOURNEYS.md +691 -0
  8. package/package.json +7 -5
  9. package/script/Deploy.s.sol +26 -13
  10. package/script/helpers/DefifaDeploymentLib.sol +30 -14
  11. package/src/DefifaDeployer.sol +225 -187
  12. package/src/DefifaGovernor.sol +291 -281
  13. package/src/DefifaHook.sol +81 -42
  14. package/src/DefifaProjectOwner.sol +27 -4
  15. package/src/DefifaTokenUriResolver.sol +137 -134
  16. package/src/enums/DefifaGamePhase.sol +1 -1
  17. package/src/enums/DefifaScorecardState.sol +1 -1
  18. package/src/interfaces/IDefifaDeployer.sol +52 -50
  19. package/src/interfaces/IDefifaGamePhaseReporter.sol +2 -2
  20. package/src/interfaces/IDefifaGamePotReporter.sol +1 -1
  21. package/src/interfaces/IDefifaGovernor.sol +53 -54
  22. package/src/interfaces/IDefifaHook.sol +104 -103
  23. package/src/interfaces/IDefifaTokenUriResolver.sol +2 -2
  24. package/src/libraries/DefifaFontImporter.sol +11 -9
  25. package/src/libraries/DefifaHookLib.sol +68 -53
  26. package/src/structs/DefifaAttestations.sol +1 -1
  27. package/src/structs/DefifaDelegation.sol +1 -1
  28. package/src/structs/DefifaLaunchProjectData.sol +4 -4
  29. package/src/structs/DefifaOpsData.sol +1 -1
  30. package/src/structs/DefifaScorecard.sol +1 -1
  31. package/src/structs/DefifaTierCashOutWeight.sol +1 -1
  32. package/src/structs/DefifaTierParams.sol +2 -1
  33. package/test/DefifaAdversarialQuorum.t.sol +602 -0
  34. package/test/DefifaAuditLowGuards.t.sol +304 -0
  35. package/test/DefifaFeeAccounting.t.sol +37 -16
  36. package/test/DefifaGovernor.t.sol +37 -11
  37. package/test/DefifaHookRegressions.t.sol +14 -12
  38. package/test/DefifaMintCostInvariant.t.sol +31 -12
  39. package/test/DefifaNoContest.t.sol +33 -13
  40. package/test/DefifaSecurity.t.sol +45 -25
  41. package/test/DefifaUSDC.t.sol +44 -34
  42. package/test/Fork.t.sol +42 -40
  43. package/test/SVG.t.sol +2 -2
  44. package/test/TestAuditGaps.sol +982 -0
  45. package/test/TestQALastMile.t.sol +511 -0
  46. package/test/regression/FulfillmentBlocksRatification.t.sol +36 -30
  47. package/test/regression/GracePeriodBypass.t.sol +15 -10
package/RISKS.md CHANGED
@@ -1,349 +1,52 @@
1
- # defifa-collection-deployer-v6 -- Risks
1
+ # RISKS.md -- defifa-collection-deployer-v6
2
2
 
3
- Deep implementation-level risk analysis with line references, severity ratings, and test coverage mapping.
3
+ ## 1. Trust Assumptions
4
4
 
5
- ## Trust Assumptions
5
+ - **Governor as Hook Owner.** The DefifaGovernor owns each DefifaHook clone. The governor can set tier cash-out weights via `ratifyScorecardFrom`, which executes an arbitrary call to the hook. If the governor is compromised, the hook's cash-out weights can be set to any values.
6
+ - **Deployer as Project Owner.** The DefifaDeployer contract owns all game projects. It controls ruleset queuing, payout sending, and split configuration. Its logic is immutable (no upgradability), so the trust boundary is the contract code itself.
7
+ - **DefifaProjectOwner Irrecoverability.** Once the Defifa project NFT is transferred to DefifaProjectOwner, it cannot be recovered. This is intentional but irreversible.
8
+ - **External Dependencies.** Relies on JB721TiersHookStore, JBController, JBMultiTerminal, JBRulesets, and JBPrices. Bugs in any upstream contract affect all Defifa games.
9
+ - **Default Attestation Delegate.** If set, the default attestation delegate receives delegated attestation power for all new minters who do not specify a delegate. This entity accumulates significant governance power.
6
10
 
7
- 1. **DefifaGovernor** -- All games share one `DefifaGovernor` instance (singleton `Ownable` by `DefifaDeployer`). A bug in the governor affects every game simultaneously.
8
- 2. **DefifaDeployer** -- Owns all game JB projects. Controls ruleset queuing for `fulfillCommitmentsOf` and `triggerNoContestFor`. Cannot be upgraded.
9
- 3. **Tier Holders (Attestors)** -- Score outcomes via attestation-weighted governance. 50% quorum of minted tiers' attestation power determines the scorecard.
10
- 4. **Core Protocol** -- Relies on `JBMultiTerminal` for payment processing, `JBTerminalStore` for balance tracking, `JB721TiersHookStore` for NFT tier data, and `JBRulesets` for phase management. Bugs in any of these propagate.
11
- 5. **Immutable Fee Configuration** -- `DEFIFA_FEE_DIVISOR = 20` (5%) and `BASE_PROTOCOL_FEE_DIVISOR = 40` (2.5%) are compile-time constants. Cannot be updated without redeploying.
11
+ ## 2. Economic Risks
12
12
 
13
- ---
13
+ - **Scorecard manipulation via 50% quorum.** A single entity that acquires 50%+ of attestation power across tiers can unilaterally ratify any scorecard, directing the entire pot to chosen tiers. Per-tier cap at `MAX_ATTESTATION_POWER_TIER` limits single-tier dominance. 1-day minimum grace period gives counter-attestors time to respond.
14
+ - **Dynamic quorum from live supply.** Quorum is computed from `currentSupplyOfTier()` at call time, not from a snapshot. Token burns between attestation and ratification decrease quorum. During SCORING phase, burns revert with `NothingToClaim` preventing practical exploitation, but a future code path allowing SCORING burns could re-enable this.
15
+ - **Cash-out weight integer division truncation.** `_weight / _totalTokensForCashoutInTier` rounds down, permanently locking dust in the contract. Maximum loss: 1 wei per tier per game (128 wei max with 128 tiers).
16
+ - **Fee token dilution from reserved mints.** Reserved mints increment `_totalMintCost` by `tier.price * count` even though no ETH was paid. This dilutes paid minters' share of fee tokens (`$DEFIFA` / `$NANA`).
17
+ - **128-tier limit hard-coded.** `_tierCashOutWeights` is a fixed `uint256[128]` array. Games with more than 128 tiers have tiers beyond index 128 unable to receive cash-out weights.
14
18
 
15
- ## Risk Inventory
19
+ ## 3. Governance Risks
16
20
 
17
- ### RISK-1: Whale Tier Dominance via Multi-Tier Accumulation
21
+ - **Single governor instance across all games.** All games share one DefifaGovernor. A bug in `ratifyScorecardFrom`, `attestToScorecardFrom`, or `submitScorecardFor` affects every game simultaneously.
22
+ - **Scorecard timeout can block legitimate ratification.** If `scorecardTimeout` elapses before ratification, the game permanently enters NO_CONTEST. Even a scorecard that has reached quorum cannot be ratified. `triggerNoContestFor()` is permissionless and allows fund recovery.
23
+ - **Delegation locked after MINT phase.** `setTierDelegateTo` only works during MINT phase. After MINT, NFT transfers auto-delegate to the recipient, but holders cannot explicitly re-delegate to a third party.
24
+ - **No-contest requires explicit trigger.** In NO_CONTEST, users cannot immediately cash out -- someone must call `triggerNoContestFor()` to queue a refund ruleset. Without this trigger, the SCORING ruleset allocates the entire balance as payouts, leaving surplus at 0.
18
25
 
19
- **Severity:** MEDIUM
20
- **Status:** KNOWN, ACCEPTED
21
- **Tested:** `DefifaSecurityTest.testQuorum_50pctMintedTiers`
26
+ ## 4. Reentrancy Surface
22
27
 
23
- **Description:** An attacker buys the majority of tokens in 50%+ of tiers, gaining enough attestation power to single-handedly reach quorum and ratify a self-serving scorecard.
28
+ - **afterCashOutRecordedWith.** Burns tokens before external calls. `_claimTokensFor` calls `safeTransfer` on ERC-20 tokens (DEFIFA_TOKEN, BASE_PROTOCOL_TOKEN). Preceding burn and state updates prevent meaningful reentrancy profit.
29
+ - **fulfillCommitmentsOf.** Uses `fulfilledCommitmentsOf[gameId]` as a reentrancy guard (set before `sendPayoutsOf`). Returns early if already non-zero. Uses `max(feeAmount, 1)` to ensure the guard works even when pot rounds to 0. `sendPayoutsOf` is wrapped in try-catch: on failure, resets to sentinel (1) and emits `CommitmentPayoutFailed`, ensuring the final ruleset is always queued.
30
+ - **ratifyScorecardFrom.** Executes arbitrary calldata on the hook via low-level call. The hook's `setTierCashOutWeightsTo` has an `onlyOwner` guard and a `cashOutWeightIsSet` check preventing double-set.
24
31
 
25
- **Mechanism:** Each tier caps attestation power at `MAX_ATTESTATION_POWER_TIER` (1e9, DefifaGovernor line 64). Quorum is 50% of minted tiers' total power (DefifaGovernor `quorum()`, line 203-223). Holding `>50%` of tokens in `>50%` of minted tiers gives the attacker `>25%` of total power per tier, easily exceeding quorum.
32
+ ## 5. DoS Vectors
26
33
 
27
- **Attack scenario:**
28
- 1. Game has 10 tiers, 6 minted. Quorum = `6 * 1e9 / 2 = 3e9`.
29
- 2. Attacker mints majority in 4 tiers. Per tier: `1e9 * (attacker_tokens / tier_total)`.
30
- 3. If attacker holds 80% of each of 4 tiers: `4 * 0.8 * 1e9 = 3.2e9 > 3e9` quorum.
31
- 4. Attacker submits scorecard giving 100% weight to their tiers, attests alone, and ratifies after grace period.
34
+ - **Unbounded tier iteration in governance.** `getAttestationWeight` and `quorum` iterate over all tiers (`maxTierIdOf`). Gas cost grows linearly. Games with many tiers may cause view functions to exceed block gas limits.
35
+ - **_buildSplits iteration.** Iterates over user-provided splits array. No explicit cap, but total percent constraint limits practical count.
32
36
 
33
- **Mitigation:** Per-tier cap ensures dominance in a single high-supply tier is insufficient. Capital cost scales with number of tiers to control. Grace period (minimum 1 day, DefifaGovernor line 300) gives other holders time to counter-attest.
37
+ ## 6. Integration Risks
34
38
 
35
- ---
39
+ - **Immutable phase timing.** Game rulesets are queued at launch and progress automatically based on duration. Once deployed, phase timing cannot be changed.
40
+ - **Permanent cash-out weights.** Cash-out weights are set once via the governor. There is no mechanism to correct a ratified scorecard.
41
+ - **No deployer upgrade.** The deployer contract has no upgrade mechanism. Bugs require deploying a new deployer.
42
+ - **Clone initialization.** Clones use `cloneDeterministic` with `msg.sender` + nonce in the salt. Salt includes `msg.sender`, preventing cross-caller collision. `initialize()` has a re-initialization guard.
36
43
 
37
- ### RISK-2: Dynamic Quorum Based on Live Supply
44
+ ## 7. Invariants to Verify
38
45
 
39
- **Severity:** MEDIUM
40
- **Status:** KNOWN, ACCEPTED
41
- **Tested:** `DefifaSecurityTest.testQuorum_50pctMintedTiers`
42
-
43
- **Description:** Quorum is computed from `currentSupplyOfTier()` (live supply = minted - burned) at call time, not from a snapshot. If tokens are burned between attestation and ratification, quorum can decrease, making it easier to ratify.
44
-
45
- **Mechanism:** `quorum()` (DefifaGovernor line 203-223) calls `IDefifaHook.currentSupplyOfTier()` for each tier. This reads the live minted-minus-burned count from `DefifaHookLib.computeCurrentSupply()` (line 260-271). During SCORING phase, cash-outs burn tokens but revert with `NothingToClaim` (DefifaHook line 675) because weights are not set yet. This effectively prevents burns during SCORING before scorecard ratification.
46
-
47
- **Residual risk:** If a future code path allows burns during SCORING (e.g., via a different hook), quorum could drift downward.
48
-
49
- **Mitigation:** `NothingToClaim` revert during SCORING prevents practical exploitation. After ratification, quorum changes are irrelevant.
50
-
51
- ---
52
-
53
- ### RISK-3: Cash-Out Weight Integer Division Truncation
54
-
55
- **Severity:** LOW
56
- **Status:** KNOWN, BOUNDED
57
- **Tested:** `DefifaSecurityTest.testRounding_extremeWeights`
58
-
59
- **Description:** `computeCashOutWeight()` (DefifaHookLib line 129) divides `_weight / _totalTokensForCashoutInTier` using integer division, permanently locking dust in the contract.
60
-
61
- **Bound:** Maximum loss = 1 wei per tier per game. With 128 maximum tiers, at most 128 wei locked per game.
62
-
63
- **Proof from test:** `testRounding_extremeWeights` allocates weight 1 to tier 1, `TOTAL_CASHOUT_WEIGHT - 2` to tier 2, and weight 1 to tier 3. Verifies fund conservation within 3 wei tolerance.
64
-
65
- ---
66
-
67
- ### RISK-4: Fee Token Dilution from Reserved Mints
68
-
69
- **Severity:** LOW
70
- **Status:** BY DESIGN
71
- **Tested:** `DefifaSecurityTest.testC_D3_reservedMintersGetFeeTokens`
72
-
73
- **Description:** Reserved mints increment `_totalMintCost` by `tier.price * count` (DefifaHook line 568), even though no ETH was actually paid. This dilutes paid minters' share of fee tokens (`$DEFIFA` / `$NANA`).
74
-
75
- **Mechanism:** `_claimTokensFor()` distributes fee tokens proportional to `shareToBeneficiary / outOfTotal` where `outOfTotal = _totalMintCost` (DefifaHook line 670-671). Reserved mints inflate `_totalMintCost` without adding to the hook's fee token balance, reducing each paid minter's proportional claim.
76
-
77
- **Example:** 2 paid mints at 1 ETH + 2 reserved mints at 1 ETH tier price. `_totalMintCost = 4 ETH`. Each token gets 25% of fee tokens. Paid minters effectively subsidize reserved recipients.
78
-
79
- **Mitigation:** By design. The test `testC_D3_reservedMintersGetFeeTokens` verifies this exact behavior: reserved minters receive fee tokens proportional to tier price, paid minters receive proportional to their contribution, and all fee tokens are distributed with nothing left in the hook.
80
-
81
- ---
82
-
83
- ### RISK-5: Scorecard Timeout Can Block Legitimate Ratification
84
-
85
- **Severity:** MEDIUM
86
- **Status:** KNOWN, MITIGATED
87
- **Tested:** `DefifaNoContestTest.testScorecardTimeout_elapsed_noContest`, `testNoContest_scorecardBlocked`
88
-
89
- **Description:** If `scorecardTimeout` is set and elapses before a scorecard is ratified, the game permanently enters `NO_CONTEST`. Even a scorecard that has reached quorum cannot be ratified because `setTierCashOutWeightsTo` checks for SCORING phase (DefifaHook line 708).
90
-
91
- **Mechanism:** `currentGamePhaseOf()` (DefifaDeployer line 258) returns `NO_CONTEST` when `block.timestamp > _currentRuleset.start + _ops.scorecardTimeout`. Once this condition is true, `setTierCashOutWeightsTo` reverts with `DefifaHook_GameIsntScoringYet` because the hook checks `gamePhaseReporter.currentGamePhaseOf(PROJECT_ID) != DefifaGamePhase.SCORING`.
92
-
93
- **Mitigation:**
94
- - Ratified scorecards take priority: `cashOutWeightIsSet` is checked before the timeout (DefifaDeployer line 239), so ratifying before timeout is definitive.
95
- - `scorecardTimeout = 0` disables the mechanism entirely.
96
- - The `triggerNoContestFor()` function allows anyone to queue a refund ruleset, ensuring players can recover funds.
97
-
98
- ---
99
-
100
- ### RISK-6: Delegation Locked After MINT Phase
101
-
102
- **Severity:** MEDIUM
103
- **Status:** BY DESIGN
104
- **Tested:** `DefifaSecurityTest.testM_D6_delegationBlocked`, `DefifaHookRegressions.test_M5_attestationUnitsPreservedOnTransferToUndelegatedRecipient`
105
-
106
- **Description:** `setTierDelegateTo` and `setTierDelegatesTo` only work during MINT phase (DefifaHook lines 730, 740). After MINT, NFT transfers auto-delegate to the recipient (if no delegate set), but holders cannot explicitly re-delegate.
107
-
108
- **Implication:** If a holder transfers an NFT to a new owner during REFUND or SCORING, the new owner auto-delegates to themselves (DefifaHook `_transferTierAttestationUnits`, line 1002-1005). But if the new owner wants to delegate to a third party, they cannot.
109
-
110
- **Mitigation:** By design. Prevents post-MINT governance manipulation. Auto-delegation on transfer (audit finding M-5 fix) ensures attestation units are never lost to `address(0)`.
111
-
112
- ---
113
-
114
- ### RISK-7: Single Governor Instance Across All Games
115
-
116
- **Severity:** MEDIUM
117
- **Status:** KNOWN, ACCEPTED
118
-
119
- **Description:** All games share a single `DefifaGovernor` contract. A bug in `ratifyScorecardFrom`, `attestToScorecardFrom`, or `submitScorecardFor` affects every game.
120
-
121
- **Specific concern:** The governor executes scorecard calldata via low-level call `_metadata.dataHook.call(_calldata)` (DefifaGovernor line 395). If the calldata construction in `_buildScorecardCalldataFor` (line 490-497) has a vulnerability, it could affect all games.
122
-
123
- **Mitigation:** Governor logic is deliberately simple (no upgradability, no complex state transitions). Scorecard calldata is a deterministic ABI encoding of `setTierCashOutWeightsTo.selector` with tier weights.
124
-
125
- ---
126
-
127
- ### RISK-8: Front-Running of Clone Initialization
128
-
129
- **Severity:** LOW
130
- **Status:** MITIGATED
131
- **Tested:** Implicitly by `launchGameWith` tests
132
-
133
- **Description:** `DefifaHook` clones are created via `Clones.cloneDeterministic` with salt `keccak256(abi.encodePacked(msg.sender, _currentNonce))` (DefifaDeployer line 526). This prevents front-running because a different caller produces a different address.
134
-
135
- **Residual risk:** The `initialize()` function has a re-initialization guard (`if (address(store) != address(0)) revert()`, DefifaHook line 487), but between clone creation and initialization (within the same transaction), there is no window for front-running.
136
-
137
- ---
138
-
139
- ### RISK-9: Fulfillment Failure Does Not Block Ratification
140
-
141
- **Severity:** LOW
142
- **Status:** MITIGATED
143
- **Tested:** `FulfillmentBlocksRatification.test_ratificationSucceedsWhenFulfillmentReverts`
144
-
145
- **Description:** `ratifyScorecardFrom` wraps `fulfillCommitmentsOf` in a try-catch (DefifaGovernor lines 402-405). If fulfillment fails (e.g., `sendPayoutsOf` reverts), the scorecard is still ratified and `FulfillmentFailed` event is emitted.
146
-
147
- **Implication:** Fees may not be distributed, but the game proceeds to COMPLETE. `fulfillCommitmentsOf` can be retried separately.
148
-
149
- **Mitigation:** The try-catch is intentional to prevent fulfillment issues from permanently blocking game completion. The `fulfilledCommitmentsOf` guard (DefifaDeployer line 304) allows exactly one successful fulfillment.
150
-
151
- ---
152
-
153
- ### RISK-10: Grace Period Bypass on Early Scorecard Submission
154
-
155
- **Severity:** LOW
156
- **Status:** FIXED (Regression tested)
157
- **Tested:** `GracePeriodBypass.test_gracePeriodExtendsFromAttestationStart`
158
-
159
- **Description:** When a scorecard is submitted before `attestationStartTime`, the grace period could previously expire before attestations even begin. Fixed by anchoring `gracePeriodEnds` to `attestationsBegin` rather than submission time.
160
-
161
- **Implementation:** `_scorecard.gracePeriodEnds = uint48(_attestationsBegin + attestationGracePeriodOf(_gameId))` (DefifaGovernor line 468). The `attestationsBegin` is `max(block.timestamp, attestationStartTime)` (lines 460-463).
162
-
163
- ---
164
-
165
- ### RISK-11: Overweight Scorecard Rejection
166
-
167
- **Severity:** LOW
168
- **Status:** SAFE
169
- **Tested:** `DefifaSecurityTest.testC_D2_rejectsOverweight`
170
-
171
- **Description:** `validateAndBuildWeights` (DefifaHookLib line 85) enforces `_cumulativeCashOutWeight == TOTAL_CASHOUT_WEIGHT`. Any scorecard that does not sum to exactly 1e18 reverts with `DefifaHook_InvalidCashoutWeights`.
172
-
173
- **Additional validations:**
174
- - Tier IDs must be in strict ascending order (line 61): prevents duplicate tier entries.
175
- - Tier must be in category 0 (line 68): prevents weight assignment to non-game tiers.
176
- - Tier must exist (line 71): prevents weight assignment to nonexistent tiers.
177
-
178
- ---
179
-
180
- ### RISK-12: No-Contest Cash-Out Requires Explicit Trigger
181
-
182
- **Severity:** MEDIUM
183
- **Status:** BY DESIGN
184
- **Tested:** `DefifaNoContestTest.testNoContest_cashOutBeforeTrigger_reverts`, `testMinParticipation_cashOutReturnsMintPrice`, `testScorecardTimeout_cashOutReturnsMintPrice`, `testNoContest_allUsersCanRefund`
185
-
186
- **Description:** When a game enters NO_CONTEST, users cannot immediately cash out. They must first call `triggerNoContestFor()` (DefifaDeployer line 585), which queues a new ruleset without payout limits. Without this trigger, the SCORING ruleset has payout limits consuming the entire balance, leaving surplus at 0.
187
-
188
- **Mechanism:** The SCORING ruleset sets `payoutLimits` to `type(uint224).max` (DefifaDeployer line 759), meaning all balance is allocated as payout. Since `ownerMustSendPayouts = true`, no one can send payouts, but the balance is not counted as surplus either. `triggerNoContestFor()` queues a ruleset with no `fundAccessLimitGroups`, making the entire balance available as surplus.
189
-
190
- **Mitigation:** `triggerNoContestFor()` is permissionless -- anyone can call it. The function is idempotent (cannot be called twice: line 592).
191
-
192
- ---
193
-
194
- ### RISK-13: `_totalMintCost` Accounting Integrity
195
-
196
- **Severity:** CRITICAL (if violated)
197
- **Status:** SAFE (proven by invariant tests)
198
- **Tested:** `DefifaMintCostInvariant.invariant_totalMintCostMatchesExpected`, `invariant_totalMintCostEqualsPriceTimesLiveTokens`, `invariant_tokenCountConsistency`
199
-
200
- **Description:** `_totalMintCost` tracks the cumulative price of all live (non-burned) NFTs. It is incremented on mint (DefifaHook line 859) and reserved mint (line 568), and decremented on cash-out (line 678). If this value drifts, fee token distribution (`_claimTokensFor`) will over/under-allocate.
201
-
202
- **Invariant proof:** Stateful fuzz testing (`MintCostHandler`) performs random mints and refunds, verifying after each operation that `_totalMintCost == tierPrice * liveTokenCount` and `_totalMintCost == expectedMintCost` (shadow accounting).
203
-
204
- ---
205
-
206
- ### RISK-14: Fee Accounting After Split Normalization
207
-
208
- **Severity:** MEDIUM
209
- **Status:** SAFE (proven by tests)
210
- **Tested:** `DefifaFeeAccountingTest.testFeeAccounting_defaultSplits`, `testCashOutAfterFees`, `testFeeAccounting_noRoundingLoss`, `testFeeAccounting_withUserSplits`, `testCashOutAfterFees_withUserSplits`, `testSplitNormalization_noRoundingLoss`
211
-
212
- **Description:** Fee splits are normalized in `_buildSplits()` (DefifaDeployer lines 825-894). The NANA split absorbs the rounding remainder (line 883). `_commitmentPercentOf[_gameId]` stores the absolute total, and `fulfillCommitmentsOf` computes `mulDiv(_pot, _commitmentPercentOf[gameId], SPLITS_TOTAL_PERCENT)` (line 326) to determine the fee amount.
213
-
214
- **Proven property:** `fee + surplus == originalPot` (exact equality, tested in `testFeeAccounting_noRoundingLoss`).
215
-
216
- ---
217
-
218
- ### RISK-15: Reentrancy in `afterCashOutRecordedWith`
219
-
220
- **Severity:** LOW
221
- **Status:** SAFE
222
-
223
- **Description:** `afterCashOutRecordedWith` (DefifaHook lines 602-679) burns tokens before making external calls. The burn sequence:
224
- 1. Burns tokens in a loop (line 648)
225
- 2. Calls `_didBurn` to record burns in the store (line 658)
226
- 3. Increments `amountRedeemed` (line 666)
227
- 4. Calls `_claimTokensFor` which transfers fee tokens (line 669-671)
228
-
229
- **Analysis:** Tokens are burned before state updates and before any external token transfers. The JB terminal has already committed the cash-out amount before calling this hook. A reentrant call would fail because the burned tokens no longer exist (ownership check at line 643 would revert).
230
-
231
- ---
232
-
233
- ### RISK-16: Reentrancy in `fulfillCommitmentsOf`
234
-
235
- **Severity:** LOW
236
- **Status:** SAFE
237
-
238
- **Description:** `fulfillCommitmentsOf` (DefifaDeployer lines 302-388) sets `fulfilledCommitmentsOf[gameId]` (line 330) before calling `sendPayoutsOf` (line 334) and `queueRulesetsOf` (line 383). The guard at line 304 (`if (fulfilledCommitmentsOf[gameId] != 0) return`) prevents re-entry.
239
-
240
- **Edge case:** Uses `max(feeAmount, 1)` (line 330) to ensure the guard works even when the pot rounds to 0 fee.
241
-
242
- ---
243
-
244
- ### RISK-17: Attestation Unit Conservation on Transfer
245
-
246
- **Severity:** HIGH (before fix)
247
- **Status:** FIXED
248
- **Tested:** `DefifaHookRegressions.test_M5_attestationUnitsPreservedOnTransferToUndelegatedRecipient`, `test_M5_multipleTransfersToUndelegatedRecipientsPreserveUnits`
249
-
250
- **Description:** Previously, transferring an NFT to a recipient with no delegate set would lose attestation units (sender's delegate lost units but no one gained them). Fixed by auto-delegating undelegated recipients to themselves in `_transferTierAttestationUnits` (DefifaHook lines 1001-1006).
251
-
252
- **Invariant verified:** Sum of all delegate attestation units equals total attestation supply across chains of 3+ sequential transfers.
253
-
254
- ---
255
-
256
- ### RISK-18: Fund Conservation Across Varying Game Parameters
257
-
258
- **Severity:** CRITICAL (if violated)
259
- **Status:** SAFE
260
- **Tested:** `DefifaSecurityTest.testFuzz_fundConservation` (fuzz), `testHighVolume_32tiers`, `testMultiPlayer_winnerTakesAll`, `testRefundIntegrity`
261
-
262
- **Description:** Total cash-outs + remaining surplus must equal the pre-fulfillment pot (minus fees). This is the fundamental economic invariant.
263
-
264
- **Fuzz test parameters:** 2-12 tiers, 1-3 players per tier, 1 ETH tier price. Tolerance: N wei where N = total user count.
265
-
266
- **Proven for edge cases:**
267
- - 32 tiers at 100 ETH each (3,200 ETH pot): dust <= 1e15 wei
268
- - Winner-takes-all (100% to one tier): losers get 0 ETH, winners split evenly within 0.1%
269
- - Extreme weights (1, TOTAL-2, 1): tier 2 gets >99% of pot
270
-
271
- ---
272
-
273
- ### RISK-19: `uint208` Overflow in Attestation Checkpoints
274
-
275
- **Severity:** LOW
276
- **Status:** SAFE (bounded)
277
-
278
- **Description:** Attestation units use OpenZeppelin `Checkpoints.Trace208` which stores values as `uint208`. The `_moveTierDelegateAttestations` function casts amounts to `uint208` (DefifaHook lines 892, 902).
279
-
280
- **Bound:** Maximum attestation units per tier = `tier.votingUnits * tier.initialSupply`. With `initialSupply = 999_999_999` and typical `votingUnits` values, overflow of `uint208` (max ~4.1e62) is practically impossible.
281
-
282
- ---
283
-
284
- ### RISK-20: Stale `block.timestamp` in Via-IR Compiled Tests
285
-
286
- **Severity:** LOW (test-only)
287
- **Status:** MITIGATED
288
-
289
- **Description:** Multiple test files use a `TimestampReader` helper contract to read `block.timestamp` via an external call, bypassing the Solidity via-IR optimizer's timestamp caching. This is a test infrastructure concern, not a production risk.
290
-
291
- ---
292
-
293
- ## Reentrancy Summary
294
-
295
- | Function | Protection | External Calls After State Updates | Risk |
296
- |----------|-----------|-----------------------------------|------|
297
- | `afterCashOutRecordedWith` | Tokens burned before state updates; terminal already committed | `_claimTokensFor` (fee token transfer) | LOW |
298
- | `afterPayRecordedWith` | Payment recorded before minting | None after mint | LOW |
299
- | `fulfillCommitmentsOf` | `fulfilledCommitmentsOf` guard set before `sendPayoutsOf` | `sendPayoutsOf`, `queueRulesetsOf` | LOW |
300
- | `triggerNoContestFor` | `noContestTriggeredFor` set before `queueRulesetsOf` | `queueRulesetsOf` | LOW |
301
- | `ratifyScorecardFrom` | `ratifiedScorecardIdOf` set before low-level call | `dataHook.call`, `fulfillCommitmentsOf` (try-catch) | LOW |
302
-
303
- ---
304
-
305
- ## Test Coverage Map
306
-
307
- | Risk | Test File(s) | Specific Test(s) |
308
- |------|-------------|-----------------|
309
- | RISK-1 Whale dominance | `DefifaSecurity.t.sol` | `testQuorum_50pctMintedTiers` |
310
- | RISK-2 Dynamic quorum | `DefifaSecurity.t.sol` | `testQuorum_50pctMintedTiers`, `testNoCashOut_beforeScorecard` |
311
- | RISK-3 Weight truncation | `DefifaSecurity.t.sol` | `testRounding_extremeWeights` |
312
- | RISK-4 Fee token dilution | `DefifaSecurity.t.sol` | `testC_D3_reservedMintersGetFeeTokens` |
313
- | RISK-5 Scorecard timeout | `DefifaNoContest.t.sol` | `testScorecardTimeout_elapsed_noContest`, `testScorecardTimeout_exactBoundary_scoring`, `testNoContest_scorecardBlocked` |
314
- | RISK-6 Delegation locked | `DefifaSecurity.t.sol` | `testM_D6_delegationBlocked` |
315
- | RISK-7 Single governor | (No isolation test) | Design review only |
316
- | RISK-8 Clone front-running | (Implicit) | All `launchGameWith` tests |
317
- | RISK-9 Fulfillment failure | `regression/FulfillmentBlocksRatification.t.sol` | `test_ratificationSucceedsWhenFulfillmentReverts` |
318
- | RISK-10 Grace period bypass | `regression/GracePeriodBypass.t.sol` | `test_gracePeriodExtendsFromAttestationStart` |
319
- | RISK-11 Overweight scorecard | `DefifaSecurity.t.sol` | `testC_D2_rejectsOverweight` |
320
- | RISK-12 No-contest trigger | `DefifaNoContest.t.sol` | `testNoContest_cashOutBeforeTrigger_reverts`, `testTriggerNoContest_revertsWhenNotNoContest`, `testTriggerNoContest_revertsWhenAlreadyTriggered` |
321
- | RISK-13 `_totalMintCost` | `DefifaMintCostInvariant.t.sol` | `invariant_totalMintCostMatchesExpected`, `invariant_totalMintCostEqualsPriceTimesLiveTokens`, `invariant_tokenCountConsistency` |
322
- | RISK-14 Fee accounting | `DefifaFeeAccounting.t.sol` | All 6 tests |
323
- | RISK-15 Cash-out reentrancy | (Code review) | State ordering analysis |
324
- | RISK-16 Fulfillment reentrancy | (Code review) | Guard analysis |
325
- | RISK-17 Attestation conservation | `DefifaHookRegressions.t.sol` | `test_M5_attestationUnitsPreservedOnTransferToUndelegatedRecipient`, `test_M5_multipleTransfersToUndelegatedRecipientsPreserveUnits` |
326
- | RISK-18 Fund conservation | `DefifaSecurity.t.sol` | `testFuzz_fundConservation`, `testHighVolume_32tiers`, `testMultiPlayer_winnerTakesAll`, `testRefundIntegrity` |
327
- | RISK-19 uint208 overflow | (Bounded analysis) | Arithmetic review |
328
- | RISK-20 Timestamp caching | (Test infrastructure) | `TimestampReader` pattern |
329
-
330
- ### Untested Areas
331
-
332
- | Area | Reason | Risk |
333
- |------|--------|------|
334
- | ERC-20 token games (non-ETH) | All tests use `NATIVE_TOKEN` | LOW -- same code path, `SafeERC20` used |
335
- | Games with >32 tiers | Fuzz tests cap at 12, security tests at 32 | LOW -- 128-element array bounds |
336
- | Custom `tokenUriResolver` interaction | SVG test exists but no adversarial URI resolver | LOW -- resolver is view-only |
337
- | Concurrent multi-game governor state | Tests use single game per governor | MEDIUM -- storage isolation via `gameId` mapping keys |
338
- | `Clones.cloneDeterministic` collision | Deterministic but no collision test | LOW -- `keccak256(sender, nonce)` salt is unique per caller per call |
339
-
340
- ---
341
-
342
- ## Severity Legend
343
-
344
- | Rating | Definition |
345
- |--------|-----------|
346
- | CRITICAL | Fund loss or protocol-breaking if violated; proven safe by invariant tests |
347
- | HIGH | Significant impact on game fairness or fund distribution; fixed via audit |
348
- | MEDIUM | Exploitable under specific conditions; mitigated by design choices or economic constraints |
349
- | LOW | Theoretical risk with bounded impact or test-only concern |
46
+ - `_totalMintCost == tierPrice * liveTokenCount` after every mint and burn.
47
+ - Total cash-outs + remaining surplus == pre-fulfillment pot minus fees.
48
+ - Scorecard weights sum to exactly `TOTAL_CASHOUT_WEIGHT` (1e18).
49
+ - Attestation units are conserved across all transfers (no units lost to `address(0)`).
50
+ - `fulfilledCommitmentsOf[gameId]` is set at most once per game.
51
+ - Per-tier supply never exceeds `initialSupply`.
52
+ - Sum of all delegate attestation units equals total attestation supply.
package/SKILLS.md CHANGED
@@ -110,7 +110,7 @@ During COMPLETE phase cash outs, players also receive proportional $DEFIFA and $
110
110
  - Scorecard attestation weight uses `mulDiv(MAX_ATTESTATION_POWER_TIER, userTierUnits, totalTierUnits)` per tier. If `totalTierUnits` is 0 for a tier (no delegations), that tier contributes no attestation power.
111
111
  - The governor's `quorum` is **dynamic**: it only counts tiers that have at least one minted token. Adding minted tiers changes the quorum retroactively for all active proposals.
112
112
  - `ratifyScorecardFrom` executes the scorecard via a **low-level `.call`** to the hook address. This is necessary because the hook's `setTierCashOutWeightsTo` is `onlyOwner` and the governor is the hook's owner.
113
- - `fulfillCommitmentsOf` uses `max(amount, 1)` as a reentrancy guard. If called when the pot is 0, it stores 1 as the fulfilled amount to prevent re-entry.
113
+ - `fulfillCommitmentsOf` uses `max(amount, 1)` as a reentrancy guard. If called when the pot is 0, it stores 1 as the fulfilled amount to prevent re-entry. `sendPayoutsOf` is wrapped in try-catch: on failure, resets to sentinel (1) and emits `CommitmentPayoutFailed`.
114
114
  - `_buildSplits` normalizes all split percentages relative to the total absolute percent. Rounding remainder is absorbed by the protocol fee split (last in the array).
115
115
  - `_totalMintCost` tracks cumulative mint prices of all live tokens (paid + reserved). It's incremented on pay and reserve mint, decremented on cash out. This is the denominator for fee token ($DEFIFA/$NANA) distribution.
116
116
  - Cash outs during COMPLETE phase revert with `DefifaHook_NothingToClaim` if **both** the reclaimed ETH amount is 0 **and** no fee tokens were transferred. This prevents burning NFTs for nothing.
package/STYLE_GUIDE.md CHANGED
@@ -253,9 +253,12 @@ uint256 public constant MAX_RESERVED_PERCENT = 10_000;
253
253
 
254
254
  ## Function Calls
255
255
 
256
- Use named parameters for readability when calling functions with 3+ arguments:
256
+ Use named arguments for all function calls with 2 or more arguments — in both `src/` and `script/`:
257
257
 
258
258
  ```solidity
259
+ // Good — named arguments
260
+ token.mint({account: beneficiary, amount: count});
261
+ _transferOwnership({newOwner: address(0), projectId: 0});
259
262
  PERMISSIONS.hasPermission({
260
263
  operator: sender,
261
264
  account: account,
@@ -264,8 +267,18 @@ PERMISSIONS.hasPermission({
264
267
  includeRoot: true,
265
268
  includeWildcardProjectId: true
266
269
  });
270
+
271
+ // Bad — positional arguments with 2+ args
272
+ token.mint(beneficiary, count);
273
+ _transferOwnership(address(0), 0);
267
274
  ```
268
275
 
276
+ Single-argument calls use positional style: `_burn(amount)`.
277
+
278
+ This also applies to constructor calls, struct literals, and inherited/library calls (e.g., OZ `_mint`, `_safeMint`, `safeTransfer`, `allowance`, `Clones.cloneDeterministic`).
279
+
280
+ Named argument keys must use **camelCase** — never underscores. If a function's parameter names use underscores, rename them to camelCase first.
281
+
269
282
  ## Multiline Signatures
270
283
 
271
284
  ```solidity