@ballkidz/defifa 0.0.12 → 0.0.14

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 (44) hide show
  1. package/ADMINISTRATION.md +3 -3
  2. package/ARCHITECTURE.md +3 -2
  3. package/AUDIT_INSTRUCTIONS.md +5 -5
  4. package/CHANGE_LOG.md +62 -5
  5. package/CRYPTO_ECON.md +506 -271
  6. package/CRYPTO_ECON.pdf +0 -0
  7. package/CRYPTO_ECON.tex +438 -241
  8. package/RISKS.md +13 -1
  9. package/SKILLS.md +5 -3
  10. package/USER_JOURNEYS.md +4 -3
  11. package/package.json +6 -6
  12. package/src/DefifaDeployer.sol +128 -130
  13. package/src/DefifaGovernor.sol +304 -83
  14. package/src/DefifaHook.sol +184 -171
  15. package/src/enums/DefifaScorecardState.sol +1 -0
  16. package/src/interfaces/IDefifaGovernor.sol +42 -2
  17. package/src/libraries/DefifaHookLib.sol +69 -62
  18. package/src/structs/DefifaAttestations.sol +3 -3
  19. package/src/structs/DefifaLaunchProjectData.sol +1 -0
  20. package/src/structs/DefifaScorecard.sol +2 -0
  21. package/test/BWAFunctionComparison.t.sol +1320 -0
  22. package/test/DefifaAdversarialQuorum.t.sol +52 -37
  23. package/test/DefifaAuditLowGuards.t.sol +9 -5
  24. package/test/DefifaFeeAccounting.t.sol +2 -1
  25. package/test/DefifaGovernanceHardening.t.sol +1315 -0
  26. package/test/DefifaGovernor.t.sol +8 -4
  27. package/test/DefifaHookRegressions.t.sol +2 -1
  28. package/test/DefifaMintCostInvariant.t.sol +2 -1
  29. package/test/DefifaNoContest.t.sol +3 -2
  30. package/test/DefifaSecurity.t.sol +55 -47
  31. package/test/DefifaUSDC.t.sol +3 -2
  32. package/test/Fork.t.sol +37 -32
  33. package/test/TestAuditGaps.sol +6 -4
  34. package/test/TestQALastMile.t.sol +6 -3
  35. package/test/audit/{CodexAttestationDoubleCount.t.sol → AttestationDoubleCount.t.sol} +3 -2
  36. package/test/audit/FixPendingReserveDilution.t.sol +366 -0
  37. package/test/audit/PendingReserveDilution.t.sol +298 -0
  38. package/test/audit/PendingReserveQuorumGrief.t.sol +355 -0
  39. package/test/audit/PendingReserveSnapshotBypass.t.sol +279 -0
  40. package/test/regression/AttestationDelegateBeneficiary.t.sol +2 -1
  41. package/test/regression/FulfillmentBlocksRatification.t.sol +2 -1
  42. package/test/regression/GracePeriodBypass.t.sol +2 -1
  43. package/test/SVG.t.sol +0 -164
  44. package/test/deployScript.t.sol +0 -144
@@ -0,0 +1,355 @@
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
+ }
@@ -0,0 +1,279 @@
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 {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
15
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
16
+ import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
17
+ import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
18
+ import {DefifaDelegation} from "../../src/structs/DefifaDelegation.sol";
19
+ import {DefifaLaunchProjectData} from "../../src/structs/DefifaLaunchProjectData.sol";
20
+ import {DefifaTierParams} from "../../src/structs/DefifaTierParams.sol";
21
+ import {DefifaTierCashOutWeight} from "../../src/structs/DefifaTierCashOutWeight.sol";
22
+ import {DefifaScorecardState} from "../../src/enums/DefifaScorecardState.sol";
23
+ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
24
+ import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
25
+ import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
26
+ import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
27
+ import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
28
+ import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
29
+ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
30
+ import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
31
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
32
+ import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
33
+ import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
34
+ import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
35
+
36
+ /// @notice Verifies that the pending-reserve snapshot fix prevents reserve minting from inflating
37
+ /// attestation power after scorecard submission.
38
+ contract PendingReserveSnapshotBypassTest is JBTest, TestBaseWorkflow {
39
+ using JBRulesetMetadataResolver for JBRuleset;
40
+
41
+ uint256 internal _protocolFeeProjectId;
42
+ uint256 internal _defifaProjectId;
43
+ uint256 internal _gameId = 3;
44
+
45
+ DefifaDeployer internal _deployer;
46
+ DefifaHook internal _hookImpl;
47
+ DefifaGovernor internal _governorImpl;
48
+
49
+ address internal _projectOwner = address(bytes20(keccak256("projectOwner")));
50
+ address internal _reserveBeneficiary = address(bytes20(keccak256("reserveBeneficiary")));
51
+ address internal _player0 = address(bytes20(keccak256("player0")));
52
+ address internal _player1 = address(bytes20(keccak256("player1")));
53
+ address internal _player2 = address(bytes20(keccak256("player2")));
54
+ address internal _player3 = address(bytes20(keccak256("player3")));
55
+
56
+ DefifaHook internal _nft;
57
+ DefifaGovernor internal _gov;
58
+ uint256 internal _pid;
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
+
66
+ JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
67
+ terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: tokens});
68
+
69
+ JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
70
+ rulesetConfigs[0] = JBRulesetConfig({
71
+ mustStartAtOrAfter: 0,
72
+ duration: 10 days,
73
+ weight: 1e18,
74
+ weightCutPercent: 0,
75
+ approvalHook: IJBRulesetApprovalHook(address(0)),
76
+ metadata: JBRulesetMetadata({
77
+ reservedPercent: 0,
78
+ cashOutTaxRate: 0,
79
+ baseCurrency: JBCurrencyIds.ETH,
80
+ pausePay: false,
81
+ pauseCreditTransfers: false,
82
+ allowOwnerMinting: false,
83
+ allowSetCustomToken: false,
84
+ allowTerminalMigration: false,
85
+ allowSetTerminals: false,
86
+ allowSetController: false,
87
+ allowAddAccountingContext: false,
88
+ allowAddPriceFeed: false,
89
+ ownerMustSendPayouts: false,
90
+ holdFees: false,
91
+ useTotalSurplusForCashOuts: false,
92
+ useDataHookForPay: true,
93
+ useDataHookForCashOut: true,
94
+ dataHook: address(0),
95
+ metadata: 0
96
+ }),
97
+ splitGroups: new JBSplitGroup[](0),
98
+ fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
99
+ });
100
+
101
+ _protocolFeeProjectId =
102
+ jbController().launchProjectFor(address(_projectOwner), "", rulesetConfigs, terminalConfigs, "");
103
+ vm.prank(_projectOwner);
104
+ address nanaToken =
105
+ address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
106
+
107
+ _defifaProjectId =
108
+ jbController().launchProjectFor(address(_projectOwner), "", rulesetConfigs, terminalConfigs, "");
109
+ vm.prank(_projectOwner);
110
+ address defifaToken = address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
111
+
112
+ _hookImpl = new DefifaHook(jbDirectory(), IERC20(defifaToken), IERC20(nanaToken));
113
+ _governorImpl = new DefifaGovernor(jbController(), address(this));
114
+ _deployer = new DefifaDeployer(
115
+ address(_hookImpl),
116
+ new DefifaTokenUriResolver(ITypeface(address(0))),
117
+ _governorImpl,
118
+ jbController(),
119
+ new JBAddressRegistry(),
120
+ _defifaProjectId,
121
+ _protocolFeeProjectId
122
+ );
123
+
124
+ _hookImpl.transferOwnership(address(_deployer));
125
+ _governorImpl.transferOwnership(address(_deployer));
126
+ }
127
+
128
+ /// @notice Confirms the snapshot fix: minting pending reserves after scorecard submission does NOT
129
+ /// inflate the submitter's BWA attestation weight. The snapshot locks pending-reserve counts at
130
+ /// submission time so that post-submission reserve minting cannot remove the dilution.
131
+ function test_mintingPendingReserveAfterSnapshotInflatesVotingPowerAndFlipsOutcome() external {
132
+ (_pid, _nft, _gov) = _launch(_launchData());
133
+
134
+ vm.warp(block.timestamp + 1 days + 1);
135
+ _mint(_player0, 1);
136
+ _mint(_player1, 2);
137
+ _mint(_player2, 3);
138
+ _mint(_player3, 4);
139
+ _delegateSelf(_player0, 1);
140
+ _delegateSelf(_player1, 2);
141
+ _delegateSelf(_player2, 3);
142
+ _delegateSelf(_player3, 4);
143
+
144
+ assertEq(_nft.store().numberOfPendingReservesFor(address(_nft), 1), 1, "tier 1 starts with pending reserve");
145
+
146
+ vm.warp(block.timestamp + 2 days + 1);
147
+
148
+ DefifaTierCashOutWeight[] memory scorecard = _evenScorecard();
149
+ uint256 scorecardId = _gov.submitScorecardFor(_gameId, scorecard);
150
+ uint48 snapshotTime = uint48(block.timestamp);
151
+
152
+ uint256 preBwa0 = _gov.getBWAAttestationWeight(_gameId, scorecardId, _player0, snapshotTime);
153
+ uint256 preBwa1 = _gov.getBWAAttestationWeight(_gameId, scorecardId, _player1, snapshotTime);
154
+
155
+ assertEq(preBwa0, 375_000_000, "pending reserve should dilute tier 1 holder at snapshot");
156
+ assertEq(preBwa1, 750_000_000, "non-reserve tier holder keeps full post-BWA weight");
157
+ assertEq(preBwa0 + preBwa1 + preBwa1, 1_875_000_000, "three attestors start below adjusted quorum");
158
+
159
+ vm.warp(block.timestamp + 1);
160
+
161
+ JB721TiersMintReservesConfig[] memory reserveConfigs = new JB721TiersMintReservesConfig[](1);
162
+ reserveConfigs[0] = JB721TiersMintReservesConfig({tierId: 1, count: 1});
163
+ _nft.mintReservesFor(reserveConfigs);
164
+
165
+ uint256 postRaw0 = _gov.getAttestationWeight(_gameId, _player0, snapshotTime);
166
+ uint256 postBwa0 = _gov.getBWAAttestationWeight(_gameId, scorecardId, _player0, snapshotTime);
167
+
168
+ // After fix: getAttestationWeight reads live state (reserves are now minted), so raw weight goes up.
169
+ assertEq(postRaw0, 1_000_000_000, "minting reserves removes pending-reserve dilution in live view");
170
+
171
+ // After fix: getBWAAttestationWeight uses the snapshot, so pending-reserve dilution is preserved.
172
+ // The BWA weight does NOT double -- the snapshot prevents inflation.
173
+ assertEq(postBwa0, 375_000_000, "snapshot prevents reserve minting from inflating BWA power");
174
+
175
+ vm.startPrank(_player0);
176
+ _gov.attestToScorecardFrom(_gameId, scorecardId);
177
+ vm.stopPrank();
178
+ vm.startPrank(_player1);
179
+ _gov.attestToScorecardFrom(_gameId, scorecardId);
180
+ vm.stopPrank();
181
+ vm.startPrank(_player2);
182
+ _gov.attestToScorecardFrom(_gameId, scorecardId);
183
+ vm.stopPrank();
184
+
185
+ vm.warp(block.timestamp + _gov.attestationGracePeriodOf(_gameId) + 1);
186
+
187
+ // After fix: three attestors cannot reach quorum because the snapshot preserves the dilution.
188
+ // Their combined weight: 375M + 750M + 750M = 1,875M < quorum (2,000M base).
189
+ assertEq(
190
+ uint256(_gov.stateOf(_gameId, scorecardId)),
191
+ uint256(DefifaScorecardState.ACTIVE),
192
+ "reserve mint after snapshot does NOT let three attestors reach quorum"
193
+ );
194
+ }
195
+
196
+ function _evenScorecard() internal view returns (DefifaTierCashOutWeight[] memory scorecard) {
197
+ scorecard = new DefifaTierCashOutWeight[](4);
198
+ uint256 totalWeight = _nft.TOTAL_CASHOUT_WEIGHT();
199
+ uint256 perTier = totalWeight / 4;
200
+ for (uint256 i; i < 4; i++) {
201
+ scorecard[i] = DefifaTierCashOutWeight({id: i + 1, cashOutWeight: perTier});
202
+ }
203
+ }
204
+
205
+ function _launchData() internal returns (DefifaLaunchProjectData memory data) {
206
+ DefifaTierParams[] memory tiers = new DefifaTierParams[](4);
207
+ tiers[0] = DefifaTierParams({
208
+ reservedRate: 1,
209
+ reservedTokenBeneficiary: _reserveBeneficiary,
210
+ encodedIPFSUri: bytes32(0),
211
+ shouldUseReservedTokenBeneficiaryAsDefault: false,
212
+ name: "TEAM"
213
+ });
214
+ for (uint256 i = 1; i < 4; i++) {
215
+ tiers[i] = DefifaTierParams({
216
+ reservedRate: 1001,
217
+ reservedTokenBeneficiary: address(0),
218
+ encodedIPFSUri: bytes32(0),
219
+ shouldUseReservedTokenBeneficiaryAsDefault: false,
220
+ name: "TEAM"
221
+ });
222
+ }
223
+
224
+ data = DefifaLaunchProjectData({
225
+ name: "DEFIFA",
226
+ projectUri: "",
227
+ contractUri: "",
228
+ baseUri: "",
229
+ tiers: tiers,
230
+ tierPrice: 1 ether,
231
+ token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
232
+ mintPeriodDuration: 1 days,
233
+ refundPeriodDuration: 1 days,
234
+ start: uint48(block.timestamp + 3 days),
235
+ splits: new JBSplit[](0),
236
+ attestationStartTime: 0,
237
+ attestationGracePeriod: 100_381,
238
+ defaultAttestationDelegate: address(0),
239
+ defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
240
+ terminal: jbMultiTerminal(),
241
+ store: new JB721TiersHookStore(),
242
+ minParticipation: 0,
243
+ scorecardTimeout: 0,
244
+ timelockDuration: 0
245
+ });
246
+ }
247
+
248
+ function _launch(DefifaLaunchProjectData memory data)
249
+ internal
250
+ returns (uint256 projectId, DefifaHook nft, DefifaGovernor gov)
251
+ {
252
+ gov = _governorImpl;
253
+ projectId = _deployer.launchGameWith(data);
254
+ JBRuleset memory ruleset = jbRulesets().currentOf(projectId);
255
+ if (ruleset.dataHook() == address(0)) (ruleset,) = jbRulesets().latestQueuedOf(projectId);
256
+ nft = DefifaHook(ruleset.dataHook());
257
+ }
258
+
259
+ function _mint(address user, uint256 tierId) internal {
260
+ vm.deal(user, 1 ether);
261
+ uint16[] memory tiers = new uint16[](1);
262
+ tiers[0] = uint16(tierId);
263
+ bytes[] memory data = new bytes[](1);
264
+ data[0] = abi.encode(user, tiers);
265
+ bytes4[] memory ids = new bytes4[](1);
266
+ ids[0] = metadataHelper().getId("pay", address(_hookImpl));
267
+ bytes memory metadata = metadataHelper().createMetadata(ids, data);
268
+
269
+ vm.prank(user);
270
+ jbMultiTerminal().pay{value: 1 ether}(_pid, JBConstants.NATIVE_TOKEN, 1 ether, user, 0, "", metadata);
271
+ }
272
+
273
+ function _delegateSelf(address user, uint256 tierId) internal {
274
+ DefifaDelegation[] memory delegations = new DefifaDelegation[](1);
275
+ delegations[0] = DefifaDelegation({delegatee: user, tierId: tierId});
276
+ vm.prank(user);
277
+ _nft.setTierDelegatesTo(delegations);
278
+ }
279
+ }
@@ -248,7 +248,8 @@ contract AttestationDelegateBeneficiary is JBTest, TestBaseWorkflow {
248
248
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
249
249
  terminal: jbMultiTerminal(),
250
250
  minParticipation: 0,
251
- scorecardTimeout: 0
251
+ scorecardTimeout: 0,
252
+ timelockDuration: 0
252
253
  });
253
254
 
254
255
  _mintPhaseStart = d.start - d.mintPeriodDuration - d.refundPeriodDuration;
@@ -251,7 +251,8 @@ contract FulfillmentBlocksRatification is JBTest, TestBaseWorkflow {
251
251
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
252
252
  terminal: jbMultiTerminal(),
253
253
  minParticipation: 0,
254
- scorecardTimeout: 0
254
+ scorecardTimeout: 0,
255
+ timelockDuration: 0
255
256
  });
256
257
  }
257
258
 
@@ -274,7 +274,8 @@ contract GracePeriodBypass is JBTest, TestBaseWorkflow {
274
274
  defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
275
275
  terminal: jbMultiTerminal(),
276
276
  minParticipation: 0,
277
- scorecardTimeout: 0
277
+ scorecardTimeout: 0,
278
+ timelockDuration: 0
278
279
  });
279
280
  }
280
281