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