@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,765 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.23;
|
|
3
|
+
|
|
4
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
5
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
6
|
+
import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
|
|
7
|
+
import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
|
|
8
|
+
import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
9
|
+
import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
|
|
10
|
+
import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
|
|
11
|
+
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|
|
12
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
13
|
+
import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
|
|
14
|
+
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
15
|
+
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
16
|
+
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
|
|
17
|
+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
18
|
+
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
19
|
+
|
|
20
|
+
import {JB721Hook} from "./abstract/JB721Hook.sol";
|
|
21
|
+
import {IJB721TiersHook} from "./interfaces/IJB721TiersHook.sol";
|
|
22
|
+
import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
|
|
23
|
+
import {IJB721TokenUriResolver} from "./interfaces/IJB721TokenUriResolver.sol";
|
|
24
|
+
import {JB721TiersRulesetMetadataResolver} from "./libraries/JB721TiersRulesetMetadataResolver.sol";
|
|
25
|
+
import {JBIpfsDecoder} from "./libraries/JBIpfsDecoder.sol";
|
|
26
|
+
import {JB721Tier} from "./structs/JB721Tier.sol";
|
|
27
|
+
import {JB721TierConfig} from "./structs/JB721TierConfig.sol";
|
|
28
|
+
import {JB721TiersSetDiscountPercentConfig} from "./structs/JB721TiersSetDiscountPercentConfig.sol";
|
|
29
|
+
import {JB721InitTiersConfig} from "./structs/JB721InitTiersConfig.sol";
|
|
30
|
+
import {JB721TiersHookFlags} from "./structs/JB721TiersHookFlags.sol";
|
|
31
|
+
import {JB721TiersMintReservesConfig} from "./structs/JB721TiersMintReservesConfig.sol";
|
|
32
|
+
|
|
33
|
+
/// @title JB721TiersHook
|
|
34
|
+
/// @notice A Juicebox project can use this hook to sell tiered ERC-721 NFTs with different prices and metadata. When
|
|
35
|
+
/// the project is paid, the hook may mint NFTs to the payer, depending on the hook's setup, the amount paid, and
|
|
36
|
+
/// information specified by the payer. The project's owner can enable NFT cash outs through this hook, allowing
|
|
37
|
+
/// holders to burn their NFTs to reclaim funds from the project (in proportion to the NFT's price).
|
|
38
|
+
contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook {
|
|
39
|
+
//*********************************************************************//
|
|
40
|
+
// --------------------------- custom errors ------------------------- //
|
|
41
|
+
//*********************************************************************//
|
|
42
|
+
|
|
43
|
+
error JB721TiersHook_AlreadyInitialized(uint256 projectId);
|
|
44
|
+
error JB721TiersHook_CurrencyMismatch(uint256 paymentCurrency, uint256 tierCurrency);
|
|
45
|
+
error JB721TiersHook_InvalidPricingDecimals(uint256 decimals);
|
|
46
|
+
error JB721TiersHook_MintReserveNftsPaused();
|
|
47
|
+
error JB721TiersHook_NoProjectId();
|
|
48
|
+
error JB721TiersHook_Overspending(uint256 leftoverAmount);
|
|
49
|
+
error JB721TiersHook_TierTransfersPaused();
|
|
50
|
+
|
|
51
|
+
//*********************************************************************//
|
|
52
|
+
// --------------- public immutable stored properties ---------------- //
|
|
53
|
+
//*********************************************************************//
|
|
54
|
+
|
|
55
|
+
/// @notice The contract storing and managing project rulesets.
|
|
56
|
+
IJBRulesets public immutable override RULESETS;
|
|
57
|
+
|
|
58
|
+
/// @notice The contract that stores and manages data for this contract's NFTs.
|
|
59
|
+
IJB721TiersHookStore public immutable override STORE;
|
|
60
|
+
|
|
61
|
+
//*********************************************************************//
|
|
62
|
+
// ---------------------- public stored properties ------------------- //
|
|
63
|
+
//*********************************************************************//
|
|
64
|
+
/// @notice The base URI for the NFT `tokenUris`.
|
|
65
|
+
string public override baseURI;
|
|
66
|
+
|
|
67
|
+
/// @notice This contract's metadata URI.
|
|
68
|
+
string public override contractURI;
|
|
69
|
+
|
|
70
|
+
/// @notice If an address pays more than the price of the NFT they received, the extra amount is stored as credits
|
|
71
|
+
/// which can be cashed out to mint NFTs.
|
|
72
|
+
/// @custom:param addr The address to get the NFT credits balance of.
|
|
73
|
+
/// @return The amount of credits the address has.
|
|
74
|
+
mapping(address addr => uint256) public override payCreditsOf;
|
|
75
|
+
|
|
76
|
+
//*********************************************************************//
|
|
77
|
+
// --------------------- internal stored properties ------------------ //
|
|
78
|
+
//*********************************************************************//
|
|
79
|
+
|
|
80
|
+
/// @notice The first owner of each token ID, stored on first transfer out.
|
|
81
|
+
/// @custom:param The token ID of the NFT to get the stored first owner of.
|
|
82
|
+
mapping(uint256 tokenId => address) internal _firstOwnerOf;
|
|
83
|
+
|
|
84
|
+
/// @notice Packed context for the pricing of this contract's tiers.
|
|
85
|
+
/// @dev Packed into a uint256:
|
|
86
|
+
/// - currency in bits 0-31 (32 bits),
|
|
87
|
+
/// - pricing decimals in bits 32-39 (8 bits), and
|
|
88
|
+
/// - prices contract in bits 40-199 (160 bits).
|
|
89
|
+
uint256 internal _packedPricingContext;
|
|
90
|
+
|
|
91
|
+
//*********************************************************************//
|
|
92
|
+
// -------------------------- constructor ---------------------------- //
|
|
93
|
+
//*********************************************************************//
|
|
94
|
+
|
|
95
|
+
/// @param directory A directory of terminals and controllers for projects.
|
|
96
|
+
/// @param permissions A contract storing permissions.
|
|
97
|
+
/// @param rulesets A contract storing and managing project rulesets.
|
|
98
|
+
/// @param store The contract which stores the NFT's data.
|
|
99
|
+
/// @param trustedForwarder The trusted forwarder for the ERC2771Context.
|
|
100
|
+
constructor(
|
|
101
|
+
IJBDirectory directory,
|
|
102
|
+
IJBPermissions permissions,
|
|
103
|
+
IJBRulesets rulesets,
|
|
104
|
+
IJB721TiersHookStore store,
|
|
105
|
+
address trustedForwarder
|
|
106
|
+
)
|
|
107
|
+
JBOwnable(permissions, directory.PROJECTS(), msg.sender, uint88(0))
|
|
108
|
+
JB721Hook(directory)
|
|
109
|
+
ERC2771Context(trustedForwarder)
|
|
110
|
+
{
|
|
111
|
+
RULESETS = rulesets;
|
|
112
|
+
STORE = store;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
//*********************************************************************//
|
|
116
|
+
// ------------------------- external views -------------------------- //
|
|
117
|
+
//*********************************************************************//
|
|
118
|
+
|
|
119
|
+
/// @notice The first owner of an NFT.
|
|
120
|
+
/// @dev This is generally the address which paid for the NFT.
|
|
121
|
+
/// @param tokenId The token ID of the NFT to get the first owner of.
|
|
122
|
+
/// @return The address of the NFT's first owner.
|
|
123
|
+
function firstOwnerOf(uint256 tokenId) external view override returns (address) {
|
|
124
|
+
// Get a reference to the first owner.
|
|
125
|
+
address storedFirstOwner = _firstOwnerOf[tokenId];
|
|
126
|
+
|
|
127
|
+
// If the stored first owner is set, return it.
|
|
128
|
+
if (storedFirstOwner != address(0)) return storedFirstOwner;
|
|
129
|
+
|
|
130
|
+
// Otherwise, the first owner must be the current owner.
|
|
131
|
+
return _ownerOf(tokenId);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// @notice Context for the pricing of this hook's tiers.
|
|
135
|
+
/// @dev If the `prices` contract is the zero address, this contract only accepts payments in the `currency` token.
|
|
136
|
+
/// @return currency The currency used for tier prices.
|
|
137
|
+
/// @return decimals The amount of decimals being used in tier prices.
|
|
138
|
+
/// @return prices The prices contract used to resolve the value of payments in currencies other than `currency`.
|
|
139
|
+
function pricingContext() external view override returns (uint256 currency, uint256 decimals, IJBPrices prices) {
|
|
140
|
+
// Get a reference to the packed pricing context.
|
|
141
|
+
uint256 packed = _packedPricingContext;
|
|
142
|
+
// currency in bits 0-31 (32 bits).
|
|
143
|
+
currency = uint256(uint32(packed));
|
|
144
|
+
// pricing decimals in bits 32-39 (8 bits).
|
|
145
|
+
decimals = uint256(uint8(packed >> 32));
|
|
146
|
+
// prices contract in bits 40-199 (160 bits).
|
|
147
|
+
prices = IJBPrices(address(uint160(packed >> 40)));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
//*********************************************************************//
|
|
151
|
+
// -------------------------- public views --------------------------- //
|
|
152
|
+
//*********************************************************************//
|
|
153
|
+
|
|
154
|
+
/// @notice The total number of this hook's NFTs that an address holds (from all tiers).
|
|
155
|
+
/// @param owner The address to check the balance of.
|
|
156
|
+
/// @return balance The number of NFTs the address owns across this hook's tiers.
|
|
157
|
+
function balanceOf(address owner) public view override returns (uint256 balance) {
|
|
158
|
+
return STORE.balanceOf({hook: address(this), owner: owner});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// @notice Initializes a cloned copy of the original `JB721Hook` contract.
|
|
162
|
+
/// @param projectId The ID of the project this this hook is associated with.
|
|
163
|
+
/// @param name The name of the NFT collection.
|
|
164
|
+
/// @param symbol The symbol representing the NFT collection.
|
|
165
|
+
/// @param baseUri The URI to use as a base for full NFT `tokenUri`s.
|
|
166
|
+
/// @param tokenUriResolver An optional contract responsible for resolving the token URI for each NFT's token ID.
|
|
167
|
+
/// @param contractUri A URI where this contract's metadata can be found.
|
|
168
|
+
/// @param tiersConfig The NFT tiers and pricing context to initialize the hook with. The tiers must be sorted by
|
|
169
|
+
/// category (from least to greatest).
|
|
170
|
+
/// @param flags A set of additional options which dictate how the hook behaves.
|
|
171
|
+
function initialize(
|
|
172
|
+
uint256 projectId,
|
|
173
|
+
string memory name,
|
|
174
|
+
string memory symbol,
|
|
175
|
+
string memory baseUri,
|
|
176
|
+
IJB721TokenUriResolver tokenUriResolver,
|
|
177
|
+
string memory contractUri,
|
|
178
|
+
JB721InitTiersConfig memory tiersConfig,
|
|
179
|
+
JB721TiersHookFlags memory flags
|
|
180
|
+
)
|
|
181
|
+
public
|
|
182
|
+
override
|
|
183
|
+
{
|
|
184
|
+
// Stop re-initialization by ensuring a projectId is provided and doesn't already exist.
|
|
185
|
+
if (PROJECT_ID != 0) revert JB721TiersHook_AlreadyInitialized(PROJECT_ID);
|
|
186
|
+
|
|
187
|
+
// Make sure a projectId is provided.
|
|
188
|
+
if (projectId == 0) revert JB721TiersHook_NoProjectId();
|
|
189
|
+
|
|
190
|
+
// Initialize the superclass.
|
|
191
|
+
JB721Hook._initialize({projectId: projectId, name: name, symbol: symbol});
|
|
192
|
+
|
|
193
|
+
// Validate pricing decimals are within a reasonable range.
|
|
194
|
+
if (tiersConfig.decimals > 18) revert JB721TiersHook_InvalidPricingDecimals(tiersConfig.decimals);
|
|
195
|
+
|
|
196
|
+
// Pack pricing context from the `tiersConfig`.
|
|
197
|
+
uint256 packed;
|
|
198
|
+
// pack the currency in bits 0-31 (32 bits).
|
|
199
|
+
packed |= uint256(tiersConfig.currency);
|
|
200
|
+
// pack the pricing decimals in bits 32-39 (8 bits).
|
|
201
|
+
packed |= uint256(tiersConfig.decimals) << 32;
|
|
202
|
+
// pack the prices contract in bits 40-199 (160 bits).
|
|
203
|
+
packed |= uint256(uint160(address(tiersConfig.prices))) << 40;
|
|
204
|
+
// Store the packed value.
|
|
205
|
+
// slither-disable-next-line events-maths
|
|
206
|
+
_packedPricingContext = packed;
|
|
207
|
+
|
|
208
|
+
// Store the base URI if provided.
|
|
209
|
+
if (bytes(baseUri).length != 0) baseURI = baseUri;
|
|
210
|
+
|
|
211
|
+
// Set the contract URI if provided.
|
|
212
|
+
if (bytes(contractUri).length != 0) contractURI = contractUri;
|
|
213
|
+
|
|
214
|
+
// Set the token URI resolver if provided.
|
|
215
|
+
if (tokenUriResolver != IJB721TokenUriResolver(address(0))) {
|
|
216
|
+
_recordSetTokenUriResolver(tokenUriResolver);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Record the tiers in this hook's store.
|
|
220
|
+
// slither-disable-next-line unused-return
|
|
221
|
+
if (tiersConfig.tiers.length != 0) STORE.recordAddTiers(tiersConfig.tiers);
|
|
222
|
+
|
|
223
|
+
// Set the flags if needed.
|
|
224
|
+
if (
|
|
225
|
+
flags.noNewTiersWithReserves || flags.noNewTiersWithVotes || flags.noNewTiersWithOwnerMinting
|
|
226
|
+
|| flags.preventOverspending
|
|
227
|
+
) STORE.recordFlags(flags);
|
|
228
|
+
|
|
229
|
+
// Transfer ownership to the initializer.
|
|
230
|
+
_transferOwnership(_msgSender());
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// @notice The combined cash out weight of the NFTs with the specified token IDs.
|
|
234
|
+
/// @dev An NFT's cash out weight is its price.
|
|
235
|
+
/// @dev To get their relative cash out weight, divide the result by the `totalCashOutWeight(...)`.
|
|
236
|
+
/// @param tokenIds The token IDs of the NFTs to get the cumulative cash out weight of.
|
|
237
|
+
/// @return weight The cash out weight of the tokenIds.
|
|
238
|
+
function cashOutWeightOf(
|
|
239
|
+
uint256[] memory tokenIds,
|
|
240
|
+
JBBeforeCashOutRecordedContext calldata
|
|
241
|
+
)
|
|
242
|
+
public
|
|
243
|
+
view
|
|
244
|
+
virtual
|
|
245
|
+
override
|
|
246
|
+
returns (uint256)
|
|
247
|
+
{
|
|
248
|
+
return STORE.cashOutWeightOf({hook: address(this), tokenIds: tokenIds});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// @notice Indicates if this contract adheres to the specified interface.
|
|
252
|
+
/// @dev See {IERC165-supportsInterface}.
|
|
253
|
+
/// @param interfaceId The ID of the interface to check for adherence to.
|
|
254
|
+
function supportsInterface(bytes4 interfaceId) public view override(IERC165, JB721Hook) returns (bool) {
|
|
255
|
+
return interfaceId == type(IJB721TiersHook).interfaceId || JB721Hook.supportsInterface(interfaceId);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// @notice The metadata URI of the NFT with the specified token ID.
|
|
259
|
+
/// @dev Defers to the `tokenUriResolver` if it is set. Otherwise, use the `tokenUri` corresponding with the NFT's
|
|
260
|
+
/// tier.
|
|
261
|
+
/// @param tokenId The token ID of the NFT to get the metadata URI of.
|
|
262
|
+
/// @return The token URI from the `tokenUriResolver` if it is set. If it isn't set, the token URI for the NFT's
|
|
263
|
+
/// tier.
|
|
264
|
+
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
|
|
265
|
+
// Get a reference to the `tokenUriResolver`.
|
|
266
|
+
IJB721TokenUriResolver resolver = STORE.tokenUriResolverOf(address(this));
|
|
267
|
+
|
|
268
|
+
// If a `tokenUriResolver` is set, use it to resolve the token URI.
|
|
269
|
+
if (address(resolver) != address(0)) return resolver.tokenUriOf({nft: address(this), tokenId: tokenId});
|
|
270
|
+
|
|
271
|
+
// Otherwise, return the token URI corresponding with the NFT's tier.
|
|
272
|
+
return JBIpfsDecoder.decode({
|
|
273
|
+
baseUri: baseURI, hexString: STORE.encodedTierIPFSUriOf({hook: address(this), tokenId: tokenId})
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/// @notice The combined cash out weight of all outstanding NFTs.
|
|
278
|
+
/// @dev An NFT's cash out weight is its price.
|
|
279
|
+
/// @return weight The total cash out weight.
|
|
280
|
+
function totalCashOutWeight(JBBeforeCashOutRecordedContext calldata)
|
|
281
|
+
public
|
|
282
|
+
view
|
|
283
|
+
virtual
|
|
284
|
+
override
|
|
285
|
+
returns (uint256)
|
|
286
|
+
{
|
|
287
|
+
return STORE.totalCashOutWeight(address(this));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
//*********************************************************************//
|
|
291
|
+
// -------------------------- internal views ------------------------- //
|
|
292
|
+
//*********************************************************************//
|
|
293
|
+
|
|
294
|
+
/// @dev ERC-2771 specifies the context as being a single address (20 bytes).
|
|
295
|
+
function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) {
|
|
296
|
+
return super._contextSuffixLength();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/// @notice The project's current ruleset.
|
|
300
|
+
/// @param projectId The ID of the project to check.
|
|
301
|
+
/// @return The project's current ruleset.
|
|
302
|
+
function _currentRulesetOf(uint256 projectId) internal view returns (JBRuleset memory) {
|
|
303
|
+
// slither-disable-next-line calls-loop
|
|
304
|
+
return RULESETS.currentOf(projectId);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// @notice Returns the calldata, preferred to use over `msg.data`
|
|
308
|
+
/// @return calldata the `msg.data` of this call
|
|
309
|
+
function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
|
|
310
|
+
return ERC2771Context._msgData();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/// @notice Returns the sender, preferred to use over `msg.sender`
|
|
314
|
+
/// @return sender the sender address of this call.
|
|
315
|
+
function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) {
|
|
316
|
+
return ERC2771Context._msgSender();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
//*********************************************************************//
|
|
320
|
+
// ---------------------- external transactions ---------------------- //
|
|
321
|
+
//*********************************************************************//
|
|
322
|
+
|
|
323
|
+
/// @notice Add or delete tiers.
|
|
324
|
+
/// @dev Only the contract's owner or an operator with the `ADJUST_TIERS` permission from the owner can adjust the
|
|
325
|
+
/// tiers.
|
|
326
|
+
/// @dev Any added tiers must adhere to this hook's `JB721TiersHookFlags`.
|
|
327
|
+
/// @param tiersToAdd The tiers to add, as an array of `JB721TierConfig` structs`.
|
|
328
|
+
/// @param tierIdsToRemove The tiers to remove, as an array of tier IDs.
|
|
329
|
+
function adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata tierIdsToRemove) external override {
|
|
330
|
+
// Enforce permissions.
|
|
331
|
+
_requirePermissionFrom({
|
|
332
|
+
account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.ADJUST_721_TIERS
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Remove the tiers.
|
|
336
|
+
if (tierIdsToRemove.length != 0) {
|
|
337
|
+
// Emit events for each removed tier.
|
|
338
|
+
for (uint256 i; i < tierIdsToRemove.length; i++) {
|
|
339
|
+
emit RemoveTier({tierId: tierIdsToRemove[i], caller: _msgSender()});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Record the removed tiers.
|
|
343
|
+
// slither-disable-next-line reentrancy-events
|
|
344
|
+
STORE.recordRemoveTierIds(tierIdsToRemove);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Add the tiers.
|
|
348
|
+
if (tiersToAdd.length != 0) {
|
|
349
|
+
// Record the added tiers in the store.
|
|
350
|
+
uint256[] memory tierIdsAdded = STORE.recordAddTiers(tiersToAdd);
|
|
351
|
+
|
|
352
|
+
// Emit events for each added tier.
|
|
353
|
+
for (uint256 i; i < tiersToAdd.length; i++) {
|
|
354
|
+
emit AddTier({tierId: tierIdsAdded[i], tier: tiersToAdd[i], caller: _msgSender()});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// @notice Manually mint NFTs from the provided tiers .
|
|
360
|
+
/// @param tierIds The IDs of the tiers to mint from.
|
|
361
|
+
/// @param beneficiary The address to mint to.
|
|
362
|
+
/// @return tokenIds The IDs of the newly minted tokens.
|
|
363
|
+
function mintFor(
|
|
364
|
+
uint16[] calldata tierIds,
|
|
365
|
+
address beneficiary
|
|
366
|
+
)
|
|
367
|
+
external
|
|
368
|
+
override
|
|
369
|
+
returns (uint256[] memory tokenIds)
|
|
370
|
+
{
|
|
371
|
+
// Enforce permissions.
|
|
372
|
+
_requirePermissionFrom({account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.MINT_721});
|
|
373
|
+
|
|
374
|
+
// Record the mint. The token IDs returned correspond to the tiers passed in.
|
|
375
|
+
// slither-disable-next-line reentrancy-events,unused-return
|
|
376
|
+
(tokenIds,) = STORE.recordMint({
|
|
377
|
+
amount: type(uint256).max, // force the mint.
|
|
378
|
+
tierIds: tierIds,
|
|
379
|
+
isOwnerMint: true // manual mint.
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
for (uint256 i; i < tierIds.length; i++) {
|
|
383
|
+
// Set the token ID.
|
|
384
|
+
uint256 tokenId = tokenIds[i];
|
|
385
|
+
|
|
386
|
+
// Mint the NFT.
|
|
387
|
+
_mint(beneficiary, tokenId);
|
|
388
|
+
|
|
389
|
+
emit Mint({
|
|
390
|
+
tokenId: tokenId, tierId: tierIds[i], beneficiary: beneficiary, totalAmountPaid: 0, caller: _msgSender()
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/// @notice Mint pending reserved NFTs based on the provided information.
|
|
396
|
+
/// @dev "Pending" means that the NFTs have been reserved, but have not been minted yet.
|
|
397
|
+
/// @param reserveMintConfigs Contains information about how many reserved tokens to mint for each tier.
|
|
398
|
+
function mintPendingReservesFor(JB721TiersMintReservesConfig[] calldata reserveMintConfigs) external override {
|
|
399
|
+
for (uint256 i; i < reserveMintConfigs.length; i++) {
|
|
400
|
+
// Get a reference to the params being iterated upon.
|
|
401
|
+
JB721TiersMintReservesConfig memory params = reserveMintConfigs[i];
|
|
402
|
+
|
|
403
|
+
// Mint pending reserved NFTs from the tier.
|
|
404
|
+
mintPendingReservesFor({tierId: params.tierId, count: params.count});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/// @notice Allows the collection's owner to set the discount for a tier, if the tier allows it.
|
|
409
|
+
/// @dev Only the contract's owner or an operator with the `SET_721_DISCOUNT_PERCENT` permission from the owner can
|
|
410
|
+
/// adjust the
|
|
411
|
+
/// tiers.
|
|
412
|
+
/// @param tierId The ID of the tier to set the discount of.
|
|
413
|
+
/// @param discountPercent The discount percent to set.
|
|
414
|
+
function setDiscountPercentOf(uint256 tierId, uint256 discountPercent) external override {
|
|
415
|
+
// Enforce permissions.
|
|
416
|
+
_requirePermissionFrom({
|
|
417
|
+
account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.SET_721_DISCOUNT_PERCENT
|
|
418
|
+
});
|
|
419
|
+
_setDiscountPercentOf({tierId: tierId, discountPercent: discountPercent});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/// @notice Allows the collection's owner to set the discount percent for multiple tiers.
|
|
423
|
+
/// @param configs The configs to set the discount percent for.
|
|
424
|
+
function setDiscountPercentsOf(JB721TiersSetDiscountPercentConfig[] calldata configs) external override {
|
|
425
|
+
// Enforce permissions.
|
|
426
|
+
_requirePermissionFrom({
|
|
427
|
+
account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.SET_721_DISCOUNT_PERCENT
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
for (uint256 i; i < configs.length; i++) {
|
|
431
|
+
// Set the config being iterated on.
|
|
432
|
+
JB721TiersSetDiscountPercentConfig memory config = configs[i];
|
|
433
|
+
|
|
434
|
+
_setDiscountPercentOf({tierId: config.tierId, discountPercent: config.discountPercent});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/// @notice Update this hook's URI metadata properties.
|
|
439
|
+
/// @dev Only this contract's owner can set the metadata.
|
|
440
|
+
/// @param baseUri The new base URI.
|
|
441
|
+
/// @param contractUri The new contract URI.
|
|
442
|
+
/// @param tokenUriResolver The new URI resolver.
|
|
443
|
+
/// @param encodedIPFSUriTierId The ID of the tier to set the encoded IPFS URI of.
|
|
444
|
+
/// @param encodedIPFSUri The encoded IPFS URI to set.
|
|
445
|
+
function setMetadata(
|
|
446
|
+
string calldata baseUri,
|
|
447
|
+
string calldata contractUri,
|
|
448
|
+
IJB721TokenUriResolver tokenUriResolver,
|
|
449
|
+
uint256 encodedIPFSUriTierId,
|
|
450
|
+
bytes32 encodedIPFSUri
|
|
451
|
+
)
|
|
452
|
+
external
|
|
453
|
+
override
|
|
454
|
+
{
|
|
455
|
+
// Enforce permissions.
|
|
456
|
+
_requirePermissionFrom({
|
|
457
|
+
account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.SET_721_METADATA
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
if (bytes(baseUri).length != 0) {
|
|
461
|
+
// Store the new base URI.
|
|
462
|
+
baseURI = baseUri;
|
|
463
|
+
emit SetBaseUri({baseUri: baseUri, caller: _msgSender()});
|
|
464
|
+
}
|
|
465
|
+
if (bytes(contractUri).length != 0) {
|
|
466
|
+
// Store the new contract URI.
|
|
467
|
+
contractURI = contractUri;
|
|
468
|
+
emit SetContractUri({uri: contractUri, caller: _msgSender()});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (tokenUriResolver != IJB721TokenUriResolver(address(this))) {
|
|
472
|
+
// Store the new URI resolver.
|
|
473
|
+
// slither-disable-next-line reentrancy-events
|
|
474
|
+
_recordSetTokenUriResolver(tokenUriResolver);
|
|
475
|
+
}
|
|
476
|
+
if (encodedIPFSUriTierId != 0 && encodedIPFSUri != bytes32(0)) {
|
|
477
|
+
emit SetEncodedIPFSUri({tierId: encodedIPFSUriTierId, encodedUri: encodedIPFSUri, caller: _msgSender()});
|
|
478
|
+
|
|
479
|
+
// Store the new encoded IPFS URI.
|
|
480
|
+
STORE.recordSetEncodedIPFSUriOf({tierId: encodedIPFSUriTierId, encodedIPFSUri: encodedIPFSUri});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
//*********************************************************************//
|
|
485
|
+
// ----------------------- public transactions ----------------------- //
|
|
486
|
+
//*********************************************************************//
|
|
487
|
+
|
|
488
|
+
/// @notice Mint reserved pending reserved NFTs within the provided tier.
|
|
489
|
+
/// @dev "Pending" means that the NFTs have been reserved, but have not been minted yet.
|
|
490
|
+
/// @param tierId The ID of the tier to mint reserved NFTs from.
|
|
491
|
+
/// @param count The number of reserved NFTs to mint.
|
|
492
|
+
function mintPendingReservesFor(uint256 tierId, uint256 count) public override {
|
|
493
|
+
// Get a reference to the project's current ruleset.
|
|
494
|
+
JBRuleset memory ruleset = _currentRulesetOf(PROJECT_ID);
|
|
495
|
+
|
|
496
|
+
// Pending reserve mints must not be paused.
|
|
497
|
+
if (JB721TiersRulesetMetadataResolver.mintPendingReservesPaused((JBRulesetMetadataResolver.metadata(ruleset))))
|
|
498
|
+
{
|
|
499
|
+
revert JB721TiersHook_MintReserveNftsPaused();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Record the reserved mint for the tier.
|
|
503
|
+
// slither-disable-next-line reentrancy-events,calls-loop
|
|
504
|
+
uint256[] memory tokenIds = STORE.recordMintReservesFor({tierId: tierId, count: count});
|
|
505
|
+
|
|
506
|
+
// Keep a reference to the beneficiary.
|
|
507
|
+
// slither-disable-next-line calls-loop
|
|
508
|
+
address reserveBeneficiary = STORE.reserveBeneficiaryOf({hook: address(this), tierId: tierId});
|
|
509
|
+
|
|
510
|
+
for (uint256 i; i < count; i++) {
|
|
511
|
+
// Set the token ID.
|
|
512
|
+
uint256 tokenId = tokenIds[i];
|
|
513
|
+
|
|
514
|
+
emit MintReservedNft({
|
|
515
|
+
tokenId: tokenId, tierId: tierId, beneficiary: reserveBeneficiary, caller: _msgSender()
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Mint the NFT.
|
|
519
|
+
// slither-disable-next-line reentrency-events
|
|
520
|
+
_mint(reserveBeneficiary, tokenId);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
//*********************************************************************//
|
|
525
|
+
// ------------------------ internal functions ----------------------- //
|
|
526
|
+
//*********************************************************************//
|
|
527
|
+
|
|
528
|
+
/// @notice A function which gets called after NFTs have been cashed out and recorded by the terminal.
|
|
529
|
+
/// @param tokenIds The token IDs of the NFTs that were burned.
|
|
530
|
+
function _didBurn(uint256[] memory tokenIds) internal virtual override {
|
|
531
|
+
// Add to burned counter.
|
|
532
|
+
STORE.recordBurn(tokenIds);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/// @notice Mints one NFT from each of the specified tiers for the beneficiary.
|
|
536
|
+
/// @dev The same tier can be specified more than once.
|
|
537
|
+
/// @param amount The amount to base the mints on. The total price of the NFTs being minted cannot be larger than
|
|
538
|
+
/// this amount.
|
|
539
|
+
/// @param mintTierIds An array of NFT tier IDs to be minted.
|
|
540
|
+
/// @param beneficiary The address receiving the newly minted NFTs.
|
|
541
|
+
/// @return leftoverAmount The `amount` leftover after minting.
|
|
542
|
+
function _mintAll(
|
|
543
|
+
uint256 amount,
|
|
544
|
+
uint16[] memory mintTierIds,
|
|
545
|
+
address beneficiary
|
|
546
|
+
)
|
|
547
|
+
internal
|
|
548
|
+
returns (uint256 leftoverAmount)
|
|
549
|
+
{
|
|
550
|
+
// Keep a reference to the NFT token IDs.
|
|
551
|
+
uint256[] memory tokenIds;
|
|
552
|
+
|
|
553
|
+
// Record the NFT mints. The token IDs returned correspond to the tier IDs passed in.
|
|
554
|
+
(tokenIds, leftoverAmount) = STORE.recordMint({
|
|
555
|
+
amount: amount,
|
|
556
|
+
tierIds: mintTierIds,
|
|
557
|
+
isOwnerMint: false // Not a manual mint
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// Loop through each token ID and mint the corresponding NFT.
|
|
561
|
+
for (uint256 i; i < tokenIds.length; i++) {
|
|
562
|
+
// Get a reference to the token ID being iterated on.
|
|
563
|
+
uint256 tokenId = tokenIds[i];
|
|
564
|
+
|
|
565
|
+
emit Mint({
|
|
566
|
+
tokenId: tokenId,
|
|
567
|
+
tierId: mintTierIds[i],
|
|
568
|
+
beneficiary: beneficiary,
|
|
569
|
+
totalAmountPaid: amount,
|
|
570
|
+
caller: _msgSender()
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Mint the NFT.
|
|
574
|
+
// slither-disable-next-line reentrancy-events
|
|
575
|
+
_mint(beneficiary, tokenId);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/// @notice Process a payment, minting NFTs and updating credits as necessary.
|
|
580
|
+
/// @dev Pay credits are tracked per beneficiary, not per payer. When the payer differs from the beneficiary,
|
|
581
|
+
/// the payer's existing credits are NOT applied to the mint. Only the beneficiary's credits are combined with
|
|
582
|
+
/// the incoming payment value. Leftover funds after minting are stored as credits for the beneficiary.
|
|
583
|
+
/// @param context Payment context provided by the terminal after it has recorded the payment in the terminal store.
|
|
584
|
+
function _processPayment(JBAfterPayRecordedContext calldata context) internal virtual override {
|
|
585
|
+
// Normalize the payment value based on the pricing context.
|
|
586
|
+
uint256 value;
|
|
587
|
+
|
|
588
|
+
{
|
|
589
|
+
uint256 packed = _packedPricingContext;
|
|
590
|
+
// pricing currency in bits 0-31 (32 bits).
|
|
591
|
+
uint256 pricingCurrency = uint256(uint32(packed));
|
|
592
|
+
if (context.amount.currency == pricingCurrency) {
|
|
593
|
+
value = context.amount.value;
|
|
594
|
+
} else {
|
|
595
|
+
// prices in bits 40-199 (160 bits).
|
|
596
|
+
IJBPrices prices = IJBPrices(address(uint160(packed >> 40)));
|
|
597
|
+
if (prices != IJBPrices(address(0))) {
|
|
598
|
+
// pricing decimals in bits 32-39 (8 bits).
|
|
599
|
+
uint256 pricingDecimals = uint256(uint8(packed >> 32));
|
|
600
|
+
value = mulDiv(
|
|
601
|
+
context.amount.value,
|
|
602
|
+
10 ** pricingDecimals,
|
|
603
|
+
prices.pricePerUnitOf({
|
|
604
|
+
projectId: PROJECT_ID,
|
|
605
|
+
pricingCurrency: context.amount.currency,
|
|
606
|
+
unitCurrency: pricingCurrency,
|
|
607
|
+
decimals: context.amount.decimals
|
|
608
|
+
})
|
|
609
|
+
);
|
|
610
|
+
} else {
|
|
611
|
+
revert JB721TiersHook_CurrencyMismatch(context.amount.currency, pricingCurrency);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Keep a reference to the number of NFT credits the beneficiary already has.
|
|
617
|
+
uint256 payCredits = payCreditsOf[context.beneficiary];
|
|
618
|
+
|
|
619
|
+
// Set the leftover amount as the initial value.
|
|
620
|
+
uint256 leftoverAmount = value;
|
|
621
|
+
|
|
622
|
+
// If the payer is the beneficiary, combine their NFT credits with the amount paid.
|
|
623
|
+
uint256 unusedPayCredits;
|
|
624
|
+
if (context.payer == context.beneficiary) {
|
|
625
|
+
unchecked {
|
|
626
|
+
leftoverAmount += payCredits;
|
|
627
|
+
}
|
|
628
|
+
} else {
|
|
629
|
+
// Otherwise, the payer's NFT credits won't be used, and we keep track of the unused credits.
|
|
630
|
+
unusedPayCredits = payCredits;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Keep a reference to the boolean indicating whether paying more than the price of the NFTs being minted is
|
|
634
|
+
// allowed. Defaults to the collection's flag.
|
|
635
|
+
bool allowOverspending = !STORE.flagsOf(address(this)).preventOverspending;
|
|
636
|
+
|
|
637
|
+
// Resolve the metadata.
|
|
638
|
+
(bool found, bytes memory metadata) = JBMetadataResolver.getDataFor({
|
|
639
|
+
id: JBMetadataResolver.getId({purpose: "pay", target: METADATA_ID_TARGET}), metadata: context.payerMetadata
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
if (found) {
|
|
643
|
+
// Keep a reference to the IDs of the tier be to minted.
|
|
644
|
+
uint16[] memory tierIdsToMint;
|
|
645
|
+
|
|
646
|
+
// Keep a reference to the payer's flag indicating whether overspending is allowed.
|
|
647
|
+
bool payerAllowsOverspending;
|
|
648
|
+
|
|
649
|
+
// Decode the metadata.
|
|
650
|
+
(payerAllowsOverspending, tierIdsToMint) = abi.decode(metadata, (bool, uint16[]));
|
|
651
|
+
|
|
652
|
+
// Make sure overspending is allowed if requested.
|
|
653
|
+
if (allowOverspending && !payerAllowsOverspending) {
|
|
654
|
+
allowOverspending = false;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Mint NFTs from the tiers as specified.
|
|
658
|
+
if (tierIdsToMint.length != 0) {
|
|
659
|
+
// slither-disable-next-line reentrancy-events,reentrancy-no-eth
|
|
660
|
+
leftoverAmount =
|
|
661
|
+
_mintAll({amount: leftoverAmount, mintTierIds: tierIdsToMint, beneficiary: context.beneficiary});
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// If overspending is allowed and there are leftover funds, add those funds to the beneficiary's NFT credits.
|
|
666
|
+
if (leftoverAmount != 0) {
|
|
667
|
+
// If overspending isn't allowed, revert.
|
|
668
|
+
if (!allowOverspending) revert JB721TiersHook_Overspending(leftoverAmount);
|
|
669
|
+
|
|
670
|
+
// Store the leftover amount as NFT credits.
|
|
671
|
+
unchecked {
|
|
672
|
+
// Keep a reference to the amount of new NFT credits.
|
|
673
|
+
uint256 newPayCredits = leftoverAmount + unusedPayCredits;
|
|
674
|
+
|
|
675
|
+
// Emit the change in NFT credits.
|
|
676
|
+
if (newPayCredits > payCredits) {
|
|
677
|
+
emit AddPayCredits({
|
|
678
|
+
amount: newPayCredits - payCredits,
|
|
679
|
+
newTotalCredits: newPayCredits,
|
|
680
|
+
account: context.beneficiary,
|
|
681
|
+
caller: _msgSender()
|
|
682
|
+
});
|
|
683
|
+
} else if (payCredits > newPayCredits) {
|
|
684
|
+
emit UsePayCredits({
|
|
685
|
+
amount: payCredits - newPayCredits,
|
|
686
|
+
newTotalCredits: newPayCredits,
|
|
687
|
+
account: context.beneficiary,
|
|
688
|
+
caller: _msgSender()
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Store the new NFT credits for the beneficiary.
|
|
693
|
+
payCreditsOf[context.beneficiary] = newPayCredits;
|
|
694
|
+
}
|
|
695
|
+
// Otherwise, reset their NFT credits.
|
|
696
|
+
} else if (payCredits != unusedPayCredits) {
|
|
697
|
+
// Emit the change in NFT credits.
|
|
698
|
+
emit UsePayCredits({
|
|
699
|
+
amount: payCredits - unusedPayCredits,
|
|
700
|
+
newTotalCredits: unusedPayCredits,
|
|
701
|
+
account: context.beneficiary,
|
|
702
|
+
caller: _msgSender()
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
// Store the new NFT credits.
|
|
706
|
+
payCreditsOf[context.beneficiary] = unusedPayCredits;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/// @notice Record the setting of a new token URI resolver.
|
|
711
|
+
/// @param tokenUriResolver The new token URI resolver.
|
|
712
|
+
function _recordSetTokenUriResolver(IJB721TokenUriResolver tokenUriResolver) internal {
|
|
713
|
+
emit SetTokenUriResolver({resolver: tokenUriResolver, caller: _msgSender()});
|
|
714
|
+
|
|
715
|
+
STORE.recordSetTokenUriResolver(tokenUriResolver);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/// @notice Internal function to set the discount percent for a tier.
|
|
719
|
+
/// @param tierId The ID of the tier to set the discount percent for.
|
|
720
|
+
/// @param discountPercent The discount percent to set for the tier.
|
|
721
|
+
function _setDiscountPercentOf(uint256 tierId, uint256 discountPercent) internal {
|
|
722
|
+
emit SetDiscountPercent({tierId: tierId, discountPercent: discountPercent, caller: _msgSender()});
|
|
723
|
+
|
|
724
|
+
// Record the discount percent for the tier.
|
|
725
|
+
// slither-disable-next-line calls-loop
|
|
726
|
+
STORE.recordSetDiscountPercentOf({tierId: tierId, discountPercent: discountPercent});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/// @notice Before transferring an NFT, register its first owner (if necessary).
|
|
730
|
+
/// @param to The address the NFT is being transferred to.
|
|
731
|
+
/// @param tokenId The token ID of the NFT being transferred.
|
|
732
|
+
function _update(address to, uint256 tokenId, address auth) internal virtual override returns (address from) {
|
|
733
|
+
// Get a reference to the tier.
|
|
734
|
+
// slither-disable-next-line calls-loop
|
|
735
|
+
JB721Tier memory tier = STORE.tierOfTokenId({hook: address(this), tokenId: tokenId, includeResolvedUri: false});
|
|
736
|
+
|
|
737
|
+
// Record the transfers and keep a reference to where the token is coming from.
|
|
738
|
+
from = super._update(to, tokenId, auth);
|
|
739
|
+
|
|
740
|
+
// Transfers must not be paused (when not minting or burning).
|
|
741
|
+
if (from != address(0)) {
|
|
742
|
+
// If transfers are pausable, check if they're paused.
|
|
743
|
+
if (tier.transfersPausable) {
|
|
744
|
+
// Get a reference to the project's current ruleset.
|
|
745
|
+
JBRuleset memory ruleset = _currentRulesetOf(PROJECT_ID);
|
|
746
|
+
|
|
747
|
+
// If transfers are paused and the NFT isn't being transferred to the zero address, revert.
|
|
748
|
+
if (
|
|
749
|
+
to != address(0)
|
|
750
|
+
&& JB721TiersRulesetMetadataResolver.transfersPaused(
|
|
751
|
+
(JBRulesetMetadataResolver.metadata(ruleset))
|
|
752
|
+
)
|
|
753
|
+
) revert JB721TiersHook_TierTransfersPaused();
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// If the token isn't already associated with a first owner, store the sender as the first owner.
|
|
757
|
+
// slither-disable-next-line calls-loop
|
|
758
|
+
if (_firstOwnerOf[tokenId] == address(0)) _firstOwnerOf[tokenId] = from;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Record the transfer.
|
|
762
|
+
// slither-disable-next-line reentrency-events,calls-loop
|
|
763
|
+
STORE.recordTransferForTier({tierId: tier.id, from: from, to: to});
|
|
764
|
+
}
|
|
765
|
+
}
|