@bananapus/721-hook-v6 0.0.42 → 0.0.45

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 (86) 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 +61 -19
  6. package/src/JB721CheckpointsDeployer.sol +10 -5
  7. package/src/JB721TiersHook.sol +66 -53
  8. package/src/JB721TiersHookDeployer.sol +8 -5
  9. package/src/JB721TiersHookProjectDeployer.sol +87 -46
  10. package/src/JB721TiersHookStore.sol +137 -107
  11. package/src/abstract/JB721Hook.sol +8 -6
  12. package/src/interfaces/IJB721Checkpoints.sol +21 -14
  13. package/src/interfaces/IJB721CheckpointsDeployer.sol +7 -3
  14. package/src/interfaces/IJB721TiersHook.sol +3 -3
  15. package/src/interfaces/IJB721TiersHookProjectDeployer.sol +4 -2
  16. package/src/interfaces/IJB721TiersHookStore.sol +11 -11
  17. package/src/libraries/JB721TiersHookLib.sol +1 -1
  18. package/src/structs/JB721TiersHookFlags.sol +1 -1
  19. package/src/structs/JBPayDataHookRulesetMetadata.sol +1 -1
  20. package/test/utils/AccessJBLib.sol +49 -0
  21. package/test/utils/ForTest_JB721TiersHook.sol +246 -0
  22. package/test/utils/TestBaseWorkflow.sol +213 -0
  23. package/test/utils/UnitTestSetup.sol +805 -0
  24. package/.gas-snapshot +0 -152
  25. package/ADMINISTRATION.md +0 -87
  26. package/ARCHITECTURE.md +0 -98
  27. package/AUDIT_INSTRUCTIONS.md +0 -77
  28. package/RISKS.md +0 -118
  29. package/SKILLS.md +0 -43
  30. package/STYLE_GUIDE.md +0 -610
  31. package/USER_JOURNEYS.md +0 -121
  32. package/assets/findings/nana-721-hook-v6-pashov-ai-audit-report-20260330-091257.md +0 -83
  33. package/slither-ci.config.json +0 -10
  34. package/test/721HookAttacks.t.sol +0 -408
  35. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +0 -985
  36. package/test/Fork.t.sol +0 -2346
  37. package/test/TestAuditGaps.sol +0 -1075
  38. package/test/TestCheckpoints.t.sol +0 -341
  39. package/test/TestSafeTransferReentrancy.t.sol +0 -305
  40. package/test/TestVotingUnitsLifecycle.t.sol +0 -313
  41. package/test/audit/AuditRegressions.t.sol +0 -83
  42. package/test/audit/CodexNemesisReserveSellout.t.sol +0 -66
  43. package/test/audit/CrossCurrencySplitNoPrices.t.sol +0 -123
  44. package/test/audit/FreshAudit.t.sol +0 -197
  45. package/test/audit/FutureTierPoC.t.sol +0 -39
  46. package/test/audit/FutureTierRemoval.t.sol +0 -47
  47. package/test/audit/Pass12L18.t.sol +0 -80
  48. package/test/audit/PayCreditsBypassTierSplits.t.sol +0 -200
  49. package/test/audit/ProjectDeployerAuth.t.sol +0 -266
  50. package/test/audit/RepoFindings.t.sol +0 -195
  51. package/test/audit/ReserveActivation.t.sol +0 -87
  52. package/test/audit/ReserveSlotProtection.t.sol +0 -273
  53. package/test/audit/RetroactiveReserveBeneficiaryDilution.t.sol +0 -149
  54. package/test/audit/SameCurrencyDecimalMismatch.t.sol +0 -249
  55. package/test/audit/SplitCreditsMismatch.t.sol +0 -219
  56. package/test/audit/SplitFailureRedistribution.t.sol +0 -143
  57. package/test/audit/USDTVoidReturnCompat.t.sol +0 -301
  58. package/test/fork/ERC20CashOutFork.t.sol +0 -633
  59. package/test/fork/ERC20TierSplitFork.t.sol +0 -596
  60. package/test/fork/IssueTokensForSplitsFork.t.sol +0 -516
  61. package/test/invariants/TierLifecycleInvariant.t.sol +0 -188
  62. package/test/invariants/TieredHookStoreInvariant.t.sol +0 -86
  63. package/test/invariants/handlers/TierLifecycleHandler.sol +0 -300
  64. package/test/invariants/handlers/TierStoreHandler.sol +0 -165
  65. package/test/regression/BrokenTerminalDoesNotDos.t.sol +0 -277
  66. package/test/regression/CacheTierLookup.t.sol +0 -190
  67. package/test/regression/ProjectDeployerRulesets.t.sol +0 -358
  68. package/test/regression/ReserveBeneficiaryOverwrite.t.sol +0 -155
  69. package/test/regression/SplitDistributionBugs.t.sol +0 -751
  70. package/test/regression/SplitNoBeneficiary.t.sol +0 -140
  71. package/test/unit/AuditFixes_Unit.t.sol +0 -624
  72. package/test/unit/JB721CheckpointsDeployer_AccessControl.t.sol +0 -116
  73. package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +0 -144
  74. package/test/unit/JBBitmap.t.sol +0 -170
  75. package/test/unit/JBIpfsDecoder.t.sol +0 -136
  76. package/test/unit/TierSupplyReserveCheck.t.sol +0 -221
  77. package/test/unit/adjustTier_Unit.t.sol +0 -1942
  78. package/test/unit/deployer_Unit.t.sol +0 -114
  79. package/test/unit/getters_constructor_Unit.t.sol +0 -593
  80. package/test/unit/mintFor_mintReservesFor_Unit.t.sol +0 -452
  81. package/test/unit/pay_CrossCurrency_Unit.t.sol +0 -530
  82. package/test/unit/pay_Unit.t.sol +0 -1661
  83. package/test/unit/redeem_Unit.t.sol +0 -473
  84. package/test/unit/relayBeneficiary_Unit.t.sol +0 -182
  85. package/test/unit/splitHookDistribution_Unit.t.sol +0 -604
  86. 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
- }