@bananapus/721-hook-v6 0.0.43 → 0.0.46
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/CHANGELOG.md +2 -3
- package/README.md +2 -2
- package/package.json +2 -2
- package/references/operations.md +2 -2
- package/references/runtime.md +1 -1
- package/script/helpers/Hook721DeploymentLib.sol +21 -5
- package/src/JB721Checkpoints.sol +5 -7
- package/src/JB721CheckpointsDeployer.sol +9 -1
- package/src/JB721TiersHook.sol +72 -76
- package/src/JB721TiersHookDeployer.sol +8 -6
- package/src/JB721TiersHookProjectDeployer.sol +22 -20
- package/src/JB721TiersHookStore.sol +170 -134
- package/src/abstract/ERC721.sol +24 -22
- package/src/abstract/JB721Hook.sol +20 -14
- package/src/interfaces/IJB721Checkpoints.sol +1 -1
- package/src/interfaces/IJB721CheckpointsDeployer.sol +0 -3
- package/src/interfaces/IJB721TiersHook.sol +2 -10
- package/src/interfaces/IJB721TiersHookProjectDeployer.sol +2 -2
- package/src/interfaces/IJB721TiersHookStore.sol +11 -11
- package/src/libraries/JB721Constants.sol +1 -0
- package/src/libraries/JB721TiersHookLib.sol +20 -33
- package/src/libraries/JBBitmap.sol +1 -1
- package/src/structs/JB721TiersHookFlags.sol +1 -1
- package/src/structs/JBPayDataHookRulesetMetadata.sol +3 -3
- package/test/utils/UnitTestSetup.sol +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -20,7 +20,7 @@ This file describes the verified change from `nana-721-hook-v5` to the current `
|
|
|
20
20
|
- The repo now carries a dedicated helper library to keep the hook surface manageable and to support the larger v6 feature set.
|
|
21
21
|
- The repo was upgraded from the v5 Solidity baseline to `0.8.28`.
|
|
22
22
|
|
|
23
|
-
## Local
|
|
23
|
+
## Local review remediations
|
|
24
24
|
|
|
25
25
|
- `JB721TiersHookProjectDeployer.launchRulesetsFor` now checks `LAUNCH_RULESETS` instead of `QUEUE_RULESETS`. The previous check was semantically wrong — launching active rulesets should require the launch permission, not the queue permission.
|
|
26
26
|
|
|
@@ -43,7 +43,7 @@ This file describes the verified change from `nana-721-hook-v5` to the current `
|
|
|
43
43
|
|
|
44
44
|
## Indexer impact
|
|
45
45
|
|
|
46
|
-
- New events: `
|
|
46
|
+
- New events: `SetName`, `SetSymbol`, `SplitPayoutReverted`.
|
|
47
47
|
- Tier config decoding changed because `JB721TierConfig` is no longer v5-compatible.
|
|
48
48
|
- Collection metadata can now change after deployment, so one-time indexing of `name` and `symbol` is no longer sufficient.
|
|
49
49
|
|
|
@@ -62,7 +62,6 @@ This file describes the verified change from `nana-721-hook-v5` to the current `
|
|
|
62
62
|
- `pricingContext()`
|
|
63
63
|
- `setMetadata(...)`
|
|
64
64
|
- Added events
|
|
65
|
-
- `AddToBalanceReverted` (declared in interface but no longer emitted; the library now reverts with `JB721TiersHookLib_SplitFallbackFailed` instead)
|
|
66
65
|
- `SetName`
|
|
67
66
|
- `SetSymbol`
|
|
68
67
|
- `SplitPayoutReverted`
|
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ That split is why UI bugs, economic bugs, and deployment bugs often land in diff
|
|
|
73
73
|
1. `test/E2E/Pay_Mint_Redeem_E2E.t.sol`
|
|
74
74
|
2. `test/invariants/TierLifecycleInvariant.t.sol`
|
|
75
75
|
3. `test/invariants/TieredHookStoreInvariant.t.sol`
|
|
76
|
-
4. `test/
|
|
76
|
+
4. `test/regression/RegressionSplitCreditsMismatch.t.sol`
|
|
77
77
|
5. `test/regression/ProjectDeployerRulesets.t.sol`
|
|
78
78
|
|
|
79
79
|
## Install
|
|
@@ -112,7 +112,7 @@ src/
|
|
|
112
112
|
libraries/
|
|
113
113
|
structs/
|
|
114
114
|
test/
|
|
115
|
-
unit, E2E, fork, invariant,
|
|
115
|
+
unit, E2E, fork, invariant, review, and regression coverage
|
|
116
116
|
script/
|
|
117
117
|
Deploy.s.sol
|
|
118
118
|
helpers/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/721-hook-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.46",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@bananapus/address-registry-v6": "0.0.25",
|
|
32
|
-
"@bananapus/core-v6": "
|
|
32
|
+
"@bananapus/core-v6": "0.0.44",
|
|
33
33
|
"@bananapus/ownable-v6": "^0.0.24",
|
|
34
34
|
"@bananapus/permission-ids-v6": "0.0.22",
|
|
35
35
|
"@openzeppelin/contracts": "5.6.1",
|
package/references/operations.md
CHANGED
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
## Useful Proof Points
|
|
26
26
|
|
|
27
27
|
- [`test/Fork.t.sol`](../test/Fork.t.sol) for live-integration assumptions.
|
|
28
|
-
- [`test/
|
|
28
|
+
- [`test/TestRegressionGaps.sol`](../test/TestRegressionGaps.sol) for known edge cases the repo authors considered worth pinning down.
|
|
29
29
|
- [`test/TestCheckpoints.t.sol`](../test/TestCheckpoints.t.sol) when you need a narrow function-level proof before editing a broad runtime path.
|
|
30
30
|
- [`test/invariants/TierLifecycleInvariant.t.sol`](../test/invariants/TierLifecycleInvariant.t.sol) and [`test/invariants/TieredHookStoreInvariant.t.sol`](../test/invariants/TieredHookStoreInvariant.t.sol) when a local patch may have broken store-level relationships.
|
|
31
|
-
- [`test/
|
|
31
|
+
- [`test/regression/RetroactiveReserveBeneficiaryDilution.t.sol`](../test/regression/RetroactiveReserveBeneficiaryDilution.t.sol) when reserve-beneficiary or pending-reserve behavior changes.
|
|
32
32
|
- [`script/Deploy.s.sol`](../script/Deploy.s.sol) when a deployment or launch question is really about config assembly rather than contract behavior.
|
package/references/runtime.md
CHANGED
|
@@ -30,4 +30,4 @@
|
|
|
30
30
|
- [`test/TestVotingUnitsLifecycle.t.sol`](../test/TestVotingUnitsLifecycle.t.sol) for voting-unit lifecycle behavior.
|
|
31
31
|
- [`test/TestCheckpoints.t.sol`](../test/TestCheckpoints.t.sol) for checkpoint/module behavior.
|
|
32
32
|
- [`test/invariants/TierLifecycleInvariant.t.sol`](../test/invariants/TierLifecycleInvariant.t.sol) and [`test/invariants/TieredHookStoreInvariant.t.sol`](../test/invariants/TieredHookStoreInvariant.t.sol) for store-level lifecycle invariants.
|
|
33
|
-
- [`test/TestSafeTransferReentrancy.t.sol`](../test/TestSafeTransferReentrancy.t.sol), [`test/721HookAttacks.t.sol`](../test/721HookAttacks.t.sol), [`test/
|
|
33
|
+
- [`test/TestSafeTransferReentrancy.t.sol`](../test/TestSafeTransferReentrancy.t.sol), [`test/721HookAttacks.t.sol`](../test/721HookAttacks.t.sol), [`test/regression/RetroactiveReserveBeneficiaryDilution.t.sol`](../test/regression/RetroactiveReserveBeneficiaryDilution.t.sol), and [`test/TestRegressionGaps.sol`](../test/TestRegressionGaps.sol) for reentrancy and attack-surface checks.
|
|
@@ -35,7 +35,7 @@ library Hook721DeploymentLib {
|
|
|
35
35
|
|
|
36
36
|
for (uint256 _i; _i < networks.length; _i++) {
|
|
37
37
|
if (networks[_i].chainId == chainId) {
|
|
38
|
-
return getDeployment(path, networks[_i].name);
|
|
38
|
+
return getDeployment({path: path, network_name: networks[_i].name});
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -52,15 +52,31 @@ library Hook721DeploymentLib {
|
|
|
52
52
|
returns (Hook721Deployment memory deployment)
|
|
53
53
|
{
|
|
54
54
|
deployment.hook_deployer = IJB721TiersHookDeployer(
|
|
55
|
-
_getDeploymentAddress(
|
|
55
|
+
_getDeploymentAddress({
|
|
56
|
+
path: path,
|
|
57
|
+
project_name: "nana-721-hook-v6",
|
|
58
|
+
network_name: network_name,
|
|
59
|
+
contractName: "JB721TiersHookDeployer"
|
|
60
|
+
})
|
|
56
61
|
);
|
|
57
62
|
|
|
58
63
|
deployment.project_deployer = IJB721TiersHookProjectDeployer(
|
|
59
|
-
_getDeploymentAddress(
|
|
64
|
+
_getDeploymentAddress({
|
|
65
|
+
path: path,
|
|
66
|
+
project_name: "nana-721-hook-v6",
|
|
67
|
+
network_name: network_name,
|
|
68
|
+
contractName: "JB721TiersHookProjectDeployer"
|
|
69
|
+
})
|
|
60
70
|
);
|
|
61
71
|
|
|
62
|
-
deployment.store =
|
|
63
|
-
|
|
72
|
+
deployment.store = IJB721TiersHookStore(
|
|
73
|
+
_getDeploymentAddress({
|
|
74
|
+
path: path,
|
|
75
|
+
project_name: "nana-721-hook-v6",
|
|
76
|
+
network_name: network_name,
|
|
77
|
+
contractName: "JB721TiersHookStore"
|
|
78
|
+
})
|
|
79
|
+
);
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
/// @notice Get the address of a contract that was deployed by the Deploy script.
|
package/src/JB721Checkpoints.sol
CHANGED
|
@@ -23,8 +23,8 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
|
|
|
23
23
|
// --------------------------- custom errors ------------------------- //
|
|
24
24
|
//*********************************************************************//
|
|
25
25
|
|
|
26
|
-
error JB721Checkpoints_AlreadyInitialized();
|
|
27
|
-
error JB721Checkpoints_Unauthorized();
|
|
26
|
+
error JB721Checkpoints_AlreadyInitialized(address hook);
|
|
27
|
+
error JB721Checkpoints_Unauthorized(address caller, address hook);
|
|
28
28
|
|
|
29
29
|
//*********************************************************************//
|
|
30
30
|
// --------------- public immutable stored properties ---------------- //
|
|
@@ -68,9 +68,8 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
|
|
|
68
68
|
/// @dev Can only be called once. Called by the deployer after cloning.
|
|
69
69
|
/// @param hook The hook this module serves.
|
|
70
70
|
function initialize(address hook) external override {
|
|
71
|
-
if (HOOK != address(0)) revert JB721Checkpoints_AlreadyInitialized();
|
|
71
|
+
if (HOOK != address(0)) revert JB721Checkpoints_AlreadyInitialized({hook: HOOK});
|
|
72
72
|
// `hook` cannot be zero when called through the deployer because `msg.sender` must equal `hook`.
|
|
73
|
-
// slither-disable-next-line missing-zero-check
|
|
74
73
|
HOOK = hook;
|
|
75
74
|
}
|
|
76
75
|
|
|
@@ -78,13 +77,12 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
|
|
|
78
77
|
/// @dev Only callable by the HOOK. Looks up the token's tier voting units from the store.
|
|
79
78
|
/// @param from The previous owner (address(0) on mint).
|
|
80
79
|
/// @param to The new owner (address(0) on burn).
|
|
81
|
-
/// @param tokenId The token ID
|
|
80
|
+
/// @param tokenId The token ID to transfer.
|
|
82
81
|
function onTransfer(address from, address to, uint256 tokenId) external override {
|
|
83
|
-
if (msg.sender != HOOK) revert JB721Checkpoints_Unauthorized();
|
|
82
|
+
if (msg.sender != HOOK) revert JB721Checkpoints_Unauthorized({caller: msg.sender, hook: HOOK});
|
|
84
83
|
|
|
85
84
|
if (from != address(0)) {
|
|
86
85
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
87
|
-
// slither-disable-next-line unused-return
|
|
88
86
|
_ownerCheckpointsOf[tokenId].push({key: uint96(block.number), value: uint160(to)});
|
|
89
87
|
}
|
|
90
88
|
|
|
@@ -13,6 +13,12 @@ import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
|
|
|
13
13
|
/// @dev The implementation is deployed once in the constructor. Each `deploy()` call clones it (~45k gas) and
|
|
14
14
|
/// initializes the clone with the hook and store references.
|
|
15
15
|
contract JB721CheckpointsDeployer is IJB721CheckpointsDeployer {
|
|
16
|
+
//*********************************************************************//
|
|
17
|
+
// --------------------------- custom errors ------------------------- //
|
|
18
|
+
//*********************************************************************//
|
|
19
|
+
|
|
20
|
+
error JB721CheckpointsDeployer_Unauthorized(address caller, address hook);
|
|
21
|
+
|
|
16
22
|
//*********************************************************************//
|
|
17
23
|
// --------------- public immutable stored properties ---------------- //
|
|
18
24
|
//*********************************************************************//
|
|
@@ -42,7 +48,9 @@ contract JB721CheckpointsDeployer is IJB721CheckpointsDeployer {
|
|
|
42
48
|
/// @param hook The hook address the module will serve.
|
|
43
49
|
/// @return module The newly deployed and initialized checkpoint module.
|
|
44
50
|
function deploy(address hook) external override returns (IJB721Checkpoints module) {
|
|
45
|
-
if (msg.sender != hook)
|
|
51
|
+
if (msg.sender != hook) {
|
|
52
|
+
revert JB721CheckpointsDeployer_Unauthorized({caller: msg.sender, hook: hook});
|
|
53
|
+
}
|
|
46
54
|
|
|
47
55
|
module = IJB721Checkpoints(
|
|
48
56
|
LibClone.cloneDeterministic({implementation: IMPLEMENTATION, salt: bytes32(uint256(uint160(hook)))})
|
package/src/JB721TiersHook.sol
CHANGED
|
@@ -42,12 +42,10 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
42
42
|
//*********************************************************************//
|
|
43
43
|
|
|
44
44
|
error JB721TiersHook_AlreadyInitialized(uint256 projectId);
|
|
45
|
-
error JB721TiersHook_CantBuyWithCredits();
|
|
46
45
|
error JB721TiersHook_InvalidPricingDecimals(uint256 decimals);
|
|
47
|
-
error JB721TiersHook_MintReserveNftsPaused();
|
|
48
|
-
error JB721TiersHook_NoProjectId();
|
|
49
|
-
error
|
|
50
|
-
error JB721TiersHook_TierTransfersPaused();
|
|
46
|
+
error JB721TiersHook_MintReserveNftsPaused(uint256 projectId, uint256 tierId);
|
|
47
|
+
error JB721TiersHook_NoProjectId(uint256 projectId);
|
|
48
|
+
error JB721TiersHook_TierTransfersPaused(uint256 projectId, uint256 tokenId, address from, address to);
|
|
51
49
|
|
|
52
50
|
//*********************************************************************//
|
|
53
51
|
// --------------- public immutable stored properties ---------------- //
|
|
@@ -151,18 +149,19 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
151
149
|
// ------------------------- external views -------------------------- //
|
|
152
150
|
//*********************************************************************//
|
|
153
151
|
|
|
154
|
-
/// @notice The
|
|
155
|
-
///
|
|
156
|
-
/// @param tokenId The token ID of the NFT
|
|
152
|
+
/// @notice The address that originally received an NFT (typically the payer). Tracked separately from the current
|
|
153
|
+
/// owner so it persists through transfers, useful for provenance and historical voting checkpoints.
|
|
154
|
+
/// @param tokenId The token ID of the NFT.
|
|
157
155
|
/// @return The address of the NFT's first owner.
|
|
158
156
|
function firstOwnerOf(uint256 tokenId) external view override returns (address) {
|
|
159
157
|
address first = _firstOwnerOf[tokenId];
|
|
160
158
|
return first != address(0) ? first : _ownerOf(tokenId);
|
|
161
159
|
}
|
|
162
160
|
|
|
163
|
-
/// @notice
|
|
161
|
+
/// @notice The currency and decimal precision used for this hook's tier prices. For example, if tiers are priced
|
|
162
|
+
/// in ETH with 18 decimals, `currency` would be the ETH currency ID and `decimals` would be 18.
|
|
164
163
|
/// @return currency The currency used for tier prices.
|
|
165
|
-
/// @return decimals The
|
|
164
|
+
/// @return decimals The number of decimals used in tier prices.
|
|
166
165
|
function pricingContext() external view override returns (uint256 currency, uint256 decimals) {
|
|
167
166
|
// Get a reference to the packed pricing context.
|
|
168
167
|
uint256 packed = _packedPricingContext;
|
|
@@ -185,12 +184,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
185
184
|
return STORE.balanceOf({hook: address(this), owner: owner});
|
|
186
185
|
}
|
|
187
186
|
|
|
188
|
-
/// @notice
|
|
189
|
-
///
|
|
190
|
-
/// @
|
|
191
|
-
/// @
|
|
192
|
-
/// project (
|
|
193
|
-
/// @return hookSpecifications
|
|
187
|
+
/// @notice Called by the terminal before recording a payment. Calculates how much of the payment should be routed
|
|
188
|
+
/// to tier-based splits vs. kept by the project, and adjusts the minting weight accordingly.
|
|
189
|
+
/// @dev Overrides the base to compute tier split amounts from each tier's `splitPercent`.
|
|
190
|
+
/// @param context The payment context from the terminal.
|
|
191
|
+
/// @return weight The adjusted weight for project token minting (reduced when splits route funds away).
|
|
192
|
+
/// @return hookSpecifications Specifies this hook as the pay hook, with the split amount to forward.
|
|
194
193
|
function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
|
|
195
194
|
public
|
|
196
195
|
view
|
|
@@ -222,17 +221,20 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
222
221
|
});
|
|
223
222
|
}
|
|
224
223
|
|
|
225
|
-
/// @notice The combined cash
|
|
226
|
-
///
|
|
227
|
-
/// @
|
|
228
|
-
/// @
|
|
229
|
-
/// @return weight The cash out weight of the tokenIds.
|
|
224
|
+
/// @notice The combined cash-out weight of specific NFTs. Divide by `totalCashOutWeight()` to get the fraction of
|
|
225
|
+
/// surplus these NFTs can reclaim. Weight is based on the original tier price, not any discount paid.
|
|
226
|
+
/// @param tokenIds The token IDs of the NFTs to get the combined cash-out weight of.
|
|
227
|
+
/// @return weight The combined cash-out weight.
|
|
230
228
|
function cashOutWeightOf(uint256[] memory tokenIds) public view virtual override returns (uint256) {
|
|
231
229
|
return STORE.cashOutWeightOf({hook: address(this), tokenIds: tokenIds});
|
|
232
230
|
}
|
|
233
231
|
|
|
234
|
-
/// @notice
|
|
235
|
-
///
|
|
232
|
+
/// @notice Initialize a cloned copy of the hook. Sets the project association, ERC-721 name/symbol, pricing
|
|
233
|
+
/// context (currency + decimals), metadata URIs, initial tiers, and behavioral flags. Can only be called once
|
|
234
|
+
/// per clone — the implementation contract is pre-initialized in its constructor to prevent misuse.
|
|
235
|
+
/// @dev Called by `JB721TiersHookDeployer` immediately after cloning. Reverts with
|
|
236
|
+
/// `JB721TiersHook_AlreadyInitialized` if called more than once, or `JB721TiersHook_NoProjectId` if projectId is 0.
|
|
237
|
+
/// @param projectId The ID of the project this hook is associated with.
|
|
236
238
|
/// @param name The name of the NFT collection.
|
|
237
239
|
/// @param symbol The symbol representing the NFT collection.
|
|
238
240
|
/// @param baseUri The URI to use as a base for full NFT `tokenUri`s.
|
|
@@ -260,7 +262,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
260
262
|
_initialized = true;
|
|
261
263
|
|
|
262
264
|
// Make sure a projectId is provided.
|
|
263
|
-
if (projectId == 0) revert JB721TiersHook_NoProjectId();
|
|
265
|
+
if (projectId == 0) revert JB721TiersHook_NoProjectId({projectId: projectId});
|
|
264
266
|
|
|
265
267
|
// Initialize the superclass.
|
|
266
268
|
JB721Hook._initialize({projectId: projectId, name: name, symbol: symbol});
|
|
@@ -275,7 +277,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
275
277
|
// pack the pricing decimals in bits 32-39 (8 bits).
|
|
276
278
|
packed |= uint256(tiersConfig.decimals) << 32;
|
|
277
279
|
// Store the packed value.
|
|
278
|
-
// slither-disable-next-line events-maths
|
|
279
280
|
_packedPricingContext = packed;
|
|
280
281
|
|
|
281
282
|
// Store the base URI if provided.
|
|
@@ -328,9 +329,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
328
329
|
return JB721TiersHookLib.resolveTokenURI(STORE, address(this), baseURI, tokenId);
|
|
329
330
|
}
|
|
330
331
|
|
|
331
|
-
/// @notice The
|
|
332
|
-
///
|
|
333
|
-
/// @return weight The total cash
|
|
332
|
+
/// @notice The total cash-out weight across all outstanding NFTs and pending reserves. This is the denominator
|
|
333
|
+
/// for cash-out calculations — an NFT's share of the surplus is its weight divided by this total.
|
|
334
|
+
/// @return weight The total cash-out weight.
|
|
334
335
|
function totalCashOutWeight() public view virtual override returns (uint256) {
|
|
335
336
|
return STORE.totalCashOutWeight(address(this));
|
|
336
337
|
}
|
|
@@ -339,12 +340,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
339
340
|
// ---------------------- external transactions ---------------------- //
|
|
340
341
|
//*********************************************************************//
|
|
341
342
|
|
|
342
|
-
/// @notice Add or
|
|
343
|
-
///
|
|
344
|
-
///
|
|
345
|
-
/// @dev
|
|
346
|
-
/// @param tiersToAdd The tiers to add, as an array of `JB721TierConfig` structs
|
|
347
|
-
/// @param tierIdsToRemove The tiers to remove
|
|
343
|
+
/// @notice Add new NFT tiers or remove existing ones. Added tiers get sequential IDs and must be sorted by
|
|
344
|
+
/// category. Removed tiers stop accepting new mints but existing NFTs remain valid.
|
|
345
|
+
/// @dev Only the collection owner or an operator with `ADJUST_721_TIERS` permission can call this.
|
|
346
|
+
/// @dev Added tiers must respect this hook's flags (e.g. `noNewTiersWithVotes`, `noNewTiersWithReserves`).
|
|
347
|
+
/// @param tiersToAdd The tiers to add, as an array of `JB721TierConfig` structs.
|
|
348
|
+
/// @param tierIdsToRemove The IDs of the tiers to remove.
|
|
348
349
|
function adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata tierIdsToRemove) external override {
|
|
349
350
|
// Enforce permissions.
|
|
350
351
|
_requirePermissionFrom({
|
|
@@ -363,9 +364,11 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
363
364
|
});
|
|
364
365
|
}
|
|
365
366
|
|
|
366
|
-
/// @notice Manually mint NFTs from
|
|
367
|
+
/// @notice Manually mint NFTs from specific tiers to a beneficiary, without requiring payment. Only tiers with
|
|
368
|
+
/// `allowOwnerMint` enabled can be minted this way.
|
|
369
|
+
/// @dev Only the collection owner or an operator with `MINT_721` permission can call this.
|
|
367
370
|
/// @param tierIds The IDs of the tiers to mint from.
|
|
368
|
-
/// @param beneficiary The address to mint to.
|
|
371
|
+
/// @param beneficiary The address to mint the NFTs to.
|
|
369
372
|
/// @return tokenIds The IDs of the newly minted tokens.
|
|
370
373
|
function mintFor(
|
|
371
374
|
uint16[] calldata tierIds,
|
|
@@ -379,7 +382,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
379
382
|
_requirePermissionFrom({account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.MINT_721});
|
|
380
383
|
|
|
381
384
|
// Record the mint. The token IDs returned correspond to the tiers passed in.
|
|
382
|
-
// slither-disable-next-line reentrancy-events,unused-return
|
|
383
385
|
(tokenIds,,) = STORE.recordMint({
|
|
384
386
|
amount: type(uint256).max, // force the mint.
|
|
385
387
|
tierIds: tierIds,
|
|
@@ -389,9 +391,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
389
391
|
_mintTokens({tokenIds: tokenIds, tierIds: tierIds, beneficiary: beneficiary, totalAmountPaid: 0});
|
|
390
392
|
}
|
|
391
393
|
|
|
392
|
-
/// @notice Mint pending reserved NFTs
|
|
393
|
-
///
|
|
394
|
-
/// @param reserveMintConfigs
|
|
394
|
+
/// @notice Mint pending reserved NFTs across multiple tiers in a single call. Reserves accumulate automatically
|
|
395
|
+
/// as NFTs are sold (based on each tier's `reserveFrequency`) and anyone can trigger their minting.
|
|
396
|
+
/// @param reserveMintConfigs The tier IDs and counts specifying how many reserves to mint from each tier.
|
|
395
397
|
function mintPendingReservesFor(JB721TiersMintReservesConfig[] calldata reserveMintConfigs) external override {
|
|
396
398
|
for (uint256 i; i < reserveMintConfigs.length;) {
|
|
397
399
|
// Get a reference to the params being iterated upon.
|
|
@@ -406,12 +408,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
406
408
|
}
|
|
407
409
|
}
|
|
408
410
|
|
|
409
|
-
/// @notice
|
|
410
|
-
///
|
|
411
|
-
///
|
|
412
|
-
///
|
|
411
|
+
/// @notice Set a discount on a tier's price. Discounts reduce the price payers must pay, but don't affect the
|
|
412
|
+
/// NFT's cash-out weight (which always uses the original price). The tier must have `cannotIncreaseDiscountPercent`
|
|
413
|
+
/// set appropriately.
|
|
414
|
+
/// @dev Only the collection owner or an operator with `SET_721_DISCOUNT_PERCENT` permission can call this.
|
|
413
415
|
/// @param tierId The ID of the tier to set the discount of.
|
|
414
|
-
/// @param discountPercent The discount percent to set.
|
|
416
|
+
/// @param discountPercent The discount percent to set (0–100).
|
|
415
417
|
function setDiscountPercentOf(uint256 tierId, uint256 discountPercent) external override {
|
|
416
418
|
// Enforce permissions.
|
|
417
419
|
_requirePermissionFrom({
|
|
@@ -420,8 +422,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
420
422
|
_setDiscountPercentOf({tierId: tierId, discountPercent: discountPercent});
|
|
421
423
|
}
|
|
422
424
|
|
|
423
|
-
/// @notice
|
|
424
|
-
/// @
|
|
425
|
+
/// @notice Set discount percentages for multiple tiers in a single call.
|
|
426
|
+
/// @dev Only the collection owner or an operator with `SET_721_DISCOUNT_PERCENT` permission can call this.
|
|
427
|
+
/// @param configs An array of tier ID + discount percent pairs to apply.
|
|
425
428
|
function setDiscountPercentsOf(JB721TiersSetDiscountPercentConfig[] calldata configs) external override {
|
|
426
429
|
// Enforce permissions.
|
|
427
430
|
_requirePermissionFrom({
|
|
@@ -440,7 +443,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
440
443
|
}
|
|
441
444
|
}
|
|
442
445
|
|
|
443
|
-
/// @notice Update this hook's metadata properties.
|
|
446
|
+
/// @notice Update any combination of this hook's metadata properties in a single call. Pass empty strings or
|
|
447
|
+
/// sentinel values for fields you don't want to change.
|
|
444
448
|
/// @dev Only this contract's owner or an operator with the `SET_721_METADATA` permission can set the metadata.
|
|
445
449
|
/// @param name The new collection name. Send empty to leave unchanged.
|
|
446
450
|
/// @param symbol The new collection symbol. Send empty to leave unchanged.
|
|
@@ -495,7 +499,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
495
499
|
// `address(this)` is the sentinel value meaning "leave unchanged" (since `address(0)` clears the resolver).
|
|
496
500
|
if (tokenUriResolver != IJB721TokenUriResolver(address(this))) {
|
|
497
501
|
// Store the new URI resolver.
|
|
498
|
-
// slither-disable-next-line reentrancy-events
|
|
499
502
|
_recordSetTokenUriResolver(tokenUriResolver);
|
|
500
503
|
}
|
|
501
504
|
if (encodedIPFSUriTierId != 0 && encodedIPFSUri != bytes32(0)) {
|
|
@@ -510,8 +513,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
510
513
|
// ----------------------- public transactions ----------------------- //
|
|
511
514
|
//*********************************************************************//
|
|
512
515
|
|
|
513
|
-
/// @notice Mint
|
|
514
|
-
///
|
|
516
|
+
/// @notice Mint pending reserved NFTs from a specific tier. Anyone can call this — reserves are minted to the
|
|
517
|
+
/// tier's reserve beneficiary (or the hook's default). Reverts if the ruleset has reserve minting paused.
|
|
515
518
|
/// @param tierId The ID of the tier to mint reserved NFTs from.
|
|
516
519
|
/// @param count The number of reserved NFTs to mint.
|
|
517
520
|
function mintPendingReservesFor(uint256 tierId, uint256 count) public override {
|
|
@@ -521,15 +524,13 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
521
524
|
// Pending reserve mints must not be paused.
|
|
522
525
|
if (JB721TiersRulesetMetadataResolver.mintPendingReservesPaused((JBRulesetMetadataResolver.metadata(ruleset))))
|
|
523
526
|
{
|
|
524
|
-
revert JB721TiersHook_MintReserveNftsPaused();
|
|
527
|
+
revert JB721TiersHook_MintReserveNftsPaused({projectId: PROJECT_ID, tierId: tierId});
|
|
525
528
|
}
|
|
526
529
|
|
|
527
530
|
// Record the reserved mint for the tier.
|
|
528
|
-
// slither-disable-next-line reentrancy-events,calls-loop
|
|
529
531
|
uint256[] memory tokenIds = STORE.recordMintReservesFor({tierId: tierId, count: count});
|
|
530
532
|
|
|
531
533
|
// Keep a reference to the beneficiary.
|
|
532
|
-
// slither-disable-next-line calls-loop
|
|
533
534
|
address reserveBeneficiary = STORE.reserveBeneficiaryOf({hook: address(this), tierId: tierId});
|
|
534
535
|
|
|
535
536
|
// Cache _msgSender() before the loop to avoid repeated calls.
|
|
@@ -542,7 +543,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
542
543
|
emit MintReservedNft({tokenId: tokenId, tierId: tierId, beneficiary: reserveBeneficiary, caller: caller});
|
|
543
544
|
|
|
544
545
|
// Mint the NFT.
|
|
545
|
-
// slither-disable-next-line reentrency-events
|
|
546
546
|
_mint({to: reserveBeneficiary, tokenId: tokenId});
|
|
547
547
|
|
|
548
548
|
unchecked {
|
|
@@ -564,7 +564,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
564
564
|
/// @param projectId The ID of the project to check.
|
|
565
565
|
/// @return The project's current ruleset.
|
|
566
566
|
function _currentRulesetOf(uint256 projectId) internal view returns (JBRuleset memory) {
|
|
567
|
-
// slither-disable-next-line calls-loop
|
|
568
567
|
return RULESETS.currentOf(projectId);
|
|
569
568
|
}
|
|
570
569
|
|
|
@@ -591,11 +590,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
591
590
|
STORE.recordBurn(tokenIds);
|
|
592
591
|
}
|
|
593
592
|
|
|
594
|
-
/// @notice
|
|
595
|
-
///
|
|
596
|
-
/// @param
|
|
593
|
+
/// @notice Mint a batch of NFTs to the beneficiary and emit a `Mint` event for each. Called after the store has
|
|
594
|
+
/// recorded the mint and generated token IDs.
|
|
595
|
+
/// @param tokenIds The token IDs to mint (generated by the store based on tier and sequence number).
|
|
596
|
+
/// @param tierIds The tier IDs corresponding to each token (same length and order as `tokenIds`).
|
|
597
597
|
/// @param beneficiary The address receiving the NFTs.
|
|
598
|
-
/// @param totalAmountPaid The amount to report in the Mint event.
|
|
598
|
+
/// @param totalAmountPaid The total payment amount (including credits) to report in the Mint event for indexing.
|
|
599
599
|
function _mintTokens(
|
|
600
600
|
uint256[] memory tokenIds,
|
|
601
601
|
uint16[] memory tierIds,
|
|
@@ -616,7 +616,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
616
616
|
caller: caller
|
|
617
617
|
});
|
|
618
618
|
|
|
619
|
-
// slither-disable-next-line reentrancy-events
|
|
620
619
|
_mint({to: beneficiary, tokenId: tokenIds[i]});
|
|
621
620
|
|
|
622
621
|
unchecked {
|
|
@@ -656,7 +655,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
656
655
|
if (tokenIds.length != 0) {
|
|
657
656
|
// totalAmountPaid is the full amount available before recordMint deducted tier prices.
|
|
658
657
|
uint256 totalAmountPaid = (payer == beneficiary) ? value + payCredits : value;
|
|
659
|
-
// slither-disable-next-line reentrancy-events
|
|
660
658
|
_mintTokens({
|
|
661
659
|
tokenIds: tokenIds, tierIds: tierIdsToMint, beneficiary: beneficiary, totalAmountPaid: totalAmountPaid
|
|
662
660
|
});
|
|
@@ -680,7 +678,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
680
678
|
});
|
|
681
679
|
}
|
|
682
680
|
|
|
683
|
-
// slither-disable-next-line reentrancy-no-eth
|
|
684
681
|
payCreditsOf[beneficiary] = newPayCredits;
|
|
685
682
|
}
|
|
686
683
|
}
|
|
@@ -735,30 +732,29 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
735
732
|
}
|
|
736
733
|
}
|
|
737
734
|
|
|
738
|
-
/// @notice
|
|
739
|
-
///
|
|
735
|
+
/// @notice Emit the `SetTokenUriResolver` event and persist the new resolver in the store. Pass `address(0)` to
|
|
736
|
+
/// clear the resolver and fall back to the default IPFS-based URI.
|
|
737
|
+
/// @param tokenUriResolver The new token URI resolver (or address(0) to clear).
|
|
740
738
|
function _recordSetTokenUriResolver(IJB721TokenUriResolver tokenUriResolver) internal {
|
|
741
739
|
emit SetTokenUriResolver({resolver: tokenUriResolver, caller: _msgSender()});
|
|
742
740
|
|
|
743
741
|
STORE.recordSetTokenUriResolver(tokenUriResolver);
|
|
744
742
|
}
|
|
745
743
|
|
|
746
|
-
/// @notice
|
|
744
|
+
/// @notice Delegate discount percent storage to the library, which validates and records it in the store.
|
|
747
745
|
/// @param tierId The ID of the tier to set the discount percent for.
|
|
748
|
-
/// @param discountPercent The discount percent to set
|
|
746
|
+
/// @param discountPercent The discount percent to set (0 = no discount, up to DISCOUNT_DENOMINATOR = free).
|
|
749
747
|
function _setDiscountPercentOf(uint256 tierId, uint256 discountPercent) internal {
|
|
750
|
-
// slither-disable-next-line calls-loop
|
|
751
748
|
JB721TiersHookLib.setDiscountPercentOf({
|
|
752
749
|
store: STORE, tierId: tierId, discountPercent: discountPercent, caller: _msgSender()
|
|
753
750
|
});
|
|
754
751
|
}
|
|
755
752
|
|
|
756
753
|
/// @notice Before transferring an NFT, register its first owner (if necessary).
|
|
757
|
-
/// @param to The address the NFT
|
|
758
|
-
/// @param tokenId The token ID of the NFT
|
|
754
|
+
/// @param to The address to transfer the NFT to.
|
|
755
|
+
/// @param tokenId The token ID of the NFT to transfer.
|
|
759
756
|
function _update(address to, uint256 tokenId, address auth) internal virtual override returns (address from) {
|
|
760
757
|
// Get only the tier ID and transfersPausable flag (lightweight — avoids full struct construction).
|
|
761
|
-
// slither-disable-next-line calls-loop
|
|
762
758
|
(uint256 tierId, bool transfersPausable) =
|
|
763
759
|
STORE.tierTransferInfoOfTokenId({hook: address(this), tokenId: tokenId});
|
|
764
760
|
|
|
@@ -778,26 +774,26 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
778
774
|
&& JB721TiersRulesetMetadataResolver.transfersPaused(
|
|
779
775
|
(JBRulesetMetadataResolver.metadata(ruleset))
|
|
780
776
|
)
|
|
781
|
-
)
|
|
777
|
+
) {
|
|
778
|
+
revert JB721TiersHook_TierTransfersPaused({
|
|
779
|
+
projectId: PROJECT_ID, tokenId: tokenId, from: from, to: to
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
782
|
}
|
|
783
783
|
|
|
784
784
|
// If the token isn't already associated with a first owner, store the sender as the first owner.
|
|
785
|
-
// slither-disable-next-line calls-loop
|
|
786
785
|
if (_firstOwnerOf[tokenId] == address(0)) _firstOwnerOf[tokenId] = from;
|
|
787
786
|
}
|
|
788
787
|
|
|
789
788
|
// Record the transfer.
|
|
790
|
-
// slither-disable-next-line reentrency-events,calls-loop
|
|
791
789
|
STORE.recordTransferForTier({tierId: tierId, from: from, to: to});
|
|
792
790
|
|
|
793
791
|
// Deploy the checkpoint module lazily on the first transfer.
|
|
794
792
|
if (address(CHECKPOINTS) == address(0)) {
|
|
795
|
-
// slither-disable-next-line calls-loop,reentrancy-events
|
|
796
793
|
CHECKPOINTS = CHECKPOINTS_DEPLOYER.deploy(address(this));
|
|
797
794
|
}
|
|
798
795
|
|
|
799
796
|
// Notify the checkpoint module to update checkpointed voting power.
|
|
800
|
-
// slither-disable-next-line calls-loop,reentrancy-events
|
|
801
797
|
CHECKPOINTS.onTransfer({from: from, to: to, tokenId: tokenId});
|
|
802
798
|
}
|
|
803
799
|
}
|
|
@@ -13,7 +13,9 @@ import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
|
|
|
13
13
|
import {JBDeploy721TiersHookConfig} from "./structs/JBDeploy721TiersHookConfig.sol";
|
|
14
14
|
|
|
15
15
|
/// @title JB721TiersHookDeployer
|
|
16
|
-
/// @notice
|
|
16
|
+
/// @notice Factory that deploys EIP-1167 clones of `JB721TiersHook` for existing projects. Each clone is initialized
|
|
17
|
+
/// with its own tiers, metadata, and flags, then ownership is transferred to the caller. The deployed hook is
|
|
18
|
+
/// registered in the `IJBAddressRegistry` for cross-chain address verification.
|
|
17
19
|
contract JB721TiersHookDeployer is ERC2771Context, IJB721TiersHookDeployer {
|
|
18
20
|
//*********************************************************************//
|
|
19
21
|
// --------------- public immutable stored properties ---------------- //
|
|
@@ -22,7 +24,7 @@ contract JB721TiersHookDeployer is ERC2771Context, IJB721TiersHookDeployer {
|
|
|
22
24
|
/// @notice A registry which stores references to contracts and their deployers.
|
|
23
25
|
IJBAddressRegistry public immutable ADDRESS_REGISTRY;
|
|
24
26
|
|
|
25
|
-
/// @notice
|
|
27
|
+
/// @notice The reference 721 tiers hook implementation that gets cloned for each new deployment.
|
|
26
28
|
JB721TiersHook public immutable HOOK;
|
|
27
29
|
|
|
28
30
|
/// @notice The contract that stores and manages data for this contract's NFTs.
|
|
@@ -60,10 +62,11 @@ contract JB721TiersHookDeployer is ERC2771Context, IJB721TiersHookDeployer {
|
|
|
60
62
|
// ---------------------- external transactions ---------------------- //
|
|
61
63
|
//*********************************************************************//
|
|
62
64
|
|
|
63
|
-
/// @notice
|
|
65
|
+
/// @notice Deploy a new 721 tiers hook for a project. Clones the implementation, initializes it with the provided
|
|
66
|
+
/// tiers and flags, transfers ownership to the caller, and registers the hook in the address registry.
|
|
64
67
|
/// @param projectId The ID of the project to deploy the hook for.
|
|
65
|
-
/// @param deployTiersHookConfig The
|
|
66
|
-
/// @param salt A salt
|
|
68
|
+
/// @param deployTiersHookConfig The tiers, metadata, and flags to initialize the hook with.
|
|
69
|
+
/// @param salt A salt for deterministic (CREATE2) deployment. Pass `bytes32(0)` for non-deterministic deployment.
|
|
67
70
|
/// @return newHook The address of the newly deployed hook.
|
|
68
71
|
function deployHookFor(
|
|
69
72
|
uint256 projectId,
|
|
@@ -100,7 +103,6 @@ contract JB721TiersHookDeployer is ERC2771Context, IJB721TiersHookDeployer {
|
|
|
100
103
|
JBOwnable(address(newHook)).transferOwnership(_msgSender());
|
|
101
104
|
|
|
102
105
|
// Increment the nonce.
|
|
103
|
-
// slither-disable-next-line reentrancy-benign
|
|
104
106
|
++_nonce;
|
|
105
107
|
|
|
106
108
|
// Add the hook to the address registry. This contract's nonce starts at 1.
|