@bananapus/721-hook-v6 0.0.42 → 0.0.45
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/foundry.lock +1 -7
- package/foundry.toml +1 -1
- package/package.json +20 -9
- package/script/Deploy.s.sol +2 -2
- package/src/JB721Checkpoints.sol +61 -19
- package/src/JB721CheckpointsDeployer.sol +10 -5
- package/src/JB721TiersHook.sol +66 -53
- package/src/JB721TiersHookDeployer.sol +8 -5
- package/src/JB721TiersHookProjectDeployer.sol +87 -46
- package/src/JB721TiersHookStore.sol +137 -107
- package/src/abstract/JB721Hook.sol +8 -6
- package/src/interfaces/IJB721Checkpoints.sol +21 -14
- package/src/interfaces/IJB721CheckpointsDeployer.sol +7 -3
- package/src/interfaces/IJB721TiersHook.sol +3 -3
- package/src/interfaces/IJB721TiersHookProjectDeployer.sol +4 -2
- package/src/interfaces/IJB721TiersHookStore.sol +11 -11
- package/src/libraries/JB721TiersHookLib.sol +1 -1
- package/src/structs/JB721TiersHookFlags.sol +1 -1
- package/src/structs/JBPayDataHookRulesetMetadata.sol +1 -1
- package/test/utils/AccessJBLib.sol +49 -0
- package/test/utils/ForTest_JB721TiersHook.sol +246 -0
- package/test/utils/TestBaseWorkflow.sol +213 -0
- package/test/utils/UnitTestSetup.sol +805 -0
- package/.gas-snapshot +0 -152
- package/ADMINISTRATION.md +0 -87
- package/ARCHITECTURE.md +0 -98
- package/AUDIT_INSTRUCTIONS.md +0 -77
- package/RISKS.md +0 -118
- package/SKILLS.md +0 -43
- package/STYLE_GUIDE.md +0 -610
- package/USER_JOURNEYS.md +0 -121
- package/assets/findings/nana-721-hook-v6-pashov-ai-audit-report-20260330-091257.md +0 -83
- package/slither-ci.config.json +0 -10
- package/test/721HookAttacks.t.sol +0 -408
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +0 -985
- package/test/Fork.t.sol +0 -2346
- package/test/TestAuditGaps.sol +0 -1075
- package/test/TestCheckpoints.t.sol +0 -341
- package/test/TestSafeTransferReentrancy.t.sol +0 -305
- package/test/TestVotingUnitsLifecycle.t.sol +0 -313
- package/test/audit/AuditRegressions.t.sol +0 -83
- package/test/audit/CodexNemesisReserveSellout.t.sol +0 -66
- package/test/audit/CrossCurrencySplitNoPrices.t.sol +0 -123
- package/test/audit/FreshAudit.t.sol +0 -197
- package/test/audit/FutureTierPoC.t.sol +0 -39
- package/test/audit/FutureTierRemoval.t.sol +0 -47
- package/test/audit/Pass12L18.t.sol +0 -80
- package/test/audit/PayCreditsBypassTierSplits.t.sol +0 -200
- package/test/audit/ProjectDeployerAuth.t.sol +0 -266
- package/test/audit/RepoFindings.t.sol +0 -195
- package/test/audit/ReserveActivation.t.sol +0 -87
- package/test/audit/ReserveSlotProtection.t.sol +0 -273
- package/test/audit/RetroactiveReserveBeneficiaryDilution.t.sol +0 -149
- package/test/audit/SameCurrencyDecimalMismatch.t.sol +0 -249
- package/test/audit/SplitCreditsMismatch.t.sol +0 -219
- package/test/audit/SplitFailureRedistribution.t.sol +0 -143
- package/test/audit/USDTVoidReturnCompat.t.sol +0 -301
- package/test/fork/ERC20CashOutFork.t.sol +0 -633
- package/test/fork/ERC20TierSplitFork.t.sol +0 -596
- package/test/fork/IssueTokensForSplitsFork.t.sol +0 -516
- package/test/invariants/TierLifecycleInvariant.t.sol +0 -188
- package/test/invariants/TieredHookStoreInvariant.t.sol +0 -86
- package/test/invariants/handlers/TierLifecycleHandler.sol +0 -300
- package/test/invariants/handlers/TierStoreHandler.sol +0 -165
- package/test/regression/BrokenTerminalDoesNotDos.t.sol +0 -277
- package/test/regression/CacheTierLookup.t.sol +0 -190
- package/test/regression/ProjectDeployerRulesets.t.sol +0 -358
- package/test/regression/ReserveBeneficiaryOverwrite.t.sol +0 -155
- package/test/regression/SplitDistributionBugs.t.sol +0 -751
- package/test/regression/SplitNoBeneficiary.t.sol +0 -140
- package/test/unit/AuditFixes_Unit.t.sol +0 -624
- package/test/unit/JB721CheckpointsDeployer_AccessControl.t.sol +0 -116
- package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +0 -144
- package/test/unit/JBBitmap.t.sol +0 -170
- package/test/unit/JBIpfsDecoder.t.sol +0 -136
- package/test/unit/TierSupplyReserveCheck.t.sol +0 -221
- package/test/unit/adjustTier_Unit.t.sol +0 -1942
- package/test/unit/deployer_Unit.t.sol +0 -114
- package/test/unit/getters_constructor_Unit.t.sol +0 -593
- package/test/unit/mintFor_mintReservesFor_Unit.t.sol +0 -452
- package/test/unit/pay_CrossCurrency_Unit.t.sol +0 -530
- package/test/unit/pay_Unit.t.sol +0 -1661
- package/test/unit/redeem_Unit.t.sol +0 -473
- package/test/unit/relayBeneficiary_Unit.t.sol +0 -182
- package/test/unit/splitHookDistribution_Unit.t.sol +0 -604
- package/test/unit/tierSplitRouting_Unit.t.sol +0 -757
package/foundry.lock
CHANGED
package/foundry.toml
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/721-hook-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.45",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/Bananapus/nana-721-hook-v6"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"CHANGELOG.md",
|
|
11
|
+
"foundry.lock",
|
|
12
|
+
"foundry.toml",
|
|
13
|
+
"references/",
|
|
14
|
+
"remappings.txt",
|
|
15
|
+
"script/",
|
|
16
|
+
"sphinx.lock",
|
|
17
|
+
"src/",
|
|
18
|
+
"test/utils/"
|
|
19
|
+
],
|
|
9
20
|
"engines": {
|
|
10
21
|
"node": ">=20.0.0"
|
|
11
22
|
},
|
|
@@ -17,15 +28,15 @@
|
|
|
17
28
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-721-hook-v6'"
|
|
18
29
|
},
|
|
19
30
|
"dependencies": {
|
|
20
|
-
"@bananapus/address-registry-v6": "
|
|
21
|
-
"@bananapus/core-v6": "^0.0.
|
|
22
|
-
"@bananapus/ownable-v6": "^0.0.
|
|
23
|
-
"@bananapus/permission-ids-v6": "
|
|
24
|
-
"@openzeppelin/contracts": "
|
|
25
|
-
"@prb/math": "
|
|
26
|
-
"solady": "
|
|
31
|
+
"@bananapus/address-registry-v6": "0.0.25",
|
|
32
|
+
"@bananapus/core-v6": "^0.0.39",
|
|
33
|
+
"@bananapus/ownable-v6": "^0.0.24",
|
|
34
|
+
"@bananapus/permission-ids-v6": "0.0.22",
|
|
35
|
+
"@openzeppelin/contracts": "5.6.1",
|
|
36
|
+
"@prb/math": "4.1.1",
|
|
37
|
+
"solady": "0.1.26"
|
|
27
38
|
},
|
|
28
39
|
"devDependencies": {
|
|
29
|
-
"@sphinx-labs/plugins": "
|
|
40
|
+
"@sphinx-labs/plugins": "0.33.3"
|
|
30
41
|
}
|
|
31
42
|
}
|
package/script/Deploy.s.sol
CHANGED
|
@@ -87,12 +87,12 @@ contract DeployScript is Script, Sphinx {
|
|
|
87
87
|
(address _deployer, bool _deployerIsDeployed) = _isDeployed({
|
|
88
88
|
salt: CHECKPOINTS_DEPLOYER_SALT,
|
|
89
89
|
creationCode: type(JB721CheckpointsDeployer).creationCode,
|
|
90
|
-
arguments:
|
|
90
|
+
arguments: abi.encode(store)
|
|
91
91
|
});
|
|
92
92
|
|
|
93
93
|
// Deploy it if it has not been deployed yet.
|
|
94
94
|
checkpointsDeployer = !_deployerIsDeployed
|
|
95
|
-
? new JB721CheckpointsDeployer{salt: CHECKPOINTS_DEPLOYER_SALT}()
|
|
95
|
+
? new JB721CheckpointsDeployer{salt: CHECKPOINTS_DEPLOYER_SALT}(store)
|
|
96
96
|
: JB721CheckpointsDeployer(_deployer);
|
|
97
97
|
}
|
|
98
98
|
|
package/src/JB721Checkpoints.sol
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.28;
|
|
3
3
|
|
|
4
|
-
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
|
|
5
4
|
import {Votes} from "@openzeppelin/contracts/governance/utils/Votes.sol";
|
|
5
|
+
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
|
|
6
|
+
import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol";
|
|
7
|
+
|
|
6
8
|
import {IJB721Checkpoints} from "./interfaces/IJB721Checkpoints.sol";
|
|
9
|
+
import {IJB721TiersHook} from "./interfaces/IJB721TiersHook.sol";
|
|
7
10
|
import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
|
|
8
11
|
|
|
9
12
|
/// @title JB721Checkpoints
|
|
@@ -14,6 +17,8 @@ import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
|
|
|
14
17
|
/// (`_cachedThis`) is uninitialized on clones, so `domainSeparatorV4()` always rebuilds using the clone's
|
|
15
18
|
/// `address(this)` — correct behavior, tiny gas overhead.
|
|
16
19
|
contract JB721Checkpoints is Votes, IJB721Checkpoints {
|
|
20
|
+
using Checkpoints for Checkpoints.Trace160;
|
|
21
|
+
|
|
17
22
|
//*********************************************************************//
|
|
18
23
|
// --------------------------- custom errors ------------------------- //
|
|
19
24
|
//*********************************************************************//
|
|
@@ -22,55 +27,67 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
|
|
|
22
27
|
error JB721Checkpoints_Unauthorized();
|
|
23
28
|
|
|
24
29
|
//*********************************************************************//
|
|
25
|
-
//
|
|
30
|
+
// --------------- public immutable stored properties ---------------- //
|
|
26
31
|
//*********************************************************************//
|
|
27
32
|
|
|
28
|
-
/// @notice
|
|
29
|
-
|
|
33
|
+
/// @notice The store that holds tier and voting data for the hook's NFTs.
|
|
34
|
+
IJB721TiersHookStore public immutable override STORE;
|
|
30
35
|
|
|
31
36
|
//*********************************************************************//
|
|
32
|
-
//
|
|
37
|
+
// --------------------- public stored properties -------------------- //
|
|
33
38
|
//*********************************************************************//
|
|
34
39
|
|
|
35
40
|
/// @notice The hook that this module tracks voting power for.
|
|
36
41
|
address public override HOOK;
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
//*********************************************************************//
|
|
44
|
+
// -------------------- internal stored properties ------------------- //
|
|
45
|
+
//*********************************************************************//
|
|
46
|
+
|
|
47
|
+
/// @notice Checkpointed token owners for historical reward eligibility after first transfer.
|
|
48
|
+
/// @custom:param tokenId The token ID to get historical owner checkpoints for.
|
|
49
|
+
mapping(uint256 tokenId => Checkpoints.Trace160) internal _ownerCheckpointsOf;
|
|
40
50
|
|
|
41
51
|
//*********************************************************************//
|
|
42
52
|
// -------------------------- constructor ---------------------------- //
|
|
43
53
|
//*********************************************************************//
|
|
44
54
|
|
|
45
|
-
/// @dev
|
|
46
|
-
///
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
/// @dev The implementation contract is initialized in the constructor to prevent direct use. Clones are initialized
|
|
56
|
+
/// via `initialize()`.
|
|
57
|
+
/// @param store The store that holds tier data for each hook's NFTs.
|
|
58
|
+
constructor(IJB721TiersHookStore store) EIP712("JB721Checkpoints", "1") {
|
|
59
|
+
STORE = store;
|
|
60
|
+
HOOK = address(1);
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
//*********************************************************************//
|
|
52
64
|
// ---------------------- external transactions ---------------------- //
|
|
53
65
|
//*********************************************************************//
|
|
54
66
|
|
|
55
|
-
/// @notice Initializes a cloned module with its hook
|
|
67
|
+
/// @notice Initializes a cloned module with its hook reference.
|
|
56
68
|
/// @dev Can only be called once. Called by the deployer after cloning.
|
|
57
69
|
/// @param hook The hook this module serves.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
function initialize(address hook) external override {
|
|
71
|
+
if (HOOK != address(0)) revert JB721Checkpoints_AlreadyInitialized();
|
|
72
|
+
// `hook` cannot be zero when called through the deployer because `msg.sender` must equal `hook`.
|
|
73
|
+
// slither-disable-next-line missing-zero-check
|
|
62
74
|
HOOK = hook;
|
|
63
|
-
STORE = store;
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
/// @notice Called by the hook after every NFT transfer to update checkpointed voting power.
|
|
67
78
|
/// @dev Only callable by the HOOK. Looks up the token's tier voting units from the store.
|
|
68
79
|
/// @param from The previous owner (address(0) on mint).
|
|
69
80
|
/// @param to The new owner (address(0) on burn).
|
|
70
|
-
/// @param tokenId The token ID
|
|
81
|
+
/// @param tokenId The token ID to transfer.
|
|
71
82
|
function onTransfer(address from, address to, uint256 tokenId) external override {
|
|
72
83
|
if (msg.sender != HOOK) revert JB721Checkpoints_Unauthorized();
|
|
73
84
|
|
|
85
|
+
if (from != address(0)) {
|
|
86
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
87
|
+
// slither-disable-next-line unused-return
|
|
88
|
+
_ownerCheckpointsOf[tokenId].push({key: uint96(block.number), value: uint160(to)});
|
|
89
|
+
}
|
|
90
|
+
|
|
74
91
|
// Look up this token's tier to get its voting units.
|
|
75
92
|
uint256 votingUnits = STORE.tierOfTokenId({hook: HOOK, tokenId: tokenId, includeResolvedUri: false}).votingUnits;
|
|
76
93
|
|
|
@@ -79,7 +96,32 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
|
|
|
79
96
|
}
|
|
80
97
|
|
|
81
98
|
//*********************************************************************//
|
|
82
|
-
//
|
|
99
|
+
// ----------------------- external views ---------------------------- //
|
|
100
|
+
//*********************************************************************//
|
|
101
|
+
|
|
102
|
+
/// @notice The owner of an NFT at a past block.
|
|
103
|
+
/// @dev Mints do not write per-token checkpoint storage. Until a token's first non-mint transfer, ownership is
|
|
104
|
+
/// inferred from the hook's `firstOwnerOf`.
|
|
105
|
+
/// @param tokenId The token ID of the NFT to get the historical owner of.
|
|
106
|
+
/// @param blockNumber The block number to look up.
|
|
107
|
+
/// @return The owner of the token at `blockNumber`, or zero if the token has no known owner.
|
|
108
|
+
function ownerOfAt(uint256 tokenId, uint256 blockNumber) external view override returns (address) {
|
|
109
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
110
|
+
uint96 blockNumber96 = uint96(blockNumber);
|
|
111
|
+
|
|
112
|
+
Checkpoints.Trace160 storage checkpoints = _ownerCheckpointsOf[tokenId];
|
|
113
|
+
uint256 checkpointCount = checkpoints.length();
|
|
114
|
+
|
|
115
|
+
// Before the first transfer/burn checkpoint, the mint owner is implicit in the hook's first-owner tracking.
|
|
116
|
+
if (checkpointCount == 0 || checkpoints.at(0)._key > blockNumber96) {
|
|
117
|
+
return IJB721TiersHook(HOOK).firstOwnerOf(tokenId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return address(uint160(checkpoints.upperLookupRecent(blockNumber96)));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
//*********************************************************************//
|
|
124
|
+
// ----------------------- internal views ---------------------------- //
|
|
83
125
|
//*********************************************************************//
|
|
84
126
|
|
|
85
127
|
/// @notice Returns the total voting units held by an account (across all tiers).
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
pragma solidity 0.8.28;
|
|
3
3
|
|
|
4
4
|
import {LibClone} from "solady/src/utils/LibClone.sol";
|
|
5
|
+
|
|
5
6
|
import {JB721Checkpoints} from "./JB721Checkpoints.sol";
|
|
6
7
|
import {IJB721Checkpoints} from "./interfaces/IJB721Checkpoints.sol";
|
|
7
8
|
import {IJB721CheckpointsDeployer} from "./interfaces/IJB721CheckpointsDeployer.sol";
|
|
@@ -19,12 +20,17 @@ contract JB721CheckpointsDeployer is IJB721CheckpointsDeployer {
|
|
|
19
20
|
/// @notice The checkpoint module implementation that clones delegate to.
|
|
20
21
|
address public immutable override IMPLEMENTATION;
|
|
21
22
|
|
|
23
|
+
/// @notice The store that holds tier and voting data for each hook's NFTs.
|
|
24
|
+
IJB721TiersHookStore public immutable override STORE;
|
|
25
|
+
|
|
22
26
|
//*********************************************************************//
|
|
23
27
|
// -------------------------- constructor ---------------------------- //
|
|
24
28
|
//*********************************************************************//
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
/// @param store The store that holds tier data for each hook's NFTs.
|
|
31
|
+
constructor(IJB721TiersHookStore store) {
|
|
32
|
+
STORE = store;
|
|
33
|
+
IMPLEMENTATION = address(new JB721Checkpoints(store));
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
//*********************************************************************//
|
|
@@ -34,14 +40,13 @@ contract JB721CheckpointsDeployer is IJB721CheckpointsDeployer {
|
|
|
34
40
|
/// @notice Deploys a new deterministic checkpoint clone for the given hook.
|
|
35
41
|
/// @dev Uses CREATE2 with the hook address as salt so the clone address is the same across chains.
|
|
36
42
|
/// @param hook The hook address the module will serve.
|
|
37
|
-
/// @param store The store that holds tier data for the hook's NFTs.
|
|
38
43
|
/// @return module The newly deployed and initialized checkpoint module.
|
|
39
|
-
function deploy(address hook
|
|
44
|
+
function deploy(address hook) external override returns (IJB721Checkpoints module) {
|
|
40
45
|
if (msg.sender != hook) revert JB721CheckpointsDeployer_Unauthorized();
|
|
41
46
|
|
|
42
47
|
module = IJB721Checkpoints(
|
|
43
48
|
LibClone.cloneDeterministic({implementation: IMPLEMENTATION, salt: bytes32(uint256(uint160(hook)))})
|
|
44
49
|
);
|
|
45
|
-
module.initialize(
|
|
50
|
+
module.initialize(hook);
|
|
46
51
|
}
|
|
47
52
|
}
|
package/src/JB721TiersHook.sol
CHANGED
|
@@ -151,18 +151,19 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
151
151
|
// ------------------------- external views -------------------------- //
|
|
152
152
|
//*********************************************************************//
|
|
153
153
|
|
|
154
|
-
/// @notice The
|
|
155
|
-
///
|
|
156
|
-
/// @param tokenId The token ID of the NFT
|
|
154
|
+
/// @notice The address that originally received an NFT (typically the payer). Tracked separately from the current
|
|
155
|
+
/// owner so it persists through transfers, useful for provenance and historical voting checkpoints.
|
|
156
|
+
/// @param tokenId The token ID of the NFT.
|
|
157
157
|
/// @return The address of the NFT's first owner.
|
|
158
158
|
function firstOwnerOf(uint256 tokenId) external view override returns (address) {
|
|
159
159
|
address first = _firstOwnerOf[tokenId];
|
|
160
160
|
return first != address(0) ? first : _ownerOf(tokenId);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
/// @notice
|
|
163
|
+
/// @notice The currency and decimal precision used for this hook's tier prices. For example, if tiers are priced
|
|
164
|
+
/// in ETH with 18 decimals, `currency` would be the ETH currency ID and `decimals` would be 18.
|
|
164
165
|
/// @return currency The currency used for tier prices.
|
|
165
|
-
/// @return decimals The
|
|
166
|
+
/// @return decimals The number of decimals used in tier prices.
|
|
166
167
|
function pricingContext() external view override returns (uint256 currency, uint256 decimals) {
|
|
167
168
|
// Get a reference to the packed pricing context.
|
|
168
169
|
uint256 packed = _packedPricingContext;
|
|
@@ -185,12 +186,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
185
186
|
return STORE.balanceOf({hook: address(this), owner: owner});
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
/// @notice
|
|
189
|
-
///
|
|
190
|
-
/// @
|
|
191
|
-
/// @
|
|
192
|
-
/// project (
|
|
193
|
-
/// @return hookSpecifications
|
|
189
|
+
/// @notice Called by the terminal before recording a payment. Calculates how much of the payment should be routed
|
|
190
|
+
/// to tier-based splits vs. kept by the project, and adjusts the minting weight accordingly.
|
|
191
|
+
/// @dev Overrides the base to compute tier split amounts from each tier's `splitPercent`.
|
|
192
|
+
/// @param context The payment context from the terminal.
|
|
193
|
+
/// @return weight The adjusted weight for project token minting (reduced when splits route funds away).
|
|
194
|
+
/// @return hookSpecifications Specifies this hook as the pay hook, with the split amount to forward.
|
|
194
195
|
function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
|
|
195
196
|
public
|
|
196
197
|
view
|
|
@@ -222,17 +223,20 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
222
223
|
});
|
|
223
224
|
}
|
|
224
225
|
|
|
225
|
-
/// @notice The combined cash
|
|
226
|
-
///
|
|
227
|
-
/// @
|
|
228
|
-
/// @
|
|
229
|
-
/// @return weight The cash out weight of the tokenIds.
|
|
226
|
+
/// @notice The combined cash-out weight of specific NFTs. Divide by `totalCashOutWeight()` to get the fraction of
|
|
227
|
+
/// surplus these NFTs can reclaim. Weight is based on the original tier price, not any discount paid.
|
|
228
|
+
/// @param tokenIds The token IDs of the NFTs to get the combined cash-out weight of.
|
|
229
|
+
/// @return weight The combined cash-out weight.
|
|
230
230
|
function cashOutWeightOf(uint256[] memory tokenIds) public view virtual override returns (uint256) {
|
|
231
231
|
return STORE.cashOutWeightOf({hook: address(this), tokenIds: tokenIds});
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
/// @notice
|
|
235
|
-
///
|
|
234
|
+
/// @notice Initialize a cloned copy of the hook. Sets the project association, ERC-721 name/symbol, pricing
|
|
235
|
+
/// context (currency + decimals), metadata URIs, initial tiers, and behavioral flags. Can only be called once
|
|
236
|
+
/// per clone — the implementation contract is pre-initialized in its constructor to prevent misuse.
|
|
237
|
+
/// @dev Called by `JB721TiersHookDeployer` immediately after cloning. Reverts with
|
|
238
|
+
/// `JB721TiersHook_AlreadyInitialized` if called more than once, or `JB721TiersHook_NoProjectId` if projectId is 0.
|
|
239
|
+
/// @param projectId The ID of the project this hook is associated with.
|
|
236
240
|
/// @param name The name of the NFT collection.
|
|
237
241
|
/// @param symbol The symbol representing the NFT collection.
|
|
238
242
|
/// @param baseUri The URI to use as a base for full NFT `tokenUri`s.
|
|
@@ -328,9 +332,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
328
332
|
return JB721TiersHookLib.resolveTokenURI(STORE, address(this), baseURI, tokenId);
|
|
329
333
|
}
|
|
330
334
|
|
|
331
|
-
/// @notice The
|
|
332
|
-
///
|
|
333
|
-
/// @return weight The total cash
|
|
335
|
+
/// @notice The total cash-out weight across all outstanding NFTs and pending reserves. This is the denominator
|
|
336
|
+
/// for cash-out calculations — an NFT's share of the surplus is its weight divided by this total.
|
|
337
|
+
/// @return weight The total cash-out weight.
|
|
334
338
|
function totalCashOutWeight() public view virtual override returns (uint256) {
|
|
335
339
|
return STORE.totalCashOutWeight(address(this));
|
|
336
340
|
}
|
|
@@ -339,12 +343,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
339
343
|
// ---------------------- external transactions ---------------------- //
|
|
340
344
|
//*********************************************************************//
|
|
341
345
|
|
|
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
|
|
346
|
+
/// @notice Add new NFT tiers or remove existing ones. Added tiers get sequential IDs and must be sorted by
|
|
347
|
+
/// category. Removed tiers stop accepting new mints but existing NFTs remain valid.
|
|
348
|
+
/// @dev Only the collection owner or an operator with `ADJUST_721_TIERS` permission can call this.
|
|
349
|
+
/// @dev Added tiers must respect this hook's flags (e.g. `noNewTiersWithVotes`, `noNewTiersWithReserves`).
|
|
350
|
+
/// @param tiersToAdd The tiers to add, as an array of `JB721TierConfig` structs.
|
|
351
|
+
/// @param tierIdsToRemove The IDs of the tiers to remove.
|
|
348
352
|
function adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata tierIdsToRemove) external override {
|
|
349
353
|
// Enforce permissions.
|
|
350
354
|
_requirePermissionFrom({
|
|
@@ -363,9 +367,11 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
363
367
|
});
|
|
364
368
|
}
|
|
365
369
|
|
|
366
|
-
/// @notice Manually mint NFTs from
|
|
370
|
+
/// @notice Manually mint NFTs from specific tiers to a beneficiary, without requiring payment. Only tiers with
|
|
371
|
+
/// `allowOwnerMint` enabled can be minted this way.
|
|
372
|
+
/// @dev Only the collection owner or an operator with `MINT_721` permission can call this.
|
|
367
373
|
/// @param tierIds The IDs of the tiers to mint from.
|
|
368
|
-
/// @param beneficiary The address to mint to.
|
|
374
|
+
/// @param beneficiary The address to mint the NFTs to.
|
|
369
375
|
/// @return tokenIds The IDs of the newly minted tokens.
|
|
370
376
|
function mintFor(
|
|
371
377
|
uint16[] calldata tierIds,
|
|
@@ -389,9 +395,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
389
395
|
_mintTokens({tokenIds: tokenIds, tierIds: tierIds, beneficiary: beneficiary, totalAmountPaid: 0});
|
|
390
396
|
}
|
|
391
397
|
|
|
392
|
-
/// @notice Mint pending reserved NFTs
|
|
393
|
-
///
|
|
394
|
-
/// @param reserveMintConfigs
|
|
398
|
+
/// @notice Mint pending reserved NFTs across multiple tiers in a single call. Reserves accumulate automatically
|
|
399
|
+
/// as NFTs are sold (based on each tier's `reserveFrequency`) and anyone can trigger their minting.
|
|
400
|
+
/// @param reserveMintConfigs The tier IDs and counts specifying how many reserves to mint from each tier.
|
|
395
401
|
function mintPendingReservesFor(JB721TiersMintReservesConfig[] calldata reserveMintConfigs) external override {
|
|
396
402
|
for (uint256 i; i < reserveMintConfigs.length;) {
|
|
397
403
|
// Get a reference to the params being iterated upon.
|
|
@@ -406,12 +412,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
406
412
|
}
|
|
407
413
|
}
|
|
408
414
|
|
|
409
|
-
/// @notice
|
|
410
|
-
///
|
|
411
|
-
///
|
|
412
|
-
///
|
|
415
|
+
/// @notice Set a discount on a tier's price. Discounts reduce the price payers must pay, but don't affect the
|
|
416
|
+
/// NFT's cash-out weight (which always uses the original price). The tier must have `cannotIncreaseDiscountPercent`
|
|
417
|
+
/// set appropriately.
|
|
418
|
+
/// @dev Only the collection owner or an operator with `SET_721_DISCOUNT_PERCENT` permission can call this.
|
|
413
419
|
/// @param tierId The ID of the tier to set the discount of.
|
|
414
|
-
/// @param discountPercent The discount percent to set.
|
|
420
|
+
/// @param discountPercent The discount percent to set (0–100).
|
|
415
421
|
function setDiscountPercentOf(uint256 tierId, uint256 discountPercent) external override {
|
|
416
422
|
// Enforce permissions.
|
|
417
423
|
_requirePermissionFrom({
|
|
@@ -420,8 +426,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
420
426
|
_setDiscountPercentOf({tierId: tierId, discountPercent: discountPercent});
|
|
421
427
|
}
|
|
422
428
|
|
|
423
|
-
/// @notice
|
|
424
|
-
/// @
|
|
429
|
+
/// @notice Set discount percentages for multiple tiers in a single call.
|
|
430
|
+
/// @dev Only the collection owner or an operator with `SET_721_DISCOUNT_PERCENT` permission can call this.
|
|
431
|
+
/// @param configs An array of tier ID + discount percent pairs to apply.
|
|
425
432
|
function setDiscountPercentsOf(JB721TiersSetDiscountPercentConfig[] calldata configs) external override {
|
|
426
433
|
// Enforce permissions.
|
|
427
434
|
_requirePermissionFrom({
|
|
@@ -440,7 +447,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
440
447
|
}
|
|
441
448
|
}
|
|
442
449
|
|
|
443
|
-
/// @notice Update this hook's metadata properties.
|
|
450
|
+
/// @notice Update any combination of this hook's metadata properties in a single call. Pass empty strings or
|
|
451
|
+
/// sentinel values for fields you don't want to change.
|
|
444
452
|
/// @dev Only this contract's owner or an operator with the `SET_721_METADATA` permission can set the metadata.
|
|
445
453
|
/// @param name The new collection name. Send empty to leave unchanged.
|
|
446
454
|
/// @param symbol The new collection symbol. Send empty to leave unchanged.
|
|
@@ -510,8 +518,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
510
518
|
// ----------------------- public transactions ----------------------- //
|
|
511
519
|
//*********************************************************************//
|
|
512
520
|
|
|
513
|
-
/// @notice Mint
|
|
514
|
-
///
|
|
521
|
+
/// @notice Mint pending reserved NFTs from a specific tier. Anyone can call this — reserves are minted to the
|
|
522
|
+
/// tier's reserve beneficiary (or the hook's default). Reverts if the ruleset has reserve minting paused.
|
|
515
523
|
/// @param tierId The ID of the tier to mint reserved NFTs from.
|
|
516
524
|
/// @param count The number of reserved NFTs to mint.
|
|
517
525
|
function mintPendingReservesFor(uint256 tierId, uint256 count) public override {
|
|
@@ -591,11 +599,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
591
599
|
STORE.recordBurn(tokenIds);
|
|
592
600
|
}
|
|
593
601
|
|
|
594
|
-
/// @notice
|
|
595
|
-
///
|
|
596
|
-
/// @param
|
|
602
|
+
/// @notice Mint a batch of NFTs to the beneficiary and emit a `Mint` event for each. Called after the store has
|
|
603
|
+
/// recorded the mint and generated token IDs.
|
|
604
|
+
/// @param tokenIds The token IDs to mint (generated by the store based on tier and sequence number).
|
|
605
|
+
/// @param tierIds The tier IDs corresponding to each token (same length and order as `tokenIds`).
|
|
597
606
|
/// @param beneficiary The address receiving the NFTs.
|
|
598
|
-
/// @param totalAmountPaid The amount to report in the Mint event.
|
|
607
|
+
/// @param totalAmountPaid The total payment amount (including credits) to report in the Mint event for indexing.
|
|
599
608
|
function _mintTokens(
|
|
600
609
|
uint256[] memory tokenIds,
|
|
601
610
|
uint16[] memory tierIds,
|
|
@@ -656,6 +665,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
656
665
|
if (tokenIds.length != 0) {
|
|
657
666
|
// totalAmountPaid is the full amount available before recordMint deducted tier prices.
|
|
658
667
|
uint256 totalAmountPaid = (payer == beneficiary) ? value + payCredits : value;
|
|
668
|
+
// slither-disable-next-line reentrancy-events
|
|
659
669
|
_mintTokens({
|
|
660
670
|
tokenIds: tokenIds, tierIds: tierIdsToMint, beneficiary: beneficiary, totalAmountPaid: totalAmountPaid
|
|
661
671
|
});
|
|
@@ -734,17 +744,18 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
734
744
|
}
|
|
735
745
|
}
|
|
736
746
|
|
|
737
|
-
/// @notice
|
|
738
|
-
///
|
|
747
|
+
/// @notice Emit the `SetTokenUriResolver` event and persist the new resolver in the store. Pass `address(0)` to
|
|
748
|
+
/// clear the resolver and fall back to the default IPFS-based URI.
|
|
749
|
+
/// @param tokenUriResolver The new token URI resolver (or address(0) to clear).
|
|
739
750
|
function _recordSetTokenUriResolver(IJB721TokenUriResolver tokenUriResolver) internal {
|
|
740
751
|
emit SetTokenUriResolver({resolver: tokenUriResolver, caller: _msgSender()});
|
|
741
752
|
|
|
742
753
|
STORE.recordSetTokenUriResolver(tokenUriResolver);
|
|
743
754
|
}
|
|
744
755
|
|
|
745
|
-
/// @notice
|
|
756
|
+
/// @notice Delegate discount percent storage to the library, which validates and records it in the store.
|
|
746
757
|
/// @param tierId The ID of the tier to set the discount percent for.
|
|
747
|
-
/// @param discountPercent The discount percent to set
|
|
758
|
+
/// @param discountPercent The discount percent to set (0 = no discount, up to DISCOUNT_DENOMINATOR = free).
|
|
748
759
|
function _setDiscountPercentOf(uint256 tierId, uint256 discountPercent) internal {
|
|
749
760
|
// slither-disable-next-line calls-loop
|
|
750
761
|
JB721TiersHookLib.setDiscountPercentOf({
|
|
@@ -753,8 +764,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
753
764
|
}
|
|
754
765
|
|
|
755
766
|
/// @notice Before transferring an NFT, register its first owner (if necessary).
|
|
756
|
-
/// @param to The address the NFT
|
|
757
|
-
/// @param tokenId The token ID of the NFT
|
|
767
|
+
/// @param to The address to transfer the NFT to.
|
|
768
|
+
/// @param tokenId The token ID of the NFT to transfer.
|
|
758
769
|
function _update(address to, uint256 tokenId, address auth) internal virtual override returns (address from) {
|
|
759
770
|
// Get only the tier ID and transfersPausable flag (lightweight — avoids full struct construction).
|
|
760
771
|
// slither-disable-next-line calls-loop
|
|
@@ -791,10 +802,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
791
802
|
|
|
792
803
|
// Deploy the checkpoint module lazily on the first transfer.
|
|
793
804
|
if (address(CHECKPOINTS) == address(0)) {
|
|
794
|
-
|
|
805
|
+
// slither-disable-next-line calls-loop,reentrancy-events
|
|
806
|
+
CHECKPOINTS = CHECKPOINTS_DEPLOYER.deploy(address(this));
|
|
795
807
|
}
|
|
796
808
|
|
|
797
809
|
// Notify the checkpoint module to update checkpointed voting power.
|
|
810
|
+
// slither-disable-next-line calls-loop,reentrancy-events
|
|
798
811
|
CHECKPOINTS.onTransfer({from: from, to: to, tokenId: tokenId});
|
|
799
812
|
}
|
|
800
813
|
}
|
|
@@ -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,
|