@ballkidz/defifa 0.0.7 → 0.0.8

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 (46) hide show
  1. package/ADMINISTRATION.md +3 -3
  2. package/AUDIT_INSTRUCTIONS.md +422 -0
  3. package/CRYPTO_ECON.md +5 -5
  4. package/RISKS.md +38 -335
  5. package/SKILLS.md +1 -1
  6. package/USER_JOURNEYS.md +691 -0
  7. package/package.json +7 -7
  8. package/script/Deploy.s.sol +14 -3
  9. package/script/helpers/DefifaDeploymentLib.sol +13 -15
  10. package/src/DefifaDeployer.sol +221 -192
  11. package/src/DefifaGovernor.sol +286 -276
  12. package/src/DefifaHook.sol +65 -32
  13. package/src/DefifaProjectOwner.sol +27 -4
  14. package/src/DefifaTokenUriResolver.sol +136 -134
  15. package/src/enums/DefifaGamePhase.sol +1 -1
  16. package/src/enums/DefifaScorecardState.sol +1 -1
  17. package/src/interfaces/IDefifaDeployer.sol +52 -50
  18. package/src/interfaces/IDefifaGamePhaseReporter.sol +2 -2
  19. package/src/interfaces/IDefifaGamePotReporter.sol +1 -1
  20. package/src/interfaces/IDefifaGovernor.sol +53 -54
  21. package/src/interfaces/IDefifaHook.sol +104 -103
  22. package/src/interfaces/IDefifaTokenUriResolver.sol +2 -2
  23. package/src/libraries/DefifaFontImporter.sol +11 -9
  24. package/src/libraries/DefifaHookLib.sol +66 -53
  25. package/src/structs/DefifaAttestations.sol +1 -1
  26. package/src/structs/DefifaDelegation.sol +1 -1
  27. package/src/structs/DefifaLaunchProjectData.sol +4 -4
  28. package/src/structs/DefifaOpsData.sol +1 -1
  29. package/src/structs/DefifaScorecard.sol +1 -1
  30. package/src/structs/DefifaTierCashOutWeight.sol +1 -1
  31. package/src/structs/DefifaTierParams.sol +2 -1
  32. package/test/DefifaAdversarialQuorum.t.sol +602 -0
  33. package/test/DefifaAuditLowGuards.t.sol +304 -0
  34. package/test/DefifaFeeAccounting.t.sol +37 -16
  35. package/test/DefifaGovernor.t.sol +37 -11
  36. package/test/DefifaHookRegressions.t.sol +14 -12
  37. package/test/DefifaMintCostInvariant.t.sol +31 -12
  38. package/test/DefifaNoContest.t.sol +33 -13
  39. package/test/DefifaSecurity.t.sol +45 -25
  40. package/test/DefifaUSDC.t.sol +44 -34
  41. package/test/Fork.t.sol +42 -40
  42. package/test/SVG.t.sol +2 -2
  43. package/test/TestAuditGaps.sol +982 -0
  44. package/test/TestQALastMile.t.sol +511 -0
  45. package/test/regression/FulfillmentBlocksRatification.t.sol +36 -30
  46. package/test/regression/GracePeriodBypass.t.sol +15 -10
@@ -1,50 +1,43 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
- import {mulDiv} from "@prb/math/src/Common.sol";
5
- import {Address} from "@openzeppelin/contracts/utils/Address.sol";
4
+ import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
5
+ import {
6
+ JB721TiersRulesetMetadata,
7
+ JB721TiersRulesetMetadataResolver
8
+ } from "@bananapus/721-hook-v6/src/libraries/JB721TiersRulesetMetadataResolver.sol";
9
+ import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
10
+ import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
11
+ import {IJBController, JBRulesetConfig, JBTerminalConfig} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
12
+ import {IJBMultiTerminal} from "@bananapus/core-v6/src/interfaces/IJBMultiTerminal.sol";
13
+ import {IJBRulesetApprovalHook, JBRuleset} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
14
+ import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
15
+ import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
16
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
17
+ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
18
+ import {JBCurrencyAmount} from "@bananapus/core-v6/src/structs/JBCurrencyAmount.sol";
19
+ import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
20
+ import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
21
+ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
22
+ import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
6
23
  import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
7
- import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
8
- import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
9
24
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
25
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
26
+ import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
10
27
  import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
28
+ import {mulDiv} from "@prb/math/src/Common.sol";
29
+
30
+ import {DefifaHook} from "./DefifaHook.sol";
11
31
  import {DefifaGamePhase} from "./enums/DefifaGamePhase.sol";
12
32
  import {IDefifaDeployer} from "./interfaces/IDefifaDeployer.sol";
13
- import {IDefifaHook} from "./interfaces/IDefifaHook.sol";
14
33
  import {IDefifaGamePhaseReporter} from "./interfaces/IDefifaGamePhaseReporter.sol";
15
34
  import {IDefifaGamePotReporter} from "./interfaces/IDefifaGamePotReporter.sol";
16
35
  import {IDefifaGovernor} from "./interfaces/IDefifaGovernor.sol";
36
+ import {IDefifaHook} from "./interfaces/IDefifaHook.sol";
17
37
  import {DefifaLaunchProjectData} from "./structs/DefifaLaunchProjectData.sol";
18
- import {DefifaTierParams} from "./structs/DefifaTierParams.sol";
19
38
  import {DefifaOpsData} from "./structs/DefifaOpsData.sol";
20
- import {DefifaHook} from "./DefifaHook.sol";
21
- import {DefifaTokenUriResolver} from "./DefifaTokenUriResolver.sol";
22
-
23
- import {IJBToken} from "@bananapus/core-v6/src/interfaces/IJBToken.sol";
24
- import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
25
- import {IJBController, JBRulesetConfig, JBTerminalConfig} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
26
- import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
27
- import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
28
- import {IJBMultiTerminal} from "@bananapus/core-v6/src/interfaces/IJBMultiTerminal.sol";
29
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
30
- import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
31
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
32
- import {JBCurrencyAmount} from "@bananapus/core-v6/src/structs/JBCurrencyAmount.sol";
33
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
34
- import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
35
- import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
36
- import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
37
- import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
38
- import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
39
- import {JBSplitHookContext} from "@bananapus/core-v6/src/structs/JBSplitHookContext.sol";
40
- import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
41
- import {
42
- JB721TiersRulesetMetadata,
43
- JB721TiersRulesetMetadataResolver
44
- } from "@bananapus/721-hook-v6/src/libraries/JB721TiersRulesetMetadataResolver.sol";
45
- import {IJBRulesets, IJBRulesetApprovalHook, JBRuleset} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
39
+ import {DefifaTierParams} from "./structs/DefifaTierParams.sol";
46
40
 
47
- /// @title DefifaDeployer
48
41
  /// @notice Deploys and manages Defifa games.
49
42
  contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGamePotReporter, IERC721Receiver {
50
43
  using Strings for uint256;
@@ -55,7 +48,6 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
55
48
  //*********************************************************************//
56
49
 
57
50
  error DefifaDeployer_CantFulfillYet();
58
- error DefifaDeployer_NothingToFulfill();
59
51
  error DefifaDeployer_GameOver();
60
52
  error DefifaDeployer_InvalidFeePercent();
61
53
  error DefifaDeployer_InvalidGameConfiguration();
@@ -84,29 +76,29 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
84
76
 
85
77
  /// @notice The group relative to which splits are stored.
86
78
  /// @dev This could be any fixed number.
87
- uint256 public immutable override splitGroup;
79
+ uint256 public immutable override SPLIT_GROUP;
88
80
 
89
81
  /// @notice The project ID that'll receive game fees, and relative to which splits are stored.
90
82
  /// @dev The owner of this project ID must give this contract operator permissions over the SET_SPLITS operation.
91
- uint256 public immutable override defifaProjectId;
83
+ uint256 public immutable override DEFIFA_PROJECT_ID;
92
84
 
93
85
  /// @notice The project ID that'll receive protocol fees as commitments are fulfilled.
94
- uint256 public immutable override baseProtocolProjectId;
86
+ uint256 public immutable override BASE_PROTOCOL_PROJECT_ID;
95
87
 
96
88
  /// @notice The original code for the Defifa hook to base subsequent instances on.
97
- address public immutable override hookCodeOrigin;
89
+ address public immutable override HOOK_CODE_ORIGIN;
98
90
 
99
91
  /// @notice The default Defifa token URI resolver.
100
- IJB721TokenUriResolver public immutable override tokenUriResolver;
92
+ IJB721TokenUriResolver public immutable override TOKEN_URI_RESOLVER;
101
93
 
102
94
  /// @notice The Defifa governor.
103
- IDefifaGovernor public immutable override governor;
95
+ IDefifaGovernor public immutable override GOVERNOR;
104
96
 
105
97
  /// @notice The controller with which new projects should be deployed.
106
- IJBController public immutable override controller;
98
+ IJBController public immutable override CONTROLLER;
107
99
 
108
100
  /// @notice The hooks registry.
109
- IJBAddressRegistry public immutable registry;
101
+ IJBAddressRegistry public immutable REGISTRY;
110
102
 
111
103
  /// @notice The divisor that describes the protocol fee that should be taken.
112
104
  /// @dev This is equal to 100 divided by the fee percent (e.g. 40 = 2.5% fee).
@@ -154,7 +146,7 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
154
146
  address _token = _opsOf[gameId].token;
155
147
 
156
148
  // Get a reference to the terminal.
157
- IJBTerminal _terminal = controller.DIRECTORY().primaryTerminalOf({projectId: gameId, token: _token});
149
+ IJBTerminal _terminal = CONTROLLER.DIRECTORY().primaryTerminalOf({projectId: gameId, token: _token});
158
150
 
159
151
  // Get the accounting context for the project.
160
152
  JBAccountingContext memory _context = _terminal.accountingContextForTokenOf({projectId: gameId, token: _token});
@@ -174,10 +166,10 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
174
166
  /// @return Whether or not the next phase still needs queuing.
175
167
  function nextPhaseNeedsQueueing(uint256 gameId) external view override returns (bool) {
176
168
  // Get the game's current funding cycle along with its metadata.
177
- JBRuleset memory _currentRuleset = controller.RULESETS().currentOf(gameId);
169
+ JBRuleset memory _currentRuleset = CONTROLLER.RULESETS().currentOf(gameId);
178
170
  // Get the game's queued funding cycle along with its metadata.
179
171
  // slither-disable-next-line unused-return
180
- (JBRuleset memory _queuedRuleset,) = controller.RULESETS().latestQueuedOf(gameId);
172
+ (JBRuleset memory _queuedRuleset,) = CONTROLLER.RULESETS().latestQueuedOf(gameId);
181
173
 
182
174
  // If the configurations are the same and the game hasn't ended, queueing is still needed.
183
175
  return _currentRuleset.duration != 0 && _currentRuleset.id == _queuedRuleset.id;
@@ -226,7 +218,7 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
226
218
  /// @return The game phase.
227
219
  function currentGamePhaseOf(uint256 gameId) public view override returns (DefifaGamePhase) {
228
220
  // Get the game's current funding cycle along with its metadata.
229
- (JBRuleset memory _currentRuleset, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(gameId);
221
+ (JBRuleset memory _currentRuleset, JBRulesetMetadata memory _metadata) = CONTROLLER.currentRulesetOf(gameId);
230
222
 
231
223
  if (_currentRuleset.cycleNumber == 0) return DefifaGamePhase.COUNTDOWN;
232
224
  if (_currentRuleset.cycleNumber == 1) return DefifaGamePhase.MINT;
@@ -247,7 +239,7 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
247
239
  // Check minimum participation threshold: if the treasury balance is below the threshold, the game is
248
240
  // NO_CONTEST.
249
241
  if (_ops.minParticipation > 0) {
250
- IJBTerminal _terminal = controller.DIRECTORY().primaryTerminalOf({projectId: gameId, token: _ops.token});
242
+ IJBTerminal _terminal = CONTROLLER.DIRECTORY().primaryTerminalOf({projectId: gameId, token: _ops.token});
251
243
  uint256 _balance = IJBMultiTerminal(address(_terminal)).STORE()
252
244
  .balanceOf({terminal: address(_terminal), projectId: gameId, token: _ops.token});
253
245
  if (_balance < _ops.minParticipation) return DefifaGamePhase.NO_CONTEST;
@@ -282,15 +274,15 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
282
274
  uint256 _defifaProjectId,
283
275
  uint256 _baseProtocolProjectId
284
276
  ) {
285
- hookCodeOrigin = _hookCodeOrigin;
286
- tokenUriResolver = _tokenUriResolver;
287
- governor = _governor;
288
- controller = _controller;
289
- registry = _registry;
290
- defifaProjectId = _defifaProjectId;
291
- baseProtocolProjectId = _baseProtocolProjectId;
277
+ HOOK_CODE_ORIGIN = _hookCodeOrigin;
278
+ TOKEN_URI_RESOLVER = _tokenUriResolver;
279
+ GOVERNOR = _governor;
280
+ CONTROLLER = _controller;
281
+ REGISTRY = _registry;
282
+ DEFIFA_PROJECT_ID = _defifaProjectId;
283
+ BASE_PROTOCOL_PROJECT_ID = _baseProtocolProjectId;
292
284
  /// @dev Uses the deployer address as group ID. Game scoring rulesets use uint160(token) as group ID.
293
- splitGroup = uint256(uint160(address(this)));
285
+ SPLIT_GROUP = uint256(uint160(address(this)));
294
286
  }
295
287
 
296
288
  //*********************************************************************//
@@ -305,7 +297,7 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
305
297
 
306
298
  // Get the game's current funding cycle along with its metadata.
307
299
  // slither-disable-next-line unused-return
308
- (, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(gameId);
300
+ (, JBRulesetMetadata memory _metadata) = CONTROLLER.currentRulesetOf(gameId);
309
301
 
310
302
  // Make sure the game's commitments can be fulfilled.
311
303
  if (!IDefifaHook(_metadata.dataHook).cashOutWeightIsSet()) {
@@ -315,12 +307,19 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
315
307
  // Get the game token and the terminal.
316
308
  address _token = _opsOf[gameId].token;
317
309
  IJBMultiTerminal _terminal =
318
- IJBMultiTerminal(address(controller.DIRECTORY().primaryTerminalOf({projectId: gameId, token: _token})));
310
+ IJBMultiTerminal(address(CONTROLLER.DIRECTORY().primaryTerminalOf({projectId: gameId, token: _token})));
319
311
 
320
312
  // Get the current pot and store it. This also prevents re-entrance since the check above will return early.
321
313
  uint256 _pot = _terminal.STORE().balanceOf({terminal: address(_terminal), projectId: gameId, token: _token});
314
+
315
+ // If the pot is empty, set the sentinel and queue the final ruleset without attempting payouts.
322
316
  // slither-disable-next-line incorrect-equality
323
- if (_pot == 0) revert DefifaDeployer_NothingToFulfill();
317
+ if (_pot == 0) {
318
+ fulfilledCommitmentsOf[gameId] = 1;
319
+ _queueFinalRuleset({gameId: gameId, metadata: _metadata});
320
+ emit FulfilledCommitments({gameId: gameId, pot: 0, caller: msg.sender});
321
+ return;
322
+ }
324
323
 
325
324
  // Compute the fee amount based on the total absolute split percent stored at game creation.
326
325
  uint256 _feeAmount =
@@ -331,59 +330,26 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
331
330
  fulfilledCommitmentsOf[gameId] = _feeAmount > 0 ? _feeAmount : 1;
332
331
 
333
332
  // Send only the fee portion as payouts. The remaining balance stays as surplus for cash-outs.
333
+ // Wrapped in try-catch so the final ruleset is always queued even if payout fails.
334
334
  // slither-disable-next-line unused-return
335
- _terminal.sendPayoutsOf({
335
+ try _terminal.sendPayoutsOf({
336
336
  projectId: gameId,
337
337
  token: _token,
338
338
  amount: _feeAmount,
339
+ // Casting address to uint32 via uint160 is the standard Juicebox token-to-currency conversion.
340
+ // forge-lint: disable-next-line(unsafe-typecast)
339
341
  currency: _token == JBConstants.NATIVE_TOKEN ? _metadata.baseCurrency : uint32(uint160(_token)),
340
- minTokensPaidOut: _feeAmount
341
- });
342
-
343
- // Queue the final ruleset.
344
- JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
345
- rulesetConfigs[0] = JBRulesetConfig({
346
- mustStartAtOrAfter: 0,
347
- duration: 0,
348
- weight: 0,
349
- weightCutPercent: 0,
350
- approvalHook: IJBRulesetApprovalHook(address(0)),
351
- metadata: JBRulesetMetadata({
352
- reservedPercent: 0,
353
- cashOutTaxRate: 0,
354
- baseCurrency: _metadata.baseCurrency,
355
- pausePay: true,
356
- pauseCreditTransfers: false,
357
- allowOwnerMinting: false,
358
- allowSetCustomToken: false,
359
- allowTerminalMigration: false,
360
- allowSetTerminals: false,
361
- allowSetController: false,
362
- allowAddAccountingContext: false,
363
- allowAddPriceFeed: false,
364
- // Set this to true so only the deployer can fulfill the commitments.
365
- ownerMustSendPayouts: true,
366
- holdFees: false,
367
- useTotalSurplusForCashOuts: false,
368
- useDataHookForPay: true,
369
- useDataHookForCashOut: true,
370
- dataHook: _metadata.dataHook,
371
- metadata: uint16(
372
- JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(
373
- JB721TiersRulesetMetadata({pauseTransfers: false, pauseMintPendingReserves: false})
374
- )
375
- )
376
- }),
377
- // No more payouts.
378
- splitGroups: new JBSplitGroup[](0),
379
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
380
- });
342
+ minTokensPaidOut: 0
343
+ }) {}
344
+ catch (bytes memory reason) {
345
+ // Payout failed fee stays in pot. Reset to sentinel (1) so currentGamePotOf
346
+ // doesn't double-count the fee, while preserving the reentrancy guard.
347
+ fulfilledCommitmentsOf[gameId] = 1;
348
+ emit CommitmentPayoutFailed({gameId: gameId, amount: _feeAmount, reason: reason});
349
+ }
381
350
 
382
- // Update the ruleset to the final one.
383
- // slither-disable-next-line unused-return
384
- controller.queueRulesetsOf({
385
- projectId: gameId, rulesetConfigurations: rulesetConfigs, memo: "Defifa game has finished."
386
- });
351
+ // Queue the final ruleset and emit.
352
+ _queueFinalRuleset({gameId: gameId, metadata: _metadata});
387
353
 
388
354
  emit FulfilledCommitments({gameId: gameId, pot: _pot, caller: msg.sender});
389
355
  }
@@ -420,7 +386,10 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
420
386
  ) revert DefifaDeployer_InvalidGameConfiguration();
421
387
 
422
388
  // Get the game ID, optimistically knowing it will be one greater than the current count.
423
- gameId = controller.PROJECTS().count() + 1;
389
+ // Note: this prediction can race with other concurrent project deployments. If another project is
390
+ // created between reading count() and launchProjectFor(), the actual ID will differ. This is
391
+ // caught by the equality check after launch (gameId != _actualGameId reverts).
392
+ gameId = CONTROLLER.PROJECTS().count() + 1;
424
393
 
425
394
  {
426
395
  // Store the timestamps that'll define the game phases.
@@ -453,8 +422,10 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
453
422
  // Add a split for the fee.
454
423
  _splits[_numberOfSplits] = JBSplit({
455
424
  preferAddToBalance: false,
425
+ // forge-lint: disable-next-line(unsafe-typecast)
456
426
  percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT / DEFIFA_FEE_DIVISOR),
457
- projectId: uint64(defifaProjectId),
427
+ // forge-lint: disable-next-line(unsafe-typecast)
428
+ projectId: uint64(DEFIFA_PROJECT_ID),
458
429
  beneficiary: payable(address(this)),
459
430
  lockedUntil: 0,
460
431
  hook: IJBSplitHook(address(0))
@@ -462,11 +433,11 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
462
433
 
463
434
  // Store the splits.
464
435
  JBSplitGroup[] memory _groupedSplits = new JBSplitGroup[](1);
465
- _groupedSplits[0] = JBSplitGroup({groupId: splitGroup, splits: _splits});
436
+ _groupedSplits[0] = JBSplitGroup({groupId: SPLIT_GROUP, splits: _splits});
466
437
 
467
438
  // This contract must have SET_SPLIT_GROUPS permission from the defifa project owner.
468
- controller.setSplitGroupsOf({
469
- projectId: defifaProjectId, rulesetId: gameId, splitGroups: _groupedSplits
439
+ CONTROLLER.setSplitGroupsOf({
440
+ projectId: DEFIFA_PROJECT_ID, rulesetId: gameId, splitGroups: _groupedSplits
470
441
  });
471
442
  }
472
443
  }
@@ -525,7 +496,7 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
525
496
  // caller produces a different address.
526
497
  DefifaHook _hook = DefifaHook(
527
498
  Clones.cloneDeterministic({
528
- implementation: hookCodeOrigin, salt: keccak256(abi.encodePacked(msg.sender, _currentNonce))
499
+ implementation: HOOK_CODE_ORIGIN, salt: keccak256(abi.encodePacked(msg.sender, _currentNonce))
529
500
  })
530
501
  );
531
502
 
@@ -533,13 +504,13 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
533
504
  IJB721TokenUriResolver _uriResolver = launchProjectData.defaultTokenUriResolver
534
505
  != IJB721TokenUriResolver(address(0))
535
506
  ? launchProjectData.defaultTokenUriResolver
536
- : tokenUriResolver;
507
+ : TOKEN_URI_RESOLVER;
537
508
 
538
509
  _hook.initialize({
539
510
  _gameId: gameId,
540
511
  _name: launchProjectData.name,
541
512
  _symbol: string.concat("DEFIFA #", gameId.toString()),
542
- _rulesets: controller.RULESETS(),
513
+ _rulesets: CONTROLLER.RULESETS(),
543
514
  _baseUri: launchProjectData.baseUri,
544
515
  _tokenUriResolver: _uriResolver,
545
516
  _contractUri: launchProjectData.contractUri,
@@ -560,19 +531,19 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
560
531
  if (gameId != _actualGameId) revert DefifaDeployer_InvalidGameConfiguration();
561
532
 
562
533
  // Clone and initialize the new governor.
563
- governor.initializeGame({
534
+ GOVERNOR.initializeGame({
564
535
  gameId: gameId,
565
536
  attestationStartTime: uint256(launchProjectData.attestationStartTime),
566
537
  attestationGracePeriod: uint256(launchProjectData.attestationGracePeriod)
567
538
  });
568
539
 
569
540
  // Transfer ownership to the specified owner.
570
- _hook.transferOwnership(address(governor));
541
+ _hook.transferOwnership(address(GOVERNOR));
571
542
 
572
543
  // Add the hook to the registry, contract nonce starts at 1
573
- registry.registerAddress({deployer: address(this), nonce: _currentNonce});
544
+ REGISTRY.registerAddress({deployer: address(this), nonce: _currentNonce});
574
545
 
575
- emit LaunchGame(gameId, _hook, governor, _uriResolver, msg.sender);
546
+ emit LaunchGame(gameId, _hook, GOVERNOR, _uriResolver, msg.sender);
576
547
  }
577
548
 
578
549
  /// @notice Allows this contract to receive 721s.
@@ -595,11 +566,15 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
595
566
  if (noContestTriggeredFor[gameId]) revert DefifaDeployer_NoContestAlreadyTriggered();
596
567
 
597
568
  // Mark as triggered.
569
+ // Note: the queued ruleset does not take effect until the current ruleset's cycle ends (or immediately
570
+ // if duration is 0). During this gap, the game reports NO_CONTEST but the on-chain ruleset still has
571
+ // payout limits, so cash-out reclaim values may differ from the full-refund expectation. Callers
572
+ // should verify the active ruleset before cashing out.
598
573
  noContestTriggeredFor[gameId] = true;
599
574
 
600
575
  // Get the game's current ruleset metadata for the data hook address.
601
576
  // slither-disable-next-line unused-return
602
- (, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(gameId);
577
+ (, JBRulesetMetadata memory _metadata) = CONTROLLER.currentRulesetOf(gameId);
603
578
 
604
579
  // Queue a new ruleset without payout limits so surplus = balance, enabling refunds.
605
580
  JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
@@ -640,7 +615,7 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
640
615
 
641
616
  // Queue the no-contest refund ruleset.
642
617
  // slither-disable-next-line unused-return
643
- controller.queueRulesetsOf({
618
+ CONTROLLER.queueRulesetsOf({
644
619
  projectId: gameId, rulesetConfigurations: rulesetConfigs, memo: "Defifa game: no contest."
645
620
  });
646
621
 
@@ -651,6 +626,90 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
651
626
  // ------------------------ internal functions ----------------------- //
652
627
  //*********************************************************************//
653
628
 
629
+ function _buildSplits(
630
+ uint256 _gameId,
631
+ address _dataHook,
632
+ address _token,
633
+ JBSplit[] memory _initialSplits
634
+ )
635
+ internal
636
+ returns (JBSplitGroup[] memory)
637
+ {
638
+ uint256 _numberOfUserSplits = _initialSplits.length;
639
+
640
+ // Compute absolute percents for protocol fees.
641
+ uint256 _nanaAbsolutePercent = JBConstants.SPLITS_TOTAL_PERCENT / BASE_PROTOCOL_FEE_DIVISOR;
642
+ uint256 _defifaAbsolutePercent = JBConstants.SPLITS_TOTAL_PERCENT / DEFIFA_FEE_DIVISOR;
643
+
644
+ // Sum all absolute percents.
645
+ uint256 _totalAbsolutePercent = _nanaAbsolutePercent + _defifaAbsolutePercent;
646
+ for (uint256 _i; _i < _numberOfUserSplits; _i++) {
647
+ _totalAbsolutePercent += _initialSplits[_i].percent;
648
+ }
649
+
650
+ // Validate that total fee splits don't exceed 100%.
651
+ if (_totalAbsolutePercent > JBConstants.SPLITS_TOTAL_PERCENT) revert DefifaDeployer_SplitsDontAddUp();
652
+
653
+ // Store the total absolute percent for use in fulfillCommitmentsOf.
654
+ _commitmentPercentOf[_gameId] = _totalAbsolutePercent;
655
+
656
+ // Build the splits array: user splits + Defifa + NANA (NANA last to absorb rounding).
657
+ uint256 _splitCount = _numberOfUserSplits + 2;
658
+ JBSplit[] memory _splits = new JBSplit[](_splitCount);
659
+
660
+ // Normalize user splits and copy them over.
661
+ uint256 _normalizedTotal;
662
+ for (uint256 _i; _i < _numberOfUserSplits; _i++) {
663
+ _splits[_i] = _initialSplits[_i];
664
+ _splits[_i].percent = uint32(
665
+ mulDiv({
666
+ x: _initialSplits[_i].percent,
667
+ y: JBConstants.SPLITS_TOTAL_PERCENT,
668
+ denominator: _totalAbsolutePercent
669
+ })
670
+ );
671
+ _normalizedTotal += _splits[_i].percent;
672
+ }
673
+
674
+ // Add Defifa fee split (normalized).
675
+ uint256 _defifaNormalized = mulDiv({
676
+ x: _defifaAbsolutePercent, y: JBConstants.SPLITS_TOTAL_PERCENT, denominator: _totalAbsolutePercent
677
+ });
678
+ _splits[_numberOfUserSplits] = JBSplit({
679
+ preferAddToBalance: false,
680
+ // forge-lint: disable-next-line(unsafe-typecast)
681
+ percent: uint32(_defifaNormalized),
682
+ // forge-lint: disable-next-line(unsafe-typecast)
683
+ projectId: uint64(DEFIFA_PROJECT_ID),
684
+ beneficiary: payable(address(_dataHook)),
685
+ lockedUntil: 0,
686
+ hook: IJBSplitHook(address(0))
687
+ });
688
+ _normalizedTotal += _defifaNormalized;
689
+
690
+ // Add NANA protocol fee split last — absorbs rounding remainder from normalization.
691
+ // Because mulDiv rounds down, the sum of normalized percents can be slightly less than SPLITS_TOTAL_PERCENT.
692
+ // The NANA split receives the difference, so its effective percent may be a few basis points above its
693
+ // proportional share. This is economically negligible (< 1 bps at typical split counts).
694
+ // Beneficiary is the data hook so the hook receives NANA tokens for distribution during cash-outs.
695
+ _splits[_splitCount - 1] = JBSplit({
696
+ preferAddToBalance: false,
697
+ // forge-lint: disable-next-line(unsafe-typecast)
698
+ percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT - _normalizedTotal),
699
+ // forge-lint: disable-next-line(unsafe-typecast)
700
+ projectId: uint64(BASE_PROTOCOL_PROJECT_ID),
701
+ beneficiary: payable(address(_dataHook)),
702
+ lockedUntil: 0,
703
+ hook: IJBSplitHook(address(0))
704
+ });
705
+
706
+ // Build the grouped split for the payment of the game token.
707
+ JBSplitGroup[] memory _groupedSplits = new JBSplitGroup[](1);
708
+ _groupedSplits[0] = JBSplitGroup({groupId: uint256(uint160(_token)), splits: _splits});
709
+
710
+ return _groupedSplits;
711
+ }
712
+
654
713
  function _launchGame(
655
714
  DefifaLaunchProjectData memory launchProjectData,
656
715
  uint256 _gameId,
@@ -816,7 +875,7 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
816
875
  });
817
876
 
818
877
  // launch the project.
819
- return controller.launchProjectFor({
878
+ return CONTROLLER.launchProjectFor({
820
879
  owner: address(this),
821
880
  projectUri: launchProjectData.projectUri,
822
881
  rulesetConfigurations: rulesetConfigs,
@@ -825,80 +884,50 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
825
884
  });
826
885
  }
827
886
 
828
- function _buildSplits(
829
- uint256 _gameId,
830
- address _dataHook,
831
- address _token,
832
- JBSplit[] memory _initialSplits
833
- )
834
- internal
835
- returns (JBSplitGroup[] memory)
836
- {
837
- uint256 _numberOfUserSplits = _initialSplits.length;
838
-
839
- // Compute absolute percents for protocol fees.
840
- uint256 _nanaAbsolutePercent = JBConstants.SPLITS_TOTAL_PERCENT / BASE_PROTOCOL_FEE_DIVISOR;
841
- uint256 _defifaAbsolutePercent = JBConstants.SPLITS_TOTAL_PERCENT / DEFIFA_FEE_DIVISOR;
842
-
843
- // Sum all absolute percents.
844
- uint256 _totalAbsolutePercent = _nanaAbsolutePercent + _defifaAbsolutePercent;
845
- for (uint256 _i; _i < _numberOfUserSplits; _i++) {
846
- _totalAbsolutePercent += _initialSplits[_i].percent;
847
- }
848
-
849
- // Validate that total fee splits don't exceed 100%.
850
- if (_totalAbsolutePercent > JBConstants.SPLITS_TOTAL_PERCENT) revert DefifaDeployer_SplitsDontAddUp();
851
-
852
- // Store the total absolute percent for use in fulfillCommitmentsOf.
853
- _commitmentPercentOf[_gameId] = _totalAbsolutePercent;
854
-
855
- // Build the splits array: user splits + Defifa + NANA (NANA last to absorb rounding).
856
- uint256 _splitCount = _numberOfUserSplits + 2;
857
- JBSplit[] memory _splits = new JBSplit[](_splitCount);
858
-
859
- // Normalize user splits and copy them over.
860
- uint256 _normalizedTotal;
861
- for (uint256 _i; _i < _numberOfUserSplits; _i++) {
862
- _splits[_i] = _initialSplits[_i];
863
- _splits[_i].percent = uint32(
864
- mulDiv({
865
- x: _initialSplits[_i].percent,
866
- y: JBConstants.SPLITS_TOTAL_PERCENT,
867
- denominator: _totalAbsolutePercent
868
- })
869
- );
870
- _normalizedTotal += _splits[_i].percent;
871
- }
872
-
873
- // Add Defifa fee split (normalized).
874
- uint256 _defifaNormalized = mulDiv({
875
- x: _defifaAbsolutePercent, y: JBConstants.SPLITS_TOTAL_PERCENT, denominator: _totalAbsolutePercent
876
- });
877
- _splits[_numberOfUserSplits] = JBSplit({
878
- preferAddToBalance: false,
879
- percent: uint32(_defifaNormalized),
880
- projectId: uint64(defifaProjectId),
881
- beneficiary: payable(address(_dataHook)),
882
- lockedUntil: 0,
883
- hook: IJBSplitHook(address(0))
887
+ /// @notice Queues the final ruleset for a game: no payouts, no fund access limits, surplus = entire balance.
888
+ /// @param gameId The ID of the game.
889
+ /// @param metadata The current ruleset metadata (used to carry forward baseCurrency and dataHook).
890
+ function _queueFinalRuleset(uint256 gameId, JBRulesetMetadata memory metadata) internal {
891
+ JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
892
+ rulesetConfigs[0] = JBRulesetConfig({
893
+ mustStartAtOrAfter: 0,
894
+ duration: 0,
895
+ weight: 0,
896
+ weightCutPercent: 0,
897
+ approvalHook: IJBRulesetApprovalHook(address(0)),
898
+ metadata: JBRulesetMetadata({
899
+ reservedPercent: 0,
900
+ cashOutTaxRate: 0,
901
+ baseCurrency: metadata.baseCurrency,
902
+ pausePay: true,
903
+ pauseCreditTransfers: false,
904
+ allowOwnerMinting: false,
905
+ allowSetCustomToken: false,
906
+ allowTerminalMigration: false,
907
+ allowSetTerminals: false,
908
+ allowSetController: false,
909
+ allowAddAccountingContext: false,
910
+ allowAddPriceFeed: false,
911
+ ownerMustSendPayouts: false,
912
+ holdFees: false,
913
+ useTotalSurplusForCashOuts: false,
914
+ useDataHookForPay: true,
915
+ useDataHookForCashOut: true,
916
+ dataHook: metadata.dataHook,
917
+ metadata: uint16(
918
+ JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(
919
+ JB721TiersRulesetMetadata({pauseTransfers: false, pauseMintPendingReserves: false})
920
+ )
921
+ )
922
+ }),
923
+ // No more payouts.
924
+ splitGroups: new JBSplitGroup[](0),
925
+ fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
884
926
  });
885
- _normalizedTotal += _defifaNormalized;
886
927
 
887
- // Add NANA protocol fee split last — absorbs rounding remainder.
888
- // Beneficiary is the data hook so the hook receives NANA tokens for distribution during cash-outs.
889
- _splits[_splitCount - 1] = JBSplit({
890
- preferAddToBalance: false,
891
- percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT - _normalizedTotal),
892
- projectId: uint64(baseProtocolProjectId),
893
- beneficiary: payable(address(_dataHook)),
894
- lockedUntil: 0,
895
- hook: IJBSplitHook(address(0))
928
+ // slither-disable-next-line unused-return
929
+ CONTROLLER.queueRulesetsOf({
930
+ projectId: gameId, rulesetConfigurations: rulesetConfigs, memo: "Defifa game has finished."
896
931
  });
897
-
898
- // Build the grouped split for the payment of the game token.
899
- JBSplitGroup[] memory _groupedSplits = new JBSplitGroup[](1);
900
- _groupedSplits[0] = JBSplitGroup({groupId: uint256(uint160(_token)), splits: _splits});
901
-
902
- return _groupedSplits;
903
932
  }
904
933
  }