@bananapus/721-hook-v6 0.0.1
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/.gas-snapshot +152 -0
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/SKILLS.md +140 -0
- package/docs/book.css +13 -0
- package/docs/book.toml +12 -0
- package/docs/solidity.min.js +74 -0
- package/docs/src/README.md +253 -0
- package/docs/src/SUMMARY.md +38 -0
- package/docs/src/src/JB721TiersHook.sol/contract.JB721TiersHook.md +645 -0
- package/docs/src/src/JB721TiersHookDeployer.sol/contract.JB721TiersHookDeployer.md +99 -0
- package/docs/src/src/JB721TiersHookProjectDeployer.sol/contract.JB721TiersHookProjectDeployer.md +288 -0
- package/docs/src/src/JB721TiersHookStore.sol/contract.JB721TiersHookStore.md +1096 -0
- package/docs/src/src/README.md +11 -0
- package/docs/src/src/abstract/ERC721.sol/abstract.ERC721.md +430 -0
- package/docs/src/src/abstract/JB721Hook.sol/abstract.JB721Hook.md +309 -0
- package/docs/src/src/abstract/README.md +5 -0
- package/docs/src/src/interfaces/IJB721Hook.sol/interface.IJB721Hook.md +29 -0
- package/docs/src/src/interfaces/IJB721TiersHook.sol/interface.IJB721TiersHook.md +203 -0
- package/docs/src/src/interfaces/IJB721TiersHookDeployer.sol/interface.IJB721TiersHookDeployer.md +25 -0
- package/docs/src/src/interfaces/IJB721TiersHookProjectDeployer.sol/interface.IJB721TiersHookProjectDeployer.md +64 -0
- package/docs/src/src/interfaces/IJB721TiersHookStore.sol/interface.IJB721TiersHookStore.md +265 -0
- package/docs/src/src/interfaces/IJB721TokenUriResolver.sol/interface.IJB721TokenUriResolver.md +12 -0
- package/docs/src/src/interfaces/README.md +9 -0
- package/docs/src/src/libraries/JB721Constants.sol/library.JB721Constants.md +14 -0
- package/docs/src/src/libraries/JB721TiersRulesetMetadataResolver.sol/library.JB721TiersRulesetMetadataResolver.md +68 -0
- package/docs/src/src/libraries/JBBitmap.sol/library.JBBitmap.md +82 -0
- package/docs/src/src/libraries/JBIpfsDecoder.sol/library.JBIpfsDecoder.md +61 -0
- package/docs/src/src/libraries/README.md +7 -0
- package/docs/src/src/structs/JB721InitTiersConfig.sol/struct.JB721InitTiersConfig.md +27 -0
- package/docs/src/src/structs/JB721Tier.sol/struct.JB721Tier.md +59 -0
- package/docs/src/src/structs/JB721TierConfig.sol/struct.JB721TierConfig.md +60 -0
- package/docs/src/src/structs/JB721TiersHookFlags.sol/struct.JB721TiersHookFlags.md +26 -0
- package/docs/src/src/structs/JB721TiersMintReservesConfig.sol/struct.JB721TiersMintReservesConfig.md +16 -0
- package/docs/src/src/structs/JB721TiersRulesetMetadata.sol/struct.JB721TiersRulesetMetadata.md +20 -0
- package/docs/src/src/structs/JB721TiersSetDiscountPercentConfig.sol/struct.JB721TiersSetDiscountPercentConfig.md +16 -0
- package/docs/src/src/structs/JBBitmapWord.sol/struct.JBBitmapWord.md +19 -0
- package/docs/src/src/structs/JBDeploy721TiersHookConfig.sol/struct.JBDeploy721TiersHookConfig.md +34 -0
- package/docs/src/src/structs/JBLaunchProjectConfig.sol/struct.JBLaunchProjectConfig.md +23 -0
- package/docs/src/src/structs/JBLaunchRulesetsConfig.sol/struct.JBLaunchRulesetsConfig.md +22 -0
- package/docs/src/src/structs/JBPayDataHookRulesetConfig.sol/struct.JBPayDataHookRulesetConfig.md +51 -0
- package/docs/src/src/structs/JBPayDataHookRulesetMetadata.sol/struct.JBPayDataHookRulesetMetadata.md +66 -0
- package/docs/src/src/structs/JBQueueRulesetsConfig.sol/struct.JBQueueRulesetsConfig.md +21 -0
- package/docs/src/src/structs/JBStored721Tier.sol/struct.JBStored721Tier.md +42 -0
- package/docs/src/src/structs/README.md +18 -0
- package/foundry.lock +11 -0
- package/foundry.toml +22 -0
- package/package.json +31 -0
- package/remappings.txt +1 -0
- package/script/Deploy.s.sol +140 -0
- package/script/helpers/Hook721DeploymentLib.sol +81 -0
- package/slither-ci.config.json +10 -0
- package/sphinx.lock +476 -0
- package/src/JB721TiersHook.sol +765 -0
- package/src/JB721TiersHookDeployer.sol +114 -0
- package/src/JB721TiersHookProjectDeployer.sol +413 -0
- package/src/JB721TiersHookStore.sol +1195 -0
- package/src/abstract/ERC721.sol +484 -0
- package/src/abstract/JB721Hook.sol +279 -0
- package/src/interfaces/IJB721Hook.sol +21 -0
- package/src/interfaces/IJB721TiersHook.sol +135 -0
- package/src/interfaces/IJB721TiersHookDeployer.sol +22 -0
- package/src/interfaces/IJB721TiersHookProjectDeployer.sol +76 -0
- package/src/interfaces/IJB721TiersHookStore.sol +220 -0
- package/src/interfaces/IJB721TokenUriResolver.sol +10 -0
- package/src/libraries/JB721Constants.sol +7 -0
- package/src/libraries/JB721TiersRulesetMetadataResolver.sol +44 -0
- package/src/libraries/JBBitmap.sol +57 -0
- package/src/libraries/JBIpfsDecoder.sol +95 -0
- package/src/structs/JB721InitTiersConfig.sol +20 -0
- package/src/structs/JB721Tier.sol +39 -0
- package/src/structs/JB721TierConfig.sol +40 -0
- package/src/structs/JB721TiersHookFlags.sol +17 -0
- package/src/structs/JB721TiersMintReservesConfig.sol +9 -0
- package/src/structs/JB721TiersRulesetMetadata.sol +12 -0
- package/src/structs/JB721TiersSetDiscountPercentConfig.sol +9 -0
- package/src/structs/JBBitmapWord.sol +11 -0
- package/src/structs/JBDeploy721TiersHookConfig.sol +25 -0
- package/src/structs/JBLaunchProjectConfig.sol +18 -0
- package/src/structs/JBLaunchRulesetsConfig.sol +17 -0
- package/src/structs/JBPayDataHookRulesetConfig.sol +44 -0
- package/src/structs/JBPayDataHookRulesetMetadata.sol +46 -0
- package/src/structs/JBQueueRulesetsConfig.sol +13 -0
- package/src/structs/JBStored721Tier.sol +24 -0
- package/test/721HookAttacks.t.sol +396 -0
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +944 -0
- package/test/invariants/TierLifecycleInvariant.t.sol +187 -0
- package/test/invariants/TieredHookStoreInvariant.t.sol +81 -0
- package/test/invariants/handlers/TierLifecycleHandler.sol +262 -0
- package/test/invariants/handlers/TierStoreHandler.sol +155 -0
- package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +141 -0
- package/test/unit/JBBitmap.t.sol +169 -0
- package/test/unit/JBIpfsDecoder.t.sol +131 -0
- package/test/unit/M6_TierSupplyCheck.t.sol +220 -0
- package/test/unit/adjustTier_Unit.t.sol +1740 -0
- package/test/unit/deployer_Unit.t.sol +103 -0
- package/test/unit/getters_constructor_Unit.t.sol +548 -0
- package/test/unit/mintFor_mintReservesFor_Unit.t.sol +443 -0
- package/test/unit/pay_Unit.t.sol +1537 -0
- package/test/unit/redeem_Unit.t.sol +459 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
5
|
+
|
|
6
|
+
import {JBPayDataHookRulesetConfig} from "./JBPayDataHookRulesetConfig.sol";
|
|
7
|
+
|
|
8
|
+
/// @custom:member projectId The ID of the project to launch rulesets for.
|
|
9
|
+
/// @custom:member rulesetConfigurations The ruleset configurations to queue.
|
|
10
|
+
/// @custom:member terminalConfigurations The terminal configurations to add for the project.
|
|
11
|
+
/// @custom:member memo A memo to pass along to the emitted event.
|
|
12
|
+
struct JBLaunchRulesetsConfig {
|
|
13
|
+
uint56 projectId;
|
|
14
|
+
JBPayDataHookRulesetConfig[] rulesetConfigurations;
|
|
15
|
+
JBTerminalConfig[] terminalConfigurations;
|
|
16
|
+
string memo;
|
|
17
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
|
|
5
|
+
import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
|
|
6
|
+
import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
|
|
7
|
+
|
|
8
|
+
import {JBPayDataHookRulesetMetadata} from "./JBPayDataHookRulesetMetadata.sol";
|
|
9
|
+
|
|
10
|
+
/// @custom:member mustStartAtOrAfter The earliest time the ruleset can start.
|
|
11
|
+
/// @custom:member duration The number of seconds the ruleset lasts for, after which a new ruleset will start. A
|
|
12
|
+
/// duration of 0 means that the ruleset will stay active until the project owner explicitly issues a reconfiguration,
|
|
13
|
+
/// at which point a new ruleset will immediately start with the updated properties. If the duration is greater than 0,
|
|
14
|
+
/// a project owner cannot make changes to a ruleset's parameters while it is active – any proposed changes will apply
|
|
15
|
+
/// to the subsequent ruleset. If no changes are proposed, a ruleset rolls over to another one with the same properties
|
|
16
|
+
/// but new `start` timestamp and a decayed `weight`.
|
|
17
|
+
/// @custom:member weight A fixed point number with 18 decimals that contracts can use to base arbitrary calculations
|
|
18
|
+
/// on. For example, payment terminals can use this to determine how many tokens should be minted when a payment is
|
|
19
|
+
/// received.
|
|
20
|
+
/// @custom:member weightCutPercent A percent by how much the `weight` of the subsequent ruleset should be reduced, if
|
|
21
|
+
/// the
|
|
22
|
+
/// project owner hasn't queued the subsequent ruleset with an explicit `weight`. If it's 0, each ruleset will have
|
|
23
|
+
/// equal weight. If the number is 90%, the next ruleset will have a 10% smaller weight. This weight is out of
|
|
24
|
+
/// `JBConstants.MAX_WEIGHT_CUT_PERCENT`.
|
|
25
|
+
/// @custom:member approvalHook An address of a contract that says whether a proposed ruleset should be accepted or
|
|
26
|
+
/// rejected. It
|
|
27
|
+
/// can be used to create rules around how a project owner can change ruleset parameters over time.
|
|
28
|
+
/// @custom:member metadata Metadata specifying the controller-specific parameters that a ruleset can have. These
|
|
29
|
+
/// properties cannot change until the next ruleset starts.
|
|
30
|
+
/// @custom:member splitGroups An array of splits to use for any number of groups while the ruleset is active.
|
|
31
|
+
/// @custom:member fundAccessLimitGroups An array of structs which dictate the amount of funds a project can access from
|
|
32
|
+
/// its balance in each payment terminal while the ruleset is active. Amounts are fixed point numbers using the same
|
|
33
|
+
/// number of decimals as the corresponding terminal. The `payoutLimit` and `surplusAllowance` parameters must fit in
|
|
34
|
+
/// a `uint232`.
|
|
35
|
+
struct JBPayDataHookRulesetConfig {
|
|
36
|
+
uint48 mustStartAtOrAfter;
|
|
37
|
+
uint32 duration;
|
|
38
|
+
uint112 weight;
|
|
39
|
+
uint32 weightCutPercent;
|
|
40
|
+
IJBRulesetApprovalHook approvalHook;
|
|
41
|
+
JBPayDataHookRulesetMetadata metadata;
|
|
42
|
+
JBSplitGroup[] splitGroups;
|
|
43
|
+
JBFundAccessLimitGroup[] fundAccessLimitGroups;
|
|
44
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
/// @custom:member reservedPercent The reserved percent of the ruleset. This number is a percentage calculated out of
|
|
5
|
+
/// `JBConstants.MAX_RESERVED_PERCENT`.
|
|
6
|
+
/// @custom:member cashOutTaxRate The cash out tax rate of the ruleset. This number is a percentage calculated out of
|
|
7
|
+
/// `JBConstants.MAX_CASH_OUT_TAX_RATE`.
|
|
8
|
+
/// @custom:member baseCurrency The currency on which to base the ruleset's weight.
|
|
9
|
+
/// @custom:member pausePay A flag indicating if the pay functionality should be paused during the ruleset.
|
|
10
|
+
/// @custom:member pauseCreditTransfers A flag indicating if the project token transfer functionality should be paused
|
|
11
|
+
/// during the funding cycle.
|
|
12
|
+
/// @custom:member allowOwnerMinting A flag indicating if the project owner or an operator with the `MINT_TOKENS`
|
|
13
|
+
/// permission from the owner should be allowed to mint project tokens on demand during this ruleset.
|
|
14
|
+
/// @custom:member allowTerminalMigration A flag indicating if migrating terminals should be allowed during this
|
|
15
|
+
/// ruleset.
|
|
16
|
+
/// @custom:member allowSetTerminals A flag indicating if a project's terminals can be added or removed.
|
|
17
|
+
/// @custom:member allowSetController A flag indicating if a project's controller can be changed.
|
|
18
|
+
/// @custom:member allowAddAccountingContext A flag indicating if a project can add new accounting contexts for its
|
|
19
|
+
/// terminals to use.
|
|
20
|
+
/// @custom:member allowAddPriceFeed A flag indicating if a project can add new price feeds to calculate exchange rates
|
|
21
|
+
/// between its tokens.
|
|
22
|
+
/// @custom:member holdFees A flag indicating if fees should be held during this ruleset.
|
|
23
|
+
/// @custom:member useTotalSurplusForCashOut A flag indicating if cash outs should use the project's balance held
|
|
24
|
+
/// in all terminals instead of the project's local terminal balance from which the cash out is being fulfilled.
|
|
25
|
+
/// @custom:member useDataHookForCashOuts A flag indicating if the data hook should be used for cash out transactions
|
|
26
|
+
/// during
|
|
27
|
+
/// this ruleset.
|
|
28
|
+
/// @custom:member metadata Metadata of the metadata, up to uint8 in size.
|
|
29
|
+
struct JBPayDataHookRulesetMetadata {
|
|
30
|
+
uint16 reservedPercent;
|
|
31
|
+
uint16 cashOutTaxRate;
|
|
32
|
+
uint32 baseCurrency;
|
|
33
|
+
bool pausePay;
|
|
34
|
+
bool pauseCreditTransfers;
|
|
35
|
+
bool allowOwnerMinting;
|
|
36
|
+
bool allowTerminalMigration;
|
|
37
|
+
bool allowSetTerminals;
|
|
38
|
+
bool allowSetController;
|
|
39
|
+
bool allowAddAccountingContext;
|
|
40
|
+
bool allowAddPriceFeed;
|
|
41
|
+
bool ownerMustSendPayouts;
|
|
42
|
+
bool holdFees;
|
|
43
|
+
bool useTotalSurplusForCashOuts;
|
|
44
|
+
bool useDataHookForCashOut;
|
|
45
|
+
uint16 metadata;
|
|
46
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {JBPayDataHookRulesetConfig} from "./JBPayDataHookRulesetConfig.sol";
|
|
5
|
+
|
|
6
|
+
/// @custom:member projectId The ID of the project to queue rulesets for.
|
|
7
|
+
/// @custom:member rulesetConfigurations The ruleset configurations to queue.
|
|
8
|
+
/// @custom:member memo A memo to pass along to the emitted event.
|
|
9
|
+
struct JBQueueRulesetsConfig {
|
|
10
|
+
uint56 projectId;
|
|
11
|
+
JBPayDataHookRulesetConfig[] rulesetConfigurations;
|
|
12
|
+
string memo;
|
|
13
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
/// @custom:member price The price to buy an NFT in this tier, in terms of the currency in its `JBInitTiersConfig`.
|
|
5
|
+
/// @custom:member remainingSupply The remaining number of NFTs which can be minted from this tier.
|
|
6
|
+
/// @custom:member initialSupply The total number of NFTs which can be minted from this tier.
|
|
7
|
+
/// @custom:member votingUnits The number of votes that each NFT in this tier gets.
|
|
8
|
+
/// @custom:member category The category that NFTs in this tier belongs to. Used to group NFT tiers.
|
|
9
|
+
/// @custom:member discountPercent The discount that should be applied to the tier.
|
|
10
|
+
/// @custom:member reserveFrequency The frequency at which an extra NFT is minted for the `reserveBeneficiary` from this
|
|
11
|
+
/// tier. With a `reserveFrequency` of 5, an extra NFT will be minted for the `reserveBeneficiary` for every 5 NFTs
|
|
12
|
+
/// purchased.
|
|
13
|
+
/// @custom:member packedBools A packed uint8 containing boolean flags: bit 0 = allowOwnerMint, bit 1 =
|
|
14
|
+
/// transfersPausable, bit 2 = useVotingUnits, bit 3 = cannotBeRemoved, bit 4 = cannotIncreaseDiscountPercent.
|
|
15
|
+
struct JBStored721Tier {
|
|
16
|
+
uint104 price;
|
|
17
|
+
uint32 remainingSupply;
|
|
18
|
+
uint32 initialSupply;
|
|
19
|
+
uint32 votingUnits;
|
|
20
|
+
uint24 category;
|
|
21
|
+
uint8 discountPercent;
|
|
22
|
+
uint16 reserveFrequency;
|
|
23
|
+
uint8 packedBools;
|
|
24
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.23;
|
|
3
|
+
|
|
4
|
+
import "./utils/UnitTestSetup.sol";
|
|
5
|
+
|
|
6
|
+
/// @title 721HookAttacks
|
|
7
|
+
/// @notice Adversarial security tests for JB721TiersHook and JB721TiersHookStore.
|
|
8
|
+
contract NFTHookAttacks is UnitTestSetup {
|
|
9
|
+
using stdStorage for StdStorage;
|
|
10
|
+
|
|
11
|
+
// =========================================================================
|
|
12
|
+
// Helpers
|
|
13
|
+
// =========================================================================
|
|
14
|
+
|
|
15
|
+
/// @dev Mock the directory to accept `mockTerminalAddress` as a terminal for `projectId`.
|
|
16
|
+
function _mockTerminalAuth() internal {
|
|
17
|
+
mockAndExpect(
|
|
18
|
+
mockJBDirectory,
|
|
19
|
+
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
20
|
+
abi.encode(true)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// @dev Create a pay context that requests minting specific tier IDs.
|
|
25
|
+
function _buildPayContext(
|
|
26
|
+
address targetHook,
|
|
27
|
+
uint256 value,
|
|
28
|
+
uint16[] memory tierIds
|
|
29
|
+
)
|
|
30
|
+
internal
|
|
31
|
+
view
|
|
32
|
+
returns (JBAfterPayRecordedContext memory)
|
|
33
|
+
{
|
|
34
|
+
bytes[] memory data = new bytes[](1);
|
|
35
|
+
data[0] = abi.encode(false, tierIds);
|
|
36
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
37
|
+
ids[0] = metadataHelper.getId("pay", targetHook);
|
|
38
|
+
bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
|
|
39
|
+
|
|
40
|
+
return JBAfterPayRecordedContext({
|
|
41
|
+
payer: beneficiary,
|
|
42
|
+
projectId: projectId,
|
|
43
|
+
rulesetId: 0,
|
|
44
|
+
amount: JBTokenAmount({
|
|
45
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
46
|
+
value: value,
|
|
47
|
+
decimals: 18,
|
|
48
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
49
|
+
}),
|
|
50
|
+
forwardedAmount: JBTokenAmount({
|
|
51
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
52
|
+
value: 0,
|
|
53
|
+
decimals: 18,
|
|
54
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
55
|
+
}),
|
|
56
|
+
weight: 10 ** 18,
|
|
57
|
+
newlyIssuedTokenCount: 0,
|
|
58
|
+
beneficiary: beneficiary,
|
|
59
|
+
hookMetadata: bytes(""),
|
|
60
|
+
payerMetadata: hookMetadata
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// =========================================================================
|
|
65
|
+
// Test 1: Zero-price tier — can an attacker mint for free?
|
|
66
|
+
// =========================================================================
|
|
67
|
+
/// @notice Add a tier with price=0 via adjustTiers. Verify the hook handles it correctly.
|
|
68
|
+
function test_zeroPriceTier_mintBehavior() public {
|
|
69
|
+
// Create hook with 1 default tier (price=10).
|
|
70
|
+
ForTest_JB721TiersHook targetHook = _initializeForTestHook(1);
|
|
71
|
+
|
|
72
|
+
// Add a zero-price tier via adjustTiers (tier ID 2).
|
|
73
|
+
JB721TierConfig[] memory newTiers = new JB721TierConfig[](1);
|
|
74
|
+
newTiers[0] = JB721TierConfig({
|
|
75
|
+
price: 0,
|
|
76
|
+
initialSupply: 100,
|
|
77
|
+
votingUnits: 0,
|
|
78
|
+
reserveFrequency: 0,
|
|
79
|
+
reserveBeneficiary: reserveBeneficiary,
|
|
80
|
+
encodedIPFSUri: tokenUris[0],
|
|
81
|
+
category: 2,
|
|
82
|
+
discountPercent: 0,
|
|
83
|
+
allowOwnerMint: false,
|
|
84
|
+
useReserveBeneficiaryAsDefault: false,
|
|
85
|
+
transfersPausable: false,
|
|
86
|
+
cannotBeRemoved: false,
|
|
87
|
+
cannotIncreaseDiscountPercent: false,
|
|
88
|
+
useVotingUnits: false
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
vm.prank(owner);
|
|
92
|
+
targetHook.adjustTiers(newTiers, new uint256[](0));
|
|
93
|
+
|
|
94
|
+
_mockTerminalAuth();
|
|
95
|
+
|
|
96
|
+
// Try to mint tier 2 (price=0) with 0 value.
|
|
97
|
+
uint16[] memory tierIds = new uint16[](1);
|
|
98
|
+
tierIds[0] = 2;
|
|
99
|
+
|
|
100
|
+
JBAfterPayRecordedContext memory ctx = _buildPayContext(address(targetHook), 0, tierIds);
|
|
101
|
+
|
|
102
|
+
vm.prank(mockTerminalAddress);
|
|
103
|
+
targetHook.afterPayRecordedWith(ctx);
|
|
104
|
+
|
|
105
|
+
// Verify the NFT was minted to the beneficiary.
|
|
106
|
+
assertEq(targetHook.balanceOf(beneficiary), 1, "Should mint 1 NFT at price 0");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// =========================================================================
|
|
110
|
+
// Test 2: Discount percent at maximum — effective price becomes 0
|
|
111
|
+
// =========================================================================
|
|
112
|
+
/// @notice Set discount to 100%, verify the effective price for the tier.
|
|
113
|
+
function test_maxDiscountPercent_effectivePrice() public {
|
|
114
|
+
defaultTierConfig.discountPercent = 0;
|
|
115
|
+
defaultTierConfig.cannotIncreaseDiscountPercent = false;
|
|
116
|
+
|
|
117
|
+
JB721TiersHook targetHook = _initHookDefaultTiers(1);
|
|
118
|
+
|
|
119
|
+
// Owner sets discount to 100%.
|
|
120
|
+
vm.mockCall(mockJBPermissions, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true));
|
|
121
|
+
|
|
122
|
+
vm.prank(owner);
|
|
123
|
+
targetHook.setDiscountPercentOf(1, 100);
|
|
124
|
+
|
|
125
|
+
// Read the tier and verify the discount was applied.
|
|
126
|
+
JB721Tier memory tier = store.tierOf(address(targetHook), 1, false);
|
|
127
|
+
assertEq(tier.discountPercent, 100, "Discount should be 100%");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// =========================================================================
|
|
131
|
+
// Test 3: cannotIncreaseDiscountPercent flag enforcement
|
|
132
|
+
// =========================================================================
|
|
133
|
+
/// @notice Try to increase discount when the flag forbids it.
|
|
134
|
+
function test_cannotIncreaseDiscountPercent_enforcement() public {
|
|
135
|
+
defaultTierConfig.discountPercent = 10;
|
|
136
|
+
defaultTierConfig.cannotIncreaseDiscountPercent = true;
|
|
137
|
+
|
|
138
|
+
JB721TiersHook targetHook = _initHookDefaultTiers(1);
|
|
139
|
+
|
|
140
|
+
vm.mockCall(mockJBPermissions, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true));
|
|
141
|
+
|
|
142
|
+
// Try to increase discount from 10 to 50 — should revert.
|
|
143
|
+
vm.prank(owner);
|
|
144
|
+
vm.expectRevert();
|
|
145
|
+
targetHook.setDiscountPercentOf(1, 50);
|
|
146
|
+
|
|
147
|
+
// Decreasing should still work.
|
|
148
|
+
vm.prank(owner);
|
|
149
|
+
targetHook.setDiscountPercentOf(1, 5);
|
|
150
|
+
|
|
151
|
+
JB721Tier memory tier = store.tierOf(address(targetHook), 1, false);
|
|
152
|
+
assertEq(tier.discountPercent, 5, "Discount decrease should work");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// =========================================================================
|
|
156
|
+
// Test 4: Reserve minting drain — high reserve frequency
|
|
157
|
+
// =========================================================================
|
|
158
|
+
/// @notice With reserveFrequency=1 (reserve on every mint), mint 5 paid NFTs
|
|
159
|
+
/// then call mintPendingReservesFor to drain reserves.
|
|
160
|
+
function test_reserveDrain_highFrequency() public {
|
|
161
|
+
defaultTierConfig.initialSupply = 100;
|
|
162
|
+
defaultTierConfig.reserveFrequency = 1; // Reserve 1 per 1 paid mint.
|
|
163
|
+
|
|
164
|
+
ForTest_JB721TiersHook targetHook = _initializeForTestHook(1);
|
|
165
|
+
IJB721TiersHookStore hookStore = targetHook.STORE();
|
|
166
|
+
|
|
167
|
+
_mockTerminalAuth();
|
|
168
|
+
|
|
169
|
+
// Mint 5 paid NFTs from tier 1 (price=10 each, so value=50).
|
|
170
|
+
uint16[] memory tierIds = new uint16[](5);
|
|
171
|
+
for (uint256 i; i < 5; i++) {
|
|
172
|
+
tierIds[i] = 1;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
JBAfterPayRecordedContext memory ctx = _buildPayContext(address(targetHook), 50, tierIds);
|
|
176
|
+
|
|
177
|
+
vm.prank(mockTerminalAddress);
|
|
178
|
+
targetHook.afterPayRecordedWith(ctx);
|
|
179
|
+
|
|
180
|
+
assertEq(targetHook.balanceOf(beneficiary), 5, "5 paid NFTs minted");
|
|
181
|
+
|
|
182
|
+
// Pending reserves should be 5 (1 per paid mint with frequency=1).
|
|
183
|
+
// With frequency=1: reserveCount = nftsMinted / frequency = 5, plus 1 if remainder > 0.
|
|
184
|
+
// 5/1 = 5, remainder 0, so pending = 5+1 = 6? Actually the formula is:
|
|
185
|
+
// numberOfPendingReservesFor = (numberOfMints + frequency - 1) / frequency - processedReserves
|
|
186
|
+
// Let's just check what the store reports.
|
|
187
|
+
uint256 pending = hookStore.numberOfPendingReservesFor(address(targetHook), 1);
|
|
188
|
+
assertTrue(pending > 0, "Should have pending reserves");
|
|
189
|
+
|
|
190
|
+
// Mint all pending reserves.
|
|
191
|
+
vm.prank(owner);
|
|
192
|
+
targetHook.mintPendingReservesFor(1, pending);
|
|
193
|
+
|
|
194
|
+
// After minting, pending should be 0.
|
|
195
|
+
uint256 pendingAfter = hookStore.numberOfPendingReservesFor(address(targetHook), 1);
|
|
196
|
+
assertEq(pendingAfter, 0, "No pending reserves after minting");
|
|
197
|
+
|
|
198
|
+
// Try to mint more reserves — should revert (nothing pending).
|
|
199
|
+
vm.prank(owner);
|
|
200
|
+
vm.expectRevert();
|
|
201
|
+
targetHook.mintPendingReservesFor(1, 1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// =========================================================================
|
|
205
|
+
// Test 5: Cash-out weight after tier removal
|
|
206
|
+
// =========================================================================
|
|
207
|
+
/// @notice Mint NFTs from a tier, then remove the tier. Verify that
|
|
208
|
+
/// totalCashOutWeight still accounts for the minted tokens.
|
|
209
|
+
function test_cashOutWeight_afterTierRemoval() public {
|
|
210
|
+
defaultTierConfig.initialSupply = 100;
|
|
211
|
+
defaultTierConfig.votingUnits = 10;
|
|
212
|
+
defaultTierConfig.useVotingUnits = true;
|
|
213
|
+
|
|
214
|
+
ForTest_JB721TiersHook targetHook = _initializeForTestHook(1);
|
|
215
|
+
IJB721TiersHookStore hookStore = targetHook.STORE();
|
|
216
|
+
|
|
217
|
+
_mockTerminalAuth();
|
|
218
|
+
|
|
219
|
+
// Mint 3 NFTs from tier 1 (price=10 each, value=30).
|
|
220
|
+
uint16[] memory tierIds = new uint16[](3);
|
|
221
|
+
tierIds[0] = 1;
|
|
222
|
+
tierIds[1] = 1;
|
|
223
|
+
tierIds[2] = 1;
|
|
224
|
+
|
|
225
|
+
JBAfterPayRecordedContext memory ctx = _buildPayContext(address(targetHook), 30, tierIds);
|
|
226
|
+
|
|
227
|
+
vm.prank(mockTerminalAddress);
|
|
228
|
+
targetHook.afterPayRecordedWith(ctx);
|
|
229
|
+
|
|
230
|
+
assertEq(targetHook.balanceOf(beneficiary), 3, "3 NFTs minted");
|
|
231
|
+
|
|
232
|
+
// Get total cash-out weight before removal (from the store).
|
|
233
|
+
uint256 weightBefore = hookStore.totalCashOutWeight(address(targetHook));
|
|
234
|
+
|
|
235
|
+
// Remove tier 1.
|
|
236
|
+
vm.mockCall(mockJBPermissions, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true));
|
|
237
|
+
|
|
238
|
+
uint256[] memory tierIdsToRemove = new uint256[](1);
|
|
239
|
+
tierIdsToRemove[0] = 1;
|
|
240
|
+
|
|
241
|
+
vm.prank(owner);
|
|
242
|
+
targetHook.adjustTiers(new JB721TierConfig[](0), tierIdsToRemove);
|
|
243
|
+
|
|
244
|
+
// Verify the tier is removed.
|
|
245
|
+
assertTrue(hookStore.isTierRemoved(address(targetHook), 1), "Tier should be removed");
|
|
246
|
+
|
|
247
|
+
// Total cash-out weight should still include the minted tokens.
|
|
248
|
+
uint256 weightAfter = hookStore.totalCashOutWeight(address(targetHook));
|
|
249
|
+
assertEq(weightAfter, weightBefore, "Cash-out weight should be preserved after removal");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// =========================================================================
|
|
253
|
+
// Test 6: Invalid tier ID in pay metadata — reverts when overspending prevented
|
|
254
|
+
// =========================================================================
|
|
255
|
+
/// @notice Pass a tier ID that doesn't exist. With preventOverspending=true, must revert.
|
|
256
|
+
function test_invalidTierIdInMetadata_reverts() public {
|
|
257
|
+
// Use preventOverspending=true so invalid tiers cause a revert.
|
|
258
|
+
JB721TiersHook targetHook = _initHookDefaultTiers(1, true);
|
|
259
|
+
|
|
260
|
+
_mockTerminalAuth();
|
|
261
|
+
|
|
262
|
+
// Try to mint tier 999 which doesn't exist.
|
|
263
|
+
uint16[] memory tierIds = new uint16[](1);
|
|
264
|
+
tierIds[0] = 999;
|
|
265
|
+
|
|
266
|
+
JBAfterPayRecordedContext memory ctx = _buildPayContext(address(targetHook), 1 ether, tierIds);
|
|
267
|
+
|
|
268
|
+
vm.prank(mockTerminalAddress);
|
|
269
|
+
vm.expectRevert();
|
|
270
|
+
targetHook.afterPayRecordedWith(ctx);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// =========================================================================
|
|
274
|
+
// Test 7: Duplicate tier IDs in pay metadata — mints multiple NFTs
|
|
275
|
+
// =========================================================================
|
|
276
|
+
/// @notice Pass the same tier ID multiple times. Should mint multiple NFTs
|
|
277
|
+
/// from that tier.
|
|
278
|
+
function test_duplicateTierIdsInMetadata_mintsMultiple() public {
|
|
279
|
+
defaultTierConfig.initialSupply = 100;
|
|
280
|
+
|
|
281
|
+
ForTest_JB721TiersHook targetHook = _initializeForTestHook(1);
|
|
282
|
+
IJB721TiersHookStore hookStore = targetHook.STORE();
|
|
283
|
+
|
|
284
|
+
_mockTerminalAuth();
|
|
285
|
+
|
|
286
|
+
// Mint 3 of the same tier (price=10 each, value=30).
|
|
287
|
+
uint16[] memory tierIds = new uint16[](3);
|
|
288
|
+
tierIds[0] = 1;
|
|
289
|
+
tierIds[1] = 1;
|
|
290
|
+
tierIds[2] = 1;
|
|
291
|
+
|
|
292
|
+
JBAfterPayRecordedContext memory ctx = _buildPayContext(address(targetHook), 30, tierIds);
|
|
293
|
+
|
|
294
|
+
vm.prank(mockTerminalAddress);
|
|
295
|
+
targetHook.afterPayRecordedWith(ctx);
|
|
296
|
+
|
|
297
|
+
assertEq(targetHook.balanceOf(beneficiary), 3, "3 NFTs minted from same tier");
|
|
298
|
+
|
|
299
|
+
// Verify remaining supply decreased.
|
|
300
|
+
JB721Tier memory tier = hookStore.tierOf(address(targetHook), 1, false);
|
|
301
|
+
assertEq(tier.remainingSupply, 97, "Supply should decrease by 3");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// =========================================================================
|
|
305
|
+
// Test 8: Supply exhaustion — no additional NFTs minted after supply drained
|
|
306
|
+
// =========================================================================
|
|
307
|
+
/// @notice Mint the entire supply of a tier, then verify no more can be minted.
|
|
308
|
+
function test_supplyExhaustion_noOvermint() public {
|
|
309
|
+
defaultTierConfig.initialSupply = 3; // Only 3 available.
|
|
310
|
+
defaultTierConfig.reserveFrequency = 0; // No reserves to keep it simple.
|
|
311
|
+
|
|
312
|
+
ForTest_JB721TiersHook targetHook = _initializeForTestHook(1);
|
|
313
|
+
IJB721TiersHookStore hookStore = targetHook.STORE();
|
|
314
|
+
|
|
315
|
+
_mockTerminalAuth();
|
|
316
|
+
|
|
317
|
+
// Mint all 3 (price=10 each, value=30).
|
|
318
|
+
uint16[] memory tierIds = new uint16[](3);
|
|
319
|
+
tierIds[0] = 1;
|
|
320
|
+
tierIds[1] = 1;
|
|
321
|
+
tierIds[2] = 1;
|
|
322
|
+
|
|
323
|
+
JBAfterPayRecordedContext memory ctx = _buildPayContext(address(targetHook), 30, tierIds);
|
|
324
|
+
|
|
325
|
+
vm.prank(mockTerminalAddress);
|
|
326
|
+
targetHook.afterPayRecordedWith(ctx);
|
|
327
|
+
|
|
328
|
+
assertEq(targetHook.balanceOf(beneficiary), 3, "All 3 minted");
|
|
329
|
+
|
|
330
|
+
// Verify supply is exhausted.
|
|
331
|
+
JB721Tier memory tier = hookStore.tierOf(address(targetHook), 1, false);
|
|
332
|
+
assertEq(tier.remainingSupply, 0, "No remaining supply");
|
|
333
|
+
|
|
334
|
+
// Try to mint one more — store enforces supply limit and reverts.
|
|
335
|
+
uint16[] memory oneMore = new uint16[](1);
|
|
336
|
+
oneMore[0] = 1;
|
|
337
|
+
|
|
338
|
+
JBAfterPayRecordedContext memory ctx2 = _buildPayContext(address(targetHook), 10, oneMore);
|
|
339
|
+
|
|
340
|
+
vm.prank(mockTerminalAddress);
|
|
341
|
+
vm.expectRevert();
|
|
342
|
+
targetHook.afterPayRecordedWith(ctx2);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// =========================================================================
|
|
346
|
+
// Test 9: adjustTiers without permission — must revert
|
|
347
|
+
// =========================================================================
|
|
348
|
+
/// @notice Non-owner without ADJUST_721_TIERS permission tries to add/remove tiers.
|
|
349
|
+
function test_adjustTiers_noPermission_reverts() public {
|
|
350
|
+
JB721TiersHook targetHook = _initHookDefaultTiers(1);
|
|
351
|
+
|
|
352
|
+
// Mock permissions to return false.
|
|
353
|
+
vm.mockCall(mockJBPermissions, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(false));
|
|
354
|
+
|
|
355
|
+
address attacker = makeAddr("attacker");
|
|
356
|
+
|
|
357
|
+
// Try to add a new tier.
|
|
358
|
+
JB721TierConfig[] memory newTiers = new JB721TierConfig[](1);
|
|
359
|
+
newTiers[0] = JB721TierConfig({
|
|
360
|
+
price: 1,
|
|
361
|
+
initialSupply: type(uint32).max,
|
|
362
|
+
votingUnits: 0,
|
|
363
|
+
reserveFrequency: 0,
|
|
364
|
+
reserveBeneficiary: attacker,
|
|
365
|
+
encodedIPFSUri: tokenUris[0],
|
|
366
|
+
category: 1,
|
|
367
|
+
discountPercent: 0,
|
|
368
|
+
allowOwnerMint: true,
|
|
369
|
+
useReserveBeneficiaryAsDefault: false,
|
|
370
|
+
transfersPausable: false,
|
|
371
|
+
cannotBeRemoved: false,
|
|
372
|
+
cannotIncreaseDiscountPercent: false,
|
|
373
|
+
useVotingUnits: false
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
vm.prank(attacker);
|
|
377
|
+
vm.expectRevert();
|
|
378
|
+
targetHook.adjustTiers(newTiers, new uint256[](0));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// =========================================================================
|
|
382
|
+
// Test 10: Tier with max supply — verify no overflow
|
|
383
|
+
// =========================================================================
|
|
384
|
+
/// @notice Add a tier with initialSupply = 999_999_999 (store maximum).
|
|
385
|
+
/// Verify it's created correctly and doesn't overflow.
|
|
386
|
+
function test_maxSupplyTier_noOverflow() public {
|
|
387
|
+
defaultTierConfig.initialSupply = 999_999_999; // Store maximum
|
|
388
|
+
|
|
389
|
+
JB721TiersHook targetHook = _initHookDefaultTiers(1);
|
|
390
|
+
|
|
391
|
+
// Read the tier to verify the supply was stored correctly.
|
|
392
|
+
JB721Tier memory tier = store.tierOf(address(targetHook), 1, false);
|
|
393
|
+
assertEq(tier.initialSupply, 999_999_999, "Initial supply should be 999_999_999");
|
|
394
|
+
assertEq(tier.remainingSupply, 999_999_999, "Remaining supply should be 999_999_999");
|
|
395
|
+
}
|
|
396
|
+
}
|