@ballkidz/defifa 0.0.29 → 0.0.30

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.
@@ -17,7 +17,7 @@ contract DefifaProjectOwner is IERC721Receiver {
17
17
  // --------------------------- custom errors ------------------------- //
18
18
  //*********************************************************************//
19
19
 
20
- error DefifaProjectOwner_InvalidSender();
20
+ error DefifaProjectOwner_InvalidSender(address caller, address expectedSender);
21
21
 
22
22
  //*********************************************************************//
23
23
  // --------------- public immutable stored properties ---------------- //
@@ -64,7 +64,9 @@ contract DefifaProjectOwner is IERC721Receiver {
64
64
  operator;
65
65
 
66
66
  // Make sure the 721 received is the JBProjects contract.
67
- if (msg.sender != address(PROJECTS)) revert DefifaProjectOwner_InvalidSender();
67
+ if (msg.sender != address(PROJECTS)) {
68
+ revert DefifaProjectOwner_InvalidSender({caller: msg.sender, expectedSender: address(PROJECTS)});
69
+ }
68
70
 
69
71
  // Set the correct permission.
70
72
  uint8[] memory permissionIds = new uint8[](1);
@@ -205,27 +205,30 @@ contract DefifaTokenUriResolver is IDefifaTokenUriResolver, IJB721TokenUriResolv
205
205
  gamePhaseText,
206
206
  "</text>",
207
207
  '<text x="10" y="85" style="font-size:26px; font-family: Capsules-500; font-weight:500; fill: #c0b3f1;">',
208
- _getSubstring(titleSvg, 0, 30),
208
+ _getSubstring({str: titleSvg, startIndex: 0, endIndex: 30}),
209
209
  "</text>",
210
210
  '<text x="10" y="120" style="font-size:26px; font-family: Capsules-500; font-weight:500; fill: #c0b3f1;">',
211
- _getSubstring(titleSvg, 30, 60),
211
+ _getSubstring({str: titleSvg, startIndex: 30, endIndex: 60}),
212
212
  "</text>",
213
213
  '<text x="10" y="205" style="font-size:80px; font-family: Capsules-700; font-weight:700; fill: #fea282;">',
214
- bytes(_getSubstring(teamSvg, 20, 30)).length != 0 && bytes(_getSubstring(teamSvg, 10, 20)).length != 0
215
- ? _getSubstring(teamSvg, 0, 10)
214
+ bytes(_getSubstring({str: teamSvg, startIndex: 20, endIndex: 30})).length != 0
215
+ && bytes(_getSubstring({str: teamSvg, startIndex: 10, endIndex: 20})).length != 0
216
+ ? _getSubstring({str: teamSvg, startIndex: 0, endIndex: 10})
216
217
  : "",
217
218
  "</text>",
218
219
  '<text x="10" y="295" style="font-size:80px; font-family: Capsules-700; font-weight:700; fill: #fea282;">',
219
- bytes(_getSubstring(teamSvg, 20, 30)).length != 0
220
- ? _getSubstring(teamSvg, 10, 20)
221
- : bytes(_getSubstring(teamSvg, 10, 20)).length != 0 ? _getSubstring(teamSvg, 0, 10) : "",
220
+ bytes(_getSubstring({str: teamSvg, startIndex: 20, endIndex: 30})).length != 0
221
+ ? _getSubstring({str: teamSvg, startIndex: 10, endIndex: 20})
222
+ : bytes(_getSubstring({str: teamSvg, startIndex: 10, endIndex: 20})).length != 0
223
+ ? _getSubstring({str: teamSvg, startIndex: 0, endIndex: 10})
224
+ : "",
222
225
  "</text>",
223
226
  '<text x="10" y="385" style="font-size:80px; font-family: Capsules-700; font-weight:700; fill: #fea282;">',
224
- bytes(_getSubstring(teamSvg, 20, 30)).length != 0
225
- ? _getSubstring(teamSvg, 20, 30)
226
- : bytes(_getSubstring(teamSvg, 10, 20)).length != 0
227
- ? _getSubstring(teamSvg, 10, 20)
228
- : _getSubstring(teamSvg, 0, 10),
227
+ bytes(_getSubstring({str: teamSvg, startIndex: 20, endIndex: 30})).length != 0
228
+ ? _getSubstring({str: teamSvg, startIndex: 20, endIndex: 30})
229
+ : bytes(_getSubstring({str: teamSvg, startIndex: 10, endIndex: 20})).length != 0
230
+ ? _getSubstring({str: teamSvg, startIndex: 10, endIndex: 20})
231
+ : _getSubstring({str: teamSvg, startIndex: 0, endIndex: 10}),
229
232
  "</text>",
230
233
  '<text x="10" y="430" style="font-size:16px; font-family: Capsules-500; font-weight:500; fill: #c0b3f1;">TOKEN ID: ',
231
234
  tokenId.toString(),
@@ -240,7 +243,6 @@ contract DefifaTokenUriResolver is IDefifaTokenUriResolver, IJB721TokenUriResolv
240
243
  )
241
244
  );
242
245
  parts[3] = string('"}');
243
- // slither-disable-next-line encode-packed-collision
244
246
  return string.concat(parts[0], Base64.encode(abi.encodePacked(parts[1], parts[2], parts[3])));
245
247
  }
246
248
 
@@ -20,12 +20,6 @@ interface IDefifaDeployer {
20
20
  /// @param reason The revert reason bytes from the failed payout.
21
21
  event CommitmentPayoutFailed(uint256 indexed gameId, uint256 amount, bytes reason);
22
22
 
23
- /// @notice Emitted when a split receives a portion of the game pot.
24
- /// @param split The split that received funds.
25
- /// @param amount The amount sent to the split.
26
- /// @param caller The address that triggered the distribution.
27
- event DistributeToSplit(JBSplit split, uint256 amount, address caller);
28
-
29
23
  /// @notice Emitted when a game's commitments have been fulfilled.
30
24
  /// @param gameId The ID of the fulfilled game.
31
25
  /// @param pot The total game pot that was fulfilled.
@@ -51,16 +45,6 @@ interface IDefifaDeployer {
51
45
  /// @param caller The address that queued the phase transition.
52
46
  event QueuedNoContest(uint256 indexed gameId, address caller);
53
47
 
54
- /// @notice Emitted when a game is queued into its refund phase.
55
- /// @param gameId The ID of the game.
56
- /// @param caller The address that queued the phase transition.
57
- event QueuedRefundPhase(uint256 indexed gameId, address caller);
58
-
59
- /// @notice Emitted when a game is queued into its scoring phase.
60
- /// @param gameId The ID of the game.
61
- /// @param caller The address that queued the phase transition.
62
- event QueuedScoringPhase(uint256 indexed gameId, address caller);
63
-
64
48
  /// @notice The fee divisor for base protocol fees (100 / fee percent).
65
49
  /// @return The fee divisor.
66
50
  function BASE_PROTOCOL_FEE_DIVISOR() external view returns (uint256);
@@ -193,10 +193,6 @@ interface IDefifaHook is IJB721Hook {
193
193
  /// @return The tier name.
194
194
  function tierNameOf(uint256 tierId) external view returns (string memory);
195
195
 
196
- /// @notice The token allocations (Defifa token amount, base protocol token amount).
197
- /// @return The Defifa token allocation and the base protocol token allocation.
198
- function tokenAllocations() external view returns (uint256, uint256);
199
-
200
196
  /// @notice Get the claimable Defifa and base protocol tokens for a set of token IDs.
201
197
  /// @param tokenIds The token IDs to check.
202
198
  /// @return The claimable Defifa token amount and base protocol token amount.
@@ -17,78 +17,162 @@ import {DefifaTierCashOutWeight} from "../structs/DefifaTierCashOutWeight.sol";
17
17
  library DefifaHookLib {
18
18
  using SafeERC20 for IERC20;
19
19
 
20
- error DefifaHook_BadTierOrder();
21
- error DefifaHook_InvalidTierId();
22
- error DefifaHook_InvalidCashoutWeights();
20
+ //*********************************************************************//
21
+ // --------------------------- custom errors ------------------------- //
22
+ //*********************************************************************//
23
+
24
+ error DefifaHook_BadTierOrder(uint256 previousTierId, uint256 tierId);
25
+ error DefifaHook_InvalidTierId(uint256 tierId, uint256 actualTierId, uint256 maxTierId, uint256 category);
26
+ error DefifaHook_InvalidCashoutWeights(uint256 totalWeight, uint256 expectedWeight);
27
+
28
+ //*********************************************************************//
29
+ // ----------------------------- events ------------------------------ //
30
+ //*********************************************************************//
23
31
 
24
32
  event ClaimedTokens(
25
33
  address indexed beneficiary, uint256 defifaTokenAmount, uint256 baseProtocolTokenAmount, address caller
26
34
  );
27
35
 
36
+ //*********************************************************************//
37
+ // ----------------------- internal constants ------------------------ //
38
+ //*********************************************************************//
39
+
28
40
  /// @notice The total cashOut weight that can be divided among tiers.
29
41
  uint256 internal constant TOTAL_CASHOUT_WEIGHT = 1_000_000_000_000_000_000;
30
42
 
31
- /// @notice Validates tier cash out weights and returns the weight array to store.
32
- /// @param tierWeights The tier weights to validate and set.
43
+ //*********************************************************************//
44
+ // -------------------------- public views --------------------------- //
45
+ //*********************************************************************//
46
+
47
+ /// @notice Returns the adjusted pending reserve count for a tier, accounting for refund-phase burns.
48
+ /// @param tierId The tier ID.
33
49
  /// @param hookStore The 721 tiers hook store.
34
50
  /// @param hook The hook address.
35
- /// @return weights The 128-element array of validated weights.
36
- function validateAndBuildWeights(
37
- DefifaTierCashOutWeight[] memory tierWeights,
51
+ /// @param refundBurns The number of refund-phase burns for the tier.
52
+ /// @return The adjusted pending reserve count.
53
+ function adjustedPendingReservesFor(
54
+ uint256 tierId,
38
55
  IJB721TiersHookStore hookStore,
39
- address hook
56
+ address hook,
57
+ uint256 refundBurns
40
58
  )
41
59
  public
42
60
  view
43
- returns (uint256[128] memory weights)
61
+ returns (uint256)
44
62
  {
45
- // Keep a reference to the max tier ID.
46
- uint256 maxTierId = hookStore.maxTierIdOf(hook);
63
+ // If no refund burns, return the store's value directly.
64
+ if (refundBurns == 0) return hookStore.numberOfPendingReservesFor({hook: hook, tierId: tierId});
47
65
 
48
- // Keep a reference to the cumulative amounts.
49
- uint256 cumulativeCashOutWeight;
50
-
51
- // Keep a reference to the number of tier weights.
52
- uint256 numberOfTierWeights = tierWeights.length;
66
+ // Get the tier to access reserveFrequency and supply data.
67
+ JB721Tier memory tier = hookStore.tierOf({hook: hook, id: tierId, includeResolvedUri: false});
53
68
 
54
- // Keep a reference to the tier being iterated on.
55
- JB721Tier memory tier;
69
+ // No reserves if no reserve frequency.
70
+ if (tier.reserveFrequency == 0) return 0;
56
71
 
57
- // Keep a reference to the last tier ID to enforce ascending order (no duplicates).
58
- uint256 lastTierId;
72
+ // Calculate the number of reserves already minted.
73
+ uint256 reservesMinted = hookStore.numberOfReservesMintedFor({hook: hook, tierId: tierId});
59
74
 
60
- for (uint256 i; i < numberOfTierWeights;) {
61
- // Enforce strict ascending order to prevent duplicate tier IDs.
62
- if (tierWeights[i].id <= lastTierId && i != 0) revert DefifaHook_BadTierOrder();
63
- lastTierId = tierWeights[i].id;
75
+ // Calculate non-reserve mints: initialSupply - remainingSupply - reservesMinted.
76
+ uint256 nonReserveMints = tier.initialSupply - tier.remainingSupply - reservesMinted;
64
77
 
65
- // Get the tier.
66
- // slither-disable-next-line calls-loop
67
- tier = hookStore.tierOf({hook: hook, id: tierWeights[i].id, includeResolvedUri: false});
78
+ // Subtract refund burns from non-reserve mints (burns can't exceed non-reserve mints).
79
+ uint256 adjustedMints = nonReserveMints > refundBurns ? nonReserveMints - refundBurns : 0;
68
80
 
69
- // Guard against uint32 truncation: if the caller passes a tier ID > type(uint32).max,
70
- // the store may silently truncate and return a different tier.
71
- if (tierWeights[i].id != tier.id) revert DefifaHook_InvalidTierId();
81
+ // Recalculate available reserves: ceil(adjustedMints / reserveFrequency).
82
+ uint256 availableReserves = adjustedMints / tier.reserveFrequency;
83
+ if (adjustedMints % tier.reserveFrequency > 0) ++availableReserves;
72
84
 
73
- // Can't set a cashOut weight for tiers not in category 0.
74
- if (tier.category != 0) revert DefifaHook_InvalidTierId();
85
+ // Return pending = available - already minted (floored at 0).
86
+ return availableReserves > reservesMinted ? availableReserves - reservesMinted : 0;
87
+ }
75
88
 
76
- // Attempting to set the cashOut weight for a tier that does not exist (yet) reverts.
77
- if (tier.id > maxTierId) revert DefifaHook_InvalidTierId();
89
+ /// @notice Computes the attestation units for tiers during payment processing.
90
+ /// @dev Returns parallel arrays: tier IDs, cumulative attestation units per tier, and whether to switch delegate.
91
+ /// @param tierIdsToMint The tier IDs to mint (must be in ascending order).
92
+ /// @param hookStore The 721 tiers hook store.
93
+ /// @param hook The hook address.
94
+ /// @return tierIds The unique tier IDs.
95
+ /// @return attestationAmounts The cumulative attestation units for each unique tier.
96
+ /// @return count The number of unique tiers.
97
+ function computeAttestationUnits(
98
+ uint16[] memory tierIdsToMint,
99
+ IJB721TiersHookStore hookStore,
100
+ address hook
101
+ )
102
+ public
103
+ view
104
+ returns (uint256[] memory tierIds, uint256[] memory attestationAmounts, uint256 count)
105
+ {
106
+ uint256 numberOfTiers = tierIdsToMint.length;
107
+ tierIds = new uint256[](numberOfTiers);
108
+ attestationAmounts = new uint256[](numberOfTiers);
78
109
 
79
- // Save the tier weight. Tiers are 1 indexed and should be stored 0 indexed.
80
- weights[tier.id - 1] = tierWeights[i].cashOutWeight;
110
+ if (numberOfTiers == 0) return (tierIds, attestationAmounts, 0);
81
111
 
82
- // Increment the cumulative amount.
83
- cumulativeCashOutWeight += tierWeights[i].cashOutWeight;
112
+ uint256 currentTierId;
113
+ uint256 attestationUnits;
114
+ uint256 accumulated;
84
115
 
116
+ for (uint256 i; i < numberOfTiers;) {
117
+ if (currentTierId != tierIdsToMint[i]) {
118
+ // Flush accumulated units for previous tier.
119
+ if (currentTierId != 0) {
120
+ tierIds[count] = currentTierId;
121
+ attestationAmounts[count] = accumulated;
122
+ count++;
123
+ }
124
+ if (tierIdsToMint[i] < currentTierId) {
125
+ revert DefifaHook_BadTierOrder({previousTierId: currentTierId, tierId: tierIdsToMint[i]});
126
+ }
127
+ currentTierId = tierIdsToMint[i];
128
+ attestationUnits =
129
+ hookStore.tierOf({hook: hook, id: currentTierId, includeResolvedUri: false}).votingUnits;
130
+ accumulated = attestationUnits;
131
+ } else {
132
+ accumulated += attestationUnits;
133
+ }
85
134
  unchecked {
86
135
  ++i;
87
136
  }
88
137
  }
138
+ // Flush the last tier.
139
+ if (currentTierId != 0) {
140
+ tierIds[count] = currentTierId;
141
+ attestationAmounts[count] = accumulated;
142
+ count++;
143
+ }
144
+ }
89
145
 
90
- // Make sure the cumulative amount is exactly the total cashOut weight.
91
- if (cumulativeCashOutWeight != TOTAL_CASHOUT_WEIGHT) revert DefifaHook_InvalidCashoutWeights();
146
+ /// @notice Compute the cash out count for the beforeCashOutRecorded hook.
147
+ /// @param gamePhase The current game phase.
148
+ /// @param cumulativeMintPrice The cumulative mint price of the tokens to cash out.
149
+ /// @param surplusValue The surplus value from the context.
150
+ /// @param totalAmountRedeemed The amount already redeemed.
151
+ /// @param cumulativeCashOutWeight The cumulative cash out weight of the tokens.
152
+ /// @return cashOutCount The computed cash out count.
153
+ function computeCashOutCount(
154
+ DefifaGamePhase gamePhase,
155
+ uint256 cumulativeMintPrice,
156
+ uint256 surplusValue,
157
+ uint256 totalAmountRedeemed,
158
+ uint256 cumulativeCashOutWeight
159
+ )
160
+ public
161
+ pure
162
+ returns (uint256 cashOutCount)
163
+ {
164
+ // If the game is in its minting, refund, or no-contest phase, reclaim amount is the same as it cost to mint.
165
+ if (
166
+ gamePhase == DefifaGamePhase.MINT || gamePhase == DefifaGamePhase.REFUND
167
+ || gamePhase == DefifaGamePhase.NO_CONTEST
168
+ ) {
169
+ cashOutCount = cumulativeMintPrice;
170
+ } else {
171
+ // If the game is in its scoring or complete phase, reclaim amount is based on the tier weights.
172
+ cashOutCount = mulDiv({
173
+ x: surplusValue + totalAmountRedeemed, y: cumulativeCashOutWeight, denominator: TOTAL_CASHOUT_WEIGHT
174
+ });
175
+ }
92
176
  }
93
177
 
94
178
  /// @notice Compute the cash out weight for a single token.
@@ -110,11 +194,9 @@ library DefifaHookLib {
110
194
  returns (uint256)
111
195
  {
112
196
  // Keep a reference to the token's tier ID.
113
- // slither-disable-next-line calls-loop
114
197
  uint256 tierId = hookStore.tierIdOfToken(tokenId);
115
198
 
116
199
  // Keep a reference to the tier.
117
- // slither-disable-next-line calls-loop
118
200
  JB721Tier memory tier = hookStore.tierOf({hook: hook, id: tierId, includeResolvedUri: false});
119
201
 
120
202
  // Get the tier's weight.
@@ -124,7 +206,6 @@ library DefifaHookLib {
124
206
  if (weight == 0) return 0;
125
207
 
126
208
  // Get the amount of tokens that have already been burned.
127
- // slither-disable-next-line calls-loop
128
209
  uint256 burnedTokens = hookStore.numberOfBurnedFor({hook: hook, tierId: tierId});
129
210
 
130
211
  // If no tiers were minted, nothing to redeem.
@@ -135,7 +216,6 @@ library DefifaHookLib {
135
216
  tier.initialSupply - tier.remainingSupply - (burnedTokens - tokensRedeemedFrom[tierId]);
136
217
 
137
218
  // Include pending (unminted) reserve NFTs in the denominator, adjusted for refund-phase burns.
138
- // slither-disable-next-line calls-loop
139
219
  totalTokensForCashoutInTier += IDefifaHook(hook).adjustedPendingReservesFor(tierId);
140
220
 
141
221
  // Calculate the percentage of the tier cashOut amount a single token counts for.
@@ -178,6 +258,49 @@ library DefifaHookLib {
178
258
  }
179
259
  }
180
260
 
261
+ /// @notice Compute the cumulative mint price for a set of token IDs.
262
+ /// @param tokenIds The token IDs.
263
+ /// @param hookStore The 721 tiers hook store.
264
+ /// @param hook The hook address.
265
+ /// @param excludeReserveMints Whether reserve-minted tokens should be excluded.
266
+ /// @return cumulativeMintPrice The total mint price.
267
+ function computeCumulativeMintPriceForCashOut(
268
+ uint256[] memory tokenIds,
269
+ IJB721TiersHookStore hookStore,
270
+ address hook,
271
+ bool excludeReserveMints
272
+ )
273
+ public
274
+ view
275
+ returns (uint256 cumulativeMintPrice)
276
+ {
277
+ uint256 numberOfTokenIds = tokenIds.length;
278
+ for (uint256 i; i < numberOfTokenIds; i++) {
279
+ if (excludeReserveMints && IDefifaHook(hook).isReserveMint(tokenIds[i])) continue;
280
+ cumulativeMintPrice += hookStore.tierOfTokenId({
281
+ hook: hook, tokenId: tokenIds[i], includeResolvedUri: false
282
+ }).price;
283
+ }
284
+ }
285
+
286
+ /// @notice Compute the current supply of a tier (minted - burned).
287
+ /// @param hookStore The 721 tiers hook store.
288
+ /// @param hook The hook address.
289
+ /// @param tierId The ID of the tier.
290
+ /// @return The current supply.
291
+ function computeCurrentSupply(
292
+ IJB721TiersHookStore hookStore,
293
+ address hook,
294
+ uint256 tierId
295
+ )
296
+ public
297
+ view
298
+ returns (uint256)
299
+ {
300
+ JB721Tier memory tier = hookStore.tierOf({hook: hook, id: tierId, includeResolvedUri: false});
301
+ return tier.initialSupply - (tier.remainingSupply + hookStore.numberOfBurnedFor({hook: hook, tierId: tierId}));
302
+ }
303
+
181
304
  /// @notice Compute the claimable token amounts for a set of token IDs.
182
305
  /// @param tokenIds The token IDs.
183
306
  /// @param hookStore The 721 tiers hook store.
@@ -208,7 +331,6 @@ library DefifaHookLib {
208
331
  // Calculate the amount paid to mint the tokens that are being burned.
209
332
  uint256 cumulativeMintPrice;
210
333
  for (uint256 i; i < numberOfTokens; i++) {
211
- // slither-disable-next-line calls-loop
212
334
  cumulativeMintPrice += hookStore.tierOfTokenId({
213
335
  hook: hook, tokenId: tokenIds[i], includeResolvedUri: false
214
336
  }).price;
@@ -219,135 +341,118 @@ library DefifaHookLib {
219
341
  baseProtocolTokenAmount = mulDiv({x: baseProtocolBalance, y: cumulativeMintPrice, denominator: totalMintCost});
220
342
  }
221
343
 
222
- /// @notice Compute the cumulative mint price for a set of token IDs.
223
- /// @param tokenIds The token IDs.
344
+ /// @notice Computes the total mint cost of all pending reserve NFTs across all tiers.
224
345
  /// @param hookStore The 721 tiers hook store.
225
346
  /// @param hook The hook address.
226
- /// @return cumulativeMintPrice The total mint price.
227
- function computeCumulativeMintPrice(
228
- uint256[] memory tokenIds,
229
- IJB721TiersHookStore hookStore,
230
- address hook
231
- )
232
- public
233
- view
234
- returns (uint256 cumulativeMintPrice)
235
- {
236
- uint256 numberOfTokenIds = tokenIds.length;
237
- for (uint256 i; i < numberOfTokenIds; i++) {
238
- // slither-disable-next-line calls-loop
239
- cumulativeMintPrice += hookStore.tierOfTokenId({
240
- hook: hook, tokenId: tokenIds[i], includeResolvedUri: false
241
- }).price;
242
- }
243
- }
347
+ /// @return cost The total pending reserve mint cost.
348
+ function pendingReserveMintCost(IJB721TiersHookStore hookStore, address hook) public view returns (uint256 cost) {
349
+ // Keep a reference to the max tier ID. Tier IDs are 1-indexed, so the loop adds 1 to `i`.
350
+ uint256 numberOfTiers = hookStore.maxTierIdOf(hook);
244
351
 
245
- /// @notice Compute the cash out count for the beforeCashOutRecorded hook.
246
- /// @param gamePhase The current game phase.
247
- /// @param cumulativeMintPrice The cumulative mint price of the tokens to cash out.
248
- /// @param surplusValue The surplus value from the context.
249
- /// @param totalAmountRedeemed The amount already redeemed.
250
- /// @param cumulativeCashOutWeight The cumulative cash out weight of the tokens.
251
- /// @return cashOutCount The computed cash out count.
252
- function computeCashOutCount(
253
- DefifaGamePhase gamePhase,
254
- uint256 cumulativeMintPrice,
255
- uint256 surplusValue,
256
- uint256 totalAmountRedeemed,
257
- uint256 cumulativeCashOutWeight
258
- )
259
- public
260
- pure
261
- returns (uint256 cashOutCount)
262
- {
263
- // If the game is in its minting, refund, or no-contest phase, reclaim amount is the same as it cost to mint.
264
- if (
265
- gamePhase == DefifaGamePhase.MINT || gamePhase == DefifaGamePhase.REFUND
266
- || gamePhase == DefifaGamePhase.NO_CONTEST
267
- ) {
268
- cashOutCount = cumulativeMintPrice;
269
- } else {
270
- // If the game is in its scoring or complete phase, reclaim amount is based on the tier weights.
271
- cashOutCount = mulDiv({
272
- x: surplusValue + totalAmountRedeemed, y: cumulativeCashOutWeight, denominator: TOTAL_CASHOUT_WEIGHT
273
- });
274
- }
275
- }
352
+ for (uint256 i; i < numberOfTiers;) {
353
+ uint256 tierId = i + 1;
276
354
 
277
- /// @notice Compute the current supply of a tier (minted - burned).
278
- /// @param hookStore The 721 tiers hook store.
279
- /// @param hook The hook address.
280
- /// @param tierId The ID of the tier.
281
- /// @return The current supply.
282
- function computeCurrentSupply(
283
- IJB721TiersHookStore hookStore,
284
- address hook,
285
- uint256 tierId
286
- )
287
- public
288
- view
289
- returns (uint256)
290
- {
291
- JB721Tier memory tier = hookStore.tierOf({hook: hook, id: tierId, includeResolvedUri: false});
292
- return tier.initialSupply - (tier.remainingSupply + hookStore.numberOfBurnedFor({hook: hook, tierId: tierId}));
355
+ // Use the hook's adjusted reserve count so refund-phase burns reduce unminted reserve liabilities.
356
+ uint256 pendingReserves = IDefifaHook(hook).adjustedPendingReservesFor(tierId);
357
+
358
+ // Skip empty tiers to avoid an unnecessary store read.
359
+ if (pendingReserves != 0) {
360
+ JB721Tier memory tier = hookStore.tierOf({hook: hook, id: tierId, includeResolvedUri: false});
361
+
362
+ // Pending reserves claim fee tokens at the tier price once minted, so include them in the denominator.
363
+ cost += pendingReserves * tier.price;
364
+ }
365
+
366
+ unchecked {
367
+ ++i;
368
+ }
369
+ }
293
370
  }
294
371
 
295
- /// @notice Computes the attestation units for tiers during payment processing.
296
- /// @dev Returns parallel arrays: tier IDs, cumulative attestation units per tier, and whether to switch delegate.
297
- /// @param tierIdsToMint The tier IDs to mint (must be in ascending order).
372
+ /// @notice Validates tier cash out weights and returns the weight array to store.
373
+ /// @param tierWeights The tier weights to validate and set.
298
374
  /// @param hookStore The 721 tiers hook store.
299
375
  /// @param hook The hook address.
300
- /// @return tierIds The unique tier IDs.
301
- /// @return attestationAmounts The cumulative attestation units for each unique tier.
302
- /// @return count The number of unique tiers.
303
- function computeAttestationUnits(
304
- uint16[] memory tierIdsToMint,
376
+ /// @return weights The 128-element array of validated weights.
377
+ function validateAndBuildWeights(
378
+ DefifaTierCashOutWeight[] memory tierWeights,
305
379
  IJB721TiersHookStore hookStore,
306
380
  address hook
307
381
  )
308
382
  public
309
383
  view
310
- returns (uint256[] memory tierIds, uint256[] memory attestationAmounts, uint256 count)
384
+ returns (uint256[128] memory weights)
311
385
  {
312
- uint256 numberOfTiers = tierIdsToMint.length;
313
- tierIds = new uint256[](numberOfTiers);
314
- attestationAmounts = new uint256[](numberOfTiers);
386
+ // Keep a reference to the max tier ID.
387
+ uint256 maxTierId = hookStore.maxTierIdOf(hook);
315
388
 
316
- if (numberOfTiers == 0) return (tierIds, attestationAmounts, 0);
389
+ // Keep a reference to the cumulative amounts.
390
+ uint256 cumulativeCashOutWeight;
317
391
 
318
- uint256 currentTierId;
319
- uint256 attestationUnits;
320
- uint256 accumulated;
392
+ // Keep a reference to the number of tier weights.
393
+ uint256 numberOfTierWeights = tierWeights.length;
321
394
 
322
- for (uint256 i; i < numberOfTiers;) {
323
- if (currentTierId != tierIdsToMint[i]) {
324
- // Flush accumulated units for previous tier.
325
- if (currentTierId != 0) {
326
- tierIds[count] = currentTierId;
327
- attestationAmounts[count] = accumulated;
328
- count++;
329
- }
330
- if (tierIdsToMint[i] < currentTierId) revert DefifaHook_BadTierOrder();
331
- currentTierId = tierIdsToMint[i];
332
- // slither-disable-next-line calls-loop
333
- attestationUnits =
334
- hookStore.tierOf({hook: hook, id: currentTierId, includeResolvedUri: false}).votingUnits;
335
- accumulated = attestationUnits;
336
- } else {
337
- accumulated += attestationUnits;
395
+ // Keep a reference to the tier being iterated on.
396
+ JB721Tier memory tier;
397
+
398
+ // Keep a reference to the last tier ID to enforce ascending order (no duplicates).
399
+ uint256 lastTierId;
400
+
401
+ for (uint256 i; i < numberOfTierWeights;) {
402
+ // Enforce strict ascending order to prevent duplicate tier IDs.
403
+ if (tierWeights[i].id <= lastTierId && i != 0) {
404
+ revert DefifaHook_BadTierOrder({previousTierId: lastTierId, tierId: tierWeights[i].id});
405
+ }
406
+ lastTierId = tierWeights[i].id;
407
+
408
+ // Get the tier.
409
+ tier = hookStore.tierOf({hook: hook, id: tierWeights[i].id, includeResolvedUri: false});
410
+
411
+ // Guard against uint32 truncation: if the caller passes a tier ID > type(uint32).max,
412
+ // the store may silently truncate and return a different tier.
413
+ if (tierWeights[i].id != tier.id) {
414
+ revert DefifaHook_InvalidTierId({
415
+ tierId: tierWeights[i].id, actualTierId: tier.id, maxTierId: maxTierId, category: tier.category
416
+ });
417
+ }
418
+
419
+ // Can't set a cashOut weight for tiers not in category 0.
420
+ if (tier.category != 0) {
421
+ revert DefifaHook_InvalidTierId({
422
+ tierId: tierWeights[i].id, actualTierId: tier.id, maxTierId: maxTierId, category: tier.category
423
+ });
338
424
  }
425
+
426
+ // Attempting to set the cashOut weight for a tier that does not exist (yet) reverts.
427
+ if (tier.id > maxTierId) {
428
+ revert DefifaHook_InvalidTierId({
429
+ tierId: tierWeights[i].id, actualTierId: tier.id, maxTierId: maxTierId, category: tier.category
430
+ });
431
+ }
432
+
433
+ // Save the tier weight. Tiers are 1 indexed and should be stored 0 indexed.
434
+ weights[tier.id - 1] = tierWeights[i].cashOutWeight;
435
+
436
+ // Increment the cumulative amount.
437
+ cumulativeCashOutWeight += tierWeights[i].cashOutWeight;
438
+
339
439
  unchecked {
340
440
  ++i;
341
441
  }
342
442
  }
343
- // Flush the last tier.
344
- if (currentTierId != 0) {
345
- tierIds[count] = currentTierId;
346
- attestationAmounts[count] = accumulated;
347
- count++;
443
+
444
+ // Make sure the cumulative amount is exactly the total cashOut weight.
445
+ if (cumulativeCashOutWeight != TOTAL_CASHOUT_WEIGHT) {
446
+ revert DefifaHook_InvalidCashoutWeights({
447
+ totalWeight: cumulativeCashOutWeight, expectedWeight: TOTAL_CASHOUT_WEIGHT
448
+ });
348
449
  }
349
450
  }
350
451
 
452
+ //*********************************************************************//
453
+ // ----------------------- public transactions ----------------------- //
454
+ //*********************************************************************//
455
+
351
456
  /// @notice Claims the defifa and base protocol tokens for a beneficiary.
352
457
  /// @dev Executes via delegatecall, so `address(this)` is the calling contract. Transfers are from the hook's
353
458
  /// balance.
@@ -380,7 +485,12 @@ library DefifaHookLib {
380
485
  if (defifaAmount != 0) defifaToken.safeTransfer({to: beneficiary, value: defifaAmount});
381
486
  if (baseProtocolAmount != 0) baseProtocolToken.safeTransfer({to: beneficiary, value: baseProtocolAmount});
382
487
 
383
- emit ClaimedTokens(beneficiary, defifaAmount, baseProtocolAmount, msg.sender);
488
+ emit ClaimedTokens({
489
+ beneficiary: beneficiary,
490
+ defifaTokenAmount: defifaAmount,
491
+ baseProtocolTokenAmount: baseProtocolAmount,
492
+ caller: msg.sender
493
+ });
384
494
 
385
495
  return (defifaAmount != 0 || baseProtocolAmount != 0);
386
496
  }