@bananapus/721-hook-v6 0.0.21 → 0.0.23
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/ADMINISTRATION.md +3 -3
- package/ARCHITECTURE.md +3 -3
- package/AUDIT_INSTRUCTIONS.md +13 -13
- package/CHANGE_LOG.md +3 -3
- package/RISKS.md +2 -2
- package/SKILLS.md +7 -6
- package/USER_JOURNEYS.md +3 -4
- package/assets/findings/nana-721-hook-v6-pashov-ai-audit-report-20260330-091257.md +83 -0
- package/package.json +4 -4
- package/src/JB721TiersHook.sol +23 -6
- package/src/JB721TiersHookStore.sol +15 -1
- package/src/libraries/JB721TiersHookLib.sol +143 -88
- package/src/structs/JBDeploy721TiersHookConfig.sol +0 -2
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +0 -2
- package/test/Fork.t.sol +0 -1
- package/test/audit/CodexPayCreditsBypassTierSplits.t.sol +196 -0
- package/test/audit/CodexSplitCreditsMismatch.t.sol +214 -0
- package/test/audit/{CodexNemesis_CrossCurrencySplitNoPrices.t.sol → CrossCurrencySplitNoPrices.t.sol} +2 -2
- package/test/audit/SplitFailureRedistribution.t.sol +142 -0
- package/test/fork/ERC20CashOutFork.t.sol +0 -1
- package/test/fork/ERC20TierSplitFork.t.sol +0 -2
- package/test/fork/IssueTokensForSplitsFork.t.sol +0 -1
- package/test/regression/ProjectDeployerRulesets.t.sol +0 -1
- package/test/unit/AuditFixes_Unit.t.sol +611 -0
- package/test/unit/getters_constructor_Unit.t.sol +0 -1
- package/test/unit/pay_CrossCurrency_Unit.t.sol +1 -1
|
@@ -26,6 +26,7 @@ import {JBIpfsDecoder} from "./JBIpfsDecoder.sol";
|
|
|
26
26
|
/// @notice External library for JB721TiersHook operations extracted to stay within the EIP-170 contract size limit.
|
|
27
27
|
/// @dev Handles tier adjustments, split calculations, price normalization, and split fund distribution.
|
|
28
28
|
library JB721TiersHookLib {
|
|
29
|
+
error JB721TiersHookLib_NoTerminalForLeftover(uint256 projectId, address token, uint256 leftoverAmount);
|
|
29
30
|
// Events mirrored from IJB721TiersHook (emitted via DELEGATECALL from the hook's context).
|
|
30
31
|
event AddToBalanceReverted(uint256 indexed projectId, address token, uint256 amount, bytes reason);
|
|
31
32
|
event AddTier(uint256 indexed tierId, JB721TierConfig tier, address caller);
|
|
@@ -221,73 +222,26 @@ library JB721TiersHookLib {
|
|
|
221
222
|
}
|
|
222
223
|
}
|
|
223
224
|
|
|
224
|
-
/// @notice Converts split amounts from tier pricing denomination to payment token denomination.
|
|
225
|
-
/// @dev Called after `calculateSplitAmounts` when the payment currency differs from the tier pricing currency.
|
|
226
|
-
/// @param totalSplitAmount The total split amount in tier pricing denomination.
|
|
227
|
-
/// @param splitMetadata The encoded per-tier breakdown (tierIds, amounts) from calculateSplitAmounts.
|
|
228
|
-
/// @param packedPricingContext The packed pricing context (currency, decimals).
|
|
229
|
-
/// @param prices The prices contract used for currency conversion.
|
|
230
|
-
/// @param projectId The project ID.
|
|
231
|
-
/// @param amountCurrency The payment amount currency.
|
|
232
|
-
/// @param amountDecimals The payment amount decimals.
|
|
233
|
-
/// @return convertedTotal The total split amount converted to payment token denomination.
|
|
234
|
-
/// @return convertedMetadata The re-encoded per-tier breakdown with converted amounts.
|
|
235
|
-
function convertSplitAmounts(
|
|
236
|
-
uint256 totalSplitAmount,
|
|
237
|
-
bytes memory splitMetadata,
|
|
238
|
-
uint256 packedPricingContext,
|
|
239
|
-
IJBPrices prices,
|
|
240
|
-
uint256 projectId,
|
|
241
|
-
uint256 amountCurrency,
|
|
242
|
-
uint256 amountDecimals
|
|
243
|
-
)
|
|
244
|
-
external
|
|
245
|
-
view
|
|
246
|
-
returns (uint256 convertedTotal, bytes memory convertedMetadata)
|
|
247
|
-
{
|
|
248
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
249
|
-
uint256 pricingCurrency = uint256(uint32(packedPricingContext));
|
|
250
|
-
if (amountCurrency == pricingCurrency) return (totalSplitAmount, splitMetadata);
|
|
251
|
-
|
|
252
|
-
// No price oracle available to convert between currencies. Return 0 to skip the split rather than
|
|
253
|
-
// forwarding an unconverted amount denominated in the wrong currency, which would over- or under-pay.
|
|
254
|
-
if (address(prices) == address(0)) return (0, splitMetadata);
|
|
255
|
-
|
|
256
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
257
|
-
uint256 pricingDecimals = uint256(uint8(packedPricingContext >> 32));
|
|
258
|
-
uint256 ratio = prices.pricePerUnitOf({
|
|
259
|
-
projectId: projectId,
|
|
260
|
-
pricingCurrency: amountCurrency,
|
|
261
|
-
unitCurrency: pricingCurrency,
|
|
262
|
-
decimals: amountDecimals
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
(uint16[] memory tierIds, uint256[] memory amounts) = abi.decode(splitMetadata, (uint16[], uint256[]));
|
|
266
|
-
for (uint256 i; i < amounts.length; i++) {
|
|
267
|
-
amounts[i] = mulDiv({x: amounts[i], y: ratio, denominator: 10 ** pricingDecimals});
|
|
268
|
-
convertedTotal += amounts[i];
|
|
269
|
-
}
|
|
270
|
-
convertedMetadata = abi.encode(tierIds, amounts);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
225
|
/// @notice Calculates the weight for token minting after accounting for tier split amounts.
|
|
274
226
|
/// @dev Extracted from the hook to keep mulDiv's bytecode out of the hook (EIP-170 compliance).
|
|
275
227
|
/// @param contextWeight The original weight from the payment context.
|
|
276
228
|
/// @param amountValue The payment amount value.
|
|
277
229
|
/// @param totalSplitAmount The total amount routed to tier splits.
|
|
278
|
-
/// @param
|
|
230
|
+
/// @param store The 721 tiers hook store (to read flags).
|
|
231
|
+
/// @param hook The hook address.
|
|
279
232
|
/// @return weight The adjusted weight for token minting.
|
|
280
233
|
function calculateWeight(
|
|
281
234
|
uint256 contextWeight,
|
|
282
235
|
uint256 amountValue,
|
|
283
236
|
uint256 totalSplitAmount,
|
|
284
|
-
|
|
237
|
+
IJB721TiersHookStore store,
|
|
238
|
+
address hook
|
|
285
239
|
)
|
|
286
240
|
external
|
|
287
|
-
|
|
241
|
+
view
|
|
288
242
|
returns (uint256 weight)
|
|
289
243
|
{
|
|
290
|
-
if (totalSplitAmount == 0 || issueTokensForSplits) {
|
|
244
|
+
if (totalSplitAmount == 0 || store.flagsOf(hook).issueTokensForSplits) {
|
|
291
245
|
// No splits, or hook configured to give full token credit regardless — full weight.
|
|
292
246
|
weight = contextWeight;
|
|
293
247
|
} else if (amountValue > totalSplitAmount) {
|
|
@@ -299,6 +253,96 @@ library JB721TiersHookLib {
|
|
|
299
253
|
}
|
|
300
254
|
}
|
|
301
255
|
|
|
256
|
+
/// @notice Converts split amounts from tier pricing to payment denomination (if currencies differ), then caps
|
|
257
|
+
/// the total at the actual payment value — proportionally reducing per-tier amounts when the cap applies.
|
|
258
|
+
/// @dev Combines currency conversion and cap into one external call to keep hook bytecode under EIP-170.
|
|
259
|
+
/// @param totalSplitAmount The total split amount in tier pricing denomination.
|
|
260
|
+
/// @param splitMetadata The encoded per-tier breakdown (tierIds, amounts).
|
|
261
|
+
/// @param packedPricingContext The packed pricing context (currency in bits 0-31, decimals in bits 32-39).
|
|
262
|
+
/// @param prices The prices contract used for currency conversion.
|
|
263
|
+
/// @param projectId The project ID.
|
|
264
|
+
/// @param amountCurrency The payment amount currency.
|
|
265
|
+
/// @param amountDecimals The payment amount decimals.
|
|
266
|
+
/// @param amountValue The actual payment value (used as the cap).
|
|
267
|
+
/// @return convertedTotal The total split amount after conversion and capping.
|
|
268
|
+
/// @return convertedMetadata The re-encoded per-tier breakdown with adjusted amounts.
|
|
269
|
+
function convertAndCapSplitAmounts(
|
|
270
|
+
uint256 totalSplitAmount,
|
|
271
|
+
bytes memory splitMetadata,
|
|
272
|
+
uint256 packedPricingContext,
|
|
273
|
+
IJBPrices prices,
|
|
274
|
+
uint256 projectId,
|
|
275
|
+
uint256 amountCurrency,
|
|
276
|
+
uint256 amountDecimals,
|
|
277
|
+
uint256 amountValue
|
|
278
|
+
)
|
|
279
|
+
external
|
|
280
|
+
view
|
|
281
|
+
returns (uint256 convertedTotal, bytes memory convertedMetadata)
|
|
282
|
+
{
|
|
283
|
+
// Start from the input values; conversion and capping modify them in-place below.
|
|
284
|
+
convertedTotal = totalSplitAmount;
|
|
285
|
+
convertedMetadata = splitMetadata;
|
|
286
|
+
|
|
287
|
+
// Convert each per-tier amount from the tier pricing currency to the payment currency.
|
|
288
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
289
|
+
if (amountCurrency != uint256(uint32(packedPricingContext))) {
|
|
290
|
+
// No price oracle available — return 0 to skip the split rather than forwarding an unconverted
|
|
291
|
+
// amount denominated in the wrong currency, which would over- or under-pay.
|
|
292
|
+
if (address(prices) == address(0)) return (0, convertedMetadata);
|
|
293
|
+
|
|
294
|
+
{
|
|
295
|
+
// Get the price ratio: how many payment-currency units per one tier-pricing-currency unit.
|
|
296
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
297
|
+
uint256 ratio = prices.pricePerUnitOf({
|
|
298
|
+
projectId: projectId,
|
|
299
|
+
pricingCurrency: amountCurrency,
|
|
300
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
301
|
+
unitCurrency: uint256(uint32(packedPricingContext)),
|
|
302
|
+
decimals: amountDecimals
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// The denominator scales each amount from tier-pricing decimals to payment-token decimals.
|
|
306
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
307
|
+
uint256 denom = 10 ** uint256(uint8(packedPricingContext >> 32));
|
|
308
|
+
|
|
309
|
+
// Decode per-tier breakdown so each amount can be converted individually.
|
|
310
|
+
(uint16[] memory tierIds, uint256[] memory amounts) =
|
|
311
|
+
abi.decode(convertedMetadata, (uint16[], uint256[]));
|
|
312
|
+
|
|
313
|
+
// Re-accumulate the total from converted amounts to avoid rounding drift.
|
|
314
|
+
convertedTotal = 0;
|
|
315
|
+
for (uint256 i; i < amounts.length; i++) {
|
|
316
|
+
// Convert this tier's amount: amount * ratio / 10^pricingDecimals.
|
|
317
|
+
amounts[i] = mulDiv({x: amounts[i], y: ratio, denominator: denom});
|
|
318
|
+
convertedTotal += amounts[i];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Re-encode with the converted amounts.
|
|
322
|
+
convertedMetadata = abi.encode(tierIds, amounts);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Cap the total at the actual payment value. Pay credits fund NFT minting (virtual), but splits
|
|
327
|
+
// require real tokens to distribute. Without this cap, a user with sufficient pay credits but
|
|
328
|
+
// insufficient ETH would revert because the terminal can't forward more than what was actually paid.
|
|
329
|
+
if (convertedTotal > amountValue) {
|
|
330
|
+
// Proportionally reduce each per-tier amount to stay in sync with the capped total.
|
|
331
|
+
if (convertedMetadata.length != 0) {
|
|
332
|
+
(uint16[] memory tierIds, uint256[] memory amounts) =
|
|
333
|
+
abi.decode(convertedMetadata, (uint16[], uint256[]));
|
|
334
|
+
for (uint256 i; i < amounts.length; i++) {
|
|
335
|
+
// Scale down: amount * amountValue / originalTotal.
|
|
336
|
+
amounts[i] = mulDiv({x: amounts[i], y: amountValue, denominator: convertedTotal});
|
|
337
|
+
}
|
|
338
|
+
convertedMetadata = abi.encode(tierIds, amounts);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Clamp the total to the payment value.
|
|
342
|
+
convertedTotal = amountValue;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
302
346
|
/// @notice Sets split groups in JBSplits for tiers that have splits configured.
|
|
303
347
|
function _setSplitGroupsFor(
|
|
304
348
|
IJBSplits splits,
|
|
@@ -395,15 +439,19 @@ library JB721TiersHookLib {
|
|
|
395
439
|
bool isNativeToken = token == JBConstants.NATIVE_TOKEN;
|
|
396
440
|
uint256 leftoverPercentage = JBConstants.SPLITS_TOTAL_PERCENT;
|
|
397
441
|
uint256 leftoverAmount = amount;
|
|
442
|
+
amount = 0;
|
|
398
443
|
|
|
399
444
|
for (uint256 j; j < tierSplits.length; j++) {
|
|
400
445
|
uint256 payoutAmount =
|
|
401
446
|
mulDiv({x: leftoverAmount, y: tierSplits[j].percent, denominator: leftoverPercentage});
|
|
402
447
|
if (payoutAmount != 0) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
448
|
+
unchecked {
|
|
449
|
+
leftoverAmount -= payoutAmount;
|
|
450
|
+
}
|
|
451
|
+
// On failure, don't re-add to leftoverAmount — this prevents inflating later recipients.
|
|
452
|
+
// Failed amounts accumulate as the gap between `amount` and `leftoverAmount + total sent`.
|
|
453
|
+
// After the loop, we re-add leftoverPercentage-based residual naturally.
|
|
454
|
+
if (!_sendPayoutToSplit({
|
|
407
455
|
directory: directory,
|
|
408
456
|
split: tierSplits[j],
|
|
409
457
|
token: token,
|
|
@@ -412,8 +460,10 @@ library JB721TiersHookLib {
|
|
|
412
460
|
groupId: groupId,
|
|
413
461
|
decimals: decimals
|
|
414
462
|
})) {
|
|
463
|
+
// Payout failed — route to project balance by returning to leftover after the loop.
|
|
464
|
+
// We add back to `amount` (parameter, no longer used for its original purpose).
|
|
415
465
|
unchecked {
|
|
416
|
-
|
|
466
|
+
amount += payoutAmount;
|
|
417
467
|
}
|
|
418
468
|
}
|
|
419
469
|
}
|
|
@@ -422,39 +472,44 @@ library JB721TiersHookLib {
|
|
|
422
472
|
}
|
|
423
473
|
}
|
|
424
474
|
|
|
475
|
+
// Route failed payout amounts to the project's balance.
|
|
476
|
+
leftoverAmount += amount;
|
|
477
|
+
|
|
425
478
|
if (leftoverAmount != 0) {
|
|
426
479
|
// slither-disable-next-line calls-loop
|
|
427
480
|
IJBTerminal terminal = directory.primaryTerminalOf({projectId: projectId, token: token});
|
|
428
|
-
if
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
481
|
+
// Revert if there are leftover funds but no terminal to route them to.
|
|
482
|
+
if (address(terminal) == address(0)) {
|
|
483
|
+
revert JB721TiersHookLib_NoTerminalForLeftover(projectId, token, leftoverAmount);
|
|
484
|
+
}
|
|
485
|
+
if (isNativeToken) {
|
|
486
|
+
// slither-disable-next-line arbitrary-send-eth,calls-loop
|
|
487
|
+
try terminal.addToBalanceOf{value: leftoverAmount}({
|
|
488
|
+
projectId: projectId,
|
|
489
|
+
token: token,
|
|
490
|
+
amount: leftoverAmount,
|
|
491
|
+
shouldReturnHeldFees: false,
|
|
492
|
+
memo: "",
|
|
493
|
+
metadata: bytes("")
|
|
494
|
+
}) {}
|
|
495
|
+
catch (bytes memory reason) {
|
|
496
|
+
emit AddToBalanceReverted(projectId, token, leftoverAmount, reason);
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: leftoverAmount});
|
|
500
|
+
// slither-disable-next-line calls-loop
|
|
501
|
+
try terminal.addToBalanceOf({
|
|
502
|
+
projectId: projectId,
|
|
503
|
+
token: token,
|
|
504
|
+
amount: leftoverAmount,
|
|
505
|
+
shouldReturnHeldFees: false,
|
|
506
|
+
memo: "",
|
|
507
|
+
metadata: bytes("")
|
|
508
|
+
}) {}
|
|
509
|
+
catch (bytes memory reason) {
|
|
510
|
+
// Reset approval on failure.
|
|
511
|
+
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
|
|
512
|
+
emit AddToBalanceReverted(projectId, token, leftoverAmount, reason);
|
|
458
513
|
}
|
|
459
514
|
}
|
|
460
515
|
}
|
|
@@ -11,7 +11,6 @@ import {IJB721TokenUriResolver} from "../interfaces/IJB721TokenUriResolver.sol";
|
|
|
11
11
|
/// @custom:member tokenUriResolver The contract responsible for resolving the URI for each NFT.
|
|
12
12
|
/// @custom:member contractUri The URI where this contract's metadata can be found.
|
|
13
13
|
/// @custom:member tiersConfig The NFT tiers and pricing config to launch the hook with.
|
|
14
|
-
/// @custom:member reserveBeneficiary The default reserved beneficiary for all tiers.
|
|
15
14
|
/// @custom:member flags A set of boolean options to configure the hook with.
|
|
16
15
|
// forge-lint: disable-next-line(pascal-case-struct)
|
|
17
16
|
struct JBDeploy721TiersHookConfig {
|
|
@@ -21,6 +20,5 @@ struct JBDeploy721TiersHookConfig {
|
|
|
21
20
|
IJB721TokenUriResolver tokenUriResolver;
|
|
22
21
|
string contractUri;
|
|
23
22
|
JB721InitTiersConfig tiersConfig;
|
|
24
|
-
address reserveBeneficiary;
|
|
25
23
|
JB721TiersHookFlags flags;
|
|
26
24
|
}
|
|
@@ -815,7 +815,6 @@ contract Test_TiersHook_E2E is TestBaseWorkflow {
|
|
|
815
815
|
tiersConfig: JB721InitTiersConfig({
|
|
816
816
|
tiers: tierConfigs, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
|
|
817
817
|
}),
|
|
818
|
-
reserveBeneficiary: reserveBeneficiary,
|
|
819
818
|
flags: JB721TiersHookFlags({
|
|
820
819
|
preventOverspending: false,
|
|
821
820
|
issueTokensForSplits: false,
|
|
@@ -908,7 +907,6 @@ contract Test_TiersHook_E2E is TestBaseWorkflow {
|
|
|
908
907
|
tiersConfig: JB721InitTiersConfig({
|
|
909
908
|
tiers: tierConfigs, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
|
|
910
909
|
}),
|
|
911
|
-
reserveBeneficiary: reserveBeneficiary,
|
|
912
910
|
flags: JB721TiersHookFlags({
|
|
913
911
|
preventOverspending: false,
|
|
914
912
|
issueTokensForSplits: false,
|
package/test/Fork.t.sol
CHANGED
|
@@ -0,0 +1,196 @@
|
|
|
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
|
+
import {IJB721TiersHookStore} from "../../src/interfaces/IJB721TiersHookStore.sol";
|
|
7
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
8
|
+
import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
|
|
9
|
+
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
10
|
+
import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
|
|
11
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
12
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
13
|
+
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
14
|
+
|
|
15
|
+
contract CodexPayCreditsBypassTierSplits is UnitTestSetup {
|
|
16
|
+
address internal splitBeneficiary = makeAddr("splitBeneficiary");
|
|
17
|
+
|
|
18
|
+
function setUp() public override {
|
|
19
|
+
super.setUp();
|
|
20
|
+
vm.etch(mockJBSplits, new bytes(0x69));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function _buildPayMetadata(
|
|
24
|
+
address hookAddress,
|
|
25
|
+
bool allowOverspending,
|
|
26
|
+
uint16[] memory tierIdsToMint
|
|
27
|
+
)
|
|
28
|
+
internal
|
|
29
|
+
view
|
|
30
|
+
returns (bytes memory)
|
|
31
|
+
{
|
|
32
|
+
bytes[] memory data = new bytes[](1);
|
|
33
|
+
data[0] = abi.encode(allowOverspending, tierIdsToMint);
|
|
34
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
35
|
+
ids[0] = metadataHelper.getId("pay", hookAddress);
|
|
36
|
+
return metadataHelper.createMetadata(ids, data);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function _beforePayContext(
|
|
40
|
+
address hookAddress,
|
|
41
|
+
uint256 amountValue,
|
|
42
|
+
uint16[] memory tierIdsToMint
|
|
43
|
+
)
|
|
44
|
+
internal
|
|
45
|
+
view
|
|
46
|
+
returns (JBBeforePayRecordedContext memory)
|
|
47
|
+
{
|
|
48
|
+
return JBBeforePayRecordedContext({
|
|
49
|
+
terminal: mockTerminalAddress,
|
|
50
|
+
payer: beneficiary,
|
|
51
|
+
amount: JBTokenAmount({
|
|
52
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
53
|
+
value: amountValue,
|
|
54
|
+
decimals: 18,
|
|
55
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
56
|
+
}),
|
|
57
|
+
projectId: projectId,
|
|
58
|
+
rulesetId: 0,
|
|
59
|
+
beneficiary: beneficiary,
|
|
60
|
+
weight: 10e18,
|
|
61
|
+
reservedPercent: 5000,
|
|
62
|
+
metadata: _buildPayMetadata(hookAddress, false, tierIdsToMint)
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function _afterPayContext(
|
|
67
|
+
address hookAddress,
|
|
68
|
+
uint256 amountValue,
|
|
69
|
+
uint256 forwardedAmountValue,
|
|
70
|
+
bytes memory hookMetadata,
|
|
71
|
+
uint16[] memory tierIdsToMint
|
|
72
|
+
)
|
|
73
|
+
internal
|
|
74
|
+
view
|
|
75
|
+
returns (JBAfterPayRecordedContext memory)
|
|
76
|
+
{
|
|
77
|
+
return JBAfterPayRecordedContext({
|
|
78
|
+
payer: beneficiary,
|
|
79
|
+
projectId: projectId,
|
|
80
|
+
rulesetId: 0,
|
|
81
|
+
amount: JBTokenAmount({
|
|
82
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
83
|
+
value: amountValue,
|
|
84
|
+
decimals: 18,
|
|
85
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
86
|
+
}),
|
|
87
|
+
forwardedAmount: JBTokenAmount({
|
|
88
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
89
|
+
value: forwardedAmountValue,
|
|
90
|
+
decimals: 18,
|
|
91
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
92
|
+
}),
|
|
93
|
+
weight: 10e18,
|
|
94
|
+
newlyIssuedTokenCount: 0,
|
|
95
|
+
beneficiary: beneficiary,
|
|
96
|
+
hookMetadata: hookMetadata,
|
|
97
|
+
payerMetadata: _buildPayMetadata(hookAddress, true, tierIdsToMint)
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function test_payCredits_can_bypass_most_of_tier_split_payment() public {
|
|
102
|
+
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
103
|
+
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
104
|
+
|
|
105
|
+
vm.mockCall(
|
|
106
|
+
mockJBDirectory,
|
|
107
|
+
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
108
|
+
abi.encode(true)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
112
|
+
tierConfigs[0] = JB721TierConfig({
|
|
113
|
+
price: 1 ether,
|
|
114
|
+
initialSupply: 100,
|
|
115
|
+
votingUnits: 0,
|
|
116
|
+
reserveFrequency: 0,
|
|
117
|
+
reserveBeneficiary: reserveBeneficiary,
|
|
118
|
+
encodedIPFSUri: bytes32(uint256(0x1234)),
|
|
119
|
+
category: 1,
|
|
120
|
+
discountPercent: 0,
|
|
121
|
+
allowOwnerMint: false,
|
|
122
|
+
useReserveBeneficiaryAsDefault: false,
|
|
123
|
+
transfersPausable: false,
|
|
124
|
+
cannotBeRemoved: false,
|
|
125
|
+
cannotIncreaseDiscountPercent: false,
|
|
126
|
+
useVotingUnits: false,
|
|
127
|
+
splitPercent: 1_000_000_000,
|
|
128
|
+
splits: new JBSplit[](0)
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
vm.prank(address(testHook));
|
|
132
|
+
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
133
|
+
|
|
134
|
+
uint16[] memory noTiers = new uint16[](0);
|
|
135
|
+
JBAfterPayRecordedContext memory creditSeedContext = JBAfterPayRecordedContext({
|
|
136
|
+
payer: beneficiary,
|
|
137
|
+
projectId: projectId,
|
|
138
|
+
rulesetId: 0,
|
|
139
|
+
amount: JBTokenAmount({
|
|
140
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
141
|
+
value: 1 ether,
|
|
142
|
+
decimals: 18,
|
|
143
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
144
|
+
}),
|
|
145
|
+
forwardedAmount: JBTokenAmount({
|
|
146
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
147
|
+
value: 0,
|
|
148
|
+
decimals: 18,
|
|
149
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
150
|
+
}),
|
|
151
|
+
weight: 10e18,
|
|
152
|
+
newlyIssuedTokenCount: 0,
|
|
153
|
+
beneficiary: beneficiary,
|
|
154
|
+
hookMetadata: "",
|
|
155
|
+
payerMetadata: _buildPayMetadata(address(testHook), true, noTiers)
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
vm.prank(mockTerminalAddress);
|
|
159
|
+
testHook.afterPayRecordedWith(creditSeedContext);
|
|
160
|
+
assertEq(testHook.payCreditsOf(beneficiary), 1 ether, "setup: credits should be seeded");
|
|
161
|
+
|
|
162
|
+
uint16[] memory mintIds = new uint16[](1);
|
|
163
|
+
mintIds[0] = uint16(tierIds[0]);
|
|
164
|
+
|
|
165
|
+
(, JBPayHookSpecification[] memory specs) =
|
|
166
|
+
testHook.beforePayRecordedWith(_beforePayContext(address(testHook), 1, mintIds));
|
|
167
|
+
assertEq(specs[0].amount, 1, "forwarded amount is capped to the fresh payment");
|
|
168
|
+
|
|
169
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
170
|
+
splits[0] = JBSplit({
|
|
171
|
+
percent: 1_000_000_000,
|
|
172
|
+
projectId: 0,
|
|
173
|
+
beneficiary: payable(splitBeneficiary),
|
|
174
|
+
preferAddToBalance: false,
|
|
175
|
+
lockedUntil: 0,
|
|
176
|
+
hook: IJBSplitHook(address(0))
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
180
|
+
vm.mockCall(
|
|
181
|
+
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
uint256 splitBalanceBefore = splitBeneficiary.balance;
|
|
185
|
+
|
|
186
|
+
JBAfterPayRecordedContext memory creditMintContext =
|
|
187
|
+
_afterPayContext(address(testHook), 1, 1, specs[0].metadata, mintIds);
|
|
188
|
+
vm.deal(mockTerminalAddress, 1);
|
|
189
|
+
vm.prank(mockTerminalAddress);
|
|
190
|
+
testHook.afterPayRecordedWith{value: 1}(creditMintContext);
|
|
191
|
+
|
|
192
|
+
assertEq(testHook.balanceOf(beneficiary), 1, "beneficiary should receive the NFT");
|
|
193
|
+
assertEq(testHook.payCreditsOf(beneficiary), 1, "credits should fund almost the entire mint");
|
|
194
|
+
assertEq(splitBeneficiary.balance - splitBalanceBefore, 1, "split recipient only gets the fresh payment");
|
|
195
|
+
}
|
|
196
|
+
}
|