@bananapus/721-hook-v6 0.0.63 → 0.0.64
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/package.json
CHANGED
package/src/JB721TiersHook.sol
CHANGED
|
@@ -222,7 +222,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
222
222
|
});
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
/// @notice The combined cash-out weight of specific NFTs. Divide by `
|
|
225
|
+
/// @notice The combined cash-out weight of specific NFTs. Divide by `totalCashOutWeightOf()` to get the fraction of
|
|
226
226
|
/// surplus these NFTs can reclaim. Weight is based on the original tier price, not any discount paid.
|
|
227
227
|
/// @param tokenIds The token IDs of the NFTs to get the combined cash-out weight of.
|
|
228
228
|
/// @return weight The combined cash-out weight.
|
|
@@ -334,8 +334,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
334
334
|
/// @notice The total cash-out weight across all outstanding NFTs and pending reserves. This is the denominator
|
|
335
335
|
/// for cash-out calculations — an NFT's share of the surplus is its weight divided by this total.
|
|
336
336
|
/// @return weight The total cash-out weight.
|
|
337
|
-
function
|
|
338
|
-
return STORE.
|
|
337
|
+
function totalCashOutWeightOf() public view virtual override returns (uint256) {
|
|
338
|
+
return STORE.totalCashOutWeightOf(address(this));
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
//*********************************************************************//
|
|
@@ -61,6 +61,14 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
61
61
|
// --------------------- public stored properties -------------------- //
|
|
62
62
|
//*********************************************************************//
|
|
63
63
|
|
|
64
|
+
/// @notice How many NFTs an address owns from a hook, totaled across all tiers, as a running aggregate so this
|
|
65
|
+
/// is O(1) instead of O(maxTierId).
|
|
66
|
+
/// @dev Maintained in `recordTransferForTier` (a mint increments the receiver, a burn decrements the sender, a
|
|
67
|
+
/// transfer does both), so an attacker spamming empty tiers cannot make this read run out of gas.
|
|
68
|
+
/// @custom:param hook The 721 contract to check.
|
|
69
|
+
/// @custom:param owner The address to get the balance of.
|
|
70
|
+
mapping(address hook => mapping(address owner => uint256)) public override balanceOf;
|
|
71
|
+
|
|
64
72
|
/// @notice Returns the default reserve beneficiary for the provided 721 contract.
|
|
65
73
|
/// @dev If a tier has a reserve beneficiary set, it will override this value.
|
|
66
74
|
/// @custom:param hook The 721 contract to get the default reserve beneficiary of.
|
|
@@ -102,6 +110,16 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
102
110
|
/// @custom:param hook The 721 contract to get the custom token URI resolver of.
|
|
103
111
|
mapping(address hook => IJB721TokenUriResolver) public override tokenUriResolverOf;
|
|
104
112
|
|
|
113
|
+
/// @notice The combined cash-out weight of all of a hook's NFTs, as a running aggregate so cash-out pricing is
|
|
114
|
+
/// O(1) instead of O(maxTierId).
|
|
115
|
+
/// @dev Maintained incrementally in `recordMint` (+ the tier's full price for the new outstanding NFT plus any
|
|
116
|
+
/// newly-accrued pending reserve) and `recordBurn` (- the tier's full price). It is invariant under everything
|
|
117
|
+
/// else: reserve mints are weight-neutral (a pending reserve becomes an outstanding NFT), removed tiers keep
|
|
118
|
+
/// their already-minted weight, and newly added (unminted) tiers contribute zero — so spamming empty tiers can
|
|
119
|
+
/// never grow this read or its cost. Uses the full tier price, not the discounted price (see `cashOutWeightOf`).
|
|
120
|
+
/// @custom:param hook The 721 contract to get the total cash-out weight of.
|
|
121
|
+
mapping(address hook => uint256) public override totalCashOutWeightOf;
|
|
122
|
+
|
|
105
123
|
//*********************************************************************//
|
|
106
124
|
// --------------------- internal stored properties ------------------ //
|
|
107
125
|
//*********************************************************************//
|
|
@@ -467,26 +485,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
467
485
|
// -------------------------- public views --------------------------- //
|
|
468
486
|
//*********************************************************************//
|
|
469
487
|
|
|
470
|
-
/// @notice
|
|
471
|
-
/// @param hook The 721 hook contract to check.
|
|
472
|
-
/// @param owner The address to check the balance of.
|
|
473
|
-
/// @return balance The total number of NFTs the owner holds.
|
|
474
|
-
function balanceOf(address hook, address owner) public view override returns (uint256 balance) {
|
|
475
|
-
// Keep a reference to the greatest tier ID.
|
|
476
|
-
uint256 maxTierId = maxTierIdOf[hook];
|
|
477
|
-
|
|
478
|
-
// Loop through all tiers.
|
|
479
|
-
for (uint256 i = maxTierId; i != 0;) {
|
|
480
|
-
// Get a reference to the account's balance within this tier.
|
|
481
|
-
balance += tierBalanceOf[hook][owner][i];
|
|
482
|
-
|
|
483
|
-
unchecked {
|
|
484
|
-
--i;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/// @notice The combined cash-out weight of specific NFTs. Divide by `totalCashOutWeight` to get the fraction of
|
|
488
|
+
/// @notice The combined cash-out weight of specific NFTs. Divide by `totalCashOutWeightOf` to get the fraction of
|
|
490
489
|
/// the project's surplus that cashing out these NFTs would reclaim.
|
|
491
490
|
/// @dev Weight is based on each NFT's original tier price (not the discounted price paid). Discounts are
|
|
492
491
|
/// transient purchase incentives and don't affect an NFT's share of the cash-out pool.
|
|
@@ -547,48 +546,6 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
547
546
|
|
|
548
547
|
/// @notice The total cash-out weight across all outstanding NFTs (including pending reserves). This is the
|
|
549
548
|
/// denominator used to determine what fraction of a project's surplus each NFT can reclaim on cash out.
|
|
550
|
-
/// @param hook The 721 hook contract to get the total cash-out weight of.
|
|
551
|
-
/// @return weight The total cash-out weight.
|
|
552
|
-
// Changing defaultReserveBeneficiary retroactively affects totalCashOutWeight. By design —
|
|
553
|
-
// cashOutWeight is calculated dynamically, not snapshotted. The defaultReserveBeneficiary determines which
|
|
554
|
-
// tiers have pending reserves (via _numberOfPendingReservesFor), affecting the denominator. Changing it is
|
|
555
|
-
// an admin action that naturally affects future cash out calculations. Snapshotting would add storage
|
|
556
|
-
// overhead and complexity.
|
|
557
|
-
function totalCashOutWeight(address hook) public view override returns (uint256 weight) {
|
|
558
|
-
// Keep a reference to the greatest tier ID.
|
|
559
|
-
uint256 maxTierId = maxTierIdOf[hook];
|
|
560
|
-
|
|
561
|
-
// Add each 721's original price (from its tier) to the weight.
|
|
562
|
-
// Uses the full tier price, not the discounted price — by design. See `cashOutWeightOf` for rationale.
|
|
563
|
-
for (uint256 i = 1; i <= maxTierId;) {
|
|
564
|
-
// Keep a reference to the stored tier.
|
|
565
|
-
JBStored721Tier memory storedTier = _storedTierOf[hook][i];
|
|
566
|
-
|
|
567
|
-
// Skip empty tiers (zero mints and zero burns) — they contribute zero weight.
|
|
568
|
-
if (storedTier.initialSupply == storedTier.remainingSupply && numberOfBurnedFor[hook][i] == 0) {
|
|
569
|
-
unchecked {
|
|
570
|
-
++i;
|
|
571
|
-
}
|
|
572
|
-
continue;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// Add the tier's price multiplied by the number of minted NFTs plus pending reserves.
|
|
576
|
-
// Pending reserves are included by design — they represent committed obligations that will be
|
|
577
|
-
// minted to the reserve beneficiary. Including them in the denominator ensures cash-out values
|
|
578
|
-
// account for the full diluted supply, preventing early cashers from extracting more than their
|
|
579
|
-
// fair share before reserves are minted.
|
|
580
|
-
// Note: removed tiers are NOT skipped here because minted NFTs from removed tiers still carry
|
|
581
|
-
// cash-out weight, and their pending reserves can still be minted.
|
|
582
|
-
weight += storedTier.price
|
|
583
|
-
* ((storedTier.initialSupply - (storedTier.remainingSupply + numberOfBurnedFor[hook][i]))
|
|
584
|
-
+ _numberOfPendingReservesFor({hook: hook, tierId: i, storedTier: storedTier}));
|
|
585
|
-
|
|
586
|
-
unchecked {
|
|
587
|
-
++i;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
549
|
//*********************************************************************//
|
|
593
550
|
// -------------------------- internal views ------------------------- //
|
|
594
551
|
//*********************************************************************//
|
|
@@ -1189,6 +1146,12 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1189
1146
|
// Increment the number of NFTs burned from the tier.
|
|
1190
1147
|
numberOfBurnedFor[msg.sender][tierId]++;
|
|
1191
1148
|
|
|
1149
|
+
// Maintain the running cash-out weight: a burn removes one outstanding NFT (weighted by the tier's full
|
|
1150
|
+
// price). Pending reserves are unaffected — they track non-reserve MINTS, which a burn does not change.
|
|
1151
|
+
unchecked {
|
|
1152
|
+
totalCashOutWeightOf[msg.sender] -= _storedTierOf[msg.sender][tierId].price;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1192
1155
|
unchecked {
|
|
1193
1156
|
++i;
|
|
1194
1157
|
}
|
|
@@ -1282,6 +1245,10 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1282
1245
|
// Make sure there's at least one NFT remaining to mint.
|
|
1283
1246
|
if (storedTier.remainingSupply == 0) revert JB721TiersHookStore_InsufficientSupplyRemaining(tierId);
|
|
1284
1247
|
|
|
1248
|
+
// Pending reserves before this mint, captured to maintain the O(1) cash-out weight aggregate.
|
|
1249
|
+
uint256 pendingReservesBefore =
|
|
1250
|
+
_numberOfPendingReservesFor({hook: msg.sender, tierId: tierId, storedTier: storedTier});
|
|
1251
|
+
|
|
1285
1252
|
// Mint the 721 — decrement remaining supply first so the reserve check below
|
|
1286
1253
|
// sees the correct post-mint non-reserve-mint count.
|
|
1287
1254
|
unchecked {
|
|
@@ -1292,14 +1259,24 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1292
1259
|
leftoverAmount = leftoverAmount - price;
|
|
1293
1260
|
}
|
|
1294
1261
|
|
|
1262
|
+
// Pending reserves after this mint (also used for the supply check below). A non-reserve mint can only
|
|
1263
|
+
// increase pending reserves, by at most one, so the difference is 0 or 1.
|
|
1264
|
+
uint256 pendingReservesAfter =
|
|
1265
|
+
_numberOfPendingReservesFor({hook: msg.sender, tierId: tierId, storedTier: storedTier});
|
|
1266
|
+
|
|
1295
1267
|
// Make sure there are still enough NFTs remaining to satisfy pending reserves.
|
|
1296
|
-
if (
|
|
1297
|
-
storedTier.remainingSupply
|
|
1298
|
-
< _numberOfPendingReservesFor({hook: msg.sender, tierId: tierId, storedTier: storedTier})
|
|
1299
|
-
) {
|
|
1268
|
+
if (storedTier.remainingSupply < pendingReservesAfter) {
|
|
1300
1269
|
revert JB721TiersHookStore_InsufficientSupplyRemaining({tierId: tierId});
|
|
1301
1270
|
}
|
|
1302
1271
|
|
|
1272
|
+
// Maintain the running cash-out weight: this mint adds one outstanding NFT plus any newly-accrued pending
|
|
1273
|
+
// reserve, each weighted by the tier's FULL price (the cash-out weight uses the full price, not the
|
|
1274
|
+
// discounted one). Keeps `totalCashOutWeightOf` O(1) regardless of the tier count.
|
|
1275
|
+
unchecked {
|
|
1276
|
+
totalCashOutWeightOf[msg.sender] += storedTier.price
|
|
1277
|
+
* (1 + (pendingReservesAfter - pendingReservesBefore));
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1303
1280
|
unchecked {
|
|
1304
1281
|
++i;
|
|
1305
1282
|
}
|
|
@@ -1443,15 +1420,17 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1443
1420
|
function recordTransferForTier(uint256 tierId, address from, address to) external override {
|
|
1444
1421
|
// If this is not a mint,
|
|
1445
1422
|
if (from != address(0)) {
|
|
1446
|
-
// then subtract the tier balance from the sender
|
|
1423
|
+
// then subtract the tier balance from the sender, and the running per-owner balance read by `balanceOf`.
|
|
1447
1424
|
--tierBalanceOf[msg.sender][from][tierId];
|
|
1425
|
+
--balanceOf[msg.sender][from];
|
|
1448
1426
|
}
|
|
1449
1427
|
|
|
1450
1428
|
// If this is not a burn,
|
|
1451
1429
|
if (to != address(0)) {
|
|
1452
1430
|
unchecked {
|
|
1453
|
-
// then increase the tier balance for the receiver.
|
|
1431
|
+
// then increase the tier balance for the receiver, and the running per-owner balance.
|
|
1454
1432
|
++tierBalanceOf[msg.sender][to][tierId];
|
|
1433
|
+
++balanceOf[msg.sender][to];
|
|
1455
1434
|
}
|
|
1456
1435
|
}
|
|
1457
1436
|
}
|
|
@@ -117,7 +117,7 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
|
117
117
|
cashOutCount = cashOutWeightOf(decodedTokenIds);
|
|
118
118
|
|
|
119
119
|
// Use the total cash out weight of the 721s.
|
|
120
|
-
totalSupply =
|
|
120
|
+
totalSupply = totalCashOutWeightOf();
|
|
121
121
|
|
|
122
122
|
// Use the surplus from the context.
|
|
123
123
|
effectiveSurplusValue = context.surplus.value;
|
|
@@ -156,7 +156,7 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
|
156
156
|
//*********************************************************************//
|
|
157
157
|
|
|
158
158
|
/// @notice Returns the cumulative cash out weight of the specified token IDs relative to the
|
|
159
|
-
/// `
|
|
159
|
+
/// `totalCashOutWeightOf`.
|
|
160
160
|
/// @param tokenIds The NFT token IDs to calculate the cumulative cash out weight of.
|
|
161
161
|
/// @return The cumulative cash out weight of the specified token IDs.
|
|
162
162
|
function cashOutWeightOf(uint256[] memory tokenIds) public view virtual returns (uint256) {
|
|
@@ -176,7 +176,7 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
|
176
176
|
|
|
177
177
|
/// @notice Calculates the cumulative cash out weight of all NFT token IDs.
|
|
178
178
|
/// @return The total cumulative cash out weight of all NFT token IDs.
|
|
179
|
-
function
|
|
179
|
+
function totalCashOutWeightOf() public view virtual returns (uint256) {
|
|
180
180
|
return 0;
|
|
181
181
|
}
|
|
182
182
|
|
|
@@ -190,7 +190,7 @@ interface IJB721TiersHookStore {
|
|
|
190
190
|
/// @notice The combined cash out weight for all NFTs from the provided 721 contract.
|
|
191
191
|
/// @param hook The 721 contract to get the total cash out weight of.
|
|
192
192
|
/// @return weight The total cash out weight.
|
|
193
|
-
function
|
|
193
|
+
function totalCashOutWeightOf(address hook) external view returns (uint256 weight);
|
|
194
194
|
|
|
195
195
|
/// @notice The total number of NFTs minted from the provided 721 contract.
|
|
196
196
|
/// @param hook The 721 contract to get a total supply of.
|
|
@@ -198,9 +198,31 @@ contract ForTest_JB721TiersHookStore is JB721TiersHookStore, IJB721TiersHookStor
|
|
|
198
198
|
tierId = _lastSortedTierIdOf(nft);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
/// @dev The cash-out weight contribution of a single tier, using the same formula as the production aggregate, so
|
|
202
|
+
/// the `ForTest_*` manufacturing setters can keep the O(1) `totalCashOutWeightOf` aggregate consistent.
|
|
203
|
+
function _forTestTierWeight(
|
|
204
|
+
address hook,
|
|
205
|
+
uint256 index,
|
|
206
|
+
JBStored721Tier memory tier
|
|
207
|
+
)
|
|
208
|
+
internal
|
|
209
|
+
view
|
|
210
|
+
returns (uint256)
|
|
211
|
+
{
|
|
212
|
+
if (tier.initialSupply == tier.remainingSupply && numberOfBurnedFor[hook][index] == 0) return 0;
|
|
213
|
+
return tier.price
|
|
214
|
+
* ((tier.initialSupply - (tier.remainingSupply + numberOfBurnedFor[hook][index]))
|
|
215
|
+
+ _numberOfPendingReservesFor({hook: hook, tierId: index, storedTier: tier}));
|
|
216
|
+
}
|
|
217
|
+
|
|
201
218
|
// forge-lint: disable-next-line(mixed-case-function)
|
|
202
219
|
function ForTest_setTier(address hook, uint256 index, JBStored721Tier calldata newTier) public override {
|
|
220
|
+
// Keep the O(1) `totalCashOutWeightOf` aggregate in sync with the manufactured tier state.
|
|
221
|
+
totalCashOutWeightOf[
|
|
222
|
+
address(hook)
|
|
223
|
+
] -= _forTestTierWeight(address(hook), index, _storedTierOf[address(hook)][index]);
|
|
203
224
|
_storedTierOf[address(hook)][index] = newTier;
|
|
225
|
+
totalCashOutWeightOf[address(hook)] += _forTestTierWeight(address(hook), index, newTier);
|
|
204
226
|
}
|
|
205
227
|
|
|
206
228
|
// forge-lint: disable-next-line(mixed-case-function)
|
|
@@ -210,12 +232,22 @@ contract ForTest_JB721TiersHookStore is JB721TiersHookStore, IJB721TiersHookStor
|
|
|
210
232
|
|
|
211
233
|
// forge-lint: disable-next-line(mixed-case-function)
|
|
212
234
|
function ForTest_setBalanceOf(address hook, address holder, uint256 tier, uint256 balance) public override {
|
|
235
|
+
// Keep the O(1) `balanceOf` aggregate in sync with the manufactured per-tier balance.
|
|
236
|
+
balanceOf[address(hook)][holder] =
|
|
237
|
+
balanceOf[address(hook)][holder] - tierBalanceOf[address(hook)][holder][tier] + balance;
|
|
213
238
|
tierBalanceOf[address(hook)][holder][tier] = balance;
|
|
214
239
|
}
|
|
215
240
|
|
|
216
241
|
// forge-lint: disable-next-line(mixed-case-function)
|
|
217
242
|
function ForTest_setReservesMintedFor(address hook, uint256 tier, uint256 amount) public override {
|
|
243
|
+
// Reserve count changes pending reserves, which feed `totalCashOutWeightOf`; keep the aggregate in sync.
|
|
244
|
+
totalCashOutWeightOf[
|
|
245
|
+
address(hook)
|
|
246
|
+
] -= _forTestTierWeight(address(hook), tier, _storedTierOf[address(hook)][tier]);
|
|
218
247
|
numberOfReservesMintedFor[address(hook)][tier] = amount;
|
|
248
|
+
totalCashOutWeightOf[
|
|
249
|
+
address(hook)
|
|
250
|
+
] += _forTestTierWeight(address(hook), tier, _storedTierOf[address(hook)][tier]);
|
|
219
251
|
}
|
|
220
252
|
|
|
221
253
|
// forge-lint: disable-next-line(mixed-case-function)
|