@bananapus/721-hook-v6 0.0.62 → 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/README.md +2 -0
- package/package.json +1 -1
- package/references/operations.md +1 -0
- package/src/JB721Checkpoints.sol +54 -5
- package/src/JB721TiersHook.sol +3 -3
- package/src/JB721TiersHookStore.sol +47 -68
- package/src/abstract/JB721Hook.sol +3 -3
- package/src/interfaces/IJB721Checkpoints.sol +9 -0
- package/src/interfaces/IJB721TiersHookStore.sol +1 -1
- package/test/utils/ForTest_JB721TiersHook.sol +32 -0
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ This repo does more than "mint NFTs on pay." It changes how payment value, tier
|
|
|
34
34
|
| `JB721TiersHookDeployer` | Clone factory for deploying a hook for an existing project. |
|
|
35
35
|
| `JB721TiersHookProjectDeployer` | Convenience deployer for launching a project with a hook already wired in. |
|
|
36
36
|
| `JB721Hook` | Abstract base for 721 pay and cash-out hook behavior. |
|
|
37
|
+
| `JB721Checkpoints` | Per-hook IVotes checkpoint module. Tracks historical owner checkpoints plus per-tier eligible voting units (`getPastTierVotingUnits`) for tier-scoped reward distribution. |
|
|
37
38
|
|
|
38
39
|
## Mental Model
|
|
39
40
|
|
|
@@ -59,6 +60,7 @@ If a bug affects supply, reserve minting, or tier lookup, it usually lives in th
|
|
|
59
60
|
- custom token URI resolvers should be treated as part of the trusted surface
|
|
60
61
|
- adding a 721 hook through a deployer is easy; carrying the right ruleset behavior forward is where mistakes happen
|
|
61
62
|
- projects should be explicit about whether the hook affects pay, cash out, or only metadata-facing paths
|
|
63
|
+
- per-tier eligible voting units are queryable via `getPastTierVotingUnits(tierId, blockNumber)` for tier-scoped reward denominators, but minting alone does not enroll a token: a token only counts toward that total once it is enrolled (`delegate(address, uint256[])`) or transferred for the first time, and stops counting when burned
|
|
62
64
|
|
|
63
65
|
## Where State Lives
|
|
64
66
|
|
package/package.json
CHANGED
package/references/operations.md
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
- If you edit tier config or metadata behavior, inspect the corresponding structs and interfaces in `src/structs/` and `src/interfaces/`.
|
|
13
13
|
- If you edit reserve behavior, verify pending reserve counts, default reserve beneficiary semantics, and cash-out denominator effects together.
|
|
14
14
|
- If you edit discount behavior, verify mint price and cash-out weight separately. They are intentionally not the same quantity.
|
|
15
|
+
- If you touch checkpoint, `onTransfer`, or `delegate` behavior, verify the per-tier eligible-voting-units trace (`_tierEligibleUnitsOf`, read via `getPastTierVotingUnits`) still moves only on eligibility changes: increment on enrollment or a token's first transfer, decrement on burn, and nothing on mint (so the mint path keeps its zero added checkpoint gas). Keep it in lockstep with `ownerOfAt` eligibility.
|
|
15
16
|
- If you touch permissions, verify the caller path and permission constants still line up with the downstream ecosystem package that defines them.
|
|
16
17
|
- If you touch URI behavior, confirm whether the issue belongs in this repo or in a downstream resolver contract that the hook calls.
|
|
17
18
|
|
package/src/JB721Checkpoints.sol
CHANGED
|
@@ -50,6 +50,12 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
|
|
|
50
50
|
/// @custom:param tokenId The token ID to get historical owner checkpoints for.
|
|
51
51
|
mapping(uint256 tokenId => Checkpoints.Trace160) internal _ownerCheckpointsOf;
|
|
52
52
|
|
|
53
|
+
/// @notice Checkpointed total eligible voting units per tier. A token contributes its tier voting units from the
|
|
54
|
+
/// block it first gains an owner checkpoint (enrollment or first transfer) until it is burned. Mints write
|
|
55
|
+
/// nothing, mirroring `_ownerCheckpointsOf` eligibility. Distributors read this as the tier-scoped denominator.
|
|
56
|
+
/// @custom:param tierId The tier to get the historical eligible voting units for.
|
|
57
|
+
mapping(uint256 tierId => Checkpoints.Trace160) internal _tierEligibleUnitsOf;
|
|
58
|
+
|
|
53
59
|
//*********************************************************************//
|
|
54
60
|
// -------------------------- constructor ---------------------------- //
|
|
55
61
|
//*********************************************************************//
|
|
@@ -85,10 +91,15 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
|
|
|
85
91
|
revert JB721Checkpoints_NotOwner({tokenId: tokenId, caller: msg.sender});
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
// Write an owner checkpoint if the token has none yet.
|
|
94
|
+
// Write an owner checkpoint if the token has none yet, and enroll its tier voting units.
|
|
89
95
|
if (_ownerCheckpointsOf[tokenId].length() == 0) {
|
|
90
96
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
91
97
|
_ownerCheckpointsOf[tokenId].push({key: uint96(block.number), value: uint160(msg.sender)});
|
|
98
|
+
_updateTierEligibleUnits({
|
|
99
|
+
tierId: STORE.tierIdOfToken(tokenId),
|
|
100
|
+
amount: STORE.tierVotingUnitsOfTokenId({hook: hook, tokenId: tokenId}),
|
|
101
|
+
increase: true
|
|
102
|
+
});
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
unchecked {
|
|
@@ -114,14 +125,31 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
|
|
|
114
125
|
function onTransfer(address from, address to, uint256 tokenId) external override {
|
|
115
126
|
if (msg.sender != hook) revert JB721Checkpoints_Unauthorized({caller: msg.sender, hook: hook});
|
|
116
127
|
|
|
128
|
+
// Look up this token's tier voting units (lightweight getter — avoids full tier struct construction).
|
|
129
|
+
uint256 votingUnits = STORE.tierVotingUnitsOfTokenId({hook: hook, tokenId: tokenId});
|
|
130
|
+
|
|
131
|
+
// On mint (`from == 0`) nothing is checkpointed: the token is ineligible until enrolled or transferred,
|
|
132
|
+
// so neither the owner trace nor the per-tier eligible-units trace is written.
|
|
117
133
|
if (from != address(0)) {
|
|
134
|
+
Checkpoints.Trace160 storage ownerTrace = _ownerCheckpointsOf[tokenId];
|
|
135
|
+
bool wasEligible = ownerTrace.length() != 0;
|
|
136
|
+
|
|
118
137
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
119
|
-
|
|
138
|
+
ownerTrace.push({key: uint96(block.number), value: uint160(to)});
|
|
139
|
+
|
|
140
|
+
if (to == address(0)) {
|
|
141
|
+
// Burn: remove the tier's units only if the token was already eligible.
|
|
142
|
+
if (wasEligible) {
|
|
143
|
+
_updateTierEligibleUnits({
|
|
144
|
+
tierId: STORE.tierIdOfToken(tokenId), amount: votingUnits, increase: false
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
} else if (!wasEligible) {
|
|
148
|
+
// First transfer of a never-enrolled token makes it eligible: add the tier's units.
|
|
149
|
+
_updateTierEligibleUnits({tierId: STORE.tierIdOfToken(tokenId), amount: votingUnits, increase: true});
|
|
150
|
+
}
|
|
120
151
|
}
|
|
121
152
|
|
|
122
|
-
// Look up this token's tier voting units (lightweight getter — avoids full tier struct construction).
|
|
123
|
-
uint256 votingUnits = STORE.tierVotingUnitsOfTokenId({hook: hook, tokenId: tokenId});
|
|
124
|
-
|
|
125
153
|
// Move checkpointed voting power from the previous owner to the new owner.
|
|
126
154
|
_transferVotingUnits({from: from, to: to, amount: votingUnits});
|
|
127
155
|
}
|
|
@@ -130,6 +158,12 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
|
|
|
130
158
|
// ----------------------- external views ---------------------------- //
|
|
131
159
|
//*********************************************************************//
|
|
132
160
|
|
|
161
|
+
/// @inheritdoc IJB721Checkpoints
|
|
162
|
+
function getPastTierVotingUnits(uint256 tierId, uint256 blockNumber) external view override returns (uint256) {
|
|
163
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
164
|
+
return _tierEligibleUnitsOf[tierId].upperLookupRecent(uint96(_validateTimepoint(blockNumber)));
|
|
165
|
+
}
|
|
166
|
+
|
|
133
167
|
/// @notice The owner of an NFT at a past block.
|
|
134
168
|
/// @dev Returns `address(0)` for tokens that have never been enrolled (via `delegate(address, uint256[])`) or
|
|
135
169
|
/// transferred. Unenrolled tokens are ineligible for snapshot-based distribution.
|
|
@@ -163,4 +197,19 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
|
|
|
163
197
|
function _getVotingUnits(address account) internal view override returns (uint256) {
|
|
164
198
|
return STORE.votingUnitsOf({hook: hook, account: account});
|
|
165
199
|
}
|
|
200
|
+
|
|
201
|
+
//*********************************************************************//
|
|
202
|
+
// ------------------------ private helpers -------------------------- //
|
|
203
|
+
//*********************************************************************//
|
|
204
|
+
|
|
205
|
+
/// @notice Add or remove units from a tier's eligible-voting-units checkpoint at the current block.
|
|
206
|
+
/// @param tierId The tier whose eligible-voting-units trace to update.
|
|
207
|
+
/// @param amount The voting units to add or remove.
|
|
208
|
+
/// @param increase Whether to add `amount`; if false, `amount` is removed.
|
|
209
|
+
function _updateTierEligibleUnits(uint256 tierId, uint256 amount, bool increase) private {
|
|
210
|
+
Checkpoints.Trace160 storage trace = _tierEligibleUnitsOf[tierId];
|
|
211
|
+
uint256 updated = increase ? trace.latest() + amount : trace.latest() - amount;
|
|
212
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
213
|
+
trace.push({key: uint96(block.number), value: uint160(updated)});
|
|
214
|
+
}
|
|
166
215
|
}
|
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
|
|
|
@@ -8,6 +8,15 @@ import {IJB721TiersHookStore} from "./IJB721TiersHookStore.sol";
|
|
|
8
8
|
/// @dev Deployed as a clone via JB721CheckpointsDeployer during hook initialization. One module per hook.
|
|
9
9
|
/// Pass this address to JBTokenDistributor as the IVotes token.
|
|
10
10
|
interface IJB721Checkpoints is IERC5805 {
|
|
11
|
+
/// @notice The total eligible voting units of a tier at a past block.
|
|
12
|
+
/// @dev "Eligible" means a token has an owner checkpoint: it was enrolled via `delegate(address,uint256[])` or
|
|
13
|
+
/// transferred at least once. Minted-but-unenrolled tokens are excluded, mirroring `ownerOfAt` eligibility, and
|
|
14
|
+
/// mints never write to this trace. Distributors use this as the denominator for tier-scoped reward pots.
|
|
15
|
+
/// @param tierId The tier to get the eligible voting units of.
|
|
16
|
+
/// @param blockNumber The block number to look up (must be strictly in the past).
|
|
17
|
+
/// @return The tier's eligible voting units at `blockNumber`.
|
|
18
|
+
function getPastTierVotingUnits(uint256 tierId, uint256 blockNumber) external view returns (uint256);
|
|
19
|
+
|
|
11
20
|
/// @notice The hook that this module tracks voting power for.
|
|
12
21
|
/// @return The hook address.
|
|
13
22
|
// forge-lint: disable-next-line(mixed-case-function)
|
|
@@ -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)
|