@ballkidz/defifa 0.0.17 → 0.0.19

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.
package/USER_JOURNEYS.md CHANGED
@@ -1,1035 +1,69 @@
1
- # defifa-collection-deployer-v6 -- User Journeys
1
+ # User Journeys
2
2
 
3
- Complete interaction paths for every user role in the Defifa prediction game system. Each journey traces exact function signatures, parameters, state changes, events, and edge cases.
3
+ ## Who This Repo Serves
4
4
 
5
- ---
5
+ - teams launching Defifa prediction games
6
+ - players minting outcome pieces and later redeeming winning positions
7
+ - participants submitting, attesting to, and ratifying scorecards
8
+ - operators handling refund, no-contest, and fee-settlement edges
6
9
 
7
- ## Roles
10
+ ## Journey 1: Launch A Defifa Game
8
11
 
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 |
12
+ **Starting state:** the team knows the countdown, mint window, scoring mechanics, and payout assumptions for a new game.
21
13
 
22
- ---
14
+ **Success:** the game launches as a staged Juicebox project with hook, governor, and metadata surfaces all aligned.
23
15
 
24
- ## Journey 1: Create a Game
16
+ **Flow**
17
+ 1. Use `DefifaDeployer` with launch config, tier params, governance settings, and fee commitments.
18
+ 2. The deployer launches the project, clones or wires the game hook, and initializes governance through `DefifaGovernor`.
19
+ 3. The game now has a defined lifecycle instead of being a plain NFT sale.
25
20
 
26
- **Entry point:** `DefifaDeployer.launchGameWith(DefifaLaunchProjectData memory launchProjectData) external returns (uint256 gameId)`
21
+ ## Journey 2: Participate As A Player During The Mint Phase
27
22
 
28
- **Who can call:** Anyone (no access control).
23
+ **Starting state:** the game is in its countdown or live mint window and players want to buy outcome pieces.
29
24
 
30
- **Actor:** Game Creator
31
- **Phase:** Any (game is created and starts in COUNTDOWN)
25
+ **Success:** the player mints the intended game pieces and their payment becomes part of the prize pot.
32
26
 
33
- ### Parameters
27
+ **Flow**
28
+ 1. Wait until the lifecycle enters the mintable phase.
29
+ 2. Pay into the game to mint the chosen outcome NFTs through `DefifaHook`.
30
+ 3. The treasury accumulates the prize pot and the player's position is now represented by the minted pieces.
34
31
 
35
- - `launchProjectData.name` -- Game name (e.g. `"Super Bowl LXII"`).
36
- - `launchProjectData.projectUri` -- IPFS URI for project metadata.
37
- - `launchProjectData.contractUri` -- Contract-level metadata URI.
38
- - `launchProjectData.baseUri` -- Base URI for token metadata.
39
- - `launchProjectData.tiers` -- Array of `DefifaTierParams` (name, reservedRate, reservedTokenBeneficiary, encodedIPFSUri, shouldUseReservedTokenBeneficiaryAsDefault).
40
- - `launchProjectData.tierPrice` -- Uniform price per NFT across all tiers.
41
- - `launchProjectData.token` -- `JBAccountingContext` (token address, decimals, currency).
42
- - `launchProjectData.mintPeriodDuration` -- Duration of MINT phase in seconds.
43
- - `launchProjectData.refundPeriodDuration` -- Duration of REFUND phase in seconds (0 = no refund phase).
44
- - `launchProjectData.start` -- Unix timestamp when SCORING begins (0 = auto-calculate).
45
- - `launchProjectData.splits` -- Optional custom splits for fee distribution.
46
- - `launchProjectData.attestationStartTime` -- Timestamp when attestation begins (0 = `block.timestamp` at deploy).
47
- - `launchProjectData.attestationGracePeriod` -- Minimum grace period before ratification (0 = enforced minimum of 1 day).
48
- - `launchProjectData.defaultAttestationDelegate` -- Default attestation delegate (0 = each beneficiary delegates to self).
49
- - `launchProjectData.defaultTokenUriResolver` -- Token URI resolver (0 = use default SVG).
50
- - `launchProjectData.terminal` -- `IJBTerminal` instance (e.g. a `JBMultiTerminal`).
51
- - `launchProjectData.store` -- `JB721TiersHookStore` instance.
52
- - `launchProjectData.minParticipation` -- Minimum treasury balance for game to proceed to SCORING.
53
- - `launchProjectData.scorecardTimeout` -- Max time after SCORING begins for a scorecard to be ratified.
32
+ ## Journey 3: Handle Refund Or No-Contest Outcomes
54
33
 
55
- ### Step 1: Prepare launch data
34
+ **Starting state:** the game cannot settle normally, either because the refund window is triggered or because governance fails to reach a contestable result.
56
35
 
57
- Build a `DefifaLaunchProjectData` struct:
36
+ **Success:** participants can exit under the repo's explicit failure-mode rules instead of ad hoc admin intervention.
58
37
 
59
- ```solidity
60
- DefifaLaunchProjectData({
61
- name: "Super Bowl LXII",
62
- projectUri: "ipfs://...",
63
- contractUri: "ipfs://...",
64
- baseUri: "ipfs://",
65
- tiers: [
66
- DefifaTierParams({
67
- name: "Kansas City Chiefs",
68
- reservedRate: 0,
69
- reservedTokenBeneficiary: address(0),
70
- encodedIPFSUri: bytes32(0),
71
- shouldUseReservedTokenBeneficiaryAsDefault: false
72
- }),
73
- DefifaTierParams({
74
- name: "Philadelphia Eagles",
75
- reservedRate: 0,
76
- reservedTokenBeneficiary: address(0),
77
- encodedIPFSUri: bytes32(0),
78
- shouldUseReservedTokenBeneficiaryAsDefault: false
79
- })
80
- // ... more tiers
81
- ],
82
- tierPrice: 0.01 ether,
83
- token: JBAccountingContext({
84
- token: JBConstants.NATIVE_TOKEN,
85
- decimals: 18,
86
- currency: JBCurrencyIds.ETH
87
- }),
88
- mintPeriodDuration: 7 days,
89
- refundPeriodDuration: 1 days,
90
- start: uint48(block.timestamp + 8 days),
91
- splits: [], // Optional custom splits
92
- attestationStartTime: 0, // 0 = block.timestamp at deploy
93
- attestationGracePeriod: 0, // 0 = enforced minimum of 1 day
94
- defaultAttestationDelegate: address(0), // 0 = each beneficiary delegates to self
95
- defaultTokenUriResolver: IJB721TokenUriResolver(address(0)), // use default SVG
96
- terminal: IJBTerminal(address(jbMultiTerminal)),
97
- store: jb721TiersHookStore,
98
- minParticipation: 1 ether, // Game needs >= 1 ETH to proceed
99
- scorecardTimeout: 7 days // 7 days to ratify or NO_CONTEST
100
- })
101
- ```
38
+ **Flow**
39
+ 1. Observe the current game phase and whether it has entered refund or no-contest handling.
40
+ 2. Use the game-defined exit path for participants rather than assuming the winning-scorecard path will eventually resolve.
41
+ 3. Keep treasury and piece-state assumptions aligned with the phase actually reached.
102
42
 
103
- ### Step 2: Launch
43
+ ## Journey 4: Submit, Attest To, And Ratify A Scorecard
104
44
 
105
- ```solidity
106
- uint256 gameId = deployer.launchGameWith(launchProjectData);
107
- ```
45
+ **Starting state:** minting is over and the game is in its scoring phase.
108
46
 
109
- ### State changes
47
+ **Success:** a valid scorecard reaches quorum, survives any grace period, and becomes the game's settled result.
110
48
 
111
- 1. `DefifaDeployer._opsOf[gameId]` -- Stores `DefifaOpsData` with token, start, durations, safety params.
112
- 2. `DefifaDeployer._commitmentPercentOf[gameId]` -- Stores total absolute split percent for fee distribution.
113
- 3. `DefifaDeployer._nonce` -- Incremented for deterministic clone salt.
114
- 4. `DefifaHook.store` -- Set to the provided `JB721TiersHookStore`.
115
- 5. `DefifaHook.rulesets` -- Set to `CONTROLLER.RULESETS()`.
116
- 6. `DefifaHook.pricingCurrency` -- Set to `launchProjectData.token.currency`.
117
- 7. `DefifaHook.gamePhaseReporter` -- Set to `DefifaDeployer` (this).
118
- 8. `DefifaHook.gamePotReporter` -- Set to `DefifaDeployer` (this).
119
- 9. `DefifaHook.defaultAttestationDelegate` -- Set to the provided address.
120
- 10. `DefifaHook.baseURI` -- Set if non-empty.
121
- 11. `DefifaHook.contractURI` -- Set if non-empty.
122
- 12. `DefifaGovernor._packedScorecardInfoOf[gameId]` -- Packed attestation start time + grace period.
123
- 13. JB project created via `CONTROLLER.launchProjectFor()` with 2-3 rulesets (MINT, optional REFUND, SCORING).
49
+ **Flow**
50
+ 1. A participant submits a scorecard through `DefifaGovernor`.
51
+ 2. Holders attest, delegate where permitted, and push the preferred scorecard toward quorum.
52
+ 3. After the grace period, the governor ratifies the winning scorecard if it still satisfies the game's rules.
53
+ 4. `DefifaHook` updates the relevant cash-out weights for settlement.
124
54
 
125
- ### Events
55
+ ## Journey 5: Redeem Winning Pieces And Settle The Pot
126
56
 
127
- - `LaunchGame(uint256 indexed gameId, IDefifaHook indexed hook, IDefifaGovernor indexed governor, IJB721TokenUriResolver tokenUriResolver, address caller)` -- Emitted by `DefifaDeployer` on successful launch.
128
- - `GameInitialized(uint256 indexed gameId, uint256 attestationStartTime, uint256 attestationGracePeriod, address caller)` -- Emitted by `DefifaGovernor` when `initializeGame` is called internally.
57
+ **Starting state:** the game has a ratified result and winning positions are now known.
129
58
 
130
- ### Edge cases
59
+ **Success:** holders of winning pieces burn or cash out them for their share of the prize pot.
131
60
 
132
- - `DefifaDeployer_InvalidGameConfiguration` -- `mintPeriodDuration == 0` or `start < block.timestamp + refundPeriodDuration + mintPeriodDuration`.
133
- - `DefifaDeployer_InvalidGameConfiguration` -- JB project ID mismatch (front-run by another project creation).
134
- - `DefifaDeployer_SplitsDontAddUp` -- User splits + protocol fees exceed 100%.
135
- - If `start == 0`: auto-calculated as `block.timestamp + mintPeriodDuration + refundPeriodDuration`.
136
- - If `start > 0` and `mintPeriodDuration == 0`: mint duration auto-fills to `start - block.timestamp - refundPeriodDuration`.
137
- - MINT ruleset `mustStartAtOrAfter = start - mintPeriodDuration - refundPeriodDuration`.
138
- - REFUND ruleset `mustStartAtOrAfter = start - refundPeriodDuration`.
139
- - SCORING ruleset `mustStartAtOrAfter = start`.
61
+ **Flow**
62
+ 1. Holders use the game's redemption path after settlement.
63
+ 2. The hook applies the now-final weights associated with the winning scorecard.
64
+ 3. Winners receive their proportional share while losers no longer have equivalent claim on the pot.
140
65
 
141
- ---
66
+ ## Hand-Offs
142
67
 
143
- ## Journey 2: Play a Game (Buy NFTs)
144
-
145
- **Entry point:** `JBMultiTerminal.pay{value: amount}(uint256 projectId, address token, uint256 amount, address beneficiary, uint256 minReturnedTokens, string memo, bytes metadata) external payable returns (uint256)`
146
-
147
- **Who can call:** Anyone. The terminal forwards the call to `DefifaHook.afterPayRecordedWith()` which validates the caller is a registered terminal for the project.
148
-
149
- **Actor:** Player
150
- **Phase:** MINT
151
-
152
- ### Parameters
153
-
154
- - `projectId` -- The game ID.
155
- - `token` -- Token address (e.g. `JBConstants.NATIVE_TOKEN` for ETH).
156
- - `amount` -- Must equal `tierPrice * numberOfTiersMinted` exactly.
157
- - `beneficiary` -- Address that receives the minted NFTs.
158
- - `minReturnedTokens` -- Minimum tokens to receive (typically 0 for NFT mints).
159
- - `memo` -- Optional memo string.
160
- - `metadata` -- JBMetadataResolver-encoded bytes containing `(address attestationDelegate, uint16[] tierIds)`.
161
-
162
- ### Step 1: Prepare payment metadata
163
-
164
- Encode the tier IDs to mint and optional attestation delegate:
165
-
166
- ```solidity
167
- // Tier IDs to mint (must be ascending order)
168
- uint16[] memory tierIds = new uint16[](2);
169
- tierIds[0] = 1; // "Kansas City Chiefs"
170
- tierIds[1] = 1; // Mint 2 of the same tier
171
-
172
- address attestationDelegate = address(0); // 0 = use default or self
173
-
174
- bytes memory payMetadata = abi.encode(attestationDelegate, tierIds);
175
- ```
176
-
177
- Wrap in JBMetadataResolver format:
178
-
179
- ```solidity
180
- bytes memory metadata = metadataHelper.createMetadata({
181
- id: JBMetadataResolver.getId("pay", hookCodeOrigin),
182
- data: payMetadata
183
- });
184
- ```
185
-
186
- ### Step 2: Pay the terminal
187
-
188
- ```solidity
189
- jbMultiTerminal.pay{value: 0.02 ether}({
190
- projectId: gameId,
191
- token: JBConstants.NATIVE_TOKEN,
192
- amount: 0.02 ether, // Must equal tierPrice * numberOfTiers minted
193
- beneficiary: msg.sender,
194
- minReturnedTokens: 0,
195
- memo: "Go Chiefs!",
196
- metadata: metadata
197
- });
198
- ```
199
-
200
- ### State changes
201
-
202
- 1. `DefifaHook._totalMintCost` -- Incremented by `context.amount.value` (the paid amount).
203
- 2. `DefifaHook._tierDelegation[beneficiary][tierId]` -- Set to `attestationDelegate` for each minted tier (if different from the beneficiary's existing delegate for that tier). When no explicit delegate is provided and no `defaultAttestationDelegate` is configured, defaults to the beneficiary.
204
- 3. `DefifaHook._delegateTierCheckpoints[delegate][tierId]` -- Checkpointed with new attestation units.
205
- 4. `DefifaHook._totalTierCheckpoints[tierId]` -- Checkpointed with increased total attestation units.
206
- 5. ERC-721 token ownership records updated (one token per tier mint).
207
- 6. `JB721TiersHookStore` records the mint (supply, token IDs).
208
-
209
- ### Events
210
-
211
- - `Mint(uint256 indexed tokenId, uint256 indexed tierId, address indexed beneficiary, uint256 totalAmountContributed, address caller)` -- Emitted per token minted by `DefifaHook._mintAll()`.
212
- - `DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate)` -- Emitted when attestation delegation is set for a tier.
213
- - `TierDelegateAttestationsChanged(address indexed delegate, uint256 indexed tierId, uint256 previousBalance, uint256 newBalance, address caller)` -- Emitted when attestation units are transferred.
214
-
215
- ### Edge cases
216
-
217
- - `DefifaHook_WrongCurrency` -- Payment currency does not match `pricingCurrency`.
218
- - `DefifaHook_NothingToMint` -- No tier IDs in metadata, or metadata not found.
219
- - `DefifaHook_Overspending` -- Payment amount exceeds exact cost of tiers minted (leftover != 0).
220
- - `DefifaHook_BadTierOrder` -- Tier IDs in metadata not in ascending order (validated by `DefifaHookLib.computeAttestationUnits`).
221
- - `JB721Hook_InvalidPay` -- Caller not a terminal, or wrong project ID, or ETH sent directly to hook.
222
-
223
- ---
224
-
225
- ## Journey 3: Refund During MINT Phase
226
-
227
- **Entry point:** `JBMultiTerminal.cashOutTokensOf(address holder, uint256 projectId, uint256 cashOutCount, address tokenToReclaim, uint256 minTokensReclaimed, address payable beneficiary, bytes metadata) external returns (uint256)`
228
-
229
- **Who can call:** Anyone can initiate, but the hook validates that `context.holder` owns the tokens being burned.
230
-
231
- **Actor:** Refunder
232
- **Phase:** MINT
233
-
234
- ### Parameters
235
-
236
- - `holder` -- Address that holds the NFTs being cashed out.
237
- - `projectId` -- The game ID.
238
- - `cashOutCount` -- Pass `0` for NFT cash-outs.
239
- - `tokenToReclaim` -- Token to receive (e.g. `JBConstants.NATIVE_TOKEN`).
240
- - `minTokensReclaimed` -- Minimum amount to receive (set to expected mint price).
241
- - `beneficiary` -- Address that receives the reclaimed funds.
242
- - `metadata` -- JBMetadataResolver-encoded bytes containing `(uint256[] tokenIds)`.
243
-
244
- ### Step 1: Prepare cash-out metadata
245
-
246
- ```solidity
247
- uint256[] memory tokenIds = new uint256[](1);
248
- tokenIds[0] = myTokenId;
249
-
250
- bytes memory cashOutMetadata = metadataHelper.createMetadata({
251
- id: JBMetadataResolver.getId("cashOut", hookCodeOrigin),
252
- data: abi.encode(tokenIds)
253
- });
254
- ```
255
-
256
- ### Step 2: Cash out
257
-
258
- ```solidity
259
- jbMultiTerminal.cashOutTokensOf({
260
- holder: msg.sender,
261
- projectId: gameId,
262
- cashOutCount: 0, // 0 for NFT cash-outs
263
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
264
- minTokensReclaimed: 0.01 ether, // Expect full tier price back
265
- beneficiary: payable(msg.sender),
266
- metadata: cashOutMetadata
267
- });
268
- ```
269
-
270
- ### State changes
271
-
272
- 1. ERC-721 token burned via `DefifaHook._burn(tokenId)`.
273
- 2. `DefifaHook._totalMintCost` -- Decremented by `cumulativeMintPrice` of burned tokens.
274
- 3. `JB721TiersHookStore` records the burn.
275
- 4. During MINT phase: `DefifaHook.tokensRedeemedFrom[tierId]` is NOT incremented (only during COMPLETE).
276
- 5. `DefifaHook.amountRedeemed` -- NOT incremented (only during COMPLETE).
277
-
278
- ### Events
279
-
280
- No Defifa-specific events are emitted during MINT/REFUND phase cash-outs. Standard ERC-721 `Transfer(from, address(0), tokenId)` is emitted by the burn.
281
-
282
- ### Edge cases
283
-
284
- - `DefifaHook_Unauthorized(tokenId, owner, caller)` -- Token holder in context does not own the token.
285
- - `DefifaHook_NothingToClaim` -- Reclaimed amount is 0 AND no fee tokens distributed.
286
- - `JB721Hook_InvalidCashOut` -- Caller not a terminal, or wrong project ID.
287
- - During MINT phase: `cashOutTaxRate = 0`, so full mint price is refunded.
288
-
289
- ---
290
-
291
- ## Journey 4: Refund During REFUND Phase
292
-
293
- **Entry point:** Same as Journey 3: `JBMultiTerminal.cashOutTokensOf(...)`
294
-
295
- **Who can call:** Anyone (same restrictions as Journey 3).
296
-
297
- **Actor:** Refunder
298
- **Phase:** REFUND
299
-
300
- 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`.
301
-
302
- ### State changes
303
-
304
- Same as Journey 3.
305
-
306
- ### Events
307
-
308
- Same as Journey 3 (no Defifa-specific events; standard ERC-721 burn `Transfer` event).
309
-
310
- ### Edge cases
311
-
312
- Same as Journey 3. Additionally, new payments are blocked (`pausePay: true`).
313
-
314
- ---
315
-
316
- ## Journey 5: Submit a Scorecard
317
-
318
- **Entry point:** `DefifaGovernor.submitScorecardFor(uint256 gameId, DefifaTierCashOutWeight[] calldata tierWeights) external returns (uint256 scorecardId)`
319
-
320
- **Who can call:** Anyone. No access control on submission. However, if `msg.sender == defaultAttestationDelegate`, the scorecard is stored as `defaultAttestationDelegateProposalOf[gameId]`.
321
-
322
- **Actor:** Scorer (anyone)
323
- **Phase:** SCORING
324
-
325
- ### Parameters
326
-
327
- - `gameId` -- The ID of the game.
328
- - `tierWeights` -- Array of `DefifaTierCashOutWeight` structs. Each has `id` (tier ID) and `cashOutWeight` (weight). All weights must sum to exactly `TOTAL_CASHOUT_WEIGHT` (1e18). Tier IDs must be in ascending order.
329
-
330
- ### Step 1: Prepare tier weights
331
-
332
- All weights must sum to exactly `TOTAL_CASHOUT_WEIGHT` (1e18). Tier IDs must be in ascending order.
333
-
334
- ```solidity
335
- DefifaTierCashOutWeight[] memory tierWeights = new DefifaTierCashOutWeight[](3);
336
- tierWeights[0] = DefifaTierCashOutWeight({id: 1, cashOutWeight: 500_000_000_000_000_000}); // 50%
337
- tierWeights[1] = DefifaTierCashOutWeight({id: 2, cashOutWeight: 300_000_000_000_000_000}); // 30%
338
- tierWeights[2] = DefifaTierCashOutWeight({id: 3, cashOutWeight: 200_000_000_000_000_000}); // 20%
339
- // Sum = 1e18
340
- ```
341
-
342
- ### Step 2: Submit
343
-
344
- ```solidity
345
- uint256 scorecardId = governor.submitScorecardFor(gameId, tierWeights);
346
- ```
347
-
348
- ### State changes
349
-
350
- 1. `DefifaGovernor._scorecardOf[gameId][scorecardId].attestationsBegin` -- Set to `max(block.timestamp, attestationStartTime)`.
351
- 2. `DefifaGovernor._scorecardOf[gameId][scorecardId].gracePeriodEnds` -- Set to `attestationsBegin + attestationGracePeriod`.
352
- 3. `DefifaGovernor._pendingReservesSnapshotOf[gameId][scorecardId][tierId]` -- Snapshots `numberOfPendingReservesFor()` for every tier. Used by `getBWAAttestationWeight()` to prevent reserve minting from inflating attestation power.
353
- 4. `DefifaGovernor.defaultAttestationDelegateProposalOf[gameId]` -- Set to `scorecardId` if sender is the default attestation delegate.
354
-
355
- ### Events
356
-
357
- - `ScorecardSubmitted(uint256 indexed gameId, uint256 indexed scorecardId, DefifaTierCashOutWeight[] tierWeights, bool isDefaultAttestationDelegate, address caller)` -- Emitted by `DefifaGovernor`.
358
-
359
- ### Edge cases
360
-
361
- - `DefifaGovernor_AlreadyRatified` -- A scorecard has already been ratified for this game.
362
- - `DefifaGovernor_GameNotFound` -- Game not initialized (`_packedScorecardInfoOf[gameId] == 0`).
363
- - `DefifaGovernor_NotAllowed` -- Game not in SCORING phase.
364
- - `DefifaGovernor_UnownedProposedCashoutValue` -- Weight > 0 assigned to a tier with `currentSupplyOfTier == 0`.
365
- - `DefifaGovernor_DuplicateScorecard` -- Identical scorecard (same hash) already submitted.
366
- - Scorecard state starts as PENDING (until `attestationsBegin`) or ACTIVE (if attestations start immediately).
367
-
368
- ---
369
-
370
- ## Journey 6: Attest to a Scorecard
371
-
372
- **Entry point:** `DefifaGovernor.attestToScorecardFrom(uint256 gameId, uint256 scorecardId) external returns (uint256 weight)`
373
-
374
- **Who can call:** Anyone. However, attestation weight is zero unless the caller (or their delegate) held NFTs at the `attestationsBegin - 1` checkpoint timestamp (one second before the attestation window opens).
375
-
376
- **Actor:** Attestor (NFT holder or delegate)
377
- **Phase:** SCORING
378
-
379
- ### Parameters
380
-
381
- - `gameId` -- The ID of the game.
382
- - `scorecardId` -- The scorecard ID to attest to.
383
-
384
- ### Step 1: Get scorecard ID
385
-
386
- Either compute it or use the ID from the `ScorecardSubmitted` event:
387
-
388
- ```solidity
389
- uint256 scorecardId = governor.scorecardIdOf(hookAddress, tierWeights);
390
- ```
391
-
392
- ### Step 2: Attest
393
-
394
- ```solidity
395
- uint256 weight = governor.attestToScorecardFrom(gameId, scorecardId);
396
- ```
397
-
398
- ### State changes
399
-
400
- 1. `DefifaGovernor._scorecardAttestationsOf[gameId][scorecardId].count` -- Incremented by `weight`.
401
- 2. `DefifaGovernor._scorecardAttestationsOf[gameId][scorecardId].hasAttested[msg.sender]` -- Set to `true`.
402
-
403
- ### Events
404
-
405
- - `ScorecardAttested(uint256 indexed gameId, uint256 indexed scorecardId, uint256 weight, address caller)` -- Emitted by `DefifaGovernor`.
406
-
407
- ### Edge cases
408
-
409
- - `DefifaGovernor_NotAllowed` -- Game not in SCORING phase, or scorecard not in ACTIVE/SUCCEEDED state.
410
- - `DefifaGovernor_AlreadyAttested` -- Account already attested to this scorecard.
411
- - `DefifaGovernor_UnknownProposal` -- Scorecard ID has no submission record.
412
- - Attestation weight is computed at `attestationsBegin - 1` timestamp using checkpointed values (snapshot, not live). This prevents same-block transfer manipulation.
413
- - Each tier caps at `MAX_ATTESTATION_POWER_TIER` (1e9) regardless of how many tokens exist in that tier.
414
-
415
- ---
416
-
417
- ## Journey 7: Ratify a Scorecard
418
-
419
- **Entry point:** `DefifaGovernor.ratifyScorecardFrom(uint256 gameId, DefifaTierCashOutWeight[] calldata tierWeights) external returns (uint256 scorecardId)`
420
-
421
- **Who can call:** Anyone. No access control -- the function validates that the scorecard is in SUCCEEDED state.
422
-
423
- **Actor:** Ratifier (anyone)
424
- **Phase:** SCORING (scorecard in SUCCEEDED state)
425
-
426
- ### Parameters
427
-
428
- - `gameId` -- The ID of the game.
429
- - `tierWeights` -- The tier weights that match the scorecard being ratified (used to recompute the scorecard hash).
430
-
431
- ### Precondition
432
-
433
- A scorecard must be in SUCCEEDED state:
434
- - `attestationsBegin <= block.timestamp`
435
- - `gracePeriodEnds <= block.timestamp`
436
- - `attestation count >= quorum`
437
-
438
- ### Step 1: Ratify
439
-
440
- ```solidity
441
- uint256 scorecardId = governor.ratifyScorecardFrom(gameId, tierWeights);
442
- ```
443
-
444
- ### State changes
445
-
446
- 1. `DefifaGovernor.ratifiedScorecardIdOf[gameId]` -- Set to `scorecardId`.
447
- 2. `DefifaHook._tierCashOutWeights` -- Set via `setTierCashOutWeightsTo()` executed as a low-level call.
448
- 3. `DefifaHook.cashOutWeightIsSet` -- Set to `true`.
449
- 4. `DefifaDeployer.fulfilledCommitmentsOf[gameId]` -- Set to the fee amount (or sentinel value 1 if pot is 0 or payout fails).
450
- 5. Final ruleset queued via `CONTROLLER.queueRulesetsOf()` with no payout limits.
451
-
452
- ### Events
453
-
454
- - `TierCashOutWeightsSet(DefifaTierCashOutWeight[] tierWeights, address caller)` -- Emitted by `DefifaHook.setTierCashOutWeightsTo()`.
455
- - `FulfilledCommitments(uint256 indexed gameId, uint256 pot, address caller)` -- Emitted by `DefifaDeployer.fulfillCommitmentsOf()`.
456
- - `CommitmentPayoutFailed(uint256 indexed gameId, uint256 amount, bytes reason)` -- Emitted if `sendPayoutsOf` fails (try-catch).
457
- - `ScorecardRatified(uint256 indexed gameId, uint256 indexed scorecardId, address caller)` -- Emitted by `DefifaGovernor`.
458
-
459
- ### Edge cases
460
-
461
- - `DefifaGovernor_AlreadyRatified` -- Game already has a ratified scorecard.
462
- - `DefifaGovernor_NotAllowed` -- Scorecard not in SUCCEEDED state.
463
- - `DefifaGovernor_UnknownProposal` -- Scorecard ID has no submission record.
464
- - If `sendPayoutsOf` fails: try-catch emits `CommitmentPayoutFailed`, fee stays in pot, but final ruleset is still queued.
465
- - If `queueRulesetsOf` fails: the entire ratification reverts (no try-catch on that call).
466
- - Game state transitions to COMPLETE because `cashOutWeightIsSet == true`.
467
-
468
- ---
469
-
470
- ## Journey 8: Cash Out as Winner
471
-
472
- **Entry point:** `JBMultiTerminal.cashOutTokensOf(address holder, uint256 projectId, uint256 cashOutCount, address tokenToReclaim, uint256 minTokensReclaimed, address payable beneficiary, bytes metadata) external returns (uint256)`
473
-
474
- **Who can call:** Anyone can initiate, but the hook validates that `context.holder` owns the tokens being burned.
475
-
476
- **Actor:** Winner
477
- **Phase:** COMPLETE
478
-
479
- ### Parameters
480
-
481
- Same as Journey 3 (Refund).
482
-
483
- ### Step 1: Check claimable amounts
484
-
485
- ```solidity
486
- // Check cash-out value
487
- uint256 weight = hook.cashOutWeightOf(myTokenId);
488
- // weight > 0 means this tier won something
489
-
490
- // Check fee token claims
491
- (uint256 defifaTokens, uint256 nanaTokens) = hook.tokensClaimableFor(tokenIds);
492
- ```
493
-
494
- ### Step 2: Cash out
495
-
496
- ```solidity
497
- uint256[] memory tokenIds = new uint256[](1);
498
- tokenIds[0] = myTokenId;
499
-
500
- bytes memory cashOutMetadata = metadataHelper.createMetadata({
501
- id: JBMetadataResolver.getId("cashOut", hookCodeOrigin),
502
- data: abi.encode(tokenIds)
503
- });
504
-
505
- jbMultiTerminal.cashOutTokensOf({
506
- holder: msg.sender,
507
- projectId: gameId,
508
- cashOutCount: 0,
509
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
510
- minTokensReclaimed: expectedAmount,
511
- beneficiary: payable(msg.sender),
512
- metadata: cashOutMetadata
513
- });
514
- ```
515
-
516
- ### State changes
517
-
518
- 1. ERC-721 tokens burned via `DefifaHook._burn(tokenId)`.
519
- 2. `DefifaHook.tokensRedeemedFrom[tierId]` -- Incremented for each burned token (only during COMPLETE).
520
- 3. `DefifaHook.amountRedeemed` -- Incremented by `context.reclaimedAmount.value`.
521
- 4. `DefifaHook._totalMintCost` -- Decremented by `cumulativeMintPrice` of burned tokens.
522
- 5. Fee tokens ($DEFIFA and $NANA) transferred to holder proportional to their mint cost share.
523
- 6. `JB721TiersHookStore` records the burn.
524
-
525
- ### Events
526
-
527
- - `ClaimedTokens(address indexed beneficiary, uint256 defifaTokenAmount, uint256 baseProtocolTokenAmount, address caller)` -- Emitted by `DefifaHookLib.claimTokensFor()` when fee tokens are distributed.
528
-
529
- Standard ERC-721 `Transfer(from, address(0), tokenId)` emitted by the burn. Standard ERC-20 `Transfer` events emitted by the token transfers.
530
-
531
- ### Edge cases
532
-
533
- - `DefifaHook_Unauthorized(tokenId, owner, caller)` -- Token holder in context does not own the token.
534
- - `DefifaHook_NothingToClaim` -- Reclaimed amount is 0 AND no fee tokens distributed.
535
- - `JB721Hook_InvalidCashOut` -- Caller not a terminal, or wrong project ID.
536
- - Reclaim calculation: `perTokenWeight = tierCashOutWeight[tierId] / totalTokensForCashoutInTier`, then `reclaimAmount = mulDiv(surplus + amountRedeemed, perTokenWeight, TOTAL_CASHOUT_WEIGHT)`.
537
- - `totalTokensForCashoutInTier = initialSupply - remainingSupply - (burnedTokens - tokensRedeemedFrom[tierId])`.
538
-
539
- ---
540
-
541
- ## Journey 9: Cash Out from Losing Tier
542
-
543
- **Entry point:** Same as Journey 8: `JBMultiTerminal.cashOutTokensOf(...)`
544
-
545
- **Who can call:** Anyone (same restrictions as Journey 8).
546
-
547
- **Actor:** Holder of a zero-weight tier
548
- **Phase:** COMPLETE
549
-
550
- If a tier received `cashOutWeight = 0` in the ratified scorecard:
551
-
552
- ```solidity
553
- // cashOutWeightOf(tokenId) returns 0
554
- // beforeCashOutRecordedWith returns cashOutCount = 0
555
- // afterCashOutRecordedWith: reclaimedAmount.value == 0
556
- // _claimTokensFor is called -- if fee tokens exist, they are distributed
557
- // If no fee tokens distributed either -> reverts with DefifaHook_NothingToClaim
558
- ```
559
-
560
- ### State changes
561
-
562
- Same as Journey 8, but `context.reclaimedAmount.value == 0`.
563
-
564
- ### Events
565
-
566
- - `ClaimedTokens(address indexed beneficiary, uint256 defifaTokenAmount, uint256 baseProtocolTokenAmount, address caller)` -- Emitted only if fee tokens are available to distribute.
567
-
568
- ### Edge cases
569
-
570
- - `DefifaHook_NothingToClaim` -- Reverts if both reclaimed ETH is 0 AND no fee tokens are distributed.
571
- - Holders of losing tiers receive fee tokens proportional to their mint cost but zero ETH. If no fee tokens exist at all, the cash-out reverts.
572
-
573
- ---
574
-
575
- ## Journey 10: No-Contest via Minimum Participation
576
-
577
- **Entry point:** `DefifaDeployer.triggerNoContestFor(uint256 gameId) external`
578
-
579
- **Who can call:** Anyone. No access control -- the function validates that the game is in NO_CONTEST phase.
580
-
581
- **Actor:** Any user
582
- **Phase:** SCORING (when balance < minParticipation)
583
-
584
- ### Parameters
585
-
586
- - `gameId` -- The ID of the game to trigger no-contest for.
587
-
588
- ### Scenario
589
-
590
- Game had `minParticipation = 10 ether`. During MINT, 5 ETH was deposited but then refunded down to 3 ETH. When SCORING begins, `currentGamePhaseOf()` checks:
591
-
592
- ```solidity
593
- if (_ops.minParticipation > 0) {
594
- uint256 _balance = terminal.STORE().balanceOf(terminal, gameId, token);
595
- if (_balance < _ops.minParticipation) return DefifaGamePhase.NO_CONTEST;
596
- }
597
- ```
598
-
599
- Since 3 ETH < 10 ETH, the game is NO_CONTEST.
600
-
601
- ### Step 1: Trigger no-contest
602
-
603
- ```solidity
604
- deployer.triggerNoContestFor(gameId);
605
- ```
606
-
607
- ### State changes
608
-
609
- 1. `DefifaDeployer.noContestTriggeredFor[gameId]` -- Set to `true`.
610
- 2. New ruleset queued via `CONTROLLER.queueRulesetsOf()` with no `fundAccessLimitGroups`, making entire balance = surplus. Has `pausePay: true` and `cashOutTaxRate: 0`.
611
-
612
- ### Events
613
-
614
- - `QueuedNoContest(uint256 indexed gameId, address caller)` -- Emitted by `DefifaDeployer`.
615
-
616
- ### Edge cases
617
-
618
- - `DefifaDeployer_NotNoContest` -- Game not in NO_CONTEST phase.
619
- - `DefifaDeployer_NoContestAlreadyTriggered` -- Already triggered for this game.
620
- - The queued ruleset does not take effect until the current ruleset's cycle ends. During this gap, the game reports NO_CONTEST but the on-chain ruleset still has payout limits. Callers should verify the active ruleset before cashing out.
621
-
622
- ### Step 2: Cash out (full refund)
623
-
624
- 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`.
625
-
626
- ---
627
-
628
- ## Journey 11: No-Contest via Scorecard Timeout
629
-
630
- **Entry point:** Same as Journey 10: `DefifaDeployer.triggerNoContestFor(uint256 gameId) external`
631
-
632
- **Who can call:** Anyone. Same restrictions as Journey 10.
633
-
634
- **Actor:** Any user
635
- **Phase:** SCORING (when timeout elapsed without ratification)
636
-
637
- ### Parameters
638
-
639
- - `gameId` -- The ID of the game.
640
-
641
- ### Scenario
642
-
643
- Game had `scorecardTimeout = 7 days`. Scoring started 8 days ago. No scorecard was ratified.
644
-
645
- ```solidity
646
- if (_ops.scorecardTimeout > 0 && block.timestamp > _currentRuleset.start + _ops.scorecardTimeout) {
647
- return DefifaGamePhase.NO_CONTEST;
648
- }
649
- ```
650
-
651
- ### State changes
652
-
653
- Same as Journey 10.
654
-
655
- ### Events
656
-
657
- Same as Journey 10: `QueuedNoContest(uint256 indexed gameId, address caller)`.
658
-
659
- ### Edge cases
660
-
661
- Same as Journey 10. Additionally: 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()`.
662
-
663
- ---
664
-
665
- ## Journey 12: Delegate Attestation Power
666
-
667
- **Entry point (single tier):** `DefifaHook.setTierDelegateTo(address delegatee, uint256 tierId) public`
668
-
669
- **Entry point (multiple tiers):** `DefifaHook.setTierDelegatesTo(DefifaDelegation[] memory delegations) external`
670
-
671
- **Who can call:** Any NFT holder (`msg.sender` is the delegator). Only callable during MINT phase.
672
-
673
- **Actor:** Player (NFT holder)
674
- **Phase:** MINT only
675
-
676
- ### Parameters (single)
677
-
678
- - `delegatee` -- Address to delegate attestation power to. Cannot be `address(0)`.
679
- - `tierId` -- The tier ID to delegate attestation units for.
680
-
681
- ### Parameters (multiple)
682
-
683
- - `delegations` -- Array of `DefifaDelegation` structs, each containing `delegatee` and `tierId`.
684
-
685
- ### Single tier delegation
686
-
687
- ```solidity
688
- hook.setTierDelegateTo(trustedDelegate, tierId);
689
- ```
690
-
691
- ### Multiple tier delegations
692
-
693
- ```solidity
694
- DefifaDelegation[] memory delegations = new DefifaDelegation[](2);
695
- delegations[0] = DefifaDelegation({delegatee: trustedDelegate, tierId: 1});
696
- delegations[1] = DefifaDelegation({delegatee: anotherDelegate, tierId: 2});
697
-
698
- hook.setTierDelegatesTo(delegations);
699
- ```
700
-
701
- ### State changes
702
-
703
- 1. `DefifaHook._tierDelegation[msg.sender][tierId]` -- Set to the new `delegatee`.
704
- 2. `DefifaHook._delegateTierCheckpoints[oldDelegate][tierId]` -- Checkpointed with decreased attestation units.
705
- 3. `DefifaHook._delegateTierCheckpoints[newDelegate][tierId]` -- Checkpointed with increased attestation units.
706
-
707
- ### Events
708
-
709
- - `DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate)` -- Emitted per tier delegation change.
710
- - `TierDelegateAttestationsChanged(address indexed delegate, uint256 indexed tierId, uint256 previousBalance, uint256 newBalance, address caller)` -- Emitted for both the old delegate (units removed) and the new delegate (units added).
711
-
712
- ### Edge cases
713
-
714
- - `DefifaHook_DelegateAddressZero` -- Delegatee is `address(0)`.
715
- - `DefifaHook_DelegateChangesUnavailableInThisPhase` -- Not in MINT phase.
716
- - On NFT transfer after MINT: auto-delegates to recipient if recipient has no delegate.
717
-
718
- ---
719
-
720
- ## Journey 13: Mint Reserved Tokens
721
-
722
- **Entry point (single tier):** `DefifaHook.mintReservesFor(uint256 tierId, uint256 count) public`
723
-
724
- **Entry point (multiple tiers):** `DefifaHook.mintReservesFor(JB721TiersMintReservesConfig[] calldata mintReservesForTiersData) external`
725
-
726
- **Who can call:** Anyone. No access control. Must not be paused (`pauseMintPendingReserves` must be false).
727
-
728
- **Actor:** Anyone
729
- **Phase:** SCORING or later (reserved minting is paused during both MINT and REFUND via `pauseMintPendingReserves: true`)
730
-
731
- ### Parameters (single)
732
-
733
- - `tierId` -- The tier ID to mint reserved tokens for.
734
- - `count` -- Number of reserved tokens to mint.
735
-
736
- ### Parameters (multiple)
737
-
738
- - `mintReservesForTiersData` -- Array of `JB721TiersMintReservesConfig` structs, each containing `tierId` and `count`.
739
-
740
- ### Single tier
741
-
742
- ```solidity
743
- hook.mintReservesFor(tierId, count);
744
- ```
745
-
746
- ### Multiple tiers
747
-
748
- ```solidity
749
- JB721TiersMintReservesConfig[] memory configs = new JB721TiersMintReservesConfig[](1);
750
- configs[0] = JB721TiersMintReservesConfig({tierId: 1, count: 5});
751
-
752
- hook.mintReservesFor(configs);
753
- ```
754
-
755
- ### State changes
756
-
757
- 1. `DefifaHook._totalMintCost` -- Incremented by `tier.price * count`.
758
- 2. `DefifaHook._tierDelegation[beneficiary][tierId]` -- Set to `defaultAttestationDelegate` or self (if no delegate exists).
759
- 3. `DefifaHook._delegateTierCheckpoints[delegate][tierId]` -- Checkpointed with new attestation units.
760
- 4. `DefifaHook._totalTierCheckpoints[tierId]` -- Checkpointed with increased total attestation units.
761
- 5. ERC-721 tokens minted to `reserveBeneficiary`.
762
- 6. `JB721TiersHookStore` records the reserve mint.
763
-
764
- ### Events
765
-
766
- - `MintReservedToken(uint256 indexed tokenId, uint256 indexed tierId, address indexed beneficiary, address caller)` -- Emitted per reserved token minted.
767
- - `DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate)` -- Emitted if delegation is set for the reserve beneficiary.
768
- - `TierDelegateAttestationsChanged(address indexed delegate, uint256 indexed tierId, uint256 previousBalance, uint256 newBalance, address caller)` -- Emitted when attestation units are transferred to the delegate.
769
-
770
- ### Edge cases
771
-
772
- - `DefifaHook_ReservedTokenMintingPaused` -- `pauseMintPendingReserves` is true in current ruleset metadata.
773
- - 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).
774
-
775
- ---
776
-
777
- ## Journey 14: Fulfill Commitments Separately
778
-
779
- **Entry point:** `DefifaDeployer.fulfillCommitmentsOf(uint256 gameId) external`
780
-
781
- **Who can call:** Anyone. No access control. Requires `cashOutWeightIsSet == true`.
782
-
783
- **Actor:** Anyone
784
- **Phase:** COMPLETE (after scorecard ratification)
785
-
786
- ### Parameters
787
-
788
- - `gameId` -- The ID of the game to fulfill commitments for.
789
-
790
- `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.
791
-
792
- 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):
793
-
794
- ```solidity
795
- deployer.fulfillCommitmentsOf(gameId);
796
- ```
797
-
798
- ### State changes
799
-
800
- 1. `DefifaDeployer.fulfilledCommitmentsOf[gameId]` -- Set to fee amount (or sentinel value 1 if pot is 0 or payout fails).
801
- 2. Fee payouts sent via `terminal.sendPayoutsOf()` (distributes to splits).
802
- 3. Final ruleset queued via `CONTROLLER.queueRulesetsOf()` with no payout limits.
803
-
804
- ### Events
805
-
806
- - `FulfilledCommitments(uint256 indexed gameId, uint256 pot, address caller)` -- Emitted by `DefifaDeployer` on success.
807
- - `CommitmentPayoutFailed(uint256 indexed gameId, uint256 amount, bytes reason)` -- Emitted if `sendPayoutsOf` fails (try-catch).
808
-
809
- ### Edge cases
810
-
811
- - `DefifaDeployer_CantFulfillYet` -- `cashOutWeightIsSet == false`.
812
- - Idempotent: If `fulfilledCommitmentsOf[gameId] != 0`, returns immediately without reverting.
813
- - Fee computation: `mulDiv(pot, _commitmentPercentOf[gameId], SPLITS_TOTAL_PERCENT)`.
814
-
815
- ---
816
-
817
- ## Journey 15: Transfer NFT to Another Player
818
-
819
- **Entry point:** `DefifaHook.transferFrom(address from, address to, uint256 tokenId) external` or `DefifaHook.safeTransferFrom(address from, address to, uint256 tokenId) external`
820
-
821
- **Who can call:** Token owner or approved operator (standard ERC-721 access control). Transfers may be paused if `transfersPausable` is set and paused in the current ruleset.
822
-
823
- **Actor:** NFT holder
824
- **Phase:** Any (unless `transfersPausable` is set and transfers are paused)
825
-
826
- ### Parameters
827
-
828
- - `from` -- Current token owner.
829
- - `to` -- Recipient address.
830
- - `tokenId` -- The token to transfer.
831
-
832
- ```solidity
833
- hook.transferFrom(from, to, tokenId);
834
- // or
835
- hook.safeTransferFrom(from, to, tokenId);
836
- ```
837
-
838
- ### State changes
839
-
840
- 1. ERC-721 ownership updated from `from` to `to`.
841
- 2. `DefifaHook._firstOwnerOf[tokenId]` -- Stored as `from` on first transfer of this token.
842
- 3. `JB721TiersHookStore` records the transfer via `recordTransferForTier(tierId, from, to)`.
843
- 4. `DefifaHook._tierDelegation[to][tierId]` -- Auto-set to `to` if recipient has no delegate.
844
- 5. `DefifaHook._delegateTierCheckpoints[fromDelegate][tierId]` -- Checkpointed with decreased attestation units.
845
- 6. `DefifaHook._delegateTierCheckpoints[toDelegate][tierId]` -- Checkpointed with increased attestation units.
846
-
847
- ### Events
848
-
849
- - `DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate)` -- Emitted if recipient has no delegate and auto-delegates to self.
850
- - `TierDelegateAttestationsChanged(address indexed delegate, uint256 indexed tierId, uint256 previousBalance, uint256 newBalance, address caller)` -- Emitted for both sender's delegate (units removed) and recipient's delegate (units added).
851
-
852
- Standard ERC-721 `Transfer(from, to, tokenId)` is also emitted.
853
-
854
- ### Edge cases
855
-
856
- - `DefifaHook_TransfersPaused` -- `transfersPausable` is set and transfers paused in current ruleset.
857
- - On transfer after MINT phase: attestation units are transferred but delegation cannot be changed by the sender.
858
- - Auto-delegation: if recipient has no delegate, they auto-delegate to themselves.
859
-
860
- ---
861
-
862
- ## Journey 16: Query Game State
863
-
864
- **Actor:** Frontend / anyone
865
-
866
- ### Check game phase
867
-
868
- **Entry point:** `DefifaDeployer.currentGamePhaseOf(uint256 gameId) public view returns (DefifaGamePhase)`
869
-
870
- **Who can call:** Anyone (view function).
871
-
872
- ```solidity
873
- DefifaGamePhase phase = deployer.currentGamePhaseOf(gameId);
874
- ```
875
-
876
- ### Check game pot
877
-
878
- **Entry point:** `DefifaDeployer.currentGamePotOf(uint256 gameId, bool includeCommitments) external view returns (uint256, address, uint256)`
879
-
880
- **Who can call:** Anyone (view function).
881
-
882
- ```solidity
883
- (uint256 pot, address token, uint256 decimals) = deployer.currentGamePotOf(gameId, false);
884
- // includeCommitments = true adds fulfilled fee amounts back
885
- ```
886
-
887
- ### Check timing
888
-
889
- **Entry point:** `DefifaDeployer.timesFor(uint256 gameId) external view returns (uint48, uint24, uint24)`
890
-
891
- **Who can call:** Anyone (view function).
892
-
893
- ```solidity
894
- (uint48 start, uint24 mintDuration, uint24 refundDuration) = deployer.timesFor(gameId);
895
- ```
896
-
897
- ### Check safety params
898
-
899
- **Entry point:** `DefifaDeployer.safetyParamsOf(uint256 gameId) external view returns (uint256 minParticipation, uint32 scorecardTimeout)`
900
-
901
- **Who can call:** Anyone (view function).
902
-
903
- ```solidity
904
- (uint256 minParticipation, uint32 scorecardTimeout) = deployer.safetyParamsOf(gameId);
905
- ```
906
-
907
- ### Check scorecard state
908
-
909
- **Entry point:** `DefifaGovernor.stateOf(uint256 gameId, uint256 scorecardId) public view returns (DefifaScorecardState)`
910
-
911
- **Who can call:** Anyone (view function).
912
-
913
- ```solidity
914
- DefifaScorecardState state = governor.stateOf(gameId, scorecardId);
915
- // PENDING -> ACTIVE -> SUCCEEDED -> RATIFIED (or DEFEATED)
916
- ```
917
-
918
- ### Check attestation status
919
-
920
- **Entry points:**
921
- - `DefifaGovernor.attestationCountOf(uint256 gameId, uint256 scorecardId) external view returns (uint256)`
922
- - `DefifaGovernor.quorum(uint256 gameId) public view returns (uint256)`
923
- - `DefifaGovernor.hasAttestedTo(uint256 gameId, uint256 scorecardId, address account) external view returns (bool)`
924
-
925
- **Who can call:** Anyone (view functions).
926
-
927
- ```solidity
928
- uint256 count = governor.attestationCountOf(gameId, scorecardId);
929
- uint256 needed = governor.quorum(gameId);
930
- bool hasAttested = governor.hasAttestedTo(gameId, scorecardId, account);
931
- ```
932
-
933
- ### Check cash-out value
934
-
935
- **Entry points:**
936
- - `DefifaHook.cashOutWeightOf(uint256 tokenId) external view returns (uint256)`
937
- - `DefifaHook.cashOutWeightOf(uint256[] tokenIds) external view returns (uint256)` (aggregate)
938
-
939
- **Who can call:** Anyone (view functions).
940
-
941
- ```solidity
942
- // Single token
943
- uint256 weight = hook.cashOutWeightOf(tokenId);
944
-
945
- // Multiple tokens
946
- uint256[] memory ids = new uint256[](2);
947
- ids[0] = tokenId1;
948
- ids[1] = tokenId2;
949
- uint256 totalWeight = hook.cashOutWeightOf(ids);
950
- ```
951
-
952
- ### Check fee token claims
953
-
954
- **Entry points:**
955
- - `DefifaHook.tokensClaimableFor(uint256[] memory tokenIds) external view returns (uint256, uint256)`
956
- - `DefifaHook.tokenAllocations() external view returns (uint256, uint256)`
957
-
958
- **Who can call:** Anyone (view functions).
959
-
960
- ```solidity
961
- (uint256 defifaTokens, uint256 nanaTokens) = hook.tokensClaimableFor(tokenIds);
962
- (uint256 defifaBalance, uint256 nanaBalance) = hook.tokenAllocations();
963
- ```
964
-
965
- ---
966
-
967
- ## Error Conditions by Journey
968
-
969
- ### Payment Errors (Journey 2)
970
-
971
- | Error | Condition |
972
- |-------|-----------|
973
- | `DefifaHook_WrongCurrency` | Payment currency does not match `pricingCurrency` |
974
- | `DefifaHook_NothingToMint` | No tier IDs in metadata, or metadata not found |
975
- | `DefifaHook_Overspending` | Payment amount exceeds exact cost of tiers minted |
976
- | `DefifaHook_BadTierOrder` | Tier IDs in metadata not in ascending order |
977
- | `JB721Hook_InvalidPay` | Caller not a terminal, or wrong project ID, or ETH sent to hook |
978
-
979
- ### Cash-Out Errors (Journeys 3, 4, 8, 9)
980
-
981
- | Error | Condition |
982
- |-------|-----------|
983
- | `DefifaHook_Unauthorized(tokenId, owner, caller)` | Token holder in context does not own the token |
984
- | `DefifaHook_NothingToClaim` | Reclaimed amount is 0 AND no fee tokens distributed |
985
- | `JB721Hook_InvalidCashOut` | Caller not a terminal, or wrong project ID |
986
-
987
- ### Scorecard Errors (Journeys 5, 6, 7)
988
-
989
- | Error | Condition | Journey |
990
- |-------|-----------|---------|
991
- | `DefifaGovernor_NotAllowed` | Game not in SCORING, or scorecard not in correct state | 5, 6, 7 |
992
- | `DefifaGovernor_UnownedProposedCashoutValue` | Weight > 0 assigned to tier with 0 supply | 5 |
993
- | `DefifaGovernor_DuplicateScorecard` | Identical scorecard already submitted | 5 |
994
- | `DefifaGovernor_AlreadyAttested` | Account already attested to this scorecard | 6 |
995
- | `DefifaGovernor_AlreadyRatified` | Game already has a ratified scorecard | 5, 7 |
996
- | `DefifaGovernor_UnknownProposal` | Scorecard ID has no submission record | 6, 7 |
997
- | `DefifaHook_InvalidCashoutWeights` | Weights do not sum to TOTAL_CASHOUT_WEIGHT | 7 (ratification) |
998
- | `DefifaHook_BadTierOrder` | Tier IDs not in ascending order | 7 (ratification) |
999
- | `DefifaHook_InvalidTierId` | Tier not in category 0, or tier ID > maxTierId | 7 (ratification) |
1000
- | `DefifaHook_GameIsntScoringYet` | Game not in SCORING phase when setting weights | 7 (ratification) |
1001
- | `DefifaHook_CashoutWeightsAlreadySet` | Weights already set (double-set attempt) | 7 (ratification) |
1002
-
1003
- ### No-Contest Errors (Journeys 10, 11)
1004
-
1005
- | Error | Condition |
1006
- |-------|-----------|
1007
- | `DefifaDeployer_NotNoContest` | Game not in NO_CONTEST when triggering |
1008
- | `DefifaDeployer_NoContestAlreadyTriggered` | Already triggered for this game |
1009
-
1010
- ### Delegation Errors (Journey 12)
1011
-
1012
- | Error | Condition |
1013
- |-------|-----------|
1014
- | `DefifaHook_DelegateAddressZero` | Delegatee is address(0) |
1015
- | `DefifaHook_DelegateChangesUnavailableInThisPhase` | Not in MINT phase |
1016
-
1017
- ### Fulfillment Errors (Journey 14)
1018
-
1019
- | Error | Condition |
1020
- |-------|-----------|
1021
- | `DefifaDeployer_CantFulfillYet` | `cashOutWeightIsSet == false` |
1022
-
1023
- ### Game Creation Errors (Journey 1)
1024
-
1025
- | Error | Condition |
1026
- |-------|-----------|
1027
- | `DefifaDeployer_InvalidGameConfiguration` | Timing constraints violated: `mintPeriodDuration == 0` or `start < block.timestamp + refund + mint` |
1028
- | `DefifaDeployer_SplitsDontAddUp` | User splits + protocol fees exceed 100% |
1029
- | `DefifaDeployer_InvalidGameConfiguration` | JB project ID mismatch (front-run) |
1030
-
1031
- ### Transfer Errors (Journey 15)
1032
-
1033
- | Error | Condition |
1034
- |-------|-----------|
1035
- | `DefifaHook_TransfersPaused` | `transfersPausable` is set and transfers paused in current ruleset |
68
+ - Use [nana-721-hook-v6](../nana-721-hook-v6/USER_JOURNEYS.md) for the standard tiered NFT mechanics underneath the game-specific logic.
69
+ - Use [nana-core-v6](../nana-core-v6/USER_JOURNEYS.md) for base project accounting once the question is no longer Defifa-specific lifecycle or governance behavior.