@ballkidz/defifa 0.0.1

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 (59) hide show
  1. package/.gas-snapshot +2 -0
  2. package/CRYPTO_ECON.md +955 -0
  3. package/CRYPTO_ECON.pdf +0 -0
  4. package/CRYPTO_ECON.tex +800 -0
  5. package/README.md +119 -0
  6. package/SKILLS.md +177 -0
  7. package/deployments/defifa-v5/arbitrum_sepolia/DefifaDelegate.json +4867 -0
  8. package/deployments/defifa-v5/arbitrum_sepolia/DefifaDeployer.json +1719 -0
  9. package/deployments/defifa-v5/arbitrum_sepolia/DefifaGovernor.json +1535 -0
  10. package/deployments/defifa-v5/arbitrum_sepolia/DefifaTokenUriResolver.json +295 -0
  11. package/deployments/defifa-v5/base_sepolia/DefifaDelegate.json +4875 -0
  12. package/deployments/defifa-v5/base_sepolia/DefifaDeployer.json +1725 -0
  13. package/deployments/defifa-v5/base_sepolia/DefifaGovernor.json +1543 -0
  14. package/deployments/defifa-v5/base_sepolia/DefifaTokenUriResolver.json +301 -0
  15. package/deployments/defifa-v5/optimism_sepolia/DefifaDelegate.json +4875 -0
  16. package/deployments/defifa-v5/optimism_sepolia/DefifaDeployer.json +1725 -0
  17. package/deployments/defifa-v5/optimism_sepolia/DefifaGovernor.json +1543 -0
  18. package/deployments/defifa-v5/optimism_sepolia/DefifaTokenUriResolver.json +301 -0
  19. package/deployments/defifa-v5/sepolia/DefifaDelegate.json +4875 -0
  20. package/deployments/defifa-v5/sepolia/DefifaDeployer.json +1725 -0
  21. package/deployments/defifa-v5/sepolia/DefifaGovernor.json +1543 -0
  22. package/deployments/defifa-v5/sepolia/DefifaTokenUriResolver.json +301 -0
  23. package/foundry.lock +17 -0
  24. package/foundry.toml +35 -0
  25. package/package.json +33 -0
  26. package/remappings.txt +6 -0
  27. package/script/Deploy.s.sol +109 -0
  28. package/script/helpers/DefifaDeploymentLib.sol +83 -0
  29. package/slither-ci.config.json +10 -0
  30. package/sphinx.lock +521 -0
  31. package/src/DefifaDeployer.sol +894 -0
  32. package/src/DefifaGovernor.sol +490 -0
  33. package/src/DefifaHook.sol +1056 -0
  34. package/src/DefifaProjectOwner.sol +63 -0
  35. package/src/DefifaTokenUriResolver.sol +312 -0
  36. package/src/enums/DefifaGamePhase.sol +11 -0
  37. package/src/enums/DefifaScorecardState.sol +10 -0
  38. package/src/interfaces/IDefifaDeployer.sol +108 -0
  39. package/src/interfaces/IDefifaGamePhaseReporter.sol +8 -0
  40. package/src/interfaces/IDefifaGamePotReporter.sol +8 -0
  41. package/src/interfaces/IDefifaGovernor.sol +132 -0
  42. package/src/interfaces/IDefifaHook.sol +228 -0
  43. package/src/interfaces/IDefifaTokenUriResolver.sol +10 -0
  44. package/src/libraries/DefifaFontImporter.sol +19 -0
  45. package/src/libraries/DefifaHookLib.sol +358 -0
  46. package/src/structs/DefifaAttestations.sol +9 -0
  47. package/src/structs/DefifaDelegation.sol +9 -0
  48. package/src/structs/DefifaLaunchProjectData.sol +59 -0
  49. package/src/structs/DefifaOpsData.sol +20 -0
  50. package/src/structs/DefifaScorecard.sol +9 -0
  51. package/src/structs/DefifaTierCashOutWeight.sol +9 -0
  52. package/src/structs/DefifaTierParams.sol +16 -0
  53. package/test/DefifaFeeAccounting.t.sol +559 -0
  54. package/test/DefifaGovernor.t.sol +1333 -0
  55. package/test/DefifaMintCostInvariant.t.sol +299 -0
  56. package/test/DefifaNoContest.t.sol +922 -0
  57. package/test/DefifaSecurity.t.sol +717 -0
  58. package/test/SVG.t.sol +164 -0
  59. package/test/deployScript.t.sol +144 -0
@@ -0,0 +1,922 @@
1
+ // SPDX-License-Identifier: UNLICENSED
2
+ pragma solidity 0.8.26;
3
+
4
+ import "forge-std/Test.sol";
5
+ import "../src/DefifaGovernor.sol";
6
+ import "../src/DefifaDeployer.sol";
7
+ import "../src/DefifaHook.sol";
8
+ import "../src/DefifaTokenUriResolver.sol";
9
+ import "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
10
+
11
+ import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
12
+ import {MetadataResolverHelper} from "@bananapus/core-v6/test/helpers/MetadataResolverHelper.sol";
13
+ import "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
14
+ import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
15
+ import "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
16
+ import "@bananapus/721-hook-v6/src/libraries/JB721TiersRulesetMetadataResolver.sol";
17
+ import "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
18
+
19
+ /// @title DefifaNoContestTest
20
+ /// @notice Tests for the NO_CONTEST safety mechanisms: minParticipation threshold and scorecardTimeout.
21
+ contract DefifaNoContestTest is JBTest, TestBaseWorkflow {
22
+ using JBRulesetMetadataResolver for JBRuleset;
23
+
24
+ address _protocolFeeProjectTokenAccount;
25
+ address _defifaProjectTokenAccount;
26
+ uint256 _protocolFeeProjectId;
27
+ uint256 _defifaProjectId;
28
+ uint256 _gameId = 3;
29
+
30
+ DefifaDeployer deployer;
31
+ DefifaHook hook;
32
+ DefifaGovernor governor;
33
+ address projectOwner = address(bytes20(keccak256("projectOwner")));
34
+
35
+ // Shared test state
36
+ uint256 _pid;
37
+ DefifaHook _nft;
38
+ DefifaGovernor _gov;
39
+ address[] _users;
40
+
41
+ function setUp() public virtual override {
42
+ super.setUp();
43
+
44
+ JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
45
+ _tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
46
+ JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
47
+ tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
48
+ JBRulesetConfig[] memory rc = new JBRulesetConfig[](1);
49
+ rc[0] = JBRulesetConfig({
50
+ mustStartAtOrAfter: 0,
51
+ duration: 10 days,
52
+ weight: 1e18,
53
+ weightCutPercent: 0,
54
+ approvalHook: IJBRulesetApprovalHook(address(0)),
55
+ metadata: JBRulesetMetadata({
56
+ reservedPercent: 0,
57
+ cashOutTaxRate: 0,
58
+ baseCurrency: JBCurrencyIds.ETH,
59
+ pausePay: false,
60
+ pauseCreditTransfers: false,
61
+ allowOwnerMinting: false,
62
+ allowSetCustomToken: false,
63
+ allowTerminalMigration: false,
64
+ allowSetTerminals: false,
65
+ allowSetController: false,
66
+ allowAddAccountingContext: false,
67
+ allowAddPriceFeed: false,
68
+ ownerMustSendPayouts: false,
69
+ holdFees: false,
70
+ useTotalSurplusForCashOuts: false,
71
+ useDataHookForPay: true,
72
+ useDataHookForCashOut: true,
73
+ dataHook: address(0),
74
+ metadata: 0
75
+ }),
76
+ splitGroups: new JBSplitGroup[](0),
77
+ fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
78
+ });
79
+
80
+ _protocolFeeProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
81
+ vm.prank(projectOwner);
82
+ _protocolFeeProjectTokenAccount =
83
+ address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
84
+ _defifaProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
85
+ vm.prank(projectOwner);
86
+ _defifaProjectTokenAccount =
87
+ address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
88
+
89
+ hook =
90
+ new DefifaHook(jbDirectory(), IERC20(_defifaProjectTokenAccount), IERC20(_protocolFeeProjectTokenAccount));
91
+ governor = new DefifaGovernor(jbController(), address(this));
92
+ deployer = new DefifaDeployer(
93
+ address(hook),
94
+ new DefifaTokenUriResolver(ITypeface(address(0))),
95
+ governor,
96
+ jbController(),
97
+ new JBAddressRegistry(),
98
+ _protocolFeeProjectId,
99
+ _defifaProjectId
100
+ );
101
+ hook.transferOwnership(address(deployer));
102
+ governor.transferOwnership(address(deployer));
103
+ }
104
+
105
+ // =========================================================================
106
+ // MIN PARTICIPATION THRESHOLD
107
+ // =========================================================================
108
+
109
+ /// @notice Game with balance below minParticipation returns NO_CONTEST.
110
+ function testMinParticipation_belowThreshold_noContest() external {
111
+ // Set threshold to 5 ETH, but only mint 1 ETH total
112
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 5 ether, 0);
113
+ (_pid, _nft, _gov) = _launch(d);
114
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
115
+
116
+ // Mint 1 token at 1 ETH — pot = 1 ETH < 5 ETH threshold
117
+ _users = new address[](1);
118
+ _users[0] = _addr(0);
119
+ _mint(_users[0], 1, 1 ether);
120
+
121
+ // Advance to scoring phase
122
+ _toScoring();
123
+
124
+ // Should be NO_CONTEST, not SCORING
125
+ assertEq(
126
+ uint256(deployer.currentGamePhaseOf(_pid)),
127
+ uint256(DefifaGamePhase.NO_CONTEST),
128
+ "phase should be NO_CONTEST"
129
+ );
130
+ }
131
+
132
+ /// @notice Game with balance at or above minParticipation proceeds to SCORING.
133
+ function testMinParticipation_atThreshold_scoring() external {
134
+ // Set threshold to 4 ETH, mint exactly 4 ETH
135
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 4 ether, 0);
136
+ (_pid, _nft, _gov) = _launch(d);
137
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
138
+
139
+ _users = new address[](4);
140
+ for (uint256 i; i < 4; i++) {
141
+ _users[i] = _addr(i);
142
+ _mint(_users[i], i + 1, 1 ether);
143
+ _delegateSelf(_users[i], i + 1);
144
+ vm.warp(block.timestamp + 1);
145
+ }
146
+
147
+ _toScoring();
148
+
149
+ // Balance = 4 ETH >= 4 ETH threshold → SCORING
150
+ assertEq(
151
+ uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.SCORING), "phase should be SCORING"
152
+ );
153
+ }
154
+
155
+ /// @notice Cash-out during NO_CONTEST (from threshold) returns mint price after triggering.
156
+ function testMinParticipation_cashOutReturnsMintPrice() external {
157
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 10 ether, 0);
158
+ (_pid, _nft, _gov) = _launch(d);
159
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
160
+
161
+ _users = new address[](2);
162
+ _users[0] = _addr(0);
163
+ _users[1] = _addr(1);
164
+ _mint(_users[0], 1, 1 ether);
165
+ _mint(_users[1], 2, 1 ether);
166
+
167
+ _toScoring();
168
+
169
+ // Confirm NO_CONTEST
170
+ assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
171
+
172
+ // Trigger no-contest to queue a ruleset without payout limits (anyone can call this)
173
+ deployer.triggerNoContestFor(_pid);
174
+
175
+ // Still NO_CONTEST after trigger
176
+ assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
177
+
178
+ // Cash out should return exactly 1 ETH (mint price)
179
+ uint256 balBefore = _users[0].balance;
180
+ _refund(_users[0], 1);
181
+ uint256 received = _users[0].balance - balBefore;
182
+ assertEq(received, 1 ether, "should receive exact mint price");
183
+ assertEq(_nft.balanceOf(_users[0]), 0, "NFT should be burned");
184
+ }
185
+
186
+ /// @notice Refunds during REFUND phase can push balance below threshold, triggering NO_CONTEST when SCORING starts.
187
+ function testMinParticipation_refundPushesBelow() external {
188
+ // 4 tiers at 1 ETH, threshold 3 ETH
189
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 3 ether, 0);
190
+ (_pid, _nft, _gov) = _launch(d);
191
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
192
+
193
+ _users = new address[](4);
194
+ for (uint256 i; i < 4; i++) {
195
+ _users[i] = _addr(i);
196
+ _mint(_users[i], i + 1, 1 ether);
197
+ _delegateSelf(_users[i], i + 1);
198
+ vm.warp(block.timestamp + 1);
199
+ }
200
+
201
+ // During MINT, pot = 4 ETH > 3 ETH threshold
202
+ // Refund 2 users during MINT phase (balance drops to 2 ETH)
203
+ _refund(_users[2], 3);
204
+ _refund(_users[3], 4);
205
+
206
+ _toScoring();
207
+
208
+ // Now balance = 2 ETH < 3 ETH threshold → NO_CONTEST
209
+ assertEq(
210
+ uint256(deployer.currentGamePhaseOf(_pid)),
211
+ uint256(DefifaGamePhase.NO_CONTEST),
212
+ "refunds push below threshold"
213
+ );
214
+ }
215
+
216
+ /// @notice minParticipation = 0 means the check is disabled.
217
+ function testMinParticipation_zeroDisabled() external {
218
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 0);
219
+ (_pid, _nft, _gov) = _launch(d);
220
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
221
+
222
+ // Mint only 1 token (very low participation)
223
+ _users = new address[](1);
224
+ _users[0] = _addr(0);
225
+ _mint(_users[0], 1, 1 ether);
226
+ _delegateSelf(_users[0], 1);
227
+
228
+ _toScoring();
229
+
230
+ // With threshold = 0, game proceeds to SCORING regardless
231
+ assertEq(
232
+ uint256(deployer.currentGamePhaseOf(_pid)),
233
+ uint256(DefifaGamePhase.SCORING),
234
+ "should be SCORING when threshold disabled"
235
+ );
236
+ }
237
+
238
+ // =========================================================================
239
+ // SCORECARD TIMEOUT
240
+ // =========================================================================
241
+
242
+ /// @notice Game enters NO_CONTEST after scorecardTimeout elapses without ratification.
243
+ function testScorecardTimeout_elapsed_noContest() external {
244
+ // 30-day timeout
245
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 30 days);
246
+ (_pid, _nft, _gov) = _launch(d);
247
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
248
+
249
+ _users = new address[](4);
250
+ for (uint256 i; i < 4; i++) {
251
+ _users[i] = _addr(i);
252
+ _mint(_users[i], i + 1, 1 ether);
253
+ _delegateSelf(_users[i], i + 1);
254
+ vm.warp(block.timestamp + 1);
255
+ }
256
+
257
+ _toScoring();
258
+
259
+ // Still within timeout → SCORING
260
+ assertEq(
261
+ uint256(deployer.currentGamePhaseOf(_pid)),
262
+ uint256(DefifaGamePhase.SCORING),
263
+ "should be SCORING before timeout"
264
+ );
265
+
266
+ // Warp past the timeout
267
+ vm.warp(block.timestamp + 30 days + 1);
268
+
269
+ // Now NO_CONTEST
270
+ assertEq(
271
+ uint256(deployer.currentGamePhaseOf(_pid)),
272
+ uint256(DefifaGamePhase.NO_CONTEST),
273
+ "should be NO_CONTEST after timeout"
274
+ );
275
+ }
276
+
277
+ /// @notice Game at exactly the timeout boundary is still SCORING.
278
+ function testScorecardTimeout_exactBoundary_scoring() external {
279
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 30 days);
280
+ (_pid, _nft, _gov) = _launch(d);
281
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
282
+
283
+ _users = new address[](4);
284
+ for (uint256 i; i < 4; i++) {
285
+ _users[i] = _addr(i);
286
+ _mint(_users[i], i + 1, 1 ether);
287
+ _delegateSelf(_users[i], i + 1);
288
+ vm.warp(block.timestamp + 1);
289
+ }
290
+
291
+ // Advance exactly to scoring start
292
+ vm.warp(d.start);
293
+
294
+ // At exactly scoringStart + timeout, block.timestamp == start + timeout, so NOT >
295
+ vm.warp(d.start + 30 days);
296
+ assertEq(
297
+ uint256(deployer.currentGamePhaseOf(_pid)),
298
+ uint256(DefifaGamePhase.SCORING),
299
+ "should be SCORING at exact boundary"
300
+ );
301
+
302
+ // One second later → NO_CONTEST
303
+ vm.warp(d.start + 30 days + 1);
304
+ assertEq(
305
+ uint256(deployer.currentGamePhaseOf(_pid)),
306
+ uint256(DefifaGamePhase.NO_CONTEST),
307
+ "should be NO_CONTEST one second after"
308
+ );
309
+ }
310
+
311
+ /// @notice Cash-out during NO_CONTEST (from timeout) returns mint price after triggering.
312
+ function testScorecardTimeout_cashOutReturnsMintPrice() external {
313
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 7 days);
314
+ (_pid, _nft, _gov) = _launch(d);
315
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
316
+
317
+ _users = new address[](4);
318
+ for (uint256 i; i < 4; i++) {
319
+ _users[i] = _addr(i);
320
+ _mint(_users[i], i + 1, 1 ether);
321
+ _delegateSelf(_users[i], i + 1);
322
+ vm.warp(block.timestamp + 1);
323
+ }
324
+
325
+ // Warp past scoring start + timeout
326
+ vm.warp(d.start + 7 days + 1);
327
+
328
+ // Confirm NO_CONTEST
329
+ assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
330
+
331
+ // Trigger no-contest to unlock refunds
332
+ deployer.triggerNoContestFor(_pid);
333
+
334
+ // Cash out all users — each should get 1 ETH back
335
+ for (uint256 i; i < 4; i++) {
336
+ uint256 balBefore = _users[i].balance;
337
+ _refund(_users[i], i + 1);
338
+ uint256 received = _users[i].balance - balBefore;
339
+ assertEq(received, 1 ether, "should receive exact mint price");
340
+ assertEq(_nft.balanceOf(_users[i]), 0, "NFT should be burned");
341
+ }
342
+ }
343
+
344
+ /// @notice scorecardTimeout = 0 means the check is disabled.
345
+ function testScorecardTimeout_zeroDisabled() external {
346
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 0);
347
+ (_pid, _nft, _gov) = _launch(d);
348
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
349
+
350
+ _users = new address[](4);
351
+ for (uint256 i; i < 4; i++) {
352
+ _users[i] = _addr(i);
353
+ _mint(_users[i], i + 1, 1 ether);
354
+ _delegateSelf(_users[i], i + 1);
355
+ vm.warp(block.timestamp + 1);
356
+ }
357
+
358
+ _toScoring();
359
+
360
+ // Warp very far forward (1 year) — should still be SCORING
361
+ vm.warp(block.timestamp + 365 days);
362
+ assertEq(
363
+ uint256(deployer.currentGamePhaseOf(_pid)),
364
+ uint256(DefifaGamePhase.SCORING),
365
+ "should be SCORING forever when timeout disabled"
366
+ );
367
+ }
368
+
369
+ // =========================================================================
370
+ // SCORECARD BLOCKED DURING NO_CONTEST
371
+ // =========================================================================
372
+
373
+ /// @notice setTierCashOutWeightsTo reverts during NO_CONTEST (requires SCORING).
374
+ function testNoContest_scorecardBlocked() external {
375
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 7 days);
376
+ (_pid, _nft, _gov) = _launch(d);
377
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
378
+
379
+ _users = new address[](4);
380
+ for (uint256 i; i < 4; i++) {
381
+ _users[i] = _addr(i);
382
+ _mint(_users[i], i + 1, 1 ether);
383
+ _delegateSelf(_users[i], i + 1);
384
+ vm.warp(block.timestamp + 1);
385
+ }
386
+
387
+ // Submit and attest to a scorecard while still in SCORING
388
+ _toScoring();
389
+ DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
390
+ for (uint256 i; i < 4; i++) {
391
+ sc[i].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() / 4;
392
+ }
393
+ uint256 pid = _gov.submitScorecardFor(_gameId, sc);
394
+ _attestAllFor(pid);
395
+
396
+ // Now warp past timeout → NO_CONTEST
397
+ vm.warp(d.start + 7 days + 1);
398
+ assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
399
+
400
+ // Attempting to ratify should revert because setTierCashOutWeightsTo checks for SCORING phase
401
+ vm.expectRevert(DefifaHook.DefifaHook_GameIsntScoringYet.selector);
402
+ _gov.ratifyScorecardFrom(_gameId, sc);
403
+ }
404
+
405
+ // =========================================================================
406
+ // RATIFICATION BEFORE TIMEOUT PREVENTS NO_CONTEST
407
+ // =========================================================================
408
+
409
+ /// @notice If scorecard is ratified before timeout, game becomes COMPLETE (not NO_CONTEST).
410
+ function testScorecardTimeout_ratifiedBeforeTimeout_complete() external {
411
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 30 days);
412
+ (_pid, _nft, _gov) = _launch(d);
413
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
414
+
415
+ _users = new address[](4);
416
+ for (uint256 i; i < 4; i++) {
417
+ _users[i] = _addr(i);
418
+ _mint(_users[i], i + 1, 1 ether);
419
+ _delegateSelf(_users[i], i + 1);
420
+ vm.warp(block.timestamp + 1);
421
+ }
422
+
423
+ _toScoring();
424
+
425
+ // Ratify a scorecard before timeout
426
+ DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
427
+ for (uint256 i; i < 4; i++) {
428
+ sc[i].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() / 4;
429
+ }
430
+ _attestAndRatify(sc);
431
+
432
+ // Should be COMPLETE, not SCORING or NO_CONTEST
433
+ assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.COMPLETE), "should be COMPLETE");
434
+
435
+ // Even after timeout elapses, stays COMPLETE (cashOutWeightIsSet is checked first)
436
+ vm.warp(d.start + 30 days + 1);
437
+ assertEq(
438
+ uint256(deployer.currentGamePhaseOf(_pid)),
439
+ uint256(DefifaGamePhase.COMPLETE),
440
+ "should stay COMPLETE after timeout"
441
+ );
442
+ }
443
+
444
+ // =========================================================================
445
+ // BOTH MECHANISMS COMBINED
446
+ // =========================================================================
447
+
448
+ /// @notice When both are set, minParticipation triggers first if balance is low.
449
+ function testBothMechanisms_thresholdTriggersFirst() external {
450
+ // Threshold: 10 ETH, Timeout: 90 days — but only mint 1 ETH
451
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 10 ether, uint32(90 days));
452
+ (_pid, _nft, _gov) = _launch(d);
453
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
454
+
455
+ _users = new address[](1);
456
+ _users[0] = _addr(0);
457
+ _mint(_users[0], 1, 1 ether);
458
+
459
+ _toScoring();
460
+
461
+ // Threshold triggers NO_CONTEST immediately (no need to wait for timeout)
462
+ assertEq(
463
+ uint256(deployer.currentGamePhaseOf(_pid)),
464
+ uint256(DefifaGamePhase.NO_CONTEST),
465
+ "threshold should trigger NO_CONTEST"
466
+ );
467
+ }
468
+
469
+ /// @notice When both set and balance is above threshold, timeout triggers eventually.
470
+ function testBothMechanisms_timeoutTriggersIfThresholdMet() external {
471
+ // Threshold: 2 ETH, Timeout: 7 days — mint 4 ETH
472
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 2 ether, uint32(7 days));
473
+ (_pid, _nft, _gov) = _launch(d);
474
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
475
+
476
+ _users = new address[](4);
477
+ for (uint256 i; i < 4; i++) {
478
+ _users[i] = _addr(i);
479
+ _mint(_users[i], i + 1, 1 ether);
480
+ _delegateSelf(_users[i], i + 1);
481
+ vm.warp(block.timestamp + 1);
482
+ }
483
+
484
+ _toScoring();
485
+
486
+ // Balance = 4 ETH > 2 ETH threshold → SCORING
487
+ assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.SCORING), "should be SCORING");
488
+
489
+ // After timeout → NO_CONTEST
490
+ vm.warp(d.start + 7 days + 1);
491
+ assertEq(
492
+ uint256(deployer.currentGamePhaseOf(_pid)),
493
+ uint256(DefifaGamePhase.NO_CONTEST),
494
+ "timeout should trigger NO_CONTEST"
495
+ );
496
+ }
497
+
498
+ // =========================================================================
499
+ // BACKWARD COMPATIBILITY
500
+ // =========================================================================
501
+
502
+ /// @notice Both mechanisms disabled (0) — game functions exactly as before.
503
+ function testBackwardCompat_noSafetyMechanisms() external {
504
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 0);
505
+ (_pid, _nft, _gov) = _launch(d);
506
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
507
+
508
+ _users = new address[](4);
509
+ for (uint256 i; i < 4; i++) {
510
+ _users[i] = _addr(i);
511
+ _mint(_users[i], i + 1, 1 ether);
512
+ _delegateSelf(_users[i], i + 1);
513
+ vm.warp(block.timestamp + 1);
514
+ }
515
+
516
+ _toScoring();
517
+ assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.SCORING));
518
+
519
+ // Full lifecycle: submit scorecard, attest, ratify, cash out
520
+ DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
521
+ for (uint256 i; i < 4; i++) {
522
+ sc[i].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() / 4;
523
+ }
524
+ _attestAndRatify(sc);
525
+
526
+ assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.COMPLETE));
527
+
528
+ // Cash out all users
529
+ uint256 totalOut;
530
+ for (uint256 i; i < 4; i++) {
531
+ uint256 bb = _users[i].balance;
532
+ _cashOut(_users[i], i + 1, 1);
533
+ totalOut += _users[i].balance - bb;
534
+ }
535
+ assertGt(totalOut, 0, "should receive ETH");
536
+ }
537
+
538
+ // =========================================================================
539
+ // SAFETY PARAMS VIEW
540
+ // =========================================================================
541
+
542
+ /// @notice safetyParamsOf returns the stored parameters.
543
+ function testSafetyParamsOf() external {
544
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 42 ether, uint32(90 days));
545
+ (_pid, _nft, _gov) = _launch(d);
546
+
547
+ (uint256 minP, uint32 timeout) = deployer.safetyParamsOf(_pid);
548
+ assertEq(minP, 42 ether, "minParticipation should match");
549
+ assertEq(timeout, uint32(90 days), "scorecardTimeout should match");
550
+ }
551
+
552
+ /// @notice safetyParamsOf returns 0s when not set.
553
+ function testSafetyParamsOf_defaults() external {
554
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 0);
555
+ (_pid, _nft, _gov) = _launch(d);
556
+
557
+ (uint256 minP, uint32 timeout) = deployer.safetyParamsOf(_pid);
558
+ assertEq(minP, 0, "default minParticipation should be 0");
559
+ assertEq(timeout, 0, "default scorecardTimeout should be 0");
560
+ }
561
+
562
+ // =========================================================================
563
+ // FUND CONSERVATION DURING NO_CONTEST
564
+ // =========================================================================
565
+
566
+ /// @notice All users can refund at mint price during NO_CONTEST — no funds stuck.
567
+ function testNoContest_allUsersCanRefund() external {
568
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 2 ether, 0, 7 days);
569
+ (_pid, _nft, _gov) = _launch(d);
570
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
571
+
572
+ _users = new address[](4);
573
+ for (uint256 i; i < 4; i++) {
574
+ _users[i] = _addr(i);
575
+ _mint(_users[i], i + 1, 2 ether);
576
+ vm.warp(block.timestamp + 1);
577
+ }
578
+
579
+ // Warp past timeout
580
+ vm.warp(d.start + 7 days + 1);
581
+ assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
582
+
583
+ // Trigger no-contest to unlock refunds
584
+ deployer.triggerNoContestFor(_pid);
585
+
586
+ // All users refund
587
+ uint256 totalRefunded;
588
+ for (uint256 i; i < 4; i++) {
589
+ uint256 bb = _users[i].balance;
590
+ _refund(_users[i], i + 1);
591
+ uint256 received = _users[i].balance - bb;
592
+ assertEq(received, 2 ether, "each user gets exact mint price back");
593
+ totalRefunded += received;
594
+ }
595
+ assertEq(totalRefunded, 8 ether, "total refunded = total minted");
596
+ }
597
+
598
+ // =========================================================================
599
+ // PHASE TRANSITIONS: NO_CONTEST only during SCORING phase window
600
+ // =========================================================================
601
+
602
+ /// @notice During COUNTDOWN, MINT, and REFUND phases, NO_CONTEST is not returned even if threshold/timeout would
603
+ /// trigger.
604
+ function testNoContest_onlyDuringScoringWindow() external {
605
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 100 ether, uint32(1));
606
+ (_pid, _nft, _gov) = _launch(d);
607
+
608
+ // COUNTDOWN
609
+ assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.COUNTDOWN), "should be COUNTDOWN");
610
+
611
+ // MINT
612
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
613
+ assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.MINT), "should be MINT");
614
+
615
+ // REFUND (warp past mint duration)
616
+ vm.warp(d.start - d.refundPeriodDuration);
617
+ assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.REFUND), "should be REFUND");
618
+ }
619
+
620
+ // =========================================================================
621
+ // TRIGGER NO_CONTEST MECHANISM
622
+ // =========================================================================
623
+
624
+ /// @notice triggerNoContestFor reverts when the game is not in NO_CONTEST phase.
625
+ function testTriggerNoContest_revertsWhenNotNoContest() external {
626
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 0);
627
+ (_pid, _nft, _gov) = _launch(d);
628
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
629
+
630
+ _users = new address[](4);
631
+ for (uint256 i; i < 4; i++) {
632
+ _users[i] = _addr(i);
633
+ _mint(_users[i], i + 1, 1 ether);
634
+ _delegateSelf(_users[i], i + 1);
635
+ vm.warp(block.timestamp + 1);
636
+ }
637
+
638
+ _toScoring();
639
+ assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.SCORING));
640
+
641
+ // Should revert since the game is SCORING, not NO_CONTEST
642
+ vm.expectRevert(DefifaDeployer.DefifaDeployer_NotNoContest.selector);
643
+ deployer.triggerNoContestFor(_pid);
644
+ }
645
+
646
+ /// @notice triggerNoContestFor reverts when called twice.
647
+ function testTriggerNoContest_revertsWhenAlreadyTriggered() external {
648
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 7 days);
649
+ (_pid, _nft, _gov) = _launch(d);
650
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
651
+
652
+ _users = new address[](1);
653
+ _users[0] = _addr(0);
654
+ _mint(_users[0], 1, 1 ether);
655
+
656
+ vm.warp(d.start + 7 days + 1);
657
+ assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
658
+
659
+ // First trigger succeeds
660
+ deployer.triggerNoContestFor(_pid);
661
+
662
+ // Second trigger reverts
663
+ vm.expectRevert(DefifaDeployer.DefifaDeployer_NoContestAlreadyTriggered.selector);
664
+ deployer.triggerNoContestFor(_pid);
665
+ }
666
+
667
+ /// @notice Cash-out before triggerNoContestFor reverts with NOTHING_TO_CLAIM (surplus = 0).
668
+ function testNoContest_cashOutBeforeTrigger_reverts() external {
669
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 10 ether, 0);
670
+ (_pid, _nft, _gov) = _launch(d);
671
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
672
+
673
+ _users = new address[](1);
674
+ _users[0] = _addr(0);
675
+ _mint(_users[0], 1, 1 ether);
676
+
677
+ _toScoring();
678
+ assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.NO_CONTEST));
679
+
680
+ // Build the cash-out metadata inline so vm.expectRevert is right before the terminal call
681
+ uint256[] memory cid = new uint256[](1);
682
+ JB721Tier memory tier = _nft.store().tierOf(address(_nft), 1, false);
683
+ uint256 nb = _nft.store().numberOfBurnedFor(address(_nft), 1);
684
+ uint256 tnum = tier.initialSupply - tier.remainingSupply + nb;
685
+ cid[0] = (1 * 1_000_000_000) + tnum;
686
+ bytes[] memory data = new bytes[](1);
687
+ data[0] = abi.encode(cid);
688
+ bytes4[] memory ids = new bytes4[](1);
689
+ ids[0] = metadataHelper().getId("cashOut", address(hook));
690
+ bytes memory meta = metadataHelper().createMetadata(ids, data);
691
+
692
+ // Cash out should revert before trigger (surplus = 0 on SCORING ruleset)
693
+ vm.prank(_users[0]);
694
+ vm.expectRevert(DefifaHook.DefifaHook_NothingToClaim.selector);
695
+ JBMultiTerminal(address(jbMultiTerminal()))
696
+ .cashOutTokensOf({
697
+ holder: _users[0],
698
+ projectId: _pid,
699
+ cashOutCount: 0,
700
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
701
+ minTokensReclaimed: 0,
702
+ beneficiary: payable(_users[0]),
703
+ metadata: meta
704
+ });
705
+ }
706
+
707
+ // =========================================================================
708
+ // RATIFIED SCORECARD: PREVENTS NO_CONTEST FOREVER + CASH-OUTS WORK FOREVER
709
+ // =========================================================================
710
+
711
+ /// @notice After scorecard ratification and commitment fulfillment, cash-outs work at ratified values
712
+ /// even long after the timeout would have elapsed. NO_CONTEST never occurs.
713
+ function testRatifiedScorecard_cashOutsWorkForever() external {
714
+ // Set a 7-day timeout, but we'll ratify before it
715
+ DefifaLaunchProjectData memory d = _launchDataWith(4, 1 ether, 0, 7 days);
716
+ (_pid, _nft, _gov) = _launch(d);
717
+ vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
718
+
719
+ _users = new address[](4);
720
+ for (uint256 i; i < 4; i++) {
721
+ _users[i] = _addr(i);
722
+ _mint(_users[i], i + 1, 1 ether);
723
+ _delegateSelf(_users[i], i + 1);
724
+ vm.warp(block.timestamp + 1);
725
+ }
726
+
727
+ _toScoring();
728
+ assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.SCORING));
729
+
730
+ // Ratify scorecard: equal distribution (25% each)
731
+ DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
732
+ for (uint256 i; i < 4; i++) {
733
+ sc[i].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() / 4;
734
+ }
735
+ _attestAndRatify(sc);
736
+
737
+ // Should be COMPLETE
738
+ assertEq(uint256(deployer.currentGamePhaseOf(_pid)), uint256(DefifaGamePhase.COMPLETE));
739
+
740
+ // Fulfill commitments (sends payouts and queues final ruleset)
741
+ deployer.fulfillCommitmentsOf(_pid);
742
+
743
+ // Warp far past the timeout (1 year) — should STILL be COMPLETE, never NO_CONTEST
744
+ vm.warp(block.timestamp + 365 days);
745
+ assertEq(
746
+ uint256(deployer.currentGamePhaseOf(_pid)),
747
+ uint256(DefifaGamePhase.COMPLETE),
748
+ "should stay COMPLETE forever"
749
+ );
750
+
751
+ // triggerNoContestFor should revert since we're COMPLETE, not NO_CONTEST
752
+ vm.expectRevert(DefifaDeployer.DefifaDeployer_NotNoContest.selector);
753
+ deployer.triggerNoContestFor(_pid);
754
+
755
+ // Cash out user 0 — should receive their share (approximately 1 ETH minus fees)
756
+ uint256 balBefore = _users[0].balance;
757
+ _cashOut(_users[0], 1, 1);
758
+ uint256 received = _users[0].balance - balBefore;
759
+ assertGt(received, 0, "should receive ETH from ratified scorecard");
760
+
761
+ // Cash out user 3 — should also work even after a very long time
762
+ uint256 balBefore3 = _users[3].balance;
763
+ _cashOut(_users[3], 4, 1);
764
+ uint256 received3 = _users[3].balance - balBefore3;
765
+ assertGt(received3, 0, "should still receive ETH long after timeout");
766
+ }
767
+
768
+ // =========================================================================
769
+ // SETUP HELPERS
770
+ // =========================================================================
771
+
772
+ function _launchDataWith(
773
+ uint8 n,
774
+ uint256 tierPrice,
775
+ uint256 minParticipation,
776
+ uint32 scorecardTimeout
777
+ )
778
+ internal
779
+ returns (DefifaLaunchProjectData memory)
780
+ {
781
+ DefifaTierParams[] memory tp = new DefifaTierParams[](n);
782
+ for (uint256 i; i < n; i++) {
783
+ tp[i] = DefifaTierParams({
784
+ reservedRate: 1001,
785
+ reservedTokenBeneficiary: address(0),
786
+ encodedIPFSUri: bytes32(0),
787
+ shouldUseReservedTokenBeneficiaryAsDefault: false,
788
+ name: "DEFIFA"
789
+ });
790
+ }
791
+ return DefifaLaunchProjectData({
792
+ name: "DEFIFA",
793
+ projectUri: "",
794
+ contractUri: "",
795
+ baseUri: "",
796
+ token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
797
+ mintPeriodDuration: 1 days,
798
+ start: uint48(block.timestamp + 3 days),
799
+ refundPeriodDuration: 1 days,
800
+ store: new JB721TiersHookStore(),
801
+ splits: new JBSplit[](0),
802
+ attestationStartTime: 0,
803
+ attestationGracePeriod: 100_381,
804
+ defaultAttestationDelegate: address(0),
805
+ tierPrice: uint104(tierPrice),
806
+ tiers: tp,
807
+ defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
808
+ terminal: jbMultiTerminal(),
809
+ minParticipation: minParticipation,
810
+ scorecardTimeout: scorecardTimeout
811
+ });
812
+ }
813
+
814
+ function _toScoring() internal {
815
+ vm.warp(block.timestamp + 3 days + 1);
816
+ }
817
+
818
+ function _launch(DefifaLaunchProjectData memory d) internal returns (uint256 p, DefifaHook n, DefifaGovernor g) {
819
+ g = governor;
820
+ p = deployer.launchGameWith(d);
821
+ JBRuleset memory fc = jbRulesets().currentOf(p);
822
+ if (fc.dataHook() == address(0)) (fc,) = jbRulesets().latestQueuedOf(p);
823
+ n = DefifaHook(fc.dataHook());
824
+ }
825
+
826
+ function _addr(uint256 i) internal pure returns (address) {
827
+ return address(bytes20(keccak256(abi.encode("su", i))));
828
+ }
829
+
830
+ function _mint(address user, uint256 tid, uint256 amt) internal {
831
+ vm.deal(user, amt);
832
+ uint16[] memory m = new uint16[](1);
833
+ m[0] = uint16(tid);
834
+ bytes[] memory data = new bytes[](1);
835
+ data[0] = abi.encode(user, m);
836
+ bytes4[] memory ids = new bytes4[](1);
837
+ ids[0] = metadataHelper().getId("pay", address(hook));
838
+ vm.prank(user);
839
+ jbMultiTerminal().pay{value: amt}(
840
+ _pid, JBConstants.NATIVE_TOKEN, amt, user, 0, "", metadataHelper().createMetadata(ids, data)
841
+ );
842
+ }
843
+
844
+ function _delegateSelf(address user, uint256 tid) internal {
845
+ DefifaDelegation[] memory dd = new DefifaDelegation[](1);
846
+ dd[0] = DefifaDelegation({delegatee: user, tierId: tid});
847
+ vm.prank(user);
848
+ _nft.setTierDelegatesTo(dd);
849
+ }
850
+
851
+ function _buildScorecard(uint256 n) internal pure returns (DefifaTierCashOutWeight[] memory sc) {
852
+ sc = new DefifaTierCashOutWeight[](n);
853
+ for (uint256 i; i < n; i++) {
854
+ sc[i].id = i + 1;
855
+ }
856
+ }
857
+
858
+ function _attestAndRatify(DefifaTierCashOutWeight[] memory sc) internal {
859
+ uint256 pid = _gov.submitScorecardFor(_gameId, sc);
860
+ _attestAllFor(pid);
861
+ _gov.ratifyScorecardFrom(_gameId, sc);
862
+ vm.warp(block.timestamp + 1);
863
+ }
864
+
865
+ function _attestAllFor(uint256 pid) internal {
866
+ vm.warp(block.timestamp + _gov.attestationStartTimeOf(_gameId) + 1);
867
+ for (uint256 i; i < _users.length; i++) {
868
+ vm.prank(_users[i]);
869
+ _gov.attestToScorecardFrom(_gameId, pid);
870
+ }
871
+ vm.warp(block.timestamp + _gov.attestationGracePeriodOf(_gameId) + 1);
872
+ }
873
+
874
+ function _surplus() internal view returns (uint256) {
875
+ return
876
+ jbMultiTerminal()
877
+ .currentSurplusOf(_pid, jbMultiTerminal().accountingContextsOf(_pid), 18, JBCurrencyIds.ETH);
878
+ }
879
+
880
+ function _cashOut(address user, uint256 tid, uint256 tnum) internal {
881
+ bytes memory meta = _cashOutMeta(tid, tnum);
882
+ vm.prank(user);
883
+ JBMultiTerminal(address(jbMultiTerminal()))
884
+ .cashOutTokensOf({
885
+ holder: user,
886
+ projectId: _pid,
887
+ cashOutCount: 0,
888
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
889
+ minTokensReclaimed: 0,
890
+ beneficiary: payable(user),
891
+ metadata: meta
892
+ });
893
+ }
894
+
895
+ function _cashOutMeta(uint256 tid, uint256 tnum) internal returns (bytes memory) {
896
+ uint256[] memory cid = new uint256[](1);
897
+ cid[0] = (tid * 1_000_000_000) + tnum;
898
+ bytes[] memory data = new bytes[](1);
899
+ data[0] = abi.encode(cid);
900
+ bytes4[] memory ids = new bytes4[](1);
901
+ ids[0] = metadataHelper().getId("cashOut", address(hook));
902
+ return metadataHelper().createMetadata(ids, data);
903
+ }
904
+
905
+ function _refund(address user, uint256 tid) internal {
906
+ JB721Tier memory tier = _nft.store().tierOf(address(_nft), tid, false);
907
+ uint256 nb = _nft.store().numberOfBurnedFor(address(_nft), tid);
908
+ uint256 tnum = tier.initialSupply - tier.remainingSupply + nb;
909
+ bytes memory meta = _cashOutMeta(tid, tnum);
910
+ vm.prank(user);
911
+ JBMultiTerminal(address(jbMultiTerminal()))
912
+ .cashOutTokensOf({
913
+ holder: user,
914
+ projectId: _pid,
915
+ cashOutCount: 0,
916
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
917
+ minTokensReclaimed: 0,
918
+ beneficiary: payable(user),
919
+ metadata: meta
920
+ });
921
+ }
922
+ }