@ballkidz/defifa 0.0.24 → 0.0.26

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 (50) 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 +74 -46
  8. package/src/DefifaGovernor.sol +53 -11
  9. package/src/DefifaHook.sol +79 -25
  10. package/src/DefifaTokenUriResolver.sol +111 -19
  11. package/src/interfaces/IDefifaDeployer.sol +5 -0
  12. package/src/interfaces/IDefifaGovernor.sol +4 -0
  13. package/src/interfaces/IDefifaHook.sol +5 -0
  14. package/src/libraries/DefifaHookLib.sol +9 -10
  15. package/src/structs/DefifaLaunchProjectData.sol +0 -3
  16. package/CRYPTO_ECON.pdf +0 -0
  17. package/CRYPTO_ECON.tex +0 -997
  18. package/foundry.lock +0 -17
  19. package/references/operations.md +0 -32
  20. package/references/runtime.md +0 -43
  21. package/slither-ci.config.json +0 -10
  22. package/sphinx.lock +0 -521
  23. package/test/BWAFunctionComparison.t.sol +0 -1320
  24. package/test/DefifaAdversarialQuorum.t.sol +0 -617
  25. package/test/DefifaAuditLowGuards.t.sol +0 -308
  26. package/test/DefifaFeeAccounting.t.sol +0 -581
  27. package/test/DefifaGovernanceHardening.t.sol +0 -1315
  28. package/test/DefifaGovernor.t.sol +0 -1378
  29. package/test/DefifaHookRegressions.t.sol +0 -415
  30. package/test/DefifaMintCostInvariant.t.sol +0 -319
  31. package/test/DefifaNoContest.t.sol +0 -941
  32. package/test/DefifaSecurity.t.sol +0 -741
  33. package/test/DefifaUSDC.t.sol +0 -480
  34. package/test/Fork.t.sol +0 -2388
  35. package/test/TestAuditGaps.sol +0 -984
  36. package/test/TestQALastMile.t.sol +0 -514
  37. package/test/audit/AttestationDoubleCount.t.sol +0 -218
  38. package/test/audit/CodexNemesisCurrencyMismatchBypass.t.sol +0 -112
  39. package/test/audit/CodexNemesisNoContestReserveDrain.t.sol +0 -238
  40. package/test/audit/CodexRegistryMismatch.t.sol +0 -191
  41. package/test/audit/CodexTierCapMismatch.t.sol +0 -171
  42. package/test/audit/CurrencyMismatchFix.t.sol +0 -265
  43. package/test/audit/FixPendingReserveDilution.t.sol +0 -366
  44. package/test/audit/H5TierCapValidation.t.sol +0 -184
  45. package/test/audit/PendingReserveDilution.t.sol +0 -298
  46. package/test/audit/PendingReserveQuorumGrief.t.sol +0 -355
  47. package/test/audit/PendingReserveSnapshotBypass.t.sol +0 -319
  48. package/test/regression/AttestationDelegateBeneficiary.t.sol +0 -271
  49. package/test/regression/FulfillmentBlocksRatification.t.sol +0 -279
  50. package/test/regression/GracePeriodBypass.t.sol +0 -302
@@ -1,741 +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 {JB721TiersMintReservesConfig} from "@bananapus/721-hook-v6/src/structs/JB721TiersMintReservesConfig.sol";
20
- import {DefifaDelegation} from "../src/structs/DefifaDelegation.sol";
21
- import {DefifaLaunchProjectData} from "../src/structs/DefifaLaunchProjectData.sol";
22
- import {DefifaTierParams} from "../src/structs/DefifaTierParams.sol";
23
- import {DefifaTierCashOutWeight} from "../src/structs/DefifaTierCashOutWeight.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
- /// @dev Helper to read block.timestamp via an external call, bypassing the via-ir optimizer's timestamp caching.
38
- contract TimestampReader {
39
- function timestamp() external view returns (uint256) {
40
- return block.timestamp;
41
- }
42
- }
43
-
44
- /// @title DefifaSecurityTest
45
- /// @notice High-volume game integrity, fund conservation, scorecard validation, and governance tests.
46
- contract DefifaSecurityTest is JBTest, TestBaseWorkflow {
47
- using JBRulesetMetadataResolver for JBRuleset;
48
-
49
- TimestampReader private _tsReader = new TimestampReader();
50
-
51
- address _protocolFeeProjectTokenAccount;
52
- address _defifaProjectTokenAccount;
53
- uint256 _protocolFeeProjectId;
54
- uint256 _defifaProjectId;
55
- uint256 _gameId = 3;
56
-
57
- DefifaDeployer deployer;
58
- DefifaHook hook;
59
- DefifaGovernor governor;
60
- address projectOwner = address(bytes20(keccak256("projectOwner")));
61
-
62
- // Shared test state (set by _setupGame helpers)
63
- uint256 _pid;
64
- DefifaHook _nft;
65
- DefifaGovernor _gov;
66
- address[] _users;
67
-
68
- function setUp() public virtual override {
69
- super.setUp();
70
-
71
- JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
72
- _tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
73
- JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
74
- tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
75
- JBRulesetConfig[] memory rc = new JBRulesetConfig[](1);
76
- rc[0] = JBRulesetConfig({
77
- mustStartAtOrAfter: 0,
78
- duration: 10 days,
79
- weight: 1e18,
80
- weightCutPercent: 0,
81
- approvalHook: IJBRulesetApprovalHook(address(0)),
82
- metadata: JBRulesetMetadata({
83
- reservedPercent: 0,
84
- cashOutTaxRate: 0,
85
- baseCurrency: JBCurrencyIds.ETH,
86
- pausePay: false,
87
- pauseCreditTransfers: false,
88
- allowOwnerMinting: false,
89
- allowSetCustomToken: false,
90
- allowTerminalMigration: false,
91
- allowSetTerminals: false,
92
- allowSetController: false,
93
- allowAddAccountingContext: false,
94
- allowAddPriceFeed: false,
95
- ownerMustSendPayouts: false,
96
- holdFees: false,
97
- useTotalSurplusForCashOuts: false,
98
- useDataHookForPay: true,
99
- useDataHookForCashOut: true,
100
- dataHook: address(0),
101
- metadata: 0
102
- }),
103
- splitGroups: new JBSplitGroup[](0),
104
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
105
- });
106
-
107
- _protocolFeeProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
108
- vm.prank(projectOwner);
109
- _protocolFeeProjectTokenAccount =
110
- address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
111
- _defifaProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
112
- vm.prank(projectOwner);
113
- _defifaProjectTokenAccount =
114
- address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
115
-
116
- hook =
117
- new DefifaHook(jbDirectory(), IERC20(_defifaProjectTokenAccount), IERC20(_protocolFeeProjectTokenAccount));
118
- governor = new DefifaGovernor(jbController(), address(this));
119
- deployer = new DefifaDeployer(
120
- address(hook),
121
- new DefifaTokenUriResolver(ITypeface(address(0))),
122
- governor,
123
- jbController(),
124
- new JBAddressRegistry(),
125
- _protocolFeeProjectId,
126
- _defifaProjectId
127
- );
128
- hook.transferOwnership(address(deployer));
129
- governor.transferOwnership(address(deployer));
130
- }
131
-
132
- // =========================================================================
133
- // HIGH-VOLUME: 32 tiers at 100 ETH each = 3,200 ETH pot
134
- // =========================================================================
135
- function testHighVolume_32tiers() external {
136
- _setupGame(32, 100 ether);
137
- _toScoring();
138
-
139
- // Tier 1 gets 50%, rest split 50% — must sum to exactly TOTAL_CASHOUT_WEIGHT
140
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
141
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(32);
142
- uint256 half = tw / 2;
143
- uint256 perTier = half / 31;
144
- uint256 assigned;
145
- for (uint256 i; i < 32; i++) {
146
- if (i == 0) {
147
- sc[i].cashOutWeight = half;
148
- } else if (i == 31) {
149
- // Last tier absorbs rounding remainder
150
- sc[i].cashOutWeight = tw - assigned;
151
- } else {
152
- sc[i].cashOutWeight = perTier;
153
- }
154
- assigned += sc[i].cashOutWeight;
155
- }
156
-
157
- _attestAndRatify(sc);
158
- uint256 pot = _surplus();
159
- uint256 out = _cashOutAllUsers();
160
-
161
- assertApproxEqAbs(out, pot, 1e15, "cashed out vs pot");
162
- assertLe(_surplus(), 1e15, "remaining dust");
163
- // No fee tokens left in hook
164
- assertEq(IERC20(_protocolFeeProjectTokenAccount).balanceOf(address(_nft)), 0, "no NANA left");
165
- assertEq(IERC20(_defifaProjectTokenAccount).balanceOf(address(_nft)), 0, "no DEFIFA left");
166
- }
167
-
168
- // =========================================================================
169
- // MULTI-PLAYER: 5 users in winning tier, 1 each in losing tiers
170
- // =========================================================================
171
- function testMultiPlayer_winnerTakesAll() external {
172
- _setupMultiPlayer();
173
- _toScoring();
174
-
175
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
176
- sc[0].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT();
177
- // tiers 2-4 get 0
178
-
179
- _attestAndRatify(sc);
180
-
181
- // All winners should get approximately equal shares
182
- uint256[] memory winnerPayouts = new uint256[](5);
183
- for (uint256 i; i < 5; i++) {
184
- uint256 bb = _users[i].balance;
185
- _cashOut(_users[i], 1, i + 1);
186
- winnerPayouts[i] = _users[i].balance - bb;
187
- assertGt(winnerPayouts[i], 0, "winner should receive ETH");
188
- }
189
-
190
- // All winners should get approximately equal amounts (within 0.1%)
191
- for (uint256 i = 1; i < 5; i++) {
192
- assertApproxEqRel(winnerPayouts[i], winnerPayouts[0], 0.001 ether, "winner payouts should be equal");
193
- }
194
-
195
- // Losers get 0 ETH (but still receive fee tokens, so no NOTHING_TO_CLAIM revert)
196
- for (uint256 i; i < 3; i++) {
197
- uint256 bb = _users[5 + i].balance;
198
- _cashOut(_users[5 + i], i + 2, 1);
199
- assertEq(_users[5 + i].balance, bb, "loser should receive 0 ETH");
200
- assertEq(_nft.balanceOf(_users[5 + i]), 0, "loser NFT burned");
201
- }
202
- }
203
-
204
- // =========================================================================
205
- // REFUND: exact price returned during MINT phase
206
- // =========================================================================
207
- function testRefundIntegrity() external {
208
- _setupGame(8, 50 ether);
209
-
210
- // Refund first 4 during MINT phase
211
- for (uint256 i; i < 4; i++) {
212
- uint256 bb = _users[i].balance;
213
- _refund(_users[i], i + 1);
214
- assertEq(_users[i].balance - bb, 50 ether, "exact refund");
215
- assertEq(_nft.balanceOf(_users[i]), 0, "NFT burned");
216
- }
217
- assertEq(_surplus(), 50 ether * 4, "pot = remaining mints");
218
- }
219
-
220
- // =========================================================================
221
- // ROUNDING: extreme weights at 1000 ETH per tier
222
- // With BWA + HHI, highly concentrated scorecards on fewer than 4 tiers
223
- // cannot reach quorum (total BWA = MAX*(n-1) < adjusted quorum for HHI~1).
224
- // Using 5 tiers ensures total BWA (4*MAX) exceeds the adjusted quorum.
225
- // =========================================================================
226
- function testRounding_extremeWeights() external {
227
- _setupGame(5, 1000 ether);
228
- _toScoring();
229
-
230
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(5);
231
- sc[0].cashOutWeight = 1;
232
- sc[1].cashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT() - 4;
233
- sc[2].cashOutWeight = 1;
234
- sc[3].cashOutWeight = 1;
235
- sc[4].cashOutWeight = 1;
236
-
237
- _attestAndRatify(sc);
238
- uint256 pot = _surplus();
239
- uint256 out = _cashOutAllUsers();
240
- assertApproxEqAbs(out + _surplus(), pot, 5, "fund conservation");
241
- assertGt(_users[1].balance, pot * 99 / 100, "tier 2 > 99%");
242
- }
243
-
244
- // =========================================================================
245
- // C-D2: overweight scorecard rejected
246
- // =========================================================================
247
- function testC_D2_rejectsOverweight() external {
248
- _setupGame(4, 1 ether);
249
- _toScoring();
250
-
251
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
252
- for (uint256 i; i < 4; i++) {
253
- sc[i].cashOutWeight = (_nft.TOTAL_CASHOUT_WEIGHT() * 30) / 100; // 120% total
254
- }
255
-
256
- vm.expectRevert(DefifaHook.DefifaHook_InvalidCashoutWeights.selector);
257
- _gov.submitScorecardFor(_gameId, sc);
258
- }
259
-
260
- // =========================================================================
261
- // M-D6: delegation blocked after MINT phase
262
- // =========================================================================
263
- function testM_D6_delegationBlocked() external {
264
- _setupGame(4, 1 ether);
265
-
266
- // REFUND phase
267
- vm.warp(block.timestamp + 1 days);
268
- vm.prank(_users[0]);
269
- vm.expectRevert(abi.encodeWithSignature("DefifaHook_DelegateChangesUnavailableInThisPhase()"));
270
- _nft.setTierDelegateTo(address(1), 1);
271
-
272
- // SCORING phase
273
- vm.warp(block.timestamp + 2 days);
274
- vm.prank(_users[0]);
275
- vm.expectRevert(abi.encodeWithSignature("DefifaHook_DelegateChangesUnavailableInThisPhase()"));
276
- _nft.setTierDelegateTo(address(1), 1);
277
- }
278
-
279
- // =========================================================================
280
- // QUORUM: 50% of minted tiers
281
- // =========================================================================
282
- function testQuorum_50pctMintedTiers() external {
283
- // Launch game with 10 tiers but only mint 6
284
- _setupPartial(10, 6, 1 ether);
285
- uint256 expected = (6 * _gov.MAX_ATTESTATION_POWER_TIER()) / 2;
286
- assertEq(_gov.quorum(_gameId), expected, "quorum = 50% of minted tiers");
287
- }
288
-
289
- // =========================================================================
290
- // FUZZ: fund conservation across varying tier/player counts
291
- // =========================================================================
292
- function testFuzz_fundConservation(uint8 rawTiers, uint8 rawPlayers) external {
293
- // Minimum 3 tiers: with BWA + HHI-adjusted quorum, 2-tier games with equal scorecards
294
- // can never reach quorum (total BWA = MAX*(n-1) < adjusted quorum when n < 3).
295
- uint8 nTiers = uint8(bound(rawTiers, 3, 12));
296
- uint8 nPpt = uint8(bound(rawPlayers, 1, 3));
297
-
298
- _setupMultiN(nTiers, nPpt, 1 ether);
299
- _toScoring();
300
-
301
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
302
- uint256 wpt = tw / nTiers;
303
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(nTiers);
304
- uint256 assigned;
305
- for (uint256 i; i < nTiers; i++) {
306
- if (i == nTiers - 1) {
307
- // Last tier absorbs rounding remainder to satisfy exact weight requirement
308
- sc[i].cashOutWeight = tw - assigned;
309
- } else {
310
- sc[i].cashOutWeight = wpt;
311
- }
312
- assigned += sc[i].cashOutWeight;
313
- }
314
-
315
- _attestAndRatify(sc);
316
- uint256 pot = _surplus();
317
-
318
- uint256 total;
319
- for (uint256 i; i < _users.length; i++) {
320
- uint256 bb = _users[i].balance;
321
- uint256 tid = (i / nPpt) + 1;
322
- uint256 tnum = (i % nPpt) + 1;
323
- _cashOut(_users[i], tid, tnum);
324
- total += _users[i].balance - bb;
325
- }
326
-
327
- assertApproxEqAbs(total + _surplus(), pot, _users.length, "fund conservation");
328
- }
329
-
330
- // =========================================================================
331
- // SCORECARD: valid equal-weight scorecard passes
332
- // =========================================================================
333
- function testScorecard_equalWeight_passes() external {
334
- _setupGame(4, 1 ether);
335
- _toScoring();
336
-
337
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
338
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
339
- for (uint256 i; i < 4; i++) {
340
- sc[i].cashOutWeight = tw / 4;
341
- }
342
-
343
- // Should succeed without reverting
344
- _attestAndRatify(sc);
345
- assertTrue(_nft.cashOutWeightIsSet(), "weights should be set");
346
- }
347
-
348
- // =========================================================================
349
- // C-D3: reserved minters get proportional fee tokens ($DEFIFA/$NANA)
350
- // With BWA + HHI, 2-tier games cannot reach quorum (total BWA power for
351
- // n tiers = MAX*(n-1) which is always less than HHI-adjusted quorum for n=2).
352
- // We use 4 tiers with equal weight, all having reserveRate=1. This ensures
353
- // enough attestation power from all participants to meet the adjusted quorum.
354
- // =========================================================================
355
- function testC_D3_reservedMintersGetFeeTokens() external {
356
- // Setup: 4 tiers, reservedRate=1, reserveBeneficiary = _reserveAddr
357
- address _reserveAddr = address(bytes20(keccak256("reserveBeneficiary")));
358
-
359
- DefifaTierParams[] memory tp = new DefifaTierParams[](4);
360
- for (uint256 i; i < 4; i++) {
361
- tp[i] = DefifaTierParams({
362
- reservedRate: 1, // 1 reserve per 1 paid mint
363
- reservedTokenBeneficiary: _reserveAddr,
364
- encodedIPFSUri: bytes32(0),
365
- shouldUseReservedTokenBeneficiaryAsDefault: false,
366
- name: "DEFIFA"
367
- });
368
- }
369
- DefifaLaunchProjectData memory d = DefifaLaunchProjectData({
370
- name: "DEFIFA",
371
- projectUri: "",
372
- contractUri: "",
373
- baseUri: "",
374
- token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
375
- mintPeriodDuration: 1 days,
376
- start: uint48(block.timestamp + 3 days),
377
- refundPeriodDuration: 1 days,
378
- store: new JB721TiersHookStore(),
379
- splits: new JBSplit[](0),
380
- attestationStartTime: 0,
381
- attestationGracePeriod: 100_381,
382
- defaultAttestationDelegate: address(0),
383
- tierPrice: uint104(1 ether),
384
- tiers: tp,
385
- defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
386
- terminal: jbMultiTerminal(),
387
- minParticipation: 0,
388
- scorecardTimeout: 0,
389
- timelockDuration: 0
390
- });
391
- (_pid, _nft, _gov) = _launch(d);
392
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
393
-
394
- // Paid mints: 1 user per tier
395
- _users = new address[](4);
396
- for (uint256 i; i < 4; i++) {
397
- _users[i] = _addr(i);
398
- _mint(_users[i], i + 1, 1 ether);
399
- _delegateSelf(_users[i], i + 1);
400
- vm.warp(_tsReader.timestamp() + 1);
401
- }
402
-
403
- // Move to scoring phase (reserves can only be minted here)
404
- _toScoring();
405
-
406
- // Mint reserved tokens (1 per tier since reserveFrequency=1)
407
- JB721TiersMintReservesConfig[] memory reserveConfigs = new JB721TiersMintReservesConfig[](4);
408
- reserveConfigs[0] = JB721TiersMintReservesConfig({tierId: 1, count: 1});
409
- reserveConfigs[1] = JB721TiersMintReservesConfig({tierId: 2, count: 1});
410
- reserveConfigs[2] = JB721TiersMintReservesConfig({tierId: 3, count: 1});
411
- reserveConfigs[3] = JB721TiersMintReservesConfig({tierId: 4, count: 1});
412
- _nft.mintReservesFor(reserveConfigs);
413
-
414
- // Reserve beneficiary should hold 4 NFTs (mintReservesFor auto-delegates to self)
415
- assertEq(_nft.balanceOf(_reserveAddr), 4, "reserve beneficiary holds 4 NFTs");
416
-
417
- // Seed fee tokens into the hook (simulating protocol fee distribution)
418
- uint256 defifaAmount = 1000 ether;
419
- uint256 nanaAmount = 500 ether;
420
- deal(address(IERC20(_defifaProjectTokenAccount)), address(_nft), defifaAmount);
421
- deal(address(IERC20(_protocolFeeProjectTokenAccount)), address(_nft), nanaAmount);
422
-
423
- // Scorecard: equal weight across all 4 tiers
424
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
425
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
426
- sc[0].cashOutWeight = tw / 4;
427
- sc[1].cashOutWeight = tw / 4;
428
- sc[2].cashOutWeight = tw / 4;
429
- sc[3].cashOutWeight = tw / 4;
430
-
431
- // Only paid minters attest -- reserve beneficiary has zero attestation weight at the
432
- // snapshot (attestationsBegin - 1) because they received NFTs via reserve minting after
433
- // the snapshot timestamp.
434
- uint256 pid = _gov.submitScorecardFor(_gameId, sc);
435
- vm.warp(_tsReader.timestamp() + _gov.attestationStartTimeOf(_gameId) + 1);
436
- for (uint256 i; i < _users.length; i++) {
437
- vm.prank(_users[i]);
438
- _gov.attestToScorecardFrom(_gameId, pid);
439
- }
440
- vm.warp(_tsReader.timestamp() + _gov.attestationGracePeriodOf(_gameId) + 1);
441
- _gov.ratifyScorecardFrom(_gameId, sc);
442
- vm.warp(_tsReader.timestamp() + 1);
443
-
444
- // Cash out paid minters from tiers 1 and 2
445
- _cashOut(_users[0], 1, 1);
446
- _cashOut(_users[1], 2, 1);
447
-
448
- // Check that paid minters got fee tokens
449
- uint256 user0Defifa = IERC20(_defifaProjectTokenAccount).balanceOf(_users[0]);
450
- uint256 user0Nana = IERC20(_protocolFeeProjectTokenAccount).balanceOf(_users[0]);
451
- assertGt(user0Defifa, 0, "paid minter got DEFIFA tokens");
452
- assertGt(user0Nana, 0, "paid minter got NANA tokens");
453
-
454
- // Cash out reserved minter's tokens from tiers 1 and 2 (token #2 in each tier)
455
- bytes memory meta1 = _cashOutMeta(1, 2);
456
- vm.prank(_reserveAddr);
457
- JBMultiTerminal(address(jbMultiTerminal()))
458
- .cashOutTokensOf({
459
- holder: _reserveAddr,
460
- projectId: _pid,
461
- cashOutCount: 0,
462
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
463
- minTokensReclaimed: 0,
464
- beneficiary: payable(_reserveAddr),
465
- metadata: meta1
466
- });
467
-
468
- bytes memory meta2 = _cashOutMeta(2, 2);
469
- vm.prank(_reserveAddr);
470
- JBMultiTerminal(address(jbMultiTerminal()))
471
- .cashOutTokensOf({
472
- holder: _reserveAddr,
473
- projectId: _pid,
474
- cashOutCount: 0,
475
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
476
- minTokensReclaimed: 0,
477
- beneficiary: payable(_reserveAddr),
478
- metadata: meta2
479
- });
480
-
481
- // Reserved minter should have gotten fee tokens from tiers 1+2 cash-outs
482
- uint256 reserveDefifa = IERC20(_defifaProjectTokenAccount).balanceOf(_reserveAddr);
483
- uint256 reserveNana = IERC20(_protocolFeeProjectTokenAccount).balanceOf(_reserveAddr);
484
- assertGt(reserveDefifa, 0, "reserved minter got DEFIFA tokens");
485
- assertGt(reserveNana, 0, "reserved minter got NANA tokens");
486
-
487
- // Each tier has 2 tokens (1 paid + 1 reserve), all at 1 ether.
488
- // Paid minter (1 token in 1 tier) vs reserved minter (2 tokens in 2 tiers).
489
- // Reserved minter gets 2x fee tokens relative to paid minter.
490
- assertApproxEqAbs(user0Defifa, reserveDefifa / 2, 1, "reserved gets 2x (2 tokens) vs paid (1 token)");
491
- assertApproxEqAbs(user0Nana, reserveNana / 2, 1, "NANA distribution matches");
492
- }
493
-
494
- // =========================================================================
495
- // GAME LIFECYCLE: cash-out before scorecard gives 0 ETH (weights = 0)
496
- // =========================================================================
497
- function testNoCashOut_beforeScorecard() external {
498
- _setupGame(4, 1 ether);
499
- _toScoring();
500
-
501
- // Cash out during scoring before scorecard — weight=0 means NOTHING_TO_CLAIM revert
502
- bytes memory meta = _cashOutMeta(1, 1);
503
- vm.expectRevert(DefifaHook.DefifaHook_NothingToClaim.selector);
504
- vm.prank(_users[0]);
505
- JBMultiTerminal(address(jbMultiTerminal()))
506
- .cashOutTokensOf({
507
- holder: _users[0],
508
- projectId: _pid,
509
- cashOutCount: 0,
510
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
511
- minTokensReclaimed: 0,
512
- beneficiary: payable(_users[0]),
513
- metadata: meta
514
- });
515
- // NFT should NOT have been burned (revert rolled it back)
516
- assertEq(_nft.balanceOf(_users[0]), 1, "NFT not burned on revert");
517
- }
518
-
519
- // =========================================================================
520
- // SETUP HELPERS
521
- // =========================================================================
522
-
523
- function _setupGame(uint8 nTiers, uint256 tierPrice) internal {
524
- DefifaLaunchProjectData memory d = _launchData(nTiers, tierPrice);
525
- (_pid, _nft, _gov) = _launch(d);
526
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
527
- _users = new address[](nTiers);
528
- for (uint256 i; i < nTiers; i++) {
529
- _users[i] = _addr(i);
530
- _mint(_users[i], i + 1, tierPrice);
531
- _delegateSelf(_users[i], i + 1);
532
- vm.warp(block.timestamp + 1);
533
- }
534
- }
535
-
536
- function _setupMultiPlayer() internal {
537
- DefifaLaunchProjectData memory d = _launchData(4, 1 ether);
538
- (_pid, _nft, _gov) = _launch(d);
539
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
540
- _users = new address[](8);
541
- for (uint256 i; i < 5; i++) {
542
- _users[i] = _addr(100 + i);
543
- _mint(_users[i], 1, 1 ether);
544
- _delegateSelf(_users[i], 1);
545
- vm.warp(block.timestamp + 1);
546
- }
547
- for (uint256 i; i < 3; i++) {
548
- _users[5 + i] = _addr(200 + i);
549
- _mint(_users[5 + i], i + 2, 1 ether);
550
- _delegateSelf(_users[5 + i], i + 2);
551
- vm.warp(block.timestamp + 1);
552
- }
553
- }
554
-
555
- function _setupMultiN(uint8 nTiers, uint8 nPpt, uint256 tierPrice) internal {
556
- DefifaLaunchProjectData memory d = _launchData(nTiers, tierPrice);
557
- (_pid, _nft, _gov) = _launch(d);
558
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
559
- uint256 total = uint256(nTiers) * uint256(nPpt);
560
- _users = new address[](total);
561
- uint256 idx;
562
- for (uint256 t; t < nTiers; t++) {
563
- for (uint256 p; p < nPpt; p++) {
564
- _users[idx] = _addr(idx);
565
- _mint(_users[idx], t + 1, tierPrice);
566
- _delegateSelf(_users[idx], t + 1);
567
- vm.warp(block.timestamp + 1);
568
- idx++;
569
- }
570
- }
571
- }
572
-
573
- function _setupPartial(uint8 nTiers, uint256 nMint, uint256 tierPrice) internal {
574
- DefifaLaunchProjectData memory d = _launchData(nTiers, tierPrice);
575
- (_pid, _nft, _gov) = _launch(d);
576
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
577
- _users = new address[](nMint);
578
- for (uint256 i; i < nMint; i++) {
579
- _users[i] = _addr(i);
580
- _mint(_users[i], i + 1, tierPrice);
581
- }
582
- }
583
-
584
- function _toScoring() internal {
585
- // Warp 3 days forward (past mint + refund) into scoring
586
- vm.warp(_tsReader.timestamp() + 3 days + 1);
587
- }
588
-
589
- // =========================================================================
590
- // PRIMITIVE HELPERS
591
- // =========================================================================
592
-
593
- function _launchData(uint8 n, uint256 tierPrice) internal returns (DefifaLaunchProjectData memory) {
594
- DefifaTierParams[] memory tp = new DefifaTierParams[](n);
595
- for (uint256 i; i < n; i++) {
596
- tp[i] = DefifaTierParams({
597
- reservedRate: 1001,
598
- reservedTokenBeneficiary: address(0),
599
- encodedIPFSUri: bytes32(0),
600
- shouldUseReservedTokenBeneficiaryAsDefault: false,
601
- name: "DEFIFA"
602
- });
603
- }
604
- return DefifaLaunchProjectData({
605
- name: "DEFIFA",
606
- projectUri: "",
607
- contractUri: "",
608
- baseUri: "",
609
- token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
610
- mintPeriodDuration: 1 days,
611
- start: uint48(block.timestamp + 3 days),
612
- refundPeriodDuration: 1 days,
613
- store: new JB721TiersHookStore(),
614
- splits: new JBSplit[](0),
615
- attestationStartTime: 0,
616
- attestationGracePeriod: 100_381,
617
- defaultAttestationDelegate: address(0),
618
- // forge-lint: disable-next-line(unsafe-typecast)
619
- tierPrice: uint104(tierPrice),
620
- tiers: tp,
621
- defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
622
- terminal: jbMultiTerminal(),
623
- minParticipation: 0,
624
- scorecardTimeout: 0,
625
- timelockDuration: 0
626
- });
627
- }
628
-
629
- function _launch(DefifaLaunchProjectData memory d) internal returns (uint256 p, DefifaHook n, DefifaGovernor g) {
630
- g = governor;
631
- p = deployer.launchGameWith(d);
632
- JBRuleset memory fc = jbRulesets().currentOf(p);
633
- if (fc.dataHook() == address(0)) (fc,) = jbRulesets().latestQueuedOf(p);
634
- n = DefifaHook(fc.dataHook());
635
- }
636
-
637
- function _addr(uint256 i) internal pure returns (address) {
638
- return address(bytes20(keccak256(abi.encode("su", i))));
639
- }
640
-
641
- function _mint(address user, uint256 tid, uint256 amt) internal {
642
- vm.deal(user, amt);
643
- uint16[] memory m = new uint16[](1);
644
- // forge-lint: disable-next-line(unsafe-typecast)
645
- m[0] = uint16(tid);
646
- bytes[] memory data = new bytes[](1);
647
- data[0] = abi.encode(user, m);
648
- bytes4[] memory ids = new bytes4[](1);
649
- ids[0] = metadataHelper().getId("pay", address(hook));
650
- vm.prank(user);
651
- jbMultiTerminal().pay{value: amt}(
652
- _pid, JBConstants.NATIVE_TOKEN, amt, user, 0, "", metadataHelper().createMetadata(ids, data)
653
- );
654
- }
655
-
656
- function _delegateSelf(address user, uint256 tid) internal {
657
- DefifaDelegation[] memory dd = new DefifaDelegation[](1);
658
- dd[0] = DefifaDelegation({delegatee: user, tierId: tid});
659
- vm.prank(user);
660
- _nft.setTierDelegatesTo(dd);
661
- }
662
-
663
- function _buildScorecard(uint256 n) internal pure returns (DefifaTierCashOutWeight[] memory sc) {
664
- sc = new DefifaTierCashOutWeight[](n);
665
- for (uint256 i; i < n; i++) {
666
- sc[i].id = i + 1;
667
- }
668
- }
669
-
670
- function _attestAndRatify(DefifaTierCashOutWeight[] memory sc) internal {
671
- uint256 pid = _gov.submitScorecardFor(_gameId, sc);
672
- _attestAllFor(pid);
673
- _gov.ratifyScorecardFrom(_gameId, sc);
674
- vm.warp(block.timestamp + 1);
675
- }
676
-
677
- function _attestAllFor(uint256 pid) internal {
678
- vm.warp(block.timestamp + _gov.attestationStartTimeOf(_gameId) + 1);
679
- for (uint256 i; i < _users.length; i++) {
680
- vm.prank(_users[i]);
681
- try _gov.attestToScorecardFrom(_gameId, pid) {} catch {}
682
- }
683
- vm.warp(block.timestamp + _gov.attestationGracePeriodOf(_gameId) + 1);
684
- }
685
-
686
- function _surplus() internal view returns (uint256) {
687
- return jbMultiTerminal().currentSurplusOf(_pid, new address[](0), 18, JBCurrencyIds.ETH);
688
- }
689
-
690
- function _cashOut(address user, uint256 tid, uint256 tnum) internal {
691
- bytes memory meta = _cashOutMeta(tid, tnum);
692
- vm.prank(user);
693
- JBMultiTerminal(address(jbMultiTerminal()))
694
- .cashOutTokensOf({
695
- holder: user,
696
- projectId: _pid,
697
- cashOutCount: 0,
698
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
699
- minTokensReclaimed: 0,
700
- beneficiary: payable(user),
701
- metadata: meta
702
- });
703
- }
704
-
705
- function _cashOutMeta(uint256 tid, uint256 tnum) internal view returns (bytes memory) {
706
- uint256[] memory cid = new uint256[](1);
707
- cid[0] = (tid * 1_000_000_000) + tnum;
708
- bytes[] memory data = new bytes[](1);
709
- data[0] = abi.encode(cid);
710
- bytes4[] memory ids = new bytes4[](1);
711
- ids[0] = metadataHelper().getId("cashOut", address(hook));
712
- return metadataHelper().createMetadata(ids, data);
713
- }
714
-
715
- function _cashOutAllUsers() internal returns (uint256 total) {
716
- for (uint256 i; i < _users.length; i++) {
717
- uint256 bb = _users[i].balance;
718
- _cashOut(_users[i], i + 1, 1);
719
- total += _users[i].balance - bb;
720
- }
721
- }
722
-
723
- function _refund(address user, uint256 tid) internal {
724
- JB721Tier memory tier = _nft.store().tierOf(address(_nft), tid, false);
725
- uint256 nb = _nft.store().numberOfBurnedFor(address(_nft), tid);
726
- // Tier fetched AFTER mint: remainingSupply already decremented, so no +1
727
- uint256 tnum = tier.initialSupply - tier.remainingSupply + nb;
728
- bytes memory meta = _cashOutMeta(tid, tnum);
729
- vm.prank(user);
730
- JBMultiTerminal(address(jbMultiTerminal()))
731
- .cashOutTokensOf({
732
- holder: user,
733
- projectId: _pid,
734
- cashOutCount: 0,
735
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
736
- minTokensReclaimed: 0,
737
- beneficiary: payable(user),
738
- metadata: meta
739
- });
740
- }
741
- }