@bananapus/721-hook-v6 0.0.40 → 0.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/JB721TiersHook.sol +10 -10
- package/src/JB721TiersHookStore.sol +11 -1
- package/src/interfaces/IJB721TiersHook.sol +0 -1
- package/src/libraries/JB721TiersHookLib.sol +16 -1
- package/test/TestVotingUnitsLifecycle.t.sol +9 -9
- package/test/audit/CodexNemesisReserveSellout.t.sol +66 -0
- package/test/audit/{CodexNemesisFreshAudit.t.sol → FreshAudit.t.sol} +8 -83
- package/test/audit/{CodexNemesisFutureTierPoC.t.sol → FutureTierPoC.t.sol} +1 -1
- package/test/audit/{20260425CodexNemesisFutureTierRemoval.t.sol → FutureTierRemoval.t.sol} +1 -1
- package/test/audit/{CodexPayCreditsBypassTierSplits.t.sol → PayCreditsBypassTierSplits.t.sol} +1 -1
- package/test/audit/{CodexNemesisProjectDeployerAuth.t.sol → ProjectDeployerAuth.t.sol} +11 -18
- package/test/audit/{CodexNemesisRepoFindings.t.sol → RepoFindings.t.sol} +8 -83
- package/test/audit/{20260425CodexNemesisReserveActivation.t.sol → ReserveActivation.t.sol} +25 -31
- package/test/audit/ReserveSlotProtection.t.sol +273 -0
- package/test/audit/RetroactiveReserveBeneficiaryDilution.t.sol +149 -0
- package/test/audit/{CodexSplitCreditsMismatch.t.sol → SplitCreditsMismatch.t.sol} +1 -1
- package/test/unit/getters_constructor_Unit.t.sol +42 -42
- package/test/unit/mintFor_mintReservesFor_Unit.t.sol +92 -108
- package/test/unit/redeem_Unit.t.sol +36 -36
- package/test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol +0 -161
|
@@ -9,14 +9,17 @@ import {JB721Tier} from "../../src/structs/JB721Tier.sol";
|
|
|
9
9
|
import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
|
|
10
10
|
import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
|
|
11
11
|
|
|
12
|
-
contract
|
|
12
|
+
contract Test_ReserveActivation is Test {
|
|
13
13
|
JB721TiersHookStore internal store;
|
|
14
14
|
|
|
15
15
|
function setUp() external {
|
|
16
16
|
store = new JB721TiersHookStore();
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
/// @notice Creating a tier with reserveFrequency > 0 and no beneficiary (tier-specific or default)
|
|
20
|
+
/// is now rejected at creation time. This prevents the phantom-reserves scenario entirely.
|
|
21
|
+
function test_soldOutTier_noPhantomReserves_afterDefaultBeneficiaryChange() external {
|
|
22
|
+
// Attempt to add a tier with reserve frequency but no beneficiary — should revert.
|
|
20
23
|
JB721TierConfig[] memory initialTiers = new JB721TierConfig[](1);
|
|
21
24
|
initialTiers[0] = _tier({
|
|
22
25
|
price: 1,
|
|
@@ -26,40 +29,31 @@ contract Test_20260425CodexNemesisReserveActivation is Test {
|
|
|
26
29
|
useReserveBeneficiaryAsDefault: false,
|
|
27
30
|
category: 1
|
|
28
31
|
});
|
|
29
|
-
store.recordAddTiers(initialTiers);
|
|
30
|
-
|
|
31
|
-
uint16[] memory tierIds = new uint16[](10);
|
|
32
|
-
for (uint256 i; i < tierIds.length; i++) {
|
|
33
|
-
tierIds[i] = 1;
|
|
34
|
-
}
|
|
35
|
-
store.recordMint({amount: 10, tierIds: tierIds, isOwnerMint: false});
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
vm.expectRevert(
|
|
34
|
+
abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_MissingReserveBeneficiary.selector, 1)
|
|
35
|
+
);
|
|
36
|
+
store.recordAddTiers(initialTiers);
|
|
37
|
+
}
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
/// @notice Creating a tier with reserveFrequency > 0 and no beneficiary (tier-specific or default)
|
|
40
|
+
/// is now rejected at creation time. This prevents the retroactive reserve activation scenario entirely.
|
|
41
|
+
function test_nonSoldOutTier_reservesStillWork_afterDefaultBeneficiaryChange() external {
|
|
42
|
+
// Attempt to add a tier with reserve frequency but no beneficiary — should revert.
|
|
43
|
+
JB721TierConfig[] memory initialTiers = new JB721TierConfig[](1);
|
|
44
|
+
initialTiers[0] = _tier({
|
|
45
45
|
price: 1,
|
|
46
|
-
initialSupply:
|
|
47
|
-
reserveFrequency:
|
|
48
|
-
reserveBeneficiary: address(
|
|
49
|
-
useReserveBeneficiaryAsDefault:
|
|
50
|
-
category:
|
|
46
|
+
initialSupply: 100,
|
|
47
|
+
reserveFrequency: 5,
|
|
48
|
+
reserveBeneficiary: address(0),
|
|
49
|
+
useReserveBeneficiaryAsDefault: false,
|
|
50
|
+
category: 1
|
|
51
51
|
});
|
|
52
|
-
store.recordAddTiers(laterTiers);
|
|
53
|
-
|
|
54
|
-
JB721Tier memory afterDefault = store.tierOf({hook: address(this), id: 1, includeResolvedUri: false});
|
|
55
|
-
assertEq(afterDefault.remainingSupply, 0);
|
|
56
|
-
assertEq(afterDefault.reserveFrequency, 2);
|
|
57
|
-
assertEq(afterDefault.reserveBeneficiary, address(0xBEEF));
|
|
58
|
-
assertEq(store.numberOfPendingReservesFor({hook: address(this), tierId: 1}), 5);
|
|
59
|
-
assertEq(store.totalCashOutWeight(address(this)), 15);
|
|
60
52
|
|
|
61
|
-
vm.expectRevert(
|
|
62
|
-
|
|
53
|
+
vm.expectRevert(
|
|
54
|
+
abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_MissingReserveBeneficiary.selector, 1)
|
|
55
|
+
);
|
|
56
|
+
store.recordAddTiers(initialTiers);
|
|
63
57
|
}
|
|
64
58
|
|
|
65
59
|
function _tier(
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
6
|
+
|
|
7
|
+
import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
|
|
8
|
+
import {JB721Tier} from "../../src/structs/JB721Tier.sol";
|
|
9
|
+
import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
|
|
10
|
+
import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
|
|
11
|
+
|
|
12
|
+
/// @notice Tests that paid mints cannot consume slots reserved for the reserve beneficiary.
|
|
13
|
+
/// @dev Regression test for the vulnerability where `_numberOfPendingReservesFor` returns 0
|
|
14
|
+
/// when `remainingSupply == 0` (sold-out early-return), allowing the post-decrement guard
|
|
15
|
+
/// `0 < 0` to pass and permanently stealing reserved slots from the beneficiary.
|
|
16
|
+
/// The fix uses `_numberOfPendingReservesForMintGuard` which omits the sold-out early-return.
|
|
17
|
+
contract ReserveSlotProtection is Test {
|
|
18
|
+
JB721TiersHookStore internal store;
|
|
19
|
+
|
|
20
|
+
function setUp() public {
|
|
21
|
+
store = new JB721TiersHookStore();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// @notice Proves the fix: a paid mint that would consume the last reserved slot reverts.
|
|
25
|
+
/// Scenario: tier with initialSupply=2, reserveFrequency=1 (1 reserve per paid mint).
|
|
26
|
+
/// After 1 paid mint, 1 slot remains but it's reserved. Second paid mint must revert.
|
|
27
|
+
/// Post-decrement trace: remaining goes 2->1, pending=ceil(1/1)=1, check 1<1 = false -> ok.
|
|
28
|
+
/// Second mint: remaining goes 1->0, _numberOfPendingReservesForMintGuard returns 2
|
|
29
|
+
/// (ceil(2/1)-0=2), check 0<2 -> true -> reverts.
|
|
30
|
+
function test_paidMintRevertsWhenOnlyReservedSlotsRemain() public {
|
|
31
|
+
JB721TierConfig[] memory tiers = new JB721TierConfig[](1);
|
|
32
|
+
tiers[0] = JB721TierConfig({
|
|
33
|
+
price: 1 ether,
|
|
34
|
+
initialSupply: 2,
|
|
35
|
+
votingUnits: 0,
|
|
36
|
+
reserveFrequency: 1,
|
|
37
|
+
reserveBeneficiary: address(0xBEEF),
|
|
38
|
+
encodedIPFSUri: bytes32(0),
|
|
39
|
+
category: 0,
|
|
40
|
+
discountPercent: 0,
|
|
41
|
+
flags: JB721TierConfigFlags({
|
|
42
|
+
allowOwnerMint: false,
|
|
43
|
+
useReserveBeneficiaryAsDefault: false,
|
|
44
|
+
transfersPausable: false,
|
|
45
|
+
useVotingUnits: false,
|
|
46
|
+
cantBeRemoved: false,
|
|
47
|
+
cantIncreaseDiscountPercent: false,
|
|
48
|
+
cantBuyWithCredits: false
|
|
49
|
+
}),
|
|
50
|
+
splitPercent: 0,
|
|
51
|
+
splits: new JBSplit[](0)
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
store.recordAddTiers(tiers);
|
|
55
|
+
|
|
56
|
+
uint16[] memory tierIds = new uint16[](1);
|
|
57
|
+
tierIds[0] = 1;
|
|
58
|
+
|
|
59
|
+
// First paid mint succeeds — uses 1 of 2 slots, leaving 1 for the pending reserve.
|
|
60
|
+
store.recordMint({amount: 1 ether, tierIds: tierIds, isOwnerMint: false});
|
|
61
|
+
|
|
62
|
+
// Verify state: 1 remaining supply, 1 pending reserve.
|
|
63
|
+
JB721Tier memory tier = store.tierOf(address(this), 1, false);
|
|
64
|
+
assertEq(tier.remainingSupply, 1, "should have 1 slot remaining after first mint");
|
|
65
|
+
assertEq(store.numberOfPendingReservesFor(address(this), 1), 1, "should have 1 pending reserve");
|
|
66
|
+
|
|
67
|
+
// Second paid mint MUST revert — the only remaining slot belongs to the reserve beneficiary.
|
|
68
|
+
vm.expectRevert(
|
|
69
|
+
abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientSupplyRemaining.selector, 1)
|
|
70
|
+
);
|
|
71
|
+
store.recordMint({amount: 1 ether, tierIds: tierIds, isOwnerMint: false});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// @notice Proves that the reserve beneficiary can still mint their reserved slot after fix.
|
|
75
|
+
function test_reserveBeneficiaryCanMintAfterPaidSlotsExhausted() public {
|
|
76
|
+
JB721TierConfig[] memory tiers = new JB721TierConfig[](1);
|
|
77
|
+
tiers[0] = JB721TierConfig({
|
|
78
|
+
price: 1 ether,
|
|
79
|
+
initialSupply: 2,
|
|
80
|
+
votingUnits: 0,
|
|
81
|
+
reserveFrequency: 1,
|
|
82
|
+
reserveBeneficiary: address(0xBEEF),
|
|
83
|
+
encodedIPFSUri: bytes32(0),
|
|
84
|
+
category: 0,
|
|
85
|
+
discountPercent: 0,
|
|
86
|
+
flags: JB721TierConfigFlags({
|
|
87
|
+
allowOwnerMint: false,
|
|
88
|
+
useReserveBeneficiaryAsDefault: false,
|
|
89
|
+
transfersPausable: false,
|
|
90
|
+
useVotingUnits: false,
|
|
91
|
+
cantBeRemoved: false,
|
|
92
|
+
cantIncreaseDiscountPercent: false,
|
|
93
|
+
cantBuyWithCredits: false
|
|
94
|
+
}),
|
|
95
|
+
splitPercent: 0,
|
|
96
|
+
splits: new JBSplit[](0)
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
store.recordAddTiers(tiers);
|
|
100
|
+
|
|
101
|
+
uint16[] memory tierIds = new uint16[](1);
|
|
102
|
+
tierIds[0] = 1;
|
|
103
|
+
|
|
104
|
+
// First paid mint.
|
|
105
|
+
store.recordMint({amount: 1 ether, tierIds: tierIds, isOwnerMint: false});
|
|
106
|
+
|
|
107
|
+
// Reserve beneficiary mints their entitled reserve.
|
|
108
|
+
uint256[] memory reserveTokenIds = store.recordMintReservesFor({tierId: 1, count: 1});
|
|
109
|
+
assertEq(reserveTokenIds.length, 1, "reserve beneficiary should get 1 token");
|
|
110
|
+
|
|
111
|
+
// After reserve mint, remaining supply is 0 and reserves are fulfilled.
|
|
112
|
+
JB721Tier memory tier = store.tierOf(address(this), 1, false);
|
|
113
|
+
assertEq(tier.remainingSupply, 0, "tier should be fully minted");
|
|
114
|
+
assertEq(store.numberOfReservesMintedFor(address(this), 1), 1, "1 reserve should be minted");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// @notice Tests a larger tier: initialSupply=10, reserveFrequency=2.
|
|
118
|
+
/// With frequency=2, every 2 paid mints earn 1 reserve (rounded up).
|
|
119
|
+
/// Post-decrement analysis for the 7th mint:
|
|
120
|
+
/// remaining goes 4->3, nonReserveMints=7, pending=ceil(7/2)-0=4.
|
|
121
|
+
/// Check: 3 < 4 -> true -> reverts. Correct!
|
|
122
|
+
/// So mints 1-6 succeed, mint 7 reverts.
|
|
123
|
+
function test_largerTierReserveProtection() public {
|
|
124
|
+
JB721TierConfig[] memory tiers = new JB721TierConfig[](1);
|
|
125
|
+
tiers[0] = JB721TierConfig({
|
|
126
|
+
price: 0.1 ether,
|
|
127
|
+
initialSupply: 10,
|
|
128
|
+
votingUnits: 0,
|
|
129
|
+
reserveFrequency: 2,
|
|
130
|
+
reserveBeneficiary: address(0xCAFE),
|
|
131
|
+
encodedIPFSUri: bytes32(0),
|
|
132
|
+
category: 0,
|
|
133
|
+
discountPercent: 0,
|
|
134
|
+
flags: JB721TierConfigFlags({
|
|
135
|
+
allowOwnerMint: false,
|
|
136
|
+
useReserveBeneficiaryAsDefault: false,
|
|
137
|
+
transfersPausable: false,
|
|
138
|
+
useVotingUnits: false,
|
|
139
|
+
cantBeRemoved: false,
|
|
140
|
+
cantIncreaseDiscountPercent: false,
|
|
141
|
+
cantBuyWithCredits: false
|
|
142
|
+
}),
|
|
143
|
+
splitPercent: 0,
|
|
144
|
+
splits: new JBSplit[](0)
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
store.recordAddTiers(tiers);
|
|
148
|
+
|
|
149
|
+
uint16[] memory tierIds = new uint16[](1);
|
|
150
|
+
tierIds[0] = 1;
|
|
151
|
+
|
|
152
|
+
// Mint 6 paid NFTs. After 6 mints: remaining=4, pending=ceil(6/2)=3.
|
|
153
|
+
for (uint256 i; i < 6; i++) {
|
|
154
|
+
store.recordMint({amount: 0.1 ether, tierIds: tierIds, isOwnerMint: false});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
JB721Tier memory tier = store.tierOf(address(this), 1, false);
|
|
158
|
+
assertEq(tier.remainingSupply, 4, "should have 4 remaining after 6 mints");
|
|
159
|
+
assertEq(store.numberOfPendingReservesFor(address(this), 1), 3, "should have 3 pending reserves");
|
|
160
|
+
|
|
161
|
+
// 7th paid mint should revert: after decrement, remaining=3, pending=ceil(7/2)=4. 3<4 -> reverts.
|
|
162
|
+
vm.expectRevert(
|
|
163
|
+
abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientSupplyRemaining.selector, 1)
|
|
164
|
+
);
|
|
165
|
+
store.recordMint({amount: 0.1 ether, tierIds: tierIds, isOwnerMint: false});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// @notice Verifies that when reserves are minted between paid mints, paid mints can continue.
|
|
169
|
+
/// initialSupply=4, reserveFrequency=1.
|
|
170
|
+
function test_paidMintsResumeAfterReservesMinted() public {
|
|
171
|
+
JB721TierConfig[] memory tiers = new JB721TierConfig[](1);
|
|
172
|
+
tiers[0] = JB721TierConfig({
|
|
173
|
+
price: 1 ether,
|
|
174
|
+
initialSupply: 4,
|
|
175
|
+
votingUnits: 0,
|
|
176
|
+
reserveFrequency: 1,
|
|
177
|
+
reserveBeneficiary: address(0xBEEF),
|
|
178
|
+
encodedIPFSUri: bytes32(0),
|
|
179
|
+
category: 0,
|
|
180
|
+
discountPercent: 0,
|
|
181
|
+
flags: JB721TierConfigFlags({
|
|
182
|
+
allowOwnerMint: false,
|
|
183
|
+
useReserveBeneficiaryAsDefault: false,
|
|
184
|
+
transfersPausable: false,
|
|
185
|
+
useVotingUnits: false,
|
|
186
|
+
cantBeRemoved: false,
|
|
187
|
+
cantIncreaseDiscountPercent: false,
|
|
188
|
+
cantBuyWithCredits: false
|
|
189
|
+
}),
|
|
190
|
+
splitPercent: 0,
|
|
191
|
+
splits: new JBSplit[](0)
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
store.recordAddTiers(tiers);
|
|
195
|
+
|
|
196
|
+
uint16[] memory tierIds = new uint16[](1);
|
|
197
|
+
tierIds[0] = 1;
|
|
198
|
+
|
|
199
|
+
// Paid mint 1: remaining 4->3, nonReserveMints=1, pending=ceil(1/1)=1. Check: 3<1? No. OK.
|
|
200
|
+
store.recordMint({amount: 1 ether, tierIds: tierIds, isOwnerMint: false});
|
|
201
|
+
|
|
202
|
+
// Paid mint 2: remaining 3->2, nonReserveMints=2, pending=ceil(2/1)=2. Check: 2<2? No. OK.
|
|
203
|
+
store.recordMint({amount: 1 ether, tierIds: tierIds, isOwnerMint: false});
|
|
204
|
+
|
|
205
|
+
// Paid mint 3: remaining 2->1, nonReserveMints=3, pending=ceil(3/1)=3. Check: 1<3? Yes! Revert.
|
|
206
|
+
vm.expectRevert(
|
|
207
|
+
abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientSupplyRemaining.selector, 1)
|
|
208
|
+
);
|
|
209
|
+
store.recordMint({amount: 1 ether, tierIds: tierIds, isOwnerMint: false});
|
|
210
|
+
|
|
211
|
+
// Mint 1 reserve — frees up a slot. Now reservesMinted=1.
|
|
212
|
+
store.recordMintReservesFor({tierId: 1, count: 1});
|
|
213
|
+
|
|
214
|
+
// State: remaining=1, reservesMinted=1, nonReserveMints=2.
|
|
215
|
+
// Paid mint attempt: remaining 1->0, nonReserveMints=3, pending=ceil(3/1)-1=2. Check: 0<2? Yes! Revert.
|
|
216
|
+
vm.expectRevert(
|
|
217
|
+
abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientSupplyRemaining.selector, 1)
|
|
218
|
+
);
|
|
219
|
+
store.recordMint({amount: 1 ether, tierIds: tierIds, isOwnerMint: false});
|
|
220
|
+
|
|
221
|
+
// Mint remaining reserve.
|
|
222
|
+
store.recordMintReservesFor({tierId: 1, count: 1});
|
|
223
|
+
|
|
224
|
+
// Tier fully minted — 0 remaining.
|
|
225
|
+
JB721Tier memory tier = store.tierOf(address(this), 1, false);
|
|
226
|
+
assertEq(tier.remainingSupply, 0, "tier should be fully minted");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// @notice Without reserves, all NFTs in a tier should be mintable (no off-by-one).
|
|
230
|
+
function test_noReservesFullSupplyMintable() public {
|
|
231
|
+
JB721TierConfig[] memory tiers = new JB721TierConfig[](1);
|
|
232
|
+
tiers[0] = JB721TierConfig({
|
|
233
|
+
price: 0.1 ether,
|
|
234
|
+
initialSupply: 5,
|
|
235
|
+
votingUnits: 0,
|
|
236
|
+
reserveFrequency: 0,
|
|
237
|
+
reserveBeneficiary: address(0),
|
|
238
|
+
encodedIPFSUri: bytes32(0),
|
|
239
|
+
category: 0,
|
|
240
|
+
discountPercent: 0,
|
|
241
|
+
flags: JB721TierConfigFlags({
|
|
242
|
+
allowOwnerMint: false,
|
|
243
|
+
useReserveBeneficiaryAsDefault: false,
|
|
244
|
+
transfersPausable: false,
|
|
245
|
+
useVotingUnits: false,
|
|
246
|
+
cantBeRemoved: false,
|
|
247
|
+
cantIncreaseDiscountPercent: false,
|
|
248
|
+
cantBuyWithCredits: false
|
|
249
|
+
}),
|
|
250
|
+
splitPercent: 0,
|
|
251
|
+
splits: new JBSplit[](0)
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
store.recordAddTiers(tiers);
|
|
255
|
+
|
|
256
|
+
uint16[] memory tierIds = new uint16[](1);
|
|
257
|
+
tierIds[0] = 1;
|
|
258
|
+
|
|
259
|
+
// Mint all 5 — should succeed since no reserves to protect.
|
|
260
|
+
for (uint256 i; i < 5; i++) {
|
|
261
|
+
store.recordMint({amount: 0.1 ether, tierIds: tierIds, isOwnerMint: false});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
JB721Tier memory tier = store.tierOf(address(this), 1, false);
|
|
265
|
+
assertEq(tier.remainingSupply, 0, "fully minted");
|
|
266
|
+
|
|
267
|
+
// 6th mint should revert (supply exhausted).
|
|
268
|
+
vm.expectRevert(
|
|
269
|
+
abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientSupplyRemaining.selector, 1)
|
|
270
|
+
);
|
|
271
|
+
store.recordMint({amount: 0.1 ether, tierIds: tierIds, isOwnerMint: false});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
|
+
import "../utils/UnitTestSetup.sol";
|
|
6
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
7
|
+
import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
|
|
8
|
+
import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
|
|
9
|
+
import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
|
|
10
|
+
|
|
11
|
+
contract RetroactiveReserveBeneficiaryDilution is UnitTestSetup {
|
|
12
|
+
/// @notice Creating a tier with reserveFrequency > 0 but no beneficiary (tier-specific or default) now reverts,
|
|
13
|
+
/// preventing the phantom reserve scenario where a later default beneficiary retroactively inflates
|
|
14
|
+
/// totalCashOutWeight and dilutes existing holders.
|
|
15
|
+
function test_adjustTier_reverts_when_reserve_has_no_beneficiary() public {
|
|
16
|
+
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
17
|
+
|
|
18
|
+
JB721TierConfig[] memory tier1 = new JB721TierConfig[](1);
|
|
19
|
+
tier1[0] = JB721TierConfig({
|
|
20
|
+
price: 1 ether,
|
|
21
|
+
initialSupply: 100,
|
|
22
|
+
votingUnits: 0,
|
|
23
|
+
reserveFrequency: 2,
|
|
24
|
+
reserveBeneficiary: address(0),
|
|
25
|
+
encodedIPFSUri: bytes32(uint256(0x1234)),
|
|
26
|
+
category: 1,
|
|
27
|
+
discountPercent: 0,
|
|
28
|
+
flags: JB721TierConfigFlags({
|
|
29
|
+
allowOwnerMint: false,
|
|
30
|
+
useReserveBeneficiaryAsDefault: false,
|
|
31
|
+
transfersPausable: false,
|
|
32
|
+
useVotingUnits: false,
|
|
33
|
+
cantBeRemoved: false,
|
|
34
|
+
cantIncreaseDiscountPercent: false,
|
|
35
|
+
cantBuyWithCredits: false
|
|
36
|
+
}),
|
|
37
|
+
splitPercent: 0,
|
|
38
|
+
splits: new JBSplit[](0)
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
vm.prank(owner);
|
|
42
|
+
vm.expectRevert(
|
|
43
|
+
abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_MissingReserveBeneficiary.selector, 1)
|
|
44
|
+
);
|
|
45
|
+
testHook.adjustTiers(tier1, new uint256[](0));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// @notice A tier with reserveFrequency > 0 succeeds when an explicit beneficiary is provided.
|
|
49
|
+
function test_adjustTier_succeeds_with_explicit_reserve_beneficiary() public {
|
|
50
|
+
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
51
|
+
|
|
52
|
+
JB721TierConfig[] memory tier1 = new JB721TierConfig[](1);
|
|
53
|
+
tier1[0] = JB721TierConfig({
|
|
54
|
+
price: 1 ether,
|
|
55
|
+
initialSupply: 100,
|
|
56
|
+
votingUnits: 0,
|
|
57
|
+
reserveFrequency: 2,
|
|
58
|
+
reserveBeneficiary: owner,
|
|
59
|
+
encodedIPFSUri: bytes32(uint256(0x1234)),
|
|
60
|
+
category: 1,
|
|
61
|
+
discountPercent: 0,
|
|
62
|
+
flags: JB721TierConfigFlags({
|
|
63
|
+
allowOwnerMint: false,
|
|
64
|
+
useReserveBeneficiaryAsDefault: false,
|
|
65
|
+
transfersPausable: false,
|
|
66
|
+
useVotingUnits: false,
|
|
67
|
+
cantBeRemoved: false,
|
|
68
|
+
cantIncreaseDiscountPercent: false,
|
|
69
|
+
cantBuyWithCredits: false
|
|
70
|
+
}),
|
|
71
|
+
splitPercent: 0,
|
|
72
|
+
splits: new JBSplit[](0)
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
vm.prank(owner);
|
|
76
|
+
testHook.adjustTiers(tier1, new uint256[](0));
|
|
77
|
+
|
|
78
|
+
assertEq(
|
|
79
|
+
testHook.STORE().reserveBeneficiaryOf(address(testHook), 1),
|
|
80
|
+
owner,
|
|
81
|
+
"tier-specific beneficiary should be set"
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// @notice A tier with reserveFrequency > 0 and no explicit beneficiary succeeds when a default beneficiary
|
|
86
|
+
/// was previously set by an earlier tier.
|
|
87
|
+
function test_adjustTier_succeeds_with_default_reserve_beneficiary() public {
|
|
88
|
+
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
89
|
+
|
|
90
|
+
// Add two tiers: first sets the default beneficiary, second relies on it.
|
|
91
|
+
JB721TierConfig[] memory tiers = new JB721TierConfig[](2);
|
|
92
|
+
|
|
93
|
+
// Tier 1: sets default beneficiary via useReserveBeneficiaryAsDefault.
|
|
94
|
+
tiers[0] = JB721TierConfig({
|
|
95
|
+
price: 1 ether,
|
|
96
|
+
initialSupply: 100,
|
|
97
|
+
votingUnits: 0,
|
|
98
|
+
reserveFrequency: 2,
|
|
99
|
+
reserveBeneficiary: owner,
|
|
100
|
+
encodedIPFSUri: bytes32(uint256(0x1234)),
|
|
101
|
+
category: 1,
|
|
102
|
+
discountPercent: 0,
|
|
103
|
+
flags: JB721TierConfigFlags({
|
|
104
|
+
allowOwnerMint: false,
|
|
105
|
+
useReserveBeneficiaryAsDefault: true,
|
|
106
|
+
transfersPausable: false,
|
|
107
|
+
useVotingUnits: false,
|
|
108
|
+
cantBeRemoved: false,
|
|
109
|
+
cantIncreaseDiscountPercent: false,
|
|
110
|
+
cantBuyWithCredits: false
|
|
111
|
+
}),
|
|
112
|
+
splitPercent: 0,
|
|
113
|
+
splits: new JBSplit[](0)
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Tier 2: relies on the default beneficiary (no explicit one).
|
|
117
|
+
tiers[1] = JB721TierConfig({
|
|
118
|
+
price: 2 ether,
|
|
119
|
+
initialSupply: 100,
|
|
120
|
+
votingUnits: 0,
|
|
121
|
+
reserveFrequency: 3,
|
|
122
|
+
reserveBeneficiary: address(0),
|
|
123
|
+
encodedIPFSUri: bytes32(uint256(0x5678)),
|
|
124
|
+
category: 2,
|
|
125
|
+
discountPercent: 0,
|
|
126
|
+
flags: JB721TierConfigFlags({
|
|
127
|
+
allowOwnerMint: false,
|
|
128
|
+
useReserveBeneficiaryAsDefault: false,
|
|
129
|
+
transfersPausable: false,
|
|
130
|
+
useVotingUnits: false,
|
|
131
|
+
cantBeRemoved: false,
|
|
132
|
+
cantIncreaseDiscountPercent: false,
|
|
133
|
+
cantBuyWithCredits: false
|
|
134
|
+
}),
|
|
135
|
+
splitPercent: 0,
|
|
136
|
+
splits: new JBSplit[](0)
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
vm.prank(owner);
|
|
140
|
+
testHook.adjustTiers(tiers, new uint256[](0));
|
|
141
|
+
|
|
142
|
+
// Tier 2 should inherit the default beneficiary set by tier 1.
|
|
143
|
+
assertEq(
|
|
144
|
+
testHook.STORE().reserveBeneficiaryOf(address(testHook), 2),
|
|
145
|
+
owner,
|
|
146
|
+
"tier 2 should inherit default beneficiary"
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -15,7 +15,7 @@ import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
|
|
|
15
15
|
/// @notice Regression test: split metadata is proportionally scaled when credits fund a split-bearing tier mint.
|
|
16
16
|
/// @dev Previously (pre-fix), the per-tier split amounts were left at the uncapped value, trapping forwarded ETH.
|
|
17
17
|
/// After the F-2 fix, split amounts are scaled down to match the actual forwarded amount.
|
|
18
|
-
contract
|
|
18
|
+
contract SplitCreditsMismatch is UnitTestSetup {
|
|
19
19
|
address internal splitBeneficiary = makeAddr("splitBeneficiary");
|
|
20
20
|
|
|
21
21
|
function setUp() public override {
|
|
@@ -181,15 +181,15 @@ contract Test_Getters_Constructor_Unit is UnitTestSetup {
|
|
|
181
181
|
address(hook),
|
|
182
182
|
i + 1,
|
|
183
183
|
JBStored721Tier({
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
184
|
+
price: uint104((i + 1) * 10),
|
|
185
|
+
remainingSupply: uint32(100 - (i + 1)),
|
|
186
|
+
initialSupply: uint32(100),
|
|
187
|
+
reserveFrequency: uint16(0),
|
|
188
|
+
category: uint24(100),
|
|
189
|
+
discountPercent: uint8(0),
|
|
190
|
+
packedBools: hook.test_store().ForTest_packBools(false, false, false, false, false, false),
|
|
191
|
+
splitPercent: 0
|
|
192
|
+
})
|
|
193
193
|
);
|
|
194
194
|
}
|
|
195
195
|
|
|
@@ -235,18 +235,18 @@ contract Test_Getters_Constructor_Unit is UnitTestSetup {
|
|
|
235
235
|
address(hook),
|
|
236
236
|
i + 1,
|
|
237
237
|
JBStored721Tier({
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
238
|
+
price: uint104((i + 1) * 10),
|
|
239
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
240
|
+
remainingSupply: uint32(initialSupply - totalMinted),
|
|
241
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
242
|
+
initialSupply: uint32(initialSupply),
|
|
243
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
244
|
+
reserveFrequency: uint16(reserveFrequency),
|
|
245
|
+
category: uint24(100),
|
|
246
|
+
discountPercent: uint8(0),
|
|
247
|
+
packedBools: hook.test_store().ForTest_packBools(false, false, false, false, false, false),
|
|
248
|
+
splitPercent: 0
|
|
249
|
+
})
|
|
250
250
|
);
|
|
251
251
|
// Manually set the number of reserve mints for each tier.
|
|
252
252
|
hook.test_store().ForTest_setReservesMintedFor(address(hook), i + 1, reservedMinted);
|
|
@@ -280,15 +280,15 @@ contract Test_Getters_Constructor_Unit is UnitTestSetup {
|
|
|
280
280
|
address(hook),
|
|
281
281
|
1,
|
|
282
282
|
JBStored721Tier({
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
283
|
+
price: uint104(10),
|
|
284
|
+
remainingSupply: uint32(10),
|
|
285
|
+
initialSupply: uint32(20),
|
|
286
|
+
reserveFrequency: uint16(100),
|
|
287
|
+
category: uint24(100),
|
|
288
|
+
discountPercent: uint8(0),
|
|
289
|
+
packedBools: hook.test_store().ForTest_packBools(false, false, true, false, false, false),
|
|
290
|
+
splitPercent: 0
|
|
291
|
+
})
|
|
292
292
|
);
|
|
293
293
|
// Clear the voting units mapping for tier 1 (ForTest_setTier only overwrites the packed struct).
|
|
294
294
|
hook.test_store().ForTest_setTierVotingUnits(address(hook), 1, 0);
|
|
@@ -438,18 +438,18 @@ contract Test_Getters_Constructor_Unit is UnitTestSetup {
|
|
|
438
438
|
address(hook),
|
|
439
439
|
i,
|
|
440
440
|
JBStored721Tier({
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
441
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
442
|
+
price: uint104(i * 10),
|
|
443
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
444
|
+
remainingSupply: uint32(10 * i - 5 * i),
|
|
445
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
446
|
+
initialSupply: uint32(10 * i),
|
|
447
|
+
reserveFrequency: uint16(0),
|
|
448
|
+
category: uint24(100),
|
|
449
|
+
discountPercent: uint8(0),
|
|
450
|
+
packedBools: hook.test_store().ForTest_packBools(false, false, false, false, false, false),
|
|
451
|
+
splitPercent: 0
|
|
452
|
+
})
|
|
453
453
|
);
|
|
454
454
|
// Calculate the theoretical weight for the current tier. 10 the price multiplier.
|
|
455
455
|
theoreticalWeight += (10 * i - 5 * i) * i * 10;
|