@bananapus/721-hook-v6 0.0.43 → 0.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -3
- package/README.md +2 -2
- package/package.json +2 -2
- package/references/operations.md +2 -2
- package/references/runtime.md +1 -1
- package/script/helpers/Hook721DeploymentLib.sol +21 -5
- package/src/JB721Checkpoints.sol +5 -7
- package/src/JB721CheckpointsDeployer.sol +9 -1
- package/src/JB721TiersHook.sol +72 -76
- package/src/JB721TiersHookDeployer.sol +8 -6
- package/src/JB721TiersHookProjectDeployer.sol +22 -20
- package/src/JB721TiersHookStore.sol +170 -134
- package/src/abstract/ERC721.sol +24 -22
- package/src/abstract/JB721Hook.sol +20 -14
- package/src/interfaces/IJB721Checkpoints.sol +1 -1
- package/src/interfaces/IJB721CheckpointsDeployer.sol +0 -3
- package/src/interfaces/IJB721TiersHook.sol +2 -10
- package/src/interfaces/IJB721TiersHookProjectDeployer.sol +2 -2
- package/src/interfaces/IJB721TiersHookStore.sol +11 -11
- package/src/libraries/JB721Constants.sol +1 -0
- package/src/libraries/JB721TiersHookLib.sol +20 -33
- package/src/libraries/JBBitmap.sol +1 -1
- package/src/structs/JB721TiersHookFlags.sol +1 -1
- package/src/structs/JBPayDataHookRulesetMetadata.sol +3 -3
- package/test/utils/UnitTestSetup.sol +2 -2
package/src/abstract/ERC721.sol
CHANGED
|
@@ -72,7 +72,7 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
|
|
|
72
72
|
*/
|
|
73
73
|
function balanceOf(address owner) public view virtual returns (uint256) {
|
|
74
74
|
if (owner == address(0)) {
|
|
75
|
-
revert ERC721InvalidOwner(address(0));
|
|
75
|
+
revert ERC721InvalidOwner({owner: address(0)});
|
|
76
76
|
}
|
|
77
77
|
return _balances[owner];
|
|
78
78
|
}
|
|
@@ -153,13 +153,13 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
|
|
|
153
153
|
*/
|
|
154
154
|
function transferFrom(address from, address to, uint256 tokenId) public virtual {
|
|
155
155
|
if (to == address(0)) {
|
|
156
|
-
revert ERC721InvalidReceiver(address(0));
|
|
156
|
+
revert ERC721InvalidReceiver({receiver: address(0)});
|
|
157
157
|
}
|
|
158
158
|
// Setting an "auth" arguments enables the `_isAuthorized` check which verifies that the token exists
|
|
159
159
|
// (from != 0). Therefore, it is not needed to verify that the return value is not 0 here.
|
|
160
160
|
address previousOwner = _update({to: to, tokenId: tokenId, auth: _msgSender()});
|
|
161
161
|
if (previousOwner != from) {
|
|
162
|
-
revert ERC721IncorrectOwner(from, tokenId, previousOwner);
|
|
162
|
+
revert ERC721IncorrectOwner({sender: from, tokenId: tokenId, owner: previousOwner});
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
|
|
@@ -206,7 +206,9 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
|
|
|
206
206
|
*/
|
|
207
207
|
function _isAuthorized(address owner, address spender, uint256 tokenId) internal view virtual returns (bool) {
|
|
208
208
|
return spender != address(0)
|
|
209
|
-
&& (owner == spender
|
|
209
|
+
&& (owner == spender
|
|
210
|
+
|| isApprovedForAll({owner: owner, operator: spender})
|
|
211
|
+
|| _getApproved(tokenId) == spender);
|
|
210
212
|
}
|
|
211
213
|
|
|
212
214
|
/**
|
|
@@ -218,11 +220,11 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
|
|
|
218
220
|
* assumption.
|
|
219
221
|
*/
|
|
220
222
|
function _checkAuthorized(address owner, address spender, uint256 tokenId) internal view virtual {
|
|
221
|
-
if (!_isAuthorized(owner, spender, tokenId)) {
|
|
223
|
+
if (!_isAuthorized({owner: owner, spender: spender, tokenId: tokenId})) {
|
|
222
224
|
if (owner == address(0)) {
|
|
223
|
-
revert ERC721NonexistentToken(tokenId);
|
|
225
|
+
revert ERC721NonexistentToken({tokenId: tokenId});
|
|
224
226
|
} else {
|
|
225
|
-
revert ERC721InsufficientApproval(spender, tokenId);
|
|
227
|
+
revert ERC721InsufficientApproval({operator: spender, tokenId: tokenId});
|
|
226
228
|
}
|
|
227
229
|
}
|
|
228
230
|
}
|
|
@@ -280,7 +282,7 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
|
|
|
280
282
|
|
|
281
283
|
_owners[tokenId] = to;
|
|
282
284
|
|
|
283
|
-
emit Transfer(from, to, tokenId);
|
|
285
|
+
emit Transfer({from: from, to: to, tokenId: tokenId});
|
|
284
286
|
|
|
285
287
|
return from;
|
|
286
288
|
}
|
|
@@ -299,11 +301,11 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
|
|
|
299
301
|
*/
|
|
300
302
|
function _mint(address to, uint256 tokenId) internal {
|
|
301
303
|
if (to == address(0)) {
|
|
302
|
-
revert ERC721InvalidReceiver(address(0));
|
|
304
|
+
revert ERC721InvalidReceiver({receiver: address(0)});
|
|
303
305
|
}
|
|
304
306
|
address previousOwner = _update({to: to, tokenId: tokenId, auth: address(0)});
|
|
305
307
|
if (previousOwner != address(0)) {
|
|
306
|
-
revert ERC721InvalidSender(address(0));
|
|
308
|
+
revert ERC721InvalidSender({sender: address(0)});
|
|
307
309
|
}
|
|
308
310
|
}
|
|
309
311
|
|
|
@@ -345,7 +347,7 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
|
|
|
345
347
|
function _burn(uint256 tokenId) internal {
|
|
346
348
|
address previousOwner = _update({to: address(0), tokenId: tokenId, auth: address(0)});
|
|
347
349
|
if (previousOwner == address(0)) {
|
|
348
|
-
revert ERC721NonexistentToken(tokenId);
|
|
350
|
+
revert ERC721NonexistentToken({tokenId: tokenId});
|
|
349
351
|
}
|
|
350
352
|
}
|
|
351
353
|
|
|
@@ -362,13 +364,13 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
|
|
|
362
364
|
*/
|
|
363
365
|
function _transfer(address from, address to, uint256 tokenId) internal {
|
|
364
366
|
if (to == address(0)) {
|
|
365
|
-
revert ERC721InvalidReceiver(address(0));
|
|
367
|
+
revert ERC721InvalidReceiver({receiver: address(0)});
|
|
366
368
|
}
|
|
367
369
|
address previousOwner = _update({to: to, tokenId: tokenId, auth: address(0)});
|
|
368
370
|
if (previousOwner == address(0)) {
|
|
369
|
-
revert ERC721NonexistentToken(tokenId);
|
|
371
|
+
revert ERC721NonexistentToken({tokenId: tokenId});
|
|
370
372
|
} else if (previousOwner != from) {
|
|
371
|
-
revert ERC721IncorrectOwner(from, tokenId, previousOwner);
|
|
373
|
+
revert ERC721IncorrectOwner({sender: from, tokenId: tokenId, owner: previousOwner});
|
|
372
374
|
}
|
|
373
375
|
}
|
|
374
376
|
|
|
@@ -430,12 +432,12 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
|
|
|
430
432
|
address owner = _requireOwned(tokenId);
|
|
431
433
|
|
|
432
434
|
// We do not use _isAuthorized because single-token approvals should not be able to call approve
|
|
433
|
-
if (auth != address(0) && owner != auth && !isApprovedForAll(owner, auth)) {
|
|
434
|
-
revert ERC721InvalidApprover(auth);
|
|
435
|
+
if (auth != address(0) && owner != auth && !isApprovedForAll({owner: owner, operator: auth})) {
|
|
436
|
+
revert ERC721InvalidApprover({approver: auth});
|
|
435
437
|
}
|
|
436
438
|
|
|
437
439
|
if (emitEvent) {
|
|
438
|
-
emit Approval(owner, to, tokenId);
|
|
440
|
+
emit Approval({owner: owner, approved: to, tokenId: tokenId});
|
|
439
441
|
}
|
|
440
442
|
}
|
|
441
443
|
|
|
@@ -452,10 +454,10 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
|
|
|
452
454
|
*/
|
|
453
455
|
function _setApprovalForAll(address owner, address operator, bool approved) internal virtual {
|
|
454
456
|
if (operator == address(0)) {
|
|
455
|
-
revert ERC721InvalidOperator(operator);
|
|
457
|
+
revert ERC721InvalidOperator({operator: operator});
|
|
456
458
|
}
|
|
457
459
|
_operatorApprovals[owner][operator] = approved;
|
|
458
|
-
emit ApprovalForAll(owner, operator, approved);
|
|
460
|
+
emit ApprovalForAll({owner: owner, operator: operator, approved: approved});
|
|
459
461
|
}
|
|
460
462
|
|
|
461
463
|
/**
|
|
@@ -467,7 +469,7 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
|
|
|
467
469
|
function _requireOwned(uint256 tokenId) internal view returns (address) {
|
|
468
470
|
address owner = _ownerOf(tokenId);
|
|
469
471
|
if (owner == address(0)) {
|
|
470
|
-
revert ERC721NonexistentToken(tokenId);
|
|
472
|
+
revert ERC721NonexistentToken({tokenId: tokenId});
|
|
471
473
|
}
|
|
472
474
|
return owner;
|
|
473
475
|
}
|
|
@@ -489,11 +491,11 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
|
|
|
489
491
|
bytes4 retval
|
|
490
492
|
) {
|
|
491
493
|
if (retval != IERC721Receiver.onERC721Received.selector) {
|
|
492
|
-
revert ERC721InvalidReceiver(to);
|
|
494
|
+
revert ERC721InvalidReceiver({receiver: to});
|
|
493
495
|
}
|
|
494
496
|
} catch (bytes memory reason) {
|
|
495
497
|
if (reason.length == 0) {
|
|
496
|
-
revert ERC721InvalidReceiver(to);
|
|
498
|
+
revert ERC721InvalidReceiver({receiver: to});
|
|
497
499
|
} else {
|
|
498
500
|
/// @solidity memory-safe-assembly
|
|
499
501
|
assembly {
|
|
@@ -20,19 +20,20 @@ import {IJB721Hook} from "../interfaces/IJB721Hook.sol";
|
|
|
20
20
|
import {ERC721} from "./ERC721.sol";
|
|
21
21
|
|
|
22
22
|
/// @title JB721Hook
|
|
23
|
-
/// @notice
|
|
24
|
-
///
|
|
25
|
-
///
|
|
26
|
-
/// the
|
|
23
|
+
/// @notice Abstract base for Juicebox 721 hooks. Implements the pay hook and cash-out hook interfaces: when a project
|
|
24
|
+
/// is paid through its terminal, this hook mints NFTs to the payer; when NFT holders cash out, this hook burns their
|
|
25
|
+
/// NFTs and lets the terminal send them their share of the project's surplus (proportional to the NFT's price).
|
|
26
|
+
/// @dev Subclasses (like `JB721TiersHook`) implement the actual minting logic via `_processPayment` and burn
|
|
27
|
+
/// tracking via `_didBurn`.
|
|
27
28
|
abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
28
29
|
//*********************************************************************//
|
|
29
30
|
// --------------------------- custom errors ------------------------- //
|
|
30
31
|
//*********************************************************************//
|
|
31
32
|
|
|
32
|
-
error JB721Hook_InvalidCashOut();
|
|
33
|
-
error JB721Hook_InvalidPay();
|
|
33
|
+
error JB721Hook_InvalidCashOut(address caller, uint256 contextProjectId, uint256 projectId, uint256 msgValue);
|
|
34
|
+
error JB721Hook_InvalidPay(address caller, uint256 contextProjectId, uint256 projectId);
|
|
34
35
|
error JB721Hook_UnauthorizedToken(uint256 tokenId, address holder);
|
|
35
|
-
error JB721Hook_UnexpectedTokenCashedOut();
|
|
36
|
+
error JB721Hook_UnexpectedTokenCashedOut(uint256 cashOutCount);
|
|
36
37
|
|
|
37
38
|
//*********************************************************************//
|
|
38
39
|
// --------------- public immutable stored properties ---------------- //
|
|
@@ -93,7 +94,9 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
|
93
94
|
)
|
|
94
95
|
{
|
|
95
96
|
// Make sure (fungible) project tokens aren't also being cashed out.
|
|
96
|
-
if (context.cashOutCount > 0)
|
|
97
|
+
if (context.cashOutCount > 0) {
|
|
98
|
+
revert JB721Hook_UnexpectedTokenCashedOut({cashOutCount: context.cashOutCount});
|
|
99
|
+
}
|
|
97
100
|
|
|
98
101
|
// Fetch the cash out hook metadata using the corresponding metadata ID.
|
|
99
102
|
(bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
|
|
@@ -184,7 +187,6 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
|
184
187
|
/// `context.beneficiary`. Part of `IJBCashOutHook`.
|
|
185
188
|
/// @dev Reverts if the calling contract is not one of the project's terminals.
|
|
186
189
|
/// @param context The cash out context passed in by the terminal.
|
|
187
|
-
// slither-disable-next-line locked-ether
|
|
188
190
|
function afterCashOutRecordedWith(JBAfterCashOutRecordedContext calldata context)
|
|
189
191
|
external
|
|
190
192
|
payable
|
|
@@ -199,7 +201,11 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
|
199
201
|
if (
|
|
200
202
|
msg.value != 0 || !DIRECTORY.isTerminalOf({projectId: projectId, terminal: IJBTerminal(msg.sender)})
|
|
201
203
|
|| context.projectId != projectId
|
|
202
|
-
)
|
|
204
|
+
) {
|
|
205
|
+
revert JB721Hook_InvalidCashOut({
|
|
206
|
+
caller: msg.sender, contextProjectId: context.projectId, projectId: projectId, msgValue: msg.value
|
|
207
|
+
});
|
|
208
|
+
}
|
|
203
209
|
|
|
204
210
|
// Fetch the cash out hook metadata using the corresponding metadata ID.
|
|
205
211
|
(bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
|
|
@@ -236,7 +242,6 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
|
236
242
|
/// `IJBPayHook`.
|
|
237
243
|
/// @dev Reverts if the calling contract is not one of the project's terminals.
|
|
238
244
|
/// @param context The payment context passed in by the terminal.
|
|
239
|
-
// slither-disable-next-line locked-ether
|
|
240
245
|
function afterPayRecordedWith(JBAfterPayRecordedContext calldata context) external payable virtual override {
|
|
241
246
|
uint256 projectId = PROJECT_ID;
|
|
242
247
|
|
|
@@ -246,7 +251,7 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
|
246
251
|
!DIRECTORY.isTerminalOf({projectId: projectId, terminal: IJBTerminal(msg.sender)})
|
|
247
252
|
|| context.projectId != projectId
|
|
248
253
|
) {
|
|
249
|
-
revert JB721Hook_InvalidPay();
|
|
254
|
+
revert JB721Hook_InvalidPay({caller: msg.sender, contextProjectId: context.projectId, projectId: projectId});
|
|
250
255
|
}
|
|
251
256
|
|
|
252
257
|
// Process the payment.
|
|
@@ -270,7 +275,8 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
|
270
275
|
PROJECT_ID = projectId;
|
|
271
276
|
}
|
|
272
277
|
|
|
273
|
-
/// @notice Process a received payment.
|
|
274
|
-
///
|
|
278
|
+
/// @notice Process a received payment by minting NFTs and/or updating credits. Subclasses implement the
|
|
279
|
+
/// specific minting logic (e.g., tier selection, credit tracking, split distribution).
|
|
280
|
+
/// @param context The payment context passed in by the terminal (includes amount, payer, beneficiary, metadata).
|
|
275
281
|
function _processPayment(JBAfterPayRecordedContext calldata context) internal virtual;
|
|
276
282
|
}
|
|
@@ -36,6 +36,6 @@ interface IJB721Checkpoints is IERC5805 {
|
|
|
36
36
|
/// Auto-self-delegates on first receive so checkpoints work without manual delegation.
|
|
37
37
|
/// @param from The previous owner (address(0) on mint).
|
|
38
38
|
/// @param to The new owner (address(0) on burn).
|
|
39
|
-
/// @param tokenId The token ID
|
|
39
|
+
/// @param tokenId The token ID to transfer (used to look up tier voting units).
|
|
40
40
|
function onTransfer(address from, address to, uint256 tokenId) external;
|
|
41
41
|
}
|
|
@@ -6,9 +6,6 @@ import {IJB721TiersHookStore} from "./IJB721TiersHookStore.sol";
|
|
|
6
6
|
|
|
7
7
|
/// @notice Deploys JB721Checkpoints clones for JB721TiersHook instances.
|
|
8
8
|
interface IJB721CheckpointsDeployer {
|
|
9
|
-
/// @notice Thrown when the caller is not the hook that the checkpoint module is being deployed for.
|
|
10
|
-
error JB721CheckpointsDeployer_Unauthorized();
|
|
11
|
-
|
|
12
9
|
/// @notice The implementation contract that clones are based on.
|
|
13
10
|
/// @return The implementation address.
|
|
14
11
|
// forge-lint: disable-next-line(mixed-case-function)
|
|
@@ -18,14 +18,6 @@ import {JB721TiersSetDiscountPercentConfig} from "../structs/JB721TiersSetDiscou
|
|
|
18
18
|
|
|
19
19
|
/// @notice A 721 tiers hook that mints tiered NFTs for payments and tracks their cash out weight.
|
|
20
20
|
interface IJB721TiersHook is IJB721Hook {
|
|
21
|
-
/// @notice Emitted when an `addToBalanceOf` call reverts during leftover distribution. The funds remain
|
|
22
|
-
/// stranded in the hook contract.
|
|
23
|
-
/// @param projectId The project ID whose terminal reverted.
|
|
24
|
-
/// @param token The token being sent.
|
|
25
|
-
/// @param amount The amount that failed to send.
|
|
26
|
-
/// @param reason The revert reason bytes.
|
|
27
|
-
event AddToBalanceReverted(uint256 indexed projectId, address token, uint256 amount, bytes reason);
|
|
28
|
-
|
|
29
21
|
/// @notice Emitted when pay credits are added for an account.
|
|
30
22
|
/// @param amount The amount of credits added.
|
|
31
23
|
/// @param newTotalCredits The new total credits balance for the account.
|
|
@@ -108,7 +100,7 @@ interface IJB721TiersHook is IJB721Hook {
|
|
|
108
100
|
/// project's balance.
|
|
109
101
|
/// @param projectId The project ID the split belongs to.
|
|
110
102
|
/// @param split The split that reverted.
|
|
111
|
-
/// @param amount The amount that was
|
|
103
|
+
/// @param amount The amount that was paid out.
|
|
112
104
|
/// @param reason The revert reason bytes.
|
|
113
105
|
/// @param caller The address that called the function.
|
|
114
106
|
event SplitPayoutReverted(uint256 indexed projectId, JBSplit split, uint256 amount, bytes reason, address caller);
|
|
@@ -142,7 +134,7 @@ interface IJB721TiersHook is IJB721Hook {
|
|
|
142
134
|
|
|
143
135
|
/// @notice Context for the pricing of this hook's tiers.
|
|
144
136
|
/// @return currency The currency used for tier prices.
|
|
145
|
-
/// @return decimals The
|
|
137
|
+
/// @return decimals The number of decimals used in tier prices.
|
|
146
138
|
function pricingContext() external view returns (uint256 currency, uint256 decimals);
|
|
147
139
|
|
|
148
140
|
/// @notice The checkpoint module that manages IVotes-compatible checkpointed voting power for this hook's NFTs.
|
|
@@ -40,7 +40,7 @@ interface IJB721TiersHookProjectDeployer {
|
|
|
40
40
|
returns (uint256 projectId, IJB721TiersHook hook);
|
|
41
41
|
|
|
42
42
|
/// @notice Launches rulesets for a project with an attached 721 tiers hook.
|
|
43
|
-
/// @param projectId The ID of the project
|
|
43
|
+
/// @param projectId The ID of the project to launch rulesets for.
|
|
44
44
|
/// @param deployTiersHookConfig Configuration which dictates the behavior of the 721 tiers hook.
|
|
45
45
|
/// @param launchRulesetsConfig Configuration which dictates the project's new rulesets.
|
|
46
46
|
/// @param projectUri Metadata URI to associate with the project. Pass an empty string to leave it unchanged.
|
|
@@ -60,7 +60,7 @@ interface IJB721TiersHookProjectDeployer {
|
|
|
60
60
|
returns (uint256 rulesetId, IJB721TiersHook hook);
|
|
61
61
|
|
|
62
62
|
/// @notice Queues rulesets for a project with an attached 721 tiers hook.
|
|
63
|
-
/// @param projectId The ID of the project
|
|
63
|
+
/// @param projectId The ID of the project to queue rulesets for.
|
|
64
64
|
/// @param deployTiersHookConfig Configuration which dictates the behavior of the 721 tiers hook.
|
|
65
65
|
/// @param queueRulesetsConfig Configuration which dictates the project's newly queued rulesets.
|
|
66
66
|
/// @param controller The controller that the project's rulesets will be queued with.
|
|
@@ -203,19 +203,19 @@ interface IJB721TiersHookStore {
|
|
|
203
203
|
|
|
204
204
|
/// @notice Record newly added tiers.
|
|
205
205
|
/// @param tiersToAdd The tiers to add.
|
|
206
|
-
/// @return tierIds The IDs of the tiers
|
|
206
|
+
/// @return tierIds The IDs of the tiers added.
|
|
207
207
|
function recordAddTiers(JB721TierConfig[] calldata tiersToAdd) external returns (uint256[] memory tierIds);
|
|
208
208
|
|
|
209
|
-
/// @notice
|
|
209
|
+
/// @notice Records the burning of tiered 721 tokens, updating supply tracking for the affected tiers.
|
|
210
210
|
/// @param tokenIds The token IDs of the NFTs to burn.
|
|
211
211
|
function recordBurn(uint256[] calldata tokenIds) external;
|
|
212
212
|
|
|
213
|
-
/// @notice
|
|
213
|
+
/// @notice Stores updated ruleset metadata flags that control minting, transferring, and tier behavior.
|
|
214
214
|
/// @param flags The flags to set.
|
|
215
215
|
function recordFlags(JB721TiersHookFlags calldata flags) external;
|
|
216
216
|
|
|
217
217
|
/// @notice Record 721 mints from the provided tiers.
|
|
218
|
-
/// @param amount The amount
|
|
218
|
+
/// @param amount The amount to spend on NFTs.
|
|
219
219
|
/// @param tierIds The IDs of the tiers to mint from.
|
|
220
220
|
/// @param isOwnerMint Whether this is a direct owner mint.
|
|
221
221
|
/// @return tokenIds The token IDs of the NFTs which were minted.
|
|
@@ -235,13 +235,13 @@ interface IJB721TiersHookStore {
|
|
|
235
235
|
/// @return tokenIds The token IDs of the reserve NFTs which were minted.
|
|
236
236
|
function recordMintReservesFor(uint256 tierId, uint256 count) external returns (uint256[] memory tokenIds);
|
|
237
237
|
|
|
238
|
-
/// @notice Record tiers
|
|
239
|
-
/// @param tierIds The IDs of the tiers
|
|
238
|
+
/// @notice Record tiers to remove.
|
|
239
|
+
/// @param tierIds The IDs of the tiers to remove.
|
|
240
240
|
function recordRemoveTierIds(uint256[] calldata tierIds) external;
|
|
241
241
|
|
|
242
242
|
/// @notice Record the setting of a discount for a tier.
|
|
243
243
|
/// @param tierId The ID of the tier to set the discount of.
|
|
244
|
-
/// @param discountPercent The new discount percent
|
|
244
|
+
/// @param discountPercent The new discount percent to apply.
|
|
245
245
|
function recordSetDiscountPercentOf(uint256 tierId, uint256 discountPercent) external;
|
|
246
246
|
|
|
247
247
|
/// @notice Record a new encoded IPFS URI for a tier.
|
|
@@ -254,9 +254,9 @@ interface IJB721TiersHookStore {
|
|
|
254
254
|
/// @param resolver The resolver to set.
|
|
255
255
|
function recordSetTokenUriResolver(IJB721TokenUriResolver resolver) external;
|
|
256
256
|
|
|
257
|
-
/// @notice
|
|
258
|
-
/// @param tierId The ID of the tier that the 721
|
|
259
|
-
/// @param from The address
|
|
260
|
-
/// @param to The address
|
|
257
|
+
/// @notice Records a 721 token transfer, updating the first-owner tracking for the receiving address.
|
|
258
|
+
/// @param tierId The ID of the tier that the 721 to transfer belongs to.
|
|
259
|
+
/// @param from The address to transfer the 721 from.
|
|
260
|
+
/// @param to The address to transfer the 721 to.
|
|
261
261
|
function recordTransferForTier(uint256 tierId, address from, address to) external;
|
|
262
262
|
}
|
|
@@ -3,6 +3,7 @@ pragma solidity 0.8.28;
|
|
|
3
3
|
|
|
4
4
|
/// @notice Global constants used across 721 hook contracts.
|
|
5
5
|
library JB721Constants {
|
|
6
|
+
/// @notice The denominator used when applying tier discount percentages.
|
|
6
7
|
uint16 public constant DISCOUNT_DENOMINATOR = 200;
|
|
7
8
|
|
|
8
9
|
/// @notice The metadata ID used to identify the 721 beneficiary entry in payment metadata.
|
|
@@ -31,7 +31,7 @@ library JB721TiersHookLib {
|
|
|
31
31
|
// --------------------------- custom errors ------------------------- //
|
|
32
32
|
//*********************************************************************//
|
|
33
33
|
|
|
34
|
-
error JB721TiersHook_CantBuyWithCredits();
|
|
34
|
+
error JB721TiersHook_CantBuyWithCredits(uint256 restrictedCost, uint256 freshValue);
|
|
35
35
|
error JB721TiersHook_Overspending(uint256 leftoverAmount);
|
|
36
36
|
error JB721TiersHookLib_NoTerminalForLeftover(uint256 projectId, address token, uint256 leftoverAmount);
|
|
37
37
|
error JB721TiersHookLib_SplitFallbackFailed(uint256 projectId, address token, uint256 amount, bytes reason);
|
|
@@ -79,7 +79,6 @@ library JB721TiersHookLib {
|
|
|
79
79
|
++i;
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
-
// slither-disable-next-line reentrancy-events
|
|
83
82
|
store.recordRemoveTierIds(tierIdsToRemove);
|
|
84
83
|
}
|
|
85
84
|
|
|
@@ -87,7 +86,6 @@ library JB721TiersHookLib {
|
|
|
87
86
|
if (tiersToAdd.length != 0) {
|
|
88
87
|
uint256[] memory tierIdsAdded = store.recordAddTiers(tiersToAdd);
|
|
89
88
|
|
|
90
|
-
// slither-disable-next-line reentrancy-events
|
|
91
89
|
for (uint256 i; i < tiersToAdd.length;) {
|
|
92
90
|
emit AddTier({tierId: tierIdsAdded[i], tier: tiersToAdd[i], caller: caller});
|
|
93
91
|
|
|
@@ -113,7 +111,7 @@ library JB721TiersHookLib {
|
|
|
113
111
|
/// @param splits The splits contract to read tier split groups from.
|
|
114
112
|
/// @param projectId The project ID of the hook.
|
|
115
113
|
/// @param hookAddress The hook address (for computing split group IDs).
|
|
116
|
-
/// @param token The token
|
|
114
|
+
/// @param token The token to distribute.
|
|
117
115
|
/// @param amount The total amount to distribute.
|
|
118
116
|
/// @param decimals The token decimals.
|
|
119
117
|
/// @param encodedSplitData The encoded per-tier breakdown from hookMetadata.
|
|
@@ -135,7 +133,9 @@ library JB721TiersHookLib {
|
|
|
135
133
|
SafeERC20.safeTransferFrom({token: IERC20(token), from: msg.sender, to: address(this), value: amount});
|
|
136
134
|
uint256 receivedAmount = IERC20(token).balanceOf(address(this)) - balanceBefore;
|
|
137
135
|
if (receivedAmount != amount) {
|
|
138
|
-
revert JB721TiersHookLib_TokenTransferAmountMismatch(
|
|
136
|
+
revert JB721TiersHookLib_TokenTransferAmountMismatch({
|
|
137
|
+
expectedAmount: amount, receivedAmount: receivedAmount
|
|
138
|
+
});
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -223,16 +223,19 @@ library JB721TiersHookLib {
|
|
|
223
223
|
if (tierIdsToMint.length != 0) {
|
|
224
224
|
uint256 restrictedCost;
|
|
225
225
|
|
|
226
|
-
// slither-disable-next-line reentrancy-events,reentrancy-no-eth
|
|
227
226
|
(tokenIds, leftoverAmount, restrictedCost) =
|
|
228
227
|
store.recordMint({amount: leftoverAmount, tierIds: tierIdsToMint, isOwnerMint: false});
|
|
229
228
|
|
|
230
229
|
// Credit-restricted tiers must be fully covered by fresh payment (not stored credits).
|
|
231
|
-
if (restrictedCost > value)
|
|
230
|
+
if (restrictedCost > value) {
|
|
231
|
+
revert JB721TiersHook_CantBuyWithCredits({restrictedCost: restrictedCost, freshValue: value});
|
|
232
|
+
}
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
// If overspending isn't allowed, revert.
|
|
235
|
-
if (leftoverAmount != 0 && !allowOverspending)
|
|
236
|
+
if (leftoverAmount != 0 && !allowOverspending) {
|
|
237
|
+
revert JB721TiersHook_Overspending({leftoverAmount: leftoverAmount});
|
|
238
|
+
}
|
|
236
239
|
|
|
237
240
|
// Compute the new pay credits balance: leftover + unused credits (held in newPayCredits).
|
|
238
241
|
newPayCredits = leftoverAmount + newPayCredits;
|
|
@@ -259,7 +262,6 @@ library JB721TiersHookLib {
|
|
|
259
262
|
uint256[] memory tierIdsAdded = store.recordAddTiers(tiersToAdd);
|
|
260
263
|
|
|
261
264
|
for (uint256 i; i < tiersToAdd.length;) {
|
|
262
|
-
// slither-disable-next-line reentrancy-events
|
|
263
265
|
emit AddTier({tierId: tierIdsAdded[i], tier: tiersToAdd[i], caller: caller});
|
|
264
266
|
|
|
265
267
|
unchecked {
|
|
@@ -489,7 +491,6 @@ library JB721TiersHookLib {
|
|
|
489
491
|
|
|
490
492
|
for (uint256 i; i < tierIdsToMint.length;) {
|
|
491
493
|
// Get only the pricing fields (lightweight — avoids full struct construction).
|
|
492
|
-
// slither-disable-next-line calls-loop
|
|
493
494
|
(uint104 tierPrice, uint32 tierSplitPercent, uint8 tierDiscountPercent) =
|
|
494
495
|
store.tierPricingOf({hook: hook, id: tierIdsToMint[i]});
|
|
495
496
|
if (tierSplitPercent != 0) {
|
|
@@ -707,7 +708,6 @@ library JB721TiersHookLib {
|
|
|
707
708
|
)
|
|
708
709
|
private
|
|
709
710
|
{
|
|
710
|
-
// slither-disable-next-line calls-loop
|
|
711
711
|
JBSplit[] memory tierSplits = splitsContract.splitsOf({projectId: projectId, rulesetId: 0, groupId: groupId});
|
|
712
712
|
|
|
713
713
|
bool isNativeToken = token == JBConstants.NATIVE_TOKEN;
|
|
@@ -725,7 +725,6 @@ library JB721TiersHookLib {
|
|
|
725
725
|
// On failure, don't re-add to leftoverAmount — this prevents inflating later recipients.
|
|
726
726
|
// Failed amounts accumulate as the gap between `amount` and `leftoverAmount + total sent`.
|
|
727
727
|
// After the loop, we re-add leftoverPercentage-based residual naturally.
|
|
728
|
-
// slither-disable-next-line calls-loop,reentrancy-no-eth,reentrancy-benign,reentrancy-events
|
|
729
728
|
if (!_sendPayoutToSplit({
|
|
730
729
|
directory: directory,
|
|
731
730
|
split: tierSplits[j],
|
|
@@ -752,14 +751,14 @@ library JB721TiersHookLib {
|
|
|
752
751
|
leftoverAmount += amount;
|
|
753
752
|
|
|
754
753
|
if (leftoverAmount != 0) {
|
|
755
|
-
// slither-disable-next-line calls-loop
|
|
756
754
|
IJBTerminal terminal = directory.primaryTerminalOf({projectId: projectId, token: token});
|
|
757
755
|
// Revert if there are leftover funds but no terminal to route them to.
|
|
758
756
|
if (address(terminal) == address(0)) {
|
|
759
|
-
revert JB721TiersHookLib_NoTerminalForLeftover(
|
|
757
|
+
revert JB721TiersHookLib_NoTerminalForLeftover({
|
|
758
|
+
projectId: projectId, token: token, leftoverAmount: leftoverAmount
|
|
759
|
+
});
|
|
760
760
|
}
|
|
761
761
|
if (isNativeToken) {
|
|
762
|
-
// slither-disable-next-line arbitrary-send-eth,calls-loop
|
|
763
762
|
try terminal.addToBalanceOf{value: leftoverAmount}({
|
|
764
763
|
projectId: projectId,
|
|
765
764
|
token: token,
|
|
@@ -769,11 +768,12 @@ library JB721TiersHookLib {
|
|
|
769
768
|
metadata: bytes("")
|
|
770
769
|
}) {}
|
|
771
770
|
catch (bytes memory reason) {
|
|
772
|
-
revert JB721TiersHookLib_SplitFallbackFailed(
|
|
771
|
+
revert JB721TiersHookLib_SplitFallbackFailed({
|
|
772
|
+
projectId: projectId, token: token, amount: leftoverAmount, reason: reason
|
|
773
|
+
});
|
|
773
774
|
}
|
|
774
775
|
} else {
|
|
775
776
|
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: leftoverAmount});
|
|
776
|
-
// slither-disable-next-line calls-loop
|
|
777
777
|
try terminal.addToBalanceOf({
|
|
778
778
|
projectId: projectId,
|
|
779
779
|
token: token,
|
|
@@ -785,7 +785,9 @@ library JB721TiersHookLib {
|
|
|
785
785
|
catch (bytes memory reason) {
|
|
786
786
|
// Reset approval on failure.
|
|
787
787
|
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
|
|
788
|
-
revert JB721TiersHookLib_SplitFallbackFailed(
|
|
788
|
+
revert JB721TiersHookLib_SplitFallbackFailed({
|
|
789
|
+
projectId: projectId, token: token, amount: leftoverAmount, reason: reason
|
|
790
|
+
});
|
|
789
791
|
}
|
|
790
792
|
}
|
|
791
793
|
}
|
|
@@ -817,11 +819,9 @@ library JB721TiersHookLib {
|
|
|
817
819
|
if (isNativeToken) {
|
|
818
820
|
// Wrap in try-catch so a reverting hook doesn't brick all project payments.
|
|
819
821
|
// On revert, ETH stays with the caller and we return false.
|
|
820
|
-
// slither-disable-next-line calls-loop,reentrancy-no-eth,reentrancy-events
|
|
821
822
|
try split.hook.processSplitWith{value: amount}(context) {
|
|
822
823
|
return true;
|
|
823
824
|
} catch (bytes memory reason) {
|
|
824
|
-
// slither-disable-next-line reentrancy-events
|
|
825
825
|
emit SplitPayoutReverted({
|
|
826
826
|
projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
|
|
827
827
|
});
|
|
@@ -834,10 +834,8 @@ library JB721TiersHookLib {
|
|
|
834
834
|
// cause the caller to skip subtracting this amount from leftoverAmount, leading
|
|
835
835
|
// to a double-spend when the leftover is later sent to the project's balance.
|
|
836
836
|
SafeERC20.safeTransfer({token: IERC20(token), to: address(split.hook), value: amount});
|
|
837
|
-
// slither-disable-next-line calls-loop,reentrancy-no-eth,reentrancy-events
|
|
838
837
|
try split.hook.processSplitWith(context) {}
|
|
839
838
|
catch (bytes memory reason) {
|
|
840
|
-
// slither-disable-next-line reentrancy-events
|
|
841
839
|
emit SplitPayoutReverted({
|
|
842
840
|
projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
|
|
843
841
|
});
|
|
@@ -845,14 +843,12 @@ library JB721TiersHookLib {
|
|
|
845
843
|
return true;
|
|
846
844
|
}
|
|
847
845
|
} else if (split.projectId != 0) {
|
|
848
|
-
// slither-disable-next-line calls-loop
|
|
849
846
|
IJBTerminal terminal = directory.primaryTerminalOf({projectId: split.projectId, token: token});
|
|
850
847
|
if (address(terminal) == address(0)) return false;
|
|
851
848
|
|
|
852
849
|
// Wrap terminal calls in try-catch to prevent a failing terminal from bricking payments.
|
|
853
850
|
if (split.preferAddToBalance) {
|
|
854
851
|
if (isNativeToken) {
|
|
855
|
-
// slither-disable-next-line arbitrary-send-eth,calls-loop,reentrancy-no-eth,reentrancy-events
|
|
856
852
|
try terminal.addToBalanceOf{value: amount}({
|
|
857
853
|
projectId: split.projectId,
|
|
858
854
|
token: token,
|
|
@@ -863,7 +859,6 @@ library JB721TiersHookLib {
|
|
|
863
859
|
}) {
|
|
864
860
|
return true;
|
|
865
861
|
} catch (bytes memory reason) {
|
|
866
|
-
// slither-disable-next-line reentrancy-events
|
|
867
862
|
emit SplitPayoutReverted({
|
|
868
863
|
projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
|
|
869
864
|
});
|
|
@@ -871,7 +866,6 @@ library JB721TiersHookLib {
|
|
|
871
866
|
}
|
|
872
867
|
} else {
|
|
873
868
|
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: amount});
|
|
874
|
-
// slither-disable-next-line calls-loop,reentrancy-no-eth,reentrancy-events
|
|
875
869
|
try terminal.addToBalanceOf({
|
|
876
870
|
projectId: split.projectId,
|
|
877
871
|
token: token,
|
|
@@ -884,7 +878,6 @@ library JB721TiersHookLib {
|
|
|
884
878
|
} catch (bytes memory reason) {
|
|
885
879
|
// Reset approval on failure so tokens aren't left approved to the terminal.
|
|
886
880
|
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
|
|
887
|
-
// slither-disable-next-line reentrancy-events
|
|
888
881
|
emit SplitPayoutReverted({
|
|
889
882
|
projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
|
|
890
883
|
});
|
|
@@ -893,7 +886,6 @@ library JB721TiersHookLib {
|
|
|
893
886
|
}
|
|
894
887
|
} else {
|
|
895
888
|
if (isNativeToken) {
|
|
896
|
-
// slither-disable-next-line arbitrary-send-eth,unused-return,calls-loop,reentrancy-events
|
|
897
889
|
try terminal.pay{value: amount}({
|
|
898
890
|
projectId: split.projectId,
|
|
899
891
|
token: token,
|
|
@@ -905,7 +897,6 @@ library JB721TiersHookLib {
|
|
|
905
897
|
}) {
|
|
906
898
|
return true;
|
|
907
899
|
} catch (bytes memory reason) {
|
|
908
|
-
// slither-disable-next-line reentrancy-events
|
|
909
900
|
emit SplitPayoutReverted({
|
|
910
901
|
projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
|
|
911
902
|
});
|
|
@@ -913,7 +904,6 @@ library JB721TiersHookLib {
|
|
|
913
904
|
}
|
|
914
905
|
} else {
|
|
915
906
|
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: amount});
|
|
916
|
-
// slither-disable-next-line unused-return,calls-loop,reentrancy-no-eth,reentrancy-events
|
|
917
907
|
try terminal.pay({
|
|
918
908
|
projectId: split.projectId,
|
|
919
909
|
token: token,
|
|
@@ -927,7 +917,6 @@ library JB721TiersHookLib {
|
|
|
927
917
|
} catch (bytes memory reason) {
|
|
928
918
|
// Reset approval on failure so tokens aren't left approved to the terminal.
|
|
929
919
|
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
|
|
930
|
-
// slither-disable-next-line reentrancy-events
|
|
931
920
|
emit SplitPayoutReverted({
|
|
932
921
|
projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
|
|
933
922
|
});
|
|
@@ -937,7 +926,6 @@ library JB721TiersHookLib {
|
|
|
937
926
|
}
|
|
938
927
|
} else if (split.beneficiary != address(0)) {
|
|
939
928
|
if (isNativeToken) {
|
|
940
|
-
// slither-disable-next-line arbitrary-send-eth,calls-loop
|
|
941
929
|
(bool success,) = split.beneficiary.call{value: amount}("");
|
|
942
930
|
if (!success) return false;
|
|
943
931
|
} else {
|
|
@@ -945,7 +933,6 @@ library JB721TiersHookLib {
|
|
|
945
933
|
// false on failure instead of reverting. This handles non-standard tokens (e.g. USDT)
|
|
946
934
|
// that return void, while routing failed transfers to the project's balance instead
|
|
947
935
|
// of bricking all payments.
|
|
948
|
-
// slither-disable-next-line calls-loop
|
|
949
936
|
(bool callSuccess, bytes memory returndata) =
|
|
950
937
|
address(token).call(abi.encodeCall(IERC20.transfer, (split.beneficiary, amount)));
|
|
951
938
|
if (!callSuccess || (returndata.length != 0 && !abi.decode(returndata, (bool)))) return false;
|
|
@@ -32,7 +32,7 @@ library JBBitmap {
|
|
|
32
32
|
/// @dev The `index` is the index that the bit would have if the bitmap were reshaped to a 1*n matrix.
|
|
33
33
|
function isTierIdRemoved(mapping(uint256 => uint256) storage self, uint256 index) internal view returns (bool) {
|
|
34
34
|
uint256 depth = _retrieveDepth(index);
|
|
35
|
-
return isTierIdRemoved(JBBitmapWord({currentWord: self[depth], currentDepth: depth}), index);
|
|
35
|
+
return isTierIdRemoved({self: JBBitmapWord({currentWord: self[depth], currentDepth: depth}), index: index});
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/// @notice Set the bit at the given index to true, indicating that the corresponding tier has been removed.
|
|
@@ -8,7 +8,7 @@ pragma solidity ^0.8.0;
|
|
|
8
8
|
/// @custom:member noNewTiersWithOwnerMinting A boolean indicating whether attempts to add new tiers with
|
|
9
9
|
/// `allowOwnerMint` set to true will revert.
|
|
10
10
|
/// @custom:member preventOverspending A boolean indicating whether payments attempting to spend more than the price of
|
|
11
|
-
/// the NFTs
|
|
11
|
+
/// the NFTs to mint will revert.
|
|
12
12
|
/// @custom:member issueTokensForSplits A boolean indicating whether payers receive token credit for the portion of
|
|
13
13
|
/// their payment that is routed to tier splits. When false (default), weight is reduced proportionally.
|
|
14
14
|
struct JB721TiersHookFlags {
|