@ballkidz/defifa 0.0.10 → 0.0.12

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 (39) hide show
  1. package/ADMINISTRATION.md +26 -15
  2. package/ARCHITECTURE.md +35 -3
  3. package/AUDIT_INSTRUCTIONS.md +127 -45
  4. package/CHANGE_LOG.md +107 -0
  5. package/CRYPTO_ECON.md +2 -2
  6. package/README.md +120 -2
  7. package/RISKS.md +21 -4
  8. package/SKILLS.md +174 -59
  9. package/STYLE_GUIDE.md +2 -2
  10. package/USER_JOURNEYS.md +482 -139
  11. package/foundry.toml +1 -1
  12. package/package.json +7 -7
  13. package/script/Deploy.s.sol +1 -1
  14. package/script/helpers/DefifaDeploymentLib.sol +2 -2
  15. package/src/DefifaDeployer.sol +2 -2
  16. package/src/DefifaGovernor.sol +1 -1
  17. package/src/DefifaHook.sol +7 -6
  18. package/src/DefifaProjectOwner.sol +1 -1
  19. package/src/DefifaTokenUriResolver.sol +1 -1
  20. package/src/libraries/DefifaFontImporter.sol +1 -1
  21. package/src/libraries/DefifaHookLib.sol +1 -1
  22. package/test/DefifaAdversarialQuorum.t.sol +1 -1
  23. package/test/DefifaAuditLowGuards.t.sol +1 -1
  24. package/test/DefifaFeeAccounting.t.sol +1 -1
  25. package/test/DefifaGovernor.t.sol +1 -1
  26. package/test/DefifaHookRegressions.t.sol +39 -1
  27. package/test/DefifaMintCostInvariant.t.sol +1 -1
  28. package/test/DefifaNoContest.t.sol +1 -1
  29. package/test/DefifaSecurity.t.sol +1 -1
  30. package/test/DefifaUSDC.t.sol +1 -1
  31. package/test/Fork.t.sol +1 -1
  32. package/test/SVG.t.sol +1 -1
  33. package/test/TestAuditGaps.sol +1 -1
  34. package/test/TestQALastMile.t.sol +1 -1
  35. package/test/audit/CodexAttestationDoubleCount.t.sol +217 -0
  36. package/test/deployScript.t.sol +1 -1
  37. package/test/regression/AttestationDelegateBeneficiary.t.sol +272 -0
  38. package/test/regression/FulfillmentBlocksRatification.t.sol +1 -1
  39. package/test/regression/GracePeriodBypass.t.sol +1 -1
package/ADMINISTRATION.md CHANGED
@@ -9,9 +9,9 @@ Admin privileges and their scope in defifa-collection-deployer-v6.
9
9
  | **Game Creator** | Any EOA or contract that calls `DefifaDeployer.launchGameWith()` | Self-selected; permissionless |
10
10
  | **DefifaDeployer** (contract) | Singleton deployed at protocol setup | Immutable; owns all game JB projects and the governor |
11
11
  | **DefifaGovernor** (contract) | Singleton; `Ownable` by the DefifaDeployer | Ownership transferred from deployer at construction |
12
- | **DefifaHook** (per-game clone) | One clone per game; `Ownable` by the DefifaGovernor | Ownership transferred from deployer during `launchGameWith()` (line 567) |
12
+ | **DefifaHook** (per-game clone) | One clone per game; `Ownable` by the DefifaGovernor | Ownership transferred from deployer during `launchGameWith()` |
13
13
  | **DefifaProjectOwner** | Optional proxy contract that holds the Defifa fee project NFT | Receives project NFT; grants `SET_SPLIT_GROUPS` to deployer |
14
- | **Scorecard Submitter** | Any address during SCORING phase | Permissionless (`submitScorecardFor`, DefifaGovernor line 413) |
14
+ | **Scorecard Submitter** | Any address during SCORING phase | Permissionless (`submitScorecardFor`) |
15
15
  | **Attestor** | Any NFT holder with attestation weight | Must hold game NFTs; weight proportional to holdings per tier |
16
16
  | **Default Attestation Delegate** | Address set at game launch | Set via `DefifaLaunchProjectData.defaultAttestationDelegate` |
17
17
  | **Tier Delegate** | Any address delegated attestation units by an NFT holder | Set via `setTierDelegateTo` / `setTierDelegatesTo` during MINT phase only |
@@ -30,7 +30,7 @@ Admin privileges and their scope in defifa-collection-deployer-v6.
30
30
 
31
31
  | Function | Required Role | Permission Check | What It Does |
32
32
  |----------|--------------|-----------------|-------------|
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()`. |
33
+ | `initializeGame()` | DefifaDeployer (owner) | `onlyOwner` | 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
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`. |
@@ -39,20 +39,20 @@ Admin privileges and their scope in defifa-collection-deployer-v6.
39
39
 
40
40
  | Function | Required Role | Permission Check | What It Does |
41
41
  |----------|--------------|-----------------|-------------|
42
- | `initialize()` | DefifaDeployer | Reverts if `address(this) == codeOrigin` (line 484) or already initialized (`store != address(0)`, line 487) | One-time initialization of the cloned hook with game configuration, tiers, and tier names. Transfers ownership to caller. |
43
- | `setTierCashOutWeightsTo()` | DefifaGovernor (owner) | `onlyOwner` (line 703); must be in SCORING phase; weights not already set | Sets the cash-out weight distribution across tiers. Validates weights sum to `TOTAL_CASHOUT_WEIGHT` (1e18). Irreversible -- once set, `cashOutWeightIsSet` is permanently true. |
44
- | `setTierDelegateTo()` | Any NFT holder | Must be in MINT phase (line 730-732) | Delegates attestation units for a specific tier to another address. |
45
- | `setTierDelegatesTo()` | Any NFT holder | Must be in MINT phase (line 740-742); delegatee cannot be address(0) | Batch delegation of attestation units across multiple tiers. |
46
- | `mintReservesFor()` | Anyone | Reverts if `pauseMintPendingReserves` is set in ruleset metadata (line 537-539) | Mints reserved tokens to the tier's reserve beneficiary. Increments `_totalMintCost` so reserved recipients can claim fee tokens. |
47
- | `afterPayRecordedWith()` | JB Terminal | Caller must be a terminal of the project (line 436-439); `msg.value` must be 0 | Processes payment: validates currency, mints NFTs, sets up attestation delegation. |
48
- | `afterCashOutRecordedWith()` | JB Terminal | Caller must be a terminal of the project (line 610-613); `msg.value` must be 0 | Burns NFTs on cash-out, tracks redeemed amounts, distributes fee tokens during COMPLETE phase. |
42
+ | `initialize()` | DefifaDeployer | Reverts if `address(this) == codeOrigin` or already initialized (`store != address(0)`) | One-time initialization of the cloned hook with game configuration, tiers, and tier names. Transfers ownership to caller. |
43
+ | `setTierCashOutWeightsTo()` | DefifaGovernor (owner) | `onlyOwner`; must be in SCORING phase; weights not already set | Sets the cash-out weight distribution across tiers. Validates weights sum to `TOTAL_CASHOUT_WEIGHT` (1e18). Irreversible -- once set, `cashOutWeightIsSet` is permanently true. |
44
+ | `setTierDelegateTo()` | Any NFT holder | Must be in MINT phase | Delegates attestation units for a specific tier to another address. |
45
+ | `setTierDelegatesTo()` | Any NFT holder | Must be in MINT phase; delegatee cannot be address(0) | Batch delegation of attestation units across multiple tiers. |
46
+ | `mintReservesFor()` | Anyone | Reverts if `pauseMintPendingReserves` is set in ruleset metadata | Mints reserved tokens to the tier's reserve beneficiary. Increments `_totalMintCost` so reserved recipients can claim fee tokens. |
47
+ | `afterPayRecordedWith()` | JB Terminal | Caller must be a terminal of the project; `msg.value` must be 0 | Processes payment: validates currency, mints NFTs, sets up attestation delegation. |
48
+ | `afterCashOutRecordedWith()` | JB Terminal | Caller must be a terminal of the project; `msg.value` must be 0 | Burns NFTs on cash-out, tracks redeemed amounts, distributes fee tokens during COMPLETE phase. |
49
49
  | `transferOwnership()` | Current owner | `onlyOwner` (inherited from Ownable) | Transfers hook ownership. Used once during deployment to transfer from deployer to governor. |
50
50
 
51
51
  ### DefifaProjectOwner
52
52
 
53
53
  | Function | Required Role | Permission Check | What It Does |
54
54
  |----------|--------------|-----------------|-------------|
55
- | `onERC721Received()` | JBProjects contract | `msg.sender` must be the JBProjects contract (line 47) | When the Defifa fee project NFT is transferred here, auto-grants `SET_SPLIT_GROUPS` permission to the DefifaDeployer. The project NFT is permanently locked -- cannot be recovered. |
55
+ | `onERC721Received()` | JBProjects contract | `msg.sender` must be the JBProjects contract | When the Defifa fee project NFT is transferred here, auto-grants `SET_SPLIT_GROUPS` permission to the DefifaDeployer. The project NFT is permanently locked -- cannot be recovered. |
56
56
 
57
57
  ## Game Lifecycle Administration
58
58
 
@@ -77,6 +77,17 @@ COUNTDOWN --> MINT --> REFUND (optional) --> SCORING --> COMPLETE or NO_CONTEST
77
77
  4. The governor calls `setTierCashOutWeightsTo` on the hook via low-level call
78
78
  5. `fulfillCommitmentsOf` sends fee payouts (try-catch) and queues the final ruleset
79
79
 
80
+ ### Attestation Quorum Details
81
+
82
+ The quorum threshold is 50% of the total attestation power across all tiers with nonzero mint supply. Attestation power per tier is proportional to the tier's minted supply at the `attestationsBegin` snapshot timestamp.
83
+
84
+ **Edge cases:**
85
+ - **Tiers with zero mints:** Tiers with `currentSupplyOfTier(tierId) == 0` are excluded from the quorum calculation. They have no attestation power and cannot influence scoring.
86
+ - **All mints in a single tier:** If all participation concentrates in one tier, that tier's holders control the quorum. The 50% threshold still applies -- holders of 50% of that tier's supply can ratify a scorecard.
87
+ - **Grace period:** After a scorecard reaches quorum (SUCCEEDED state), a grace period (`attestationsGracePeriod`, minimum 1 day) must elapse before ratification. This gives dissenters time to attest to a competing scorecard that could overtake the first.
88
+ - **Competing scorecards:** Multiple scorecards can be submitted. Each tracks attestations independently. Only the first to be ratified (quorum met + grace period elapsed + `ratifyScorecardFrom()` called) takes effect. Once ratified, no other scorecard can be ratified for the same game.
89
+ - **Scorecard timeout:** If `scorecardTimeout` is nonzero and elapses without ratification, the game enters NO_CONTEST state, enabling full refunds via `triggerNoContestFor()`.
90
+
80
91
  **No single entity controls scoring.** The process requires collective attestation from NFT holders across tiers.
81
92
 
82
93
  ## Immutable Configuration
@@ -91,7 +102,7 @@ The following are set at game creation and cannot be changed:
91
102
  | Payment token | `launchGameWith()` | Single token per game |
92
103
  | Fee structure | Constructor constants | `DEFIFA_FEE_DIVISOR = 20` (5%), `BASE_PROTOCOL_FEE_DIVISOR = 40` (2.5%) |
93
104
  | Attestation start time | `launchGameWith()` | Stored in governor via `initializeGame()` |
94
- | Attestation grace period | `launchGameWith()` | Minimum 1 day enforced (governor line 300) |
105
+ | Attestation grace period | `launchGameWith()` | Minimum 1 day enforced in `initializeGame()` |
95
106
  | Default attestation delegate | `launchGameWith()` | Stored in hook |
96
107
  | `minParticipation` threshold | `launchGameWith()` | 0 = disabled |
97
108
  | `scorecardTimeout` | `launchGameWith()` | 0 = disabled |
@@ -116,11 +127,11 @@ What admins CANNOT do:
116
127
 
117
128
  6. **No one can change delegates after MINT phase.** `setTierDelegateTo` and `setTierDelegatesTo` both revert with `DefifaHook_DelegateChangesUnavailableInThisPhase` outside MINT.
118
129
 
119
- 7. **No one can re-set cash-out weights.** The `cashOutWeightIsSet` flag is checked before setting (line 713) and the function reverts with `DefifaHook_CashoutWeightsAlreadySet`.
130
+ 7. **No one can re-set cash-out weights.** The `cashOutWeightIsSet` flag is checked before setting and the function reverts with `DefifaHook_CashoutWeightsAlreadySet`.
120
131
 
121
- 8. **No one can re-ratify a scorecard.** The `ratifiedScorecardIdOf[gameId] != 0` check prevents double ratification (governor line 374).
132
+ 8. **No one can re-ratify a scorecard.** The `ratifiedScorecardIdOf[gameId] != 0` check prevents double ratification.
122
133
 
123
- 9. **No one can fulfill commitments twice.** The `fulfilledCommitmentsOf[gameId] != 0` check (deployer line 304) prevents re-entry.
134
+ 9. **No one can fulfill commitments twice.** The `fulfilledCommitmentsOf[gameId] != 0` check prevents re-entry.
124
135
 
125
136
  10. **No one can recover the project NFT from DefifaProjectOwner.** Once transferred, the NFT is permanently locked.
126
137
 
package/ARCHITECTURE.md CHANGED
@@ -14,10 +14,11 @@ src/
14
14
  ├── DefifaProjectOwner.sol — Proxy owner for Defifa projects
15
15
  ├── DefifaTokenUriResolver.sol — On-chain SVG metadata for game NFTs
16
16
  ├── enums/
17
- │ ├── DefifaGamePhase.sol — MINT → REFUND → SCORING → COMPLETE
17
+ │ ├── DefifaGamePhase.sol — COUNTDOWN → MINT → REFUND → SCORING → COMPLETE → NO_CONTEST
18
18
  │ └── DefifaScorecardState.sol
19
19
  ├── interfaces/ — IDefifaDeployer, IDefifaHook, IDefifaGovernor, etc.
20
20
  ├── libraries/
21
+ │ ├── DefifaFontImporter.sol — Font loading for on-chain SVG rendering
21
22
  │ └── DefifaHookLib.sol — Game logic helpers
22
23
  └── structs/ — Scorecards, attestations, tier params, delegations
23
24
  ```
@@ -43,8 +44,8 @@ SCORING Phase:
43
44
  → DefifaHook receives final cash-out weights per tier
44
45
 
45
46
  COMPLETE Phase:
46
- Winnerscash out NFTs at scored weights
47
- DeployerfulfillCommitmentsOf() distributes fee tokens
47
+ DeployerfulfillCommitmentsOf() sends fee payouts and queues the final ruleset
48
+ Winnerscash out NFTs at scored weights (see "Scored Weight Redemption" below)
48
49
  ```
49
50
 
50
51
  ### Governance Flow
@@ -60,6 +61,25 @@ Attestor → DefifaGovernor.attestToScorecard(proposalId)
60
61
  → DefifaHook.setScorecard() called
61
62
  ```
62
63
 
64
+ ### Scored Weight Redemption
65
+
66
+ When a scorecard is ratified, `setTierCashOutWeightsTo` stores a weight per tier. Weights must sum to `TOTAL_CASHOUT_WEIGHT` (1e18). A tier with weight 0 means that outcome lost and holders get nothing.
67
+
68
+ When a holder cashes out an NFT during the COMPLETE phase:
69
+
70
+ 1. **Per-token weight**: The tier's weight is divided equally among all tokens minted in that tier: `tokenWeight = tierWeight / tokensInTier`.
71
+ 2. **Cash out amount**: `amount = (surplus + totalAmountRedeemed) * tokenWeight / TOTAL_CASHOUT_WEIGHT`. The formula uses `surplus + totalAmountRedeemed` (the original pot size) so that early and late redeemers receive the same value.
72
+ 3. The token is burned and the holder receives their share of the treasury.
73
+
74
+ Example: 100 ETH pot, 4 tiers, winning tier gets weight 500000000000000000 (50%). If that tier had 10 minted tokens, each token redeems for `100 * (500000000000000000 / 10) / 1e18 = 5 ETH`.
75
+
76
+ ### fulfillCommitmentsOf
77
+
78
+ Called automatically when a scorecard is ratified (by `ratifyScorecardFrom`). It performs two actions:
79
+
80
+ 1. **Sends fee payouts**: Computes the fee portion of the pot based on the split percentages configured at game creation, then calls `sendPayoutsOf` to distribute fees to the protocol. If the payout fails, the fee stays in the pot and the function continues (try-catch ensures the final ruleset is always queued).
81
+ 2. **Queues the final ruleset**: Queues a new Juicebox ruleset with `pausePay: true` (no new payments), `cashOutTaxRate: 0` (no tax on cash-outs), and the data hook still active so scored weights are enforced. This transitions the game to its terminal state where only cash-outs remain.
82
+
63
83
  ## Extension Points
64
84
 
65
85
  | Point | Interface | Purpose |
@@ -80,3 +100,15 @@ Attestor → DefifaGovernor.attestToScorecard(proposalId)
80
100
  - `@openzeppelin/contracts` — Checkpoints, Ownable, Clones
81
101
  - `@prb/math` — mulDiv
82
102
  - `scripty.sol` — On-chain scripting for SVG
103
+
104
+ ## Design Decisions
105
+
106
+ **Attestation-based governance over token-weighted voting.** Each tier gets equal max attestation power (`MAX_ATTESTATION_POWER_TIER = 1e9`) regardless of how many tokens it sold. A holder's power within a tier is proportional to their share of that tier's supply. This prevents a popular outcome (e.g., the favorite team) from dominating the scorecard simply by having more buyers. Every outcome's community has equal say in ratification.
107
+
108
+ **Six distinct game phases.** The MINT, REFUND, SCORING, and COMPLETE phases enforce a strict lifecycle where each action is only valid in its phase. MINT allows buying in, REFUND provides a grace period for full refunds, SCORING locks the treasury while governance resolves, and COMPLETE enables scored cash-outs. The COUNTDOWN phase gates minting before the game starts, and NO_CONTEST acts as a fallback if no scorecard is ratified within the timeout. This phased approach prevents timing exploits (e.g., buying in after seeing results) and ensures the treasury is never drained during scoring.
109
+
110
+ **Proxy owner pattern (DefifaProjectOwner).** Game projects are owned by `DefifaDeployer` itself (`launchProjectFor` sets `owner: address(this)`), which restricts game operations to the deployer's hardcoded logic -- no EOA can rug the game by migrating terminals, minting tokens, or changing controllers. Separately, `DefifaProjectOwner` permanently holds the Defifa fee project NFT (DEFIFA_PROJECT_ID) and grants only `SET_SPLIT_GROUPS` permission to the `DefifaDeployer`, so the deployer can set splits on the fee project as needed for fee distribution.
111
+
112
+ **Weight-based redemption instead of per-tier pots.** Rather than splitting the treasury into separate pots per tier at scoring time, the system assigns each tier a weight out of `TOTAL_CASHOUT_WEIGHT` (1e18). Cash-outs compute their share of the entire surplus on the fly. This avoids complex accounting for partial redemptions and lets the bonding math work naturally as tokens are burned. Early and late redeemers within the same tier get the same per-token value because the formula uses `surplus + totalAmountRedeemed` as the denominator base.
113
+
114
+ **Scorecard immutability after ratification.** Once a scorecard reaches quorum and is ratified, `cashOutWeightIsSet` is permanently set to `true` and no new weights can be written. Combined with the final ruleset that pauses payments, this guarantees the game's outcome is final and the treasury can only decrease through cash-outs.
@@ -6,15 +6,15 @@ Prediction game platform built on Juicebox V6. Players buy NFT tiers representin
6
6
 
7
7
  ## Architecture
8
8
 
9
- Five contracts, one library. Total ~2,800 lines of production Solidity.
9
+ Five contracts, one library. Total ~3,990 lines in `src/` (~3,320 in the six main files below).
10
10
 
11
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.
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
18
  ```
19
19
 
20
20
  ### Contract Relationships
@@ -67,7 +67,7 @@ DefifaGovernor (singleton, shared across all games)
67
67
 
68
68
  ### Phase State Machine
69
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).
70
+ Phases are determined by Juicebox ruleset cycle numbers, safety mechanism checks, and scorecard ratification status. The state machine is in `DefifaDeployer.currentGamePhaseOf()`.
71
71
 
72
72
  ```
73
73
  COUNTDOWN (cycleNumber == 0)
@@ -116,7 +116,7 @@ NO_CONTEST (safety mechanism triggered)
116
116
  1. Verify caller is a project terminal, currency matches `pricingCurrency`.
117
117
  2. Decode metadata: `(address _attestationDelegate, uint16[] _tierIdsToMint)`.
118
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.
119
+ 4. For each unique tier: set delegation if needed, transfer attestation units from address(0) to beneficiary.
120
120
  5. Call `_mintAll()`: `store.recordMint()`, increment `_totalMintCost += amount`, mint ERC-721s.
121
121
  6. Revert if `leftoverAmount != 0` (exact pricing enforced, `DefifaHook_Overspending`).
122
122
 
@@ -265,15 +265,30 @@ Where `shareToBeneficiary = cumulativeMintPrice` of burned tokens and `outOfTota
265
265
 
266
266
  ## Priority Audit Areas
267
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
+
268
283
  ### P0 -- Critical (Fund Safety)
269
284
 
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.
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.
271
286
 
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.
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.
273
288
 
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.
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.
275
290
 
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.
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.
277
292
 
278
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.
279
294
 
@@ -283,19 +298,19 @@ Where `shareToBeneficiary = cumulativeMintPrice` of burned tokens and `outOfTota
283
298
 
284
299
  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
300
 
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).
301
+ 8. **Double attestation prevention**: `_attestations.hasAttested[msg.sender]` 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
302
 
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.
303
+ 9. **Grace period anchoring**: `gracePeriodEnds = attestationsBegin + attestationGracePeriod`. Verify that early scorecard submission (before `attestationStartTime`) correctly delays the grace period start, preventing instant ratification.
289
304
 
290
305
  ### P2 -- Medium (Access Control and State Transitions)
291
306
 
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`).
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`).
293
308
 
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.
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.
295
310
 
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.
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.
297
312
 
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.
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.
299
314
 
300
315
  ### P3 -- Low (Edge Cases and Rounding)
301
316
 
@@ -332,7 +347,7 @@ These properties should hold for all games in all states. The test suite validat
332
347
  - Attestation power is snapshotted at `attestationsBegin` (not live)
333
348
  - Quorum threshold: 50% of minted tiers' total max attestation power (live at call time)
334
349
  - Only one scorecard can be ratified per game
335
- - Minimum grace period: 1 day (enforced in `initializeGame`, line 303)
350
+ - Minimum grace period: 1 day (enforced in `initializeGame`)
336
351
 
337
352
  ### Phase Transitions
338
353
  - A ratified scorecard (`cashOutWeightIsSet == true`) always produces COMPLETE, regardless of other conditions
@@ -349,7 +364,7 @@ These properties should hold for all games in all states. The test suite validat
349
364
 
350
365
  ## Testing
351
366
 
352
- ### Test Files (14 files, ~100 test functions)
367
+ ### Test Files (16 files, ~172 test functions)
353
368
 
354
369
  | File | Focus |
355
370
  |------|-------|
@@ -360,11 +375,15 @@ These properties should hold for all games in all states. The test suite validat
360
375
  | `DefifaMintCostInvariant.t.sol` | Stateful fuzz: `_totalMintCost` invariant across random mints and refunds. |
361
376
  | `DefifaHookRegressions.t.sol` | Audit finding M-5: attestation unit conservation on transfer to undelegated recipients. |
362
377
  | `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. |
378
+ | `Fork.t.sol` | Mainnet fork tests: full lifecycle, edge cases, all revert conditions, scorecard state machine. 69 tests. |
364
379
  | `regression/FulfillmentBlocksRatification.t.sol` | Fulfillment failure does not block ratification (try-catch behavior). |
365
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. |
366
384
  | `DefifaUSDC.t.sol` | ERC-20 (USDC) game variant. |
367
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. |
368
387
 
369
388
  ### Running Tests
370
389
 
@@ -382,9 +401,9 @@ forge test --match-contract DefifaMintCostInvariant -vvv
382
401
 
383
402
  | Area | Current Coverage | Risk |
384
403
  |------|-----------------|------|
385
- | ERC-20 token games (non-ETH) | Single USDC test file | LOW |
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 |
386
405
  | 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 |
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 |
388
407
  | Adversarial token URI resolver | No malicious resolver test | LOW |
389
408
  | Clone address collision | No explicit collision test | LOW |
390
409
 
@@ -394,29 +413,92 @@ forge test --match-contract DefifaMintCostInvariant -vvv
394
413
 
395
414
  | Constant | Value | Location |
396
415
  |----------|-------|---------|
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 |
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 |
406
425
 
407
426
  ---
408
427
 
409
- ## Entry Points for Review
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 |
410
446
 
411
- Start with the money: follow ETH from payment to cash-out.
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
+ ---
412
487
 
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
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.
package/CHANGE_LOG.md ADDED
@@ -0,0 +1,107 @@
1
+ # defifa-collection-deployer-v6 Changelog (v5 → v6)
2
+
3
+ This document describes the changes between `defifa-collection-deployer` (v5) and `defifa-collection-deployer-v6` (v6). Defifa is an on-chain prediction game framework built on Juicebox where players mint NFTs representing outcomes, a governor ratifies scorecard distributions, and winners burn NFTs to claim proportional shares.
4
+
5
+ ## Summary
6
+
7
+ - **V6 hook migration**: All 721 hook interactions updated from `nana-721-hook-v5` to `nana-721-hook-v6`, including the new tier splits system and `splitPercent` field in `JB721TierConfig`.
8
+ - **Dependency modernization**: Core, permission IDs, address registry, and ownable dependencies all updated to v6. New ecosystem dependencies on `croptop-core-v6` and `revnet-core-v6`.
9
+ - **Error naming standardized**: Error names changed from bare names (e.g., `InvalidCashoutWeights`) to contract-prefixed names (e.g., `DefifaHook_InvalidCashoutWeights`).
10
+ - **Cash out hook spec gains `noop` field**: `beforeCashOutRecordedWith` now returns `noop=false` in all specifications, ensuring the terminal always calls the hook callback.
11
+ - **Game lifecycle unchanged**: The core game phases (COUNTDOWN → MINT → REFUND → SCORING → COMPLETE) and governance model (50% quorum, scorecard ratification) remain identical.
12
+
13
+ ---
14
+
15
+ ## 1. Breaking Changes
16
+
17
+ ### 1.1 Dependency Updates
18
+
19
+ | Dependency | v5 | v6 |
20
+ |------------|----|----|
21
+ | `@bananapus/core` | `v5` | `v6` |
22
+ | `@bananapus/721-hook` | `v5` | `v6` |
23
+ | `@bananapus/address-registry` | `v5` | `v6` |
24
+ | `@bananapus/permission-ids` | `v5` | `v6` (new dependency) |
25
+ | `@openzeppelin/contracts` | `^5.4.0` | `5.2.0` (pinned) |
26
+ | `@croptop/core` | N/A | `v6` (new) |
27
+ | `@rev-net/core` | N/A | `v6` (new) |
28
+
29
+ ### 1.2 Error Naming Convention
30
+
31
+ All custom errors now use a contract-name prefix:
32
+
33
+ | v5 | v6 |
34
+ |----|----|
35
+ | `InvalidCashoutWeights` | `DefifaHook_InvalidCashoutWeights` |
36
+ | `InvalidPhase` | `DefifaHook_InvalidPhase` |
37
+ | (and similar for all other errors) | Contract prefix added throughout |
38
+
39
+ ### 1.3 `JBCashOutHookSpecification` Gains `noop` Field
40
+
41
+ The v6 `JBCashOutHookSpecification` struct has a `noop` boolean field. `DefifaHook.beforeCashOutRecordedWith` returns `noop=false` in all specifications, ensuring the terminal always invokes `afterCashOutRecordedWith` for game-phase-aware cashout processing.
42
+
43
+ ### 1.4 Solidity Version
44
+
45
+ Compiler remains at `pragma solidity 0.8.23` (not bumped to 0.8.26 unlike most other v6 repos).
46
+
47
+ ### 1.5 721 Hook API Changes
48
+
49
+ Inherited from `nana-721-hook-v6`:
50
+ - `cashOutWeightOf()` and `totalCashOutWeight()` signatures simplified (removed `JBBeforeCashOutRecordedContext` parameter)
51
+ - `pricingContext()` returns 2 values instead of 3
52
+ - `JB721TierConfig` gained `splitPercent` and `splits` fields
53
+ - `JB721TiersHookFlags` gained `issueTokensForSplits` field
54
+
55
+ ---
56
+
57
+ ## 2. Game Lifecycle (Unchanged)
58
+
59
+ The core game phases remain identical between v5 and v6:
60
+
61
+ ```
62
+ COUNTDOWN (ruleset 0) → MINT (ruleset 1) → REFUND (ruleset 2)
63
+ → SCORING (ruleset 3+) → COMPLETE (after ratification)
64
+ ```
65
+
66
+ Safety mechanisms:
67
+ - **NO_CONTEST**: Triggered if `minParticipation` not met or `scorecardTimeout` exceeded
68
+ - **Scorecard governance**: 50% quorum, tier-delegated voting power with checkpointed snapshots
69
+ - **Grace period**: Minimum 1 day for governance proposals
70
+
71
+ ---
72
+
73
+ ## 3. Architecture
74
+
75
+ ### 3.1 Key Contracts
76
+
77
+ | Contract | Role |
78
+ |----------|------|
79
+ | `DefifaDeployer` | Game factory -- launches projects with phased rulesets, manages fulfillment |
80
+ | `DefifaHook` | ERC-721 hook with game logic, attestation, per-tier cashout weights |
81
+ | `DefifaGovernor` | Shared singleton for scorecard submission/attestation/ratification |
82
+ | `DefifaProjectOwner` | Proxy that receives project NFT and grants deployer permissions |
83
+ | `DefifaTokenUriResolver` | On-chain SVG metadata with dynamic game state |
84
+
85
+ ### 3.2 Deployment Pattern
86
+
87
+ DefifaHook instances are deployed as minimal proxy clones via `Clones.cloneDeterministic()`:
88
+ - Salt includes `msg.sender` + nonce (prevents cross-caller collision)
89
+ - One-time `initialize()` call per clone
90
+ - Owned by DefifaGovernor for scorecard weight setting
91
+
92
+ ---
93
+
94
+ ## 4. Migration Table
95
+
96
+ | Aspect | v5 | v6 |
97
+ |--------|----|----|
98
+ | Core dependency | `@bananapus/core-v5` | `@bananapus/core-v6` |
99
+ | 721 hook dependency | `@bananapus/721-hook-v5` | `@bananapus/721-hook-v6` |
100
+ | Permission IDs | Not a direct dependency | `@bananapus/permission-ids-v6` |
101
+ | Error naming | Bare names | Contract-prefixed names |
102
+ | `JBCashOutHookSpecification` | No `noop` field | `noop=false` on all specs |
103
+ | Solidity version | `0.8.23` | `0.8.23` (unchanged) |
104
+ | Game lifecycle | COUNTDOWN->MINT->REFUND->SCORING->COMPLETE | Identical |
105
+ | Governance model | 50% quorum, tier-delegated | Identical |
106
+
107
+ > **Cross-repo impact**: Uses `nana-721-hook-v6` for all tier management and cashout weight distribution. The `nana-permission-ids-v6` ID shifts affect any hardcoded permission checks. Not yet included in `deploy-all-v6` deployment phases (awaiting source updates).
package/CRYPTO_ECON.md CHANGED
@@ -829,7 +829,7 @@ where $n_{\text{min}} \geq 2$ (or better, a configurable parameter). Alternative
829
829
 
830
830
  **Severity: Significant.**
831
831
 
832
- The weight validation in `setTierCashOutWeightsTo` (`DefifaHook.sol:643`) uses a strict greater-than check:
832
+ The weight validation in `setTierCashOutWeightsTo` (`DefifaHook.sol`) uses a strict greater-than check:
833
833
 
834
834
  ```solidity
835
835
  if (_cumulativeCashOutWeight > TOTAL_CASHOUT_WEIGHT) revert INVALID_CASHOUT_WEIGHTS();
@@ -892,7 +892,7 @@ if (gamePhaseReporter.currentGamePhaseOf(_gameId) != DefifaGamePhase.SCORING) {
892
892
 
893
893
  **Severity: Moderate.**
894
894
 
895
- In `fulfillCommitmentsOf` (`DefifaDeployer.sol:439–445`), the function calls `sendPayoutsOf` with `minTokensPaidOut` set to the full treasury balance:
895
+ In `fulfillCommitmentsOf` (`DefifaDeployer.sol`), the function calls `sendPayoutsOf` with `minTokensPaidOut` set to the full treasury balance:
896
896
 
897
897
  ```solidity
898
898
  _terminal.sendPayoutsOf({