@bananapus/721-hook-v6 0.0.1
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/.gas-snapshot +152 -0
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/SKILLS.md +140 -0
- package/docs/book.css +13 -0
- package/docs/book.toml +12 -0
- package/docs/solidity.min.js +74 -0
- package/docs/src/README.md +253 -0
- package/docs/src/SUMMARY.md +38 -0
- package/docs/src/src/JB721TiersHook.sol/contract.JB721TiersHook.md +645 -0
- package/docs/src/src/JB721TiersHookDeployer.sol/contract.JB721TiersHookDeployer.md +99 -0
- package/docs/src/src/JB721TiersHookProjectDeployer.sol/contract.JB721TiersHookProjectDeployer.md +288 -0
- package/docs/src/src/JB721TiersHookStore.sol/contract.JB721TiersHookStore.md +1096 -0
- package/docs/src/src/README.md +11 -0
- package/docs/src/src/abstract/ERC721.sol/abstract.ERC721.md +430 -0
- package/docs/src/src/abstract/JB721Hook.sol/abstract.JB721Hook.md +309 -0
- package/docs/src/src/abstract/README.md +5 -0
- package/docs/src/src/interfaces/IJB721Hook.sol/interface.IJB721Hook.md +29 -0
- package/docs/src/src/interfaces/IJB721TiersHook.sol/interface.IJB721TiersHook.md +203 -0
- package/docs/src/src/interfaces/IJB721TiersHookDeployer.sol/interface.IJB721TiersHookDeployer.md +25 -0
- package/docs/src/src/interfaces/IJB721TiersHookProjectDeployer.sol/interface.IJB721TiersHookProjectDeployer.md +64 -0
- package/docs/src/src/interfaces/IJB721TiersHookStore.sol/interface.IJB721TiersHookStore.md +265 -0
- package/docs/src/src/interfaces/IJB721TokenUriResolver.sol/interface.IJB721TokenUriResolver.md +12 -0
- package/docs/src/src/interfaces/README.md +9 -0
- package/docs/src/src/libraries/JB721Constants.sol/library.JB721Constants.md +14 -0
- package/docs/src/src/libraries/JB721TiersRulesetMetadataResolver.sol/library.JB721TiersRulesetMetadataResolver.md +68 -0
- package/docs/src/src/libraries/JBBitmap.sol/library.JBBitmap.md +82 -0
- package/docs/src/src/libraries/JBIpfsDecoder.sol/library.JBIpfsDecoder.md +61 -0
- package/docs/src/src/libraries/README.md +7 -0
- package/docs/src/src/structs/JB721InitTiersConfig.sol/struct.JB721InitTiersConfig.md +27 -0
- package/docs/src/src/structs/JB721Tier.sol/struct.JB721Tier.md +59 -0
- package/docs/src/src/structs/JB721TierConfig.sol/struct.JB721TierConfig.md +60 -0
- package/docs/src/src/structs/JB721TiersHookFlags.sol/struct.JB721TiersHookFlags.md +26 -0
- package/docs/src/src/structs/JB721TiersMintReservesConfig.sol/struct.JB721TiersMintReservesConfig.md +16 -0
- package/docs/src/src/structs/JB721TiersRulesetMetadata.sol/struct.JB721TiersRulesetMetadata.md +20 -0
- package/docs/src/src/structs/JB721TiersSetDiscountPercentConfig.sol/struct.JB721TiersSetDiscountPercentConfig.md +16 -0
- package/docs/src/src/structs/JBBitmapWord.sol/struct.JBBitmapWord.md +19 -0
- package/docs/src/src/structs/JBDeploy721TiersHookConfig.sol/struct.JBDeploy721TiersHookConfig.md +34 -0
- package/docs/src/src/structs/JBLaunchProjectConfig.sol/struct.JBLaunchProjectConfig.md +23 -0
- package/docs/src/src/structs/JBLaunchRulesetsConfig.sol/struct.JBLaunchRulesetsConfig.md +22 -0
- package/docs/src/src/structs/JBPayDataHookRulesetConfig.sol/struct.JBPayDataHookRulesetConfig.md +51 -0
- package/docs/src/src/structs/JBPayDataHookRulesetMetadata.sol/struct.JBPayDataHookRulesetMetadata.md +66 -0
- package/docs/src/src/structs/JBQueueRulesetsConfig.sol/struct.JBQueueRulesetsConfig.md +21 -0
- package/docs/src/src/structs/JBStored721Tier.sol/struct.JBStored721Tier.md +42 -0
- package/docs/src/src/structs/README.md +18 -0
- package/foundry.lock +11 -0
- package/foundry.toml +22 -0
- package/package.json +31 -0
- package/remappings.txt +1 -0
- package/script/Deploy.s.sol +140 -0
- package/script/helpers/Hook721DeploymentLib.sol +81 -0
- package/slither-ci.config.json +10 -0
- package/sphinx.lock +476 -0
- package/src/JB721TiersHook.sol +765 -0
- package/src/JB721TiersHookDeployer.sol +114 -0
- package/src/JB721TiersHookProjectDeployer.sol +413 -0
- package/src/JB721TiersHookStore.sol +1195 -0
- package/src/abstract/ERC721.sol +484 -0
- package/src/abstract/JB721Hook.sol +279 -0
- package/src/interfaces/IJB721Hook.sol +21 -0
- package/src/interfaces/IJB721TiersHook.sol +135 -0
- package/src/interfaces/IJB721TiersHookDeployer.sol +22 -0
- package/src/interfaces/IJB721TiersHookProjectDeployer.sol +76 -0
- package/src/interfaces/IJB721TiersHookStore.sol +220 -0
- package/src/interfaces/IJB721TokenUriResolver.sol +10 -0
- package/src/libraries/JB721Constants.sol +7 -0
- package/src/libraries/JB721TiersRulesetMetadataResolver.sol +44 -0
- package/src/libraries/JBBitmap.sol +57 -0
- package/src/libraries/JBIpfsDecoder.sol +95 -0
- package/src/structs/JB721InitTiersConfig.sol +20 -0
- package/src/structs/JB721Tier.sol +39 -0
- package/src/structs/JB721TierConfig.sol +40 -0
- package/src/structs/JB721TiersHookFlags.sol +17 -0
- package/src/structs/JB721TiersMintReservesConfig.sol +9 -0
- package/src/structs/JB721TiersRulesetMetadata.sol +12 -0
- package/src/structs/JB721TiersSetDiscountPercentConfig.sol +9 -0
- package/src/structs/JBBitmapWord.sol +11 -0
- package/src/structs/JBDeploy721TiersHookConfig.sol +25 -0
- package/src/structs/JBLaunchProjectConfig.sol +18 -0
- package/src/structs/JBLaunchRulesetsConfig.sol +17 -0
- package/src/structs/JBPayDataHookRulesetConfig.sol +44 -0
- package/src/structs/JBPayDataHookRulesetMetadata.sol +46 -0
- package/src/structs/JBQueueRulesetsConfig.sol +13 -0
- package/src/structs/JBStored721Tier.sol +24 -0
- package/test/721HookAttacks.t.sol +396 -0
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +944 -0
- package/test/invariants/TierLifecycleInvariant.t.sol +187 -0
- package/test/invariants/TieredHookStoreInvariant.t.sol +81 -0
- package/test/invariants/handlers/TierLifecycleHandler.sol +262 -0
- package/test/invariants/handlers/TierStoreHandler.sol +155 -0
- package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +141 -0
- package/test/unit/JBBitmap.t.sol +169 -0
- package/test/unit/JBIpfsDecoder.t.sol +131 -0
- package/test/unit/M6_TierSupplyCheck.t.sol +220 -0
- package/test/unit/adjustTier_Unit.t.sol +1740 -0
- package/test/unit/deployer_Unit.t.sol +103 -0
- package/test/unit/getters_constructor_Unit.t.sol +548 -0
- package/test/unit/mintFor_mintReservesFor_Unit.t.sol +443 -0
- package/test/unit/pay_Unit.t.sol +1537 -0
- package/test/unit/redeem_Unit.t.sol +459 -0
|
@@ -0,0 +1,1195 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.23;
|
|
3
|
+
|
|
4
|
+
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
5
|
+
|
|
6
|
+
import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
|
|
7
|
+
import {IJB721TokenUriResolver} from "./interfaces/IJB721TokenUriResolver.sol";
|
|
8
|
+
import {JB721Constants} from "./libraries/JB721Constants.sol";
|
|
9
|
+
import {JBBitmap} from "./libraries/JBBitmap.sol";
|
|
10
|
+
import {JB721Tier} from "./structs/JB721Tier.sol";
|
|
11
|
+
import {JB721TierConfig} from "./structs/JB721TierConfig.sol";
|
|
12
|
+
import {JB721TiersHookFlags} from "./structs/JB721TiersHookFlags.sol";
|
|
13
|
+
import {JBBitmapWord} from "./structs/JBBitmapWord.sol";
|
|
14
|
+
import {JBStored721Tier} from "./structs/JBStored721Tier.sol";
|
|
15
|
+
|
|
16
|
+
/// @title JB721TiersHookStore
|
|
17
|
+
/// @notice This contract stores and manages data for many `IJB721TiersHook`s and their NFTs.
|
|
18
|
+
contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
19
|
+
using JBBitmap for mapping(uint256 => uint256);
|
|
20
|
+
using JBBitmap for JBBitmapWord;
|
|
21
|
+
|
|
22
|
+
//*********************************************************************//
|
|
23
|
+
// --------------------------- custom errors ------------------------- //
|
|
24
|
+
//*********************************************************************//
|
|
25
|
+
|
|
26
|
+
error JB721TiersHookStore_CantMintManually(uint256 tierId);
|
|
27
|
+
error JB721TiersHookStore_CantRemoveTier(uint256 tierId);
|
|
28
|
+
error JB721TiersHookStore_DiscountPercentExceedsBounds(uint256 percent, uint256 limit);
|
|
29
|
+
error JB721TiersHookStore_DiscountPercentIncreaseNotAllowed(uint256 percent, uint256 storedPercent);
|
|
30
|
+
error JB721TiersHookStore_InsufficientPendingReserves(uint256 count, uint256 numberOfPendingReserves);
|
|
31
|
+
error JB721TiersHookStore_InsufficientSupplyRemaining(uint256 tierId);
|
|
32
|
+
error JB721TiersHookStore_InvalidCategorySortOrder(uint256 tierCategory, uint256 previousTierCategory);
|
|
33
|
+
error JB721TiersHookStore_InvalidQuantity(uint256 quantity, uint256 limit);
|
|
34
|
+
error JB721TiersHookStore_ManualMintingNotAllowed(uint256 tierId);
|
|
35
|
+
error JB721TiersHookStore_MaxTiersExceeded(uint256 numberOfTiers, uint256 limit);
|
|
36
|
+
error JB721TiersHookStore_PriceExceedsAmount(uint256 price, uint256 leftoverAmount);
|
|
37
|
+
error JB721TiersHookStore_ReserveFrequencyNotAllowed(uint256 tierId);
|
|
38
|
+
error JB721TiersHookStore_TierRemoved(uint256 tierId);
|
|
39
|
+
error JB721TiersHookStore_UnrecognizedTier(uint256 tierId);
|
|
40
|
+
error JB721TiersHookStore_VotingUnitsNotAllowed(uint256 tierId);
|
|
41
|
+
error JB721TiersHookStore_ZeroInitialSupply(uint256 tierId);
|
|
42
|
+
|
|
43
|
+
//*********************************************************************//
|
|
44
|
+
// -------------------- private constant properties ------------------ //
|
|
45
|
+
//*********************************************************************//
|
|
46
|
+
|
|
47
|
+
/// @notice Just a kind reminder to our readers.
|
|
48
|
+
/// @dev Used in 721 token ID generation.
|
|
49
|
+
uint256 private constant _ONE_BILLION = 1_000_000_000;
|
|
50
|
+
|
|
51
|
+
//*********************************************************************//
|
|
52
|
+
// --------------------- public stored properties -------------------- //
|
|
53
|
+
//*********************************************************************//
|
|
54
|
+
|
|
55
|
+
/// @notice Returns the default reserve beneficiary for the provided 721 contract.
|
|
56
|
+
/// @dev If a tier has a reserve beneficiary set, it will override this value.
|
|
57
|
+
/// @custom:param hook The 721 contract to get the default reserve beneficiary of.
|
|
58
|
+
mapping(address hook => address) public override defaultReserveBeneficiaryOf;
|
|
59
|
+
|
|
60
|
+
/// @notice Returns the encoded IPFS URI for the provided tier ID of the provided 721 contract.
|
|
61
|
+
/// @dev Token URIs managed by this contract are stored in 32 bytes, based on stripped down IPFS hashes.
|
|
62
|
+
/// @custom:param hook The 721 contract that the tier belongs to.
|
|
63
|
+
/// @custom:param tierId The ID of the tier to get the encoded IPFS URI of.
|
|
64
|
+
/// @custom:returns The encoded IPFS URI.
|
|
65
|
+
mapping(address hook => mapping(uint256 tierId => bytes32)) public override encodedIPFSUriOf;
|
|
66
|
+
|
|
67
|
+
/// @notice Returns the largest tier ID currently used on the provided 721 contract.
|
|
68
|
+
/// @dev This may not include the last tier ID if it has been removed.
|
|
69
|
+
/// @custom:param hook The 721 contract to get the largest tier ID from.
|
|
70
|
+
mapping(address hook => uint256) public override maxTierIdOf;
|
|
71
|
+
|
|
72
|
+
/// @notice Returns the number of NFTs which have been burned from the provided tier ID of the provided 721
|
|
73
|
+
/// contract.
|
|
74
|
+
/// @custom:param hook The 721 contract that the tier belongs to.
|
|
75
|
+
/// @custom:param tierId The ID of the tier to get the burn count of.
|
|
76
|
+
mapping(address hook => mapping(uint256 tierId => uint256)) public override numberOfBurnedFor;
|
|
77
|
+
|
|
78
|
+
/// @notice Returns the number of reserve NFTs which have been minted from the provided tier ID of the provided 721
|
|
79
|
+
/// contract.
|
|
80
|
+
/// @custom:param hook The 721 contract that the tier belongs to.
|
|
81
|
+
/// @custom:param tierId The ID of the tier to get the reserve mint count of.
|
|
82
|
+
mapping(address hook => mapping(uint256 tierId => uint256)) public override numberOfReservesMintedFor;
|
|
83
|
+
|
|
84
|
+
/// @notice Returns the number of NFTs which the provided owner address owns from the provided 721 contract and tier
|
|
85
|
+
/// ID.
|
|
86
|
+
/// @custom:param hook The 721 contract to get the balance from.
|
|
87
|
+
/// @custom:param owner The address to get the tier balance of.
|
|
88
|
+
/// @custom:param tierId The ID of the tier to get the balance for.
|
|
89
|
+
mapping(address hook => mapping(address owner => mapping(uint256 tierId => uint256))) public override tierBalanceOf;
|
|
90
|
+
|
|
91
|
+
/// @notice Returns the custom token URI resolver which overrides the default token URI resolver for the provided
|
|
92
|
+
/// 721 contract.
|
|
93
|
+
/// @custom:param hook The 721 contract to get the custom token URI resolver of.
|
|
94
|
+
mapping(address hook => IJB721TokenUriResolver) public override tokenUriResolverOf;
|
|
95
|
+
|
|
96
|
+
//*********************************************************************//
|
|
97
|
+
// --------------------- internal stored properties ------------------ //
|
|
98
|
+
//*********************************************************************//
|
|
99
|
+
|
|
100
|
+
/// @notice Returns the flags which dictate the behavior of the provided `IJB721TiersHook` contract.
|
|
101
|
+
/// @custom:param hook The address of the 721 contract to get the flags for.
|
|
102
|
+
/// @custom:returns The flags.
|
|
103
|
+
mapping(address hook => JB721TiersHookFlags) internal _flagsOf;
|
|
104
|
+
|
|
105
|
+
/// @notice Return the ID of the last sorted tier from the provided 721 contract.
|
|
106
|
+
/// @dev If not set, it is assumed the `maxTierIdOf` is the last sorted tier ID.
|
|
107
|
+
/// @custom:param hook The 721 contract to get the last sorted tier ID from.
|
|
108
|
+
mapping(address hook => uint256) internal _lastTrackedSortedTierIdOf;
|
|
109
|
+
|
|
110
|
+
/// @notice Get the bitmap word at the provided depth from the provided 721 contract's tier removal bitmap.
|
|
111
|
+
/// @dev See `JBBitmap` for more information.
|
|
112
|
+
/// @custom:param hook The 721 contract to get the bitmap word from.
|
|
113
|
+
/// @custom:param depth The depth of the bitmap row to get. Each row stores 256 tiers.
|
|
114
|
+
/// @custom:returns word The bitmap row's content.
|
|
115
|
+
mapping(address hook => mapping(uint256 depth => uint256 word)) internal _removedTiersBitmapWordOf;
|
|
116
|
+
|
|
117
|
+
/// @notice Returns the reserve beneficiary (if there is one) for the provided tier ID on the provided
|
|
118
|
+
/// `IJB721TiersHook` contract.
|
|
119
|
+
/// @custom:param hook The address of the 721 contract to get the reserve beneficiary from.
|
|
120
|
+
/// @custom:param tierId The ID of the tier to get the reserve beneficiary of.
|
|
121
|
+
/// @custom:returns The address of the reserved token beneficiary.
|
|
122
|
+
mapping(address hook => mapping(uint256 tierId => address)) internal _reserveBeneficiaryOf;
|
|
123
|
+
|
|
124
|
+
/// @notice Returns the ID of the first tier in the provided category on the provided 721 contract.
|
|
125
|
+
/// @custom:param hook The 721 contract to get the category's first tier ID from.
|
|
126
|
+
/// @custom:param category The category to get the first tier ID of.
|
|
127
|
+
mapping(address hook => mapping(uint256 category => uint256)) internal _startingTierIdOfCategory;
|
|
128
|
+
|
|
129
|
+
/// @notice Returns the stored tier of the provided tier ID on the provided `IJB721TiersHook` contract.
|
|
130
|
+
/// @custom:param hook The address of the 721 contract to get the tier from.
|
|
131
|
+
/// @custom:param tierId The ID of the tier to get.
|
|
132
|
+
/// @custom:returns The stored tier, as a `JBStored721Tier` struct.
|
|
133
|
+
mapping(address hook => mapping(uint256 tierId => JBStored721Tier)) internal _storedTierOf;
|
|
134
|
+
|
|
135
|
+
/// @notice Returns the ID of the tier which comes after the provided tier ID (sorted by price).
|
|
136
|
+
/// @dev If empty, assume the next tier ID should come after.
|
|
137
|
+
/// @custom:param hook The address of the 721 contract to get the next tier ID from.
|
|
138
|
+
/// @custom:param tierId The ID of the tier to get the next tier ID in relation to.
|
|
139
|
+
/// @custom:returns The following tier's ID.
|
|
140
|
+
mapping(address hook => mapping(uint256 tierId => uint256)) internal _tierIdAfter;
|
|
141
|
+
|
|
142
|
+
//*********************************************************************//
|
|
143
|
+
// ------------------------- external views -------------------------- //
|
|
144
|
+
//*********************************************************************//
|
|
145
|
+
|
|
146
|
+
/// @notice Resolves the encoded IPFS URI for the tier of the 721 with the provided token ID from the provided 721
|
|
147
|
+
/// contract.
|
|
148
|
+
/// @param hook The 721 contract that the encoded IPFS URI belongs to.
|
|
149
|
+
/// @param tokenId The token ID of the 721 to get the encoded tier IPFS URI of.
|
|
150
|
+
/// @return The encoded IPFS URI.
|
|
151
|
+
function encodedTierIPFSUriOf(address hook, uint256 tokenId) external view override returns (bytes32) {
|
|
152
|
+
return encodedIPFSUriOf[hook][tierIdOfToken(tokenId)];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// @notice Get the flags that dictate the behavior of the provided 721 contract.
|
|
156
|
+
/// @param hook The 721 contract to get the flags of.
|
|
157
|
+
/// @return The flags.
|
|
158
|
+
function flagsOf(address hook) external view override returns (JB721TiersHookFlags memory) {
|
|
159
|
+
return _flagsOf[hook];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// @notice Check if the provided tier has been removed from the provided 721 contract.
|
|
163
|
+
/// @param hook The 721 contract the tier belongs to.
|
|
164
|
+
/// @param tierId The ID of the tier to check the removal status of.
|
|
165
|
+
/// @return A bool which is `true` if the tier has been removed, and `false` otherwise.
|
|
166
|
+
function isTierRemoved(address hook, uint256 tierId) external view override returns (bool) {
|
|
167
|
+
JBBitmapWord memory bitmapWord = _removedTiersBitmapWordOf[hook].readId(tierId);
|
|
168
|
+
|
|
169
|
+
return bitmapWord.isTierIdRemoved(tierId);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/// @notice Get the number of pending reserve NFTs for the provided tier ID of the provided 721 contract.
|
|
173
|
+
/// @dev "Pending" means that the NFTs have been reserved, but have not been minted yet.
|
|
174
|
+
/// @param hook The 721 contract to check for pending reserved NFTs.
|
|
175
|
+
/// @param tierId The ID of the tier to get the number of pending reserves for.
|
|
176
|
+
/// @return The number of pending reserved NFTs.
|
|
177
|
+
function numberOfPendingReservesFor(address hook, uint256 tierId) external view override returns (uint256) {
|
|
178
|
+
return _numberOfPendingReservesFor({hook: hook, tierId: tierId, storedTier: _storedTierOf[hook][tierId]});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// @notice Get the tier of the 721 with the provided token ID in the provided 721 contract.
|
|
182
|
+
/// @param hook The 721 contract that the tier belongs to.
|
|
183
|
+
/// @param tokenId The token ID of the 721 to get the tier of.
|
|
184
|
+
/// @param includeResolvedUri If set to `true`, if the contract has a token URI resolver, its content will be
|
|
185
|
+
/// resolved and included.
|
|
186
|
+
/// @return The tier.
|
|
187
|
+
function tierOfTokenId(
|
|
188
|
+
address hook,
|
|
189
|
+
uint256 tokenId,
|
|
190
|
+
bool includeResolvedUri
|
|
191
|
+
)
|
|
192
|
+
external
|
|
193
|
+
view
|
|
194
|
+
override
|
|
195
|
+
returns (JB721Tier memory)
|
|
196
|
+
{
|
|
197
|
+
// Get a reference to the tier's ID.
|
|
198
|
+
uint256 tierId = tierIdOfToken(tokenId);
|
|
199
|
+
return _getTierFrom({
|
|
200
|
+
hook: hook, tierId: tierId, storedTier: _storedTierOf[hook][tierId], includeResolvedUri: includeResolvedUri
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// @notice Returns the number of voting units an addresses has within the specified tier of the specified 721
|
|
205
|
+
/// contract.
|
|
206
|
+
/// @dev NFTs have a tier-specific number of voting units. If the tier does not have a custom number of voting
|
|
207
|
+
/// units, the price is used.
|
|
208
|
+
/// @param hook The 721 contract that the tier belongs to.
|
|
209
|
+
/// @param account The address to get the voting units of within the tier.
|
|
210
|
+
/// @param tierId The ID of the tier to get voting units within.
|
|
211
|
+
/// @return The address' voting units within the tier.
|
|
212
|
+
function tierVotingUnitsOf(
|
|
213
|
+
address hook,
|
|
214
|
+
address account,
|
|
215
|
+
uint256 tierId
|
|
216
|
+
)
|
|
217
|
+
external
|
|
218
|
+
view
|
|
219
|
+
virtual
|
|
220
|
+
override
|
|
221
|
+
returns (uint256)
|
|
222
|
+
{
|
|
223
|
+
// Get a reference to the account's balance in this tier.
|
|
224
|
+
uint256 balance = tierBalanceOf[hook][account][tierId];
|
|
225
|
+
|
|
226
|
+
if (balance == 0) return 0;
|
|
227
|
+
|
|
228
|
+
// Keep a reference to the stored tier.
|
|
229
|
+
JBStored721Tier memory storedTier = _storedTierOf[hook][tierId];
|
|
230
|
+
|
|
231
|
+
// Check if voting units should be used. Price will be used otherwise.
|
|
232
|
+
(,, bool useVotingUnits,,) = _unpackBools(storedTier.packedBools);
|
|
233
|
+
|
|
234
|
+
// Return the address' voting units within the tier.
|
|
235
|
+
return balance * (useVotingUnits ? storedTier.votingUnits : storedTier.price);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// @notice Gets an array of currently active 721 tiers for the provided 721 contract.
|
|
239
|
+
/// @param hook The 721 contract to get the tiers of.
|
|
240
|
+
/// @param categories An array tier categories to get tiers from. Send an empty array to get all categories.
|
|
241
|
+
/// @param includeResolvedUri If set to `true`, if the contract has a token URI resolver, its content will be
|
|
242
|
+
/// resolved and included.
|
|
243
|
+
/// @param startingId The ID of the first tier to get (sorted by price). Send 0 to get all active tiers.
|
|
244
|
+
/// @param size The number of tiers to include.
|
|
245
|
+
/// @return tiers An array of active 721 tiers.
|
|
246
|
+
function tiersOf(
|
|
247
|
+
address hook,
|
|
248
|
+
uint256[] calldata categories,
|
|
249
|
+
bool includeResolvedUri,
|
|
250
|
+
uint256 startingId,
|
|
251
|
+
uint256 size
|
|
252
|
+
)
|
|
253
|
+
external
|
|
254
|
+
view
|
|
255
|
+
override
|
|
256
|
+
returns (JB721Tier[] memory tiers)
|
|
257
|
+
{
|
|
258
|
+
// Keep a reference to the last tier ID.
|
|
259
|
+
uint256 lastTierId = _lastSortedTierIdOf(hook);
|
|
260
|
+
|
|
261
|
+
// Return an empty array if there are no tiers.
|
|
262
|
+
if (lastTierId == 0) return tiers;
|
|
263
|
+
|
|
264
|
+
// Initialize an array with the provided length.
|
|
265
|
+
tiers = new JB721Tier[](size);
|
|
266
|
+
|
|
267
|
+
// Count the number of tiers to include in the result.
|
|
268
|
+
uint256 numberOfIncludedTiers;
|
|
269
|
+
|
|
270
|
+
// Keep a reference to the tier being iterated upon.
|
|
271
|
+
JBStored721Tier memory storedTier;
|
|
272
|
+
|
|
273
|
+
// Initialize a `JBBitmapWord` to track if whether tiers have been removed.
|
|
274
|
+
JBBitmapWord memory bitmapWord;
|
|
275
|
+
|
|
276
|
+
// Keep a reference to an iterator variable to represent the category being iterated upon.
|
|
277
|
+
uint256 i;
|
|
278
|
+
|
|
279
|
+
// Iterate at least once.
|
|
280
|
+
do {
|
|
281
|
+
// Stop iterating if the size limit has been reached.
|
|
282
|
+
if (numberOfIncludedTiers == size) break;
|
|
283
|
+
|
|
284
|
+
// Get a reference to the ID of the tier being iterated upon, starting with the first tier ID if no starting
|
|
285
|
+
// ID was specified.
|
|
286
|
+
uint256 currentSortedTierId = startingId != 0
|
|
287
|
+
? startingId
|
|
288
|
+
: _firstSortedTierIdOf({hook: hook, category: categories.length == 0 ? 0 : categories[i]});
|
|
289
|
+
|
|
290
|
+
// Add the tiers from the category being iterated upon.
|
|
291
|
+
while (currentSortedTierId != 0 && numberOfIncludedTiers < size) {
|
|
292
|
+
if (!_isTierRemovedWithRefresh({hook: hook, tierId: currentSortedTierId, bitmapWord: bitmapWord})) {
|
|
293
|
+
storedTier = _storedTierOf[hook][currentSortedTierId];
|
|
294
|
+
|
|
295
|
+
// If categories were provided and the current tier's category is greater than category being added,
|
|
296
|
+
// break.
|
|
297
|
+
if (categories.length != 0 && storedTier.category > categories[i]) {
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
// If a category is specified and matches, add the returned values.
|
|
301
|
+
else if (categories.length == 0 || storedTier.category == categories[i]) {
|
|
302
|
+
// Add the tier to the array being returned.
|
|
303
|
+
tiers[numberOfIncludedTiers++] = _getTierFrom({
|
|
304
|
+
hook: hook,
|
|
305
|
+
tierId: currentSortedTierId,
|
|
306
|
+
storedTier: storedTier,
|
|
307
|
+
includeResolvedUri: includeResolvedUri
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Set the next sorted tier ID.
|
|
312
|
+
currentSortedTierId = _nextSortedTierIdOf({hook: hook, id: currentSortedTierId, max: lastTierId});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
unchecked {
|
|
316
|
+
i++;
|
|
317
|
+
}
|
|
318
|
+
} while (i < categories.length);
|
|
319
|
+
|
|
320
|
+
// Resize the array if there are removed tiers.
|
|
321
|
+
if (numberOfIncludedTiers != size) {
|
|
322
|
+
assembly ("memory-safe") {
|
|
323
|
+
mstore(tiers, numberOfIncludedTiers)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/// @notice Get the number of NFTs which have been minted from the provided 721 contract (across all tiers).
|
|
329
|
+
/// @param hook The 721 contract to get a total supply of.
|
|
330
|
+
/// @return supply The total number of NFTs minted from all tiers on the contract.
|
|
331
|
+
function totalSupplyOf(address hook) external view override returns (uint256 supply) {
|
|
332
|
+
// Keep a reference to the greatest tier ID.
|
|
333
|
+
uint256 maxTierId = maxTierIdOf[hook];
|
|
334
|
+
|
|
335
|
+
for (uint256 i = maxTierId; i != 0; i--) {
|
|
336
|
+
// Set the tier being iterated on.
|
|
337
|
+
JBStored721Tier memory storedTier = _storedTierOf[hook][i];
|
|
338
|
+
|
|
339
|
+
// Increment the total supply by the number of tokens already minted.
|
|
340
|
+
supply += storedTier.initialSupply - (storedTier.remainingSupply + numberOfBurnedFor[hook][i]);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/// @notice Get the number of voting units the provided address has for the provided 721 contract (across all
|
|
345
|
+
/// tiers).
|
|
346
|
+
/// @dev NFTs have a tier-specific number of voting units. If the tier does not have a custom number of voting
|
|
347
|
+
/// units, the price is used.
|
|
348
|
+
/// @param hook The 721 contract to get the voting units within.
|
|
349
|
+
/// @param account The address to get the voting unit total of.
|
|
350
|
+
/// @return units The total voting units the address has within the 721 contract.
|
|
351
|
+
function votingUnitsOf(address hook, address account) external view virtual override returns (uint256 units) {
|
|
352
|
+
// Keep a reference to the greatest tier ID.
|
|
353
|
+
uint256 maxTierId = maxTierIdOf[hook];
|
|
354
|
+
|
|
355
|
+
// Loop through all tiers.
|
|
356
|
+
for (uint256 i = maxTierId; i != 0; i--) {
|
|
357
|
+
// Get a reference to the account's balance in this tier.
|
|
358
|
+
uint256 balance = tierBalanceOf[hook][account][i];
|
|
359
|
+
|
|
360
|
+
// If the account has no balance, return.
|
|
361
|
+
if (balance == 0) continue;
|
|
362
|
+
|
|
363
|
+
// Get the tier.
|
|
364
|
+
JBStored721Tier memory storedTier = _storedTierOf[hook][i];
|
|
365
|
+
|
|
366
|
+
// Parse the flags.
|
|
367
|
+
(,, bool useVotingUnits,,) = _unpackBools(storedTier.packedBools);
|
|
368
|
+
|
|
369
|
+
// Add the voting units for the address' balance in this tier.
|
|
370
|
+
// Use custom voting units if set. Otherwise, use the tier's price.
|
|
371
|
+
units += balance * (useVotingUnits ? storedTier.votingUnits : storedTier.price);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
//*********************************************************************//
|
|
376
|
+
// -------------------------- public views --------------------------- //
|
|
377
|
+
//*********************************************************************//
|
|
378
|
+
|
|
379
|
+
/// @notice Get the tier with the provided ID from the provided 721 contract.
|
|
380
|
+
/// @param hook The 721 contract to get the tier from.
|
|
381
|
+
/// @param id The ID of the tier to get.
|
|
382
|
+
/// @param includeResolvedUri If set to `true`, if the contract has a token URI resolver, its content will be
|
|
383
|
+
/// resolved and included.
|
|
384
|
+
/// @return The tier.
|
|
385
|
+
function tierOf(address hook, uint256 id, bool includeResolvedUri) public view override returns (JB721Tier memory) {
|
|
386
|
+
return _getTierFrom({
|
|
387
|
+
hook: hook, tierId: id, storedTier: _storedTierOf[hook][id], includeResolvedUri: includeResolvedUri
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/// @notice Get the number of NFTs that the specified address has from the specified 721 contract (across all
|
|
392
|
+
/// tiers).
|
|
393
|
+
/// @param hook The 721 contract to get the balance within.
|
|
394
|
+
/// @param owner The address to check the balance of.
|
|
395
|
+
/// @return balance The number of NFTs the owner has from the 721 contract.
|
|
396
|
+
function balanceOf(address hook, address owner) public view override returns (uint256 balance) {
|
|
397
|
+
// Keep a reference to the greatest tier ID.
|
|
398
|
+
uint256 maxTierId = maxTierIdOf[hook];
|
|
399
|
+
|
|
400
|
+
// Loop through all tiers.
|
|
401
|
+
for (uint256 i = maxTierId; i != 0; i--) {
|
|
402
|
+
// Get a reference to the account's balance within this tier.
|
|
403
|
+
balance += tierBalanceOf[hook][owner][i];
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/// @notice The combined cash out weight of the NFTs with the provided token IDs.
|
|
408
|
+
/// @dev Cash out weight is based on 721 price.
|
|
409
|
+
/// @dev Divide this result by the `totalCashOutWeight` to get the portion of funds that can be reclaimed by
|
|
410
|
+
/// cashing out these NFTs.
|
|
411
|
+
/// @param hook The 721 contract that the NFTs belong to.
|
|
412
|
+
/// @param tokenIds The token IDs of the NFTs to get the cash out weight of.
|
|
413
|
+
/// @return weight The cash out weight.
|
|
414
|
+
function cashOutWeightOf(address hook, uint256[] calldata tokenIds) public view override returns (uint256 weight) {
|
|
415
|
+
// Add each 721's original price (from its tier) to the weight.
|
|
416
|
+
// Uses the full tier price, not the discounted price — by design. Discounts are transient incentives
|
|
417
|
+
// that affect the purchase price, but the NFT's weight in the cash out curve is always based on its
|
|
418
|
+
// tier's original price. This prevents discount changes from altering the cash out value of already-minted
|
|
419
|
+
// NFTs.
|
|
420
|
+
for (uint256 i; i < tokenIds.length; i++) {
|
|
421
|
+
weight += _storedTierOf[hook][tierIdOfToken(tokenIds[i])].price;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/// @notice The reserve beneficiary for the provided tier ID on the provided 721 contract.
|
|
426
|
+
/// @param hook The 721 contract that the tier belongs to.
|
|
427
|
+
/// @param tierId The ID of the tier to get the reserve beneficiary of.
|
|
428
|
+
/// @return The reserve beneficiary for the tier.
|
|
429
|
+
function reserveBeneficiaryOf(address hook, uint256 tierId) public view override returns (address) {
|
|
430
|
+
// Get the stored reserve beneficiary.
|
|
431
|
+
address storedReserveBeneficiaryOfTier = _reserveBeneficiaryOf[hook][tierId];
|
|
432
|
+
|
|
433
|
+
// If the tier has a beneficiary specified, return it.
|
|
434
|
+
if (storedReserveBeneficiaryOfTier != address(0)) {
|
|
435
|
+
return storedReserveBeneficiaryOfTier;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Otherwise, return the contract's default reserve beneficiary.
|
|
439
|
+
return defaultReserveBeneficiaryOf[hook];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/// @notice The tier ID for the 721 with the provided token ID.
|
|
443
|
+
/// @dev Tiers are 1-indexed from the `tiers` array, meaning the 0th element of the array is tier 1.
|
|
444
|
+
/// @param tokenId The token ID of the 721 to get the tier ID of.
|
|
445
|
+
/// @return The ID of the 721's tier.
|
|
446
|
+
function tierIdOfToken(uint256 tokenId) public pure override returns (uint256) {
|
|
447
|
+
return tokenId / _ONE_BILLION;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/// @notice The combined cash out weight for all NFTs from the provided 721 contract.
|
|
451
|
+
/// @param hook The 721 contract to get the total cash out weight of.
|
|
452
|
+
/// @return weight The total cash out weight.
|
|
453
|
+
function totalCashOutWeight(address hook) public view override returns (uint256 weight) {
|
|
454
|
+
// Keep a reference to the greatest tier ID.
|
|
455
|
+
uint256 maxTierId = maxTierIdOf[hook];
|
|
456
|
+
|
|
457
|
+
// Add each 721's original price (from its tier) to the weight.
|
|
458
|
+
// Uses the full tier price, not the discounted price — by design. See `cashOutWeightOf` for rationale.
|
|
459
|
+
for (uint256 i = 1; i <= maxTierId; i++) {
|
|
460
|
+
// Keep a reference to the stored tier.
|
|
461
|
+
JBStored721Tier memory storedTier = _storedTierOf[hook][i];
|
|
462
|
+
|
|
463
|
+
// Add the tier's price multiplied by the number of minted NFTs plus pending reserves.
|
|
464
|
+
// Pending reserves are included by design — they represent committed obligations that will be
|
|
465
|
+
// minted to the reserve beneficiary. Including them in the denominator ensures cash-out values
|
|
466
|
+
// account for the full diluted supply, preventing early cashers from extracting more than their
|
|
467
|
+
// fair share before reserves are minted.
|
|
468
|
+
weight += storedTier.price
|
|
469
|
+
* ((storedTier.initialSupply - (storedTier.remainingSupply + numberOfBurnedFor[hook][i]))
|
|
470
|
+
+ _numberOfPendingReservesFor({hook: hook, tierId: i, storedTier: storedTier}));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
//*********************************************************************//
|
|
475
|
+
// -------------------------- internal views ------------------------- //
|
|
476
|
+
//*********************************************************************//
|
|
477
|
+
|
|
478
|
+
/// @notice Get the first tier ID from an 721 contract (when sorted by price) within a provided category.
|
|
479
|
+
/// @param hook The 721 contract to get the first sorted tier ID of.
|
|
480
|
+
/// @param category The category to get the first sorted tier ID within. Send 0 for the first ID across all tiers,
|
|
481
|
+
/// which might not be in the 0th category if the 0th category does not exist.
|
|
482
|
+
/// @return id The first sorted tier ID within the provided category.
|
|
483
|
+
function _firstSortedTierIdOf(address hook, uint256 category) internal view returns (uint256 id) {
|
|
484
|
+
id = category == 0 ? _tierIdAfter[hook][0] : _startingTierIdOfCategory[hook][category];
|
|
485
|
+
// Start at the first tier ID if nothing is specified.
|
|
486
|
+
if (id == 0) id = 1;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/// @notice Generate a token ID for an 721 given a tier ID and a token number within that tier.
|
|
490
|
+
/// @param tierId The ID of the tier to generate a token ID for.
|
|
491
|
+
/// @param tokenNumber The token number of the 721 within the tier.
|
|
492
|
+
/// @return The token ID of the 721.
|
|
493
|
+
function _generateTokenId(uint256 tierId, uint256 tokenNumber) internal pure returns (uint256) {
|
|
494
|
+
return (tierId * _ONE_BILLION) + tokenNumber;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/// @notice Returns the tier corresponding to the stored tier provided.
|
|
498
|
+
/// @dev Translate `JBStored721Tier` to `JB721Tier`.
|
|
499
|
+
/// @param hook The 721 contract to get the tier from.
|
|
500
|
+
/// @param tierId The ID of the tier to get.
|
|
501
|
+
/// @param storedTier The stored tier to get the corresponding tier for.
|
|
502
|
+
/// @param includeResolvedUri If set to `true`, if the contract has a token URI resolver, its content will be
|
|
503
|
+
/// resolved and included.
|
|
504
|
+
/// @return tier The tier as a `JB721Tier` struct.
|
|
505
|
+
function _getTierFrom(
|
|
506
|
+
address hook,
|
|
507
|
+
uint256 tierId,
|
|
508
|
+
JBStored721Tier memory storedTier,
|
|
509
|
+
bool includeResolvedUri
|
|
510
|
+
)
|
|
511
|
+
internal
|
|
512
|
+
view
|
|
513
|
+
returns (JB721Tier memory)
|
|
514
|
+
{
|
|
515
|
+
// Get a reference to the reserve beneficiary.
|
|
516
|
+
address reserveBeneficiary = reserveBeneficiaryOf({hook: hook, tierId: tierId});
|
|
517
|
+
|
|
518
|
+
(
|
|
519
|
+
bool allowOwnerMint,
|
|
520
|
+
bool transfersPausable,
|
|
521
|
+
bool useVotingUnits,
|
|
522
|
+
bool cannotBeRemoved,
|
|
523
|
+
bool cannotIncreaseDiscountPercent
|
|
524
|
+
) = _unpackBools(storedTier.packedBools);
|
|
525
|
+
|
|
526
|
+
// slither-disable-next-line calls-loop
|
|
527
|
+
return JB721Tier({
|
|
528
|
+
id: uint32(tierId),
|
|
529
|
+
price: storedTier.price,
|
|
530
|
+
remainingSupply: storedTier.remainingSupply,
|
|
531
|
+
initialSupply: storedTier.initialSupply,
|
|
532
|
+
votingUnits: useVotingUnits ? storedTier.votingUnits : storedTier.price,
|
|
533
|
+
// No reserve frequency if there is no reserve beneficiary.
|
|
534
|
+
reserveFrequency: reserveBeneficiary == address(0) ? 0 : storedTier.reserveFrequency,
|
|
535
|
+
reserveBeneficiary: reserveBeneficiary,
|
|
536
|
+
encodedIPFSUri: encodedIPFSUriOf[hook][tierId],
|
|
537
|
+
category: storedTier.category,
|
|
538
|
+
discountPercent: storedTier.discountPercent,
|
|
539
|
+
allowOwnerMint: allowOwnerMint,
|
|
540
|
+
transfersPausable: transfersPausable,
|
|
541
|
+
cannotBeRemoved: cannotBeRemoved,
|
|
542
|
+
cannotIncreaseDiscountPercent: cannotIncreaseDiscountPercent,
|
|
543
|
+
resolvedUri: !includeResolvedUri || tokenUriResolverOf[hook] == IJB721TokenUriResolver(address(0))
|
|
544
|
+
? ""
|
|
545
|
+
: tokenUriResolverOf[hook].tokenUriOf({
|
|
546
|
+
nft: hook, tokenId: _generateTokenId({tierId: tierId, tokenNumber: 0})
|
|
547
|
+
})
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/// @notice Check whether a tier has been removed while refreshing the relevant bitmap word if needed.
|
|
552
|
+
/// @param hook The 721 contract to check for removals on.
|
|
553
|
+
/// @param tierId The ID of the tier to check the removal status of.
|
|
554
|
+
/// @param bitmapWord The bitmap word to use.
|
|
555
|
+
/// @return A boolean which is `true` if the tier has been removed.
|
|
556
|
+
function _isTierRemovedWithRefresh(
|
|
557
|
+
address hook,
|
|
558
|
+
uint256 tierId,
|
|
559
|
+
JBBitmapWord memory bitmapWord
|
|
560
|
+
)
|
|
561
|
+
internal
|
|
562
|
+
view
|
|
563
|
+
returns (bool)
|
|
564
|
+
{
|
|
565
|
+
// If the current tier ID is outside current bitmap word (depth), refresh the bitmap word.
|
|
566
|
+
if (bitmapWord.refreshBitmapNeeded(tierId) || (bitmapWord.currentWord == 0 && bitmapWord.currentDepth == 0)) {
|
|
567
|
+
bitmapWord = _removedTiersBitmapWordOf[hook].readId(tierId);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return bitmapWord.isTierIdRemoved(tierId);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/// @notice The last sorted tier ID from an 721 contract (when sorted by price).
|
|
574
|
+
/// @param hook The 721 contract to get the last sorted tier ID of.
|
|
575
|
+
/// @return id The last sorted tier ID.
|
|
576
|
+
function _lastSortedTierIdOf(address hook) internal view returns (uint256 id) {
|
|
577
|
+
id = _lastTrackedSortedTierIdOf[hook];
|
|
578
|
+
// Use the maximum tier ID if nothing is specified.
|
|
579
|
+
if (id == 0) id = maxTierIdOf[hook];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/// @notice Get the tier ID which comes after the provided one when sorted by price.
|
|
583
|
+
/// @param hook The 721 contract to get the next sorted tier ID from.
|
|
584
|
+
/// @param id The tier ID to get the next sorted tier ID relative to.
|
|
585
|
+
/// @param max The maximum tier ID.
|
|
586
|
+
/// @return The next sorted tier ID.
|
|
587
|
+
function _nextSortedTierIdOf(address hook, uint256 id, uint256 max) internal view returns (uint256) {
|
|
588
|
+
// If this is the last tier (maximum), return zero.
|
|
589
|
+
if (id == max) return 0;
|
|
590
|
+
|
|
591
|
+
// If a tier ID is saved to come after the provided ID, return it.
|
|
592
|
+
uint256 storedNext = _tierIdAfter[hook][id];
|
|
593
|
+
|
|
594
|
+
if (storedNext != 0) return storedNext;
|
|
595
|
+
|
|
596
|
+
// Otherwise, increment the provided tier ID.
|
|
597
|
+
return id + 1;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/// @notice Get the number of pending reserve NFTs for the specified tier ID.
|
|
601
|
+
/// @dev The reserve frequency is immutable once a tier is created (set in `recordAddTiers`) and cannot be modified
|
|
602
|
+
/// afterward. The pending reserve count is derived from the ratio of non-reserve mints to the reserve frequency,
|
|
603
|
+
/// rounded up. This means each batch of `reserveFrequency` non-reserve mints entitles exactly one reserve mint,
|
|
604
|
+
/// plus one additional reserve mint if there is any remainder. Because the reserve frequency is fixed per tier,
|
|
605
|
+
/// the accounting remains consistent: the total available reserve mints always equals
|
|
606
|
+
/// `ceil(numberOfNonReserveMints / reserveFrequency)`. If a project wishes to use a different reserve frequency,
|
|
607
|
+
/// it must create a new tier — the existing tier's pending reserves will continue to be calculated using its
|
|
608
|
+
/// original frequency. Removing a tier does not affect already-pending reserves; they can still be minted.
|
|
609
|
+
/// @param hook The 721 contract that the tier belongs to.
|
|
610
|
+
/// @param tierId The ID of the tier to get the number of pending reserve NFTs for.
|
|
611
|
+
/// @param storedTier The stored tier to get the number of pending reserve NFTs for.
|
|
612
|
+
/// @return numberReservedTokensOutstanding The number of pending reserve NFTs for the tier.
|
|
613
|
+
function _numberOfPendingReservesFor(
|
|
614
|
+
address hook,
|
|
615
|
+
uint256 tierId,
|
|
616
|
+
JBStored721Tier memory storedTier
|
|
617
|
+
)
|
|
618
|
+
internal
|
|
619
|
+
view
|
|
620
|
+
returns (uint256)
|
|
621
|
+
{
|
|
622
|
+
// Get a reference to the initial supply with burned NFTs included.
|
|
623
|
+
uint256 initialSupply = storedTier.initialSupply;
|
|
624
|
+
|
|
625
|
+
// No pending reserves if no mints, no reserve frequency, or no reserve beneficiary.
|
|
626
|
+
if (
|
|
627
|
+
storedTier.reserveFrequency == 0 || initialSupply == storedTier.remainingSupply
|
|
628
|
+
|| reserveBeneficiaryOf({hook: hook, tierId: tierId}) == address(0)
|
|
629
|
+
) return 0;
|
|
630
|
+
|
|
631
|
+
// The number of reserve NFTs which have already been minted from the tier.
|
|
632
|
+
uint256 numberOfReserveMints = numberOfReservesMintedFor[hook][tierId];
|
|
633
|
+
|
|
634
|
+
// If only the reserved 721 (from rounding up) has been minted so far, return 0.
|
|
635
|
+
if (initialSupply == storedTier.remainingSupply + numberOfReserveMints) {
|
|
636
|
+
return 0;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Get a reference to the number of NFTs minted from the tier (not counting reserve mints or burned tokens).
|
|
640
|
+
uint256 numberOfNonReserveMints;
|
|
641
|
+
unchecked {
|
|
642
|
+
numberOfNonReserveMints = initialSupply - storedTier.remainingSupply - numberOfReserveMints;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Get the number of total available reserve 721 mints given the number of non-reserve NFTs minted divided by
|
|
646
|
+
// the reserve frequency. This will round down.
|
|
647
|
+
uint256 totalNumberOfAvailableReserveMints = numberOfNonReserveMints / storedTier.reserveFrequency;
|
|
648
|
+
|
|
649
|
+
// Round up.
|
|
650
|
+
if (numberOfNonReserveMints % storedTier.reserveFrequency > 0) ++totalNumberOfAvailableReserveMints;
|
|
651
|
+
|
|
652
|
+
// Return the difference between the number of available reserve mints and the amount already minted.
|
|
653
|
+
unchecked {
|
|
654
|
+
return totalNumberOfAvailableReserveMints - numberOfReserveMints;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/// @notice Pack five bools into a single uint8.
|
|
659
|
+
/// @param allowOwnerMint Whether or not owner minting is allowed in new tiers.
|
|
660
|
+
/// @param transfersPausable Whether or not 721 transfers can be paused.
|
|
661
|
+
/// @param useVotingUnits Whether or not custom voting unit amounts are allowed in new tiers.
|
|
662
|
+
/// @param cannotBeRemoved Whether or not attempts to remove the tier will revert.
|
|
663
|
+
/// @param cannotIncreaseDiscountPercent Whether or not attempts to increase the discount percent will revert.
|
|
664
|
+
/// @return packed The packed bools.
|
|
665
|
+
function _packBools(
|
|
666
|
+
bool allowOwnerMint,
|
|
667
|
+
bool transfersPausable,
|
|
668
|
+
bool useVotingUnits,
|
|
669
|
+
bool cannotBeRemoved,
|
|
670
|
+
bool cannotIncreaseDiscountPercent
|
|
671
|
+
)
|
|
672
|
+
internal
|
|
673
|
+
pure
|
|
674
|
+
returns (uint8 packed)
|
|
675
|
+
{
|
|
676
|
+
assembly {
|
|
677
|
+
packed := or(allowOwnerMint, packed)
|
|
678
|
+
packed := or(shl(0x1, transfersPausable), packed)
|
|
679
|
+
packed := or(shl(0x2, useVotingUnits), packed)
|
|
680
|
+
packed := or(shl(0x3, cannotBeRemoved), packed)
|
|
681
|
+
packed := or(shl(0x4, cannotIncreaseDiscountPercent), packed)
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/// @notice Unpack five bools from a single uint8.
|
|
686
|
+
/// @param packed The packed bools.
|
|
687
|
+
/// @param allowOwnerMint Whether or not owner minting is allowed in new tiers.
|
|
688
|
+
/// @param transfersPausable Whether or not 721 transfers can be paused.
|
|
689
|
+
/// @param useVotingUnits Whether or not custom voting unit amounts are allowed in new tiers.
|
|
690
|
+
/// @param cannotBeRemoved Whether or not the tier can be removed once added.
|
|
691
|
+
/// @param cannotIncreaseDiscountPercent Whether or not the discount percent cannot be increased.
|
|
692
|
+
function _unpackBools(uint8 packed)
|
|
693
|
+
internal
|
|
694
|
+
pure
|
|
695
|
+
returns (
|
|
696
|
+
bool allowOwnerMint,
|
|
697
|
+
bool transfersPausable,
|
|
698
|
+
bool useVotingUnits,
|
|
699
|
+
bool cannotBeRemoved,
|
|
700
|
+
bool cannotIncreaseDiscountPercent
|
|
701
|
+
)
|
|
702
|
+
{
|
|
703
|
+
assembly {
|
|
704
|
+
allowOwnerMint := iszero(iszero(and(0x1, packed)))
|
|
705
|
+
transfersPausable := iszero(iszero(and(0x2, packed)))
|
|
706
|
+
useVotingUnits := iszero(iszero(and(0x4, packed)))
|
|
707
|
+
cannotBeRemoved := iszero(iszero(and(0x8, packed)))
|
|
708
|
+
cannotIncreaseDiscountPercent := iszero(iszero(and(0x10, packed)))
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
//*********************************************************************//
|
|
713
|
+
// ---------------------- external transactions ---------------------- //
|
|
714
|
+
//*********************************************************************//
|
|
715
|
+
|
|
716
|
+
/// @notice Cleans an 721 contract's removed tiers from the tier sorting sequence.
|
|
717
|
+
/// @param hook The 721 contract to clean tiers for.
|
|
718
|
+
function cleanTiers(address hook) external override {
|
|
719
|
+
// Keep a reference to the last tier ID.
|
|
720
|
+
uint256 lastSortedTierId = _lastSortedTierIdOf(hook);
|
|
721
|
+
|
|
722
|
+
// Get a reference to the tier ID being iterated on, starting with the starting tier ID.
|
|
723
|
+
uint256 currentSortedTierId = _firstSortedTierIdOf({hook: hook, category: 0});
|
|
724
|
+
|
|
725
|
+
// Keep track of the previous non-removed tier ID.
|
|
726
|
+
uint256 previousSortedTierId;
|
|
727
|
+
|
|
728
|
+
// Initialize a `JBBitmapWord` for tracking removed tiers.
|
|
729
|
+
JBBitmapWord memory bitmapWord;
|
|
730
|
+
|
|
731
|
+
// Make the sorted array.
|
|
732
|
+
while (currentSortedTierId != 0) {
|
|
733
|
+
// If the current tier ID being iterated on isn't an increment of the previous one,
|
|
734
|
+
if (!_isTierRemovedWithRefresh({hook: hook, tierId: currentSortedTierId, bitmapWord: bitmapWord})) {
|
|
735
|
+
// Update its `_tierIdAfter` if needed.
|
|
736
|
+
if (currentSortedTierId != previousSortedTierId + 1) {
|
|
737
|
+
if (_tierIdAfter[hook][previousSortedTierId] != currentSortedTierId) {
|
|
738
|
+
_tierIdAfter[hook][previousSortedTierId] = currentSortedTierId;
|
|
739
|
+
}
|
|
740
|
+
// Otherwise, if the current tier ID IS an increment of the previous one,
|
|
741
|
+
// AND the tier ID after it isn't 0,
|
|
742
|
+
} else if (_tierIdAfter[hook][previousSortedTierId] != 0) {
|
|
743
|
+
// Set its `_tierIdAfter` to 0.
|
|
744
|
+
_tierIdAfter[hook][previousSortedTierId] = 0;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Iterate by setting the previous tier ID for the next loop to the current tier ID.
|
|
748
|
+
previousSortedTierId = currentSortedTierId;
|
|
749
|
+
}
|
|
750
|
+
// Iterate by updating the current sorted tier ID to the next sorted tier ID.
|
|
751
|
+
currentSortedTierId = _nextSortedTierIdOf({hook: hook, id: currentSortedTierId, max: lastSortedTierId});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
emit CleanTiers({hook: hook, caller: msg.sender});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/// @notice Record newly added tiers.
|
|
758
|
+
/// @param tiersToAdd The tiers to add.
|
|
759
|
+
/// @return tierIds The IDs of the tiers being added.
|
|
760
|
+
function recordAddTiers(JB721TierConfig[] calldata tiersToAdd)
|
|
761
|
+
external
|
|
762
|
+
override
|
|
763
|
+
returns (uint256[] memory tierIds)
|
|
764
|
+
{
|
|
765
|
+
// Keep a reference to the current greatest tier ID.
|
|
766
|
+
uint256 currentMaxTierIdOf = maxTierIdOf[msg.sender];
|
|
767
|
+
|
|
768
|
+
// Make sure the max number of tiers won't be exceeded.
|
|
769
|
+
if (currentMaxTierIdOf + tiersToAdd.length > type(uint16).max) {
|
|
770
|
+
revert JB721TiersHookStore_MaxTiersExceeded(currentMaxTierIdOf + tiersToAdd.length, type(uint16).max);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Keep a reference to the current last sorted tier ID (sorted by price).
|
|
774
|
+
uint256 currentLastSortedTierId = _lastSortedTierIdOf(msg.sender);
|
|
775
|
+
|
|
776
|
+
// Initialize an array for the new tier IDs to be returned.
|
|
777
|
+
tierIds = new uint256[](tiersToAdd.length);
|
|
778
|
+
|
|
779
|
+
// Keep a reference to the first sorted tier ID, to use when sorting new tiers if needed.
|
|
780
|
+
// There's no need for sorting if there are no current tiers.
|
|
781
|
+
uint256 startSortedTierId = currentMaxTierIdOf == 0 ? 0 : _firstSortedTierIdOf({hook: msg.sender, category: 0});
|
|
782
|
+
|
|
783
|
+
// Keep track of the previous tier's ID while iterating.
|
|
784
|
+
uint256 previousTierId;
|
|
785
|
+
|
|
786
|
+
// Keep a reference to the 721 contract's flags.
|
|
787
|
+
JB721TiersHookFlags memory flags = _flagsOf[msg.sender];
|
|
788
|
+
|
|
789
|
+
for (uint256 i; i < tiersToAdd.length; i++) {
|
|
790
|
+
// Set the tier being iterated upon.
|
|
791
|
+
JB721TierConfig memory tierToAdd = tiersToAdd[i];
|
|
792
|
+
|
|
793
|
+
// Make sure the supply maximum is enforced. If it's greater than one billion, it would overflow into the
|
|
794
|
+
// next tier.
|
|
795
|
+
if (tierToAdd.initialSupply > _ONE_BILLION - 1) {
|
|
796
|
+
revert JB721TiersHookStore_InvalidQuantity(tierToAdd.initialSupply, _ONE_BILLION - 1);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Keep a reference to the previous tier.
|
|
800
|
+
JB721TierConfig memory previousTier;
|
|
801
|
+
|
|
802
|
+
// Make sure the tier's category is greater than or equal to the previously added tier's category.
|
|
803
|
+
if (i != 0) {
|
|
804
|
+
// Set the reference to the previously added tier.
|
|
805
|
+
previousTier = tiersToAdd[i - 1];
|
|
806
|
+
|
|
807
|
+
// Revert if the category is not equal or greater than the previously added tier's category.
|
|
808
|
+
if (tierToAdd.category < previousTier.category) {
|
|
809
|
+
revert JB721TiersHookStore_InvalidCategorySortOrder(tierToAdd.category, previousTier.category);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Get a reference to the ID for the new tier.
|
|
814
|
+
uint256 tierId = currentMaxTierIdOf + i + 1;
|
|
815
|
+
|
|
816
|
+
// Make sure the new tier doesn't have voting units if the 721 contract's flags don't allow it to.
|
|
817
|
+
if (
|
|
818
|
+
flags.noNewTiersWithVotes
|
|
819
|
+
&& ((tierToAdd.useVotingUnits && tierToAdd.votingUnits != 0)
|
|
820
|
+
|| (!tierToAdd.useVotingUnits && tierToAdd.price != 0))
|
|
821
|
+
) {
|
|
822
|
+
revert JB721TiersHookStore_VotingUnitsNotAllowed(tierId);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Make sure the new tier doesn't have a reserve frequency if the 721 contract's flags don't allow it to,
|
|
826
|
+
// OR if manual minting is allowed.
|
|
827
|
+
if ((flags.noNewTiersWithReserves || tierToAdd.allowOwnerMint) && tierToAdd.reserveFrequency != 0) {
|
|
828
|
+
revert JB721TiersHookStore_ReserveFrequencyNotAllowed(tierId);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Make sure the new tier doesn't have owner minting enabled if the 721 contract's flags don't allow it to.
|
|
832
|
+
if (flags.noNewTiersWithOwnerMinting && tierToAdd.allowOwnerMint) {
|
|
833
|
+
revert JB721TiersHookStore_ManualMintingNotAllowed(tierId);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Make sure the discount percent is within the bound.
|
|
837
|
+
if (tierToAdd.discountPercent > JB721Constants.DISCOUNT_DENOMINATOR) {
|
|
838
|
+
revert JB721TiersHookStore_DiscountPercentExceedsBounds(
|
|
839
|
+
tierToAdd.discountPercent, JB721Constants.DISCOUNT_DENOMINATOR
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Make sure the tier has a non-zero supply.
|
|
844
|
+
if (tierToAdd.initialSupply == 0) revert JB721TiersHookStore_ZeroInitialSupply(tierId);
|
|
845
|
+
|
|
846
|
+
// Store the tier with that ID.
|
|
847
|
+
_storedTierOf[msg.sender][tierId] = JBStored721Tier({
|
|
848
|
+
price: uint104(tierToAdd.price),
|
|
849
|
+
remainingSupply: uint32(tierToAdd.initialSupply),
|
|
850
|
+
initialSupply: uint32(tierToAdd.initialSupply),
|
|
851
|
+
votingUnits: uint32(tierToAdd.votingUnits),
|
|
852
|
+
reserveFrequency: uint16(tierToAdd.reserveFrequency),
|
|
853
|
+
category: uint24(tierToAdd.category),
|
|
854
|
+
discountPercent: uint8(tierToAdd.discountPercent),
|
|
855
|
+
packedBools: _packBools({
|
|
856
|
+
allowOwnerMint: tierToAdd.allowOwnerMint,
|
|
857
|
+
transfersPausable: tierToAdd.transfersPausable,
|
|
858
|
+
useVotingUnits: tierToAdd.useVotingUnits,
|
|
859
|
+
cannotBeRemoved: tierToAdd.cannotBeRemoved,
|
|
860
|
+
cannotIncreaseDiscountPercent: tierToAdd.cannotIncreaseDiscountPercent
|
|
861
|
+
})
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// If this is the first tier in a new category, store it as the first tier in that category.
|
|
865
|
+
// The `_startingTierIdOfCategory` of the category "0" will always be the same as the `_tierIdAfter` the 0th
|
|
866
|
+
// tier.
|
|
867
|
+
if (previousTier.category != tierToAdd.category && tierToAdd.category != 0) {
|
|
868
|
+
_startingTierIdOfCategory[msg.sender][tierToAdd.category] = tierId;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Set the reserve beneficiary if needed.
|
|
872
|
+
if (tierToAdd.reserveBeneficiary != address(0) && tierToAdd.reserveFrequency != 0) {
|
|
873
|
+
if (tierToAdd.useReserveBeneficiaryAsDefault) {
|
|
874
|
+
if (defaultReserveBeneficiaryOf[msg.sender] != tierToAdd.reserveBeneficiary) {
|
|
875
|
+
defaultReserveBeneficiaryOf[msg.sender] = tierToAdd.reserveBeneficiary;
|
|
876
|
+
}
|
|
877
|
+
} else {
|
|
878
|
+
_reserveBeneficiaryOf[msg.sender][tierId] = tierToAdd.reserveBeneficiary;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Set the `encodedIPFSUri` if needed.
|
|
883
|
+
if (tierToAdd.encodedIPFSUri != bytes32(0)) {
|
|
884
|
+
encodedIPFSUriOf[msg.sender][tierId] = tierToAdd.encodedIPFSUri;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (startSortedTierId != 0) {
|
|
888
|
+
// Keep track of the sorted tier ID being iterated on.
|
|
889
|
+
uint256 currentSortedTierId = startSortedTierId;
|
|
890
|
+
|
|
891
|
+
// Keep a reference to the tier ID to iterate to next.
|
|
892
|
+
uint256 nextTierId;
|
|
893
|
+
|
|
894
|
+
// Make sure the tier is sorted correctly.
|
|
895
|
+
while (currentSortedTierId != 0) {
|
|
896
|
+
// Set the next tier ID.
|
|
897
|
+
nextTierId =
|
|
898
|
+
_nextSortedTierIdOf({hook: msg.sender, id: currentSortedTierId, max: currentLastSortedTierId});
|
|
899
|
+
|
|
900
|
+
// If the category is less than or equal to the sorted tier being iterated on,
|
|
901
|
+
// AND the tier being iterated on isn't among those being added, store the order.
|
|
902
|
+
if (
|
|
903
|
+
tierToAdd.category <= _storedTierOf[msg.sender][currentSortedTierId].category
|
|
904
|
+
&& currentSortedTierId <= currentMaxTierIdOf
|
|
905
|
+
) {
|
|
906
|
+
// If the tier ID being iterated on isn't the next tier ID, set the `_tierIdAfter` (next tier
|
|
907
|
+
// ID).
|
|
908
|
+
if (currentSortedTierId != tierId + 1) {
|
|
909
|
+
_tierIdAfter[msg.sender][tierId] = currentSortedTierId;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// If this is the first tier being added, track it as the current last sorted tier ID (if it's
|
|
913
|
+
// not already tracked).
|
|
914
|
+
if (_lastTrackedSortedTierIdOf[msg.sender] != currentLastSortedTierId) {
|
|
915
|
+
_lastTrackedSortedTierIdOf[msg.sender] = currentLastSortedTierId;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// If the previous tier's `_tierIdAfter` was set to something else, update it.
|
|
919
|
+
if (previousTierId != tierId - 1 || _tierIdAfter[msg.sender][previousTierId] != 0) {
|
|
920
|
+
// Set the the previous tier's `_tierIdAfter` to the tier being added, or 0 if the tier ID
|
|
921
|
+
// is
|
|
922
|
+
// incremented.
|
|
923
|
+
_tierIdAfter[msg.sender][previousTierId] = previousTierId == tierId - 1 ? 0 : tierId;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// When the next tier is being added, start at the sorted tier just set.
|
|
927
|
+
startSortedTierId = currentSortedTierId;
|
|
928
|
+
|
|
929
|
+
// Use the current tier ID as the "previous tier ID" when the next tier is being added.
|
|
930
|
+
previousTierId = tierId;
|
|
931
|
+
|
|
932
|
+
// Set the current sorted tier ID to zero to break out of the loop (the tier has been sorted).
|
|
933
|
+
currentSortedTierId = 0;
|
|
934
|
+
}
|
|
935
|
+
// If the tier being iterated on is the last tier, add the new tier after it.
|
|
936
|
+
else if (nextTierId == 0 || nextTierId > currentMaxTierIdOf) {
|
|
937
|
+
if (tierId != currentSortedTierId + 1) {
|
|
938
|
+
_tierIdAfter[msg.sender][currentSortedTierId] = tierId;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// For the next tier being added, start at this current tier ID.
|
|
942
|
+
startSortedTierId = tierId;
|
|
943
|
+
|
|
944
|
+
// Break out.
|
|
945
|
+
currentSortedTierId = 0;
|
|
946
|
+
|
|
947
|
+
// If there's currently a last sorted tier ID tracked, override it.
|
|
948
|
+
if (_lastTrackedSortedTierIdOf[msg.sender] != 0) _lastTrackedSortedTierIdOf[msg.sender] = 0;
|
|
949
|
+
}
|
|
950
|
+
// Move on to the next tier ID.
|
|
951
|
+
else {
|
|
952
|
+
// Set the previous tier ID to be the current tier ID.
|
|
953
|
+
previousTierId = currentSortedTierId;
|
|
954
|
+
|
|
955
|
+
// Go to the next tier ID.
|
|
956
|
+
currentSortedTierId = nextTierId;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Add the tier ID to the array being returned.
|
|
962
|
+
tierIds[i] = tierId;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Update the maximum tier ID to include the new tiers.
|
|
966
|
+
maxTierIdOf[msg.sender] = currentMaxTierIdOf + tiersToAdd.length;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/// @notice Records 721 burns.
|
|
970
|
+
/// @dev This function trusts `msg.sender` (the hook contract) to only call it after actually burning the
|
|
971
|
+
/// tokens. It does not verify ownership or existence of the token IDs — the hook is responsible for
|
|
972
|
+
/// performing those checks before calling this function.
|
|
973
|
+
/// @param tokenIds The token IDs of the NFTs to burn.
|
|
974
|
+
function recordBurn(uint256[] calldata tokenIds) external override {
|
|
975
|
+
// Iterate through all token IDs to increment the burn count.
|
|
976
|
+
for (uint256 i; i < tokenIds.length; i++) {
|
|
977
|
+
// Set the 721's token ID.
|
|
978
|
+
uint256 tokenId = tokenIds[i];
|
|
979
|
+
|
|
980
|
+
uint256 tierId = tierIdOfToken(tokenId);
|
|
981
|
+
|
|
982
|
+
// Increment the number of NFTs burned from the tier.
|
|
983
|
+
numberOfBurnedFor[msg.sender][tierId]++;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/// @notice Record newly set flags.
|
|
988
|
+
/// @param flags The flags to set.
|
|
989
|
+
function recordFlags(JB721TiersHookFlags calldata flags) external override {
|
|
990
|
+
_flagsOf[msg.sender] = flags;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/// @notice Record 721 mints from the provided tiers.
|
|
994
|
+
/// @param amount The amount being spent on NFTs. The total price must not exceed this amount.
|
|
995
|
+
/// @param tierIds The IDs of the tiers to mint from.
|
|
996
|
+
/// @param isOwnerMint A flag indicating whether this function is being directly called by the 721 contract's owner.
|
|
997
|
+
/// @return tokenIds The token IDs of the NFTs which were minted.
|
|
998
|
+
/// @return leftoverAmount The `amount` remaining after minting.
|
|
999
|
+
function recordMint(
|
|
1000
|
+
uint256 amount,
|
|
1001
|
+
uint16[] calldata tierIds,
|
|
1002
|
+
bool isOwnerMint
|
|
1003
|
+
)
|
|
1004
|
+
external
|
|
1005
|
+
override
|
|
1006
|
+
returns (uint256[] memory tokenIds, uint256 leftoverAmount)
|
|
1007
|
+
{
|
|
1008
|
+
// Set the leftover amount as the initial amount.
|
|
1009
|
+
leftoverAmount = amount;
|
|
1010
|
+
|
|
1011
|
+
// Keep a reference to the tier being iterated on.
|
|
1012
|
+
JBStored721Tier storage storedTier;
|
|
1013
|
+
|
|
1014
|
+
// Get a reference to the number of tiers.
|
|
1015
|
+
uint256 numberOfTiers = tierIds.length;
|
|
1016
|
+
|
|
1017
|
+
// Initialize the array for the token IDs to be returned.
|
|
1018
|
+
tokenIds = new uint256[](numberOfTiers);
|
|
1019
|
+
|
|
1020
|
+
// Initialize a `JBBitmapWord` for checking whether tiers have been removed.
|
|
1021
|
+
JBBitmapWord memory bitmapWord;
|
|
1022
|
+
|
|
1023
|
+
for (uint256 i; i < numberOfTiers; i++) {
|
|
1024
|
+
// Set the tier ID being iterated on.
|
|
1025
|
+
uint256 tierId = tierIds[i];
|
|
1026
|
+
|
|
1027
|
+
// Make sure the tier hasn't been removed.
|
|
1028
|
+
if (_isTierRemovedWithRefresh({hook: msg.sender, tierId: tierId, bitmapWord: bitmapWord})) {
|
|
1029
|
+
revert JB721TiersHookStore_TierRemoved(tierId);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Keep a reference to the stored tier being iterated on.
|
|
1033
|
+
storedTier = _storedTierOf[msg.sender][tierId];
|
|
1034
|
+
|
|
1035
|
+
// Parse the flags.
|
|
1036
|
+
(bool allowOwnerMint,,,,) = _unpackBools(storedTier.packedBools);
|
|
1037
|
+
|
|
1038
|
+
// If this is an owner mint, make sure owner minting is allowed.
|
|
1039
|
+
if (isOwnerMint && !allowOwnerMint) revert JB721TiersHookStore_CantMintManually(tierId);
|
|
1040
|
+
|
|
1041
|
+
// Make sure the provided tier exists (tiers cannot have a supply of 0).
|
|
1042
|
+
if (storedTier.initialSupply == 0) revert JB721TiersHookStore_UnrecognizedTier(tierId);
|
|
1043
|
+
|
|
1044
|
+
// Get a reference to the price.
|
|
1045
|
+
uint256 price = storedTier.price;
|
|
1046
|
+
|
|
1047
|
+
// Apply a discount if needed.
|
|
1048
|
+
if (storedTier.discountPercent > 0) {
|
|
1049
|
+
price -= mulDiv(price, storedTier.discountPercent, JB721Constants.DISCOUNT_DENOMINATOR);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Make sure the `amount` is greater than or equal to the tier's price.
|
|
1053
|
+
if (price > leftoverAmount) revert JB721TiersHookStore_PriceExceedsAmount(price, leftoverAmount);
|
|
1054
|
+
|
|
1055
|
+
// Make sure there's at least one NFT remaining to mint.
|
|
1056
|
+
if (storedTier.remainingSupply == 0) revert JB721TiersHookStore_InsufficientSupplyRemaining(tierId);
|
|
1057
|
+
|
|
1058
|
+
// Mint the 721 — decrement remaining supply first so the reserve check below
|
|
1059
|
+
// sees the post-mint state (this non-reserve mint may increase pending reserves).
|
|
1060
|
+
unchecked {
|
|
1061
|
+
// Keep a reference to its token ID.
|
|
1062
|
+
tokenIds[i] = _generateTokenId({
|
|
1063
|
+
tierId: tierId, tokenNumber: storedTier.initialSupply - --storedTier.remainingSupply
|
|
1064
|
+
});
|
|
1065
|
+
leftoverAmount = leftoverAmount - price;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Make sure there are still enough NFTs remaining to satisfy pending reserves.
|
|
1069
|
+
if (
|
|
1070
|
+
storedTier.remainingSupply
|
|
1071
|
+
< _numberOfPendingReservesFor({hook: msg.sender, tierId: tierId, storedTier: storedTier})
|
|
1072
|
+
) {
|
|
1073
|
+
revert JB721TiersHookStore_InsufficientSupplyRemaining(tierId);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/// @notice Record reserve 721 minting for the provided tier ID on the provided 721 contract.
|
|
1079
|
+
/// @param tierId The ID of the tier to mint reserves from.
|
|
1080
|
+
/// @param count The number of reserve NFTs to mint.
|
|
1081
|
+
/// @return tokenIds The token IDs of the reserve NFTs which were minted.
|
|
1082
|
+
function recordMintReservesFor(uint256 tierId, uint256 count)
|
|
1083
|
+
external
|
|
1084
|
+
override
|
|
1085
|
+
returns (uint256[] memory tokenIds)
|
|
1086
|
+
{
|
|
1087
|
+
// Get a reference to the stored tier.
|
|
1088
|
+
JBStored721Tier storage storedTier = _storedTierOf[msg.sender][tierId];
|
|
1089
|
+
|
|
1090
|
+
// Get a reference to the number of pending reserve NFTs for the tier.
|
|
1091
|
+
// "Pending" means that the NFTs have been reserved, but have not been minted yet.
|
|
1092
|
+
uint256 numberOfPendingReserves =
|
|
1093
|
+
_numberOfPendingReservesFor({hook: msg.sender, tierId: tierId, storedTier: storedTier});
|
|
1094
|
+
|
|
1095
|
+
// Can't mint more than the number of pending reserves.
|
|
1096
|
+
if (count > numberOfPendingReserves) {
|
|
1097
|
+
revert JB721TiersHookStore_InsufficientPendingReserves(count, numberOfPendingReserves);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Increment the number of reserve NFTs minted.
|
|
1101
|
+
numberOfReservesMintedFor[msg.sender][tierId] += count;
|
|
1102
|
+
|
|
1103
|
+
// Initialize an array for the token IDs to be returned.
|
|
1104
|
+
tokenIds = new uint256[](count);
|
|
1105
|
+
|
|
1106
|
+
for (uint256 i; i < count; i++) {
|
|
1107
|
+
// Generate the NFTs.
|
|
1108
|
+
tokenIds[i] = _generateTokenId({
|
|
1109
|
+
tierId: tierId, tokenNumber: storedTier.initialSupply - --storedTier.remainingSupply
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/// @notice Record tiers being removed.
|
|
1115
|
+
/// @dev Removing a tier only marks it in a bitmap — it does not update the sorted tier linked list.
|
|
1116
|
+
/// Call `cleanTiers()` after removing tiers to update the sorting sequence and prevent stale tier iteration.
|
|
1117
|
+
/// @param tierIds The IDs of the tiers being removed.
|
|
1118
|
+
function recordRemoveTierIds(uint256[] calldata tierIds) external override {
|
|
1119
|
+
for (uint256 i; i < tierIds.length; i++) {
|
|
1120
|
+
// Set the tier being iterated upon (0-indexed).
|
|
1121
|
+
uint256 tierId = tierIds[i];
|
|
1122
|
+
|
|
1123
|
+
// Get a reference to the stored tier.
|
|
1124
|
+
JBStored721Tier storage storedTier = _storedTierOf[msg.sender][tierId];
|
|
1125
|
+
|
|
1126
|
+
// Parse the flags.
|
|
1127
|
+
(,,, bool cannotBeRemoved,) = _unpackBools(storedTier.packedBools);
|
|
1128
|
+
|
|
1129
|
+
// Make sure the tier can be removed.
|
|
1130
|
+
if (cannotBeRemoved) revert JB721TiersHookStore_CantRemoveTier(tierId);
|
|
1131
|
+
|
|
1132
|
+
// Remove the tier by marking it as removed in the bitmap.
|
|
1133
|
+
_removedTiersBitmapWordOf[msg.sender].removeTier(tierId);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/// @notice Records the setting of a discount for a tier.
|
|
1138
|
+
/// @param tierId The ID of the tier to record a discount for.
|
|
1139
|
+
/// @param discountPercent The new discount percent being applied.
|
|
1140
|
+
function recordSetDiscountPercentOf(uint256 tierId, uint256 discountPercent) external override {
|
|
1141
|
+
// Make sure the discount percent is within the bound.
|
|
1142
|
+
if (discountPercent > JB721Constants.DISCOUNT_DENOMINATOR) {
|
|
1143
|
+
revert JB721TiersHookStore_DiscountPercentExceedsBounds(
|
|
1144
|
+
discountPercent, JB721Constants.DISCOUNT_DENOMINATOR
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// Get a reference to the stored tier.
|
|
1149
|
+
JBStored721Tier storage storedTier = _storedTierOf[msg.sender][tierId];
|
|
1150
|
+
|
|
1151
|
+
// Parse the flags.
|
|
1152
|
+
(,,,, bool cannotIncreaseDiscountPercent) = _unpackBools(storedTier.packedBools);
|
|
1153
|
+
|
|
1154
|
+
// Make sure that increasing the discount is allowed for the tier.
|
|
1155
|
+
if (discountPercent > storedTier.discountPercent && cannotIncreaseDiscountPercent) {
|
|
1156
|
+
revert JB721TiersHookStore_DiscountPercentIncreaseNotAllowed(discountPercent, storedTier.discountPercent);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Set the discount.
|
|
1160
|
+
storedTier.discountPercent = uint8(discountPercent);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/// @notice Record a new encoded IPFS URI for a tier.
|
|
1164
|
+
/// @param tierId The ID of the tier to set the encoded IPFS URI of.
|
|
1165
|
+
/// @param encodedIPFSUri The encoded IPFS URI to set for the tier.
|
|
1166
|
+
function recordSetEncodedIPFSUriOf(uint256 tierId, bytes32 encodedIPFSUri) external override {
|
|
1167
|
+
encodedIPFSUriOf[msg.sender][tierId] = encodedIPFSUri;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/// @notice Record a newly set token URI resolver.
|
|
1171
|
+
/// @param resolver The resolver to set.
|
|
1172
|
+
function recordSetTokenUriResolver(IJB721TokenUriResolver resolver) external override {
|
|
1173
|
+
tokenUriResolverOf[msg.sender] = resolver;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/// @notice Record an 721 transfer.
|
|
1177
|
+
/// @param tierId The ID of the tier that the 721 being transferred belongs to.
|
|
1178
|
+
/// @param from The address that the 721 is being transferred from.
|
|
1179
|
+
/// @param to The address that the 721 is being transferred to.
|
|
1180
|
+
function recordTransferForTier(uint256 tierId, address from, address to) external override {
|
|
1181
|
+
// If this is not a mint,
|
|
1182
|
+
if (from != address(0)) {
|
|
1183
|
+
// then subtract the tier balance from the sender.
|
|
1184
|
+
--tierBalanceOf[msg.sender][from][tierId];
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// If this is not a burn,
|
|
1188
|
+
if (to != address(0)) {
|
|
1189
|
+
unchecked {
|
|
1190
|
+
// then increase the tier balance for the receiver.
|
|
1191
|
+
++tierBalanceOf[msg.sender][to][tierId];
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|