@bananapus/721-hook-v6 0.0.32 → 0.0.34
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/USER_JOURNEYS.md +11 -0
- package/package.json +3 -3
- package/script/Deploy.s.sol +53 -19
- package/src/JB721Checkpoints.sol +92 -0
- package/src/JB721CheckpointsDeployer.sol +45 -0
- package/src/JB721TiersHook.sol +90 -116
- package/src/abstract/JB721Hook.sol +5 -0
- package/src/interfaces/IJB721Checkpoints.sol +34 -0
- package/src/interfaces/IJB721CheckpointsDeployer.sol +20 -0
- package/src/interfaces/IJB721TiersHook.sol +8 -0
- package/src/libraries/JB721Constants.sol +6 -0
- package/src/libraries/JB721TiersHookLib.sol +353 -146
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +11 -1
- package/test/Fork.t.sol +11 -2
- package/test/TestAuditGaps.sol +1 -1
- package/test/TestCheckpoints.t.sol +329 -0
- package/test/audit/CodexNemesisRepoFindings.t.sol +270 -0
- package/test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol +161 -0
- package/test/audit/CodexSplitCreditsMismatch.t.sol +2 -1
- package/test/audit/CrossCurrencySplitNoPrices.t.sol +1 -0
- package/test/audit/SameCurrencyDecimalMismatch.t.sol +249 -0
- package/test/audit/SplitFailureRedistribution.t.sol +2 -1
- package/test/fork/ERC20CashOutFork.t.sol +11 -2
- package/test/fork/ERC20TierSplitFork.t.sol +11 -2
- package/test/fork/IssueTokensForSplitsFork.t.sol +11 -2
- package/test/regression/BrokenTerminalDoesNotDos.t.sol +2 -2
- package/test/regression/SplitDistributionBugs.t.sol +5 -5
- package/test/regression/SplitNoBeneficiary.t.sol +1 -1
- package/test/unit/AuditFixes_Unit.t.sol +5 -5
- package/test/unit/pay_CrossCurrency_Unit.t.sol +1 -0
- package/test/unit/pay_Unit.t.sol +1 -0
- package/test/unit/redeem_Unit.t.sol +3 -3
- package/test/unit/relayBeneficiary_Unit.t.sol +182 -0
- package/test/unit/splitHookDistribution_Unit.t.sol +6 -6
- package/test/unit/tierSplitRouting_Unit.t.sol +2 -2
|
@@ -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
|
{
|
|
@@ -310,9 +568,13 @@ library JB721TiersHookLib {
|
|
|
310
568
|
convertedTotal = totalSplitAmount;
|
|
311
569
|
convertedMetadata = splitMetadata;
|
|
312
570
|
|
|
571
|
+
// Extract pricing decimals for reuse below.
|
|
572
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
573
|
+
uint256 pricingDecimals = uint256(uint8(packedPricingContext >> 32));
|
|
574
|
+
|
|
313
575
|
// Convert each per-tier amount from the tier pricing currency to the payment currency.
|
|
314
576
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
315
|
-
if (
|
|
577
|
+
if (context.amount.currency != uint256(uint32(packedPricingContext))) {
|
|
316
578
|
// No price oracle available — return 0 to skip the split rather than forwarding an unconverted
|
|
317
579
|
// amount denominated in the wrong currency, which would over- or under-pay.
|
|
318
580
|
if (address(prices) == address(0)) return (0, convertedMetadata);
|
|
@@ -321,16 +583,15 @@ library JB721TiersHookLib {
|
|
|
321
583
|
// Get the price ratio: how many payment-currency units per one tier-pricing-currency unit.
|
|
322
584
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
323
585
|
uint256 ratio = prices.pricePerUnitOf({
|
|
324
|
-
projectId: projectId,
|
|
325
|
-
pricingCurrency:
|
|
586
|
+
projectId: context.projectId,
|
|
587
|
+
pricingCurrency: context.amount.currency,
|
|
326
588
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
327
589
|
unitCurrency: uint256(uint32(packedPricingContext)),
|
|
328
|
-
decimals:
|
|
590
|
+
decimals: context.amount.decimals
|
|
329
591
|
});
|
|
330
592
|
|
|
331
593
|
// The denominator scales each amount from tier-pricing decimals to payment-token decimals.
|
|
332
|
-
|
|
333
|
-
uint256 denom = 10 ** uint256(uint8(packedPricingContext >> 32));
|
|
594
|
+
uint256 denom = 10 ** pricingDecimals;
|
|
334
595
|
|
|
335
596
|
// Decode per-tier breakdown so each amount can be converted individually.
|
|
336
597
|
(uint16[] memory tierIds, uint256[] memory amounts) =
|
|
@@ -351,12 +612,41 @@ library JB721TiersHookLib {
|
|
|
351
612
|
// Re-encode with the converted amounts.
|
|
352
613
|
convertedMetadata = abi.encode(tierIds, amounts);
|
|
353
614
|
}
|
|
615
|
+
} else if (context.amount.decimals != pricingDecimals) {
|
|
616
|
+
// Same currency but different decimal scales (e.g. pricing at 18 decimals, payment token at 6).
|
|
617
|
+
// Without this branch, split amounts stay in pricing decimals while the cap comparison uses
|
|
618
|
+
// payment decimals — causing orders-of-magnitude mis-scaling. This mirrors the same-currency
|
|
619
|
+
// decimal adjustment in `normalizePaymentValue` (which handles the mint path).
|
|
620
|
+
|
|
621
|
+
// Decode the per-tier breakdown so each amount can be rescaled individually.
|
|
622
|
+
(uint16[] memory tierIds, uint256[] memory amounts) = abi.decode(convertedMetadata, (uint16[], uint256[]));
|
|
623
|
+
|
|
624
|
+
// Re-accumulate the total from rescaled amounts to avoid rounding drift.
|
|
625
|
+
convertedTotal = 0;
|
|
626
|
+
for (uint256 i; i < amounts.length;) {
|
|
627
|
+
// Scale each amount from pricing decimals to payment decimals.
|
|
628
|
+
if (context.amount.decimals > pricingDecimals) {
|
|
629
|
+
// Payment has more decimals — multiply to add precision (e.g. 6→18: multiply by 10^12).
|
|
630
|
+
amounts[i] = amounts[i] * (10 ** (context.amount.decimals - pricingDecimals));
|
|
631
|
+
} else {
|
|
632
|
+
// Payment has fewer decimals — divide to remove precision (e.g. 18→6: divide by 10^12).
|
|
633
|
+
amounts[i] = amounts[i] / (10 ** (pricingDecimals - context.amount.decimals));
|
|
634
|
+
}
|
|
635
|
+
convertedTotal += amounts[i];
|
|
636
|
+
|
|
637
|
+
unchecked {
|
|
638
|
+
++i;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Re-encode with the rescaled amounts.
|
|
643
|
+
convertedMetadata = abi.encode(tierIds, amounts);
|
|
354
644
|
}
|
|
355
645
|
|
|
356
646
|
// Cap the total at the actual payment value. Pay credits fund NFT minting (virtual), but splits
|
|
357
647
|
// require real tokens to distribute. Without this cap, a user with sufficient pay credits but
|
|
358
648
|
// insufficient ETH would revert because the terminal can't forward more than what was actually paid.
|
|
359
|
-
if (convertedTotal >
|
|
649
|
+
if (convertedTotal > context.amount.value) {
|
|
360
650
|
// Proportionally reduce each per-tier amount to stay in sync with the capped total.
|
|
361
651
|
if (convertedMetadata.length != 0) {
|
|
362
652
|
(uint16[] memory tierIds, uint256[] memory amounts) =
|
|
@@ -365,7 +655,7 @@ library JB721TiersHookLib {
|
|
|
365
655
|
convertedTotal = 0;
|
|
366
656
|
for (uint256 i; i < amounts.length;) {
|
|
367
657
|
// Scale down: amount * amountValue / originalTotal.
|
|
368
|
-
amounts[i] = mulDiv({x: amounts[i], y:
|
|
658
|
+
amounts[i] = mulDiv({x: amounts[i], y: context.amount.value, denominator: uncappedTotal});
|
|
369
659
|
convertedTotal += amounts[i];
|
|
370
660
|
|
|
371
661
|
unchecked {
|
|
@@ -375,104 +665,14 @@ library JB721TiersHookLib {
|
|
|
375
665
|
convertedMetadata = abi.encode(tierIds, amounts);
|
|
376
666
|
} else {
|
|
377
667
|
// Clamp the total to the payment value.
|
|
378
|
-
convertedTotal =
|
|
668
|
+
convertedTotal = context.amount.value;
|
|
379
669
|
}
|
|
380
670
|
}
|
|
381
671
|
}
|
|
382
672
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
uint256 projectId,
|
|
387
|
-
address hookAddress,
|
|
388
|
-
JB721TierConfig[] memory tiersToAdd,
|
|
389
|
-
uint256[] memory tierIdsAdded
|
|
390
|
-
)
|
|
391
|
-
private
|
|
392
|
-
{
|
|
393
|
-
uint256 splitGroupCount;
|
|
394
|
-
for (uint256 i; i < tiersToAdd.length;) {
|
|
395
|
-
if (tiersToAdd[i].splits.length != 0) splitGroupCount++;
|
|
396
|
-
|
|
397
|
-
unchecked {
|
|
398
|
-
++i;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
if (splitGroupCount == 0) return;
|
|
402
|
-
|
|
403
|
-
JBSplitGroup[] memory splitGroups = new JBSplitGroup[](splitGroupCount);
|
|
404
|
-
uint256 groupIndex;
|
|
405
|
-
for (uint256 i; i < tiersToAdd.length;) {
|
|
406
|
-
if (tiersToAdd[i].splits.length != 0) {
|
|
407
|
-
splitGroups[groupIndex] = JBSplitGroup({
|
|
408
|
-
groupId: uint256(uint160(hookAddress)) | (tierIdsAdded[i] << 160), splits: tiersToAdd[i].splits
|
|
409
|
-
});
|
|
410
|
-
groupIndex++;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
unchecked {
|
|
414
|
-
++i;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
splits.setSplitGroupsOf({projectId: projectId, rulesetId: 0, splitGroups: splitGroups});
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/// @notice Pulls ERC-20 tokens from the terminal (if needed) and distributes forwarded funds to tier splits.
|
|
421
|
-
/// @dev For ERC-20 tokens, pulls from the terminal using the allowance it granted via _beforeTransferTo.
|
|
422
|
-
/// @param directory The directory to look up terminals.
|
|
423
|
-
/// @param splits The splits contract to read tier split groups from.
|
|
424
|
-
/// @param projectId The project ID of the hook.
|
|
425
|
-
/// @param hookAddress The hook address (for computing split group IDs).
|
|
426
|
-
/// @param token The token being distributed.
|
|
427
|
-
/// @param amount The total amount to distribute.
|
|
428
|
-
/// @param encodedSplitData The encoded per-tier breakdown from hookMetadata.
|
|
429
|
-
function distributeAll(
|
|
430
|
-
IJBDirectory directory,
|
|
431
|
-
IJBSplits splits,
|
|
432
|
-
uint256 projectId,
|
|
433
|
-
address hookAddress,
|
|
434
|
-
address token,
|
|
435
|
-
uint256 amount,
|
|
436
|
-
uint256 decimals,
|
|
437
|
-
bytes calldata encodedSplitData
|
|
438
|
-
)
|
|
439
|
-
external
|
|
440
|
-
{
|
|
441
|
-
// For ERC20 tokens, pull from terminal using the allowance it granted via _beforeTransferTo.
|
|
442
|
-
if (token != JBConstants.NATIVE_TOKEN) {
|
|
443
|
-
uint256 balanceBefore = IERC20(token).balanceOf(address(this));
|
|
444
|
-
SafeERC20.safeTransferFrom({token: IERC20(token), from: msg.sender, to: address(this), value: amount});
|
|
445
|
-
uint256 receivedAmount = IERC20(token).balanceOf(address(this)) - balanceBefore;
|
|
446
|
-
if (receivedAmount != amount) {
|
|
447
|
-
revert JB721TiersHookLib_TokenTransferAmountMismatch(amount, receivedAmount);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
(uint16[] memory tierIds, uint256[] memory amounts) = abi.decode(encodedSplitData, (uint16[], uint256[]));
|
|
452
|
-
|
|
453
|
-
for (uint256 i; i < tierIds.length;) {
|
|
454
|
-
if (amounts[i] == 0) {
|
|
455
|
-
unchecked {
|
|
456
|
-
++i;
|
|
457
|
-
}
|
|
458
|
-
continue;
|
|
459
|
-
}
|
|
460
|
-
uint256 groupId = uint256(uint160(hookAddress)) | (uint256(tierIds[i]) << 160);
|
|
461
|
-
_distributeSingleSplit({
|
|
462
|
-
directory: directory,
|
|
463
|
-
splitsContract: splits,
|
|
464
|
-
projectId: projectId,
|
|
465
|
-
token: token,
|
|
466
|
-
groupId: groupId,
|
|
467
|
-
amount: amounts[i],
|
|
468
|
-
decimals: decimals
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
unchecked {
|
|
472
|
-
++i;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
673
|
+
//*********************************************************************//
|
|
674
|
+
// ----------------------- private helpers --------------------------- //
|
|
675
|
+
//*********************************************************************//
|
|
476
676
|
|
|
477
677
|
/// @notice Distributes funds for a single tier's split group.
|
|
478
678
|
/// @dev Edge case: if both `_sendPayoutToSplit` returns false (reverting hook/terminal/beneficiary) AND the
|
|
@@ -741,33 +941,40 @@ library JB721TiersHookLib {
|
|
|
741
941
|
return false;
|
|
742
942
|
}
|
|
743
943
|
|
|
744
|
-
/// @notice
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
function resolveTokenURI(
|
|
752
|
-
IJB721TiersHookStore store,
|
|
753
|
-
address hook,
|
|
754
|
-
string memory baseUri,
|
|
755
|
-
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
|
|
756
951
|
)
|
|
757
|
-
|
|
758
|
-
view
|
|
759
|
-
returns (string memory)
|
|
952
|
+
private
|
|
760
953
|
{
|
|
761
|
-
|
|
762
|
-
|
|
954
|
+
uint256 splitGroupCount;
|
|
955
|
+
for (uint256 i; i < tiersToAdd.length;) {
|
|
956
|
+
if (tiersToAdd[i].splits.length != 0) splitGroupCount++;
|
|
763
957
|
|
|
764
|
-
|
|
765
|
-
|
|
958
|
+
unchecked {
|
|
959
|
+
++i;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (splitGroupCount == 0) return;
|
|
766
963
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
+
}
|
|
973
|
+
|
|
974
|
+
unchecked {
|
|
975
|
+
++i;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
splits.setSplitGroupsOf({projectId: projectId, rulesetId: 0, splitGroups: splitGroups});
|
|
772
979
|
}
|
|
773
980
|
}
|
|
@@ -15,6 +15,9 @@ import "../../src/JB721TiersHookDeployer.sol";
|
|
|
15
15
|
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
16
16
|
import "../../src/JB721TiersHookStore.sol";
|
|
17
17
|
|
|
18
|
+
import {JB721CheckpointsDeployer} from "../../src/JB721CheckpointsDeployer.sol";
|
|
19
|
+
import {IJB721CheckpointsDeployer} from "../../src/interfaces/IJB721CheckpointsDeployer.sol";
|
|
20
|
+
|
|
18
21
|
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
19
22
|
import "../utils/TestBaseWorkflow.sol";
|
|
20
23
|
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
@@ -69,7 +72,14 @@ contract Test_TiersHook_E2E is TestBaseWorkflow {
|
|
|
69
72
|
super.setUp();
|
|
70
73
|
store = new JB721TiersHookStore();
|
|
71
74
|
hook = new JB721TiersHook(
|
|
72
|
-
jbDirectory,
|
|
75
|
+
jbDirectory,
|
|
76
|
+
jbPermissions,
|
|
77
|
+
jbPrices,
|
|
78
|
+
jbRulesets,
|
|
79
|
+
store,
|
|
80
|
+
IJBSplits(address(jbSplits)),
|
|
81
|
+
IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
|
|
82
|
+
trustedForwarder
|
|
73
83
|
);
|
|
74
84
|
addressRegistry = new JBAddressRegistry();
|
|
75
85
|
JB721TiersHookDeployer hookDeployer = new JB721TiersHookDeployer(hook, store, addressRegistry, trustedForwarder);
|