@ballkidz/defifa 0.0.1 → 0.0.3

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 (26) hide show
  1. package/package.json +5 -5
  2. package/src/DefifaDeployer.sol +125 -124
  3. package/src/DefifaGovernor.sol +160 -145
  4. package/src/DefifaHook.sol +287 -293
  5. package/src/DefifaTokenUriResolver.sol +30 -30
  6. package/src/interfaces/IDefifaGovernor.sol +2 -0
  7. package/test/regression/M35_GracePeriodBypass.t.sol +296 -0
  8. package/test/regression/M36_FulfillmentBlocksRatification.t.sol +272 -0
  9. package/.gas-snapshot +0 -2
  10. package/deployments/defifa-v5/arbitrum_sepolia/DefifaDelegate.json +0 -4867
  11. package/deployments/defifa-v5/arbitrum_sepolia/DefifaDeployer.json +0 -1719
  12. package/deployments/defifa-v5/arbitrum_sepolia/DefifaGovernor.json +0 -1535
  13. package/deployments/defifa-v5/arbitrum_sepolia/DefifaTokenUriResolver.json +0 -295
  14. package/deployments/defifa-v5/base_sepolia/DefifaDelegate.json +0 -4875
  15. package/deployments/defifa-v5/base_sepolia/DefifaDeployer.json +0 -1725
  16. package/deployments/defifa-v5/base_sepolia/DefifaGovernor.json +0 -1543
  17. package/deployments/defifa-v5/base_sepolia/DefifaTokenUriResolver.json +0 -301
  18. package/deployments/defifa-v5/optimism_sepolia/DefifaDelegate.json +0 -4875
  19. package/deployments/defifa-v5/optimism_sepolia/DefifaDeployer.json +0 -1725
  20. package/deployments/defifa-v5/optimism_sepolia/DefifaGovernor.json +0 -1543
  21. package/deployments/defifa-v5/optimism_sepolia/DefifaTokenUriResolver.json +0 -301
  22. package/deployments/defifa-v5/sepolia/DefifaDelegate.json +0 -4875
  23. package/deployments/defifa-v5/sepolia/DefifaDeployer.json +0 -1725
  24. package/deployments/defifa-v5/sepolia/DefifaGovernor.json +0 -1543
  25. package/deployments/defifa-v5/sepolia/DefifaTokenUriResolver.json +0 -301
  26. package/foundry.lock +0 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ballkidz/defifa",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "license": "MIT",
5
5
  "engines": {
6
6
  "node": ">=20.0.0"
@@ -13,10 +13,10 @@
13
13
  "url": "https://github.com/BallKidz/defifa-collection-deployer"
14
14
  },
15
15
  "dependencies": {
16
- "@bananapus/721-hook-v6": "^0.0.8",
17
- "@bananapus/address-registry-v6": "^0.0.3",
18
- "@bananapus/core-v6": "^0.0.6",
19
- "@bananapus/permission-ids-v6": "^0.0.3",
16
+ "@bananapus/721-hook-v6": "^0.0.9",
17
+ "@bananapus/address-registry-v6": "^0.0.4",
18
+ "@bananapus/core-v6": "^0.0.10",
19
+ "@bananapus/permission-ids-v6": "^0.0.5",
20
20
  "@openzeppelin/contracts": "5.2.0",
21
21
  "@prb/math": "^4.1.1",
22
22
  "scripty.sol": "^2.1.1"
@@ -135,37 +135,6 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
135
135
  // ------------------------- external views -------------------------- //
136
136
  //*********************************************************************//
137
137
 
138
- /// @notice The game times.
139
- /// @param gameId The ID of the game for which the game times apply.
140
- /// @return The game's start time, as a unix timestamp.
141
- /// @return The game's minting period duration, in seconds.
142
- /// @return The game's refund period duration, in seconds.
143
- function timesFor(uint256 gameId) external view override returns (uint48, uint24, uint24) {
144
- DefifaOpsData memory _ops = _opsOf[gameId];
145
- return (_ops.start, _ops.mintPeriodDuration, _ops.refundPeriodDuration);
146
- }
147
-
148
- /// @notice The token of a game.
149
- /// @param gameId The ID of the game to get the token of.
150
- /// @return The game's token.
151
- function tokenOf(uint256 gameId) external view override returns (address) {
152
- return _opsOf[gameId].token;
153
- }
154
-
155
- /// @notice The safety mechanism parameters of a game.
156
- /// @param gameId The ID of the game to get the safety params of.
157
- /// @return minParticipation The minimum treasury balance for the game to proceed to scoring.
158
- /// @return scorecardTimeout The maximum time after scoring begins for a scorecard to be ratified.
159
- function safetyParamsOf(uint256 gameId)
160
- external
161
- view
162
- override
163
- returns (uint256 minParticipation, uint32 scorecardTimeout)
164
- {
165
- DefifaOpsData memory _ops = _opsOf[gameId];
166
- return (_ops.minParticipation, _ops.scorecardTimeout);
167
- }
168
-
169
138
  /// @notice The current pot the game is being played with.
170
139
  /// @param gameId The ID of the game for which the pot applies.
171
140
  /// @param includeCommitments A flag indicating if the portion of the pot committed to fulfill preprogrammed
@@ -214,6 +183,37 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
214
183
  return _currentRuleset.duration != 0 && _currentRuleset.id == _queuedRuleset.id;
215
184
  }
216
185
 
186
+ /// @notice The safety mechanism parameters of a game.
187
+ /// @param gameId The ID of the game to get the safety params of.
188
+ /// @return minParticipation The minimum treasury balance for the game to proceed to scoring.
189
+ /// @return scorecardTimeout The maximum time after scoring begins for a scorecard to be ratified.
190
+ function safetyParamsOf(uint256 gameId)
191
+ external
192
+ view
193
+ override
194
+ returns (uint256 minParticipation, uint32 scorecardTimeout)
195
+ {
196
+ DefifaOpsData memory _ops = _opsOf[gameId];
197
+ return (_ops.minParticipation, _ops.scorecardTimeout);
198
+ }
199
+
200
+ /// @notice The game times.
201
+ /// @param gameId The ID of the game for which the game times apply.
202
+ /// @return The game's start time, as a unix timestamp.
203
+ /// @return The game's minting period duration, in seconds.
204
+ /// @return The game's refund period duration, in seconds.
205
+ function timesFor(uint256 gameId) external view override returns (uint48, uint24, uint24) {
206
+ DefifaOpsData memory _ops = _opsOf[gameId];
207
+ return (_ops.start, _ops.mintPeriodDuration, _ops.refundPeriodDuration);
208
+ }
209
+
210
+ /// @notice The token of a game.
211
+ /// @param gameId The ID of the game to get the token of.
212
+ /// @return The game's token.
213
+ function tokenOf(uint256 gameId) external view override returns (address) {
214
+ return _opsOf[gameId].token;
215
+ }
216
+
217
217
  //*********************************************************************//
218
218
  // -------------------------- public views --------------------------- //
219
219
  //*********************************************************************//
@@ -297,6 +297,96 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
297
297
  // ---------------------- external transactions ---------------------- //
298
298
  //*********************************************************************//
299
299
 
300
+ /// @notice Fulfill split amounts between all splits for a game.
301
+ /// @param gameId The ID of the game to fulfill splits for.
302
+ function fulfillCommitmentsOf(uint256 gameId) external virtual override {
303
+ // Make sure commitments haven't already been fulfilled.
304
+ if (fulfilledCommitmentsOf[gameId] != 0) return;
305
+
306
+ // Get the game's current funding cycle along with its metadata.
307
+ // slither-disable-next-line unused-return
308
+ (, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(gameId);
309
+
310
+ // Make sure the game's commitments can be fulfilled.
311
+ if (!IDefifaHook(_metadata.dataHook).cashOutWeightIsSet()) {
312
+ revert DefifaDeployer_CantFulfillYet();
313
+ }
314
+
315
+ // Get the game token and the terminal.
316
+ address _token = _opsOf[gameId].token;
317
+ IJBMultiTerminal _terminal =
318
+ IJBMultiTerminal(address(controller.DIRECTORY().primaryTerminalOf({projectId: gameId, token: _token})));
319
+
320
+ // Get the current pot and store it. This also prevents re-entrance since the check above will return early.
321
+ uint256 _pot = _terminal.STORE().balanceOf({terminal: address(_terminal), projectId: gameId, token: _token});
322
+ // slither-disable-next-line incorrect-equality
323
+ if (_pot == 0) revert DefifaDeployer_NothingToFulfill();
324
+
325
+ // 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);
327
+
328
+ // Store the actual fee amount for accurate currentGamePotOf reporting.
329
+ // Use max(feeAmount, 1) to preserve the reentrancy guard when pot is 0.
330
+ fulfilledCommitmentsOf[gameId] = _feeAmount > 0 ? _feeAmount : 1;
331
+
332
+ // Send only the fee portion as payouts. The remaining balance stays as surplus for cash-outs.
333
+ // slither-disable-next-line unused-return
334
+ _terminal.sendPayoutsOf({
335
+ projectId: gameId,
336
+ token: _token,
337
+ amount: _feeAmount,
338
+ 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
+ });
380
+
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
+ });
386
+
387
+ emit FulfilledCommitments({gameId: gameId, pot: _pot, caller: msg.sender});
388
+ }
389
+
300
390
  /// @notice Launches a new game owned by this contract with a DefifaHook attached.
301
391
  /// @param launchProjectData Data necessary to fulfill the transaction to launch a game.
302
392
  /// @return gameId The ID of the newly configured game.
@@ -311,6 +401,7 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
311
401
  uint48(block.timestamp + launchProjectData.mintPeriodDuration + launchProjectData.refundPeriodDuration);
312
402
  }
313
403
  // Start minting right away if a start time isn't provided.
404
+ // slither-disable-next-line incorrect-equality
314
405
  else if (
315
406
  launchProjectData.mintPeriodDuration == 0
316
407
  && launchProjectData.start > block.timestamp + launchProjectData.refundPeriodDuration
@@ -481,94 +572,9 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
481
572
  emit LaunchGame(gameId, _hook, governor, _uriResolver, msg.sender);
482
573
  }
483
574
 
484
- /// @notice Fulfill split amounts between all splits for a game.
485
- /// @param gameId The ID of the game to fulfill splits for.
486
- function fulfillCommitmentsOf(uint256 gameId) external virtual override {
487
- // Make sure commitments haven't already been fulfilled.
488
- if (fulfilledCommitmentsOf[gameId] != 0) return;
489
-
490
- // Get the game's current funding cycle along with its metadata.
491
- // slither-disable-next-line unused-return
492
- (, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(gameId);
493
-
494
- // Make sure the game's commitments can be fulfilled.
495
- if (!IDefifaHook(_metadata.dataHook).cashOutWeightIsSet()) {
496
- revert DefifaDeployer_CantFulfillYet();
497
- }
498
-
499
- // Get the game token and the terminal.
500
- address _token = _opsOf[gameId].token;
501
- IJBMultiTerminal _terminal =
502
- IJBMultiTerminal(address(controller.DIRECTORY().primaryTerminalOf({projectId: gameId, token: _token})));
503
-
504
- // Get the current pot and store it. This also prevents re-entrance since the check above will return early.
505
- uint256 _pot = _terminal.STORE().balanceOf({terminal: address(_terminal), projectId: gameId, token: _token});
506
- // slither-disable-next-line incorrect-equality
507
- if (_pot == 0) revert DefifaDeployer_NothingToFulfill();
508
-
509
- // Compute the fee amount based on the total absolute split percent stored at game creation.
510
- uint256 _feeAmount = mulDiv(_pot, _commitmentPercentOf[gameId], JBConstants.SPLITS_TOTAL_PERCENT);
511
-
512
- // Store the actual fee amount for accurate currentGamePotOf reporting.
513
- // Use max(feeAmount, 1) to preserve the reentrancy guard when pot is 0.
514
- fulfilledCommitmentsOf[gameId] = _feeAmount > 0 ? _feeAmount : 1;
515
-
516
- // Send only the fee portion as payouts. The remaining balance stays as surplus for cash-outs.
517
- // slither-disable-next-line unused-return
518
- _terminal.sendPayoutsOf({
519
- projectId: gameId,
520
- token: _token,
521
- amount: _feeAmount,
522
- currency: _token == JBConstants.NATIVE_TOKEN ? _metadata.baseCurrency : uint32(uint160(_token)),
523
- minTokensPaidOut: _feeAmount
524
- });
525
-
526
- // Queue the final ruleset.
527
- JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
528
- rulesetConfigs[0] = JBRulesetConfig({
529
- mustStartAtOrAfter: 0,
530
- duration: 0,
531
- weight: 0,
532
- weightCutPercent: 0,
533
- approvalHook: IJBRulesetApprovalHook(address(0)),
534
- metadata: JBRulesetMetadata({
535
- reservedPercent: 0,
536
- cashOutTaxRate: 0,
537
- baseCurrency: _metadata.baseCurrency,
538
- pausePay: true,
539
- pauseCreditTransfers: false,
540
- allowOwnerMinting: false,
541
- allowSetCustomToken: false,
542
- allowTerminalMigration: false,
543
- allowSetTerminals: false,
544
- allowSetController: false,
545
- allowAddAccountingContext: false,
546
- allowAddPriceFeed: false,
547
- // Set this to true so only the deployer can fulfill the commitments.
548
- ownerMustSendPayouts: true,
549
- holdFees: false,
550
- useTotalSurplusForCashOuts: false,
551
- useDataHookForPay: true,
552
- useDataHookForCashOut: true,
553
- dataHook: _metadata.dataHook,
554
- metadata: uint16(
555
- JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(
556
- JB721TiersRulesetMetadata({pauseTransfers: false, pauseMintPendingReserves: false})
557
- )
558
- )
559
- }),
560
- // No more payouts.
561
- splitGroups: new JBSplitGroup[](0),
562
- fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
563
- });
564
-
565
- // Update the ruleset to the final one.
566
- // slither-disable-next-line unused-return
567
- controller.queueRulesetsOf({
568
- projectId: gameId, rulesetConfigurations: rulesetConfigs, memo: "Defifa game has finished."
569
- });
570
-
571
- emit FulfilledCommitments({gameId: gameId, pot: _pot, caller: msg.sender});
575
+ /// @notice Allows this contract to receive 721s.
576
+ function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) {
577
+ return IERC721Receiver.onERC721Received.selector;
572
578
  }
573
579
 
574
580
  /// @notice Triggers the no-contest refund mechanism for a game.
@@ -638,11 +644,6 @@ contract DefifaDeployer is IDefifaDeployer, IDefifaGamePhaseReporter, IDefifaGam
638
644
  emit QueuedNoContest(gameId, msg.sender);
639
645
  }
640
646
 
641
- /// @notice Allows this contract to receive 721s.
642
- function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) {
643
- return IERC721Receiver.onERC721Received.selector;
644
- }
645
-
646
647
  //*********************************************************************//
647
648
  // ------------------------ internal functions ----------------------- //
648
649
  //*********************************************************************//
@@ -123,43 +123,12 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
123
123
  // -------------------------- public views --------------------------- //
124
124
  //*********************************************************************//
125
125
 
126
- /// @notice The state of a proposal.
127
- /// @param gameId The ID of the game to get a proposal state of.
128
- /// @param scorecardId The ID of the proposal to get the state of.
129
- /// @return The state.
130
- function stateOf(uint256 gameId, uint256 scorecardId) public view virtual override returns (DefifaScorecardState) {
131
- // Keep a reference to the ratified scorecard ID.
132
- uint256 _ratifiedScorecardId = ratifiedScorecardIdOf[gameId];
133
-
134
- // If the game has already ratified a scorecard, return succeeded if the ratified proposal is being checked.
135
- // Else return defeated.
136
- if (_ratifiedScorecardId != 0) {
137
- return _ratifiedScorecardId == scorecardId ? DefifaScorecardState.RATIFIED : DefifaScorecardState.DEFEATED;
138
- }
139
-
140
- // Get a reference to the scorecard.
141
- DefifaScorecard memory _scorecard = _scorecardOf[gameId][scorecardId];
142
-
143
- // Make sure the proposal is known.
144
- // slither-disable-next-line incorrect-equality
145
- if (_scorecard.attestationsBegin == 0) {
146
- revert DefifaGovernor_UnknownProposal();
147
- }
148
-
149
- // If the scorecard has attestations beginning in the future, the state is PENDING.
150
- if (_scorecard.attestationsBegin >= block.timestamp) {
151
- return DefifaScorecardState.PENDING;
152
- }
153
-
154
- // If the scorecard has a grace period expiring in the future, the state is ACTIVE.
155
- if (_scorecard.gracePeriodEnds >= block.timestamp) {
156
- return DefifaScorecardState.ACTIVE;
157
- }
158
-
159
- // If quorum has been reached, the state is SUCCEEDED, otherwise it is ACTIVE.
160
- return quorum(gameId) <= _scorecardAttestationsOf[gameId][scorecardId].count
161
- ? DefifaScorecardState.SUCCEEDED
162
- : DefifaScorecardState.ACTIVE;
126
+ /// @notice The amount of time that must go by before a scorecard can be ratified.
127
+ /// @param gameId The ID of the game to get the attestation period of.
128
+ /// @return The attestation period in number of blocks.
129
+ function attestationGracePeriodOf(uint256 gameId) public view override returns (uint256) {
130
+ // attestation grace period in bits 48-95 (48 bits).
131
+ return uint256(uint48(_packedScorecardInfoOf[gameId] >> 48));
163
132
  }
164
133
 
165
134
  /// @notice The amount of time between a scorecard being submitted and attestations to it being enabled, measured in
@@ -173,43 +142,6 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
173
142
  return uint256(uint48(_packedScorecardInfoOf[gameId]));
174
143
  }
175
144
 
176
- /// @notice The amount of time that must go by before a scorecard can be ratified.
177
- /// @param gameId The ID of the game to get the attestation period of.
178
- /// @return The attestation period in number of blocks.
179
- function attestationGracePeriodOf(uint256 gameId) public view override returns (uint256) {
180
- // attestation grace period in bits 48-95 (48 bits).
181
- return uint256(uint48(_packedScorecardInfoOf[gameId] >> 48));
182
- }
183
-
184
- /// @notice The number of attestation units that must have participated in a proposal for it to be ratified.
185
- /// @dev Each tier with at least one minted token contributes MAX_ATTESTATION_POWER_TIER to the total
186
- /// eligible weight. Quorum is 50% of this total. Because every tier has equal max attestation power
187
- /// regardless of supply, each tier's community has equal influence — a tier with 1 token and a tier
188
- /// with 100 tokens both cap at MAX_ATTESTATION_POWER_TIER when fully attested. This prevents
189
- /// high-supply tiers from dominating governance, keeping the game fair across all outcomes.
190
- /// @return The quorum number of attestations.
191
- function quorum(uint256 gameId) public view override returns (uint256) {
192
- // Get the game's current funding cycle along with its metadata.
193
- // slither-disable-next-line unused-return
194
- (, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(gameId);
195
-
196
- // Get a reference to the number of tiers.
197
- uint256 _numberOfTiers = IDefifaHook(_metadata.dataHook).store().maxTierIdOf(_metadata.dataHook);
198
-
199
- // Keep a reference to the total eligible tier weight.
200
- uint256 _eligibleTierWeights;
201
-
202
- for (uint256 _i; _i < _numberOfTiers; _i++) {
203
- // Each minted tier contributes MAX_ATTESTATION_POWER_TIER to the quorum denominator.
204
- if (IDefifaHook(_metadata.dataHook).currentSupplyOfTier(_i + 1) != 0) {
205
- _eligibleTierWeights += MAX_ATTESTATION_POWER_TIER;
206
- }
207
- }
208
-
209
- // Quorum = 50% of all minted tiers' attestation power.
210
- return _eligibleTierWeights / 2;
211
- }
212
-
213
145
  /// @notice Gets an account's attestation power given a number of tiers to look through.
214
146
  /// @dev An account's power per tier = MAX_ATTESTATION_POWER_TIER * (account's units / tier's total units).
215
147
  /// This means within a tier, power is proportional to token holdings, but across tiers, each tier's
@@ -261,6 +193,80 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
261
193
  }
262
194
  }
263
195
 
196
+ /// @notice The number of attestation units that must have participated in a proposal for it to be ratified.
197
+ /// @dev Each tier with at least one minted token contributes MAX_ATTESTATION_POWER_TIER to the total
198
+ /// eligible weight. Quorum is 50% of this total. Because every tier has equal max attestation power
199
+ /// regardless of supply, each tier's community has equal influence — a tier with 1 token and a tier
200
+ /// with 100 tokens both cap at MAX_ATTESTATION_POWER_TIER when fully attested. This prevents
201
+ /// high-supply tiers from dominating governance, keeping the game fair across all outcomes.
202
+ /// @return The quorum number of attestations.
203
+ function quorum(uint256 gameId) public view override returns (uint256) {
204
+ // Get the game's current funding cycle along with its metadata.
205
+ // slither-disable-next-line unused-return
206
+ (, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(gameId);
207
+
208
+ // Get a reference to the number of tiers.
209
+ uint256 _numberOfTiers = IDefifaHook(_metadata.dataHook).store().maxTierIdOf(_metadata.dataHook);
210
+
211
+ // Keep a reference to the total eligible tier weight.
212
+ uint256 _eligibleTierWeights;
213
+
214
+ for (uint256 _i; _i < _numberOfTiers; _i++) {
215
+ // Each minted tier contributes MAX_ATTESTATION_POWER_TIER to the quorum denominator.
216
+ if (IDefifaHook(_metadata.dataHook).currentSupplyOfTier(_i + 1) != 0) {
217
+ _eligibleTierWeights += MAX_ATTESTATION_POWER_TIER;
218
+ }
219
+ }
220
+
221
+ // Quorum = 50% of all minted tiers' attestation power.
222
+ return _eligibleTierWeights / 2;
223
+ }
224
+
225
+ /// @notice The state of a proposal.
226
+ /// @param gameId The ID of the game to get a proposal state of.
227
+ /// @param scorecardId The ID of the proposal to get the state of.
228
+ /// @return The state.
229
+ /// @dev Boundary semantics (inclusive):
230
+ /// - At exactly `attestationsBegin`, the state transitions from PENDING to ACTIVE (attestations are open).
231
+ /// - At exactly `gracePeriodEnds`, the grace period has elapsed and the state transitions from ACTIVE to
232
+ /// SUCCEEDED (if quorum is met) or remains ACTIVE (if not).
233
+ function stateOf(uint256 gameId, uint256 scorecardId) public view virtual override returns (DefifaScorecardState) {
234
+ // Keep a reference to the ratified scorecard ID.
235
+ uint256 _ratifiedScorecardId = ratifiedScorecardIdOf[gameId];
236
+
237
+ // If the game has already ratified a scorecard, return succeeded if the ratified proposal is being checked.
238
+ // Else return defeated.
239
+ if (_ratifiedScorecardId != 0) {
240
+ return _ratifiedScorecardId == scorecardId ? DefifaScorecardState.RATIFIED : DefifaScorecardState.DEFEATED;
241
+ }
242
+
243
+ // Get a reference to the scorecard.
244
+ DefifaScorecard memory _scorecard = _scorecardOf[gameId][scorecardId];
245
+
246
+ // Make sure the proposal is known.
247
+ // slither-disable-next-line incorrect-equality
248
+ if (_scorecard.attestationsBegin == 0) {
249
+ revert DefifaGovernor_UnknownProposal();
250
+ }
251
+
252
+ // If the scorecard has attestations beginning in the future, the state is PENDING.
253
+ // At exactly `attestationsBegin`, attestations are open so the state is ACTIVE.
254
+ if (_scorecard.attestationsBegin > block.timestamp) {
255
+ return DefifaScorecardState.PENDING;
256
+ }
257
+
258
+ // If the scorecard's grace period has not yet ended, the state is ACTIVE.
259
+ // At exactly `gracePeriodEnds`, the grace period has elapsed so we fall through to the quorum check.
260
+ if (_scorecard.gracePeriodEnds > block.timestamp) {
261
+ return DefifaScorecardState.ACTIVE;
262
+ }
263
+
264
+ // If quorum has been reached, the state is SUCCEEDED, otherwise it is ACTIVE.
265
+ return quorum(gameId) <= _scorecardAttestationsOf[gameId][scorecardId].count
266
+ ? DefifaScorecardState.SUCCEEDED
267
+ : DefifaScorecardState.ACTIVE;
268
+ }
269
+
264
270
  //*********************************************************************//
265
271
  // -------------------------- constructor ---------------------------- //
266
272
  //*********************************************************************//
@@ -310,75 +316,6 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
310
316
  // ---------------------- external transactions ---------------------- //
311
317
  //*********************************************************************//
312
318
 
313
- /// @notice Submits a scorecard to be attested to.
314
- /// @param _tierWeights The weights of each tier in the scorecard.
315
- /// @return scorecardId The scorecard's ID.
316
- function submitScorecardFor(
317
- uint256 _gameId,
318
- DefifaTierCashOutWeight[] calldata _tierWeights
319
- )
320
- external
321
- override
322
- returns (uint256 scorecardId)
323
- {
324
- // Make sure a proposal hasn't yet been ratified.
325
- if (ratifiedScorecardIdOf[_gameId] != 0) revert DefifaGovernor_AlreadyRatified();
326
-
327
- // Make sure the game has been initialized.
328
- // slither-disable-next-line incorrect-equality
329
- if (_packedScorecardInfoOf[_gameId] == 0) revert DefifaGovernor_GameNotFound();
330
-
331
- // Make sure no weight is assigned to an unowned tier.
332
- uint256 _numberOfTierWeights = _tierWeights.length;
333
-
334
- // Get the game's current funding cycle along with its metadata.
335
- // slither-disable-next-line unused-return
336
- (, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(_gameId);
337
-
338
- // Make sure the game is in its scoring phase.
339
- if (IDefifaHook(_metadata.dataHook).gamePhaseReporter().currentGamePhaseOf(_gameId) != DefifaGamePhase.SCORING)
340
- {
341
- revert DefifaGovernor_NotAllowed();
342
- }
343
-
344
- // If there's a weight assigned to the tier, make sure there is a token backed by it.
345
- for (uint256 _i; _i < _numberOfTierWeights; _i++) {
346
- if (
347
- _tierWeights[_i].cashOutWeight > 0
348
- && IDefifaHook(_metadata.dataHook).currentSupplyOfTier(_tierWeights[_i].id) == 0
349
- ) {
350
- revert DefifaGovernor_UnownedProposedCashoutValue();
351
- }
352
- }
353
-
354
- // Hash the scorecard.
355
- scorecardId =
356
- _hashScorecardOf({_gameHook: _metadata.dataHook, _calldata: _buildScorecardCalldataFor(_tierWeights)});
357
-
358
- // Store the scorecard
359
- DefifaScorecard storage _scorecard = _scorecardOf[_gameId][scorecardId];
360
- if (_scorecard.attestationsBegin != 0) revert DefifaGovernor_DuplicateScorecard();
361
-
362
- uint256 _attestationStartTime = attestationStartTimeOf(_gameId);
363
- uint256 _timeUntilAttestationsBegin =
364
- block.timestamp > _attestationStartTime ? 0 : _attestationStartTime - block.timestamp;
365
-
366
- _scorecard.attestationsBegin = uint48(block.timestamp + _timeUntilAttestationsBegin);
367
- _scorecard.gracePeriodEnds = uint48(block.timestamp + attestationGracePeriodOf(_gameId));
368
-
369
- // Keep a reference to the default attestation delegate.
370
- address _defaultAttestationDelegate = IDefifaHook(_metadata.dataHook).defaultAttestationDelegate();
371
-
372
- // If the scorecard is being sent from the default attestation delegate, store it.
373
- if (msg.sender == _defaultAttestationDelegate) {
374
- defaultAttestationDelegateProposalOf[_gameId] = scorecardId;
375
- }
376
-
377
- emit ScorecardSubmitted(
378
- _gameId, scorecardId, _tierWeights, msg.sender == _defaultAttestationDelegate, msg.sender
379
- );
380
- }
381
-
382
319
  /// @notice Attests to a scorecard.
383
320
  /// @param gameId The ID of the game to which the scorecard belongs.
384
321
  /// @param scorecardId The scorecard ID.
@@ -459,12 +396,90 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
459
396
  // slither-disable-next-line unused-return
460
397
  Address.verifyCallResult({success: success, returndata: returndata});
461
398
 
462
- // Fulfill any commitments for the game.
463
- IDefifaDeployer(controller.PROJECTS().ownerOf(gameId)).fulfillCommitmentsOf(gameId);
399
+ // Fulfill any commitments for the game. Wrapped in try-catch so that a fulfillment
400
+ // failure (e.g. from sendPayoutsOf reverting) does not permanently block ratification.
401
+ // Fulfillment can be retried separately by calling fulfillCommitmentsOf directly.
402
+ try IDefifaDeployer(controller.PROJECTS().ownerOf(gameId)).fulfillCommitmentsOf(gameId) {}
403
+ catch (bytes memory reason) {
404
+ emit FulfillmentFailed(gameId, reason);
405
+ }
464
406
 
465
407
  emit ScorecardRatified(gameId, scorecardId, msg.sender);
466
408
  }
467
409
 
410
+ /// @notice Submits a scorecard to be attested to.
411
+ /// @param _tierWeights The weights of each tier in the scorecard.
412
+ /// @return scorecardId The scorecard's ID.
413
+ function submitScorecardFor(
414
+ uint256 _gameId,
415
+ DefifaTierCashOutWeight[] calldata _tierWeights
416
+ )
417
+ external
418
+ override
419
+ returns (uint256 scorecardId)
420
+ {
421
+ // Make sure a proposal hasn't yet been ratified.
422
+ if (ratifiedScorecardIdOf[_gameId] != 0) revert DefifaGovernor_AlreadyRatified();
423
+
424
+ // Make sure the game has been initialized.
425
+ // slither-disable-next-line incorrect-equality
426
+ if (_packedScorecardInfoOf[_gameId] == 0) revert DefifaGovernor_GameNotFound();
427
+
428
+ // Make sure no weight is assigned to an unowned tier.
429
+ uint256 _numberOfTierWeights = _tierWeights.length;
430
+
431
+ // Get the game's current funding cycle along with its metadata.
432
+ // slither-disable-next-line unused-return
433
+ (, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(_gameId);
434
+
435
+ // Make sure the game is in its scoring phase.
436
+ if (IDefifaHook(_metadata.dataHook).gamePhaseReporter().currentGamePhaseOf(_gameId) != DefifaGamePhase.SCORING)
437
+ {
438
+ revert DefifaGovernor_NotAllowed();
439
+ }
440
+
441
+ // If there's a weight assigned to the tier, make sure there is a token backed by it.
442
+ for (uint256 _i; _i < _numberOfTierWeights; _i++) {
443
+ if (
444
+ _tierWeights[_i].cashOutWeight > 0
445
+ && IDefifaHook(_metadata.dataHook).currentSupplyOfTier(_tierWeights[_i].id) == 0
446
+ ) {
447
+ revert DefifaGovernor_UnownedProposedCashoutValue();
448
+ }
449
+ }
450
+
451
+ // Hash the scorecard.
452
+ scorecardId =
453
+ _hashScorecardOf({_gameHook: _metadata.dataHook, _calldata: _buildScorecardCalldataFor(_tierWeights)});
454
+
455
+ // Store the scorecard
456
+ DefifaScorecard storage _scorecard = _scorecardOf[_gameId][scorecardId];
457
+ if (_scorecard.attestationsBegin != 0) revert DefifaGovernor_DuplicateScorecard();
458
+
459
+ uint256 _attestationStartTime = attestationStartTimeOf(_gameId);
460
+ uint256 _timeUntilAttestationsBegin =
461
+ block.timestamp > _attestationStartTime ? 0 : _attestationStartTime - block.timestamp;
462
+
463
+ uint48 _attestationsBegin = uint48(block.timestamp + _timeUntilAttestationsBegin);
464
+ _scorecard.attestationsBegin = _attestationsBegin;
465
+ // Grace period extends from when attestations begin, not from submission time.
466
+ // This prevents the grace period from expiring before attestations even start
467
+ // when a scorecard is submitted early.
468
+ _scorecard.gracePeriodEnds = uint48(_attestationsBegin + attestationGracePeriodOf(_gameId));
469
+
470
+ // Keep a reference to the default attestation delegate.
471
+ address _defaultAttestationDelegate = IDefifaHook(_metadata.dataHook).defaultAttestationDelegate();
472
+
473
+ // If the scorecard is being sent from the default attestation delegate, store it.
474
+ if (msg.sender == _defaultAttestationDelegate) {
475
+ defaultAttestationDelegateProposalOf[_gameId] = scorecardId;
476
+ }
477
+
478
+ emit ScorecardSubmitted(
479
+ _gameId, scorecardId, _tierWeights, msg.sender == _defaultAttestationDelegate, msg.sender
480
+ );
481
+ }
482
+
468
483
  //*********************************************************************//
469
484
  // ------------------------ internal functions ----------------------- //
470
485
  //*********************************************************************//