@bananapus/721-hook-v6 0.0.32 → 0.0.34
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/USER_JOURNEYS.md +11 -0
- package/package.json +3 -3
- package/script/Deploy.s.sol +53 -19
- package/src/JB721Checkpoints.sol +92 -0
- package/src/JB721CheckpointsDeployer.sol +45 -0
- package/src/JB721TiersHook.sol +90 -116
- package/src/abstract/JB721Hook.sol +5 -0
- package/src/interfaces/IJB721Checkpoints.sol +34 -0
- package/src/interfaces/IJB721CheckpointsDeployer.sol +20 -0
- package/src/interfaces/IJB721TiersHook.sol +8 -0
- package/src/libraries/JB721Constants.sol +6 -0
- package/src/libraries/JB721TiersHookLib.sol +353 -146
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +11 -1
- package/test/Fork.t.sol +11 -2
- package/test/TestAuditGaps.sol +1 -1
- package/test/TestCheckpoints.t.sol +329 -0
- package/test/audit/CodexNemesisRepoFindings.t.sol +270 -0
- package/test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol +161 -0
- package/test/audit/CodexSplitCreditsMismatch.t.sol +2 -1
- package/test/audit/CrossCurrencySplitNoPrices.t.sol +1 -0
- package/test/audit/SameCurrencyDecimalMismatch.t.sol +249 -0
- package/test/audit/SplitFailureRedistribution.t.sol +2 -1
- package/test/fork/ERC20CashOutFork.t.sol +11 -2
- package/test/fork/ERC20TierSplitFork.t.sol +11 -2
- package/test/fork/IssueTokensForSplitsFork.t.sol +11 -2
- package/test/regression/BrokenTerminalDoesNotDos.t.sol +2 -2
- package/test/regression/SplitDistributionBugs.t.sol +5 -5
- package/test/regression/SplitNoBeneficiary.t.sol +1 -1
- package/test/unit/AuditFixes_Unit.t.sol +5 -5
- package/test/unit/pay_CrossCurrency_Unit.t.sol +1 -0
- package/test/unit/pay_Unit.t.sol +1 -0
- package/test/unit/redeem_Unit.t.sol +3 -3
- package/test/unit/relayBeneficiary_Unit.t.sol +182 -0
- package/test/unit/splitHookDistribution_Unit.t.sol +6 -6
- package/test/unit/tierSplitRouting_Unit.t.sol +2 -2
package/test/Fork.t.sol
CHANGED
|
@@ -66,6 +66,8 @@ import "../src/JB721TiersHookDeployer.sol";
|
|
|
66
66
|
import "../src/JB721TiersHookProjectDeployer.sol";
|
|
67
67
|
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
68
68
|
import "../src/JB721TiersHookStore.sol";
|
|
69
|
+
import {JB721CheckpointsDeployer} from "../src/JB721CheckpointsDeployer.sol";
|
|
70
|
+
import {IJB721CheckpointsDeployer} from "../src/interfaces/IJB721CheckpointsDeployer.sol";
|
|
69
71
|
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
70
72
|
import "../src/interfaces/IJB721TiersHook.sol";
|
|
71
73
|
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
@@ -183,7 +185,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
183
185
|
jbPermissions = new JBPermissions(address(0));
|
|
184
186
|
jbProjects = new JBProjects(multisig, address(0), address(0));
|
|
185
187
|
jbDirectory = new JBDirectory(jbPermissions, jbProjects, multisig);
|
|
186
|
-
JBERC20 jbErc20 = new JBERC20();
|
|
188
|
+
JBERC20 jbErc20 = new JBERC20(jbPermissions, jbProjects);
|
|
187
189
|
jbTokens = new JBTokens(jbDirectory, jbErc20);
|
|
188
190
|
jbRulesets = new JBRulesets(jbDirectory);
|
|
189
191
|
jbPrices = new JBPrices(jbDirectory, jbPermissions, jbProjects, multisig, address(0));
|
|
@@ -231,7 +233,14 @@ contract Fork_721Hook_Test is Test {
|
|
|
231
233
|
function _deploy721Hook() internal {
|
|
232
234
|
store = new JB721TiersHookStore();
|
|
233
235
|
hookImpl = new JB721TiersHook(
|
|
234
|
-
jbDirectory,
|
|
236
|
+
jbDirectory,
|
|
237
|
+
jbPermissions,
|
|
238
|
+
jbPrices,
|
|
239
|
+
jbRulesets,
|
|
240
|
+
store,
|
|
241
|
+
IJBSplits(address(jbSplits)),
|
|
242
|
+
IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
|
|
243
|
+
address(0)
|
|
235
244
|
);
|
|
236
245
|
addressRegistry = new JBAddressRegistry();
|
|
237
246
|
hookDeployer = new JB721TiersHookDeployer(hookImpl, store, addressRegistry, address(0));
|
package/test/TestAuditGaps.sol
CHANGED
|
@@ -164,7 +164,7 @@ contract TestAuditGaps_Reentrancy is UnitTestSetup {
|
|
|
164
164
|
weight: 10e18,
|
|
165
165
|
newlyIssuedTokenCount: 0,
|
|
166
166
|
beneficiary: beneficiary,
|
|
167
|
-
hookMetadata: abi.encode(splitTierIds, splitAmounts),
|
|
167
|
+
hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
|
|
168
168
|
payerMetadata: payerMetadata
|
|
169
169
|
});
|
|
170
170
|
}
|
|
@@ -0,0 +1,329 @@
|
|
|
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
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
7
|
+
import "./utils/ForTest_JB721TiersHook.sol";
|
|
8
|
+
import {JB721Checkpoints} from "../src/JB721Checkpoints.sol";
|
|
9
|
+
import {IJB721Checkpoints} from "../src/interfaces/IJB721Checkpoints.sol";
|
|
10
|
+
import {IJB721TiersHook} from "../src/interfaces/IJB721TiersHook.sol";
|
|
11
|
+
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
12
|
+
|
|
13
|
+
/// @title TestCheckpoints
|
|
14
|
+
/// @notice Tests the checkpoint module IVotes checkpointed voting power baked into the base hook:
|
|
15
|
+
/// delegation, checkpoints, transfer, multi-tier, burn, and module deployment.
|
|
16
|
+
contract TestCheckpoints is UnitTestSetup {
|
|
17
|
+
/// @notice Deploys a ForTest hook with the given number of tiers.
|
|
18
|
+
function _initializeHookWithCheckpoints(uint256 numberOfTiers) internal returns (ForTest_JB721TiersHook tiersHook) {
|
|
19
|
+
(JB721TierConfig[] memory tierConfigs,) = _createTiers(defaultTierConfig, numberOfTiers);
|
|
20
|
+
|
|
21
|
+
ForTest_JB721TiersHookStore hookStore = new ForTest_JB721TiersHookStore();
|
|
22
|
+
|
|
23
|
+
tiersHook = new ForTest_JB721TiersHook(
|
|
24
|
+
ForTest_JB721TiersHook.ForTestInitConfig({
|
|
25
|
+
projectId: projectId,
|
|
26
|
+
name: name,
|
|
27
|
+
symbol: symbol,
|
|
28
|
+
baseUri: baseUri,
|
|
29
|
+
tokenUriResolver: IJB721TokenUriResolver(mockTokenUriResolver),
|
|
30
|
+
contractUri: contractUri,
|
|
31
|
+
tiers: tierConfigs,
|
|
32
|
+
flags: JB721TiersHookFlags({
|
|
33
|
+
preventOverspending: false,
|
|
34
|
+
issueTokensForSplits: false,
|
|
35
|
+
noNewTiersWithReserves: false,
|
|
36
|
+
noNewTiersWithVotes: false,
|
|
37
|
+
noNewTiersWithOwnerMinting: true
|
|
38
|
+
})
|
|
39
|
+
}),
|
|
40
|
+
IJBDirectory(mockJBDirectory),
|
|
41
|
+
IJBPrices(mockJBPrices),
|
|
42
|
+
IJBRulesets(mockJBRulesets),
|
|
43
|
+
IJB721TiersHookStore(address(hookStore)),
|
|
44
|
+
IJBSplits(mockJBSplits)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
tiersHook.transferOwnership(owner);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// -------------------------------------------------------------------
|
|
51
|
+
// Test 1: Checkpoint module is deployed eagerly during initialize
|
|
52
|
+
// -------------------------------------------------------------------
|
|
53
|
+
function test_checkpointModule_isDeployed() public {
|
|
54
|
+
defaultTierConfig.flags.allowOwnerMint = true;
|
|
55
|
+
defaultTierConfig.reserveFrequency = 0;
|
|
56
|
+
|
|
57
|
+
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(1);
|
|
58
|
+
|
|
59
|
+
// CHECKPOINTS should be deployed immediately after initialization.
|
|
60
|
+
assertTrue(
|
|
61
|
+
address(tiersHook.CHECKPOINTS()) != address(0), "Checkpoint module should be deployed after initialization"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// -------------------------------------------------------------------
|
|
66
|
+
// Test 2: supportsInterface still works for base hook
|
|
67
|
+
// -------------------------------------------------------------------
|
|
68
|
+
function test_supportsInterface() public {
|
|
69
|
+
defaultTierConfig.flags.allowOwnerMint = true;
|
|
70
|
+
defaultTierConfig.reserveFrequency = 0;
|
|
71
|
+
|
|
72
|
+
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(1);
|
|
73
|
+
|
|
74
|
+
assertTrue(tiersHook.supportsInterface(type(IJB721TiersHook).interfaceId), "Should support IJB721TiersHook");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// -------------------------------------------------------------------
|
|
78
|
+
// Test 3: Mint + manual delegate -> getVotes equals tier votingUnits
|
|
79
|
+
// -------------------------------------------------------------------
|
|
80
|
+
function test_mintAndDelegate_getVotes() public {
|
|
81
|
+
defaultTierConfig.flags.allowOwnerMint = true;
|
|
82
|
+
defaultTierConfig.reserveFrequency = 0;
|
|
83
|
+
defaultTierConfig.flags.useVotingUnits = true;
|
|
84
|
+
defaultTierConfig.votingUnits = 100;
|
|
85
|
+
|
|
86
|
+
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(1);
|
|
87
|
+
|
|
88
|
+
address user = makeAddr("user");
|
|
89
|
+
|
|
90
|
+
// Mint an NFT to user (CHECKPOINTS already deployed during init).
|
|
91
|
+
uint16[] memory tiersToMint = new uint16[](1);
|
|
92
|
+
tiersToMint[0] = 1;
|
|
93
|
+
vm.prank(owner);
|
|
94
|
+
tiersHook.mintFor(tiersToMint, user);
|
|
95
|
+
|
|
96
|
+
IJB721Checkpoints module = tiersHook.CHECKPOINTS();
|
|
97
|
+
|
|
98
|
+
// Without delegation, getVotes should be 0.
|
|
99
|
+
assertEq(module.getVotes(user), 0, "Votes should be 0 before delegation");
|
|
100
|
+
|
|
101
|
+
// User self-delegates.
|
|
102
|
+
vm.prank(user);
|
|
103
|
+
module.delegate(user);
|
|
104
|
+
|
|
105
|
+
assertEq(module.getVotes(user), 100, "Votes should be 100 after delegation");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// -------------------------------------------------------------------
|
|
109
|
+
// Test 4: No auto-delegation — delegates(user) stays address(0) after mint
|
|
110
|
+
// -------------------------------------------------------------------
|
|
111
|
+
function test_noAutoDelegation_delegateStaysZero() public {
|
|
112
|
+
defaultTierConfig.flags.allowOwnerMint = true;
|
|
113
|
+
defaultTierConfig.reserveFrequency = 0;
|
|
114
|
+
defaultTierConfig.flags.useVotingUnits = true;
|
|
115
|
+
defaultTierConfig.votingUnits = 100;
|
|
116
|
+
|
|
117
|
+
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(1);
|
|
118
|
+
|
|
119
|
+
address user = makeAddr("user");
|
|
120
|
+
|
|
121
|
+
// Mint an NFT to user (CHECKPOINTS already deployed during init).
|
|
122
|
+
uint16[] memory tiersToMint = new uint16[](1);
|
|
123
|
+
tiersToMint[0] = 1;
|
|
124
|
+
vm.prank(owner);
|
|
125
|
+
tiersHook.mintFor(tiersToMint, user);
|
|
126
|
+
|
|
127
|
+
IJB721Checkpoints module = tiersHook.CHECKPOINTS();
|
|
128
|
+
|
|
129
|
+
// Delegate should be address(0) — no auto-delegation.
|
|
130
|
+
assertEq(module.delegates(user), address(0), "Delegate should be zero after mint");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// -------------------------------------------------------------------
|
|
134
|
+
// Test 5: Transfer moves checkpointed votes (with manual delegation)
|
|
135
|
+
// -------------------------------------------------------------------
|
|
136
|
+
function test_transfer_movesCheckpointedVotes() public {
|
|
137
|
+
defaultTierConfig.flags.allowOwnerMint = true;
|
|
138
|
+
defaultTierConfig.reserveFrequency = 0;
|
|
139
|
+
defaultTierConfig.flags.useVotingUnits = true;
|
|
140
|
+
defaultTierConfig.votingUnits = 100;
|
|
141
|
+
|
|
142
|
+
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(1);
|
|
143
|
+
|
|
144
|
+
address alice = makeAddr("alice");
|
|
145
|
+
address bob = makeAddr("bob");
|
|
146
|
+
|
|
147
|
+
// Mint to alice (CHECKPOINTS already deployed during init).
|
|
148
|
+
uint16[] memory tiersToMint = new uint16[](1);
|
|
149
|
+
tiersToMint[0] = 1;
|
|
150
|
+
vm.prank(owner);
|
|
151
|
+
tiersHook.mintFor(tiersToMint, alice);
|
|
152
|
+
|
|
153
|
+
IJB721Checkpoints module = tiersHook.CHECKPOINTS();
|
|
154
|
+
|
|
155
|
+
// Both delegate to themselves.
|
|
156
|
+
vm.prank(alice);
|
|
157
|
+
module.delegate(alice);
|
|
158
|
+
vm.prank(bob);
|
|
159
|
+
module.delegate(bob);
|
|
160
|
+
|
|
161
|
+
assertEq(module.getVotes(alice), 100, "Alice should have 100 votes");
|
|
162
|
+
assertEq(module.getVotes(bob), 0, "Bob should have 0 votes");
|
|
163
|
+
|
|
164
|
+
// Transfer NFT from alice to bob.
|
|
165
|
+
uint256 tokenId = _generateTokenId(1, 1);
|
|
166
|
+
vm.prank(alice);
|
|
167
|
+
IERC721(address(tiersHook)).transferFrom(alice, bob, tokenId);
|
|
168
|
+
|
|
169
|
+
assertEq(module.getVotes(alice), 0, "Alice should have 0 votes after transfer");
|
|
170
|
+
assertEq(module.getVotes(bob), 100, "Bob should have 100 votes after transfer");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// -------------------------------------------------------------------
|
|
174
|
+
// Test 6: getPastVotes / getPastTotalSupply checkpoints
|
|
175
|
+
// -------------------------------------------------------------------
|
|
176
|
+
function test_getPastVotes_checkpoint() public {
|
|
177
|
+
defaultTierConfig.flags.allowOwnerMint = true;
|
|
178
|
+
defaultTierConfig.reserveFrequency = 0;
|
|
179
|
+
defaultTierConfig.flags.useVotingUnits = true;
|
|
180
|
+
defaultTierConfig.votingUnits = 100;
|
|
181
|
+
|
|
182
|
+
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(1);
|
|
183
|
+
|
|
184
|
+
address user = makeAddr("user");
|
|
185
|
+
|
|
186
|
+
// Mint first NFT to test checkpoint tracking.
|
|
187
|
+
uint16[] memory tiersToMint = new uint16[](1);
|
|
188
|
+
tiersToMint[0] = 1;
|
|
189
|
+
vm.prank(owner);
|
|
190
|
+
tiersHook.mintFor(tiersToMint, user);
|
|
191
|
+
|
|
192
|
+
IJB721Checkpoints module = tiersHook.CHECKPOINTS();
|
|
193
|
+
|
|
194
|
+
// User self-delegates so checkpoints are created going forward.
|
|
195
|
+
vm.prank(user);
|
|
196
|
+
module.delegate(user);
|
|
197
|
+
|
|
198
|
+
uint256 blockBeforeSecondMint = block.number;
|
|
199
|
+
vm.roll(block.number + 1);
|
|
200
|
+
|
|
201
|
+
// Mint a second NFT.
|
|
202
|
+
vm.prank(owner);
|
|
203
|
+
tiersHook.mintFor(tiersToMint, user);
|
|
204
|
+
|
|
205
|
+
uint256 blockAfterSecondMint = block.number;
|
|
206
|
+
vm.roll(block.number + 1);
|
|
207
|
+
|
|
208
|
+
// Past votes before second mint = 100 (from first NFT + delegation).
|
|
209
|
+
assertEq(module.getPastVotes(user, blockBeforeSecondMint), 100, "Past votes before second mint should be 100");
|
|
210
|
+
// Past votes after second mint = 200.
|
|
211
|
+
assertEq(module.getPastVotes(user, blockAfterSecondMint), 200, "Past votes after second mint should be 200");
|
|
212
|
+
|
|
213
|
+
// Past total supply.
|
|
214
|
+
assertEq(
|
|
215
|
+
module.getPastTotalSupply(blockBeforeSecondMint), 100, "Past total supply before second mint should be 100"
|
|
216
|
+
);
|
|
217
|
+
assertEq(
|
|
218
|
+
module.getPastTotalSupply(blockAfterSecondMint), 200, "Past total supply after second mint should be 200"
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// -------------------------------------------------------------------
|
|
223
|
+
// Test 7: Multi-tier with different voting units
|
|
224
|
+
// -------------------------------------------------------------------
|
|
225
|
+
function test_multiTier_differentVotingUnits() public {
|
|
226
|
+
defaultTierConfig.flags.allowOwnerMint = true;
|
|
227
|
+
defaultTierConfig.reserveFrequency = 0;
|
|
228
|
+
defaultTierConfig.flags.useVotingUnits = true;
|
|
229
|
+
|
|
230
|
+
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(3);
|
|
231
|
+
|
|
232
|
+
// Set custom voting units per tier.
|
|
233
|
+
tiersHook.test_store().ForTest_setTierVotingUnits(address(tiersHook), 1, 100);
|
|
234
|
+
tiersHook.test_store().ForTest_setTierVotingUnits(address(tiersHook), 2, 200);
|
|
235
|
+
tiersHook.test_store().ForTest_setTierVotingUnits(address(tiersHook), 3, 500);
|
|
236
|
+
|
|
237
|
+
address user = makeAddr("user");
|
|
238
|
+
|
|
239
|
+
// Mint one from tier 1 to test checkpoint tracking.
|
|
240
|
+
uint16[] memory tier1 = new uint16[](1);
|
|
241
|
+
tier1[0] = 1;
|
|
242
|
+
vm.prank(owner);
|
|
243
|
+
tiersHook.mintFor(tier1, user);
|
|
244
|
+
|
|
245
|
+
IJB721Checkpoints module = tiersHook.CHECKPOINTS();
|
|
246
|
+
|
|
247
|
+
// User self-delegates.
|
|
248
|
+
vm.prank(user);
|
|
249
|
+
module.delegate(user);
|
|
250
|
+
|
|
251
|
+
// Mint from remaining tiers.
|
|
252
|
+
uint16[] memory tier2 = new uint16[](1);
|
|
253
|
+
tier2[0] = 2;
|
|
254
|
+
uint16[] memory tier3 = new uint16[](1);
|
|
255
|
+
tier3[0] = 3;
|
|
256
|
+
|
|
257
|
+
vm.startPrank(owner);
|
|
258
|
+
tiersHook.mintFor(tier2, user);
|
|
259
|
+
tiersHook.mintFor(tier3, user);
|
|
260
|
+
vm.stopPrank();
|
|
261
|
+
|
|
262
|
+
// 100 + 200 + 500 = 800.
|
|
263
|
+
assertEq(module.getVotes(user), 800, "User should have 800 checkpointed votes");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// -------------------------------------------------------------------
|
|
267
|
+
// Test 8: Burn decreases checkpointed total supply
|
|
268
|
+
// -------------------------------------------------------------------
|
|
269
|
+
function test_burn_decreasesTotalSupply() public {
|
|
270
|
+
defaultTierConfig.flags.allowOwnerMint = true;
|
|
271
|
+
defaultTierConfig.reserveFrequency = 0;
|
|
272
|
+
defaultTierConfig.flags.useVotingUnits = true;
|
|
273
|
+
defaultTierConfig.votingUnits = 100;
|
|
274
|
+
|
|
275
|
+
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(1);
|
|
276
|
+
|
|
277
|
+
address user = makeAddr("user");
|
|
278
|
+
|
|
279
|
+
// Mint 2 NFTs (first one deploys CHECKPOINTS lazily).
|
|
280
|
+
uint16[] memory tiersToMint = new uint16[](2);
|
|
281
|
+
tiersToMint[0] = 1;
|
|
282
|
+
tiersToMint[1] = 1;
|
|
283
|
+
vm.prank(owner);
|
|
284
|
+
tiersHook.mintFor(tiersToMint, user);
|
|
285
|
+
|
|
286
|
+
IJB721Checkpoints module = tiersHook.CHECKPOINTS();
|
|
287
|
+
|
|
288
|
+
// User self-delegates.
|
|
289
|
+
vm.prank(user);
|
|
290
|
+
module.delegate(user);
|
|
291
|
+
|
|
292
|
+
assertEq(module.getVotes(user), 200, "User should have 200 votes from 2 NFTs");
|
|
293
|
+
|
|
294
|
+
uint256 blockBeforeBurn = block.number;
|
|
295
|
+
vm.roll(block.number + 1);
|
|
296
|
+
|
|
297
|
+
// Burn one NFT.
|
|
298
|
+
uint256[] memory tokensToBurn = new uint256[](1);
|
|
299
|
+
tokensToBurn[0] = _generateTokenId(1, 1);
|
|
300
|
+
tiersHook.burn(tokensToBurn);
|
|
301
|
+
|
|
302
|
+
assertEq(module.getVotes(user), 100, "User should have 100 votes after burning 1 NFT");
|
|
303
|
+
|
|
304
|
+
vm.roll(block.number + 1);
|
|
305
|
+
|
|
306
|
+
assertEq(module.getPastTotalSupply(blockBeforeBurn), 200, "Past total supply before burn should be 200");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// -------------------------------------------------------------------
|
|
310
|
+
// Test 9: Unauthorized onTransfer reverts
|
|
311
|
+
// -------------------------------------------------------------------
|
|
312
|
+
function test_unauthorizedOnTransfer_reverts() public {
|
|
313
|
+
defaultTierConfig.flags.allowOwnerMint = true;
|
|
314
|
+
defaultTierConfig.reserveFrequency = 0;
|
|
315
|
+
|
|
316
|
+
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(1);
|
|
317
|
+
|
|
318
|
+
// Mint to test checkpoint tracking.
|
|
319
|
+
uint16[] memory tiersToMint = new uint16[](1);
|
|
320
|
+
tiersToMint[0] = 1;
|
|
321
|
+
vm.prank(owner);
|
|
322
|
+
tiersHook.mintFor(tiersToMint, makeAddr("user"));
|
|
323
|
+
|
|
324
|
+
IJB721Checkpoints module = tiersHook.CHECKPOINTS();
|
|
325
|
+
|
|
326
|
+
vm.expectRevert(JB721Checkpoints.JB721Checkpoints_Unauthorized.selector);
|
|
327
|
+
module.onTransfer(address(0), address(1), 1);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import "../utils/UnitTestSetup.sol";
|
|
5
|
+
|
|
6
|
+
import {IJB721TiersHookStore} from "../../src/interfaces/IJB721TiersHookStore.sol";
|
|
7
|
+
import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
|
|
8
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
9
|
+
|
|
10
|
+
contract CodexNemesisRepoFindings is UnitTestSetup {
|
|
11
|
+
address payable internal splitBeneficiary = payable(makeAddr("splitBeneficiary"));
|
|
12
|
+
|
|
13
|
+
function _payMetadata(
|
|
14
|
+
address hookAddress,
|
|
15
|
+
bool allowOverspending,
|
|
16
|
+
uint16[] memory tierIds
|
|
17
|
+
)
|
|
18
|
+
internal
|
|
19
|
+
view
|
|
20
|
+
returns (bytes memory)
|
|
21
|
+
{
|
|
22
|
+
bytes[] memory data = new bytes[](1);
|
|
23
|
+
data[0] = abi.encode(allowOverspending, tierIds);
|
|
24
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
25
|
+
ids[0] = metadataHelper.getId("pay", hookAddress);
|
|
26
|
+
return metadataHelper.createMetadata(ids, data);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function _nativeTokenAmount(uint256 value) internal pure returns (JBTokenAmount memory) {
|
|
30
|
+
return JBTokenAmount({
|
|
31
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
32
|
+
value: value,
|
|
33
|
+
decimals: 18,
|
|
34
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function test_payCredits_can_underfund_split_bearing_tier_mints() public {
|
|
39
|
+
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
40
|
+
|
|
41
|
+
vm.mockCall(mockJBSplits, abi.encodeWithSelector(IJBSplits.setSplitGroupsOf.selector), abi.encode());
|
|
42
|
+
|
|
43
|
+
JBSplit[] memory tierSplits = new JBSplit[](1);
|
|
44
|
+
tierSplits[0] = JBSplit({
|
|
45
|
+
preferAddToBalance: false,
|
|
46
|
+
percent: JBConstants.SPLITS_TOTAL_PERCENT,
|
|
47
|
+
projectId: 0,
|
|
48
|
+
beneficiary: splitBeneficiary,
|
|
49
|
+
lockedUntil: 0,
|
|
50
|
+
hook: IJBSplitHook(address(0))
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
54
|
+
tierConfigs[0] = JB721TierConfig({
|
|
55
|
+
price: 1 ether,
|
|
56
|
+
initialSupply: 10,
|
|
57
|
+
votingUnits: 0,
|
|
58
|
+
reserveFrequency: 0,
|
|
59
|
+
reserveBeneficiary: address(0),
|
|
60
|
+
encodedIPFSUri: bytes32(uint256(0x1234)),
|
|
61
|
+
category: 1,
|
|
62
|
+
discountPercent: 0,
|
|
63
|
+
flags: JB721TierConfigFlags({
|
|
64
|
+
allowOwnerMint: false,
|
|
65
|
+
useReserveBeneficiaryAsDefault: false,
|
|
66
|
+
transfersPausable: false,
|
|
67
|
+
useVotingUnits: false,
|
|
68
|
+
cantBeRemoved: false,
|
|
69
|
+
cantIncreaseDiscountPercent: false,
|
|
70
|
+
cantBuyWithCredits: false
|
|
71
|
+
}),
|
|
72
|
+
splitPercent: JBConstants.SPLITS_TOTAL_PERCENT,
|
|
73
|
+
splits: tierSplits
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
vm.prank(owner);
|
|
77
|
+
testHook.adjustTiers(tierConfigs, new uint256[](0));
|
|
78
|
+
|
|
79
|
+
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(1) << 160);
|
|
80
|
+
vm.mockCall(
|
|
81
|
+
mockJBSplits,
|
|
82
|
+
abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, uint256(0), groupId),
|
|
83
|
+
abi.encode(tierSplits)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
mockAndExpect(
|
|
87
|
+
mockJBDirectory,
|
|
88
|
+
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
89
|
+
abi.encode(true)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
vm.prank(mockTerminalAddress);
|
|
93
|
+
testHook.afterPayRecordedWith(
|
|
94
|
+
JBAfterPayRecordedContext({
|
|
95
|
+
payer: beneficiary,
|
|
96
|
+
projectId: projectId,
|
|
97
|
+
rulesetId: 0,
|
|
98
|
+
amount: _nativeTokenAmount(1 ether),
|
|
99
|
+
forwardedAmount: _nativeTokenAmount(0),
|
|
100
|
+
weight: 10e18,
|
|
101
|
+
newlyIssuedTokenCount: 0,
|
|
102
|
+
beneficiary: beneficiary,
|
|
103
|
+
hookMetadata: bytes(""),
|
|
104
|
+
payerMetadata: bytes("")
|
|
105
|
+
})
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
assertEq(testHook.payCreditsOf(beneficiary), 1 ether, "setup: credits should be seeded");
|
|
109
|
+
|
|
110
|
+
uint16[] memory tierIdsToMint = new uint16[](1);
|
|
111
|
+
tierIdsToMint[0] = 1;
|
|
112
|
+
bytes memory payerMetadata = _payMetadata(address(testHook), true, tierIdsToMint);
|
|
113
|
+
|
|
114
|
+
JBBeforePayRecordedContext memory beforeContext = JBBeforePayRecordedContext({
|
|
115
|
+
terminal: mockTerminalAddress,
|
|
116
|
+
payer: beneficiary,
|
|
117
|
+
amount: _nativeTokenAmount(1),
|
|
118
|
+
projectId: projectId,
|
|
119
|
+
rulesetId: 0,
|
|
120
|
+
beneficiary: beneficiary,
|
|
121
|
+
weight: 10e18,
|
|
122
|
+
reservedPercent: 5000,
|
|
123
|
+
metadata: payerMetadata
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
(uint256 weight, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(beforeContext);
|
|
127
|
+
|
|
128
|
+
assertEq(weight, 0, "only the fresh 1 wei payment is considered for split weight adjustment");
|
|
129
|
+
assertEq(specs.length, 1);
|
|
130
|
+
assertEq(specs[0].amount, 1, "split forwarding is capped to the fresh payment, not the credit-backed mint");
|
|
131
|
+
|
|
132
|
+
uint256 splitBeneficiaryBalanceBefore = splitBeneficiary.balance;
|
|
133
|
+
vm.deal(mockTerminalAddress, 1);
|
|
134
|
+
|
|
135
|
+
vm.prank(mockTerminalAddress);
|
|
136
|
+
testHook.afterPayRecordedWith{value: 1}(
|
|
137
|
+
JBAfterPayRecordedContext({
|
|
138
|
+
payer: beneficiary,
|
|
139
|
+
projectId: projectId,
|
|
140
|
+
rulesetId: 0,
|
|
141
|
+
amount: _nativeTokenAmount(1),
|
|
142
|
+
forwardedAmount: _nativeTokenAmount(1),
|
|
143
|
+
weight: weight,
|
|
144
|
+
newlyIssuedTokenCount: 0,
|
|
145
|
+
beneficiary: beneficiary,
|
|
146
|
+
hookMetadata: specs[0].metadata,
|
|
147
|
+
payerMetadata: payerMetadata
|
|
148
|
+
})
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
assertEq(testHook.balanceOf(beneficiary), 1, "beneficiary still receives the split-bearing NFT");
|
|
152
|
+
assertEq(testHook.payCreditsOf(beneficiary), 1, "stored credits fund essentially the entire mint");
|
|
153
|
+
assertEq(
|
|
154
|
+
splitBeneficiary.balance - splitBeneficiaryBalanceBefore,
|
|
155
|
+
1,
|
|
156
|
+
"split beneficiary only receives the fresh 1 wei payment instead of the tier's 1 ether split amount"
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function test_new_default_reserve_beneficiary_retroactively_dilutes_existing_tiers() public {
|
|
161
|
+
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
162
|
+
|
|
163
|
+
JB721TierConfig[] memory initialTier = new JB721TierConfig[](1);
|
|
164
|
+
initialTier[0] = JB721TierConfig({
|
|
165
|
+
price: 1 ether,
|
|
166
|
+
initialSupply: 10,
|
|
167
|
+
votingUnits: 0,
|
|
168
|
+
reserveFrequency: 2,
|
|
169
|
+
reserveBeneficiary: address(0),
|
|
170
|
+
encodedIPFSUri: bytes32(uint256(0x1111)),
|
|
171
|
+
category: 1,
|
|
172
|
+
discountPercent: 0,
|
|
173
|
+
flags: JB721TierConfigFlags({
|
|
174
|
+
allowOwnerMint: false,
|
|
175
|
+
useReserveBeneficiaryAsDefault: false,
|
|
176
|
+
transfersPausable: false,
|
|
177
|
+
useVotingUnits: false,
|
|
178
|
+
cantBeRemoved: false,
|
|
179
|
+
cantIncreaseDiscountPercent: false,
|
|
180
|
+
cantBuyWithCredits: false
|
|
181
|
+
}),
|
|
182
|
+
splitPercent: 0,
|
|
183
|
+
splits: new JBSplit[](0)
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
vm.prank(owner);
|
|
187
|
+
testHook.adjustTiers(initialTier, new uint256[](0));
|
|
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"
|
|
222
|
+
);
|
|
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
|
+
vm.prank(owner);
|
|
248
|
+
testHook.adjustTiers(defaultingTier, new uint256[](0));
|
|
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");
|
|
269
|
+
}
|
|
270
|
+
}
|