@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,617 +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
-
12
- import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.sol";
13
- import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
14
- import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
15
- import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
16
-
17
- import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
18
- import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
19
- import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.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 {DefifaScorecardState} from "../src/enums/DefifaScorecardState.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
- 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 TSReader {
39
- function ts() external view returns (uint256) {
40
- return block.timestamp;
41
- }
42
- }
43
-
44
- /// @title DefifaAdversarialQuorumTest
45
- /// @notice Tests that the Defifa quorum/attestation system is resistant to manipulation:
46
- /// 1. Attestation power is snapshot-based (getPastTierAttestationUnitsOf), so buying
47
- /// tokens after a scorecard is submitted gives zero attestation power.
48
- /// 2. Delegation changes after the mint phase are blocked, preventing post-scorecard vote shifting.
49
- /// 3. Transferring NFTs after attestation does not create double-voting opportunities.
50
- /// 4. A single tier with a dominant holder cannot override the quorum system because
51
- /// each tier caps at MAX_ATTESTATION_POWER_TIER regardless of token count.
52
- contract DefifaAdversarialQuorumTest is JBTest, TestBaseWorkflow {
53
- using JBRulesetMetadataResolver for JBRuleset;
54
-
55
- TSReader private _tsReader = new TSReader();
56
-
57
- address _protocolFeeProjectTokenAccount;
58
- address _defifaProjectTokenAccount;
59
- uint256 _protocolFeeProjectId;
60
- uint256 _defifaProjectId;
61
- uint256 _gameId = 3;
62
-
63
- DefifaDeployer deployer;
64
- DefifaHook hook;
65
- DefifaGovernor governor;
66
- address projectOwner = address(bytes20(keccak256("projectOwner")));
67
-
68
- uint256 _pid;
69
- DefifaHook _nft;
70
- DefifaGovernor _gov;
71
- address[] _users;
72
-
73
- function setUp() public virtual override {
74
- super.setUp();
75
-
76
- JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
77
- _tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
78
- JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
79
- tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
80
- JBRulesetConfig[] memory rc = new JBRulesetConfig[](1);
81
- rc[0] = JBRulesetConfig({
82
- mustStartAtOrAfter: 0,
83
- duration: 10 days,
84
- weight: 1e18,
85
- weightCutPercent: 0,
86
- approvalHook: IJBRulesetApprovalHook(address(0)),
87
- metadata: JBRulesetMetadata({
88
- reservedPercent: 0,
89
- cashOutTaxRate: 0,
90
- baseCurrency: JBCurrencyIds.ETH,
91
- pausePay: false,
92
- pauseCreditTransfers: false,
93
- allowOwnerMinting: false,
94
- allowSetCustomToken: false,
95
- allowTerminalMigration: false,
96
- allowSetTerminals: false,
97
- allowSetController: false,
98
- allowAddAccountingContext: false,
99
- allowAddPriceFeed: false,
100
- ownerMustSendPayouts: false,
101
- holdFees: false,
102
- useTotalSurplusForCashOuts: false,
103
- useDataHookForPay: true,
104
- useDataHookForCashOut: true,
105
- dataHook: address(0),
106
- metadata: 0
107
- }),
108
- splitGroups: new JBSplitGroup[](0),
109
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
110
- });
111
-
112
- _protocolFeeProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
113
- vm.prank(projectOwner);
114
- _protocolFeeProjectTokenAccount =
115
- address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
116
- _defifaProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
117
- vm.prank(projectOwner);
118
- _defifaProjectTokenAccount =
119
- address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
120
-
121
- hook =
122
- new DefifaHook(jbDirectory(), IERC20(_defifaProjectTokenAccount), IERC20(_protocolFeeProjectTokenAccount));
123
- governor = new DefifaGovernor(jbController(), address(this));
124
- deployer = new DefifaDeployer(
125
- address(hook),
126
- new DefifaTokenUriResolver(ITypeface(address(0))),
127
- governor,
128
- jbController(),
129
- new JBAddressRegistry(),
130
- _protocolFeeProjectId,
131
- _defifaProjectId
132
- );
133
- hook.transferOwnership(address(deployer));
134
- governor.transferOwnership(address(deployer));
135
- }
136
-
137
- // =========================================================================
138
- // TEST 1: Attestation power uses snapshot, so late buyers get zero power.
139
- // This is the core defense against flash-mint-vote-sell attacks.
140
- // =========================================================================
141
- function test_lateBuyerHasZeroAttestationPower() external {
142
- // Setup: 4 tiers, 1 minter each
143
- _setupGame(4, 1 ether);
144
- _toScoring();
145
-
146
- // Record the attestation start snapshot time (before any scorecard)
147
- uint48 snapshotTime = uint48(_tsReader.ts());
148
-
149
- // Submit a scorecard
150
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
151
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
152
- for (uint256 i; i < 4; i++) {
153
- sc[i].cashOutWeight = tw / 4;
154
- }
155
- _gov.submitScorecardFor(_gameId, sc);
156
-
157
- // Now the attacker mints in a new block (after the scorecard was submitted).
158
- // Payments are paused in scoring phase, so this would revert. But even if somehow
159
- // an attacker acquired tokens after the snapshot, their getPastTierAttestationUnitsOf
160
- // at the snapshotTime would be 0.
161
- address attacker = _addr(999);
162
-
163
- // Verify attacker has 0 attestation power at the snapshot timestamp.
164
- assertEq(
165
- _gov.getAttestationWeight(_gameId, attacker, snapshotTime),
166
- 0,
167
- "attacker who did not hold tokens at snapshot has 0 attestation power"
168
- );
169
-
170
- // Verify legitimate users still have full power.
171
- for (uint256 i; i < _users.length; i++) {
172
- assertGt(
173
- _gov.getAttestationWeight(_gameId, _users[i], snapshotTime), 0, "legitimate user has attestation power"
174
- );
175
- }
176
- }
177
-
178
- // =========================================================================
179
- // TEST 2: Delegation changes are blocked after the mint phase.
180
- // This prevents an attacker from delegating to themselves after the scorecard.
181
- // =========================================================================
182
- function test_delegationBlockedInScoringPhase() external {
183
- _setupGame(4, 1 ether);
184
- _toScoring();
185
-
186
- // Attacker tries to change their tier 1 delegation during scoring phase.
187
- address attacker = _users[0];
188
- vm.prank(attacker);
189
- vm.expectRevert(abi.encodeWithSignature("DefifaHook_DelegateChangesUnavailableInThisPhase()"));
190
- _nft.setTierDelegateTo(address(0xdead), 1);
191
- }
192
-
193
- // =========================================================================
194
- // TEST 3: Delegation changes are also blocked in the refund phase.
195
- // =========================================================================
196
- function test_delegationBlockedInRefundPhase() external {
197
- _setupGame(4, 1 ether);
198
-
199
- // Warp to refund phase (after mint, before start)
200
- vm.warp(_tsReader.ts() + 1 days);
201
-
202
- vm.prank(_users[0]);
203
- vm.expectRevert(abi.encodeWithSignature("DefifaHook_DelegateChangesUnavailableInThisPhase()"));
204
- _nft.setTierDelegateTo(address(0xdead), 1);
205
- }
206
-
207
- // =========================================================================
208
- // TEST 4: Double attestation is prevented.
209
- // An attacker who already attested cannot attest again even after transferring
210
- // their NFT to a new address.
211
- // =========================================================================
212
- function test_doubleAttestationPrevented() external {
213
- _setupGame(4, 1 ether);
214
- _toScoring();
215
-
216
- // Submit scorecard and begin attestation.
217
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
218
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
219
- for (uint256 i; i < 4; i++) {
220
- sc[i].cashOutWeight = tw / 4;
221
- }
222
- uint256 proposalId = _gov.submitScorecardFor(_gameId, sc);
223
-
224
- // Wait for attestation start.
225
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
226
-
227
- // User 0 attests.
228
- vm.prank(_users[0]);
229
- _gov.attestToScorecardFrom(_gameId, proposalId);
230
-
231
- // User 0 tries to attest again.
232
- vm.prank(_users[0]);
233
- vm.expectRevert(DefifaGovernor.DefifaGovernor_AlreadyAttested.selector);
234
- _gov.attestToScorecardFrom(_gameId, proposalId);
235
- }
236
-
237
- // =========================================================================
238
- // TEST 5: Each tier caps at MAX_ATTESTATION_POWER_TIER regardless of
239
- // how many tokens are held. A whale buying 100 tokens in one tier
240
- // gets the same attestation power as someone who holds 1.
241
- // =========================================================================
242
- function test_tierPowerCappedAtMax() external {
243
- // Setup: 4 tiers, user 0 holds ALL of tier 1 (the only minter).
244
- // User 0's attestation weight for tier 1 should be MAX_ATTESTATION_POWER_TIER.
245
- _setupGame(4, 1 ether);
246
-
247
- // Verify that user 0 (sole holder of tier 1) has exactly MAX_ATTESTATION_POWER_TIER.
248
- vm.warp(_tsReader.ts() + 1);
249
- uint256 power = _gov.getAttestationWeight(_gameId, _users[0], uint48(_tsReader.ts()));
250
- assertEq(
251
- power,
252
- _gov.MAX_ATTESTATION_POWER_TIER(),
253
- "sole holder of one tier should have exactly MAX_ATTESTATION_POWER_TIER"
254
- );
255
- }
256
-
257
- // =========================================================================
258
- // TEST 6: Quorum requires 50% of minted tier weight.
259
- // With 4 minted tiers, quorum = 2 * MAX_ATTESTATION_POWER_TIER.
260
- // 1 out of 4 attestors cannot reach quorum alone.
261
- // =========================================================================
262
- function test_singleAttestorCannotReachQuorum() external {
263
- _setupGame(4, 1 ether);
264
- _toScoring();
265
-
266
- uint256 quorum = _gov.quorum(_gameId);
267
- uint256 expectedQuorum = (4 * _gov.MAX_ATTESTATION_POWER_TIER()) / 2;
268
- assertEq(quorum, expectedQuorum, "quorum = 50% of 4 minted tiers");
269
-
270
- // Submit scorecard.
271
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
272
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
273
- for (uint256 i; i < 4; i++) {
274
- sc[i].cashOutWeight = tw / 4;
275
- }
276
- uint256 proposalId = _gov.submitScorecardFor(_gameId, sc);
277
-
278
- // Wait for attestation + grace period.
279
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
280
-
281
- // Only user 0 attests (25% of total power).
282
- vm.prank(_users[0]);
283
- _gov.attestToScorecardFrom(_gameId, proposalId);
284
-
285
- // After grace period, quorum should NOT be met.
286
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + 1);
287
-
288
- // The proposal should still be ACTIVE (not SUCCEEDED) because quorum is unmet.
289
- DefifaScorecardState state = _gov.stateOf(_gameId, proposalId);
290
- assertEq(uint256(state), uint256(DefifaScorecardState.ACTIVE), "1/4 attestors should not reach quorum");
291
-
292
- // Attempting to ratify should revert.
293
- vm.expectRevert(DefifaGovernor.DefifaGovernor_NotAllowed.selector);
294
- _gov.ratifyScorecardFrom(_gameId, sc);
295
- }
296
-
297
- // =========================================================================
298
- // TEST 7: Three out of four attestors can reach the HHI-adjusted quorum.
299
- // With BWA (Benefit-Weighted Attestation), each attestor's power is reduced
300
- // by their tier's share of the scorecard. For an equal 4-tier scorecard:
301
- // BWA power per user = MAX_ATTESTATION_POWER_TIER * 0.75 = 750_000_000
302
- // HHI-adjusted quorum = baseQuorum * 1.125 = 2_250_000_000
303
- // So 3 users (2_250_000_000) just meets quorum, but 2 users (1_500_000_000) does not.
304
- // =========================================================================
305
- function test_halfAttestorsCanReachQuorum() external {
306
- _setupGame(4, 1 ether);
307
- _toScoring();
308
-
309
- // Submit scorecard.
310
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
311
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
312
- for (uint256 i; i < 4; i++) {
313
- sc[i].cashOutWeight = tw / 4;
314
- }
315
- uint256 proposalId = _gov.submitScorecardFor(_gameId, sc);
316
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
317
-
318
- // Users 0, 1, and 2 attest (75% of raw power, but BWA-adjusted to meet quorum).
319
- vm.prank(_users[0]);
320
- _gov.attestToScorecardFrom(_gameId, proposalId);
321
- vm.prank(_users[1]);
322
- _gov.attestToScorecardFrom(_gameId, proposalId);
323
- vm.prank(_users[2]);
324
- _gov.attestToScorecardFrom(_gameId, proposalId);
325
-
326
- // After grace period.
327
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + 1);
328
-
329
- // The proposal should be SUCCEEDED.
330
- DefifaScorecardState state = _gov.stateOf(_gameId, proposalId);
331
- assertEq(uint256(state), uint256(DefifaScorecardState.SUCCEEDED), "3/4 attestation should reach quorum");
332
-
333
- // Ratification should succeed.
334
- _gov.ratifyScorecardFrom(_gameId, sc);
335
- assertTrue(_nft.cashOutWeightIsSet(), "weights should be set after ratification");
336
- }
337
-
338
- // =========================================================================
339
- // TEST 8: A second scorecard can be ratified if the first doesn't reach quorum.
340
- // =========================================================================
341
- function test_competingScorecards_firstFails_secondSucceeds() external {
342
- _setupGame(4, 1 ether);
343
- _toScoring();
344
-
345
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
346
-
347
- // Scorecard A: tier 1 gets everything.
348
- DefifaTierCashOutWeight[] memory scA = _buildScorecard(4);
349
- scA[0].cashOutWeight = tw;
350
-
351
- // Scorecard B: equal distribution.
352
- DefifaTierCashOutWeight[] memory scB = _buildScorecard(4);
353
- for (uint256 i; i < 4; i++) {
354
- scB[i].cashOutWeight = tw / 4;
355
- }
356
-
357
- uint256 proposalA = _gov.submitScorecardFor(_gameId, scA);
358
- uint256 proposalB = _gov.submitScorecardFor(_gameId, scB);
359
-
360
- // Wait for attestation.
361
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
362
-
363
- // User 0 (tier 1, 100% beneficiary of scorecard A) has BWA power = 0 and cannot attest.
364
- vm.prank(_users[0]);
365
- vm.expectRevert(DefifaGovernor.DefifaGovernor_NotAllowed.selector);
366
- _gov.attestToScorecardFrom(_gameId, proposalA);
367
-
368
- // Users 1, 2, 3 like scorecard B (3/4, quorum met).
369
- vm.prank(_users[1]);
370
- _gov.attestToScorecardFrom(_gameId, proposalB);
371
- vm.prank(_users[2]);
372
- _gov.attestToScorecardFrom(_gameId, proposalB);
373
- vm.prank(_users[3]);
374
- _gov.attestToScorecardFrom(_gameId, proposalB);
375
-
376
- // After grace period.
377
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + 1);
378
-
379
- // Scorecard A should still be ACTIVE (no quorum).
380
- assertEq(
381
- uint256(_gov.stateOf(_gameId, proposalA)),
382
- uint256(DefifaScorecardState.ACTIVE),
383
- "scorecard A should not reach quorum"
384
- );
385
-
386
- // Scorecard B should be SUCCEEDED.
387
- assertEq(
388
- uint256(_gov.stateOf(_gameId, proposalB)),
389
- uint256(DefifaScorecardState.SUCCEEDED),
390
- "scorecard B should reach quorum"
391
- );
392
-
393
- // Ratify scorecard B.
394
- _gov.ratifyScorecardFrom(_gameId, scB);
395
-
396
- // After ratification, scorecard A is DEFEATED.
397
- assertEq(
398
- uint256(_gov.stateOf(_gameId, proposalA)),
399
- uint256(DefifaScorecardState.DEFEATED),
400
- "scorecard A defeated after B is ratified"
401
- );
402
- }
403
-
404
- // =========================================================================
405
- // TEST 9: Burns (refunds) lower quorum, allowing ratification that
406
- // would otherwise require more attestors.
407
- //
408
- // This proves the game handles burn-to-lower-quorum gracefully.
409
- // The quorum() function uses live supply (currentSupplyOfTier) rather
410
- // than a snapshot, so when tokens are burned the quorum threshold
411
- // decreases. This is documented and accepted behavior.
412
- //
413
- // With BWA + HHI-adjusted quorum, a minimum of 4 remaining tiers is needed
414
- // for a balanced scorecard to reach quorum (since total BWA power for n tiers
415
- // is MAX*(n-1) and the adjusted quorum for n=2 always exceeds that).
416
- // We use 6 tiers, burn 2, leaving 4 tiers where 3/4 attestors suffice.
417
- // =========================================================================
418
- function test_burnTiersLowersQuorumAllowsRatification() external {
419
- // --- Step 1: Setup 6 tiers, 1 user per tier ---
420
- _setupGame(6, 1 ether);
421
-
422
- // Verify initial quorum: 6 minted tiers -> quorum = 3 * MAX_ATTESTATION_POWER_TIER.
423
- uint256 initialQuorum = _gov.quorum(_gameId);
424
- uint256 maxTier = _gov.MAX_ATTESTATION_POWER_TIER();
425
- assertEq(initialQuorum, (6 * maxTier) / 2, "initial quorum = 50% of 6 tiers");
426
-
427
- // --- Step 2: Warp to REFUND phase, users 4 and 5 refund ---
428
- _toRefund();
429
-
430
- // Users in tiers 5 and 6 refund (burn their tokens).
431
- _cashOut(_users[4], 5, 1);
432
- _cashOut(_users[5], 6, 1);
433
-
434
- // Verify tiers 5 and 6 now have zero supply.
435
- assertEq(_nft.currentSupplyOfTier(5), 0, "tier 5 supply = 0 after refund");
436
- assertEq(_nft.currentSupplyOfTier(6), 0, "tier 6 supply = 0 after refund");
437
-
438
- // Quorum should now reflect only 4 minted tiers.
439
- uint256 newQuorum = _gov.quorum(_gameId);
440
- assertEq(newQuorum, (4 * maxTier) / 2, "quorum drops to 50% of 4 remaining tiers");
441
- assertLt(newQuorum, initialQuorum, "new quorum < initial quorum");
442
-
443
- // --- Step 3: Advance to SCORING phase ---
444
- _toScoring();
445
-
446
- // --- Step 4: Submit scorecard ---
447
- // Equal split across remaining tiers 1-4; burned tiers 5+6 get 0.
448
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(6);
449
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
450
- sc[0].cashOutWeight = tw / 4;
451
- sc[1].cashOutWeight = tw / 4;
452
- sc[2].cashOutWeight = tw / 4;
453
- sc[3].cashOutWeight = tw / 4;
454
- // sc[4].cashOutWeight = 0; (default, tier 5 burned)
455
- // sc[5].cashOutWeight = 0; (default, tier 6 burned)
456
- uint256 proposalId = _gov.submitScorecardFor(_gameId, sc);
457
-
458
- // --- Step 5: Users 0, 1, and 2 attest (3 of 4 remaining tiers) ---
459
- // BWA power per user (25% tier weight): 1e9 * 0.75 = 750_000_000.
460
- // HHI-adjusted quorum for equal 4-tier scorecard = 2e9 * 1.125 = 2_250_000_000.
461
- // 3 users * 750M = 2_250_000_000, meeting the adjusted quorum exactly.
462
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
463
-
464
- vm.prank(_users[0]);
465
- _gov.attestToScorecardFrom(_gameId, proposalId);
466
- vm.prank(_users[1]);
467
- _gov.attestToScorecardFrom(_gameId, proposalId);
468
- vm.prank(_users[2]);
469
- _gov.attestToScorecardFrom(_gameId, proposalId);
470
-
471
- // --- Step 6: After grace period, proposal should be SUCCEEDED ---
472
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + 1);
473
-
474
- DefifaScorecardState state = _gov.stateOf(_gameId, proposalId);
475
- assertEq(
476
- uint256(state), uint256(DefifaScorecardState.SUCCEEDED), "3 attestors reach quorum after tiers 5+6 burned"
477
- );
478
-
479
- // --- Step 7: Ratification should succeed ---
480
- _gov.ratifyScorecardFrom(_gameId, sc);
481
- assertTrue(_nft.cashOutWeightIsSet(), "scorecard ratified - weights are set");
482
-
483
- // --- Step 8: Verify game resilience ---
484
- assertEq(_nft.currentSupplyOfTier(1), 1, "tier 1 supply intact");
485
- assertEq(_nft.currentSupplyOfTier(2), 1, "tier 2 supply intact");
486
- assertEq(_nft.currentSupplyOfTier(3), 1, "tier 3 supply intact");
487
- assertEq(_nft.currentSupplyOfTier(4), 1, "tier 4 supply intact");
488
- }
489
-
490
- // =========================================================================
491
- // SETUP + PRIMITIVE HELPERS (mirrors DefifaSecurity.t.sol)
492
- // =========================================================================
493
-
494
- function _setupGame(uint8 nTiers, uint256 tierPrice) internal {
495
- DefifaLaunchProjectData memory d = _launchData(nTiers, tierPrice);
496
- (_pid, _nft, _gov) = _launch(d);
497
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
498
- _users = new address[](nTiers);
499
- for (uint256 i; i < nTiers; i++) {
500
- _users[i] = _addr(i);
501
- _mint(_users[i], i + 1, tierPrice);
502
- _delegateSelf(_users[i], i + 1);
503
- vm.warp(block.timestamp + 1);
504
- }
505
- }
506
-
507
- function _toScoring() internal {
508
- vm.warp(_tsReader.ts() + 3 days + 1);
509
- }
510
-
511
- function _launchData(uint8 n, uint256 tierPrice) internal returns (DefifaLaunchProjectData memory) {
512
- DefifaTierParams[] memory tp = new DefifaTierParams[](n);
513
- for (uint256 i; i < n; i++) {
514
- tp[i] = DefifaTierParams({
515
- reservedRate: 1001,
516
- reservedTokenBeneficiary: address(0),
517
- encodedIPFSUri: bytes32(0),
518
- shouldUseReservedTokenBeneficiaryAsDefault: false,
519
- name: "DEFIFA"
520
- });
521
- }
522
- return DefifaLaunchProjectData({
523
- name: "DEFIFA",
524
- projectUri: "",
525
- contractUri: "",
526
- baseUri: "",
527
- token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
528
- mintPeriodDuration: 1 days,
529
- start: uint48(block.timestamp + 3 days),
530
- refundPeriodDuration: 1 days,
531
- store: new JB721TiersHookStore(),
532
- splits: new JBSplit[](0),
533
- attestationStartTime: 0,
534
- attestationGracePeriod: 100_381,
535
- defaultAttestationDelegate: address(0),
536
- // forge-lint: disable-next-line(unsafe-typecast)
537
- tierPrice: uint104(tierPrice),
538
- tiers: tp,
539
- defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
540
- terminal: jbMultiTerminal(),
541
- minParticipation: 0,
542
- scorecardTimeout: 0,
543
- timelockDuration: 0
544
- });
545
- }
546
-
547
- function _launch(DefifaLaunchProjectData memory d) internal returns (uint256 p, DefifaHook n, DefifaGovernor g) {
548
- g = governor;
549
- p = deployer.launchGameWith(d);
550
- JBRuleset memory fc = jbRulesets().currentOf(p);
551
- if (fc.dataHook() == address(0)) (fc,) = jbRulesets().latestQueuedOf(p);
552
- n = DefifaHook(fc.dataHook());
553
- }
554
-
555
- function _addr(uint256 i) internal pure returns (address) {
556
- return address(bytes20(keccak256(abi.encode("aq", i))));
557
- }
558
-
559
- function _mint(address user, uint256 tid, uint256 amt) internal {
560
- vm.deal(user, amt);
561
- uint16[] memory m = new uint16[](1);
562
- // forge-lint: disable-next-line(unsafe-typecast)
563
- m[0] = uint16(tid);
564
- bytes[] memory data = new bytes[](1);
565
- data[0] = abi.encode(user, m);
566
- bytes4[] memory ids = new bytes4[](1);
567
- ids[0] = metadataHelper().getId("pay", address(hook));
568
- // Build metadata before vm.prank so the external call to createMetadata doesn't consume the prank.
569
- bytes memory metadata = metadataHelper().createMetadata(ids, data);
570
- vm.prank(user);
571
- jbMultiTerminal().pay{value: amt}(_pid, JBConstants.NATIVE_TOKEN, amt, user, 0, "", metadata);
572
- }
573
-
574
- function _delegateSelf(address user, uint256 tid) internal {
575
- DefifaDelegation[] memory dd = new DefifaDelegation[](1);
576
- dd[0] = DefifaDelegation({delegatee: user, tierId: tid});
577
- vm.prank(user);
578
- _nft.setTierDelegatesTo(dd);
579
- }
580
-
581
- function _toRefund() internal {
582
- // Advance to the refund phase (1 day after mint phase start = start - refundDuration).
583
- vm.warp(_tsReader.ts() + 1 days + 1);
584
- }
585
-
586
- function _buildScorecard(uint256 n) internal pure returns (DefifaTierCashOutWeight[] memory sc) {
587
- sc = new DefifaTierCashOutWeight[](n);
588
- for (uint256 i; i < n; i++) {
589
- sc[i].id = i + 1;
590
- }
591
- }
592
-
593
- function _cashOut(address user, uint256 tid, uint256 tnum) internal {
594
- bytes memory meta = _cashOutMeta(tid, tnum);
595
- vm.prank(user);
596
- JBMultiTerminal(address(jbMultiTerminal()))
597
- .cashOutTokensOf({
598
- holder: user,
599
- projectId: _pid,
600
- cashOutCount: 0,
601
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
602
- minTokensReclaimed: 0,
603
- beneficiary: payable(user),
604
- metadata: meta
605
- });
606
- }
607
-
608
- function _cashOutMeta(uint256 tid, uint256 tnum) internal view returns (bytes memory) {
609
- uint256[] memory cid = new uint256[](1);
610
- cid[0] = (tid * 1_000_000_000) + tnum;
611
- bytes[] memory data = new bytes[](1);
612
- data[0] = abi.encode(cid);
613
- bytes4[] memory ids = new bytes4[](1);
614
- ids[0] = metadataHelper().getId("cashOut", address(hook));
615
- return metadataHelper().createMetadata(ids, data);
616
- }
617
- }