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