@bananapus/721-hook-v6 0.0.41 → 0.0.43

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.
Files changed (77) hide show
  1. package/foundry.lock +1 -7
  2. package/foundry.toml +1 -1
  3. package/package.json +20 -9
  4. package/script/Deploy.s.sol +2 -2
  5. package/src/JB721Checkpoints.sol +60 -18
  6. package/src/JB721CheckpointsDeployer.sol +10 -5
  7. package/src/JB721TiersHook.sol +4 -1
  8. package/src/JB721TiersHookProjectDeployer.sol +68 -30
  9. package/src/JB721TiersHookStore.sol +1 -4
  10. package/src/interfaces/IJB721Checkpoints.sol +21 -14
  11. package/src/interfaces/IJB721CheckpointsDeployer.sol +6 -2
  12. package/src/interfaces/IJB721TiersHookProjectDeployer.sol +2 -0
  13. package/test/utils/AccessJBLib.sol +49 -0
  14. package/test/utils/ForTest_JB721TiersHook.sol +246 -0
  15. package/test/utils/TestBaseWorkflow.sol +213 -0
  16. package/test/utils/UnitTestSetup.sol +805 -0
  17. package/.gas-snapshot +0 -152
  18. package/ADMINISTRATION.md +0 -87
  19. package/ARCHITECTURE.md +0 -98
  20. package/AUDIT_INSTRUCTIONS.md +0 -77
  21. package/RISKS.md +0 -118
  22. package/SKILLS.md +0 -43
  23. package/STYLE_GUIDE.md +0 -610
  24. package/USER_JOURNEYS.md +0 -121
  25. package/assets/findings/nana-721-hook-v6-pashov-ai-audit-report-20260330-091257.md +0 -83
  26. package/slither-ci.config.json +0 -10
  27. package/test/721HookAttacks.t.sol +0 -408
  28. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +0 -985
  29. package/test/Fork.t.sol +0 -2346
  30. package/test/TestAuditGaps.sol +0 -1075
  31. package/test/TestCheckpoints.t.sol +0 -341
  32. package/test/TestSafeTransferReentrancy.t.sol +0 -305
  33. package/test/TestVotingUnitsLifecycle.t.sol +0 -313
  34. package/test/audit/AuditRegressions.t.sol +0 -83
  35. package/test/audit/CrossCurrencySplitNoPrices.t.sol +0 -123
  36. package/test/audit/FreshAudit.t.sol +0 -197
  37. package/test/audit/FutureTierPoC.t.sol +0 -39
  38. package/test/audit/FutureTierRemoval.t.sol +0 -47
  39. package/test/audit/Pass12L18.t.sol +0 -80
  40. package/test/audit/PayCreditsBypassTierSplits.t.sol +0 -200
  41. package/test/audit/ProjectDeployerAuth.t.sol +0 -266
  42. package/test/audit/RepoFindings.t.sol +0 -195
  43. package/test/audit/ReserveActivation.t.sol +0 -87
  44. package/test/audit/RetroactiveReserveBeneficiaryDilution.t.sol +0 -149
  45. package/test/audit/SameCurrencyDecimalMismatch.t.sol +0 -249
  46. package/test/audit/SplitCreditsMismatch.t.sol +0 -219
  47. package/test/audit/SplitFailureRedistribution.t.sol +0 -143
  48. package/test/audit/USDTVoidReturnCompat.t.sol +0 -301
  49. package/test/fork/ERC20CashOutFork.t.sol +0 -633
  50. package/test/fork/ERC20TierSplitFork.t.sol +0 -596
  51. package/test/fork/IssueTokensForSplitsFork.t.sol +0 -516
  52. package/test/invariants/TierLifecycleInvariant.t.sol +0 -188
  53. package/test/invariants/TieredHookStoreInvariant.t.sol +0 -86
  54. package/test/invariants/handlers/TierLifecycleHandler.sol +0 -300
  55. package/test/invariants/handlers/TierStoreHandler.sol +0 -165
  56. package/test/regression/BrokenTerminalDoesNotDos.t.sol +0 -277
  57. package/test/regression/CacheTierLookup.t.sol +0 -190
  58. package/test/regression/ProjectDeployerRulesets.t.sol +0 -358
  59. package/test/regression/ReserveBeneficiaryOverwrite.t.sol +0 -155
  60. package/test/regression/SplitDistributionBugs.t.sol +0 -751
  61. package/test/regression/SplitNoBeneficiary.t.sol +0 -140
  62. package/test/unit/AuditFixes_Unit.t.sol +0 -624
  63. package/test/unit/JB721CheckpointsDeployer_AccessControl.t.sol +0 -116
  64. package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +0 -144
  65. package/test/unit/JBBitmap.t.sol +0 -170
  66. package/test/unit/JBIpfsDecoder.t.sol +0 -136
  67. package/test/unit/TierSupplyReserveCheck.t.sol +0 -221
  68. package/test/unit/adjustTier_Unit.t.sol +0 -1942
  69. package/test/unit/deployer_Unit.t.sol +0 -114
  70. package/test/unit/getters_constructor_Unit.t.sol +0 -593
  71. package/test/unit/mintFor_mintReservesFor_Unit.t.sol +0 -452
  72. package/test/unit/pay_CrossCurrency_Unit.t.sol +0 -530
  73. package/test/unit/pay_Unit.t.sol +0 -1661
  74. package/test/unit/redeem_Unit.t.sol +0 -473
  75. package/test/unit/relayBeneficiary_Unit.t.sol +0 -182
  76. package/test/unit/splitHookDistribution_Unit.t.sol +0 -604
  77. package/test/unit/tierSplitRouting_Unit.t.sol +0 -757
@@ -1,188 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- // forge-lint: disable-next-line(unaliased-plain-import)
5
- import "forge-std/StdInvariant.sol";
6
- // forge-lint: disable-next-line(unaliased-plain-import)
7
- import "../utils/UnitTestSetup.sol";
8
- // forge-lint: disable-next-line(unaliased-plain-import)
9
- import "./handlers/TierLifecycleHandler.sol";
10
-
11
- /// @title TierLifecycleInvariant
12
- /// @notice State machine fuzzing for 721 tier lifecycle.
13
- /// 6 invariants covering supply, cash out weight, credits, reserves, removal, and discounts.
14
- contract TierLifecycleInvariant_Local is StdInvariant, UnitTestSetup {
15
- TierLifecycleHandler public handler;
16
-
17
- function setUp() public override {
18
- super.setUp();
19
-
20
- handler = new TierLifecycleHandler(hook, store, owner, mockJBController);
21
-
22
- bytes4[] memory selectors = new bytes4[](10);
23
- selectors[0] = TierLifecycleHandler.payAndMintNFT.selector;
24
- selectors[1] = TierLifecycleHandler.cashOutNFT.selector;
25
- selectors[2] = TierLifecycleHandler.addTier.selector;
26
- selectors[3] = TierLifecycleHandler.removeTier.selector;
27
- selectors[4] = TierLifecycleHandler.mintReserves.selector;
28
- selectors[5] = TierLifecycleHandler.setDiscount.selector;
29
- selectors[6] = TierLifecycleHandler.ownerMint.selector;
30
- selectors[7] = TierLifecycleHandler.advanceTime.selector;
31
- // Double-weight common operations
32
- selectors[8] = TierLifecycleHandler.payAndMintNFT.selector;
33
- selectors[9] = TierLifecycleHandler.payAndMintNFT.selector;
34
-
35
- targetSelector(FuzzSelector({addr: address(handler), selectors: selectors}));
36
- targetContract(address(handler));
37
- }
38
-
39
- // =========================================================================
40
- // INV-721-1: Per-tier supply accounting
41
- // =========================================================================
42
- /// @notice For each active tier: remaining + minted == initialSupply.
43
- /// minted = initialSupply - remaining (from store), should match ghost tracking.
44
- function invariant_721_1_perTierSupplyAccounting() public {
45
- uint256 maxTierId = store.maxTierIdOf(address(hook));
46
-
47
- for (uint256 tierId = 1; tierId <= maxTierId; tierId++) {
48
- // Get tier info
49
- uint256[] memory categories = new uint256[](0);
50
- JB721Tier[] memory allTiers = store.tiersOf(address(hook), categories, false, 0, 100);
51
-
52
- for (uint256 i = 0; i < allTiers.length; i++) {
53
- if (allTiers[i].id == tierId) {
54
- uint256 initial = allTiers[i].initialSupply;
55
- uint256 remaining = allTiers[i].remainingSupply;
56
- uint256 burned = store.numberOfBurnedFor(address(hook), tierId);
57
-
58
- // remaining + (minted including burned) should relate to initial
59
- // minted = initial - remaining (total ever minted)
60
- uint256 totalMinted = initial - remaining;
61
- // totalMinted >= burned (can't burn more than minted)
62
- assertGe(totalMinted, burned, "INV-721-1: Cannot burn more NFTs than were minted from tier");
63
-
64
- // Outstanding = totalMinted - burned
65
- uint256 outstanding = totalMinted - burned;
66
-
67
- // Verify: remaining + outstanding + burned == initial
68
- assertEq(remaining + outstanding + burned, initial, "INV-721-1: Supply accounting mismatch");
69
- break;
70
- }
71
- }
72
- }
73
- }
74
-
75
- // =========================================================================
76
- // INV-721-2: Total cash out weight consistency
77
- // =========================================================================
78
- /// @notice totalCashOutWeight should equal sum(tier.price * outstanding) for all tiers.
79
- function invariant_721_2_totalCashOutWeightConsistency() public {
80
- uint256 totalWeight = store.totalCashOutWeight(address(hook));
81
-
82
- uint256 computedWeight = 0;
83
-
84
- uint256[] memory categories = new uint256[](0);
85
- JB721Tier[] memory allTiers = store.tiersOf(address(hook), categories, false, 0, 100);
86
-
87
- for (uint256 i = 0; i < allTiers.length; i++) {
88
- uint256 tierId = allTiers[i].id;
89
- uint256 initial = allTiers[i].initialSupply;
90
- uint256 remaining = allTiers[i].remainingSupply;
91
- uint256 burned = store.numberOfBurnedFor(address(hook), tierId);
92
- uint256 price = allTiers[i].price;
93
-
94
- // Outstanding = minted - burned
95
- uint256 minted = initial - remaining;
96
- uint256 outstanding = minted > burned ? minted - burned : 0;
97
-
98
- // Pending reserves are also counted in totalCashOutWeight
99
- // (included in the store's calculation)
100
- computedWeight += price * outstanding;
101
- }
102
-
103
- // totalWeight >= computedWeight (it also includes pending reserves)
104
- assertGe(totalWeight, computedWeight, "INV-721-2: totalCashOutWeight must >= sum(price * outstanding)");
105
- }
106
-
107
- // =========================================================================
108
- // INV-721-3: Pay credits non-negative
109
- // =========================================================================
110
- /// @notice Pay credits for each actor should be >= 0 (trivially true for uint,
111
- /// but verifies no underflow/corruption).
112
- function invariant_721_3_payCreditsNonNegative() public {
113
- for (uint256 i = 0; i < handler.NUM_ACTORS(); i++) {
114
- address actor = handler.getActor(i);
115
- uint256 credits = hook.payCreditsOf(actor);
116
- // uint256 is always >= 0, but this validates the slot isn't corrupted
117
- assertGe(credits, 0, "INV-721-3: Pay credits should be non-negative");
118
- }
119
- }
120
-
121
- // =========================================================================
122
- // INV-721-4: Reserve mints bounded by frequency
123
- // =========================================================================
124
- /// @notice For each tier with reserve frequency > 0:
125
- /// reservesMinted <= ceil(totalMinted / reserveFrequency).
126
- function invariant_721_4_reserveMintsBounded() public {
127
- uint256[] memory categories = new uint256[](0);
128
- JB721Tier[] memory allTiers = store.tiersOf(address(hook), categories, false, 0, 100);
129
-
130
- for (uint256 i = 0; i < allTiers.length; i++) {
131
- uint256 tierId = allTiers[i].id;
132
- uint16 reserveFreq = allTiers[i].reserveFrequency;
133
-
134
- if (reserveFreq == 0) continue;
135
-
136
- uint256 reservesMinted = store.numberOfReservesMintedFor(address(hook), tierId);
137
- uint256 initial = allTiers[i].initialSupply;
138
- uint256 remaining = allTiers[i].remainingSupply;
139
- uint256 totalMinted = initial - remaining;
140
-
141
- // Non-reserve mints = totalMinted - reservesMinted
142
- uint256 nonReserveMints = totalMinted > reservesMinted ? totalMinted - reservesMinted : 0;
143
-
144
- // Max allowed reserves = ceil(nonReserveMints / reserveFrequency)
145
- uint256 maxReserves = 0;
146
- if (nonReserveMints > 0) {
147
- maxReserves = (nonReserveMints + reserveFreq - 1) / reserveFreq;
148
- }
149
-
150
- assertLe(reservesMinted, maxReserves, "INV-721-4: Reserve mints exceed allowed maximum");
151
- }
152
- }
153
-
154
- // =========================================================================
155
- // INV-721-5: Removed tiers tracked correctly
156
- // =========================================================================
157
- /// @notice Removed tiers should not appear in tiersOf() listing.
158
- function invariant_721_5_removedTiersExcluded() public {
159
- uint256[] memory categories = new uint256[](0);
160
- JB721Tier[] memory activeTiers = store.tiersOf(address(hook), categories, false, 0, 200);
161
-
162
- for (uint256 i = 0; i < activeTiers.length; i++) {
163
- // If handler tracked this tier as removed, it should NOT appear in active list
164
- assertFalse(
165
- handler.ghost_tierRemoved(activeTiers[i].id),
166
- "INV-721-5: Removed tier should not appear in active tiers list"
167
- );
168
- }
169
- }
170
-
171
- // =========================================================================
172
- // INV-721-6: Cash out weight bounded after discount
173
- // =========================================================================
174
- /// @notice After setDiscount, the per-token cash out weight should be <= original price.
175
- function invariant_721_6_cashOutWeightBoundedByPrice() public {
176
- uint256[] memory categories = new uint256[](0);
177
- JB721Tier[] memory allTiers = store.tiersOf(address(hook), categories, false, 0, 100);
178
-
179
- for (uint256 i = 0; i < allTiers.length; i++) {
180
- uint256 price = allTiers[i].price;
181
-
182
- // Cash out weight per token for this tier is just `price` (from the store)
183
- // The discount only affects the mint price, not the cash out weight
184
- // But verify the store returns a non-corrupt price
185
- assertGt(price, 0, "INV-721-6: Tier price should be > 0 for active tiers");
186
- }
187
- }
188
- }
@@ -1,86 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- // forge-lint: disable-next-line(unaliased-plain-import)
5
- import "forge-std/Test.sol";
6
-
7
- import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
8
- // forge-lint: disable-next-line(unused-import)
9
- import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
10
- // forge-lint: disable-next-line(unused-import)
11
- import {JBStored721Tier} from "../../src/structs/JBStored721Tier.sol";
12
- // forge-lint: disable-next-line(unused-import)
13
- import {JB721TiersHookFlags} from "../../src/structs/JB721TiersHookFlags.sol";
14
- // forge-lint: disable-next-line(unused-import)
15
- import {JBBitmapWord} from "../../src/structs/JBBitmapWord.sol";
16
- import {JB721Tier} from "../../src/structs/JB721Tier.sol";
17
- import {TierStoreHandler} from "./handlers/TierStoreHandler.sol";
18
-
19
- /// @notice Invariant tests for `JB721TiersHookStore` tier supply tracking.
20
- contract TestTieredHookStoreInvariant is Test {
21
- JB721TiersHookStore store;
22
- TierStoreHandler handler;
23
-
24
- function setUp() public {
25
- store = new JB721TiersHookStore();
26
- handler = new TierStoreHandler(store);
27
- targetContract(address(handler));
28
-
29
- // Exclude internal wrapper functions from fuzzing — they bypass handler safety checks.
30
- bytes4[] memory selectors = new bytes4[](4);
31
- selectors[0] = TierStoreHandler._doAddTiers.selector;
32
- selectors[1] = TierStoreHandler._doRemoveTiers.selector;
33
- selectors[2] = TierStoreHandler._doMint.selector;
34
- selectors[3] = TierStoreHandler._doBurn.selector;
35
-
36
- targetSelector(FuzzSelector({addr: address(handler), selectors: _handlerSelectors()}));
37
- }
38
-
39
- function _handlerSelectors() internal pure returns (bytes4[] memory selectors) {
40
- selectors = new bytes4[](4);
41
- selectors[0] = TierStoreHandler.addTier.selector;
42
- selectors[1] = TierStoreHandler.removeTier.selector;
43
- selectors[2] = TierStoreHandler.mint.selector;
44
- selectors[3] = TierStoreHandler.burn.selector;
45
- }
46
-
47
- /// @notice INV-721-1: For any tier, remaining + burned <= initial supply.
48
- function invariant_supplyConservation() public {
49
- address hook = handler.HOOK();
50
- uint256 maxTier = store.maxTierIdOf(hook);
51
-
52
- for (uint256 i = 1; i <= maxTier; i++) {
53
- JB721Tier memory tier = store.tierOf(hook, i, false);
54
- if (tier.initialSupply == 0) continue; // Tier doesn't exist.
55
-
56
- uint256 burned = store.numberOfBurnedFor(hook, i);
57
- // remaining + burned <= initial (remaining = initial - minted, but minted >= burned)
58
- assertTrue(
59
- tier.remainingSupply + burned <= tier.initialSupply, "remaining + burned must not exceed initial supply"
60
- );
61
- }
62
- }
63
-
64
- /// @notice INV-721-2: Reserve mints never exceed the proportional reserve allocation.
65
- function invariant_reserveMintBounds() public {
66
- address hook = handler.HOOK();
67
- uint256 maxTier = store.maxTierIdOf(hook);
68
-
69
- for (uint256 i = 1; i <= maxTier; i++) {
70
- JB721Tier memory tier = store.tierOf(hook, i, false);
71
- if (tier.initialSupply == 0) continue;
72
-
73
- uint256 reservesMinted = store.numberOfReservesMintedFor(hook, i);
74
- if (tier.reserveFrequency == 0) {
75
- assertEq(reservesMinted, 0, "no reserves without reserveFrequency");
76
- }
77
- }
78
- }
79
-
80
- /// @notice INV-721-3: maxTierIdOf is monotonically increasing (never decreases).
81
- function invariant_maxTierIdMonotonic() public {
82
- address hook = handler.HOOK();
83
- uint256 currentMax = store.maxTierIdOf(hook);
84
- assertTrue(currentMax >= handler.lowestMaxTierIdSeen(), "maxTierIdOf should never decrease");
85
- }
86
- }
@@ -1,300 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- // forge-lint: disable-next-line(unaliased-plain-import)
5
- import "forge-std/Test.sol";
6
- // forge-lint: disable-next-line(unaliased-plain-import)
7
- import "../../../src/JB721TiersHook.sol";
8
- // forge-lint: disable-next-line(unaliased-plain-import)
9
- import "../../../src/JB721TiersHookStore.sol";
10
- // forge-lint: disable-next-line(unaliased-plain-import)
11
- import "../../../src/structs/JB721TierConfig.sol";
12
- // forge-lint: disable-next-line(unaliased-plain-import)
13
- import "../../../src/structs/JB721Tier.sol";
14
- import {JB721TierConfigFlags} from "../../../src/structs/JB721TierConfigFlags.sol";
15
- // forge-lint: disable-next-line(unaliased-plain-import)
16
- import "../../../src/interfaces/IJB721TiersHookStore.sol";
17
- // forge-lint: disable-next-line(unaliased-plain-import)
18
- import "@bananapus/core-v6/src/libraries/JBConstants.sol";
19
- // forge-lint: disable-next-line(unaliased-plain-import)
20
- import "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
21
- // forge-lint: disable-next-line(unaliased-plain-import)
22
- import "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
23
-
24
- /// @title TierLifecycleHandler
25
- /// @notice Handler for 721 tier lifecycle invariant testing.
26
- /// 8 operations: payAndMintNFT, cashOutNFT, addTier, removeTier,
27
- /// mintReserves, setDiscount, ownerMint, advanceTime.
28
- contract TierLifecycleHandler is Test {
29
- JB721TiersHook public hook;
30
- JB721TiersHookStore public store;
31
- address public hookAddress;
32
- address public owner;
33
- address public mockController;
34
-
35
- uint256 public constant PROJECT_ID = 69;
36
- uint256 public constant NUM_ACTORS = 5;
37
- address[] public actors;
38
-
39
- // Ghost variables for supply tracking
40
- // forge-lint: disable-next-line(mixed-case-variable)
41
- mapping(uint256 => uint256) public ghost_mintedPerTier; // tierId => minted count
42
- // forge-lint: disable-next-line(mixed-case-variable)
43
- mapping(uint256 => uint256) public ghost_burnedPerTier; // tierId => burned count
44
- // forge-lint: disable-next-line(mixed-case-variable)
45
- mapping(uint256 => uint256) public ghost_reservesMintedPerTier; // tierId => reserves minted
46
- // forge-lint: disable-next-line(mixed-case-variable)
47
- uint256 public ghost_totalPayCredits;
48
- // forge-lint: disable-next-line(mixed-case-variable)
49
- mapping(address => uint256) public ghost_actorCredits; // actor => credit balance
50
- // forge-lint: disable-next-line(mixed-case-variable)
51
- uint256 public ghost_tiersAdded;
52
- // forge-lint: disable-next-line(mixed-case-variable)
53
- uint256 public ghost_tiersRemoved;
54
-
55
- // Track token IDs per actor for cash outs
56
- mapping(address => uint256[]) internal _actorTokenIds;
57
-
58
- // Track removed tier IDs
59
- // forge-lint: disable-next-line(mixed-case-variable)
60
- mapping(uint256 => bool) public ghost_tierRemoved;
61
-
62
- // Operation counters
63
- // forge-lint: disable-next-line(mixed-case-variable)
64
- uint256 public callCount_payAndMint;
65
- // forge-lint: disable-next-line(mixed-case-variable)
66
- uint256 public callCount_cashOut;
67
- // forge-lint: disable-next-line(mixed-case-variable)
68
- uint256 public callCount_addTier;
69
- // forge-lint: disable-next-line(mixed-case-variable)
70
- uint256 public callCount_removeTier;
71
- // forge-lint: disable-next-line(mixed-case-variable)
72
- uint256 public callCount_mintReserves;
73
- // forge-lint: disable-next-line(mixed-case-variable)
74
- uint256 public callCount_setDiscount;
75
- // forge-lint: disable-next-line(mixed-case-variable)
76
- uint256 public callCount_ownerMint;
77
- // forge-lint: disable-next-line(mixed-case-variable)
78
- uint256 public callCount_advanceTime;
79
-
80
- constructor(JB721TiersHook _hook, JB721TiersHookStore _store, address _owner, address _mockController) {
81
- hook = _hook;
82
- store = _store;
83
- hookAddress = address(_hook);
84
- owner = _owner;
85
- mockController = _mockController;
86
-
87
- for (uint256 i = 0; i < NUM_ACTORS; i++) {
88
- // forge-lint: disable-next-line(unsafe-typecast)
89
- address actor = address(uint160(0x6000 + i));
90
- actors.push(actor);
91
- }
92
- }
93
-
94
- function _getActor(uint256 seed) internal view returns (address) {
95
- return actors[seed % actors.length];
96
- }
97
-
98
- /// @notice Simulate a payment that mints NFTs by directly calling store.recordMint.
99
- // forge-lint: disable-next-line(mixed-case-function)
100
- function payAndMintNFT(uint256 seed) external {
101
- address actor = _getActor(seed);
102
-
103
- // Get max tier ID to pick a valid tier
104
- uint256 maxTierId = store.maxTierIdOf(hookAddress);
105
- if (maxTierId == 0) return;
106
-
107
- // Pick a tier (1-indexed)
108
- uint256 tierId = (seed % maxTierId) + 1;
109
-
110
- // Check if tier is removed
111
- if (ghost_tierRemoved[tierId]) return;
112
-
113
- // Get tier info
114
- uint256[] memory categories = new uint256[](0);
115
- JB721Tier[] memory tierInfo = store.tiersOf(hookAddress, categories, false, 0, 100);
116
-
117
- // Find the target tier
118
- uint104 tierPrice = 0;
119
- for (uint256 i = 0; i < tierInfo.length; i++) {
120
- if (tierInfo[i].id == tierId) {
121
- tierPrice = tierInfo[i].price;
122
- break;
123
- }
124
- }
125
- if (tierPrice == 0) return;
126
-
127
- // Call recordMint as the hook
128
- uint16[] memory tierIds = new uint16[](1);
129
- // forge-lint: disable-next-line(unsafe-typecast)
130
- tierIds[0] = uint16(tierId);
131
-
132
- vm.prank(hookAddress);
133
- try store.recordMint(tierPrice, tierIds, false) returns (uint256[] memory tokenIds, uint256, uint256) {
134
- // Track minted tokens
135
- for (uint256 i = 0; i < tokenIds.length; i++) {
136
- _actorTokenIds[actor].push(tokenIds[i]);
137
- }
138
- ghost_mintedPerTier[tierId]++;
139
- callCount_payAndMint++;
140
- } catch {}
141
- }
142
-
143
- /// @notice Simulate a cash out by burning an NFT.
144
- // forge-lint: disable-next-line(mixed-case-function)
145
- function cashOutNFT(uint256 seed) external {
146
- address actor = _getActor(seed);
147
-
148
- // Check if actor has any tokens
149
- if (_actorTokenIds[actor].length == 0) return;
150
-
151
- // Pick a token to burn
152
- uint256 tokenIndex = seed % _actorTokenIds[actor].length;
153
- uint256 tokenId = _actorTokenIds[actor][tokenIndex];
154
-
155
- uint256 tierId = store.tierIdOfToken(tokenId);
156
-
157
- // Call recordBurn as the hook
158
- uint256[] memory tokenIds = new uint256[](1);
159
- tokenIds[0] = tokenId;
160
-
161
- vm.prank(hookAddress);
162
- try store.recordBurn(tokenIds) {
163
- ghost_burnedPerTier[tierId]++;
164
-
165
- // Remove token from actor's list (swap and pop)
166
- uint256 lastIndex = _actorTokenIds[actor].length - 1;
167
- if (tokenIndex != lastIndex) {
168
- _actorTokenIds[actor][tokenIndex] = _actorTokenIds[actor][lastIndex];
169
- }
170
- _actorTokenIds[actor].pop();
171
-
172
- callCount_cashOut++;
173
- } catch {}
174
- }
175
-
176
- /// @notice Add a new tier.
177
- function addTier(uint256 seed) external {
178
- uint256 price = bound(seed, 1, 1000);
179
- uint256 supply = bound(seed >> 8, 10, 500);
180
-
181
- JB721TierConfig[] memory newTiers = new JB721TierConfig[](1);
182
- newTiers[0] = JB721TierConfig({
183
- // forge-lint: disable-next-line(unsafe-typecast)
184
- price: uint104(price),
185
- // forge-lint: disable-next-line(unsafe-typecast)
186
- initialSupply: uint32(supply),
187
- votingUnits: 0,
188
- reserveFrequency: 0,
189
- reserveBeneficiary: address(0),
190
- encodedIPFSUri: bytes32(0),
191
- category: uint24(100),
192
- discountPercent: 0,
193
- flags: JB721TierConfigFlags({
194
- allowOwnerMint: true,
195
- useReserveBeneficiaryAsDefault: false,
196
- transfersPausable: false,
197
- useVotingUnits: false,
198
- cantBeRemoved: false,
199
- cantIncreaseDiscountPercent: false,
200
- cantBuyWithCredits: false
201
- }),
202
- splitPercent: 0,
203
- splits: new JBSplit[](0)
204
- });
205
-
206
- vm.prank(hookAddress);
207
- try store.recordAddTiers(newTiers) returns (uint256[] memory tierIds) {
208
- ghost_tiersAdded += tierIds.length;
209
- callCount_addTier++;
210
- } catch {}
211
- }
212
-
213
- /// @notice Remove a tier.
214
- function removeTier(uint256 seed) external {
215
- uint256 maxTierId = store.maxTierIdOf(hookAddress);
216
- if (maxTierId == 0) return;
217
-
218
- uint256 tierId = (seed % maxTierId) + 1;
219
- if (ghost_tierRemoved[tierId]) return;
220
-
221
- uint256[] memory tierIds = new uint256[](1);
222
- tierIds[0] = tierId;
223
-
224
- vm.prank(hookAddress);
225
- try store.recordRemoveTierIds(tierIds) {
226
- ghost_tierRemoved[tierId] = true;
227
- ghost_tiersRemoved++;
228
- callCount_removeTier++;
229
- } catch {}
230
- }
231
-
232
- /// @notice Mint reserves for a tier.
233
- function mintReserves(uint256 seed) external {
234
- uint256 maxTierId = store.maxTierIdOf(hookAddress);
235
- if (maxTierId == 0) return;
236
-
237
- uint256 tierId = (seed % maxTierId) + 1;
238
- if (ghost_tierRemoved[tierId]) return;
239
-
240
- // Try to mint 1 reserve
241
- vm.prank(hookAddress);
242
- try store.recordMintReservesFor(tierId, 1) returns (uint256[] memory tokenIds) {
243
- ghost_reservesMintedPerTier[tierId] += tokenIds.length;
244
- callCount_mintReserves++;
245
- } catch {}
246
- }
247
-
248
- /// @notice Set discount for a tier.
249
- function setDiscount(uint256 seed) external {
250
- uint256 maxTierId = store.maxTierIdOf(hookAddress);
251
- if (maxTierId == 0) return;
252
-
253
- uint256 tierId = (seed % maxTierId) + 1;
254
- uint256 discount = bound(seed >> 8, 0, 100); // 0-100%
255
-
256
- vm.prank(hookAddress);
257
- try store.recordSetDiscountPercentOf(tierId, discount) {
258
- callCount_setDiscount++;
259
- } catch {}
260
- }
261
-
262
- /// @notice Owner mint (direct mint with isOwnerMint=true).
263
- function ownerMint(uint256 seed) external {
264
- uint256 maxTierId = store.maxTierIdOf(hookAddress);
265
- if (maxTierId == 0) return;
266
-
267
- uint256 tierId = (seed % maxTierId) + 1;
268
- if (ghost_tierRemoved[tierId]) return;
269
-
270
- uint16[] memory tierIds = new uint16[](1);
271
- // forge-lint: disable-next-line(unsafe-typecast)
272
- tierIds[0] = uint16(tierId);
273
-
274
- vm.prank(hookAddress);
275
- try store.recordMint(0, tierIds, true) returns (uint256[] memory tokenIds, uint256, uint256) {
276
- address actor = _getActor(seed);
277
- for (uint256 i = 0; i < tokenIds.length; i++) {
278
- _actorTokenIds[actor].push(tokenIds[i]);
279
- }
280
- ghost_mintedPerTier[tierId]++;
281
- callCount_ownerMint++;
282
- } catch {}
283
- }
284
-
285
- /// @notice Advance time.
286
- function advanceTime(uint256 seed) external {
287
- uint256 delta = bound(seed, 1 hours, 30 days);
288
- vm.warp(block.timestamp + delta);
289
- callCount_advanceTime++;
290
- }
291
-
292
- // View helpers
293
- function getActor(uint256 index) external view returns (address) {
294
- return actors[index];
295
- }
296
-
297
- function getActorTokenCount(address actor) external view returns (uint256) {
298
- return _actorTokenIds[actor].length;
299
- }
300
- }