@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,89 +1,257 @@
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 {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
4
+ import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
5
+ import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
6
6
  import {Address} from "@openzeppelin/contracts/utils/Address.sol";
7
- import {IDefifaHook} from "./interfaces/IDefifaHook.sol";
8
- import {IDefifaGovernor} from "./interfaces/IDefifaGovernor.sol";
7
+ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
8
+ import {mulDiv} from "@prb/math/src/Common.sol";
9
+
10
+ import {DefifaHook} from "./DefifaHook.sol";
11
+ import {DefifaGamePhase} from "./enums/DefifaGamePhase.sol";
12
+ import {DefifaScorecardState} from "./enums/DefifaScorecardState.sol";
9
13
  import {IDefifaDeployer} from "./interfaces/IDefifaDeployer.sol";
10
- import {DefifaScorecard} from "./structs/DefifaScorecard.sol";
14
+ import {IDefifaGovernor} from "./interfaces/IDefifaGovernor.sol";
15
+ import {IDefifaHook} from "./interfaces/IDefifaHook.sol";
11
16
  import {DefifaAttestations} from "./structs/DefifaAttestations.sol";
17
+ import {DefifaScorecard} from "./structs/DefifaScorecard.sol";
12
18
  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
19
 
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
20
  /// @notice Manages the ratification of Defifa scorecards.
24
21
  contract DefifaGovernor is Ownable, IDefifaGovernor {
25
22
  //*********************************************************************//
26
23
  // --------------------------- custom errors ------------------------- //
27
24
  //*********************************************************************//
25
+
28
26
  error DefifaGovernor_AlreadyAttested();
27
+ error DefifaGovernor_AlreadyInitialized();
29
28
  error DefifaGovernor_AlreadyRatified();
30
- error DefifaGovernor_GameNotFound();
31
- error DefifaGovernor_NotAllowed();
32
29
  error DefifaGovernor_DuplicateScorecard();
30
+ error DefifaGovernor_GameNotFound();
33
31
  error DefifaGovernor_IncorrectTierOrder();
32
+ error DefifaGovernor_NotAllowed();
33
+ error DefifaGovernor_Uint48Overflow();
34
34
  error DefifaGovernor_UnknownProposal();
35
35
  error DefifaGovernor_UnownedProposedCashoutValue();
36
36
 
37
37
  //*********************************************************************//
38
- // ---------------- immutable internal stored properties ------------- //
38
+ // ------------------------- public constants ------------------------ //
39
39
  //*********************************************************************//
40
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;
41
+ /// @notice The max attestation power each tier has if every token within the tier attestations.
42
+ uint256 public constant override MAX_ATTESTATION_POWER_TIER = 1_000_000_000;
45
43
 
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;
44
+ //*********************************************************************//
45
+ // --------------- public immutable stored properties ---------------- //
46
+ //*********************************************************************//
47
+
48
+ /// @notice The controller with which new projects should be deployed.
49
+ IJBController public immutable override CONTROLLER;
50
50
 
51
51
  //*********************************************************************//
52
- // --------------------- internal stored properties ------------------ //
52
+ // --------------------- public stored properties -------------------- //
53
53
  //*********************************************************************//
54
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;
55
+ /// @notice The latest proposal submitted by the default attestation delegate.
56
+ /// @custom:param gameId The ID of the game of the default attestation delegate proposal.
57
+ mapping(uint256 => uint256) public override defaultAttestationDelegateProposalOf;
58
+
59
+ /// @notice The scorecard that has been ratified.
60
+ /// @custom:param gameId The ID of the game of the ratified scorecard.
61
+ mapping(uint256 => uint256) public override ratifiedScorecardIdOf;
58
62
 
59
63
  //*********************************************************************//
60
- // ------------------------ public constants ------------------------- //
64
+ // -------------------- internal stored properties ------------------- //
61
65
  //*********************************************************************//
62
66
 
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;
67
+ /// @notice The scorecard information, packed into a uint256.
68
+ /// @custom:param gameId The ID of the game for which the scorecard info applies.
69
+ mapping(uint256 => uint256) internal _packedScorecardInfoOf;
70
+
71
+ /// @notice The scorecards.
72
+ /// @custom:param gameId The ID of the game for which the scorecard affects.
73
+ /// @custom:param scorecardId The ID of the scorecard to retrieve.
74
+ mapping(uint256 => mapping(uint256 => DefifaScorecard)) internal _scorecardOf;
75
+
76
+ /// @notice The attestations to a scorecard.
77
+ /// @custom:param gameId The ID of the game for which the scorecard affects.
78
+ /// @custom:param scorecardId The ID of the scorecard that has been attested to.
79
+ mapping(uint256 => mapping(uint256 => DefifaAttestations)) internal _scorecardAttestationsOf;
65
80
 
66
81
  //*********************************************************************//
67
- // --------------- public immutable stored properties ---------------- //
82
+ // -------------------------- constructor ---------------------------- //
68
83
  //*********************************************************************//
69
84
 
70
- /// @notice The controller with which new projects should be deployed.
71
- IJBController public immutable override controller;
85
+ constructor(IJBController controller, address owner) Ownable(owner) {
86
+ CONTROLLER = controller;
87
+ }
72
88
 
73
89
  //*********************************************************************//
74
- // -------------------- public stored properties --------------------- //
90
+ // ---------------------- external transactions ---------------------- //
75
91
  //*********************************************************************//
76
92
 
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;
93
+ /// @notice Attests to a scorecard.
94
+ /// @param gameId The ID of the game to which the scorecard belongs.
95
+ /// @param scorecardId The scorecard ID.
96
+ /// @return weight The attestation weight that was applied.
97
+ function attestToScorecardFrom(uint256 gameId, uint256 scorecardId) external override returns (uint256 weight) {
98
+ // Get the game's current funding cycle along with its metadata.
99
+ // slither-disable-next-line unused-return
100
+ (, JBRulesetMetadata memory _metadata) = CONTROLLER.currentRulesetOf(gameId);
80
101
 
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;
102
+ // Make sure the game is in its scoring phase.
103
+ if (IDefifaHook(_metadata.dataHook).gamePhaseReporter().currentGamePhaseOf(gameId) != DefifaGamePhase.SCORING) {
104
+ revert DefifaGovernor_NotAllowed();
105
+ }
106
+
107
+ // Keep a reference to the scorecard being attested to.
108
+ DefifaScorecard storage _scorecard = _scorecardOf[gameId][scorecardId];
109
+
110
+ // Keep a reference to the scorecard state.
111
+ DefifaScorecardState _state = stateOf({gameId: gameId, scorecardId: scorecardId});
112
+
113
+ if (_state != DefifaScorecardState.ACTIVE && _state != DefifaScorecardState.SUCCEEDED) {
114
+ revert DefifaGovernor_NotAllowed();
115
+ }
116
+
117
+ // Keep a reference to the attestations for the scorecard.
118
+ DefifaAttestations storage _attestations = _scorecardAttestationsOf[gameId][scorecardId];
119
+
120
+ // Make sure the account isn't attesting to the same scorecard again.
121
+ if (_attestations.hasAttested[msg.sender]) revert DefifaGovernor_AlreadyAttested();
122
+
123
+ // Get a reference to the attestation weight.
124
+ weight = getAttestationWeight({gameId: gameId, account: msg.sender, timestamp: _scorecard.attestationsBegin});
125
+
126
+ // Increase the attestation count.
127
+ _attestations.count += weight;
128
+
129
+ // Store the fact that the account has attested to the scorecard.
130
+ _attestations.hasAttested[msg.sender] = true;
131
+
132
+ emit ScorecardAttested(gameId, scorecardId, weight, msg.sender);
133
+ }
134
+
135
+ /// @notice Ratifies a scorecard that has been approved.
136
+ /// @param gameId The ID of the game.
137
+ /// @param tierWeights The weights of each tier in the approved scorecard.
138
+ /// @return scorecardId The scorecard ID that was ratified.
139
+ function ratifyScorecardFrom(
140
+ uint256 gameId,
141
+ DefifaTierCashOutWeight[] calldata tierWeights
142
+ )
143
+ external
144
+ override
145
+ returns (uint256 scorecardId)
146
+ {
147
+ // Make sure a scorecard hasn't been ratified yet.
148
+ if (ratifiedScorecardIdOf[gameId] != 0) revert DefifaGovernor_AlreadyRatified();
149
+
150
+ // Get the game's current funding cycle along with its metadata.
151
+ // slither-disable-next-line unused-return
152
+ (, JBRulesetMetadata memory _metadata) = CONTROLLER.currentRulesetOf(gameId);
153
+
154
+ // Build the calldata to the target
155
+ bytes memory _calldata = _buildScorecardCalldataFor(tierWeights);
156
+
157
+ // Attempt to execute the proposal.
158
+ scorecardId = _hashScorecardOf({gameHook: _metadata.dataHook, calldataBytes: _calldata});
159
+
160
+ // Make sure the proposal being ratified has succeeded.
161
+ if (stateOf({gameId: gameId, scorecardId: scorecardId}) != DefifaScorecardState.SUCCEEDED) {
162
+ revert DefifaGovernor_NotAllowed();
163
+ }
164
+
165
+ // Set the ratified scorecard.
166
+ ratifiedScorecardIdOf[gameId] = scorecardId;
167
+
168
+ // Execute the scorecard via low-level call since the governor is the delegate's owner.
169
+ (bool success, bytes memory returndata) = _metadata.dataHook.call(_calldata);
170
+ // slither-disable-next-line unused-return
171
+ Address.verifyCallResult({success: success, returndata: returndata});
172
+
173
+ // Fulfill any commitments for the game. The internal try-catch in fulfillCommitmentsOf
174
+ // handles sendPayoutsOf failures, ensuring the final ruleset is always queued.
175
+ IDefifaDeployer(CONTROLLER.PROJECTS().ownerOf(gameId)).fulfillCommitmentsOf(gameId);
176
+
177
+ emit ScorecardRatified(gameId, scorecardId, msg.sender);
178
+ }
179
+
180
+ /// @notice Submits a scorecard to be attested to.
181
+ /// @param gameId The ID of the game.
182
+ /// @param tierWeights The weights of each tier in the scorecard.
183
+ /// @return scorecardId The scorecard's ID.
184
+ function submitScorecardFor(
185
+ uint256 gameId,
186
+ DefifaTierCashOutWeight[] calldata tierWeights
187
+ )
188
+ external
189
+ override
190
+ returns (uint256 scorecardId)
191
+ {
192
+ // Make sure a proposal hasn't yet been ratified.
193
+ if (ratifiedScorecardIdOf[gameId] != 0) revert DefifaGovernor_AlreadyRatified();
194
+
195
+ // Make sure the game has been initialized.
196
+ // slither-disable-next-line incorrect-equality
197
+ if (_packedScorecardInfoOf[gameId] == 0) revert DefifaGovernor_GameNotFound();
198
+
199
+ // Make sure no weight is assigned to an unowned tier.
200
+ uint256 _numberOfTierWeights = tierWeights.length;
201
+
202
+ // Get the game's current funding cycle along with its metadata.
203
+ // slither-disable-next-line unused-return
204
+ (, JBRulesetMetadata memory _metadata) = CONTROLLER.currentRulesetOf(gameId);
205
+
206
+ // Make sure the game is in its scoring phase.
207
+ if (IDefifaHook(_metadata.dataHook).gamePhaseReporter().currentGamePhaseOf(gameId) != DefifaGamePhase.SCORING) {
208
+ revert DefifaGovernor_NotAllowed();
209
+ }
210
+
211
+ // If there's a weight assigned to the tier, make sure there is a token backed by it.
212
+ for (uint256 _i; _i < _numberOfTierWeights; _i++) {
213
+ if (
214
+ tierWeights[_i].cashOutWeight > 0
215
+ && IDefifaHook(_metadata.dataHook).currentSupplyOfTier(tierWeights[_i].id) == 0
216
+ ) {
217
+ revert DefifaGovernor_UnownedProposedCashoutValue();
218
+ }
219
+ }
220
+
221
+ // Hash the scorecard.
222
+ scorecardId =
223
+ _hashScorecardOf({gameHook: _metadata.dataHook, calldataBytes: _buildScorecardCalldataFor(tierWeights)});
224
+
225
+ // Store the scorecard
226
+ DefifaScorecard storage _scorecard = _scorecardOf[gameId][scorecardId];
227
+ if (_scorecard.attestationsBegin != 0) revert DefifaGovernor_DuplicateScorecard();
228
+
229
+ uint256 _attestationStartTime = attestationStartTimeOf(gameId);
230
+ uint256 _timeUntilAttestationsBegin =
231
+ block.timestamp > _attestationStartTime ? 0 : _attestationStartTime - block.timestamp;
232
+
233
+ // Casting to uint48 is safe because block.timestamp fits in uint48 until year 8921556.
234
+ // forge-lint: disable-next-line(unsafe-typecast)
235
+ uint48 _attestationsBegin = uint48(block.timestamp + _timeUntilAttestationsBegin);
236
+ _scorecard.attestationsBegin = _attestationsBegin;
237
+ // Grace period extends from when attestations begin, not from submission time.
238
+ // This prevents the grace period from expiring before attestations even start
239
+ // when a scorecard is submitted early.
240
+ _scorecard.gracePeriodEnds = uint48(_attestationsBegin + attestationGracePeriodOf(gameId));
241
+
242
+ // Keep a reference to the default attestation delegate.
243
+ address _defaultAttestationDelegate = IDefifaHook(_metadata.dataHook).defaultAttestationDelegate();
244
+
245
+ // If the scorecard is being sent from the default attestation delegate, store it.
246
+ if (msg.sender == _defaultAttestationDelegate) {
247
+ defaultAttestationDelegateProposalOf[gameId] = scorecardId;
248
+ }
249
+
250
+ emit ScorecardSubmitted(gameId, scorecardId, tierWeights, msg.sender == _defaultAttestationDelegate, msg.sender);
251
+ }
84
252
 
85
253
  //*********************************************************************//
86
- // -------------------------- external views --------------------------- //
254
+ // ----------------------- external views ---------------------------- //
87
255
  //*********************************************************************//
88
256
 
89
257
  /// @notice The number of attestations the given scorecard has.
@@ -116,7 +284,52 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
116
284
  override
117
285
  returns (uint256)
118
286
  {
119
- return _hashScorecardOf({_gameHook: gameHook, _calldata: _buildScorecardCalldataFor(tierWeights)});
287
+ return _hashScorecardOf({gameHook: gameHook, calldataBytes: _buildScorecardCalldataFor(tierWeights)});
288
+ }
289
+
290
+ //*********************************************************************//
291
+ // ----------------------- public transactions ----------------------- //
292
+ //*********************************************************************//
293
+
294
+ /// @notice Initializes a game.
295
+ /// @param gameId The ID of the game.
296
+ /// @param attestationStartTime The amount of time between a scorecard being submitted and attestations to it being
297
+ /// enabled, measured in seconds.
298
+ /// @param attestationGracePeriod The amount of time that must go by before a scorecard can be ratified.
299
+ function initializeGame(
300
+ uint256 gameId,
301
+ uint256 attestationStartTime,
302
+ uint256 attestationGracePeriod
303
+ )
304
+ public
305
+ virtual
306
+ override
307
+ onlyOwner
308
+ {
309
+ // Make sure the game hasn't already been initialized.
310
+ if (_packedScorecardInfoOf[gameId] != 0) revert DefifaGovernor_AlreadyInitialized();
311
+
312
+ // Set a default attestation start time if needed.
313
+ if (attestationStartTime == 0) attestationStartTime = block.timestamp;
314
+
315
+ // Enforce a minimum grace period of 1 day to prevent instant ratification.
316
+ if (attestationGracePeriod < 1 days) attestationGracePeriod = 1 days;
317
+
318
+ // Ensure values fit within their allocated 48-bit widths before packing.
319
+ if (attestationStartTime > type(uint48).max) revert DefifaGovernor_Uint48Overflow();
320
+ if (attestationGracePeriod > type(uint48).max) revert DefifaGovernor_Uint48Overflow();
321
+
322
+ // Pack the values.
323
+ uint256 _packed;
324
+ // attestation start time in bits 0-47 (48 bits).
325
+ _packed |= attestationStartTime;
326
+ // attestation grace period in bits 48-95 (48 bits).
327
+ _packed |= attestationGracePeriod << 48;
328
+
329
+ // Store the packed value.
330
+ _packedScorecardInfoOf[gameId] = _packed;
331
+
332
+ emit GameInitialized(gameId, attestationStartTime, attestationGracePeriod, msg.sender);
120
333
  }
121
334
 
122
335
  //*********************************************************************//
@@ -149,14 +362,14 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
149
362
  /// MAX_ATTESTATION_POWER_TIER; a holder of 1-of-100 gets MAX_ATTESTATION_POWER_TIER / 100.
150
363
  /// This ensures each game outcome (tier) has equal governance weight — the scorecard reflects
151
364
  /// consensus across outcomes, not dominance by whichever outcome sold the most tokens.
152
- /// @param _gameId The ID of the game for which attestations are being counted.
153
- /// @param _account The account to get attestations for.
154
- /// @param _timestamp The timestamp to measure attestations from.
365
+ /// @param gameId The ID of the game for which attestations are being counted.
366
+ /// @param account The account to get attestations for.
367
+ /// @param timestamp The timestamp to measure attestations from.
155
368
  /// @return attestationPower The amount of attestation power of an account.
156
369
  function getAttestationWeight(
157
- uint256 _gameId,
158
- address _account,
159
- uint48 _timestamp
370
+ uint256 gameId,
371
+ address account,
372
+ uint48 timestamp
160
373
  )
161
374
  public
162
375
  view
@@ -165,29 +378,30 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
165
378
  {
166
379
  // Get the game's current funding cycle along with its metadata.
167
380
  // slither-disable-next-line unused-return
168
- (, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(_gameId);
381
+ (, JBRulesetMetadata memory _metadata) = CONTROLLER.currentRulesetOf(gameId);
169
382
 
170
383
  // Get a reference to the number of tiers.
171
384
  uint256 _numberOfTiers = IDefifaHook(_metadata.dataHook).store().maxTierIdOf(_metadata.dataHook);
172
385
 
386
+ // slither-disable-next-line calls-inside-a-loop
173
387
  for (uint256 _i; _i < _numberOfTiers; _i++) {
174
388
  // Tiers are 1-indexed.
175
389
  uint256 _tierId = _i + 1;
176
390
 
177
- // Get this account's attestation units within the tier (snapshot at _timestamp).
391
+ // Get this account's attestation units within the tier (snapshot at timestamp).
178
392
  uint256 _tierAttestationUnitsForAccount = IDefifaHook(_metadata.dataHook)
179
- .getPastTierAttestationUnitsOf({account: _account, tier: _tierId, timestamp: _timestamp});
393
+ .getPastTierAttestationUnitsOf({account: account, tier: _tierId, timestamp: timestamp});
180
394
 
181
395
  // Scale the account's share of the tier to MAX_ATTESTATION_POWER_TIER.
182
396
  // e.g. holding 3 of 10 tokens → 3/10 * MAX_ATTESTATION_POWER_TIER attestation power from this tier.
183
397
  unchecked {
184
398
  if (_tierAttestationUnitsForAccount != 0) {
185
- attestationPower += mulDiv(
186
- MAX_ATTESTATION_POWER_TIER,
187
- _tierAttestationUnitsForAccount,
188
- IDefifaHook(_metadata.dataHook)
189
- .getPastTierTotalAttestationUnitsOf({tier: _tierId, timestamp: _timestamp})
190
- );
399
+ attestationPower += mulDiv({
400
+ x: MAX_ATTESTATION_POWER_TIER,
401
+ y: _tierAttestationUnitsForAccount,
402
+ denominator: IDefifaHook(_metadata.dataHook)
403
+ .getPastTierTotalAttestationUnitsOf({tier: _tierId, timestamp: timestamp})
404
+ });
191
405
  }
192
406
  }
193
407
  }
@@ -199,11 +413,16 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
199
413
  /// regardless of supply, each tier's community has equal influence — a tier with 1 token and a tier
200
414
  /// with 100 tokens both cap at MAX_ATTESTATION_POWER_TIER when fully attested. This prevents
201
415
  /// high-supply tiers from dominating governance, keeping the game fair across all outcomes.
416
+ /// @dev Note: quorum is computed from the live supply (currentSupplyOfTier) rather than a snapshot. This means
417
+ /// the quorum threshold can shift between when a scorecard is submitted and when it is evaluated, e.g. if
418
+ /// tokens are burned after attestation. In practice this is acceptable because attestation weights are
419
+ /// snapshotted and quorum only increases (new mints) or decreases (burns) — the latter makes ratification
420
+ /// easier, not harder.
202
421
  /// @return The quorum number of attestations.
203
422
  function quorum(uint256 gameId) public view override returns (uint256) {
204
423
  // Get the game's current funding cycle along with its metadata.
205
424
  // slither-disable-next-line unused-return
206
- (, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(gameId);
425
+ (, JBRulesetMetadata memory _metadata) = CONTROLLER.currentRulesetOf(gameId);
207
426
 
208
427
  // Get a reference to the number of tiers.
209
428
  uint256 _numberOfTiers = IDefifaHook(_metadata.dataHook).store().maxTierIdOf(_metadata.dataHook);
@@ -211,6 +430,7 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
211
430
  // Keep a reference to the total eligible tier weight.
212
431
  uint256 _eligibleTierWeights;
213
432
 
433
+ // slither-disable-next-line calls-inside-a-loop
214
434
  for (uint256 _i; _i < _numberOfTiers; _i++) {
215
435
  // Each minted tier contributes MAX_ATTESTATION_POWER_TIER to the quorum denominator.
216
436
  if (IDefifaHook(_metadata.dataHook).currentSupplyOfTier(_i + 1) != 0) {
@@ -262,244 +482,34 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
262
482
  }
263
483
 
264
484
  // If quorum has been reached, the state is SUCCEEDED, otherwise it is ACTIVE.
485
+ // Note: scorecards that fail to reach quorum remain ACTIVE indefinitely — there is no DEFEATED
486
+ // state transition for unratified scorecards. This is by design: new scorecards can always be
487
+ // submitted and the game's no-contest timeout (scorecardTimeout) provides the ultimate backstop.
265
488
  return quorum(gameId) <= _scorecardAttestationsOf[gameId][scorecardId].count
266
489
  ? DefifaScorecardState.SUCCEEDED
267
490
  : DefifaScorecardState.ACTIVE;
268
491
  }
269
492
 
270
493
  //*********************************************************************//
271
- // -------------------------- constructor ---------------------------- //
272
- //*********************************************************************//
273
-
274
- constructor(IJBController _controller, address _owner) Ownable(_owner) {
275
- controller = _controller;
276
- }
277
-
278
- //*********************************************************************//
279
- // ----------------------- public transactions ----------------------- //
280
- //*********************************************************************//
281
-
282
- /// @notice Initializes a game.
283
- /// @param _attestationStartTime The amount of time between a scorecard being submitted and attestations to it being
284
- /// enabled, measured in seconds. @param _attestationGracePeriod The amount of time that must go by before a
285
- /// scorecard can be ratified.
286
- function initializeGame(
287
- uint256 _gameId,
288
- uint256 _attestationStartTime,
289
- uint256 _attestationGracePeriod
290
- )
291
- public
292
- virtual
293
- override
294
- onlyOwner
295
- {
296
- // Set a default attestation start time if needed.
297
- if (_attestationStartTime == 0) _attestationStartTime = block.timestamp;
298
-
299
- // Enforce a minimum grace period of 1 day to prevent instant ratification.
300
- if (_attestationGracePeriod < 1 days) _attestationGracePeriod = 1 days;
301
-
302
- // Pack the values.
303
- uint256 _packed;
304
- // attestation start time in bits 0-47 (48 bits).
305
- _packed |= _attestationStartTime;
306
- // attestation grace period in bits 48-95 (48 bits).
307
- _packed |= _attestationGracePeriod << 48;
308
-
309
- // Store the packed value.
310
- _packedScorecardInfoOf[_gameId] = _packed;
311
-
312
- emit GameInitialized(_gameId, _attestationStartTime, _attestationGracePeriod, msg.sender);
313
- }
314
-
315
- //*********************************************************************//
316
- // ---------------------- external transactions ---------------------- //
317
- //*********************************************************************//
318
-
319
- /// @notice Attests to a scorecard.
320
- /// @param gameId The ID of the game to which the scorecard belongs.
321
- /// @param scorecardId The scorecard ID.
322
- /// @return weight The attestation weight that was applied.
323
- function attestToScorecardFrom(uint256 gameId, uint256 scorecardId) external override returns (uint256 weight) {
324
- // Get the game's current funding cycle along with its metadata.
325
- // slither-disable-next-line unused-return
326
- (, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(gameId);
327
-
328
- // Make sure the game is in its scoring phase.
329
- if (IDefifaHook(_metadata.dataHook).gamePhaseReporter().currentGamePhaseOf(gameId) != DefifaGamePhase.SCORING) {
330
- revert DefifaGovernor_NotAllowed();
331
- }
332
-
333
- // Keep a reference to the scorecard being attested to.
334
- DefifaScorecard storage _scorecard = _scorecardOf[gameId][scorecardId];
335
-
336
- // Keep a reference to the scorecard state.
337
- DefifaScorecardState _state = stateOf({gameId: gameId, scorecardId: scorecardId});
338
-
339
- if (_state != DefifaScorecardState.ACTIVE && _state != DefifaScorecardState.SUCCEEDED) {
340
- revert DefifaGovernor_NotAllowed();
341
- }
342
-
343
- // Keep a reference to the attestations for the scorecard.
344
- DefifaAttestations storage _attestations = _scorecardAttestationsOf[gameId][scorecardId];
345
-
346
- // Make sure the account isn't attesting to the same scorecard again.
347
- if (_attestations.hasAttested[msg.sender]) revert DefifaGovernor_AlreadyAttested();
348
-
349
- // Get a reference to the attestation weight.
350
- weight = getAttestationWeight({_gameId: gameId, _account: msg.sender, _timestamp: _scorecard.attestationsBegin});
351
-
352
- // Increase the attestation count.
353
- _attestations.count += weight;
354
-
355
- // Store the fact that the account has attested to the scorecard.
356
- _attestations.hasAttested[msg.sender] = true;
357
-
358
- emit ScorecardAttested(gameId, scorecardId, weight, msg.sender);
359
- }
360
-
361
- /// @notice Ratifies a scorecard that has been approved.
362
- /// @param gameId The ID of the game.
363
- /// @param tierWeights The weights of each tier in the approved scorecard.
364
- /// @return scorecardId The scorecard ID that was ratified.
365
- function ratifyScorecardFrom(
366
- uint256 gameId,
367
- DefifaTierCashOutWeight[] calldata tierWeights
368
- )
369
- external
370
- override
371
- returns (uint256 scorecardId)
372
- {
373
- // Make sure a scorecard hasn't been ratified yet.
374
- if (ratifiedScorecardIdOf[gameId] != 0) revert DefifaGovernor_AlreadyRatified();
375
-
376
- // Get the game's current funding cycle along with its metadata.
377
- // slither-disable-next-line unused-return
378
- (, JBRulesetMetadata memory _metadata) = controller.currentRulesetOf(gameId);
379
-
380
- // Build the calldata to the target
381
- bytes memory _calldata = _buildScorecardCalldataFor(tierWeights);
382
-
383
- // Attempt to execute the proposal.
384
- scorecardId = _hashScorecardOf({_gameHook: _metadata.dataHook, _calldata: _calldata});
385
-
386
- // Make sure the proposal being ratified has succeeded.
387
- if (stateOf({gameId: gameId, scorecardId: scorecardId}) != DefifaScorecardState.SUCCEEDED) {
388
- revert DefifaGovernor_NotAllowed();
389
- }
390
-
391
- // Set the ratified scorecard.
392
- ratifiedScorecardIdOf[gameId] = scorecardId;
393
-
394
- // Execute the scorecard via low-level call since the governor is the delegate's owner.
395
- (bool success, bytes memory returndata) = _metadata.dataHook.call(_calldata);
396
- // slither-disable-next-line unused-return
397
- Address.verifyCallResult({success: success, returndata: returndata});
398
-
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
- }
406
-
407
- emit ScorecardRatified(gameId, scorecardId, msg.sender);
408
- }
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
-
483
- //*********************************************************************//
484
- // ------------------------ internal functions ----------------------- //
494
+ // ----------------------- internal helpers -------------------------- //
485
495
  //*********************************************************************//
486
496
 
487
497
  /// @notice Build the normalized calldata.
488
- /// @param _tierWeights The weights of each tier in the scorecard data.
498
+ /// @param tierWeights The weights of each tier in the scorecard data.
489
499
  /// @return The calldata to send alongside the transactions.
490
- function _buildScorecardCalldataFor(DefifaTierCashOutWeight[] calldata _tierWeights)
500
+ function _buildScorecardCalldataFor(DefifaTierCashOutWeight[] calldata tierWeights)
491
501
  internal
492
502
  pure
493
503
  returns (bytes memory)
494
504
  {
495
505
  // Build the calldata from the tier weights.
496
- return abi.encodeWithSelector(DefifaHook.setTierCashOutWeightsTo.selector, (_tierWeights));
506
+ return abi.encodeWithSelector(DefifaHook.setTierCashOutWeightsTo.selector, (tierWeights));
497
507
  }
498
508
 
499
509
  /// @notice A value representing the contents of a scorecard.
500
- /// @param _gameHook The address where the game is being played.
501
- /// @param _calldata The calldata that will be sent if the scorecard is ratified.
502
- function _hashScorecardOf(address _gameHook, bytes memory _calldata) internal pure virtual returns (uint256) {
503
- return uint256(keccak256(abi.encode(_gameHook, _calldata)));
510
+ /// @param gameHook The address where the game is being played.
511
+ /// @param calldataBytes The calldata that will be sent if the scorecard is ratified.
512
+ function _hashScorecardOf(address gameHook, bytes memory calldataBytes) internal pure virtual returns (uint256) {
513
+ return uint256(keccak256(abi.encode(gameHook, calldataBytes)));
504
514
  }
505
515
  }