@ballkidz/defifa 0.0.24 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/AUDIT_INSTRUCTIONS.md +6 -2
  2. package/README.md +11 -2
  3. package/RISKS.md +3 -1
  4. package/STYLE_GUIDE.md +14 -11
  5. package/package.json +31 -14
  6. package/script/Deploy.s.sol +4 -1
  7. package/src/DefifaDeployer.sol +74 -46
  8. package/src/DefifaGovernor.sol +53 -11
  9. package/src/DefifaHook.sol +79 -25
  10. package/src/DefifaTokenUriResolver.sol +111 -19
  11. package/src/interfaces/IDefifaDeployer.sol +5 -0
  12. package/src/interfaces/IDefifaGovernor.sol +4 -0
  13. package/src/interfaces/IDefifaHook.sol +5 -0
  14. package/src/libraries/DefifaHookLib.sol +9 -10
  15. package/src/structs/DefifaLaunchProjectData.sol +0 -3
  16. package/CRYPTO_ECON.pdf +0 -0
  17. package/CRYPTO_ECON.tex +0 -997
  18. package/foundry.lock +0 -17
  19. package/references/operations.md +0 -32
  20. package/references/runtime.md +0 -43
  21. package/slither-ci.config.json +0 -10
  22. package/sphinx.lock +0 -521
  23. package/test/BWAFunctionComparison.t.sol +0 -1320
  24. package/test/DefifaAdversarialQuorum.t.sol +0 -617
  25. package/test/DefifaAuditLowGuards.t.sol +0 -308
  26. package/test/DefifaFeeAccounting.t.sol +0 -581
  27. package/test/DefifaGovernanceHardening.t.sol +0 -1315
  28. package/test/DefifaGovernor.t.sol +0 -1378
  29. package/test/DefifaHookRegressions.t.sol +0 -415
  30. package/test/DefifaMintCostInvariant.t.sol +0 -319
  31. package/test/DefifaNoContest.t.sol +0 -941
  32. package/test/DefifaSecurity.t.sol +0 -741
  33. package/test/DefifaUSDC.t.sol +0 -480
  34. package/test/Fork.t.sol +0 -2388
  35. package/test/TestAuditGaps.sol +0 -984
  36. package/test/TestQALastMile.t.sol +0 -514
  37. package/test/audit/AttestationDoubleCount.t.sol +0 -218
  38. package/test/audit/CodexNemesisCurrencyMismatchBypass.t.sol +0 -112
  39. package/test/audit/CodexNemesisNoContestReserveDrain.t.sol +0 -238
  40. package/test/audit/CodexRegistryMismatch.t.sol +0 -191
  41. package/test/audit/CodexTierCapMismatch.t.sol +0 -171
  42. package/test/audit/CurrencyMismatchFix.t.sol +0 -265
  43. package/test/audit/FixPendingReserveDilution.t.sol +0 -366
  44. package/test/audit/H5TierCapValidation.t.sol +0 -184
  45. package/test/audit/PendingReserveDilution.t.sol +0 -298
  46. package/test/audit/PendingReserveQuorumGrief.t.sol +0 -355
  47. package/test/audit/PendingReserveSnapshotBypass.t.sol +0 -319
  48. package/test/regression/AttestationDelegateBeneficiary.t.sol +0 -271
  49. package/test/regression/FulfillmentBlocksRatification.t.sol +0 -279
  50. package/test/regression/GracePeriodBypass.t.sol +0 -302
@@ -1,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
- }