@ballkidz/defifa 0.0.25 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/AUDIT_INSTRUCTIONS.md +6 -2
  2. package/README.md +11 -2
  3. package/RISKS.md +3 -1
  4. package/STYLE_GUIDE.md +14 -11
  5. package/package.json +31 -14
  6. package/script/Deploy.s.sol +4 -1
  7. package/src/DefifaDeployer.sol +79 -47
  8. package/src/DefifaGovernor.sol +57 -12
  9. package/src/DefifaHook.sol +83 -26
  10. package/src/DefifaProjectOwner.sol +4 -3
  11. package/src/DefifaTokenUriResolver.sol +113 -20
  12. package/src/enums/DefifaGamePhase.sol +6 -0
  13. package/src/enums/DefifaScorecardState.sol +4 -0
  14. package/src/interfaces/IDefifaDeployer.sol +5 -0
  15. package/src/interfaces/IDefifaGamePhaseReporter.sol +4 -0
  16. package/src/interfaces/IDefifaGamePotReporter.sol +10 -0
  17. package/src/interfaces/IDefifaGovernor.sol +4 -0
  18. package/src/interfaces/IDefifaHook.sol +5 -0
  19. package/src/interfaces/IDefifaTokenUriResolver.sol +3 -0
  20. package/src/libraries/DefifaFontImporter.sol +1 -1
  21. package/src/libraries/DefifaHookLib.sol +9 -10
  22. package/src/structs/DefifaAttestations.sol +3 -2
  23. package/src/structs/DefifaDelegation.sol +1 -0
  24. package/src/structs/DefifaLaunchProjectData.sol +2 -3
  25. package/src/structs/DefifaOpsData.sol +1 -0
  26. package/src/structs/DefifaScorecard.sol +2 -0
  27. package/src/structs/DefifaTierCashOutWeight.sol +3 -1
  28. package/src/structs/DefifaTierParams.sol +1 -0
  29. package/CRYPTO_ECON.pdf +0 -0
  30. package/CRYPTO_ECON.tex +0 -997
  31. package/foundry.lock +0 -17
  32. package/references/operations.md +0 -32
  33. package/references/runtime.md +0 -43
  34. package/slither-ci.config.json +0 -10
  35. package/sphinx.lock +0 -521
  36. package/test/BWAFunctionComparison.t.sol +0 -1320
  37. package/test/DefifaAdversarialQuorum.t.sol +0 -617
  38. package/test/DefifaAuditLowGuards.t.sol +0 -308
  39. package/test/DefifaFeeAccounting.t.sol +0 -581
  40. package/test/DefifaGovernanceHardening.t.sol +0 -1315
  41. package/test/DefifaGovernor.t.sol +0 -1378
  42. package/test/DefifaHookRegressions.t.sol +0 -415
  43. package/test/DefifaMintCostInvariant.t.sol +0 -319
  44. package/test/DefifaNoContest.t.sol +0 -941
  45. package/test/DefifaSecurity.t.sol +0 -741
  46. package/test/DefifaUSDC.t.sol +0 -480
  47. package/test/Fork.t.sol +0 -2388
  48. package/test/TestAuditGaps.sol +0 -984
  49. package/test/TestQALastMile.t.sol +0 -514
  50. package/test/audit/AttestationDoubleCount.t.sol +0 -218
  51. package/test/audit/CodexNemesisCurrencyMismatchBypass.t.sol +0 -112
  52. package/test/audit/CodexNemesisNoContestReserveDrain.t.sol +0 -238
  53. package/test/audit/CodexNemesisOneTierZeroTimeoutLockVerified.t.sol +0 -218
  54. package/test/audit/CodexNemesisSingleTierTimeoutLock.t.sol +0 -237
  55. package/test/audit/CodexRegistryMismatch.t.sol +0 -191
  56. package/test/audit/CodexTierCapMismatch.t.sol +0 -171
  57. package/test/audit/CurrencyMismatchFix.t.sol +0 -265
  58. package/test/audit/FixPendingReserveDilution.t.sol +0 -366
  59. package/test/audit/H5TierCapValidation.t.sol +0 -184
  60. package/test/audit/PendingReserveDilution.t.sol +0 -298
  61. package/test/audit/PendingReserveQuorumGrief.t.sol +0 -355
  62. package/test/audit/PendingReserveSnapshotBypass.t.sol +0 -319
  63. package/test/regression/AttestationDelegateBeneficiary.t.sol +0 -271
  64. package/test/regression/FulfillmentBlocksRatification.t.sol +0 -279
  65. package/test/regression/GracePeriodBypass.t.sol +0 -302
package/test/Fork.t.sol DELETED
@@ -1,2388 +0,0 @@
1
- // SPDX-License-Identifier: UNLICENSED
2
- pragma solidity 0.8.28;
3
-
4
- import {DefifaGovernor} from "../src/DefifaGovernor.sol";
5
- import {DefifaDeployer} from "../src/DefifaDeployer.sol";
6
- import {DefifaHook} from "../src/DefifaHook.sol";
7
- import {DefifaTokenUriResolver} from "../src/DefifaTokenUriResolver.sol";
8
- import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
9
-
10
- import {TestBaseWorkflow} from "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
11
- import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
12
- import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
13
- import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
14
- import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
15
- import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
16
- import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
17
- import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
18
- import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
19
- import {DefifaDelegation} from "../src/structs/DefifaDelegation.sol";
20
- import {DefifaLaunchProjectData} from "../src/structs/DefifaLaunchProjectData.sol";
21
- import {DefifaTierParams} from "../src/structs/DefifaTierParams.sol";
22
- import {DefifaTierCashOutWeight} from "../src/structs/DefifaTierCashOutWeight.sol";
23
- import {DefifaGamePhase} from "../src/enums/DefifaGamePhase.sol";
24
- import {DefifaScorecardState} from "../src/enums/DefifaScorecardState.sol";
25
- import {DefifaHookLib} from "../src/libraries/DefifaHookLib.sol";
26
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
27
- import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
28
- import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
29
- import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
30
- import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
31
- import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
32
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
33
- import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
34
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
35
- import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
36
- import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
37
- import {JBMultiTerminal} from "@bananapus/core-v6/src/JBMultiTerminal.sol";
38
- import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
39
- import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
40
- import {JB721TiersMintReservesConfig} from "@bananapus/721-hook-v6/src/structs/JB721TiersMintReservesConfig.sol";
41
-
42
- /// @dev Helper to read block.timestamp via an external call, bypassing the via-ir optimizer's timestamp caching.
43
- contract TimestampReader {
44
- function timestamp() external view returns (uint256) {
45
- return block.timestamp;
46
- }
47
- }
48
-
49
- /// @title DefifaForkTest
50
- /// @notice Comprehensive fork tests covering full game lifecycle, edge cases, adversarial conditions, and fund
51
- /// conservation. Forks Ethereum mainnet to test in realistic conditions.
52
- contract DefifaForkTest is JBTest, TestBaseWorkflow {
53
- using JBRulesetMetadataResolver for JBRuleset;
54
-
55
- TimestampReader private _tsReader;
56
-
57
- address _protocolFeeProjectTokenAccount;
58
- address _defifaProjectTokenAccount;
59
- uint256 _protocolFeeProjectId;
60
- uint256 _defifaProjectId;
61
- uint256 _gameId = 3;
62
-
63
- DefifaDeployer deployer;
64
- DefifaHook hook;
65
- DefifaGovernor governor;
66
- address projectOwner = address(bytes20(keccak256("projectOwner")));
67
-
68
- // Shared test state
69
- uint256 _pid;
70
- DefifaHook _nft;
71
- DefifaGovernor _gov;
72
- address[] _users;
73
-
74
- function setUp() public virtual override {
75
- vm.createSelectFork("ethereum");
76
-
77
- // Deploy JB core fresh on fork.
78
- super.setUp();
79
-
80
- _tsReader = new TimestampReader();
81
-
82
- JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
83
- _tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
84
- JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
85
- tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
86
- JBRulesetConfig[] memory rc = new JBRulesetConfig[](1);
87
- rc[0] = JBRulesetConfig({
88
- mustStartAtOrAfter: 0,
89
- duration: 10 days,
90
- weight: 1e18,
91
- weightCutPercent: 0,
92
- approvalHook: IJBRulesetApprovalHook(address(0)),
93
- metadata: JBRulesetMetadata({
94
- reservedPercent: 0,
95
- cashOutTaxRate: 0,
96
- baseCurrency: JBCurrencyIds.ETH,
97
- pausePay: false,
98
- pauseCreditTransfers: false,
99
- allowOwnerMinting: false,
100
- allowSetCustomToken: false,
101
- allowTerminalMigration: false,
102
- allowSetTerminals: false,
103
- allowSetController: false,
104
- allowAddAccountingContext: false,
105
- allowAddPriceFeed: false,
106
- ownerMustSendPayouts: false,
107
- holdFees: false,
108
- useTotalSurplusForCashOuts: false,
109
- useDataHookForPay: true,
110
- useDataHookForCashOut: true,
111
- dataHook: address(0),
112
- metadata: 0
113
- }),
114
- splitGroups: new JBSplitGroup[](0),
115
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
116
- });
117
-
118
- _protocolFeeProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
119
- vm.prank(projectOwner);
120
- _protocolFeeProjectTokenAccount =
121
- address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
122
- _defifaProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
123
- vm.prank(projectOwner);
124
- _defifaProjectTokenAccount =
125
- address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
126
-
127
- hook =
128
- new DefifaHook(jbDirectory(), IERC20(_defifaProjectTokenAccount), IERC20(_protocolFeeProjectTokenAccount));
129
- governor = new DefifaGovernor(jbController(), address(this));
130
- deployer = new DefifaDeployer(
131
- address(hook),
132
- new DefifaTokenUriResolver(ITypeface(address(0))),
133
- governor,
134
- jbController(),
135
- new JBAddressRegistry(),
136
- _defifaProjectId,
137
- _protocolFeeProjectId
138
- );
139
-
140
- // Grant deployer SET_SPLIT_GROUPS permission on the defifa fee project.
141
- uint8[] memory permissionIds = new uint8[](1);
142
- permissionIds[0] = JBPermissionIds.SET_SPLIT_GROUPS;
143
- vm.prank(projectOwner);
144
- jbPermissions()
145
- .setPermissionsFor(
146
- projectOwner,
147
- JBPermissionsData({
148
- operator: address(deployer),
149
- // forge-lint: disable-next-line(unsafe-typecast)
150
- projectId: uint64(_defifaProjectId),
151
- permissionIds: permissionIds
152
- })
153
- );
154
-
155
- hook.transferOwnership(address(deployer));
156
- governor.transferOwnership(address(deployer));
157
- }
158
-
159
- // =========================================================================
160
- // FULL LIFECYCLE: Mint → Refund → Score → Ratify → Cash Out
161
- // =========================================================================
162
-
163
- function test_fork_fullLifecycle_4tiers() external {
164
- _setupGame(4, 1 ether);
165
-
166
- // Verify MINT phase.
167
- assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.MINT));
168
-
169
- // Verify all users hold NFTs.
170
- for (uint256 i; i < 4; i++) {
171
- assertEq(_nft.balanceOf(_users[i]), 1, "each user holds 1 NFT");
172
- }
173
-
174
- // Record pot before fees.
175
- uint256 potBefore = _balance();
176
- assertEq(potBefore, 4 ether, "pot = 4 ETH from 4 mints");
177
-
178
- // Advance to SCORING.
179
- _toScoring();
180
- assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.SCORING));
181
-
182
- // Set winner-take-all scorecard: tier 1 gets everything.
183
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
184
- sc[0].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT();
185
-
186
- _attestAndRatify(sc);
187
- assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.COMPLETE));
188
-
189
- // Winner cashes out.
190
- uint256 winnerBefore = _users[0].balance;
191
- _cashOut(_users[0], 1, 1);
192
- uint256 winnerReceived = _users[0].balance - winnerBefore;
193
- assertGt(winnerReceived, 0, "winner received ETH");
194
-
195
- // Losers get only fee tokens (no ETH).
196
- for (uint256 i = 1; i < 4; i++) {
197
- uint256 bb = _users[i].balance;
198
- _cashOut(_users[i], i + 1, 1);
199
- assertEq(_users[i].balance, bb, "loser gets 0 ETH");
200
- // But they should have received fee tokens.
201
- uint256 defifa = IERC20(_defifaProjectTokenAccount).balanceOf(_users[i]);
202
- uint256 nana = IERC20(_protocolFeeProjectTokenAccount).balanceOf(_users[i]);
203
- assertTrue(defifa > 0 || nana > 0, "loser got fee tokens");
204
- }
205
-
206
- // All fee tokens distributed.
207
- assertEq(IERC20(_defifaProjectTokenAccount).balanceOf(address(_nft)), 0, "no DEFIFA left in hook");
208
- assertEq(IERC20(_protocolFeeProjectTokenAccount).balanceOf(address(_nft)), 0, "no NANA left in hook");
209
- }
210
-
211
- // =========================================================================
212
- // REFUND PHASE: Full refund during MINT, partial refund patterns
213
- // =========================================================================
214
-
215
- function test_fork_refundDuringMint_exactPrice() external {
216
- _setupGame(8, 2 ether);
217
-
218
- // Refund first 4 users during MINT.
219
- for (uint256 i; i < 4; i++) {
220
- uint256 bb = _users[i].balance;
221
- _refund(_users[i], i + 1);
222
- assertEq(_users[i].balance - bb, 2 ether, "exact refund of mint price");
223
- assertEq(_nft.balanceOf(_users[i]), 0, "NFT burned on refund");
224
- }
225
-
226
- // Remaining pot = 4 users * 2 ETH = 8 ETH.
227
- assertEq(_balance(), 8 ether, "pot = remaining mints");
228
- }
229
-
230
- function test_fork_refundDuringRefundPhase() external {
231
- _setupGame(4, 1 ether);
232
-
233
- // Advance past MINT into REFUND phase.
234
- vm.warp(_tsReader.timestamp() + 1 days + 1);
235
- assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.REFUND));
236
-
237
- // Refund during REFUND phase.
238
- uint256 bb = _users[0].balance;
239
- _refund(_users[0], 1);
240
- assertEq(_users[0].balance - bb, 1 ether, "refund works in REFUND phase");
241
- }
242
-
243
- // =========================================================================
244
- // HIGH VOLUME: 32 tiers × 100 ETH each = 3,200 ETH pot
245
- // =========================================================================
246
-
247
- function test_fork_highVolume_32tiers_100eth() external {
248
- _setupGame(32, 100 ether);
249
- _toScoring();
250
-
251
- // Tier 1 = 50%, rest split evenly.
252
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
253
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(32);
254
- uint256 half = tw / 2;
255
- uint256 perTier = half / 31;
256
- uint256 assigned;
257
- for (uint256 i; i < 32; i++) {
258
- if (i == 0) {
259
- sc[i].cashOutWeight = half;
260
- } else if (i == 31) {
261
- sc[i].cashOutWeight = tw - assigned;
262
- } else {
263
- sc[i].cashOutWeight = perTier;
264
- }
265
- assigned += sc[i].cashOutWeight;
266
- }
267
-
268
- _attestAndRatify(sc);
269
- uint256 pot = _surplus();
270
- uint256 out = _cashOutAllUsers();
271
-
272
- assertApproxEqAbs(out, pot, 1e15, "total cashed out ~ pot");
273
- assertLe(_surplus(), 1e15, "negligible dust remains");
274
- }
275
-
276
- // =========================================================================
277
- // EXTREME ROUNDING: 1 wei weights, 1000 ETH per tier
278
- // =========================================================================
279
-
280
- function test_fork_extremeWeights_1weiAnd999999() external {
281
- _setupGame(3, 1000 ether);
282
- _toScoring();
283
-
284
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(3);
285
- sc[0].cashOutWeight = 1;
286
- sc[1].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() - 2;
287
- sc[2].cashOutWeight = 1;
288
-
289
- _attestAndRatify(sc);
290
- uint256 pot = _surplus();
291
- uint256 out = _cashOutAllUsers();
292
- assertApproxEqAbs(out + _surplus(), pot, 3, "fund conservation with extreme weights");
293
- assertGt(_users[1].balance, pot * 99 / 100, "tier 2 > 99% of pot");
294
- }
295
-
296
- // =========================================================================
297
- // MULTI-PLAYER PER TIER: 5 winners, 3 losers
298
- // =========================================================================
299
-
300
- function test_fork_multiPlayerPerTier_winnerTakeAll() external {
301
- _setupMultiPlayer();
302
- _toScoring();
303
-
304
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
305
- sc[0].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT();
306
-
307
- _attestAndRatify(sc);
308
-
309
- // All 5 winners should get approximately equal shares.
310
- uint256[] memory payouts = new uint256[](5);
311
- for (uint256 i; i < 5; i++) {
312
- uint256 bb = _users[i].balance;
313
- _cashOut(_users[i], 1, i + 1);
314
- payouts[i] = _users[i].balance - bb;
315
- assertGt(payouts[i], 0, "winner receives ETH");
316
- }
317
- for (uint256 i = 1; i < 5; i++) {
318
- assertApproxEqRel(payouts[i], payouts[0], 0.001 ether, "payouts approx equal");
319
- }
320
-
321
- // Losers get 0 ETH.
322
- for (uint256 i; i < 3; i++) {
323
- uint256 bb = _users[5 + i].balance;
324
- _cashOut(_users[5 + i], i + 2, 1);
325
- assertEq(_users[5 + i].balance, bb, "loser gets 0 ETH");
326
- }
327
- }
328
-
329
- // =========================================================================
330
- // ADVERSARIAL: Overweight scorecard (120%) rejected
331
- // =========================================================================
332
-
333
- function test_fork_rejectsOverweightScorecard() external {
334
- _setupGame(4, 1 ether);
335
- _toScoring();
336
-
337
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
338
- for (uint256 i; i < 4; i++) {
339
- sc[i].cashOutWeight = (_nft.TOTAL_CASHOUT_WEIGHT() * 30) / 100; // 120% total
340
- }
341
-
342
- vm.expectRevert(DefifaHook.DefifaHook_InvalidCashoutWeights.selector);
343
- _gov.submitScorecardFor(_gameId, sc);
344
- }
345
-
346
- // =========================================================================
347
- // ADVERSARIAL: Underweight scorecard (80%) rejected
348
- // =========================================================================
349
-
350
- function test_fork_rejectsUnderweightScorecard() external {
351
- _setupGame(4, 1 ether);
352
- _toScoring();
353
-
354
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
355
- for (uint256 i; i < 4; i++) {
356
- sc[i].cashOutWeight = (_nft.TOTAL_CASHOUT_WEIGHT() * 20) / 100; // 80% total
357
- }
358
-
359
- vm.expectRevert(DefifaHook.DefifaHook_InvalidCashoutWeights.selector);
360
- _gov.submitScorecardFor(_gameId, sc);
361
- }
362
-
363
- // =========================================================================
364
- // ADVERSARIAL: Double attestation attempt
365
- // =========================================================================
366
-
367
- function test_fork_doubleAttestationReverts() external {
368
- _setupGame(4, 1 ether);
369
- _toScoring();
370
-
371
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
372
- uint256 pid = _gov.submitScorecardFor(_gameId, sc);
373
-
374
- vm.warp(_tsReader.timestamp() + _gov.attestationStartTimeOf(_gameId) + 1);
375
-
376
- vm.prank(_users[0]);
377
- _gov.attestToScorecardFrom(_gameId, pid);
378
-
379
- // Second attestation should revert.
380
- vm.prank(_users[0]);
381
- vm.expectRevert(DefifaGovernor.DefifaGovernor_AlreadyAttested.selector);
382
- _gov.attestToScorecardFrom(_gameId, pid);
383
- }
384
-
385
- // =========================================================================
386
- // ADVERSARIAL: Duplicate scorecard submission reverts
387
- // =========================================================================
388
-
389
- function test_fork_duplicateScorecardReverts() external {
390
- _setupGame(4, 1 ether);
391
- _toScoring();
392
-
393
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
394
- _gov.submitScorecardFor(_gameId, sc);
395
-
396
- vm.expectRevert(DefifaGovernor.DefifaGovernor_DuplicateScorecard.selector);
397
- _gov.submitScorecardFor(_gameId, sc);
398
- }
399
-
400
- // =========================================================================
401
- // ADVERSARIAL: Double ratification reverts
402
- // =========================================================================
403
-
404
- function test_fork_doubleRatificationReverts() external {
405
- _setupGame(4, 1 ether);
406
- _toScoring();
407
-
408
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
409
- _attestAndRatify(sc);
410
-
411
- vm.expectRevert(DefifaGovernor.DefifaGovernor_AlreadyRatified.selector);
412
- _gov.ratifyScorecardFrom(_gameId, sc);
413
- }
414
-
415
- // =========================================================================
416
- // ADVERSARIAL: Cash out weights set twice reverts
417
- // =========================================================================
418
-
419
- function test_fork_cashOutWeightsAlreadySetReverts() external {
420
- _setupGame(4, 1 ether);
421
- _toScoring();
422
-
423
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
424
- _attestAndRatify(sc);
425
- assertTrue(_nft.cashOutWeightIsSet(), "weights set");
426
-
427
- // Trying to set weights again via governor owner (which is the governor itself) should revert
428
- // because cashOutWeightIsSet is true. But since governor already ratified, the hook owner is the governor.
429
- // The governor can't call setTierCashOutWeightsTo directly without going through ratification again.
430
- // The ratification path will revert with AlreadyRatified.
431
- vm.expectRevert(DefifaGovernor.DefifaGovernor_AlreadyRatified.selector);
432
- _gov.ratifyScorecardFrom(_gameId, sc);
433
- }
434
-
435
- // =========================================================================
436
- // ADVERSARIAL: Delegation blocked after MINT phase
437
- // =========================================================================
438
-
439
- function test_fork_delegationBlockedAfterMint() external {
440
- _setupGame(4, 1 ether);
441
-
442
- // Advance to REFUND phase.
443
- vm.warp(_tsReader.timestamp() + 1 days);
444
-
445
- vm.prank(_users[0]);
446
- vm.expectRevert(abi.encodeWithSignature("DefifaHook_DelegateChangesUnavailableInThisPhase()"));
447
- _nft.setTierDelegateTo(address(1), 1);
448
-
449
- // SCORING phase.
450
- _toScoring();
451
-
452
- vm.prank(_users[0]);
453
- vm.expectRevert(abi.encodeWithSignature("DefifaHook_DelegateChangesUnavailableInThisPhase()"));
454
- _nft.setTierDelegateTo(address(1), 1);
455
- }
456
-
457
- // =========================================================================
458
- // ADVERSARIAL: Cash out before scorecard — reverts (nothing to claim)
459
- // =========================================================================
460
-
461
- function test_fork_cashOutBeforeScorecard_reverts() external {
462
- _setupGame(4, 1 ether);
463
- _toScoring();
464
-
465
- bytes memory meta = _cashOutMeta(1, 1);
466
- vm.prank(_users[0]);
467
- vm.expectRevert(DefifaHook.DefifaHook_NothingToClaim.selector);
468
- JBMultiTerminal(address(jbMultiTerminal()))
469
- .cashOutTokensOf({
470
- holder: _users[0],
471
- projectId: _pid,
472
- cashOutCount: 0,
473
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
474
- minTokensReclaimed: 0,
475
- beneficiary: payable(_users[0]),
476
- metadata: meta
477
- });
478
-
479
- // NFT not burned (revert rolled it back).
480
- assertEq(_nft.balanceOf(_users[0]), 1, "NFT intact after revert");
481
- }
482
-
483
- // =========================================================================
484
- // ADVERSARIAL: Non-holder tries to cash out someone else's NFT
485
- // =========================================================================
486
-
487
- function test_fork_nonHolderCashOutReverts() external {
488
- _setupGame(4, 1 ether);
489
- _toScoring();
490
-
491
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
492
- _attestAndRatify(sc);
493
-
494
- // Attacker tries to cash out user[0]'s token.
495
- address attacker = address(bytes20(keccak256("attacker")));
496
- bytes memory meta = _cashOutMeta(1, 1);
497
-
498
- vm.prank(attacker);
499
- vm.expectRevert();
500
- JBMultiTerminal(address(jbMultiTerminal()))
501
- .cashOutTokensOf({
502
- holder: attacker,
503
- projectId: _pid,
504
- cashOutCount: 0,
505
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
506
- minTokensReclaimed: 0,
507
- beneficiary: payable(attacker),
508
- metadata: meta
509
- });
510
- }
511
-
512
- // =========================================================================
513
- // ADVERSARIAL: Scorecard with weight on unminted tier reverts
514
- // =========================================================================
515
-
516
- function test_fork_weightOnUnmintedTierReverts() external {
517
- // Launch 8-tier game but only mint 4 tiers.
518
- _setupPartial(8, 4, 1 ether);
519
- _toScoring();
520
-
521
- // Try to give weight to tier 5 (unminted).
522
- DefifaTierCashOutWeight[] memory sc = new DefifaTierCashOutWeight[](8);
523
- for (uint256 i; i < 8; i++) {
524
- sc[i].id = i + 1;
525
- sc[i].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() / 8;
526
- }
527
- // Fix rounding for last tier.
528
- // forge-lint: disable-next-line(divide-before-multiply)
529
- sc[7].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() - ((_nft.TOTAL_CASHOUT_WEIGHT() / 8) * 7);
530
-
531
- vm.expectRevert(DefifaGovernor.DefifaGovernor_UnownedProposedCashoutValue.selector);
532
- _gov.submitScorecardFor(_gameId, sc);
533
- }
534
-
535
- // =========================================================================
536
- // ADVERSARIAL: Scorecard submission outside SCORING phase
537
- // =========================================================================
538
-
539
- function test_fork_scorecardSubmitOutsideScoring_reverts() external {
540
- _setupGame(4, 1 ether);
541
-
542
- // Still in MINT phase.
543
- assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.MINT));
544
-
545
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
546
-
547
- vm.expectRevert(DefifaGovernor.DefifaGovernor_NotAllowed.selector);
548
- _gov.submitScorecardFor(_gameId, sc);
549
- }
550
-
551
- // =========================================================================
552
- // ADVERSARIAL: Attestation outside SCORING phase (during COMPLETE)
553
- // =========================================================================
554
-
555
- function test_fork_attestationAfterRatification_reverts() external {
556
- _setupGame(4, 1 ether);
557
- _toScoring();
558
-
559
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
560
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
561
- _attestAllFor(scorecardId);
562
- _gov.ratifyScorecardFrom(_gameId, sc);
563
-
564
- // Now in COMPLETE phase. Try to attest to another scorecard.
565
- assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.COMPLETE));
566
-
567
- // Submit a different scorecard? Can't — already ratified.
568
- DefifaTierCashOutWeight[] memory sc2 = _buildScorecard(4);
569
- sc2[0].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT();
570
- vm.expectRevert(DefifaGovernor.DefifaGovernor_AlreadyRatified.selector);
571
- _gov.submitScorecardFor(_gameId, sc2);
572
- }
573
-
574
- // =========================================================================
575
- // ADVERSARIAL: NFT transfer then try to double-vote
576
- // =========================================================================
577
-
578
- function test_fork_nftTransferDoesNotDoubleVote() external {
579
- _setupGame(4, 1 ether);
580
-
581
- // user[0] transfers their NFT to user[1] (who already has tier 2)
582
- uint256 tokenId = _generateTokenId(1, 1);
583
- vm.prank(_users[0]);
584
- _nft.transferFrom(_users[0], _users[1], tokenId);
585
-
586
- _toScoring();
587
-
588
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
589
- uint256 pid = _gov.submitScorecardFor(_gameId, sc);
590
-
591
- vm.warp(_tsReader.timestamp() + _gov.attestationStartTimeOf(_gameId) + 1);
592
-
593
- // user[0] has no tokens now — attestation weight should be 0.
594
- _gov.getAttestationWeight(_gameId, _users[0], uint48(_gov.attestationStartTimeOf(_gameId)));
595
- // user[0]'s delegation was set at mint time. The checkpoint was recorded. But they transferred.
596
- // Since attestation uses snapshot at submission time, user[0]'s weight depends on when they delegated.
597
-
598
- // user[1] attests (has tokens from both tiers now).
599
- vm.prank(_users[1]);
600
- uint256 weight1 = _gov.attestToScorecardFrom(_gameId, pid);
601
- assertGt(weight1, 0, "user1 has attestation weight");
602
-
603
- // Other users attest.
604
- for (uint256 i = 2; i < 4; i++) {
605
- vm.prank(_users[i]);
606
- _gov.attestToScorecardFrom(_gameId, pid);
607
- }
608
-
609
- // Advance past grace period.
610
- vm.warp(_tsReader.timestamp() + _gov.attestationGracePeriodOf(_gameId) + 1);
611
-
612
- // Should be able to ratify if quorum is met.
613
- DefifaScorecardState state = _gov.stateOf(_gameId, pid);
614
- assertTrue(
615
- state == DefifaScorecardState.SUCCEEDED || state == DefifaScorecardState.ACTIVE,
616
- "state should be SUCCEEDED or ACTIVE"
617
- );
618
- }
619
-
620
- // =========================================================================
621
- // ADVERSARIAL: Competing scorecards — only one can be ratified
622
- // =========================================================================
623
-
624
- function test_fork_competingScorecards_onlyOneRatified() external {
625
- _setupGame(4, 1 ether);
626
- _toScoring();
627
-
628
- // Scorecard A: even distribution.
629
- DefifaTierCashOutWeight[] memory scA = _evenScorecard(4);
630
- uint256 pidA = _gov.submitScorecardFor(_gameId, scA);
631
-
632
- // Scorecard B: winner-take-all (different from A).
633
- DefifaTierCashOutWeight[] memory scB = _buildScorecard(4);
634
- scB[0].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT();
635
- uint256 pidB = _gov.submitScorecardFor(_gameId, scB);
636
-
637
- assertTrue(pidA != pidB, "different scorecards have different IDs");
638
-
639
- // Attest and ratify scorecard A.
640
- _attestAllFor(pidA);
641
- _gov.ratifyScorecardFrom(_gameId, scA);
642
-
643
- // Scorecard B should now be DEFEATED.
644
- assertEq(uint256(_gov.stateOf(_gameId, pidB)), uint256(DefifaScorecardState.DEFEATED));
645
- }
646
-
647
- // =========================================================================
648
- // ADVERSARIAL: fulfillCommitmentsOf double-call (idempotent)
649
- // =========================================================================
650
-
651
- function test_fork_doubleFulfillment_idempotent() external {
652
- _setupGame(4, 1 ether);
653
- _toScoring();
654
-
655
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
656
- _attestAndRatify(sc);
657
-
658
- uint256 fulfilled = deployer.fulfilledCommitmentsOf(_pid);
659
- assertGt(fulfilled, 0, "commitments fulfilled");
660
-
661
- // Second call should be a no-op.
662
- deployer.fulfillCommitmentsOf(_pid);
663
- assertEq(deployer.fulfilledCommitmentsOf(_pid), fulfilled, "no change on second call");
664
- }
665
-
666
- // =========================================================================
667
- // ADVERSARIAL: fulfillCommitmentsOf before ratification reverts
668
- // =========================================================================
669
-
670
- function test_fork_fulfillBeforeRatification_reverts() external {
671
- _setupGame(4, 1 ether);
672
- _toScoring();
673
-
674
- vm.expectRevert(DefifaDeployer.DefifaDeployer_CantFulfillYet.selector);
675
- deployer.fulfillCommitmentsOf(_pid);
676
- }
677
-
678
- // =========================================================================
679
- // GOVERNANCE: Quorum calculation with partial minting
680
- // =========================================================================
681
-
682
- function test_fork_quorum_partialMinting() external {
683
- _setupPartial(10, 6, 1 ether);
684
- uint256 expected = (6 * _gov.MAX_ATTESTATION_POWER_TIER()) / 2;
685
- assertEq(_gov.quorum(_gameId), expected, "quorum = 50% of minted tiers");
686
- }
687
-
688
- // =========================================================================
689
- // GOVERNANCE: Single-tier game (minimum viable game)
690
- // =========================================================================
691
-
692
- function test_fork_singleTierGame() external {
693
- // BWA prevents a sole beneficiary (100% weight) from self-attesting.
694
- // Use 2 tiers: tier 1 gets all weight, tier 2 provides neutral attestation.
695
- DefifaLaunchProjectData memory d = _launchData(2, 1 ether);
696
- (_pid, _nft, _gov) = _launch(d);
697
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
698
-
699
- _users = new address[](2);
700
- _users[0] = _addr(0);
701
- _mint(_users[0], 1, 1 ether);
702
- _delegateSelf(_users[0], 1);
703
- vm.warp(_tsReader.timestamp() + 1);
704
-
705
- _users[1] = _addr(1);
706
- _mint(_users[1], 2, 1 ether);
707
- _delegateSelf(_users[1], 2);
708
- vm.warp(_tsReader.timestamp() + 1);
709
-
710
- _toScoring();
711
-
712
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(2);
713
- sc[0].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT();
714
- sc[1].cashOutWeight = 0;
715
-
716
- _attestAndRatify(sc);
717
-
718
- // Cash out the winner (tier 1).
719
- uint256 bb = _users[0].balance;
720
- _cashOut(_users[0], 1, 1);
721
- uint256 received = _users[0].balance - bb;
722
- assertGt(received, 0, "winner receives ETH");
723
- }
724
-
725
- // =========================================================================
726
- // NO CONTEST: minParticipation threshold triggers NO_CONTEST
727
- // =========================================================================
728
-
729
- function test_fork_noContest_minParticipation() external {
730
- DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 5 ether, 0);
731
- (_pid, _nft, _gov) = _launch(d);
732
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
733
-
734
- // Mint only 1 ETH < 5 ETH threshold.
735
- _users = new address[](1);
736
- _users[0] = _addr(0);
737
- _mint(_users[0], 1, 1 ether);
738
-
739
- _toScoring();
740
-
741
- assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
742
- }
743
-
744
- // =========================================================================
745
- // NO CONTEST: scorecardTimeout triggers NO_CONTEST
746
- // =========================================================================
747
-
748
- function test_fork_noContest_scorecardTimeout() external {
749
- DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 7 days);
750
- (_pid, _nft, _gov) = _launch(d);
751
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
752
-
753
- _users = new address[](4);
754
- for (uint256 i; i < 4; i++) {
755
- _users[i] = _addr(i);
756
- _mint(_users[i], i + 1, 1 ether);
757
- _delegateSelf(_users[i], i + 1);
758
- vm.warp(_tsReader.timestamp() + 1);
759
- }
760
-
761
- _toScoring();
762
- assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.SCORING));
763
-
764
- // Warp past timeout.
765
- vm.warp(d.start + 7 days + 1);
766
- assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
767
- }
768
-
769
- // =========================================================================
770
- // NO CONTEST: triggerNoContestFor + full refund
771
- // =========================================================================
772
-
773
- function test_fork_noContest_triggerAndRefund() external {
774
- DefifaLaunchProjectData memory d = _launchDataWith(4, 2 ether, 0, 7 days);
775
- (_pid, _nft, _gov) = _launch(d);
776
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
777
-
778
- _users = new address[](4);
779
- for (uint256 i; i < 4; i++) {
780
- _users[i] = _addr(i);
781
- _mint(_users[i], i + 1, 2 ether);
782
- vm.warp(_tsReader.timestamp() + 1);
783
- }
784
-
785
- vm.warp(d.start + 7 days + 1);
786
- deployer.triggerNoContestFor(_pid);
787
-
788
- // All users refund.
789
- uint256 totalRefunded;
790
- for (uint256 i; i < 4; i++) {
791
- uint256 bb = _users[i].balance;
792
- _refund(_users[i], i + 1);
793
- uint256 received = _users[i].balance - bb;
794
- assertEq(received, 2 ether, "exact refund");
795
- totalRefunded += received;
796
- }
797
- assertEq(totalRefunded, 8 ether, "total refunded = total minted");
798
- }
799
-
800
- // =========================================================================
801
- // NO CONTEST: Double trigger reverts
802
- // =========================================================================
803
-
804
- function test_fork_noContest_doubleTriggerReverts() external {
805
- DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 7 days);
806
- (_pid, _nft, _gov) = _launch(d);
807
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
808
-
809
- _users = new address[](1);
810
- _users[0] = _addr(0);
811
- _mint(_users[0], 1, 1 ether);
812
-
813
- vm.warp(d.start + 7 days + 1);
814
-
815
- deployer.triggerNoContestFor(_pid);
816
-
817
- vm.expectRevert(DefifaDeployer.DefifaDeployer_NoContestAlreadyTriggered.selector);
818
- deployer.triggerNoContestFor(_pid);
819
- }
820
-
821
- // =========================================================================
822
- // NO CONTEST: triggerNoContest outside NO_CONTEST phase reverts
823
- // =========================================================================
824
-
825
- function test_fork_noContest_triggerWhenScoring_reverts() external {
826
- _setupGame(4, 1 ether);
827
- _toScoring();
828
-
829
- vm.expectRevert(DefifaDeployer.DefifaDeployer_NotNoContest.selector);
830
- deployer.triggerNoContestFor(_pid);
831
- }
832
-
833
- // =========================================================================
834
- // NO CONTEST: Ratified scorecard prevents NO_CONTEST forever
835
- // =========================================================================
836
-
837
- function test_fork_ratifiedPreventsNoContest() external {
838
- DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 7 days);
839
- (_pid, _nft, _gov) = _launch(d);
840
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
841
-
842
- _users = new address[](4);
843
- for (uint256 i; i < 4; i++) {
844
- _users[i] = _addr(i);
845
- _mint(_users[i], i + 1, 1 ether);
846
- _delegateSelf(_users[i], i + 1);
847
- vm.warp(_tsReader.timestamp() + 1);
848
- }
849
-
850
- _toScoring();
851
- _attestAndRatify(_evenScorecard(4));
852
-
853
- // Even after timeout, should remain COMPLETE.
854
- vm.warp(d.start + 7 days + 1);
855
- assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.COMPLETE));
856
-
857
- vm.expectRevert(DefifaDeployer.DefifaDeployer_NotNoContest.selector);
858
- deployer.triggerNoContestFor(_pid);
859
- }
860
-
861
- // =========================================================================
862
- // FEE ACCOUNTING: Default splits (no user splits)
863
- // =========================================================================
864
-
865
- function test_fork_feeAccounting_defaultSplits() external {
866
- _setupGame(4, 1 ether);
867
-
868
- uint256 potBefore = _balance();
869
- assertEq(potBefore, 4 ether);
870
-
871
- // Expected fee: 7.5% (2.5% NANA + 5% DEFIFA).
872
- uint256 expectedFee = (potBefore * 75_000_000) / JBConstants.SPLITS_TOTAL_PERCENT;
873
- uint256 expectedSurplus = potBefore - expectedFee;
874
-
875
- _toScoring();
876
- _attestAndRatify(_evenScorecard(4));
877
-
878
- uint256 potAfter = _balance();
879
- assertEq(potAfter, expectedSurplus, "surplus after fees");
880
- assertEq(deployer.fulfilledCommitmentsOf(_pid), expectedFee, "fulfilled = fee");
881
- }
882
-
883
- // =========================================================================
884
- // FEE ACCOUNTING: fee + surplus = original pot (zero rounding loss)
885
- // =========================================================================
886
-
887
- function test_fork_feeAccounting_noRoundingLoss() external {
888
- _setupGame(4, 1 ether);
889
-
890
- uint256 potBefore = _balance();
891
-
892
- _toScoring();
893
- _attestAndRatify(_evenScorecard(4));
894
-
895
- uint256 potAfter = _balance();
896
- uint256 fee = deployer.fulfilledCommitmentsOf(_pid);
897
- assertEq(fee + potAfter, potBefore, "fee + surplus = pot exactly");
898
- }
899
-
900
- // =========================================================================
901
- // FEE ACCOUNTING: With user-provided custom splits
902
- // =========================================================================
903
-
904
- function test_fork_feeAccounting_withUserSplits() external {
905
- JBSplit[] memory customSplits = new JBSplit[](1);
906
- address charity = address(bytes20(keccak256("charity")));
907
- customSplits[0] = JBSplit({
908
- preferAddToBalance: false,
909
- percent: JBConstants.SPLITS_TOTAL_PERCENT / 10, // 10%
910
- projectId: 0,
911
- beneficiary: payable(charity),
912
- lockedUntil: 0,
913
- hook: IJBSplitHook(address(0))
914
- });
915
-
916
- DefifaLaunchProjectData memory d = _launchDataWithSplits(4, 1 ether, customSplits);
917
- (_pid, _nft, _gov) = _launch(d);
918
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
919
-
920
- _users = new address[](4);
921
- for (uint256 i; i < 4; i++) {
922
- _users[i] = _addr(i);
923
- _mint(_users[i], i + 1, 1 ether);
924
- _delegateSelf(_users[i], i + 1);
925
- vm.warp(_tsReader.timestamp() + 1);
926
- }
927
-
928
- uint256 potBefore = _balance();
929
- // totalAbsolutePercent = 25M + 50M + 100M = 175M (17.5%).
930
- // expectedFee = (potBefore * 175_000_000) / JBConstants.SPLITS_TOTAL_PERCENT;
931
-
932
- _toScoring();
933
- _attestAndRatify(_evenScorecard(4));
934
-
935
- uint256 fee = deployer.fulfilledCommitmentsOf(_pid);
936
- assertEq(fee + _balance(), potBefore, "no rounding loss with user splits");
937
- assertTrue(charity.balance > 0, "charity received funds");
938
- }
939
-
940
- // =========================================================================
941
- // FEE TOKENS: Reserved minters get proportional $DEFIFA/$NANA
942
- // =========================================================================
943
-
944
- function test_fork_reservedMintersGetFeeTokens() external {
945
- address reserveAddr = address(bytes20(keccak256("reserveBeneficiary")));
946
-
947
- DefifaTierParams[] memory tp = new DefifaTierParams[](2);
948
- for (uint256 i; i < 2; i++) {
949
- tp[i] = DefifaTierParams({
950
- reservedRate: 1,
951
- reservedTokenBeneficiary: reserveAddr,
952
- encodedIPFSUri: bytes32(0),
953
- shouldUseReservedTokenBeneficiaryAsDefault: false,
954
- name: "DEFIFA"
955
- });
956
- }
957
- DefifaLaunchProjectData memory d = DefifaLaunchProjectData({
958
- name: "DEFIFA",
959
- projectUri: "",
960
- contractUri: "",
961
- baseUri: "",
962
- token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
963
- mintPeriodDuration: 1 days,
964
- start: uint48(block.timestamp + 3 days),
965
- refundPeriodDuration: 1 days,
966
- store: new JB721TiersHookStore(),
967
- splits: new JBSplit[](0),
968
- attestationStartTime: 0,
969
- attestationGracePeriod: 100_381,
970
- defaultAttestationDelegate: address(0),
971
- tierPrice: uint104(1 ether),
972
- tiers: tp,
973
- defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
974
- terminal: jbMultiTerminal(),
975
- minParticipation: 0,
976
- scorecardTimeout: 0,
977
- timelockDuration: 0
978
- });
979
- (_pid, _nft, _gov) = _launch(d);
980
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
981
-
982
- _users = new address[](2);
983
- _users[0] = _addr(0);
984
- _users[1] = _addr(1);
985
- _mint(_users[0], 1, 1 ether);
986
- _delegateSelf(_users[0], 1);
987
- vm.warp(_tsReader.timestamp() + 1);
988
- _mint(_users[1], 2, 1 ether);
989
- _delegateSelf(_users[1], 2);
990
- vm.warp(_tsReader.timestamp() + 1);
991
-
992
- _toScoring();
993
-
994
- // Mint reserved tokens.
995
- JB721TiersMintReservesConfig[] memory reserveConfigs = new JB721TiersMintReservesConfig[](2);
996
- reserveConfigs[0] = JB721TiersMintReservesConfig({tierId: 1, count: 1});
997
- reserveConfigs[1] = JB721TiersMintReservesConfig({tierId: 2, count: 1});
998
- _nft.mintReservesFor(reserveConfigs);
999
-
1000
- assertEq(_nft.balanceOf(reserveAddr), 2, "reserve beneficiary holds 2 NFTs");
1001
-
1002
- // Seed fee tokens into the hook.
1003
- deal(address(IERC20(_defifaProjectTokenAccount)), address(_nft), 1000 ether);
1004
- deal(address(IERC20(_protocolFeeProjectTokenAccount)), address(_nft), 500 ether);
1005
-
1006
- // Scorecard: equal weight.
1007
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(2);
1008
- sc[0].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() / 2;
1009
- sc[1].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() / 2;
1010
-
1011
- // Only paid minters attest -- reserve beneficiary has zero attestation weight at the
1012
- // snapshot (attestationsBegin - 1) because they received NFTs via reserve minting after
1013
- // the snapshot timestamp.
1014
- uint256 pid = _gov.submitScorecardFor(_gameId, sc);
1015
- vm.warp(_tsReader.timestamp() + _gov.attestationStartTimeOf(_gameId) + 1);
1016
- for (uint256 i; i < _users.length; i++) {
1017
- vm.prank(_users[i]);
1018
- _gov.attestToScorecardFrom(_gameId, pid);
1019
- }
1020
- vm.warp(_tsReader.timestamp() + _gov.attestationGracePeriodOf(_gameId) + 1);
1021
- _gov.ratifyScorecardFrom(_gameId, sc);
1022
- vm.warp(_tsReader.timestamp() + 1);
1023
-
1024
- // Cash out paid minters.
1025
- _cashOut(_users[0], 1, 1);
1026
- _cashOut(_users[1], 2, 1);
1027
-
1028
- uint256 user0Defifa = IERC20(_defifaProjectTokenAccount).balanceOf(_users[0]);
1029
- assertGt(user0Defifa, 0, "paid minter got DEFIFA tokens");
1030
-
1031
- // Cash out reserved minter's tokens.
1032
- bytes memory meta1 = _cashOutMeta(1, 2);
1033
- vm.prank(reserveAddr);
1034
- JBMultiTerminal(address(jbMultiTerminal()))
1035
- .cashOutTokensOf({
1036
- holder: reserveAddr,
1037
- projectId: _pid,
1038
- cashOutCount: 0,
1039
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
1040
- minTokensReclaimed: 0,
1041
- beneficiary: payable(reserveAddr),
1042
- metadata: meta1
1043
- });
1044
-
1045
- bytes memory meta2 = _cashOutMeta(2, 2);
1046
- vm.prank(reserveAddr);
1047
- JBMultiTerminal(address(jbMultiTerminal()))
1048
- .cashOutTokensOf({
1049
- holder: reserveAddr,
1050
- projectId: _pid,
1051
- cashOutCount: 0,
1052
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
1053
- minTokensReclaimed: 0,
1054
- beneficiary: payable(reserveAddr),
1055
- metadata: meta2
1056
- });
1057
-
1058
- uint256 reserveDefifa = IERC20(_defifaProjectTokenAccount).balanceOf(reserveAddr);
1059
- assertGt(reserveDefifa, 0, "reserved minter got DEFIFA tokens");
1060
-
1061
- // All fee tokens distributed.
1062
- assertEq(IERC20(_defifaProjectTokenAccount).balanceOf(address(_nft)), 0, "no DEFIFA left");
1063
- assertEq(IERC20(_protocolFeeProjectTokenAccount).balanceOf(address(_nft)), 0, "no NANA left");
1064
- }
1065
-
1066
- // =========================================================================
1067
- // CASH OUT ORDERING: First vs last to exit — fair distribution
1068
- // =========================================================================
1069
-
1070
- function test_fork_cashOutOrdering_fairAcrossExitOrder() external {
1071
- _setupGame(4, 10 ether);
1072
- _toScoring();
1073
-
1074
- _attestAndRatify(_evenScorecard(4));
1075
-
1076
- // Cash out users in reverse order and forward order — results should be similar.
1077
- uint256[] memory received = new uint256[](4);
1078
- for (uint256 i; i < 4; i++) {
1079
- uint256 bb = _users[i].balance;
1080
- _cashOut(_users[i], i + 1, 1);
1081
- received[i] = _users[i].balance - bb;
1082
- }
1083
-
1084
- // With equal weights, all should receive approximately equal amounts.
1085
- for (uint256 i = 1; i < 4; i++) {
1086
- assertApproxEqRel(received[i], received[0], 0.001 ether, "equal-weight payouts are equal");
1087
- }
1088
- }
1089
-
1090
- // =========================================================================
1091
- // GAME POT REPORTING: currentGamePotOf accuracy
1092
- // =========================================================================
1093
-
1094
- function test_fork_gamePotReporting() external {
1095
- _setupGame(4, 1 ether);
1096
- _toScoring();
1097
-
1098
- (uint256 potExcluding,,) = deployer.currentGamePotOf(_pid, false);
1099
- (uint256 potIncluding,,) = deployer.currentGamePotOf(_pid, true);
1100
- assertEq(potExcluding, 4 ether, "pot excluding = 4 ETH");
1101
- assertEq(potIncluding, 4 ether, "pot including = 4 ETH (no fulfillment yet)");
1102
-
1103
- _attestAndRatify(_evenScorecard(4));
1104
-
1105
- uint256 fee = deployer.fulfilledCommitmentsOf(_pid);
1106
- (potExcluding,,) = deployer.currentGamePotOf(_pid, false);
1107
- (potIncluding,,) = deployer.currentGamePotOf(_pid, true);
1108
- assertEq(potExcluding, 4 ether - fee, "pot excluding = surplus");
1109
- assertEq(potIncluding, 4 ether, "pot including = original pot");
1110
- }
1111
-
1112
- // =========================================================================
1113
- // PHASE TRANSITIONS: Correct sequence
1114
- // =========================================================================
1115
-
1116
- function test_fork_phaseTransitions_correctSequence() external {
1117
- DefifaLaunchProjectData memory d = _launchData(4, 1 ether);
1118
- (_pid, _nft, _gov) = _launch(d);
1119
-
1120
- // COUNTDOWN.
1121
- assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.COUNTDOWN));
1122
-
1123
- // MINT.
1124
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
1125
- assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.MINT));
1126
-
1127
- // REFUND.
1128
- vm.warp(d.start - d.refundPeriodDuration);
1129
- assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.REFUND));
1130
-
1131
- // SCORING.
1132
- vm.warp(d.start);
1133
- assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.SCORING));
1134
- }
1135
-
1136
- // =========================================================================
1137
- // GAME TIMES: timesFor view returns correct values
1138
- // =========================================================================
1139
-
1140
- function test_fork_timesFor() external {
1141
- DefifaLaunchProjectData memory d = _launchData(4, 1 ether);
1142
- (_pid, _nft, _gov) = _launch(d);
1143
-
1144
- (uint48 start, uint24 mintDur, uint24 refundDur) = deployer.timesFor(_pid);
1145
- assertEq(start, d.start, "start matches");
1146
- assertEq(mintDur, d.mintPeriodDuration, "mint duration matches");
1147
- assertEq(refundDur, d.refundPeriodDuration, "refund duration matches");
1148
- }
1149
-
1150
- // =========================================================================
1151
- // EDGE: NFT transfer → new owner cashes out, firstOwnerOf preserved
1152
- // =========================================================================
1153
-
1154
- function test_fork_nftTransfer_newOwnerCashesOut() external {
1155
- _setupGame(4, 1 ether);
1156
- _toScoring();
1157
-
1158
- // Give tier 1 all the weight.
1159
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
1160
- sc[0].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT();
1161
- _attestAndRatify(sc);
1162
-
1163
- address original = _users[0];
1164
- address recipient = _addr(999);
1165
-
1166
- uint256 tokenId = _generateTokenId(1, 1);
1167
-
1168
- // Verify firstOwnerOf before transfer.
1169
- assertEq(_nft.firstOwnerOf(tokenId), original, "firstOwner = minter before transfer");
1170
-
1171
- // Transfer NFT.
1172
- vm.prank(original);
1173
- _nft.transferFrom(original, recipient, tokenId);
1174
-
1175
- // firstOwnerOf should still be the original minter.
1176
- assertEq(_nft.firstOwnerOf(tokenId), original, "firstOwner = minter after transfer");
1177
-
1178
- // New owner cashes out.
1179
- uint256 bb = recipient.balance;
1180
- bytes memory meta = _cashOutMeta(1, 1);
1181
- vm.prank(recipient);
1182
- JBMultiTerminal(address(jbMultiTerminal()))
1183
- .cashOutTokensOf({
1184
- holder: recipient,
1185
- projectId: _pid,
1186
- cashOutCount: 0,
1187
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
1188
- minTokensReclaimed: 0,
1189
- beneficiary: payable(recipient),
1190
- metadata: meta
1191
- });
1192
- assertGt(recipient.balance - bb, 0, "new owner received ETH");
1193
- }
1194
-
1195
- // =========================================================================
1196
- // EDGE: Intra-tier fairness — 5 holders same tier, sequential cash outs
1197
- // =========================================================================
1198
-
1199
- function test_fork_intraTierFairness_5holders() external {
1200
- DefifaLaunchProjectData memory d = _launchData(2, 1 ether);
1201
- (_pid, _nft, _gov) = _launch(d);
1202
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
1203
-
1204
- // 5 people mint tier 1, 1 person mints tier 2.
1205
- _users = new address[](6);
1206
- for (uint256 i; i < 5; i++) {
1207
- _users[i] = _addr(i);
1208
- _mint(_users[i], 1, 1 ether);
1209
- _delegateSelf(_users[i], 1);
1210
- vm.warp(_tsReader.timestamp() + 1);
1211
- }
1212
- _users[5] = _addr(5);
1213
- _mint(_users[5], 2, 1 ether);
1214
- _delegateSelf(_users[5], 2);
1215
- vm.warp(_tsReader.timestamp() + 1);
1216
-
1217
- _toScoring();
1218
-
1219
- // All weight to tier 1.
1220
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(2);
1221
- sc[0].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT();
1222
- _attestAndRatify(sc);
1223
-
1224
- // Each of the 5 holders cashes out sequentially.
1225
- // Due to integer division (weight / 5), each should get the same amount.
1226
- uint256[] memory received = new uint256[](5);
1227
- for (uint256 i; i < 5; i++) {
1228
- uint256 bb = _users[i].balance;
1229
- _cashOut(_users[i], 1, i + 1);
1230
- received[i] = _users[i].balance - bb;
1231
- }
1232
-
1233
- // All 5 should receive the same amount (integer division means equal shares).
1234
- for (uint256 i = 1; i < 5; i++) {
1235
- assertEq(received[i], received[0], "all tier-1 holders get equal cash out");
1236
- }
1237
- }
1238
-
1239
- // =========================================================================
1240
- // EDGE: Multi-token cash out — burn 3 NFTs from same tier in one tx
1241
- // =========================================================================
1242
-
1243
- function test_fork_multiTokenCashOut_sameTier() external {
1244
- DefifaLaunchProjectData memory d = _launchData(2, 1 ether);
1245
- (_pid, _nft, _gov) = _launch(d);
1246
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
1247
-
1248
- address user = _addr(0);
1249
- // Mint 3 tokens in tier 1.
1250
- for (uint256 i; i < 3; i++) {
1251
- _mint(user, 1, 1 ether);
1252
- vm.warp(_tsReader.timestamp() + 1);
1253
- }
1254
- // Need someone in tier 2 for delegation/quorum.
1255
- address user2 = _addr(1);
1256
- _mint(user2, 2, 1 ether);
1257
- _delegateSelf(user2, 2);
1258
- vm.warp(_tsReader.timestamp() + 1);
1259
-
1260
- // Delegate tier 1.
1261
- DefifaDelegation[] memory dd = new DefifaDelegation[](1);
1262
- dd[0] = DefifaDelegation({delegatee: user, tierId: 1});
1263
- vm.prank(user);
1264
- _nft.setTierDelegatesTo(dd);
1265
- vm.warp(_tsReader.timestamp() + 1);
1266
-
1267
- _users = new address[](2);
1268
- _users[0] = user;
1269
- _users[1] = user2;
1270
-
1271
- _toScoring();
1272
-
1273
- // All weight to tier 1.
1274
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(2);
1275
- sc[0].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT();
1276
- _attestAndRatify(sc);
1277
-
1278
- // Build multi-token cash out metadata (3 tokens at once).
1279
- uint256[] memory tokenIds = new uint256[](3);
1280
- tokenIds[0] = _generateTokenId(1, 1);
1281
- tokenIds[1] = _generateTokenId(1, 2);
1282
- tokenIds[2] = _generateTokenId(1, 3);
1283
- bytes[] memory data = new bytes[](1);
1284
- data[0] = abi.encode(tokenIds);
1285
- bytes4[] memory ids = new bytes4[](1);
1286
- ids[0] = metadataHelper().getId("cashOut", address(hook));
1287
- bytes memory meta = metadataHelper().createMetadata(ids, data);
1288
-
1289
- uint256 bb = user.balance;
1290
- vm.prank(user);
1291
- JBMultiTerminal(address(jbMultiTerminal()))
1292
- .cashOutTokensOf({
1293
- holder: user,
1294
- projectId: _pid,
1295
- cashOutCount: 0,
1296
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
1297
- minTokensReclaimed: 0,
1298
- beneficiary: payable(user),
1299
- metadata: meta
1300
- });
1301
- assertGt(user.balance - bb, 0, "batch cash out returned ETH");
1302
- assertEq(_nft.balanceOf(user), 0, "all 3 NFTs burned");
1303
- }
1304
-
1305
- // =========================================================================
1306
- // EDGE: Cross-tier cash out — burn tokens from different tiers in one tx
1307
- // =========================================================================
1308
-
1309
- function test_fork_crossTierCashOut_singleTx() external {
1310
- _setupGame(4, 1 ether);
1311
- _toScoring();
1312
-
1313
- _attestAndRatify(_evenScorecard(4));
1314
-
1315
- // User 0 holds tier 1, user 1 holds tier 2. Transfer tier 2 to user 0.
1316
- vm.prank(_users[1]);
1317
- _nft.transferFrom(_users[1], _users[0], _generateTokenId(2, 1));
1318
-
1319
- // User 0 now holds tier 1 token 1 and tier 2 token 1. Cash out both in one tx.
1320
- uint256[] memory tokenIds = new uint256[](2);
1321
- tokenIds[0] = _generateTokenId(1, 1);
1322
- tokenIds[1] = _generateTokenId(2, 1);
1323
- bytes[] memory data = new bytes[](1);
1324
- data[0] = abi.encode(tokenIds);
1325
- bytes4[] memory ids = new bytes4[](1);
1326
- ids[0] = metadataHelper().getId("cashOut", address(hook));
1327
- bytes memory meta = metadataHelper().createMetadata(ids, data);
1328
-
1329
- uint256 bb = _users[0].balance;
1330
- vm.prank(_users[0]);
1331
- JBMultiTerminal(address(jbMultiTerminal()))
1332
- .cashOutTokensOf({
1333
- holder: _users[0],
1334
- projectId: _pid,
1335
- cashOutCount: 0,
1336
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
1337
- minTokensReclaimed: 0,
1338
- beneficiary: payable(_users[0]),
1339
- metadata: meta
1340
- });
1341
- assertGt(_users[0].balance - bb, 0, "cross-tier batch cash out returned ETH");
1342
- }
1343
-
1344
- // =========================================================================
1345
- // EDGE: Zero-power attestation — non-holder attests with 0 weight
1346
- // =========================================================================
1347
-
1348
- function test_fork_zeroPowerAttestation() external {
1349
- _setupGame(4, 1 ether);
1350
- _toScoring();
1351
-
1352
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
1353
- uint256 pid = _gov.submitScorecardFor(_gameId, sc);
1354
-
1355
- // Warp to attestation period.
1356
- uint256 attestStart = _gov.attestationStartTimeOf(_gameId);
1357
- uint256 current = _tsReader.timestamp();
1358
- vm.warp((attestStart > current ? attestStart : current) + 1);
1359
-
1360
- // Non-holder attests — should revert because BWA weight is 0.
1361
- address stranger = _addr(999);
1362
- vm.prank(stranger);
1363
- vm.expectRevert(DefifaGovernor.DefifaGovernor_NotAllowed.selector);
1364
- _gov.attestToScorecardFrom(_gameId, pid);
1365
- }
1366
-
1367
- // =========================================================================
1368
- // EDGE: Delegation to address(0) via setTierDelegateTo (no validation)
1369
- // =========================================================================
1370
-
1371
- function test_fork_delegateToZero_viaSetTierDelegateTo() external {
1372
- _setupGame(4, 1 ether);
1373
-
1374
- // Both setTierDelegateTo and setTierDelegatesTo revert on address(0).
1375
- vm.prank(_users[0]);
1376
- vm.expectRevert(DefifaHook.DefifaHook_DelegateAddressZero.selector);
1377
- _nft.setTierDelegateTo(address(0), 1);
1378
-
1379
- DefifaDelegation[] memory dd = new DefifaDelegation[](1);
1380
- dd[0] = DefifaDelegation({delegatee: address(0), tierId: 1});
1381
- vm.prank(_users[0]);
1382
- vm.expectRevert(DefifaHook.DefifaHook_DelegateAddressZero.selector);
1383
- _nft.setTierDelegatesTo(dd);
1384
- }
1385
-
1386
- // =========================================================================
1387
- // EDGE: minParticipation boundary — balance == minParticipation → NO_CONTEST
1388
- // =========================================================================
1389
-
1390
- function test_fork_minParticipation_exactBoundary_meets() external {
1391
- // balance == minParticipation: check uses `<`, so 4 < 4 = false → SCORING.
1392
- DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 4 ether, 0);
1393
- (_pid, _nft, _gov) = _launch(d);
1394
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
1395
-
1396
- _users = new address[](4);
1397
- for (uint256 i; i < 4; i++) {
1398
- _users[i] = _addr(i);
1399
- _mint(_users[i], i + 1, 1 ether);
1400
- _delegateSelf(_users[i], i + 1);
1401
- vm.warp(_tsReader.timestamp() + 1);
1402
- }
1403
-
1404
- _toScoring();
1405
- assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.SCORING));
1406
- }
1407
-
1408
- function test_fork_minParticipation_belowThreshold() external {
1409
- // balance < minParticipation → NO_CONTEST.
1410
- DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 4 ether + 1, 0);
1411
- (_pid, _nft, _gov) = _launch(d);
1412
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
1413
-
1414
- _users = new address[](4);
1415
- for (uint256 i; i < 4; i++) {
1416
- _users[i] = _addr(i);
1417
- _mint(_users[i], i + 1, 1 ether);
1418
- vm.warp(_tsReader.timestamp() + 1);
1419
- }
1420
-
1421
- _toScoring();
1422
- // balance = 4 ether, minParticipation = 4 ether + 1 wei → 4e18 < 4e18+1 → NO_CONTEST.
1423
- assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
1424
- }
1425
-
1426
- // =========================================================================
1427
- // EDGE: Cash out during SCORING (before weights set) → NothingToClaim
1428
- // =========================================================================
1429
-
1430
- function test_fork_cashOutDuringScoring_reverts() external {
1431
- _setupGame(4, 1 ether);
1432
- _toScoring();
1433
-
1434
- // No scorecard ratified yet. Cash out with 0 weight → hook reverts with NothingToClaim.
1435
- bytes memory meta = _cashOutMeta(1, 1);
1436
- vm.prank(_users[0]);
1437
- vm.expectRevert(DefifaHook.DefifaHook_NothingToClaim.selector);
1438
- JBMultiTerminal(address(jbMultiTerminal()))
1439
- .cashOutTokensOf({
1440
- holder: _users[0],
1441
- projectId: _pid,
1442
- cashOutCount: 0,
1443
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
1444
- minTokensReclaimed: 0,
1445
- beneficiary: payable(_users[0]),
1446
- metadata: meta
1447
- });
1448
- }
1449
-
1450
- // =========================================================================
1451
- // EDGE: NFT transfer destroys delegation — new owner can't vote in SCORING
1452
- // =========================================================================
1453
-
1454
- function test_fork_nftTransfer_recipientGetsAttestationPower() external {
1455
- _setupGame(4, 1 ether);
1456
-
1457
- address recipient = _addr(999);
1458
-
1459
- // Transfer tier 1 NFT during MINT.
1460
- vm.prank(_users[0]);
1461
- _nft.transferFrom(_users[0], recipient, _generateTokenId(1, 1));
1462
-
1463
- // Recipient has the NFT but delegation went to address(0) (recipient's default).
1464
- // Recipient can re-delegate during MINT.
1465
- DefifaDelegation[] memory dd = new DefifaDelegation[](1);
1466
- dd[0] = DefifaDelegation({delegatee: recipient, tierId: 1});
1467
- vm.prank(recipient);
1468
- _nft.setTierDelegatesTo(dd);
1469
- vm.warp(_tsReader.timestamp() + 1);
1470
-
1471
- _toScoring();
1472
-
1473
- // Both the original owner and recipient have attestation power due to checkpoint history.
1474
- // The key invariant: the recipient who re-delegated can attest with non-zero weight.
1475
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
1476
- uint256 pid = _gov.submitScorecardFor(_gameId, sc);
1477
-
1478
- uint256 attestStart = _gov.attestationStartTimeOf(_gameId);
1479
- uint256 current = _tsReader.timestamp();
1480
- vm.warp((attestStart > current ? attestStart : current) + 1);
1481
-
1482
- // Recipient should have attestation power after re-delegation.
1483
- vm.prank(recipient);
1484
- uint256 w1 = _gov.attestToScorecardFrom(_gameId, pid);
1485
- assertGt(w1, 0, "recipient has attestation power after transfer and re-delegation");
1486
- }
1487
-
1488
- // =========================================================================
1489
- // EDGE: Delegation change reverts outside MINT phase
1490
- // =========================================================================
1491
-
1492
- function test_fork_delegateDuringScoring_reverts() external {
1493
- _setupGame(4, 1 ether);
1494
- _toScoring();
1495
-
1496
- DefifaDelegation[] memory dd = new DefifaDelegation[](1);
1497
- dd[0] = DefifaDelegation({delegatee: _users[0], tierId: 1});
1498
- vm.prank(_users[0]);
1499
- vm.expectRevert(DefifaHook.DefifaHook_DelegateChangesUnavailableInThisPhase.selector);
1500
- _nft.setTierDelegatesTo(dd);
1501
- }
1502
-
1503
- // =========================================================================
1504
- // EDGE: Scorecard timeout boundary — exact tick
1505
- // =========================================================================
1506
-
1507
- function test_fork_scorecardTimeout_exactBoundary() external {
1508
- DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 7 days);
1509
- (_pid, _nft, _gov) = _launch(d);
1510
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
1511
-
1512
- _users = new address[](4);
1513
- for (uint256 i; i < 4; i++) {
1514
- _users[i] = _addr(i);
1515
- _mint(_users[i], i + 1, 1 ether);
1516
- _delegateSelf(_users[i], i + 1);
1517
- vm.warp(_tsReader.timestamp() + 1);
1518
- }
1519
-
1520
- // Advance to scoring start.
1521
- vm.warp(d.start);
1522
- assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.SCORING));
1523
-
1524
- // At exactly start + scorecardTimeout, check uses `>` so should still be SCORING.
1525
- vm.warp(d.start + 7 days);
1526
- assertEq(
1527
- uint256(deployer.currentGamePhaseOf(_pid)),
1528
- uint256(DefifaGamePhase.SCORING),
1529
- "at exact boundary: still SCORING"
1530
- );
1531
-
1532
- // One second later → NO_CONTEST.
1533
- vm.warp(d.start + 7 days + 1);
1534
- assertEq(
1535
- uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST), "past boundary: NO_CONTEST"
1536
- );
1537
- }
1538
-
1539
- // =========================================================================
1540
- // EDGE: Refund then re-mint same tier (user comes back)
1541
- // =========================================================================
1542
-
1543
- function test_fork_refundThenRemint() external {
1544
- _setupGame(4, 1 ether);
1545
-
1546
- address user = _users[0];
1547
- uint256 tokenId1 = _generateTokenId(1, 1);
1548
-
1549
- // Verify user holds token.
1550
- assertEq(_nft.ownerOf(tokenId1), user);
1551
-
1552
- // Refund.
1553
- _refund(user, 1);
1554
- assertEq(_nft.balanceOf(user), 0, "NFT burned after refund");
1555
-
1556
- // Re-mint same tier. Token number should be 2 now (first was burned).
1557
- _mint(user, 1, 1 ether);
1558
- uint256 tokenId2 = _generateTokenId(1, 2);
1559
- assertEq(_nft.ownerOf(tokenId2), user, "user re-minted tier 1 with new token number");
1560
- }
1561
-
1562
- // =========================================================================
1563
- // EDGE: Defeated scorecard after another is ratified
1564
- // =========================================================================
1565
-
1566
- function test_fork_defeatedScorecard_afterRatification() external {
1567
- _setupGame(4, 1 ether);
1568
- _toScoring();
1569
-
1570
- // Submit two competing scorecards.
1571
- DefifaTierCashOutWeight[] memory scA = _buildScorecard(4);
1572
- scA[0].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT();
1573
- uint256 pidA = _gov.submitScorecardFor(_gameId, scA);
1574
-
1575
- DefifaTierCashOutWeight[] memory scB = _evenScorecard(4);
1576
- uint256 pidB = _gov.submitScorecardFor(_gameId, scB);
1577
-
1578
- // Attest and ratify scorecard A.
1579
- _attestAllFor(pidA);
1580
- _gov.ratifyScorecardFrom(_gameId, scA);
1581
-
1582
- // Scorecard A is RATIFIED, scorecard B is DEFEATED.
1583
- assertEq(uint256(_gov.stateOf(_gameId, pidA)), uint256(DefifaScorecardState.RATIFIED));
1584
- assertEq(uint256(_gov.stateOf(_gameId, pidB)), uint256(DefifaScorecardState.DEFEATED));
1585
- }
1586
-
1587
- // =========================================================================
1588
- // EDGE: Reserve mint → attestation power
1589
- // =========================================================================
1590
-
1591
- function test_fork_reserveMint_getsAttestationPower() external {
1592
- _setupGame(4, 1 ether);
1593
-
1594
- // The reserve beneficiary is address(0) in our default params (no reserved token beneficiary).
1595
- // But reservedRate is 1001 (1 reserve per 1001 mints). With only 1 mint, no reserves trigger.
1596
- // Let's verify the existing voting power works correctly even with the reserve rate set.
1597
-
1598
- // Verify all 4 users have attestation power through their delegation.
1599
- _toScoring();
1600
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
1601
- uint256 pid = _gov.submitScorecardFor(_gameId, sc);
1602
-
1603
- uint256 attestStart = _gov.attestationStartTimeOf(_gameId);
1604
- uint256 current = _tsReader.timestamp();
1605
- vm.warp((attestStart > current ? attestStart : current) + 1);
1606
-
1607
- // Each user (sole holder of their tier) should get equal, non-zero attestation power.
1608
- // Note: the protocol gives sole holders 2x MAX_ATTESTATION_POWER_TIER because
1609
- // delegate checkpoint units (from store's votingUnits = price) are 2x total tier checkpoints.
1610
- uint256 firstWeight;
1611
- for (uint256 i; i < 4; i++) {
1612
- vm.prank(_users[i]);
1613
- uint256 w = _gov.attestToScorecardFrom(_gameId, pid);
1614
- assertGt(w, 0, "sole holder has attestation power");
1615
- if (i == 0) firstWeight = w;
1616
- else assertEq(w, firstWeight, "all sole holders get equal power");
1617
- }
1618
- }
1619
-
1620
- // =========================================================================
1621
- // EDGE: Quorum with odd number of minted tiers (rounding)
1622
- // =========================================================================
1623
-
1624
- function test_fork_quorum_oddTierCount() external {
1625
- // 3 tiers minted. Quorum = (3 * MAX_ATTESTATION_POWER_TIER) / 2 = 1.5e9 → rounds to 1_500_000_000.
1626
- _setupGame(3, 1 ether);
1627
- _toScoring();
1628
-
1629
- uint256 q = _gov.quorum(_gameId);
1630
- uint256 expectedQuorum = (3 * _gov.MAX_ATTESTATION_POWER_TIER()) / 2;
1631
- assertEq(q, expectedQuorum, "quorum = floor(3 * 1e9 / 2)");
1632
-
1633
- // All 3 tiers attesting should exceed quorum (BWA reduces each holder's power by their tier share).
1634
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(3);
1635
- uint256 pid = _gov.submitScorecardFor(_gameId, sc);
1636
-
1637
- uint256 attestStart = _gov.attestationStartTimeOf(_gameId);
1638
- uint256 current = _tsReader.timestamp();
1639
- vm.warp((attestStart > current ? attestStart : current) + 1);
1640
-
1641
- // All 3 users attest. BWA reduces each to ~2/3 power; 3 * 2/3 * MAX = 2*MAX > quorum.
1642
- for (uint256 i; i < 3; i++) {
1643
- vm.prank(_users[i]);
1644
- _gov.attestToScorecardFrom(_gameId, pid);
1645
- }
1646
-
1647
- vm.warp(_tsReader.timestamp() + _gov.attestationGracePeriodOf(_gameId) + 1);
1648
- assertEq(uint256(_gov.stateOf(_gameId, pid)), uint256(DefifaScorecardState.SUCCEEDED));
1649
- }
1650
-
1651
- // =========================================================================
1652
- // EDGE: Quorum NOT met with 1 of 5 tiers (below 50%)
1653
- // =========================================================================
1654
-
1655
- function test_fork_quorum_notMet_1of5() external {
1656
- // Use 5 tiers so quorum = 5 * MAX / 2 = 2.5e9.
1657
- // A sole holder contributes ~2e9 (due to 2x attestation factor) < 2.5e9 → quorum NOT met.
1658
- _setupGame(5, 1 ether);
1659
- _toScoring();
1660
-
1661
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(5);
1662
- uint256 pid = _gov.submitScorecardFor(_gameId, sc);
1663
-
1664
- uint256 attestStart = _gov.attestationStartTimeOf(_gameId);
1665
- uint256 current = _tsReader.timestamp();
1666
- vm.warp((attestStart > current ? attestStart : current) + 1);
1667
-
1668
- // Only 1 of 5 tiers attests.
1669
- vm.prank(_users[0]);
1670
- uint256 w = _gov.attestToScorecardFrom(_gameId, pid);
1671
-
1672
- // Verify the single attestation is below quorum.
1673
- uint256 q = _gov.quorum(_gameId);
1674
- assertLt(w, q, "single holder weight < quorum");
1675
-
1676
- // State should still be ACTIVE after grace period (quorum not met).
1677
- vm.warp(_tsReader.timestamp() + _gov.attestationGracePeriodOf(_gameId) + 1);
1678
- assertEq(uint256(_gov.stateOf(_gameId, pid)), uint256(DefifaScorecardState.ACTIVE));
1679
-
1680
- // Ratification should fail.
1681
- vm.expectRevert(DefifaGovernor.DefifaGovernor_NotAllowed.selector);
1682
- _gov.ratifyScorecardFrom(_gameId, sc);
1683
- }
1684
-
1685
- // =========================================================================
1686
- // EDGE: NO_CONTEST full cycle — trigger, then all users refund at mint price
1687
- // =========================================================================
1688
-
1689
- function test_fork_noContest_fullRefundCycle() external {
1690
- DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 7 days);
1691
- (_pid, _nft, _gov) = _launch(d);
1692
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
1693
-
1694
- _users = new address[](4);
1695
- for (uint256 i; i < 4; i++) {
1696
- _users[i] = _addr(i);
1697
- _mint(_users[i], i + 1, 1 ether);
1698
- _delegateSelf(_users[i], i + 1);
1699
- vm.warp(_tsReader.timestamp() + 1);
1700
- }
1701
-
1702
- // Let scorecard timeout expire → NO_CONTEST.
1703
- vm.warp(d.start + 7 days + 1);
1704
- assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
1705
-
1706
- // Trigger no-contest.
1707
- deployer.triggerNoContestFor(_pid);
1708
-
1709
- // Advance to let the new ruleset take effect.
1710
- vm.warp(_tsReader.timestamp() + 1);
1711
-
1712
- // All users should be able to refund at mint price (1 ETH each).
1713
- for (uint256 i; i < 4; i++) {
1714
- uint256 bb = _users[i].balance;
1715
- _refund(_users[i], i + 1);
1716
- assertEq(_users[i].balance - bb, 1 ether, "NO_CONTEST refund = mint price");
1717
- }
1718
-
1719
- // Treasury should be empty.
1720
- assertEq(_balance(), 0, "treasury empty after all refunds");
1721
- }
1722
-
1723
- // =========================================================================
1724
- // EDGE: Attestation weight shared proportionally within tier
1725
- // =========================================================================
1726
-
1727
- function test_fork_attestationWeight_proportionalInTier() external {
1728
- DefifaLaunchProjectData memory d = _launchData(2, 1 ether);
1729
- (_pid, _nft, _gov) = _launch(d);
1730
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
1731
-
1732
- // 3 people in tier 1, 1 person in tier 2.
1733
- address alice = _addr(0);
1734
- address bob = _addr(1);
1735
- address carol = _addr(2);
1736
- address dan = _addr(3);
1737
-
1738
- _mint(alice, 1, 1 ether);
1739
- _delegateSelf(alice, 1);
1740
- vm.warp(_tsReader.timestamp() + 1);
1741
- _mint(bob, 1, 1 ether);
1742
- _delegateSelf(bob, 1);
1743
- vm.warp(_tsReader.timestamp() + 1);
1744
- _mint(carol, 1, 1 ether);
1745
- _delegateSelf(carol, 1);
1746
- vm.warp(_tsReader.timestamp() + 1);
1747
- _mint(dan, 2, 1 ether);
1748
- _delegateSelf(dan, 2);
1749
- vm.warp(_tsReader.timestamp() + 1);
1750
-
1751
- _users = new address[](4);
1752
- _users[0] = alice;
1753
- _users[1] = bob;
1754
- _users[2] = carol;
1755
- _users[3] = dan;
1756
-
1757
- _toScoring();
1758
-
1759
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(2);
1760
- uint256 pid = _gov.submitScorecardFor(_gameId, sc);
1761
-
1762
- uint256 attestStart = _gov.attestationStartTimeOf(_gameId);
1763
- uint256 current = _tsReader.timestamp();
1764
- vm.warp((attestStart > current ? attestStart : current) + 1);
1765
-
1766
- // Alice holds 1/3 of tier 1 → proportionally less power than Dan (sole holder of tier 2).
1767
- vm.prank(alice);
1768
- uint256 wAlice = _gov.attestToScorecardFrom(_gameId, pid);
1769
-
1770
- // Dan holds 1/1 of tier 2 → full power for his tier.
1771
- vm.prank(dan);
1772
- uint256 wDan = _gov.attestToScorecardFrom(_gameId, pid);
1773
-
1774
- // Verify proportionality: Alice should have roughly 1/3 of Dan's power.
1775
- assertGt(wDan, wAlice, "sole holder has more power than 1/3 holder");
1776
- assertGt(wAlice, 0, "partial holder still has power");
1777
- // Alice = 1/3 of tier 1, Dan = all of tier 2. Allow 1 wei rounding tolerance.
1778
- assertApproxEqAbs(wAlice * 3, wDan, 3, "3 x alice power ~= dan power");
1779
- }
1780
-
1781
- // =========================================================================
1782
- // EDGE: Cash out non-owned token ID → Unauthorized
1783
- // =========================================================================
1784
-
1785
- function test_fork_cashOut_wrongTokenId_reverts() external {
1786
- _setupGame(4, 1 ether);
1787
- _toScoring();
1788
- _attestAndRatify(_evenScorecard(4));
1789
-
1790
- // User 0 tries to cash out user 1's token.
1791
- uint256[] memory tokenIds = new uint256[](1);
1792
- tokenIds[0] = _generateTokenId(2, 1); // tier 2, token 1 — belongs to user 1
1793
- bytes[] memory data = new bytes[](1);
1794
- data[0] = abi.encode(tokenIds);
1795
- bytes4[] memory ids = new bytes4[](1);
1796
- ids[0] = metadataHelper().getId("cashOut", address(hook));
1797
- bytes memory meta = metadataHelper().createMetadata(ids, data);
1798
-
1799
- vm.prank(_users[0]);
1800
- vm.expectRevert(
1801
- abi.encodeWithSelector(
1802
- DefifaHook.DefifaHook_Unauthorized.selector, _generateTokenId(2, 1), _users[1], _users[0]
1803
- )
1804
- );
1805
- JBMultiTerminal(address(jbMultiTerminal()))
1806
- .cashOutTokensOf({
1807
- holder: _users[0],
1808
- projectId: _pid,
1809
- cashOutCount: 0,
1810
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
1811
- minTokensReclaimed: 0,
1812
- beneficiary: payable(_users[0]),
1813
- metadata: meta
1814
- });
1815
- }
1816
-
1817
- // =========================================================================
1818
- // EDGE: Submit scorecard outside SCORING phase
1819
- // =========================================================================
1820
-
1821
- function test_fork_submitScorecard_duringMint_reverts() external {
1822
- _setupGame(4, 1 ether);
1823
- // Still in MINT phase.
1824
-
1825
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
1826
- vm.expectRevert(DefifaGovernor.DefifaGovernor_NotAllowed.selector);
1827
- _gov.submitScorecardFor(_gameId, sc);
1828
- }
1829
-
1830
- // =========================================================================
1831
- // EDGE: Two-player precise accounting — winner takes (surplus - dust)
1832
- // =========================================================================
1833
-
1834
- function test_fork_twoplayer_preciseAccounting() external {
1835
- _setupGame(2, 5 ether);
1836
-
1837
- uint256 totalPot = _balance();
1838
- assertEq(totalPot, 10 ether, "2 players x 5 ETH");
1839
-
1840
- _toScoring();
1841
-
1842
- // Tier 1 wins 100%.
1843
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(2);
1844
- sc[0].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT();
1845
- _attestAndRatify(sc);
1846
-
1847
- uint256 surplus = _surplus();
1848
- // surplus = totalPot - fees (7.5%)
1849
- uint256 expectedSurplus = totalPot - (totalPot * 75_000_000 / JBConstants.SPLITS_TOTAL_PERCENT);
1850
- assertEq(surplus, expectedSurplus, "surplus = pot - 7.5% fees");
1851
-
1852
- // Winner cashes out entire surplus.
1853
- uint256 bb = _users[0].balance;
1854
- _cashOut(_users[0], 1, 1);
1855
- uint256 winnerGot = _users[0].balance - bb;
1856
-
1857
- // Winner should get the full surplus (minus rounding dust).
1858
- assertApproxEqAbs(winnerGot, surplus, 1, "winner gets full surplus");
1859
- }
1860
-
1861
- // =========================================================================
1862
- // FUZZ: Fund conservation across varying tier/player counts
1863
- // =========================================================================
1864
-
1865
- function test_fork_fuzz_fundConservation(uint8 rawTiers, uint8 rawPlayers) external {
1866
- // N≥3 avoids BWA rounding shortfall with N=2 even split + multiple holders per tier.
1867
- uint8 nTiers = uint8(bound(rawTiers, 3, 12));
1868
- uint8 nPpt = uint8(bound(rawPlayers, 1, 3));
1869
-
1870
- _setupMultiN(nTiers, nPpt, 1 ether);
1871
- _toScoring();
1872
-
1873
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
1874
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(nTiers);
1875
- uint256 assigned;
1876
- for (uint256 i; i < nTiers; i++) {
1877
- if (i == nTiers - 1) {
1878
- sc[i].cashOutWeight = tw - assigned;
1879
- } else {
1880
- sc[i].cashOutWeight = tw / nTiers;
1881
- }
1882
- assigned += sc[i].cashOutWeight;
1883
- }
1884
-
1885
- _attestAndRatify(sc);
1886
- uint256 pot = _surplus();
1887
-
1888
- uint256 total;
1889
- for (uint256 i; i < _users.length; i++) {
1890
- uint256 bb = _users[i].balance;
1891
- uint256 tid = (i / nPpt) + 1;
1892
- uint256 tnum = (i % nPpt) + 1;
1893
- _cashOut(_users[i], tid, tnum);
1894
- total += _users[i].balance - bb;
1895
- }
1896
-
1897
- assertApproxEqAbs(total + _surplus(), pot, _users.length, "fund conservation");
1898
- }
1899
-
1900
- // =========================================================================
1901
- // SCORECARD STATE MACHINE: PENDING → ACTIVE → SUCCEEDED → RATIFIED
1902
- // =========================================================================
1903
-
1904
- function test_fork_scorecardStateMachine() external {
1905
- _setupGame(4, 1 ether);
1906
- _toScoring();
1907
-
1908
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
1909
- uint256 pid = _gov.submitScorecardFor(_gameId, sc);
1910
-
1911
- // On fork, attestationStartTimeOf is an absolute timestamp already in the past,
1912
- // so the scorecard goes straight to ACTIVE (no PENDING window).
1913
- uint256 attestStart = _gov.attestationStartTimeOf(_gameId);
1914
- uint256 current = _tsReader.timestamp();
1915
- vm.warp((attestStart > current ? attestStart : current) + 1);
1916
- assertEq(uint256(_gov.stateOf(_gameId, pid)), uint256(DefifaScorecardState.ACTIVE));
1917
-
1918
- // Attest all.
1919
- for (uint256 i; i < _users.length; i++) {
1920
- vm.prank(_users[i]);
1921
- _gov.attestToScorecardFrom(_gameId, pid);
1922
- }
1923
-
1924
- // Still ACTIVE during grace period (even if quorum met).
1925
- assertEq(uint256(_gov.stateOf(_gameId, pid)), uint256(DefifaScorecardState.ACTIVE));
1926
-
1927
- // SUCCEEDED: after grace period.
1928
- vm.warp(_tsReader.timestamp() + _gov.attestationGracePeriodOf(_gameId) + 1);
1929
- assertEq(uint256(_gov.stateOf(_gameId, pid)), uint256(DefifaScorecardState.SUCCEEDED));
1930
-
1931
- // RATIFIED: after ratification.
1932
- _gov.ratifyScorecardFrom(_gameId, sc);
1933
- assertEq(uint256(_gov.stateOf(_gameId, pid)), uint256(DefifaScorecardState.RATIFIED));
1934
- }
1935
-
1936
- // =========================================================================
1937
- // SCORECARD: Ratification before SUCCEEDED state reverts
1938
- // =========================================================================
1939
-
1940
- function test_fork_ratifyBeforeSucceeded_reverts() external {
1941
- _setupGame(4, 1 ether);
1942
- _toScoring();
1943
-
1944
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
1945
- _gov.submitScorecardFor(_gameId, sc);
1946
-
1947
- // No attestations yet — try to ratify.
1948
- vm.expectRevert(DefifaGovernor.DefifaGovernor_NotAllowed.selector);
1949
- _gov.ratifyScorecardFrom(_gameId, sc);
1950
- }
1951
-
1952
- // =========================================================================
1953
- // SCORECARD: Unknown scorecard reverts
1954
- // =========================================================================
1955
-
1956
- function test_fork_unknownScorecardState_reverts() external {
1957
- _setupGame(4, 1 ether);
1958
- _toScoring();
1959
-
1960
- // Query state for a non-existent scorecard.
1961
- vm.expectRevert(DefifaGovernor.DefifaGovernor_UnknownProposal.selector);
1962
- _gov.stateOf(_gameId, 12_345);
1963
- }
1964
-
1965
- // =========================================================================
1966
- // ADVERSARIAL: Zero-weight scorecard (all zeros except minimum)
1967
- // =========================================================================
1968
-
1969
- function test_fork_zeroWeightTiers_winnerTakeAll() external {
1970
- _setupGame(4, 1 ether);
1971
- _toScoring();
1972
-
1973
- // Give all weight to tier 4, zero to others.
1974
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
1975
- sc[3].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT();
1976
-
1977
- _attestAndRatify(sc);
1978
-
1979
- // Tiers 1-3 get nothing.
1980
- for (uint256 i; i < 3; i++) {
1981
- uint256 weight = _nft.cashOutWeightOf(_generateTokenId(i + 1, 1));
1982
- assertEq(weight, 0, "zero-weight tier has 0 cash out weight");
1983
- }
1984
-
1985
- // Tier 4 gets everything.
1986
- uint256 weight4 = _nft.cashOutWeightOf(_generateTokenId(4, 1));
1987
- assertGt(weight4, 0, "tier 4 has weight");
1988
-
1989
- uint256 bb = _users[3].balance;
1990
- _cashOut(_users[3], 4, 1);
1991
- assertGt(_users[3].balance - bb, 0, "tier 4 holder received ETH");
1992
- }
1993
-
1994
- // =========================================================================
1995
- // ADVERSARIAL: Scorecard tier order violation reverts
1996
- // =========================================================================
1997
-
1998
- function test_fork_scorecardBadTierOrder_reverts() external {
1999
- _setupGame(4, 1 ether);
2000
- _toScoring();
2001
-
2002
- // Tiers out of order: [3, 1, 2, 4].
2003
- DefifaTierCashOutWeight[] memory sc = new DefifaTierCashOutWeight[](4);
2004
- sc[0] = DefifaTierCashOutWeight({id: 3, cashOutWeight: _nft.TOTAL_CASHOUT_WEIGHT() / 4});
2005
- sc[1] = DefifaTierCashOutWeight({id: 1, cashOutWeight: _nft.TOTAL_CASHOUT_WEIGHT() / 4});
2006
- sc[2] = DefifaTierCashOutWeight({id: 2, cashOutWeight: _nft.TOTAL_CASHOUT_WEIGHT() / 4});
2007
- sc[3] = DefifaTierCashOutWeight({id: 4, cashOutWeight: _nft.TOTAL_CASHOUT_WEIGHT() / 4});
2008
-
2009
- vm.expectRevert(DefifaHookLib.DefifaHook_BadTierOrder.selector);
2010
- _gov.submitScorecardFor(_gameId, sc);
2011
- }
2012
-
2013
- // =========================================================================
2014
- // MINTING: Multi-tier mint in single transaction
2015
- // =========================================================================
2016
-
2017
- function test_fork_multiTierMint_singleTx() external {
2018
- DefifaLaunchProjectData memory d = _launchData(4, 1 ether);
2019
- (_pid, _nft, _gov) = _launch(d);
2020
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
2021
-
2022
- address user = _addr(0);
2023
- vm.deal(user, 3 ether);
2024
-
2025
- // Mint tiers 1, 2, 3 in one tx (3 ETH).
2026
- uint16[] memory m = new uint16[](3);
2027
- m[0] = 1;
2028
- m[1] = 2;
2029
- m[2] = 3;
2030
- bytes[] memory data = new bytes[](1);
2031
- data[0] = abi.encode(user, m);
2032
- bytes4[] memory ids = new bytes4[](1);
2033
- ids[0] = metadataHelper().getId("pay", address(hook));
2034
-
2035
- vm.prank(user);
2036
- jbMultiTerminal().pay{value: 3 ether}(
2037
- _pid, JBConstants.NATIVE_TOKEN, 3 ether, user, 0, "", metadataHelper().createMetadata(ids, data)
2038
- );
2039
-
2040
- assertEq(_nft.balanceOf(user), 3, "user holds 3 NFTs");
2041
- }
2042
-
2043
- // =========================================================================
2044
- // GRACE PERIOD: Enforced minimum of 1 day
2045
- // =========================================================================
2046
-
2047
- function test_fork_gracePeriod_minimumEnforced() external {
2048
- DefifaLaunchProjectData memory d = DefifaLaunchProjectData({
2049
- name: "DEFIFA",
2050
- projectUri: "",
2051
- contractUri: "",
2052
- baseUri: "",
2053
- token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
2054
- mintPeriodDuration: 1 days,
2055
- start: uint48(block.timestamp + 3 days),
2056
- refundPeriodDuration: 1 days,
2057
- store: new JB721TiersHookStore(),
2058
- splits: new JBSplit[](0),
2059
- attestationStartTime: 0,
2060
- attestationGracePeriod: 1, // Very short — should be clamped to 1 day.
2061
- defaultAttestationDelegate: address(0),
2062
- tierPrice: uint104(1 ether),
2063
- tiers: _makeTierParams(4),
2064
- defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
2065
- terminal: jbMultiTerminal(),
2066
- minParticipation: 0,
2067
- scorecardTimeout: 0,
2068
- timelockDuration: 0
2069
- });
2070
- (_pid, _nft, _gov) = _launch(d);
2071
-
2072
- // Governor should enforce minimum grace period of 1 day.
2073
- assertGe(_gov.attestationGracePeriodOf(_gameId), 1 days, "grace period >= 1 day");
2074
- }
2075
-
2076
- // =========================================================================
2077
- // CASH OUT WEIGHT: totalCashOutWeight() is constant
2078
- // =========================================================================
2079
-
2080
- function test_fork_totalCashOutWeight_constant() external {
2081
- _setupGame(4, 1 ether);
2082
-
2083
- // Before scorecard.
2084
- assertEq(_nft.totalCashOutWeight(), _nft.TOTAL_CASHOUT_WEIGHT(), "constant before scorecard");
2085
-
2086
- _toScoring();
2087
- _attestAndRatify(_evenScorecard(4));
2088
-
2089
- // After scorecard.
2090
- assertEq(_nft.totalCashOutWeight(), _nft.TOTAL_CASHOUT_WEIGHT(), "constant after scorecard");
2091
- }
2092
-
2093
- // =========================================================================
2094
- // SETUP HELPERS
2095
- // =========================================================================
2096
-
2097
- function _setupGame(uint8 nTiers, uint256 tierPrice) internal {
2098
- DefifaLaunchProjectData memory d = _launchData(nTiers, tierPrice);
2099
- (_pid, _nft, _gov) = _launch(d);
2100
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
2101
- _users = new address[](nTiers);
2102
- for (uint256 i; i < nTiers; i++) {
2103
- _users[i] = _addr(i);
2104
- _mint(_users[i], i + 1, tierPrice);
2105
- _delegateSelf(_users[i], i + 1);
2106
- vm.warp(_tsReader.timestamp() + 1);
2107
- }
2108
- }
2109
-
2110
- function _setupMultiPlayer() internal {
2111
- DefifaLaunchProjectData memory d = _launchData(4, 1 ether);
2112
- (_pid, _nft, _gov) = _launch(d);
2113
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
2114
- _users = new address[](8);
2115
- for (uint256 i; i < 5; i++) {
2116
- _users[i] = _addr(100 + i);
2117
- _mint(_users[i], 1, 1 ether);
2118
- _delegateSelf(_users[i], 1);
2119
- vm.warp(_tsReader.timestamp() + 1);
2120
- }
2121
- for (uint256 i; i < 3; i++) {
2122
- _users[5 + i] = _addr(200 + i);
2123
- _mint(_users[5 + i], i + 2, 1 ether);
2124
- _delegateSelf(_users[5 + i], i + 2);
2125
- vm.warp(_tsReader.timestamp() + 1);
2126
- }
2127
- }
2128
-
2129
- function _setupMultiN(uint8 nTiers, uint8 nPpt, uint256 tierPrice) internal {
2130
- DefifaLaunchProjectData memory d = _launchData(nTiers, tierPrice);
2131
- (_pid, _nft, _gov) = _launch(d);
2132
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
2133
- uint256 total = uint256(nTiers) * uint256(nPpt);
2134
- _users = new address[](total);
2135
- uint256 idx;
2136
- for (uint256 t; t < nTiers; t++) {
2137
- for (uint256 p; p < nPpt; p++) {
2138
- _users[idx] = _addr(idx);
2139
- _mint(_users[idx], t + 1, tierPrice);
2140
- _delegateSelf(_users[idx], t + 1);
2141
- vm.warp(_tsReader.timestamp() + 1);
2142
- idx++;
2143
- }
2144
- }
2145
- }
2146
-
2147
- function _setupPartial(uint8 nTiers, uint256 nMint, uint256 tierPrice) internal {
2148
- DefifaLaunchProjectData memory d = _launchData(nTiers, tierPrice);
2149
- (_pid, _nft, _gov) = _launch(d);
2150
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
2151
- _users = new address[](nMint);
2152
- for (uint256 i; i < nMint; i++) {
2153
- _users[i] = _addr(i);
2154
- _mint(_users[i], i + 1, tierPrice);
2155
- _delegateSelf(_users[i], i + 1);
2156
- vm.warp(_tsReader.timestamp() + 1);
2157
- }
2158
- }
2159
-
2160
- function _toScoring() internal {
2161
- vm.warp(_tsReader.timestamp() + 3 days + 1);
2162
- }
2163
-
2164
- // =========================================================================
2165
- // PRIMITIVE HELPERS
2166
- // =========================================================================
2167
-
2168
- function _launchData(uint8 n, uint256 tierPrice) internal returns (DefifaLaunchProjectData memory) {
2169
- return _launchDataWith(n, tierPrice, 0, 0);
2170
- }
2171
-
2172
- function _launchDataWith(
2173
- uint8 n,
2174
- uint256 tierPrice,
2175
- uint256 minParticipation,
2176
- uint32 scorecardTimeout
2177
- )
2178
- internal
2179
- returns (DefifaLaunchProjectData memory)
2180
- {
2181
- return DefifaLaunchProjectData({
2182
- name: "DEFIFA",
2183
- projectUri: "",
2184
- contractUri: "",
2185
- baseUri: "",
2186
- token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
2187
- mintPeriodDuration: 1 days,
2188
- start: uint48(block.timestamp + 3 days),
2189
- refundPeriodDuration: 1 days,
2190
- store: new JB721TiersHookStore(),
2191
- splits: new JBSplit[](0),
2192
- attestationStartTime: 0,
2193
- attestationGracePeriod: 100_381,
2194
- defaultAttestationDelegate: address(0),
2195
- // forge-lint: disable-next-line(unsafe-typecast)
2196
- tierPrice: uint104(tierPrice),
2197
- tiers: _makeTierParams(n),
2198
- defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
2199
- terminal: jbMultiTerminal(),
2200
- minParticipation: minParticipation,
2201
- scorecardTimeout: scorecardTimeout,
2202
- timelockDuration: 0
2203
- });
2204
- }
2205
-
2206
- function _launchDataWithSplits(
2207
- uint8 n,
2208
- uint256 tierPrice,
2209
- JBSplit[] memory splits
2210
- )
2211
- internal
2212
- returns (DefifaLaunchProjectData memory)
2213
- {
2214
- return DefifaLaunchProjectData({
2215
- name: "DEFIFA",
2216
- projectUri: "",
2217
- contractUri: "",
2218
- baseUri: "",
2219
- token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
2220
- mintPeriodDuration: 1 days,
2221
- start: uint48(block.timestamp + 3 days),
2222
- refundPeriodDuration: 1 days,
2223
- store: new JB721TiersHookStore(),
2224
- splits: splits,
2225
- attestationStartTime: 0,
2226
- attestationGracePeriod: 100_381,
2227
- defaultAttestationDelegate: address(0),
2228
- // forge-lint: disable-next-line(unsafe-typecast)
2229
- tierPrice: uint104(tierPrice),
2230
- tiers: _makeTierParams(n),
2231
- defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
2232
- terminal: jbMultiTerminal(),
2233
- minParticipation: 0,
2234
- scorecardTimeout: 0,
2235
- timelockDuration: 0
2236
- });
2237
- }
2238
-
2239
- function _makeTierParams(uint8 n) internal pure returns (DefifaTierParams[] memory tp) {
2240
- tp = new DefifaTierParams[](n);
2241
- for (uint256 i; i < n; i++) {
2242
- tp[i] = DefifaTierParams({
2243
- reservedRate: 1001,
2244
- reservedTokenBeneficiary: address(0),
2245
- encodedIPFSUri: bytes32(0),
2246
- shouldUseReservedTokenBeneficiaryAsDefault: false,
2247
- name: "DEFIFA"
2248
- });
2249
- }
2250
- }
2251
-
2252
- function _launch(DefifaLaunchProjectData memory d) internal returns (uint256 p, DefifaHook n, DefifaGovernor g) {
2253
- g = governor;
2254
- p = deployer.launchGameWith(d);
2255
- JBRuleset memory fc = jbRulesets().currentOf(p);
2256
- if (fc.dataHook() == address(0)) (fc,) = jbRulesets().latestQueuedOf(p);
2257
- n = DefifaHook(fc.dataHook());
2258
- }
2259
-
2260
- function _addr(uint256 i) internal pure returns (address) {
2261
- return address(bytes20(keccak256(abi.encode("fork_user", i))));
2262
- }
2263
-
2264
- function _mint(address user, uint256 tid, uint256 amt) internal {
2265
- vm.deal(user, amt);
2266
- uint16[] memory m = new uint16[](1);
2267
- // forge-lint: disable-next-line(unsafe-typecast)
2268
- m[0] = uint16(tid);
2269
- bytes[] memory data = new bytes[](1);
2270
- data[0] = abi.encode(user, m);
2271
- bytes4[] memory ids = new bytes4[](1);
2272
- ids[0] = metadataHelper().getId("pay", address(hook));
2273
- vm.prank(user);
2274
- jbMultiTerminal().pay{value: amt}(
2275
- _pid, JBConstants.NATIVE_TOKEN, amt, user, 0, "", metadataHelper().createMetadata(ids, data)
2276
- );
2277
- }
2278
-
2279
- function _delegateSelf(address user, uint256 tid) internal {
2280
- DefifaDelegation[] memory dd = new DefifaDelegation[](1);
2281
- dd[0] = DefifaDelegation({delegatee: user, tierId: tid});
2282
- vm.prank(user);
2283
- _nft.setTierDelegatesTo(dd);
2284
- }
2285
-
2286
- function _buildScorecard(uint256 n) internal pure returns (DefifaTierCashOutWeight[] memory sc) {
2287
- sc = new DefifaTierCashOutWeight[](n);
2288
- for (uint256 i; i < n; i++) {
2289
- sc[i].id = i + 1;
2290
- }
2291
- }
2292
-
2293
- function _evenScorecard(uint256 n) internal view returns (DefifaTierCashOutWeight[] memory sc) {
2294
- sc = _buildScorecard(n);
2295
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
2296
- uint256 assigned;
2297
- for (uint256 i; i < n; i++) {
2298
- if (i == n - 1) {
2299
- sc[i].cashOutWeight = tw - assigned;
2300
- } else {
2301
- sc[i].cashOutWeight = tw / n;
2302
- }
2303
- assigned += sc[i].cashOutWeight;
2304
- }
2305
- }
2306
-
2307
- function _attestAndRatify(DefifaTierCashOutWeight[] memory sc) internal {
2308
- uint256 pid = _gov.submitScorecardFor(_gameId, sc);
2309
- _attestAllFor(pid);
2310
- _gov.ratifyScorecardFrom(_gameId, sc);
2311
- vm.warp(_tsReader.timestamp() + 1);
2312
- }
2313
-
2314
- function _attestAllFor(uint256 pid) internal {
2315
- // attestationStartTimeOf returns an absolute timestamp; on fork it may already be in the past.
2316
- uint256 attestStart = _gov.attestationStartTimeOf(_gameId);
2317
- uint256 current = _tsReader.timestamp();
2318
- vm.warp((attestStart > current ? attestStart : current) + 1);
2319
- for (uint256 i; i < _users.length; i++) {
2320
- vm.prank(_users[i]);
2321
- try _gov.attestToScorecardFrom(_gameId, pid) {} catch {}
2322
- }
2323
- vm.warp(_tsReader.timestamp() + _gov.attestationGracePeriodOf(_gameId) + 1);
2324
- }
2325
-
2326
- function _surplus() internal view returns (uint256) {
2327
- return jbMultiTerminal().currentSurplusOf(_pid, new address[](0), 18, JBCurrencyIds.ETH);
2328
- }
2329
-
2330
- function _balance() internal view returns (uint256) {
2331
- return jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), _pid, JBConstants.NATIVE_TOKEN);
2332
- }
2333
-
2334
- function _cashOut(address user, uint256 tid, uint256 tnum) internal {
2335
- bytes memory meta = _cashOutMeta(tid, tnum);
2336
- vm.prank(user);
2337
- JBMultiTerminal(address(jbMultiTerminal()))
2338
- .cashOutTokensOf({
2339
- holder: user,
2340
- projectId: _pid,
2341
- cashOutCount: 0,
2342
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
2343
- minTokensReclaimed: 0,
2344
- beneficiary: payable(user),
2345
- metadata: meta
2346
- });
2347
- }
2348
-
2349
- function _cashOutMeta(uint256 tid, uint256 tnum) internal view returns (bytes memory) {
2350
- uint256[] memory cid = new uint256[](1);
2351
- cid[0] = (tid * 1_000_000_000) + tnum;
2352
- bytes[] memory data = new bytes[](1);
2353
- data[0] = abi.encode(cid);
2354
- bytes4[] memory ids = new bytes4[](1);
2355
- ids[0] = metadataHelper().getId("cashOut", address(hook));
2356
- return metadataHelper().createMetadata(ids, data);
2357
- }
2358
-
2359
- function _cashOutAllUsers() internal returns (uint256 total) {
2360
- for (uint256 i; i < _users.length; i++) {
2361
- uint256 bb = _users[i].balance;
2362
- _cashOut(_users[i], i + 1, 1);
2363
- total += _users[i].balance - bb;
2364
- }
2365
- }
2366
-
2367
- function _refund(address user, uint256 tid) internal {
2368
- JB721Tier memory tier = _nft.store().tierOf(address(_nft), tid, false);
2369
- uint256 nb = _nft.store().numberOfBurnedFor(address(_nft), tid);
2370
- uint256 tnum = tier.initialSupply - tier.remainingSupply + nb;
2371
- bytes memory meta = _cashOutMeta(tid, tnum);
2372
- vm.prank(user);
2373
- JBMultiTerminal(address(jbMultiTerminal()))
2374
- .cashOutTokensOf({
2375
- holder: user,
2376
- projectId: _pid,
2377
- cashOutCount: 0,
2378
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
2379
- minTokensReclaimed: 0,
2380
- beneficiary: payable(user),
2381
- metadata: meta
2382
- });
2383
- }
2384
-
2385
- function _generateTokenId(uint256 _tierId, uint256 _tokenNumber) internal pure returns (uint256) {
2386
- return (_tierId * 1_000_000_000) + _tokenNumber;
2387
- }
2388
- }