@bananapus/721-hook-v6 0.0.42 → 0.0.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/foundry.lock +1 -7
- package/foundry.toml +1 -1
- package/package.json +20 -9
- package/script/Deploy.s.sol +2 -2
- package/src/JB721Checkpoints.sol +61 -19
- package/src/JB721CheckpointsDeployer.sol +10 -5
- package/src/JB721TiersHook.sol +66 -53
- package/src/JB721TiersHookDeployer.sol +8 -5
- package/src/JB721TiersHookProjectDeployer.sol +87 -46
- package/src/JB721TiersHookStore.sol +137 -107
- package/src/abstract/JB721Hook.sol +8 -6
- package/src/interfaces/IJB721Checkpoints.sol +21 -14
- package/src/interfaces/IJB721CheckpointsDeployer.sol +7 -3
- package/src/interfaces/IJB721TiersHook.sol +3 -3
- package/src/interfaces/IJB721TiersHookProjectDeployer.sol +4 -2
- package/src/interfaces/IJB721TiersHookStore.sol +11 -11
- package/src/libraries/JB721TiersHookLib.sol +1 -1
- package/src/structs/JB721TiersHookFlags.sol +1 -1
- package/src/structs/JBPayDataHookRulesetMetadata.sol +1 -1
- package/test/utils/AccessJBLib.sol +49 -0
- package/test/utils/ForTest_JB721TiersHook.sol +246 -0
- package/test/utils/TestBaseWorkflow.sol +213 -0
- package/test/utils/UnitTestSetup.sol +805 -0
- package/.gas-snapshot +0 -152
- package/ADMINISTRATION.md +0 -87
- package/ARCHITECTURE.md +0 -98
- package/AUDIT_INSTRUCTIONS.md +0 -77
- package/RISKS.md +0 -118
- package/SKILLS.md +0 -43
- package/STYLE_GUIDE.md +0 -610
- package/USER_JOURNEYS.md +0 -121
- package/assets/findings/nana-721-hook-v6-pashov-ai-audit-report-20260330-091257.md +0 -83
- package/slither-ci.config.json +0 -10
- package/test/721HookAttacks.t.sol +0 -408
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +0 -985
- package/test/Fork.t.sol +0 -2346
- package/test/TestAuditGaps.sol +0 -1075
- package/test/TestCheckpoints.t.sol +0 -341
- package/test/TestSafeTransferReentrancy.t.sol +0 -305
- package/test/TestVotingUnitsLifecycle.t.sol +0 -313
- package/test/audit/AuditRegressions.t.sol +0 -83
- package/test/audit/CodexNemesisReserveSellout.t.sol +0 -66
- package/test/audit/CrossCurrencySplitNoPrices.t.sol +0 -123
- package/test/audit/FreshAudit.t.sol +0 -197
- package/test/audit/FutureTierPoC.t.sol +0 -39
- package/test/audit/FutureTierRemoval.t.sol +0 -47
- package/test/audit/Pass12L18.t.sol +0 -80
- package/test/audit/PayCreditsBypassTierSplits.t.sol +0 -200
- package/test/audit/ProjectDeployerAuth.t.sol +0 -266
- package/test/audit/RepoFindings.t.sol +0 -195
- package/test/audit/ReserveActivation.t.sol +0 -87
- package/test/audit/ReserveSlotProtection.t.sol +0 -273
- package/test/audit/RetroactiveReserveBeneficiaryDilution.t.sol +0 -149
- package/test/audit/SameCurrencyDecimalMismatch.t.sol +0 -249
- package/test/audit/SplitCreditsMismatch.t.sol +0 -219
- package/test/audit/SplitFailureRedistribution.t.sol +0 -143
- package/test/audit/USDTVoidReturnCompat.t.sol +0 -301
- package/test/fork/ERC20CashOutFork.t.sol +0 -633
- package/test/fork/ERC20TierSplitFork.t.sol +0 -596
- package/test/fork/IssueTokensForSplitsFork.t.sol +0 -516
- package/test/invariants/TierLifecycleInvariant.t.sol +0 -188
- package/test/invariants/TieredHookStoreInvariant.t.sol +0 -86
- package/test/invariants/handlers/TierLifecycleHandler.sol +0 -300
- package/test/invariants/handlers/TierStoreHandler.sol +0 -165
- package/test/regression/BrokenTerminalDoesNotDos.t.sol +0 -277
- package/test/regression/CacheTierLookup.t.sol +0 -190
- package/test/regression/ProjectDeployerRulesets.t.sol +0 -358
- package/test/regression/ReserveBeneficiaryOverwrite.t.sol +0 -155
- package/test/regression/SplitDistributionBugs.t.sol +0 -751
- package/test/regression/SplitNoBeneficiary.t.sol +0 -140
- package/test/unit/AuditFixes_Unit.t.sol +0 -624
- package/test/unit/JB721CheckpointsDeployer_AccessControl.t.sol +0 -116
- package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +0 -144
- package/test/unit/JBBitmap.t.sol +0 -170
- package/test/unit/JBIpfsDecoder.t.sol +0 -136
- package/test/unit/TierSupplyReserveCheck.t.sol +0 -221
- package/test/unit/adjustTier_Unit.t.sol +0 -1942
- package/test/unit/deployer_Unit.t.sol +0 -114
- package/test/unit/getters_constructor_Unit.t.sol +0 -593
- package/test/unit/mintFor_mintReservesFor_Unit.t.sol +0 -452
- package/test/unit/pay_CrossCurrency_Unit.t.sol +0 -530
- package/test/unit/pay_Unit.t.sol +0 -1661
- package/test/unit/redeem_Unit.t.sol +0 -473
- package/test/unit/relayBeneficiary_Unit.t.sol +0 -182
- package/test/unit/splitHookDistribution_Unit.t.sol +0 -604
- package/test/unit/tierSplitRouting_Unit.t.sol +0 -757
|
@@ -1,273 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,149 +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
|
-
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
|
-
}
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.28;
|
|
3
|
-
|
|
4
|
-
import {UnitTestSetup} from "../utils/UnitTestSetup.sol";
|
|
5
|
-
import {IJB721TokenUriResolver} from "../../src/interfaces/IJB721TokenUriResolver.sol";
|
|
6
|
-
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
7
|
-
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
8
|
-
import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
|
|
9
|
-
import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
|
|
10
|
-
import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
|
|
11
|
-
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
12
|
-
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
13
|
-
import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
|
|
14
|
-
import {JB721TiersHook} from "../../src/JB721TiersHook.sol";
|
|
15
|
-
import {JB721CheckpointsDeployer} from "../../src/JB721CheckpointsDeployer.sol";
|
|
16
|
-
import {IJB721CheckpointsDeployer} from "../../src/interfaces/IJB721CheckpointsDeployer.sol";
|
|
17
|
-
import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
|
|
18
|
-
import {JB721InitTiersConfig} from "../../src/structs/JB721InitTiersConfig.sol";
|
|
19
|
-
import {JB721TiersHookFlags} from "../../src/structs/JB721TiersHookFlags.sol";
|
|
20
|
-
|
|
21
|
-
/// @notice Regression test for: same-currency decimal mismatch in split forwarding.
|
|
22
|
-
/// @dev When pricing decimals differ from payment decimals but the currency is the same,
|
|
23
|
-
/// `convertAndCapSplitAmounts` must rescale split amounts before comparing to `amountValue`.
|
|
24
|
-
/// Without the fix, split amounts stay in pricing decimals (e.g. 18), the cap comparison uses
|
|
25
|
-
/// payment decimals (e.g. 6), and the cap clips the split to 100% of the payment.
|
|
26
|
-
contract SameCurrencyDecimalMismatch is UnitTestSetup {
|
|
27
|
-
// Shared constants.
|
|
28
|
-
address constant MOCK_TOKEN = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
|
|
29
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
30
|
-
uint32 constant CURRENCY = uint32(uint160(MOCK_TOKEN));
|
|
31
|
-
|
|
32
|
-
/// @notice Prove that a 50% split with same currency but different decimals (pricing=18, payment=6)
|
|
33
|
-
/// correctly forwards ~50% of the payment, not 100%.
|
|
34
|
-
function test_sameCurrency_differentDecimals_splitAmountScaledCorrectly() public {
|
|
35
|
-
// Deploy hook with PRICES=address(0), tier priced at 1e18 (18-decimal), 50% split.
|
|
36
|
-
JB721TiersHook testHook;
|
|
37
|
-
{
|
|
38
|
-
JB721TiersHook origin = new JB721TiersHook(
|
|
39
|
-
IJBDirectory(mockJBDirectory),
|
|
40
|
-
IJBPermissions(mockJBPermissions),
|
|
41
|
-
IJBPrices(address(0)),
|
|
42
|
-
IJBRulesets(mockJBRulesets),
|
|
43
|
-
store,
|
|
44
|
-
IJBSplits(mockJBSplits),
|
|
45
|
-
IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
|
|
46
|
-
trustedForwarder
|
|
47
|
-
);
|
|
48
|
-
address hookAddr = makeAddr("hook18to6");
|
|
49
|
-
vm.etch(hookAddr, address(origin).code);
|
|
50
|
-
testHook = JB721TiersHook(hookAddr);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
{
|
|
54
|
-
(JB721TierConfig[] memory tierConfigs,) = _createTiers(defaultTierConfig, 1);
|
|
55
|
-
tierConfigs[0].price = 1e18;
|
|
56
|
-
tierConfigs[0].splitPercent = 500_000_000; // 50%.
|
|
57
|
-
testHook.initialize(
|
|
58
|
-
projectId,
|
|
59
|
-
name,
|
|
60
|
-
symbol,
|
|
61
|
-
baseUri,
|
|
62
|
-
IJB721TokenUriResolver(mockTokenUriResolver),
|
|
63
|
-
contractUri,
|
|
64
|
-
JB721InitTiersConfig({tiers: tierConfigs, currency: CURRENCY, decimals: 18}),
|
|
65
|
-
JB721TiersHookFlags({
|
|
66
|
-
preventOverspending: false,
|
|
67
|
-
issueTokensForSplits: false,
|
|
68
|
-
noNewTiersWithReserves: false,
|
|
69
|
-
noNewTiersWithVotes: false,
|
|
70
|
-
noNewTiersWithOwnerMinting: false
|
|
71
|
-
})
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Build payer metadata requesting tier 1.
|
|
76
|
-
bytes memory payerMetadata;
|
|
77
|
-
{
|
|
78
|
-
uint16[] memory tierIdsToMint = new uint16[](1);
|
|
79
|
-
tierIdsToMint[0] = 1;
|
|
80
|
-
bytes[] memory data = new bytes[](1);
|
|
81
|
-
data[0] = abi.encode(true, tierIdsToMint);
|
|
82
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
83
|
-
ids[0] = metadataHelper.getId("pay", testHook.METADATA_ID_TARGET());
|
|
84
|
-
payerMetadata = metadataHelper.createMetadata(ids, data);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Pay 1.0 token reported as 6 decimals (value = 1e6). Same currency, different decimals.
|
|
88
|
-
(uint256 weight, JBPayHookSpecification[] memory hookSpecs) = testHook.beforePayRecordedWith(
|
|
89
|
-
JBBeforePayRecordedContext({
|
|
90
|
-
terminal: mockTerminalAddress,
|
|
91
|
-
payer: beneficiary,
|
|
92
|
-
amount: JBTokenAmount({token: MOCK_TOKEN, value: 1e6, decimals: 6, currency: CURRENCY}),
|
|
93
|
-
projectId: projectId,
|
|
94
|
-
rulesetId: 0,
|
|
95
|
-
beneficiary: beneficiary,
|
|
96
|
-
weight: 10e18,
|
|
97
|
-
reservedPercent: 0,
|
|
98
|
-
metadata: payerMetadata
|
|
99
|
-
})
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
// Without the fix: split amount (5e17 in 18-decimal pricing) is compared to amountValue (1e6),
|
|
103
|
-
// causing the cap to clip it to 1e6 (100% of payment) and weight becomes 0.
|
|
104
|
-
// With the fix: split is rescaled to 5e5 (50% of 1e6) and weight is 5e18.
|
|
105
|
-
assertEq(hookSpecs[0].amount, 5e5, "split should be 50% of payment (5e5), not capped to 100%");
|
|
106
|
-
assertEq(weight, 5e18, "weight should be 50% (half goes to splits)");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/// @notice Sanity check: same currency AND same decimals — no rescaling needed.
|
|
110
|
-
function test_sameCurrency_sameDecimals_splitAmountUnchanged() public {
|
|
111
|
-
JB721TiersHook testHook;
|
|
112
|
-
{
|
|
113
|
-
JB721TiersHook origin = new JB721TiersHook(
|
|
114
|
-
IJBDirectory(mockJBDirectory),
|
|
115
|
-
IJBPermissions(mockJBPermissions),
|
|
116
|
-
IJBPrices(address(0)),
|
|
117
|
-
IJBRulesets(mockJBRulesets),
|
|
118
|
-
store,
|
|
119
|
-
IJBSplits(mockJBSplits),
|
|
120
|
-
IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
|
|
121
|
-
trustedForwarder
|
|
122
|
-
);
|
|
123
|
-
address hookAddr = makeAddr("hook18to18");
|
|
124
|
-
vm.etch(hookAddr, address(origin).code);
|
|
125
|
-
testHook = JB721TiersHook(hookAddr);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
{
|
|
129
|
-
(JB721TierConfig[] memory tierConfigs,) = _createTiers(defaultTierConfig, 1);
|
|
130
|
-
tierConfigs[0].price = 1e18;
|
|
131
|
-
tierConfigs[0].splitPercent = 500_000_000;
|
|
132
|
-
testHook.initialize(
|
|
133
|
-
projectId,
|
|
134
|
-
name,
|
|
135
|
-
symbol,
|
|
136
|
-
baseUri,
|
|
137
|
-
IJB721TokenUriResolver(mockTokenUriResolver),
|
|
138
|
-
contractUri,
|
|
139
|
-
JB721InitTiersConfig({tiers: tierConfigs, currency: CURRENCY, decimals: 18}),
|
|
140
|
-
JB721TiersHookFlags({
|
|
141
|
-
preventOverspending: false,
|
|
142
|
-
issueTokensForSplits: false,
|
|
143
|
-
noNewTiersWithReserves: false,
|
|
144
|
-
noNewTiersWithVotes: false,
|
|
145
|
-
noNewTiersWithOwnerMinting: false
|
|
146
|
-
})
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
bytes memory payerMetadata;
|
|
151
|
-
{
|
|
152
|
-
uint16[] memory tierIdsToMint = new uint16[](1);
|
|
153
|
-
tierIdsToMint[0] = 1;
|
|
154
|
-
bytes[] memory data = new bytes[](1);
|
|
155
|
-
data[0] = abi.encode(true, tierIdsToMint);
|
|
156
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
157
|
-
ids[0] = metadataHelper.getId("pay", testHook.METADATA_ID_TARGET());
|
|
158
|
-
payerMetadata = metadataHelper.createMetadata(ids, data);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
(uint256 weight, JBPayHookSpecification[] memory hookSpecs) = testHook.beforePayRecordedWith(
|
|
162
|
-
JBBeforePayRecordedContext({
|
|
163
|
-
terminal: mockTerminalAddress,
|
|
164
|
-
payer: beneficiary,
|
|
165
|
-
amount: JBTokenAmount({token: MOCK_TOKEN, value: 1e18, decimals: 18, currency: CURRENCY}),
|
|
166
|
-
projectId: projectId,
|
|
167
|
-
rulesetId: 0,
|
|
168
|
-
beneficiary: beneficiary,
|
|
169
|
-
weight: 10e18,
|
|
170
|
-
reservedPercent: 0,
|
|
171
|
-
metadata: payerMetadata
|
|
172
|
-
})
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
assertEq(hookSpecs[0].amount, 5e17, "split should be 50% of payment");
|
|
176
|
-
assertEq(weight, 5e18, "weight should be 50%");
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/// @notice Same currency, payment has MORE decimals than pricing (pricing=6, payment=18).
|
|
180
|
-
function test_sameCurrency_paymentMoreDecimals_splitScaledUp() public {
|
|
181
|
-
JB721TiersHook testHook;
|
|
182
|
-
{
|
|
183
|
-
JB721TiersHook origin = new JB721TiersHook(
|
|
184
|
-
IJBDirectory(mockJBDirectory),
|
|
185
|
-
IJBPermissions(mockJBPermissions),
|
|
186
|
-
IJBPrices(address(0)),
|
|
187
|
-
IJBRulesets(mockJBRulesets),
|
|
188
|
-
store,
|
|
189
|
-
IJBSplits(mockJBSplits),
|
|
190
|
-
IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
|
|
191
|
-
trustedForwarder
|
|
192
|
-
);
|
|
193
|
-
address hookAddr = makeAddr("hook6to18");
|
|
194
|
-
vm.etch(hookAddr, address(origin).code);
|
|
195
|
-
testHook = JB721TiersHook(hookAddr);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
{
|
|
199
|
-
(JB721TierConfig[] memory tierConfigs,) = _createTiers(defaultTierConfig, 1);
|
|
200
|
-
tierConfigs[0].price = 1e6; // 1.0 token in 6-decimal pricing.
|
|
201
|
-
tierConfigs[0].splitPercent = 500_000_000;
|
|
202
|
-
testHook.initialize(
|
|
203
|
-
projectId,
|
|
204
|
-
name,
|
|
205
|
-
symbol,
|
|
206
|
-
baseUri,
|
|
207
|
-
IJB721TokenUriResolver(mockTokenUriResolver),
|
|
208
|
-
contractUri,
|
|
209
|
-
JB721InitTiersConfig({tiers: tierConfigs, currency: CURRENCY, decimals: 6}),
|
|
210
|
-
JB721TiersHookFlags({
|
|
211
|
-
preventOverspending: false,
|
|
212
|
-
issueTokensForSplits: false,
|
|
213
|
-
noNewTiersWithReserves: false,
|
|
214
|
-
noNewTiersWithVotes: false,
|
|
215
|
-
noNewTiersWithOwnerMinting: false
|
|
216
|
-
})
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
bytes memory payerMetadata;
|
|
221
|
-
{
|
|
222
|
-
uint16[] memory tierIdsToMint = new uint16[](1);
|
|
223
|
-
tierIdsToMint[0] = 1;
|
|
224
|
-
bytes[] memory data = new bytes[](1);
|
|
225
|
-
data[0] = abi.encode(true, tierIdsToMint);
|
|
226
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
227
|
-
ids[0] = metadataHelper.getId("pay", testHook.METADATA_ID_TARGET());
|
|
228
|
-
payerMetadata = metadataHelper.createMetadata(ids, data);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
(uint256 weight, JBPayHookSpecification[] memory hookSpecs) = testHook.beforePayRecordedWith(
|
|
232
|
-
JBBeforePayRecordedContext({
|
|
233
|
-
terminal: mockTerminalAddress,
|
|
234
|
-
payer: beneficiary,
|
|
235
|
-
amount: JBTokenAmount({token: MOCK_TOKEN, value: 1e18, decimals: 18, currency: CURRENCY}),
|
|
236
|
-
projectId: projectId,
|
|
237
|
-
rulesetId: 0,
|
|
238
|
-
beneficiary: beneficiary,
|
|
239
|
-
weight: 10e18,
|
|
240
|
-
reservedPercent: 0,
|
|
241
|
-
metadata: payerMetadata
|
|
242
|
-
})
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
// 50% of 1.0 token in 18-decimal payment = 5e17.
|
|
246
|
-
assertEq(hookSpecs[0].amount, 5e17, "split scaled up to 18-decimal payment");
|
|
247
|
-
assertEq(weight, 5e18, "weight should be 50%");
|
|
248
|
-
}
|
|
249
|
-
}
|