@bananapus/721-hook-v6 0.0.61 → 0.0.63
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
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 to get its voting units.
|
|
123
|
-
uint256 votingUnits = STORE.tierOfTokenId({hook: hook, tokenId: tokenId, includeResolvedUri: false}).votingUnits;
|
|
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
|
}
|
|
@@ -261,6 +261,24 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
261
261
|
transfersPausable = (storedTier.packedBools & 0x2) != 0;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
/// @notice Get only the per-unit voting value for a token's tier, avoiding full struct construction.
|
|
265
|
+
/// @dev Mirrors the `votingUnits` field computed in `_getTierFrom`: a tier with custom voting units configured
|
|
266
|
+
/// (the `useVotingUnits` flag, bit 2 of `packedBools`) contributes its stored voting units per NFT, otherwise it
|
|
267
|
+
/// contributes its price per NFT. Multiply by an account's balance in the tier to get its voting power there.
|
|
268
|
+
/// @param hook The 721 hook contract that the token belongs to.
|
|
269
|
+
/// @param tokenId The token ID.
|
|
270
|
+
/// @return The per-unit voting value for the token's tier.
|
|
271
|
+
function tierVotingUnitsOfTokenId(address hook, uint256 tokenId) external view override returns (uint256) {
|
|
272
|
+
// Get a reference to the tier's ID.
|
|
273
|
+
uint256 tierId = tierIdOfToken(tokenId);
|
|
274
|
+
|
|
275
|
+
// Keep a reference to the stored tier.
|
|
276
|
+
JBStored721Tier memory storedTier = _storedTierOf[hook][tierId];
|
|
277
|
+
|
|
278
|
+
// Bit 2 (0-indexed) of packedBools is useVotingUnits. Use custom voting units if set, otherwise the price.
|
|
279
|
+
return (storedTier.packedBools & 0x4 != 0) ? _tierVotingUnitsOf[hook][tierId] : storedTier.price;
|
|
280
|
+
}
|
|
281
|
+
|
|
264
282
|
/// @notice Get all active (non-removed) tiers for a hook, with optional filtering by category and pagination.
|
|
265
283
|
/// Tiers are returned sorted by category.
|
|
266
284
|
/// @param hook The 721 hook contract to get tiers from.
|
|
@@ -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)
|
|
@@ -151,6 +151,12 @@ interface IJB721TiersHookStore {
|
|
|
151
151
|
view
|
|
152
152
|
returns (uint256 tierId, bool transfersPausable);
|
|
153
153
|
|
|
154
|
+
/// @notice Get only the per-unit voting value for a token's tier, avoiding full struct construction.
|
|
155
|
+
/// @param hook The 721 hook address.
|
|
156
|
+
/// @param tokenId The token ID.
|
|
157
|
+
/// @return The per-unit voting value for the token's tier (custom voting units if set, otherwise the tier price).
|
|
158
|
+
function tierVotingUnitsOfTokenId(address hook, uint256 tokenId) external view returns (uint256);
|
|
159
|
+
|
|
154
160
|
/// @notice Get an array of currently active 721 tiers for the provided 721 contract.
|
|
155
161
|
/// @param hook The 721 contract to get the tiers of.
|
|
156
162
|
/// @param categories An array of tier categories to get tiers from. Empty for all categories.
|