@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.
- package/ADMINISTRATION.md +26 -15
- package/ARCHITECTURE.md +35 -3
- package/AUDIT_INSTRUCTIONS.md +127 -45
- package/CHANGE_LOG.md +107 -0
- package/CRYPTO_ECON.md +2 -2
- package/README.md +120 -2
- package/RISKS.md +21 -4
- package/SKILLS.md +174 -59
- package/STYLE_GUIDE.md +2 -2
- package/USER_JOURNEYS.md +482 -139
- package/foundry.toml +1 -1
- package/package.json +7 -7
- package/script/Deploy.s.sol +1 -1
- package/script/helpers/DefifaDeploymentLib.sol +2 -2
- package/src/DefifaDeployer.sol +2 -2
- package/src/DefifaGovernor.sol +1 -1
- package/src/DefifaHook.sol +7 -6
- package/src/DefifaProjectOwner.sol +1 -1
- package/src/DefifaTokenUriResolver.sol +1 -1
- package/src/libraries/DefifaFontImporter.sol +1 -1
- package/src/libraries/DefifaHookLib.sol +1 -1
- package/test/DefifaAdversarialQuorum.t.sol +1 -1
- package/test/DefifaAuditLowGuards.t.sol +1 -1
- package/test/DefifaFeeAccounting.t.sol +1 -1
- package/test/DefifaGovernor.t.sol +1 -1
- package/test/DefifaHookRegressions.t.sol +39 -1
- package/test/DefifaMintCostInvariant.t.sol +1 -1
- package/test/DefifaNoContest.t.sol +1 -1
- package/test/DefifaSecurity.t.sol +1 -1
- package/test/DefifaUSDC.t.sol +1 -1
- package/test/Fork.t.sol +1 -1
- package/test/SVG.t.sol +1 -1
- package/test/TestAuditGaps.sol +1 -1
- package/test/TestQALastMile.t.sol +1 -1
- package/test/audit/CodexAttestationDoubleCount.t.sol +217 -0
- package/test/deployScript.t.sol +1 -1
- package/test/regression/AttestationDelegateBeneficiary.t.sol +272 -0
- package/test/regression/FulfillmentBlocksRatification.t.sol +1 -1
- package/test/regression/GracePeriodBypass.t.sol +1 -1
package/USER_JOURNEYS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# defifa-collection-deployer-v6 -- 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, and
|
|
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.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -23,9 +23,35 @@ Complete interaction paths for every user role in the Defifa prediction game sys
|
|
|
23
23
|
|
|
24
24
|
## Journey 1: Create a Game
|
|
25
25
|
|
|
26
|
+
**Entry point:** `DefifaDeployer.launchGameWith(DefifaLaunchProjectData memory launchProjectData) external returns (uint256 gameId)`
|
|
27
|
+
|
|
28
|
+
**Who can call:** Anyone (no access control).
|
|
29
|
+
|
|
26
30
|
**Actor:** Game Creator
|
|
27
31
|
**Phase:** Any (game is created and starts in COUNTDOWN)
|
|
28
32
|
|
|
33
|
+
### Parameters
|
|
34
|
+
|
|
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.
|
|
54
|
+
|
|
29
55
|
### Step 1: Prepare launch data
|
|
30
56
|
|
|
31
57
|
Build a `DefifaLaunchProjectData` struct:
|
|
@@ -65,9 +91,9 @@ DefifaLaunchProjectData({
|
|
|
65
91
|
splits: [], // Optional custom splits
|
|
66
92
|
attestationStartTime: 0, // 0 = block.timestamp at deploy
|
|
67
93
|
attestationGracePeriod: 0, // 0 = enforced minimum of 1 day
|
|
68
|
-
defaultAttestationDelegate: address(0), // 0 = each
|
|
94
|
+
defaultAttestationDelegate: address(0), // 0 = each beneficiary delegates to self
|
|
69
95
|
defaultTokenUriResolver: IJB721TokenUriResolver(address(0)), // use default SVG
|
|
70
|
-
terminal: jbMultiTerminal,
|
|
96
|
+
terminal: IJBTerminal(address(jbMultiTerminal)),
|
|
71
97
|
store: jb721TiersHookStore,
|
|
72
98
|
minParticipation: 1 ether, // Game needs >= 1 ETH to proceed
|
|
73
99
|
scorecardTimeout: 7 days // 7 days to ratify or NO_CONTEST
|
|
@@ -80,20 +106,32 @@ DefifaLaunchProjectData({
|
|
|
80
106
|
uint256 gameId = deployer.launchGameWith(launchProjectData);
|
|
81
107
|
```
|
|
82
108
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
109
|
+
### State changes
|
|
110
|
+
|
|
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).
|
|
124
|
+
|
|
125
|
+
### Events
|
|
126
|
+
|
|
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.
|
|
129
|
+
|
|
130
|
+
### Edge cases
|
|
131
|
+
|
|
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%.
|
|
97
135
|
- If `start == 0`: auto-calculated as `block.timestamp + mintPeriodDuration + refundPeriodDuration`.
|
|
98
136
|
- If `start > 0` and `mintPeriodDuration == 0`: mint duration auto-fills to `start - block.timestamp - refundPeriodDuration`.
|
|
99
137
|
- MINT ruleset `mustStartAtOrAfter = start - mintPeriodDuration - refundPeriodDuration`.
|
|
@@ -104,9 +142,23 @@ uint256 gameId = deployer.launchGameWith(launchProjectData);
|
|
|
104
142
|
|
|
105
143
|
## Journey 2: Play a Game (Buy NFTs)
|
|
106
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
|
+
|
|
107
149
|
**Actor:** Player
|
|
108
150
|
**Phase:** MINT
|
|
109
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
|
+
|
|
110
162
|
### Step 1: Prepare payment metadata
|
|
111
163
|
|
|
112
164
|
Encode the tier IDs to mint and optional attestation delegate:
|
|
@@ -145,28 +197,50 @@ jbMultiTerminal.pay{value: 0.02 ether}({
|
|
|
145
197
|
});
|
|
146
198
|
```
|
|
147
199
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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.
|
|
157
214
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
-
|
|
161
|
-
-
|
|
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.
|
|
162
222
|
|
|
163
223
|
---
|
|
164
224
|
|
|
165
225
|
## Journey 3: Refund During MINT Phase
|
|
166
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
|
+
|
|
167
231
|
**Actor:** Refunder
|
|
168
232
|
**Phase:** MINT
|
|
169
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
|
+
|
|
170
244
|
### Step 1: Prepare cash-out metadata
|
|
171
245
|
|
|
172
246
|
```solidity
|
|
@@ -193,30 +267,66 @@ jbMultiTerminal.cashOutTokensOf({
|
|
|
193
267
|
});
|
|
194
268
|
```
|
|
195
269
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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.
|
|
203
288
|
|
|
204
289
|
---
|
|
205
290
|
|
|
206
291
|
## Journey 4: Refund During REFUND Phase
|
|
207
292
|
|
|
293
|
+
**Entry point:** Same as Journey 3: `JBMultiTerminal.cashOutTokensOf(...)`
|
|
294
|
+
|
|
295
|
+
**Who can call:** Anyone (same restrictions as Journey 3).
|
|
296
|
+
|
|
208
297
|
**Actor:** Refunder
|
|
209
298
|
**Phase:** REFUND
|
|
210
299
|
|
|
211
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`.
|
|
212
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
|
+
|
|
213
314
|
---
|
|
214
315
|
|
|
215
316
|
## Journey 5: Submit a Scorecard
|
|
216
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
|
+
|
|
217
322
|
**Actor:** Scorer (anyone)
|
|
218
323
|
**Phase:** SCORING
|
|
219
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
|
+
|
|
220
330
|
### Step 1: Prepare tier weights
|
|
221
331
|
|
|
222
332
|
All weights must sum to exactly `TOTAL_CASHOUT_WEIGHT` (1e18). Tier IDs must be in ascending order.
|
|
@@ -235,25 +345,41 @@ tierWeights[2] = DefifaTierCashOutWeight({id: 3, cashOutWeight: 200_000_000_000_
|
|
|
235
345
|
uint256 scorecardId = governor.submitScorecardFor(gameId, tierWeights);
|
|
236
346
|
```
|
|
237
347
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
7. If sender is `defaultAttestationDelegate`: stores as `defaultAttestationDelegateProposalOf`.
|
|
246
|
-
8. Emits `ScorecardSubmitted(gameId, scorecardId, tierWeights, isDefault, msg.sender)`.
|
|
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.defaultAttestationDelegateProposalOf[gameId]` -- Set to `scorecardId` if sender is the default attestation delegate.
|
|
353
|
+
|
|
354
|
+
### Events
|
|
247
355
|
|
|
248
|
-
|
|
356
|
+
- `ScorecardSubmitted(uint256 indexed gameId, uint256 indexed scorecardId, DefifaTierCashOutWeight[] tierWeights, bool isDefaultAttestationDelegate, address caller)` -- Emitted by `DefifaGovernor`.
|
|
357
|
+
|
|
358
|
+
### Edge cases
|
|
359
|
+
|
|
360
|
+
- `DefifaGovernor_AlreadyRatified` -- A scorecard has already been ratified for this game.
|
|
361
|
+
- `DefifaGovernor_GameNotFound` -- Game not initialized (`_packedScorecardInfoOf[gameId] == 0`).
|
|
362
|
+
- `DefifaGovernor_NotAllowed` -- Game not in SCORING phase.
|
|
363
|
+
- `DefifaGovernor_UnownedProposedCashoutValue` -- Weight > 0 assigned to a tier with `currentSupplyOfTier == 0`.
|
|
364
|
+
- `DefifaGovernor_DuplicateScorecard` -- Identical scorecard (same hash) already submitted.
|
|
365
|
+
- Scorecard state starts as PENDING (until `attestationsBegin`) or ACTIVE (if attestations start immediately).
|
|
249
366
|
|
|
250
367
|
---
|
|
251
368
|
|
|
252
369
|
## Journey 6: Attest to a Scorecard
|
|
253
370
|
|
|
371
|
+
**Entry point:** `DefifaGovernor.attestToScorecardFrom(uint256 gameId, uint256 scorecardId) external returns (uint256 weight)`
|
|
372
|
+
|
|
373
|
+
**Who can call:** Anyone. However, attestation weight is zero unless the caller (or their delegate) held NFTs at the `attestationsBegin` snapshot timestamp.
|
|
374
|
+
|
|
254
375
|
**Actor:** Attestor (NFT holder or delegate)
|
|
255
376
|
**Phase:** SCORING
|
|
256
377
|
|
|
378
|
+
### Parameters
|
|
379
|
+
|
|
380
|
+
- `gameId` -- The ID of the game.
|
|
381
|
+
- `scorecardId` -- The scorecard ID to attest to.
|
|
382
|
+
|
|
257
383
|
### Step 1: Get scorecard ID
|
|
258
384
|
|
|
259
385
|
Either compute it or use the ID from the `ScorecardSubmitted` event:
|
|
@@ -268,28 +394,39 @@ uint256 scorecardId = governor.scorecardIdOf(hookAddress, tierWeights);
|
|
|
268
394
|
uint256 weight = governor.attestToScorecardFrom(gameId, scorecardId);
|
|
269
395
|
```
|
|
270
396
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
397
|
+
### State changes
|
|
398
|
+
|
|
399
|
+
1. `DefifaGovernor._scorecardAttestationsOf[gameId][scorecardId].count` -- Incremented by `weight`.
|
|
400
|
+
2. `DefifaGovernor._scorecardAttestationsOf[gameId][scorecardId].hasAttested[msg.sender]` -- Set to `true`.
|
|
401
|
+
|
|
402
|
+
### Events
|
|
403
|
+
|
|
404
|
+
- `ScorecardAttested(uint256 indexed gameId, uint256 indexed scorecardId, uint256 weight, address caller)` -- Emitted by `DefifaGovernor`.
|
|
405
|
+
|
|
406
|
+
### Edge cases
|
|
280
407
|
|
|
281
|
-
|
|
282
|
-
-
|
|
283
|
-
-
|
|
408
|
+
- `DefifaGovernor_NotAllowed` -- Game not in SCORING phase, or scorecard not in ACTIVE/SUCCEEDED state.
|
|
409
|
+
- `DefifaGovernor_AlreadyAttested` -- Account already attested to this scorecard.
|
|
410
|
+
- `DefifaGovernor_UnknownProposal` -- Scorecard ID has no submission record.
|
|
411
|
+
- Attestation weight is computed at `attestationsBegin` timestamp using checkpointed values (snapshot, not live).
|
|
284
412
|
- Each tier caps at `MAX_ATTESTATION_POWER_TIER` (1e9) regardless of how many tokens exist in that tier.
|
|
285
413
|
|
|
286
414
|
---
|
|
287
415
|
|
|
288
416
|
## Journey 7: Ratify a Scorecard
|
|
289
417
|
|
|
418
|
+
**Entry point:** `DefifaGovernor.ratifyScorecardFrom(uint256 gameId, DefifaTierCashOutWeight[] calldata tierWeights) external returns (uint256 scorecardId)`
|
|
419
|
+
|
|
420
|
+
**Who can call:** Anyone. No access control -- the function validates that the scorecard is in SUCCEEDED state.
|
|
421
|
+
|
|
290
422
|
**Actor:** Ratifier (anyone)
|
|
291
423
|
**Phase:** SCORING (scorecard in SUCCEEDED state)
|
|
292
424
|
|
|
425
|
+
### Parameters
|
|
426
|
+
|
|
427
|
+
- `gameId` -- The ID of the game.
|
|
428
|
+
- `tierWeights` -- The tier weights that match the scorecard being ratified (used to recompute the scorecard hash).
|
|
429
|
+
|
|
293
430
|
### Precondition
|
|
294
431
|
|
|
295
432
|
A scorecard must be in SUCCEEDED state:
|
|
@@ -303,27 +440,45 @@ A scorecard must be in SUCCEEDED state:
|
|
|
303
440
|
uint256 scorecardId = governor.ratifyScorecardFrom(gameId, tierWeights);
|
|
304
441
|
```
|
|
305
442
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
5.
|
|
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)`.
|
|
443
|
+
### State changes
|
|
444
|
+
|
|
445
|
+
1. `DefifaGovernor.ratifiedScorecardIdOf[gameId]` -- Set to `scorecardId`.
|
|
446
|
+
2. `DefifaHook._tierCashOutWeights` -- Set via `setTierCashOutWeightsTo()` executed as a low-level call.
|
|
447
|
+
3. `DefifaHook.cashOutWeightIsSet` -- Set to `true`.
|
|
448
|
+
4. `DefifaDeployer.fulfilledCommitmentsOf[gameId]` -- Set to the fee amount (or sentinel value 1 if pot is 0 or payout fails).
|
|
449
|
+
5. Final ruleset queued via `CONTROLLER.queueRulesetsOf()` with no payout limits.
|
|
317
450
|
|
|
318
|
-
|
|
451
|
+
### Events
|
|
452
|
+
|
|
453
|
+
- `TierCashOutWeightsSet(DefifaTierCashOutWeight[] tierWeights, address caller)` -- Emitted by `DefifaHook.setTierCashOutWeightsTo()`.
|
|
454
|
+
- `FulfilledCommitments(uint256 indexed gameId, uint256 pot, address caller)` -- Emitted by `DefifaDeployer.fulfillCommitmentsOf()`.
|
|
455
|
+
- `CommitmentPayoutFailed(uint256 indexed gameId, uint256 amount, bytes reason)` -- Emitted if `sendPayoutsOf` fails (try-catch).
|
|
456
|
+
- `ScorecardRatified(uint256 indexed gameId, uint256 indexed scorecardId, address caller)` -- Emitted by `DefifaGovernor`.
|
|
457
|
+
|
|
458
|
+
### Edge cases
|
|
459
|
+
|
|
460
|
+
- `DefifaGovernor_AlreadyRatified` -- Game already has a ratified scorecard.
|
|
461
|
+
- `DefifaGovernor_NotAllowed` -- Scorecard not in SUCCEEDED state.
|
|
462
|
+
- `DefifaGovernor_UnknownProposal` -- Scorecard ID has no submission record.
|
|
463
|
+
- If `sendPayoutsOf` fails: try-catch emits `CommitmentPayoutFailed`, fee stays in pot, but final ruleset is still queued.
|
|
464
|
+
- If `queueRulesetsOf` fails: the entire ratification reverts (no try-catch on that call).
|
|
465
|
+
- Game state transitions to COMPLETE because `cashOutWeightIsSet == true`.
|
|
319
466
|
|
|
320
467
|
---
|
|
321
468
|
|
|
322
469
|
## Journey 8: Cash Out as Winner
|
|
323
470
|
|
|
471
|
+
**Entry point:** `JBMultiTerminal.cashOutTokensOf(address holder, uint256 projectId, uint256 cashOutCount, address tokenToReclaim, uint256 minTokensReclaimed, address payable beneficiary, bytes metadata) external returns (uint256)`
|
|
472
|
+
|
|
473
|
+
**Who can call:** Anyone can initiate, but the hook validates that `context.holder` owns the tokens being burned.
|
|
474
|
+
|
|
324
475
|
**Actor:** Winner
|
|
325
476
|
**Phase:** COMPLETE
|
|
326
477
|
|
|
478
|
+
### Parameters
|
|
479
|
+
|
|
480
|
+
Same as Journey 3 (Refund).
|
|
481
|
+
|
|
327
482
|
### Step 1: Check claimable amounts
|
|
328
483
|
|
|
329
484
|
```solidity
|
|
@@ -357,26 +512,37 @@ jbMultiTerminal.cashOutTokensOf({
|
|
|
357
512
|
});
|
|
358
513
|
```
|
|
359
514
|
|
|
360
|
-
|
|
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`.
|
|
515
|
+
### State changes
|
|
367
516
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
517
|
+
1. ERC-721 tokens burned via `DefifaHook._burn(tokenId)`.
|
|
518
|
+
2. `DefifaHook.tokensRedeemedFrom[tierId]` -- Incremented for each burned token (only during COMPLETE).
|
|
519
|
+
3. `DefifaHook.amountRedeemed` -- Incremented by `context.reclaimedAmount.value`.
|
|
520
|
+
4. `DefifaHook._totalMintCost` -- Decremented by `cumulativeMintPrice` of burned tokens.
|
|
521
|
+
5. Fee tokens ($DEFIFA and $NANA) transferred to holder proportional to their mint cost share.
|
|
522
|
+
6. `JB721TiersHookStore` records the burn.
|
|
373
523
|
|
|
374
|
-
|
|
524
|
+
### Events
|
|
525
|
+
|
|
526
|
+
- `ClaimedTokens(address indexed beneficiary, uint256 defifaTokenAmount, uint256 baseProtocolTokenAmount, address caller)` -- Emitted by `DefifaHookLib.claimTokensFor()` when fee tokens are distributed.
|
|
527
|
+
|
|
528
|
+
Standard ERC-721 `Transfer(from, address(0), tokenId)` emitted by the burn. Standard ERC-20 `Transfer` events emitted by the token transfers.
|
|
529
|
+
|
|
530
|
+
### Edge cases
|
|
531
|
+
|
|
532
|
+
- `DefifaHook_Unauthorized(tokenId, owner, caller)` -- Token holder in context does not own the token.
|
|
533
|
+
- `DefifaHook_NothingToClaim` -- Reclaimed amount is 0 AND no fee tokens distributed.
|
|
534
|
+
- `JB721Hook_InvalidCashOut` -- Caller not a terminal, or wrong project ID.
|
|
535
|
+
- Reclaim calculation: `perTokenWeight = tierCashOutWeight[tierId] / totalTokensForCashoutInTier`, then `reclaimAmount = mulDiv(surplus + amountRedeemed, perTokenWeight, TOTAL_CASHOUT_WEIGHT)`.
|
|
536
|
+
- `totalTokensForCashoutInTier = initialSupply - remainingSupply - (burnedTokens - tokensRedeemedFrom[tierId])`.
|
|
375
537
|
|
|
376
538
|
---
|
|
377
539
|
|
|
378
540
|
## Journey 9: Cash Out from Losing Tier
|
|
379
541
|
|
|
542
|
+
**Entry point:** Same as Journey 8: `JBMultiTerminal.cashOutTokensOf(...)`
|
|
543
|
+
|
|
544
|
+
**Who can call:** Anyone (same restrictions as Journey 8).
|
|
545
|
+
|
|
380
546
|
**Actor:** Holder of a zero-weight tier
|
|
381
547
|
**Phase:** COMPLETE
|
|
382
548
|
|
|
@@ -387,18 +553,37 @@ If a tier received `cashOutWeight = 0` in the ratified scorecard:
|
|
|
387
553
|
// beforeCashOutRecordedWith returns cashOutCount = 0
|
|
388
554
|
// afterCashOutRecordedWith: reclaimedAmount.value == 0
|
|
389
555
|
// _claimTokensFor is called -- if fee tokens exist, they are distributed
|
|
390
|
-
// If no fee tokens distributed either
|
|
556
|
+
// If no fee tokens distributed either -> reverts with DefifaHook_NothingToClaim
|
|
391
557
|
```
|
|
392
558
|
|
|
393
|
-
|
|
559
|
+
### State changes
|
|
560
|
+
|
|
561
|
+
Same as Journey 8, but `context.reclaimedAmount.value == 0`.
|
|
562
|
+
|
|
563
|
+
### Events
|
|
564
|
+
|
|
565
|
+
- `ClaimedTokens(address indexed beneficiary, uint256 defifaTokenAmount, uint256 baseProtocolTokenAmount, address caller)` -- Emitted only if fee tokens are available to distribute.
|
|
566
|
+
|
|
567
|
+
### Edge cases
|
|
568
|
+
|
|
569
|
+
- `DefifaHook_NothingToClaim` -- Reverts if both reclaimed ETH is 0 AND no fee tokens are distributed.
|
|
570
|
+
- 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.
|
|
394
571
|
|
|
395
572
|
---
|
|
396
573
|
|
|
397
574
|
## Journey 10: No-Contest via Minimum Participation
|
|
398
575
|
|
|
576
|
+
**Entry point:** `DefifaDeployer.triggerNoContestFor(uint256 gameId) external`
|
|
577
|
+
|
|
578
|
+
**Who can call:** Anyone. No access control -- the function validates that the game is in NO_CONTEST phase.
|
|
579
|
+
|
|
399
580
|
**Actor:** Any user
|
|
400
581
|
**Phase:** SCORING (when balance < minParticipation)
|
|
401
582
|
|
|
583
|
+
### Parameters
|
|
584
|
+
|
|
585
|
+
- `gameId` -- The ID of the game to trigger no-contest for.
|
|
586
|
+
|
|
402
587
|
### Scenario
|
|
403
588
|
|
|
404
589
|
Game had `minParticipation = 10 ether`. During MINT, 5 ETH was deposited but then refunded down to 3 ETH. When SCORING begins, `currentGamePhaseOf()` checks:
|
|
@@ -418,12 +603,20 @@ Since 3 ETH < 10 ETH, the game is NO_CONTEST.
|
|
|
418
603
|
deployer.triggerNoContestFor(gameId);
|
|
419
604
|
```
|
|
420
605
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
606
|
+
### State changes
|
|
607
|
+
|
|
608
|
+
1. `DefifaDeployer.noContestTriggeredFor[gameId]` -- Set to `true`.
|
|
609
|
+
2. New ruleset queued via `CONTROLLER.queueRulesetsOf()` with no `fundAccessLimitGroups`, making entire balance = surplus. Has `pausePay: true` and `cashOutTaxRate: 0`.
|
|
610
|
+
|
|
611
|
+
### Events
|
|
612
|
+
|
|
613
|
+
- `QueuedNoContest(uint256 indexed gameId, address caller)` -- Emitted by `DefifaDeployer`.
|
|
614
|
+
|
|
615
|
+
### Edge cases
|
|
616
|
+
|
|
617
|
+
- `DefifaDeployer_NotNoContest` -- Game not in NO_CONTEST phase.
|
|
618
|
+
- `DefifaDeployer_NoContestAlreadyTriggered` -- Already triggered for this game.
|
|
619
|
+
- 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.
|
|
427
620
|
|
|
428
621
|
### Step 2: Cash out (full refund)
|
|
429
622
|
|
|
@@ -433,9 +626,17 @@ After triggering, users can cash out at mint price (same as Journey 3/4). The ne
|
|
|
433
626
|
|
|
434
627
|
## Journey 11: No-Contest via Scorecard Timeout
|
|
435
628
|
|
|
629
|
+
**Entry point:** Same as Journey 10: `DefifaDeployer.triggerNoContestFor(uint256 gameId) external`
|
|
630
|
+
|
|
631
|
+
**Who can call:** Anyone. Same restrictions as Journey 10.
|
|
632
|
+
|
|
436
633
|
**Actor:** Any user
|
|
437
634
|
**Phase:** SCORING (when timeout elapsed without ratification)
|
|
438
635
|
|
|
636
|
+
### Parameters
|
|
637
|
+
|
|
638
|
+
- `gameId` -- The ID of the game.
|
|
639
|
+
|
|
439
640
|
### Scenario
|
|
440
641
|
|
|
441
642
|
Game had `scorecardTimeout = 7 days`. Scoring started 8 days ago. No scorecard was ratified.
|
|
@@ -446,19 +647,40 @@ if (_ops.scorecardTimeout > 0 && block.timestamp > _currentRuleset.start + _ops.
|
|
|
446
647
|
}
|
|
447
648
|
```
|
|
448
649
|
|
|
449
|
-
###
|
|
650
|
+
### State changes
|
|
450
651
|
|
|
451
|
-
Same as Journey 10
|
|
652
|
+
Same as Journey 10.
|
|
452
653
|
|
|
453
|
-
|
|
654
|
+
### Events
|
|
655
|
+
|
|
656
|
+
Same as Journey 10: `QueuedNoContest(uint256 indexed gameId, address caller)`.
|
|
657
|
+
|
|
658
|
+
### Edge cases
|
|
659
|
+
|
|
660
|
+
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()`.
|
|
454
661
|
|
|
455
662
|
---
|
|
456
663
|
|
|
457
664
|
## Journey 12: Delegate Attestation Power
|
|
458
665
|
|
|
666
|
+
**Entry point (single tier):** `DefifaHook.setTierDelegateTo(address delegatee, uint256 tierId) public`
|
|
667
|
+
|
|
668
|
+
**Entry point (multiple tiers):** `DefifaHook.setTierDelegatesTo(DefifaDelegation[] memory delegations) external`
|
|
669
|
+
|
|
670
|
+
**Who can call:** Any NFT holder (`msg.sender` is the delegator). Only callable during MINT phase.
|
|
671
|
+
|
|
459
672
|
**Actor:** Player (NFT holder)
|
|
460
673
|
**Phase:** MINT only
|
|
461
674
|
|
|
675
|
+
### Parameters (single)
|
|
676
|
+
|
|
677
|
+
- `delegatee` -- Address to delegate attestation power to. Cannot be `address(0)`.
|
|
678
|
+
- `tierId` -- The tier ID to delegate attestation units for.
|
|
679
|
+
|
|
680
|
+
### Parameters (multiple)
|
|
681
|
+
|
|
682
|
+
- `delegations` -- Array of `DefifaDelegation` structs, each containing `delegatee` and `tierId`.
|
|
683
|
+
|
|
462
684
|
### Single tier delegation
|
|
463
685
|
|
|
464
686
|
```solidity
|
|
@@ -475,17 +697,44 @@ delegations[1] = DefifaDelegation({delegatee: anotherDelegate, tierId: 2});
|
|
|
475
697
|
hook.setTierDelegatesTo(delegations);
|
|
476
698
|
```
|
|
477
699
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
700
|
+
### State changes
|
|
701
|
+
|
|
702
|
+
1. `DefifaHook._tierDelegation[msg.sender][tierId]` -- Set to the new `delegatee`.
|
|
703
|
+
2. `DefifaHook._delegateTierCheckpoints[oldDelegate][tierId]` -- Checkpointed with decreased attestation units.
|
|
704
|
+
3. `DefifaHook._delegateTierCheckpoints[newDelegate][tierId]` -- Checkpointed with increased attestation units.
|
|
705
|
+
|
|
706
|
+
### Events
|
|
707
|
+
|
|
708
|
+
- `DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate)` -- Emitted per tier delegation change.
|
|
709
|
+
- `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).
|
|
710
|
+
|
|
711
|
+
### Edge cases
|
|
712
|
+
|
|
713
|
+
- `DefifaHook_DelegateAddressZero` -- Delegatee is `address(0)`.
|
|
714
|
+
- `DefifaHook_DelegateChangesUnavailableInThisPhase` -- Not in MINT phase.
|
|
715
|
+
- On NFT transfer after MINT: auto-delegates to recipient if recipient has no delegate.
|
|
482
716
|
|
|
483
717
|
---
|
|
484
718
|
|
|
485
719
|
## Journey 13: Mint Reserved Tokens
|
|
486
720
|
|
|
721
|
+
**Entry point (single tier):** `DefifaHook.mintReservesFor(uint256 tierId, uint256 count) public`
|
|
722
|
+
|
|
723
|
+
**Entry point (multiple tiers):** `DefifaHook.mintReservesFor(JB721TiersMintReservesConfig[] calldata mintReservesForTiersData) external`
|
|
724
|
+
|
|
725
|
+
**Who can call:** Anyone. No access control. Must not be paused (`pauseMintPendingReserves` must be false).
|
|
726
|
+
|
|
487
727
|
**Actor:** Anyone
|
|
488
|
-
**Phase:**
|
|
728
|
+
**Phase:** SCORING or later (reserved minting is paused during both MINT and REFUND via `pauseMintPendingReserves: true`)
|
|
729
|
+
|
|
730
|
+
### Parameters (single)
|
|
731
|
+
|
|
732
|
+
- `tierId` -- The tier ID to mint reserved tokens for.
|
|
733
|
+
- `count` -- Number of reserved tokens to mint.
|
|
734
|
+
|
|
735
|
+
### Parameters (multiple)
|
|
736
|
+
|
|
737
|
+
- `mintReservesForTiersData` -- Array of `JB721TiersMintReservesConfig` structs, each containing `tierId` and `count`.
|
|
489
738
|
|
|
490
739
|
### Single tier
|
|
491
740
|
|
|
@@ -502,61 +751,110 @@ configs[0] = JB721TiersMintReservesConfig({tierId: 1, count: 5});
|
|
|
502
751
|
hook.mintReservesFor(configs);
|
|
503
752
|
```
|
|
504
753
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
754
|
+
### State changes
|
|
755
|
+
|
|
756
|
+
1. `DefifaHook._totalMintCost` -- Incremented by `tier.price * count`.
|
|
757
|
+
2. `DefifaHook._tierDelegation[beneficiary][tierId]` -- Set to `defaultAttestationDelegate` or self (if no delegate exists).
|
|
758
|
+
3. `DefifaHook._delegateTierCheckpoints[delegate][tierId]` -- Checkpointed with new attestation units.
|
|
759
|
+
4. `DefifaHook._totalTierCheckpoints[tierId]` -- Checkpointed with increased total attestation units.
|
|
760
|
+
5. ERC-721 tokens minted to `reserveBeneficiary`.
|
|
761
|
+
6. `JB721TiersHookStore` records the reserve mint.
|
|
762
|
+
|
|
763
|
+
### Events
|
|
764
|
+
|
|
765
|
+
- `MintReservedToken(uint256 indexed tokenId, uint256 indexed tierId, address indexed beneficiary, address caller)` -- Emitted per reserved token minted.
|
|
766
|
+
- `DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate)` -- Emitted if delegation is set for the reserve beneficiary.
|
|
767
|
+
- `TierDelegateAttestationsChanged(address indexed delegate, uint256 indexed tierId, uint256 previousBalance, uint256 newBalance, address caller)` -- Emitted when attestation units are transferred to the delegate.
|
|
512
768
|
|
|
513
|
-
|
|
769
|
+
### Edge cases
|
|
770
|
+
|
|
771
|
+
- `DefifaHook_ReservedTokenMintingPaused` -- `pauseMintPendingReserves` is true in current ruleset metadata.
|
|
772
|
+
- 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
773
|
|
|
515
774
|
---
|
|
516
775
|
|
|
517
776
|
## Journey 14: Fulfill Commitments Separately
|
|
518
777
|
|
|
778
|
+
**Entry point:** `DefifaDeployer.fulfillCommitmentsOf(uint256 gameId) external`
|
|
779
|
+
|
|
780
|
+
**Who can call:** Anyone. No access control. Requires `cashOutWeightIsSet == true`.
|
|
781
|
+
|
|
519
782
|
**Actor:** Anyone
|
|
520
783
|
**Phase:** COMPLETE (after scorecard ratification)
|
|
521
784
|
|
|
785
|
+
### Parameters
|
|
786
|
+
|
|
787
|
+
- `gameId` -- The ID of the game to fulfill commitments for.
|
|
788
|
+
|
|
522
789
|
`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
790
|
|
|
524
|
-
If needed, `fulfillCommitmentsOf` can be called again manually
|
|
791
|
+
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
792
|
|
|
526
793
|
```solidity
|
|
527
794
|
deployer.fulfillCommitmentsOf(gameId);
|
|
528
795
|
```
|
|
529
796
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
797
|
+
### State changes
|
|
798
|
+
|
|
799
|
+
1. `DefifaDeployer.fulfilledCommitmentsOf[gameId]` -- Set to fee amount (or sentinel value 1 if pot is 0 or payout fails).
|
|
800
|
+
2. Fee payouts sent via `terminal.sendPayoutsOf()` (distributes to splits).
|
|
801
|
+
3. Final ruleset queued via `CONTROLLER.queueRulesetsOf()` with no payout limits.
|
|
802
|
+
|
|
803
|
+
### Events
|
|
804
|
+
|
|
805
|
+
- `FulfilledCommitments(uint256 indexed gameId, uint256 pot, address caller)` -- Emitted by `DefifaDeployer` on success.
|
|
806
|
+
- `CommitmentPayoutFailed(uint256 indexed gameId, uint256 amount, bytes reason)` -- Emitted if `sendPayoutsOf` fails (try-catch).
|
|
807
|
+
|
|
808
|
+
### Edge cases
|
|
809
|
+
|
|
810
|
+
- `DefifaDeployer_CantFulfillYet` -- `cashOutWeightIsSet == false`.
|
|
811
|
+
- Idempotent: If `fulfilledCommitmentsOf[gameId] != 0`, returns immediately without reverting.
|
|
812
|
+
- Fee computation: `mulDiv(pot, _commitmentPercentOf[gameId], SPLITS_TOTAL_PERCENT)`.
|
|
536
813
|
|
|
537
814
|
---
|
|
538
815
|
|
|
539
816
|
## Journey 15: Transfer NFT to Another Player
|
|
540
817
|
|
|
818
|
+
**Entry point:** `DefifaHook.transferFrom(address from, address to, uint256 tokenId) external` or `DefifaHook.safeTransferFrom(address from, address to, uint256 tokenId) external`
|
|
819
|
+
|
|
820
|
+
**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.
|
|
821
|
+
|
|
541
822
|
**Actor:** NFT holder
|
|
542
823
|
**Phase:** Any (unless `transfersPausable` is set and transfers are paused)
|
|
543
824
|
|
|
825
|
+
### Parameters
|
|
826
|
+
|
|
827
|
+
- `from` -- Current token owner.
|
|
828
|
+
- `to` -- Recipient address.
|
|
829
|
+
- `tokenId` -- The token to transfer.
|
|
830
|
+
|
|
544
831
|
```solidity
|
|
545
832
|
hook.transferFrom(from, to, tokenId);
|
|
546
833
|
// or
|
|
547
834
|
hook.safeTransferFrom(from, to, tokenId);
|
|
548
835
|
```
|
|
549
836
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
837
|
+
### State changes
|
|
838
|
+
|
|
839
|
+
1. ERC-721 ownership updated from `from` to `to`.
|
|
840
|
+
2. `DefifaHook._firstOwnerOf[tokenId]` -- Stored as `from` on first transfer of this token.
|
|
841
|
+
3. `JB721TiersHookStore` records the transfer via `recordTransferForTier(tierId, from, to)`.
|
|
842
|
+
4. `DefifaHook._tierDelegation[to][tierId]` -- Auto-set to `to` if recipient has no delegate.
|
|
843
|
+
5. `DefifaHook._delegateTierCheckpoints[fromDelegate][tierId]` -- Checkpointed with decreased attestation units.
|
|
844
|
+
6. `DefifaHook._delegateTierCheckpoints[toDelegate][tierId]` -- Checkpointed with increased attestation units.
|
|
845
|
+
|
|
846
|
+
### Events
|
|
847
|
+
|
|
848
|
+
- `DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate)` -- Emitted if recipient has no delegate and auto-delegates to self.
|
|
849
|
+
- `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).
|
|
850
|
+
|
|
851
|
+
Standard ERC-721 `Transfer(from, to, tokenId)` is also emitted.
|
|
852
|
+
|
|
853
|
+
### Edge cases
|
|
854
|
+
|
|
855
|
+
- `DefifaHook_TransfersPaused` -- `transfersPausable` is set and transfers paused in current ruleset.
|
|
856
|
+
- On transfer after MINT phase: attestation units are transferred but delegation cannot be changed by the sender.
|
|
857
|
+
- Auto-delegation: if recipient has no delegate, they auto-delegate to themselves.
|
|
560
858
|
|
|
561
859
|
---
|
|
562
860
|
|
|
@@ -566,12 +864,20 @@ hook.safeTransferFrom(from, to, tokenId);
|
|
|
566
864
|
|
|
567
865
|
### Check game phase
|
|
568
866
|
|
|
867
|
+
**Entry point:** `DefifaDeployer.currentGamePhaseOf(uint256 gameId) public view returns (DefifaGamePhase)`
|
|
868
|
+
|
|
869
|
+
**Who can call:** Anyone (view function).
|
|
870
|
+
|
|
569
871
|
```solidity
|
|
570
872
|
DefifaGamePhase phase = deployer.currentGamePhaseOf(gameId);
|
|
571
873
|
```
|
|
572
874
|
|
|
573
875
|
### Check game pot
|
|
574
876
|
|
|
877
|
+
**Entry point:** `DefifaDeployer.currentGamePotOf(uint256 gameId, bool includeCommitments) external view returns (uint256, address, uint256)`
|
|
878
|
+
|
|
879
|
+
**Who can call:** Anyone (view function).
|
|
880
|
+
|
|
575
881
|
```solidity
|
|
576
882
|
(uint256 pot, address token, uint256 decimals) = deployer.currentGamePotOf(gameId, false);
|
|
577
883
|
// includeCommitments = true adds fulfilled fee amounts back
|
|
@@ -579,25 +885,44 @@ DefifaGamePhase phase = deployer.currentGamePhaseOf(gameId);
|
|
|
579
885
|
|
|
580
886
|
### Check timing
|
|
581
887
|
|
|
888
|
+
**Entry point:** `DefifaDeployer.timesFor(uint256 gameId) external view returns (uint48, uint24, uint24)`
|
|
889
|
+
|
|
890
|
+
**Who can call:** Anyone (view function).
|
|
891
|
+
|
|
582
892
|
```solidity
|
|
583
893
|
(uint48 start, uint24 mintDuration, uint24 refundDuration) = deployer.timesFor(gameId);
|
|
584
894
|
```
|
|
585
895
|
|
|
586
896
|
### Check safety params
|
|
587
897
|
|
|
898
|
+
**Entry point:** `DefifaDeployer.safetyParamsOf(uint256 gameId) external view returns (uint256 minParticipation, uint32 scorecardTimeout)`
|
|
899
|
+
|
|
900
|
+
**Who can call:** Anyone (view function).
|
|
901
|
+
|
|
588
902
|
```solidity
|
|
589
903
|
(uint256 minParticipation, uint32 scorecardTimeout) = deployer.safetyParamsOf(gameId);
|
|
590
904
|
```
|
|
591
905
|
|
|
592
906
|
### Check scorecard state
|
|
593
907
|
|
|
908
|
+
**Entry point:** `DefifaGovernor.stateOf(uint256 gameId, uint256 scorecardId) public view returns (DefifaScorecardState)`
|
|
909
|
+
|
|
910
|
+
**Who can call:** Anyone (view function).
|
|
911
|
+
|
|
594
912
|
```solidity
|
|
595
913
|
DefifaScorecardState state = governor.stateOf(gameId, scorecardId);
|
|
596
|
-
// PENDING
|
|
914
|
+
// PENDING -> ACTIVE -> SUCCEEDED -> RATIFIED (or DEFEATED)
|
|
597
915
|
```
|
|
598
916
|
|
|
599
917
|
### Check attestation status
|
|
600
918
|
|
|
919
|
+
**Entry points:**
|
|
920
|
+
- `DefifaGovernor.attestationCountOf(uint256 gameId, uint256 scorecardId) external view returns (uint256)`
|
|
921
|
+
- `DefifaGovernor.quorum(uint256 gameId) public view returns (uint256)`
|
|
922
|
+
- `DefifaGovernor.hasAttestedTo(uint256 gameId, uint256 scorecardId, address account) external view returns (bool)`
|
|
923
|
+
|
|
924
|
+
**Who can call:** Anyone (view functions).
|
|
925
|
+
|
|
601
926
|
```solidity
|
|
602
927
|
uint256 count = governor.attestationCountOf(gameId, scorecardId);
|
|
603
928
|
uint256 needed = governor.quorum(gameId);
|
|
@@ -606,6 +931,12 @@ bool hasAttested = governor.hasAttestedTo(gameId, scorecardId, account);
|
|
|
606
931
|
|
|
607
932
|
### Check cash-out value
|
|
608
933
|
|
|
934
|
+
**Entry points:**
|
|
935
|
+
- `DefifaHook.cashOutWeightOf(uint256 tokenId) external view returns (uint256)`
|
|
936
|
+
- `DefifaHook.cashOutWeightOf(uint256[] tokenIds) external view returns (uint256)` (aggregate)
|
|
937
|
+
|
|
938
|
+
**Who can call:** Anyone (view functions).
|
|
939
|
+
|
|
609
940
|
```solidity
|
|
610
941
|
// Single token
|
|
611
942
|
uint256 weight = hook.cashOutWeightOf(tokenId);
|
|
@@ -619,6 +950,12 @@ uint256 totalWeight = hook.cashOutWeightOf(ids);
|
|
|
619
950
|
|
|
620
951
|
### Check fee token claims
|
|
621
952
|
|
|
953
|
+
**Entry points:**
|
|
954
|
+
- `DefifaHook.tokensClaimableFor(uint256[] memory tokenIds) external view returns (uint256, uint256)`
|
|
955
|
+
- `DefifaHook.tokenAllocations() external view returns (uint256, uint256)`
|
|
956
|
+
|
|
957
|
+
**Who can call:** Anyone (view functions).
|
|
958
|
+
|
|
622
959
|
```solidity
|
|
623
960
|
(uint256 defifaTokens, uint256 nanaTokens) = hook.tokensClaimableFor(tokenIds);
|
|
624
961
|
(uint256 defifaBalance, uint256 nanaBalance) = hook.tokenAllocations();
|
|
@@ -648,19 +985,19 @@ uint256 totalWeight = hook.cashOutWeightOf(ids);
|
|
|
648
985
|
|
|
649
986
|
### Scorecard Errors (Journeys 5, 6, 7)
|
|
650
987
|
|
|
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) |
|
|
988
|
+
| Error | Condition | Journey |
|
|
989
|
+
|-------|-----------|---------|
|
|
990
|
+
| `DefifaGovernor_NotAllowed` | Game not in SCORING, or scorecard not in correct state | 5, 6, 7 |
|
|
991
|
+
| `DefifaGovernor_UnownedProposedCashoutValue` | Weight > 0 assigned to tier with 0 supply | 5 |
|
|
992
|
+
| `DefifaGovernor_DuplicateScorecard` | Identical scorecard already submitted | 5 |
|
|
993
|
+
| `DefifaGovernor_AlreadyAttested` | Account already attested to this scorecard | 6 |
|
|
994
|
+
| `DefifaGovernor_AlreadyRatified` | Game already has a ratified scorecard | 5, 7 |
|
|
995
|
+
| `DefifaGovernor_UnknownProposal` | Scorecard ID has no submission record | 6, 7 |
|
|
996
|
+
| `DefifaHook_InvalidCashoutWeights` | Weights do not sum to TOTAL_CASHOUT_WEIGHT | 7 (ratification) |
|
|
997
|
+
| `DefifaHook_BadTierOrder` | Tier IDs not in ascending order | 7 (ratification) |
|
|
998
|
+
| `DefifaHook_InvalidTierId` | Tier not in category 0, or tier ID > maxTierId | 7 (ratification) |
|
|
999
|
+
| `DefifaHook_GameIsntScoringYet` | Game not in SCORING phase when setting weights | 7 (ratification) |
|
|
1000
|
+
| `DefifaHook_CashoutWeightsAlreadySet` | Weights already set (double-set attempt) | 7 (ratification) |
|
|
664
1001
|
|
|
665
1002
|
### No-Contest Errors (Journeys 10, 11)
|
|
666
1003
|
|
|
@@ -689,3 +1026,9 @@ uint256 totalWeight = hook.cashOutWeightOf(ids);
|
|
|
689
1026
|
| `DefifaDeployer_InvalidGameConfiguration` | Timing constraints violated: `mintPeriodDuration == 0` or `start < block.timestamp + refund + mint` |
|
|
690
1027
|
| `DefifaDeployer_SplitsDontAddUp` | User splits + protocol fees exceed 100% |
|
|
691
1028
|
| `DefifaDeployer_InvalidGameConfiguration` | JB project ID mismatch (front-run) |
|
|
1029
|
+
|
|
1030
|
+
### Transfer Errors (Journey 15)
|
|
1031
|
+
|
|
1032
|
+
| Error | Condition |
|
|
1033
|
+
|-------|-----------|
|
|
1034
|
+
| `DefifaHook_TransfersPaused` | `transfersPausable` is set and transfers paused in current ruleset |
|