@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,514 +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 {DefifaGamePhase} from "../src/enums/DefifaGamePhase.sol";
11
- import {IDefifaDeployer} from "../src/interfaces/IDefifaDeployer.sol";
12
- import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
13
-
14
- import {JBTest} from "@bananapus/core-v6/test/helpers/JBTest.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 {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
22
- import {DefifaDelegation} from "../src/structs/DefifaDelegation.sol";
23
- import {DefifaLaunchProjectData} from "../src/structs/DefifaLaunchProjectData.sol";
24
- import {DefifaTierParams} from "../src/structs/DefifaTierParams.sol";
25
- import {DefifaTierCashOutWeight} from "../src/structs/DefifaTierCashOutWeight.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 {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
34
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
35
- import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
36
- import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
37
- import {JBMultiTerminal} from "@bananapus/core-v6/src/JBMultiTerminal.sol";
38
-
39
- /// @dev Helper to read block.timestamp via an external call, bypassing the via-ir optimizer's timestamp caching.
40
- contract QATimestampReader {
41
- function timestamp() external view returns (uint256) {
42
- return block.timestamp;
43
- }
44
- }
45
-
46
- // =============================================================================
47
- // QA LAST-MILE TEST 1: CASHOUT DoS WHEN FULFILLMENT FAILS DURING RATIFICATION
48
- // =============================================================================
49
-
50
- /// @title TestQACashOutDoSDuringFulfillmentWindow
51
- /// @notice Documents the cashout denial-of-service window when fulfillCommitmentsOf reverts during ratification.
52
- /// @dev When fulfillCommitmentsOf reverts during ratification (try-catch), the game enters COMPLETE phase
53
- /// (scorecard is set) but the final ruleset — which has empty fundAccessLimitGroups (surplus = balance) —
54
- /// is never queued. The SCORING ruleset remains active with payoutLimits = type(uint224).max, making
55
- /// surplus = 0, which causes cashOutCount = 0 in the hook's computeCashOutCount.
56
- /// Players cannot cash out until fulfillCommitmentsOf is successfully retried.
57
- /// This is a known, accepted behavior: the DoS is temporary and funds are safe.
58
- contract TestQACashOutDoSDuringFulfillmentWindow is JBTest, TestBaseWorkflow {
59
- using JBRulesetMetadataResolver for JBRuleset;
60
-
61
- QATimestampReader private _tsReader = new QATimestampReader();
62
-
63
- address _protocolFeeProjectTokenAccount;
64
- address _defifaProjectTokenAccount;
65
- uint256 _protocolFeeProjectId;
66
- uint256 _defifaProjectId;
67
- uint256 _gameId = 3;
68
-
69
- DefifaDeployer deployer;
70
- DefifaHook hook;
71
- DefifaGovernor governor;
72
-
73
- address projectOwner = address(bytes20(keccak256("projectOwner")));
74
-
75
- function setUp() public virtual override {
76
- super.setUp();
77
-
78
- JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
79
- _tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
80
-
81
- JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
82
- terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
83
-
84
- JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
85
- rulesetConfigs[0] = JBRulesetConfig({
86
- mustStartAtOrAfter: 0,
87
- duration: 10 days,
88
- weight: 1e18,
89
- weightCutPercent: 0,
90
- approvalHook: IJBRulesetApprovalHook(address(0)),
91
- metadata: JBRulesetMetadata({
92
- reservedPercent: 0,
93
- cashOutTaxRate: 0,
94
- baseCurrency: JBCurrencyIds.ETH,
95
- pausePay: false,
96
- pauseCreditTransfers: false,
97
- allowOwnerMinting: false,
98
- allowSetCustomToken: false,
99
- allowTerminalMigration: false,
100
- allowSetTerminals: false,
101
- allowSetController: false,
102
- allowAddAccountingContext: false,
103
- allowAddPriceFeed: false,
104
- ownerMustSendPayouts: false,
105
- holdFees: false,
106
- useTotalSurplusForCashOuts: false,
107
- useDataHookForPay: true,
108
- useDataHookForCashOut: true,
109
- dataHook: address(0),
110
- metadata: 0
111
- }),
112
- splitGroups: new JBSplitGroup[](0),
113
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
114
- });
115
-
116
- _protocolFeeProjectId =
117
- jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
118
- vm.prank(projectOwner);
119
- _protocolFeeProjectTokenAccount =
120
- address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
121
-
122
- _defifaProjectId =
123
- jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
124
- vm.prank(projectOwner);
125
- _defifaProjectTokenAccount =
126
- address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
127
-
128
- hook = new DefifaHook(
129
- jbDirectory(), IERC20(address(_defifaProjectTokenAccount)), IERC20(_protocolFeeProjectTokenAccount)
130
- );
131
- governor = new DefifaGovernor(jbController(), address(this));
132
- JBAddressRegistry _registry = new JBAddressRegistry();
133
- DefifaTokenUriResolver _tokenUriResolver = new DefifaTokenUriResolver(ITypeface(address(0)));
134
- deployer = new DefifaDeployer(
135
- address(hook),
136
- _tokenUriResolver,
137
- governor,
138
- jbController(),
139
- _registry,
140
- _defifaProjectId,
141
- _protocolFeeProjectId
142
- );
143
-
144
- hook.transferOwnership(address(deployer));
145
- governor.transferOwnership(address(deployer));
146
- }
147
-
148
- /// @notice Proves the fix: when sendPayoutsOf reverts during fulfillCommitmentsOf,
149
- /// the internal try-catch handles it gracefully. The final ruleset is still queued,
150
- /// and players can cash out immediately (no DoS).
151
- function test_cashOutDoSDuringFulfillmentWindow() public {
152
- uint8 nTiers = 4;
153
- address[] memory _users = new address[](nTiers);
154
- DefifaLaunchProjectData memory defifaData = _getBasicLaunchData(nTiers);
155
- (uint256 _projectId, DefifaHook _nft, DefifaGovernor _governor) = _createProject(defifaData);
156
-
157
- // --- Phase 1: Mint NFTs (1 ETH per user, 4 users = 4 ETH pot) ---
158
- vm.warp(defifaData.start - defifaData.mintPeriodDuration - defifaData.refundPeriodDuration);
159
- for (uint256 i = 0; i < nTiers; i++) {
160
- _users[i] = address(bytes20(keccak256(abi.encode("qa_user", Strings.toString(i)))));
161
- vm.deal(_users[i], 1 ether);
162
- uint16[] memory rawMetadata = new uint16[](1);
163
- // forge-lint: disable-next-line(unsafe-typecast)
164
- rawMetadata[0] = uint16(i + 1);
165
- bytes memory metadata = _buildPayMetadata(abi.encode(_users[i], rawMetadata));
166
-
167
- vm.prank(_users[i]);
168
- jbMultiTerminal().pay{value: 1 ether}(
169
- _projectId, JBConstants.NATIVE_TOKEN, 1 ether, _users[i], 0, "", metadata
170
- );
171
-
172
- DefifaDelegation[] memory dd = new DefifaDelegation[](1);
173
- dd[0] = DefifaDelegation({delegatee: _users[i], tierId: i + 1});
174
- vm.prank(_users[i]);
175
- _nft.setTierDelegatesTo(dd);
176
-
177
- vm.warp(_tsReader.timestamp() + 1);
178
- }
179
-
180
- // Verify the pot is 4 ETH.
181
- uint256 potBefore =
182
- jbMultiTerminal().STORE().balanceOf(address(jbMultiTerminal()), _projectId, JBConstants.NATIVE_TOKEN);
183
- assertEq(potBefore, 4 ether, "pot should be 4 ETH");
184
-
185
- // --- Advance to SCORING phase ---
186
- vm.warp(defifaData.start + 1);
187
- assertEq(uint256(deployer.currentGamePhaseOf(_projectId)), uint256(DefifaGamePhase.SCORING));
188
-
189
- // --- Build and ratify scorecard with tier 1 getting all weight ---
190
- DefifaTierCashOutWeight[] memory scorecards = new DefifaTierCashOutWeight[](nTiers);
191
- scorecards[0] = DefifaTierCashOutWeight({id: 1, cashOutWeight: _nft.TOTAL_CASHOUT_WEIGHT()});
192
- for (uint256 i = 1; i < nTiers; i++) {
193
- scorecards[i] = DefifaTierCashOutWeight({id: i + 1, cashOutWeight: 0});
194
- }
195
-
196
- uint256 _proposalId = _governor.submitScorecardFor(_gameId, scorecards);
197
- vm.warp(_tsReader.timestamp() + _governor.attestationStartTimeOf(_gameId) + 1);
198
- // Start from i=1: user 0 holds tier 1 which gets 100% weight, so BWA reduces their power to 0.
199
- for (uint256 i = 1; i < _users.length; i++) {
200
- vm.prank(_users[i]);
201
- _governor.attestToScorecardFrom(_gameId, _proposalId);
202
- }
203
- vm.warp(_tsReader.timestamp() + _governor.attestationGracePeriodOf(_gameId) + 1);
204
-
205
- // --- Mock sendPayoutsOf on the terminal to revert (the actual failure point) ---
206
- vm.mockCallRevert(
207
- address(jbMultiTerminal()),
208
- abi.encodeWithSelector(JBMultiTerminal.sendPayoutsOf.selector),
209
- abi.encodeWithSignature("Error(string)", "simulated payout failure")
210
- );
211
-
212
- // Ratify — sendPayoutsOf fails but fulfillCommitmentsOf handles it gracefully.
213
- // CommitmentPayoutFailed is emitted and the final ruleset is still queued.
214
- vm.expectEmit(true, false, false, false);
215
- emit IDefifaDeployer.CommitmentPayoutFailed(_gameId, 0, "");
216
- _governor.ratifyScorecardFrom(_gameId, scorecards);
217
-
218
- // Clear mock so subsequent calls work.
219
- vm.clearMockedCalls();
220
-
221
- // Verify game is in COMPLETE phase (scorecard is set).
222
- assertEq(uint256(deployer.currentGamePhaseOf(_projectId)), uint256(DefifaGamePhase.COMPLETE));
223
-
224
- // Verify fulfilledCommitmentsOf returns 1 (sentinel).
225
- assertEq(deployer.fulfilledCommitmentsOf(_projectId), 1, "should be sentinel value 1");
226
-
227
- // --- Players CAN cash out immediately (no DoS) ---
228
- uint256 user0BalBefore = _users[0].balance;
229
- {
230
- uint256[] memory cashOutIds = new uint256[](1);
231
- cashOutIds[0] = _generateTokenId(1, 1);
232
- bytes memory cashOutMetadata = _buildCashOutMetadata(abi.encode(cashOutIds));
233
-
234
- vm.prank(_users[0]);
235
- JBMultiTerminal(address(jbMultiTerminal()))
236
- .cashOutTokensOf({
237
- holder: _users[0],
238
- projectId: _projectId,
239
- cashOutCount: 0,
240
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
241
- minTokensReclaimed: 0,
242
- beneficiary: payable(_users[0]),
243
- metadata: cashOutMetadata
244
- });
245
- }
246
- uint256 reclaimed = _users[0].balance - user0BalBefore;
247
- assertGt(reclaimed, 0, "winner should reclaim ETH immediately (no DoS)");
248
-
249
- // --- Calling fulfillCommitmentsOf again is a no-op (idempotent) ---
250
- deployer.fulfillCommitmentsOf(_projectId);
251
- assertEq(deployer.fulfilledCommitmentsOf(_projectId), 1, "should still be sentinel value 1");
252
- }
253
-
254
- // ----- Internal helpers ------
255
-
256
- function _getBasicLaunchData(uint8 nTiers) internal returns (DefifaLaunchProjectData memory) {
257
- DefifaTierParams[] memory tierParams = new DefifaTierParams[](nTiers);
258
- for (uint256 i = 0; i < nTiers; i++) {
259
- tierParams[i] = DefifaTierParams({
260
- reservedRate: 1001,
261
- reservedTokenBeneficiary: address(0),
262
- encodedIPFSUri: bytes32(0),
263
- shouldUseReservedTokenBeneficiaryAsDefault: false,
264
- name: "DEFIFA"
265
- });
266
- }
267
-
268
- return DefifaLaunchProjectData({
269
- name: "DEFIFA",
270
- projectUri: "",
271
- contractUri: "",
272
- baseUri: "",
273
- tierPrice: 1 ether,
274
- token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
275
- mintPeriodDuration: 1 days,
276
- start: uint48(block.timestamp + 3 days),
277
- refundPeriodDuration: 1 days,
278
- store: new JB721TiersHookStore(),
279
- splits: new JBSplit[](0),
280
- attestationStartTime: 0,
281
- attestationGracePeriod: 100_381,
282
- defaultAttestationDelegate: address(0),
283
- tiers: tierParams,
284
- defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
285
- terminal: jbMultiTerminal(),
286
- minParticipation: 0,
287
- scorecardTimeout: 0,
288
- timelockDuration: 0
289
- });
290
- }
291
-
292
- function _createProject(DefifaLaunchProjectData memory defifaLaunchData)
293
- internal
294
- returns (uint256 projectId, DefifaHook nft, DefifaGovernor _governor)
295
- {
296
- _governor = governor;
297
- (projectId) = deployer.launchGameWith(defifaLaunchData);
298
- JBRuleset memory _fc = jbRulesets().currentOf(projectId);
299
- if (_fc.dataHook() == address(0)) {
300
- (_fc,) = jbRulesets().latestQueuedOf(projectId);
301
- }
302
- nft = DefifaHook(_fc.dataHook());
303
- }
304
-
305
- function _generateTokenId(uint256 _tierId, uint256 _tokenNumber) internal pure returns (uint256) {
306
- return (_tierId * 1_000_000_000) + _tokenNumber;
307
- }
308
-
309
- function _buildPayMetadata(bytes memory metadata) internal view returns (bytes memory) {
310
- bytes[] memory data = new bytes[](1);
311
- data[0] = metadata;
312
- bytes4[] memory ids = new bytes4[](1);
313
- ids[0] = metadataHelper().getId("pay", address(hook));
314
- return metadataHelper().createMetadata(ids, data);
315
- }
316
-
317
- function _buildCashOutMetadata(bytes memory decodedData) internal view returns (bytes memory) {
318
- bytes4[] memory ids = new bytes4[](1);
319
- ids[0] = metadataHelper().getId("cashOut", address(hook));
320
- bytes[] memory datas = new bytes[](1);
321
- datas[0] = decodedData;
322
- return metadataHelper().createMetadata(ids, datas);
323
- }
324
- }
325
-
326
- // =============================================================================
327
- // QA LAST-MILE TEST 2: GAME-ID PREDICTION RACE — STALE STORAGE ON REVERT
328
- // =============================================================================
329
-
330
- /// @title TestQAGameIdPredictionRace
331
- /// @notice Tests the gameId prediction race condition in DefifaDeployer.launchGameWith.
332
- /// @dev The deployer predicts gameId = PROJECTS().count() + 1, then clones and initializes a hook with that ID.
333
- /// If another project is created between the count() read and launchProjectFor(), the actual ID differs and
334
- /// the transaction reverts with DefifaDeployer_InvalidGameConfiguration. Because the clone uses
335
- /// cloneDeterministic with msg.sender in the salt, a retry from the same caller succeeds with a new nonce.
336
- /// No orphaned state remains after the revert.
337
- contract TestQAGameIdPredictionRace is JBTest, TestBaseWorkflow {
338
- using JBRulesetMetadataResolver for JBRuleset;
339
-
340
- QATimestampReader private _tsReader = new QATimestampReader();
341
-
342
- address _protocolFeeProjectTokenAccount;
343
- address _defifaProjectTokenAccount;
344
- uint256 _protocolFeeProjectId;
345
- uint256 _defifaProjectId;
346
-
347
- DefifaDeployer deployer;
348
- DefifaHook hook;
349
- DefifaGovernor governor;
350
-
351
- address projectOwner = address(bytes20(keccak256("projectOwner")));
352
-
353
- function setUp() public virtual override {
354
- super.setUp();
355
-
356
- JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
357
- _tokens[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH});
358
-
359
- JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
360
- terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: _tokens});
361
-
362
- JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
363
- rulesetConfigs[0] = JBRulesetConfig({
364
- mustStartAtOrAfter: 0,
365
- duration: 10 days,
366
- weight: 1e18,
367
- weightCutPercent: 0,
368
- approvalHook: IJBRulesetApprovalHook(address(0)),
369
- metadata: JBRulesetMetadata({
370
- reservedPercent: 0,
371
- cashOutTaxRate: 0,
372
- baseCurrency: JBCurrencyIds.ETH,
373
- pausePay: false,
374
- pauseCreditTransfers: false,
375
- allowOwnerMinting: false,
376
- allowSetCustomToken: false,
377
- allowTerminalMigration: false,
378
- allowSetTerminals: false,
379
- allowSetController: false,
380
- allowAddAccountingContext: false,
381
- allowAddPriceFeed: false,
382
- ownerMustSendPayouts: false,
383
- holdFees: false,
384
- useTotalSurplusForCashOuts: false,
385
- useDataHookForPay: true,
386
- useDataHookForCashOut: true,
387
- dataHook: address(0),
388
- metadata: 0
389
- }),
390
- splitGroups: new JBSplitGroup[](0),
391
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
392
- });
393
-
394
- _protocolFeeProjectId =
395
- jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
396
- vm.prank(projectOwner);
397
- _protocolFeeProjectTokenAccount =
398
- address(jbController().deployERC20For(_protocolFeeProjectId, "Bananapus", "NANA", bytes32(0)));
399
-
400
- _defifaProjectId =
401
- jbController().launchProjectFor(address(projectOwner), "", rulesetConfigs, terminalConfigs, "");
402
- vm.prank(projectOwner);
403
- _defifaProjectTokenAccount =
404
- address(jbController().deployERC20For(_defifaProjectId, "Defifa", "DEFIFA", bytes32(0)));
405
-
406
- hook = new DefifaHook(
407
- jbDirectory(), IERC20(address(_defifaProjectTokenAccount)), IERC20(_protocolFeeProjectTokenAccount)
408
- );
409
- governor = new DefifaGovernor(jbController(), address(this));
410
- JBAddressRegistry _registry = new JBAddressRegistry();
411
- DefifaTokenUriResolver _tokenUriResolver = new DefifaTokenUriResolver(ITypeface(address(0)));
412
- deployer = new DefifaDeployer(
413
- address(hook),
414
- _tokenUriResolver,
415
- governor,
416
- jbController(),
417
- _registry,
418
- _defifaProjectId,
419
- _protocolFeeProjectId
420
- );
421
-
422
- hook.transferOwnership(address(deployer));
423
- governor.transferOwnership(address(deployer));
424
- }
425
-
426
- /// @notice Proves the gameId prediction race: when count() returns a stale value (simulating a front-run
427
- /// where another project is created between the deployer's count() read and launchProjectFor()),
428
- /// the deployment reverts. The revert is caught by JBDirectory's own project ID validation
429
- /// (JBDirectory_InvalidProjectIdInDirectory) before reaching the deployer's explicit check,
430
- /// providing defense-in-depth. The caller can retry successfully with a fresh count.
431
- function test_gameIdPredictionRaceRevertsAndRetrySucceeds() public {
432
- // Record the current project count before the race.
433
- uint256 countBefore = jbController().PROJECTS().count();
434
- address projectsAddr = address(jbController().PROJECTS());
435
-
436
- // Build a valid game launch payload.
437
- DefifaLaunchProjectData memory defifaData = _getBasicLaunchData(4);
438
-
439
- // --- Simulate the front-run via mock: make count() return a stale (lower) value ---
440
- vm.mockCall(projectsAddr, abi.encodeWithSignature("count()"), abi.encode(countBefore - 1));
441
-
442
- // The deployer's launch reverts due to ID mismatch.
443
- vm.expectRevert();
444
- deployer.launchGameWith(defifaData);
445
-
446
- // Clear the mock so future calls get the real count.
447
- vm.clearMockedCalls();
448
-
449
- // --- Verify no orphaned state exists after the revert ---
450
- uint256 staleGameId = countBefore;
451
-
452
- assertEq(governor.attestationStartTimeOf(staleGameId), 0, "governor should not be initialized for stale gameId");
453
- assertEq(
454
- governor.attestationGracePeriodOf(staleGameId), 0, "governor grace period should be 0 for stale gameId"
455
- );
456
- assertEq(deployer.tokenOf(staleGameId), address(0), "no ops stored for stale gameId");
457
- assertEq(deployer.fulfilledCommitmentsOf(staleGameId), 0, "no commitments for stale gameId");
458
-
459
- // --- Retry: the caller can successfully launch with the correct gameId ---
460
- DefifaLaunchProjectData memory retryData = _getBasicLaunchData(4);
461
-
462
- uint256 countNow = jbController().PROJECTS().count();
463
- assertEq(countNow, countBefore, "count unchanged after reverted launch");
464
-
465
- uint256 retryGameId = deployer.launchGameWith(retryData);
466
-
467
- assertEq(retryGameId, countNow + 1, "retry should get the correct gameId");
468
- assertEq(
469
- uint256(deployer.currentGamePhaseOf(retryGameId)),
470
- uint256(DefifaGamePhase.COUNTDOWN),
471
- "game should be in COUNTDOWN phase"
472
- );
473
- assertGt(governor.attestationGracePeriodOf(retryGameId), 0, "governor should be initialized for retry gameId");
474
- assertEq(deployer.tokenOf(retryGameId), JBConstants.NATIVE_TOKEN, "ops stored correctly for retry gameId");
475
- }
476
-
477
- // ----- Internal helpers ------
478
-
479
- function _getBasicLaunchData(uint8 nTiers) internal returns (DefifaLaunchProjectData memory) {
480
- DefifaTierParams[] memory tierParams = new DefifaTierParams[](nTiers);
481
- for (uint256 i = 0; i < nTiers; i++) {
482
- tierParams[i] = DefifaTierParams({
483
- reservedRate: 1001,
484
- reservedTokenBeneficiary: address(0),
485
- encodedIPFSUri: bytes32(0),
486
- shouldUseReservedTokenBeneficiaryAsDefault: false,
487
- name: "DEFIFA"
488
- });
489
- }
490
-
491
- return DefifaLaunchProjectData({
492
- name: "DEFIFA",
493
- projectUri: "",
494
- contractUri: "",
495
- baseUri: "",
496
- tierPrice: 1 ether,
497
- token: JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: JBCurrencyIds.ETH}),
498
- mintPeriodDuration: 1 days,
499
- start: uint48(block.timestamp + 3 days),
500
- refundPeriodDuration: 1 days,
501
- store: new JB721TiersHookStore(),
502
- splits: new JBSplit[](0),
503
- attestationStartTime: 0,
504
- attestationGracePeriod: 100_381,
505
- defaultAttestationDelegate: address(0),
506
- tiers: tierParams,
507
- defaultTokenUriResolver: IJB721TokenUriResolver(address(0)),
508
- terminal: jbMultiTerminal(),
509
- minParticipation: 0,
510
- scorecardTimeout: 0,
511
- timelockDuration: 0
512
- });
513
- }
514
- }