@bananapus/721-hook-v6 0.0.25 → 0.0.27
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 +1 -1
- package/src/JB721TiersHook.sol +35 -45
- package/src/JB721TiersHookStore.sol +46 -35
- package/src/interfaces/IJB721TiersHookStore.sol +2 -1
- package/src/structs/JB721Tier.sol +6 -4
- package/src/structs/JB721TierConfig.sol +6 -3
- package/src/structs/JBStored721Tier.sol +1 -1
- package/test/721HookAttacks.t.sol +10 -8
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +6 -4
- package/test/Fork.t.sol +26 -19
- package/test/TestAuditGaps.sol +24 -16
- package/test/TestVotingUnitsLifecycle.t.sol +1 -1
- package/test/audit/CodexPayCreditsBypassTierSplits.t.sol +3 -2
- package/test/audit/CodexSplitCreditsMismatch.t.sol +3 -2
- package/test/fork/ERC20CashOutFork.t.sol +12 -8
- package/test/fork/ERC20TierSplitFork.t.sol +9 -6
- package/test/fork/IssueTokensForSplitsFork.t.sol +3 -2
- package/test/invariants/handlers/TierLifecycleHandler.sol +5 -4
- package/test/invariants/handlers/TierStoreHandler.sol +4 -3
- package/test/regression/ProjectDeployerRulesets.t.sol +3 -2
- package/test/unit/AuditFixes_Unit.t.sol +12 -8
- package/test/unit/adjustTier_Unit.t.sol +96 -64
- package/test/unit/getters_constructor_Unit.t.sol +14 -11
- package/test/unit/mintFor_mintReservesFor_Unit.t.sol +6 -6
- package/test/unit/redeem_Unit.t.sol +3 -3
package/package.json
CHANGED
package/src/JB721TiersHook.sol
CHANGED
|
@@ -42,6 +42,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
42
42
|
//*********************************************************************//
|
|
43
43
|
|
|
44
44
|
error JB721TiersHook_AlreadyInitialized(uint256 projectId);
|
|
45
|
+
error JB721TiersHook_CantBuyWithCredits();
|
|
45
46
|
error JB721TiersHook_CurrencyMismatch(uint256 paymentCurrency, uint256 tierCurrency);
|
|
46
47
|
error JB721TiersHook_InvalidPricingDecimals(uint256 decimals);
|
|
47
48
|
error JB721TiersHook_MintReserveNftsPaused();
|
|
@@ -387,23 +388,13 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
387
388
|
|
|
388
389
|
// Record the mint. The token IDs returned correspond to the tiers passed in.
|
|
389
390
|
// slither-disable-next-line reentrancy-events,unused-return
|
|
390
|
-
(tokenIds
|
|
391
|
+
(tokenIds,,) = STORE.recordMint({
|
|
391
392
|
amount: type(uint256).max, // force the mint.
|
|
392
393
|
tierIds: tierIds,
|
|
393
394
|
isOwnerMint: true // manual mint.
|
|
394
395
|
});
|
|
395
396
|
|
|
396
|
-
|
|
397
|
-
// Set the token ID.
|
|
398
|
-
uint256 tokenId = tokenIds[i];
|
|
399
|
-
|
|
400
|
-
// Mint the NFT.
|
|
401
|
-
_mint({to: beneficiary, tokenId: tokenId});
|
|
402
|
-
|
|
403
|
-
emit Mint({
|
|
404
|
-
tokenId: tokenId, tierId: tierIds[i], beneficiary: beneficiary, totalAmountPaid: 0, caller: _msgSender()
|
|
405
|
-
});
|
|
406
|
-
}
|
|
397
|
+
_mintTokens({tokenIds: tokenIds, tierIds: tierIds, beneficiary: beneficiary, totalAmountPaid: 0});
|
|
407
398
|
}
|
|
408
399
|
|
|
409
400
|
/// @notice Mint pending reserved NFTs based on the provided information.
|
|
@@ -594,47 +585,30 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
594
585
|
STORE.recordBurn(tokenIds);
|
|
595
586
|
}
|
|
596
587
|
|
|
597
|
-
/// @notice Mints
|
|
598
|
-
/// @
|
|
599
|
-
/// @param
|
|
600
|
-
///
|
|
601
|
-
/// @param
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
address beneficiary
|
|
588
|
+
/// @notice Mints NFTs and emits events for each.
|
|
589
|
+
/// @param tokenIds The token IDs to mint.
|
|
590
|
+
/// @param tierIds The tier IDs corresponding to each token.
|
|
591
|
+
/// @param beneficiary The address receiving the NFTs.
|
|
592
|
+
/// @param totalAmountPaid The amount to report in the Mint event.
|
|
593
|
+
function _mintTokens(
|
|
594
|
+
uint256[] memory tokenIds,
|
|
595
|
+
uint16[] memory tierIds,
|
|
596
|
+
address beneficiary,
|
|
597
|
+
uint256 totalAmountPaid
|
|
608
598
|
)
|
|
609
599
|
internal
|
|
610
|
-
returns (uint256 leftoverAmount)
|
|
611
600
|
{
|
|
612
|
-
// Keep a reference to the NFT token IDs.
|
|
613
|
-
uint256[] memory tokenIds;
|
|
614
|
-
|
|
615
|
-
// Record the NFT mints. The token IDs returned correspond to the tier IDs passed in.
|
|
616
|
-
(tokenIds, leftoverAmount) = STORE.recordMint({
|
|
617
|
-
amount: amount,
|
|
618
|
-
tierIds: mintTierIds,
|
|
619
|
-
isOwnerMint: false // Not a manual mint
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
// Loop through each token ID and mint the corresponding NFT.
|
|
623
601
|
for (uint256 i; i < tokenIds.length; i++) {
|
|
624
|
-
// Get a reference to the token ID being iterated on.
|
|
625
|
-
uint256 tokenId = tokenIds[i];
|
|
626
|
-
|
|
627
602
|
emit Mint({
|
|
628
|
-
tokenId:
|
|
629
|
-
tierId:
|
|
603
|
+
tokenId: tokenIds[i],
|
|
604
|
+
tierId: tierIds[i],
|
|
630
605
|
beneficiary: beneficiary,
|
|
631
|
-
totalAmountPaid:
|
|
606
|
+
totalAmountPaid: totalAmountPaid,
|
|
632
607
|
caller: _msgSender()
|
|
633
608
|
});
|
|
634
609
|
|
|
635
|
-
// Mint the NFT.
|
|
636
610
|
// slither-disable-next-line reentrancy-events
|
|
637
|
-
_mint({to: beneficiary, tokenId:
|
|
611
|
+
_mint({to: beneficiary, tokenId: tokenIds[i]});
|
|
638
612
|
}
|
|
639
613
|
}
|
|
640
614
|
|
|
@@ -683,9 +657,25 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
683
657
|
|
|
684
658
|
// Mint NFTs from the tiers as specified.
|
|
685
659
|
if (tierIdsToMint.length != 0) {
|
|
660
|
+
uint256[] memory tokenIds;
|
|
661
|
+
uint256 restrictedCost;
|
|
662
|
+
uint256 totalAmountPaid = leftoverAmount;
|
|
663
|
+
|
|
664
|
+
// Record the mints.
|
|
686
665
|
// slither-disable-next-line reentrancy-events,reentrancy-no-eth
|
|
687
|
-
leftoverAmount =
|
|
688
|
-
|
|
666
|
+
(tokenIds, leftoverAmount, restrictedCost) =
|
|
667
|
+
STORE.recordMint({amount: leftoverAmount, tierIds: tierIdsToMint, isOwnerMint: false});
|
|
668
|
+
|
|
669
|
+
// Enforce `cantBuyWithCredits`: restricted tiers must be fully covered by fresh payment (not credits).
|
|
670
|
+
if (restrictedCost > value) revert JB721TiersHook_CantBuyWithCredits();
|
|
671
|
+
|
|
672
|
+
// Mint each token.
|
|
673
|
+
_mintTokens({
|
|
674
|
+
tokenIds: tokenIds,
|
|
675
|
+
tierIds: tierIdsToMint,
|
|
676
|
+
beneficiary: context.beneficiary,
|
|
677
|
+
totalAmountPaid: totalAmountPaid
|
|
678
|
+
});
|
|
689
679
|
}
|
|
690
680
|
}
|
|
691
681
|
|
|
@@ -331,7 +331,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
331
331
|
JBStored721Tier memory storedTier = _storedTierOf[hook][tierId];
|
|
332
332
|
|
|
333
333
|
// Check if voting units should be used. Price will be used otherwise.
|
|
334
|
-
(,, bool useVotingUnits
|
|
334
|
+
(,, bool useVotingUnits,,,) = _unpackBools(storedTier.packedBools);
|
|
335
335
|
|
|
336
336
|
// Return the address' voting units within the tier.
|
|
337
337
|
return balance * (useVotingUnits ? _tierVotingUnitsOf[hook][tierId] : storedTier.price);
|
|
@@ -376,7 +376,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
376
376
|
JBStored721Tier memory storedTier = _storedTierOf[hook][i];
|
|
377
377
|
|
|
378
378
|
// Parse the flags.
|
|
379
|
-
(,, bool useVotingUnits
|
|
379
|
+
(,, bool useVotingUnits,,,) = _unpackBools(storedTier.packedBools);
|
|
380
380
|
|
|
381
381
|
// Add the voting units for the address' balance in this tier.
|
|
382
382
|
// Use custom voting units if set. Otherwise, use the tier's price.
|
|
@@ -532,13 +532,8 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
532
532
|
// Get a reference to the reserve beneficiary.
|
|
533
533
|
address reserveBeneficiary = reserveBeneficiaryOf({hook: hook, tierId: tierId});
|
|
534
534
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
bool transfersPausable,
|
|
538
|
-
bool useVotingUnits,
|
|
539
|
-
bool cannotBeRemoved,
|
|
540
|
-
bool cannotIncreaseDiscountPercent
|
|
541
|
-
) = _unpackBools(storedTier.packedBools);
|
|
535
|
+
// Cache packed bools to avoid stack-too-deep from destructuring all 6 bools.
|
|
536
|
+
uint8 packed = storedTier.packedBools;
|
|
542
537
|
|
|
543
538
|
// slither-disable-next-line calls-loop
|
|
544
539
|
return JB721Tier({
|
|
@@ -547,17 +542,18 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
547
542
|
price: storedTier.price,
|
|
548
543
|
remainingSupply: storedTier.remainingSupply,
|
|
549
544
|
initialSupply: storedTier.initialSupply,
|
|
550
|
-
votingUnits:
|
|
545
|
+
votingUnits: (packed & 0x4 != 0) ? _tierVotingUnitsOf[hook][tierId] : storedTier.price,
|
|
551
546
|
// No reserve frequency if there is no reserve beneficiary.
|
|
552
547
|
reserveFrequency: reserveBeneficiary == address(0) ? 0 : storedTier.reserveFrequency,
|
|
553
548
|
reserveBeneficiary: reserveBeneficiary,
|
|
554
549
|
encodedIPFSUri: encodedIPFSUriOf[hook][tierId],
|
|
555
550
|
category: storedTier.category,
|
|
556
551
|
discountPercent: storedTier.discountPercent,
|
|
557
|
-
allowOwnerMint:
|
|
558
|
-
transfersPausable:
|
|
559
|
-
|
|
560
|
-
|
|
552
|
+
allowOwnerMint: (packed & 0x1 != 0),
|
|
553
|
+
transfersPausable: (packed & 0x2 != 0),
|
|
554
|
+
cantBeRemoved: (packed & 0x8 != 0),
|
|
555
|
+
cantIncreaseDiscountPercent: (packed & 0x10 != 0),
|
|
556
|
+
cantBuyWithCredits: (packed & 0x20 != 0),
|
|
561
557
|
splitPercent: storedTier.splitPercent,
|
|
562
558
|
resolvedUri: !includeResolvedUri || tokenUriResolverOf[hook] == IJB721TokenUriResolver(address(0))
|
|
563
559
|
? ""
|
|
@@ -678,15 +674,17 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
678
674
|
/// @param allowOwnerMint Whether or not owner minting is allowed in new tiers.
|
|
679
675
|
/// @param transfersPausable Whether or not 721 transfers can be paused.
|
|
680
676
|
/// @param useVotingUnits Whether or not custom voting unit amounts are allowed in new tiers.
|
|
681
|
-
/// @param
|
|
682
|
-
/// @param
|
|
677
|
+
/// @param cantBeRemoved Whether or not attempts to remove the tier will revert.
|
|
678
|
+
/// @param cantIncreaseDiscountPercent Whether or not attempts to increase the discount percent will revert.
|
|
679
|
+
/// @param cantBuyWithCredits Whether or not the tier cannot be purchased using accumulated pay credits.
|
|
683
680
|
/// @return packed The packed bools.
|
|
684
681
|
function _packBools(
|
|
685
682
|
bool allowOwnerMint,
|
|
686
683
|
bool transfersPausable,
|
|
687
684
|
bool useVotingUnits,
|
|
688
|
-
bool
|
|
689
|
-
bool
|
|
685
|
+
bool cantBeRemoved,
|
|
686
|
+
bool cantIncreaseDiscountPercent,
|
|
687
|
+
bool cantBuyWithCredits
|
|
690
688
|
)
|
|
691
689
|
internal
|
|
692
690
|
pure
|
|
@@ -696,18 +694,20 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
696
694
|
packed := or(allowOwnerMint, packed)
|
|
697
695
|
packed := or(shl(0x1, transfersPausable), packed)
|
|
698
696
|
packed := or(shl(0x2, useVotingUnits), packed)
|
|
699
|
-
packed := or(shl(0x3,
|
|
700
|
-
packed := or(shl(0x4,
|
|
697
|
+
packed := or(shl(0x3, cantBeRemoved), packed)
|
|
698
|
+
packed := or(shl(0x4, cantIncreaseDiscountPercent), packed)
|
|
699
|
+
packed := or(shl(0x5, cantBuyWithCredits), packed)
|
|
701
700
|
}
|
|
702
701
|
}
|
|
703
702
|
|
|
704
|
-
/// @notice Unpack
|
|
703
|
+
/// @notice Unpack six bools from a single uint8.
|
|
705
704
|
/// @param packed The packed bools.
|
|
706
705
|
/// @param allowOwnerMint Whether or not owner minting is allowed in new tiers.
|
|
707
706
|
/// @param transfersPausable Whether or not 721 transfers can be paused.
|
|
708
707
|
/// @param useVotingUnits Whether or not custom voting unit amounts are allowed in new tiers.
|
|
709
|
-
/// @param
|
|
710
|
-
/// @param
|
|
708
|
+
/// @param cantBeRemoved Whether or not the tier can be removed once added.
|
|
709
|
+
/// @param cantIncreaseDiscountPercent Whether or not the discount percent cannot be increased.
|
|
710
|
+
/// @param cantBuyWithCredits Whether or not the tier cannot be purchased using accumulated pay credits.
|
|
711
711
|
function _unpackBools(uint8 packed)
|
|
712
712
|
internal
|
|
713
713
|
pure
|
|
@@ -715,16 +715,18 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
715
715
|
bool allowOwnerMint,
|
|
716
716
|
bool transfersPausable,
|
|
717
717
|
bool useVotingUnits,
|
|
718
|
-
bool
|
|
719
|
-
bool
|
|
718
|
+
bool cantBeRemoved,
|
|
719
|
+
bool cantIncreaseDiscountPercent,
|
|
720
|
+
bool cantBuyWithCredits
|
|
720
721
|
)
|
|
721
722
|
{
|
|
722
723
|
assembly {
|
|
723
724
|
allowOwnerMint := iszero(iszero(and(0x1, packed)))
|
|
724
725
|
transfersPausable := iszero(iszero(and(0x2, packed)))
|
|
725
726
|
useVotingUnits := iszero(iszero(and(0x4, packed)))
|
|
726
|
-
|
|
727
|
-
|
|
727
|
+
cantBeRemoved := iszero(iszero(and(0x8, packed)))
|
|
728
|
+
cantIncreaseDiscountPercent := iszero(iszero(and(0x10, packed)))
|
|
729
|
+
cantBuyWithCredits := iszero(iszero(and(0x20, packed)))
|
|
728
730
|
}
|
|
729
731
|
}
|
|
730
732
|
|
|
@@ -906,8 +908,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
906
908
|
allowOwnerMint: tierToAdd.allowOwnerMint,
|
|
907
909
|
transfersPausable: tierToAdd.transfersPausable,
|
|
908
910
|
useVotingUnits: tierToAdd.useVotingUnits,
|
|
909
|
-
|
|
910
|
-
|
|
911
|
+
cantBeRemoved: tierToAdd.cantBeRemoved,
|
|
912
|
+
cantIncreaseDiscountPercent: tierToAdd.cantIncreaseDiscountPercent,
|
|
913
|
+
cantBuyWithCredits: tierToAdd.cantBuyWithCredits
|
|
911
914
|
})
|
|
912
915
|
});
|
|
913
916
|
|
|
@@ -1055,6 +1058,8 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1055
1058
|
/// @param isOwnerMint A flag indicating whether this function is being directly called by the 721 contract's owner.
|
|
1056
1059
|
/// @return tokenIds The token IDs of the NFTs which were minted.
|
|
1057
1060
|
/// @return leftoverAmount The `amount` remaining after minting.
|
|
1061
|
+
/// @return restrictedCost Total cost of tiers with `cantBuyWithCredits` set. The caller can use this to enforce
|
|
1062
|
+
/// credit restrictions.
|
|
1058
1063
|
function recordMint(
|
|
1059
1064
|
uint256 amount,
|
|
1060
1065
|
uint16[] calldata tierIds,
|
|
@@ -1062,7 +1067,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1062
1067
|
)
|
|
1063
1068
|
external
|
|
1064
1069
|
override
|
|
1065
|
-
returns (uint256[] memory tokenIds, uint256 leftoverAmount)
|
|
1070
|
+
returns (uint256[] memory tokenIds, uint256 leftoverAmount, uint256 restrictedCost)
|
|
1066
1071
|
{
|
|
1067
1072
|
// Set the leftover amount as the initial amount.
|
|
1068
1073
|
leftoverAmount = amount;
|
|
@@ -1079,6 +1084,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1079
1084
|
// Initialize a `JBBitmapWord` for checking whether tiers have been removed.
|
|
1080
1085
|
JBBitmapWord memory bitmapWord;
|
|
1081
1086
|
|
|
1087
|
+
// Track total cost of tiers that can't be bought with credits (order-independent check).
|
|
1088
|
+
uint256 restrictedCost;
|
|
1089
|
+
|
|
1082
1090
|
for (uint256 i; i < numberOfTiers; i++) {
|
|
1083
1091
|
// Set the tier ID being iterated on.
|
|
1084
1092
|
uint256 tierId = tierIds[i];
|
|
@@ -1092,7 +1100,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1092
1100
|
storedTier = _storedTierOf[msg.sender][tierId];
|
|
1093
1101
|
|
|
1094
1102
|
// Parse the flags.
|
|
1095
|
-
(bool allowOwnerMint
|
|
1103
|
+
(bool allowOwnerMint,,,,, bool cantBuyWithCredits) = _unpackBools(storedTier.packedBools);
|
|
1096
1104
|
|
|
1097
1105
|
// If this is an owner mint, make sure owner minting is allowed.
|
|
1098
1106
|
if (isOwnerMint && !allowOwnerMint) revert JB721TiersHookStore_CantMintManually(tierId);
|
|
@@ -1113,6 +1121,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1113
1121
|
});
|
|
1114
1122
|
}
|
|
1115
1123
|
|
|
1124
|
+
// Accumulate cost of credit-restricted tiers.
|
|
1125
|
+
if (cantBuyWithCredits) restrictedCost += price;
|
|
1126
|
+
|
|
1116
1127
|
// Make sure the `amount` is greater than or equal to the tier's price.
|
|
1117
1128
|
if (price > leftoverAmount) revert JB721TiersHookStore_PriceExceedsAmount(price, leftoverAmount);
|
|
1118
1129
|
|
|
@@ -1188,10 +1199,10 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1188
1199
|
JBStored721Tier storage storedTier = _storedTierOf[msg.sender][tierId];
|
|
1189
1200
|
|
|
1190
1201
|
// Parse the flags.
|
|
1191
|
-
(,,, bool
|
|
1202
|
+
(,,, bool cantBeRemoved,,) = _unpackBools(storedTier.packedBools);
|
|
1192
1203
|
|
|
1193
1204
|
// Make sure the tier can be removed.
|
|
1194
|
-
if (
|
|
1205
|
+
if (cantBeRemoved) revert JB721TiersHookStore_CantRemoveTier(tierId);
|
|
1195
1206
|
|
|
1196
1207
|
// Remove the tier by marking it as removed in the bitmap.
|
|
1197
1208
|
_removedTiersBitmapWordOf[msg.sender].removeTier(tierId);
|
|
@@ -1217,10 +1228,10 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1217
1228
|
JBStored721Tier storage storedTier = _storedTierOf[msg.sender][tierId];
|
|
1218
1229
|
|
|
1219
1230
|
// Parse the flags.
|
|
1220
|
-
(,,,, bool
|
|
1231
|
+
(,,,, bool cantIncreaseDiscountPercent,) = _unpackBools(storedTier.packedBools);
|
|
1221
1232
|
|
|
1222
1233
|
// Make sure that increasing the discount is allowed for the tier.
|
|
1223
|
-
if (discountPercent > storedTier.discountPercent &&
|
|
1234
|
+
if (discountPercent > storedTier.discountPercent && cantIncreaseDiscountPercent) {
|
|
1224
1235
|
revert JB721TiersHookStore_DiscountPercentIncreaseNotAllowed(discountPercent, storedTier.discountPercent);
|
|
1225
1236
|
}
|
|
1226
1237
|
|
|
@@ -193,13 +193,14 @@ interface IJB721TiersHookStore {
|
|
|
193
193
|
/// @param isOwnerMint Whether this is a direct owner mint.
|
|
194
194
|
/// @return tokenIds The token IDs of the NFTs which were minted.
|
|
195
195
|
/// @return leftoverAmount The amount remaining after minting.
|
|
196
|
+
/// @return restrictedCost Total cost of tiers with `cantBuyWithCredits` set.
|
|
196
197
|
function recordMint(
|
|
197
198
|
uint256 amount,
|
|
198
199
|
uint16[] calldata tierIds,
|
|
199
200
|
bool isOwnerMint
|
|
200
201
|
)
|
|
201
202
|
external
|
|
202
|
-
returns (uint256[] memory tokenIds, uint256 leftoverAmount);
|
|
203
|
+
returns (uint256[] memory tokenIds, uint256 leftoverAmount, uint256 restrictedCost);
|
|
203
204
|
|
|
204
205
|
/// @notice Record reserve 721 minting for the provided tier ID.
|
|
205
206
|
/// @param tierId The ID of the tier to mint reserves from.
|
|
@@ -15,8 +15,9 @@ pragma solidity ^0.8.0;
|
|
|
15
15
|
/// @custom:member discountPercent The discount that should be applied to the tier.
|
|
16
16
|
/// @custom:member allowOwnerMint A boolean indicating whether the contract's owner can mint NFTs from this tier
|
|
17
17
|
/// on-demand.
|
|
18
|
-
/// @custom:member
|
|
19
|
-
/// @custom:member
|
|
18
|
+
/// @custom:member cantBeRemoved A boolean indicating whether attempts to remove this tier will revert.
|
|
19
|
+
/// @custom:member cantIncreaseDiscountPercent If the tier cannot have its discount increased.
|
|
20
|
+
/// @custom:member cantBuyWithCredits If true, this tier cannot be purchased using accumulated pay credits.
|
|
20
21
|
/// @custom:member transfersPausable A boolean indicating whether transfers for NFTs in tier can be paused.
|
|
21
22
|
/// @custom:member splitPercent The percentage of the tier's price that gets routed to the project's split group when
|
|
22
23
|
/// an NFT from this tier is minted. Out of `JBConstants.SPLITS_TOTAL_PERCENT`.
|
|
@@ -37,8 +38,9 @@ struct JB721Tier {
|
|
|
37
38
|
uint8 discountPercent;
|
|
38
39
|
bool allowOwnerMint;
|
|
39
40
|
bool transfersPausable;
|
|
40
|
-
bool
|
|
41
|
-
bool
|
|
41
|
+
bool cantBeRemoved;
|
|
42
|
+
bool cantIncreaseDiscountPercent;
|
|
43
|
+
bool cantBuyWithCredits;
|
|
42
44
|
uint32 splitPercent;
|
|
43
45
|
string resolvedUri;
|
|
44
46
|
}
|
|
@@ -24,8 +24,10 @@ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
|
24
24
|
/// @custom:member transfersPausable A boolean indicating whether transfers for NFTs in tier can be paused.
|
|
25
25
|
/// @custom:member useVotingUnits A boolean indicating whether the `votingUnits` should be used to calculate voting
|
|
26
26
|
/// power. If `useVotingUnits` is false, voting power is based on the tier's price.
|
|
27
|
-
/// @custom:member
|
|
27
|
+
/// @custom:member cantBeRemoved If the tier cannot be removed once added.
|
|
28
28
|
/// @custom:member cannotIncreaseDiscount If the tier cannot have its discount increased.
|
|
29
|
+
/// @custom:member cantBuyWithCredits If true, this tier cannot be purchased using accumulated pay credits. Only fresh
|
|
30
|
+
/// payment value counts toward this tier's price.
|
|
29
31
|
/// @custom:member splitPercent The percentage of the tier's price that gets routed to the tier's split group when
|
|
30
32
|
/// an NFT from this tier is minted. Out of `JBConstants.SPLITS_TOTAL_PERCENT`.
|
|
31
33
|
/// @custom:member splits The splits to use for this tier's split group. These define where the split portion of the
|
|
@@ -45,8 +47,9 @@ struct JB721TierConfig {
|
|
|
45
47
|
bool useReserveBeneficiaryAsDefault;
|
|
46
48
|
bool transfersPausable;
|
|
47
49
|
bool useVotingUnits;
|
|
48
|
-
bool
|
|
49
|
-
bool
|
|
50
|
+
bool cantBeRemoved;
|
|
51
|
+
bool cantIncreaseDiscountPercent;
|
|
52
|
+
bool cantBuyWithCredits;
|
|
50
53
|
uint32 splitPercent;
|
|
51
54
|
JBSplit[] splits;
|
|
52
55
|
}
|
|
@@ -12,7 +12,7 @@ pragma solidity ^0.8.0;
|
|
|
12
12
|
/// tier. With a `reserveFrequency` of 5, an extra NFT will be minted for the `reserveBeneficiary` for every 5 NFTs
|
|
13
13
|
/// purchased.
|
|
14
14
|
/// @custom:member packedBools Packed boolean flags: allowOwnerMint, transfersPausable, useVotingUnits,
|
|
15
|
-
///
|
|
15
|
+
/// cantBeRemoved, cantIncreaseDiscountPercent, cantBuyWithCredits.
|
|
16
16
|
// forge-lint: disable-next-line(pascal-case-struct)
|
|
17
17
|
struct JBStored721Tier {
|
|
18
18
|
uint104 price;
|
|
@@ -84,8 +84,9 @@ contract NFTHookAttacks is UnitTestSetup {
|
|
|
84
84
|
allowOwnerMint: false,
|
|
85
85
|
useReserveBeneficiaryAsDefault: false,
|
|
86
86
|
transfersPausable: false,
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
cantBeRemoved: false,
|
|
88
|
+
cantIncreaseDiscountPercent: false,
|
|
89
|
+
cantBuyWithCredits: false,
|
|
89
90
|
useVotingUnits: false,
|
|
90
91
|
splitPercent: 0,
|
|
91
92
|
splits: new JBSplit[](0)
|
|
@@ -115,7 +116,7 @@ contract NFTHookAttacks is UnitTestSetup {
|
|
|
115
116
|
/// @notice Set discount to 100%, verify the effective price for the tier.
|
|
116
117
|
function test_maxDiscountPercent_effectivePrice() public {
|
|
117
118
|
defaultTierConfig.discountPercent = 0;
|
|
118
|
-
defaultTierConfig.
|
|
119
|
+
defaultTierConfig.cantIncreaseDiscountPercent = false;
|
|
119
120
|
|
|
120
121
|
JB721TiersHook targetHook = _initHookDefaultTiers(1);
|
|
121
122
|
|
|
@@ -131,12 +132,12 @@ contract NFTHookAttacks is UnitTestSetup {
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
// =========================================================================
|
|
134
|
-
// Test 3:
|
|
135
|
+
// Test 3: cantIncreaseDiscountPercent flag enforcement
|
|
135
136
|
// =========================================================================
|
|
136
137
|
/// @notice Try to increase discount when the flag forbids it.
|
|
137
|
-
function
|
|
138
|
+
function test_cantIncreaseDiscountPercent_enforcement() public {
|
|
138
139
|
defaultTierConfig.discountPercent = 10;
|
|
139
|
-
defaultTierConfig.
|
|
140
|
+
defaultTierConfig.cantIncreaseDiscountPercent = true;
|
|
140
141
|
|
|
141
142
|
JB721TiersHook targetHook = _initHookDefaultTiers(1);
|
|
142
143
|
|
|
@@ -371,8 +372,9 @@ contract NFTHookAttacks is UnitTestSetup {
|
|
|
371
372
|
allowOwnerMint: true,
|
|
372
373
|
useReserveBeneficiaryAsDefault: false,
|
|
373
374
|
transfersPausable: false,
|
|
374
|
-
|
|
375
|
-
|
|
375
|
+
cantBeRemoved: false,
|
|
376
|
+
cantIncreaseDiscountPercent: false,
|
|
377
|
+
cantBuyWithCredits: false,
|
|
376
378
|
useVotingUnits: false,
|
|
377
379
|
splitPercent: 0,
|
|
378
380
|
splits: new JBSplit[](0)
|
|
@@ -800,8 +800,9 @@ contract Test_TiersHook_E2E is TestBaseWorkflow {
|
|
|
800
800
|
useReserveBeneficiaryAsDefault: false,
|
|
801
801
|
transfersPausable: false,
|
|
802
802
|
useVotingUnits: false,
|
|
803
|
-
|
|
804
|
-
|
|
803
|
+
cantBeRemoved: false,
|
|
804
|
+
cantIncreaseDiscountPercent: false,
|
|
805
|
+
cantBuyWithCredits: false,
|
|
805
806
|
splitPercent: 0,
|
|
806
807
|
splits: new JBSplit[](0)
|
|
807
808
|
});
|
|
@@ -892,8 +893,9 @@ contract Test_TiersHook_E2E is TestBaseWorkflow {
|
|
|
892
893
|
useReserveBeneficiaryAsDefault: false,
|
|
893
894
|
transfersPausable: false,
|
|
894
895
|
useVotingUnits: false,
|
|
895
|
-
|
|
896
|
-
|
|
896
|
+
cantBeRemoved: false,
|
|
897
|
+
cantIncreaseDiscountPercent: false,
|
|
898
|
+
cantBuyWithCredits: false,
|
|
897
899
|
splitPercent: 0,
|
|
898
900
|
splits: new JBSplit[](0)
|
|
899
901
|
});
|
package/test/Fork.t.sol
CHANGED
|
@@ -362,8 +362,9 @@ contract Fork_721Hook_Test is Test {
|
|
|
362
362
|
useReserveBeneficiaryAsDefault: false,
|
|
363
363
|
transfersPausable: false,
|
|
364
364
|
useVotingUnits: false,
|
|
365
|
-
|
|
366
|
-
|
|
365
|
+
cantBeRemoved: false,
|
|
366
|
+
cantIncreaseDiscountPercent: false,
|
|
367
|
+
cantBuyWithCredits: false,
|
|
367
368
|
splitPercent: 0,
|
|
368
369
|
splits: new JBSplit[](0)
|
|
369
370
|
});
|
|
@@ -792,8 +793,9 @@ contract Fork_721Hook_Test is Test {
|
|
|
792
793
|
useReserveBeneficiaryAsDefault: false,
|
|
793
794
|
transfersPausable: false,
|
|
794
795
|
useVotingUnits: false,
|
|
795
|
-
|
|
796
|
-
|
|
796
|
+
cantBeRemoved: false,
|
|
797
|
+
cantIncreaseDiscountPercent: false,
|
|
798
|
+
cantBuyWithCredits: false,
|
|
797
799
|
splitPercent: 0,
|
|
798
800
|
splits: new JBSplit[](0)
|
|
799
801
|
});
|
|
@@ -824,8 +826,9 @@ contract Fork_721Hook_Test is Test {
|
|
|
824
826
|
useReserveBeneficiaryAsDefault: false,
|
|
825
827
|
transfersPausable: false,
|
|
826
828
|
useVotingUnits: false,
|
|
827
|
-
|
|
828
|
-
|
|
829
|
+
cantBeRemoved: false,
|
|
830
|
+
cantIncreaseDiscountPercent: false,
|
|
831
|
+
cantBuyWithCredits: false,
|
|
829
832
|
splitPercent: 0,
|
|
830
833
|
splits: new JBSplit[](0)
|
|
831
834
|
});
|
|
@@ -835,10 +838,10 @@ contract Fork_721Hook_Test is Test {
|
|
|
835
838
|
IJB721TiersHook(hook).adjustTiers(newTiers, new uint256[](0));
|
|
836
839
|
}
|
|
837
840
|
|
|
838
|
-
/// @notice
|
|
839
|
-
function
|
|
841
|
+
/// @notice cantBeRemoved: removing an immutable tier should revert.
|
|
842
|
+
function test_fork_cantBeRemoved_reverts() public {
|
|
840
843
|
JB721TierConfig[] memory tierConfigs = _makeStandardTiers(1, 10, false);
|
|
841
|
-
tierConfigs[0].
|
|
844
|
+
tierConfigs[0].cantBeRemoved = true;
|
|
842
845
|
JB721TiersHookFlags memory flags = _defaultFlags();
|
|
843
846
|
(, address hook) = _launchProject(tierConfigs, flags, 5000, true, 0x00);
|
|
844
847
|
|
|
@@ -899,11 +902,11 @@ contract Fork_721Hook_Test is Test {
|
|
|
899
902
|
assertEq(IERC721(hook).balanceOf(beneficiary), 1, "NFT minted for free");
|
|
900
903
|
}
|
|
901
904
|
|
|
902
|
-
/// @notice
|
|
905
|
+
/// @notice cantIncreaseDiscountPercent: setting higher discount reverts.
|
|
903
906
|
function test_fork_cannotIncreaseDiscount() public {
|
|
904
907
|
JB721TierConfig[] memory tierConfigs = _makeStandardTiers(1, 10, false);
|
|
905
908
|
tierConfigs[0].discountPercent = 50;
|
|
906
|
-
tierConfigs[0].
|
|
909
|
+
tierConfigs[0].cantIncreaseDiscountPercent = true;
|
|
907
910
|
JB721TiersHookFlags memory flags = _defaultFlags();
|
|
908
911
|
(, address hook) = _launchProject(tierConfigs, flags, 5000, true, 0x00);
|
|
909
912
|
|
|
@@ -1131,8 +1134,9 @@ contract Fork_721Hook_Test is Test {
|
|
|
1131
1134
|
useReserveBeneficiaryAsDefault: false,
|
|
1132
1135
|
transfersPausable: false,
|
|
1133
1136
|
useVotingUnits: false,
|
|
1134
|
-
|
|
1135
|
-
|
|
1137
|
+
cantBeRemoved: false,
|
|
1138
|
+
cantIncreaseDiscountPercent: false,
|
|
1139
|
+
cantBuyWithCredits: false,
|
|
1136
1140
|
splitPercent: 0,
|
|
1137
1141
|
splits: new JBSplit[](0)
|
|
1138
1142
|
});
|
|
@@ -1524,8 +1528,9 @@ contract Fork_721Hook_Test is Test {
|
|
|
1524
1528
|
useReserveBeneficiaryAsDefault: false,
|
|
1525
1529
|
transfersPausable: false,
|
|
1526
1530
|
useVotingUnits: false,
|
|
1527
|
-
|
|
1528
|
-
|
|
1531
|
+
cantBeRemoved: false,
|
|
1532
|
+
cantIncreaseDiscountPercent: false,
|
|
1533
|
+
cantBuyWithCredits: false,
|
|
1529
1534
|
splitPercent: 0,
|
|
1530
1535
|
splits: new JBSplit[](0)
|
|
1531
1536
|
});
|
|
@@ -1770,8 +1775,9 @@ contract Fork_721Hook_Test is Test {
|
|
|
1770
1775
|
useReserveBeneficiaryAsDefault: false,
|
|
1771
1776
|
transfersPausable: false,
|
|
1772
1777
|
useVotingUnits: false,
|
|
1773
|
-
|
|
1774
|
-
|
|
1778
|
+
cantBeRemoved: false,
|
|
1779
|
+
cantIncreaseDiscountPercent: false,
|
|
1780
|
+
cantBuyWithCredits: false,
|
|
1775
1781
|
splitPercent: 0,
|
|
1776
1782
|
splits: new JBSplit[](0)
|
|
1777
1783
|
});
|
|
@@ -2045,8 +2051,9 @@ contract Fork_721Hook_Test is Test {
|
|
|
2045
2051
|
useReserveBeneficiaryAsDefault: false,
|
|
2046
2052
|
transfersPausable: false,
|
|
2047
2053
|
useVotingUnits: false,
|
|
2048
|
-
|
|
2049
|
-
|
|
2054
|
+
cantBeRemoved: false,
|
|
2055
|
+
cantIncreaseDiscountPercent: false,
|
|
2056
|
+
cantBuyWithCredits: false,
|
|
2050
2057
|
splitPercent: splitPct,
|
|
2051
2058
|
splits: splits
|
|
2052
2059
|
});
|