@ballkidz/defifa 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/ADMINISTRATION.md +3 -3
  2. package/AUDIT_INSTRUCTIONS.md +422 -0
  3. package/CRYPTO_ECON.md +5 -5
  4. package/RISKS.md +38 -335
  5. package/SKILLS.md +1 -1
  6. package/USER_JOURNEYS.md +691 -0
  7. package/package.json +7 -7
  8. package/script/Deploy.s.sol +14 -3
  9. package/script/helpers/DefifaDeploymentLib.sol +13 -15
  10. package/src/DefifaDeployer.sol +221 -192
  11. package/src/DefifaGovernor.sol +286 -276
  12. package/src/DefifaHook.sol +65 -32
  13. package/src/DefifaProjectOwner.sol +27 -4
  14. package/src/DefifaTokenUriResolver.sol +136 -134
  15. package/src/enums/DefifaGamePhase.sol +1 -1
  16. package/src/enums/DefifaScorecardState.sol +1 -1
  17. package/src/interfaces/IDefifaDeployer.sol +52 -50
  18. package/src/interfaces/IDefifaGamePhaseReporter.sol +2 -2
  19. package/src/interfaces/IDefifaGamePotReporter.sol +1 -1
  20. package/src/interfaces/IDefifaGovernor.sol +53 -54
  21. package/src/interfaces/IDefifaHook.sol +104 -103
  22. package/src/interfaces/IDefifaTokenUriResolver.sol +2 -2
  23. package/src/libraries/DefifaFontImporter.sol +11 -9
  24. package/src/libraries/DefifaHookLib.sol +66 -53
  25. package/src/structs/DefifaAttestations.sol +1 -1
  26. package/src/structs/DefifaDelegation.sol +1 -1
  27. package/src/structs/DefifaLaunchProjectData.sol +4 -4
  28. package/src/structs/DefifaOpsData.sol +1 -1
  29. package/src/structs/DefifaScorecard.sol +1 -1
  30. package/src/structs/DefifaTierCashOutWeight.sol +1 -1
  31. package/src/structs/DefifaTierParams.sol +2 -1
  32. package/test/DefifaAdversarialQuorum.t.sol +602 -0
  33. package/test/DefifaAuditLowGuards.t.sol +304 -0
  34. package/test/DefifaFeeAccounting.t.sol +37 -16
  35. package/test/DefifaGovernor.t.sol +37 -11
  36. package/test/DefifaHookRegressions.t.sol +14 -12
  37. package/test/DefifaMintCostInvariant.t.sol +31 -12
  38. package/test/DefifaNoContest.t.sol +33 -13
  39. package/test/DefifaSecurity.t.sol +45 -25
  40. package/test/DefifaUSDC.t.sol +44 -34
  41. package/test/Fork.t.sol +42 -40
  42. package/test/SVG.t.sol +2 -2
  43. package/test/TestAuditGaps.sol +982 -0
  44. package/test/TestQALastMile.t.sol +511 -0
  45. package/test/regression/FulfillmentBlocksRatification.t.sol +36 -30
  46. package/test/regression/GracePeriodBypass.t.sol +15 -10
@@ -0,0 +1,691 @@
1
+ # defifa-collection-deployer-v6 -- User Journeys
2
+
3
+ Complete interaction paths for every user role in the Defifa prediction game system. Each journey traces exact function signatures, parameters, and state changes.
4
+
5
+ ---
6
+
7
+ ## Roles
8
+
9
+ | Role | Description |
10
+ |------|-------------|
11
+ | **Game Creator** | Calls `DefifaDeployer.launchGameWith()` to deploy a new prediction game |
12
+ | **Player** | Pays ETH/tokens to mint NFT tiers during MINT phase |
13
+ | **Refunder** | Cashes out NFTs during MINT, REFUND, or NO_CONTEST for full mint price |
14
+ | **Scorer** | Submits a scorecard proposing tier cash-out weights |
15
+ | **Attestor** | NFT holder who attests (votes) for a submitted scorecard |
16
+ | **Ratifier** | Anyone who triggers ratification of a scorecard that reached quorum |
17
+ | **Winner** | Cashes out NFTs during COMPLETE phase at scored weights |
18
+ | **No-Contest Trigger** | Anyone who triggers the no-contest refund mechanism |
19
+ | **Reserve Minter** | Anyone who mints pending reserved tokens for a tier |
20
+ | **Fulfiller** | Anyone who calls `fulfillCommitmentsOf()` to distribute fees |
21
+
22
+ ---
23
+
24
+ ## Journey 1: Create a Game
25
+
26
+ **Actor:** Game Creator
27
+ **Phase:** Any (game is created and starts in COUNTDOWN)
28
+
29
+ ### Step 1: Prepare launch data
30
+
31
+ Build a `DefifaLaunchProjectData` struct:
32
+
33
+ ```solidity
34
+ DefifaLaunchProjectData({
35
+ name: "Super Bowl LXII",
36
+ projectUri: "ipfs://...",
37
+ contractUri: "ipfs://...",
38
+ baseUri: "ipfs://",
39
+ tiers: [
40
+ DefifaTierParams({
41
+ name: "Kansas City Chiefs",
42
+ reservedRate: 0,
43
+ reservedTokenBeneficiary: address(0),
44
+ encodedIPFSUri: bytes32(0),
45
+ shouldUseReservedTokenBeneficiaryAsDefault: false
46
+ }),
47
+ DefifaTierParams({
48
+ name: "Philadelphia Eagles",
49
+ reservedRate: 0,
50
+ reservedTokenBeneficiary: address(0),
51
+ encodedIPFSUri: bytes32(0),
52
+ shouldUseReservedTokenBeneficiaryAsDefault: false
53
+ })
54
+ // ... more tiers
55
+ ],
56
+ tierPrice: 0.01 ether,
57
+ token: JBAccountingContext({
58
+ token: JBConstants.NATIVE_TOKEN,
59
+ decimals: 18,
60
+ currency: JBCurrencyIds.ETH
61
+ }),
62
+ mintPeriodDuration: 7 days,
63
+ refundPeriodDuration: 1 days,
64
+ start: uint48(block.timestamp + 8 days),
65
+ splits: [], // Optional custom splits
66
+ attestationStartTime: 0, // 0 = block.timestamp at deploy
67
+ attestationGracePeriod: 0, // 0 = enforced minimum of 1 day
68
+ defaultAttestationDelegate: address(0), // 0 = each payer delegates to self
69
+ defaultTokenUriResolver: IJB721TokenUriResolver(address(0)), // use default SVG
70
+ terminal: jbMultiTerminal,
71
+ store: jb721TiersHookStore,
72
+ minParticipation: 1 ether, // Game needs >= 1 ETH to proceed
73
+ scorecardTimeout: 7 days // 7 days to ratify or NO_CONTEST
74
+ })
75
+ ```
76
+
77
+ ### Step 2: Launch
78
+
79
+ ```solidity
80
+ uint256 gameId = deployer.launchGameWith(launchProjectData);
81
+ ```
82
+
83
+ **What happens internally:**
84
+ 1. `DefifaDeployer` validates timing: `mintPeriodDuration > 0`, start >= now + refund + mint.
85
+ 2. Stores `_opsOf[gameId]` with token, start, durations, safety params.
86
+ 3. If user splits provided: copies them plus a Defifa fee split, stores via `CONTROLLER.setSplitGroupsOf()`.
87
+ 4. Creates `JB721TierConfig[]` from `DefifaTierParams[]`. All tiers share `tierPrice`, `initialSupply = 999_999_999`, `category = 0`.
88
+ 5. Clones `DefifaHook` deterministically: `Clones.cloneDeterministic(HOOK_CODE_ORIGIN, keccak256(abi.encodePacked(msg.sender, nonce)))`.
89
+ 6. Calls `hook.initialize(...)` with game config, tier names, URI resolver.
90
+ 7. Launches JB project via `CONTROLLER.launchProjectFor()` with 2-3 rulesets (MINT, optional REFUND, SCORING).
91
+ 8. Initializes governor: `GOVERNOR.initializeGame(gameId, attestationStartTime, attestationGracePeriod)`.
92
+ 9. Transfers hook ownership: `hook.transferOwnership(address(GOVERNOR))`.
93
+ 10. Registers hook in address registry.
94
+ 11. Emits `LaunchGame(gameId, hook, governor, uriResolver, msg.sender)`.
95
+
96
+ **Timing rules:**
97
+ - If `start == 0`: auto-calculated as `block.timestamp + mintPeriodDuration + refundPeriodDuration`.
98
+ - If `start > 0` and `mintPeriodDuration == 0`: mint duration auto-fills to `start - block.timestamp - refundPeriodDuration`.
99
+ - MINT ruleset `mustStartAtOrAfter = start - mintPeriodDuration - refundPeriodDuration`.
100
+ - REFUND ruleset `mustStartAtOrAfter = start - refundPeriodDuration`.
101
+ - SCORING ruleset `mustStartAtOrAfter = start`.
102
+
103
+ ---
104
+
105
+ ## Journey 2: Play a Game (Buy NFTs)
106
+
107
+ **Actor:** Player
108
+ **Phase:** MINT
109
+
110
+ ### Step 1: Prepare payment metadata
111
+
112
+ Encode the tier IDs to mint and optional attestation delegate:
113
+
114
+ ```solidity
115
+ // Tier IDs to mint (must be ascending order)
116
+ uint16[] memory tierIds = new uint16[](2);
117
+ tierIds[0] = 1; // "Kansas City Chiefs"
118
+ tierIds[1] = 1; // Mint 2 of the same tier
119
+
120
+ address attestationDelegate = address(0); // 0 = use default or self
121
+
122
+ bytes memory payMetadata = abi.encode(attestationDelegate, tierIds);
123
+ ```
124
+
125
+ Wrap in JBMetadataResolver format:
126
+
127
+ ```solidity
128
+ bytes memory metadata = metadataHelper.createMetadata({
129
+ id: JBMetadataResolver.getId("pay", hookCodeOrigin),
130
+ data: payMetadata
131
+ });
132
+ ```
133
+
134
+ ### Step 2: Pay the terminal
135
+
136
+ ```solidity
137
+ jbMultiTerminal.pay{value: 0.02 ether}({
138
+ projectId: gameId,
139
+ token: JBConstants.NATIVE_TOKEN,
140
+ amount: 0.02 ether, // Must equal tierPrice * numberOfTiers minted
141
+ beneficiary: msg.sender,
142
+ minReturnedTokens: 0,
143
+ memo: "Go Chiefs!",
144
+ metadata: metadata
145
+ });
146
+ ```
147
+
148
+ **What happens internally:**
149
+ 1. `JBMultiTerminal` processes payment, calls `DefifaHook.afterPayRecordedWith()`.
150
+ 2. Hook verifies: caller is terminal, currency matches `pricingCurrency`.
151
+ 3. Decodes `(attestationDelegate, tierIdsToMint)` from metadata.
152
+ 4. If `attestationDelegate == address(0)`: uses `defaultAttestationDelegate` or `context.payer`.
153
+ 5. Computes attestation units per unique tier via `DefifaHookLib.computeAttestationUnits()`.
154
+ 6. For each unique tier: delegates attestation if payer has no delegate set, transfers attestation units.
155
+ 7. Calls `_mintAll()`: records mint in store, increments `_totalMintCost`, mints ERC-721 tokens.
156
+ 8. Reverts with `DefifaHook_Overspending` if payment exceeds exact tier prices.
157
+
158
+ **Player receives:**
159
+ - NFT token(s) representing their chosen tier(s).
160
+ - Attestation delegation set to their chosen delegate (or themselves).
161
+ - Attestation units proportional to tier's `votingUnits` (used for scorecard governance).
162
+
163
+ ---
164
+
165
+ ## Journey 3: Refund During MINT Phase
166
+
167
+ **Actor:** Refunder
168
+ **Phase:** MINT
169
+
170
+ ### Step 1: Prepare cash-out metadata
171
+
172
+ ```solidity
173
+ uint256[] memory tokenIds = new uint256[](1);
174
+ tokenIds[0] = myTokenId;
175
+
176
+ bytes memory cashOutMetadata = metadataHelper.createMetadata({
177
+ id: JBMetadataResolver.getId("cashOut", hookCodeOrigin),
178
+ data: abi.encode(tokenIds)
179
+ });
180
+ ```
181
+
182
+ ### Step 2: Cash out
183
+
184
+ ```solidity
185
+ jbMultiTerminal.cashOutTokensOf({
186
+ holder: msg.sender,
187
+ projectId: gameId,
188
+ cashOutCount: 0, // 0 for NFT cash-outs
189
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
190
+ minTokensReclaimed: 0.01 ether, // Expect full tier price back
191
+ beneficiary: payable(msg.sender),
192
+ metadata: cashOutMetadata
193
+ });
194
+ ```
195
+
196
+ **What happens internally:**
197
+ 1. `beforeCashOutRecordedWith` computes `cashOutCount = cumulativeMintPrice` (full refund during MINT).
198
+ 2. Terminal computes reclaim amount from the bonding curve with `cashOutTaxRate = 0`.
199
+ 3. `afterCashOutRecordedWith` burns the NFT(s), decrements `_totalMintCost`.
200
+ 4. During MINT phase: `tokensRedeemedFrom` is NOT incremented (only during COMPLETE).
201
+ 5. No fee tokens distributed (game not COMPLETE).
202
+ 6. ETH returned to beneficiary at exact mint price.
203
+
204
+ ---
205
+
206
+ ## Journey 4: Refund During REFUND Phase
207
+
208
+ **Actor:** Refunder
209
+ **Phase:** REFUND
210
+
211
+ Identical to Journey 3. The REFUND phase has `pausePay: true` (no new mints) but cash-outs still return full mint price. The `cashOutTaxRate = 0` and the hook returns `cashOutCount = cumulativeMintPrice`.
212
+
213
+ ---
214
+
215
+ ## Journey 5: Submit a Scorecard
216
+
217
+ **Actor:** Scorer (anyone)
218
+ **Phase:** SCORING
219
+
220
+ ### Step 1: Prepare tier weights
221
+
222
+ All weights must sum to exactly `TOTAL_CASHOUT_WEIGHT` (1e18). Tier IDs must be in ascending order.
223
+
224
+ ```solidity
225
+ DefifaTierCashOutWeight[] memory tierWeights = new DefifaTierCashOutWeight[](3);
226
+ tierWeights[0] = DefifaTierCashOutWeight({id: 1, cashOutWeight: 500_000_000_000_000_000}); // 50%
227
+ tierWeights[1] = DefifaTierCashOutWeight({id: 2, cashOutWeight: 300_000_000_000_000_000}); // 30%
228
+ tierWeights[2] = DefifaTierCashOutWeight({id: 3, cashOutWeight: 200_000_000_000_000_000}); // 20%
229
+ // Sum = 1e18
230
+ ```
231
+
232
+ ### Step 2: Submit
233
+
234
+ ```solidity
235
+ uint256 scorecardId = governor.submitScorecardFor(gameId, tierWeights);
236
+ ```
237
+
238
+ **What happens internally:**
239
+ 1. Verifies: game initialized, no ratified scorecard, game in SCORING phase.
240
+ 2. For each weight > 0: verifies `currentSupplyOfTier(tierId) > 0` (cannot assign weight to unminted tiers).
241
+ 3. Hashes the scorecard: `keccak256(abi.encode(dataHook, abi.encodeWithSelector(setTierCashOutWeightsTo.selector, tierWeights)))`.
242
+ 4. Reverts with `DefifaGovernor_DuplicateScorecard` if this exact scorecard was already submitted.
243
+ 5. Sets `attestationsBegin = max(block.timestamp, attestationStartTime)`.
244
+ 6. Sets `gracePeriodEnds = attestationsBegin + attestationGracePeriod`.
245
+ 7. If sender is `defaultAttestationDelegate`: stores as `defaultAttestationDelegateProposalOf`.
246
+ 8. Emits `ScorecardSubmitted(gameId, scorecardId, tierWeights, isDefault, msg.sender)`.
247
+
248
+ **Scorecard state:** PENDING (until `attestationsBegin`) or ACTIVE (if attestations start immediately).
249
+
250
+ ---
251
+
252
+ ## Journey 6: Attest to a Scorecard
253
+
254
+ **Actor:** Attestor (NFT holder or delegate)
255
+ **Phase:** SCORING
256
+
257
+ ### Step 1: Get scorecard ID
258
+
259
+ Either compute it or use the ID from the `ScorecardSubmitted` event:
260
+
261
+ ```solidity
262
+ uint256 scorecardId = governor.scorecardIdOf(hookAddress, tierWeights);
263
+ ```
264
+
265
+ ### Step 2: Attest
266
+
267
+ ```solidity
268
+ uint256 weight = governor.attestToScorecardFrom(gameId, scorecardId);
269
+ ```
270
+
271
+ **What happens internally:**
272
+ 1. Verifies: game in SCORING phase, scorecard is ACTIVE or SUCCEEDED.
273
+ 2. Verifies: `!hasAttested[msg.sender]` for this scorecard. Reverts with `DefifaGovernor_AlreadyAttested` otherwise.
274
+ 3. Computes attestation weight at `attestationsBegin` timestamp:
275
+ - For each tier: `MAX_ATTESTATION_POWER_TIER * (account's checkpoint units / tier's total checkpoint units)`.
276
+ - Uses `getPastTierAttestationUnitsOf()` (snapshot, not live).
277
+ 4. Increments `_attestations.count += weight`.
278
+ 5. Marks `hasAttested[msg.sender] = true`.
279
+ 6. Emits `ScorecardAttested(gameId, scorecardId, weight, msg.sender)`.
280
+
281
+ **Attestation power depends on:**
282
+ - How many NFTs the attestor (or their delegate) held at `attestationsBegin` timestamp.
283
+ - What fraction of each tier's total supply those NFTs represent.
284
+ - Each tier caps at `MAX_ATTESTATION_POWER_TIER` (1e9) regardless of how many tokens exist in that tier.
285
+
286
+ ---
287
+
288
+ ## Journey 7: Ratify a Scorecard
289
+
290
+ **Actor:** Ratifier (anyone)
291
+ **Phase:** SCORING (scorecard in SUCCEEDED state)
292
+
293
+ ### Precondition
294
+
295
+ A scorecard must be in SUCCEEDED state:
296
+ - `attestationsBegin <= block.timestamp`
297
+ - `gracePeriodEnds <= block.timestamp`
298
+ - `attestation count >= quorum`
299
+
300
+ ### Step 1: Ratify
301
+
302
+ ```solidity
303
+ uint256 scorecardId = governor.ratifyScorecardFrom(gameId, tierWeights);
304
+ ```
305
+
306
+ **What happens internally:**
307
+ 1. Verifies: no prior ratification (`ratifiedScorecardIdOf[gameId] == 0`).
308
+ 2. Computes `scorecardId` from `tierWeights`, verifies it matches a SUCCEEDED scorecard.
309
+ 3. Stores `ratifiedScorecardIdOf[gameId] = scorecardId`.
310
+ 4. Executes scorecard via low-level call: `dataHook.call(abi.encodeWithSelector(setTierCashOutWeightsTo.selector, tierWeights))`.
311
+ - This calls `DefifaHook.setTierCashOutWeightsTo()` which validates weights sum to `TOTAL_CASHOUT_WEIGHT` and sets `cashOutWeightIsSet = true`.
312
+ 5. Calls `DefifaDeployer.fulfillCommitmentsOf(gameId)`:
313
+ - Sends fee payouts via `terminal.sendPayoutsOf()` (try-catch: if payout fails, emits `CommitmentPayoutFailed` and sets sentinel).
314
+ - Queues final ruleset with no payout limits.
315
+ - Exceptional failures (e.g., `queueRulesetsOf` failure) propagate and revert ratification.
316
+ 6. Emits `ScorecardRatified(gameId, scorecardId, msg.sender)`.
317
+
318
+ **Game state transitions to:** COMPLETE (because `cashOutWeightIsSet == true`).
319
+
320
+ ---
321
+
322
+ ## Journey 8: Cash Out as Winner
323
+
324
+ **Actor:** Winner
325
+ **Phase:** COMPLETE
326
+
327
+ ### Step 1: Check claimable amounts
328
+
329
+ ```solidity
330
+ // Check cash-out value
331
+ uint256 weight = hook.cashOutWeightOf(myTokenId);
332
+ // weight > 0 means this tier won something
333
+
334
+ // Check fee token claims
335
+ (uint256 defifaTokens, uint256 nanaTokens) = hook.tokensClaimableFor(tokenIds);
336
+ ```
337
+
338
+ ### Step 2: Cash out
339
+
340
+ ```solidity
341
+ uint256[] memory tokenIds = new uint256[](1);
342
+ tokenIds[0] = myTokenId;
343
+
344
+ bytes memory cashOutMetadata = metadataHelper.createMetadata({
345
+ id: JBMetadataResolver.getId("cashOut", hookCodeOrigin),
346
+ data: abi.encode(tokenIds)
347
+ });
348
+
349
+ jbMultiTerminal.cashOutTokensOf({
350
+ holder: msg.sender,
351
+ projectId: gameId,
352
+ cashOutCount: 0,
353
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
354
+ minTokensReclaimed: expectedAmount,
355
+ beneficiary: payable(msg.sender),
356
+ metadata: cashOutMetadata
357
+ });
358
+ ```
359
+
360
+ **What happens internally:**
361
+ 1. `beforeCashOutRecordedWith`: computes `cashOutCount = mulDiv(surplus + amountRedeemed, cumulativeCashOutWeight, TOTAL_CASHOUT_WEIGHT)`.
362
+ 2. Terminal sends reclaimed ETH to beneficiary.
363
+ 3. `afterCashOutRecordedWith`: burns NFTs, increments `tokensRedeemedFrom[tierId]`, increments `amountRedeemed`.
364
+ 4. Distributes fee tokens: `_claimTokensFor(holder, cumulativeMintPrice, _totalMintCost)`.
365
+ - Transfers proportional share of `$DEFIFA` and `$NANA` tokens held by the hook.
366
+ 5. Decrements `_totalMintCost -= cumulativeMintPrice`.
367
+
368
+ **Reclaim calculation:**
369
+ ```
370
+ perTokenWeight = tierCashOutWeight[tierId] / totalTokensForCashoutInTier
371
+ reclaimAmount = mulDiv(surplus + amountRedeemed, perTokenWeight, TOTAL_CASHOUT_WEIGHT)
372
+ ```
373
+
374
+ Where `totalTokensForCashoutInTier = initialSupply - remainingSupply - (burnedTokens - tokensRedeemedFrom[tierId])`.
375
+
376
+ ---
377
+
378
+ ## Journey 9: Cash Out from Losing Tier
379
+
380
+ **Actor:** Holder of a zero-weight tier
381
+ **Phase:** COMPLETE
382
+
383
+ If a tier received `cashOutWeight = 0` in the ratified scorecard:
384
+
385
+ ```solidity
386
+ // cashOutWeightOf(tokenId) returns 0
387
+ // beforeCashOutRecordedWith returns cashOutCount = 0
388
+ // afterCashOutRecordedWith: reclaimedAmount.value == 0
389
+ // _claimTokensFor is called -- if fee tokens exist, they are distributed
390
+ // If no fee tokens distributed either → reverts with DefifaHook_NothingToClaim
391
+ ```
392
+
393
+ **Result:** Holders of losing tiers can only cash out if fee tokens (`$DEFIFA`/`$NANA`) are available. They receive fee tokens proportional to their mint cost but zero ETH. If no fee tokens exist at all, the cash-out reverts.
394
+
395
+ ---
396
+
397
+ ## Journey 10: No-Contest via Minimum Participation
398
+
399
+ **Actor:** Any user
400
+ **Phase:** SCORING (when balance < minParticipation)
401
+
402
+ ### Scenario
403
+
404
+ Game had `minParticipation = 10 ether`. During MINT, 5 ETH was deposited but then refunded down to 3 ETH. When SCORING begins, `currentGamePhaseOf()` checks:
405
+
406
+ ```solidity
407
+ if (_ops.minParticipation > 0) {
408
+ uint256 _balance = terminal.STORE().balanceOf(terminal, gameId, token);
409
+ if (_balance < _ops.minParticipation) return DefifaGamePhase.NO_CONTEST;
410
+ }
411
+ ```
412
+
413
+ Since 3 ETH < 10 ETH, the game is NO_CONTEST.
414
+
415
+ ### Step 1: Trigger no-contest
416
+
417
+ ```solidity
418
+ deployer.triggerNoContestFor(gameId);
419
+ ```
420
+
421
+ **What happens internally:**
422
+ 1. Verifies `currentGamePhaseOf(gameId) == NO_CONTEST`.
423
+ 2. Verifies `!noContestTriggeredFor[gameId]`.
424
+ 3. Sets `noContestTriggeredFor[gameId] = true`.
425
+ 4. Queues new ruleset: no `fundAccessLimitGroups`, making entire balance = surplus.
426
+ 5. Emits `QueuedNoContest(gameId, msg.sender)`.
427
+
428
+ ### Step 2: Cash out (full refund)
429
+
430
+ After triggering, users can cash out at mint price (same as Journey 3/4). The new ruleset has no payout limits, so all balance is surplus and `cashOutTaxRate = 0`.
431
+
432
+ ---
433
+
434
+ ## Journey 11: No-Contest via Scorecard Timeout
435
+
436
+ **Actor:** Any user
437
+ **Phase:** SCORING (when timeout elapsed without ratification)
438
+
439
+ ### Scenario
440
+
441
+ Game had `scorecardTimeout = 7 days`. Scoring started 8 days ago. No scorecard was ratified.
442
+
443
+ ```solidity
444
+ if (_ops.scorecardTimeout > 0 && block.timestamp > _currentRuleset.start + _ops.scorecardTimeout) {
445
+ return DefifaGamePhase.NO_CONTEST;
446
+ }
447
+ ```
448
+
449
+ ### Steps
450
+
451
+ Same as Journey 10: call `triggerNoContestFor()`, then cash out.
452
+
453
+ **Important:** If a scorecard is ratified BEFORE the timeout, the game transitions to COMPLETE and the timeout becomes irrelevant. `cashOutWeightIsSet` is checked before the timeout condition in `currentGamePhaseOf()`.
454
+
455
+ ---
456
+
457
+ ## Journey 12: Delegate Attestation Power
458
+
459
+ **Actor:** Player (NFT holder)
460
+ **Phase:** MINT only
461
+
462
+ ### Single tier delegation
463
+
464
+ ```solidity
465
+ hook.setTierDelegateTo(trustedDelegate, tierId);
466
+ ```
467
+
468
+ ### Multiple tier delegations
469
+
470
+ ```solidity
471
+ DefifaDelegation[] memory delegations = new DefifaDelegation[](2);
472
+ delegations[0] = DefifaDelegation({delegatee: trustedDelegate, tierId: 1});
473
+ delegations[1] = DefifaDelegation({delegatee: anotherDelegate, tierId: 2});
474
+
475
+ hook.setTierDelegatesTo(delegations);
476
+ ```
477
+
478
+ **Restrictions:**
479
+ - Only during MINT phase. Reverts with `DefifaHook_DelegateChangesUnavailableInThisPhase` after MINT.
480
+ - Cannot delegate to `address(0)`. Reverts with `DefifaHook_DelegateAddressZero`.
481
+ - On NFT transfer after MINT: auto-delegates to recipient if recipient has no delegate (DefifaHook lines 1027-1031).
482
+
483
+ ---
484
+
485
+ ## Journey 13: Mint Reserved Tokens
486
+
487
+ **Actor:** Anyone
488
+ **Phase:** After MINT (reserved minting is paused during MINT via `pauseMintPendingReserves: true`)
489
+
490
+ ### Single tier
491
+
492
+ ```solidity
493
+ hook.mintReservesFor(tierId, count);
494
+ ```
495
+
496
+ ### Multiple tiers
497
+
498
+ ```solidity
499
+ JB721TiersMintReservesConfig[] memory configs = new JB721TiersMintReservesConfig[](1);
500
+ configs[0] = JB721TiersMintReservesConfig({tierId: 1, count: 5});
501
+
502
+ hook.mintReservesFor(configs);
503
+ ```
504
+
505
+ **What happens internally:**
506
+ 1. Checks `pauseMintPendingReserves` is false in current ruleset metadata.
507
+ 2. Gets `reserveBeneficiary` from store for the tier.
508
+ 3. If beneficiary has no delegate: auto-delegates to `defaultAttestationDelegate` or self.
509
+ 4. Records mint in store, increments `_totalMintCost += tier.price * count`.
510
+ 5. Mints ERC-721 tokens to the reserve beneficiary.
511
+ 6. Transfers attestation units to the beneficiary's delegate.
512
+
513
+ **Note:** Reserved mints inflate `_totalMintCost` even though no ETH was paid. This dilutes paid minters' share of fee tokens. This is by design (see RISKS.md, RISK-4).
514
+
515
+ ---
516
+
517
+ ## Journey 14: Fulfill Commitments Separately
518
+
519
+ **Actor:** Anyone
520
+ **Phase:** COMPLETE (after scorecard ratification)
521
+
522
+ `fulfillCommitmentsOf()` is called automatically during ratification. If `sendPayoutsOf` fails internally, the try-catch in `fulfillCommitmentsOf` emits `CommitmentPayoutFailed`, sets the sentinel value, and still queues the final ruleset. The fee amount stays in the pot.
523
+
524
+ If needed, `fulfillCommitmentsOf` can be called again manually — but since the sentinel is already set and the final ruleset already queued, it returns immediately (idempotent):
525
+
526
+ ```solidity
527
+ deployer.fulfillCommitmentsOf(gameId);
528
+ ```
529
+
530
+ **What happens internally:**
531
+ 1. If `fulfilledCommitmentsOf[gameId] != 0`: returns immediately (idempotent).
532
+ 2. Requires `cashOutWeightIsSet == true`.
533
+ 3. Computes fee from pot: `mulDiv(pot, _commitmentPercentOf[gameId], SPLITS_TOTAL_PERCENT)`.
534
+ 4. Try-catch: calls `terminal.sendPayoutsOf()` to distribute fees to splits. On failure, emits `CommitmentPayoutFailed` and sets sentinel.
535
+ 5. Queues final ruleset with no payout limits.
536
+
537
+ ---
538
+
539
+ ## Journey 15: Transfer NFT to Another Player
540
+
541
+ **Actor:** NFT holder
542
+ **Phase:** Any (unless `transfersPausable` is set and transfers are paused)
543
+
544
+ ```solidity
545
+ hook.transferFrom(from, to, tokenId);
546
+ // or
547
+ hook.safeTransferFrom(from, to, tokenId);
548
+ ```
549
+
550
+ **What happens internally (DefifaHook._update):**
551
+ 1. Gets tier info from store.
552
+ 2. Calls `super._update()` for standard ERC-721 transfer.
553
+ 3. If `transfersPausable` and transfers paused in current ruleset: reverts.
554
+ 4. If first transfer of this token: stores `_firstOwnerOf[tokenId] = from`.
555
+ 5. Records transfer in store: `store.recordTransferForTier(tierId, from, to)`.
556
+ 6. Skips attestation transfer on mint (handled separately in `_processPayment`).
557
+ 7. On regular transfer: `_transferTierAttestationUnits(from, to, tierId, tier.votingUnits)`.
558
+ - If recipient has no delegate set: auto-delegates to self.
559
+ - Moves attestation units from sender's delegate to recipient's delegate.
560
+
561
+ ---
562
+
563
+ ## Journey 16: Query Game State
564
+
565
+ **Actor:** Frontend / anyone
566
+
567
+ ### Check game phase
568
+
569
+ ```solidity
570
+ DefifaGamePhase phase = deployer.currentGamePhaseOf(gameId);
571
+ ```
572
+
573
+ ### Check game pot
574
+
575
+ ```solidity
576
+ (uint256 pot, address token, uint256 decimals) = deployer.currentGamePotOf(gameId, false);
577
+ // includeCommitments = true adds fulfilled fee amounts back
578
+ ```
579
+
580
+ ### Check timing
581
+
582
+ ```solidity
583
+ (uint48 start, uint24 mintDuration, uint24 refundDuration) = deployer.timesFor(gameId);
584
+ ```
585
+
586
+ ### Check safety params
587
+
588
+ ```solidity
589
+ (uint256 minParticipation, uint32 scorecardTimeout) = deployer.safetyParamsOf(gameId);
590
+ ```
591
+
592
+ ### Check scorecard state
593
+
594
+ ```solidity
595
+ DefifaScorecardState state = governor.stateOf(gameId, scorecardId);
596
+ // PENDING → ACTIVE → SUCCEEDED → RATIFIED (or DEFEATED)
597
+ ```
598
+
599
+ ### Check attestation status
600
+
601
+ ```solidity
602
+ uint256 count = governor.attestationCountOf(gameId, scorecardId);
603
+ uint256 needed = governor.quorum(gameId);
604
+ bool hasAttested = governor.hasAttestedTo(gameId, scorecardId, account);
605
+ ```
606
+
607
+ ### Check cash-out value
608
+
609
+ ```solidity
610
+ // Single token
611
+ uint256 weight = hook.cashOutWeightOf(tokenId);
612
+
613
+ // Multiple tokens
614
+ uint256[] memory ids = new uint256[](2);
615
+ ids[0] = tokenId1;
616
+ ids[1] = tokenId2;
617
+ uint256 totalWeight = hook.cashOutWeightOf(ids);
618
+ ```
619
+
620
+ ### Check fee token claims
621
+
622
+ ```solidity
623
+ (uint256 defifaTokens, uint256 nanaTokens) = hook.tokensClaimableFor(tokenIds);
624
+ (uint256 defifaBalance, uint256 nanaBalance) = hook.tokenAllocations();
625
+ ```
626
+
627
+ ---
628
+
629
+ ## Error Conditions by Journey
630
+
631
+ ### Payment Errors (Journey 2)
632
+
633
+ | Error | Condition |
634
+ |-------|-----------|
635
+ | `DefifaHook_WrongCurrency` | Payment currency does not match `pricingCurrency` |
636
+ | `DefifaHook_NothingToMint` | No tier IDs in metadata, or metadata not found |
637
+ | `DefifaHook_Overspending` | Payment amount exceeds exact cost of tiers minted |
638
+ | `DefifaHook_BadTierOrder` | Tier IDs in metadata not in ascending order |
639
+ | `JB721Hook_InvalidPay` | Caller not a terminal, or wrong project ID, or ETH sent to hook |
640
+
641
+ ### Cash-Out Errors (Journeys 3, 4, 8, 9)
642
+
643
+ | Error | Condition |
644
+ |-------|-----------|
645
+ | `DefifaHook_Unauthorized(tokenId, owner, caller)` | Token holder in context does not own the token |
646
+ | `DefifaHook_NothingToClaim` | Reclaimed amount is 0 AND no fee tokens distributed |
647
+ | `JB721Hook_InvalidCashOut` | Caller not a terminal, or wrong project ID |
648
+
649
+ ### Scorecard Errors (Journeys 5, 6, 7)
650
+
651
+ | Error | Condition |
652
+ |-------|-----------|
653
+ | `DefifaGovernor_NotAllowed` | Game not in SCORING, or scorecard not in correct state |
654
+ | `DefifaGovernor_UnownedProposedCashoutValue` | Weight > 0 assigned to tier with 0 supply |
655
+ | `DefifaGovernor_DuplicateScorecard` | Identical scorecard already submitted |
656
+ | `DefifaGovernor_AlreadyAttested` | Account already attested to this scorecard |
657
+ | `DefifaGovernor_AlreadyRatified` | Game already has a ratified scorecard |
658
+ | `DefifaGovernor_UnknownProposal` | Scorecard ID has no submission record |
659
+ | `DefifaHook_InvalidCashoutWeights` | Weights do not sum to TOTAL_CASHOUT_WEIGHT |
660
+ | `DefifaHook_BadTierOrder` | Tier IDs not in ascending order |
661
+ | `DefifaHook_InvalidTierId` | Tier not in category 0, or tier ID > maxTierId |
662
+ | `DefifaHook_GameIsntScoringYet` | Game not in SCORING phase when setting weights |
663
+ | `DefifaHook_CashoutWeightsAlreadySet` | Weights already set (double-set attempt) |
664
+
665
+ ### No-Contest Errors (Journeys 10, 11)
666
+
667
+ | Error | Condition |
668
+ |-------|-----------|
669
+ | `DefifaDeployer_NotNoContest` | Game not in NO_CONTEST when triggering |
670
+ | `DefifaDeployer_NoContestAlreadyTriggered` | Already triggered for this game |
671
+
672
+ ### Delegation Errors (Journey 12)
673
+
674
+ | Error | Condition |
675
+ |-------|-----------|
676
+ | `DefifaHook_DelegateAddressZero` | Delegatee is address(0) |
677
+ | `DefifaHook_DelegateChangesUnavailableInThisPhase` | Not in MINT phase |
678
+
679
+ ### Fulfillment Errors (Journey 14)
680
+
681
+ | Error | Condition |
682
+ |-------|-----------|
683
+ | `DefifaDeployer_CantFulfillYet` | `cashOutWeightIsSet == false` |
684
+
685
+ ### Game Creation Errors (Journey 1)
686
+
687
+ | Error | Condition |
688
+ |-------|-----------|
689
+ | `DefifaDeployer_InvalidGameConfiguration` | Timing constraints violated: `mintPeriodDuration == 0` or `start < block.timestamp + refund + mint` |
690
+ | `DefifaDeployer_SplitsDontAddUp` | User splits + protocol fees exceed 100% |
691
+ | `DefifaDeployer_InvalidGameConfiguration` | JB project ID mismatch (front-run) |