@bananapus/721-hook-v6 0.0.46 → 0.0.47

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/721-hook-v6",
3
- "version": "0.0.46",
3
+ "version": "0.0.47",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.28;
3
3
 
4
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
4
5
  import {Votes} from "@openzeppelin/contracts/governance/utils/Votes.sol";
5
6
  import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
6
7
  import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol";
@@ -24,6 +25,7 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
24
25
  //*********************************************************************//
25
26
 
26
27
  error JB721Checkpoints_AlreadyInitialized(address hook);
28
+ error JB721Checkpoints_NotOwner(uint256 tokenId, address caller);
27
29
  error JB721Checkpoints_Unauthorized(address caller, address hook);
28
30
 
29
31
  //*********************************************************************//
@@ -44,7 +46,7 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
44
46
  // -------------------- internal stored properties ------------------- //
45
47
  //*********************************************************************//
46
48
 
47
- /// @notice Checkpointed token owners for historical reward eligibility after first transfer.
49
+ /// @notice Checkpointed token owners for historical reward eligibility. Written on enrollment or transfer.
48
50
  /// @custom:param tokenId The token ID to get historical owner checkpoints for.
49
51
  mapping(uint256 tokenId => Checkpoints.Trace160) internal _ownerCheckpointsOf;
50
52
 
@@ -64,6 +66,37 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
64
66
  // ---------------------- external transactions ---------------------- //
65
67
  //*********************************************************************//
66
68
 
69
+ /// @notice Delegates voting power and enrolls tokens for distribution eligibility.
70
+ /// @dev Writes per-token owner checkpoints so `ownerOfAt` can prove ownership at past blocks.
71
+ /// Only the current token owner can enroll. Tokens without checkpoints are ineligible for snapshot-based
72
+ /// distribution. The existing `delegate(address)` from OZ Votes still works for pure delegation without enrollment.
73
+ /// @param delegatee The address to delegate voting power to. Use your own address for self-delegation.
74
+ /// @param tokenIds The token IDs to enroll for distribution eligibility.
75
+ function delegate(address delegatee, uint256[] calldata tokenIds) external override {
76
+ // Delegate voting power (reuses OZ Votes internals).
77
+ _delegate({account: msg.sender, delegatee: delegatee});
78
+
79
+ // Write per-token owner checkpoints for distribution eligibility.
80
+ for (uint256 i; i < tokenIds.length;) {
81
+ uint256 tokenId = tokenIds[i];
82
+
83
+ // Only the current owner can enroll their tokens.
84
+ if (IERC721(HOOK).ownerOf(tokenId) != msg.sender) {
85
+ revert JB721Checkpoints_NotOwner({tokenId: tokenId, caller: msg.sender});
86
+ }
87
+
88
+ // Write an owner checkpoint if the token has none yet.
89
+ if (_ownerCheckpointsOf[tokenId].length() == 0) {
90
+ // forge-lint: disable-next-line(unsafe-typecast)
91
+ _ownerCheckpointsOf[tokenId].push({key: uint96(block.number), value: uint160(msg.sender)});
92
+ }
93
+
94
+ unchecked {
95
+ ++i;
96
+ }
97
+ }
98
+ }
99
+
67
100
  /// @notice Initializes a cloned module with its hook reference.
68
101
  /// @dev Can only be called once. Called by the deployer after cloning.
69
102
  /// @param hook The hook this module serves.
@@ -98,11 +131,11 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
98
131
  //*********************************************************************//
99
132
 
100
133
  /// @notice The owner of an NFT at a past block.
101
- /// @dev Mints do not write per-token checkpoint storage. Until a token's first non-mint transfer, ownership is
102
- /// inferred from the hook's `firstOwnerOf`.
134
+ /// @dev Returns `address(0)` for tokens that have never been enrolled (via `delegate(address, uint256[])`) or
135
+ /// transferred. Unenrolled tokens are ineligible for snapshot-based distribution.
103
136
  /// @param tokenId The token ID of the NFT to get the historical owner of.
104
137
  /// @param blockNumber The block number to look up.
105
- /// @return The owner of the token at `blockNumber`, or zero if the token has no known owner.
138
+ /// @return The owner of the token at `blockNumber`, or zero if the token is unenrolled or has no known owner.
106
139
  function ownerOfAt(uint256 tokenId, uint256 blockNumber) external view override returns (address) {
107
140
  // forge-lint: disable-next-line(unsafe-typecast)
108
141
  uint96 blockNumber96 = uint96(blockNumber);
@@ -110,10 +143,11 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
110
143
  Checkpoints.Trace160 storage checkpoints = _ownerCheckpointsOf[tokenId];
111
144
  uint256 checkpointCount = checkpoints.length();
112
145
 
113
- // Before the first transfer/burn checkpoint, the mint owner is implicit in the hook's first-owner tracking.
114
- if (checkpointCount == 0 || checkpoints.at(0)._key > blockNumber96) {
115
- return IJB721TiersHook(HOOK).firstOwnerOf(tokenId);
116
- }
146
+ // No checkpoints = not enrolled and never transferred. Not eligible.
147
+ if (checkpointCount == 0) return address(0);
148
+
149
+ // Query is before the first checkpoint — token not yet enrolled/transferred at this block.
150
+ if (checkpoints.at(0)._key > blockNumber96) return address(0);
117
151
 
118
152
  return address(uint160(checkpoints.upperLookupRecent(blockNumber96)));
119
153
  }
@@ -14,11 +14,11 @@ interface IJB721Checkpoints is IERC5805 {
14
14
  function HOOK() external view returns (address);
15
15
 
16
16
  /// @notice The owner of an NFT at a past block.
17
- /// @dev Mints do not write per-token checkpoint storage. Until a token's first non-mint transfer, ownership is
18
- /// inferred from the hook's `firstOwnerOf`.
17
+ /// @dev Returns `address(0)` for tokens that have never been enrolled (via `delegate(address, uint256[])`) or
18
+ /// transferred. Unenrolled tokens are ineligible for snapshot-based distribution.
19
19
  /// @param tokenId The token ID of the NFT to get the historical owner of.
20
20
  /// @param blockNumber The block number to look up.
21
- /// @return The owner of the token at `blockNumber`, or zero if the token has no known owner.
21
+ /// @return The owner of the token at `blockNumber`, or zero if the token is unenrolled or has no known owner.
22
22
  function ownerOfAt(uint256 tokenId, uint256 blockNumber) external view returns (address);
23
23
 
24
24
  /// @notice The store that holds tier and voting data for the hook's NFTs.
@@ -26,6 +26,14 @@ interface IJB721Checkpoints is IERC5805 {
26
26
  // forge-lint: disable-next-line(mixed-case-function)
27
27
  function STORE() external view returns (IJB721TiersHookStore);
28
28
 
29
+ /// @notice Delegates voting power and enrolls tokens for distribution eligibility.
30
+ /// @dev Writes per-token owner checkpoints so `ownerOfAt` can prove ownership at past blocks.
31
+ /// Only the current token owner can enroll. Tokens without checkpoints are ineligible for snapshot-based
32
+ /// distribution.
33
+ /// @param delegatee The address to delegate voting power to. Use your own address for self-delegation.
34
+ /// @param tokenIds The token IDs to enroll for distribution eligibility.
35
+ function delegate(address delegatee, uint256[] calldata tokenIds) external;
36
+
29
37
  /// @notice Initializes a cloned module with its hook reference.
30
38
  /// @dev Can only be called once. Called by the deployer after cloning.
31
39
  /// @param hook The hook this module serves.
@@ -33,7 +41,6 @@ interface IJB721Checkpoints is IERC5805 {
33
41
 
34
42
  /// @notice Called by the hook after every NFT transfer to update checkpointed voting power.
35
43
  /// @dev Looks up the token's tier voting units from the store internally.
36
- /// Auto-self-delegates on first receive so checkpoints work without manual delegation.
37
44
  /// @param from The previous owner (address(0) on mint).
38
45
  /// @param to The new owner (address(0) on burn).
39
46
  /// @param tokenId The token ID to transfer (used to look up tier voting units).