@ballkidz/defifa 0.0.25 → 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 (52) 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/CodexNemesisOneTierZeroTimeoutLockVerified.t.sol +0 -218
  41. package/test/audit/CodexNemesisSingleTierTimeoutLock.t.sol +0 -237
  42. package/test/audit/CodexRegistryMismatch.t.sol +0 -191
  43. package/test/audit/CodexTierCapMismatch.t.sol +0 -171
  44. package/test/audit/CurrencyMismatchFix.t.sol +0 -265
  45. package/test/audit/FixPendingReserveDilution.t.sol +0 -366
  46. package/test/audit/H5TierCapValidation.t.sol +0 -184
  47. package/test/audit/PendingReserveDilution.t.sol +0 -298
  48. package/test/audit/PendingReserveQuorumGrief.t.sol +0 -355
  49. package/test/audit/PendingReserveSnapshotBypass.t.sol +0 -319
  50. package/test/regression/AttestationDelegateBeneficiary.t.sol +0 -271
  51. package/test/regression/FulfillmentBlocksRatification.t.sol +0 -279
  52. package/test/regression/GracePeriodBypass.t.sol +0 -302
@@ -1,366 +0,0 @@
1
- // SPDX-License-Identifier: UNLICENSED
2
- pragma solidity 0.8.28;
3
-
4
- import {TestBaseWorkflow} from "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
5
-
6
- import {DefifaGovernor} from "../../src/DefifaGovernor.sol";
7
- import {DefifaDeployer} from "../../src/DefifaDeployer.sol";
8
- import {DefifaHook} from "../../src/DefifaHook.sol";
9
- import {DefifaTokenUriResolver} from "../../src/DefifaTokenUriResolver.sol";
10
- import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
11
- import {JB721TiersMintReservesConfig} from "@bananapus/721-hook-v6/src/structs/JB721TiersMintReservesConfig.sol";
12
-
13
- import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
14
- import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
15
- import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
16
- import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
17
-
18
- import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
19
- import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
20
- import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
21
- import {DefifaDelegation} from "../../src/structs/DefifaDelegation.sol";
22
- import {DefifaLaunchProjectData} from "../../src/structs/DefifaLaunchProjectData.sol";
23
- import {DefifaTierParams} from "../../src/structs/DefifaTierParams.sol";
24
- import {DefifaTierCashOutWeight} from "../../src/structs/DefifaTierCashOutWeight.sol";
25
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
26
- import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
27
- import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
28
- import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
29
- import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
30
- import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
31
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.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
-
36
- /// @notice Verifies the fix for H-2: Pending reserve NFTs are now included in the cash-out weight
37
- /// denominator. Before the fix, paid holders could cash out before reserves were minted and extract
38
- /// more than their fair share.
39
- ///
40
- /// With BWA + HHI-adjusted quorum, a single-tier winner-take-all scorecard gives the sole beneficiary
41
- /// 0 attestation power (BWA multiplier = 1 - 1 = 0). To allow ratification, we add 3 disinterested
42
- /// tiers (weight = 0) whose attestors provide governance power to meet the adjusted quorum.
43
- contract FixPendingReserveDilutionTest is JBTest, TestBaseWorkflow {
44
- using JBRulesetMetadataResolver for JBRuleset;
45
-
46
- address _protocolFeeProjectTokenAccount;
47
- address _defifaProjectTokenAccount;
48
- uint256 _protocolFeeProjectId;
49
- uint256 _defifaProjectId;
50
- uint256 _gameId = 3;
51
-
52
- DefifaDeployer deployer;
53
- DefifaHook hook;
54
- DefifaGovernor governor;
55
-
56
- address projectOwner = address(bytes20(keccak256("projectOwner")));
57
- address reserveBeneficiary = address(bytes20(keccak256("reserveBeneficiary")));
58
- address player = address(bytes20(keccak256("player")));
59
- address disinterested1 = address(bytes20(keccak256("disinterested1")));
60
- address disinterested2 = address(bytes20(keccak256("disinterested2")));
61
- address disinterested3 = address(bytes20(keccak256("disinterested3")));
62
-
63
- uint256 _pid;
64
- DefifaHook _nft;
65
- DefifaGovernor _gov;
66
-
67
- function setUp() public virtual override {
68
- super.setUp();
69
-
70
- JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
71
- _tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
72
- JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
73
- tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
74
- JBRulesetConfig[] memory rc = new JBRulesetConfig[](1);
75
- rc[0] = JBRulesetConfig({
76
- mustStartAtOrAfter: 0,
77
- duration: 10 days,
78
- weight: 1e18,
79
- weightCutPercent: 0,
80
- approvalHook: IJBRulesetApprovalHook(address(0)),
81
- metadata: JBRulesetMetadata({
82
- reservedPercent: 0,
83
- cashOutTaxRate: 0,
84
- baseCurrency: JBCurrencyIds.ETH,
85
- pausePay: false,
86
- pauseCreditTransfers: false,
87
- allowOwnerMinting: false,
88
- allowSetCustomToken: false,
89
- allowTerminalMigration: false,
90
- allowSetTerminals: false,
91
- allowSetController: false,
92
- allowAddAccountingContext: false,
93
- allowAddPriceFeed: false,
94
- ownerMustSendPayouts: false,
95
- holdFees: false,
96
- useTotalSurplusForCashOuts: false,
97
- useDataHookForPay: true,
98
- useDataHookForCashOut: true,
99
- dataHook: address(0),
100
- metadata: 0
101
- }),
102
- splitGroups: new JBSplitGroup[](0),
103
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
104
- });
105
-
106
- _protocolFeeProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
107
- vm.prank(projectOwner);
108
- _protocolFeeProjectTokenAccount =
109
- address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
110
- _defifaProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
111
- vm.prank(projectOwner);
112
- _defifaProjectTokenAccount =
113
- address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
114
-
115
- hook =
116
- new DefifaHook(jbDirectory(), IERC20(_defifaProjectTokenAccount), IERC20(_protocolFeeProjectTokenAccount));
117
- governor = new DefifaGovernor(jbController(), address(this));
118
- deployer = new DefifaDeployer(
119
- address(hook),
120
- new DefifaTokenUriResolver(ITypeface(address(0))),
121
- governor,
122
- jbController(),
123
- new JBAddressRegistry(),
124
- _protocolFeeProjectId,
125
- _defifaProjectId
126
- );
127
- hook.transferOwnership(address(deployer));
128
- governor.transferOwnership(address(deployer));
129
- }
130
-
131
- /// @notice With the fix, a paid holder's cash-out share is diluted by pending reserves.
132
- /// The paid holder should NOT be able to reclaim the full post-fee surplus when pending
133
- /// reserves exist -- the reserve holder's share must be protected.
134
- function test_paidHolderCashOutDilutedByPendingReserves() external {
135
- (_pid, _nft, _gov) = _launch(_launchData());
136
-
137
- // Mint phase: player mints 1 NFT into tier 1, disinterested users mint tiers 2-4.
138
- vm.warp(block.timestamp + 1 days + 1);
139
- _mint(player, 1, 1 ether);
140
- _delegateSelf(player, 1);
141
- vm.warp(block.timestamp + 1);
142
- _mint(disinterested1, 2, 1 ether);
143
- _delegateSelf(disinterested1, 2);
144
- vm.warp(block.timestamp + 1);
145
- _mint(disinterested2, 3, 1 ether);
146
- _delegateSelf(disinterested2, 3);
147
- vm.warp(block.timestamp + 1);
148
- _mint(disinterested3, 4, 1 ether);
149
- _delegateSelf(disinterested3, 4);
150
-
151
- // Verify there is a pending reserve for tier 1.
152
- assertEq(_nft.store().numberOfPendingReservesFor(address(_nft), 1), 1, "one reserve should be pending");
153
-
154
- // Advance to scoring phase.
155
- vm.warp(block.timestamp + 2 days + 1);
156
-
157
- // Submit scorecard giving all weight to tier 1; tiers 2-4 get 0 (disinterested attestors).
158
- DefifaTierCashOutWeight[] memory sc = new DefifaTierCashOutWeight[](4);
159
- sc[0] = DefifaTierCashOutWeight({id: 1, cashOutWeight: _nft.TOTAL_CASHOUT_WEIGHT()});
160
- sc[1] = DefifaTierCashOutWeight({id: 2, cashOutWeight: 0});
161
- sc[2] = DefifaTierCashOutWeight({id: 3, cashOutWeight: 0});
162
- sc[3] = DefifaTierCashOutWeight({id: 4, cashOutWeight: 0});
163
- uint256 proposalId = _gov.submitScorecardFor(_gameId, sc);
164
-
165
- // Disinterested users attest (they have full BWA power since their tiers get 0 weight).
166
- // The player (tier 1, 100% weight) has 0 BWA power and cannot meaningfully attest.
167
- vm.prank(disinterested1);
168
- _gov.attestToScorecardFrom(_gameId, proposalId);
169
- vm.prank(disinterested2);
170
- _gov.attestToScorecardFrom(_gameId, proposalId);
171
- vm.prank(disinterested3);
172
- _gov.attestToScorecardFrom(_gameId, proposalId);
173
-
174
- vm.warp(block.timestamp + _gov.attestationGracePeriodOf(_gameId) + 1);
175
- _gov.ratifyScorecardFrom(_gameId, sc);
176
-
177
- // The player should only reclaim HALF of their tier's share of the post-fee surplus
178
- // (1 of 2 tokens in tier 1, since the pending reserve counts in the denominator).
179
- // Note: total pot includes 4 ETH from all minters but tier 1 gets 100% of weight.
180
- // Fees are taken from the terminal surplus. Post-fee surplus is available for cash-out.
181
- uint256 postFeeSurplus = 4 ether - (4 ether / 20) - (4 ether / 40);
182
- // Tier 1 gets 100% weight. Player holds 1 of 2 units (1 paid + 1 pending reserve).
183
- uint256 expectedPlayerReclaim = postFeeSurplus / 2;
184
-
185
- uint256 beforePlayerBalance = player.balance;
186
- _cashOut(player, 1, 1);
187
- uint256 playerReclaim = player.balance - beforePlayerBalance;
188
-
189
- // The player should receive approximately half the surplus, not the full amount.
190
- assertApproxEqAbs(
191
- playerReclaim,
192
- expectedPlayerReclaim,
193
- 1, // 1 wei tolerance for rounding
194
- "paid holder should only reclaim half due to pending reserve dilution"
195
- );
196
-
197
- // Specifically, the player should NOT get the full pot.
198
- assertLt(playerReclaim, postFeeSurplus, "paid holder should NOT reclaim full surplus with pending reserves");
199
- }
200
-
201
- /// @notice After reserves are minted, the reserve holder should be able to cash out their share.
202
- function test_reserveHolderCanCashOutAfterMinting() external {
203
- (_pid, _nft, _gov) = _launch(_launchData());
204
-
205
- // Mint phase: player mints 1 NFT into tier 1, disinterested users mint tiers 2-4.
206
- vm.warp(block.timestamp + 1 days + 1);
207
- _mint(player, 1, 1 ether);
208
- _delegateSelf(player, 1);
209
- vm.warp(block.timestamp + 1);
210
- _mint(disinterested1, 2, 1 ether);
211
- _delegateSelf(disinterested1, 2);
212
- vm.warp(block.timestamp + 1);
213
- _mint(disinterested2, 3, 1 ether);
214
- _delegateSelf(disinterested2, 3);
215
- vm.warp(block.timestamp + 1);
216
- _mint(disinterested3, 4, 1 ether);
217
- _delegateSelf(disinterested3, 4);
218
-
219
- // Advance to scoring phase.
220
- vm.warp(block.timestamp + 2 days + 1);
221
-
222
- // Submit scorecard: tier 1 gets all weight; tiers 2-4 are disinterested.
223
- DefifaTierCashOutWeight[] memory sc = new DefifaTierCashOutWeight[](4);
224
- sc[0] = DefifaTierCashOutWeight({id: 1, cashOutWeight: _nft.TOTAL_CASHOUT_WEIGHT()});
225
- sc[1] = DefifaTierCashOutWeight({id: 2, cashOutWeight: 0});
226
- sc[2] = DefifaTierCashOutWeight({id: 3, cashOutWeight: 0});
227
- sc[3] = DefifaTierCashOutWeight({id: 4, cashOutWeight: 0});
228
- uint256 proposalId = _gov.submitScorecardFor(_gameId, sc);
229
-
230
- // Disinterested users attest (full BWA power since 0 weight tiers).
231
- vm.prank(disinterested1);
232
- _gov.attestToScorecardFrom(_gameId, proposalId);
233
- vm.prank(disinterested2);
234
- _gov.attestToScorecardFrom(_gameId, proposalId);
235
- vm.prank(disinterested3);
236
- _gov.attestToScorecardFrom(_gameId, proposalId);
237
-
238
- vm.warp(block.timestamp + _gov.attestationGracePeriodOf(_gameId) + 1);
239
- _gov.ratifyScorecardFrom(_gameId, sc);
240
-
241
- // Mint the reserve NFTs for tier 1.
242
- JB721TiersMintReservesConfig[] memory reserveConfigs = new JB721TiersMintReservesConfig[](1);
243
- reserveConfigs[0] = JB721TiersMintReservesConfig({tierId: 1, count: 1});
244
- _nft.mintReservesFor(reserveConfigs);
245
- assertEq(_nft.balanceOf(reserveBeneficiary), 1, "reserve NFT should be minted");
246
-
247
- // Now player cashes out.
248
- uint256 beforePlayerBalance = player.balance;
249
- _cashOut(player, 1, 1);
250
- uint256 playerReclaim = player.balance - beforePlayerBalance;
251
-
252
- // Reserve holder cashes out (token ID for tier 1, token number 2).
253
- uint256 beforeReserveBalance = reserveBeneficiary.balance;
254
- _cashOut(reserveBeneficiary, 1, 2);
255
- uint256 reserveReclaim = reserveBeneficiary.balance - beforeReserveBalance;
256
-
257
- // Both should get approximately equal shares.
258
- assertApproxEqAbs(
259
- playerReclaim,
260
- reserveReclaim,
261
- 1, // 1 wei tolerance
262
- "paid and reserve holders should get equal shares"
263
- );
264
- }
265
-
266
- // ---- helpers ----
267
-
268
- function _launchData() internal returns (DefifaLaunchProjectData memory) {
269
- DefifaTierParams[] memory tp = new DefifaTierParams[](4);
270
- // Tier 1: has reserves (the tier under test)
271
- tp[0] = DefifaTierParams({
272
- reservedRate: 1, // 1 reserve per mint
273
- reservedTokenBeneficiary: reserveBeneficiary,
274
- encodedIPFSUri: bytes32(0),
275
- shouldUseReservedTokenBeneficiaryAsDefault: false,
276
- name: "TEAM"
277
- });
278
- // Tiers 2-4: disinterested attestors (no reserves, standard rate)
279
- for (uint256 i = 1; i < 4; i++) {
280
- tp[i] = DefifaTierParams({
281
- reservedRate: 1001,
282
- reservedTokenBeneficiary: address(0),
283
- encodedIPFSUri: bytes32(0),
284
- shouldUseReservedTokenBeneficiaryAsDefault: false,
285
- name: "TEAM"
286
- });
287
- }
288
-
289
- return DefifaLaunchProjectData({
290
- name: "DEFIFA",
291
- projectUri: "",
292
- contractUri: "",
293
- baseUri: "",
294
- token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
295
- mintPeriodDuration: 1 days,
296
- start: uint48(block.timestamp + 3 days),
297
- refundPeriodDuration: 1 days,
298
- store: new JB721TiersHookStore(),
299
- splits: new JBSplit[](0),
300
- attestationStartTime: 0,
301
- attestationGracePeriod: 100_381,
302
- defaultAttestationDelegate: address(0),
303
- tierPrice: 1 ether,
304
- tiers: tp,
305
- defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
306
- terminal: jbMultiTerminal(),
307
- minParticipation: 0,
308
- scorecardTimeout: 0,
309
- timelockDuration: 0
310
- });
311
- }
312
-
313
- function _launch(DefifaLaunchProjectData memory d) internal returns (uint256 p, DefifaHook n, DefifaGovernor g) {
314
- g = governor;
315
- p = deployer.launchGameWith(d);
316
- JBRuleset memory fc = jbRulesets().currentOf(p);
317
- if (fc.dataHook() == address(0)) (fc,) = jbRulesets().latestQueuedOf(p);
318
- n = DefifaHook(fc.dataHook());
319
- }
320
-
321
- function _mint(address user, uint256 tid, uint256 amt) internal {
322
- vm.deal(user, amt);
323
- uint16[] memory m = new uint16[](1);
324
- // forge-lint: disable-next-line(unsafe-typecast)
325
- m[0] = uint16(tid);
326
- bytes[] memory data = new bytes[](1);
327
- data[0] = abi.encode(user, m);
328
- bytes4[] memory ids = new bytes4[](1);
329
- ids[0] = metadataHelper().getId("pay", address(hook));
330
- bytes memory metadata = metadataHelper().createMetadata(ids, data);
331
- vm.prank(user);
332
- jbMultiTerminal().pay{value: amt}(_pid, JBConstants.NATIVE_TOKEN, amt, user, 0, "", metadata);
333
- }
334
-
335
- function _delegateSelf(address user, uint256 tid) internal {
336
- DefifaDelegation[] memory dd = new DefifaDelegation[](1);
337
- dd[0] = DefifaDelegation({delegatee: user, tierId: tid});
338
- vm.prank(user);
339
- _nft.setTierDelegatesTo(dd);
340
- }
341
-
342
- function _cashOut(address user, uint256 tid, uint256 tnum) internal {
343
- bytes memory meta = _cashOutMeta(tid, tnum);
344
- vm.prank(user);
345
- jbMultiTerminal()
346
- .cashOutTokensOf({
347
- holder: user,
348
- projectId: _pid,
349
- cashOutCount: 0,
350
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
351
- minTokensReclaimed: 0,
352
- beneficiary: payable(user),
353
- metadata: meta
354
- });
355
- }
356
-
357
- function _cashOutMeta(uint256 tid, uint256 tnum) internal view returns (bytes memory) {
358
- uint256[] memory cid = new uint256[](1);
359
- cid[0] = (tid * 1_000_000_000) + tnum;
360
- bytes[] memory data = new bytes[](1);
361
- data[0] = abi.encode(cid);
362
- bytes4[] memory ids = new bytes4[](1);
363
- ids[0] = metadataHelper().getId("cashOut", address(hook));
364
- return metadataHelper().createMetadata(ids, data);
365
- }
366
- }
@@ -1,184 +0,0 @@
1
- // SPDX-License-Identifier: UNLICENSED
2
- pragma solidity 0.8.28;
3
-
4
- import {DefifaDeployer} from "../../src/DefifaDeployer.sol";
5
- import {DefifaGovernor} from "../../src/DefifaGovernor.sol";
6
- import {DefifaHook} from "../../src/DefifaHook.sol";
7
- import {DefifaTokenUriResolver} from "../../src/DefifaTokenUriResolver.sol";
8
- import {DefifaLaunchProjectData} from "../../src/structs/DefifaLaunchProjectData.sol";
9
- import {DefifaTierParams} from "../../src/structs/DefifaTierParams.sol";
10
-
11
- import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
12
- import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
13
- import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
14
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
15
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
16
- import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
17
- import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
18
- import {JBRulesetConfig, JBTerminalConfig} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
19
- import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
20
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
21
- import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
22
- import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
23
- import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
24
- import {TestBaseWorkflow} from "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
25
-
26
- import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
27
- import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
28
-
29
- /// @notice Tests for H-5 audit fix: tier cap of 128 enforced in launchGameWith().
30
- contract H5TierCapValidationTest is JBTest, TestBaseWorkflow {
31
- DefifaDeployer internal deployer;
32
- DefifaGovernor internal governor;
33
- DefifaHook internal hookCodeOrigin;
34
-
35
- function setUp() public virtual override {
36
- super.setUp();
37
-
38
- JBAccountingContext[] memory tokens = new JBAccountingContext[](1);
39
- tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
40
-
41
- JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
42
- terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: tokens});
43
-
44
- JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
45
- rulesetConfigs[0] = JBRulesetConfig({
46
- mustStartAtOrAfter: 0,
47
- duration: 10 days,
48
- weight: 1e18,
49
- weightCutPercent: 0,
50
- approvalHook: IJBRulesetApprovalHook(address(0)),
51
- metadata: JBRulesetMetadata({
52
- reservedPercent: 0,
53
- cashOutTaxRate: 0,
54
- baseCurrency: JBCurrencyIds.ETH,
55
- pausePay: false,
56
- pauseCreditTransfers: false,
57
- allowOwnerMinting: false,
58
- allowSetCustomToken: false,
59
- allowTerminalMigration: false,
60
- allowSetTerminals: false,
61
- allowSetController: false,
62
- allowAddAccountingContext: false,
63
- allowAddPriceFeed: false,
64
- ownerMustSendPayouts: false,
65
- holdFees: false,
66
- useTotalSurplusForCashOuts: false,
67
- useDataHookForPay: true,
68
- useDataHookForCashOut: true,
69
- dataHook: address(0),
70
- metadata: 0
71
- }),
72
- splitGroups: new JBSplitGroup[](0),
73
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
74
- });
75
-
76
- address projectOwner = address(bytes20(keccak256("projectOwner")));
77
- uint256 protocolFeeProjectId =
78
- jbController().launchProjectFor(projectOwner, "", rulesetConfigs, terminalConfigs, "");
79
- vm.prank(projectOwner);
80
- address nanaToken =
81
- address(jbController().deployERC20For(protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
82
-
83
- uint256 defifaProjectId = jbController().launchProjectFor(projectOwner, "", rulesetConfigs, terminalConfigs, "");
84
- vm.prank(projectOwner);
85
- address defifaToken = address(jbController().deployERC20For(defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
86
-
87
- hookCodeOrigin = new DefifaHook(jbDirectory(), IERC20(defifaToken), IERC20(nanaToken));
88
- governor = new DefifaGovernor(jbController(), address(this));
89
- deployer = new DefifaDeployer(
90
- address(hookCodeOrigin),
91
- new DefifaTokenUriResolver(ITypeface(address(0))),
92
- governor,
93
- jbController(),
94
- new JBAddressRegistry(),
95
- defifaProjectId,
96
- protocolFeeProjectId
97
- );
98
-
99
- hookCodeOrigin.transferOwnership(address(deployer));
100
- governor.transferOwnership(address(deployer));
101
- }
102
-
103
- /// @notice Launching with exactly 128 tiers should succeed (boundary).
104
- function test_launch128TiersSucceeds() external {
105
- DefifaLaunchProjectData memory data = _launchData(128);
106
- uint256 gameId = deployer.launchGameWith(data);
107
- assertGt(gameId, 0, "game should be created with 128 tiers");
108
- }
109
-
110
- /// @notice Launching with 129 tiers must revert with DefifaDeployer_InvalidGameConfiguration.
111
- function test_launch129TiersReverts() external {
112
- DefifaLaunchProjectData memory data = _launchData(129);
113
- vm.expectRevert(DefifaDeployer.DefifaDeployer_InvalidGameConfiguration.selector);
114
- deployer.launchGameWith(data);
115
- }
116
-
117
- /// @notice Launching with 1 tier should succeed (minimum valid).
118
- function test_launch1TierSucceeds() external {
119
- DefifaLaunchProjectData memory data = _launchData(1);
120
- uint256 gameId = deployer.launchGameWith(data);
121
- assertGt(gameId, 0, "game should be created with 1 tier");
122
- }
123
-
124
- /// @notice Launching with 0 tiers does not revert at the tier cap check (no lower-bound validation exists).
125
- /// @dev This documents current behavior: the deployer only enforces the upper cap of 128.
126
- function test_launch0TiersDoesNotRevertAtTierCap() external {
127
- DefifaLaunchProjectData memory data = _launchData(0);
128
- uint256 gameId = deployer.launchGameWith(data);
129
- assertGt(gameId, 0, "0-tier game created (no lower-bound check)");
130
- }
131
-
132
- /// @notice Fuzz: any tier count above 128 reverts, any from 1-128 succeeds.
133
- function test_fuzz_tierCapBoundary(uint256 tierCount) external {
134
- tierCount = bound(tierCount, 1, 256);
135
- DefifaLaunchProjectData memory data = _launchData(tierCount);
136
-
137
- if (tierCount > 128) {
138
- vm.expectRevert(DefifaDeployer.DefifaDeployer_InvalidGameConfiguration.selector);
139
- deployer.launchGameWith(data);
140
- } else {
141
- uint256 gameId = deployer.launchGameWith(data);
142
- assertGt(gameId, 0, "game should be created within tier cap");
143
- }
144
- }
145
-
146
- // ─── Helpers
147
- // ─────────────────────────────────────────────────────────────────
148
-
149
- function _launchData(uint256 tierCount) internal returns (DefifaLaunchProjectData memory) {
150
- DefifaTierParams[] memory tiers = new DefifaTierParams[](tierCount);
151
- for (uint256 i; i < tierCount; i++) {
152
- tiers[i] = DefifaTierParams({
153
- name: "TEAM",
154
- reservedRate: 0,
155
- reservedTokenBeneficiary: address(0),
156
- encodedIPFSUri: bytes32(0),
157
- shouldUseReservedTokenBeneficiaryAsDefault: false
158
- });
159
- }
160
-
161
- return DefifaLaunchProjectData({
162
- name: "DEFIFA",
163
- projectUri: "",
164
- contractUri: "",
165
- baseUri: "",
166
- tiers: tiers,
167
- tierPrice: 1 ether,
168
- token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
169
- mintPeriodDuration: 1 days,
170
- refundPeriodDuration: 1 days,
171
- start: uint48(block.timestamp + 3 days),
172
- splits: new JBSplit[](0),
173
- attestationStartTime: 0,
174
- attestationGracePeriod: 100_381,
175
- defaultAttestationDelegate: address(0),
176
- defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
177
- terminal: jbMultiTerminal(),
178
- store: new JB721TiersHookStore(),
179
- minParticipation: 0,
180
- scorecardTimeout: 7 days,
181
- timelockDuration: 0
182
- });
183
- }
184
- }