@ballkidz/defifa 0.0.6 → 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 (47) 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/STYLE_GUIDE.md +14 -1
  7. package/USER_JOURNEYS.md +691 -0
  8. package/package.json +7 -5
  9. package/script/Deploy.s.sol +26 -13
  10. package/script/helpers/DefifaDeploymentLib.sol +30 -14
  11. package/src/DefifaDeployer.sol +225 -187
  12. package/src/DefifaGovernor.sol +291 -281
  13. package/src/DefifaHook.sol +81 -42
  14. package/src/DefifaProjectOwner.sol +27 -4
  15. package/src/DefifaTokenUriResolver.sol +137 -134
  16. package/src/enums/DefifaGamePhase.sol +1 -1
  17. package/src/enums/DefifaScorecardState.sol +1 -1
  18. package/src/interfaces/IDefifaDeployer.sol +52 -50
  19. package/src/interfaces/IDefifaGamePhaseReporter.sol +2 -2
  20. package/src/interfaces/IDefifaGamePotReporter.sol +1 -1
  21. package/src/interfaces/IDefifaGovernor.sol +53 -54
  22. package/src/interfaces/IDefifaHook.sol +104 -103
  23. package/src/interfaces/IDefifaTokenUriResolver.sol +2 -2
  24. package/src/libraries/DefifaFontImporter.sol +11 -9
  25. package/src/libraries/DefifaHookLib.sol +68 -53
  26. package/src/structs/DefifaAttestations.sol +1 -1
  27. package/src/structs/DefifaDelegation.sol +1 -1
  28. package/src/structs/DefifaLaunchProjectData.sol +4 -4
  29. package/src/structs/DefifaOpsData.sol +1 -1
  30. package/src/structs/DefifaScorecard.sol +1 -1
  31. package/src/structs/DefifaTierCashOutWeight.sol +1 -1
  32. package/src/structs/DefifaTierParams.sol +2 -1
  33. package/test/DefifaAdversarialQuorum.t.sol +602 -0
  34. package/test/DefifaAuditLowGuards.t.sol +304 -0
  35. package/test/DefifaFeeAccounting.t.sol +37 -16
  36. package/test/DefifaGovernor.t.sol +37 -11
  37. package/test/DefifaHookRegressions.t.sol +14 -12
  38. package/test/DefifaMintCostInvariant.t.sol +31 -12
  39. package/test/DefifaNoContest.t.sol +33 -13
  40. package/test/DefifaSecurity.t.sol +45 -25
  41. package/test/DefifaUSDC.t.sol +44 -34
  42. package/test/Fork.t.sol +42 -40
  43. package/test/SVG.t.sol +2 -2
  44. package/test/TestAuditGaps.sol +982 -0
  45. package/test/TestQALastMile.t.sol +511 -0
  46. package/test/regression/FulfillmentBlocksRatification.t.sol +36 -30
  47. 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,74 +307,49 @@ 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
- uint256 _feeAmount = mulDiv(_pot, _commitmentPercentOf[gameId], JBConstants.SPLITS_TOTAL_PERCENT);
325
+ uint256 _feeAmount =
326
+ mulDiv({x: _pot, y: _commitmentPercentOf[gameId], denominator: JBConstants.SPLITS_TOTAL_PERCENT});
327
327
 
328
328
  // Store the actual fee amount for accurate currentGamePotOf reporting.
329
329
  // Use max(feeAmount, 1) to preserve the reentrancy guard when pot is 0.
330
330
  fulfilledCommitmentsOf[gameId] = _feeAmount > 0 ? _feeAmount : 1;
331
331
 
332
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.
333
334
  // slither-disable-next-line unused-return
334
- _terminal.sendPayoutsOf({
335
+ try _terminal.sendPayoutsOf({
335
336
  projectId: gameId,
336
337
  token: _token,
337
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)
338
341
  currency: _token == JBConstants.NATIVE_TOKEN ? _metadata.baseCurrency : uint32(uint160(_token)),
339
- minTokensPaidOut: _feeAmount
340
- });
341
-
342
- // Queue the final ruleset.
343
- JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
344
- rulesetConfigs[0] = JBRulesetConfig({
345
- mustStartAtOrAfter: 0,
346
- duration: 0,
347
- weight: 0,
348
- weightCutPercent: 0,
349
- approvalHook: IJBRulesetApprovalHook(address(0)),
350
- metadata: JBRulesetMetadata({
351
- reservedPercent: 0,
352
- cashOutTaxRate: 0,
353
- baseCurrency: _metadata.baseCurrency,
354
- pausePay: true,
355
- pauseCreditTransfers: false,
356
- allowOwnerMinting: false,
357
- allowSetCustomToken: false,
358
- allowTerminalMigration: false,
359
- allowSetTerminals: false,
360
- allowSetController: false,
361
- allowAddAccountingContext: false,
362
- allowAddPriceFeed: false,
363
- // Set this to true so only the deployer can fulfill the commitments.
364
- ownerMustSendPayouts: true,
365
- holdFees: false,
366
- useTotalSurplusForCashOuts: false,
367
- useDataHookForPay: true,
368
- useDataHookForCashOut: true,
369
- dataHook: _metadata.dataHook,
370
- metadata: uint16(
371
- JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(
372
- JB721TiersRulesetMetadata({pauseTransfers: false, pauseMintPendingReserves: false})
373
- )
374
- )
375
- }),
376
- // No more payouts.
377
- splitGroups: new JBSplitGroup[](0),
378
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
379
- });
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
+ }
380
350
 
381
- // Update the ruleset to the final one.
382
- // slither-disable-next-line unused-return
383
- controller.queueRulesetsOf({
384
- projectId: gameId, rulesetConfigurations: rulesetConfigs, memo: "Defifa game has finished."
385
- });
351
+ // Queue the final ruleset and emit.
352
+ _queueFinalRuleset({gameId: gameId, metadata: _metadata});
386
353
 
387
354
  emit FulfilledCommitments({gameId: gameId, pot: _pot, caller: msg.sender});
388
355
  }
@@ -419,7 +386,10 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
419
386
  ) revert DefifaDeployer_InvalidGameConfiguration();
420
387
 
421
388
  // Get the game ID, optimistically knowing it will be one greater than the current count.
422
- 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;
423
393
 
424
394
  {
425
395
  // Store the timestamps that'll define the game phases.
@@ -452,8 +422,10 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
452
422
  // Add a split for the fee.
453
423
  _splits[_numberOfSplits] = JBSplit({
454
424
  preferAddToBalance: false,
425
+ // forge-lint: disable-next-line(unsafe-typecast)
455
426
  percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT / DEFIFA_FEE_DIVISOR),
456
- projectId: uint64(defifaProjectId),
427
+ // forge-lint: disable-next-line(unsafe-typecast)
428
+ projectId: uint64(DEFIFA_PROJECT_ID),
457
429
  beneficiary: payable(address(this)),
458
430
  lockedUntil: 0,
459
431
  hook: IJBSplitHook(address(0))
@@ -461,11 +433,11 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
461
433
 
462
434
  // Store the splits.
463
435
  JBSplitGroup[] memory _groupedSplits = new JBSplitGroup[](1);
464
- _groupedSplits[0] = JBSplitGroup({groupId: splitGroup, splits: _splits});
436
+ _groupedSplits[0] = JBSplitGroup({groupId: SPLIT_GROUP, splits: _splits});
465
437
 
466
438
  // This contract must have SET_SPLIT_GROUPS permission from the defifa project owner.
467
- controller.setSplitGroupsOf({
468
- projectId: defifaProjectId, rulesetId: gameId, splitGroups: _groupedSplits
439
+ CONTROLLER.setSplitGroupsOf({
440
+ projectId: DEFIFA_PROJECT_ID, rulesetId: gameId, splitGroups: _groupedSplits
469
441
  });
470
442
  }
471
443
  }
@@ -523,20 +495,22 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
523
495
  // cloneDeterministic with msg.sender in the salt prevents this since a different
524
496
  // caller produces a different address.
525
497
  DefifaHook _hook = DefifaHook(
526
- Clones.cloneDeterministic(hookCodeOrigin, keccak256(abi.encodePacked(msg.sender, _currentNonce)))
498
+ Clones.cloneDeterministic({
499
+ implementation: HOOK_CODE_ORIGIN, salt: keccak256(abi.encodePacked(msg.sender, _currentNonce))
500
+ })
527
501
  );
528
502
 
529
503
  // Use the default uri resolver if provided, else use the hardcoded generic default.
530
504
  IJB721TokenUriResolver _uriResolver = launchProjectData.defaultTokenUriResolver
531
505
  != IJB721TokenUriResolver(address(0))
532
506
  ? launchProjectData.defaultTokenUriResolver
533
- : tokenUriResolver;
507
+ : TOKEN_URI_RESOLVER;
534
508
 
535
509
  _hook.initialize({
536
510
  _gameId: gameId,
537
511
  _name: launchProjectData.name,
538
512
  _symbol: string.concat("DEFIFA #", gameId.toString()),
539
- _rulesets: controller.RULESETS(),
513
+ _rulesets: CONTROLLER.RULESETS(),
540
514
  _baseUri: launchProjectData.baseUri,
541
515
  _tokenUriResolver: _uriResolver,
542
516
  _contractUri: launchProjectData.contractUri,
@@ -557,19 +531,19 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
557
531
  if (gameId != _actualGameId) revert DefifaDeployer_InvalidGameConfiguration();
558
532
 
559
533
  // Clone and initialize the new governor.
560
- governor.initializeGame({
534
+ GOVERNOR.initializeGame({
561
535
  gameId: gameId,
562
536
  attestationStartTime: uint256(launchProjectData.attestationStartTime),
563
537
  attestationGracePeriod: uint256(launchProjectData.attestationGracePeriod)
564
538
  });
565
539
 
566
540
  // Transfer ownership to the specified owner.
567
- _hook.transferOwnership(address(governor));
541
+ _hook.transferOwnership(address(GOVERNOR));
568
542
 
569
543
  // Add the hook to the registry, contract nonce starts at 1
570
- registry.registerAddress({deployer: address(this), nonce: _currentNonce});
544
+ REGISTRY.registerAddress({deployer: address(this), nonce: _currentNonce});
571
545
 
572
- emit LaunchGame(gameId, _hook, governor, _uriResolver, msg.sender);
546
+ emit LaunchGame(gameId, _hook, GOVERNOR, _uriResolver, msg.sender);
573
547
  }
574
548
 
575
549
  /// @notice Allows this contract to receive 721s.
@@ -592,11 +566,15 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
592
566
  if (noContestTriggeredFor[gameId]) revert DefifaDeployer_NoContestAlreadyTriggered();
593
567
 
594
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.
595
573
  noContestTriggeredFor[gameId] = true;
596
574
 
597
575
  // Get the game's current ruleset metadata for the data hook address.
598
576
  // slither-disable-next-line unused-return
599
- (, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(gameId);
577
+ (, JBRulesetMetadata memory _metadata) = CONTROLLER.currentRulesetOf(gameId);
600
578
 
601
579
  // Queue a new ruleset without payout limits so surplus = balance, enabling refunds.
602
580
  JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
@@ -637,7 +615,7 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
637
615
 
638
616
  // Queue the no-contest refund ruleset.
639
617
  // slither-disable-next-line unused-return
640
- controller.queueRulesetsOf({
618
+ CONTROLLER.queueRulesetsOf({
641
619
  projectId: gameId, rulesetConfigurations: rulesetConfigs, memo: "Defifa game: no contest."
642
620
  });
643
621
 
@@ -648,6 +626,90 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
648
626
  // ------------------------ internal functions ----------------------- //
649
627
  //*********************************************************************//
650
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
+
651
713
  function _launchGame(
652
714
  DefifaLaunchProjectData memory launchProjectData,
653
715
  uint256 _gameId,
@@ -813,7 +875,7 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
813
875
  });
814
876
 
815
877
  // launch the project.
816
- return controller.launchProjectFor({
878
+ return CONTROLLER.launchProjectFor({
817
879
  owner: address(this),
818
880
  projectUri: launchProjectData.projectUri,
819
881
  rulesetConfigurations: rulesetConfigs,
@@ -822,74 +884,50 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
822
884
  });
823
885
  }
824
886
 
825
- function _buildSplits(
826
- uint256 _gameId,
827
- address _dataHook,
828
- address _token,
829
- JBSplit[] memory _initialSplits
830
- )
831
- internal
832
- returns (JBSplitGroup[] memory)
833
- {
834
- uint256 _numberOfUserSplits = _initialSplits.length;
835
-
836
- // Compute absolute percents for protocol fees.
837
- uint256 _nanaAbsolutePercent = JBConstants.SPLITS_TOTAL_PERCENT / BASE_PROTOCOL_FEE_DIVISOR;
838
- uint256 _defifaAbsolutePercent = JBConstants.SPLITS_TOTAL_PERCENT / DEFIFA_FEE_DIVISOR;
839
-
840
- // Sum all absolute percents.
841
- uint256 _totalAbsolutePercent = _nanaAbsolutePercent + _defifaAbsolutePercent;
842
- for (uint256 _i; _i < _numberOfUserSplits; _i++) {
843
- _totalAbsolutePercent += _initialSplits[_i].percent;
844
- }
845
-
846
- // Validate that total fee splits don't exceed 100%.
847
- if (_totalAbsolutePercent > JBConstants.SPLITS_TOTAL_PERCENT) revert DefifaDeployer_SplitsDontAddUp();
848
-
849
- // Store the total absolute percent for use in fulfillCommitmentsOf.
850
- _commitmentPercentOf[_gameId] = _totalAbsolutePercent;
851
-
852
- // Build the splits array: user splits + Defifa + NANA (NANA last to absorb rounding).
853
- uint256 _splitCount = _numberOfUserSplits + 2;
854
- JBSplit[] memory _splits = new JBSplit[](_splitCount);
855
-
856
- // Normalize user splits and copy them over.
857
- uint256 _normalizedTotal;
858
- for (uint256 _i; _i < _numberOfUserSplits; _i++) {
859
- _splits[_i] = _initialSplits[_i];
860
- _splits[_i].percent =
861
- uint32(mulDiv(_initialSplits[_i].percent, JBConstants.SPLITS_TOTAL_PERCENT, _totalAbsolutePercent));
862
- _normalizedTotal += _splits[_i].percent;
863
- }
864
-
865
- // Add Defifa fee split (normalized).
866
- uint256 _defifaNormalized =
867
- mulDiv(_defifaAbsolutePercent, JBConstants.SPLITS_TOTAL_PERCENT, _totalAbsolutePercent);
868
- _splits[_numberOfUserSplits] = JBSplit({
869
- preferAddToBalance: false,
870
- percent: uint32(_defifaNormalized),
871
- projectId: uint64(defifaProjectId),
872
- beneficiary: payable(address(_dataHook)),
873
- lockedUntil: 0,
874
- 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)
875
926
  });
876
- _normalizedTotal += _defifaNormalized;
877
927
 
878
- // Add NANA protocol fee split last — absorbs rounding remainder.
879
- // Beneficiary is the data hook so the hook receives NANA tokens for distribution during cash-outs.
880
- _splits[_splitCount - 1] = JBSplit({
881
- preferAddToBalance: false,
882
- percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT - _normalizedTotal),
883
- projectId: uint64(baseProtocolProjectId),
884
- beneficiary: payable(address(_dataHook)),
885
- lockedUntil: 0,
886
- hook: IJBSplitHook(address(0))
928
+ // slither-disable-next-line unused-return
929
+ CONTROLLER.queueRulesetsOf({
930
+ projectId: gameId, rulesetConfigurations: rulesetConfigs, memo: "Defifa game has finished."
887
931
  });
888
-
889
- // Build the grouped split for the payment of the game token.
890
- JBSplitGroup[] memory _groupedSplits = new JBSplitGroup[](1);
891
- _groupedSplits[0] = JBSplitGroup({groupId: uint256(uint160(_token)), splits: _splits});
892
-
893
- return _groupedSplits;
894
932
  }
895
933
  }