@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/README.md CHANGED
@@ -8,7 +8,7 @@ Each game is a Juicebox project with phased rulesets that move through Countdown
8
8
 
9
9
  ### Minting
10
10
 
11
- During the Mint phase, players pay the project's terminal to mint ERC-721 game pieces. Each tier represents a team or outcome -- all tiers share the same price. Minting delegates attestation power to the payer (or a specified delegate), which is used later during scorecard governance.
11
+ During the Mint phase, players pay the project's terminal to mint ERC-721 game pieces. Each tier represents a team or outcome -- all tiers share the same price. Minting delegates attestation power to the beneficiary (or a specified delegate), which is used later during scorecard governance.
12
12
 
13
13
  ### Refunds
14
14
 
@@ -67,6 +67,51 @@ DefifaDeployer ──── launches ────► Juicebox Project (phased ru
67
67
  | `COMPLETE` | -- | Scorecard ratified. Commitments fulfilled. Players burn NFTs to claim pot share + fee tokens. |
68
68
  | `NO_CONTEST` | -- | Safety mechanism triggered. All players can refund at mint price. |
69
69
 
70
+ ### Game Lifecycle Flow
71
+
72
+ ```mermaid
73
+ sequenceDiagram
74
+ participant Deployer as Game Creator
75
+ participant DD as DefifaDeployer
76
+ participant JB as Juicebox Project
77
+ participant Hook as DefifaHook
78
+ participant Gov as DefifaGovernor
79
+ participant Player as Players
80
+
81
+ Note over DD,JB: DEPLOY
82
+ Deployer->>DD: launchGameFor(config)
83
+ DD->>Hook: clone & initialize (tiers, token URI)
84
+ DD->>JB: launchProjectFor (phased rulesets)
85
+ DD->>Gov: initialize (hook, quorum, grace period)
86
+
87
+ Note over JB,Player: COUNTDOWN
88
+ JB-->>Player: project live, minting not yet open
89
+
90
+ Note over JB,Player: MINT
91
+ Player->>JB: pay (mint price)
92
+ JB->>Hook: afterPayRecordedWith (mint NFT)
93
+ Hook-->>Player: ERC-721 game piece
94
+ Player->>Hook: setTierDelegatesTo (delegate attestation power)
95
+
96
+ Note over JB,Player: REFUND (optional)
97
+ Player->>JB: cashOutTokensOf (burn NFT)
98
+ JB->>Hook: afterCashOutRecordedWith (full refund)
99
+ Hook-->>Player: mint price returned
100
+
101
+ Note over Gov,Player: SCORING
102
+ Player->>Gov: submitScorecard (tier weights)
103
+ Player->>Gov: castVote (attest to scorecard)
104
+ Note over Gov: quorum reached + grace period elapsed
105
+ Player->>Gov: ratifyScorecard
106
+ Gov->>Hook: setCashOutWeights (winning scorecard)
107
+ Gov->>DD: fulfillCommitments (pay fees + splits)
108
+
109
+ Note over JB,Player: COMPLETE
110
+ Player->>JB: cashOutTokensOf (burn NFT)
111
+ JB->>Hook: afterCashOutRecordedWith (pot share)
112
+ Hook-->>Player: proportional pot share + fee tokens
113
+ ```
114
+
70
115
  ### Fee Structure
71
116
 
72
117
  | Fee | Rate | Recipient |
@@ -94,6 +139,79 @@ Fees are taken as payouts during commitment fulfillment. The remaining surplus i
94
139
  | `DefifaGamePhase` | `COUNTDOWN`, `MINT`, `REFUND`, `SCORING`, `COMPLETE`, `NO_CONTEST` |
95
140
  | `DefifaScorecardState` | `PENDING`, `ACTIVE`, `DEFEATED`, `SUCCEEDED`, `RATIFIED` |
96
141
 
142
+ ## Risks
143
+
144
+ - **Scorecard timeout gap.** The `scorecardTimeout` safety mechanism triggers No Contest only if no scorecard is ratified before the deadline. However, if `minParticipation` was already met (pot above threshold), the minimum-participation safety net does not apply. A game can get stuck in the `SCORING` phase indefinitely if valid scorecards are submitted but never reach quorum -- the timeout only fires when _no_ scorecard is ratified in time, so a perpetually-contested game with active but insufficient attestation has no automatic resolution path.
145
+
146
+ - **Attestation power is per-tier, not per-NFT.** Each tier contributes equal governance weight (capped at `MAX_ATTESTATION_POWER_TIER`) regardless of how many NFTs were minted in that tier. A tier with 1 NFT holder has the same attestation power as a tier with 100 holders. This means a player who is the sole holder of a low-supply tier controls disproportionate voting power relative to their capital at risk, which could be exploited to steer scorecard outcomes.
147
+
148
+ - **Fee token accumulation depends on external terminals.** When commitments are fulfilled, fee payments to the Defifa and protocol projects generate $DEFIFA and $NANA tokens that accumulate in the hook for later distribution to players. If the receiving projects' terminals revert (e.g., paused, migrated, or misconfigured), the `fulfillCommitments` call will fail, blocking the game from entering the `COMPLETE` phase. Players would need to wait for the external terminal issue to be resolved before they can cash out.
149
+
150
+ - **Ratified scorecards are permanent.** Once a scorecard is ratified by the governor and cash-out weights are set on the hook, those weights cannot be changed for that game. There is no mechanism to re-score or appeal. If a scorecard is ratified with incorrect weights (e.g., due to a rushed attestation or a governance attack during low participation), the payout distribution is locked in permanently.
151
+
152
+ ## Repository Layout
153
+
154
+ ```
155
+ defifa-collection-deployer-v6/
156
+ ├── src/
157
+ │ ├── DefifaDeployer.sol -- Game factory, phase/pot reporter, commitment fulfillment
158
+ │ ├── DefifaGovernor.sol -- Scorecard governance, attestation, ratification
159
+ │ ├── DefifaHook.sol -- ERC-721 game pieces, cash-out weights, delegation
160
+ │ ├── DefifaProjectOwner.sol -- Defifa fee project ownership holder
161
+ │ ├── DefifaTokenUriResolver.sol -- On-chain SVG token URI renderer
162
+ │ ├── enums/
163
+ │ │ ├── DefifaGamePhase.sol -- COUNTDOWN, MINT, REFUND, SCORING, COMPLETE, NO_CONTEST
164
+ │ │ └── DefifaScorecardState.sol -- PENDING, ACTIVE, DEFEATED, SUCCEEDED, RATIFIED
165
+ │ ├── interfaces/
166
+ │ │ ├── IDefifaDeployer.sol
167
+ │ │ ├── IDefifaGamePhaseReporter.sol
168
+ │ │ ├── IDefifaGamePotReporter.sol
169
+ │ │ ├── IDefifaGovernor.sol
170
+ │ │ ├── IDefifaHook.sol
171
+ │ │ └── IDefifaTokenUriResolver.sol
172
+ │ ├── libraries/
173
+ │ │ ├── DefifaFontImporter.sol -- Capsules typeface loader for SVG rendering
174
+ │ │ └── DefifaHookLib.sol -- Scorecard validation, cash-out math, fee distribution
175
+ │ └── structs/
176
+ │ ├── DefifaAttestations.sol
177
+ │ ├── DefifaDelegation.sol
178
+ │ ├── DefifaLaunchProjectData.sol
179
+ │ ├── DefifaOpsData.sol
180
+ │ ├── DefifaScorecard.sol
181
+ │ ├── DefifaTierCashOutWeight.sol
182
+ │ └── DefifaTierParams.sol
183
+ ├── test/
184
+ │ ├── DefifaAdversarialQuorum.t.sol
185
+ │ ├── DefifaAuditLowGuards.t.sol
186
+ │ ├── DefifaFeeAccounting.t.sol
187
+ │ ├── DefifaGovernor.t.sol
188
+ │ ├── DefifaHookRegressions.t.sol
189
+ │ ├── DefifaMintCostInvariant.t.sol
190
+ │ ├── DefifaNoContest.t.sol
191
+ │ ├── DefifaSecurity.t.sol
192
+ │ ├── DefifaUSDC.t.sol
193
+ │ ├── Fork.t.sol
194
+ │ ├── SVG.t.sol
195
+ │ ├── TestAuditGaps.sol
196
+ │ ├── TestQALastMile.t.sol
197
+ │ ├── deployScript.t.sol
198
+ │ └── regression/
199
+ │ ├── AttestationDelegateBeneficiary.t.sol
200
+ │ ├── FulfillmentBlocksRatification.t.sol
201
+ │ └── GracePeriodBypass.t.sol
202
+ ├── script/
203
+ │ ├── Deploy.s.sol -- Deployment script
204
+ │ └── helpers/
205
+ │ └── DefifaDeploymentLib.sol -- Deployment helper library
206
+ ├── lib/ -- Git submodule dependencies
207
+ │ ├── base64/
208
+ │ ├── capsules/
209
+ │ ├── forge-std/
210
+ │ └── typeface/
211
+ └── docs/
212
+ └── plans/ -- Design documents
213
+ ```
214
+
97
215
  ## Install
98
216
 
99
217
  ```bash
@@ -112,7 +230,7 @@ npm install && forge install
112
230
  | Command | Description |
113
231
  |---------|-------------|
114
232
  | `forge build` | Compile contracts and write artifacts to `out`. |
115
- | `forge test` | Run the test suite (53 tests: unit, fuzz, invariant). |
233
+ | `forge test` | Run the test suite (155 tests: unit, fuzz, invariant). |
116
234
  | `forge test -vvvv` | Run tests with full traces. |
117
235
  | `forge fmt` | Format Solidity files. |
118
236
  | `forge build --sizes` | Get contract sizes. |
package/RISKS.md CHANGED
@@ -7,13 +7,14 @@
7
7
  - **DefifaProjectOwner Irrecoverability.** Once the Defifa project NFT is transferred to DefifaProjectOwner, it cannot be recovered. This is intentional but irreversible.
8
8
  - **External Dependencies.** Relies on JB721TiersHookStore, JBController, JBMultiTerminal, JBRulesets, and JBPrices. Bugs in any upstream contract affect all Defifa games.
9
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.
10
+ - **721 hook store shared with nana-721-hook-v6.** DefifaHook extends `JB721TiersHook`, sharing the same `JB721TiersHookStore`. All store-level risks from [nana-721-hook-v6 RISKS.md](../nana-721-hook-v6/RISKS.md) apply — including the `totalCashOutWeight` tier iteration cost and the category sort order enforcement. Store bugs affect all Defifa games simultaneously.
10
11
 
11
12
  ## 2. Economic Risks
12
13
 
13
14
  - **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
15
  - **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
16
  - **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
+ - **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`). Example: if 1000 NFTs are minted by payers (paying 1 ETH each = 1000 ETH total), and 100 reserved NFTs are minted (adding 100 ETH to `_totalMintCost` with no ETH deposited), fee token claims are diluted by ~9.1% (100/1100). The dilution is bounded by the reserve frequency — at `reserveFrequency=10`, every 10th mint is a reserve, capping dilution at ~10%.
17
18
  - **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.
18
19
 
19
20
  ## 3. Governance Risks
@@ -26,12 +27,10 @@
26
27
  ## 4. Reentrancy Surface
27
28
 
28
29
  - **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.
31
30
 
32
31
  ## 5. DoS Vectors
33
32
 
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.
33
+ - **Unbounded tier iteration in governance.** `getAttestationWeight` and `quorum` iterate over all tiers (`maxTierIdOf`). Gas cost: ~3-5k per tier (storage read + bitmap check). At 128 tiers (the hard cap), ~400-650k gas for a single `quorum()` call. At the block gas limit (30M), this is safe, but composing `quorum()` inside a larger transaction (e.g., `ratifyScorecardFrom`) adds the iteration cost on top of the ratification logic. Games should target <64 tiers for comfortable gas headroom.
35
34
  - **_buildSplits iteration.** Iterates over user-provided splits array. No explicit cap, but total percent constraint limits practical count.
36
35
 
37
36
  ## 6. Integration Risks
@@ -50,3 +49,21 @@
50
49
  - `fulfilledCommitmentsOf[gameId]` is set at most once per game.
51
50
  - Per-tier supply never exceeds `initialSupply`.
52
51
  - Sum of all delegate attestation units equals total attestation supply.
52
+
53
+ ## 8. Accepted Behaviors
54
+
55
+ ### 8.1 Scorecard timeout is intentionally irreversible
56
+
57
+ If `scorecardTimeout` elapses before ratification, the game permanently enters NO_CONTEST. Even a scorecard that has reached quorum cannot be ratified after timeout. This is accepted because: (1) allowing late ratification would keep player funds locked indefinitely while governance debates, (2) NO_CONTEST triggers a refund path (`triggerNoContestFor`) that returns funds pro-rata, and (3) the timeout creates a credible commitment to resolve the game within a bounded time. The timeout duration is set at deployment and cannot be changed.
58
+
59
+ ### 8.2 Permanent cash-out weights (no correction mechanism)
60
+
61
+ Cash-out weights set via `ratifyScorecardFrom` cannot be updated or corrected. This is accepted because: (1) allowing weight changes would introduce governance attack surfaces where a quorum re-ratifies to steal from other tiers, (2) the attestation process provides a dispute window (grace period) before ratification finalizes, and (3) the alternative (upgradeable weights) would undermine the trust-minimized game design. If a scorecard is wrong, the game should be allowed to timeout into NO_CONTEST for refunds.
62
+
63
+ ### 8.3 fulfillCommitmentsOf reentrancy is guarded
64
+
65
+ `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.
66
+
67
+ ### 8.4 ratifyScorecardFrom reentrancy is double-guarded
68
+
69
+ `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. Both guards prevent reentrancy exploitation.
package/SKILLS.md CHANGED
@@ -4,47 +4,79 @@
4
4
 
5
5
  On-chain prediction game framework built on Juicebox V6. Players mint NFT game pieces representing teams/outcomes, a governor-based scorecard system determines tier payouts, and winners burn NFTs to claim proportional shares of the pot plus accumulated fee tokens ($DEFIFA/$NANA).
6
6
 
7
+ ## Game Lifecycle
8
+
9
+ ```
10
+ start time reached
11
+ COUNTDOWN ──────────────────────────────► MINT
12
+
13
+ mintPeriodDuration expires
14
+
15
+ ┌───────────────────────────┤
16
+ │ ▼
17
+ │ (refundPeriodDuration=0) REFUND
18
+ │ │
19
+ │ refundPeriodDuration expires
20
+ │ │
21
+ ├◄──────────────────────────┘
22
+
23
+ SCORING
24
+
25
+ ├── scorecard ratified + commitments fulfilled ──► COMPLETE
26
+
27
+ └── safety trigger (minParticipation not met
28
+ OR scorecardTimeout elapsed) ──────────────► NO_CONTEST
29
+ (full refunds)
30
+ ```
31
+
32
+ - **COUNTDOWN**: Before `start`. No minting.
33
+ - **MINT**: Players mint NFTs and delegate attestation power. Delegation changes only allowed here.
34
+ - **REFUND**: Optional. Players can burn NFTs for full refund at mint price. Skipped if `refundPeriodDuration=0`.
35
+ - **SCORING**: Scorecards are submitted, attested, and ratified. Cash outs blocked until scorecard is set.
36
+ - **COMPLETE**: Commitments fulfilled. Players burn NFTs for weighted pot share + fee tokens.
37
+ - **NO_CONTEST**: Safety exit. Full refunds enabled. Irreversible once triggered.
38
+
7
39
  ## Contracts
8
40
 
9
41
  | Contract | Role |
10
42
  |----------|------|
11
43
  | `DefifaDeployer` | Factory that creates games as Juicebox projects with phased rulesets, cloned hooks, and governor initialization. Manages post-game commitment fulfillment. Implements `IDefifaGamePhaseReporter` and `IDefifaGamePotReporter`. |
12
- | `DefifaHook` | ERC-721 hook (extends `JB721Hook`) that manages cash-out weights per tier, attestation delegation with checkpointed voting power, and proportional pot distribution on burn. Deployed as minimal proxy clones via `Clones.cloneDeterministic`. |
13
- | `DefifaGovernor` | Governance contract for scorecard submission, attestation, and ratification with 50% quorum requirement. Shared singleton across all games. |
14
- | `DefifaHookLib` | External library with pure/view helpers: scorecard validation, cash-out weight calculation, fee token distribution, attestation unit aggregation, supply computation. |
15
- | `DefifaTokenUriResolver` | On-chain SVG renderer for game card metadata with phase-aware display, pot size, rarity, and current value. Uses embedded Capsules typeface. |
44
+ | `DefifaHook` | ERC-721 hook (extends `JB721Hook`) managing cash-out weights, attestation delegation with checkpointed voting, and proportional pot distribution. Deployed as minimal proxy clones. |
45
+ | `DefifaGovernor` | Shared singleton for scorecard submission, attestation, and ratification. |
46
+ | `DefifaHookLib` | External library: scorecard validation, cash-out weight calculation, fee token distribution, attestation aggregation. |
47
+ | `DefifaTokenUriResolver` | On-chain SVG renderer for game card metadata with phase-aware display. Uses embedded Capsules typeface. |
48
+ | `DefifaFontImporter` | Loads Capsules typeface font for SVG renderer. |
16
49
  | `DefifaProjectOwner` | Receives Defifa fee project's ownership NFT and permanently grants the deployer `SET_SPLIT_GROUPS` permission. |
17
50
 
18
51
  ## Key Functions
19
52
 
20
53
  | Function | Contract | What it does |
21
54
  |----------|----------|--------------|
22
- | `launchGameWith(data)` | `DefifaDeployer` | Creates a new game: clones the hook via `Clones.cloneDeterministic`, initializes it with tiers and reporters, launches a Juicebox project with phased rulesets (Mint → optional Refund → Scoring), initializes the governor, transfers hook ownership to the governor. Returns the game ID (Juicebox project ID). |
23
- | `fulfillCommitmentsOf(gameId)` | `DefifaDeployer` | After scorecard ratification, sends the fee portion (Defifa fee + protocol fee + user splits) as payouts via `sendPayoutsOf`, then queues a final ruleset with `pausePay=true` and zero payout limits so the remaining pot is available for cash outs. Uses `max(amount, 1)` as reentrancy guard. |
24
- | `triggerNoContestFor(gameId)` | `DefifaDeployer` | Checks safety conditions (min participation or scorecard timeout) and queues a NO_CONTEST ruleset enabling full refunds. Can only be called once per game. |
25
- | `currentGamePhaseOf(gameId)` | `DefifaDeployer` | Returns the current game phase based on ruleset cycle number, cash-out weight state, and no-contest status. Implements `IDefifaGamePhaseReporter`. |
26
- | `currentGamePotOf(gameId, includeCommitments)` | `DefifaDeployer` | Returns pot size, token address, and decimals. If `includeCommitments` is false, subtracts already-fulfilled commitment amount. |
27
- | `timesFor(gameId)` | `DefifaDeployer` | Returns `(start, mintPeriodDuration, refundPeriodDuration)` for a game. |
28
- | `safetyParamsOf(gameId)` | `DefifaDeployer` | Returns `(minParticipation, scorecardTimeout)` for a game. |
29
- | `nextPhaseNeedsQueueing(gameId)` | `DefifaDeployer` | Returns true if the current ruleset has a duration > 0 and the latest queued ruleset is the same as the current one (meaning no new ruleset has been queued yet). |
30
- | `submitScorecardFor(gameId, tierWeights)` | `DefifaGovernor` | Submits a proposed scorecard (array of tier cash-out weights). Hashes the encoded calldata to produce a scorecard ID. Stores attestation begin and grace period end timestamps. Only during SCORING phase. If `defaultAttestationDelegateProposalOf[gameId]` is 0, the first proposal from the default delegate auto-sets it. |
31
- | `attestToScorecardFrom(gameId, scorecardId)` | `DefifaGovernor` | Attests to a scorecard. Weight is proportional to the caller's tier-delegated voting power at the attestation begin timestamp. Each address can only attest once per scorecard. Returns the attestation weight. |
32
- | `ratifyScorecardFrom(gameId, tierWeights)` | `DefifaGovernor` | Ratifies a scorecard that has reached `SUCCEEDED` state (50% quorum). Executes `setTierCashOutWeightsTo` on the hook via low-level `.call`, then calls `fulfillCommitmentsOf`. Scorecard is immutable once ratified. |
33
- | `initializeGame(gameId, startTime, gracePeriod)` | `DefifaGovernor` | Sets attestation start time and grace period for a game. Grace period minimum is 1 day. Called by the deployer during game launch. |
34
- | `quorum(gameId)` | `DefifaGovernor` | Returns `50% of (MAX_ATTESTATION_POWER_TIER * numberOfMintedTiers)`. Only tiers with non-zero minted supply count toward quorum. |
35
- | `getAttestationWeight(gameId, account, timestamp)` | `DefifaGovernor` | Calculates an account's attestation power across all tiers (up to 128) using checkpointed delegation snapshots at `timestamp`. Per-tier power: `mulDiv(MAX_ATTESTATION_POWER_TIER, accountTierUnits, totalTierUnits)`. |
36
- | `stateOf(gameId, scorecardId)` | `DefifaGovernor` | Returns scorecard state: `RATIFIED` if matches ratified ID, `PENDING` if before attestation begin, `SUCCEEDED` if quorum reached + grace period elapsed, `ACTIVE` if attestation in progress, `DEFEATED` otherwise. |
37
- | `setTierCashOutWeightsTo(tierWeights)` | `DefifaHook` | Sets cash-out weights for each tier. Validates weights sum to exactly `TOTAL_CASHOUT_WEIGHT` (1e18), tiers are in ascending order, and all tiers exist. Only callable by owner (the governor). Only callable during SCORING phase. Once set, cannot be changed (`cashOutWeightIsSet` flag). |
38
- | `afterPayRecordedWith(context)` | `DefifaHook` | Processes payments: validates caller is a project terminal and `msg.value == 0`, then delegates to `_processPayment`. Overrides `JB721Hook` to add the `msg.value != 0` check. |
39
- | `beforeCashOutRecordedWith(context)` | `DefifaHook` | Returns cash-out parameters based on game phase. During MINT/REFUND/NO_CONTEST: returns cumulative mint price as `cashOutCount` (full refund). During SCORING/COMPLETE: returns weighted share based on tier scorecard weights. Uses surplus as `totalSupply`. |
40
- | `afterCashOutRecordedWith(context)` | `DefifaHook` | Burns NFTs, validates ownership, tracks redemptions per tier. During COMPLETE phase: increments `amountRedeemed`, distributes proportional $DEFIFA/$NANA tokens to the holder based on `_totalMintCost` share. Reverts with `NothingToClaim` if no ETH and no fee tokens received. Decrements `_totalMintCost` by the burned tokens' cumulative mint price. |
41
- | `cashOutWeightOf(tokenIds)` | `DefifaHook` | Returns the cumulative cash-out weight for an array of token IDs. Each token's weight: `tierWeight / (minted - burned)`, accounting for already-redeemed tokens. Overrides `JB721Hook`. |
42
- | `cashOutWeightOf(tokenId)` | `DefifaHook` | Returns the cash-out weight for a single token ID. |
43
- | `totalCashOutWeight()` | `DefifaHook` | Returns `TOTAL_CASHOUT_WEIGHT` (1e18). Overrides `JB721Hook`. |
44
- | `setTierDelegateTo(delegatee, tierId)` | `DefifaHook` | Delegates attestation voting power for a specific tier to another address. Only during MINT phase. Reverts during other phases. |
45
- | `setTierDelegatesTo(delegations)` | `DefifaHook` | Batch variant. Sets delegates for multiple tiers at once. Only during MINT phase. |
46
- | `mintReservesFor(tierId, count)` | `DefifaHook` | Mints reserved tokens for a tier. Auto-delegates to default attestation delegate if no delegate set. Increments `_totalMintCost` by `tier.price * count` so reserved recipients get their share of fee tokens. |
47
- | `initialize(gameId, name, symbol, ...)` | `DefifaHook` | One-time initialization for a cloned hook. Sets project ID, name, symbol, store, rulesets, reporters, tiers, tier names, and default attestation delegate. Reverts if called on code origin or if already initialized. |
55
+ | `launchGameWith(data)` | `DefifaDeployer` | Creates a game: clones the hook, launches a Juicebox project with phased rulesets, initializes the governor, transfers hook ownership to the governor. Returns the game ID. |
56
+ | `fulfillCommitmentsOf(gameId)` | `DefifaDeployer` | After ratification, sends fee payouts via `sendPayoutsOf`, then queues a final ruleset enabling cash outs. |
57
+ | `triggerNoContestFor(gameId)` | `DefifaDeployer` | Checks safety conditions and queues a NO_CONTEST ruleset enabling full refunds. Once per game. |
58
+ | `currentGamePhaseOf(gameId)` | `DefifaDeployer` | Returns the current `DefifaGamePhase`. |
59
+ | `currentGamePotOf(gameId, includeCommitments)` | `DefifaDeployer` | Returns pot size, token address, and decimals. |
60
+ | `timesFor(gameId)` | `DefifaDeployer` | Returns `(start, mintPeriodDuration, refundPeriodDuration)`. |
61
+ | `safetyParamsOf(gameId)` | `DefifaDeployer` | Returns `(minParticipation, scorecardTimeout)`. |
62
+ | `nextPhaseNeedsQueueing(gameId)` | `DefifaDeployer` | True if the next phase ruleset hasn't been queued yet. |
63
+ | `submitScorecardFor(gameId, tierWeights)` | `DefifaGovernor` | Submits a scorecard proposal. Only during SCORING. |
64
+ | `attestToScorecardFrom(gameId, scorecardId)` | `DefifaGovernor` | Attests to a scorecard using tier-delegated voting power. One attestation per address per scorecard. |
65
+ | `ratifyScorecardFrom(gameId, tierWeights)` | `DefifaGovernor` | Ratifies a `SUCCEEDED` scorecard (50% quorum met + grace period elapsed). Executes weights on the hook, then fulfills commitments. |
66
+ | `initializeGame(gameId, ...)` | `DefifaGovernor` | Sets attestation start time and grace period. Called by deployer during launch. |
67
+ | `quorum(gameId)` | `DefifaGovernor` | Returns the quorum threshold. See Attestation & Governance for formula. |
68
+ | `getAttestationWeight(gameId, account, timestamp)` | `DefifaGovernor` | Returns an account's attestation power. See Attestation & Governance for formula. |
69
+ | `stateOf(gameId, scorecardId)` | `DefifaGovernor` | Returns scorecard state: `RATIFIED`, `PENDING`, `SUCCEEDED`, `ACTIVE`, or `DEFEATED`. |
70
+ | `setTierCashOutWeightsTo(tierWeights)` | `DefifaHook` | Sets cash-out weights. Weights must sum to exactly `TOTAL_CASHOUT_WEIGHT` (1e18). Owner-only (governor), SCORING phase only, one-time. |
71
+ | `afterPayRecordedWith(context)` | `DefifaHook` | Processes payments. Adds `msg.value != 0` check over base `JB721Hook`. |
72
+ | `beforeCashOutRecordedWith(context)` | `DefifaHook` | Returns cash-out parameters based on game phase. Always returns `noop: false`. |
73
+ | `afterCashOutRecordedWith(context)` | `DefifaHook` | Burns NFTs and tracks redemptions. During COMPLETE, also distributes fee tokens. |
74
+ | `cashOutWeightOf(tokenIds)` | `DefifaHook` | Cumulative cash-out weight for token IDs: `tierWeight / (minted - burned)` per token. |
75
+ | `totalCashOutWeight()` | `DefifaHook` | Returns `TOTAL_CASHOUT_WEIGHT` (1e18). |
76
+ | `setTierDelegateTo(delegatee, tierId)` | `DefifaHook` | Delegates attestation power for a tier. MINT phase only. |
77
+ | `setTierDelegatesTo(delegations)` | `DefifaHook` | Batch delegation. MINT phase only. |
78
+ | `mintReservesFor(tierId, count)` | `DefifaHook` | Mints reserved tokens. Increments `_totalMintCost` so reserved recipients share fee tokens. |
79
+ | `initialize(gameId, ...)` | `DefifaHook` | One-time init for cloned hook. Sets project ID, store, reporters, tiers, and default attestation delegate. |
48
80
 
49
81
  ## Integration Points
50
82
 
@@ -53,22 +85,105 @@ On-chain prediction game framework built on Juicebox V6. Players mint NFT game p
53
85
  | `@bananapus/core-v6` | `IJBController`, `IJBDirectory`, `IJBRulesets`, `IJBTerminal`, `IJBMultiTerminal`, `JBRulesetConfig`, `JBSplit`, `JBConstants`, `JBMetadataResolver` | Project creation, ruleset management, terminal interactions, payout distribution, metadata encoding. |
54
86
  | `@bananapus/721-hook-v6` | `JB721Hook`, `IJB721TiersHookStore`, `JB721TierConfig`, `JB721Tier`, `ERC721`, `JB721TiersRulesetMetadataResolver` | Hook base class, NFT tier management, tier storage, transfer pause checking. |
55
87
  | `@bananapus/address-registry-v6` | `IJBAddressRegistry` | Hook address registration for discoverability. |
56
- | `@bananapus/permission-ids-v6` | `JBPermissionIds` | Permission constants for split management (`SET_SPLIT_GROUPS`). |
88
+ | `@bananapus/permission-ids-v6` | `JBPermissionIds` | Permission constants (`SET_SPLIT_GROUPS`). |
57
89
  | `@openzeppelin/contracts` | `Ownable`, `Clones`, `IERC721Receiver`, `SafeERC20`, `Checkpoints`, `Strings`, `IERC20` | Access control, minimal proxy cloning, safe token handling, checkpointed voting, string formatting, fee token transfers. |
58
- | `@prb/math` | `mulDiv` | Precise fixed-point arithmetic for attestation weight and pot distribution calculations. |
90
+ | `@prb/math` | `mulDiv` | Fixed-point arithmetic for attestation weight and pot distribution. |
59
91
 
60
92
  ## Key Types
61
93
 
62
94
  | Struct/Enum | Key Fields | Used In |
63
95
  |-------------|------------|---------|
64
- | `DefifaLaunchProjectData` | `name`, `tiers` (DefifaTierParams[]), `tierPrice` (uint104), `token` (JBAccountingContext), `mintPeriodDuration` (uint24), `refundPeriodDuration` (uint24), `start` (uint48), `splits` (JBSplit[]), `attestationStartTime`, `attestationGracePeriod`, `defaultAttestationDelegate`, `terminal`, `store`, `minParticipation` (uint256), `scorecardTimeout` (uint32) | `DefifaDeployer.launchGameWith` |
96
+ | `DefifaLaunchProjectData` | `name`, `projectUri`, `contractUri`, `baseUri`, `tiers` (DefifaTierParams[]), `tierPrice` (uint104), `token` (JBAccountingContext), `mintPeriodDuration` (uint24), `refundPeriodDuration` (uint24), `start` (uint48), `splits` (JBSplit[]), `attestationStartTime`, `attestationGracePeriod`, `defaultAttestationDelegate`, `defaultTokenUriResolver` (IJB721TokenUriResolver), `terminal`, `store`, `minParticipation` (uint256), `scorecardTimeout` (uint32) | `DefifaDeployer.launchGameWith` |
65
97
  | `DefifaTierParams` | `name` (string), `reservedRate` (uint16), `reservedTokenBeneficiary` (address), `encodedIPFSUri` (bytes32), `shouldUseReservedTokenBeneficiaryAsDefault` (bool) | `DefifaLaunchProjectData.tiers` |
66
98
  | `DefifaTierCashOutWeight` | `id` (uint256), `cashOutWeight` (uint256) | Scorecard proposals, `DefifaHook.setTierCashOutWeightsTo` |
67
99
  | `DefifaOpsData` | `token` (address), `start` (uint48), `mintPeriodDuration` (uint24), `refundPeriodDuration` (uint24), `minParticipation` (uint256), `scorecardTimeout` (uint32) | Internal game state in `DefifaDeployer` |
68
100
  | `DefifaDelegation` | `delegatee` (address), `tierId` (uint256) | `DefifaHook.setTierDelegatesTo` |
69
101
  | `DefifaGamePhase` | `COUNTDOWN`, `MINT`, `REFUND`, `SCORING`, `COMPLETE`, `NO_CONTEST` | Phase reporting throughout |
102
+ | `DefifaScorecard` | `attestationsBegin` (uint48), `gracePeriodEnds` (uint48) | `DefifaGovernor._scorecardOf` |
103
+ | `DefifaAttestations` | `count` (uint256), `hasAttested` (mapping(address => bool)) | `DefifaGovernor._scorecardAttestationsOf` |
70
104
  | `DefifaScorecardState` | `PENDING`, `ACTIVE`, `DEFEATED`, `SUCCEEDED`, `RATIFIED` | `DefifaGovernor.stateOf` |
71
105
 
106
+ ## Events
107
+
108
+ | Event | Contract | Parameters |
109
+ |-------|----------|------------|
110
+ | `LaunchGame` | `DefifaDeployer` | `gameId` (indexed), `hook` (indexed), `governor` (indexed), `tokenUriResolver`, `caller` |
111
+ | `FulfilledCommitments` | `DefifaDeployer` | `gameId` (indexed), `pot`, `caller` |
112
+ | `CommitmentPayoutFailed` | `DefifaDeployer` | `gameId` (indexed), `amount`, `reason` (bytes) |
113
+ | `DistributeToSplit` | `DefifaDeployer` | `split` (JBSplit), `amount`, `caller` |
114
+ | `QueuedNoContest` | `DefifaDeployer` | `gameId` (indexed), `caller` |
115
+ | `QueuedRefundPhase` | `DefifaDeployer` | `gameId` (indexed), `caller` |
116
+ | `QueuedScoringPhase` | `DefifaDeployer` | `gameId` (indexed), `caller` |
117
+ | `Mint` | `DefifaHook` | `tokenId` (indexed), `tierId` (indexed), `beneficiary` (indexed), `totalAmountContributed`, `caller` |
118
+ | `MintReservedToken` | `DefifaHook` | `tokenId` (indexed), `tierId` (indexed), `beneficiary` (indexed), `caller` |
119
+ | `TierDelegateAttestationsChanged` | `DefifaHook` | `delegate` (indexed), `tierId` (indexed), `previousBalance`, `newBalance`, `caller` |
120
+ | `DelegateChanged` | `DefifaHook` | `delegator` (indexed), `fromDelegate` (indexed), `toDelegate` (indexed) |
121
+ | `ClaimedTokens` | `DefifaHook` | `beneficiary` (indexed), `defifaTokenAmount`, `baseProtocolTokenAmount`, `caller` |
122
+ | `TierCashOutWeightsSet` | `DefifaHook` | `tierWeights` (DefifaTierCashOutWeight[]), `caller` |
123
+ | `GameInitialized` | `DefifaGovernor` | `gameId` (indexed), `attestationStartTime`, `attestationGracePeriod`, `caller` |
124
+ | `ScorecardSubmitted` | `DefifaGovernor` | `gameId` (indexed), `scorecardId` (indexed), `tierWeights` (DefifaTierCashOutWeight[]), `isDefaultAttestationDelegate`, `caller` |
125
+ | `ScorecardAttested` | `DefifaGovernor` | `gameId` (indexed), `scorecardId` (indexed), `weight`, `caller` |
126
+ | `ScorecardRatified` | `DefifaGovernor` | `gameId` (indexed), `scorecardId` (indexed), `caller` |
127
+
128
+ ## Errors
129
+
130
+ | Error | Contract | When |
131
+ |-------|----------|------|
132
+ | `DefifaDeployer_CantFulfillYet` | `DefifaDeployer` | `fulfillCommitmentsOf` called before scorecard is ratified. |
133
+ | `DefifaDeployer_GameOver` | `DefifaDeployer` | Attempting to queue a phase after the game is already complete. |
134
+ | `DefifaDeployer_InvalidFeePercent` | `DefifaDeployer` | Fee configuration is invalid. |
135
+ | `DefifaDeployer_InvalidGameConfiguration` | `DefifaDeployer` | Launch data fails validation (e.g., missing tiers, bad durations). |
136
+ | `DefifaDeployer_IncorrectDecimalAmount` | `DefifaDeployer` | Token accounting context has wrong decimal count. |
137
+ | `DefifaDeployer_NotNoContest` | `DefifaDeployer` | Safety conditions for no-contest are not met. |
138
+ | `DefifaDeployer_NoContestAlreadyTriggered` | `DefifaDeployer` | `triggerNoContestFor` called more than once for the same game. |
139
+ | `DefifaDeployer_TerminalNotFound` | `DefifaDeployer` | No terminal found for the game's project. |
140
+ | `DefifaDeployer_PhaseAlreadyQueued` | `DefifaDeployer` | The next phase ruleset has already been queued. |
141
+ | `DefifaDeployer_SplitsDontAddUp` | `DefifaDeployer` | Split percentages don't sum correctly. |
142
+ | `DefifaDeployer_UnexpectedTerminalCurrency` | `DefifaDeployer` | Terminal's accounting currency doesn't match expected currency. |
143
+ | `DefifaHook_BadTierOrder` | `DefifaHook` | Scorecard tier IDs are not in strict ascending order. |
144
+ | `DefifaHook_DelegateAddressZero` | `DefifaHook` | Attempting to delegate to the zero address. |
145
+ | `DefifaHook_DelegateChangesUnavailableInThisPhase` | `DefifaHook` | Delegation change attempted outside MINT phase. |
146
+ | `DefifaHook_GameIsntScoringYet` | `DefifaHook` | `setTierCashOutWeightsTo` called before SCORING phase. |
147
+ | `DefifaHook_InvalidTierId` | `DefifaHook` | Tier ID in scorecard doesn't exist. |
148
+ | `DefifaHook_InvalidCashoutWeights` | `DefifaHook` | Scorecard tier weights don't sum to exactly `TOTAL_CASHOUT_WEIGHT` (1e18). |
149
+ | `DefifaHook_NothingToClaim` | `DefifaHook` | Cash out during COMPLETE yields no ETH and no fee tokens. |
150
+ | `DefifaHook_NothingToMint` | `DefifaHook` | Reserved mint attempted with zero count or no available reserves. |
151
+ | `DefifaHook_WrongCurrency` | `DefifaHook` | Payment currency doesn't match the hook's pricing currency. |
152
+ | `DefifaHook_Overspending` | `DefifaHook` | Payment exceeds allowed amount for the tier. |
153
+ | `DefifaHook_CashoutWeightsAlreadySet` | `DefifaHook` | `setTierCashOutWeightsTo` called after weights were already set. |
154
+ | `DefifaHook_ReservedTokenMintingPaused` | `DefifaHook` | Reserved token minting is paused in the current ruleset. |
155
+ | `DefifaHook_TransfersPaused` | `DefifaHook` | Token transfers are paused in the current ruleset. |
156
+ | `DefifaHook_Unauthorized(tokenId, owner, caller)` | `DefifaHook` | Caller doesn't own the token being operated on. |
157
+ | `DefifaGovernor_AlreadyAttested` | `DefifaGovernor` | Account already attested to this scorecard. |
158
+ | `DefifaGovernor_AlreadyInitialized` | `DefifaGovernor` | `initializeGame` called for a game that's already initialized. |
159
+ | `DefifaGovernor_AlreadyRatified` | `DefifaGovernor` | Attempting to submit or ratify when a scorecard is already ratified. |
160
+ | `DefifaGovernor_DuplicateScorecard` | `DefifaGovernor` | Submitting a scorecard that produces the same hash as an existing one. |
161
+ | `DefifaGovernor_GameNotFound` | `DefifaGovernor` | Game has not been initialized (`_packedScorecardInfoOf` is 0). |
162
+ | `DefifaGovernor_IncorrectTierOrder` | `DefifaGovernor` | Tier weights not in ascending order. |
163
+ | `DefifaGovernor_NotAllowed` | `DefifaGovernor` | Operation not permitted in the current game phase or scorecard state. |
164
+ | `DefifaGovernor_Uint48Overflow` | `DefifaGovernor` | `attestationStartTime` or `attestationGracePeriod` exceeds uint48 max. |
165
+ | `DefifaGovernor_UnknownProposal` | `DefifaGovernor` | `stateOf` called with a scorecard ID that hasn't been submitted. |
166
+ | `DefifaGovernor_UnownedProposedCashoutValue` | `DefifaGovernor` | Scorecard assigns non-zero weight to a tier with zero minted supply. |
167
+
168
+ ## Storage
169
+
170
+ | Variable | Type | Contract | Description |
171
+ |----------|------|----------|-------------|
172
+ | `_tierCashOutWeights` | `uint256[128]` | `DefifaHook` | Fixed-size array of cash-out weights per tier, set once by the governor. |
173
+ | `cashOutWeightIsSet` | `bool` | `DefifaHook` | Flag preventing re-setting of cash-out weights. |
174
+ | `amountRedeemed` | `uint256` | `DefifaHook` | Cumulative ETH redeemed from the pot (refunds not counted). |
175
+ | `_totalMintCost` | `uint256` | `DefifaHook` | Cumulative mint price of all live tokens. Denominator for fee token distribution. |
176
+ | `tokensRedeemedFrom` | `mapping(uint256 => uint256)` | `DefifaHook` | Number of tokens redeemed per tier. |
177
+ | `ratifiedScorecardIdOf` | `mapping(uint256 => uint256)` | `DefifaGovernor` | Maps game ID to the ratified scorecard ID (0 if none). |
178
+ | `_packedScorecardInfoOf` | `mapping(uint256 => uint256)` | `DefifaGovernor` | Bit-packed: attestation start time (bits 0-47), grace period (bits 48-95). |
179
+ | `_scorecardOf` | `mapping(uint256 => mapping(uint256 => DefifaScorecard))` | `DefifaGovernor` | Maps (gameId, scorecardId) to scorecard data. |
180
+ | `_scorecardAttestationsOf` | `mapping(uint256 => mapping(uint256 => DefifaAttestations))` | `DefifaGovernor` | Maps (gameId, scorecardId) to attestation data. |
181
+ | `defaultAttestationDelegateProposalOf` | `mapping(uint256 => uint256)` | `DefifaGovernor` | Maps game ID to the scorecard ID submitted by the default attestation delegate. |
182
+ | `fulfilledCommitmentsOf` | `mapping(uint256 => uint256)` | `DefifaDeployer` | Non-zero means commitments fulfilled; value of 1 is a sentinel for reentrancy guard. |
183
+ | `noContestTriggeredFor` | `mapping(uint256 => bool)` | `DefifaDeployer` | Whether no-contest has been triggered. Can only be set once. |
184
+ | `_opsOf` | `mapping(uint256 => DefifaOpsData)` | `DefifaDeployer` | Operational data (token, start, durations, safety params). |
185
+ | `_commitmentPercentOf` | `mapping(uint256 => uint256)` | `DefifaDeployer` | Total commitment percentage (fees + splits). |
186
+
72
187
  ## Constants
73
188
 
74
189
  | Constant | Value | Location | Meaning |
@@ -92,34 +207,32 @@ During COMPLETE phase cash outs, players also receive proportional $DEFIFA and $
92
207
 
93
208
  ## Attestation & Governance
94
209
 
95
- - Each tier contributes equal `MAX_ATTESTATION_POWER_TIER` to quorum regardless of supply -- a tier with 1 NFT has the same governance weight as a tier with 100.
96
- - Attestation power per account per tier: `mulDiv(MAX_ATTESTATION_POWER_TIER, accountTierUnits, totalTierUnits)`.
97
- - Quorum: `50% of (MAX_ATTESTATION_POWER_TIER * numberOfMintedTiers)`. Only tiers with at least one minted token count.
98
- - Attestation snapshots are taken at the scorecard's `attestationsBegin` timestamp, locking voting power to prevent post-submission manipulation.
210
+ - **Per-tier power**: `mulDiv(MAX_ATTESTATION_POWER_TIER, accountTierUnits, totalTierUnits)`. Each tier contributes equal weight regardless of supply -- a tier with 1 NFT has the same governance weight as a tier with 100.
211
+ - **Quorum**: `50% of (MAX_ATTESTATION_POWER_TIER * numberOfMintedTiers)`. Only tiers with at least one minted token count.
212
+ - Snapshots taken at the scorecard's `attestationsBegin` timestamp, locking voting power to prevent post-submission manipulation.
99
213
  - Each address can only attest once per scorecard.
100
- - The grace period (minimum 1 day) prevents instant ratification after quorum is reached.
214
+ - Grace period (minimum 1 day) prevents instant ratification after quorum is reached.
101
215
 
102
216
  ## Gotchas
103
217
 
104
- - `TOTAL_CASHOUT_WEIGHT` is 1e18. Submitted scorecard tier weights must sum to **exactly** this value or `setTierCashOutWeightsTo` reverts with `DefifaHook_InvalidCashoutWeights`. No tolerance.
105
- - Tier IDs in a scorecard must be in **strict ascending order** with no duplicates, or validation reverts with `DefifaHook_BadTierOrder`.
106
- - Tier IDs are limited to 128 (`uint256[128] _tierCashOutWeights`). Games with more than 128 tiers are not supported.
107
- - `DefifaHook` is deployed as a **minimal proxy clone** (`Clones.cloneDeterministic`). The `initialize` function can only be called once -- the code origin reverts (has `store != address(0)` after its own construction prevents re-init).
108
- - All tiers share the same price (`tierPrice` on `DefifaLaunchProjectData`). The hook enforces this uniformity.
109
- - Delegation changes are **only allowed during MINT phase**. During REFUND, SCORING, and COMPLETE, attestation delegation is frozen to prevent manipulation. Calling `setTierDelegateTo` outside MINT reverts with `DefifaHook_DelegateChangesUnavailableInThisPhase`.
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
- - 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
- - `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. `sendPayoutsOf` is wrapped in try-catch: on failure, resets to sentinel (1) and emits `CommitmentPayoutFailed`.
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
- - `_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
- - 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.
117
- - `minParticipation` is compared against the terminal's surplus. If surplus never reaches this value, `triggerNoContestFor` can be called to enter NO_CONTEST. A value of 0 disables this check.
118
- - `scorecardTimeout` counts seconds from when SCORING begins. If no scorecard is ratified within this window, `triggerNoContestFor` can be called. A value of 0 disables this check.
119
- - `triggerNoContestFor` can only be called once per game. It queues a new ruleset enabling full refunds and is irreversible.
120
- - `afterPayRecordedWith` overrides `JB721Hook`'s version to add a `msg.value != 0` check. The base `JB721Hook` does not include this check.
121
- - Token IDs follow the `JB721TiersHookStore` encoding: `tierId * 1_000_000_000 + tokenNumber`.
122
- - Metadata IDs for pay and cashout use the **code origin address** (the uncloned implementation), not the clone address: `JBMetadataResolver.getId("pay", codeOrigin)`.
218
+ - `TOTAL_CASHOUT_WEIGHT` is 1e18. Submitted scorecard tier weights must sum to **exactly** this value or `setTierCashOutWeightsTo` reverts. No tolerance.
219
+ - Tier IDs in a scorecard must be in **strict ascending order** with no duplicates.
220
+ - Max 128 tiers (`uint256[128] _tierCashOutWeights`).
221
+ - `DefifaHook` is a **minimal proxy clone** (`Clones.cloneDeterministic`). `initialize` can only be called once.
222
+ - All tiers share the same price (`tierPrice` on `DefifaLaunchProjectData`).
223
+ - **Delegation only during MINT phase**. Other phases revert with `DefifaHook_DelegateChangesUnavailableInThisPhase`.
224
+ - If `totalTierUnits` is 0 for a tier (no delegations), that tier contributes no attestation power.
225
+ - **Dynamic quorum**: only counts tiers with minted supply. Minting new tiers changes quorum retroactively for active proposals.
226
+ - `ratifyScorecardFrom` uses **low-level `.call`** to execute the scorecard on the hook (necessary because `setTierCashOutWeightsTo` is `onlyOwner`).
227
+ - `fulfillCommitmentsOf` uses `max(amount, 1)` as a reentrancy sentinel. `sendPayoutsOf` is wrapped in try-catch: on failure, resets to sentinel (1) and emits `CommitmentPayoutFailed`.
228
+ - `_buildSplits` normalizes split percentages. Rounding remainder absorbed by the protocol fee split (last in array).
229
+ - `_totalMintCost` tracks cumulative mint prices (paid + reserved). Incremented on pay/reserve, decremented on cash out. Denominator for fee token distribution.
230
+ - Cash outs during COMPLETE revert with `DefifaHook_NothingToClaim` if **both** reclaimed ETH is 0 **and** no fee tokens transferred.
231
+ - `minParticipation` is compared against terminal surplus. Value of 0 disables the check.
232
+ - `scorecardTimeout` counts seconds from SCORING start. Value of 0 disables. Both enable `triggerNoContestFor` when exceeded.
233
+ - `triggerNoContestFor` can only be called once per game and is irreversible.
234
+ - Token IDs follow `JB721TiersHookStore` encoding: `tierId * 1_000_000_000 + tokenNumber`.
235
+ - Metadata IDs use the **code origin address** (uncloned implementation), not the clone: `JBMetadataResolver.getId("pay", codeOrigin)`.
123
236
 
124
237
  ## Example Integration
125
238
 
@@ -133,14 +246,16 @@ import {DefifaTierCashOutWeight} from "./structs/DefifaTierCashOutWeight.sol";
133
246
  DefifaTierParams[] memory tiers = new DefifaTierParams[](2);
134
247
  tiers[0] = DefifaTierParams({
135
248
  name: "Team A",
136
- reservedRate: 1001, // no reserves
249
+ // reservedRate maps to JB721's `reserveFrequency`: 1 reserved mint per N paid mints.
250
+ // 1001 means "1 reserve per 1001 mints" -- effectively no reserves for normal game sizes.
251
+ reservedRate: 1001,
137
252
  reservedTokenBeneficiary: address(0),
138
253
  encodedIPFSUri: bytes32(0),
139
254
  shouldUseReservedTokenBeneficiaryAsDefault: false
140
255
  });
141
256
  tiers[1] = DefifaTierParams({
142
257
  name: "Team B",
143
- reservedRate: 1001,
258
+ reservedRate: 1001, // effectively no reserves (see above)
144
259
  reservedTokenBeneficiary: address(0),
145
260
  encodedIPFSUri: bytes32(0),
146
261
  shouldUseReservedTokenBeneficiaryAsDefault: false
package/STYLE_GUIDE.md CHANGED
@@ -21,7 +21,7 @@ One contract/interface/struct/enum per file. Name the file after the type it con
21
21
 
22
22
  ```solidity
23
23
  // Contracts — pin to exact version
24
- pragma solidity 0.8.26;
24
+ pragma solidity 0.8.28;
25
25
 
26
26
  // Interfaces, structs, enums — caret for forward compatibility
27
27
  pragma solidity ^0.8.0;
@@ -326,7 +326,7 @@ Standard config across all repos:
326
326
 
327
327
  ```toml
328
328
  [profile.default]
329
- solc = '0.8.26'
329
+ solc = '0.8.28'
330
330
  evm_version = 'cancun'
331
331
  optimizer_runs = 200
332
332
  libs = ["node_modules", "lib"]