@ballkidz/defifa 0.0.12 → 0.0.13
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.
- package/CHANGE_LOG.md +60 -5
- package/CRYPTO_ECON.md +505 -270
- package/CRYPTO_ECON.pdf +0 -0
- package/CRYPTO_ECON.tex +438 -241
- package/RISKS.md +9 -1
- package/SKILLS.md +3 -2
- package/package.json +6 -6
- package/src/DefifaDeployer.sol +128 -130
- package/src/DefifaGovernor.sol +278 -83
- package/src/DefifaHook.sol +158 -171
- package/src/enums/DefifaScorecardState.sol +1 -0
- package/src/interfaces/IDefifaGovernor.sol +41 -2
- package/src/libraries/DefifaHookLib.sol +69 -62
- package/src/structs/DefifaAttestations.sol +3 -3
- package/src/structs/DefifaLaunchProjectData.sol +1 -0
- package/src/structs/DefifaScorecard.sol +2 -0
- package/test/BWAFunctionComparison.t.sol +1320 -0
- package/test/DefifaAdversarialQuorum.t.sol +52 -37
- package/test/DefifaAuditLowGuards.t.sol +9 -5
- package/test/DefifaFeeAccounting.t.sol +2 -1
- package/test/DefifaGovernanceHardening.t.sol +1311 -0
- package/test/DefifaGovernor.t.sol +4 -2
- package/test/DefifaHookRegressions.t.sol +2 -1
- package/test/DefifaMintCostInvariant.t.sol +2 -1
- package/test/DefifaNoContest.t.sol +3 -2
- package/test/DefifaSecurity.t.sol +54 -41
- package/test/DefifaUSDC.t.sol +3 -2
- package/test/Fork.t.sol +11 -12
- package/test/TestAuditGaps.sol +6 -4
- package/test/TestQALastMile.t.sol +4 -2
- package/test/audit/{CodexAttestationDoubleCount.t.sol → AttestationDoubleCount.t.sol} +3 -2
- package/test/audit/FixPendingReserveDilution.t.sol +366 -0
- package/test/audit/PendingReserveDilution.t.sol +298 -0
- package/test/audit/PendingReserveQuorumGrief.t.sol +355 -0
- package/test/regression/AttestationDelegateBeneficiary.t.sol +2 -1
- package/test/regression/FulfillmentBlocksRatification.t.sol +2 -1
- package/test/regression/GracePeriodBypass.t.sol +2 -1
package/src/DefifaGovernor.sol
CHANGED
|
@@ -12,6 +12,8 @@ import {DefifaGamePhase} from "./enums/DefifaGamePhase.sol";
|
|
|
12
12
|
import {DefifaScorecardState} from "./enums/DefifaScorecardState.sol";
|
|
13
13
|
import {IDefifaDeployer} from "./interfaces/IDefifaDeployer.sol";
|
|
14
14
|
import {IDefifaGovernor} from "./interfaces/IDefifaGovernor.sol";
|
|
15
|
+
import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
|
|
16
|
+
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
15
17
|
import {IDefifaHook} from "./interfaces/IDefifaHook.sol";
|
|
16
18
|
import {DefifaAttestations} from "./structs/DefifaAttestations.sol";
|
|
17
19
|
import {DefifaScorecard} from "./structs/DefifaScorecard.sol";
|
|
@@ -30,6 +32,7 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
30
32
|
error DefifaGovernor_GameNotFound();
|
|
31
33
|
error DefifaGovernor_IncorrectTierOrder();
|
|
32
34
|
error DefifaGovernor_NotAllowed();
|
|
35
|
+
error DefifaGovernor_NotAttested();
|
|
33
36
|
error DefifaGovernor_Uint48Overflow();
|
|
34
37
|
error DefifaGovernor_UnknownProposal();
|
|
35
38
|
error DefifaGovernor_UnownedProposedCashoutValue();
|
|
@@ -65,6 +68,7 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
65
68
|
//*********************************************************************//
|
|
66
69
|
|
|
67
70
|
/// @notice The scorecard information, packed into a uint256.
|
|
71
|
+
/// @dev Bits 0-47: attestationStartTime, bits 48-95: attestationGracePeriod, bits 96-143: timelockDuration.
|
|
68
72
|
/// @custom:param gameId The ID of the game for which the scorecard info applies.
|
|
69
73
|
mapping(uint256 => uint256) internal _packedScorecardInfoOf;
|
|
70
74
|
|
|
@@ -78,6 +82,10 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
78
82
|
/// @custom:param scorecardId The ID of the scorecard that has been attested to.
|
|
79
83
|
mapping(uint256 => mapping(uint256 => DefifaAttestations)) internal _scorecardAttestationsOf;
|
|
80
84
|
|
|
85
|
+
/// @notice Tier weights per scorecard for BWA computation.
|
|
86
|
+
/// @dev Maps gameId => scorecardId => tierId (0-indexed) => cashOutWeight.
|
|
87
|
+
mapping(uint256 => mapping(uint256 => mapping(uint256 => uint256))) internal _scorecardTierWeightsOf;
|
|
88
|
+
|
|
81
89
|
//*********************************************************************//
|
|
82
90
|
// -------------------------- constructor ---------------------------- //
|
|
83
91
|
//*********************************************************************//
|
|
@@ -97,37 +105,46 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
97
105
|
function attestToScorecardFrom(uint256 gameId, uint256 scorecardId) external override returns (uint256 weight) {
|
|
98
106
|
// Get the game's current funding cycle along with its metadata.
|
|
99
107
|
// slither-disable-next-line unused-return
|
|
100
|
-
(, JBRulesetMetadata memory
|
|
108
|
+
(, JBRulesetMetadata memory metadata) = CONTROLLER.currentRulesetOf(gameId);
|
|
101
109
|
|
|
102
110
|
// Make sure the game is in its scoring phase.
|
|
103
|
-
if (IDefifaHook(
|
|
111
|
+
if (IDefifaHook(metadata.dataHook).gamePhaseReporter().currentGamePhaseOf(gameId) != DefifaGamePhase.SCORING) {
|
|
104
112
|
revert DefifaGovernor_NotAllowed();
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
// Keep a reference to the scorecard being attested to.
|
|
108
|
-
DefifaScorecard storage
|
|
116
|
+
DefifaScorecard storage scorecard = _scorecardOf[gameId][scorecardId];
|
|
109
117
|
|
|
110
118
|
// Keep a reference to the scorecard state.
|
|
111
|
-
DefifaScorecardState
|
|
119
|
+
DefifaScorecardState state = stateOf({gameId: gameId, scorecardId: scorecardId});
|
|
112
120
|
|
|
113
|
-
if (
|
|
121
|
+
if (
|
|
122
|
+
state != DefifaScorecardState.ACTIVE && state != DefifaScorecardState.SUCCEEDED
|
|
123
|
+
&& state != DefifaScorecardState.QUEUED
|
|
124
|
+
) {
|
|
114
125
|
revert DefifaGovernor_NotAllowed();
|
|
115
126
|
}
|
|
116
127
|
|
|
117
128
|
// Keep a reference to the attestations for the scorecard.
|
|
118
|
-
DefifaAttestations storage
|
|
129
|
+
DefifaAttestations storage attestations = _scorecardAttestationsOf[gameId][scorecardId];
|
|
119
130
|
|
|
120
131
|
// Make sure the account isn't attesting to the same scorecard again.
|
|
121
|
-
if (
|
|
132
|
+
if (attestations.attestedWeightOf[msg.sender] != 0) revert DefifaGovernor_AlreadyAttested();
|
|
133
|
+
|
|
134
|
+
// Get a reference to the BWA-adjusted attestation weight, snapshotted at `attestationsBegin`.
|
|
135
|
+
weight = getBWAAttestationWeight({
|
|
136
|
+
gameId: gameId, scorecardId: scorecardId, account: msg.sender, timestamp: scorecard.attestationsBegin
|
|
137
|
+
});
|
|
122
138
|
|
|
123
|
-
//
|
|
124
|
-
|
|
139
|
+
// Revert if BWA reduces this account's power to zero (e.g. 100% beneficiary of the scorecard).
|
|
140
|
+
// Without this guard, zero-weight attestors could call repeatedly since attestedWeightOf stays 0.
|
|
141
|
+
if (weight == 0) revert DefifaGovernor_NotAllowed();
|
|
125
142
|
|
|
126
143
|
// Increase the attestation count.
|
|
127
|
-
|
|
144
|
+
attestations.count += weight;
|
|
128
145
|
|
|
129
|
-
// Store the
|
|
130
|
-
|
|
146
|
+
// Store the BWA weight that was added (used for accurate subtraction on revoke).
|
|
147
|
+
attestations.attestedWeightOf[msg.sender] = weight;
|
|
131
148
|
|
|
132
149
|
emit ScorecardAttested(gameId, scorecardId, weight, msg.sender);
|
|
133
150
|
}
|
|
@@ -149,13 +166,13 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
149
166
|
|
|
150
167
|
// Get the game's current funding cycle along with its metadata.
|
|
151
168
|
// slither-disable-next-line unused-return
|
|
152
|
-
(, JBRulesetMetadata memory
|
|
169
|
+
(, JBRulesetMetadata memory metadata) = CONTROLLER.currentRulesetOf(gameId);
|
|
153
170
|
|
|
154
171
|
// Build the calldata to the target
|
|
155
|
-
bytes memory
|
|
172
|
+
bytes memory scorecardCalldata = _buildScorecardCalldataFor(tierWeights);
|
|
156
173
|
|
|
157
174
|
// Attempt to execute the proposal.
|
|
158
|
-
scorecardId = _hashScorecardOf({gameHook:
|
|
175
|
+
scorecardId = _hashScorecardOf({gameHook: metadata.dataHook, calldataBytes: scorecardCalldata});
|
|
159
176
|
|
|
160
177
|
// Make sure the proposal being ratified has succeeded.
|
|
161
178
|
if (stateOf({gameId: gameId, scorecardId: scorecardId}) != DefifaScorecardState.SUCCEEDED) {
|
|
@@ -166,7 +183,7 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
166
183
|
ratifiedScorecardIdOf[gameId] = scorecardId;
|
|
167
184
|
|
|
168
185
|
// Execute the scorecard via low-level call since the governor is the delegate's owner.
|
|
169
|
-
(bool success, bytes memory returndata) =
|
|
186
|
+
(bool success, bytes memory returndata) = metadata.dataHook.call(scorecardCalldata);
|
|
170
187
|
// slither-disable-next-line unused-return
|
|
171
188
|
Address.verifyCallResult({success: success, returndata: returndata});
|
|
172
189
|
|
|
@@ -178,6 +195,28 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
178
195
|
emit ScorecardRatified(gameId, scorecardId, msg.sender);
|
|
179
196
|
}
|
|
180
197
|
|
|
198
|
+
/// @notice Revoke a previously submitted attestation. Only allowed during the ACTIVE phase.
|
|
199
|
+
/// @dev Once a scorecard enters QUEUED (grace period ended + quorum met), revocations are disabled.
|
|
200
|
+
/// This prevents the griefing loop (attest/revoke cycling) while allowing corrective action during debate.
|
|
201
|
+
/// @param gameId The ID of the game.
|
|
202
|
+
/// @param scorecardId The ID of the scorecard to revoke attestation from.
|
|
203
|
+
function revokeAttestationFrom(uint256 gameId, uint256 scorecardId) external virtual override {
|
|
204
|
+
// Only allow revocation during ACTIVE phase.
|
|
205
|
+
if (stateOf(gameId, scorecardId) != DefifaScorecardState.ACTIVE) revert DefifaGovernor_NotAllowed();
|
|
206
|
+
|
|
207
|
+
DefifaAttestations storage attestations = _scorecardAttestationsOf[gameId][scorecardId];
|
|
208
|
+
uint256 weight = attestations.attestedWeightOf[msg.sender];
|
|
209
|
+
|
|
210
|
+
// Must have previously attested.
|
|
211
|
+
if (weight == 0) revert DefifaGovernor_NotAttested();
|
|
212
|
+
|
|
213
|
+
// Subtract the weight and clear the attestation.
|
|
214
|
+
attestations.count -= weight;
|
|
215
|
+
attestations.attestedWeightOf[msg.sender] = 0;
|
|
216
|
+
|
|
217
|
+
emit AttestationRevoked(gameId, scorecardId, msg.sender, weight);
|
|
218
|
+
}
|
|
219
|
+
|
|
181
220
|
/// @notice Submits a scorecard to be attested to.
|
|
182
221
|
/// @param gameId The ID of the game.
|
|
183
222
|
/// @param tierWeights The weights of each tier in the scorecard.
|
|
@@ -198,22 +237,22 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
198
237
|
if (_packedScorecardInfoOf[gameId] == 0) revert DefifaGovernor_GameNotFound();
|
|
199
238
|
|
|
200
239
|
// Make sure no weight is assigned to an unowned tier.
|
|
201
|
-
uint256
|
|
240
|
+
uint256 numberOfTierWeights = tierWeights.length;
|
|
202
241
|
|
|
203
242
|
// Get the game's current funding cycle along with its metadata.
|
|
204
243
|
// slither-disable-next-line unused-return
|
|
205
|
-
(, JBRulesetMetadata memory
|
|
244
|
+
(, JBRulesetMetadata memory metadata) = CONTROLLER.currentRulesetOf(gameId);
|
|
206
245
|
|
|
207
246
|
// Make sure the game is in its scoring phase.
|
|
208
|
-
if (IDefifaHook(
|
|
247
|
+
if (IDefifaHook(metadata.dataHook).gamePhaseReporter().currentGamePhaseOf(gameId) != DefifaGamePhase.SCORING) {
|
|
209
248
|
revert DefifaGovernor_NotAllowed();
|
|
210
249
|
}
|
|
211
250
|
|
|
212
251
|
// If there's a weight assigned to the tier, make sure there is a token backed by it.
|
|
213
|
-
for (uint256
|
|
252
|
+
for (uint256 i; i < numberOfTierWeights; i++) {
|
|
214
253
|
if (
|
|
215
|
-
tierWeights[
|
|
216
|
-
&& IDefifaHook(
|
|
254
|
+
tierWeights[i].cashOutWeight > 0
|
|
255
|
+
&& IDefifaHook(metadata.dataHook).currentSupplyOfTier(tierWeights[i].id) == 0
|
|
217
256
|
) {
|
|
218
257
|
revert DefifaGovernor_UnownedProposedCashoutValue();
|
|
219
258
|
}
|
|
@@ -221,34 +260,69 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
221
260
|
|
|
222
261
|
// Hash the scorecard.
|
|
223
262
|
scorecardId =
|
|
224
|
-
_hashScorecardOf({gameHook:
|
|
263
|
+
_hashScorecardOf({gameHook: metadata.dataHook, calldataBytes: _buildScorecardCalldataFor(tierWeights)});
|
|
225
264
|
|
|
226
265
|
// Store the scorecard
|
|
227
|
-
DefifaScorecard storage
|
|
228
|
-
if (
|
|
266
|
+
DefifaScorecard storage scorecard = _scorecardOf[gameId][scorecardId];
|
|
267
|
+
if (scorecard.attestationsBegin != 0) revert DefifaGovernor_DuplicateScorecard();
|
|
229
268
|
|
|
230
|
-
uint256
|
|
231
|
-
uint256
|
|
232
|
-
block.timestamp >
|
|
269
|
+
uint256 attestationStartTime = attestationStartTimeOf(gameId);
|
|
270
|
+
uint256 timeUntilAttestationsBegin =
|
|
271
|
+
block.timestamp > attestationStartTime ? 0 : attestationStartTime - block.timestamp;
|
|
233
272
|
|
|
234
273
|
// Casting to uint48 is safe because block.timestamp fits in uint48 until year 8921556.
|
|
235
274
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
236
|
-
uint48
|
|
237
|
-
|
|
275
|
+
uint48 attestationsBegin = uint48(block.timestamp + timeUntilAttestationsBegin);
|
|
276
|
+
scorecard.attestationsBegin = attestationsBegin;
|
|
238
277
|
// Grace period extends from when attestations begin, not from submission time.
|
|
239
278
|
// This prevents the grace period from expiring before attestations even start
|
|
240
279
|
// when a scorecard is submitted early.
|
|
241
|
-
|
|
280
|
+
scorecard.gracePeriodEnds = uint48(attestationsBegin + attestationGracePeriodOf(gameId));
|
|
281
|
+
|
|
282
|
+
// Store tier weights for BWA computation.
|
|
283
|
+
for (uint256 i; i < numberOfTierWeights; i++) {
|
|
284
|
+
_scorecardTierWeightsOf[gameId][scorecardId][tierWeights[i].id - 1] = tierWeights[i].cashOutWeight;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Concentration-adjusted quorum: penalty = headroom * maxShare².
|
|
288
|
+
// Headroom = max achievable BWA - base quorum = (N-2) * MAX / 2.
|
|
289
|
+
// This is the gap honest attestors can fill above base quorum.
|
|
290
|
+
// maxShare² is nonlinear: gentle for moderate concentration, steep for extreme.
|
|
291
|
+
// The penalty can never exceed headroom, so quorum is always reachable by non-beneficiaries.
|
|
292
|
+
uint256 baseQuorum = quorum(gameId);
|
|
293
|
+
uint256 adjustedQuorum = baseQuorum;
|
|
294
|
+
|
|
295
|
+
if (baseQuorum >= MAX_ATTESTATION_POWER_TIER) {
|
|
296
|
+
// headroom = maxBWA - baseQuorum = (N-1)*MAX - N*MAX/2 = (N-2)*MAX/2.
|
|
297
|
+
uint256 headroom = baseQuorum - MAX_ATTESTATION_POWER_TIER;
|
|
298
|
+
// Subtract numberOfTierWeights to account for per-tier mulDiv truncation in BWA.
|
|
299
|
+
if (headroom > numberOfTierWeights) headroom -= numberOfTierWeights;
|
|
300
|
+
|
|
301
|
+
// Find the largest tier weight.
|
|
302
|
+
uint256 totalCashOutWeight = IDefifaHook(metadata.dataHook).TOTAL_CASHOUT_WEIGHT();
|
|
303
|
+
uint256 maxWeight;
|
|
304
|
+
for (uint256 i; i < numberOfTierWeights; i++) {
|
|
305
|
+
if (tierWeights[i].cashOutWeight > maxWeight) maxWeight = tierWeights[i].cashOutWeight;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// maxShare² in totalCashOutWeight scale (nonlinear: gentle for moderate, steep for extreme).
|
|
309
|
+
uint256 maxShareSquared = mulDiv(maxWeight, maxWeight, totalCashOutWeight);
|
|
310
|
+
|
|
311
|
+
// Penalty fills headroom proportional to concentration².
|
|
312
|
+
adjustedQuorum += mulDiv(headroom, maxShareSquared, totalCashOutWeight);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
scorecard.quorumSnapshot = adjustedQuorum;
|
|
242
316
|
|
|
243
317
|
// Keep a reference to the default attestation delegate.
|
|
244
|
-
address
|
|
318
|
+
address defaultAttestationDelegate = IDefifaHook(metadata.dataHook).defaultAttestationDelegate();
|
|
245
319
|
|
|
246
320
|
// If the scorecard is being sent from the default attestation delegate, store it.
|
|
247
|
-
if (msg.sender ==
|
|
321
|
+
if (msg.sender == defaultAttestationDelegate) {
|
|
248
322
|
defaultAttestationDelegateProposalOf[gameId] = scorecardId;
|
|
249
323
|
}
|
|
250
324
|
|
|
251
|
-
emit ScorecardSubmitted(gameId, scorecardId, tierWeights, msg.sender ==
|
|
325
|
+
emit ScorecardSubmitted(gameId, scorecardId, tierWeights, msg.sender == defaultAttestationDelegate, msg.sender);
|
|
252
326
|
}
|
|
253
327
|
|
|
254
328
|
//*********************************************************************//
|
|
@@ -269,7 +343,7 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
269
343
|
/// @param account The address to check the attestation status of.
|
|
270
344
|
/// @return A flag indicating if the given account has already attested to the scorecard.
|
|
271
345
|
function hasAttestedTo(uint256 gameId, uint256 scorecardId, address account) external view returns (bool) {
|
|
272
|
-
return _scorecardAttestationsOf[gameId][scorecardId].
|
|
346
|
+
return _scorecardAttestationsOf[gameId][scorecardId].attestedWeightOf[account] != 0;
|
|
273
347
|
}
|
|
274
348
|
|
|
275
349
|
/// @notice The ID of a scorecard representing the provided tier weights.
|
|
@@ -297,10 +371,12 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
297
371
|
/// @param attestationStartTime The amount of time between a scorecard being submitted and attestations to it being
|
|
298
372
|
/// enabled, measured in seconds.
|
|
299
373
|
/// @param attestationGracePeriod The amount of time that must go by before a scorecard can be ratified.
|
|
374
|
+
/// @param timelockDuration The cooling period after quorum is met before a scorecard can be ratified.
|
|
300
375
|
function initializeGame(
|
|
301
376
|
uint256 gameId,
|
|
302
377
|
uint256 attestationStartTime,
|
|
303
|
-
uint256 attestationGracePeriod
|
|
378
|
+
uint256 attestationGracePeriod,
|
|
379
|
+
uint256 timelockDuration
|
|
304
380
|
)
|
|
305
381
|
public
|
|
306
382
|
virtual
|
|
@@ -319,18 +395,21 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
319
395
|
// Ensure values fit within their allocated 48-bit widths before packing.
|
|
320
396
|
if (attestationStartTime > type(uint48).max) revert DefifaGovernor_Uint48Overflow();
|
|
321
397
|
if (attestationGracePeriod > type(uint48).max) revert DefifaGovernor_Uint48Overflow();
|
|
398
|
+
if (timelockDuration > type(uint48).max) revert DefifaGovernor_Uint48Overflow();
|
|
322
399
|
|
|
323
400
|
// Pack the values.
|
|
324
|
-
uint256
|
|
401
|
+
uint256 packed;
|
|
325
402
|
// attestation start time in bits 0-47 (48 bits).
|
|
326
|
-
|
|
403
|
+
packed |= attestationStartTime;
|
|
327
404
|
// attestation grace period in bits 48-95 (48 bits).
|
|
328
|
-
|
|
405
|
+
packed |= attestationGracePeriod << 48;
|
|
406
|
+
// timelock duration in bits 96-143 (48 bits).
|
|
407
|
+
packed |= timelockDuration << 96;
|
|
329
408
|
|
|
330
409
|
// Store the packed value.
|
|
331
|
-
_packedScorecardInfoOf[gameId] =
|
|
410
|
+
_packedScorecardInfoOf[gameId] = packed;
|
|
332
411
|
|
|
333
|
-
emit GameInitialized(gameId, attestationStartTime, attestationGracePeriod, msg.sender);
|
|
412
|
+
emit GameInitialized(gameId, attestationStartTime, attestationGracePeriod, timelockDuration, msg.sender);
|
|
334
413
|
}
|
|
335
414
|
|
|
336
415
|
//*********************************************************************//
|
|
@@ -379,68 +458,170 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
379
458
|
{
|
|
380
459
|
// Get the game's current funding cycle along with its metadata.
|
|
381
460
|
// slither-disable-next-line unused-return
|
|
382
|
-
(, JBRulesetMetadata memory
|
|
461
|
+
(, JBRulesetMetadata memory metadata) = CONTROLLER.currentRulesetOf(gameId);
|
|
462
|
+
|
|
463
|
+
// Get a reference to the hook and its store.
|
|
464
|
+
IDefifaHook hook = IDefifaHook(metadata.dataHook);
|
|
465
|
+
IJB721TiersHookStore store = hook.store();
|
|
383
466
|
|
|
384
467
|
// Get a reference to the number of tiers.
|
|
385
|
-
uint256
|
|
468
|
+
uint256 numberOfTiers = store.maxTierIdOf(metadata.dataHook);
|
|
386
469
|
|
|
387
470
|
// slither-disable-next-line calls-inside-a-loop
|
|
388
|
-
for (uint256
|
|
471
|
+
for (uint256 i; i < numberOfTiers; i++) {
|
|
389
472
|
// Tiers are 1-indexed.
|
|
390
|
-
uint256
|
|
473
|
+
uint256 tierId = i + 1;
|
|
391
474
|
|
|
392
475
|
// Get this account's attestation units within the tier (snapshot at timestamp).
|
|
393
|
-
uint256
|
|
394
|
-
.getPastTierAttestationUnitsOf({account: account, tier:
|
|
476
|
+
uint256 tierAttestationUnitsForAccount =
|
|
477
|
+
hook.getPastTierAttestationUnitsOf({account: account, tier: tierId, timestamp: timestamp});
|
|
478
|
+
|
|
479
|
+
// Get the total attestation units for this tier (snapshot at timestamp).
|
|
480
|
+
uint256 tierTotalAttestationUnits =
|
|
481
|
+
hook.getPastTierTotalAttestationUnitsOf({tier: tierId, timestamp: timestamp});
|
|
482
|
+
|
|
483
|
+
// Include unminted pending reserves in the total (denominator only). This ensures every
|
|
484
|
+
// token holder's voting power already accounts for reserves that will eventually be minted.
|
|
485
|
+
// When the reserve beneficiary later mints, their new NFTs add to the numerator while
|
|
486
|
+
// pending reserves decrease by the same amount — so no one's voting power shifts.
|
|
487
|
+
{
|
|
488
|
+
uint256 pendingReserves = store.numberOfPendingReservesFor(metadata.dataHook, tierId);
|
|
489
|
+
if (pendingReserves != 0) {
|
|
490
|
+
JB721Tier memory tier =
|
|
491
|
+
store.tierOf({hook: metadata.dataHook, id: tierId, includeResolvedUri: false});
|
|
492
|
+
tierTotalAttestationUnits += pendingReserves * tier.votingUnits;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
395
495
|
|
|
396
496
|
// Scale the account's share of the tier to MAX_ATTESTATION_POWER_TIER.
|
|
397
497
|
// e.g. holding 3 of 10 tokens → 3/10 * MAX_ATTESTATION_POWER_TIER attestation power from this tier.
|
|
398
498
|
unchecked {
|
|
399
|
-
if (
|
|
499
|
+
if (tierAttestationUnitsForAccount != 0) {
|
|
400
500
|
attestationPower += mulDiv({
|
|
401
501
|
x: MAX_ATTESTATION_POWER_TIER,
|
|
402
|
-
y:
|
|
403
|
-
denominator:
|
|
404
|
-
.getPastTierTotalAttestationUnitsOf({tier: _tierId, timestamp: timestamp})
|
|
502
|
+
y: tierAttestationUnitsForAccount,
|
|
503
|
+
denominator: tierTotalAttestationUnits
|
|
405
504
|
});
|
|
406
505
|
}
|
|
407
506
|
}
|
|
408
507
|
}
|
|
409
508
|
}
|
|
410
509
|
|
|
510
|
+
/// @notice Gets an account's BWA-adjusted attestation power relative to a specific scorecard.
|
|
511
|
+
/// @dev BWA (Benefit-Weighted Attestation) reduces a tier's attestation power by how much
|
|
512
|
+
/// that tier benefits from the scorecard. Power is reduced by `(tierWeight / totalCashOutWeight)`.
|
|
513
|
+
/// This means a tier with 100% of the scorecard weight gets 0 attestation power for that scorecard,
|
|
514
|
+
/// while a tier with 0% weight retains full power. This prevents beneficiaries from self-attesting.
|
|
515
|
+
/// @param gameId The ID of the game.
|
|
516
|
+
/// @param scorecardId The ID of the scorecard (determines tier weight lookup).
|
|
517
|
+
/// @param account The account to compute BWA power for.
|
|
518
|
+
/// @param timestamp The snapshot timestamp.
|
|
519
|
+
/// @return bwaAttestationPower The BWA-adjusted attestation power.
|
|
520
|
+
function getBWAAttestationWeight(
|
|
521
|
+
uint256 gameId,
|
|
522
|
+
uint256 scorecardId,
|
|
523
|
+
address account,
|
|
524
|
+
uint48 timestamp
|
|
525
|
+
)
|
|
526
|
+
public
|
|
527
|
+
view
|
|
528
|
+
virtual
|
|
529
|
+
override
|
|
530
|
+
returns (uint256 bwaAttestationPower)
|
|
531
|
+
{
|
|
532
|
+
// Get the game's current funding cycle along with its metadata.
|
|
533
|
+
// slither-disable-next-line unused-return
|
|
534
|
+
(, JBRulesetMetadata memory metadata) = CONTROLLER.currentRulesetOf(gameId);
|
|
535
|
+
|
|
536
|
+
// Get a reference to the hook and its store.
|
|
537
|
+
IDefifaHook hook = IDefifaHook(metadata.dataHook);
|
|
538
|
+
IJB721TiersHookStore store = hook.store();
|
|
539
|
+
|
|
540
|
+
// Get a reference to the number of tiers.
|
|
541
|
+
uint256 numberOfTiers = store.maxTierIdOf(metadata.dataHook);
|
|
542
|
+
|
|
543
|
+
// Cache the total cashout weight denominator from the hook.
|
|
544
|
+
uint256 totalCashOutWeight = hook.TOTAL_CASHOUT_WEIGHT();
|
|
545
|
+
|
|
546
|
+
// slither-disable-next-line calls-inside-a-loop
|
|
547
|
+
for (uint256 i; i < numberOfTiers; i++) {
|
|
548
|
+
// Tiers are 1-indexed.
|
|
549
|
+
uint256 tierId = i + 1;
|
|
550
|
+
|
|
551
|
+
// Get this account's attestation units within the tier (snapshot at timestamp).
|
|
552
|
+
uint256 tierAttestationUnitsForAccount =
|
|
553
|
+
hook.getPastTierAttestationUnitsOf({account: account, tier: tierId, timestamp: timestamp});
|
|
554
|
+
|
|
555
|
+
if (tierAttestationUnitsForAccount != 0) {
|
|
556
|
+
// Get the total attestation units for this tier (snapshot at timestamp).
|
|
557
|
+
uint256 tierTotalAttestationUnits =
|
|
558
|
+
hook.getPastTierTotalAttestationUnitsOf({tier: tierId, timestamp: timestamp});
|
|
559
|
+
|
|
560
|
+
// Include unminted pending reserves in the total (denominator only).
|
|
561
|
+
{
|
|
562
|
+
uint256 pendingReserves = store.numberOfPendingReservesFor(metadata.dataHook, tierId);
|
|
563
|
+
if (pendingReserves != 0) {
|
|
564
|
+
JB721Tier memory tier =
|
|
565
|
+
store.tierOf({hook: metadata.dataHook, id: tierId, includeResolvedUri: false});
|
|
566
|
+
tierTotalAttestationUnits += pendingReserves * tier.votingUnits;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Raw power for this tier.
|
|
571
|
+
uint256 rawPower =
|
|
572
|
+
mulDiv(MAX_ATTESTATION_POWER_TIER, tierAttestationUnitsForAccount, tierTotalAttestationUnits);
|
|
573
|
+
|
|
574
|
+
// BWA reduction: power * (1 - tierWeight / totalCashOutWeight).
|
|
575
|
+
uint256 tierWeight = _scorecardTierWeightsOf[gameId][scorecardId][i];
|
|
576
|
+
uint256 bwaMultiplier = totalCashOutWeight - tierWeight;
|
|
577
|
+
|
|
578
|
+
bwaAttestationPower += mulDiv(rawPower, bwaMultiplier, totalCashOutWeight);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
411
583
|
/// @notice The number of attestation units that must have participated in a proposal for it to be ratified.
|
|
412
|
-
/// @dev Each tier with
|
|
413
|
-
///
|
|
414
|
-
///
|
|
415
|
-
///
|
|
416
|
-
/// high-supply tiers from dominating governance
|
|
417
|
-
/// @dev
|
|
418
|
-
///
|
|
419
|
-
/// tokens are burned after attestation. In practice this is acceptable because attestation weights are
|
|
420
|
-
/// snapshotted and quorum only increases (new mints) or decreases (burns) — the latter makes ratification
|
|
421
|
-
/// easier, not harder.
|
|
584
|
+
/// @dev Each tier with participation contributes MAX_ATTESTATION_POWER_TIER to the total eligible weight.
|
|
585
|
+
/// A tier counts as "participated" if it has circulating tokens OR unminted pending reserves — the latter
|
|
586
|
+
/// means mints occurred (triggering reserve accrual) even if all paid tokens were later burned during REFUND.
|
|
587
|
+
/// Quorum is 50% of this total. Because every tier has equal max attestation power regardless of supply,
|
|
588
|
+
/// each tier's community has equal influence. This prevents high-supply tiers from dominating governance.
|
|
589
|
+
/// @dev No snapshot needed: during SCORING, supply is frozen (no new paid mints, no burns). Reserve minting
|
|
590
|
+
/// doesn't change which tiers are counted because tiers with pending reserves are already included.
|
|
422
591
|
/// @return The quorum number of attestations.
|
|
423
592
|
function quorum(uint256 gameId) public view override returns (uint256) {
|
|
424
593
|
// Get the game's current funding cycle along with its metadata.
|
|
425
594
|
// slither-disable-next-line unused-return
|
|
426
|
-
(, JBRulesetMetadata memory
|
|
595
|
+
(, JBRulesetMetadata memory metadata) = CONTROLLER.currentRulesetOf(gameId);
|
|
596
|
+
|
|
597
|
+
// Get a reference to the hook and its store.
|
|
598
|
+
IDefifaHook hook = IDefifaHook(metadata.dataHook);
|
|
599
|
+
IJB721TiersHookStore store = hook.store();
|
|
427
600
|
|
|
428
601
|
// Get a reference to the number of tiers.
|
|
429
|
-
uint256
|
|
602
|
+
uint256 numberOfTiers = store.maxTierIdOf(metadata.dataHook);
|
|
430
603
|
|
|
431
604
|
// Keep a reference to the total eligible tier weight.
|
|
432
|
-
uint256
|
|
605
|
+
uint256 eligibleTierWeights;
|
|
433
606
|
|
|
434
607
|
// slither-disable-next-line calls-inside-a-loop
|
|
435
|
-
for (uint256
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
608
|
+
for (uint256 i; i < numberOfTiers; i++) {
|
|
609
|
+
uint256 tierId = i + 1;
|
|
610
|
+
|
|
611
|
+
// A tier contributes to quorum if it has circulating tokens OR unminted pending reserves.
|
|
612
|
+
// Pending reserves exist when participation occurred (mints triggered reserve accrual),
|
|
613
|
+
// even if all paid tokens were later burned during REFUND. The reserve beneficiary still
|
|
614
|
+
// has a stake in that tier's outcome, so the tier should count toward governance quorum.
|
|
615
|
+
if (
|
|
616
|
+
hook.currentSupplyOfTier(tierId) != 0
|
|
617
|
+
|| store.numberOfPendingReservesFor(metadata.dataHook, tierId) != 0
|
|
618
|
+
) {
|
|
619
|
+
eligibleTierWeights += MAX_ATTESTATION_POWER_TIER;
|
|
439
620
|
}
|
|
440
621
|
}
|
|
441
622
|
|
|
442
|
-
// Quorum = 50% of all
|
|
443
|
-
return
|
|
623
|
+
// Quorum = 50% of all participated tiers' attestation power.
|
|
624
|
+
return eligibleTierWeights / 2;
|
|
444
625
|
}
|
|
445
626
|
|
|
446
627
|
/// @notice The state of a proposal.
|
|
@@ -450,45 +631,59 @@ contract DefifaGovernor is Ownable, IDefifaGovernor {
|
|
|
450
631
|
/// @dev Boundary semantics (inclusive):
|
|
451
632
|
/// - At exactly `attestationsBegin`, the state transitions from PENDING to ACTIVE (attestations are open).
|
|
452
633
|
/// - At exactly `gracePeriodEnds`, the grace period has elapsed and the state transitions from ACTIVE to
|
|
453
|
-
///
|
|
634
|
+
/// QUEUED (if quorum met + timelock > 0) or SUCCEEDED (if quorum met + no timelock).
|
|
454
635
|
function stateOf(uint256 gameId, uint256 scorecardId) public view virtual override returns (DefifaScorecardState) {
|
|
455
636
|
// Keep a reference to the ratified scorecard ID.
|
|
456
|
-
uint256
|
|
637
|
+
uint256 ratifiedScorecardId = ratifiedScorecardIdOf[gameId];
|
|
457
638
|
|
|
458
639
|
// If the game has already ratified a scorecard, return succeeded if the ratified proposal is being checked.
|
|
459
640
|
// Else return defeated.
|
|
460
|
-
if (
|
|
461
|
-
return
|
|
641
|
+
if (ratifiedScorecardId != 0) {
|
|
642
|
+
return ratifiedScorecardId == scorecardId ? DefifaScorecardState.RATIFIED : DefifaScorecardState.DEFEATED;
|
|
462
643
|
}
|
|
463
644
|
|
|
464
645
|
// Get a reference to the scorecard.
|
|
465
|
-
DefifaScorecard memory
|
|
646
|
+
DefifaScorecard memory scorecard = _scorecardOf[gameId][scorecardId];
|
|
466
647
|
|
|
467
648
|
// Make sure the proposal is known.
|
|
468
649
|
// slither-disable-next-line incorrect-equality
|
|
469
|
-
if (
|
|
650
|
+
if (scorecard.attestationsBegin == 0) {
|
|
470
651
|
revert DefifaGovernor_UnknownProposal();
|
|
471
652
|
}
|
|
472
653
|
|
|
473
654
|
// If the scorecard has attestations beginning in the future, the state is PENDING.
|
|
474
655
|
// At exactly `attestationsBegin`, attestations are open so the state is ACTIVE.
|
|
475
|
-
if (
|
|
656
|
+
if (scorecard.attestationsBegin > block.timestamp) {
|
|
476
657
|
return DefifaScorecardState.PENDING;
|
|
477
658
|
}
|
|
478
659
|
|
|
479
660
|
// If the scorecard's grace period has not yet ended, the state is ACTIVE.
|
|
480
661
|
// At exactly `gracePeriodEnds`, the grace period has elapsed so we fall through to the quorum check.
|
|
481
|
-
if (
|
|
662
|
+
if (scorecard.gracePeriodEnds > block.timestamp) {
|
|
482
663
|
return DefifaScorecardState.ACTIVE;
|
|
483
664
|
}
|
|
484
665
|
|
|
485
|
-
// If quorum has been reached
|
|
486
|
-
|
|
666
|
+
// If quorum has been reached (using the concentration-adjusted snapshot), check timelock.
|
|
667
|
+
if (scorecard.quorumSnapshot <= _scorecardAttestationsOf[gameId][scorecardId].count) {
|
|
668
|
+
uint256 _timelockDuration = timelockDurationOf(gameId);
|
|
669
|
+
if (_timelockDuration > 0 && block.timestamp < uint256(scorecard.gracePeriodEnds) + _timelockDuration) {
|
|
670
|
+
return DefifaScorecardState.QUEUED;
|
|
671
|
+
}
|
|
672
|
+
return DefifaScorecardState.SUCCEEDED;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Scorecards that fail to reach quorum remain ACTIVE indefinitely — there is no DEFEATED
|
|
487
676
|
// state transition for unratified scorecards. This is by design: new scorecards can always be
|
|
488
677
|
// submitted and the game's no-contest timeout (scorecardTimeout) provides the ultimate backstop.
|
|
489
|
-
return
|
|
490
|
-
|
|
491
|
-
|
|
678
|
+
return DefifaScorecardState.ACTIVE;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/// @notice The timelock duration for a game (cooling period after quorum + grace period before ratification).
|
|
682
|
+
/// @param gameId The ID of the game.
|
|
683
|
+
/// @return The timelock duration in seconds.
|
|
684
|
+
function timelockDurationOf(uint256 gameId) public view override returns (uint256) {
|
|
685
|
+
// timelock duration in bits 96-143 (48 bits).
|
|
686
|
+
return uint256(uint48(_packedScorecardInfoOf[gameId] >> 96));
|
|
492
687
|
}
|
|
493
688
|
|
|
494
689
|
//*********************************************************************//
|