@ballkidz/defifa 0.0.16 → 0.0.18

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.
@@ -1,504 +1,96 @@
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 ~3,990 lines in `src/` (~3,320 in the six main files below).
10
-
11
- ```
12
- DefifaDeployer.sol (937 lines) -- Game factory. Owns all game JB projects. Manages lifecycle rulesets, fee splits, fulfillment, no-contest.
13
- DefifaHook.sol (1097 lines) -- Pay/cashout hook. NFT minting, burning, attestation delegation, fee token distribution, cash-out weight logic.
14
- DefifaGovernor.sol (516 lines) -- Scorecard governance. Submit, attest, ratify scorecards. Singleton across all games.
15
- DefifaHookLib.sol (373 lines) -- Pure/view helpers. Weight validation, cash-out math, attestation computation, token claiming.
16
- DefifaProjectOwner.sol (86 lines) -- Permanent holder of the Defifa project NFT. Grants SET_SPLIT_GROUPS permission.
17
- DefifaTokenUriResolver.sol (315 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()`.
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 beneficiary.
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 `getBWAAttestationWeight()` at `attestationsBegin - 1` timestamp, using pending reserve snapshot from submission time.
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 - 1` 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
- ### Entry Points for Review
269
-
270
- Start with the money: follow ETH from payment to cash-out.
271
-
272
- 1. `DefifaHook._processPayment()` -- where tokens enter
273
- 2. `DefifaHook.beforeCashOutRecordedWith()` -- reclaim calculation
274
- 3. `DefifaHook.afterCashOutRecordedWith()` -- where tokens leave
275
- 4. `DefifaDeployer.fulfillCommitmentsOf()` -- fee distribution
276
- 5. `DefifaGovernor.ratifyScorecardFrom()` -- scorecard execution
277
- 6. `DefifaHookLib.validateAndBuildWeights()` -- weight validation
278
- 7. `DefifaHookLib.computeCashOutWeight()` -- per-token value
279
- 8. `DefifaDeployer._buildSplits()` -- fee normalization
280
- 9. `DefifaDeployer.currentGamePhaseOf()` -- phase state machine
281
- 10. `DefifaDeployer.triggerNoContestFor()` -- no-contest safety valve
282
-
283
- ### P0 -- Critical (Fund Safety)
284
-
285
- 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, NOT during MINT/REFUND refunds.
286
-
287
- 2. **`_totalMintCost` integrity**: This variable is the denominator for fee token distribution. It is incremented on paid mint (`_mintAll`), reserved mint (`mintReservesFor`), and decremented on cash-out (`afterCashOutRecordedWith`). Verify no path exists where `_totalMintCost` underflows or becomes inconsistent with actual live token count.
288
-
289
- 3. **Fulfillment reentrancy guard**: `fulfilledCommitmentsOf[gameId]` is set to `max(feeAmount, 1)` BEFORE external calls to `sendPayoutsOf` and `queueRulesetsOf`. Verify this guard prevents double fulfillment via reentrancy through the terminal.
290
-
291
- 4. **Scorecard execution via low-level call**: `ratifyScorecardFrom` calls `_metadata.dataHook.call(_calldata)`. 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.
292
-
293
- 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.
294
-
295
- ### P1 -- High (Governance Integrity)
296
-
297
- 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.
298
-
299
- 7. **Attestation snapshotting**: Attestation weight is computed at the `attestationsBegin - 1` timestamp via `getPastTierAttestationUnitsOf()`. Pending reserves are snapshotted at submission time (`_pendingReservesSnapshotOf`). Verify that the `Checkpoints.Trace208.upperLookup()` correctly captures the state at that timestamp, that minting or transferring NFTs after `attestationsBegin - 1` does not retroactively affect attestation power, and that minting reserves after submission does not change the snapshotted pending reserve counts.
300
-
301
- 8. **Double attestation prevention**: `_attestations.attestedWeightOf[msg.sender]` prevents double voting. Verify that an attacker cannot attest, transfer NFTs to another address, and have that address attest with the same attestation power (the checkpoint at `attestationsBegin - 1` should prevent this because same-block transfers are invisible at `attestationsBegin - 1`).
302
-
303
- 9. **Grace period anchoring**: `gracePeriodEnds = attestationsBegin + attestationGracePeriod`. Verify that early scorecard submission (before `attestationStartTime`) correctly delays the grace period start, preventing instant ratification.
304
-
305
- ### P2 -- Medium (Access Control and State Transitions)
306
-
307
- 10. **Hook ownership chain**: DefifaDeployer creates the hook clone, calls `initialize()`, then `transferOwnership(GOVERNOR)`. Verify that no window exists between `initialize()` and `transferOwnership()` where an attacker could call `setTierCashOutWeightsTo()` (requires `onlyOwner`).
308
-
309
- 11. **Phase check ordering in `currentGamePhaseOf()`**: The function checks `cashOutWeightIsSet` BEFORE `noContestTriggeredFor`. Verify this ordering is correct: a ratified scorecard should always take priority over no-contest.
310
-
311
- 12. **Clone initialization guard**: `DefifaHook.initialize()` checks `address(this) == CODE_ORIGIN` (prevents initializing the implementation) and `address(store) != address(0)` (prevents re-initialization). Verify these guards are sufficient against proxy/clone attacks.
312
-
313
- 13. **Delegation lockdown**: `setTierDelegateTo` and `setTierDelegatesTo` require `MINT` phase. Verify that auto-delegation on transfer (`_transferTierAttestationUnits`) correctly handles the case where a recipient already has a delegate set.
314
-
315
- ### P3 -- Low (Edge Cases and Rounding)
316
-
317
- 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.
318
-
319
- 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`.
320
-
321
- 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.
322
-
323
- ---
324
-
325
- ## Invariants
326
-
327
- These properties should hold for all games in all states. The test suite validates most of them.
328
-
329
- ### Fund Conservation
330
- - `totalCashOuts + remainingSurplus + fulfilledCommitments == originalPot` (within N wei where N = total user count)
331
- - `amountRedeemed` (DefifaHook) only increases during COMPLETE cash-outs, never during MINT/REFUND/NO_CONTEST
332
- - `fulfilledCommitmentsOf[gameId]` is set exactly once (idempotent guard)
333
-
334
- ### Token Accounting
335
- - `_totalMintCost == sum(tier.price * liveTokenCount[tier])` for all tiers at all times
336
- - `tokensRedeemedFrom[tierId]` only incremented during COMPLETE phase cash-outs
337
- - `_totalMintCost` decremented by exactly `cumulativeMintPrice` on each cash-out
338
-
339
- ### Scorecard Integrity
340
- - `sum(tierWeights[i].cashOutWeight) == TOTAL_CASHOUT_WEIGHT` (exactly 1e18) for any ratified scorecard
341
- - Tier IDs in scorecard are strictly ascending (no duplicates)
342
- - Only tiers in category 0 can receive cash-out weight
343
- - Only tiers with `currentSupply > 0` can receive nonzero weight at submission time
344
-
345
- ### Governance
346
- - Each account can attest to a given scorecard at most once
347
- - Attestation power is checkpointed at `attestationsBegin - 1` (not live); pending reserves snapshotted at submission time
348
- - Quorum threshold: 50% of minted tiers' total max attestation power (live at call time)
349
- - Only one scorecard can be ratified per game
350
- - Minimum grace period: 1 day (enforced in `initializeGame`)
351
-
352
- ### Phase Transitions
353
- - A ratified scorecard (`cashOutWeightIsSet == true`) always produces COMPLETE, regardless of other conditions
354
- - NO_CONTEST is only reachable from SCORING (never from MINT or REFUND)
355
- - `triggerNoContestFor` can be called exactly once per game
356
- - Phase progression is monotonic: COUNTDOWN -> MINT -> REFUND -> SCORING -> COMPLETE/NO_CONTEST
357
-
358
- ### Attestation Units
359
- - Sum of all delegate attestation units for a tier == total tier attestation units (conservation on transfer)
360
- - Auto-delegation on transfer prevents units from being lost to `address(0)`
361
- - Delegation changes only allowed during MINT phase (except auto-delegation on transfer)
362
-
363
- ---
364
-
365
- ## Testing
366
-
367
- ### Test Files (16 files, ~172 test functions)
368
-
369
- | File | Focus |
370
- |------|-------|
371
- | `DefifaGovernor.t.sol` | Core lifecycle: minting, refunding, scoring, cash-out. Fuzz tests on tier counts and distributions. |
372
- | `DefifaSecurity.t.sol` | Fund conservation (fuzz), high-volume 32 tiers, winner-take-all, extreme weights, quorum manipulation, delegation lockdown, reserved minter fee tokens. |
373
- | `DefifaNoContest.t.sol` | Both NO_CONTEST triggers: minParticipation threshold and scorecardTimeout. Trigger/refund/idempotency. |
374
- | `DefifaFeeAccounting.t.sol` | Fee split normalization, rounding loss bounds, cash-out after fees, user splits. |
375
- | `DefifaMintCostInvariant.t.sol` | Stateful fuzz: `_totalMintCost` invariant across random mints and refunds. |
376
- | `DefifaHookRegressions.t.sol` | Audit finding M-5: attestation unit conservation on transfer to undelegated recipients. |
377
- | `DefifaAuditLowGuards.t.sol` | Input validation: double initialization, uint48 overflow, zero-address delegation. |
378
- | `Fork.t.sol` | Mainnet fork tests: full lifecycle, edge cases, all revert conditions, scorecard state machine. 69 tests. |
379
- | `regression/FulfillmentBlocksRatification.t.sol` | Fulfillment failure does not block ratification (try-catch behavior). |
380
- | `regression/GracePeriodBypass.t.sol` | Grace period extends from attestation start, not submission time. |
381
- | `DefifaAdversarialQuorum.t.sol` | Adversarial governance: late-buyer attestation power, delegation lockdown, double attestation, quorum manipulation, competing scorecards. 9 tests. |
382
- | `TestQALastMile.t.sol` | Edge cases: cash-out DoS during fulfillment window, game ID prediction race condition. 2 tests. |
383
- | `deployScript.t.sol` | Deploy script smoke test. |
384
- | `DefifaUSDC.t.sol` | ERC-20 (USDC) game variant. |
385
- | `SVG.t.sol` | Token URI resolver SVG rendering. |
386
- | `TestAuditGaps.sol` | Audit gap coverage: ERC-20 game mechanics (mint, refund, scoring, fee fulfillment, cash-out distribution, no-contest with ERC-20 tokens, pot reporting), multi-game governor isolation (independent project IDs, balances, NFT hooks, scorecard submission/attestation/ratification isolation, quorum independence, fulfilled commitments independence). 17 tests across 2 test contracts. |
387
-
388
- ### Running Tests
389
-
390
- ```bash
391
- forge test --match-path "test/*.t.sol" -vvv
392
- forge test --match-path "test/regression/*.t.sol" -vvv
393
- ```
394
-
395
- For invariant tests:
396
- ```bash
397
- forge test --match-contract DefifaMintCostInvariant -vvv
398
- ```
399
-
400
- ### Known Test Gaps
401
-
402
- | Area | Current Coverage | Risk |
403
- |------|-----------------|------|
404
- | ERC-20 token games (non-ETH) | Expanded: USDC test file + 8 ERC-20 tests in TestAuditGaps.sol (mint, refund, scoring, fee accounting, even distribution, no-contest, pot calculation) | LOW |
405
- | Games with >32 tiers | Fuzz caps at 12, one test at 32 | LOW |
406
- | Concurrent multi-game governor | Expanded: 9 multi-game isolation tests in TestAuditGaps.sol (independent IDs, balances, hooks, scorecard isolation, attestation power isolation, quorum, fulfilled commitments, full lifecycle) | LOW |
407
- | Adversarial token URI resolver | No malicious resolver test | LOW |
408
- | Clone address collision | No explicit collision test | LOW |
409
-
410
- ---
411
-
412
- ## Constants Reference
413
-
414
- | Constant | Value | Location |
415
- |----------|-------|---------|
416
- | `TOTAL_CASHOUT_WEIGHT` | 1e18 | `DefifaHookLib` |
417
- | `MAX_ATTESTATION_POWER_TIER` | 1e9 | `DefifaGovernor` |
418
- | `DEFIFA_FEE_DIVISOR` | 20 (5%) | `DefifaDeployer` |
419
- | `BASE_PROTOCOL_FEE_DIVISOR` | 40 (2.5%) | `DefifaDeployer` |
420
- | `SPLITS_TOTAL_PERCENT` | 1e9 | `JBConstants` |
421
- | `initialSupply` per tier | 999,999,999 | `DefifaDeployer` |
422
- | Max tiers per game | 128 | `DefifaHook` (`uint256[128]`) |
423
- | Min grace period | 1 day | `DefifaGovernor` |
424
- | Compiler | Solidity 0.8.28 | All files |
425
-
426
- ---
427
-
428
- ## Anti-Patterns to Hunt
429
-
430
- | Pattern | Where | Why Dangerous |
431
- |---------|-------|---------------|
432
- | Low-level `.call()` with arbitrary calldata | `DefifaGovernor.ratifyScorecardFrom()` | Executes `_metadata.dataHook.call(_calldata)` -- if hash-based proposal verification is flawed, arbitrary calldata could be executed on the hook |
433
- | `unchecked` block around state mutation | `DefifaHook.afterCashOutRecordedWith()` | `++tokensRedeemedFrom[tierId]` in `unchecked` -- overflow of this counter would corrupt cash-out weight calculations for the tier |
434
- | Optimistic project ID prediction | `DefifaDeployer.launchGameWith()` | `gameId = PROJECTS.count() + 1` -- race condition with concurrent project creation. Mitigated by post-launch equality check, but `_opsOf[gameId]` is written before the check |
435
- | No reentrancy guard (no `ReentrancyGuard`) | All contracts | Relies on state ordering (storage writes before external calls) instead of explicit reentrancy locks. Any future refactor that reorders could introduce reentrancy |
436
- | `minTokensPaidOut: 0` on `sendPayoutsOf` | `DefifaDeployer.fulfillCommitmentsOf()` | Zero slippage protection -- MEV sandwich could extract value from the payout if the terminal swaps tokens |
437
- | Casting `address` to `uint32` for currency | `DefifaDeployer.fulfillCommitmentsOf()` | `uint32(uint160(_token))` truncates the address to 32 bits. Must match terminal's accounting context exactly or payout fails |
438
- | Clone initialization window | `DefifaDeployer.launchGameWith()` | Hook is initialized before `transferOwnership(GOVERNOR)`. Between `initialize()` (owner = deployer) and `transferOwnership()`, the hook's `onlyOwner` functions are callable by the deployer. Mitigated by atomic transaction |
439
- | `delegatecall` from library | `DefifaHookLib.claimTokensFor()` | Executes via `delegatecall` to the library -- `address(this)` is the hook's address, `safeTransfer` sends from the hook's balance. Incorrect library linkage could drain the hook |
440
- | Live supply in quorum calculation | `DefifaGovernor.quorum()` | Uses `currentSupplyOfTier()` (live, not snapshotted). If burns become possible during SCORING, quorum could be manipulated downward after attestations begin |
441
- | Integer division dust accumulation | `DefifaHookLib.computeCashOutWeight()` | `_weight / _totalTokensForCashoutInTier` rounds down. Dust (up to 1 wei per tier) is permanently locked. 128 tiers = max 128 wei locked per game |
442
- | `_totalMintCost` as fee token denominator | `DefifaHook._totalMintCost` internal variable | Incremented on mint, decremented on cash-out. If any path allows underflow (e.g., reserved mint followed by full refund), fee token claims revert or distribute incorrectly |
443
- | Try-catch swallowing failures silently | `DefifaDeployer.fulfillCommitmentsOf()` | `sendPayoutsOf` failure is caught and fee stays in pot. The sentinel value (1 wei) subtracted from pot in `currentGamePotOf` is an accounting approximation |
444
- | External call in view function | `DefifaTokenUriResolver.tokenUriOf()` | Calls `gamePotReporter.currentGamePotOf()`, `hook.cashOutWeightOf()`, and `hook.store().totalSupplyOf()`. A malicious URI resolver could cause excessive gas consumption in off-chain reads |
445
- | `block.timestamp` as scorecard ID component | `DefifaGovernor.submitScorecardFor()` | `attestationsBegin = uint48(block.timestamp + ...)` -- miner manipulation of timestamp (within 15s drift) could affect grace period boundaries |
446
-
447
- ---
448
-
449
- ## How to Report Findings
450
-
451
- ### Finding Format
452
-
453
- Each finding should use this 7-point structure:
454
-
455
- 1. **Title** -- One-line summary (e.g., "Quorum manipulation via token burns during SCORING phase")
456
- 2. **Affected Contract(s)** -- List the specific contract(s) and function(s) involved
457
- 3. **Description** -- Clear explanation of the vulnerability and its root cause
458
- 4. **Trigger Sequence** -- Step-by-step reproduction instructions:
459
- - Step 1: Deploy game with X configuration...
460
- - Step 2: Attacker calls Y with Z parameters...
461
- - Step 3: Observe unexpected state change...
462
- 5. **Impact** -- Concrete consequences: funds at risk (in ETH/USD), governance bypass capability, denial-of-service scope, affected user count
463
- 6. **Proof** -- Code snippet showing the vulnerable path, or a Foundry PoC test (`forge test --match-test testExploitName -vvvv`)
464
- 7. **Fix** -- Suggested remediation with specific code changes
465
-
466
- ### Severity Guide
467
-
468
- | Severity | Criteria | Examples |
469
- |----------|----------|---------|
470
- | **CRITICAL** | Direct, unconditional fund loss or theft. Exploitable by anyone without special permissions. | Draining the game pot, bypassing cash-out weight validation, minting unlimited tokens |
471
- | **HIGH** | Conditional fund loss requiring specific timing or state, or authorization/access-control bypass. | Scorecard ratification without quorum, fulfillment reentrancy, phase transition manipulation |
472
- | **MEDIUM** | State inconsistency, griefing, or economic damage bounded by dust amounts or requiring unlikely conditions. | Quorum drift from live supply, fee token dilution from reserved mints, rounding errors above documented bounds |
473
- | **LOW** | Cosmetic issues, gas inefficiencies, informational observations, or theoretical attacks with no practical exploit path. | Token URI gas consumption, unused return values, documentation inaccuracies |
474
-
475
- ---
476
-
477
- ## Previous Audit Findings
478
-
479
- No prior formal audit with finding IDs has been conducted for defifa-collection-deployer-v6. Known risks, trust assumptions, and economic edge cases are documented in [RISKS.md](./RISKS.md). The test suite (16 files, ~172 test functions) includes regression tests for specific issues discovered during development:
480
-
481
- - `DefifaHookRegressions.t.sol` -- Attestation unit conservation on transfer to undelegated recipients (M-5 equivalent)
482
- - `regression/AttestationDelegateBeneficiary.t.sol` -- Default attestation delegate is beneficiary, not payer (H-6)
483
- - `regression/FulfillmentBlocksRatification.t.sol` -- Fulfillment failure does not block ratification
484
- - `regression/GracePeriodBypass.t.sol` -- Grace period extends from attestation start, not submission time
485
-
486
- ---
487
-
488
- ## Compiler and Version Info
489
-
490
- | Setting | Value | Source |
491
- |---------|-------|--------|
492
- | Solidity version | `0.8.28` | `foundry.toml` `solc` field; all `src/*.sol` files use `pragma solidity ^0.8.26` (library uses `0.8.28`) |
493
- | EVM target | `cancun` | `foundry.toml` `evm_version` field |
494
- | Optimizer | Enabled, 200 runs | `foundry.toml` `optimizer_runs = 200` |
495
- | Via IR | `true` | `foundry.toml` `via_ir = true` -- uses the Yul-based compilation pipeline |
496
- | Fuzz runs | 4096 | `foundry.toml` `[fuzz]` section |
497
- | Invariant runs | 1024, depth 100 | `foundry.toml` `[invariant]` section |
498
- | Invariant fail-on-revert | `false` | `foundry.toml` -- reverts do not fail invariant tests |
499
- | Framework | Foundry (forge) | Standard Foundry project layout |
500
-
501
- **Notes for auditors:**
502
- - `via_ir = true` enables the Yul intermediate representation pipeline, which can produce different optimization artifacts than the legacy pipeline. Stack-too-deep workarounds may mask complexity.
503
- - `optimizer_runs = 200` balances deployment cost vs. runtime gas. Low run counts favor deployment cost, which may produce less-optimized runtime bytecode.
504
- - `evm_version = cancun` enables Cancun opcodes (TSTORE/TLOAD, MCOPY, etc.). Verify the target deployment chain supports Cancun.
1
+ # Audit Instructions
2
+
3
+ Defifa is a staged prediction-game system built on Juicebox and the 721 hook stack. The main risks are governance correctness, treasury settlement, and cash-out weight integrity.
4
+
5
+ ## Objective
6
+
7
+ Find issues that:
8
+ - let players extract more than their fair share of the game pot
9
+ - break the game-phase lifecycle or let actions occur in the wrong phase
10
+ - corrupt scorecard submission, attestation, quorum, grace-period, or ratification logic
11
+ - miscompute tier cash-out weights or fee-token distribution
12
+ - let deployment or ownership helpers leave a game misconfigured
13
+
14
+ ## Scope
15
+
16
+ In scope:
17
+ - `src/DefifaDeployer.sol`
18
+ - `src/DefifaGovernor.sol`
19
+ - `src/DefifaHook.sol`
20
+ - `src/DefifaProjectOwner.sol`
21
+ - `src/DefifaTokenUriResolver.sol`
22
+ - `src/libraries/DefifaHookLib.sol`
23
+ - `src/interfaces/`, `src/structs/`, and `src/enums/`
24
+ - deployment scripts in `script/`
25
+
26
+ Key integrations:
27
+ - `nana-core-v6`
28
+ - `nana-721-hook-v6`
29
+ - deployer and ownership helper patterns shared across the ecosystem
30
+
31
+ ## System Model
32
+
33
+ High-level lifecycle:
34
+ - deploy a game as a Juicebox project
35
+ - sell outcome NFTs during mint phase
36
+ - optionally allow refunds or no-contest handling
37
+ - run scorecard governance through submissions and attestations
38
+ - ratify a scorecard
39
+ - update cash-out weights so winning pieces can redeem the treasury
40
+
41
+ The contracts split responsibility as follows:
42
+ - `DefifaDeployer`: project launch and lifecycle orchestration
43
+ - `DefifaHook`: minting, burning, and game-specific cash-out accounting
44
+ - `DefifaGovernor`: scorecard governance and ratification
45
+ - `DefifaTokenUriResolver`: game NFT metadata
46
+
47
+ ## Critical Invariants
48
+
49
+ 1. Pot conservation
50
+ Total redeemable value across all settled tiers must not exceed the game treasury after applying intended fees.
51
+
52
+ 2. Governance phase safety
53
+ Submission, attestation, ratification, no-contest, and refund paths must be reachable only in the intended lifecycle windows.
54
+
55
+ 3. Quorum and grace-period correctness
56
+ Attestation power, delegation, quorum thresholds, and grace-period timing must not be manipulable to ratify an invalid scorecard early or indefinitely block a valid one.
57
+
58
+ 4. Settlement determinism
59
+ Once the winning scorecard is finalized, the resulting cash-out weights must match the intended outcome and remain internally consistent.
60
+
61
+ 5. No fee-token dilution bugs
62
+ Fee accounting and reserve-related side effects must not dilute players or over-credit non-paying participants.
63
+
64
+ ## Threat Model
65
+
66
+ Prioritize:
67
+ - whale participants trying to dominate attestation power
68
+ - players exploiting pending reserves, delegation, or snapshot timing
69
+ - callers trying to ratify with partially completed commitments
70
+ - phase-boundary and timestamp races
71
+ - settlement paths that assume external payout success
72
+
73
+ ## Hotspots
74
+
75
+ - `DefifaGovernor` scorecard state transitions
76
+ - `DefifaHook` cash-out weight and fee-token accounting
77
+ - reserve-related denominators during governance and settlement
78
+ - deployer logic that queues or fulfills lifecycle rulesets
79
+ - any low-level call used during ratification or fulfillment
80
+ - token URI or metadata logic only insofar as it can desync governance or settlement assumptions
81
+
82
+ ## Build And Verification
83
+
84
+ Standard workflow:
85
+ - `npm install`
86
+ - `forge build`
87
+ - `forge test`
88
+
89
+ The current tests focus on:
90
+ - quorum hardening
91
+ - no-contest and refund handling
92
+ - pending reserve dilution
93
+ - fee accounting
94
+ - regressions around attestation delegation and grace-period bypass
95
+
96
+ The best findings in this repo usually demonstrate either treasury over-redemption or a governance state transition that should be impossible.
package/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ ## Scope
4
+
5
+ This repo was not part of the deployed v5 ecosystem that the top-level changelog measures, so it is excluded from the ecosystem delta.
6
+
7
+ This file instead describes the current v6 repo at a high level and the broad migration direction from the older `defifa-v5` codebase.
8
+
9
+ ## Current v6 surface
10
+
11
+ - `DefifaDeployer`
12
+ - `DefifaHook`
13
+ - `DefifaGovernor`
14
+ - `DefifaProjectOwner`
15
+ - `DefifaTokenUriResolver`
16
+
17
+ ## Summary
18
+
19
+ - The repo is now built directly on the v6 Juicebox stack, including the v6 core and 721-hook packages.
20
+ - The v6 surface is split across dedicated deployer, hook, governor, project-owner, and token-uri contracts, with dedicated regression and audit test coverage around governance, fee accounting, attestations, and lifecycle edge cases.
21
+ - Solidity and tooling were upgraded to the v6 baseline around `0.8.28`.
22
+
23
+ ## Migration notes
24
+
25
+ - Do not treat this repo as part of the deployed v5-to-v6 ecosystem delta.
26
+ - If you need a Defifa-specific migration, rebuild from the current v6 ABIs and current contract set instead of relying on the ecosystem summary.