@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,581 +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 {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
14
-
15
- import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
16
- import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
17
- import {DefifaLaunchProjectData} from "../src/structs/DefifaLaunchProjectData.sol";
18
- import {DefifaTierParams} from "../src/structs/DefifaTierParams.sol";
19
- import {DefifaTierCashOutWeight} from "../src/structs/DefifaTierCashOutWeight.sol";
20
- import {DefifaDelegation} from "../src/structs/DefifaDelegation.sol";
21
- import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
22
- import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
23
- import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
24
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
25
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
26
- import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
27
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.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 {JBRulesetConfig, JBTerminalConfig} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
31
- import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
32
- import {JBMultiTerminal} from "@bananapus/core-v6/src/JBMultiTerminal.sol";
33
- import {JBPermissionsData} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
34
- import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
35
- import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
36
-
37
- /// @notice Tests for PR #22 (M-D8): fee accounting after removing duplicate nana fee.
38
- /// Verifies that only the fee portion of the pot is sent as payouts during fulfillment,
39
- /// and the remaining balance stays as surplus for cash-outs.
40
- contract DefifaFeeAccountingTest is JBTest, TestBaseWorkflow {
41
- using JBRulesetMetadataResolver for JBRuleset;
42
-
43
- uint256 _protocolFeeProjectId;
44
- uint256 _defifaProjectId;
45
- address projectOwner = address(bytes20(keccak256("projectOwner")));
46
-
47
- DefifaDeployer deployer;
48
- DefifaHook hook;
49
- DefifaGovernor governor;
50
- uint256 _gameId = 3;
51
-
52
- function setUp() public virtual override {
53
- super.setUp();
54
-
55
- // Terminal configurations.
56
- JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
57
- _tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
58
-
59
- JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
60
- terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
61
-
62
- JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
63
- rulesetConfigs[0] = JBRulesetConfig({
64
- mustStartAtOrAfter: 0,
65
- duration: 10 days,
66
- weight: 1e18,
67
- weightCutPercent: 0,
68
- approvalHook: IJBRulesetApprovalHook(address(0)),
69
- metadata: JBRulesetMetadata({
70
- reservedPercent: 0,
71
- cashOutTaxRate: 0,
72
- baseCurrency: JBCurrencyIds.ETH,
73
- pausePay: false,
74
- pauseCreditTransfers: false,
75
- allowOwnerMinting: false,
76
- allowSetCustomToken: false,
77
- allowTerminalMigration: false,
78
- allowSetTerminals: false,
79
- allowSetController: false,
80
- allowAddAccountingContext: false,
81
- allowAddPriceFeed: false,
82
- ownerMustSendPayouts: false,
83
- holdFees: false,
84
- useTotalSurplusForCashOuts: false,
85
- useDataHookForPay: true,
86
- useDataHookForCashOut: true,
87
- dataHook: address(0),
88
- metadata: 0
89
- }),
90
- splitGroups: new JBSplitGroup[](0),
91
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
92
- });
93
-
94
- // Launch the NANA fee project.
95
- _protocolFeeProjectId =
96
- jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
97
- vm.prank(projectOwner);
98
- jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0));
99
-
100
- // Launch the Defifa fee project.
101
- _defifaProjectId =
102
- jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
103
- vm.prank(projectOwner);
104
- address _defifaToken = address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
105
-
106
- // Look up NANA token.
107
- address _nanaToken = address(jbTokens().tokenOf(_protocolFeeProjectId));
108
-
109
- hook = new DefifaHook(jbDirectory(), IERC20(_defifaToken), IERC20(_nanaToken));
110
- governor = new DefifaGovernor(jbController(), address(this));
111
- JBAddressRegistry _registry = new JBAddressRegistry();
112
- DefifaTokenUriResolver _tokenUriResolver = new DefifaTokenUriResolver(ITypeface(address(0)));
113
- deployer = new DefifaDeployer(
114
- address(hook),
115
- _tokenUriResolver,
116
- governor,
117
- jbController(),
118
- _registry,
119
- _defifaProjectId,
120
- _protocolFeeProjectId
121
- );
122
-
123
- // Grant the deployer SET_SPLIT_GROUPS permission on the defifa fee project.
124
- // This is needed so the deployer can set custom splits via controller.setSplitGroupsOf().
125
- uint8[] memory permissionIds = new uint8[](1);
126
- permissionIds[0] = JBPermissionIds.SET_SPLIT_GROUPS;
127
- vm.prank(projectOwner);
128
- jbPermissions()
129
- .setPermissionsFor(
130
- projectOwner,
131
- JBPermissionsData({
132
- operator: address(deployer),
133
- // forge-lint: disable-next-line(unsafe-typecast)
134
- projectId: uint64(_defifaProjectId),
135
- permissionIds: permissionIds
136
- })
137
- );
138
-
139
- hook.transferOwnership(address(deployer));
140
- governor.transferOwnership(address(deployer));
141
- }
142
-
143
- // -----------------------------------------------------------------------
144
- // Test 1: Fee accounting with default splits (no user splits)
145
- // defifa = 5%, nana = 2.5%, total commitment = 7.5%
146
- // -----------------------------------------------------------------------
147
- function testFeeAccounting_defaultSplits() external {
148
- uint8 nTiers = 4;
149
- DefifaLaunchProjectData memory defifaData = _getBasicDefifaLaunchData(nTiers);
150
- (uint256 projectId, DefifaHook _nft, DefifaGovernor _governor) = _createDefifaProject(defifaData);
151
-
152
- // Mint phase: each user buys 1 NFT for 1 ETH.
153
- vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
154
- address[] memory users = _mintAllTiers(_nft, _governor, projectId, nTiers);
155
-
156
- // Record pot before fulfillment.
157
- uint256 potBefore =
158
- jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
159
- assertEq(potBefore, nTiers * 1 ether, "pot should be nTiers ETH");
160
-
161
- // Expected fee: pot * (5% + 2.5%) = pot * 7.5%
162
- // DEFIFA_FEE_DIVISOR = 20 -> 1_000_000_000 / 20 = 50_000_000 (5%)
163
- // BASE_PROTOCOL_FEE_DIVISOR = 40 -> 1_000_000_000 / 40 = 25_000_000 (2.5%)
164
- // totalAbsolutePercent = 75_000_000
165
- uint256 expectedFee = (potBefore * 75_000_000) / JBConstants.SPLITS_TOTAL_PERCENT;
166
- uint256 expectedSurplus = potBefore - expectedFee;
167
-
168
- // Advance through lifecycle and ratify scorecard.
169
- _ratifyEvenScorecard(users, _nft, _governor, projectId, nTiers);
170
-
171
- // Check balance after fulfillment (ratification triggers fulfillment).
172
- uint256 potAfter =
173
- jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
174
-
175
- assertEq(potAfter, expectedSurplus, "surplus after fees should be pot - feeAmount");
176
- assertEq(
177
- deployer.fulfilledCommitmentsOf(projectId), expectedFee, "fulfilledCommitmentsOf should equal fee amount"
178
- );
179
-
180
- // Verify currentGamePotOf reporting.
181
- (uint256 potExcluding,,) = deployer.currentGamePotOf(projectId, false);
182
- (uint256 potIncluding,,) = deployer.currentGamePotOf(projectId, true);
183
- assertEq(potExcluding, expectedSurplus, "pot excluding commitments = surplus");
184
- assertEq(potIncluding, expectedSurplus + expectedFee, "pot including commitments = original pot");
185
- }
186
-
187
- // -----------------------------------------------------------------------
188
- // Test 2: Cash-out amounts are correct after fee deduction
189
- // With 4 tiers, even scorecard, each player should get ~(pot - fees) / 4
190
- // -----------------------------------------------------------------------
191
- function testCashOutAfterFees() external {
192
- uint8 nTiers = 4;
193
- DefifaLaunchProjectData memory defifaData = _getBasicDefifaLaunchData(nTiers);
194
- (uint256 projectId, DefifaHook _nft, DefifaGovernor _governor) = _createDefifaProject(defifaData);
195
-
196
- // Mint phase.
197
- vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
198
- address[] memory users = _mintAllTiers(_nft, _governor, projectId, nTiers);
199
-
200
- uint256 potBefore =
201
- jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
202
-
203
- // Ratify scorecard (triggers fulfillment).
204
- _ratifyEvenScorecard(users, _nft, _governor, projectId, nTiers);
205
- vm.warp(block.timestamp + 1);
206
-
207
- uint256 expectedFee = (potBefore * 75_000_000) / JBConstants.SPLITS_TOTAL_PERCENT;
208
- uint256 surplus = potBefore - expectedFee;
209
-
210
- // Each user cashes out their NFT.
211
- uint256 totalCashedOut;
212
- for (uint256 i = 0; i < nTiers; i++) {
213
- uint256 balBefore = users[i].balance;
214
-
215
- uint256[] memory cashOutIds = new uint256[](1);
216
- cashOutIds[0] = _generateTokenId(i + 1, 1);
217
- bytes memory cashOutMetadata = _buildCashOutMetadata(abi.encode(cashOutIds));
218
-
219
- vm.prank(users[i]);
220
- JBMultiTerminal(address(jbMultiTerminal()))
221
- .cashOutTokensOf({
222
- holder: users[i],
223
- projectId: projectId,
224
- cashOutCount: 0,
225
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
226
- minTokensReclaimed: 0,
227
- beneficiary: payable(users[i]),
228
- metadata: cashOutMetadata
229
- });
230
-
231
- totalCashedOut += users[i].balance - balBefore;
232
- }
233
-
234
- // Total cashed out should approximately equal the post-fee surplus.
235
- assertApproxEqRel(totalCashedOut, surplus, 0.001 ether, "total cash-out should equal surplus");
236
- }
237
-
238
- // -----------------------------------------------------------------------
239
- // Test 3: Fee + remaining balance = original pot (no dust lost)
240
- // -----------------------------------------------------------------------
241
- function testFeeAccounting_noRoundingLoss() external {
242
- uint8 nTiers = 4;
243
- DefifaLaunchProjectData memory defifaData = _getBasicDefifaLaunchData(nTiers);
244
- (uint256 projectId, DefifaHook _nft, DefifaGovernor _governor) = _createDefifaProject(defifaData);
245
-
246
- vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
247
- address[] memory users = _mintAllTiers(_nft, _governor, projectId, nTiers);
248
-
249
- uint256 potBefore =
250
- jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
251
-
252
- _ratifyEvenScorecard(users, _nft, _governor, projectId, nTiers);
253
-
254
- uint256 potAfter =
255
- jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
256
-
257
- uint256 feeAmount = deployer.fulfilledCommitmentsOf(projectId);
258
- assertEq(feeAmount + potAfter, potBefore, "fee + surplus should equal original pot exactly");
259
- }
260
-
261
- // -----------------------------------------------------------------------
262
- // Test 4: Fee accounting with user-provided custom splits
263
- // User adds a 10% split -> total commitment = 5% + 5% + 10% = 20%
264
- // -----------------------------------------------------------------------
265
- function testFeeAccounting_withUserSplits() external {
266
- uint8 nTiers = 4;
267
-
268
- JBSplit[] memory customSplits = new JBSplit[](1);
269
- address splitBeneficiary = address(bytes20(keccak256("charity")));
270
- customSplits[0] = JBSplit({
271
- preferAddToBalance: false,
272
- percent: JBConstants.SPLITS_TOTAL_PERCENT / 10, // 10% = 100_000_000
273
- projectId: 0,
274
- beneficiary: payable(splitBeneficiary),
275
- lockedUntil: 0,
276
- hook: IJBSplitHook(address(0))
277
- });
278
-
279
- DefifaLaunchProjectData memory defifaData = _getDefifaLaunchDataWithSplits(nTiers, customSplits);
280
- (uint256 projectId, DefifaHook _nft, DefifaGovernor _governor) = _createDefifaProject(defifaData);
281
-
282
- // Mint phase.
283
- vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
284
- address[] memory users = _mintAllTiers(_nft, _governor, projectId, nTiers);
285
-
286
- uint256 potBefore =
287
- jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
288
-
289
- // totalAbsolutePercent = 25_000_000 + 50_000_000 + 100_000_000 = 175_000_000 (17.5%)
290
- uint256 expectedFee = (potBefore * 175_000_000) / JBConstants.SPLITS_TOTAL_PERCENT;
291
- uint256 expectedSurplus = potBefore - expectedFee;
292
-
293
- // Ratify scorecard (triggers fulfillment).
294
- _ratifyEvenScorecard(users, _nft, _governor, projectId, nTiers);
295
-
296
- uint256 potAfter =
297
- jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
298
-
299
- assertEq(potAfter, expectedSurplus, "surplus with user splits should be pot - 20%");
300
- assertEq(deployer.fulfilledCommitmentsOf(projectId), expectedFee, "fulfilled = fee amount with user splits");
301
-
302
- // Verify beneficiary received funds.
303
- assertTrue(splitBeneficiary.balance > 0, "split beneficiary should have received funds");
304
- }
305
-
306
- // -----------------------------------------------------------------------
307
- // Test 5: Cash-out with user splits — surplus is reduced by user split portion
308
- // -----------------------------------------------------------------------
309
- function testCashOutAfterFees_withUserSplits() external {
310
- uint8 nTiers = 4;
311
-
312
- JBSplit[] memory customSplits = new JBSplit[](1);
313
- customSplits[0] = JBSplit({
314
- preferAddToBalance: false,
315
- percent: JBConstants.SPLITS_TOTAL_PERCENT / 10, // 10%
316
- projectId: 0,
317
- beneficiary: payable(address(bytes20(keccak256("charity")))),
318
- lockedUntil: 0,
319
- hook: IJBSplitHook(address(0))
320
- });
321
-
322
- DefifaLaunchProjectData memory defifaData = _getDefifaLaunchDataWithSplits(nTiers, customSplits);
323
- (uint256 projectId, DefifaHook _nft, DefifaGovernor _governor) = _createDefifaProject(defifaData);
324
-
325
- vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
326
- address[] memory users = _mintAllTiers(_nft, _governor, projectId, nTiers);
327
-
328
- uint256 potBefore =
329
- jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
330
-
331
- _ratifyEvenScorecard(users, _nft, _governor, projectId, nTiers);
332
- vm.warp(block.timestamp + 1);
333
-
334
- // 17.5% fee (2.5% nana + 5% defifa + 10% user)
335
- uint256 expectedFee = (potBefore * 175_000_000) / JBConstants.SPLITS_TOTAL_PERCENT;
336
- uint256 surplus = potBefore - expectedFee;
337
-
338
- // Cash out all tiers.
339
- uint256 totalCashedOut;
340
- for (uint256 i = 0; i < nTiers; i++) {
341
- uint256 balBefore = users[i].balance;
342
-
343
- uint256[] memory cashOutIds = new uint256[](1);
344
- cashOutIds[0] = _generateTokenId(i + 1, 1);
345
- bytes memory cashOutMetadata = _buildCashOutMetadata(abi.encode(cashOutIds));
346
-
347
- vm.prank(users[i]);
348
- JBMultiTerminal(address(jbMultiTerminal()))
349
- .cashOutTokensOf({
350
- holder: users[i],
351
- projectId: projectId,
352
- cashOutCount: 0,
353
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
354
- minTokensReclaimed: 0,
355
- beneficiary: payable(users[i]),
356
- metadata: cashOutMetadata
357
- });
358
-
359
- totalCashedOut += users[i].balance - balBefore;
360
- }
361
-
362
- assertApproxEqRel(
363
- totalCashedOut, surplus, 0.001 ether, "cash-out with user splits should equal surplus after 20% fee"
364
- );
365
-
366
- // Verify that surplus is meaningfully less than with no user splits.
367
- uint256 surplusWithoutUserSplits = potBefore - (potBefore * 75_000_000) / JBConstants.SPLITS_TOTAL_PERCENT;
368
- assertTrue(surplus < surplusWithoutUserSplits, "user splits should reduce available surplus");
369
- }
370
-
371
- // -----------------------------------------------------------------------
372
- // Test 6: Splits normalization with awkward percentages
373
- // -----------------------------------------------------------------------
374
- function testSplitNormalization_noRoundingLoss() external {
375
- uint8 nTiers = 4;
376
-
377
- JBSplit[] memory customSplits = new JBSplit[](3);
378
- customSplits[0] = JBSplit({
379
- preferAddToBalance: false,
380
- percent: 33_333_333, // ~3.33%
381
- projectId: 0,
382
- beneficiary: payable(address(0x1111)),
383
- lockedUntil: 0,
384
- hook: IJBSplitHook(address(0))
385
- });
386
- customSplits[1] = JBSplit({
387
- preferAddToBalance: false,
388
- percent: 66_666_666, // ~6.67%
389
- projectId: 0,
390
- beneficiary: payable(address(0x2222)),
391
- lockedUntil: 0,
392
- hook: IJBSplitHook(address(0))
393
- });
394
- customSplits[2] = JBSplit({
395
- preferAddToBalance: false,
396
- percent: 11_111_111, // ~1.11%
397
- projectId: 0,
398
- beneficiary: payable(address(0x3333)),
399
- lockedUntil: 0,
400
- hook: IJBSplitHook(address(0))
401
- });
402
-
403
- DefifaLaunchProjectData memory defifaData = _getDefifaLaunchDataWithSplits(nTiers, customSplits);
404
- (uint256 projectId, DefifaHook _nft, DefifaGovernor _governor) = _createDefifaProject(defifaData);
405
-
406
- vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
407
- address[] memory users = _mintAllTiers(_nft, _governor, projectId, nTiers);
408
-
409
- uint256 potBefore =
410
- jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
411
-
412
- // Ratification should succeed (proves splits normalization works).
413
- _ratifyEvenScorecard(users, _nft, _governor, projectId, nTiers);
414
-
415
- uint256 potAfter =
416
- jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
417
-
418
- // fee + remaining = original (no dust lost).
419
- uint256 feeAmount = deployer.fulfilledCommitmentsOf(projectId);
420
- assertEq(feeAmount + potAfter, potBefore, "fee + surplus should equal original pot exactly");
421
- }
422
-
423
- // ========================== Helpers ==========================
424
-
425
- function _getBasicDefifaLaunchData(uint8 nTiers) internal returns (DefifaLaunchProjectData memory) {
426
- return _getDefifaLaunchDataWithSplits(nTiers, new JBSplit[](0));
427
- }
428
-
429
- function _getDefifaLaunchDataWithSplits(
430
- uint8 nTiers,
431
- JBSplit[] memory splits
432
- )
433
- internal
434
- returns (DefifaLaunchProjectData memory)
435
- {
436
- DefifaTierParams[] memory tierParams = new DefifaTierParams[](nTiers);
437
- for (uint256 i = 0; i < nTiers; i++) {
438
- tierParams[i] = DefifaTierParams({
439
- reservedRate: 1001,
440
- reservedTokenBeneficiary: address(0),
441
- encodedIPFSUri: bytes32(0),
442
- shouldUseReservedTokenBeneficiaryAsDefault: false,
443
- name: "DEFIFA"
444
- });
445
- }
446
-
447
- return DefifaLaunchProjectData({
448
- name: "DEFIFA",
449
- projectUri: "",
450
- contractUri: "",
451
- baseUri: "",
452
- token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
453
- mintPeriodDuration: 1 days,
454
- start: uint48(block.timestamp + 3 days),
455
- refundPeriodDuration: 1 days,
456
- store: new JB721TiersHookStore(),
457
- splits: splits,
458
- attestationStartTime: 0,
459
- attestationGracePeriod: 100_381,
460
- defaultAttestationDelegate: address(0),
461
- tierPrice: uint104(1 ether),
462
- tiers: tierParams,
463
- defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
464
- terminal: jbMultiTerminal(),
465
- minParticipation: 0,
466
- scorecardTimeout: 0,
467
- timelockDuration: 0
468
- });
469
- }
470
-
471
- function _createDefifaProject(DefifaLaunchProjectData memory defifaLaunchData)
472
- internal
473
- returns (uint256 projectId, DefifaHook nft, DefifaGovernor _governor)
474
- {
475
- _governor = governor;
476
- projectId = deployer.launchGameWith(defifaLaunchData);
477
- JBRuleset memory _fc = jbRulesets().currentOf(projectId);
478
- if (_fc.dataHook() == address(0)) {
479
- (_fc,) = jbRulesets().latestQueuedOf(projectId);
480
- }
481
- nft = DefifaHook(_fc.dataHook());
482
- }
483
-
484
- /// @notice Mint 1 NFT per tier, set delegation, return array of user addresses.
485
- function _mintAllTiers(
486
- DefifaHook _nft,
487
- DefifaGovernor,
488
- uint256 projectId,
489
- uint8 nTiers
490
- )
491
- internal
492
- returns (address[] memory users)
493
- {
494
- users = new address[](nTiers);
495
- for (uint256 i = 0; i < nTiers; i++) {
496
- users[i] = address(bytes20(keccak256(abi.encode("feeUser", i))));
497
- vm.deal(users[i], 1 ether);
498
-
499
- uint16[] memory rawMetadata = new uint16[](1);
500
- // forge-lint: disable-next-line(unsafe-typecast)
501
- rawMetadata[0] = uint16(i + 1);
502
- bytes memory metadata = _buildPayMetadata(abi.encode(users[i], rawMetadata));
503
-
504
- vm.prank(users[i]);
505
- jbMultiTerminal().pay{value: 1 ether}(
506
- projectId, JBConstants.NATIVE_TOKEN, 1 ether, users[i], 0, "", metadata
507
- );
508
-
509
- // Set delegation.
510
- DefifaDelegation[] memory delegations = new DefifaDelegation[](1);
511
- delegations[0] = DefifaDelegation({delegatee: users[i], tierId: i + 1});
512
- vm.prank(users[i]);
513
- _nft.setTierDelegatesTo(delegations);
514
-
515
- vm.warp(block.timestamp + 1);
516
- }
517
- }
518
-
519
- /// @notice Submit and ratify an even scorecard (equal weight per tier).
520
- function _ratifyEvenScorecard(
521
- address[] memory users,
522
- DefifaHook _nft,
523
- DefifaGovernor _governor,
524
- uint256,
525
- uint8 nTiers
526
- )
527
- internal
528
- {
529
- // Advance to SCORING phase.
530
- vm.warp(block.timestamp + 2 days);
531
-
532
- uint256 totalCashOutWeight = _nft.TOTAL_CASHOUT_WEIGHT();
533
-
534
- // Build even scorecard.
535
- DefifaTierCashOutWeight[] memory scorecards = new DefifaTierCashOutWeight[](nTiers);
536
- uint256 assigned;
537
- for (uint256 i = 0; i < nTiers; i++) {
538
- scorecards[i].id = i + 1;
539
- scorecards[i].cashOutWeight = totalCashOutWeight / nTiers;
540
- assigned += scorecards[i].cashOutWeight;
541
- }
542
- // Absorb rounding remainder into last tier.
543
- scorecards[nTiers - 1].cashOutWeight += totalCashOutWeight - assigned;
544
-
545
- // Submit scorecard.
546
- uint256 proposalId = _governor.submitScorecardFor(_gameId, scorecards);
547
-
548
- // Advance to attestation period and vote.
549
- vm.warp(block.timestamp + _governor.attestationStartTimeOf(_gameId) + 1);
550
- for (uint256 i = 0; i < users.length; i++) {
551
- vm.prank(users[i]);
552
- _governor.attestToScorecardFrom(_gameId, proposalId);
553
- }
554
-
555
- // Advance past grace period.
556
- vm.warp(block.timestamp + _governor.attestationGracePeriodOf(_gameId) + 1);
557
-
558
- // Ratify (this calls fulfillCommitmentsOf internally).
559
- _governor.ratifyScorecardFrom(_gameId, scorecards);
560
- }
561
-
562
- function _generateTokenId(uint256 _tierId, uint256 _tokenNumber) internal pure returns (uint256) {
563
- return (_tierId * 1_000_000_000) + _tokenNumber;
564
- }
565
-
566
- function _buildPayMetadata(bytes memory metadata) internal view returns (bytes memory) {
567
- bytes[] memory data = new bytes[](1);
568
- data[0] = metadata;
569
- bytes4[] memory ids = new bytes4[](1);
570
- ids[0] = metadataHelper().getId("pay", address(hook));
571
- return metadataHelper().createMetadata(ids, data);
572
- }
573
-
574
- function _buildCashOutMetadata(bytes memory metadata) internal view returns (bytes memory) {
575
- bytes[] memory data = new bytes[](1);
576
- data[0] = metadata;
577
- bytes4[] memory ids = new bytes4[](1);
578
- ids[0] = metadataHelper().getId("cashOut", address(hook));
579
- return metadataHelper().createMetadata(ids, data);
580
- }
581
- }