@ballkidz/defifa 0.0.16 → 0.0.18
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 +34 -6
- package/ARCHITECTURE.md +54 -102
- package/AUDIT_INSTRUCTIONS.md +96 -504
- package/CHANGELOG.md +26 -0
- package/README.md +61 -207
- package/RISKS.md +19 -2
- package/SKILLS.md +29 -281
- package/STYLE_GUIDE.md +57 -18
- package/USER_JOURNEYS.md +45 -1011
- package/package.json +2 -2
- package/references/operations.md +27 -0
- package/references/runtime.md +32 -0
- package/src/DefifaDeployer.sol +13 -7
- package/src/DefifaHook.sol +5 -2
- package/src/interfaces/IDefifaDeployer.sol +28 -1
- package/src/interfaces/IDefifaGovernor.sol +26 -0
- package/src/interfaces/IDefifaHook.sol +30 -1
- package/CHANGE_LOG.md +0 -164
package/USER_JOURNEYS.md
CHANGED
|
@@ -1,1035 +1,69 @@
|
|
|
1
|
-
#
|
|
1
|
+
# User Journeys
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
10
|
+
## Journey 1: Launch A Defifa Game
|
|
8
11
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
+
## Journey 2: Participate As A Player During The Mint Phase
|
|
27
22
|
|
|
28
|
-
**
|
|
23
|
+
**Starting state:** the game is in its countdown or live mint window and players want to buy outcome pieces.
|
|
29
24
|
|
|
30
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
**Success:** participants can exit under the repo's explicit failure-mode rules instead of ad hoc admin intervention.
|
|
58
37
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
43
|
+
## Journey 4: Submit, Attest To, And Ratify A Scorecard
|
|
104
44
|
|
|
105
|
-
|
|
106
|
-
uint256 gameId = deployer.launchGameWith(launchProjectData);
|
|
107
|
-
```
|
|
45
|
+
**Starting state:** minting is over and the game is in its scoring phase.
|
|
108
46
|
|
|
109
|
-
|
|
47
|
+
**Success:** a valid scorecard reaches quorum, survives any grace period, and becomes the game's settled result.
|
|
110
48
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
55
|
+
## Journey 5: Redeem Winning Pieces And Settle The Pot
|
|
126
56
|
|
|
127
|
-
|
|
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
|
-
|
|
59
|
+
**Success:** holders of winning pieces burn or cash out them for their share of the prize pot.
|
|
131
60
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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.
|