@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.
Files changed (37) hide show
  1. package/CHANGE_LOG.md +60 -5
  2. package/CRYPTO_ECON.md +505 -270
  3. package/CRYPTO_ECON.pdf +0 -0
  4. package/CRYPTO_ECON.tex +438 -241
  5. package/RISKS.md +9 -1
  6. package/SKILLS.md +3 -2
  7. package/package.json +6 -6
  8. package/src/DefifaDeployer.sol +128 -130
  9. package/src/DefifaGovernor.sol +278 -83
  10. package/src/DefifaHook.sol +158 -171
  11. package/src/enums/DefifaScorecardState.sol +1 -0
  12. package/src/interfaces/IDefifaGovernor.sol +41 -2
  13. package/src/libraries/DefifaHookLib.sol +69 -62
  14. package/src/structs/DefifaAttestations.sol +3 -3
  15. package/src/structs/DefifaLaunchProjectData.sol +1 -0
  16. package/src/structs/DefifaScorecard.sol +2 -0
  17. package/test/BWAFunctionComparison.t.sol +1320 -0
  18. package/test/DefifaAdversarialQuorum.t.sol +52 -37
  19. package/test/DefifaAuditLowGuards.t.sol +9 -5
  20. package/test/DefifaFeeAccounting.t.sol +2 -1
  21. package/test/DefifaGovernanceHardening.t.sol +1311 -0
  22. package/test/DefifaGovernor.t.sol +4 -2
  23. package/test/DefifaHookRegressions.t.sol +2 -1
  24. package/test/DefifaMintCostInvariant.t.sol +2 -1
  25. package/test/DefifaNoContest.t.sol +3 -2
  26. package/test/DefifaSecurity.t.sol +54 -41
  27. package/test/DefifaUSDC.t.sol +3 -2
  28. package/test/Fork.t.sol +11 -12
  29. package/test/TestAuditGaps.sol +6 -4
  30. package/test/TestQALastMile.t.sol +4 -2
  31. package/test/audit/{CodexAttestationDoubleCount.t.sol → AttestationDoubleCount.t.sol} +3 -2
  32. package/test/audit/FixPendingReserveDilution.t.sol +366 -0
  33. package/test/audit/PendingReserveDilution.t.sol +298 -0
  34. package/test/audit/PendingReserveQuorumGrief.t.sol +355 -0
  35. package/test/regression/AttestationDelegateBeneficiary.t.sol +2 -1
  36. package/test/regression/FulfillmentBlocksRatification.t.sol +2 -1
  37. package/test/regression/GracePeriodBypass.t.sol +2 -1
@@ -6,5 +6,6 @@ enum DefifaScorecardState {
6
6
  ACTIVE,
7
7
  DEFEATED,
8
8
  SUCCEEDED,
9
+ QUEUED,
9
10
  RATIFIED
10
11
  }
@@ -9,7 +9,11 @@ import {DefifaTierCashOutWeight} from "../structs/DefifaTierCashOutWeight.sol";
9
9
  /// @notice Manages the ratification of Defifa scorecards through attestation-based governance.
10
10
  interface IDefifaGovernor {
11
11
  event GameInitialized(
12
- uint256 indexed gameId, uint256 attestationStartTime, uint256 attestationGracePeriod, address caller
12
+ uint256 indexed gameId,
13
+ uint256 attestationStartTime,
14
+ uint256 attestationGracePeriod,
15
+ uint256 timelockDuration,
16
+ address caller
13
17
  );
14
18
 
15
19
  event ScorecardAttested(uint256 indexed gameId, uint256 indexed scorecardId, uint256 weight, address caller);
@@ -24,6 +28,8 @@ interface IDefifaGovernor {
24
28
  address caller
25
29
  );
26
30
 
31
+ event AttestationRevoked(uint256 indexed gameId, uint256 indexed scorecardId, address account, uint256 weight);
32
+
27
33
  /// @notice The number of attestations for a scorecard.
28
34
  /// @param gameId The ID of the game.
29
35
  /// @param scorecardId The ID of the scorecard.
@@ -63,6 +69,22 @@ interface IDefifaGovernor {
63
69
  view
64
70
  returns (uint256 attestationPower);
65
71
 
72
+ /// @notice Get the BWA-adjusted attestation weight for an account relative to a specific scorecard.
73
+ /// @param gameId The ID of the game.
74
+ /// @param scorecardId The ID of the scorecard (for tier weight lookup).
75
+ /// @param account The account to check.
76
+ /// @param timestamp The timestamp to check.
77
+ /// @return bwaAttestationPower The BWA-adjusted attestation power.
78
+ function getBWAAttestationWeight(
79
+ uint256 gameId,
80
+ uint256 scorecardId,
81
+ address account,
82
+ uint48 timestamp
83
+ )
84
+ external
85
+ view
86
+ returns (uint256 bwaAttestationPower);
87
+
66
88
  /// @notice Whether an account has attested to a specific scorecard.
67
89
  /// @param gameId The ID of the game.
68
90
  /// @param scorecardId The ID of the scorecard.
@@ -96,6 +118,11 @@ interface IDefifaGovernor {
96
118
  /// @return The scorecard state.
97
119
  function stateOf(uint256 gameId, uint256 scorecardId) external view returns (DefifaScorecardState);
98
120
 
121
+ /// @notice The timelock duration for a game.
122
+ /// @param gameId The ID of the game.
123
+ /// @return The timelock duration in seconds.
124
+ function timelockDurationOf(uint256 gameId) external view returns (uint256);
125
+
99
126
  /// @notice Attest to a submitted scorecard.
100
127
  /// @param gameId The ID of the game.
101
128
  /// @param scorecardId The ID of the scorecard to attest to.
@@ -106,7 +133,14 @@ interface IDefifaGovernor {
106
133
  /// @param gameId The ID of the game.
107
134
  /// @param attestationStartTime The timestamp when attestation begins.
108
135
  /// @param attestationGracePeriod The grace period duration in seconds.
109
- function initializeGame(uint256 gameId, uint256 attestationStartTime, uint256 attestationGracePeriod) external;
136
+ /// @param timelockDuration The timelock duration after quorum is met, in seconds.
137
+ function initializeGame(
138
+ uint256 gameId,
139
+ uint256 attestationStartTime,
140
+ uint256 attestationGracePeriod,
141
+ uint256 timelockDuration
142
+ )
143
+ external;
110
144
 
111
145
  /// @notice Ratify a scorecard that has reached quorum.
112
146
  /// @param gameId The ID of the game.
@@ -119,6 +153,11 @@ interface IDefifaGovernor {
119
153
  external
120
154
  returns (uint256);
121
155
 
156
+ /// @notice Revoke a previously submitted attestation during the ACTIVE phase.
157
+ /// @param gameId The ID of the game.
158
+ /// @param scorecardId The ID of the scorecard to revoke attestation from.
159
+ function revokeAttestationFrom(uint256 gameId, uint256 scorecardId) external;
160
+
122
161
  /// @notice Submit a scorecard for attestation.
123
162
  /// @param gameId The ID of the game.
124
163
  /// @param tierWeights The tier cash out weights.
@@ -42,47 +42,47 @@ library DefifaHookLib {
42
42
  returns (uint256[128] memory weights)
43
43
  {
44
44
  // Keep a reference to the max tier ID.
45
- uint256 _maxTierId = hookStore.maxTierIdOf(hook);
45
+ uint256 maxTierId = hookStore.maxTierIdOf(hook);
46
46
 
47
47
  // Keep a reference to the cumulative amounts.
48
- uint256 _cumulativeCashOutWeight;
48
+ uint256 cumulativeCashOutWeight;
49
49
 
50
50
  // Keep a reference to the number of tier weights.
51
- uint256 _numberOfTierWeights = tierWeights.length;
51
+ uint256 numberOfTierWeights = tierWeights.length;
52
52
 
53
53
  // Keep a reference to the tier being iterated on.
54
- JB721Tier memory _tier;
54
+ JB721Tier memory tier;
55
55
 
56
56
  // Keep a reference to the last tier ID to enforce ascending order (no duplicates).
57
- uint256 _lastTierId;
57
+ uint256 lastTierId;
58
58
 
59
- for (uint256 _i; _i < _numberOfTierWeights;) {
59
+ for (uint256 i; i < numberOfTierWeights;) {
60
60
  // Enforce strict ascending order to prevent duplicate tier IDs.
61
- if (tierWeights[_i].id <= _lastTierId && _i != 0) revert DefifaHook_BadTierOrder();
62
- _lastTierId = tierWeights[_i].id;
61
+ if (tierWeights[i].id <= lastTierId && i != 0) revert DefifaHook_BadTierOrder();
62
+ lastTierId = tierWeights[i].id;
63
63
 
64
64
  // Get the tier.
65
- _tier = hookStore.tierOf({hook: hook, id: tierWeights[_i].id, includeResolvedUri: false});
65
+ tier = hookStore.tierOf({hook: hook, id: tierWeights[i].id, includeResolvedUri: false});
66
66
 
67
67
  // Can't set a cashOut weight for tiers not in category 0.
68
- if (_tier.category != 0) revert DefifaHook_InvalidTierId();
68
+ if (tier.category != 0) revert DefifaHook_InvalidTierId();
69
69
 
70
70
  // Attempting to set the cashOut weight for a tier that does not exist (yet) reverts.
71
- if (_tier.id > _maxTierId) revert DefifaHook_InvalidTierId();
71
+ if (tier.id > maxTierId) revert DefifaHook_InvalidTierId();
72
72
 
73
73
  // Save the tier weight. Tiers are 1 indexed and should be stored 0 indexed.
74
- weights[_tier.id - 1] = tierWeights[_i].cashOutWeight;
74
+ weights[tier.id - 1] = tierWeights[i].cashOutWeight;
75
75
 
76
76
  // Increment the cumulative amount.
77
- _cumulativeCashOutWeight += tierWeights[_i].cashOutWeight;
77
+ cumulativeCashOutWeight += tierWeights[i].cashOutWeight;
78
78
 
79
79
  unchecked {
80
- ++_i;
80
+ ++i;
81
81
  }
82
82
  }
83
83
 
84
84
  // Make sure the cumulative amount is exactly the total cashOut weight.
85
- if (_cumulativeCashOutWeight != TOTAL_CASHOUT_WEIGHT) revert DefifaHook_InvalidCashoutWeights();
85
+ if (cumulativeCashOutWeight != TOTAL_CASHOUT_WEIGHT) revert DefifaHook_InvalidCashoutWeights();
86
86
  }
87
87
 
88
88
  /// @notice Compute the cash out weight for a single token.
@@ -104,32 +104,39 @@ library DefifaHookLib {
104
104
  returns (uint256)
105
105
  {
106
106
  // Keep a reference to the token's tier ID.
107
- uint256 _tierId = hookStore.tierIdOfToken(tokenId);
107
+ uint256 tierId = hookStore.tierIdOfToken(tokenId);
108
108
 
109
109
  // Keep a reference to the tier.
110
- JB721Tier memory _tier = hookStore.tierOf({hook: hook, id: _tierId, includeResolvedUri: false});
110
+ JB721Tier memory tier = hookStore.tierOf({hook: hook, id: tierId, includeResolvedUri: false});
111
111
 
112
112
  // Get the tier's weight.
113
- uint256 _weight = tierCashOutWeights[_tierId - 1];
113
+ uint256 weight = tierCashOutWeights[tierId - 1];
114
114
 
115
115
  // If there's no weight there's nothing to redeem.
116
- if (_weight == 0) return 0;
116
+ if (weight == 0) return 0;
117
117
 
118
118
  // Get the amount of tokens that have already been burned.
119
- uint256 _burnedTokens = hookStore.numberOfBurnedFor({hook: hook, tierId: _tierId});
119
+ uint256 burnedTokens = hookStore.numberOfBurnedFor({hook: hook, tierId: tierId});
120
120
 
121
121
  // If no tiers were minted, nothing to redeem.
122
- if (_tier.initialSupply - (_tier.remainingSupply + _burnedTokens) == 0) return 0;
122
+ if (tier.initialSupply - (tier.remainingSupply + burnedTokens) == 0) return 0;
123
123
 
124
124
  // Calculate the amount of tokens that existed at the start of the last phase.
125
- uint256 _totalTokensForCashoutInTier =
126
- _tier.initialSupply - _tier.remainingSupply - (_burnedTokens - tokensRedeemedFrom[_tierId]);
125
+ uint256 totalTokensForCashoutInTier =
126
+ tier.initialSupply - tier.remainingSupply - (burnedTokens - tokensRedeemedFrom[tierId]);
127
+
128
+ // Include pending (unminted) reserve NFTs in the denominator. Without this, paid holders
129
+ // could cash out before reserves are minted and extract value that should be diluted across
130
+ // both paid and reserved holders. By counting pending reserves, each token's share of the
131
+ // tier weight is computed against the full eventual supply.
132
+ uint256 pendingReserves = hookStore.numberOfPendingReservesFor({hook: hook, tierId: tierId});
133
+ totalTokensForCashoutInTier += pendingReserves;
127
134
 
128
135
  // Calculate the percentage of the tier cashOut amount a single token counts for.
129
136
  // Integer division rounding in cashOutWeight is unavoidable in Solidity. Rounding direction
130
137
  // (down) is consistent and conservative — it slightly favors the project over individual cash-out recipients.
131
138
  // The maximum error per operation is 1 wei per division.
132
- return _weight / _totalTokensForCashoutInTier;
139
+ return weight / totalTokensForCashoutInTier;
133
140
  }
134
141
 
135
142
  /// @notice Compute the cumulative cash out weight for multiple tokens.
@@ -150,17 +157,17 @@ library DefifaHookLib {
150
157
  view
151
158
  returns (uint256 cumulativeWeight)
152
159
  {
153
- uint256 _tokenCount = tokenIds.length;
154
- for (uint256 _i; _i < _tokenCount;) {
160
+ uint256 tokenCount = tokenIds.length;
161
+ for (uint256 i; i < tokenCount;) {
155
162
  cumulativeWeight += computeCashOutWeight({
156
- tokenId: tokenIds[_i],
163
+ tokenId: tokenIds[i],
157
164
  hookStore: hookStore,
158
165
  hook: hook,
159
166
  tierCashOutWeights: tierCashOutWeights,
160
167
  tokensRedeemedFrom: tokensRedeemedFrom
161
168
  });
162
169
  unchecked {
163
- ++_i;
170
+ ++i;
164
171
  }
165
172
  }
166
173
  }
@@ -190,20 +197,20 @@ library DefifaHookLib {
190
197
  if (totalMintCost == 0) return (0, 0);
191
198
 
192
199
  // Keep a reference to the number of tokens being used for claims.
193
- uint256 _numberOfTokens = tokenIds.length;
200
+ uint256 numberOfTokens = tokenIds.length;
194
201
 
195
202
  // Calculate the amount paid to mint the tokens that are being burned.
196
- uint256 _cumulativeMintPrice;
197
- for (uint256 _i; _i < _numberOfTokens; _i++) {
198
- _cumulativeMintPrice += hookStore.tierOfTokenId({
199
- hook: hook, tokenId: tokenIds[_i], includeResolvedUri: false
203
+ uint256 cumulativeMintPrice;
204
+ for (uint256 i; i < numberOfTokens; i++) {
205
+ cumulativeMintPrice += hookStore.tierOfTokenId({
206
+ hook: hook, tokenId: tokenIds[i], includeResolvedUri: false
200
207
  })
201
208
  .price;
202
209
  }
203
210
 
204
211
  // Calculate the user's claimable amount proportional to what they paid.
205
- defifaTokenAmount = mulDiv({x: defifaBalance, y: _cumulativeMintPrice, denominator: totalMintCost});
206
- baseProtocolTokenAmount = mulDiv({x: baseProtocolBalance, y: _cumulativeMintPrice, denominator: totalMintCost});
212
+ defifaTokenAmount = mulDiv({x: defifaBalance, y: cumulativeMintPrice, denominator: totalMintCost});
213
+ baseProtocolTokenAmount = mulDiv({x: baseProtocolBalance, y: cumulativeMintPrice, denominator: totalMintCost});
207
214
  }
208
215
 
209
216
  /// @notice Compute the cumulative mint price for a set of token IDs.
@@ -220,10 +227,10 @@ library DefifaHookLib {
220
227
  view
221
228
  returns (uint256 cumulativeMintPrice)
222
229
  {
223
- uint256 _numberOfTokenIds = tokenIds.length;
224
- for (uint256 _i; _i < _numberOfTokenIds; _i++) {
230
+ uint256 numberOfTokenIds = tokenIds.length;
231
+ for (uint256 i; i < numberOfTokenIds; i++) {
225
232
  cumulativeMintPrice += hookStore.tierOfTokenId({
226
- hook: hook, tokenId: tokenIds[_i], includeResolvedUri: false
233
+ hook: hook, tokenId: tokenIds[i], includeResolvedUri: false
227
234
  })
228
235
  .price;
229
236
  }
@@ -275,8 +282,8 @@ library DefifaHookLib {
275
282
  view
276
283
  returns (uint256)
277
284
  {
278
- JB721Tier memory _tier = hookStore.tierOf({hook: hook, id: tierId, includeResolvedUri: false});
279
- return _tier.initialSupply - (_tier.remainingSupply + hookStore.numberOfBurnedFor({hook: hook, tierId: tierId}));
285
+ JB721Tier memory tier = hookStore.tierOf({hook: hook, id: tierId, includeResolvedUri: false});
286
+ return tier.initialSupply - (tier.remainingSupply + hookStore.numberOfBurnedFor({hook: hook, tierId: tierId}));
280
287
  }
281
288
 
282
289
  /// @notice Computes the attestation units for tiers during payment processing.
@@ -296,40 +303,40 @@ library DefifaHookLib {
296
303
  view
297
304
  returns (uint256[] memory tierIds, uint256[] memory attestationAmounts, uint256 count)
298
305
  {
299
- uint256 _numberOfTiers = tierIdsToMint.length;
300
- tierIds = new uint256[](_numberOfTiers);
301
- attestationAmounts = new uint256[](_numberOfTiers);
306
+ uint256 numberOfTiers = tierIdsToMint.length;
307
+ tierIds = new uint256[](numberOfTiers);
308
+ attestationAmounts = new uint256[](numberOfTiers);
302
309
 
303
- if (_numberOfTiers == 0) return (tierIds, attestationAmounts, 0);
310
+ if (numberOfTiers == 0) return (tierIds, attestationAmounts, 0);
304
311
 
305
- uint256 _currentTierId;
306
- uint256 _attestationUnits;
307
- uint256 _accumulated;
312
+ uint256 currentTierId;
313
+ uint256 attestationUnits;
314
+ uint256 accumulated;
308
315
 
309
- for (uint256 _i; _i < _numberOfTiers;) {
310
- if (_currentTierId != tierIdsToMint[_i]) {
316
+ for (uint256 i; i < numberOfTiers;) {
317
+ if (currentTierId != tierIdsToMint[i]) {
311
318
  // Flush accumulated units for previous tier.
312
- if (_currentTierId != 0) {
313
- tierIds[count] = _currentTierId;
314
- attestationAmounts[count] = _accumulated;
319
+ if (currentTierId != 0) {
320
+ tierIds[count] = currentTierId;
321
+ attestationAmounts[count] = accumulated;
315
322
  count++;
316
323
  }
317
- if (tierIdsToMint[_i] < _currentTierId) revert DefifaHook_BadTierOrder();
318
- _currentTierId = tierIdsToMint[_i];
319
- _attestationUnits =
320
- hookStore.tierOf({hook: hook, id: _currentTierId, includeResolvedUri: false}).votingUnits;
321
- _accumulated = _attestationUnits;
324
+ if (tierIdsToMint[i] < currentTierId) revert DefifaHook_BadTierOrder();
325
+ currentTierId = tierIdsToMint[i];
326
+ attestationUnits =
327
+ hookStore.tierOf({hook: hook, id: currentTierId, includeResolvedUri: false}).votingUnits;
328
+ accumulated = attestationUnits;
322
329
  } else {
323
- _accumulated += _attestationUnits;
330
+ accumulated += attestationUnits;
324
331
  }
325
332
  unchecked {
326
- ++_i;
333
+ ++i;
327
334
  }
328
335
  }
329
336
  // Flush the last tier.
330
- if (_currentTierId != 0) {
331
- tierIds[count] = _currentTierId;
332
- attestationAmounts[count] = _accumulated;
337
+ if (currentTierId != 0) {
338
+ tierIds[count] = currentTierId;
339
+ attestationAmounts[count] = accumulated;
333
340
  count++;
334
341
  }
335
342
  }
@@ -1,9 +1,9 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.0;
3
3
 
4
- /// @custom:param A count of attestations.
5
- /// @custom:param A mapping of which accounts have attested.
4
+ /// @custom:param count A count of attestation weight.
5
+ /// @custom:param attestedWeightOf The BWA weight each account attested with (0 = not attested).
6
6
  struct DefifaAttestations {
7
7
  uint256 count;
8
- mapping(address => bool) hasAttested;
8
+ mapping(address => uint256) attestedWeightOf;
9
9
  }
@@ -54,4 +54,5 @@ struct DefifaLaunchProjectData {
54
54
  IJB721TiersHookStore store;
55
55
  uint256 minParticipation;
56
56
  uint32 scorecardTimeout;
57
+ uint256 timelockDuration;
57
58
  }
@@ -3,7 +3,9 @@ pragma solidity ^0.8.0;
3
3
 
4
4
  /// @custom:member attestationsBegin The block at which attestations to the scorecard become allowed.
5
5
  /// @custom:member gracePeriodEnds The block at which the scorecard can become ratified.
6
+ /// @custom:member quorumSnapshot The HHI-adjusted quorum threshold snapshotted at submission time.
6
7
  struct DefifaScorecard {
7
8
  uint48 attestationsBegin;
8
9
  uint48 gracePeriodEnds;
10
+ uint256 quorumSnapshot;
9
11
  }