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