@bananapus/721-hook-v6 0.0.39 → 0.0.41
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 +19 -0
- package/src/interfaces/IJB721TiersHook.sol +0 -1
- package/src/libraries/JB721TiersHookLib.sol +16 -1
- package/test/audit/{CodexNemesisFreshAudit.t.sol → FreshAudit.t.sol} +8 -83
- package/test/audit/{CodexNemesisFutureTierPoC.t.sol → FutureTierPoC.t.sol} +4 -16
- package/test/audit/{20260425CodexNemesisFutureTierRemoval.t.sol → FutureTierRemoval.t.sol} +4 -13
- package/test/audit/Pass12L18.t.sol +80 -0
- 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/RetroactiveReserveBeneficiaryDilution.t.sol +149 -0
- package/test/audit/{CodexSplitCreditsMismatch.t.sol → SplitCreditsMismatch.t.sol} +1 -1
- package/test/fork/ERC20CashOutFork.t.sol +1 -1
- package/test/unit/mintFor_mintReservesFor_Unit.t.sol +32 -48
- package/test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol +0 -161
package/package.json
CHANGED
package/src/JB721TiersHook.sol
CHANGED
|
@@ -7,7 +7,6 @@ import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
|
|
|
7
7
|
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
8
8
|
import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
|
|
9
9
|
import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
|
|
10
|
-
import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
11
10
|
import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
|
|
12
11
|
import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
|
|
13
12
|
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
@@ -24,7 +23,6 @@ import {IJB721CheckpointsDeployer} from "./interfaces/IJB721CheckpointsDeployer.
|
|
|
24
23
|
import {IJB721TiersHook} from "./interfaces/IJB721TiersHook.sol";
|
|
25
24
|
import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
|
|
26
25
|
import {IJB721TokenUriResolver} from "./interfaces/IJB721TokenUriResolver.sol";
|
|
27
|
-
import {JB721Constants} from "./libraries/JB721Constants.sol";
|
|
28
26
|
import {JB721TiersHookLib} from "./libraries/JB721TiersHookLib.sol";
|
|
29
27
|
import {JB721TiersRulesetMetadataResolver} from "./libraries/JB721TiersRulesetMetadataResolver.sol";
|
|
30
28
|
import {JB721InitTiersConfig} from "./structs/JB721InitTiersConfig.sol";
|
|
@@ -206,19 +204,21 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
206
204
|
uint256 totalSplitAmount;
|
|
207
205
|
bytes memory splitMetadata;
|
|
208
206
|
address beneficiary;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
207
|
+
uint256 splitCreditWeight;
|
|
208
|
+
(weight, totalSplitAmount, splitMetadata, beneficiary, splitCreditWeight) =
|
|
209
|
+
JB721TiersHookLib.computeSplitsAndWeight({
|
|
210
|
+
store: STORE,
|
|
211
|
+
metadataIdTarget: METADATA_ID_TARGET,
|
|
212
|
+
packedPricingContext: _packedPricingContext,
|
|
213
|
+
prices: PRICES,
|
|
214
|
+
context: context
|
|
215
|
+
});
|
|
216
216
|
|
|
217
217
|
hookSpecifications[0] = JBPayHookSpecification({
|
|
218
218
|
hook: this,
|
|
219
219
|
noop: false,
|
|
220
220
|
amount: totalSplitAmount,
|
|
221
|
-
metadata: abi.encode(beneficiary, context.payer, splitMetadata)
|
|
221
|
+
metadata: abi.encode(beneficiary, context.payer, splitMetadata, splitCreditWeight)
|
|
222
222
|
});
|
|
223
223
|
}
|
|
224
224
|
|
|
@@ -36,6 +36,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
36
36
|
error JB721TiersHookStore_InvalidQuantity(uint256 quantity, uint256 limit);
|
|
37
37
|
error JB721TiersHookStore_ManualMintingNotAllowed(uint256 tierId);
|
|
38
38
|
error JB721TiersHookStore_MaxTiersExceeded(uint256 numberOfTiers, uint256 limit);
|
|
39
|
+
error JB721TiersHookStore_MissingReserveBeneficiary(uint256 tierId);
|
|
39
40
|
error JB721TiersHookStore_PriceExceedsAmount(uint256 price, uint256 leftoverAmount);
|
|
40
41
|
error JB721TiersHookStore_ReserveFrequencyNotAllowed(uint256 tierId);
|
|
41
42
|
error JB721TiersHookStore_SplitPercentExceedsBounds(uint256 percent, uint256 limit);
|
|
@@ -724,6 +725,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
724
725
|
|| reserveBeneficiaryOf({hook: hook, tierId: tierId}) == address(0)
|
|
725
726
|
) return 0;
|
|
726
727
|
|
|
728
|
+
// A sold-out tier cannot have mintable pending reserves — minting would underflow remainingSupply.
|
|
729
|
+
if (storedTier.remainingSupply == 0) return 0;
|
|
730
|
+
|
|
727
731
|
// The number of reserve NFTs which have already been minted from the tier.
|
|
728
732
|
uint256 numberOfReserveMints = numberOfReservesMintedFor[hook][tierId];
|
|
729
733
|
|
|
@@ -976,6 +980,15 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
976
980
|
revert JB721TiersHookStore_DeadlockedReserve();
|
|
977
981
|
}
|
|
978
982
|
|
|
983
|
+
// A tier with reserves must have a beneficiary — either tier-specific or a previously set default.
|
|
984
|
+
// Without one, minted reserves would be sent to address(0).
|
|
985
|
+
if (
|
|
986
|
+
tierToAdd.reserveFrequency > 0 && tierToAdd.reserveBeneficiary == address(0)
|
|
987
|
+
&& defaultReserveBeneficiaryOf[msg.sender] == address(0)
|
|
988
|
+
) {
|
|
989
|
+
revert JB721TiersHookStore_MissingReserveBeneficiary(tierId);
|
|
990
|
+
}
|
|
991
|
+
|
|
979
992
|
// Store the tier with that ID.
|
|
980
993
|
_storedTierOf[msg.sender][tierId] = JBStored721Tier({
|
|
981
994
|
price: uint104(tierToAdd.price),
|
|
@@ -1291,6 +1304,12 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1291
1304
|
// Set the tier being iterated upon (0-indexed).
|
|
1292
1305
|
uint256 tierId = tierIds[i];
|
|
1293
1306
|
|
|
1307
|
+
// Reject tier IDs that don't exist yet — removing a future tier would cause it
|
|
1308
|
+
// to be born already removed when later added.
|
|
1309
|
+
if (tierId == 0 || tierId > maxTierIdOf[msg.sender]) {
|
|
1310
|
+
revert JB721TiersHookStore_UnrecognizedTier(tierId);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1294
1313
|
// Get a reference to the stored tier.
|
|
1295
1314
|
JBStored721Tier storage storedTier = _storedTierOf[msg.sender][tierId];
|
|
1296
1315
|
|
|
@@ -7,7 +7,6 @@ import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
|
|
|
7
7
|
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
8
8
|
|
|
9
9
|
import {IJB721Checkpoints} from "./IJB721Checkpoints.sol";
|
|
10
|
-
import {IJB721CheckpointsDeployer} from "./IJB721CheckpointsDeployer.sol";
|
|
11
10
|
import {IJB721Hook} from "./IJB721Hook.sol";
|
|
12
11
|
import {IJB721TiersHookStore} from "./IJB721TiersHookStore.sol";
|
|
13
12
|
import {IJB721TokenUriResolver} from "./IJB721TokenUriResolver.sol";
|
|
@@ -309,6 +309,8 @@ library JB721TiersHookLib {
|
|
|
309
309
|
/// @return totalSplitAmount The total amount to forward for splits.
|
|
310
310
|
/// @return splitMetadata Encoded per-tier breakdown (tierIds, amounts).
|
|
311
311
|
/// @return beneficiary The resolved beneficiary address.
|
|
312
|
+
/// @return splitCreditWeight The weight attributable to tier splits when `issueTokensForSplits` is true.
|
|
313
|
+
/// Zero when splits are absent or `issueTokensForSplits` is false.
|
|
312
314
|
function computeSplitsAndWeight(
|
|
313
315
|
IJB721TiersHookStore store,
|
|
314
316
|
address metadataIdTarget,
|
|
@@ -318,7 +320,13 @@ library JB721TiersHookLib {
|
|
|
318
320
|
)
|
|
319
321
|
external
|
|
320
322
|
view
|
|
321
|
-
returns (
|
|
323
|
+
returns (
|
|
324
|
+
uint256 weight,
|
|
325
|
+
uint256 totalSplitAmount,
|
|
326
|
+
bytes memory splitMetadata,
|
|
327
|
+
address beneficiary,
|
|
328
|
+
uint256 splitCreditWeight
|
|
329
|
+
)
|
|
322
330
|
{
|
|
323
331
|
// Calculate per-tier split amounts.
|
|
324
332
|
(totalSplitAmount, splitMetadata) = _calculateSplitAmounts({
|
|
@@ -346,6 +354,13 @@ library JB721TiersHookLib {
|
|
|
346
354
|
hook: address(this)
|
|
347
355
|
});
|
|
348
356
|
|
|
357
|
+
// When issueTokensForSplits is true and there are splits, compute the weight portion
|
|
358
|
+
// attributable to tier splits. Downstream compositors (e.g. JBOmnichainDeployer) use this
|
|
359
|
+
// to preserve split credit when an extra hook (buyback) returns weight=0.
|
|
360
|
+
if (totalSplitAmount != 0 && context.amount.value != 0 && store.flagsOf(address(this)).issueTokensForSplits) {
|
|
361
|
+
splitCreditWeight = mulDiv(context.weight, totalSplitAmount, context.amount.value);
|
|
362
|
+
}
|
|
363
|
+
|
|
349
364
|
// Resolve the effective beneficiary from payment metadata.
|
|
350
365
|
beneficiary = context.beneficiary;
|
|
351
366
|
{
|
|
@@ -4,6 +4,7 @@ pragma solidity 0.8.28;
|
|
|
4
4
|
import {UnitTestSetup} from "../utils/UnitTestSetup.sol";
|
|
5
5
|
import {ForTest_JB721TiersHook} from "../utils/ForTest_JB721TiersHook.sol";
|
|
6
6
|
import {IJB721TiersHookStore} from "../../src/interfaces/IJB721TiersHookStore.sol";
|
|
7
|
+
import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
|
|
7
8
|
import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
|
|
8
9
|
import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
|
|
9
10
|
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
@@ -16,7 +17,7 @@ import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
|
|
|
16
17
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
17
18
|
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
18
19
|
|
|
19
|
-
contract
|
|
20
|
+
contract FreshAudit is UnitTestSetup {
|
|
20
21
|
function _buildPayMetadata(
|
|
21
22
|
address hookAddress,
|
|
22
23
|
bool allowOverspending,
|
|
@@ -158,16 +159,10 @@ contract CodexNemesisFreshAudit is UnitTestSetup {
|
|
|
158
159
|
assertEq(hookStore.totalCashOutWeight(address(testHook)), 1 ether, "full-price NFT still enters cash-out math");
|
|
159
160
|
}
|
|
160
161
|
|
|
162
|
+
/// @notice Creating a tier with reserveFrequency > 0 and no beneficiary (tier-specific or default)
|
|
163
|
+
/// is now rejected at creation time, preventing the retroactive dilution bug.
|
|
161
164
|
function test_new_default_reserve_beneficiary_retroactively_dilutes_existing_tiers() public {
|
|
162
165
|
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
163
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
164
|
-
address retroReserveBeneficiary = makeAddr("retroReserveBeneficiary");
|
|
165
|
-
|
|
166
|
-
vm.mockCall(
|
|
167
|
-
mockJBDirectory,
|
|
168
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
169
|
-
abi.encode(true)
|
|
170
|
-
);
|
|
171
166
|
|
|
172
167
|
JB721TierConfig[] memory initialTier = new JB721TierConfig[](1);
|
|
173
168
|
initialTier[0] = JB721TierConfig({
|
|
@@ -192,81 +187,11 @@ contract CodexNemesisFreshAudit is UnitTestSetup {
|
|
|
192
187
|
splits: new JBSplit[](0)
|
|
193
188
|
});
|
|
194
189
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
uint16[] memory tierIds = new uint16[](2);
|
|
199
|
-
tierIds[0] = 1;
|
|
200
|
-
tierIds[1] = 1;
|
|
201
|
-
|
|
202
|
-
JBAfterPayRecordedContext memory initialMint = JBAfterPayRecordedContext({
|
|
203
|
-
payer: beneficiary,
|
|
204
|
-
projectId: projectId,
|
|
205
|
-
rulesetId: 0,
|
|
206
|
-
amount: _nativeAmount(2 ether),
|
|
207
|
-
forwardedAmount: _nativeAmount(0),
|
|
208
|
-
weight: 10e18,
|
|
209
|
-
newlyIssuedTokenCount: 0,
|
|
210
|
-
beneficiary: beneficiary,
|
|
211
|
-
hookMetadata: bytes(""),
|
|
212
|
-
payerMetadata: _buildPayMetadata(address(testHook), false, tierIds)
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
vm.prank(mockTerminalAddress);
|
|
216
|
-
testHook.afterPayRecordedWith(initialMint);
|
|
217
|
-
|
|
218
|
-
assertEq(
|
|
219
|
-
hookStore.numberOfPendingReservesFor(address(testHook), 1), 0, "no reserve obligation before default set"
|
|
190
|
+
// The new creation-time check prevents tiers with reserves but no beneficiary.
|
|
191
|
+
vm.expectRevert(
|
|
192
|
+
abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_MissingReserveBeneficiary.selector, 1)
|
|
220
193
|
);
|
|
221
|
-
assertEq(testHook.totalCashOutWeight(), 2 ether, "cash-out denominator initially matches sold NFTs");
|
|
222
|
-
|
|
223
|
-
JB721TierConfig[] memory activatingTier = new JB721TierConfig[](1);
|
|
224
|
-
activatingTier[0] = JB721TierConfig({
|
|
225
|
-
price: uint104(2 ether),
|
|
226
|
-
initialSupply: uint32(5),
|
|
227
|
-
votingUnits: 0,
|
|
228
|
-
reserveFrequency: uint16(1),
|
|
229
|
-
reserveBeneficiary: retroReserveBeneficiary,
|
|
230
|
-
encodedIPFSUri: bytes32(uint256(3)),
|
|
231
|
-
category: uint24(2),
|
|
232
|
-
discountPercent: 0,
|
|
233
|
-
flags: JB721TierConfigFlags({
|
|
234
|
-
allowOwnerMint: false,
|
|
235
|
-
useReserveBeneficiaryAsDefault: true,
|
|
236
|
-
transfersPausable: false,
|
|
237
|
-
useVotingUnits: false,
|
|
238
|
-
cantBeRemoved: false,
|
|
239
|
-
cantIncreaseDiscountPercent: false,
|
|
240
|
-
cantBuyWithCredits: false
|
|
241
|
-
}),
|
|
242
|
-
splitPercent: 0,
|
|
243
|
-
splits: new JBSplit[](0)
|
|
244
|
-
});
|
|
245
|
-
|
|
246
194
|
vm.prank(owner);
|
|
247
|
-
testHook.adjustTiers(
|
|
248
|
-
|
|
249
|
-
assertEq(
|
|
250
|
-
hookStore.defaultReserveBeneficiaryOf(address(testHook)),
|
|
251
|
-
retroReserveBeneficiary,
|
|
252
|
-
"new tier overwrites the hook-wide default reserve beneficiary"
|
|
253
|
-
);
|
|
254
|
-
assertEq(
|
|
255
|
-
hookStore.numberOfPendingReservesFor(address(testHook), 1),
|
|
256
|
-
1,
|
|
257
|
-
"historic mints on the older tier now retroactively create reserve debt"
|
|
258
|
-
);
|
|
259
|
-
assertEq(
|
|
260
|
-
testHook.totalCashOutWeight(),
|
|
261
|
-
3 ether,
|
|
262
|
-
"cash-out denominator dilutes existing holders before any new payment occurs"
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
testHook.mintPendingReservesFor(1, 1);
|
|
266
|
-
|
|
267
|
-
assertEq(
|
|
268
|
-
testHook.balanceOf(retroReserveBeneficiary), 1, "the new default beneficiary can mint retroactive reserves"
|
|
269
|
-
);
|
|
270
|
-
assertEq(hookStore.numberOfPendingReservesFor(address(testHook), 1), 0, "reserve debt is realized after mint");
|
|
195
|
+
testHook.adjustTiers(initialTier, new uint256[](0));
|
|
271
196
|
}
|
|
272
197
|
}
|
|
@@ -6,30 +6,18 @@ import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
|
|
|
6
6
|
import {IJB721TokenUriResolver} from "../../src/interfaces/IJB721TokenUriResolver.sol";
|
|
7
7
|
import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
|
|
8
8
|
|
|
9
|
-
contract
|
|
9
|
+
contract FutureTierPoC is UnitTestSetup {
|
|
10
10
|
function test_futureTierRemovalPersistsIntoNewTierAndBricksMint() external {
|
|
11
11
|
hook = _initHookDefaultTiers(0);
|
|
12
12
|
|
|
13
13
|
uint256[] memory futureTierIds = new uint256[](1);
|
|
14
14
|
futureTierIds[0] = 1;
|
|
15
15
|
|
|
16
|
+
// L-18 FIX: Removing a future (nonexistent) tier ID now reverts,
|
|
17
|
+
// preventing the "born removed" bug entirely.
|
|
16
18
|
vm.prank(owner);
|
|
19
|
+
vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_UnrecognizedTier.selector, 1));
|
|
17
20
|
hook.adjustTiers(new JB721TierConfig[](0), futureTierIds);
|
|
18
|
-
|
|
19
|
-
(JB721TierConfig[] memory tiersToAdd,) = _createTiers(defaultTierConfig, 1);
|
|
20
|
-
|
|
21
|
-
vm.prank(owner);
|
|
22
|
-
hook.adjustTiers(tiersToAdd, new uint256[](0));
|
|
23
|
-
|
|
24
|
-
assertTrue(hook.STORE().isTierRemoved(address(hook), 1), "future removal flag should persist");
|
|
25
|
-
|
|
26
|
-
uint16[] memory tierIds = new uint16[](1);
|
|
27
|
-
tierIds[0] = 1;
|
|
28
|
-
JB721TiersHookStore store = JB721TiersHookStore(address(hook.STORE()));
|
|
29
|
-
|
|
30
|
-
vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_TierRemoved.selector, 1));
|
|
31
|
-
vm.prank(address(hook));
|
|
32
|
-
store.recordMint(type(uint256).max, tierIds, false);
|
|
33
21
|
}
|
|
34
22
|
|
|
35
23
|
function test_futureTierUriCanBePoisonedBeforeTierExists() external {
|
|
@@ -8,7 +8,7 @@ import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
|
|
|
8
8
|
import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
|
|
9
9
|
import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
|
|
10
10
|
|
|
11
|
-
contract
|
|
11
|
+
contract Test_FutureTierRemoval is Test {
|
|
12
12
|
JB721TiersHookStore internal store;
|
|
13
13
|
|
|
14
14
|
function setUp() external {
|
|
@@ -21,21 +21,12 @@ contract Test_20260425CodexNemesisFutureTierRemoval is Test {
|
|
|
21
21
|
uint256[] memory firstIds = store.recordAddTiers(firstTier);
|
|
22
22
|
assertEq(firstIds[0], 1);
|
|
23
23
|
|
|
24
|
+
// Attempting to remove a future (nonexistent) tier ID should now revert
|
|
25
|
+
// thanks to the L-18 fix, preventing the "born removed" bug.
|
|
24
26
|
uint256[] memory futureIds = new uint256[](1);
|
|
25
27
|
futureIds[0] = 2;
|
|
28
|
+
vm.expectRevert(abi.encodeWithSignature("JB721TiersHookStore_UnrecognizedTier(uint256)", 2));
|
|
26
29
|
store.recordRemoveTierIds(futureIds);
|
|
27
|
-
assertTrue(store.isTierRemoved({hook: address(this), tierId: 2}));
|
|
28
|
-
|
|
29
|
-
JB721TierConfig[] memory secondTier = new JB721TierConfig[](1);
|
|
30
|
-
secondTier[0] = _tier(2);
|
|
31
|
-
uint256[] memory secondIds = store.recordAddTiers(secondTier);
|
|
32
|
-
assertEq(secondIds[0], 2);
|
|
33
|
-
assertTrue(store.isTierRemoved({hook: address(this), tierId: 2}));
|
|
34
|
-
|
|
35
|
-
uint16[] memory tierIds = new uint16[](1);
|
|
36
|
-
tierIds[0] = 2;
|
|
37
|
-
vm.expectRevert(abi.encodeWithSignature("JB721TiersHookStore_TierRemoved(uint256)", 2));
|
|
38
|
-
store.recordMint({amount: 1, tierIds: tierIds, isOwnerMint: false});
|
|
39
30
|
}
|
|
40
31
|
|
|
41
32
|
function _tier(uint24 category) internal pure returns (JB721TierConfig memory tier) {
|
|
@@ -0,0 +1,80 @@
|
|
|
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 {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
|
|
9
|
+
import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
|
|
10
|
+
|
|
11
|
+
/// @notice L-18: Prevent future tier pre-removal.
|
|
12
|
+
/// Removing a tier ID that does not yet exist should revert with `UnrecognizedTier`.
|
|
13
|
+
contract Pass12L18 is Test {
|
|
14
|
+
JB721TiersHookStore internal store;
|
|
15
|
+
|
|
16
|
+
function setUp() external {
|
|
17
|
+
store = new JB721TiersHookStore();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// @notice Removing a future tier ID (beyond maxTierIdOf) must revert.
|
|
21
|
+
function test_L18_fix_reverts_future_removal() external {
|
|
22
|
+
// Add 5 tiers so maxTierIdOf == 5.
|
|
23
|
+
JB721TierConfig[] memory tiers = new JB721TierConfig[](5);
|
|
24
|
+
for (uint256 i; i < 5; i++) {
|
|
25
|
+
tiers[i] = _tier(uint24(i + 1));
|
|
26
|
+
}
|
|
27
|
+
store.recordAddTiers(tiers);
|
|
28
|
+
assertEq(store.maxTierIdOf(address(this)), 5);
|
|
29
|
+
|
|
30
|
+
// Attempt to remove tier 10 (future) — should revert.
|
|
31
|
+
uint256[] memory futureTierIds = new uint256[](1);
|
|
32
|
+
futureTierIds[0] = 10;
|
|
33
|
+
vm.expectRevert(abi.encodeWithSignature("JB721TiersHookStore_UnrecognizedTier(uint256)", 10));
|
|
34
|
+
store.recordRemoveTierIds(futureTierIds);
|
|
35
|
+
|
|
36
|
+
// Attempt to remove tier 0 (invalid) — should also revert.
|
|
37
|
+
uint256[] memory zeroTierIds = new uint256[](1);
|
|
38
|
+
zeroTierIds[0] = 0;
|
|
39
|
+
vm.expectRevert(abi.encodeWithSignature("JB721TiersHookStore_UnrecognizedTier(uint256)", 0));
|
|
40
|
+
store.recordRemoveTierIds(zeroTierIds);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// @notice Removing an existing tier still works as before.
|
|
44
|
+
function test_L18_existing_tier_removal_works() external {
|
|
45
|
+
// Add 5 tiers.
|
|
46
|
+
JB721TierConfig[] memory tiers = new JB721TierConfig[](5);
|
|
47
|
+
for (uint256 i; i < 5; i++) {
|
|
48
|
+
tiers[i] = _tier(uint24(i + 1));
|
|
49
|
+
}
|
|
50
|
+
store.recordAddTiers(tiers);
|
|
51
|
+
|
|
52
|
+
// Remove tier 3 — should succeed.
|
|
53
|
+
uint256[] memory removeTierIds = new uint256[](1);
|
|
54
|
+
removeTierIds[0] = 3;
|
|
55
|
+
store.recordRemoveTierIds(removeTierIds);
|
|
56
|
+
assertTrue(store.isTierRemoved({hook: address(this), tierId: 3}));
|
|
57
|
+
|
|
58
|
+
// Other tiers should not be affected.
|
|
59
|
+
assertFalse(store.isTierRemoved({hook: address(this), tierId: 1}));
|
|
60
|
+
assertFalse(store.isTierRemoved({hook: address(this), tierId: 2}));
|
|
61
|
+
assertFalse(store.isTierRemoved({hook: address(this), tierId: 4}));
|
|
62
|
+
assertFalse(store.isTierRemoved({hook: address(this), tierId: 5}));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _tier(uint24 category) internal pure returns (JB721TierConfig memory tier) {
|
|
66
|
+
tier.price = 1;
|
|
67
|
+
tier.initialSupply = 10;
|
|
68
|
+
tier.category = category;
|
|
69
|
+
tier.flags = JB721TierConfigFlags({
|
|
70
|
+
allowOwnerMint: false,
|
|
71
|
+
useReserveBeneficiaryAsDefault: false,
|
|
72
|
+
transfersPausable: false,
|
|
73
|
+
useVotingUnits: false,
|
|
74
|
+
cantBeRemoved: false,
|
|
75
|
+
cantIncreaseDiscountPercent: false,
|
|
76
|
+
cantBuyWithCredits: false
|
|
77
|
+
});
|
|
78
|
+
tier.splits = new JBSplit[](0);
|
|
79
|
+
}
|
|
80
|
+
}
|
package/test/audit/{CodexPayCreditsBypassTierSplits.t.sol → PayCreditsBypassTierSplits.t.sol}
RENAMED
|
@@ -13,7 +13,7 @@ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
|
13
13
|
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
14
14
|
import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
|
|
15
15
|
|
|
16
|
-
contract
|
|
16
|
+
contract PayCreditsBypassTierSplits is UnitTestSetup {
|
|
17
17
|
address internal splitBeneficiary = makeAddr("splitBeneficiary");
|
|
18
18
|
|
|
19
19
|
function setUp() public override {
|
|
@@ -12,7 +12,6 @@ import {JBPayDataHookRulesetMetadata} from "../../src/structs/JBPayDataHookRules
|
|
|
12
12
|
import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
|
|
13
13
|
import {JB721InitTiersConfig} from "../../src/structs/JB721InitTiersConfig.sol";
|
|
14
14
|
import {JB721TiersHookFlags} from "../../src/structs/JB721TiersHookFlags.sol";
|
|
15
|
-
import {IJB721TiersHook} from "../../src/interfaces/IJB721TiersHook.sol";
|
|
16
15
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
17
16
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
18
17
|
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
@@ -24,7 +23,7 @@ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingCo
|
|
|
24
23
|
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
25
24
|
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
26
25
|
|
|
27
|
-
contract
|
|
26
|
+
contract MockProjects {
|
|
28
27
|
uint256 internal _count;
|
|
29
28
|
address internal _owner;
|
|
30
29
|
|
|
@@ -44,7 +43,7 @@ contract MockProjectsForNemesis {
|
|
|
44
43
|
fallback() external {}
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
contract
|
|
46
|
+
contract StrictController {
|
|
48
47
|
address internal immutable EXPECTED_CALLER;
|
|
49
48
|
|
|
50
49
|
error UnexpectedCaller(address caller);
|
|
@@ -62,7 +61,7 @@ contract StrictControllerForNemesis {
|
|
|
62
61
|
}
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
contract
|
|
64
|
+
contract Test_ProjectDeployerAuth is UnitTestSetup {
|
|
66
65
|
JB721TiersHookProjectDeployer internal projectDeployer;
|
|
67
66
|
address internal operator = address(0xBEEF);
|
|
68
67
|
uint256 internal testProjectId = 5;
|
|
@@ -70,9 +69,9 @@ contract Test_CodexNemesisProjectDeployerAuth is UnitTestSetup {
|
|
|
70
69
|
function setUp() public override {
|
|
71
70
|
super.setUp();
|
|
72
71
|
|
|
73
|
-
|
|
72
|
+
MockProjects projects = new MockProjects();
|
|
74
73
|
vm.etch(mockJBProjects, address(projects).code);
|
|
75
|
-
|
|
74
|
+
MockProjects(mockJBProjects).setup(testProjectId, owner);
|
|
76
75
|
|
|
77
76
|
vm.mockCall(mockJBDirectory, abi.encodeWithSelector(IJBDirectory.PROJECTS.selector), abi.encode(mockJBProjects));
|
|
78
77
|
|
|
@@ -85,12 +84,10 @@ contract Test_CodexNemesisProjectDeployerAuth is UnitTestSetup {
|
|
|
85
84
|
(JBDeploy721TiersHookConfig memory hookConfig, JBLaunchRulesetsConfig memory launchConfig) =
|
|
86
85
|
_launchConfig(testProjectId);
|
|
87
86
|
|
|
88
|
-
|
|
87
|
+
StrictController controller = new StrictController(owner);
|
|
89
88
|
|
|
90
89
|
vm.prank(owner);
|
|
91
|
-
vm.expectRevert(
|
|
92
|
-
abi.encodeWithSelector(StrictControllerForNemesis.UnexpectedCaller.selector, address(projectDeployer))
|
|
93
|
-
);
|
|
90
|
+
vm.expectRevert(abi.encodeWithSelector(StrictController.UnexpectedCaller.selector, address(projectDeployer)));
|
|
94
91
|
projectDeployer.launchRulesetsFor(
|
|
95
92
|
testProjectId, hookConfig, launchConfig, IJBController(address(controller)), bytes32(0)
|
|
96
93
|
);
|
|
@@ -100,12 +97,10 @@ contract Test_CodexNemesisProjectDeployerAuth is UnitTestSetup {
|
|
|
100
97
|
(JBDeploy721TiersHookConfig memory hookConfig, JBQueueRulesetsConfig memory queueConfig) =
|
|
101
98
|
_queueConfig(testProjectId);
|
|
102
99
|
|
|
103
|
-
|
|
100
|
+
StrictController controller = new StrictController(owner);
|
|
104
101
|
|
|
105
102
|
vm.prank(owner);
|
|
106
|
-
vm.expectRevert(
|
|
107
|
-
abi.encodeWithSelector(StrictControllerForNemesis.UnexpectedCaller.selector, address(projectDeployer))
|
|
108
|
-
);
|
|
103
|
+
vm.expectRevert(abi.encodeWithSelector(StrictController.UnexpectedCaller.selector, address(projectDeployer)));
|
|
109
104
|
projectDeployer.queueRulesetsOf(
|
|
110
105
|
testProjectId, hookConfig, queueConfig, IJBController(address(controller)), bytes32(0)
|
|
111
106
|
);
|
|
@@ -116,7 +111,7 @@ contract Test_CodexNemesisProjectDeployerAuth is UnitTestSetup {
|
|
|
116
111
|
_launchConfig(testProjectId);
|
|
117
112
|
|
|
118
113
|
address account = owner;
|
|
119
|
-
|
|
114
|
+
StrictController controller = new StrictController(owner);
|
|
120
115
|
|
|
121
116
|
// Grant LAUNCH_RULESETS and SET_TERMINALS, deny QUEUE_RULESETS.
|
|
122
117
|
vm.mockCall(
|
|
@@ -162,9 +157,7 @@ contract Test_CodexNemesisProjectDeployerAuth is UnitTestSetup {
|
|
|
162
157
|
// The permission check passes with LAUNCH_RULESETS. The call proceeds to the controller, which reverts
|
|
163
158
|
// because it sees the deployer contract as the caller rather than the original operator.
|
|
164
159
|
vm.prank(operator);
|
|
165
|
-
vm.expectRevert(
|
|
166
|
-
abi.encodeWithSelector(StrictControllerForNemesis.UnexpectedCaller.selector, address(projectDeployer))
|
|
167
|
-
);
|
|
160
|
+
vm.expectRevert(abi.encodeWithSelector(StrictController.UnexpectedCaller.selector, address(projectDeployer)));
|
|
168
161
|
projectDeployer.launchRulesetsFor(
|
|
169
162
|
testProjectId, hookConfig, launchConfig, IJBController(address(controller)), bytes32(0)
|
|
170
163
|
);
|
|
@@ -3,11 +3,11 @@ pragma solidity 0.8.28;
|
|
|
3
3
|
|
|
4
4
|
import "../utils/UnitTestSetup.sol";
|
|
5
5
|
|
|
6
|
-
import {IJB721TiersHookStore} from "../../src/interfaces/IJB721TiersHookStore.sol";
|
|
7
6
|
import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
|
|
7
|
+
import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
|
|
8
8
|
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
9
9
|
|
|
10
|
-
contract
|
|
10
|
+
contract RepoFindings is UnitTestSetup {
|
|
11
11
|
address payable internal splitBeneficiary = payable(makeAddr("splitBeneficiary"));
|
|
12
12
|
|
|
13
13
|
function _payMetadata(
|
|
@@ -157,6 +157,8 @@ contract CodexNemesisRepoFindings is UnitTestSetup {
|
|
|
157
157
|
);
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/// @notice Creating a tier with reserveFrequency > 0 and no beneficiary (tier-specific or default)
|
|
161
|
+
/// is now rejected at creation time, preventing the retroactive dilution bug.
|
|
160
162
|
function test_new_default_reserve_beneficiary_retroactively_dilutes_existing_tiers() public {
|
|
161
163
|
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
162
164
|
|
|
@@ -183,88 +185,11 @@ contract CodexNemesisRepoFindings is UnitTestSetup {
|
|
|
183
185
|
splits: new JBSplit[](0)
|
|
184
186
|
});
|
|
185
187
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
mockAndExpect(
|
|
190
|
-
mockJBDirectory,
|
|
191
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
192
|
-
abi.encode(true)
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
uint16[] memory tierIdsToMint = new uint16[](3);
|
|
196
|
-
tierIdsToMint[0] = 1;
|
|
197
|
-
tierIdsToMint[1] = 1;
|
|
198
|
-
tierIdsToMint[2] = 1;
|
|
199
|
-
bytes memory payerMetadata = _payMetadata(address(testHook), false, tierIdsToMint);
|
|
200
|
-
|
|
201
|
-
vm.prank(mockTerminalAddress);
|
|
202
|
-
testHook.afterPayRecordedWith(
|
|
203
|
-
JBAfterPayRecordedContext({
|
|
204
|
-
payer: beneficiary,
|
|
205
|
-
projectId: projectId,
|
|
206
|
-
rulesetId: 0,
|
|
207
|
-
amount: _nativeTokenAmount(3 ether),
|
|
208
|
-
forwardedAmount: _nativeTokenAmount(0),
|
|
209
|
-
weight: 10e18,
|
|
210
|
-
newlyIssuedTokenCount: 0,
|
|
211
|
-
beneficiary: beneficiary,
|
|
212
|
-
hookMetadata: bytes(""),
|
|
213
|
-
payerMetadata: payerMetadata
|
|
214
|
-
})
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
assertEq(testHook.totalCashOutWeight(), 3 ether, "denominator initially reflects only sold NFTs");
|
|
218
|
-
assertEq(
|
|
219
|
-
testHook.STORE().numberOfPendingReservesFor(address(testHook), 1),
|
|
220
|
-
0,
|
|
221
|
-
"without a reserve beneficiary the sold tier has no pending reserves"
|
|
188
|
+
// The new creation-time check prevents tiers with reserves but no beneficiary.
|
|
189
|
+
vm.expectRevert(
|
|
190
|
+
abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_MissingReserveBeneficiary.selector, 1)
|
|
222
191
|
);
|
|
223
|
-
|
|
224
|
-
JB721TierConfig[] memory defaultingTier = new JB721TierConfig[](1);
|
|
225
|
-
defaultingTier[0] = JB721TierConfig({
|
|
226
|
-
price: 2 ether,
|
|
227
|
-
initialSupply: 10,
|
|
228
|
-
votingUnits: 0,
|
|
229
|
-
reserveFrequency: 1,
|
|
230
|
-
reserveBeneficiary: owner,
|
|
231
|
-
encodedIPFSUri: bytes32(uint256(0x2222)),
|
|
232
|
-
category: 2,
|
|
233
|
-
discountPercent: 0,
|
|
234
|
-
flags: JB721TierConfigFlags({
|
|
235
|
-
allowOwnerMint: false,
|
|
236
|
-
useReserveBeneficiaryAsDefault: true,
|
|
237
|
-
transfersPausable: false,
|
|
238
|
-
useVotingUnits: false,
|
|
239
|
-
cantBeRemoved: false,
|
|
240
|
-
cantIncreaseDiscountPercent: false,
|
|
241
|
-
cantBuyWithCredits: false
|
|
242
|
-
}),
|
|
243
|
-
splitPercent: 0,
|
|
244
|
-
splits: new JBSplit[](0)
|
|
245
|
-
});
|
|
246
|
-
|
|
247
192
|
vm.prank(owner);
|
|
248
|
-
testHook.adjustTiers(
|
|
249
|
-
|
|
250
|
-
assertEq(
|
|
251
|
-
testHook.STORE().reserveBeneficiaryOf(address(testHook), 1),
|
|
252
|
-
owner,
|
|
253
|
-
"the new default reserve beneficiary retroactively applies to the older sold tier"
|
|
254
|
-
);
|
|
255
|
-
assertEq(
|
|
256
|
-
testHook.STORE().numberOfPendingReservesFor(address(testHook), 1),
|
|
257
|
-
2,
|
|
258
|
-
"the older tier now reports newly created pending reserves from past sales"
|
|
259
|
-
);
|
|
260
|
-
assertEq(
|
|
261
|
-
testHook.totalCashOutWeight(),
|
|
262
|
-
5 ether,
|
|
263
|
-
"cash-out denominator is diluted by retroactively created reserves on the existing tier"
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
testHook.mintPendingReservesFor(1, 2);
|
|
267
|
-
|
|
268
|
-
assertEq(testHook.balanceOf(owner), 2, "the owner can mint those retroactive reserve NFTs to themselves");
|
|
193
|
+
testHook.adjustTiers(initialTier, new uint256[](0));
|
|
269
194
|
}
|
|
270
195
|
}
|
|
@@ -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,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 {
|
|
@@ -360,55 +360,39 @@ contract Test_mintFor_mintReservesFor_Unit is UnitTestSetup {
|
|
|
360
360
|
}
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
+
/// @notice Tiers with reserveFrequency > 0 and no beneficiary are now rejected at creation time.
|
|
364
|
+
/// This test verifies that the creation-time check prevents such tiers from existing.
|
|
363
365
|
function test_numberOfPendingReservesFor_noReservesIfNoBeneficiarySet() public {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
hook.test_store().ForTest_setReservesMintedFor(address(hook), i + 1, reservedMinted);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Fetch the stored tiers.
|
|
398
|
-
JB721Tier[] memory storedTiers = hook.test_store().tiersOf(address(hook), new uint256[](0), false, 0, 10);
|
|
399
|
-
|
|
400
|
-
// Check: did the reserve frequency default to 0 for all tiers?
|
|
401
|
-
for (uint256 i; i < 10; i++) {
|
|
402
|
-
assertEq(storedTiers[i].reserveFrequency, 0, "Reserve frequency should be zero (no beneficiary set).");
|
|
403
|
-
}
|
|
404
|
-
// Check: are we sure there are no pending reserves for all tiers?
|
|
405
|
-
for (uint256 i; i < 10; i++) {
|
|
406
|
-
assertEq(
|
|
407
|
-
hook.test_store().numberOfPendingReservesFor(address(hook), i + 1),
|
|
408
|
-
0,
|
|
409
|
-
"There should not be any pending reserves (no beneficiary set)."
|
|
410
|
-
);
|
|
411
|
-
}
|
|
366
|
+
// Build tier configs with reserveFrequency > 0 but reserveBeneficiary == address(0).
|
|
367
|
+
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
368
|
+
tierConfigs[0] = JB721TierConfig({
|
|
369
|
+
price: uint104(10),
|
|
370
|
+
initialSupply: uint32(200),
|
|
371
|
+
votingUnits: 0,
|
|
372
|
+
reserveFrequency: uint16(9),
|
|
373
|
+
reserveBeneficiary: address(0),
|
|
374
|
+
encodedIPFSUri: bytes32(uint256(1)),
|
|
375
|
+
category: uint24(100),
|
|
376
|
+
discountPercent: 0,
|
|
377
|
+
flags: JB721TierConfigFlags({
|
|
378
|
+
allowOwnerMint: false,
|
|
379
|
+
useReserveBeneficiaryAsDefault: false,
|
|
380
|
+
transfersPausable: false,
|
|
381
|
+
useVotingUnits: true,
|
|
382
|
+
cantBeRemoved: false,
|
|
383
|
+
cantIncreaseDiscountPercent: false,
|
|
384
|
+
cantBuyWithCredits: false
|
|
385
|
+
}),
|
|
386
|
+
splitPercent: 0,
|
|
387
|
+
splits: new JBSplit[](0)
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// The creation-time check should reject a tier with reserves but no beneficiary.
|
|
391
|
+
ForTest_JB721TiersHookStore hookStore = new ForTest_JB721TiersHookStore();
|
|
392
|
+
vm.expectRevert(
|
|
393
|
+
abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_MissingReserveBeneficiary.selector, 1)
|
|
394
|
+
);
|
|
395
|
+
hookStore.recordAddTiers(tierConfigs);
|
|
412
396
|
}
|
|
413
397
|
|
|
414
398
|
function test_mintFor_mintArrayOfTiers() public {
|
|
@@ -1,161 +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 "../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
|
-
|
|
10
|
-
contract CodexRetroactiveReserveBeneficiaryDilution is UnitTestSetup {
|
|
11
|
-
function _buildPayMetadata(address hookAddress, uint16[] memory tierIdsToMint)
|
|
12
|
-
internal
|
|
13
|
-
view
|
|
14
|
-
returns (bytes memory)
|
|
15
|
-
{
|
|
16
|
-
bytes[] memory data = new bytes[](1);
|
|
17
|
-
data[0] = abi.encode(true, tierIdsToMint);
|
|
18
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
19
|
-
ids[0] = metadataHelper.getId("pay", hookAddress);
|
|
20
|
-
return metadataHelper.createMetadata(ids, data);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function _afterPayContext(
|
|
24
|
-
address hookAddress,
|
|
25
|
-
uint256 amountValue,
|
|
26
|
-
uint16[] memory tierIdsToMint
|
|
27
|
-
)
|
|
28
|
-
internal
|
|
29
|
-
view
|
|
30
|
-
returns (JBAfterPayRecordedContext memory)
|
|
31
|
-
{
|
|
32
|
-
return JBAfterPayRecordedContext({
|
|
33
|
-
payer: beneficiary,
|
|
34
|
-
projectId: projectId,
|
|
35
|
-
rulesetId: 0,
|
|
36
|
-
amount: JBTokenAmount({
|
|
37
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
38
|
-
value: amountValue,
|
|
39
|
-
decimals: 18,
|
|
40
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
41
|
-
}),
|
|
42
|
-
forwardedAmount: JBTokenAmount({
|
|
43
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
44
|
-
value: 0,
|
|
45
|
-
decimals: 18,
|
|
46
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
47
|
-
}),
|
|
48
|
-
weight: 10e18,
|
|
49
|
-
newlyIssuedTokenCount: 0,
|
|
50
|
-
beneficiary: beneficiary,
|
|
51
|
-
hookMetadata: "",
|
|
52
|
-
payerMetadata: _buildPayMetadata(hookAddress, tierIdsToMint)
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function test_adjustTier_can_retroactively_create_reserves_for_existing_supply() public {
|
|
57
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
58
|
-
|
|
59
|
-
JB721TierConfig[] memory tier1 = new JB721TierConfig[](1);
|
|
60
|
-
tier1[0] = JB721TierConfig({
|
|
61
|
-
price: 1 ether,
|
|
62
|
-
initialSupply: 100,
|
|
63
|
-
votingUnits: 0,
|
|
64
|
-
reserveFrequency: 2,
|
|
65
|
-
reserveBeneficiary: address(0),
|
|
66
|
-
encodedIPFSUri: bytes32(uint256(0x1234)),
|
|
67
|
-
category: 1,
|
|
68
|
-
discountPercent: 0,
|
|
69
|
-
flags: JB721TierConfigFlags({
|
|
70
|
-
allowOwnerMint: false,
|
|
71
|
-
useReserveBeneficiaryAsDefault: false,
|
|
72
|
-
transfersPausable: false,
|
|
73
|
-
useVotingUnits: false,
|
|
74
|
-
cantBeRemoved: false,
|
|
75
|
-
cantIncreaseDiscountPercent: false,
|
|
76
|
-
cantBuyWithCredits: false
|
|
77
|
-
}),
|
|
78
|
-
splitPercent: 0,
|
|
79
|
-
splits: new JBSplit[](0)
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
vm.prank(owner);
|
|
83
|
-
testHook.adjustTiers(tier1, new uint256[](0));
|
|
84
|
-
|
|
85
|
-
assertEq(
|
|
86
|
-
testHook.STORE().numberOfPendingReservesFor(address(testHook), 1),
|
|
87
|
-
0,
|
|
88
|
-
"no beneficiary means no reserves are pending"
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
vm.mockCall(
|
|
92
|
-
mockJBDirectory,
|
|
93
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
94
|
-
abi.encode(true)
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
uint16[] memory mintIds = new uint16[](3);
|
|
98
|
-
mintIds[0] = 1;
|
|
99
|
-
mintIds[1] = 1;
|
|
100
|
-
mintIds[2] = 1;
|
|
101
|
-
|
|
102
|
-
JBAfterPayRecordedContext memory payContext = _afterPayContext(address(testHook), 3 ether, mintIds);
|
|
103
|
-
vm.prank(mockTerminalAddress);
|
|
104
|
-
testHook.afterPayRecordedWith(payContext);
|
|
105
|
-
|
|
106
|
-
assertEq(testHook.balanceOf(beneficiary), 3, "beneficiary should own the three paid NFTs");
|
|
107
|
-
assertEq(testHook.totalCashOutWeight(), 3 ether, "denominator initially reflects only sold NFTs");
|
|
108
|
-
|
|
109
|
-
JB721TierConfig[] memory tier2 = new JB721TierConfig[](1);
|
|
110
|
-
tier2[0] = JB721TierConfig({
|
|
111
|
-
price: 2 ether,
|
|
112
|
-
initialSupply: 100,
|
|
113
|
-
votingUnits: 0,
|
|
114
|
-
reserveFrequency: 1,
|
|
115
|
-
reserveBeneficiary: owner,
|
|
116
|
-
encodedIPFSUri: bytes32(uint256(0x5678)),
|
|
117
|
-
category: 2,
|
|
118
|
-
discountPercent: 0,
|
|
119
|
-
flags: JB721TierConfigFlags({
|
|
120
|
-
allowOwnerMint: false,
|
|
121
|
-
useReserveBeneficiaryAsDefault: true,
|
|
122
|
-
transfersPausable: false,
|
|
123
|
-
useVotingUnits: false,
|
|
124
|
-
cantBeRemoved: false,
|
|
125
|
-
cantIncreaseDiscountPercent: false,
|
|
126
|
-
cantBuyWithCredits: false
|
|
127
|
-
}),
|
|
128
|
-
splitPercent: 0,
|
|
129
|
-
splits: new JBSplit[](0)
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
vm.prank(owner);
|
|
133
|
-
testHook.adjustTiers(tier2, new uint256[](0));
|
|
134
|
-
|
|
135
|
-
assertEq(
|
|
136
|
-
testHook.STORE().reserveBeneficiaryOf(address(testHook), 1),
|
|
137
|
-
owner,
|
|
138
|
-
"new default beneficiary retroactively applies to the old tier"
|
|
139
|
-
);
|
|
140
|
-
assertEq(
|
|
141
|
-
testHook.STORE().numberOfPendingReservesFor(address(testHook), 1),
|
|
142
|
-
2,
|
|
143
|
-
"historical sales now generate retroactive pending reserves"
|
|
144
|
-
);
|
|
145
|
-
assertEq(
|
|
146
|
-
testHook.totalCashOutWeight(),
|
|
147
|
-
5 ether,
|
|
148
|
-
"existing holders are diluted before any new payment enters the system"
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
vm.prank(address(0xBEEF));
|
|
152
|
-
testHook.mintPendingReservesFor(1, 2);
|
|
153
|
-
|
|
154
|
-
assertEq(testHook.balanceOf(owner), 2, "retroactive reserves mint directly to the new default beneficiary");
|
|
155
|
-
assertEq(
|
|
156
|
-
testHook.STORE().numberOfPendingReservesFor(address(testHook), 1),
|
|
157
|
-
0,
|
|
158
|
-
"all retroactive reserve entitlement can be extracted"
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
}
|