@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
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);
|
|
@@ -976,6 +977,15 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
976
977
|
revert JB721TiersHookStore_DeadlockedReserve();
|
|
977
978
|
}
|
|
978
979
|
|
|
980
|
+
// A tier with reserves must have a beneficiary — either tier-specific or a previously set default.
|
|
981
|
+
// Without one, minted reserves would be sent to address(0).
|
|
982
|
+
if (
|
|
983
|
+
tierToAdd.reserveFrequency > 0 && tierToAdd.reserveBeneficiary == address(0)
|
|
984
|
+
&& defaultReserveBeneficiaryOf[msg.sender] == address(0)
|
|
985
|
+
) {
|
|
986
|
+
revert JB721TiersHookStore_MissingReserveBeneficiary(tierId);
|
|
987
|
+
}
|
|
988
|
+
|
|
979
989
|
// Store the tier with that ID.
|
|
980
990
|
_storedTierOf[msg.sender][tierId] = JBStored721Tier({
|
|
981
991
|
price: uint104(tierToAdd.price),
|
|
@@ -1219,7 +1229,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1219
1229
|
if (storedTier.remainingSupply == 0) revert JB721TiersHookStore_InsufficientSupplyRemaining(tierId);
|
|
1220
1230
|
|
|
1221
1231
|
// Mint the 721 — decrement remaining supply first so the reserve check below
|
|
1222
|
-
// sees the post-mint
|
|
1232
|
+
// sees the correct post-mint non-reserve-mint count.
|
|
1223
1233
|
unchecked {
|
|
1224
1234
|
// Keep a reference to its token ID.
|
|
1225
1235
|
tokenIds[i] = _generateTokenId({
|
|
@@ -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
|
{
|
|
@@ -275,15 +275,15 @@ contract TestVotingUnitsLifecycle is UnitTestSetup {
|
|
|
275
275
|
address(testHook),
|
|
276
276
|
2,
|
|
277
277
|
JBStored721Tier({
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
278
|
+
price: uint104(20),
|
|
279
|
+
remainingSupply: uint32(100),
|
|
280
|
+
initialSupply: uint32(100),
|
|
281
|
+
reserveFrequency: uint16(0),
|
|
282
|
+
category: uint24(100),
|
|
283
|
+
discountPercent: uint8(0),
|
|
284
|
+
packedBools: testHook.test_store().ForTest_packBools(true, false, false, false, false, false),
|
|
285
|
+
splitPercent: 0
|
|
286
|
+
})
|
|
287
287
|
);
|
|
288
288
|
// Clear tier 2's custom voting units (so it falls back to price).
|
|
289
289
|
testHook.test_store().ForTest_setTierVotingUnits(address(testHook), 2, 0);
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
contract CodexNemesisReserveSellout is Test {
|
|
13
|
+
JB721TiersHookStore internal store;
|
|
14
|
+
|
|
15
|
+
function setUp() public {
|
|
16
|
+
store = new JB721TiersHookStore();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// @notice Verifies that a paid mint cannot consume the last slot when it is reserved.
|
|
20
|
+
/// @dev Previously this test demonstrated the bug (paid mint succeeded). Now it confirms the fix.
|
|
21
|
+
function test_paidMintCannotConsumeReservedFinalSlot() public {
|
|
22
|
+
JB721TierConfig[] memory tiers = new JB721TierConfig[](1);
|
|
23
|
+
tiers[0] = JB721TierConfig({
|
|
24
|
+
price: 1 ether,
|
|
25
|
+
initialSupply: 2,
|
|
26
|
+
votingUnits: 0,
|
|
27
|
+
reserveFrequency: 1,
|
|
28
|
+
reserveBeneficiary: address(0xBEEF),
|
|
29
|
+
encodedIPFSUri: bytes32(0),
|
|
30
|
+
category: 0,
|
|
31
|
+
discountPercent: 0,
|
|
32
|
+
flags: JB721TierConfigFlags({
|
|
33
|
+
allowOwnerMint: false,
|
|
34
|
+
useReserveBeneficiaryAsDefault: false,
|
|
35
|
+
transfersPausable: false,
|
|
36
|
+
useVotingUnits: false,
|
|
37
|
+
cantBeRemoved: false,
|
|
38
|
+
cantIncreaseDiscountPercent: false,
|
|
39
|
+
cantBuyWithCredits: false
|
|
40
|
+
}),
|
|
41
|
+
splitPercent: 0,
|
|
42
|
+
splits: new JBSplit[](0)
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
store.recordAddTiers(tiers);
|
|
46
|
+
|
|
47
|
+
uint16[] memory tierIds = new uint16[](1);
|
|
48
|
+
tierIds[0] = 1;
|
|
49
|
+
|
|
50
|
+
store.recordMint({amount: 1 ether, tierIds: tierIds, isOwnerMint: false});
|
|
51
|
+
|
|
52
|
+
JB721Tier memory tier = store.tierOf(address(this), 1, false);
|
|
53
|
+
assertEq(tier.remainingSupply, 1, "one paid mint leaves one slot");
|
|
54
|
+
assertEq(store.numberOfPendingReservesFor(address(this), 1), 1, "one reserve is pending");
|
|
55
|
+
|
|
56
|
+
// With the fix, the second paid mint reverts because the remaining slot is reserved.
|
|
57
|
+
vm.expectRevert(
|
|
58
|
+
abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientSupplyRemaining.selector, 1)
|
|
59
|
+
);
|
|
60
|
+
store.recordMint({amount: 1 ether, tierIds: tierIds, isOwnerMint: false});
|
|
61
|
+
|
|
62
|
+
// Reserve beneficiary can still claim their entitled mint.
|
|
63
|
+
store.recordMintReservesFor({tierId: 1, count: 1});
|
|
64
|
+
assertEq(store.numberOfReservesMintedFor(address(this), 1), 1, "reserve beneficiary got their token");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -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,7 +6,7 @@ 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
|
|
|
@@ -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 {
|
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
|
}
|