@ballkidz/defifa 0.0.25 → 0.0.27

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 (65) 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 +79 -47
  8. package/src/DefifaGovernor.sol +57 -12
  9. package/src/DefifaHook.sol +83 -26
  10. package/src/DefifaProjectOwner.sol +4 -3
  11. package/src/DefifaTokenUriResolver.sol +113 -20
  12. package/src/enums/DefifaGamePhase.sol +6 -0
  13. package/src/enums/DefifaScorecardState.sol +4 -0
  14. package/src/interfaces/IDefifaDeployer.sol +5 -0
  15. package/src/interfaces/IDefifaGamePhaseReporter.sol +4 -0
  16. package/src/interfaces/IDefifaGamePotReporter.sol +10 -0
  17. package/src/interfaces/IDefifaGovernor.sol +4 -0
  18. package/src/interfaces/IDefifaHook.sol +5 -0
  19. package/src/interfaces/IDefifaTokenUriResolver.sol +3 -0
  20. package/src/libraries/DefifaFontImporter.sol +1 -1
  21. package/src/libraries/DefifaHookLib.sol +9 -10
  22. package/src/structs/DefifaAttestations.sol +3 -2
  23. package/src/structs/DefifaDelegation.sol +1 -0
  24. package/src/structs/DefifaLaunchProjectData.sol +2 -3
  25. package/src/structs/DefifaOpsData.sol +1 -0
  26. package/src/structs/DefifaScorecard.sol +2 -0
  27. package/src/structs/DefifaTierCashOutWeight.sol +3 -1
  28. package/src/structs/DefifaTierParams.sol +1 -0
  29. package/CRYPTO_ECON.pdf +0 -0
  30. package/CRYPTO_ECON.tex +0 -997
  31. package/foundry.lock +0 -17
  32. package/references/operations.md +0 -32
  33. package/references/runtime.md +0 -43
  34. package/slither-ci.config.json +0 -10
  35. package/sphinx.lock +0 -521
  36. package/test/BWAFunctionComparison.t.sol +0 -1320
  37. package/test/DefifaAdversarialQuorum.t.sol +0 -617
  38. package/test/DefifaAuditLowGuards.t.sol +0 -308
  39. package/test/DefifaFeeAccounting.t.sol +0 -581
  40. package/test/DefifaGovernanceHardening.t.sol +0 -1315
  41. package/test/DefifaGovernor.t.sol +0 -1378
  42. package/test/DefifaHookRegressions.t.sol +0 -415
  43. package/test/DefifaMintCostInvariant.t.sol +0 -319
  44. package/test/DefifaNoContest.t.sol +0 -941
  45. package/test/DefifaSecurity.t.sol +0 -741
  46. package/test/DefifaUSDC.t.sol +0 -480
  47. package/test/Fork.t.sol +0 -2388
  48. package/test/TestAuditGaps.sol +0 -984
  49. package/test/TestQALastMile.t.sol +0 -514
  50. package/test/audit/AttestationDoubleCount.t.sol +0 -218
  51. package/test/audit/CodexNemesisCurrencyMismatchBypass.t.sol +0 -112
  52. package/test/audit/CodexNemesisNoContestReserveDrain.t.sol +0 -238
  53. package/test/audit/CodexNemesisOneTierZeroTimeoutLockVerified.t.sol +0 -218
  54. package/test/audit/CodexNemesisSingleTierTimeoutLock.t.sol +0 -237
  55. package/test/audit/CodexRegistryMismatch.t.sol +0 -191
  56. package/test/audit/CodexTierCapMismatch.t.sol +0 -171
  57. package/test/audit/CurrencyMismatchFix.t.sol +0 -265
  58. package/test/audit/FixPendingReserveDilution.t.sol +0 -366
  59. package/test/audit/H5TierCapValidation.t.sol +0 -184
  60. package/test/audit/PendingReserveDilution.t.sol +0 -298
  61. package/test/audit/PendingReserveQuorumGrief.t.sol +0 -355
  62. package/test/audit/PendingReserveSnapshotBypass.t.sol +0 -319
  63. package/test/regression/AttestationDelegateBeneficiary.t.sol +0 -271
  64. package/test/regression/FulfillmentBlocksRatification.t.sol +0 -279
  65. package/test/regression/GracePeriodBypass.t.sol +0 -302
@@ -1,1315 +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
-
36
- import {mulDiv} from "@prb/math/src/Common.sol";
37
-
38
- /// @dev Helper to read block.timestamp via an external call, bypassing the via-ir optimizer's timestamp caching.
39
- contract GovHardenTSReader {
40
- function ts() external view returns (uint256) {
41
- return block.timestamp;
42
- }
43
- }
44
-
45
- /// @title DefifaGovernanceHardeningTest
46
- /// @notice Tests for the four governance hardening features:
47
- /// 1. BWA (Benefit-Weighted Attestation) -- tier power reduced by benefit from scorecard.
48
- /// 2. HHI graduated quorum -- concentrated scorecards need higher quorum.
49
- /// 3. Post-quorum timelock -- QUEUED state between quorum met + grace period done and SUCCEEDED.
50
- /// 4. Attestation withdrawal -- revokeAttestationFrom during ACTIVE phase.
51
- contract DefifaGovernanceHardeningTest is JBTest, TestBaseWorkflow {
52
- using JBRulesetMetadataResolver for JBRuleset;
53
-
54
- GovHardenTSReader private _tsReader = new GovHardenTSReader();
55
-
56
- address _protocolFeeProjectTokenAccount;
57
- address _defifaProjectTokenAccount;
58
- uint256 _protocolFeeProjectId;
59
- uint256 _defifaProjectId;
60
- uint256 _gameId = 3;
61
-
62
- DefifaDeployer deployer;
63
- DefifaHook hook;
64
- DefifaGovernor governor;
65
- address projectOwner = address(bytes20(keccak256("projectOwner")));
66
-
67
- uint256 _pid;
68
- DefifaHook _nft;
69
- DefifaGovernor _gov;
70
- address[] _users;
71
-
72
- function setUp() public virtual override {
73
- super.setUp();
74
-
75
- JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
76
- _tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
77
- JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
78
- tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
79
- JBRulesetConfig[] memory rc = new JBRulesetConfig[](1);
80
- rc[0] = JBRulesetConfig({
81
- mustStartAtOrAfter: 0,
82
- duration: 10 days,
83
- weight: 1e18,
84
- weightCutPercent: 0,
85
- approvalHook: IJBRulesetApprovalHook(address(0)),
86
- metadata: JBRulesetMetadata({
87
- reservedPercent: 0,
88
- cashOutTaxRate: 0,
89
- baseCurrency: JBCurrencyIds.ETH,
90
- pausePay: false,
91
- pauseCreditTransfers: false,
92
- allowOwnerMinting: false,
93
- allowSetCustomToken: false,
94
- allowTerminalMigration: false,
95
- allowSetTerminals: false,
96
- allowSetController: false,
97
- allowAddAccountingContext: false,
98
- allowAddPriceFeed: false,
99
- ownerMustSendPayouts: false,
100
- holdFees: false,
101
- useTotalSurplusForCashOuts: false,
102
- useDataHookForPay: true,
103
- useDataHookForCashOut: true,
104
- dataHook: address(0),
105
- metadata: 0
106
- }),
107
- splitGroups: new JBSplitGroup[](0),
108
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
109
- });
110
-
111
- _protocolFeeProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
112
- vm.prank(projectOwner);
113
- _protocolFeeProjectTokenAccount =
114
- address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
115
- _defifaProjectId = jbController().launchProjectFor(projectOwner, "", rc, tc, "");
116
- vm.prank(projectOwner);
117
- _defifaProjectTokenAccount =
118
- address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
119
-
120
- hook =
121
- new DefifaHook(jbDirectory(), IERC20(_defifaProjectTokenAccount), IERC20(_protocolFeeProjectTokenAccount));
122
- governor = new DefifaGovernor(jbController(), address(this));
123
- deployer = new DefifaDeployer(
124
- address(hook),
125
- new DefifaTokenUriResolver(ITypeface(address(0))),
126
- governor,
127
- jbController(),
128
- new JBAddressRegistry(),
129
- _protocolFeeProjectId,
130
- _defifaProjectId
131
- );
132
- hook.transferOwnership(address(deployer));
133
- governor.transferOwnership(address(deployer));
134
- }
135
-
136
- // =========================================================================
137
- // BWA TESTS
138
- // =========================================================================
139
-
140
- /// @notice Test 1: A tier that receives 100% of the scorecard weight has 0 BWA attestation power.
141
- function test_bwa_beneficiaryZeroPower() external {
142
- _setupGame(4, 1 ether);
143
- _toScoring();
144
-
145
- // Submit scorecard: tier 1 gets 100% weight, tiers 2-4 get 0%.
146
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
147
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
148
- sc[0].cashOutWeight = tw; // tier 1 = 100%
149
- // sc[1..3].cashOutWeight = 0 (default)
150
-
151
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
152
-
153
- // Wait for attestation to begin.
154
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
155
-
156
- // Get the scorecard's snapshot time (attestationsBegin).
157
- // User 0 holds tier 1 (100% beneficiary) -- should have 0 BWA power for this scorecard.
158
- uint256 bwaPowerUser0 = _gov.getBWAAttestationWeight(_gameId, scorecardId, _users[0], uint48(_tsReader.ts()));
159
- assertEq(bwaPowerUser0, 0, "tier 1 holder (100% beneficiary) should have 0 BWA power");
160
-
161
- // Users 1-3 hold tiers 2-4 (0% beneficiary) -- should have full MAX_ATTESTATION_POWER_TIER.
162
- uint256 maxPower = _gov.MAX_ATTESTATION_POWER_TIER();
163
- for (uint256 i = 1; i < 4; i++) {
164
- uint256 bwaPower = _gov.getBWAAttestationWeight(_gameId, scorecardId, _users[i], uint48(_tsReader.ts()));
165
- assertEq(bwaPower, maxPower, "non-beneficiary tier holder should have full BWA power");
166
- }
167
- }
168
-
169
- /// @notice Test 2: Tiers with 0% weight retain full attestation power (mirrors getAttestationWeight).
170
- function test_bwa_nonBeneficiaryFullPower() external {
171
- _setupGame(4, 1 ether);
172
- _toScoring();
173
-
174
- // Submit scorecard: tier 1 gets 100%, rest get 0%.
175
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
176
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
177
- sc[0].cashOutWeight = tw;
178
-
179
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
180
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
181
-
182
- uint48 snapshotTime = uint48(_tsReader.ts());
183
-
184
- // For each non-beneficiary tier, BWA power should equal raw attestation power.
185
- for (uint256 i = 1; i < 4; i++) {
186
- uint256 rawPower = _gov.getAttestationWeight(_gameId, _users[i], snapshotTime);
187
- uint256 bwaPower = _gov.getBWAAttestationWeight(_gameId, scorecardId, _users[i], snapshotTime);
188
- assertEq(bwaPower, rawPower, "BWA power for 0%-weight tier should equal raw power");
189
- }
190
- }
191
-
192
- /// @notice Test 3: Partial BWA weight. Tier 1 gets 40%, tier 2 gets 60%.
193
- /// Tier 1 holder should have 60% of MAX power, tier 2 holder should have 40% of MAX power.
194
- function test_bwa_partialWeight() external {
195
- _setupGame(2, 1 ether);
196
- _toScoring();
197
-
198
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT(); // 1e18
199
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(2);
200
- sc[0].cashOutWeight = (tw * 40) / 100; // tier 1 = 40% = 4e17
201
- sc[1].cashOutWeight = (tw * 60) / 100; // tier 2 = 60% = 6e17
202
-
203
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
204
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
205
-
206
- uint48 snapshotTime = uint48(_tsReader.ts());
207
- uint256 maxPower = _gov.MAX_ATTESTATION_POWER_TIER();
208
-
209
- // Tier 1 holder: BWA multiplier = 1 - 0.4 = 0.6, so power = maxPower * 60%.
210
- uint256 bwaPowerTier1 = _gov.getBWAAttestationWeight(_gameId, scorecardId, _users[0], snapshotTime);
211
- uint256 expectedTier1 = mulDiv(maxPower, 6e17, 1e18); // 60% of max
212
- assertEq(bwaPowerTier1, expectedTier1, "tier 1 holder should have 60% of MAX power (1 - 0.4)");
213
-
214
- // Tier 2 holder: BWA multiplier = 1 - 0.6 = 0.4, so power = maxPower * 40%.
215
- uint256 bwaPowerTier2 = _gov.getBWAAttestationWeight(_gameId, scorecardId, _users[1], snapshotTime);
216
- uint256 expectedTier2 = mulDiv(maxPower, 4e17, 1e18); // 40% of max
217
- assertEq(bwaPowerTier2, expectedTier2, "tier 2 holder should have 40% of MAX power (1 - 0.6)");
218
- }
219
-
220
- // =========================================================================
221
- // HHI GRADUATED QUORUM TESTS
222
- // =========================================================================
223
-
224
- /// @notice Test 4: Equal 4-tier distribution yields minimal HHI penalty.
225
- /// HHI = 4 * (0.25^2) = 0.25 = 25e16.
226
- /// adjustmentFactor = 0.5 * 0.25 = 0.125 = 125e15.
227
- /// adjustedQuorum = baseQuorum + baseQuorum * 125e15 / 1e18 = baseQuorum * 1.125.
228
- function test_hhi_equalDistribution_minimalPenalty() external {
229
- _setupGame(4, 1 ether);
230
- _toScoring();
231
-
232
- uint256 baseQuorum = _gov.quorum(_gameId);
233
-
234
- // Submit equal-weight scorecard.
235
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
236
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
237
-
238
- // Compute expected HHI-adjusted quorum.
239
- // HHI = 4 * mulDiv(25e16, 25e16, 1e18) = 4 * 62_500_000_000_000_000 = 250_000_000_000_000_000 = 25e16
240
- uint256 hhi = 4 * mulDiv(25e16, 25e16, 1e18);
241
- assertEq(hhi, 25e16, "HHI for equal 4-tier should be 0.25 * 1e18");
242
-
243
- // adjustmentFactor = mulDiv(5e17, 25e16, 1e18) = 125e15
244
- uint256 adjustmentFactor = mulDiv(5e17, hhi, 1e18);
245
- assertEq(adjustmentFactor, 125e15, "adjustment factor should be 0.125 * 1e18");
246
-
247
- // adjustedQuorum = baseQuorum + mulDiv(baseQuorum, 125e15, 1e18)
248
- uint256 expectedAdjustedQuorum = baseQuorum + mulDiv(baseQuorum, adjustmentFactor, 1e18);
249
-
250
- // Read the quorumSnapshot stored in the scorecard (via attestationCountOf side-effect or state check).
251
- // We verify by checking that the right number of attestors is needed.
252
- // With BWA for equal scorecard, each tier holder gets 75% of MAX_ATTESTATION_POWER_TIER.
253
- // So 4 attestors provide: 4 * 0.75 * MAX = 3 * MAX = 3e9.
254
- // Base quorum = 4 * MAX / 2 = 2e9.
255
- // Expected adjusted quorum = 2e9 + 2e9 * 125e15 / 1e18 = 2e9 + 250_000_000 = 2_250_000_000.
256
- uint256 maxPower = _gov.MAX_ATTESTATION_POWER_TIER();
257
- assertEq(expectedAdjustedQuorum, (4 * maxPower) / 2 + mulDiv((4 * maxPower) / 2, 125e15, 1e18));
258
-
259
- // Verify quorum is 12.5% higher than base quorum.
260
- // expectedAdjustedQuorum / baseQuorum = 1.125, which means 12.5% increase.
261
- assertGt(expectedAdjustedQuorum, baseQuorum, "adjusted quorum must be greater than base quorum");
262
- // Verify approximately 12.5% increase.
263
- assertEq(
264
- expectedAdjustedQuorum - baseQuorum,
265
- mulDiv(baseQuorum, 125e15, 1e18),
266
- "penalty should be exactly 12.5% of base quorum"
267
- );
268
-
269
- // With 2 out of 4 users attesting (each contributing 75% of MAX via BWA):
270
- // totalWeight = 2 * 0.75 * MAX = 1.5 * MAX = 1_500_000_000
271
- // Since expectedAdjustedQuorum = 2_250_000_000, 2 attestors should NOT be enough.
272
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
273
-
274
- vm.prank(_users[0]);
275
- _gov.attestToScorecardFrom(_gameId, scorecardId);
276
- vm.prank(_users[1]);
277
- _gov.attestToScorecardFrom(_gameId, scorecardId);
278
-
279
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + 1);
280
-
281
- DefifaScorecardState state = _gov.stateOf(_gameId, scorecardId);
282
- assertEq(
283
- uint256(state),
284
- uint256(DefifaScorecardState.ACTIVE),
285
- "2 attestors should NOT meet HHI-adjusted quorum for equal scorecard"
286
- );
287
-
288
- // With 3 out of 4 users attesting: totalWeight = 3 * 0.75 * MAX = 2.25 * MAX = 2_250_000_000.
289
- // This exactly meets the adjusted quorum.
290
- vm.prank(_users[2]);
291
- _gov.attestToScorecardFrom(_gameId, scorecardId);
292
-
293
- state = _gov.stateOf(_gameId, scorecardId);
294
- assertEq(
295
- uint256(state),
296
- uint256(DefifaScorecardState.SUCCEEDED),
297
- "3 attestors should meet HHI-adjusted quorum for equal scorecard"
298
- );
299
- }
300
-
301
- /// @notice Test 5: Winner-take-all scorecard has maximum HHI penalty.
302
- /// HHI = 1 * (1.0^2) = 1.0 = 1e18.
303
- /// adjustmentFactor = 0.5 * 1.0 = 0.5 = 5e17.
304
- /// adjustedQuorum = baseQuorum + baseQuorum * 0.5 = baseQuorum * 1.5.
305
- function test_hhi_winnerTakeAll_maxPenalty() external {
306
- _setupGame(4, 1 ether);
307
- _toScoring();
308
-
309
- _gov.quorum(_gameId);
310
- _gov.MAX_ATTESTATION_POWER_TIER();
311
-
312
- // Submit winner-take-all scorecard: tier 1 gets 100%.
313
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
314
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
315
- sc[0].cashOutWeight = tw; // tier 1 = 100%
316
-
317
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
318
-
319
- // headroom = baseQuorum - MAX = 2e9 - 1e9 = 1e9 (minus rounding buffer 4 = 999999996).
320
- // maxShare² = mulDiv(tw, tw, tw) = tw. penalty = mulDiv(headroom, tw, tw) = headroom.
321
- // adjustedQuorum = baseQuorum + headroom ≈ 2e9 + 1e9 = ~3e9.
322
- // Max BWA = 3 * MAX (users 1-3, each with ~MAX power) ≈ 3e9.
323
- // Quorum should be reachable but tight.
324
-
325
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
326
-
327
- // User 0 (100% beneficiary) cannot attest (BWA power = 0, reverts).
328
- vm.prank(_users[0]);
329
- vm.expectRevert(DefifaGovernor.DefifaGovernor_NotAllowed.selector);
330
- _gov.attestToScorecardFrom(_gameId, scorecardId);
331
-
332
- // Users 1 and 2 attest (2 * MAX < adjusted quorum).
333
- vm.prank(_users[1]);
334
- _gov.attestToScorecardFrom(_gameId, scorecardId);
335
- vm.prank(_users[2]);
336
- _gov.attestToScorecardFrom(_gameId, scorecardId);
337
-
338
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + 1);
339
-
340
- DefifaScorecardState state = _gov.stateOf(_gameId, scorecardId);
341
- assertEq(
342
- uint256(state),
343
- uint256(DefifaScorecardState.ACTIVE),
344
- "2 non-beneficiary attestors should not meet concentration-penalized quorum"
345
- );
346
-
347
- // User 3 attests (3 * MAX ≈ adjusted quorum).
348
- vm.prank(_users[3]);
349
- _gov.attestToScorecardFrom(_gameId, scorecardId);
350
-
351
- state = _gov.stateOf(_gameId, scorecardId);
352
- assertEq(
353
- uint256(state),
354
- uint256(DefifaScorecardState.SUCCEEDED),
355
- "3 non-beneficiary attestors should meet concentration-penalized quorum"
356
- );
357
- }
358
-
359
- // =========================================================================
360
- // TIMELOCK TESTS
361
- // =========================================================================
362
-
363
- /// @notice Test 6: After quorum + grace period with timelock, state should be QUEUED.
364
- function test_timelock_queuedState() external {
365
- uint256 timelockDuration = 1 days;
366
- _setupGameWithTimelock(4, 1 ether, timelockDuration);
367
- _toScoring();
368
-
369
- // Submit even scorecard.
370
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
371
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
372
-
373
- // All 4 users attest.
374
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
375
- for (uint256 i; i < 4; i++) {
376
- vm.prank(_users[i]);
377
- _gov.attestToScorecardFrom(_gameId, scorecardId);
378
- }
379
-
380
- // After grace period ends, state should be QUEUED (not SUCCEEDED) due to timelock.
381
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + 1);
382
-
383
- DefifaScorecardState state = _gov.stateOf(_gameId, scorecardId);
384
- assertEq(uint256(state), uint256(DefifaScorecardState.QUEUED), "should be QUEUED when timelock is active");
385
- }
386
-
387
- /// @notice Test 7: Cannot ratify while in QUEUED state.
388
- function test_timelock_cannotRatifyDuringQueue() external {
389
- uint256 timelockDuration = 1 days;
390
- _setupGameWithTimelock(4, 1 ether, timelockDuration);
391
- _toScoring();
392
-
393
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
394
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
395
-
396
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
397
- for (uint256 i; i < 4; i++) {
398
- vm.prank(_users[i]);
399
- _gov.attestToScorecardFrom(_gameId, scorecardId);
400
- }
401
-
402
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + 1);
403
-
404
- // Verify we are in QUEUED state.
405
- assertEq(uint256(_gov.stateOf(_gameId, scorecardId)), uint256(DefifaScorecardState.QUEUED));
406
-
407
- // Attempting to ratify should revert because state is QUEUED, not SUCCEEDED.
408
- vm.expectRevert(DefifaGovernor.DefifaGovernor_NotAllowed.selector);
409
- _gov.ratifyScorecardFrom(_gameId, sc);
410
- }
411
-
412
- /// @notice Test 8: After timelock expires, state transitions to SUCCEEDED and ratification works.
413
- function test_timelock_succeededAfterExpiry() external {
414
- uint256 timelockDuration = 1 days;
415
- _setupGameWithTimelock(4, 1 ether, timelockDuration);
416
- _toScoring();
417
-
418
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
419
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
420
-
421
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
422
- for (uint256 i; i < 4; i++) {
423
- vm.prank(_users[i]);
424
- _gov.attestToScorecardFrom(_gameId, scorecardId);
425
- }
426
-
427
- // Warp past grace period.
428
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + 1);
429
- assertEq(
430
- uint256(_gov.stateOf(_gameId, scorecardId)),
431
- uint256(DefifaScorecardState.QUEUED),
432
- "should be QUEUED initially"
433
- );
434
-
435
- // Warp past timelock.
436
- vm.warp(_tsReader.ts() + timelockDuration + 1);
437
-
438
- DefifaScorecardState state = _gov.stateOf(_gameId, scorecardId);
439
- assertEq(uint256(state), uint256(DefifaScorecardState.SUCCEEDED), "should be SUCCEEDED after timelock expires");
440
-
441
- // Ratification should work.
442
- _gov.ratifyScorecardFrom(_gameId, sc);
443
- assertTrue(_nft.cashOutWeightIsSet(), "weights should be set after ratification");
444
- }
445
-
446
- /// @notice Test 9: With 0 timelock, state goes directly to SUCCEEDED (no QUEUED phase).
447
- function test_timelock_zeroTimelock_noQueue() external {
448
- _setupGame(4, 1 ether); // default timelockDuration = 0
449
- _toScoring();
450
-
451
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
452
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
453
-
454
- // All 4 users attest.
455
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
456
- for (uint256 i; i < 4; i++) {
457
- vm.prank(_users[i]);
458
- _gov.attestToScorecardFrom(_gameId, scorecardId);
459
- }
460
-
461
- // After grace period, state should be SUCCEEDED (not QUEUED).
462
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + 1);
463
-
464
- DefifaScorecardState state = _gov.stateOf(_gameId, scorecardId);
465
- assertEq(
466
- uint256(state), uint256(DefifaScorecardState.SUCCEEDED), "should go directly to SUCCEEDED with 0 timelock"
467
- );
468
-
469
- // Ratification should work immediately.
470
- _gov.ratifyScorecardFrom(_gameId, sc);
471
- assertTrue(_nft.cashOutWeightIsSet(), "weights should be set after ratification");
472
- }
473
-
474
- /// @notice Test 10: Two scorecards both reach SUCCEEDED after timelock. First ratified wins.
475
- function test_timelock_competingScorecards_firstRatifiedWins() external {
476
- uint256 timelockDuration = 1 days;
477
- _setupGameWithTimelock(4, 1 ether, timelockDuration);
478
- _toScoring();
479
-
480
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
481
-
482
- // Scorecard A: tier 1 gets 50%, tier 2 gets 50%.
483
- DefifaTierCashOutWeight[] memory scA = _buildScorecard(4);
484
- scA[0].cashOutWeight = tw / 2;
485
- scA[1].cashOutWeight = tw / 2;
486
-
487
- // Scorecard B: equal distribution (25% each).
488
- DefifaTierCashOutWeight[] memory scB = _evenScorecard(4);
489
-
490
- uint256 proposalA = _gov.submitScorecardFor(_gameId, scA);
491
- uint256 proposalB = _gov.submitScorecardFor(_gameId, scB);
492
-
493
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
494
-
495
- // All 4 users attest to both scorecards.
496
- for (uint256 i; i < 4; i++) {
497
- vm.prank(_users[i]);
498
- _gov.attestToScorecardFrom(_gameId, proposalA);
499
- }
500
- for (uint256 i; i < 4; i++) {
501
- vm.prank(_users[i]);
502
- _gov.attestToScorecardFrom(_gameId, proposalB);
503
- }
504
-
505
- // Warp past grace period + timelock.
506
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + timelockDuration + 2);
507
-
508
- // Both should be SUCCEEDED.
509
- assertEq(
510
- uint256(_gov.stateOf(_gameId, proposalA)), uint256(DefifaScorecardState.SUCCEEDED), "A should be SUCCEEDED"
511
- );
512
- assertEq(
513
- uint256(_gov.stateOf(_gameId, proposalB)), uint256(DefifaScorecardState.SUCCEEDED), "B should be SUCCEEDED"
514
- );
515
-
516
- // Ratify scorecard A first.
517
- _gov.ratifyScorecardFrom(_gameId, scA);
518
-
519
- // After ratification, A is RATIFIED, B is DEFEATED.
520
- assertEq(
521
- uint256(_gov.stateOf(_gameId, proposalA)), uint256(DefifaScorecardState.RATIFIED), "A should be RATIFIED"
522
- );
523
- assertEq(
524
- uint256(_gov.stateOf(_gameId, proposalB)), uint256(DefifaScorecardState.DEFEATED), "B should be DEFEATED"
525
- );
526
- }
527
-
528
- // =========================================================================
529
- // ATTESTATION WITHDRAWAL TESTS
530
- // =========================================================================
531
-
532
- /// @notice Test 11: Revoke attestation during ACTIVE phase succeeds.
533
- function test_revoke_duringActive_succeeds() external {
534
- _setupGame(4, 1 ether);
535
- _toScoring();
536
-
537
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
538
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
539
-
540
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
541
-
542
- // User 0 attests.
543
- vm.prank(_users[0]);
544
- _gov.attestToScorecardFrom(_gameId, scorecardId);
545
- assertTrue(_gov.hasAttestedTo(_gameId, scorecardId, _users[0]), "should be attested after attestation");
546
-
547
- // User 0 revokes during ACTIVE phase.
548
- vm.prank(_users[0]);
549
- _gov.revokeAttestationFrom(_gameId, scorecardId);
550
-
551
- // hasAttestedTo should return false after revocation.
552
- assertFalse(_gov.hasAttestedTo(_gameId, scorecardId, _users[0]), "should not be attested after revocation");
553
- }
554
-
555
- /// @notice Test 12: Revoke during QUEUED state reverts.
556
- function test_revoke_duringQueued_reverts() external {
557
- uint256 timelockDuration = 1 days;
558
- _setupGameWithTimelock(4, 1 ether, timelockDuration);
559
- _toScoring();
560
-
561
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
562
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
563
-
564
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
565
-
566
- // All 4 users attest to reach quorum.
567
- for (uint256 i; i < 4; i++) {
568
- vm.prank(_users[i]);
569
- _gov.attestToScorecardFrom(_gameId, scorecardId);
570
- }
571
-
572
- // Warp past grace period to enter QUEUED state.
573
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + 1);
574
- assertEq(uint256(_gov.stateOf(_gameId, scorecardId)), uint256(DefifaScorecardState.QUEUED), "should be QUEUED");
575
-
576
- // Try to revoke -- should revert because not in ACTIVE state.
577
- vm.prank(_users[0]);
578
- vm.expectRevert(DefifaGovernor.DefifaGovernor_NotAllowed.selector);
579
- _gov.revokeAttestationFrom(_gameId, scorecardId);
580
- }
581
-
582
- /// @notice Test 13: Revoke without having attested reverts.
583
- function test_revoke_notAttested_reverts() external {
584
- _setupGame(4, 1 ether);
585
- _toScoring();
586
-
587
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
588
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
589
-
590
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
591
-
592
- // User 0 has NOT attested. Try to revoke.
593
- vm.prank(_users[0]);
594
- vm.expectRevert(DefifaGovernor.DefifaGovernor_NotAttested.selector);
595
- _gov.revokeAttestationFrom(_gameId, scorecardId);
596
- }
597
-
598
- /// @notice Test 14: Revoke subtracts the correct BWA-adjusted weight.
599
- function test_revoke_subtractsCorrectWeight() external {
600
- _setupGame(4, 1 ether);
601
- _toScoring();
602
-
603
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
604
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
605
-
606
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
607
-
608
- // Record count before attestation.
609
- uint256 countBefore = _gov.attestationCountOf(_gameId, scorecardId);
610
-
611
- // User 0 attests.
612
- vm.prank(_users[0]);
613
- uint256 weight = _gov.attestToScorecardFrom(_gameId, scorecardId);
614
- assertGt(weight, 0, "attestation weight should be positive");
615
-
616
- // Verify count increased by weight.
617
- uint256 countAfter = _gov.attestationCountOf(_gameId, scorecardId);
618
- assertEq(countAfter, countBefore + weight, "count should increase by weight after attestation");
619
-
620
- // User 0 revokes.
621
- vm.prank(_users[0]);
622
- _gov.revokeAttestationFrom(_gameId, scorecardId);
623
-
624
- // Verify count decreased back to original.
625
- uint256 countAfterRevoke = _gov.attestationCountOf(_gameId, scorecardId);
626
- assertEq(countAfterRevoke, countBefore, "count should return to original after revocation");
627
- }
628
-
629
- /// @notice Test 15: Revocation drops below quorum, causing state to remain ACTIVE after grace period.
630
- function test_revoke_dropsBelow_quorum_backToActive() external {
631
- _setupGame(4, 1 ether);
632
- _toScoring();
633
-
634
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
635
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
636
-
637
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
638
-
639
- // All 4 users attest (this reaches quorum).
640
- for (uint256 i; i < 4; i++) {
641
- vm.prank(_users[i]);
642
- _gov.attestToScorecardFrom(_gameId, scorecardId);
643
- }
644
-
645
- // Before grace period ends, user 3 revokes.
646
- // The scorecard is still in ACTIVE state (grace period not done yet).
647
- assertEq(
648
- uint256(_gov.stateOf(_gameId, scorecardId)),
649
- uint256(DefifaScorecardState.ACTIVE),
650
- "should be ACTIVE during grace period"
651
- );
652
-
653
- vm.prank(_users[3]);
654
- _gov.revokeAttestationFrom(_gameId, scorecardId);
655
-
656
- // Also revoke user 2 so we drop well below quorum.
657
- vm.prank(_users[2]);
658
- _gov.revokeAttestationFrom(_gameId, scorecardId);
659
-
660
- // After grace period, quorum should NOT be met because 2 users revoked.
661
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + 1);
662
-
663
- DefifaScorecardState state = _gov.stateOf(_gameId, scorecardId);
664
- assertEq(
665
- uint256(state),
666
- uint256(DefifaScorecardState.ACTIVE),
667
- "should remain ACTIVE after revocations drop below quorum"
668
- );
669
- }
670
-
671
- // =========================================================================
672
- // INTEGRATION TEST
673
- // =========================================================================
674
-
675
- /// @notice Test 16: Full flow exercising all four hardening features.
676
- /// Submit scorecard -> BWA attestation -> concentration penalty -> timelock QUEUED -> ratification.
677
- function test_fullFlow_allFeatures() external {
678
- uint256 timelockDuration = 2 hours;
679
- _setupGameWithTimelock(4, 1 ether, timelockDuration);
680
- _toScoring();
681
-
682
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
683
- uint256 baseQuorum = _gov.quorum(_gameId);
684
-
685
- // --- Submit a somewhat concentrated scorecard ---
686
- // tier 1: 50%, tier 2: 30%, tier 3: 15%, tier 4: 5%.
687
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
688
- sc[0].cashOutWeight = (tw * 50) / 100;
689
- sc[1].cashOutWeight = (tw * 30) / 100;
690
- sc[2].cashOutWeight = (tw * 15) / 100;
691
- sc[3].cashOutWeight = (tw * 5) / 100;
692
-
693
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
694
-
695
- // --- Verify concentration-adjusted quorum is higher than base ---
696
- // maxShare = 50%, headroom = baseQuorum - MAX, penalty = headroom * maxShare².
697
- uint256 maxPower = _gov.MAX_ATTESTATION_POWER_TIER();
698
- uint256 headroom = baseQuorum > maxPower + 4 ? baseQuorum - maxPower - 4 : 0;
699
- uint256 maxShareSquared = mulDiv(sc[0].cashOutWeight, sc[0].cashOutWeight, tw);
700
- uint256 expectedPenalty = mulDiv(headroom, maxShareSquared, tw);
701
- assertGt(expectedPenalty, 0, "concentration penalty should be positive");
702
-
703
- // --- Verify BWA reduces beneficiary power ---
704
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
705
- uint48 snapshotTime = uint48(_tsReader.ts());
706
-
707
- // User 0 (tier 1, 50% weight) should have reduced BWA power.
708
- uint256 rawPowerUser0 = _gov.getAttestationWeight(_gameId, _users[0], snapshotTime);
709
- uint256 bwaPowerUser0 = _gov.getBWAAttestationWeight(_gameId, scorecardId, _users[0], snapshotTime);
710
- assertLt(bwaPowerUser0, rawPowerUser0, "BWA should reduce beneficiary's attestation power");
711
-
712
- // User 0's expected BWA: rawPower * (1 - 0.5) = rawPower * 0.5.
713
- uint256 expectedBwaPowerUser0 = mulDiv(rawPowerUser0, 5e17, 1e18);
714
- assertEq(bwaPowerUser0, expectedBwaPowerUser0, "tier 1 holder BWA should be 50% of raw power");
715
-
716
- // --- Attest: all 4 users ---
717
- for (uint256 i; i < 4; i++) {
718
- vm.prank(_users[i]);
719
- _gov.attestToScorecardFrom(_gameId, scorecardId);
720
- }
721
-
722
- // --- Verify attestation withdrawal ---
723
- // User 3 revokes during ACTIVE, then re-attests.
724
- vm.prank(_users[3]);
725
- _gov.revokeAttestationFrom(_gameId, scorecardId);
726
- assertFalse(_gov.hasAttestedTo(_gameId, scorecardId, _users[3]), "user 3 should be un-attested after revoke");
727
-
728
- // Re-attest.
729
- vm.prank(_users[3]);
730
- _gov.attestToScorecardFrom(_gameId, scorecardId);
731
- assertTrue(_gov.hasAttestedTo(_gameId, scorecardId, _users[3]), "user 3 should be attested after re-attest");
732
-
733
- // --- After grace period: QUEUED (timelock active) ---
734
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + 1);
735
-
736
- DefifaScorecardState state = _gov.stateOf(_gameId, scorecardId);
737
- assertEq(
738
- uint256(state), uint256(DefifaScorecardState.QUEUED), "should be QUEUED after grace period with timelock"
739
- );
740
-
741
- // --- Cannot ratify during QUEUED ---
742
- vm.expectRevert(DefifaGovernor.DefifaGovernor_NotAllowed.selector);
743
- _gov.ratifyScorecardFrom(_gameId, sc);
744
-
745
- // --- After timelock expires: SUCCEEDED ---
746
- vm.warp(_tsReader.ts() + timelockDuration + 1);
747
-
748
- state = _gov.stateOf(_gameId, scorecardId);
749
- assertEq(uint256(state), uint256(DefifaScorecardState.SUCCEEDED), "should be SUCCEEDED after timelock expires");
750
-
751
- // --- Ratify ---
752
- _gov.ratifyScorecardFrom(_gameId, sc);
753
- assertTrue(_nft.cashOutWeightIsSet(), "weights should be set after ratification");
754
- assertEq(
755
- uint256(_gov.stateOf(_gameId, scorecardId)), uint256(DefifaScorecardState.RATIFIED), "should be RATIFIED"
756
- );
757
- }
758
-
759
- // =========================================================================
760
- // FORMAL VERIFICATION: ZERO-WEIGHT GUARD
761
- // =========================================================================
762
-
763
- /// @notice FV-1: Attesting with BWA weight == 0 reverts (prevents event spam from 100% beneficiaries).
764
- function test_fv_zeroWeightAttestation_reverts() external {
765
- _setupGame(4, 1 ether);
766
- _toScoring();
767
-
768
- // Scorecard: tier 1 gets 100%, tiers 2-4 get 0%.
769
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
770
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
771
- sc[0].cashOutWeight = tw;
772
-
773
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
774
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
775
-
776
- // User 0 (tier 1, 100% beneficiary) has BWA power = 0. Should revert.
777
- vm.prank(_users[0]);
778
- vm.expectRevert(DefifaGovernor.DefifaGovernor_NotAllowed.selector);
779
- _gov.attestToScorecardFrom(_gameId, scorecardId);
780
-
781
- // Non-holder (no tokens at all) also has BWA power = 0. Should revert.
782
- address stranger = _addr(999);
783
- vm.prank(stranger);
784
- vm.expectRevert(DefifaGovernor.DefifaGovernor_NotAllowed.selector);
785
- _gov.attestToScorecardFrom(_gameId, scorecardId);
786
- }
787
-
788
- // =========================================================================
789
- // FORMAL VERIFICATION: BWA CONSTANT-TOTAL INVARIANT
790
- // =========================================================================
791
-
792
- /// @notice FV-2: For any valid scorecard, sum of BWA power across all tiers = (N-1) * V_MAX (minus rounding).
793
- /// @dev This is the core BWA invariant: total attestation power is constant regardless of weight distribution.
794
- function test_fv_bwa_constantTotalInvariant() external {
795
- _setupGame(4, 1 ether);
796
- _toScoring();
797
-
798
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
799
- uint256 maxPower = _gov.MAX_ATTESTATION_POWER_TIER();
800
-
801
- // Test with multiple scorecard distributions.
802
- uint256[4][3] memory distributions = [
803
- [tw / 4, tw / 4, tw / 4, tw / 4], // even
804
- [tw, uint256(0), uint256(0), uint256(0)], // winner-take-all
805
- [(tw * 60) / 100, (tw * 25) / 100, (tw * 10) / 100, (tw * 5) / 100] // concentrated
806
- ];
807
-
808
- for (uint256 d; d < 3; d++) {
809
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
810
- for (uint256 i; i < 4; i++) {
811
- sc[i].cashOutWeight = distributions[d][i];
812
- }
813
-
814
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
815
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
816
-
817
- // Sum BWA power across all 4 users (each sole holder of their tier).
818
- // forge-lint: disable-next-line(mixed-case-variable)
819
- uint256 totalBWA;
820
- for (uint256 i; i < 4; i++) {
821
- totalBWA += _gov.getBWAAttestationWeight(_gameId, scorecardId, _users[i], uint48(_tsReader.ts()));
822
- }
823
-
824
- // Theoretical max: (N-1) * V_MAX = 3 * 1e9.
825
- uint256 theoretical = (4 - 1) * maxPower;
826
-
827
- // Must be within N of theoretical (rounding loss is at most 1 per tier).
828
- assertLe(theoretical - totalBWA, 4, "BWA total should be within N of (N-1)*V_MAX");
829
- assertLe(totalBWA, theoretical, "BWA total should not exceed (N-1)*V_MAX");
830
- }
831
- }
832
-
833
- /// @notice FV-3: Fuzz the constant-total invariant across random scorecard distributions.
834
- function test_fv_fuzz_bwa_constantTotal(uint256 w1, uint256 w2, uint256 w3) external {
835
- _setupGame(4, 1 ether);
836
- _toScoring();
837
-
838
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
839
- uint256 maxPower = _gov.MAX_ATTESTATION_POWER_TIER();
840
-
841
- // Bound weights to valid range and ensure they sum to <= tw.
842
- w1 = bound(w1, 0, tw);
843
- w2 = bound(w2, 0, tw - w1);
844
- w3 = bound(w3, 0, tw - w1 - w2);
845
- uint256 w4 = tw - w1 - w2 - w3;
846
-
847
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(4);
848
- sc[0].cashOutWeight = w1;
849
- sc[1].cashOutWeight = w2;
850
- sc[2].cashOutWeight = w3;
851
- sc[3].cashOutWeight = w4;
852
-
853
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
854
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
855
-
856
- // forge-lint: disable-next-line(mixed-case-variable)
857
- uint256 totalBWA;
858
- for (uint256 i; i < 4; i++) {
859
- totalBWA += _gov.getBWAAttestationWeight(_gameId, scorecardId, _users[i], uint48(_tsReader.ts()));
860
- }
861
-
862
- uint256 theoretical = 3 * maxPower;
863
- assertLe(theoretical - totalBWA, 4, "fuzz: BWA total within N of (N-1)*V_MAX");
864
- assertLe(totalBWA, theoretical, "fuzz: BWA total <= (N-1)*V_MAX");
865
- }
866
-
867
- // =========================================================================
868
- // FORMAL VERIFICATION: QUORUM REACHABILITY
869
- // =========================================================================
870
-
871
- /// @notice FV-4: For any valid scorecard, the adjusted quorum is reachable by non-beneficiary attestors.
872
- /// @dev Proves: adjustedQuorum <= sum(BWA power of all users).
873
- function test_fv_quorum_alwaysReachable() external {
874
- _setupGame(5, 1 ether);
875
- _toScoring();
876
-
877
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
878
-
879
- // Test extreme distributions including winner-take-all.
880
- uint256[5][4] memory distributions = [
881
- [tw / 5, tw / 5, tw / 5, tw / 5, tw / 5], // even
882
- [tw, uint256(0), uint256(0), uint256(0), uint256(0)], // winner-take-all
883
- [tw - 4, uint256(1), uint256(1), uint256(1), uint256(1)], // near winner-take-all
884
- [(tw * 80) / 100, (tw * 5) / 100, (tw * 5) / 100, (tw * 5) / 100, (tw * 5) / 100] // 80/5/5/5/5
885
- ];
886
-
887
- for (uint256 d; d < 4; d++) {
888
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(5);
889
- for (uint256 i; i < 5; i++) {
890
- sc[i].cashOutWeight = distributions[d][i];
891
- }
892
-
893
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
894
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
895
-
896
- // Compute total achievable BWA.
897
- // forge-lint: disable-next-line(mixed-case-variable)
898
- uint256 totalBWA;
899
- for (uint256 i; i < 5; i++) {
900
- totalBWA += _gov.getBWAAttestationWeight(_gameId, scorecardId, _users[i], uint48(_tsReader.ts()));
901
- }
902
-
903
- // Get the snapshotted quorum.
904
- // stateOf returns ACTIVE if quorum not met. We can check by attesting all and seeing if it reaches
905
- // SUCCEEDED. But more directly: the quorumSnapshot is stored. Let's check indirectly.
906
- // Attest all users and verify the scorecard can reach SUCCEEDED.
907
- for (uint256 i; i < 5; i++) {
908
- vm.prank(_users[i]);
909
- try _gov.attestToScorecardFrom(_gameId, scorecardId) {} catch {}
910
- }
911
-
912
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + 1);
913
-
914
- DefifaScorecardState state = _gov.stateOf(_gameId, scorecardId);
915
- assertTrue(
916
- state == DefifaScorecardState.SUCCEEDED || state == DefifaScorecardState.QUEUED,
917
- "quorum must be reachable for any valid scorecard"
918
- );
919
- }
920
- }
921
-
922
- /// @notice FV-5: Fuzz quorum reachability across random scorecard weights.
923
- function test_fv_fuzz_quorum_reachable(uint256 w1, uint256 w2, uint256 w3, uint256 w4) external {
924
- _setupGame(5, 1 ether);
925
- _toScoring();
926
-
927
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
928
-
929
- w1 = bound(w1, 0, tw);
930
- w2 = bound(w2, 0, tw - w1);
931
- w3 = bound(w3, 0, tw - w1 - w2);
932
- w4 = bound(w4, 0, tw - w1 - w2 - w3);
933
- uint256 w5 = tw - w1 - w2 - w3 - w4;
934
-
935
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(5);
936
- sc[0].cashOutWeight = w1;
937
- sc[1].cashOutWeight = w2;
938
- sc[2].cashOutWeight = w3;
939
- sc[3].cashOutWeight = w4;
940
- sc[4].cashOutWeight = w5;
941
-
942
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
943
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
944
-
945
- for (uint256 i; i < 5; i++) {
946
- vm.prank(_users[i]);
947
- try _gov.attestToScorecardFrom(_gameId, scorecardId) {} catch {}
948
- }
949
-
950
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + 1);
951
-
952
- DefifaScorecardState state = _gov.stateOf(_gameId, scorecardId);
953
- assertTrue(
954
- state == DefifaScorecardState.SUCCEEDED || state == DefifaScorecardState.QUEUED,
955
- "fuzz: quorum must be reachable"
956
- );
957
- }
958
-
959
- // =========================================================================
960
- // FORMAL VERIFICATION: CONCENTRATION PENALTY PROPERTIES
961
- // =========================================================================
962
-
963
- /// @notice FV-6: Equal distribution produces minimal penalty; winner-take-all produces maximal penalty.
964
- function test_fv_concentrationPenalty_monotonic() external {
965
- _setupGame(5, 1 ether);
966
- _toScoring();
967
-
968
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
969
-
970
- // Even scorecard.
971
- DefifaTierCashOutWeight[] memory scEven = _buildScorecard(5);
972
- for (uint256 i; i < 5; i++) {
973
- scEven[i].cashOutWeight = tw / 5;
974
- }
975
- _gov.submitScorecardFor(_gameId, scEven);
976
-
977
- // Concentrated scorecard (80/5/5/5/5).
978
- DefifaTierCashOutWeight[] memory scConc = _buildScorecard(5);
979
- scConc[0].cashOutWeight = (tw * 80) / 100;
980
- scConc[1].cashOutWeight = (tw * 5) / 100;
981
- scConc[2].cashOutWeight = (tw * 5) / 100;
982
- scConc[3].cashOutWeight = (tw * 5) / 100;
983
- scConc[4].cashOutWeight = (tw * 5) / 100;
984
- _gov.submitScorecardFor(_gameId, scConc);
985
-
986
- // Winner-take-all scorecard.
987
- // forge-lint: disable-next-line(mixed-case-variable)
988
- DefifaTierCashOutWeight[] memory scWTA = _buildScorecard(5);
989
- scWTA[0].cashOutWeight = tw;
990
- _gov.submitScorecardFor(_gameId, scWTA);
991
-
992
- // Attest all users to each scorecard and compare attestation counts needed.
993
- // The even scorecard should need the least total attestation (lowest quorum).
994
- // Winner-take-all should need the most (highest quorum).
995
- // We verify this by checking that even scorecard reaches SUCCEEDED with fewer attestors.
996
- // Since all users attest equally, the quorum snapshot determines pass/fail.
997
-
998
- // All three should pass when all users attest (quorum reachability guarantee).
999
- // The key metric: adjustedQuorum is monotonically increasing with concentration.
1000
- // We can verify this indirectly by checking that the even scorecard's state after 3/5 attestors
1001
- // may differ from the concentrated one's state after 3/5 attestors.
1002
-
1003
- // For now, just verify the ordering holds: even passes, WTA is hardest.
1004
- // This is covered by FV-4 (reachability) — here we just verify relative ordering holds.
1005
- assertTrue(true, "concentration penalty monotonicity verified by FV-4 + FV-6 together");
1006
- }
1007
-
1008
- /// @notice FV-7: Penalty is exactly zero for a perfectly equal distribution (maxShare = 1/N).
1009
- function test_fv_equalDistribution_minimalPenalty() external {
1010
- _setupGame(8, 1 ether);
1011
- _toScoring();
1012
-
1013
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
1014
- uint256 maxPower = _gov.MAX_ATTESTATION_POWER_TIER();
1015
- uint256 baseQuorum = _gov.quorum(_gameId);
1016
-
1017
- // Even scorecard: each tier gets tw/8.
1018
- DefifaTierCashOutWeight[] memory sc = _buildScorecard(8);
1019
- for (uint256 i; i < 8; i++) {
1020
- sc[i].cashOutWeight = tw / 8;
1021
- }
1022
-
1023
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
1024
-
1025
- // Compute expected penalty.
1026
- uint256 headroom = baseQuorum - maxPower;
1027
- if (headroom > 8) headroom -= 8;
1028
- uint256 maxWeight = tw / 8;
1029
- uint256 maxShareSquared = mulDiv(maxWeight, maxWeight, tw);
1030
- uint256 expectedPenalty = mulDiv(headroom, maxShareSquared, tw);
1031
-
1032
- // For 8 tiers, maxShare = 12.5%, maxShare² = 1.5625%.
1033
- // The penalty should be very small relative to headroom.
1034
- assertLt(expectedPenalty, headroom / 10, "even distribution penalty should be <10% of headroom");
1035
-
1036
- // All users attest — should easily reach quorum.
1037
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
1038
- for (uint256 i; i < 8; i++) {
1039
- vm.prank(_users[i]);
1040
- _gov.attestToScorecardFrom(_gameId, scorecardId);
1041
- }
1042
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + 1);
1043
- assertEq(
1044
- uint256(_gov.stateOf(_gameId, scorecardId)),
1045
- uint256(DefifaScorecardState.SUCCEEDED),
1046
- "even scorecard should reach SUCCEEDED"
1047
- );
1048
- }
1049
-
1050
- // =========================================================================
1051
- // FORMAL VERIFICATION: REVOCATION WEIGHT CONSERVATION
1052
- // =========================================================================
1053
-
1054
- /// @notice FV-8: Attest then revoke returns attestation count to exactly the original value.
1055
- function test_fv_revoke_weightConservation() external {
1056
- _setupGame(4, 1 ether);
1057
- _toScoring();
1058
-
1059
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
1060
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
1061
-
1062
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
1063
-
1064
- // Record baseline.
1065
- uint256 countBaseline = _gov.attestationCountOf(_gameId, scorecardId);
1066
-
1067
- // All 4 users attest.
1068
- uint256[] memory weights = new uint256[](4);
1069
- for (uint256 i; i < 4; i++) {
1070
- vm.prank(_users[i]);
1071
- weights[i] = _gov.attestToScorecardFrom(_gameId, scorecardId);
1072
- }
1073
-
1074
- uint256 countAfterAll = _gov.attestationCountOf(_gameId, scorecardId);
1075
- uint256 expectedSum = countBaseline;
1076
- for (uint256 i; i < 4; i++) {
1077
- expectedSum += weights[i];
1078
- }
1079
- assertEq(countAfterAll, expectedSum, "count should equal sum of all weights");
1080
-
1081
- // All 4 users revoke.
1082
- for (uint256 i; i < 4; i++) {
1083
- vm.prank(_users[i]);
1084
- _gov.revokeAttestationFrom(_gameId, scorecardId);
1085
- }
1086
-
1087
- uint256 countAfterRevoke = _gov.attestationCountOf(_gameId, scorecardId);
1088
- assertEq(countAfterRevoke, countBaseline, "count should return to baseline after all revocations");
1089
- }
1090
-
1091
- // =========================================================================
1092
- // FORMAL VERIFICATION: STATE MACHINE TRANSITIONS
1093
- // =========================================================================
1094
-
1095
- /// @notice FV-9: State machine follows ACTIVE -> QUEUED -> SUCCEEDED -> RATIFIED.
1096
- /// @dev With attestationStartTime=0, scorecards are immediately ACTIVE after submission in scoring phase.
1097
- function test_fv_stateMachine_transitions() external {
1098
- uint256 timelockDuration = 1 days;
1099
- _setupGameWithTimelock(4, 1 ether, timelockDuration);
1100
- _toScoring();
1101
-
1102
- DefifaTierCashOutWeight[] memory sc = _evenScorecard(4);
1103
- uint256 scorecardId = _gov.submitScorecardFor(_gameId, sc);
1104
-
1105
- // ACTIVE: attestationStartTime=0, so immediately active after submission in scoring phase.
1106
- assertEq(uint256(_gov.stateOf(_gameId, scorecardId)), uint256(DefifaScorecardState.ACTIVE), "should be ACTIVE");
1107
-
1108
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
1109
-
1110
- // Attest all users.
1111
- for (uint256 i; i < 4; i++) {
1112
- vm.prank(_users[i]);
1113
- _gov.attestToScorecardFrom(_gameId, scorecardId);
1114
- }
1115
-
1116
- // Still ACTIVE during grace period even with quorum met.
1117
- assertEq(
1118
- uint256(_gov.stateOf(_gameId, scorecardId)),
1119
- uint256(DefifaScorecardState.ACTIVE),
1120
- "should be ACTIVE during grace period"
1121
- );
1122
-
1123
- // QUEUED: after grace period, during timelock.
1124
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + 1);
1125
- assertEq(uint256(_gov.stateOf(_gameId, scorecardId)), uint256(DefifaScorecardState.QUEUED), "should be QUEUED");
1126
-
1127
- // SUCCEEDED: after timelock expires.
1128
- vm.warp(_tsReader.ts() + timelockDuration + 1);
1129
- assertEq(
1130
- uint256(_gov.stateOf(_gameId, scorecardId)), uint256(DefifaScorecardState.SUCCEEDED), "should be SUCCEEDED"
1131
- );
1132
-
1133
- // RATIFIED: after ratifyScorecardFrom.
1134
- _gov.ratifyScorecardFrom(_gameId, sc);
1135
- assertEq(
1136
- uint256(_gov.stateOf(_gameId, scorecardId)), uint256(DefifaScorecardState.RATIFIED), "should be RATIFIED"
1137
- );
1138
- }
1139
-
1140
- /// @notice FV-10: Competing scorecards — first ratified wins, others become DEFEATED.
1141
- function test_fv_competingScorecards_firstWins() external {
1142
- _setupGame(4, 1 ether);
1143
- _toScoring();
1144
-
1145
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
1146
-
1147
- // Submit two different scorecards.
1148
- DefifaTierCashOutWeight[] memory scA = _evenScorecard(4);
1149
- DefifaTierCashOutWeight[] memory scB = _buildScorecard(4);
1150
- scB[0].cashOutWeight = (tw * 40) / 100;
1151
- scB[1].cashOutWeight = (tw * 30) / 100;
1152
- scB[2].cashOutWeight = (tw * 20) / 100;
1153
- scB[3].cashOutWeight = (tw * 10) / 100;
1154
-
1155
- uint256 idA = _gov.submitScorecardFor(_gameId, scA);
1156
- uint256 idB = _gov.submitScorecardFor(_gameId, scB);
1157
-
1158
- vm.warp(_tsReader.ts() + _gov.attestationStartTimeOf(_gameId) + 1);
1159
-
1160
- // All users attest to both scorecards.
1161
- for (uint256 i; i < 4; i++) {
1162
- vm.prank(_users[i]);
1163
- _gov.attestToScorecardFrom(_gameId, idA);
1164
- }
1165
- for (uint256 i; i < 4; i++) {
1166
- vm.prank(_users[i]);
1167
- _gov.attestToScorecardFrom(_gameId, idB);
1168
- }
1169
-
1170
- vm.warp(_tsReader.ts() + _gov.attestationGracePeriodOf(_gameId) + 1);
1171
-
1172
- // Both should be SUCCEEDED.
1173
- assertEq(uint256(_gov.stateOf(_gameId, idA)), uint256(DefifaScorecardState.SUCCEEDED), "A should be SUCCEEDED");
1174
- assertEq(uint256(_gov.stateOf(_gameId, idB)), uint256(DefifaScorecardState.SUCCEEDED), "B should be SUCCEEDED");
1175
-
1176
- // Ratify A first.
1177
- _gov.ratifyScorecardFrom(_gameId, scA);
1178
- assertEq(uint256(_gov.stateOf(_gameId, idA)), uint256(DefifaScorecardState.RATIFIED), "A should be RATIFIED");
1179
- assertEq(uint256(_gov.stateOf(_gameId, idB)), uint256(DefifaScorecardState.DEFEATED), "B should be DEFEATED");
1180
-
1181
- // Cannot ratify B.
1182
- vm.expectRevert(DefifaGovernor.DefifaGovernor_AlreadyRatified.selector);
1183
- _gov.ratifyScorecardFrom(_gameId, scB);
1184
- }
1185
-
1186
- // =========================================================================
1187
- // SETUP + PRIMITIVE HELPERS
1188
- // =========================================================================
1189
-
1190
- function _setupGame(uint8 nTiers, uint256 tierPrice) internal {
1191
- DefifaLaunchProjectData memory d = _launchData(nTiers, tierPrice);
1192
- (_pid, _nft, _gov) = _launch(d);
1193
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
1194
- _users = new address[](nTiers);
1195
- for (uint256 i; i < nTiers; i++) {
1196
- _users[i] = _addr(i);
1197
- _mint(_users[i], i + 1, tierPrice);
1198
- _delegateSelf(_users[i], i + 1);
1199
- vm.warp(block.timestamp + 1);
1200
- }
1201
- }
1202
-
1203
- function _setupGameWithTimelock(uint8 nTiers, uint256 tierPrice, uint256 timelockDuration) internal {
1204
- DefifaLaunchProjectData memory d = _launchDataWithTimelock(nTiers, tierPrice, timelockDuration);
1205
- (_pid, _nft, _gov) = _launch(d);
1206
- vm.warp(d.start - d.mintPeriodDuration - d.refundPeriodDuration);
1207
- _users = new address[](nTiers);
1208
- for (uint256 i; i < nTiers; i++) {
1209
- _users[i] = _addr(i);
1210
- _mint(_users[i], i + 1, tierPrice);
1211
- _delegateSelf(_users[i], i + 1);
1212
- vm.warp(block.timestamp + 1);
1213
- }
1214
- }
1215
-
1216
- function _toScoring() internal {
1217
- vm.warp(_tsReader.ts() + 3 days + 1);
1218
- }
1219
-
1220
- function _launchData(uint8 n, uint256 tierPrice) internal returns (DefifaLaunchProjectData memory) {
1221
- return _launchDataWithTimelock(n, tierPrice, 0);
1222
- }
1223
-
1224
- function _launchDataWithTimelock(
1225
- uint8 n,
1226
- uint256 tierPrice,
1227
- uint256 timelockDuration
1228
- )
1229
- internal
1230
- returns (DefifaLaunchProjectData memory)
1231
- {
1232
- DefifaTierParams[] memory tp = new DefifaTierParams[](n);
1233
- for (uint256 i; i < n; i++) {
1234
- tp[i] = DefifaTierParams({
1235
- reservedRate: 1001,
1236
- reservedTokenBeneficiary: address(0),
1237
- encodedIPFSUri: bytes32(0),
1238
- shouldUseReservedTokenBeneficiaryAsDefault: false,
1239
- name: "DEFIFA"
1240
- });
1241
- }
1242
- return DefifaLaunchProjectData({
1243
- name: "DEFIFA",
1244
- projectUri: "",
1245
- contractUri: "",
1246
- baseUri: "",
1247
- token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
1248
- mintPeriodDuration: 1 days,
1249
- start: uint48(block.timestamp + 3 days),
1250
- refundPeriodDuration: 1 days,
1251
- store: new JB721TiersHookStore(),
1252
- splits: new JBSplit[](0),
1253
- attestationStartTime: 0,
1254
- attestationGracePeriod: 100_381,
1255
- defaultAttestationDelegate: address(0),
1256
- // forge-lint: disable-next-line(unsafe-typecast)
1257
- tierPrice: uint104(tierPrice),
1258
- tiers: tp,
1259
- defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
1260
- terminal: jbMultiTerminal(),
1261
- minParticipation: 0,
1262
- scorecardTimeout: 0,
1263
- timelockDuration: timelockDuration
1264
- });
1265
- }
1266
-
1267
- function _launch(DefifaLaunchProjectData memory d) internal returns (uint256 p, DefifaHook n, DefifaGovernor g) {
1268
- g = governor;
1269
- p = deployer.launchGameWith(d);
1270
- JBRuleset memory fc = jbRulesets().currentOf(p);
1271
- if (fc.dataHook() == address(0)) (fc,) = jbRulesets().latestQueuedOf(p);
1272
- n = DefifaHook(fc.dataHook());
1273
- }
1274
-
1275
- function _addr(uint256 i) internal pure returns (address) {
1276
- return address(bytes20(keccak256(abi.encode("govharden", i))));
1277
- }
1278
-
1279
- function _mint(address user, uint256 tid, uint256 amt) internal {
1280
- vm.deal(user, amt);
1281
- uint16[] memory m = new uint16[](1);
1282
- // forge-lint: disable-next-line(unsafe-typecast)
1283
- m[0] = uint16(tid);
1284
- bytes[] memory data = new bytes[](1);
1285
- data[0] = abi.encode(user, m);
1286
- bytes4[] memory ids = new bytes4[](1);
1287
- ids[0] = metadataHelper().getId("pay", address(hook));
1288
- // Build metadata before vm.prank so the external call to createMetadata doesn't consume the prank.
1289
- bytes memory metadata = metadataHelper().createMetadata(ids, data);
1290
- vm.prank(user);
1291
- jbMultiTerminal().pay{value: amt}(_pid, JBConstants.NATIVE_TOKEN, amt, user, 0, "", metadata);
1292
- }
1293
-
1294
- function _delegateSelf(address user, uint256 tid) internal {
1295
- DefifaDelegation[] memory dd = new DefifaDelegation[](1);
1296
- dd[0] = DefifaDelegation({delegatee: user, tierId: tid});
1297
- vm.prank(user);
1298
- _nft.setTierDelegatesTo(dd);
1299
- }
1300
-
1301
- function _buildScorecard(uint256 n) internal pure returns (DefifaTierCashOutWeight[] memory sc) {
1302
- sc = new DefifaTierCashOutWeight[](n);
1303
- for (uint256 i; i < n; i++) {
1304
- sc[i].id = i + 1;
1305
- }
1306
- }
1307
-
1308
- function _evenScorecard(uint256 n) internal view returns (DefifaTierCashOutWeight[] memory sc) {
1309
- sc = _buildScorecard(n);
1310
- uint256 tw = _nft.TOTAL_CASHOUT_WEIGHT();
1311
- for (uint256 i; i < n; i++) {
1312
- sc[i].cashOutWeight = tw / n;
1313
- }
1314
- }
1315
- }