@bananapus/721-hook-v6 0.0.6 → 0.0.8
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/README.md +32 -28
- package/SKILLS.md +119 -27
- package/package.json +1 -1
- package/src/JB721TiersHook.sol +57 -186
- package/src/abstract/JB721Hook.sol +264 -0
- package/src/interfaces/IJB721Hook.sol +21 -0
- package/src/interfaces/IJB721TiersHook.sol +2 -17
- package/test/unit/pay_Unit.t.sol +85 -1
- package/test/unit/redeem_Unit.t.sol +4 -6
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Juicebox 721 Hook
|
|
2
2
|
|
|
3
3
|
`nana-721-hook` is:
|
|
4
4
|
|
|
5
5
|
1. A pay hook for Juicebox projects to sell tiered NFTs (ERC-721s) with different prices and artwork.
|
|
6
|
-
2. (Optionally) a
|
|
6
|
+
2. (Optionally) a cash out hook which allows holders to burn their NFTs to reclaim funds from the project, in proportion to the NFT's price.
|
|
7
7
|
|
|
8
8
|
<details>
|
|
9
9
|
<summary>Table of Contents</summary>
|
|
@@ -156,15 +156,15 @@ nana-721-hook/
|
|
|
156
156
|
│ └── helpers/
|
|
157
157
|
│ └── Hook721DeploymentLib.sol - Internal helpers for deployment scripts.
|
|
158
158
|
├── src/ - Contract source code. Top level contains implementation contracts.
|
|
159
|
-
│ ├── JB721TiersHook.sol - The core tiered NFT pay/
|
|
159
|
+
│ ├── JB721TiersHook.sol - The core tiered NFT pay/cash out hook.
|
|
160
160
|
│ ├── JB721TiersHookDeployer.sol - Deploys an NFT hook for a project.
|
|
161
161
|
│ ├── JB721TiersHookProjectDeployer.sol - Deploys a project with a tiered NFT hook.
|
|
162
162
|
│ ├── JB721TiersHookStore.sol - Stores and manages data for tiered NFT hooks.
|
|
163
163
|
│ ├── abstract/
|
|
164
|
-
│ │ ├──
|
|
165
|
-
│ │ └──
|
|
164
|
+
│ │ ├── JB721Hook.sol - Abstract base hook: handles pay/cash out lifecycle, metadata, and terminal validation.
|
|
165
|
+
│ │ └── ERC721.sol - Clone-compatible abstract ERC-721 implementation.
|
|
166
166
|
│ ├── interfaces/ - Contract interfaces.
|
|
167
|
-
│ ├── libraries/ - Libraries.
|
|
167
|
+
│ ├── libraries/ - Libraries (includes JB721TiersHookLib for tier adjustments, split distribution, price normalization, and token URI resolution).
|
|
168
168
|
│ └── structs/ - Structs.
|
|
169
169
|
└── test/ - Forge tests and testing utilities.
|
|
170
170
|
├── E2E/
|
|
@@ -190,9 +190,9 @@ graph TD;
|
|
|
190
190
|
D[JB721TiersHookDeployer] -->|Adds NFT hooks to| B
|
|
191
191
|
A -->|Deploys| C[JB721TiersHook]
|
|
192
192
|
D -->|Deploys| C
|
|
193
|
-
B -->|Calls upon pay/
|
|
193
|
+
B -->|Calls upon pay/cash out| C
|
|
194
194
|
C -->|Stores data in| E[JB721TiersHookStore]
|
|
195
|
-
B -->|Uses| F[Pay/
|
|
195
|
+
B -->|Uses| F[Pay/cash out terminal]
|
|
196
196
|
C -->|Mints NFTs upon payment through| F
|
|
197
197
|
C -->|Burns NFTs to reclaim funds through| F
|
|
198
198
|
```
|
|
@@ -201,7 +201,8 @@ graph TD;
|
|
|
201
201
|
|
|
202
202
|
| Contract | Description |
|
|
203
203
|
| --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
204
|
-
| [`
|
|
204
|
+
| [`JB721Hook.sol`](https://github.com/Bananapus/nana-721-hook/blob/main/src/abstract/JB721Hook.sol) | Abstract base for 721 hooks: handles pay/cash out lifecycle, terminal validation, and metadata resolution. |
|
|
205
|
+
| [`JB721TiersHook.sol`](https://github.com/Bananapus/nana-721-hook/blob/main/src/JB721TiersHook.sol) | The core tiered NFT pay/cash out hook implementation, extending `JB721Hook`. |
|
|
205
206
|
| [`JB721TiersHookDeployer.sol`](https://github.com/Bananapus/nana-721-hook/blob/main/src/JB721TiersHookDeployer.sol) | Exposes a `deployHookFor(…)` function which allows deploys an NFT hook for a project. |
|
|
206
207
|
| [`JB721TiersHookProjectDeployer.sol`](https://github.com/Bananapus/nana-721-hook/blob/main/src/JB721TiersHookProjectDeployer.sol) | Exposes a `launchProjectFor(…)` function which deploys a project with a tiered NFT hook already set up. |
|
|
207
208
|
| [`JB721TiersHookStore.sol`](https://github.com/Bananapus/nana-721-hook/blob/main/src/JB721TiersHookStore.sol) | Stores and manages data for tiered NFT hooks. |
|
|
@@ -210,44 +211,47 @@ graph TD;
|
|
|
210
211
|
|
|
211
212
|
### Hooks
|
|
212
213
|
|
|
213
|
-
This contract is a _data hook_, a _pay hook_, and a
|
|
214
|
+
This contract is a _data hook_, a _pay hook_, and a _cash out hook_. Data hooks receive information about a payment or a cash out, and put together a payload for the pay/cash out hook to execute.
|
|
214
215
|
|
|
215
|
-
Juicebox projects can specify a data hook in their `JBRulesetMetadata`. When someone attempts to pay or
|
|
216
|
+
Juicebox projects can specify a data hook in their `JBRulesetMetadata`. When someone attempts to pay or cash out from the project, the project's terminal records the payment in the terminal store, passing information about the payment/cash out to the data hook in the process. The data hook responds with a list of payloads – each payload specifies the address of a pay/cash out hook, as well as some custom data and an amount of funds to send to that pay/cash out hook.
|
|
216
217
|
|
|
217
|
-
Each pay/
|
|
218
|
+
Each pay/cash out hook can then execute custom behavior based on the custom data (and funds) they receive.
|
|
218
219
|
|
|
219
220
|
### Mechanism
|
|
220
221
|
|
|
221
|
-
A project using a 721 tiers hook can specify any number of NFT tiers.
|
|
222
|
+
A project using a 721 tiers hook can specify any number of NFT tiers (up to 65,535 total).
|
|
222
223
|
|
|
223
|
-
- NFT tiers can be removed by the project owner as long as they are not locked.
|
|
224
|
-
- NFT tiers can be added by the project owner as long as they respect the hook's `flags`. The flags specify if newly added tiers can have votes (voting units), if new tiers can have non-zero reserve frequencies, if new tiers can allow on-demand minting by the project's owner, and if
|
|
224
|
+
- NFT tiers can be removed by the project owner as long as they are not locked (`cannotBeRemoved`). After removing tiers, call `cleanTiers()` on the store to optimize tier iteration.
|
|
225
|
+
- NFT tiers can be added by the project owner as long as they respect the hook's `flags`. The flags specify if newly added tiers can have votes (voting units), if new tiers can have non-zero reserve frequencies, if new tiers can allow on-demand minting by the project's owner, and if overspending is allowed.
|
|
225
226
|
|
|
226
|
-
Each tier has the following
|
|
227
|
+
Each tier has the following properties:
|
|
227
228
|
|
|
228
|
-
- A price.
|
|
229
|
-
- A supply (the maximum number of NFTs which can be minted from the tier).
|
|
229
|
+
- A price (up to `uint104`).
|
|
230
|
+
- A supply (the maximum number of NFTs which can be minted from the tier, up to 999,999,999).
|
|
230
231
|
- A token URI (artwork and metadata), which can be overridden by a URI resolver. The URI resolver can return unique values for each NFT in the tier.
|
|
231
232
|
- A category, so tiers can be organized and accessed for different purposes.
|
|
232
|
-
- A
|
|
233
|
-
- A
|
|
233
|
+
- A discount percent (optional). Reduces the effective purchase price. The discount is out of 200, so a `discountPercent` of 100 means 50% off, and 200 means free. The discount can be changed later via `setDiscountPercentOf`, and tiers can be configured with `cannotIncreaseDiscountPercent` to only allow discounts to decrease. Cash out weight is always based on the original tier price, not the discounted price.
|
|
234
|
+
- A reserve frequency (optional). With a reserve frequency of 5, an extra NFT will be minted to a pre-specified beneficiary address for every 5 NFTs purchased and minted from the tier. Tiers with owner minting enabled cannot have reserves.
|
|
235
|
+
- Voting units (optional). By default, each NFT's voting power equals its tier price. If `useVotingUnits` is true, a custom `votingUnits` value is used instead.
|
|
234
236
|
- A flag to specify whether the NFTs in the tier can always be transferred, or if transfers can be paused depending on the project's ruleset.
|
|
235
237
|
- A flag to specify whether the contract's owner can mint NFTs from the tier on-demand.
|
|
236
|
-
- A set of
|
|
238
|
+
- A split percent and a set of splits (optional). Each tier can route a percentage of its mint price to configured split recipients (other projects, addresses, etc.) every time an NFT from the tier is purchased. The remaining funds stay in the project's balance. The `splitPercent` is out of `JBConstants.SPLITS_TOTAL_PERCENT` (1,000,000,000).
|
|
239
|
+
- A set of flags which restrict tiers added in the future (the votes/reserved frequency/on-demand minting/overspending flags noted above).
|
|
237
240
|
|
|
238
241
|
Additional notes:
|
|
239
242
|
|
|
240
|
-
- A payer can specify any number of tiers to mint as long as the total price does not exceed the amount being paid. If tiers aren't specified,
|
|
241
|
-
- If the payment and a tier's price are specified in different currencies, the `JBPrices` contract is used to normalize the values.
|
|
242
|
-
- If some of a payment does not go towards purchasing an NFT, those extra funds will be stored as "NFT credits" which can be used for future purchases. Optionally, the hook can disallow credits and reject payments with leftover funds.
|
|
243
|
-
- If enabled by the project owner, holders can burn their NFTs to reclaim funds from the project. These
|
|
244
|
-
- NFT
|
|
245
|
-
-
|
|
243
|
+
- A payer can specify any number of tiers to mint as long as the total price does not exceed the amount being paid. If tiers aren't specified, the leftover amount is stored as pay credits (if allowed).
|
|
244
|
+
- If the payment and a tier's price are specified in different currencies, the `JBPrices` contract is used to normalize the values. If no `JBPrices` contract is set and the currencies differ, the payment is silently ignored (no mint, no revert).
|
|
245
|
+
- If some of a payment does not go towards purchasing an NFT, those extra funds will be stored as "NFT credits" which can be used for future purchases. Credits are only combined with the payment when `payer == beneficiary`. Optionally, the hook can disallow credits and reject payments with leftover funds (via `preventOverspending`).
|
|
246
|
+
- If enabled by the project owner, holders can burn their NFTs to reclaim funds from the project. These cash outs are proportional to the NFTs price, relative to the combined price of all the NFTs (including pending reserves in the denominator).
|
|
247
|
+
- NFT cash outs can be enabled by setting `useDataHookForCashOut` to `true` in the project's `JBRulesetMetadata`. If NFT cash outs are enabled, project token cash outs are disabled -- attempting to cash out fungible tokens when the data hook is active will revert.
|
|
248
|
+
- Per-tier voting units can be configured: either custom voting units or the tier's price as the default. Voting power is computed per-address across all tiers.
|
|
249
|
+
- The hook declares support for ERC-2981 (royalties) via `supportsInterface`, but does not implement the `royaltyInfo` function. This is intended for future extension.
|
|
246
250
|
|
|
247
251
|
### Setup
|
|
248
252
|
|
|
249
253
|
To use a 721 tiers hook, a Juicebox project should be created by a `JB721TiersHookProjectDeployer` instead of a `JBController`. The deployer will create a `JB721TiersHook` (through an associated `JB721TiersHookDeployer`) and add it to the project's first ruleset. New rulesets can be queued with `JB721TiersHookProjectDeployer.queueRulesetsOf(…)` if the project's owner gives the project deployer the permission [`JBPermissions.QUEUE_RULESETS`](https://github.com/Bananapus/nana-permission-ids/blob/master/src/JBPermissionIds.sol) (ID `2`) in [`JBPermissions`](https://github.com/Bananapus/nana-core/blob/main/src/JBPermissions.sol).
|
|
250
254
|
|
|
251
|
-
It's also possible to add a 721 tiers hook to an existing project by calling `JB721TiersHookDeployer.deployHookFor(…)` and adding the hook to the project's ruleset – specifically, the project must set their [`JBRulesetMetadata.dataHook`](https://github.com/Bananapus/nana-core/blob/main/src/structs/JBRulesetMetadata.sol) to the newly deployed hook, and enable `JBRulesetMetadata.useDataHookForPay` and/or `JBRulesetMetadata.
|
|
255
|
+
It's also possible to add a 721 tiers hook to an existing project by calling `JB721TiersHookDeployer.deployHookFor(…)` and adding the hook to the project's ruleset – specifically, the project must set their [`JBRulesetMetadata.dataHook`](https://github.com/Bananapus/nana-core/blob/main/src/structs/JBRulesetMetadata.sol) to the newly deployed hook, and enable `JBRulesetMetadata.useDataHookForPay` and/or `JBRulesetMetadata.useDataHookForCashOut` depending on the functionality they'd like to enable.
|
|
252
256
|
|
|
253
257
|
All `JB721TiersHook`s store their data in the `JB721TiersHookStore` contract.
|
package/SKILLS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Juicebox 721 Hook
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
@@ -8,76 +8,166 @@ Tiered ERC-721 NFT hook for Juicebox V6 that mints NFTs when a project is paid a
|
|
|
8
8
|
|
|
9
9
|
| Contract | Role |
|
|
10
10
|
|----------|------|
|
|
11
|
-
| `
|
|
11
|
+
| `JB721Hook` (abstract) | Abstract base hook: owns `DIRECTORY`, `METADATA_ID_TARGET`, `PROJECT_ID`. Implements `afterPayRecordedWith` (terminal validation + delegates to virtual `_processPayment`), `afterCashOutRecordedWith` (terminal validation, burn loop, delegates to virtual `_didBurn`), `beforeCashOutRecordedWith` (metadata decoding, delegates to virtual `cashOutWeightOf`/`totalCashOutWeight`), `beforePayRecordedWith` (default: forward weight), `hasMintPermissionFor` (returns false), `supportsInterface`, and `_initialize`. |
|
|
12
|
+
| `JB721TiersHook` | Core hook: extends `JB721Hook`. Manages tiers, reserves, credits, metadata, and discount percents. Deployed as minimal clones. Inherits `JBOwnable`, `ERC2771Context`, `JB721Hook`, `IJB721TiersHook`. Overrides `cashOutWeightOf`, `totalCashOutWeight`, `_didBurn`, `_processPayment`, and `beforePayRecordedWith` (adds tier split calculation). |
|
|
12
13
|
| `JB721TiersHookStore` | Shared singleton storage for all hook instances. Stores tiers (`JBStored721Tier`), balances, reserves, bitmaps for removed tiers, flags, and token URI resolvers. |
|
|
13
14
|
| `JB721TiersHookDeployer` | Factory: clones `JB721TiersHook` via `LibClone.clone` / `cloneDeterministic`, initializes, registers in address registry. |
|
|
14
|
-
| `JB721TiersHookProjectDeployer` | Convenience deployer: creates a Juicebox project + hook in one transaction. Wires the hook as the data hook with `useDataHookForPay: true`. |
|
|
15
|
-
| `
|
|
15
|
+
| `JB721TiersHookProjectDeployer` | Convenience deployer: creates a Juicebox project + hook in one transaction. Also supports `launchRulesetsFor` and `queueRulesetsOf`. Wires the hook as the data hook with `useDataHookForPay: true`. |
|
|
16
|
+
| `JB721TiersHookLib` (library) | External library called via DELEGATECALL from the hook. Handles tier adjustments (`adjustTiersFor`), split amount calculation (`calculateSplitAmounts`), split fund distribution (`distributeAll`), price normalization (`normalizePaymentValue`), and token URI resolution (`resolveTokenURI`). Extracted to stay within EIP-170 contract size limit. |
|
|
17
|
+
| `IJB721Hook` (interface) | Interface for `JB721Hook`: extends `IJBRulesetDataHook`, `IJBPayHook`, `IJBCashOutHook`. Declares `DIRECTORY()`, `METADATA_ID_TARGET()`, `PROJECT_ID()`. |
|
|
16
18
|
| `ERC721` (abstract) | Clone-compatible ERC-721 with `_initialize(name, symbol)` instead of constructor args. |
|
|
17
19
|
|
|
18
20
|
## Key Functions
|
|
19
21
|
|
|
20
22
|
| Function | Contract | What it does |
|
|
21
23
|
|----------|----------|--------------|
|
|
22
|
-
| `initialize(projectId, name, symbol, baseUri, tokenUriResolver, contractUri, tiersConfig, flags)` | `JB721TiersHook` | One-time setup for a cloned hook instance. Stores pricing context (currency, decimals, prices contract packed into uint256), records tiers and flags in the store. |
|
|
23
|
-
| `afterPayRecordedWith(context)` | `JB721Hook` | Called by terminal after payment. Validates caller is a project terminal, delegates to `_processPayment`. |
|
|
24
|
-
| `_processPayment(context)` | `JB721TiersHook` | Normalizes payment value via pricing context, decodes payer metadata for tier IDs to mint, calls `_mintAll`, manages pay credits for overspending. |
|
|
25
|
-
| `afterCashOutRecordedWith(context)` | `JB721Hook` | Called by terminal during cash out. Decodes token IDs from metadata, validates ownership, burns NFTs,
|
|
26
|
-
| `beforePayRecordedWith(context)` | `
|
|
27
|
-
| `beforeCashOutRecordedWith(context)` | `JB721Hook` | Data hook: calculates `cashOutCount` (
|
|
28
|
-
| `adjustTiers(tiersToAdd, tierIdsToRemove)` | `JB721TiersHook` | Owner-only. Adds/removes tiers via
|
|
24
|
+
| `initialize(projectId, name, symbol, baseUri, tokenUriResolver, contractUri, tiersConfig, flags)` | `JB721TiersHook` | One-time setup for a cloned hook instance. Stores pricing context (currency, decimals, prices contract packed into uint256), records tiers and flags in the store. Validates `decimals <= 18`. |
|
|
25
|
+
| `afterPayRecordedWith(context)` | `JB721Hook` | Called by terminal after payment. Validates caller is a project terminal, delegates to virtual `_processPayment`. |
|
|
26
|
+
| `_processPayment(context)` | `JB721TiersHook` | Normalizes payment value via pricing context, decodes payer metadata for tier IDs to mint, calls `_mintAll`, manages pay credits for overspending. Distributes tier split funds via `JB721TiersHookLib.distributeAll` if split amounts were forwarded. |
|
|
27
|
+
| `afterCashOutRecordedWith(context)` | `JB721Hook` | Called by terminal during cash out. Decodes token IDs from metadata, validates ownership, burns NFTs, delegates to virtual `_didBurn`. Reverts if `msg.value != 0`. |
|
|
28
|
+
| `beforePayRecordedWith(context)` | `JB721TiersHook` | Data hook: returns original weight, calculates per-tier split amounts via `JB721TiersHookLib.calculateSplitAmounts`, and sets this contract as the pay hook with the total split amount forwarded. |
|
|
29
|
+
| `beforeCashOutRecordedWith(context)` | `JB721Hook` | Data hook: calculates `cashOutCount` (via virtual `cashOutWeightOf`) and `totalSupply` (via virtual `totalCashOutWeight`). Rejects if fungible tokens are also being cashed out. |
|
|
30
|
+
| `adjustTiers(tiersToAdd, tierIdsToRemove)` | `JB721TiersHook` | Owner-only. Adds/removes tiers via `JB721TiersHookLib.adjustTiersFor` (DELEGATECALL). Requires `ADJUST_721_TIERS` permission. Registers tier splits in `JBSplits` if configured. |
|
|
29
31
|
| `mintFor(tierIds, beneficiary)` | `JB721TiersHook` | Owner-only manual mint. Requires `MINT_721` permission. Passes `amount: type(uint256).max` and `isOwnerMint: true` to force the mint. |
|
|
30
32
|
| `mintPendingReservesFor(tierId, count)` | `JB721TiersHook` | Public. Mints pending reserve NFTs for a tier to the tier's `reserveBeneficiary`. Checks ruleset metadata for `mintPendingReservesPaused`. |
|
|
31
|
-
| `
|
|
33
|
+
| `mintPendingReservesFor(configs[])` | `JB721TiersHook` | Batch variant. Calls `mintPendingReservesFor(tierId, count)` for each config. |
|
|
34
|
+
| `setMetadata(baseUri, contractUri, tokenUriResolver, encodedIPFSUriTierId, encodedIPFSUri)` | `JB721TiersHook` | Owner-only. Updates base URI, contract URI, token URI resolver, or per-tier encoded IPFS URI. Requires `SET_721_METADATA` permission. |
|
|
32
35
|
| `setDiscountPercentOf(tierId, discountPercent)` | `JB721TiersHook` | Owner-only. Sets discount percent for a tier. Requires `SET_721_DISCOUNT_PERCENT` permission. |
|
|
36
|
+
| `setDiscountPercentsOf(configs[])` | `JB721TiersHook` | Batch variant. Sets discount percent for multiple tiers. Requires `SET_721_DISCOUNT_PERCENT` permission. |
|
|
37
|
+
| `tokenURI(tokenId)` | `JB721TiersHook` | Resolves token metadata URI. Delegates to `JB721TiersHookLib.resolveTokenURI`, which checks for a custom `tokenUriResolver` first, then falls back to IPFS decoding via `JBIpfsDecoder`. |
|
|
38
|
+
| `firstOwnerOf(tokenId)` | `JB721TiersHook` | Returns the first owner of an NFT (the address that originally received it). Stored on first transfer out; returns current owner if never transferred. |
|
|
39
|
+
| `pricingContext()` | `JB721TiersHook` | Unpacks and returns the currency, decimals, and prices contract from the packed `_packedPricingContext`. |
|
|
40
|
+
| `balanceOf(owner)` | `JB721TiersHook` | Overrides ERC-721 `balanceOf` to delegate to `STORE.balanceOf`, which sums across all tiers. |
|
|
41
|
+
| `hasMintPermissionFor(...)` | `JB721Hook` | Always returns `false`. Required by `IJBRulesetDataHook`; prevents the hook from granting mint permissions to anyone. |
|
|
42
|
+
| `supportsInterface(interfaceId)` | `JB721TiersHook` | Returns `true` for `IJB721TiersHook`, `IJBRulesetDataHook`, `IJBPayHook`, `IJBCashOutHook`, `IERC2981`, `IERC721`, `IERC721Metadata`, `IERC165`. |
|
|
33
43
|
| `deployHookFor(projectId, config, salt)` | `JB721TiersHookDeployer` | Clones the hook implementation, initializes it, transfers ownership to caller, registers in address registry. |
|
|
34
44
|
| `launchProjectFor(owner, deployConfig, launchConfig, controller, salt)` | `JB721TiersHookProjectDeployer` | Creates project via controller, deploys hook, wires hook as data hook with `useDataHookForPay: true`, transfers hook ownership to project. |
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
45
|
+
| `launchRulesetsFor(projectId, deployConfig, launchRulesetsConfig, controller, salt)` | `JB721TiersHookProjectDeployer` | Deploys a hook for an existing project and launches rulesets. Requires `QUEUE_RULESETS` and `SET_TERMINALS` permissions. Transfers hook ownership to project. |
|
|
46
|
+
| `queueRulesetsOf(projectId, deployConfig, queueRulesetsConfig, controller, salt)` | `JB721TiersHookProjectDeployer` | Deploys a hook and queues rulesets for an existing project. Requires `QUEUE_RULESETS` permission. Transfers hook ownership to project. |
|
|
47
|
+
| `recordMint(amount, tierIds, isOwnerMint)` | `JB721TiersHookStore` | Records minting: validates supply, checks tier prices against amount (unless owner mint), applies discount if set, generates token IDs (`tierId * 1_000_000_000 + mintCount`), ensures remaining supply covers pending reserves. |
|
|
48
|
+
| `recordAddTiers(tiers)` | `JB721TiersHookStore` | Adds new tiers sorted by category. Validates against hook flags (no new reserves/votes/owner-minting if flagged). Enforces max tier count (`type(uint16).max`), max supply per tier (`999_999_999`), discount percent bounds, non-zero supply, category sort order, and reserve+owner-mint mutual exclusion. |
|
|
49
|
+
| `recordRemoveTierIds(tierIds)` | `JB721TiersHookStore` | Marks tiers as removed in the bitmap. Validates tier is not locked (`cannotBeRemoved`). Does NOT update the sorted linked list -- call `cleanTiers()` afterward. |
|
|
50
|
+
| `recordMintReservesFor(tierId, count)` | `JB721TiersHookStore` | Mints reserve NFTs from remaining supply. Validates count does not exceed pending reserves. |
|
|
51
|
+
| `recordSetDiscountPercentOf(tierId, discountPercent)` | `JB721TiersHookStore` | Sets discount percent for a tier. Validates bounds (`<= DISCOUNT_DENOMINATOR`). If `cannotIncreaseDiscountPercent` is set, rejects increases. |
|
|
52
|
+
| `recordBurn(tokenIds)` | `JB721TiersHookStore` | Increments burn counter per tier. Trusts `msg.sender` (the hook) to have already verified ownership and burned the tokens. |
|
|
53
|
+
| `cleanTiers(hook)` | `JB721TiersHookStore` | Public. Removes stale entries from the sorted tier linked list after tiers have been removed via `recordRemoveTierIds`. Optimizes tier iteration. |
|
|
54
|
+
| `tiersOf(hook, categories, includeResolvedUri, startingId, size)` | `JB721TiersHookStore` | Returns an array of active tiers, optionally filtered by categories. Skips removed tiers. |
|
|
55
|
+
| `tierOf(hook, id, includeResolvedUri)` | `JB721TiersHookStore` | Returns a single tier by ID. |
|
|
56
|
+
| `tierOfTokenId(hook, tokenId, includeResolvedUri)` | `JB721TiersHookStore` | Returns the tier for a given token ID. |
|
|
57
|
+
| `totalSupplyOf(hook)` | `JB721TiersHookStore` | Returns total NFTs minted across all tiers (excluding burns). |
|
|
58
|
+
| `totalCashOutWeight(hook)` | `JB721TiersHookStore` | Returns total cash out weight (sum of `price * (minted + pendingReserves)` for all tiers). Uses original price, not discounted price. |
|
|
59
|
+
| `cashOutWeightOf(hook, tokenIds)` | `JB721TiersHookStore` | Returns combined cash out weight for specific token IDs. Uses original tier price, not discounted. |
|
|
60
|
+
| `votingUnitsOf(hook, account)` | `JB721TiersHookStore` | Returns total voting units for an address across all tiers. Uses custom `votingUnits` if `useVotingUnits` is set, otherwise uses tier price. |
|
|
61
|
+
| `tierVotingUnitsOf(hook, account, tierId)` | `JB721TiersHookStore` | Returns voting units for an address within a specific tier. |
|
|
62
|
+
| `calculateSplitAmounts(store, hook, metadataIdTarget, metadata)` | `JB721TiersHookLib` | Called in `beforePayRecordedWith`. Decodes tier IDs from payer metadata, looks up each tier's `splitPercent`, calculates `mulDiv(price, splitPercent, SPLITS_TOTAL_PERCENT)` per tier, returns `totalSplitAmount` (forwarded to hook as `amount`) and encoded `hookMetadata` (tier IDs + amounts). |
|
|
63
|
+
| `distributeAll(directory, projectId, hookAddress, token, encodedSplitData)` | `JB721TiersHookLib` | Called in `afterPayRecordedWith`. Decodes per-tier amounts, looks up each tier's splits from `JBSplits` by group ID (`hookAddress | (tierId << 160)`), distributes to split recipients. Leftover goes to project balance via `addToBalance`. |
|
|
64
|
+
| `adjustTiersFor(store, directory, projectId, hookAddress, caller, tiersToAdd, tierIdsToRemove)` | `JB721TiersHookLib` | Called via DELEGATECALL from `adjustTiers`. Removes tiers, adds tiers, emits events, and registers any configured splits in `JBSplits` via the project's controller. |
|
|
65
|
+
| `normalizePaymentValue(packedPricingContext, projectId, amountValue, amountCurrency, amountDecimals)` | `JB721TiersHookLib` | Converts a payment value to the hook's pricing currency using `JBPrices`. Returns `(0, false)` if currencies differ and no prices contract is set. |
|
|
66
|
+
| `resolveTokenURI(store, hook, baseUri, tokenId)` | `JB721TiersHookLib` | Resolves token URI: checks for custom `tokenUriResolver` first, otherwise decodes IPFS URI via `JBIpfsDecoder`. |
|
|
38
67
|
|
|
39
68
|
## Integration Points
|
|
40
69
|
|
|
41
70
|
| Dependency | Import | Used For |
|
|
42
71
|
|------------|--------|----------|
|
|
43
|
-
| `@bananapus/core-v6` | `IJBDirectory`, `IJBRulesets`, `IJBPrices`, `IJBController`, `IJBTerminal`, `JBRuleset`, `JBRulesetMetadata`, `JBAfterPayRecordedContext`, `JBBeforeCashOutRecordedContext`, etc. | Terminal validation, ruleset metadata, pricing, payment/cash-out contexts |
|
|
72
|
+
| `@bananapus/core-v6` | `IJBDirectory`, `IJBRulesets`, `IJBPrices`, `IJBController`, `IJBTerminal`, `IJBSplits`, `JBRuleset`, `JBRulesetMetadata`, `JBAfterPayRecordedContext`, `JBBeforeCashOutRecordedContext`, `JBSplit`, `JBSplitGroup`, `JBConstants`, etc. | Terminal validation, ruleset metadata, pricing, payment/cash-out contexts, splits |
|
|
44
73
|
| `@bananapus/ownable-v6` | `JBOwnable` | Project-based ownership for the hook (ownership can be transferred to a project NFT) |
|
|
45
74
|
| `@bananapus/permission-ids-v6` | `JBPermissionIds` | Permission IDs: `ADJUST_721_TIERS`, `MINT_721`, `SET_721_METADATA`, `SET_721_DISCOUNT_PERCENT`, `QUEUE_RULESETS`, `SET_TERMINALS` |
|
|
46
75
|
| `@bananapus/address-registry-v6` | `IJBAddressRegistry` | Registering deployed hook clones |
|
|
47
|
-
| `@openzeppelin/contracts` | `ERC2771Context`, `IERC165`, `IERC2981`, `IERC721` | Meta-transactions (trusted forwarder), interface detection, royalty standard |
|
|
48
|
-
| `@prb/math` | `mulDiv` | Safe fixed-point multiplication/division for price normalization |
|
|
76
|
+
| `@openzeppelin/contracts` | `ERC2771Context`, `IERC165`, `IERC2981`, `IERC721`, `SafeERC20` | Meta-transactions (trusted forwarder), interface detection, royalty standard declaration, safe ERC-20 transfers for split distribution |
|
|
77
|
+
| `@prb/math` | `mulDiv` | Safe fixed-point multiplication/division for price normalization and discount/split calculation |
|
|
49
78
|
| `solady` | `LibClone` | Minimal proxy (clone) deployment for hooks |
|
|
50
79
|
|
|
51
80
|
## Key Types
|
|
52
81
|
|
|
53
82
|
| Struct/Enum | Key Fields | Used In |
|
|
54
83
|
|-------------|------------|---------|
|
|
55
|
-
| `JB721TierConfig` | `uint104 price`, `uint32 initialSupply`, `uint32 votingUnits`, `uint16 reserveFrequency`, `address reserveBeneficiary`, `bytes32 encodedIPFSUri`, `uint24 category`, `uint8 discountPercent`, `bool allowOwnerMint`, `bool transfersPausable`, `bool cannotBeRemoved`, `bool cannotIncreaseDiscountPercent` | `adjustTiers`, `initialize`, `recordAddTiers` |
|
|
56
|
-
| `JB721Tier` |
|
|
57
|
-
| `JBStored721Tier` | `uint104 price`, `uint32 remainingSupply`, `uint32 initialSupply`, `uint32
|
|
58
|
-
| `JB721InitTiersConfig` | `JB721TierConfig[] tiers`, `uint32 currency`, `uint8 decimals`, `IJBPrices prices` | `initialize`
|
|
84
|
+
| `JB721TierConfig` | `uint104 price`, `uint32 initialSupply`, `uint32 votingUnits`, `uint16 reserveFrequency`, `address reserveBeneficiary`, `bytes32 encodedIPFSUri`, `uint24 category`, `uint8 discountPercent`, `bool allowOwnerMint`, `bool useReserveBeneficiaryAsDefault`, `bool transfersPausable`, `bool useVotingUnits`, `bool cannotBeRemoved`, `bool cannotIncreaseDiscountPercent`, `uint32 splitPercent`, `JBSplit[] splits` | `adjustTiers`, `initialize`, `recordAddTiers` |
|
|
85
|
+
| `JB721Tier` | `uint32 id`, `uint104 price`, `uint32 remainingSupply`, `uint32 initialSupply`, `uint104 votingUnits`, `uint16 reserveFrequency`, `address reserveBeneficiary`, `bytes32 encodedIPFSUri`, `uint24 category`, `uint8 discountPercent`, `bool allowOwnerMint`, `bool transfersPausable`, `bool cannotBeRemoved`, `bool cannotIncreaseDiscountPercent`, `uint32 splitPercent`, `string resolvedUri` | Return type from `tierOf`, `tiersOf`, `tierOfTokenId` |
|
|
86
|
+
| `JBStored721Tier` | `uint104 price`, `uint32 remainingSupply`, `uint32 initialSupply`, `uint32 splitPercent`, `uint24 category`, `uint8 discountPercent`, `uint16 reserveFrequency`, `uint8 packedBools` (allowOwnerMint, transfersPausable, useVotingUnits, cannotBeRemoved, cannotIncreaseDiscountPercent) | Internal storage in `JB721TiersHookStore`. Voting units stored separately in `_tierVotingUnitsOf` when `useVotingUnits` is true. |
|
|
87
|
+
| `JB721InitTiersConfig` | `JB721TierConfig[] tiers`, `uint32 currency`, `uint8 decimals`, `IJBPrices prices` | `initialize` -- defines tiers and pricing context |
|
|
59
88
|
| `JBDeploy721TiersHookConfig` | `string name`, `string symbol`, `string baseUri`, `IJB721TokenUriResolver tokenUriResolver`, `string contractUri`, `JB721InitTiersConfig tiersConfig`, `address reserveBeneficiary`, `JB721TiersHookFlags flags` | `deployHookFor`, `launchProjectFor` |
|
|
60
89
|
| `JB721TiersHookFlags` | `bool noNewTiersWithReserves`, `bool noNewTiersWithVotes`, `bool noNewTiersWithOwnerMinting`, `bool preventOverspending` | `initialize`, `recordFlags` |
|
|
61
|
-
| `JB721TiersRulesetMetadata` | `bool pauseTransfers`, `bool pauseMintPendingReserves` | Packed into `JBRulesetMetadata.metadata` per-ruleset |
|
|
62
|
-
| `JBPayDataHookRulesetConfig` | `uint48 mustStartAtOrAfter`, `uint32 duration`, `uint112 weight`, `uint32 weightCutPercent`, `IJBRulesetApprovalHook approvalHook`, `JBPayDataHookRulesetMetadata metadata`, `JBSplitGroup[] splitGroups`, `JBFundAccessLimitGroup[] fundAccessLimitGroups` | `JB721TiersHookProjectDeployer`
|
|
63
|
-
| `JBPayDataHookRulesetMetadata` | Same as `JBRulesetMetadata` minus `allowSetCustomToken` (hardcoded false) and `useDataHookForPay` (hardcoded true) and `dataHook` (set to deployed hook) | `launchProjectFor`, `launchRulesetsFor`, `queueRulesetsOf` |
|
|
90
|
+
| `JB721TiersRulesetMetadata` | `bool pauseTransfers`, `bool pauseMintPendingReserves` | Packed into `JBRulesetMetadata.metadata` per-ruleset (bit 0 = pauseTransfers, bit 1 = pauseMintPendingReserves) |
|
|
91
|
+
| `JBPayDataHookRulesetConfig` | `uint48 mustStartAtOrAfter`, `uint32 duration`, `uint112 weight`, `uint32 weightCutPercent`, `IJBRulesetApprovalHook approvalHook`, `JBPayDataHookRulesetMetadata metadata`, `JBSplitGroup[] splitGroups`, `JBFundAccessLimitGroup[] fundAccessLimitGroups` | `JB721TiersHookProjectDeployer` -- wraps core ruleset config with `useDataHookForPay: true` hardcoded |
|
|
92
|
+
| `JBPayDataHookRulesetMetadata` | Same as `JBRulesetMetadata` minus `allowSetCustomToken` (hardcoded false) and `useDataHookForPay` (hardcoded true) and `dataHook` (set to deployed hook). Includes `ownerMustSendPayouts`. | `launchProjectFor`, `launchRulesetsFor`, `queueRulesetsOf` |
|
|
93
|
+
| `JBLaunchProjectConfig` | `string projectUri`, `JBPayDataHookRulesetConfig[] rulesetConfigurations`, `JBTerminalConfig[] terminalConfigurations`, `string memo` | `launchProjectFor` |
|
|
94
|
+
| `JBLaunchRulesetsConfig` | `uint56 projectId`, `JBPayDataHookRulesetConfig[] rulesetConfigurations`, `JBTerminalConfig[] terminalConfigurations`, `string memo` | `launchRulesetsFor` |
|
|
95
|
+
| `JBQueueRulesetsConfig` | `uint56 projectId`, `JBPayDataHookRulesetConfig[] rulesetConfigurations`, `string memo` | `queueRulesetsOf` |
|
|
64
96
|
| `JB721TiersMintReservesConfig` | `uint32 tierId`, `uint16 count` | `mintPendingReservesFor` batch variant |
|
|
65
97
|
| `JB721TiersSetDiscountPercentConfig` | `uint32 tierId`, `uint16 discountPercent` | `setDiscountPercentsOf` batch variant |
|
|
66
98
|
| `JBBitmapWord` | `uint256 currentWord`, `uint256 currentDepth` | Internal tier removal tracking in store |
|
|
67
99
|
|
|
100
|
+
## Constants
|
|
101
|
+
|
|
102
|
+
| Constant | Value | Location | Meaning |
|
|
103
|
+
|----------|-------|----------|---------|
|
|
104
|
+
| `DISCOUNT_DENOMINATOR` | `200` | `JB721Constants` | Max `discountPercent` value. A `discountPercent` of 200 = 100% discount (free). A `discountPercent` of 100 = 50% off. Formula: `price -= mulDiv(price, discountPercent, 200)`. |
|
|
105
|
+
| `_ONE_BILLION` | `1_000_000_000` | `JB721TiersHookStore` | Used for token ID generation: `tokenId = tierId * 1_000_000_000 + tokenNumber`. Also caps max initial supply per tier at 999,999,999. |
|
|
106
|
+
| Max tier count | `type(uint16).max` (65,535) | `JB721TiersHookStore` | Maximum total number of tiers across all `recordAddTiers` calls for a single hook. |
|
|
107
|
+
|
|
108
|
+
## Discount Percent
|
|
109
|
+
|
|
110
|
+
Each tier has a `discountPercent` (uint8) that reduces its effective purchase price:
|
|
111
|
+
|
|
112
|
+
- The discount is applied during `recordMint`: `price -= mulDiv(price, discountPercent, DISCOUNT_DENOMINATOR)`.
|
|
113
|
+
- `DISCOUNT_DENOMINATOR` is 200, so `discountPercent = 100` means 50% off, `discountPercent = 200` means free.
|
|
114
|
+
- Discount can be changed via `setDiscountPercentOf` / `setDiscountPercentsOf` (requires `SET_721_DISCOUNT_PERCENT` permission).
|
|
115
|
+
- If `cannotIncreaseDiscountPercent` is set on the tier, the discount can only be decreased or kept the same -- increases are rejected by the store.
|
|
116
|
+
- Cash out weight always uses the **original tier price**, not the discounted price. This prevents discount changes from retroactively altering the cash-out value of already-minted NFTs.
|
|
117
|
+
|
|
118
|
+
## Voting Units
|
|
119
|
+
|
|
120
|
+
Each tier has configurable voting power:
|
|
121
|
+
|
|
122
|
+
- If `useVotingUnits` is `true` on the tier config, voting power per NFT is the custom `votingUnits` value (stored in `_tierVotingUnitsOf`).
|
|
123
|
+
- If `useVotingUnits` is `false`, voting power per NFT defaults to the tier's `price`.
|
|
124
|
+
- The `noNewTiersWithVotes` flag blocks adding new tiers with any voting power -- this means blocking tiers where `(useVotingUnits && votingUnits != 0)` OR `(!useVotingUnits && price != 0)`.
|
|
125
|
+
- Total voting units for an address are computed by `votingUnitsOf(hook, account)`, which sums `balance * votingPower` across all tiers.
|
|
126
|
+
|
|
127
|
+
## Reserve Minting
|
|
128
|
+
|
|
129
|
+
- Reserves accumulate as NFTs are purchased: for every `reserveFrequency` non-reserve mints, one reserve NFT becomes available.
|
|
130
|
+
- Pending count: `ceil(numberOfNonReserveMints / reserveFrequency) - numberOfReservesMintedFor`.
|
|
131
|
+
- Reserves are minted to the tier's `reserveBeneficiary` (or the hook's `defaultReserveBeneficiaryOf` as fallback).
|
|
132
|
+
- Reserve minting is permissionless (`mintPendingReservesFor`), but can be paused per-ruleset via `pauseMintPendingReserves` in `JB721TiersRulesetMetadata`.
|
|
133
|
+
- Supply is protected: `recordMint` ensures remaining supply covers pending reserves after each purchase.
|
|
134
|
+
- Tiers with `allowOwnerMint: true` cannot have a `reserveFrequency` -- the store rejects this combination.
|
|
135
|
+
|
|
136
|
+
## Tier Splits
|
|
137
|
+
|
|
138
|
+
- Each tier can route a percentage of its mint price to configured split recipients. The `splitPercent` field (out of `JBConstants.SPLITS_TOTAL_PERCENT` = 1,000,000,000) determines how much of the price is forwarded.
|
|
139
|
+
- Split recipients are stored in `JBSplits` using group IDs computed as `uint256(uint160(hookAddress)) | (uint256(tierId) << 160)`.
|
|
140
|
+
- Splits are registered via `JB721TiersHookLib._setSplitGroupsFor` when tiers with `splits` are added.
|
|
141
|
+
- In `beforePayRecordedWith`, `calculateSplitAmounts` decodes tier IDs from payer metadata, computes `mulDiv(price, splitPercent, SPLITS_TOTAL_PERCENT)` per tier, and returns the total to be forwarded to the hook.
|
|
142
|
+
- In `afterPayRecordedWith`, `distributeAll` distributes forwarded funds to each tier's split group recipients. Leftover after all splits goes back to the project's balance via `addToBalance`.
|
|
143
|
+
- Split recipients can be projects (via `terminal.pay` or `terminal.addToBalance`) or plain addresses (direct ETH transfer or `SafeERC20.safeTransfer`).
|
|
144
|
+
|
|
68
145
|
## Gotchas
|
|
69
146
|
|
|
70
147
|
- `JB721TiersHook` is deployed as a **minimal clone** (not a full deployment). The constructor sets immutables (`RULESETS`, `STORE`, `DIRECTORY`, `METADATA_ID_TARGET`), and `initialize()` sets per-instance state. Calling `initialize()` twice reverts with `JB721TiersHook_AlreadyInitialized`.
|
|
148
|
+
- **`JB721Hook` abstract base**: `JB721TiersHook` extends `JB721Hook`, which handles generic 721 hook lifecycle (terminal validation, burn loop, metadata decoding). `JB721TiersHook` overrides `cashOutWeightOf`, `totalCashOutWeight`, `_didBurn`, `_processPayment`, and `beforePayRecordedWith`. Errors like `JB721Hook_InvalidPay` and `JB721Hook_InvalidCashOut` are defined on the abstract class, not `JB721TiersHook`.
|
|
71
149
|
- **Pricing context is bit-packed** into a single `uint256`: currency (bits 0-31), decimals (bits 32-39), prices contract address (bits 40-199). Read it via `pricingContext()`.
|
|
150
|
+
- **Pricing decimals must be <= 18**: `initialize` reverts with `JB721TiersHook_InvalidPricingDecimals` otherwise.
|
|
72
151
|
- **Token IDs encode tier ID**: `tokenId = tierId * 1_000_000_000 + mintNumber`. Use `STORE.tierIdOfToken(tokenId)` to extract the tier ID.
|
|
73
152
|
- **Pay credits**: If a payer overpays (amount > total tier prices), the excess is stored as `payCreditsOf[beneficiary]` and can be applied to future mints. This only works when `preventOverspending` flag is `false`. Credits are only combined with payment when `payer == beneficiary`.
|
|
74
|
-
- **Cash outs reject fungible tokens**: `beforeCashOutRecordedWith` reverts with `
|
|
153
|
+
- **Cash outs reject fungible tokens**: `beforeCashOutRecordedWith` reverts with `JB721TiersHook_UnexpectedTokenCashedOut` if `context.cashOutCount > 0`. NFT cash outs and fungible token cash outs are mutually exclusive.
|
|
154
|
+
- **Cash out weight uses original price**: `cashOutWeightOf` and `totalCashOutWeight` use the full tier `price`, not the discounted price. This prevents discount changes from altering the cash-out value of already-minted NFTs.
|
|
155
|
+
- **Pending reserves inflate totalCashOutWeight**: `totalCashOutWeight` includes pending reserves in the denominator (`price * (minted + pendingReserves)`). This dilutes cash-out value before reserves are minted, preventing early cashers from extracting more than their fair share.
|
|
75
156
|
- **Reserve minting is permissionless** but governed by ruleset metadata. Anyone can call `mintPendingReservesFor` as long as `mintPendingReservesPaused` is not set in the current ruleset's metadata.
|
|
157
|
+
- **Reserve + owner-mint mutual exclusion**: Tiers with `allowOwnerMint: true` cannot have a `reserveFrequency`. The store rejects this combination during `recordAddTiers`.
|
|
76
158
|
- `setMetadata` uses `address(this)` as the sentinel for "no change" on `tokenUriResolver` (not `address(0)`). Passing `address(0)` will clear the resolver.
|
|
77
159
|
- `JBPayDataHookRulesetConfig` hardcodes `allowSetCustomToken: false` and `useDataHookForPay: true` when wiring rulesets through the project deployer.
|
|
78
160
|
- The `_update` override in `JB721TiersHook` checks `tier.transfersPausable` and consults the current ruleset's metadata for `transfersPaused`. Transfers to `address(0)` (burns) are never blocked.
|
|
161
|
+
- **IERC2981 declared but not implemented**: `supportsInterface` returns `true` for `IERC2981`, but no `royaltyInfo` function is implemented. Callers querying `royaltyInfo` will get a revert. This appears intentional -- the interface is declared for future extension or to signal capability to marketplaces that may override behavior.
|
|
162
|
+
- **Tier splits**: Each tier can route a percentage of its mint price to configured split recipients. `splitPercent` is out of `JBConstants.SPLITS_TOTAL_PERCENT` (1,000,000,000). Split group IDs are `uint256(uint160(hookAddress)) | (uint256(tierId) << 160)`.
|
|
163
|
+
- **Removing tiers does not update the sorted list**: `recordRemoveTierIds` only marks tiers in the bitmap. Call `cleanTiers()` afterward to remove them from the iteration sequence.
|
|
79
164
|
- `JB721TiersHookStore` is a **shared singleton** -- all hook instances on the same chain use the same store, keyed by `address(hook)`.
|
|
80
165
|
- The `ERC721` abstract uses `_initialize(name, symbol)` instead of a constructor, making it clone-compatible. The standard `_owners` mapping is `internal` (not `private`).
|
|
166
|
+
- **`hasMintPermissionFor` always returns `false`**: The hook never grants mint permission to any address. This is part of the `IJBRulesetDataHook` interface.
|
|
167
|
+
- **Max tier count is 65,535** (`type(uint16).max`). Adding tiers beyond this limit reverts.
|
|
168
|
+
- **Max initial supply per tier is 999,999,999** (`_ONE_BILLION - 1`). Exceeding this would cause token ID overflow into the next tier's ID space.
|
|
169
|
+
- **`noNewTiersWithVotes` blocks all voting power**: It rejects tiers where voting units would be non-zero, whether from custom `votingUnits` or from a non-zero `price` (when `useVotingUnits` is false).
|
|
170
|
+
- **`firstOwnerOf` is lazy**: The first owner is only stored when the token is first transferred away from its original holder. Before any transfer, `firstOwnerOf` returns the current owner.
|
|
81
171
|
|
|
82
172
|
## Example Integration
|
|
83
173
|
|
|
@@ -107,7 +197,9 @@ tiers[0] = JB721TierConfig({
|
|
|
107
197
|
transfersPausable: false,
|
|
108
198
|
useVotingUnits: false,
|
|
109
199
|
cannotBeRemoved: false,
|
|
110
|
-
cannotIncreaseDiscountPercent: false
|
|
200
|
+
cannotIncreaseDiscountPercent: false,
|
|
201
|
+
splitPercent: 0,
|
|
202
|
+
splits: new JBSplit[](0)
|
|
111
203
|
});
|
|
112
204
|
|
|
113
205
|
// Deploy a project with the 721 hook attached.
|
package/package.json
CHANGED
package/src/JB721TiersHook.sol
CHANGED
|
@@ -1,31 +1,23 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.26;
|
|
3
3
|
|
|
4
|
-
import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
|
|
5
4
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
6
|
-
import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
|
|
7
5
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
8
6
|
import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
|
|
9
7
|
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
10
8
|
import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
|
|
11
|
-
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
12
|
-
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
13
9
|
import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
14
10
|
import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
|
|
15
|
-
import {JBAfterCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterCashOutRecordedContext.sol";
|
|
16
11
|
import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
|
|
17
|
-
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|
|
18
12
|
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
19
|
-
import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
|
|
20
13
|
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
21
14
|
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
22
15
|
import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
|
|
23
16
|
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
24
|
-
import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol";
|
|
25
17
|
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
26
18
|
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
|
|
27
19
|
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
28
|
-
import {
|
|
20
|
+
import {JB721Hook} from "./abstract/JB721Hook.sol";
|
|
29
21
|
import {IJB721TiersHook} from "./interfaces/IJB721TiersHook.sol";
|
|
30
22
|
import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
|
|
31
23
|
import {IJB721TokenUriResolver} from "./interfaces/IJB721TokenUriResolver.sol";
|
|
@@ -43,33 +35,23 @@ import {JB721TiersMintReservesConfig} from "./structs/JB721TiersMintReservesConf
|
|
|
43
35
|
/// the project is paid, the hook may mint NFTs to the payer, depending on the hook's setup, the amount paid, and
|
|
44
36
|
/// information specified by the payer. The project's owner can enable NFT cash outs through this hook, allowing
|
|
45
37
|
/// holders to burn their NFTs to reclaim funds from the project (in proportion to the NFT's price).
|
|
46
|
-
contract JB721TiersHook is JBOwnable, ERC2771Context,
|
|
38
|
+
contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook {
|
|
47
39
|
//*********************************************************************//
|
|
48
40
|
// --------------------------- custom errors ------------------------- //
|
|
49
41
|
//*********************************************************************//
|
|
50
42
|
|
|
51
43
|
error JB721TiersHook_AlreadyInitialized(uint256 projectId);
|
|
52
44
|
error JB721TiersHook_CurrencyMismatch(uint256 paymentCurrency, uint256 tierCurrency);
|
|
53
|
-
error JB721TiersHook_InvalidCashOut();
|
|
54
|
-
error JB721TiersHook_InvalidPay();
|
|
55
45
|
error JB721TiersHook_InvalidPricingDecimals(uint256 decimals);
|
|
56
46
|
error JB721TiersHook_MintReserveNftsPaused();
|
|
57
47
|
error JB721TiersHook_NoProjectId();
|
|
58
48
|
error JB721TiersHook_Overspending(uint256 leftoverAmount);
|
|
59
49
|
error JB721TiersHook_TierTransfersPaused();
|
|
60
|
-
error JB721TiersHook_UnauthorizedToken(uint256 tokenId, address holder);
|
|
61
|
-
error JB721TiersHook_UnexpectedTokenCashedOut();
|
|
62
50
|
|
|
63
51
|
//*********************************************************************//
|
|
64
52
|
// --------------- public immutable stored properties ---------------- //
|
|
65
53
|
//*********************************************************************//
|
|
66
54
|
|
|
67
|
-
/// @notice The directory of terminals and controllers for projects.
|
|
68
|
-
IJBDirectory public immutable override DIRECTORY;
|
|
69
|
-
|
|
70
|
-
/// @notice The ID used when parsing metadata.
|
|
71
|
-
address public immutable override METADATA_ID_TARGET;
|
|
72
|
-
|
|
73
55
|
/// @notice The contract storing and managing project rulesets.
|
|
74
56
|
IJBRulesets public immutable override RULESETS;
|
|
75
57
|
|
|
@@ -80,9 +62,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
|
|
|
80
62
|
// ---------------------- public stored properties ------------------- //
|
|
81
63
|
//*********************************************************************//
|
|
82
64
|
|
|
83
|
-
/// @notice The ID of the project that this contract is associated with.
|
|
84
|
-
uint256 public override PROJECT_ID;
|
|
85
|
-
|
|
86
65
|
/// @notice The base URI for the NFT `tokenUris`.
|
|
87
66
|
string public override baseURI;
|
|
88
67
|
|
|
@@ -127,10 +106,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
|
|
|
127
106
|
address trustedForwarder
|
|
128
107
|
)
|
|
129
108
|
JBOwnable(permissions, directory.PROJECTS(), msg.sender, uint88(0))
|
|
109
|
+
JB721Hook(directory)
|
|
130
110
|
ERC2771Context(trustedForwarder)
|
|
131
111
|
{
|
|
132
|
-
DIRECTORY = directory;
|
|
133
|
-
METADATA_ID_TARGET = address(this);
|
|
134
112
|
RULESETS = rulesets;
|
|
135
113
|
STORE = store;
|
|
136
114
|
}
|
|
@@ -139,61 +117,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
|
|
|
139
117
|
// ------------------------- external views -------------------------- //
|
|
140
118
|
//*********************************************************************//
|
|
141
119
|
|
|
142
|
-
/// @notice The data calculated before a cash out is recorded in the terminal store. This data is provided to the
|
|
143
|
-
/// terminal's `cashOutTokensOf(...)` transaction.
|
|
144
|
-
/// @dev Sets this contract as the cash out hook. Part of `IJBRulesetDataHook`.
|
|
145
|
-
/// @dev This function is used for NFT cash outs, and will only be called if the project's ruleset has
|
|
146
|
-
/// `useDataHookForCashOut` set to `true`.
|
|
147
|
-
/// @param context The cash out context passed to this contract by the `cashOutTokensOf(...)` function.
|
|
148
|
-
/// @return cashOutTaxRate The cash out tax rate influencing the reclaim amount.
|
|
149
|
-
/// @return cashOutCount The amount of tokens that should be considered cashed out.
|
|
150
|
-
/// @return totalSupply The total amount of tokens that are considered to be existing.
|
|
151
|
-
/// @return hookSpecifications The amount and data to send to cash out hooks (this contract) instead of returning to
|
|
152
|
-
/// the beneficiary.
|
|
153
|
-
function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
|
|
154
|
-
public
|
|
155
|
-
view
|
|
156
|
-
virtual
|
|
157
|
-
override
|
|
158
|
-
returns (
|
|
159
|
-
uint256 cashOutTaxRate,
|
|
160
|
-
uint256 cashOutCount,
|
|
161
|
-
uint256 totalSupply,
|
|
162
|
-
JBCashOutHookSpecification[] memory hookSpecifications
|
|
163
|
-
)
|
|
164
|
-
{
|
|
165
|
-
// Make sure (fungible) project tokens aren't also being cashed out.
|
|
166
|
-
if (context.cashOutCount > 0) revert JB721TiersHook_UnexpectedTokenCashedOut();
|
|
167
|
-
|
|
168
|
-
// Fetch the cash out hook metadata using the corresponding metadata ID.
|
|
169
|
-
(bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
|
|
170
|
-
id: JBMetadataResolver.getId({purpose: "cashOut", target: METADATA_ID_TARGET}), metadata: context.metadata
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// Use this contract as the only cash out hook.
|
|
174
|
-
hookSpecifications = new JBCashOutHookSpecification[](1);
|
|
175
|
-
hookSpecifications[0] = JBCashOutHookSpecification({hook: this, amount: 0, metadata: bytes("")});
|
|
176
|
-
|
|
177
|
-
uint256[] memory decodedTokenIds;
|
|
178
|
-
|
|
179
|
-
// Decode the metadata.
|
|
180
|
-
if (metadataExists) decodedTokenIds = abi.decode(metadata, (uint256[]));
|
|
181
|
-
|
|
182
|
-
// Use the cash out weight of the provided 721s.
|
|
183
|
-
cashOutCount = STORE.cashOutWeightOf({hook: address(this), tokenIds: decodedTokenIds});
|
|
184
|
-
|
|
185
|
-
// Use the total cash out weight of the 721s.
|
|
186
|
-
totalSupply = STORE.totalCashOutWeight(address(this));
|
|
187
|
-
|
|
188
|
-
// Use the cash out tax rate from the context.
|
|
189
|
-
cashOutTaxRate = context.cashOutTaxRate;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/// @notice Required by the IJBRulesetDataHook interfaces. Return false to not leak any permissions.
|
|
193
|
-
function hasMintPermissionFor(uint256, JBRuleset memory, address) external pure returns (bool) {
|
|
194
|
-
return false;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
120
|
/// @notice The first owner of an NFT.
|
|
198
121
|
/// @dev This is generally the address which paid for the NFT.
|
|
199
122
|
/// @param tokenId The token ID of the NFT to get the first owner of.
|
|
@@ -236,18 +159,16 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
|
|
|
236
159
|
return STORE.balanceOf({hook: address(this), owner: owner});
|
|
237
160
|
}
|
|
238
161
|
|
|
239
|
-
/// @notice The data calculated before a payment is recorded in the terminal store.
|
|
240
|
-
///
|
|
241
|
-
/// @
|
|
242
|
-
/// @
|
|
243
|
-
/// @return
|
|
244
|
-
/// @return hookSpecifications The amount and data to send to pay hooks (this contract) instead of adding to the
|
|
245
|
-
/// terminal's balance.
|
|
162
|
+
/// @notice The data calculated before a payment is recorded in the terminal store.
|
|
163
|
+
/// @dev Overrides the base to calculate the split amount to forward based on tier split percentages.
|
|
164
|
+
/// @param context The payment context.
|
|
165
|
+
/// @return weight The weight to use for token minting (unchanged from ruleset weight).
|
|
166
|
+
/// @return hookSpecifications The hook specifications, with the split amount to forward.
|
|
246
167
|
function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
|
|
247
168
|
public
|
|
248
169
|
view
|
|
249
170
|
virtual
|
|
250
|
-
override
|
|
171
|
+
override(JB721Hook, IJBRulesetDataHook)
|
|
251
172
|
returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
|
|
252
173
|
{
|
|
253
174
|
weight = context.weight;
|
|
@@ -260,13 +181,20 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
|
|
|
260
181
|
hookSpecifications[0] = JBPayHookSpecification({hook: this, amount: totalSplitAmount, metadata: splitMetadata});
|
|
261
182
|
}
|
|
262
183
|
|
|
184
|
+
/// @notice The combined cash out weight of the NFTs with the specified token IDs.
|
|
185
|
+
/// @dev An NFT's cash out weight is its price.
|
|
186
|
+
/// @dev To get their relative cash out weight, divide the result by the `totalCashOutWeight(...)`.
|
|
187
|
+
/// @param tokenIds The token IDs of the NFTs to get the cumulative cash out weight of.
|
|
188
|
+
/// @return weight The cash out weight of the tokenIds.
|
|
189
|
+
function cashOutWeightOf(uint256[] memory tokenIds) public view virtual override returns (uint256) {
|
|
190
|
+
return STORE.cashOutWeightOf({hook: address(this), tokenIds: tokenIds});
|
|
191
|
+
}
|
|
192
|
+
|
|
263
193
|
/// @notice Indicates if this contract adheres to the specified interface.
|
|
264
194
|
/// @dev See {IERC165-supportsInterface}.
|
|
265
195
|
/// @param interfaceId The ID of the interface to check for adherence to.
|
|
266
|
-
function supportsInterface(bytes4 interfaceId) public view
|
|
267
|
-
return interfaceId == type(IJB721TiersHook).interfaceId || interfaceId
|
|
268
|
-
|| interfaceId == type(IJBPayHook).interfaceId || interfaceId == type(IJBCashOutHook).interfaceId
|
|
269
|
-
|| interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId);
|
|
196
|
+
function supportsInterface(bytes4 interfaceId) public view override(IERC165, JB721Hook) returns (bool) {
|
|
197
|
+
return interfaceId == type(IJB721TiersHook).interfaceId || JB721Hook.supportsInterface(interfaceId);
|
|
270
198
|
}
|
|
271
199
|
|
|
272
200
|
/// @notice Initializes a cloned copy of the original hook contract.
|
|
@@ -298,9 +226,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
|
|
|
298
226
|
// Make sure a projectId is provided.
|
|
299
227
|
if (projectId == 0) revert JB721TiersHook_NoProjectId();
|
|
300
228
|
|
|
301
|
-
// Initialize
|
|
302
|
-
|
|
303
|
-
PROJECT_ID = projectId;
|
|
229
|
+
// Initialize the superclass.
|
|
230
|
+
JB721Hook._initialize({projectId: projectId, name: name, symbol: symbol});
|
|
304
231
|
|
|
305
232
|
// Validate pricing decimals are within a reasonable range.
|
|
306
233
|
if (tiersConfig.decimals > 18) revert JB721TiersHook_InvalidPricingDecimals(tiersConfig.decimals);
|
|
@@ -352,76 +279,17 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
|
|
|
352
279
|
return JB721TiersHookLib.resolveTokenURI(STORE, address(this), baseURI, tokenId);
|
|
353
280
|
}
|
|
354
281
|
|
|
282
|
+
/// @notice The combined cash out weight of all outstanding NFTs.
|
|
283
|
+
/// @dev An NFT's cash out weight is its price.
|
|
284
|
+
/// @return weight The total cash out weight.
|
|
285
|
+
function totalCashOutWeight() public view virtual override returns (uint256) {
|
|
286
|
+
return STORE.totalCashOutWeight(address(this));
|
|
287
|
+
}
|
|
288
|
+
|
|
355
289
|
//*********************************************************************//
|
|
356
290
|
// ---------------------- external transactions ---------------------- //
|
|
357
291
|
//*********************************************************************//
|
|
358
292
|
|
|
359
|
-
/// @notice Mints one or more NFTs to the `context.beneficiary` upon payment if conditions are met. Part of
|
|
360
|
-
/// `IJBPayHook`.
|
|
361
|
-
/// @dev Reverts if the calling contract is not one of the project's terminals.
|
|
362
|
-
/// @param context The payment context passed in by the terminal.
|
|
363
|
-
// slither-disable-next-line locked-ether
|
|
364
|
-
function afterPayRecordedWith(JBAfterPayRecordedContext calldata context) external payable virtual override {
|
|
365
|
-
uint256 projectId = PROJECT_ID;
|
|
366
|
-
|
|
367
|
-
// Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
|
|
368
|
-
// interaction with the correct project.
|
|
369
|
-
if (!DIRECTORY.isTerminalOf(projectId, IJBTerminal(msg.sender)) || context.projectId != projectId) {
|
|
370
|
-
revert JB721TiersHook_InvalidPay();
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Process the payment.
|
|
374
|
-
_processPayment(context);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/// @notice Burns the specified NFTs upon token holder cash out, reclaiming funds from the project's balance for
|
|
378
|
-
/// `context.beneficiary`. Part of `IJBCashOutHook`.
|
|
379
|
-
/// @dev Reverts if the calling contract is not one of the project's terminals.
|
|
380
|
-
/// @param context The cash out context passed in by the terminal.
|
|
381
|
-
// slither-disable-next-line locked-ether
|
|
382
|
-
function afterCashOutRecordedWith(JBAfterCashOutRecordedContext calldata context)
|
|
383
|
-
external
|
|
384
|
-
payable
|
|
385
|
-
virtual
|
|
386
|
-
override
|
|
387
|
-
{
|
|
388
|
-
// Keep a reference to the project ID.
|
|
389
|
-
uint256 projectId = PROJECT_ID;
|
|
390
|
-
|
|
391
|
-
// Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
|
|
392
|
-
// interaction with the correct project.
|
|
393
|
-
if (
|
|
394
|
-
msg.value != 0 || !DIRECTORY.isTerminalOf({projectId: projectId, terminal: IJBTerminal(msg.sender)})
|
|
395
|
-
|| context.projectId != projectId
|
|
396
|
-
) revert JB721TiersHook_InvalidCashOut();
|
|
397
|
-
|
|
398
|
-
// Fetch the cash out hook metadata using the corresponding metadata ID.
|
|
399
|
-
(bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
|
|
400
|
-
id: JBMetadataResolver.getId({purpose: "cashOut", target: METADATA_ID_TARGET}),
|
|
401
|
-
metadata: context.cashOutMetadata
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
uint256[] memory decodedTokenIds;
|
|
405
|
-
|
|
406
|
-
// Decode the metadata.
|
|
407
|
-
if (metadataExists) decodedTokenIds = abi.decode(metadata, (uint256[]));
|
|
408
|
-
|
|
409
|
-
// Iterate through the NFTs, burning them if the owner is correct.
|
|
410
|
-
for (uint256 i; i < decodedTokenIds.length; i++) {
|
|
411
|
-
// Set the current NFT's token ID.
|
|
412
|
-
uint256 tokenId = decodedTokenIds[i];
|
|
413
|
-
|
|
414
|
-
// Make sure the token's owner is correct.
|
|
415
|
-
if (_ownerOf(tokenId) != context.holder) revert JB721TiersHook_UnauthorizedToken(tokenId, context.holder);
|
|
416
|
-
|
|
417
|
-
// Burn the token.
|
|
418
|
-
_burn(tokenId);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Add to burned counter.
|
|
422
|
-
STORE.recordBurn(decodedTokenIds);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
293
|
/// @notice Add or delete tiers.
|
|
426
294
|
/// @dev Only the contract's owner or an operator with the `ADJUST_TIERS` permission from the owner can adjust the
|
|
427
295
|
/// tiers.
|
|
@@ -638,6 +506,13 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
|
|
|
638
506
|
// ------------------------ internal functions ----------------------- //
|
|
639
507
|
//*********************************************************************//
|
|
640
508
|
|
|
509
|
+
/// @notice A function which gets called after NFTs have been cashed out and recorded by the terminal.
|
|
510
|
+
/// @param tokenIds The token IDs of the NFTs that were burned.
|
|
511
|
+
function _didBurn(uint256[] memory tokenIds) internal virtual override {
|
|
512
|
+
// Add to burned counter.
|
|
513
|
+
STORE.recordBurn(tokenIds);
|
|
514
|
+
}
|
|
515
|
+
|
|
641
516
|
/// @notice Mints one NFT from each of the specified tiers for the beneficiary.
|
|
642
517
|
/// @dev The same tier can be specified more than once.
|
|
643
518
|
/// @param amount The amount to base the mints on. The total price of the NFTs being minted cannot be larger than
|
|
@@ -687,7 +562,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
|
|
|
687
562
|
/// the payer's existing credits are NOT applied to the mint. Only the beneficiary's credits are combined with
|
|
688
563
|
/// the incoming payment value. Leftover funds after minting are stored as credits for the beneficiary.
|
|
689
564
|
/// @param context Payment context provided by the terminal after it has recorded the payment in the terminal store.
|
|
690
|
-
function _processPayment(JBAfterPayRecordedContext calldata context) internal virtual {
|
|
565
|
+
function _processPayment(JBAfterPayRecordedContext calldata context) internal virtual override {
|
|
691
566
|
// Normalize the payment value based on the pricing context.
|
|
692
567
|
uint256 value;
|
|
693
568
|
{
|
|
@@ -711,9 +586,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
|
|
|
711
586
|
// If the payer is the beneficiary, combine their NFT credits with the amount paid.
|
|
712
587
|
uint256 unusedPayCredits;
|
|
713
588
|
if (context.payer == context.beneficiary) {
|
|
714
|
-
|
|
715
|
-
leftoverAmount += payCredits;
|
|
716
|
-
}
|
|
589
|
+
leftoverAmount += payCredits;
|
|
717
590
|
} else {
|
|
718
591
|
// Otherwise, the payer's NFT credits won't be used, and we keep track of the unused credits.
|
|
719
592
|
unusedPayCredits = payCredits;
|
|
@@ -755,28 +628,26 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
|
|
|
755
628
|
if (leftoverAmount != 0 && !allowOverspending) revert JB721TiersHook_Overspending(leftoverAmount);
|
|
756
629
|
|
|
757
630
|
// Update NFT credits if they changed.
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
if (newPayCredits
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
payCreditsOf[context.beneficiary] = newPayCredits;
|
|
631
|
+
uint256 newPayCredits = leftoverAmount + unusedPayCredits;
|
|
632
|
+
|
|
633
|
+
if (newPayCredits != payCredits) {
|
|
634
|
+
if (newPayCredits > payCredits) {
|
|
635
|
+
emit AddPayCredits({
|
|
636
|
+
amount: newPayCredits - payCredits,
|
|
637
|
+
newTotalCredits: newPayCredits,
|
|
638
|
+
account: context.beneficiary,
|
|
639
|
+
caller: _msgSender()
|
|
640
|
+
});
|
|
641
|
+
} else {
|
|
642
|
+
emit UsePayCredits({
|
|
643
|
+
amount: payCredits - newPayCredits,
|
|
644
|
+
newTotalCredits: newPayCredits,
|
|
645
|
+
account: context.beneficiary,
|
|
646
|
+
caller: _msgSender()
|
|
647
|
+
});
|
|
779
648
|
}
|
|
649
|
+
|
|
650
|
+
payCreditsOf[context.beneficiary] = newPayCredits;
|
|
780
651
|
}
|
|
781
652
|
|
|
782
653
|
// Distribute any forwarded funds to tier split groups.
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
|
|
5
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
6
|
+
import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
|
|
7
|
+
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
8
|
+
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
9
|
+
import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
10
|
+
import {JBAfterCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterCashOutRecordedContext.sol";
|
|
11
|
+
import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
|
|
12
|
+
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|
|
13
|
+
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
14
|
+
import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
|
|
15
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
16
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
17
|
+
import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol";
|
|
18
|
+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
19
|
+
|
|
20
|
+
import {ERC721} from "./ERC721.sol";
|
|
21
|
+
import {IJB721Hook} from "../interfaces/IJB721Hook.sol";
|
|
22
|
+
|
|
23
|
+
/// @title JB721Hook
|
|
24
|
+
/// @notice When a project which uses this hook is paid, this hook may mint NFTs to the payer, depending on this hook's
|
|
25
|
+
/// setup, the amount paid, and information specified by the payer. The project's owner can enable NFT cash outs
|
|
26
|
+
/// through this hook, allowing the NFT holders to burn their NFTs to reclaim funds from the project (in proportion to
|
|
27
|
+
/// the NFT's price).
|
|
28
|
+
abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
29
|
+
//*********************************************************************//
|
|
30
|
+
// --------------------------- custom errors ------------------------- //
|
|
31
|
+
//*********************************************************************//
|
|
32
|
+
|
|
33
|
+
error JB721Hook_InvalidCashOut();
|
|
34
|
+
error JB721Hook_InvalidPay();
|
|
35
|
+
error JB721Hook_UnauthorizedToken(uint256 tokenId, address holder);
|
|
36
|
+
error JB721Hook_UnexpectedTokenCashedOut();
|
|
37
|
+
|
|
38
|
+
//*********************************************************************//
|
|
39
|
+
// --------------- public immutable stored properties ---------------- //
|
|
40
|
+
//*********************************************************************//
|
|
41
|
+
|
|
42
|
+
/// @notice The directory of terminals and controllers for projects.
|
|
43
|
+
IJBDirectory public immutable override DIRECTORY;
|
|
44
|
+
|
|
45
|
+
/// @notice The ID used when parsing metadata.
|
|
46
|
+
address public immutable override METADATA_ID_TARGET;
|
|
47
|
+
|
|
48
|
+
//*********************************************************************//
|
|
49
|
+
// -------------------- public stored properties --------------------- //
|
|
50
|
+
//*********************************************************************//
|
|
51
|
+
|
|
52
|
+
/// @notice The ID of the project that this contract is associated with.
|
|
53
|
+
uint256 public override PROJECT_ID;
|
|
54
|
+
|
|
55
|
+
//*********************************************************************//
|
|
56
|
+
// -------------------------- constructor ---------------------------- //
|
|
57
|
+
//*********************************************************************//
|
|
58
|
+
|
|
59
|
+
/// @param directory A directory of terminals and controllers for projects.
|
|
60
|
+
constructor(IJBDirectory directory) {
|
|
61
|
+
DIRECTORY = directory;
|
|
62
|
+
// Store the address of the original hook deploy. Clones will each use the address of the instance they're based
|
|
63
|
+
// on.
|
|
64
|
+
METADATA_ID_TARGET = address(this);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//*********************************************************************//
|
|
68
|
+
// ------------------------- external views -------------------------- //
|
|
69
|
+
//*********************************************************************//
|
|
70
|
+
|
|
71
|
+
/// @notice The data calculated before a payment is recorded in the terminal store. This data is provided to the
|
|
72
|
+
/// terminal's `pay(...)` transaction.
|
|
73
|
+
/// @dev Sets this contract as the pay hook. Part of `IJBRulesetDataHook`.
|
|
74
|
+
/// @param context The payment context passed to this contract by the `pay(...)` function.
|
|
75
|
+
/// @return weight The new `weight` to use, overriding the ruleset's `weight`.
|
|
76
|
+
/// @return hookSpecifications The amount and data to send to pay hooks (this contract) instead of adding to the
|
|
77
|
+
/// terminal's balance.
|
|
78
|
+
function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
|
|
79
|
+
public
|
|
80
|
+
view
|
|
81
|
+
virtual
|
|
82
|
+
override
|
|
83
|
+
returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
|
|
84
|
+
{
|
|
85
|
+
// Forward the received weight and use this contract as the only pay hook.
|
|
86
|
+
weight = context.weight;
|
|
87
|
+
hookSpecifications = new JBPayHookSpecification[](1);
|
|
88
|
+
hookSpecifications[0] = JBPayHookSpecification({hook: this, amount: 0, metadata: bytes("")});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// @notice The data calculated before a cash out is recorded in the terminal store. This data is provided to the
|
|
92
|
+
/// terminal's `cashOutTokensOf(...)` transaction.
|
|
93
|
+
/// @dev Sets this contract as the cash out hook. Part of `IJBRulesetDataHook`.
|
|
94
|
+
/// @dev This function is used for NFT cash outs, and will only be called if the project's ruleset has
|
|
95
|
+
/// `useDataHookForCashOut` set to `true`.
|
|
96
|
+
/// @param context The cash out context passed to this contract by the `cashOutTokensOf(...)` function.
|
|
97
|
+
/// @return cashOutTaxRate The cash out tax rate influencing the reclaim amount.
|
|
98
|
+
/// @return cashOutCount The amount of tokens that should be considered cashed out.
|
|
99
|
+
/// @return totalSupply The total amount of tokens that are considered to be existing.
|
|
100
|
+
/// @return hookSpecifications The amount and data to send to cash out hooks (this contract) instead of returning to
|
|
101
|
+
/// the beneficiary.
|
|
102
|
+
function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
|
|
103
|
+
public
|
|
104
|
+
view
|
|
105
|
+
virtual
|
|
106
|
+
override
|
|
107
|
+
returns (
|
|
108
|
+
uint256 cashOutTaxRate,
|
|
109
|
+
uint256 cashOutCount,
|
|
110
|
+
uint256 totalSupply,
|
|
111
|
+
JBCashOutHookSpecification[] memory hookSpecifications
|
|
112
|
+
)
|
|
113
|
+
{
|
|
114
|
+
// Make sure (fungible) project tokens aren't also being cashed out.
|
|
115
|
+
if (context.cashOutCount > 0) revert JB721Hook_UnexpectedTokenCashedOut();
|
|
116
|
+
|
|
117
|
+
// Fetch the cash out hook metadata using the corresponding metadata ID.
|
|
118
|
+
(bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
|
|
119
|
+
id: JBMetadataResolver.getId({purpose: "cashOut", target: METADATA_ID_TARGET}), metadata: context.metadata
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Use this contract as the only cash out hook.
|
|
123
|
+
hookSpecifications = new JBCashOutHookSpecification[](1);
|
|
124
|
+
hookSpecifications[0] = JBCashOutHookSpecification({hook: this, amount: 0, metadata: bytes("")});
|
|
125
|
+
|
|
126
|
+
uint256[] memory decodedTokenIds;
|
|
127
|
+
|
|
128
|
+
// Decode the metadata.
|
|
129
|
+
if (metadataExists) decodedTokenIds = abi.decode(metadata, (uint256[]));
|
|
130
|
+
|
|
131
|
+
// Use the cash out weight of the provided 721s.
|
|
132
|
+
cashOutCount = cashOutWeightOf(decodedTokenIds);
|
|
133
|
+
|
|
134
|
+
// Use the total cash out weight of the 721s.
|
|
135
|
+
totalSupply = totalCashOutWeight();
|
|
136
|
+
|
|
137
|
+
// Use the cash out tax rate from the context.
|
|
138
|
+
cashOutTaxRate = context.cashOutTaxRate;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// @notice Required by the IJBRulesetDataHook interfaces. Return false to not leak any permissions.
|
|
142
|
+
function hasMintPermissionFor(uint256, JBRuleset memory, address) external pure returns (bool) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
//*********************************************************************//
|
|
147
|
+
// -------------------------- public views --------------------------- //
|
|
148
|
+
//*********************************************************************//
|
|
149
|
+
|
|
150
|
+
/// @notice Returns the cumulative cash out weight of the specified token IDs relative to the
|
|
151
|
+
/// `totalCashOutWeight`.
|
|
152
|
+
/// @param tokenIds The NFT token IDs to calculate the cumulative cash out weight of.
|
|
153
|
+
/// @return The cumulative cash out weight of the specified token IDs.
|
|
154
|
+
function cashOutWeightOf(uint256[] memory tokenIds) public view virtual returns (uint256) {
|
|
155
|
+
tokenIds; // Prevents unused var compiler and natspec complaints.
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// @notice Indicates if this contract adheres to the specified interface.
|
|
160
|
+
/// @dev See {IERC165-supportsInterface}.
|
|
161
|
+
/// @param interfaceId The ID of the interface to check for adherence to.
|
|
162
|
+
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, IERC165) returns (bool) {
|
|
163
|
+
return interfaceId == type(IJB721Hook).interfaceId || interfaceId == type(IJBRulesetDataHook).interfaceId
|
|
164
|
+
|| interfaceId == type(IJBPayHook).interfaceId || interfaceId == type(IJBCashOutHook).interfaceId
|
|
165
|
+
|| interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// @notice Calculates the cumulative cash out weight of all NFT token IDs.
|
|
169
|
+
/// @return The total cumulative cash out weight of all NFT token IDs.
|
|
170
|
+
function totalCashOutWeight() public view virtual returns (uint256) {
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//*********************************************************************//
|
|
175
|
+
// ---------------------- external transactions ---------------------- //
|
|
176
|
+
//*********************************************************************//
|
|
177
|
+
|
|
178
|
+
/// @notice Mints one or more NFTs to the `context.beneficiary` upon payment if conditions are met. Part of
|
|
179
|
+
/// `IJBPayHook`.
|
|
180
|
+
/// @dev Reverts if the calling contract is not one of the project's terminals.
|
|
181
|
+
/// @param context The payment context passed in by the terminal.
|
|
182
|
+
// slither-disable-next-line locked-ether
|
|
183
|
+
function afterPayRecordedWith(JBAfterPayRecordedContext calldata context) external payable virtual override {
|
|
184
|
+
uint256 projectId = PROJECT_ID;
|
|
185
|
+
|
|
186
|
+
// Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
|
|
187
|
+
// interaction with the correct project.
|
|
188
|
+
if (!DIRECTORY.isTerminalOf(projectId, IJBTerminal(msg.sender)) || context.projectId != projectId) {
|
|
189
|
+
revert JB721Hook_InvalidPay();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Process the payment.
|
|
193
|
+
_processPayment(context);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// @notice Burns the specified NFTs upon token holder cash out, reclaiming funds from the project's balance for
|
|
197
|
+
/// `context.beneficiary`. Part of `IJBCashOutHook`.
|
|
198
|
+
/// @dev Reverts if the calling contract is not one of the project's terminals.
|
|
199
|
+
/// @param context The cash out context passed in by the terminal.
|
|
200
|
+
// slither-disable-next-line locked-ether
|
|
201
|
+
function afterCashOutRecordedWith(JBAfterCashOutRecordedContext calldata context)
|
|
202
|
+
external
|
|
203
|
+
payable
|
|
204
|
+
virtual
|
|
205
|
+
override
|
|
206
|
+
{
|
|
207
|
+
// Keep a reference to the project ID.
|
|
208
|
+
uint256 projectId = PROJECT_ID;
|
|
209
|
+
|
|
210
|
+
// Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
|
|
211
|
+
// interaction with the correct project.
|
|
212
|
+
if (
|
|
213
|
+
msg.value != 0 || !DIRECTORY.isTerminalOf({projectId: projectId, terminal: IJBTerminal(msg.sender)})
|
|
214
|
+
|| context.projectId != projectId
|
|
215
|
+
) revert JB721Hook_InvalidCashOut();
|
|
216
|
+
|
|
217
|
+
// Fetch the cash out hook metadata using the corresponding metadata ID.
|
|
218
|
+
(bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
|
|
219
|
+
id: JBMetadataResolver.getId({purpose: "cashOut", target: METADATA_ID_TARGET}),
|
|
220
|
+
metadata: context.cashOutMetadata
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
uint256[] memory decodedTokenIds;
|
|
224
|
+
|
|
225
|
+
// Decode the metadata.
|
|
226
|
+
if (metadataExists) decodedTokenIds = abi.decode(metadata, (uint256[]));
|
|
227
|
+
|
|
228
|
+
// Iterate through the NFTs, burning them if the owner is correct.
|
|
229
|
+
for (uint256 i; i < decodedTokenIds.length; i++) {
|
|
230
|
+
// Set the current NFT's token ID.
|
|
231
|
+
uint256 tokenId = decodedTokenIds[i];
|
|
232
|
+
|
|
233
|
+
// Make sure the token's owner is correct.
|
|
234
|
+
if (_ownerOf(tokenId) != context.holder) revert JB721Hook_UnauthorizedToken(tokenId, context.holder);
|
|
235
|
+
|
|
236
|
+
// Burn the token.
|
|
237
|
+
_burn(tokenId);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Call the hook.
|
|
241
|
+
_didBurn(decodedTokenIds);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
//*********************************************************************//
|
|
245
|
+
// ---------------------- internal transactions ---------------------- //
|
|
246
|
+
//*********************************************************************//
|
|
247
|
+
|
|
248
|
+
/// @notice Initializes the contract by associating it with a project and adding ERC721 details.
|
|
249
|
+
/// @param projectId The ID of the project that this contract is associated with.
|
|
250
|
+
/// @param name The name of the NFT collection.
|
|
251
|
+
/// @param symbol The symbol representing the NFT collection.
|
|
252
|
+
function _initialize(uint256 projectId, string memory name, string memory symbol) internal {
|
|
253
|
+
ERC721._initialize({name_: name, symbol_: symbol});
|
|
254
|
+
PROJECT_ID = projectId;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// @notice Executes after NFTs have been burned via cash out.
|
|
258
|
+
/// @param tokenIds The token IDs of the NFTs that were burned.
|
|
259
|
+
function _didBurn(uint256[] memory tokenIds) internal virtual;
|
|
260
|
+
|
|
261
|
+
/// @notice Process a received payment.
|
|
262
|
+
/// @param context The payment context passed in by the terminal.
|
|
263
|
+
function _processPayment(JBAfterPayRecordedContext calldata context) internal virtual;
|
|
264
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
|
|
5
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
6
|
+
import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
|
|
7
|
+
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
8
|
+
|
|
9
|
+
interface IJB721Hook is IJBRulesetDataHook, IJBPayHook, IJBCashOutHook {
|
|
10
|
+
/// @notice The directory of terminals and controllers for projects.
|
|
11
|
+
/// @return The directory contract.
|
|
12
|
+
function DIRECTORY() external view returns (IJBDirectory);
|
|
13
|
+
|
|
14
|
+
/// @notice The ID used when parsing metadata.
|
|
15
|
+
/// @return The address of the metadata ID target.
|
|
16
|
+
function METADATA_ID_TARGET() external view returns (address);
|
|
17
|
+
|
|
18
|
+
/// @notice The ID of the project that this contract is associated with.
|
|
19
|
+
/// @return The project ID.
|
|
20
|
+
function PROJECT_ID() external view returns (uint256);
|
|
21
|
+
}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity ^0.8.0;
|
|
3
3
|
|
|
4
|
-
import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
|
|
5
|
-
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
6
|
-
import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
|
|
7
4
|
import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
|
|
8
|
-
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
9
5
|
import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
|
|
10
6
|
|
|
7
|
+
import {IJB721Hook} from "./IJB721Hook.sol";
|
|
11
8
|
import {IJB721TiersHookStore} from "./IJB721TiersHookStore.sol";
|
|
12
9
|
import {IJB721TokenUriResolver} from "./IJB721TokenUriResolver.sol";
|
|
13
10
|
import {JB721InitTiersConfig} from "../structs/JB721InitTiersConfig.sol";
|
|
@@ -16,7 +13,7 @@ import {JB721TiersHookFlags} from "../structs/JB721TiersHookFlags.sol";
|
|
|
16
13
|
import {JB721TiersMintReservesConfig} from "../structs/JB721TiersMintReservesConfig.sol";
|
|
17
14
|
import {JB721TiersSetDiscountPercentConfig} from "../structs/JB721TiersSetDiscountPercentConfig.sol";
|
|
18
15
|
|
|
19
|
-
interface IJB721TiersHook is
|
|
16
|
+
interface IJB721TiersHook is IJB721Hook {
|
|
20
17
|
event AddPayCredits(
|
|
21
18
|
uint256 indexed amount, uint256 indexed newTotalCredits, address indexed account, address caller
|
|
22
19
|
);
|
|
@@ -39,18 +36,6 @@ interface IJB721TiersHook is IJBRulesetDataHook, IJBPayHook, IJBCashOutHook {
|
|
|
39
36
|
uint256 indexed amount, uint256 indexed newTotalCredits, address indexed account, address caller
|
|
40
37
|
);
|
|
41
38
|
|
|
42
|
-
/// @notice The directory of terminals and controllers for projects.
|
|
43
|
-
/// @return The directory contract.
|
|
44
|
-
function DIRECTORY() external view returns (IJBDirectory);
|
|
45
|
-
|
|
46
|
-
/// @notice The ID used when parsing metadata.
|
|
47
|
-
/// @return The address of the metadata ID target.
|
|
48
|
-
function METADATA_ID_TARGET() external view returns (address);
|
|
49
|
-
|
|
50
|
-
/// @notice The ID of the project that this contract is associated with.
|
|
51
|
-
/// @return The project ID.
|
|
52
|
-
function PROJECT_ID() external view returns (uint256);
|
|
53
|
-
|
|
54
39
|
/// @notice The contract storing and managing project rulesets.
|
|
55
40
|
/// @return The rulesets contract.
|
|
56
41
|
function RULESETS() external view returns (IJBRulesets);
|
package/test/unit/pay_Unit.t.sol
CHANGED
|
@@ -965,7 +965,7 @@ contract Test_afterPayRecorded_Unit is UnitTestSetup {
|
|
|
965
965
|
vm.prank(terminal);
|
|
966
966
|
|
|
967
967
|
// Expect a revert for the caller not being a terminal of the project.
|
|
968
|
-
vm.expectRevert(abi.encodeWithSelector(
|
|
968
|
+
vm.expectRevert(abi.encodeWithSelector(JB721Hook.JB721Hook_InvalidPay.selector));
|
|
969
969
|
|
|
970
970
|
hook.afterPayRecordedWith(
|
|
971
971
|
JBAfterPayRecordedContext({
|
|
@@ -1530,4 +1530,88 @@ contract Test_afterPayRecorded_Unit is UnitTestSetup {
|
|
|
1530
1530
|
// Check: has the holder's balance returned to 0?
|
|
1531
1531
|
assertEq(hook.balanceOf(holder), 0);
|
|
1532
1532
|
}
|
|
1533
|
+
|
|
1534
|
+
function test_afterPayRecorded_revertOnCreditOverflow_samePayerBeneficiary() public {
|
|
1535
|
+
// Mock the directory call.
|
|
1536
|
+
mockAndExpect(
|
|
1537
|
+
address(mockJBDirectory),
|
|
1538
|
+
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
1539
|
+
abi.encode(true)
|
|
1540
|
+
);
|
|
1541
|
+
|
|
1542
|
+
// Set the beneficiary's pay credits to max uint256.
|
|
1543
|
+
stdstore.target(address(hook)).sig("payCreditsOf(address)").with_key(beneficiary)
|
|
1544
|
+
.checked_write(type(uint256).max);
|
|
1545
|
+
|
|
1546
|
+
// Pay 1 wei where payer == beneficiary. No metadata → no NFT mints.
|
|
1547
|
+
// `leftoverAmount += payCredits` overflows: 1 + type(uint256).max.
|
|
1548
|
+
vm.expectRevert(stdError.arithmeticError);
|
|
1549
|
+
vm.prank(mockTerminalAddress);
|
|
1550
|
+
hook.afterPayRecordedWith(
|
|
1551
|
+
JBAfterPayRecordedContext({
|
|
1552
|
+
payer: beneficiary,
|
|
1553
|
+
projectId: projectId,
|
|
1554
|
+
rulesetId: 0,
|
|
1555
|
+
amount: JBTokenAmount({
|
|
1556
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
1557
|
+
value: 1,
|
|
1558
|
+
decimals: 18,
|
|
1559
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
1560
|
+
}),
|
|
1561
|
+
forwardedAmount: JBTokenAmount({
|
|
1562
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
1563
|
+
value: 0,
|
|
1564
|
+
decimals: 18,
|
|
1565
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
1566
|
+
}),
|
|
1567
|
+
weight: 10 ** 18,
|
|
1568
|
+
newlyIssuedTokenCount: 0,
|
|
1569
|
+
beneficiary: beneficiary,
|
|
1570
|
+
hookMetadata: new bytes(0),
|
|
1571
|
+
payerMetadata: new bytes(0)
|
|
1572
|
+
})
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function test_afterPayRecorded_revertOnCreditOverflow_differentPayerBeneficiary() public {
|
|
1577
|
+
// Mock the directory call.
|
|
1578
|
+
mockAndExpect(
|
|
1579
|
+
address(mockJBDirectory),
|
|
1580
|
+
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
1581
|
+
abi.encode(true)
|
|
1582
|
+
);
|
|
1583
|
+
|
|
1584
|
+
// Set the beneficiary's pay credits to max uint256.
|
|
1585
|
+
stdstore.target(address(hook)).sig("payCreditsOf(address)").with_key(beneficiary)
|
|
1586
|
+
.checked_write(type(uint256).max);
|
|
1587
|
+
|
|
1588
|
+
// Pay 1 wei where payer != beneficiary. No metadata → no NFT mints, overspending allowed.
|
|
1589
|
+
// leftoverAmount=1, unusedPayCredits=type(uint256).max → overflow in `leftoverAmount + unusedPayCredits`.
|
|
1590
|
+
vm.expectRevert(stdError.arithmeticError);
|
|
1591
|
+
vm.prank(mockTerminalAddress);
|
|
1592
|
+
hook.afterPayRecordedWith(
|
|
1593
|
+
JBAfterPayRecordedContext({
|
|
1594
|
+
payer: address(0xdead),
|
|
1595
|
+
projectId: projectId,
|
|
1596
|
+
rulesetId: 0,
|
|
1597
|
+
amount: JBTokenAmount({
|
|
1598
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
1599
|
+
value: 1,
|
|
1600
|
+
decimals: 18,
|
|
1601
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
1602
|
+
}),
|
|
1603
|
+
forwardedAmount: JBTokenAmount({
|
|
1604
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
1605
|
+
value: 0,
|
|
1606
|
+
decimals: 18,
|
|
1607
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
1608
|
+
}),
|
|
1609
|
+
weight: 10 ** 18,
|
|
1610
|
+
newlyIssuedTokenCount: 0,
|
|
1611
|
+
beneficiary: beneficiary,
|
|
1612
|
+
hookMetadata: new bytes(0),
|
|
1613
|
+
payerMetadata: new bytes(0)
|
|
1614
|
+
})
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1533
1617
|
}
|
|
@@ -206,7 +206,7 @@ contract Test_cashOut_Unit is UnitTestSetup {
|
|
|
206
206
|
vm.assume(tokenCount > 0);
|
|
207
207
|
|
|
208
208
|
// Expect a revert on account of the token count being non-zero while the total supply is zero.
|
|
209
|
-
vm.expectRevert(abi.encodeWithSelector(
|
|
209
|
+
vm.expectRevert(abi.encodeWithSelector(JB721Hook.JB721Hook_UnexpectedTokenCashedOut.selector));
|
|
210
210
|
|
|
211
211
|
hook.beforeCashOutRecordedWith(
|
|
212
212
|
JBBeforeCashOutRecordedContext({
|
|
@@ -347,7 +347,7 @@ contract Test_cashOut_Unit is UnitTestSetup {
|
|
|
347
347
|
);
|
|
348
348
|
|
|
349
349
|
// Expect to revert on account of the project ID being incorrect.
|
|
350
|
-
vm.expectRevert(abi.encodeWithSelector(
|
|
350
|
+
vm.expectRevert(abi.encodeWithSelector(JB721Hook.JB721Hook_InvalidCashOut.selector));
|
|
351
351
|
|
|
352
352
|
vm.prank(mockTerminalAddress);
|
|
353
353
|
hook.afterCashOutRecordedWith(
|
|
@@ -382,7 +382,7 @@ contract Test_cashOut_Unit is UnitTestSetup {
|
|
|
382
382
|
);
|
|
383
383
|
|
|
384
384
|
// Expect to revert on account of the caller not being a terminal of the project.
|
|
385
|
-
vm.expectRevert(abi.encodeWithSelector(
|
|
385
|
+
vm.expectRevert(abi.encodeWithSelector(JB721Hook.JB721Hook_InvalidCashOut.selector));
|
|
386
386
|
|
|
387
387
|
vm.prank(mockTerminalAddress);
|
|
388
388
|
hook.afterCashOutRecordedWith(
|
|
@@ -434,9 +434,7 @@ contract Test_cashOut_Unit is UnitTestSetup {
|
|
|
434
434
|
abi.encode(true)
|
|
435
435
|
);
|
|
436
436
|
|
|
437
|
-
vm.expectRevert(
|
|
438
|
-
abi.encodeWithSelector(JB721TiersHook.JB721TiersHook_UnauthorizedToken.selector, tokenId, wrongHolder)
|
|
439
|
-
);
|
|
437
|
+
vm.expectRevert(abi.encodeWithSelector(JB721Hook.JB721Hook_UnauthorizedToken.selector, tokenId, wrongHolder));
|
|
440
438
|
|
|
441
439
|
vm.prank(mockTerminalAddress);
|
|
442
440
|
hook.afterCashOutRecordedWith(
|