@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,298 +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
- contract PendingReserveDilutionTest is JBTest, TestBaseWorkflow {
37
- using JBRulesetMetadataResolver for JBRuleset;
38
-
39
- address _protocolFeeProjectTokenAccount;
40
- address _defifaProjectTokenAccount;
41
- uint256 _protocolFeeProjectId;
42
- uint256 _defifaProjectId;
43
- uint256 _gameId = 3;
44
-
45
- DefifaDeployer deployer;
46
- DefifaHook hook;
47
- DefifaGovernor governor;
48
-
49
- address projectOwner = address(bytes20(keccak256("projectOwner")));
50
- address reserveBeneficiary = address(bytes20(keccak256("reserveBeneficiary")));
51
- address player = address(bytes20(keccak256("player")));
52
- address disinterested1 = address(bytes20(keccak256("disinterested1")));
53
- address disinterested2 = address(bytes20(keccak256("disinterested2")));
54
- address disinterested3 = address(bytes20(keccak256("disinterested3")));
55
-
56
- uint256 _pid;
57
- DefifaHook _nft;
58
- DefifaGovernor _gov;
59
-
60
- function setUp() public virtual override {
61
- super.setUp();
62
-
63
- JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
64
- _tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
65
- JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
66
- tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
67
- JBRulesetConfig[] memory rc = new JBRulesetConfig[](1);
68
- rc[0] = JBRulesetConfig({
69
- mustStartAtOrAfter: 0,
70
- duration: 10 days,
71
- weight: 1e18,
72
- weightCutPercent: 0,
73
- approvalHook: IJBRulesetApprovalHook(address(0)),
74
- metadata: JBRulesetMetadata({
75
- reservedPercent: 0,
76
- cashOutTaxRate: 0,
77
- baseCurrency: JBCurrencyIds.ETH,
78
- pausePay: false,
79
- pauseCreditTransfers: false,
80
- allowOwnerMinting: false,
81
- allowSetCustomToken: false,
82
- allowTerminalMigration: false,
83
- allowSetTerminals: false,
84
- allowSetController: false,
85
- allowAddAccountingContext: false,
86
- allowAddPriceFeed: false,
87
- ownerMustSendPayouts: false,
88
- holdFees: false,
89
- useTotalSurplusForCashOuts: false,
90
- useDataHookForPay: true,
91
- useDataHookForCashOut: true,
92
- dataHook: address(0),
93
- metadata: 0
94
- }),
95
- splitGroups: new JBSplitGroup[](0),
96
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
97
- });
98
-
99
- _protocolFeeProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
100
- vm.prank(projectOwner);
101
- _protocolFeeProjectTokenAccount =
102
- address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
103
- _defifaProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
104
- vm.prank(projectOwner);
105
- _defifaProjectTokenAccount =
106
- address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
107
-
108
- hook =
109
- new DefifaHook(jbDirectory(), IERC20(_defifaProjectTokenAccount), IERC20(_protocolFeeProjectTokenAccount));
110
- governor = new DefifaGovernor(jbController(), address(this));
111
- deployer = new DefifaDeployer(
112
- address(hook),
113
- new DefifaTokenUriResolver(ITypeface(address(0))),
114
- governor,
115
- jbController(),
116
- new JBAddressRegistry(),
117
- _protocolFeeProjectId,
118
- _defifaProjectId
119
- );
120
- hook.transferOwnership(address(deployer));
121
- governor.transferOwnership(address(deployer));
122
- }
123
-
124
- /// @notice After the H-2 fix, pending reserves dilute the paid holder's cash-out share.
125
- /// The paid holder can no longer drain the full surplus; reserve holders retain their share.
126
- ///
127
- /// With BWA + HHI, a single-tier winner-take-all scorecard gives the beneficiary 0 attestation
128
- /// power. We add 3 disinterested tiers (0 weight) so their attestors can meet the adjusted quorum.
129
- function test_pendingReserveDilutesPaidHolderCashOut_afterFix() external {
130
- (_pid, _nft, _gov) = _launch(_launchData());
131
-
132
- // Mint phase: player mints tier 1, disinterested users mint tiers 2-4.
133
- vm.warp(block.timestamp + 1 days + 1);
134
- _mint(player, 1, 1 ether);
135
- _delegateSelf(player, 1);
136
- vm.warp(block.timestamp + 1);
137
- _mint(disinterested1, 2, 1 ether);
138
- _delegateSelf(disinterested1, 2);
139
- vm.warp(block.timestamp + 1);
140
- _mint(disinterested2, 3, 1 ether);
141
- _delegateSelf(disinterested2, 3);
142
- vm.warp(block.timestamp + 1);
143
- _mint(disinterested3, 4, 1 ether);
144
- _delegateSelf(disinterested3, 4);
145
-
146
- assertEq(_nft.store().numberOfPendingReservesFor(address(_nft), 1), 1, "one reserve should be pending");
147
-
148
- vm.warp(block.timestamp + 2 days + 1);
149
-
150
- // Scorecard: tier 1 gets all weight; tiers 2-4 get 0.
151
- DefifaTierCashOutWeight[] memory sc = new DefifaTierCashOutWeight[](4);
152
- sc[0] = DefifaTierCashOutWeight({id: 1, cashOutWeight: _nft.TOTAL_CASHOUT_WEIGHT()});
153
- sc[1] = DefifaTierCashOutWeight({id: 2, cashOutWeight: 0});
154
- sc[2] = DefifaTierCashOutWeight({id: 3, cashOutWeight: 0});
155
- sc[3] = DefifaTierCashOutWeight({id: 4, cashOutWeight: 0});
156
- uint256 proposalId = _gov.submitScorecardFor(_gameId, sc);
157
-
158
- // Disinterested users attest (full BWA power since 0 weight tiers).
159
- vm.prank(disinterested1);
160
- _gov.attestToScorecardFrom(_gameId, proposalId);
161
- vm.prank(disinterested2);
162
- _gov.attestToScorecardFrom(_gameId, proposalId);
163
- vm.prank(disinterested3);
164
- _gov.attestToScorecardFrom(_gameId, proposalId);
165
-
166
- vm.warp(block.timestamp + _gov.attestationGracePeriodOf(_gameId) + 1);
167
- _gov.ratifyScorecardFrom(_gameId, sc);
168
-
169
- // Post-fee surplus from all 4 ETH (4 minters). Tier 1 gets 100% weight.
170
- uint256 totalPot = 4 ether - (4 ether / 20) - (4 ether / 40);
171
- // Player holds 1 of 2 units in tier 1 (1 paid + 1 pending reserve in denominator).
172
- uint256 expectedPlayerReclaim = totalPot / 2;
173
-
174
- uint256 beforePlayerBalance = player.balance;
175
- _cashOut(player, 1, 1);
176
- uint256 playerReclaim = player.balance - beforePlayerBalance;
177
-
178
- // After the fix: paid holder gets only HALF because pending reserve dilutes the denominator.
179
- assertApproxEqAbs(
180
- playerReclaim,
181
- expectedPlayerReclaim,
182
- 1, // 1 wei tolerance for rounding
183
- "paid holder reclaims only half due to pending reserve dilution"
184
- );
185
- assertLt(playerReclaim, totalPot, "paid holder should NOT reclaim full surplus");
186
-
187
- // Mint the reserve NFTs.
188
- JB721TiersMintReservesConfig[] memory reserveConfigs = new JB721TiersMintReservesConfig[](1);
189
- reserveConfigs[0] = JB721TiersMintReservesConfig({tierId: 1, count: 1});
190
- _nft.mintReservesFor(reserveConfigs);
191
- assertEq(_nft.balanceOf(reserveBeneficiary), 1, "reserve NFT minted successfully");
192
-
193
- // Reserve holder should now be able to cash out their share (not zero).
194
- uint256 beforeReserveBalance = reserveBeneficiary.balance;
195
- _cashOut(reserveBeneficiary, 1, 2);
196
- uint256 reserveReclaim = reserveBeneficiary.balance - beforeReserveBalance;
197
- assertGt(reserveReclaim, 0, "reserve holder can reclaim their share after fix");
198
- }
199
-
200
- function _launchData() internal returns (DefifaLaunchProjectData memory) {
201
- DefifaTierParams[] memory tp = new DefifaTierParams[](4);
202
- // Tier 1: has reserves (the tier under test)
203
- tp[0] = DefifaTierParams({
204
- reservedRate: 1,
205
- reservedTokenBeneficiary: reserveBeneficiary,
206
- encodedIPFSUri: bytes32(0),
207
- shouldUseReservedTokenBeneficiaryAsDefault: false,
208
- name: "TEAM"
209
- });
210
- // Tiers 2-4: disinterested attestors (no reserves)
211
- for (uint256 i = 1; i < 4; i++) {
212
- tp[i] = DefifaTierParams({
213
- reservedRate: 1001,
214
- reservedTokenBeneficiary: address(0),
215
- encodedIPFSUri: bytes32(0),
216
- shouldUseReservedTokenBeneficiaryAsDefault: false,
217
- name: "TEAM"
218
- });
219
- }
220
-
221
- return DefifaLaunchProjectData({
222
- name: "DEFIFA",
223
- projectUri: "",
224
- contractUri: "",
225
- baseUri: "",
226
- token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
227
- mintPeriodDuration: 1 days,
228
- start: uint48(block.timestamp + 3 days),
229
- refundPeriodDuration: 1 days,
230
- store: new JB721TiersHookStore(),
231
- splits: new JBSplit[](0),
232
- attestationStartTime: 0,
233
- attestationGracePeriod: 100_381,
234
- defaultAttestationDelegate: address(0),
235
- tierPrice: 1 ether,
236
- tiers: tp,
237
- defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
238
- terminal: jbMultiTerminal(),
239
- minParticipation: 0,
240
- scorecardTimeout: 0,
241
- timelockDuration: 0
242
- });
243
- }
244
-
245
- function _launch(DefifaLaunchProjectData memory d) internal returns (uint256 p, DefifaHook n, DefifaGovernor g) {
246
- g = governor;
247
- p = deployer.launchGameWith(d);
248
- JBRuleset memory fc = jbRulesets().currentOf(p);
249
- if (fc.dataHook() == address(0)) (fc,) = jbRulesets().latestQueuedOf(p);
250
- n = DefifaHook(fc.dataHook());
251
- }
252
-
253
- function _mint(address user, uint256 tid, uint256 amt) internal {
254
- vm.deal(user, amt);
255
- uint16[] memory m = new uint16[](1);
256
- // forge-lint: disable-next-line(unsafe-typecast)
257
- m[0] = uint16(tid);
258
- bytes[] memory data = new bytes[](1);
259
- data[0] = abi.encode(user, m);
260
- bytes4[] memory ids = new bytes4[](1);
261
- ids[0] = metadataHelper().getId("pay", address(hook));
262
- bytes memory metadata = metadataHelper().createMetadata(ids, data);
263
- vm.prank(user);
264
- jbMultiTerminal().pay{value: amt}(_pid, JBConstants.NATIVE_TOKEN, amt, user, 0, "", metadata);
265
- }
266
-
267
- function _delegateSelf(address user, uint256 tid) internal {
268
- DefifaDelegation[] memory dd = new DefifaDelegation[](1);
269
- dd[0] = DefifaDelegation({delegatee: user, tierId: tid});
270
- vm.prank(user);
271
- _nft.setTierDelegatesTo(dd);
272
- }
273
-
274
- function _cashOut(address user, uint256 tid, uint256 tnum) internal {
275
- bytes memory meta = _cashOutMeta(tid, tnum);
276
- vm.prank(user);
277
- jbMultiTerminal()
278
- .cashOutTokensOf({
279
- holder: user,
280
- projectId: _pid,
281
- cashOutCount: 0,
282
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
283
- minTokensReclaimed: 0,
284
- beneficiary: payable(user),
285
- metadata: meta
286
- });
287
- }
288
-
289
- function _cashOutMeta(uint256 tid, uint256 tnum) internal view returns (bytes memory) {
290
- uint256[] memory cid = new uint256[](1);
291
- cid[0] = (tid * 1_000_000_000) + tnum;
292
- bytes[] memory data = new bytes[](1);
293
- data[0] = abi.encode(cid);
294
- bytes4[] memory ids = new bytes4[](1);
295
- ids[0] = metadataHelper().getId("cashOut", address(hook));
296
- return metadataHelper().createMetadata(ids, data);
297
- }
298
- }
@@ -1,355 +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 {DefifaScorecardState} from "../../src/enums/DefifaScorecardState.sol";
26
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
27
- import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
28
- import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
29
- import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
30
- import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
31
- import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
32
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
33
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
34
- import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
35
- import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
36
-
37
- contract PendingReserveQuorumGriefTest is JBTest, TestBaseWorkflow {
38
- using JBRulesetMetadataResolver for JBRuleset;
39
-
40
- uint256 internal _protocolFeeProjectId;
41
- uint256 internal _defifaProjectId;
42
- uint256 internal _gameId = 3;
43
-
44
- DefifaDeployer internal _deployer;
45
- DefifaHook internal _hookImpl;
46
- DefifaGovernor internal _governorImpl;
47
-
48
- address internal _projectOwner = address(bytes20(keccak256("projectOwner")));
49
- address internal _reserveBeneficiary = address(bytes20(keccak256("reserveBeneficiary")));
50
- address internal _player0 = address(bytes20(keccak256("player0")));
51
- address internal _player1 = address(bytes20(keccak256("player1")));
52
- address internal _player2 = address(bytes20(keccak256("player2")));
53
- address internal _player3 = address(bytes20(keccak256("player3")));
54
-
55
- DefifaHook internal _nft;
56
- DefifaGovernor internal _gov;
57
- uint256 internal _pid;
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
-
65
- JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
66
- terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: tokens});
67
-
68
- JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
69
- rulesetConfigs[0] = JBRulesetConfig({
70
- mustStartAtOrAfter: 0,
71
- duration: 10 days,
72
- weight: 1e18,
73
- weightCutPercent: 0,
74
- approvalHook: IJBRulesetApprovalHook(address(0)),
75
- metadata: JBRulesetMetadata({
76
- reservedPercent: 0,
77
- cashOutTaxRate: 0,
78
- baseCurrency: JBCurrencyIds.ETH,
79
- pausePay: false,
80
- pauseCreditTransfers: false,
81
- allowOwnerMinting: false,
82
- allowSetCustomToken: false,
83
- allowTerminalMigration: false,
84
- allowSetTerminals: false,
85
- allowSetController: false,
86
- allowAddAccountingContext: false,
87
- allowAddPriceFeed: false,
88
- ownerMustSendPayouts: false,
89
- holdFees: false,
90
- useTotalSurplusForCashOuts: false,
91
- useDataHookForPay: true,
92
- useDataHookForCashOut: true,
93
- dataHook: address(0),
94
- metadata: 0
95
- }),
96
- splitGroups: new JBSplitGroup[](0),
97
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
98
- });
99
-
100
- _protocolFeeProjectId =
101
- jbController().launchProjectFor(address(_projectOwner), "", rulesetConfigs, terminalConfigs, "");
102
- vm.prank(_projectOwner);
103
- address nanaToken =
104
- address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
105
-
106
- _defifaProjectId =
107
- jbController().launchProjectFor(address(_projectOwner), "", rulesetConfigs, terminalConfigs, "");
108
- vm.prank(_projectOwner);
109
- address defifaToken = address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
110
-
111
- _hookImpl = new DefifaHook(jbDirectory(), IERC20(defifaToken), IERC20(nanaToken));
112
- _governorImpl = new DefifaGovernor(jbController(), address(this));
113
- _deployer = new DefifaDeployer(
114
- address(_hookImpl),
115
- new DefifaTokenUriResolver(ITypeface(address(0))),
116
- _governorImpl,
117
- jbController(),
118
- new JBAddressRegistry(),
119
- _defifaProjectId,
120
- _protocolFeeProjectId
121
- );
122
-
123
- _hookImpl.transferOwnership(address(_deployer));
124
- _governorImpl.transferOwnership(address(_deployer));
125
- }
126
-
127
- /// @notice RPT-H-2 FIX VERIFICATION: Pending reserves in quorum + attestation denominator prevent manipulation.
128
- ///
129
- /// 1. Four players mint into tiers 1-4 (tiers 1-2 have reserveRate=1, creating pending reserves;
130
- /// tiers 3-4 have no reserves)
131
- /// 2. Pending reserves dilute tiers 1-2 players' attestation power to 50% of MAX_POWER per tier
132
- /// 3. With BWA + HHI-adjusted quorum, disinterested tiers 3-4 (0 weight) provide full power
133
- /// 4. Minting reserves doesn't change quorum (tier was already counted via pending reserves)
134
- function test_reserveMintDoesNotChangeQuorumWhenPendingReservesAlreadyCounted() external {
135
- (_pid, _nft, _gov) = _launch(_launchData());
136
-
137
- // --- MINT phase --- players mint 1 NFT each into tiers 1-4
138
- vm.warp(86_402);
139
- _mint(_player0, 1);
140
- _mint(_player1, 2);
141
- _mint(_player2, 3);
142
- _mint(_player3, 4);
143
- _delegateSelf(_player0, 1);
144
- _delegateSelf(_player1, 2);
145
- _delegateSelf(_player2, 3);
146
- _delegateSelf(_player3, 4);
147
-
148
- // --- Skip REFUND phase (no cash-outs) ---
149
- vm.warp(172_802);
150
-
151
- // --- SCORING phase --- submit scorecard
152
- vm.warp(259_202);
153
- DefifaTierCashOutWeight[] memory scorecard = _buildScorecard();
154
- uint256 proposalId = _gov.submitScorecardFor(_gameId, scorecard);
155
-
156
- // Quorum = 4 tiers * MAX_POWER / 2 = 2 * MAX_POWER
157
- uint256 snapshotQuorum = _gov.quorum(_gameId);
158
- assertEq(snapshotQuorum, _gov.MAX_ATTESTATION_POWER_TIER() * 2, "4 tiers, quorum = 2 * MAX_POWER");
159
-
160
- // Tiers 1-2 (with pending reserves): raw power = 500M per player, BWA * 0.5 = 250M.
161
- // Tiers 3-4 (no reserves, 0 weight): raw power = 1e9 per player, BWA * 1.0 = 1e9.
162
- // Total BWA = 250M + 250M + 1e9 + 1e9 = 2.5e9, meeting adjusted quorum of 2.5e9.
163
- vm.prank(_player0);
164
- _gov.attestToScorecardFrom(_gameId, proposalId);
165
- vm.prank(_player1);
166
- _gov.attestToScorecardFrom(_gameId, proposalId);
167
- vm.prank(_player2);
168
- _gov.attestToScorecardFrom(_gameId, proposalId);
169
- vm.prank(_player3);
170
- _gov.attestToScorecardFrom(_gameId, proposalId);
171
-
172
- vm.warp(block.timestamp + _gov.attestationGracePeriodOf(_gameId) + 1);
173
-
174
- assertEq(
175
- uint256(_gov.stateOf(_gameId, proposalId)),
176
- uint256(DefifaScorecardState.SUCCEEDED),
177
- "scorecard succeeds with disinterested attestors providing quorum"
178
- );
179
-
180
- // --- ATTEMPTED ATTACK --- anyone mints pending reserves for tier 1
181
- JB721TiersMintReservesConfig[] memory reserveConfigs = new JB721TiersMintReservesConfig[](1);
182
- reserveConfigs[0] = JB721TiersMintReservesConfig({tierId: 1, count: 1});
183
- _nft.mintReservesFor(reserveConfigs);
184
-
185
- // Scorecard STILL SUCCEEDED — reserve mint doesn't change which tiers are counted
186
- assertEq(
187
- uint256(_gov.stateOf(_gameId, proposalId)),
188
- uint256(DefifaScorecardState.SUCCEEDED),
189
- "quorum unchanged after reserve mint"
190
- );
191
- }
192
-
193
- /// @notice RPT-H-2 FIX VERIFICATION: Ratification succeeds after reserve mint because quorum is stable.
194
- function test_ratificationSucceedsAfterReserveMint() external {
195
- (_pid, _nft, _gov) = _launch(_launchData());
196
-
197
- // MINT phase
198
- vm.warp(86_402);
199
- _mint(_player0, 1);
200
- _mint(_player1, 2);
201
- _mint(_player2, 3);
202
- _mint(_player3, 4);
203
- _delegateSelf(_player0, 1);
204
- _delegateSelf(_player1, 2);
205
- _delegateSelf(_player2, 3);
206
- _delegateSelf(_player3, 4);
207
-
208
- // Skip REFUND phase
209
- vm.warp(172_802);
210
-
211
- // SCORING phase — submit, all 4 attest.
212
- // Tiers 3-4 (no reserves, 0 weight) provide disinterested attestation power.
213
- vm.warp(259_202);
214
- DefifaTierCashOutWeight[] memory scorecard = _buildScorecard();
215
- uint256 proposalId = _gov.submitScorecardFor(_gameId, scorecard);
216
- vm.prank(_player0);
217
- _gov.attestToScorecardFrom(_gameId, proposalId);
218
- vm.prank(_player1);
219
- _gov.attestToScorecardFrom(_gameId, proposalId);
220
- vm.prank(_player2);
221
- _gov.attestToScorecardFrom(_gameId, proposalId);
222
- vm.prank(_player3);
223
- _gov.attestToScorecardFrom(_gameId, proposalId);
224
- vm.warp(block.timestamp + _gov.attestationGracePeriodOf(_gameId) + 1);
225
-
226
- assertEq(
227
- uint256(_gov.stateOf(_gameId, proposalId)),
228
- uint256(DefifaScorecardState.SUCCEEDED),
229
- "scorecard should be succeeded"
230
- );
231
-
232
- // Mint reserves for tier 1 — shouldn't affect ratification
233
- JB721TiersMintReservesConfig[] memory reserveConfigs = new JB721TiersMintReservesConfig[](1);
234
- reserveConfigs[0] = JB721TiersMintReservesConfig({tierId: 1, count: 1});
235
- _nft.mintReservesFor(reserveConfigs);
236
-
237
- // Ratification succeeds because quorum is unchanged (tier already counted via pending reserves)
238
- uint256 ratifiedId = _gov.ratifyScorecardFrom(_gameId, scorecard);
239
- assertEq(ratifiedId, proposalId, "ratification succeeds after reserve mint");
240
- }
241
-
242
- function _buildScorecard() internal view returns (DefifaTierCashOutWeight[] memory scorecard) {
243
- scorecard = new DefifaTierCashOutWeight[](4);
244
- uint256 totalWeight = _nft.TOTAL_CASHOUT_WEIGHT();
245
- scorecard[0] = DefifaTierCashOutWeight({id: 1, cashOutWeight: totalWeight / 2});
246
- scorecard[1] = DefifaTierCashOutWeight({id: 2, cashOutWeight: totalWeight / 2});
247
- scorecard[2] = DefifaTierCashOutWeight({id: 3, cashOutWeight: 0});
248
- scorecard[3] = DefifaTierCashOutWeight({id: 4, cashOutWeight: 0});
249
- }
250
-
251
- function _launchData() internal returns (DefifaLaunchProjectData memory data) {
252
- DefifaTierParams[] memory tiers = new DefifaTierParams[](4);
253
- // Tiers 1-2: have reserves (reserveRate=1, 1 reserve per mint)
254
- for (uint256 i; i < 2; i++) {
255
- tiers[i] = DefifaTierParams({
256
- reservedRate: 1,
257
- reservedTokenBeneficiary: _reserveBeneficiary,
258
- encodedIPFSUri: bytes32(0),
259
- shouldUseReservedTokenBeneficiaryAsDefault: false,
260
- name: "TEAM"
261
- });
262
- }
263
- // Tiers 3-4: no reserves (disinterested attestors for BWA governance)
264
- for (uint256 i = 2; i < 4; i++) {
265
- tiers[i] = DefifaTierParams({
266
- reservedRate: 1001,
267
- reservedTokenBeneficiary: address(0),
268
- encodedIPFSUri: bytes32(0),
269
- shouldUseReservedTokenBeneficiaryAsDefault: false,
270
- name: "TEAM"
271
- });
272
- }
273
-
274
- data = DefifaLaunchProjectData({
275
- name: "DEFIFA",
276
- projectUri: "",
277
- contractUri: "",
278
- baseUri: "",
279
- tiers: tiers,
280
- tierPrice: 1 ether,
281
- token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
282
- mintPeriodDuration: 1 days,
283
- refundPeriodDuration: 1 days,
284
- start: uint48(block.timestamp + 3 days),
285
- splits: new JBSplit[](0),
286
- attestationStartTime: 0,
287
- attestationGracePeriod: 100_381,
288
- defaultAttestationDelegate: address(0),
289
- defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
290
- terminal: jbMultiTerminal(),
291
- store: new JB721TiersHookStore(),
292
- minParticipation: 0,
293
- scorecardTimeout: 0,
294
- timelockDuration: 0
295
- });
296
- }
297
-
298
- function _launch(DefifaLaunchProjectData memory data)
299
- internal
300
- returns (uint256 projectId, DefifaHook nft, DefifaGovernor gov)
301
- {
302
- gov = _governorImpl;
303
- projectId = _deployer.launchGameWith(data);
304
- JBRuleset memory ruleset = jbRulesets().currentOf(projectId);
305
- if (ruleset.dataHook() == address(0)) (ruleset,) = jbRulesets().latestQueuedOf(projectId);
306
- nft = DefifaHook(ruleset.dataHook());
307
- }
308
-
309
- function _mint(address user, uint256 tierId) internal {
310
- vm.deal(user, 1 ether);
311
- uint16[] memory tiers = new uint16[](1);
312
- // forge-lint: disable-next-line(unsafe-typecast)
313
- tiers[0] = uint16(tierId);
314
- bytes[] memory data = new bytes[](1);
315
- data[0] = abi.encode(user, tiers);
316
- bytes4[] memory ids = new bytes4[](1);
317
- ids[0] = metadataHelper().getId("pay", address(_hookImpl));
318
- bytes memory metadata = metadataHelper().createMetadata(ids, data);
319
-
320
- vm.prank(user);
321
- jbMultiTerminal().pay{value: 1 ether}(_pid, JBConstants.NATIVE_TOKEN, 1 ether, user, 0, "", metadata);
322
- }
323
-
324
- function _delegateSelf(address user, uint256 tierId) internal {
325
- DefifaDelegation[] memory delegations = new DefifaDelegation[](1);
326
- delegations[0] = DefifaDelegation({delegatee: user, tierId: tierId});
327
- vm.prank(user);
328
- _nft.setTierDelegatesTo(delegations);
329
- }
330
-
331
- function _cashOut(address user, uint256 tierId, uint256 tokenNumber) internal {
332
- bytes memory metadata = _cashOutMetadata(tierId, tokenNumber);
333
- vm.prank(user);
334
- jbMultiTerminal()
335
- .cashOutTokensOf({
336
- holder: user,
337
- projectId: _pid,
338
- cashOutCount: 0,
339
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
340
- minTokensReclaimed: 0,
341
- beneficiary: payable(user),
342
- metadata: metadata
343
- });
344
- }
345
-
346
- function _cashOutMetadata(uint256 tierId, uint256 tokenNumber) internal view returns (bytes memory) {
347
- uint256[] memory tokenIds = new uint256[](1);
348
- tokenIds[0] = (tierId * 1_000_000_000) + tokenNumber;
349
- bytes[] memory data = new bytes[](1);
350
- data[0] = abi.encode(tokenIds);
351
- bytes4[] memory ids = new bytes4[](1);
352
- ids[0] = metadataHelper().getId("cashOut", address(_hookImpl));
353
- return metadataHelper().createMetadata(ids, data);
354
- }
355
- }