@ballkidz/defifa 0.0.1

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 (59) hide show
  1. package/.gas-snapshot +2 -0
  2. package/CRYPTO_ECON.md +955 -0
  3. package/CRYPTO_ECON.pdf +0 -0
  4. package/CRYPTO_ECON.tex +800 -0
  5. package/README.md +119 -0
  6. package/SKILLS.md +177 -0
  7. package/deployments/defifa-v5/arbitrum_sepolia/DefifaDelegate.json +4867 -0
  8. package/deployments/defifa-v5/arbitrum_sepolia/DefifaDeployer.json +1719 -0
  9. package/deployments/defifa-v5/arbitrum_sepolia/DefifaGovernor.json +1535 -0
  10. package/deployments/defifa-v5/arbitrum_sepolia/DefifaTokenUriResolver.json +295 -0
  11. package/deployments/defifa-v5/base_sepolia/DefifaDelegate.json +4875 -0
  12. package/deployments/defifa-v5/base_sepolia/DefifaDeployer.json +1725 -0
  13. package/deployments/defifa-v5/base_sepolia/DefifaGovernor.json +1543 -0
  14. package/deployments/defifa-v5/base_sepolia/DefifaTokenUriResolver.json +301 -0
  15. package/deployments/defifa-v5/optimism_sepolia/DefifaDelegate.json +4875 -0
  16. package/deployments/defifa-v5/optimism_sepolia/DefifaDeployer.json +1725 -0
  17. package/deployments/defifa-v5/optimism_sepolia/DefifaGovernor.json +1543 -0
  18. package/deployments/defifa-v5/optimism_sepolia/DefifaTokenUriResolver.json +301 -0
  19. package/deployments/defifa-v5/sepolia/DefifaDelegate.json +4875 -0
  20. package/deployments/defifa-v5/sepolia/DefifaDeployer.json +1725 -0
  21. package/deployments/defifa-v5/sepolia/DefifaGovernor.json +1543 -0
  22. package/deployments/defifa-v5/sepolia/DefifaTokenUriResolver.json +301 -0
  23. package/foundry.lock +17 -0
  24. package/foundry.toml +35 -0
  25. package/package.json +33 -0
  26. package/remappings.txt +6 -0
  27. package/script/Deploy.s.sol +109 -0
  28. package/script/helpers/DefifaDeploymentLib.sol +83 -0
  29. package/slither-ci.config.json +10 -0
  30. package/sphinx.lock +521 -0
  31. package/src/DefifaDeployer.sol +894 -0
  32. package/src/DefifaGovernor.sol +490 -0
  33. package/src/DefifaHook.sol +1056 -0
  34. package/src/DefifaProjectOwner.sol +63 -0
  35. package/src/DefifaTokenUriResolver.sol +312 -0
  36. package/src/enums/DefifaGamePhase.sol +11 -0
  37. package/src/enums/DefifaScorecardState.sol +10 -0
  38. package/src/interfaces/IDefifaDeployer.sol +108 -0
  39. package/src/interfaces/IDefifaGamePhaseReporter.sol +8 -0
  40. package/src/interfaces/IDefifaGamePotReporter.sol +8 -0
  41. package/src/interfaces/IDefifaGovernor.sol +132 -0
  42. package/src/interfaces/IDefifaHook.sol +228 -0
  43. package/src/interfaces/IDefifaTokenUriResolver.sol +10 -0
  44. package/src/libraries/DefifaFontImporter.sol +19 -0
  45. package/src/libraries/DefifaHookLib.sol +358 -0
  46. package/src/structs/DefifaAttestations.sol +9 -0
  47. package/src/structs/DefifaDelegation.sol +9 -0
  48. package/src/structs/DefifaLaunchProjectData.sol +59 -0
  49. package/src/structs/DefifaOpsData.sol +20 -0
  50. package/src/structs/DefifaScorecard.sol +9 -0
  51. package/src/structs/DefifaTierCashOutWeight.sol +9 -0
  52. package/src/structs/DefifaTierParams.sol +16 -0
  53. package/test/DefifaFeeAccounting.t.sol +559 -0
  54. package/test/DefifaGovernor.t.sol +1333 -0
  55. package/test/DefifaMintCostInvariant.t.sol +299 -0
  56. package/test/DefifaNoContest.t.sol +922 -0
  57. package/test/DefifaSecurity.t.sol +717 -0
  58. package/test/SVG.t.sol +164 -0
  59. package/test/deployScript.t.sol +144 -0
@@ -0,0 +1,490 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ import {mulDiv} from "@prb/math/src/Common.sol";
5
+ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
6
+ import {Address} from "@openzeppelin/contracts/utils/Address.sol";
7
+ import {IDefifaHook} from "./interfaces/IDefifaHook.sol";
8
+ import {IDefifaGovernor} from "./interfaces/IDefifaGovernor.sol";
9
+ import {IDefifaDeployer} from "./interfaces/IDefifaDeployer.sol";
10
+ import {DefifaScorecard} from "./structs/DefifaScorecard.sol";
11
+ import {DefifaAttestations} from "./structs/DefifaAttestations.sol";
12
+ import {DefifaTierCashOutWeight} from "./structs/DefifaTierCashOutWeight.sol";
13
+ import {DefifaGamePhase} from "./enums/DefifaGamePhase.sol";
14
+ import {DefifaScorecardState} from "./enums/DefifaScorecardState.sol";
15
+ import {DefifaHook} from "./DefifaHook.sol";
16
+
17
+ import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
18
+ import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
19
+ import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
20
+ import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
21
+
22
+ /// @title DefifaGovernor
23
+ /// @notice Manages the ratification of Defifa scorecards.
24
+ contract DefifaGovernor is Ownable, IDefifaGovernor {
25
+ //*********************************************************************//
26
+ // --------------------------- custom errors ------------------------- //
27
+ //*********************************************************************//
28
+ error DefifaGovernor_AlreadyAttested();
29
+ error DefifaGovernor_AlreadyRatified();
30
+ error DefifaGovernor_GameNotFound();
31
+ error DefifaGovernor_NotAllowed();
32
+ error DefifaGovernor_DuplicateScorecard();
33
+ error DefifaGovernor_IncorrectTierOrder();
34
+ error DefifaGovernor_UnknownProposal();
35
+ error DefifaGovernor_UnownedProposedCashoutValue();
36
+
37
+ //*********************************************************************//
38
+ // ---------------- immutable internal stored properties ------------- //
39
+ //*********************************************************************//
40
+
41
+ /// @notice The scorecards.
42
+ /// _gameId The ID of the game for which the scorecard affects.
43
+ /// _scorecardId The ID of the scorecard to retrieve.
44
+ mapping(uint256 => mapping(uint256 => DefifaScorecard)) internal _scorecardOf;
45
+
46
+ /// @notice The attestations to a scorecard
47
+ /// _gameId The ID of the game for which the scorecard affects.
48
+ /// _scorecardId The ID of the scorecard that has been attested to.
49
+ mapping(uint256 => mapping(uint256 => DefifaAttestations)) internal _scorecardAttestationsOf;
50
+
51
+ //*********************************************************************//
52
+ // --------------------- internal stored properties ------------------ //
53
+ //*********************************************************************//
54
+
55
+ /// @notice The scorecard information, packed into a uint256.
56
+ /// _gameId The ID of the game for which the scorecard info applies.
57
+ mapping(uint256 => uint256) internal _packedScorecardInfoOf;
58
+
59
+ //*********************************************************************//
60
+ // ------------------------ public constants ------------------------- //
61
+ //*********************************************************************//
62
+
63
+ /// @notice The max attestation power each tier has if every token within the tier attestations.
64
+ uint256 public constant override MAX_ATTESTATION_POWER_TIER = 1_000_000_000;
65
+
66
+ //*********************************************************************//
67
+ // --------------- public immutable stored properties ---------------- //
68
+ //*********************************************************************//
69
+
70
+ /// @notice The controller with which new projects should be deployed.
71
+ IJBController public immutable override controller;
72
+
73
+ //*********************************************************************//
74
+ // -------------------- public stored properties --------------------- //
75
+ //*********************************************************************//
76
+
77
+ /// @notice The latest proposal submitted by the default attestation delegate.
78
+ /// _gameId The ID of the game of the default attestation delegate proposal.
79
+ mapping(uint256 => uint256) public override defaultAttestationDelegateProposalOf;
80
+
81
+ /// @notice The scorecard that has been ratified.
82
+ /// _gameId The ID of the game of the ratified scorecard.
83
+ mapping(uint256 => uint256) public override ratifiedScorecardIdOf;
84
+
85
+ //*********************************************************************//
86
+ // -------------------------- external views --------------------------- //
87
+ //*********************************************************************//
88
+
89
+ /// @notice The number of attestations the given scorecard has.
90
+ /// @param gameId The ID of the game to which the scorecard belongs.
91
+ /// @param scorecardId The ID of the scorecard to get attestations of.
92
+ /// @return The number of attestations the given scorecard has.
93
+ function attestationCountOf(uint256 gameId, uint256 scorecardId) external view returns (uint256) {
94
+ return _scorecardAttestationsOf[gameId][scorecardId].count;
95
+ }
96
+
97
+ /// @notice A flag indicating if the given account has already attested to the scorecard.
98
+ /// @param gameId The ID of the game to which the scorecard belongs.
99
+ /// @param scorecardId The ID of the scorecard to query attestations from.
100
+ /// @param account The address to check the attestation status of.
101
+ /// @return A flag indicating if the given account has already attested to the scorecard.
102
+ function hasAttestedTo(uint256 gameId, uint256 scorecardId, address account) external view returns (bool) {
103
+ return _scorecardAttestationsOf[gameId][scorecardId].hasAttested[account];
104
+ }
105
+
106
+ /// @notice The ID of a scorecard representing the provided tier weights.
107
+ /// @param gameHook The address where the game is being played.
108
+ /// @param tierWeights The weights of each tier in the scorecard.
109
+ function scorecardIdOf(
110
+ address gameHook,
111
+ DefifaTierCashOutWeight[] calldata tierWeights
112
+ )
113
+ external
114
+ pure
115
+ virtual
116
+ override
117
+ returns (uint256)
118
+ {
119
+ return _hashScorecardOf({_gameHook: gameHook, _calldata: _buildScorecardCalldataFor(tierWeights)});
120
+ }
121
+
122
+ //*********************************************************************//
123
+ // -------------------------- public views --------------------------- //
124
+ //*********************************************************************//
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;
163
+ }
164
+
165
+ /// @notice The amount of time between a scorecard being submitted and attestations to it being enabled, measured in
166
+ /// seconds.
167
+ /// @dev This can be increased to leave time for users to acquire attestation power, or delegate it, before
168
+ /// a scorecard becomes live.
169
+ /// @param gameId The ID of the game to get the attestation delay of.
170
+ /// @return The delay, in seconds.
171
+ function attestationStartTimeOf(uint256 gameId) public view override returns (uint256) {
172
+ // attestation start time in bits 0-47 (48 bits).
173
+ return uint256(uint48(_packedScorecardInfoOf[gameId]));
174
+ }
175
+
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
+ /// @notice Gets an account's attestation power given a number of tiers to look through.
214
+ /// @dev An account's power per tier = MAX_ATTESTATION_POWER_TIER * (account's units / tier's total units).
215
+ /// This means within a tier, power is proportional to token holdings, but across tiers, each tier's
216
+ /// total power is capped at MAX_ATTESTATION_POWER_TIER. A holder of 1-of-1 in a tier gets
217
+ /// MAX_ATTESTATION_POWER_TIER; a holder of 1-of-100 gets MAX_ATTESTATION_POWER_TIER / 100.
218
+ /// This ensures each game outcome (tier) has equal governance weight — the scorecard reflects
219
+ /// consensus across outcomes, not dominance by whichever outcome sold the most tokens.
220
+ /// @param _gameId The ID of the game for which attestations are being counted.
221
+ /// @param _account The account to get attestations for.
222
+ /// @param _timestamp The timestamp to measure attestations from.
223
+ /// @return attestationPower The amount of attestation power of an account.
224
+ function getAttestationWeight(
225
+ uint256 _gameId,
226
+ address _account,
227
+ uint48 _timestamp
228
+ )
229
+ public
230
+ view
231
+ virtual
232
+ returns (uint256 attestationPower)
233
+ {
234
+ // Get the game's current funding cycle along with its metadata.
235
+ // slither-disable-next-line unused-return
236
+ (, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(_gameId);
237
+
238
+ // Get a reference to the number of tiers.
239
+ uint256 _numberOfTiers = IDefifaHook(_metadata.dataHook).store().maxTierIdOf(_metadata.dataHook);
240
+
241
+ for (uint256 _i; _i < _numberOfTiers; _i++) {
242
+ // Tiers are 1-indexed.
243
+ uint256 _tierId = _i + 1;
244
+
245
+ // Get this account's attestation units within the tier (snapshot at _timestamp).
246
+ uint256 _tierAttestationUnitsForAccount = IDefifaHook(_metadata.dataHook)
247
+ .getPastTierAttestationUnitsOf({account: _account, tier: _tierId, timestamp: _timestamp});
248
+
249
+ // Scale the account's share of the tier to MAX_ATTESTATION_POWER_TIER.
250
+ // e.g. holding 3 of 10 tokens → 3/10 * MAX_ATTESTATION_POWER_TIER attestation power from this tier.
251
+ unchecked {
252
+ if (_tierAttestationUnitsForAccount != 0) {
253
+ attestationPower += mulDiv(
254
+ MAX_ATTESTATION_POWER_TIER,
255
+ _tierAttestationUnitsForAccount,
256
+ IDefifaHook(_metadata.dataHook)
257
+ .getPastTierTotalAttestationUnitsOf({tier: _tierId, timestamp: _timestamp})
258
+ );
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ //*********************************************************************//
265
+ // -------------------------- constructor ---------------------------- //
266
+ //*********************************************************************//
267
+
268
+ constructor(IJBController _controller, address _owner) Ownable(_owner) {
269
+ controller = _controller;
270
+ }
271
+
272
+ //*********************************************************************//
273
+ // ----------------------- public transactions ----------------------- //
274
+ //*********************************************************************//
275
+
276
+ /// @notice Initializes a game.
277
+ /// @param _attestationStartTime The amount of time between a scorecard being submitted and attestations to it being
278
+ /// enabled, measured in seconds. @param _attestationGracePeriod The amount of time that must go by before a
279
+ /// scorecard can be ratified.
280
+ function initializeGame(
281
+ uint256 _gameId,
282
+ uint256 _attestationStartTime,
283
+ uint256 _attestationGracePeriod
284
+ )
285
+ public
286
+ virtual
287
+ override
288
+ onlyOwner
289
+ {
290
+ // Set a default attestation start time if needed.
291
+ if (_attestationStartTime == 0) _attestationStartTime = block.timestamp;
292
+
293
+ // Enforce a minimum grace period of 1 day to prevent instant ratification.
294
+ if (_attestationGracePeriod < 1 days) _attestationGracePeriod = 1 days;
295
+
296
+ // Pack the values.
297
+ uint256 _packed;
298
+ // attestation start time in bits 0-47 (48 bits).
299
+ _packed |= _attestationStartTime;
300
+ // attestation grace period in bits 48-95 (48 bits).
301
+ _packed |= _attestationGracePeriod << 48;
302
+
303
+ // Store the packed value.
304
+ _packedScorecardInfoOf[_gameId] = _packed;
305
+
306
+ emit GameInitialized(_gameId, _attestationStartTime, _attestationGracePeriod, msg.sender);
307
+ }
308
+
309
+ //*********************************************************************//
310
+ // ---------------------- external transactions ---------------------- //
311
+ //*********************************************************************//
312
+
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
+ /// @notice Attests to a scorecard.
383
+ /// @param gameId The ID of the game to which the scorecard belongs.
384
+ /// @param scorecardId The scorecard ID.
385
+ /// @return weight The attestation weight that was applied.
386
+ function attestToScorecardFrom(uint256 gameId, uint256 scorecardId) external override returns (uint256 weight) {
387
+ // Get the game's current funding cycle along with its metadata.
388
+ // slither-disable-next-line unused-return
389
+ (, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(gameId);
390
+
391
+ // Make sure the game is in its scoring phase.
392
+ if (IDefifaHook(_metadata.dataHook).gamePhaseReporter().currentGamePhaseOf(gameId) != DefifaGamePhase.SCORING) {
393
+ revert DefifaGovernor_NotAllowed();
394
+ }
395
+
396
+ // Keep a reference to the scorecard being attested to.
397
+ DefifaScorecard storage _scorecard = _scorecardOf[gameId][scorecardId];
398
+
399
+ // Keep a reference to the scorecard state.
400
+ DefifaScorecardState _state = stateOf({gameId: gameId, scorecardId: scorecardId});
401
+
402
+ if (_state != DefifaScorecardState.ACTIVE && _state != DefifaScorecardState.SUCCEEDED) {
403
+ revert DefifaGovernor_NotAllowed();
404
+ }
405
+
406
+ // Keep a reference to the attestations for the scorecard.
407
+ DefifaAttestations storage _attestations = _scorecardAttestationsOf[gameId][scorecardId];
408
+
409
+ // Make sure the account isn't attesting to the same scorecard again.
410
+ if (_attestations.hasAttested[msg.sender]) revert DefifaGovernor_AlreadyAttested();
411
+
412
+ // Get a reference to the attestation weight.
413
+ weight = getAttestationWeight({_gameId: gameId, _account: msg.sender, _timestamp: _scorecard.attestationsBegin});
414
+
415
+ // Increase the attestation count.
416
+ _attestations.count += weight;
417
+
418
+ // Store the fact that the account has attested to the scorecard.
419
+ _attestations.hasAttested[msg.sender] = true;
420
+
421
+ emit ScorecardAttested(gameId, scorecardId, weight, msg.sender);
422
+ }
423
+
424
+ /// @notice Ratifies a scorecard that has been approved.
425
+ /// @param gameId The ID of the game.
426
+ /// @param tierWeights The weights of each tier in the approved scorecard.
427
+ /// @return scorecardId The scorecard ID that was ratified.
428
+ function ratifyScorecardFrom(
429
+ uint256 gameId,
430
+ DefifaTierCashOutWeight[] calldata tierWeights
431
+ )
432
+ external
433
+ override
434
+ returns (uint256 scorecardId)
435
+ {
436
+ // Make sure a scorecard hasn't been ratified yet.
437
+ if (ratifiedScorecardIdOf[gameId] != 0) revert DefifaGovernor_AlreadyRatified();
438
+
439
+ // Get the game's current funding cycle along with its metadata.
440
+ // slither-disable-next-line unused-return
441
+ (, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(gameId);
442
+
443
+ // Build the calldata to the target
444
+ bytes memory _calldata = _buildScorecardCalldataFor(tierWeights);
445
+
446
+ // Attempt to execute the proposal.
447
+ scorecardId = _hashScorecardOf({_gameHook: _metadata.dataHook, _calldata: _calldata});
448
+
449
+ // Make sure the proposal being ratified has succeeded.
450
+ if (stateOf({gameId: gameId, scorecardId: scorecardId}) != DefifaScorecardState.SUCCEEDED) {
451
+ revert DefifaGovernor_NotAllowed();
452
+ }
453
+
454
+ // Set the ratified scorecard.
455
+ ratifiedScorecardIdOf[gameId] = scorecardId;
456
+
457
+ // Execute the scorecard via low-level call since the governor is the delegate's owner.
458
+ (bool success, bytes memory returndata) = _metadata.dataHook.call(_calldata);
459
+ // slither-disable-next-line unused-return
460
+ Address.verifyCallResult({success: success, returndata: returndata});
461
+
462
+ // Fulfill any commitments for the game.
463
+ IDefifaDeployer(controller.PROJECTS().ownerOf(gameId)).fulfillCommitmentsOf(gameId);
464
+
465
+ emit ScorecardRatified(gameId, scorecardId, msg.sender);
466
+ }
467
+
468
+ //*********************************************************************//
469
+ // ------------------------ internal functions ----------------------- //
470
+ //*********************************************************************//
471
+
472
+ /// @notice Build the normalized calldata.
473
+ /// @param _tierWeights The weights of each tier in the scorecard data.
474
+ /// @return The calldata to send alongside the transactions.
475
+ function _buildScorecardCalldataFor(DefifaTierCashOutWeight[] calldata _tierWeights)
476
+ internal
477
+ pure
478
+ returns (bytes memory)
479
+ {
480
+ // Build the calldata from the tier weights.
481
+ return abi.encodeWithSelector(DefifaHook.setTierCashOutWeightsTo.selector, (_tierWeights));
482
+ }
483
+
484
+ /// @notice A value representing the contents of a scorecard.
485
+ /// @param _gameHook The address where the game is being played.
486
+ /// @param _calldata The calldata that will be sent if the scorecard is ratified.
487
+ function _hashScorecardOf(address _gameHook, bytes memory _calldata) internal pure virtual returns (uint256) {
488
+ return uint256(keccak256(abi.encode(_gameHook, _calldata)));
489
+ }
490
+ }