@bananapus/721-hook-v6 0.0.33 → 0.0.35
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/package.json
CHANGED
package/src/JB721TiersHook.sol
CHANGED
|
@@ -24,6 +24,7 @@ import {IJB721CheckpointsDeployer} from "./interfaces/IJB721CheckpointsDeployer.
|
|
|
24
24
|
import {IJB721TiersHook} from "./interfaces/IJB721TiersHook.sol";
|
|
25
25
|
import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
|
|
26
26
|
import {IJB721TokenUriResolver} from "./interfaces/IJB721TokenUriResolver.sol";
|
|
27
|
+
import {JB721Constants} from "./libraries/JB721Constants.sol";
|
|
27
28
|
import {JB721TiersHookLib} from "./libraries/JB721TiersHookLib.sol";
|
|
28
29
|
import {JB721TiersRulesetMetadataResolver} from "./libraries/JB721TiersRulesetMetadataResolver.sol";
|
|
29
30
|
import {JB721InitTiersConfig} from "./structs/JB721InitTiersConfig.sol";
|
|
@@ -109,7 +110,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
109
110
|
uint256 internal _packedPricingContext;
|
|
110
111
|
|
|
111
112
|
/// @notice The checkpoint module that manages IVotes-compatible checkpointed voting power for this hook's NFTs.
|
|
112
|
-
/// @dev
|
|
113
|
+
/// @dev Lazily deployed on the first transfer. Pass this to JBTokenDistributor as the IVotes token.
|
|
113
114
|
IJB721Checkpoints public override CHECKPOINTS;
|
|
114
115
|
|
|
115
116
|
//*********************************************************************//
|
|
@@ -201,40 +202,23 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
201
202
|
{
|
|
202
203
|
hookSpecifications = new JBPayHookSpecification[](1);
|
|
203
204
|
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// Convert split amounts from tier pricing to payment token denomination (if currencies differ)
|
|
210
|
-
// and cap at the actual payment value so the terminal never forwards more than was paid.
|
|
211
|
-
if (totalSplitAmount != 0) {
|
|
212
|
-
(totalSplitAmount, splitMetadata) = JB721TiersHookLib.convertAndCapSplitAmounts({
|
|
213
|
-
totalSplitAmount: totalSplitAmount,
|
|
214
|
-
splitMetadata: splitMetadata,
|
|
215
|
-
packedPricingContext: _packedPricingContext,
|
|
216
|
-
prices: PRICES,
|
|
217
|
-
projectId: context.projectId,
|
|
218
|
-
amountCurrency: context.amount.currency,
|
|
219
|
-
amountDecimals: context.amount.decimals,
|
|
220
|
-
amountValue: context.amount.value
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Adjust weight so the terminal mints tokens only for the amount that actually enters the project.
|
|
225
|
-
weight = JB721TiersHookLib.calculateWeight({
|
|
226
|
-
contextWeight: context.weight,
|
|
227
|
-
amountValue: context.amount.value,
|
|
228
|
-
totalSplitAmount: totalSplitAmount,
|
|
205
|
+
// Compute split amounts, adjusted weight, and resolved beneficiary in a single library call.
|
|
206
|
+
uint256 totalSplitAmount;
|
|
207
|
+
bytes memory splitMetadata;
|
|
208
|
+
address beneficiary;
|
|
209
|
+
(weight, totalSplitAmount, splitMetadata, beneficiary) = JB721TiersHookLib.computeSplitsAndWeight({
|
|
229
210
|
store: STORE,
|
|
230
|
-
|
|
211
|
+
metadataIdTarget: METADATA_ID_TARGET,
|
|
212
|
+
packedPricingContext: _packedPricingContext,
|
|
213
|
+
prices: PRICES,
|
|
214
|
+
context: context
|
|
231
215
|
});
|
|
232
216
|
|
|
233
217
|
hookSpecifications[0] = JBPayHookSpecification({
|
|
234
218
|
hook: this,
|
|
235
219
|
noop: false,
|
|
236
220
|
amount: totalSplitAmount,
|
|
237
|
-
metadata: abi.encode(
|
|
221
|
+
metadata: abi.encode(beneficiary, context.payer, splitMetadata)
|
|
238
222
|
});
|
|
239
223
|
}
|
|
240
224
|
|
|
@@ -323,9 +307,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
323
307
|
|| flags.preventOverspending || flags.issueTokensForSplits
|
|
324
308
|
) STORE.recordFlags(flags);
|
|
325
309
|
|
|
326
|
-
// Deploy the checkpoint module for IVotes-compatible voting power.
|
|
327
|
-
CHECKPOINTS = CHECKPOINTS_DEPLOYER.deploy({hook: address(this), store: STORE});
|
|
328
|
-
|
|
329
310
|
// Transfer ownership to the initializer.
|
|
330
311
|
_transferOwnership(_msgSender());
|
|
331
312
|
}
|
|
@@ -662,74 +643,27 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
662
643
|
// Keep a reference to the number of NFT credits the beneficiary already has.
|
|
663
644
|
uint256 payCredits = payCreditsOf[beneficiary];
|
|
664
645
|
|
|
665
|
-
//
|
|
666
|
-
uint256
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
unusedPayCredits = payCredits;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// Keep a reference to the boolean indicating whether paying more than the price of the NFTs being minted
|
|
678
|
-
// is allowed. Defaults to the collection's flag.
|
|
679
|
-
bool allowOverspending = !STORE.flagsOf(address(this)).preventOverspending;
|
|
680
|
-
|
|
681
|
-
// Resolve the metadata.
|
|
682
|
-
(bool found, bytes memory metadata) = JBMetadataResolver.getDataFor({
|
|
683
|
-
id: JBMetadataResolver.getId({purpose: "pay", target: METADATA_ID_TARGET}), metadata: payerMetadata
|
|
646
|
+
// Compute the mint: combine credits, decode metadata, record mint, and check overspending.
|
|
647
|
+
(uint256[] memory tokenIds, uint16[] memory tierIdsToMint, uint256 newPayCredits) = JB721TiersHookLib.prepareMint({
|
|
648
|
+
store: STORE,
|
|
649
|
+
metadataIdTarget: METADATA_ID_TARGET,
|
|
650
|
+
value: value,
|
|
651
|
+
payer: payer,
|
|
652
|
+
beneficiary: beneficiary,
|
|
653
|
+
payCredits: payCredits,
|
|
654
|
+
payerMetadata: payerMetadata
|
|
684
655
|
});
|
|
685
656
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
// Decode the metadata.
|
|
694
|
-
(payerAllowsOverspending, tierIdsToMint) = abi.decode(metadata, (bool, uint16[]));
|
|
695
|
-
|
|
696
|
-
// Make sure overspending is allowed if requested.
|
|
697
|
-
if (allowOverspending && !payerAllowsOverspending) {
|
|
698
|
-
allowOverspending = false;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// Mint NFTs from the tiers as specified.
|
|
702
|
-
if (tierIdsToMint.length != 0) {
|
|
703
|
-
uint256[] memory tokenIds;
|
|
704
|
-
uint256 restrictedCost;
|
|
705
|
-
uint256 totalAmountPaid = leftoverAmount;
|
|
706
|
-
|
|
707
|
-
// Record the mints.
|
|
708
|
-
// slither-disable-next-line reentrancy-events,reentrancy-no-eth
|
|
709
|
-
(tokenIds, leftoverAmount, restrictedCost) =
|
|
710
|
-
STORE.recordMint({amount: leftoverAmount, tierIds: tierIdsToMint, isOwnerMint: false});
|
|
711
|
-
|
|
712
|
-
// Enforce `cantBuyWithCredits`: only tiers explicitly configured as credit-restricted must be fully
|
|
713
|
-
// covered by fresh payment (not stored credits). Split-bearing tiers are not automatically restricted;
|
|
714
|
-
// deployers must set that flag in tier configuration when they need that invariant.
|
|
715
|
-
if (restrictedCost > value) revert JB721TiersHook_CantBuyWithCredits();
|
|
716
|
-
|
|
717
|
-
// Mint each token to the effective beneficiary.
|
|
718
|
-
_mintTokens({
|
|
719
|
-
tokenIds: tokenIds,
|
|
720
|
-
tierIds: tierIdsToMint,
|
|
721
|
-
beneficiary: beneficiary,
|
|
722
|
-
totalAmountPaid: totalAmountPaid
|
|
723
|
-
});
|
|
724
|
-
}
|
|
657
|
+
// Mint each token to the effective beneficiary.
|
|
658
|
+
if (tokenIds.length != 0) {
|
|
659
|
+
// totalAmountPaid is the full amount available before recordMint deducted tier prices.
|
|
660
|
+
uint256 totalAmountPaid = (payer == beneficiary) ? value + payCredits : value;
|
|
661
|
+
_mintTokens({
|
|
662
|
+
tokenIds: tokenIds, tierIds: tierIdsToMint, beneficiary: beneficiary, totalAmountPaid: totalAmountPaid
|
|
663
|
+
});
|
|
725
664
|
}
|
|
726
665
|
|
|
727
|
-
// If overspending isn't allowed, revert.
|
|
728
|
-
if (leftoverAmount != 0 && !allowOverspending) revert JB721TiersHook_Overspending(leftoverAmount);
|
|
729
|
-
|
|
730
666
|
// Update NFT credits if they changed.
|
|
731
|
-
uint256 newPayCredits = leftoverAmount + unusedPayCredits;
|
|
732
|
-
|
|
733
667
|
if (newPayCredits != payCredits) {
|
|
734
668
|
if (newPayCredits > payCredits) {
|
|
735
669
|
emit AddPayCredits({
|
|
@@ -747,6 +681,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
747
681
|
});
|
|
748
682
|
}
|
|
749
683
|
|
|
684
|
+
// slither-disable-next-line reentrancy-no-eth
|
|
750
685
|
payCreditsOf[beneficiary] = newPayCredits;
|
|
751
686
|
}
|
|
752
687
|
}
|
|
@@ -856,6 +791,11 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
856
791
|
// slither-disable-next-line reentrency-events,calls-loop
|
|
857
792
|
STORE.recordTransferForTier({tierId: tierId, from: from, to: to});
|
|
858
793
|
|
|
794
|
+
// Deploy the checkpoint module lazily on the first transfer.
|
|
795
|
+
if (address(CHECKPOINTS) == address(0)) {
|
|
796
|
+
CHECKPOINTS = CHECKPOINTS_DEPLOYER.deploy({hook: address(this), store: STORE});
|
|
797
|
+
}
|
|
798
|
+
|
|
859
799
|
// Notify the checkpoint module to update checkpointed voting power.
|
|
860
800
|
CHECKPOINTS.onTransfer({from: from, to: to, tokenId: tokenId});
|
|
861
801
|
}
|
|
@@ -4,4 +4,10 @@ pragma solidity 0.8.28;
|
|
|
4
4
|
/// @notice Global constants used across 721 hook contracts.
|
|
5
5
|
library JB721Constants {
|
|
6
6
|
uint16 public constant DISCOUNT_DENOMINATOR = 200;
|
|
7
|
+
|
|
8
|
+
/// @notice The metadata ID used to identify the 721 beneficiary entry in payment metadata.
|
|
9
|
+
/// @dev When a sucker pays on behalf of a remote user, the real user's address is embedded under this key
|
|
10
|
+
/// so NFTs mint to the correct recipient.
|
|
11
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
12
|
+
bytes4 public constant BENEFICIARY_METADATA_ID = bytes4(keccak256("JB_721_BENEFICIARY"));
|
|
7
13
|
}
|
|
@@ -6,32 +6,50 @@ import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
|
|
|
6
6
|
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
7
7
|
import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
|
|
8
8
|
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
9
|
-
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
10
|
-
import {JBSplitHookContext} from "@bananapus/core-v6/src/structs/JBSplitHookContext.sol";
|
|
11
9
|
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
12
10
|
import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
11
|
+
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
12
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
13
|
+
import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
|
|
14
|
+
import {JBSplitHookContext} from "@bananapus/core-v6/src/structs/JBSplitHookContext.sol";
|
|
13
15
|
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
14
16
|
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
15
17
|
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
16
18
|
|
|
17
|
-
import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
|
|
18
|
-
|
|
19
19
|
import {IJB721TiersHookStore} from "../interfaces/IJB721TiersHookStore.sol";
|
|
20
20
|
import {IJB721TokenUriResolver} from "../interfaces/IJB721TokenUriResolver.sol";
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
import {JB721Constants} from "./JB721Constants.sol";
|
|
23
23
|
import {JBIpfsDecoder} from "./JBIpfsDecoder.sol";
|
|
24
24
|
|
|
25
|
+
import {JB721TierConfig} from "../structs/JB721TierConfig.sol";
|
|
26
|
+
|
|
25
27
|
/// @notice External library for JB721TiersHook operations extracted to stay within the EIP-170 contract size limit.
|
|
26
28
|
/// @dev Handles tier adjustments, split calculations, price normalization, and split fund distribution.
|
|
27
29
|
library JB721TiersHookLib {
|
|
30
|
+
//*********************************************************************//
|
|
31
|
+
// --------------------------- custom errors ------------------------- //
|
|
32
|
+
//*********************************************************************//
|
|
33
|
+
|
|
34
|
+
error JB721TiersHook_CantBuyWithCredits();
|
|
35
|
+
error JB721TiersHook_Overspending(uint256 leftoverAmount);
|
|
28
36
|
error JB721TiersHookLib_NoTerminalForLeftover(uint256 projectId, address token, uint256 leftoverAmount);
|
|
29
37
|
error JB721TiersHookLib_SplitFallbackFailed(uint256 projectId, address token, uint256 amount, bytes reason);
|
|
30
38
|
error JB721TiersHookLib_TokenTransferAmountMismatch(uint256 expectedAmount, uint256 receivedAmount);
|
|
39
|
+
|
|
40
|
+
//*********************************************************************//
|
|
41
|
+
// ------------------------------- events ---------------------------- //
|
|
42
|
+
//*********************************************************************//
|
|
43
|
+
|
|
31
44
|
event AddTier(uint256 indexed tierId, JB721TierConfig tier, address caller);
|
|
32
45
|
event RemoveTier(uint256 indexed tierId, address caller);
|
|
46
|
+
event SetDiscountPercent(uint256 indexed tierId, uint256 discountPercent, address caller);
|
|
33
47
|
event SplitPayoutReverted(uint256 indexed projectId, JBSplit split, uint256 amount, bytes reason, address caller);
|
|
34
48
|
|
|
49
|
+
//*********************************************************************//
|
|
50
|
+
// ---------------------- external transactions ---------------------- //
|
|
51
|
+
//*********************************************************************//
|
|
52
|
+
|
|
35
53
|
/// @notice Handles the full tier adjustment logic: removes tiers, adds tiers, emits events, and sets splits.
|
|
36
54
|
/// @dev Called via DELEGATECALL from the hook, so events are emitted from the hook's address.
|
|
37
55
|
/// @param store The 721 tiers hook store.
|
|
@@ -89,6 +107,137 @@ library JB721TiersHookLib {
|
|
|
89
107
|
}
|
|
90
108
|
}
|
|
91
109
|
|
|
110
|
+
/// @notice Pulls ERC-20 tokens from the terminal (if needed) and distributes forwarded funds to tier splits.
|
|
111
|
+
/// @dev For ERC-20 tokens, pulls from the terminal using the allowance it granted via _beforeTransferTo.
|
|
112
|
+
/// @param directory The directory to look up terminals.
|
|
113
|
+
/// @param splits The splits contract to read tier split groups from.
|
|
114
|
+
/// @param projectId The project ID of the hook.
|
|
115
|
+
/// @param hookAddress The hook address (for computing split group IDs).
|
|
116
|
+
/// @param token The token being distributed.
|
|
117
|
+
/// @param amount The total amount to distribute.
|
|
118
|
+
/// @param decimals The token decimals.
|
|
119
|
+
/// @param encodedSplitData The encoded per-tier breakdown from hookMetadata.
|
|
120
|
+
function distributeAll(
|
|
121
|
+
IJBDirectory directory,
|
|
122
|
+
IJBSplits splits,
|
|
123
|
+
uint256 projectId,
|
|
124
|
+
address hookAddress,
|
|
125
|
+
address token,
|
|
126
|
+
uint256 amount,
|
|
127
|
+
uint256 decimals,
|
|
128
|
+
bytes calldata encodedSplitData
|
|
129
|
+
)
|
|
130
|
+
external
|
|
131
|
+
{
|
|
132
|
+
// For ERC20 tokens, pull from terminal using the allowance it granted via _beforeTransferTo.
|
|
133
|
+
if (token != JBConstants.NATIVE_TOKEN) {
|
|
134
|
+
uint256 balanceBefore = IERC20(token).balanceOf(address(this));
|
|
135
|
+
SafeERC20.safeTransferFrom({token: IERC20(token), from: msg.sender, to: address(this), value: amount});
|
|
136
|
+
uint256 receivedAmount = IERC20(token).balanceOf(address(this)) - balanceBefore;
|
|
137
|
+
if (receivedAmount != amount) {
|
|
138
|
+
revert JB721TiersHookLib_TokenTransferAmountMismatch(amount, receivedAmount);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
(uint16[] memory tierIds, uint256[] memory amounts) = abi.decode(encodedSplitData, (uint16[], uint256[]));
|
|
143
|
+
|
|
144
|
+
for (uint256 i; i < tierIds.length;) {
|
|
145
|
+
if (amounts[i] == 0) {
|
|
146
|
+
unchecked {
|
|
147
|
+
++i;
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
uint256 groupId = uint256(uint160(hookAddress)) | (uint256(tierIds[i]) << 160);
|
|
152
|
+
_distributeSingleSplit({
|
|
153
|
+
directory: directory,
|
|
154
|
+
splitsContract: splits,
|
|
155
|
+
projectId: projectId,
|
|
156
|
+
token: token,
|
|
157
|
+
groupId: groupId,
|
|
158
|
+
amount: amounts[i],
|
|
159
|
+
decimals: decimals
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
unchecked {
|
|
163
|
+
++i;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// @notice Prepares NFT minting data for a payment: combines credits, decodes metadata, records mint, and checks
|
|
169
|
+
/// overspending.
|
|
170
|
+
/// @dev Called via DELEGATECALL from the hook; uses address(this) as the hook address.
|
|
171
|
+
/// Reverts with JB721TiersHook_CantBuyWithCredits or JB721TiersHook_Overspending on failure.
|
|
172
|
+
/// @param store The 721 tiers hook store.
|
|
173
|
+
/// @param metadataIdTarget The metadata ID target for resolving pay metadata.
|
|
174
|
+
/// @param value The normalized payment value.
|
|
175
|
+
/// @param payer The address that initiated the payment.
|
|
176
|
+
/// @param beneficiary The address to mint NFTs to and track credits for.
|
|
177
|
+
/// @param payCredits The beneficiary's current pay credits balance.
|
|
178
|
+
/// @param payerMetadata The metadata provided by the payer.
|
|
179
|
+
/// @return tokenIds The token IDs minted (empty if none).
|
|
180
|
+
/// @return tierIdsToMint The tier IDs corresponding to each minted token (empty if none).
|
|
181
|
+
/// @return newPayCredits The beneficiary's updated pay credits balance.
|
|
182
|
+
function prepareMint(
|
|
183
|
+
IJB721TiersHookStore store,
|
|
184
|
+
address metadataIdTarget,
|
|
185
|
+
uint256 value,
|
|
186
|
+
address payer,
|
|
187
|
+
address beneficiary,
|
|
188
|
+
uint256 payCredits,
|
|
189
|
+
bytes calldata payerMetadata
|
|
190
|
+
)
|
|
191
|
+
external
|
|
192
|
+
returns (uint256[] memory tokenIds, uint16[] memory tierIdsToMint, uint256 newPayCredits)
|
|
193
|
+
{
|
|
194
|
+
// Resolve metadata first (minimal stack: only 3 return vars). Scope block frees temporaries.
|
|
195
|
+
bool payerDisallowsOverspending;
|
|
196
|
+
{
|
|
197
|
+
(bool found, bytes memory metadata) = JBMetadataResolver.getDataFor({
|
|
198
|
+
id: JBMetadataResolver.getId({purpose: "pay", target: metadataIdTarget}), metadata: payerMetadata
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (found) {
|
|
202
|
+
bool payerAllowsOverspending;
|
|
203
|
+
(payerAllowsOverspending, tierIdsToMint) = abi.decode(metadata, (bool, uint16[]));
|
|
204
|
+
payerDisallowsOverspending = !payerAllowsOverspending;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Set the leftover amount as the initial value.
|
|
209
|
+
uint256 leftoverAmount = value;
|
|
210
|
+
|
|
211
|
+
// If the payer is the effective beneficiary, combine their NFT credits with the amount paid.
|
|
212
|
+
// Reuse newPayCredits to hold unused credits (avoids an extra local variable).
|
|
213
|
+
if (payer == beneficiary) {
|
|
214
|
+
leftoverAmount += payCredits;
|
|
215
|
+
} else {
|
|
216
|
+
newPayCredits = payCredits;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Determine whether overspending is allowed (collection flag AND payer preference).
|
|
220
|
+
bool allowOverspending = !store.flagsOf(address(this)).preventOverspending && !payerDisallowsOverspending;
|
|
221
|
+
|
|
222
|
+
// Record the mints.
|
|
223
|
+
if (tierIdsToMint.length != 0) {
|
|
224
|
+
uint256 restrictedCost;
|
|
225
|
+
|
|
226
|
+
// slither-disable-next-line reentrancy-events,reentrancy-no-eth
|
|
227
|
+
(tokenIds, leftoverAmount, restrictedCost) =
|
|
228
|
+
store.recordMint({amount: leftoverAmount, tierIds: tierIdsToMint, isOwnerMint: false});
|
|
229
|
+
|
|
230
|
+
// Credit-restricted tiers must be fully covered by fresh payment (not stored credits).
|
|
231
|
+
if (restrictedCost > value) revert JB721TiersHook_CantBuyWithCredits();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// If overspending isn't allowed, revert.
|
|
235
|
+
if (leftoverAmount != 0 && !allowOverspending) revert JB721TiersHook_Overspending(leftoverAmount);
|
|
236
|
+
|
|
237
|
+
// Compute the new pay credits balance: leftover + unused credits (held in newPayCredits).
|
|
238
|
+
newPayCredits = leftoverAmount + newPayCredits;
|
|
239
|
+
}
|
|
240
|
+
|
|
92
241
|
/// @notice Records new tiers, emits events, and sets their split groups.
|
|
93
242
|
/// @dev Used during initialization when tier configs are in memory.
|
|
94
243
|
/// @param store The 721 tiers hook store.
|
|
@@ -128,6 +277,87 @@ library JB721TiersHookLib {
|
|
|
128
277
|
});
|
|
129
278
|
}
|
|
130
279
|
|
|
280
|
+
/// @notice Set the discount percent for a tier, emitting an event and recording it in the store.
|
|
281
|
+
/// @param store The 721 tiers hook store.
|
|
282
|
+
/// @param tierId The ID of the tier.
|
|
283
|
+
/// @param discountPercent The discount percent to set.
|
|
284
|
+
/// @param caller The msg.sender of the original call.
|
|
285
|
+
function setDiscountPercentOf(
|
|
286
|
+
IJB721TiersHookStore store,
|
|
287
|
+
uint256 tierId,
|
|
288
|
+
uint256 discountPercent,
|
|
289
|
+
address caller
|
|
290
|
+
)
|
|
291
|
+
external
|
|
292
|
+
{
|
|
293
|
+
emit SetDiscountPercent({tierId: tierId, discountPercent: discountPercent, caller: caller});
|
|
294
|
+
store.recordSetDiscountPercentOf({tierId: tierId, discountPercent: discountPercent});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
//*********************************************************************//
|
|
298
|
+
// ----------------------- external views ---------------------------- //
|
|
299
|
+
//*********************************************************************//
|
|
300
|
+
|
|
301
|
+
/// @notice Computes split amounts, weight adjustment, and resolved beneficiary for a payment.
|
|
302
|
+
/// @dev Called via DELEGATECALL from the hook; uses address(this) as the hook address.
|
|
303
|
+
/// @param store The 721 tiers hook store.
|
|
304
|
+
/// @param metadataIdTarget The metadata ID target for resolving pay metadata.
|
|
305
|
+
/// @param packedPricingContext The packed pricing context (currency, decimals).
|
|
306
|
+
/// @param prices The prices contract used for currency conversion.
|
|
307
|
+
/// @param context The full payment context.
|
|
308
|
+
/// @return weight The adjusted weight for token minting.
|
|
309
|
+
/// @return totalSplitAmount The total amount to forward for splits.
|
|
310
|
+
/// @return splitMetadata Encoded per-tier breakdown (tierIds, amounts).
|
|
311
|
+
/// @return beneficiary The resolved beneficiary address.
|
|
312
|
+
function computeSplitsAndWeight(
|
|
313
|
+
IJB721TiersHookStore store,
|
|
314
|
+
address metadataIdTarget,
|
|
315
|
+
uint256 packedPricingContext,
|
|
316
|
+
IJBPrices prices,
|
|
317
|
+
JBBeforePayRecordedContext calldata context
|
|
318
|
+
)
|
|
319
|
+
external
|
|
320
|
+
view
|
|
321
|
+
returns (uint256 weight, uint256 totalSplitAmount, bytes memory splitMetadata, address beneficiary)
|
|
322
|
+
{
|
|
323
|
+
// Calculate per-tier split amounts.
|
|
324
|
+
(totalSplitAmount, splitMetadata) = _calculateSplitAmounts({
|
|
325
|
+
store: store, hook: address(this), metadataIdTarget: metadataIdTarget, metadata: context.metadata
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Convert split amounts from tier pricing to payment token denomination (if currencies differ)
|
|
329
|
+
// and cap at the actual payment value so the terminal never forwards more than was paid.
|
|
330
|
+
if (totalSplitAmount != 0) {
|
|
331
|
+
(totalSplitAmount, splitMetadata) = _convertAndCapSplitAmounts({
|
|
332
|
+
totalSplitAmount: totalSplitAmount,
|
|
333
|
+
splitMetadata: splitMetadata,
|
|
334
|
+
packedPricingContext: packedPricingContext,
|
|
335
|
+
prices: prices,
|
|
336
|
+
context: context
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Adjust weight so the terminal mints tokens only for the amount that actually enters the project.
|
|
341
|
+
weight = _calculateWeight({
|
|
342
|
+
contextWeight: context.weight,
|
|
343
|
+
amountValue: context.amount.value,
|
|
344
|
+
totalSplitAmount: totalSplitAmount,
|
|
345
|
+
store: store,
|
|
346
|
+
hook: address(this)
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Resolve the effective beneficiary from payment metadata.
|
|
350
|
+
beneficiary = context.beneficiary;
|
|
351
|
+
{
|
|
352
|
+
(bool found, bytes memory data) =
|
|
353
|
+
JBMetadataResolver.getDataFor({id: JB721Constants.BENEFICIARY_METADATA_ID, metadata: context.metadata});
|
|
354
|
+
if (found && data.length >= 32) {
|
|
355
|
+
address metadataBeneficiary = abi.decode(data, (address));
|
|
356
|
+
if (metadataBeneficiary != address(0)) beneficiary = metadataBeneficiary;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
131
361
|
/// @notice Normalizes a payment value based on the packed pricing context.
|
|
132
362
|
/// @param packedPricingContext The packed pricing context (currency, decimals).
|
|
133
363
|
/// @param prices The prices contract used for currency conversion.
|
|
@@ -176,6 +406,40 @@ library JB721TiersHookLib {
|
|
|
176
406
|
valid = true;
|
|
177
407
|
}
|
|
178
408
|
|
|
409
|
+
/// @notice Resolves the token URI for a given NFT token ID.
|
|
410
|
+
/// @dev Extracted to the library to keep JBIpfsDecoder bytecode out of the hook contract (EIP-170 compliance).
|
|
411
|
+
/// @param store The 721 tiers hook store.
|
|
412
|
+
/// @param hook The hook address.
|
|
413
|
+
/// @param baseUri The base URI for IPFS-based token URIs.
|
|
414
|
+
/// @param tokenId The token ID to resolve the URI for.
|
|
415
|
+
/// @return The resolved token URI string.
|
|
416
|
+
function resolveTokenURI(
|
|
417
|
+
IJB721TiersHookStore store,
|
|
418
|
+
address hook,
|
|
419
|
+
string memory baseUri,
|
|
420
|
+
uint256 tokenId
|
|
421
|
+
)
|
|
422
|
+
external
|
|
423
|
+
view
|
|
424
|
+
returns (string memory)
|
|
425
|
+
{
|
|
426
|
+
// Get a reference to the `tokenUriResolver`.
|
|
427
|
+
IJB721TokenUriResolver resolver = store.tokenUriResolverOf(hook);
|
|
428
|
+
|
|
429
|
+
// If a `tokenUriResolver` is set, use it to resolve the token URI.
|
|
430
|
+
if (address(resolver) != address(0)) return resolver.tokenUriOf({nft: hook, tokenId: tokenId});
|
|
431
|
+
|
|
432
|
+
// Otherwise, return the token URI corresponding with the NFT's tier.
|
|
433
|
+
return
|
|
434
|
+
JBIpfsDecoder.decode({
|
|
435
|
+
baseUri: baseUri, hexString: store.encodedTierIPFSUriOf({hook: hook, tokenId: tokenId})
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
//*********************************************************************//
|
|
440
|
+
// ----------------------- internal views ---------------------------- //
|
|
441
|
+
//*********************************************************************//
|
|
442
|
+
|
|
179
443
|
/// @notice Calculates per-tier split amounts for a pay event.
|
|
180
444
|
/// @param store The 721 tiers hook store.
|
|
181
445
|
/// @param hook The hook address.
|
|
@@ -183,13 +447,13 @@ library JB721TiersHookLib {
|
|
|
183
447
|
/// @param metadata The payer metadata.
|
|
184
448
|
/// @return totalSplitAmount The total amount to forward for splits.
|
|
185
449
|
/// @return hookMetadata Encoded per-tier breakdown (tierIds, amounts) for afterPay.
|
|
186
|
-
function
|
|
450
|
+
function _calculateSplitAmounts(
|
|
187
451
|
IJB721TiersHookStore store,
|
|
188
452
|
address hook,
|
|
189
453
|
address metadataIdTarget,
|
|
190
454
|
bytes calldata metadata
|
|
191
455
|
)
|
|
192
|
-
|
|
456
|
+
internal
|
|
193
457
|
view
|
|
194
458
|
returns (uint256 totalSplitAmount, bytes memory hookMetadata)
|
|
195
459
|
{
|
|
@@ -256,14 +520,14 @@ library JB721TiersHookLib {
|
|
|
256
520
|
/// @param store The 721 tiers hook store (to read flags).
|
|
257
521
|
/// @param hook The hook address.
|
|
258
522
|
/// @return weight The adjusted weight for token minting.
|
|
259
|
-
function
|
|
523
|
+
function _calculateWeight(
|
|
260
524
|
uint256 contextWeight,
|
|
261
525
|
uint256 amountValue,
|
|
262
526
|
uint256 totalSplitAmount,
|
|
263
527
|
IJB721TiersHookStore store,
|
|
264
528
|
address hook
|
|
265
529
|
)
|
|
266
|
-
|
|
530
|
+
internal
|
|
267
531
|
view
|
|
268
532
|
returns (uint256 weight)
|
|
269
533
|
{
|
|
@@ -281,28 +545,22 @@ library JB721TiersHookLib {
|
|
|
281
545
|
|
|
282
546
|
/// @notice Converts split amounts from tier pricing to payment denomination (if currencies differ), then caps
|
|
283
547
|
/// the total at the actual payment value — proportionally reducing per-tier amounts when the cap applies.
|
|
284
|
-
/// @dev Combines currency conversion and cap into one
|
|
548
|
+
/// @dev Combines currency conversion and cap into one call to keep hook bytecode under EIP-170.
|
|
285
549
|
/// @param totalSplitAmount The total split amount in tier pricing denomination.
|
|
286
550
|
/// @param splitMetadata The encoded per-tier breakdown (tierIds, amounts).
|
|
287
551
|
/// @param packedPricingContext The packed pricing context (currency in bits 0-31, decimals in bits 32-39).
|
|
288
552
|
/// @param prices The prices contract used for currency conversion.
|
|
289
|
-
/// @param
|
|
290
|
-
/// @param amountCurrency The payment amount currency.
|
|
291
|
-
/// @param amountDecimals The payment amount decimals.
|
|
292
|
-
/// @param amountValue The actual payment value (used as the cap).
|
|
553
|
+
/// @param context The full payment context (provides projectId, amount currency/decimals/value).
|
|
293
554
|
/// @return convertedTotal The total split amount after conversion and capping.
|
|
294
555
|
/// @return convertedMetadata The re-encoded per-tier breakdown with adjusted amounts.
|
|
295
|
-
function
|
|
556
|
+
function _convertAndCapSplitAmounts(
|
|
296
557
|
uint256 totalSplitAmount,
|
|
297
558
|
bytes memory splitMetadata,
|
|
298
559
|
uint256 packedPricingContext,
|
|
299
560
|
IJBPrices prices,
|
|
300
|
-
|
|
301
|
-
uint256 amountCurrency,
|
|
302
|
-
uint256 amountDecimals,
|
|
303
|
-
uint256 amountValue
|
|
561
|
+
JBBeforePayRecordedContext calldata context
|
|
304
562
|
)
|
|
305
|
-
|
|
563
|
+
internal
|
|
306
564
|
view
|
|
307
565
|
returns (uint256 convertedTotal, bytes memory convertedMetadata)
|
|
308
566
|
{
|
|
@@ -316,7 +574,7 @@ library JB721TiersHookLib {
|
|
|
316
574
|
|
|
317
575
|
// Convert each per-tier amount from the tier pricing currency to the payment currency.
|
|
318
576
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
319
|
-
if (
|
|
577
|
+
if (context.amount.currency != uint256(uint32(packedPricingContext))) {
|
|
320
578
|
// No price oracle available — return 0 to skip the split rather than forwarding an unconverted
|
|
321
579
|
// amount denominated in the wrong currency, which would over- or under-pay.
|
|
322
580
|
if (address(prices) == address(0)) return (0, convertedMetadata);
|
|
@@ -325,11 +583,11 @@ library JB721TiersHookLib {
|
|
|
325
583
|
// Get the price ratio: how many payment-currency units per one tier-pricing-currency unit.
|
|
326
584
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
327
585
|
uint256 ratio = prices.pricePerUnitOf({
|
|
328
|
-
projectId: projectId,
|
|
329
|
-
pricingCurrency:
|
|
586
|
+
projectId: context.projectId,
|
|
587
|
+
pricingCurrency: context.amount.currency,
|
|
330
588
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
331
589
|
unitCurrency: uint256(uint32(packedPricingContext)),
|
|
332
|
-
decimals:
|
|
590
|
+
decimals: context.amount.decimals
|
|
333
591
|
});
|
|
334
592
|
|
|
335
593
|
// The denominator scales each amount from tier-pricing decimals to payment-token decimals.
|
|
@@ -354,7 +612,7 @@ library JB721TiersHookLib {
|
|
|
354
612
|
// Re-encode with the converted amounts.
|
|
355
613
|
convertedMetadata = abi.encode(tierIds, amounts);
|
|
356
614
|
}
|
|
357
|
-
} else if (
|
|
615
|
+
} else if (context.amount.decimals != pricingDecimals) {
|
|
358
616
|
// Same currency but different decimal scales (e.g. pricing at 18 decimals, payment token at 6).
|
|
359
617
|
// Without this branch, split amounts stay in pricing decimals while the cap comparison uses
|
|
360
618
|
// payment decimals — causing orders-of-magnitude mis-scaling. This mirrors the same-currency
|
|
@@ -367,12 +625,12 @@ library JB721TiersHookLib {
|
|
|
367
625
|
convertedTotal = 0;
|
|
368
626
|
for (uint256 i; i < amounts.length;) {
|
|
369
627
|
// Scale each amount from pricing decimals to payment decimals.
|
|
370
|
-
if (
|
|
628
|
+
if (context.amount.decimals > pricingDecimals) {
|
|
371
629
|
// Payment has more decimals — multiply to add precision (e.g. 6→18: multiply by 10^12).
|
|
372
|
-
amounts[i] = amounts[i] * (10 ** (
|
|
630
|
+
amounts[i] = amounts[i] * (10 ** (context.amount.decimals - pricingDecimals));
|
|
373
631
|
} else {
|
|
374
632
|
// Payment has fewer decimals — divide to remove precision (e.g. 18→6: divide by 10^12).
|
|
375
|
-
amounts[i] = amounts[i] / (10 ** (pricingDecimals -
|
|
633
|
+
amounts[i] = amounts[i] / (10 ** (pricingDecimals - context.amount.decimals));
|
|
376
634
|
}
|
|
377
635
|
convertedTotal += amounts[i];
|
|
378
636
|
|
|
@@ -388,7 +646,7 @@ library JB721TiersHookLib {
|
|
|
388
646
|
// Cap the total at the actual payment value. Pay credits fund NFT minting (virtual), but splits
|
|
389
647
|
// require real tokens to distribute. Without this cap, a user with sufficient pay credits but
|
|
390
648
|
// insufficient ETH would revert because the terminal can't forward more than what was actually paid.
|
|
391
|
-
if (convertedTotal >
|
|
649
|
+
if (convertedTotal > context.amount.value) {
|
|
392
650
|
// Proportionally reduce each per-tier amount to stay in sync with the capped total.
|
|
393
651
|
if (convertedMetadata.length != 0) {
|
|
394
652
|
(uint16[] memory tierIds, uint256[] memory amounts) =
|
|
@@ -397,7 +655,7 @@ library JB721TiersHookLib {
|
|
|
397
655
|
convertedTotal = 0;
|
|
398
656
|
for (uint256 i; i < amounts.length;) {
|
|
399
657
|
// Scale down: amount * amountValue / originalTotal.
|
|
400
|
-
amounts[i] = mulDiv({x: amounts[i], y:
|
|
658
|
+
amounts[i] = mulDiv({x: amounts[i], y: context.amount.value, denominator: uncappedTotal});
|
|
401
659
|
convertedTotal += amounts[i];
|
|
402
660
|
|
|
403
661
|
unchecked {
|
|
@@ -407,104 +665,14 @@ library JB721TiersHookLib {
|
|
|
407
665
|
convertedMetadata = abi.encode(tierIds, amounts);
|
|
408
666
|
} else {
|
|
409
667
|
// Clamp the total to the payment value.
|
|
410
|
-
convertedTotal =
|
|
668
|
+
convertedTotal = context.amount.value;
|
|
411
669
|
}
|
|
412
670
|
}
|
|
413
671
|
}
|
|
414
672
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
uint256 projectId,
|
|
419
|
-
address hookAddress,
|
|
420
|
-
JB721TierConfig[] memory tiersToAdd,
|
|
421
|
-
uint256[] memory tierIdsAdded
|
|
422
|
-
)
|
|
423
|
-
private
|
|
424
|
-
{
|
|
425
|
-
uint256 splitGroupCount;
|
|
426
|
-
for (uint256 i; i < tiersToAdd.length;) {
|
|
427
|
-
if (tiersToAdd[i].splits.length != 0) splitGroupCount++;
|
|
428
|
-
|
|
429
|
-
unchecked {
|
|
430
|
-
++i;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
if (splitGroupCount == 0) return;
|
|
434
|
-
|
|
435
|
-
JBSplitGroup[] memory splitGroups = new JBSplitGroup[](splitGroupCount);
|
|
436
|
-
uint256 groupIndex;
|
|
437
|
-
for (uint256 i; i < tiersToAdd.length;) {
|
|
438
|
-
if (tiersToAdd[i].splits.length != 0) {
|
|
439
|
-
splitGroups[groupIndex] = JBSplitGroup({
|
|
440
|
-
groupId: uint256(uint160(hookAddress)) | (tierIdsAdded[i] << 160), splits: tiersToAdd[i].splits
|
|
441
|
-
});
|
|
442
|
-
groupIndex++;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
unchecked {
|
|
446
|
-
++i;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
splits.setSplitGroupsOf({projectId: projectId, rulesetId: 0, splitGroups: splitGroups});
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/// @notice Pulls ERC-20 tokens from the terminal (if needed) and distributes forwarded funds to tier splits.
|
|
453
|
-
/// @dev For ERC-20 tokens, pulls from the terminal using the allowance it granted via _beforeTransferTo.
|
|
454
|
-
/// @param directory The directory to look up terminals.
|
|
455
|
-
/// @param splits The splits contract to read tier split groups from.
|
|
456
|
-
/// @param projectId The project ID of the hook.
|
|
457
|
-
/// @param hookAddress The hook address (for computing split group IDs).
|
|
458
|
-
/// @param token The token being distributed.
|
|
459
|
-
/// @param amount The total amount to distribute.
|
|
460
|
-
/// @param encodedSplitData The encoded per-tier breakdown from hookMetadata.
|
|
461
|
-
function distributeAll(
|
|
462
|
-
IJBDirectory directory,
|
|
463
|
-
IJBSplits splits,
|
|
464
|
-
uint256 projectId,
|
|
465
|
-
address hookAddress,
|
|
466
|
-
address token,
|
|
467
|
-
uint256 amount,
|
|
468
|
-
uint256 decimals,
|
|
469
|
-
bytes calldata encodedSplitData
|
|
470
|
-
)
|
|
471
|
-
external
|
|
472
|
-
{
|
|
473
|
-
// For ERC20 tokens, pull from terminal using the allowance it granted via _beforeTransferTo.
|
|
474
|
-
if (token != JBConstants.NATIVE_TOKEN) {
|
|
475
|
-
uint256 balanceBefore = IERC20(token).balanceOf(address(this));
|
|
476
|
-
SafeERC20.safeTransferFrom({token: IERC20(token), from: msg.sender, to: address(this), value: amount});
|
|
477
|
-
uint256 receivedAmount = IERC20(token).balanceOf(address(this)) - balanceBefore;
|
|
478
|
-
if (receivedAmount != amount) {
|
|
479
|
-
revert JB721TiersHookLib_TokenTransferAmountMismatch(amount, receivedAmount);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
(uint16[] memory tierIds, uint256[] memory amounts) = abi.decode(encodedSplitData, (uint16[], uint256[]));
|
|
484
|
-
|
|
485
|
-
for (uint256 i; i < tierIds.length;) {
|
|
486
|
-
if (amounts[i] == 0) {
|
|
487
|
-
unchecked {
|
|
488
|
-
++i;
|
|
489
|
-
}
|
|
490
|
-
continue;
|
|
491
|
-
}
|
|
492
|
-
uint256 groupId = uint256(uint160(hookAddress)) | (uint256(tierIds[i]) << 160);
|
|
493
|
-
_distributeSingleSplit({
|
|
494
|
-
directory: directory,
|
|
495
|
-
splitsContract: splits,
|
|
496
|
-
projectId: projectId,
|
|
497
|
-
token: token,
|
|
498
|
-
groupId: groupId,
|
|
499
|
-
amount: amounts[i],
|
|
500
|
-
decimals: decimals
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
unchecked {
|
|
504
|
-
++i;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
673
|
+
//*********************************************************************//
|
|
674
|
+
// ----------------------- private helpers --------------------------- //
|
|
675
|
+
//*********************************************************************//
|
|
508
676
|
|
|
509
677
|
/// @notice Distributes funds for a single tier's split group.
|
|
510
678
|
/// @dev Edge case: if both `_sendPayoutToSplit` returns false (reverting hook/terminal/beneficiary) AND the
|
|
@@ -773,52 +941,40 @@ library JB721TiersHookLib {
|
|
|
773
941
|
return false;
|
|
774
942
|
}
|
|
775
943
|
|
|
776
|
-
/// @notice
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
function resolveTokenURI(
|
|
784
|
-
IJB721TiersHookStore store,
|
|
785
|
-
address hook,
|
|
786
|
-
string memory baseUri,
|
|
787
|
-
uint256 tokenId
|
|
944
|
+
/// @notice Sets split groups in JBSplits for tiers that have splits configured.
|
|
945
|
+
function _setSplitGroupsFor(
|
|
946
|
+
IJBSplits splits,
|
|
947
|
+
uint256 projectId,
|
|
948
|
+
address hookAddress,
|
|
949
|
+
JB721TierConfig[] memory tiersToAdd,
|
|
950
|
+
uint256[] memory tierIdsAdded
|
|
788
951
|
)
|
|
789
|
-
|
|
790
|
-
view
|
|
791
|
-
returns (string memory)
|
|
952
|
+
private
|
|
792
953
|
{
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
// If a `tokenUriResolver` is set, use it to resolve the token URI.
|
|
797
|
-
if (address(resolver) != address(0)) return resolver.tokenUriOf({nft: hook, tokenId: tokenId});
|
|
954
|
+
uint256 splitGroupCount;
|
|
955
|
+
for (uint256 i; i < tiersToAdd.length;) {
|
|
956
|
+
if (tiersToAdd[i].splits.length != 0) splitGroupCount++;
|
|
798
957
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
}
|
|
958
|
+
unchecked {
|
|
959
|
+
++i;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (splitGroupCount == 0) return;
|
|
805
963
|
|
|
806
|
-
|
|
964
|
+
JBSplitGroup[] memory splitGroups = new JBSplitGroup[](splitGroupCount);
|
|
965
|
+
uint256 groupIndex;
|
|
966
|
+
for (uint256 i; i < tiersToAdd.length;) {
|
|
967
|
+
if (tiersToAdd[i].splits.length != 0) {
|
|
968
|
+
splitGroups[groupIndex] = JBSplitGroup({
|
|
969
|
+
groupId: uint256(uint160(hookAddress)) | (tierIdsAdded[i] << 160), splits: tiersToAdd[i].splits
|
|
970
|
+
});
|
|
971
|
+
groupIndex++;
|
|
972
|
+
}
|
|
807
973
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
function setDiscountPercentOf(
|
|
814
|
-
IJB721TiersHookStore store,
|
|
815
|
-
uint256 tierId,
|
|
816
|
-
uint256 discountPercent,
|
|
817
|
-
address caller
|
|
818
|
-
)
|
|
819
|
-
external
|
|
820
|
-
{
|
|
821
|
-
emit SetDiscountPercent({tierId: tierId, discountPercent: discountPercent, caller: caller});
|
|
822
|
-
store.recordSetDiscountPercentOf({tierId: tierId, discountPercent: discountPercent});
|
|
974
|
+
unchecked {
|
|
975
|
+
++i;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
splits.setSplitGroupsOf({projectId: projectId, rulesetId: 0, splitGroups: splitGroups});
|
|
823
979
|
}
|
|
824
980
|
}
|
|
@@ -48,17 +48,29 @@ contract TestCheckpoints is UnitTestSetup {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// -------------------------------------------------------------------
|
|
51
|
-
// Test 1: Checkpoint module is deployed
|
|
51
|
+
// Test 1: Checkpoint module is deployed lazily on first transfer
|
|
52
52
|
// -------------------------------------------------------------------
|
|
53
|
-
function
|
|
53
|
+
function test_checkpointModule_isDeployedLazily() public {
|
|
54
54
|
defaultTierConfig.flags.allowOwnerMint = true;
|
|
55
55
|
defaultTierConfig.reserveFrequency = 0;
|
|
56
56
|
|
|
57
57
|
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(1);
|
|
58
58
|
|
|
59
|
-
// CHECKPOINTS should be deployed
|
|
59
|
+
// CHECKPOINTS should NOT be deployed after initialization (lazy deployment).
|
|
60
60
|
assertTrue(
|
|
61
|
-
address(tiersHook.CHECKPOINTS())
|
|
61
|
+
address(tiersHook.CHECKPOINTS()) == address(0),
|
|
62
|
+
"Checkpoint module should not be deployed after initialization"
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Mint a token to trigger lazy deployment.
|
|
66
|
+
uint16[] memory tiersToMint = new uint16[](1);
|
|
67
|
+
tiersToMint[0] = 1;
|
|
68
|
+
vm.prank(owner);
|
|
69
|
+
tiersHook.mintFor(tiersToMint, owner);
|
|
70
|
+
|
|
71
|
+
// CHECKPOINTS should now be deployed after the first mint.
|
|
72
|
+
assertTrue(
|
|
73
|
+
address(tiersHook.CHECKPOINTS()) != address(0), "Checkpoint module should be deployed after first mint"
|
|
62
74
|
);
|
|
63
75
|
}
|
|
64
76
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
|
+
import "../utils/UnitTestSetup.sol";
|
|
6
|
+
|
|
7
|
+
/// @notice Tests that the 721 hook resolves the relay beneficiary from payment metadata.
|
|
8
|
+
contract Test_relayBeneficiary_Unit is UnitTestSetup {
|
|
9
|
+
/// @notice The metadata ID the 721 hook uses to look up the relay beneficiary.
|
|
10
|
+
/// Must match `_721_BENEFICIARY_METADATA_ID` in JB721TiersHook.
|
|
11
|
+
/// @notice Must match `BENEFICIARY_METADATA_ID` in JB721TiersHook.
|
|
12
|
+
bytes4 constant BENEFICIARY_METADATA_ID = bytes4(keccak256("JB_721_BENEFICIARY"));
|
|
13
|
+
|
|
14
|
+
address relayUser = makeAddr("relayUser");
|
|
15
|
+
address sucker = makeAddr("sucker");
|
|
16
|
+
|
|
17
|
+
function setUp() public override {
|
|
18
|
+
super.setUp();
|
|
19
|
+
|
|
20
|
+
// Mock directory: terminal is valid for all calls.
|
|
21
|
+
vm.mockCall(
|
|
22
|
+
address(mockJBDirectory),
|
|
23
|
+
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
24
|
+
abi.encode(true)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// @notice When metadata contains a relay beneficiary, the spec should use that address (not context.beneficiary).
|
|
29
|
+
function test_beforePay_relayBeneficiary_usedInSpec() public {
|
|
30
|
+
JB721TiersHook tiersHook = _initHookDefaultTiers(3);
|
|
31
|
+
|
|
32
|
+
// Build metadata with tier selection + relay beneficiary.
|
|
33
|
+
uint16[] memory tierIdsToMint = new uint16[](1);
|
|
34
|
+
tierIdsToMint[0] = 1;
|
|
35
|
+
|
|
36
|
+
// Encode tier selection metadata (for the hook's own metadata ID).
|
|
37
|
+
bytes memory tierData = abi.encode(false, tierIdsToMint, new bytes[](0));
|
|
38
|
+
|
|
39
|
+
// Build metadata with two entries: hook tier data + relay beneficiary.
|
|
40
|
+
bytes4[] memory ids = new bytes4[](2);
|
|
41
|
+
bytes[] memory datas = new bytes[](2);
|
|
42
|
+
|
|
43
|
+
ids[0] = JBMetadataResolver.getId({purpose: "pay", target: address(tiersHook)});
|
|
44
|
+
datas[0] = tierData;
|
|
45
|
+
ids[1] = BENEFICIARY_METADATA_ID;
|
|
46
|
+
datas[1] = abi.encode(relayUser);
|
|
47
|
+
|
|
48
|
+
bytes memory metadata = metadataHelper.createMetadata(ids, datas);
|
|
49
|
+
|
|
50
|
+
// Build pay context with sucker as beneficiary (simulating cross-chain payment).
|
|
51
|
+
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
52
|
+
terminal: mockTerminalAddress,
|
|
53
|
+
payer: sucker,
|
|
54
|
+
amount: JBTokenAmount({
|
|
55
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
56
|
+
value: 10,
|
|
57
|
+
decimals: 18,
|
|
58
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
59
|
+
}),
|
|
60
|
+
projectId: projectId,
|
|
61
|
+
rulesetId: block.timestamp,
|
|
62
|
+
beneficiary: sucker,
|
|
63
|
+
weight: 10e18,
|
|
64
|
+
reservedPercent: 5000,
|
|
65
|
+
metadata: metadata
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
(, JBPayHookSpecification[] memory specs) = tiersHook.beforePayRecordedWith(context);
|
|
69
|
+
|
|
70
|
+
// The spec's metadata should encode relayUser as the beneficiary, not sucker.
|
|
71
|
+
assertEq(specs.length, 1, "should return one spec");
|
|
72
|
+
(address specBeneficiary,,) = abi.decode(specs[0].metadata, (address, address, bytes));
|
|
73
|
+
assertEq(specBeneficiary, relayUser, "spec should use relay beneficiary, not context.beneficiary");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// @notice When no relay metadata is present, the spec should use context.beneficiary as-is.
|
|
77
|
+
function test_beforePay_noRelayMetadata_usesContextBeneficiary() public {
|
|
78
|
+
JB721TiersHook tiersHook = _initHookDefaultTiers(3);
|
|
79
|
+
|
|
80
|
+
// Build metadata with only tier selection (no relay beneficiary).
|
|
81
|
+
uint16[] memory tierIdsToMint = new uint16[](1);
|
|
82
|
+
tierIdsToMint[0] = 1;
|
|
83
|
+
bytes memory tierData = abi.encode(false, tierIdsToMint, new bytes[](0));
|
|
84
|
+
|
|
85
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
86
|
+
bytes[] memory datas = new bytes[](1);
|
|
87
|
+
ids[0] = JBMetadataResolver.getId({purpose: "pay", target: address(tiersHook)});
|
|
88
|
+
datas[0] = tierData;
|
|
89
|
+
|
|
90
|
+
bytes memory metadata = metadataHelper.createMetadata(ids, datas);
|
|
91
|
+
|
|
92
|
+
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
93
|
+
terminal: mockTerminalAddress,
|
|
94
|
+
payer: makeAddr("payer"),
|
|
95
|
+
amount: JBTokenAmount({
|
|
96
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
97
|
+
value: 10,
|
|
98
|
+
decimals: 18,
|
|
99
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
100
|
+
}),
|
|
101
|
+
projectId: projectId,
|
|
102
|
+
rulesetId: block.timestamp,
|
|
103
|
+
beneficiary: beneficiary,
|
|
104
|
+
weight: 10e18,
|
|
105
|
+
reservedPercent: 5000,
|
|
106
|
+
metadata: metadata
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
(, JBPayHookSpecification[] memory specs) = tiersHook.beforePayRecordedWith(context);
|
|
110
|
+
|
|
111
|
+
assertEq(specs.length, 1, "should return one spec");
|
|
112
|
+
(address specBeneficiary,,) = abi.decode(specs[0].metadata, (address, address, bytes));
|
|
113
|
+
assertEq(specBeneficiary, beneficiary, "spec should use context.beneficiary when no relay metadata");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// @notice When relay metadata is present but the address is zero, fall back to context.beneficiary.
|
|
117
|
+
function test_beforePay_relayBeneficiaryZero_fallsBackToContext() public {
|
|
118
|
+
JB721TiersHook tiersHook = _initHookDefaultTiers(3);
|
|
119
|
+
|
|
120
|
+
uint16[] memory tierIdsToMint = new uint16[](1);
|
|
121
|
+
tierIdsToMint[0] = 1;
|
|
122
|
+
bytes memory tierData = abi.encode(false, tierIdsToMint, new bytes[](0));
|
|
123
|
+
|
|
124
|
+
bytes4[] memory ids = new bytes4[](2);
|
|
125
|
+
bytes[] memory datas = new bytes[](2);
|
|
126
|
+
ids[0] = JBMetadataResolver.getId({purpose: "pay", target: address(tiersHook)});
|
|
127
|
+
datas[0] = tierData;
|
|
128
|
+
ids[1] = BENEFICIARY_METADATA_ID;
|
|
129
|
+
datas[1] = abi.encode(address(0));
|
|
130
|
+
|
|
131
|
+
bytes memory metadata = metadataHelper.createMetadata(ids, datas);
|
|
132
|
+
|
|
133
|
+
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
134
|
+
terminal: mockTerminalAddress,
|
|
135
|
+
payer: sucker,
|
|
136
|
+
amount: JBTokenAmount({
|
|
137
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
138
|
+
value: 10,
|
|
139
|
+
decimals: 18,
|
|
140
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
141
|
+
}),
|
|
142
|
+
projectId: projectId,
|
|
143
|
+
rulesetId: block.timestamp,
|
|
144
|
+
beneficiary: beneficiary,
|
|
145
|
+
weight: 10e18,
|
|
146
|
+
reservedPercent: 5000,
|
|
147
|
+
metadata: metadata
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
(, JBPayHookSpecification[] memory specs) = tiersHook.beforePayRecordedWith(context);
|
|
151
|
+
|
|
152
|
+
(address specBeneficiary,,) = abi.decode(specs[0].metadata, (address, address, bytes));
|
|
153
|
+
assertEq(specBeneficiary, beneficiary, "zero relay address should fall back to context.beneficiary");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// @notice When metadata is empty, use context.beneficiary.
|
|
157
|
+
function test_beforePay_emptyMetadata_usesContextBeneficiary() public {
|
|
158
|
+
JB721TiersHook tiersHook = _initHookDefaultTiers(3);
|
|
159
|
+
|
|
160
|
+
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
161
|
+
terminal: mockTerminalAddress,
|
|
162
|
+
payer: makeAddr("payer"),
|
|
163
|
+
amount: JBTokenAmount({
|
|
164
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
165
|
+
value: 10,
|
|
166
|
+
decimals: 18,
|
|
167
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
168
|
+
}),
|
|
169
|
+
projectId: projectId,
|
|
170
|
+
rulesetId: block.timestamp,
|
|
171
|
+
beneficiary: beneficiary,
|
|
172
|
+
weight: 10e18,
|
|
173
|
+
reservedPercent: 5000,
|
|
174
|
+
metadata: ""
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
(, JBPayHookSpecification[] memory specs) = tiersHook.beforePayRecordedWith(context);
|
|
178
|
+
|
|
179
|
+
(address specBeneficiary,,) = abi.decode(specs[0].metadata, (address, address, bytes));
|
|
180
|
+
assertEq(specBeneficiary, beneficiary, "empty metadata should use context.beneficiary");
|
|
181
|
+
}
|
|
182
|
+
}
|