@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
@@ -90,5 +90,9 @@ The contracts split responsibility as follows:
90
90
  ## Verification
91
91
 
92
92
  - `npm install`
93
- - `forge build`
94
- - `forge test`
93
+ - `forge fmt --check`
94
+ - `forge build --deny notes`
95
+ - `forge build --deny notes --sizes --skip "*/test/**" --skip "*/script/**"`
96
+ - `forge build --deny notes --build-info --skip "*/test/**" --skip "*/script/**"`
97
+ - `forge test --deny notes`
98
+ - `npm pack --dry-run --json`
package/README.md CHANGED
@@ -85,8 +85,17 @@ npm install @ballkidz/defifa
85
85
 
86
86
  ```bash
87
87
  npm install
88
- forge build
89
- forge test
88
+ forge build --deny notes
89
+ forge test --deny notes
90
+ ```
91
+
92
+ Useful checks before opening or updating a PR:
93
+
94
+ ```bash
95
+ forge fmt --check
96
+ forge build --deny notes --sizes --skip "*/test/**" --skip "*/script/**"
97
+ forge build --deny notes --build-info --skip "*/test/**" --skip "*/script/**"
98
+ npm pack --dry-run --json
90
99
  ```
91
100
 
92
101
  Useful scripts:
package/RISKS.md CHANGED
@@ -26,9 +26,11 @@ This file focuses on the game-theoretic, governance, and settlement risks in Def
26
26
 
27
27
  ## 2. Economic Risks
28
28
 
29
- - **Scorecard manipulation via quorum.** Enough attestation power can redirect the whole pot.
29
+ - **Scorecard manipulation via quorum.** Enough attestation power can redirect the whole pot. Once a coalition reaches the BWA (best winning attestation) quorum threshold, they can lock in a scorecard that favors their tiers. The grace period is the primary defense window for other participants to revoke attestations.
30
+ - **BWA quorum lockout.** If a scorecard reaches quorum and the grace period elapses before opponents can revoke, the scorecard becomes ratifiable. In games with concentrated attestation power (few large holders), quorum may be reached quickly, leaving little time for the grace period defense to be effective.
30
31
  - **Supply and pending-reserve drift.** Governance and settlement both depend on correct reserve-aware denominators.
31
32
  - **Cash-out-weight truncation.** Integer division can lock small dust amounts.
33
+ - **Commitment fee rounding to zero.** When commitment amounts are small relative to the fee percent, `mulDiv(amount, feePercent, MAX_FEE)` can round down to zero, allowing tiny commitments to bypass fees entirely. This is economically insignificant for practical game sizes but means fee accounting is not perfectly tight at dust amounts.
32
34
  - **Fee-token dilution from reserved mints.** Reserved mints can dilute fee-token shares even though no ETH was paid for them.
33
35
  - **128-tier settlement ceiling.** Games that rely on more than 128 scored tiers can fail settlement.
34
36
 
package/STYLE_GUIDE.md CHANGED
@@ -419,17 +419,17 @@ jobs:
419
419
  submodules: recursive
420
420
  - uses: actions/setup-node@v4
421
421
  with:
422
- node-version: 22.4.x
422
+ node-version: 25.9.0
423
423
  - name: Install npm dependencies
424
- run: npm install --omit=dev
424
+ run: npm install
425
425
  - name: Install Foundry
426
426
  uses: foundry-rs/foundry-toolchain@v1
427
427
  - name: Run tests
428
- run: forge test --fail-fast --summary --detailed --skip "*/script/**"
428
+ run: forge test --deny notes --fail-fast --summary --detailed --skip "*/script/**"
429
429
  env:
430
430
  RPC_ETHEREUM_MAINNET: ${{ secrets.RPC_ETHEREUM_MAINNET }}
431
- - name: Check contract sizes
432
- run: forge build --sizes --skip "*/test/**" --skip "*/script/**" --skip SphinxUtils
431
+ - name: Build contracts
432
+ run: forge build --deny notes --skip "*/test/**" --skip "*/script/**"
433
433
  ```
434
434
 
435
435
  **lint.yml:**
@@ -470,16 +470,19 @@ jobs:
470
470
  submodules: recursive
471
471
  - uses: actions/setup-node@v4
472
472
  with:
473
- node-version: latest
473
+ node-version: 25.9.0
474
474
  - name: Install npm dependencies
475
475
  run: npm install --omit=dev
476
476
  - name: Install Foundry
477
477
  uses: foundry-rs/foundry-toolchain@v1
478
+ - name: Prebuild contracts
479
+ run: forge build --deny notes --build-info --skip "*/test/**" --skip "*/script/**"
478
480
  - name: Run slither
479
- uses: crytic/slither-action@v0.3.1
481
+ uses: crytic/slither-action@v0.4.1
480
482
  with:
481
483
  slither-config: slither-ci.config.json
482
484
  fail-on: medium
485
+ ignore-compile: true
483
486
  ```
484
487
 
485
488
  **slither-ci.config.json:**
@@ -508,14 +511,14 @@ jobs:
508
511
  "version": "x.x.x",
509
512
  "license": "MIT",
510
513
  "repository": { "type": "git", "url": "git+https://github.com/Org/repo.git" },
511
- "engines": { "node": ">=20.0.0" },
514
+ "engines": { "node": "25.9.0" },
512
515
  "scripts": {
513
516
  "test": "forge test",
514
517
  "coverage": "forge coverage --match-path \"./src/*.sol\" --report lcov --report summary"
515
518
  },
516
519
  "dependencies": { ... },
517
520
  "devDependencies": {
518
- "@sphinx-labs/plugins": "^0.33.2"
521
+ "@sphinx-labs/plugins": "0.33.3"
519
522
  }
520
523
  }
521
524
  ```
@@ -603,8 +606,8 @@ CI checks formatting via `forge fmt --check`.
603
606
  - `forge-std` as a git submodule in `lib/`
604
607
  - Sphinx plugins as a devDependency
605
608
  - Cross-repo references use `file:../sibling-repo` in local development
606
- - Published versions use semver ranges (`^0.0.x`) for npm
609
+ - Published npm versions are pinned exactly; do not use semver ranges or `latest`
607
610
 
608
611
  ### Contract Size Checks
609
612
 
610
- CI runs `forge build --sizes` to catch contracts approaching the 24KB limit. When the repo's default `optimizer_runs` differs from what you want for size checking, use `FOUNDRY_PROFILE=ci_sizes forge build --sizes` with a `[profile.ci_sizes]` section in `foundry.toml`.
613
+ CI runs `forge build --deny notes --sizes` to catch contracts approaching the 24KB limit. When the repo's default `optimizer_runs` differs from what you want for size checking, use `FOUNDRY_PROFILE=ci_sizes forge build --deny notes --sizes` with a `[profile.ci_sizes]` section in `foundry.toml`.
package/package.json CHANGED
@@ -1,30 +1,47 @@
1
1
  {
2
2
  "name": "@ballkidz/defifa",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "license": "MIT",
5
5
  "engines": {
6
- "node": ">=20.0.0"
6
+ "node": "25.9.0"
7
7
  },
8
8
  "bugs": {
9
- "url": "https://github.com/BallKidz/defifa-collection-deployer/issues"
9
+ "url": "https://github.com/BallKidz/defifa/issues"
10
10
  },
11
11
  "repository": {
12
12
  "type": "git",
13
- "url": "https://github.com/BallKidz/defifa-collection-deployer"
13
+ "url": "git+https://github.com/BallKidz/defifa.git"
14
14
  },
15
+ "files": [
16
+ "src",
17
+ "script",
18
+ "lib/base64/base64.sol",
19
+ "lib/typeface/contracts/interfaces/ITypeface.sol",
20
+ "README.md",
21
+ "ARCHITECTURE.md",
22
+ "ADMINISTRATION.md",
23
+ "AUDIT_INSTRUCTIONS.md",
24
+ "CHANGELOG.md",
25
+ "CRYPTO_ECON.md",
26
+ "RISKS.md",
27
+ "SKILLS.md",
28
+ "STYLE_GUIDE.md",
29
+ "USER_JOURNEYS.md",
30
+ "foundry.toml",
31
+ "remappings.txt"
32
+ ],
15
33
  "dependencies": {
16
- "@bananapus/721-hook-v6": "^0.0.35",
17
- "@bananapus/core-v6": "^0.0.34",
18
- "@bananapus/permission-ids-v6": "^0.0.17",
19
- "@croptop/core-v6": "^0.0.33",
20
- "@openzeppelin/contracts": "^5.6.1",
21
- "@prb/math": "^4.1.1",
22
- "@rev-net/core-v6": "^0.0.32",
23
- "scripty.sol": "^2.1.1"
34
+ "@bananapus/721-hook-v6": "0.0.43",
35
+ "@bananapus/address-registry-v6": "0.0.25",
36
+ "@bananapus/core-v6": "0.0.39",
37
+ "@bananapus/permission-ids-v6": "0.0.22",
38
+ "@openzeppelin/contracts": "5.6.1",
39
+ "@prb/math": "4.1.1",
40
+ "scripty.sol": "2.1.1"
24
41
  },
25
42
  "devDependencies": {
26
- "@bananapus/address-registry-v6": "^0.0.17",
27
- "@sphinx-labs/plugins": "^0.33.3"
43
+ "@sphinx-labs/contracts": "0.23.1",
44
+ "@sphinx-labs/plugins": "0.33.3"
28
45
  },
29
46
  "scripts": {
30
47
  "test": "forge test",
@@ -4,6 +4,7 @@ pragma solidity 0.8.28;
4
4
  import {Script} from "forge-std/Script.sol";
5
5
  import {ITypeface} from "lib/typeface/contracts/interfaces/ITypeface.sol";
6
6
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7
+ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
7
8
  import {DefifaHook} from "../src/DefifaHook.sol";
8
9
  import {DefifaDeployer} from "../src/DefifaDeployer.sol";
9
10
  import {DefifaGovernor} from "../src/DefifaGovernor.sol";
@@ -109,6 +110,7 @@ contract DeployMainnet is Script, Sphinx {
109
110
  });
110
111
  DefifaTokenUriResolver tokenUriResolver = new DefifaTokenUriResolver{salt: _salt}(_typeface);
111
112
  DefifaGovernor governor = new DefifaGovernor{salt: _salt}({controller: core.controller, owner: safeAddress()});
113
+ JB721TiersHookStore hookStore = new JB721TiersHookStore{salt: _salt}();
112
114
  DefifaDeployer deployer = new DefifaDeployer{salt: _salt}({
113
115
  _hookCodeOrigin: address(hook),
114
116
  _tokenUriResolver: tokenUriResolver,
@@ -116,7 +118,8 @@ contract DeployMainnet is Script, Sphinx {
116
118
  _controller: core.controller,
117
119
  _registry: registry.registry,
118
120
  _defifaProjectId: _defifaProjectId,
119
- _baseProtocolProjectId: _baseProtocolProjectId
121
+ _baseProtocolProjectId: _baseProtocolProjectId,
122
+ _hookStore: hookStore
120
123
  });
121
124
 
122
125
  governor.transferOwnership(address(deployer));
@@ -28,6 +28,8 @@ import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Recei
28
28
  import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
29
29
  import {mulDiv} from "@prb/math/src/Common.sol";
30
30
 
31
+ import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
32
+
31
33
  import {DefifaHook} from "./DefifaHook.sol";
32
34
  import {DefifaGamePhase} from "./enums/DefifaGamePhase.sol";
33
35
  import {IDefifaDeployer} from "./interfaces/IDefifaDeployer.sol";
@@ -102,6 +104,9 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
102
104
  /// @notice The hooks registry.
103
105
  IJBAddressRegistry public immutable REGISTRY;
104
106
 
107
+ /// @notice The 721 tiers hook store used by all games.
108
+ IJB721TiersHookStore public immutable HOOK_STORE;
109
+
105
110
  /// @notice The divisor that describes the protocol fee that should be taken.
106
111
  /// @dev This is equal to 100 divided by the fee percent (e.g. 40 = 2.5% fee).
107
112
  uint256 public constant override BASE_PROTOCOL_FEE_DIVISOR = 40;
@@ -121,6 +126,9 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
121
126
  /// @notice The total absolute split percent for each game (out of SPLITS_TOTAL_PERCENT).
122
127
  mapping(uint256 => uint256) internal _commitmentPercentOf;
123
128
 
129
+ /// @notice Whether commitments have been fulfilled for a game.
130
+ mapping(uint256 => bool) public commitmentsFulfilledFor;
131
+
124
132
  /// @notice Whether the no-contest refund ruleset has been triggered for a game.
125
133
  /// @dev Once triggered, the game stays in NO_CONTEST and refunds are enabled.
126
134
  mapping(uint256 => bool) public noContestTriggeredFor;
@@ -253,6 +261,8 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
253
261
 
254
262
  // Check scorecard ratification timeout: if enough time has passed without a ratified scorecard, the game is
255
263
  // NO_CONTEST.
264
+ // Game phase timing is timestamp-based by design.
265
+ // forge-lint: disable-next-line(block-timestamp)
256
266
  if (ops.scorecardTimeout > 0 && block.timestamp > currentRuleset.start + ops.scorecardTimeout) {
257
267
  return DefifaGamePhase.NO_CONTEST;
258
268
  }
@@ -278,7 +288,8 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
278
288
  IJBController _controller,
279
289
  IJBAddressRegistry _registry,
280
290
  uint256 _defifaProjectId,
281
- uint256 _baseProtocolProjectId
291
+ uint256 _baseProtocolProjectId,
292
+ IJB721TiersHookStore _hookStore
282
293
  ) {
283
294
  // slither-disable-next-line missing-zero-check
284
295
  HOOK_CODE_ORIGIN = _hookCodeOrigin;
@@ -288,6 +299,7 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
288
299
  REGISTRY = _registry;
289
300
  DEFIFA_PROJECT_ID = _defifaProjectId;
290
301
  BASE_PROTOCOL_PROJECT_ID = _baseProtocolProjectId;
302
+ HOOK_STORE = _hookStore;
291
303
  /// @dev Uses the deployer address as group ID. Game scoring rulesets use uint160(token) as group ID.
292
304
  SPLIT_GROUP = uint256(uint160(address(this)));
293
305
  }
@@ -298,9 +310,11 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
298
310
 
299
311
  /// @notice Fulfill split amounts between all splits for a game.
300
312
  /// @param gameId The ID of the game to fulfill splits for.
313
+ // slither-disable-next-line reentrancy-benign,reentrancy-no-eth
301
314
  function fulfillCommitmentsOf(uint256 gameId) external virtual override {
302
315
  // Make sure commitments haven't already been fulfilled.
303
- if (fulfilledCommitmentsOf[gameId] != 0) return;
316
+ if (commitmentsFulfilledFor[gameId]) return;
317
+ commitmentsFulfilledFor[gameId] = true;
304
318
 
305
319
  // Get the game's current funding cycle along with its metadata.
306
320
  // slither-disable-next-line unused-return
@@ -319,10 +333,9 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
319
333
  // Get the current pot and store it. This also prevents re-entrance since the check above will return early.
320
334
  uint256 pot = terminal.STORE().balanceOf({terminal: address(terminal), projectId: gameId, token: token});
321
335
 
322
- // If the pot is empty, set the sentinel and queue the final ruleset without attempting payouts.
336
+ // If the pot is empty, queue the final ruleset without attempting payouts.
323
337
  // slither-disable-next-line incorrect-equality
324
338
  if (pot == 0) {
325
- fulfilledCommitmentsOf[gameId] = 1;
326
339
  _queueFinalRuleset({gameId: gameId, metadata: metadata});
327
340
  // slither-disable-next-line reentrancy-events
328
341
  emit FulfilledCommitments({gameId: gameId, pot: 0, caller: msg.sender});
@@ -334,8 +347,7 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
334
347
  mulDiv({x: pot, y: _commitmentPercentOf[gameId], denominator: JBConstants.SPLITS_TOTAL_PERCENT});
335
348
 
336
349
  // Store the actual fee amount for accurate currentGamePotOf reporting.
337
- // Use max(feeAmount, 1) to preserve the reentrancy guard when pot is 0.
338
- fulfilledCommitmentsOf[gameId] = feeAmount > 0 ? feeAmount : 1;
350
+ fulfilledCommitmentsOf[gameId] = feeAmount;
339
351
 
340
352
  // Send only the fee portion as payouts. The remaining balance stays as surplus for cash-outs.
341
353
  // Use the ruleset's baseCurrency — this matches the currency under which payout limits were stored
@@ -346,9 +358,9 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
346
358
  projectId: gameId, token: token, amount: feeAmount, currency: metadata.baseCurrency, minTokensPaidOut: 0
347
359
  }) {}
348
360
  catch (bytes memory reason) {
349
- // Payout failed — fee stays in pot. Reset to sentinel (1) so currentGamePotOf
350
- // doesn't double-count the fee, while preserving the reentrancy guard.
351
- fulfilledCommitmentsOf[gameId] = 1;
361
+ // Payout failed — fee stays in pot. Reset to 0 so currentGamePotOf
362
+ // doesn't double-count the fee.
363
+ fulfilledCommitmentsOf[gameId] = 0;
352
364
  // slither-disable-next-line reentrancy-events
353
365
  emit CommitmentPayoutFailed({gameId: gameId, amount: feeAmount, reason: reason});
354
366
  }
@@ -363,24 +375,34 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
363
375
  /// @notice Launches a new game owned by this contract with a DefifaHook attached.
364
376
  /// @param launchProjectData Data necessary to fulfill the transaction to launch a game.
365
377
  /// @return gameId The ID of the newly configured game.
378
+ // slither-disable-next-line reentrancy-benign,reentrancy-no-eth
366
379
  function launchGameWith(DefifaLaunchProjectData memory launchProjectData)
367
380
  external
368
381
  override
369
382
  returns (uint256 gameId)
370
383
  {
371
- // Start the game right after the mint and refund durations if it isnt provided.
384
+ // Game launch timing is timestamp-based by design.
385
+ uint256 currentTimestamp = block.timestamp;
386
+
387
+ // If no explicit game start is provided, start after the configured mint and refund windows.
372
388
  if (launchProjectData.start == 0) {
373
- launchProjectData.start =
374
- uint48(block.timestamp + launchProjectData.mintPeriodDuration + launchProjectData.refundPeriodDuration);
389
+ uint256 start =
390
+ currentTimestamp + launchProjectData.mintPeriodDuration + launchProjectData.refundPeriodDuration;
391
+ if (start > type(uint48).max) revert DefifaDeployer_InvalidGameConfiguration();
392
+
393
+ // Casting to uint48 is safe because `start` was bounded above.
394
+ // forge-lint: disable-next-line(unsafe-typecast)
395
+ launchProjectData.start = uint48(start);
375
396
  }
376
- // Start minting right away if a start time isn't provided.
397
+ // If callers provide a future start with no mint duration, derive a mint window that begins now and ends
398
+ // before the refund window. This preserves the requested start while keeping minting immediately available.
377
399
  // slither-disable-next-line incorrect-equality
378
400
  else if (
379
401
  launchProjectData.mintPeriodDuration == 0
380
- && launchProjectData.start > block.timestamp + launchProjectData.refundPeriodDuration
402
+ && launchProjectData.start > currentTimestamp + launchProjectData.refundPeriodDuration
381
403
  ) {
382
404
  launchProjectData.mintPeriodDuration =
383
- uint24(launchProjectData.start - (block.timestamp + launchProjectData.refundPeriodDuration));
405
+ uint24(launchProjectData.start - (currentTimestamp + launchProjectData.refundPeriodDuration));
384
406
  }
385
407
 
386
408
  // Make sure the provided gameplay timestamps are sequential and that there is a mint duration.
@@ -388,24 +410,29 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
388
410
  // slither-disable-next-line incorrect-equality
389
411
  launchProjectData.mintPeriodDuration == 0
390
412
  || launchProjectData.start
391
- < block.timestamp + launchProjectData.refundPeriodDuration + launchProjectData.mintPeriodDuration
413
+ < currentTimestamp + launchProjectData.refundPeriodDuration + launchProjectData.mintPeriodDuration
392
414
  ) revert DefifaDeployer_InvalidGameConfiguration();
393
415
 
394
416
  // The hook and governor hardcode uint256[128] tier-weight tables, so reject games with more than 128 tiers.
395
417
  if (launchProjectData.tiers.length > 128) revert DefifaDeployer_InvalidGameConfiguration();
396
418
 
397
- // Reject ERC-20 games with a zero currency a zero baseCurrency would cause payout limit lookups
419
+ // Reject ERC-20 games with a zero currency. A zero baseCurrency would cause payout limit lookups
398
420
  // in fulfillCommitmentsOf to silently fail, skipping all commitment payouts.
399
421
  // slither-disable-next-line incorrect-equality
400
422
  if (launchProjectData.token.token != JBConstants.NATIVE_TOKEN && launchProjectData.token.currency == 0) {
401
423
  revert DefifaDeployer_InvalidCurrency();
402
424
  }
403
425
 
404
- // Get the game ID, optimistically knowing it will be one greater than the current count.
405
- // Note: this prediction can race with other concurrent project deployments. If another project is
406
- // created between reading count() and launchProjectFor(), the actual ID will differ. This is
407
- // caught by the equality check after launch (gameId != actualGameId reverts).
408
- gameId = CONTROLLER.PROJECTS().count() + 1;
426
+ // If a scorecard timeout is set, it must exceed the grace period + timelock duration.
427
+ // Otherwise the game would enter NO_CONTEST before a scorecard could ever reach SUCCEEDED.
428
+ if (
429
+ launchProjectData.scorecardTimeout > 0
430
+ && launchProjectData.scorecardTimeout
431
+ <= launchProjectData.attestationGracePeriod + launchProjectData.timelockDuration
432
+ ) revert DefifaDeployer_InvalidGameConfiguration();
433
+
434
+ // Reserve the game ID up front so permissionless project creations cannot invalidate hook deployment.
435
+ gameId = CONTROLLER.PROJECTS().createFor(address(this));
409
436
 
410
437
  {
411
438
  // Store the timestamps that'll define the game phases.
@@ -536,19 +563,15 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
536
563
  _contractUri: launchProjectData.contractUri,
537
564
  _tiers: hookTiers,
538
565
  _currency: launchProjectData.token.currency,
539
- _store: launchProjectData.store,
566
+ _store: HOOK_STORE,
540
567
  _gamePhaseReporter: this,
541
568
  _gamePotReporter: this,
542
569
  _defaultAttestationDelegate: launchProjectData.defaultAttestationDelegate,
543
570
  _tierNames: tierNames
544
571
  });
545
572
 
546
- // Launch the Juicebox project.
547
- uint256 actualGameId =
548
- _launchGame({launchProjectData: launchProjectData, gameId: gameId, dataHook: address(hook)});
549
-
550
- // Revert if the game ID does not match (e.g. front-run by another project creation).
551
- if (gameId != actualGameId) revert DefifaDeployer_InvalidGameConfiguration();
573
+ // Launch the Juicebox rulesets for the reserved project.
574
+ _launchGame({launchProjectData: launchProjectData, gameId: gameId, dataHook: address(hook)});
552
575
 
553
576
  // Clone and initialize the new governor.
554
577
  GOVERNOR.initializeGame({
@@ -742,15 +765,15 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
742
765
  return groupedSplits;
743
766
  }
744
767
 
745
- function _launchGame(
746
- DefifaLaunchProjectData memory launchProjectData,
747
- uint256 gameId,
748
- address dataHook
749
- )
750
- internal
751
- returns (uint256 projectId)
752
- {
753
- //
768
+ /// @notice Launch the Juicebox project rulesets that define a Defifa game's lifecycle.
769
+ /// @dev The project ID is reserved before the hook clone is initialized, so this function launches rulesets for
770
+ /// that existing project instead of creating a new one. It also forwards `projectUri` to the controller so the
771
+ /// project metadata is set atomically with the first rulesets.
772
+ /// @param launchProjectData The normalized launch data for the game.
773
+ /// @param gameId The reserved Juicebox project ID for the game.
774
+ /// @param dataHook The initialized Defifa hook clone used by every ruleset.
775
+ function _launchGame(DefifaLaunchProjectData memory launchProjectData, uint256 gameId, address dataHook) internal {
776
+ // Accept exactly the token/accounting context the game was configured to use.
754
777
  JBAccountingContext[] memory accountingContexts = new JBAccountingContext[](1);
755
778
  accountingContexts[0] = launchProjectData.token;
756
779
 
@@ -759,11 +782,13 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
759
782
  terminalConfigurations[0] =
760
783
  JBTerminalConfig({terminal: launchProjectData.terminal, accountingContextsToAccept: accountingContexts});
761
784
 
762
- // Build the rulesets that this Defifa game will go through.
785
+ // Build the rulesets that this Defifa game will go through. Games always have MINT and SCORING phases; the
786
+ // REFUND phase is omitted entirely when its duration is zero so the scoring ruleset can follow minting.
763
787
  bool hasRefundPhase = launchProjectData.refundPeriodDuration != 0;
764
788
  JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](hasRefundPhase ? 3 : 2);
765
789
 
766
- // `MINT` cycle.
790
+ // MINT cycle: payments are accepted, cash-outs are enabled, and pending reserved tokens are paused so the
791
+ // scoring/final phases decide how reserved tokens are distributed.
767
792
  rulesetConfigs[0] = JBRulesetConfig({
768
793
  mustStartAtOrAfter: launchProjectData.start - launchProjectData.mintPeriodDuration
769
794
  - launchProjectData.refundPeriodDuration,
@@ -806,7 +831,7 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
806
831
 
807
832
  uint256 cycleNumber = 1;
808
833
  if (hasRefundPhase) {
809
- // `REFUND` cycle.
834
+ // REFUND cycle: payments are paused while cash-outs remain available at pre-scoring terms.
810
835
  rulesetConfigs[cycleNumber++] = JBRulesetConfig({
811
836
  mustStartAtOrAfter: launchProjectData.start - launchProjectData.refundPeriodDuration,
812
837
  duration: launchProjectData.refundPeriodDuration,
@@ -848,7 +873,8 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
848
873
  });
849
874
  }
850
875
 
851
- // Set fund access constraints.
876
+ // The scoring ruleset can send the whole pot as payouts. `fulfillCommitmentsOf` only sends the commitment
877
+ // portion and leaves the rest as surplus for the final cash-out ruleset.
852
878
  JBCurrencyAmount[] memory payoutAmounts = new JBCurrencyAmount[](1);
853
879
  payoutAmounts[0] = JBCurrencyAmount({
854
880
  // We allow a payout of the full amount, this will then mostly be added back to the balance of the project.
@@ -864,7 +890,8 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
864
890
  surplusAllowances: new JBCurrencyAmount[](0)
865
891
  });
866
892
 
867
- // `SCORING` cycle.
893
+ // SCORING cycle: payments pause, the game owner must send payouts, and the Defifa hook determines the final
894
+ // cash-out weights once a scorecard is ratified.
868
895
  rulesetConfigs[cycleNumber++] = JBRulesetConfig({
869
896
  mustStartAtOrAfter: launchProjectData.start,
870
897
  duration: 0,
@@ -906,9 +933,10 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
906
933
  fundAccessLimitGroups: fundAccessConstraints
907
934
  });
908
935
 
909
- // launch the project.
910
- return CONTROLLER.launchProjectFor({
911
- owner: address(this),
936
+ // Launch the rulesets for the reserved project and set the project URI in the same controller call.
937
+ // slither-disable-next-line unused-return
938
+ CONTROLLER.launchRulesetsFor({
939
+ projectId: gameId,
912
940
  projectUri: launchProjectData.projectUri,
913
941
  rulesetConfigurations: rulesetConfigs,
914
942
  terminalConfigurations: terminalConfigurations,