@bananapus/721-hook-v6 0.0.41 → 0.0.43
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 +60 -18
- package/src/JB721CheckpointsDeployer.sol +10 -5
- package/src/JB721TiersHook.sol +4 -1
- package/src/JB721TiersHookProjectDeployer.sol +68 -30
- package/src/JB721TiersHookStore.sol +1 -4
- package/src/interfaces/IJB721Checkpoints.sol +21 -14
- package/src/interfaces/IJB721CheckpointsDeployer.sol +6 -2
- package/src/interfaces/IJB721TiersHookProjectDeployer.sol +2 -0
- 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/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/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,341 +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
|
-
// 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 lazily on first transfer
|
|
52
|
-
// -------------------------------------------------------------------
|
|
53
|
-
function test_checkpointModule_isDeployedLazily() public {
|
|
54
|
-
defaultTierConfig.flags.allowOwnerMint = true;
|
|
55
|
-
defaultTierConfig.reserveFrequency = 0;
|
|
56
|
-
|
|
57
|
-
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(1);
|
|
58
|
-
|
|
59
|
-
// CHECKPOINTS should NOT be deployed after initialization (lazy deployment).
|
|
60
|
-
assertTrue(
|
|
61
|
-
address(tiersHook.CHECKPOINTS()) == address(0),
|
|
62
|
-
"Checkpoint module should not be deployed after initialization"
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
// Mint a token to trigger lazy deployment.
|
|
66
|
-
uint16[] memory tiersToMint = new uint16[](1);
|
|
67
|
-
tiersToMint[0] = 1;
|
|
68
|
-
vm.prank(owner);
|
|
69
|
-
tiersHook.mintFor(tiersToMint, owner);
|
|
70
|
-
|
|
71
|
-
// CHECKPOINTS should now be deployed after the first mint.
|
|
72
|
-
assertTrue(
|
|
73
|
-
address(tiersHook.CHECKPOINTS()) != address(0), "Checkpoint module should be deployed after first mint"
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// -------------------------------------------------------------------
|
|
78
|
-
// Test 2: supportsInterface still works for base hook
|
|
79
|
-
// -------------------------------------------------------------------
|
|
80
|
-
function test_supportsInterface() public {
|
|
81
|
-
defaultTierConfig.flags.allowOwnerMint = true;
|
|
82
|
-
defaultTierConfig.reserveFrequency = 0;
|
|
83
|
-
|
|
84
|
-
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(1);
|
|
85
|
-
|
|
86
|
-
assertTrue(tiersHook.supportsInterface(type(IJB721TiersHook).interfaceId), "Should support IJB721TiersHook");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// -------------------------------------------------------------------
|
|
90
|
-
// Test 3: Mint + manual delegate -> getVotes equals tier votingUnits
|
|
91
|
-
// -------------------------------------------------------------------
|
|
92
|
-
function test_mintAndDelegate_getVotes() public {
|
|
93
|
-
defaultTierConfig.flags.allowOwnerMint = true;
|
|
94
|
-
defaultTierConfig.reserveFrequency = 0;
|
|
95
|
-
defaultTierConfig.flags.useVotingUnits = true;
|
|
96
|
-
defaultTierConfig.votingUnits = 100;
|
|
97
|
-
|
|
98
|
-
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(1);
|
|
99
|
-
|
|
100
|
-
address user = makeAddr("user");
|
|
101
|
-
|
|
102
|
-
// Mint an NFT to user (CHECKPOINTS already deployed during init).
|
|
103
|
-
uint16[] memory tiersToMint = new uint16[](1);
|
|
104
|
-
tiersToMint[0] = 1;
|
|
105
|
-
vm.prank(owner);
|
|
106
|
-
tiersHook.mintFor(tiersToMint, user);
|
|
107
|
-
|
|
108
|
-
IJB721Checkpoints module = tiersHook.CHECKPOINTS();
|
|
109
|
-
|
|
110
|
-
// Without delegation, getVotes should be 0.
|
|
111
|
-
assertEq(module.getVotes(user), 0, "Votes should be 0 before delegation");
|
|
112
|
-
|
|
113
|
-
// User self-delegates.
|
|
114
|
-
vm.prank(user);
|
|
115
|
-
module.delegate(user);
|
|
116
|
-
|
|
117
|
-
assertEq(module.getVotes(user), 100, "Votes should be 100 after delegation");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// -------------------------------------------------------------------
|
|
121
|
-
// Test 4: No auto-delegation — delegates(user) stays address(0) after mint
|
|
122
|
-
// -------------------------------------------------------------------
|
|
123
|
-
function test_noAutoDelegation_delegateStaysZero() public {
|
|
124
|
-
defaultTierConfig.flags.allowOwnerMint = true;
|
|
125
|
-
defaultTierConfig.reserveFrequency = 0;
|
|
126
|
-
defaultTierConfig.flags.useVotingUnits = true;
|
|
127
|
-
defaultTierConfig.votingUnits = 100;
|
|
128
|
-
|
|
129
|
-
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(1);
|
|
130
|
-
|
|
131
|
-
address user = makeAddr("user");
|
|
132
|
-
|
|
133
|
-
// Mint an NFT to user (CHECKPOINTS already deployed during init).
|
|
134
|
-
uint16[] memory tiersToMint = new uint16[](1);
|
|
135
|
-
tiersToMint[0] = 1;
|
|
136
|
-
vm.prank(owner);
|
|
137
|
-
tiersHook.mintFor(tiersToMint, user);
|
|
138
|
-
|
|
139
|
-
IJB721Checkpoints module = tiersHook.CHECKPOINTS();
|
|
140
|
-
|
|
141
|
-
// Delegate should be address(0) — no auto-delegation.
|
|
142
|
-
assertEq(module.delegates(user), address(0), "Delegate should be zero after mint");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// -------------------------------------------------------------------
|
|
146
|
-
// Test 5: Transfer moves checkpointed votes (with manual delegation)
|
|
147
|
-
// -------------------------------------------------------------------
|
|
148
|
-
function test_transfer_movesCheckpointedVotes() public {
|
|
149
|
-
defaultTierConfig.flags.allowOwnerMint = true;
|
|
150
|
-
defaultTierConfig.reserveFrequency = 0;
|
|
151
|
-
defaultTierConfig.flags.useVotingUnits = true;
|
|
152
|
-
defaultTierConfig.votingUnits = 100;
|
|
153
|
-
|
|
154
|
-
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(1);
|
|
155
|
-
|
|
156
|
-
address alice = makeAddr("alice");
|
|
157
|
-
address bob = makeAddr("bob");
|
|
158
|
-
|
|
159
|
-
// Mint to alice (CHECKPOINTS already deployed during init).
|
|
160
|
-
uint16[] memory tiersToMint = new uint16[](1);
|
|
161
|
-
tiersToMint[0] = 1;
|
|
162
|
-
vm.prank(owner);
|
|
163
|
-
tiersHook.mintFor(tiersToMint, alice);
|
|
164
|
-
|
|
165
|
-
IJB721Checkpoints module = tiersHook.CHECKPOINTS();
|
|
166
|
-
|
|
167
|
-
// Both delegate to themselves.
|
|
168
|
-
vm.prank(alice);
|
|
169
|
-
module.delegate(alice);
|
|
170
|
-
vm.prank(bob);
|
|
171
|
-
module.delegate(bob);
|
|
172
|
-
|
|
173
|
-
assertEq(module.getVotes(alice), 100, "Alice should have 100 votes");
|
|
174
|
-
assertEq(module.getVotes(bob), 0, "Bob should have 0 votes");
|
|
175
|
-
|
|
176
|
-
// Transfer NFT from alice to bob.
|
|
177
|
-
uint256 tokenId = _generateTokenId(1, 1);
|
|
178
|
-
vm.prank(alice);
|
|
179
|
-
IERC721(address(tiersHook)).transferFrom(alice, bob, tokenId);
|
|
180
|
-
|
|
181
|
-
assertEq(module.getVotes(alice), 0, "Alice should have 0 votes after transfer");
|
|
182
|
-
assertEq(module.getVotes(bob), 100, "Bob should have 100 votes after transfer");
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// -------------------------------------------------------------------
|
|
186
|
-
// Test 6: getPastVotes / getPastTotalSupply checkpoints
|
|
187
|
-
// -------------------------------------------------------------------
|
|
188
|
-
function test_getPastVotes_checkpoint() public {
|
|
189
|
-
defaultTierConfig.flags.allowOwnerMint = true;
|
|
190
|
-
defaultTierConfig.reserveFrequency = 0;
|
|
191
|
-
defaultTierConfig.flags.useVotingUnits = true;
|
|
192
|
-
defaultTierConfig.votingUnits = 100;
|
|
193
|
-
|
|
194
|
-
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(1);
|
|
195
|
-
|
|
196
|
-
address user = makeAddr("user");
|
|
197
|
-
|
|
198
|
-
// Mint first NFT to test checkpoint tracking.
|
|
199
|
-
uint16[] memory tiersToMint = new uint16[](1);
|
|
200
|
-
tiersToMint[0] = 1;
|
|
201
|
-
vm.prank(owner);
|
|
202
|
-
tiersHook.mintFor(tiersToMint, user);
|
|
203
|
-
|
|
204
|
-
IJB721Checkpoints module = tiersHook.CHECKPOINTS();
|
|
205
|
-
|
|
206
|
-
// User self-delegates so checkpoints are created going forward.
|
|
207
|
-
vm.prank(user);
|
|
208
|
-
module.delegate(user);
|
|
209
|
-
|
|
210
|
-
uint256 blockBeforeSecondMint = block.number;
|
|
211
|
-
vm.roll(block.number + 1);
|
|
212
|
-
|
|
213
|
-
// Mint a second NFT.
|
|
214
|
-
vm.prank(owner);
|
|
215
|
-
tiersHook.mintFor(tiersToMint, user);
|
|
216
|
-
|
|
217
|
-
uint256 blockAfterSecondMint = block.number;
|
|
218
|
-
vm.roll(block.number + 1);
|
|
219
|
-
|
|
220
|
-
// Past votes before second mint = 100 (from first NFT + delegation).
|
|
221
|
-
assertEq(module.getPastVotes(user, blockBeforeSecondMint), 100, "Past votes before second mint should be 100");
|
|
222
|
-
// Past votes after second mint = 200.
|
|
223
|
-
assertEq(module.getPastVotes(user, blockAfterSecondMint), 200, "Past votes after second mint should be 200");
|
|
224
|
-
|
|
225
|
-
// Past total supply.
|
|
226
|
-
assertEq(
|
|
227
|
-
module.getPastTotalSupply(blockBeforeSecondMint), 100, "Past total supply before second mint should be 100"
|
|
228
|
-
);
|
|
229
|
-
assertEq(
|
|
230
|
-
module.getPastTotalSupply(blockAfterSecondMint), 200, "Past total supply after second mint should be 200"
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// -------------------------------------------------------------------
|
|
235
|
-
// Test 7: Multi-tier with different voting units
|
|
236
|
-
// -------------------------------------------------------------------
|
|
237
|
-
function test_multiTier_differentVotingUnits() public {
|
|
238
|
-
defaultTierConfig.flags.allowOwnerMint = true;
|
|
239
|
-
defaultTierConfig.reserveFrequency = 0;
|
|
240
|
-
defaultTierConfig.flags.useVotingUnits = true;
|
|
241
|
-
|
|
242
|
-
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(3);
|
|
243
|
-
|
|
244
|
-
// Set custom voting units per tier.
|
|
245
|
-
tiersHook.test_store().ForTest_setTierVotingUnits(address(tiersHook), 1, 100);
|
|
246
|
-
tiersHook.test_store().ForTest_setTierVotingUnits(address(tiersHook), 2, 200);
|
|
247
|
-
tiersHook.test_store().ForTest_setTierVotingUnits(address(tiersHook), 3, 500);
|
|
248
|
-
|
|
249
|
-
address user = makeAddr("user");
|
|
250
|
-
|
|
251
|
-
// Mint one from tier 1 to test checkpoint tracking.
|
|
252
|
-
uint16[] memory tier1 = new uint16[](1);
|
|
253
|
-
tier1[0] = 1;
|
|
254
|
-
vm.prank(owner);
|
|
255
|
-
tiersHook.mintFor(tier1, user);
|
|
256
|
-
|
|
257
|
-
IJB721Checkpoints module = tiersHook.CHECKPOINTS();
|
|
258
|
-
|
|
259
|
-
// User self-delegates.
|
|
260
|
-
vm.prank(user);
|
|
261
|
-
module.delegate(user);
|
|
262
|
-
|
|
263
|
-
// Mint from remaining tiers.
|
|
264
|
-
uint16[] memory tier2 = new uint16[](1);
|
|
265
|
-
tier2[0] = 2;
|
|
266
|
-
uint16[] memory tier3 = new uint16[](1);
|
|
267
|
-
tier3[0] = 3;
|
|
268
|
-
|
|
269
|
-
vm.startPrank(owner);
|
|
270
|
-
tiersHook.mintFor(tier2, user);
|
|
271
|
-
tiersHook.mintFor(tier3, user);
|
|
272
|
-
vm.stopPrank();
|
|
273
|
-
|
|
274
|
-
// 100 + 200 + 500 = 800.
|
|
275
|
-
assertEq(module.getVotes(user), 800, "User should have 800 checkpointed votes");
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// -------------------------------------------------------------------
|
|
279
|
-
// Test 8: Burn decreases checkpointed total supply
|
|
280
|
-
// -------------------------------------------------------------------
|
|
281
|
-
function test_burn_decreasesTotalSupply() public {
|
|
282
|
-
defaultTierConfig.flags.allowOwnerMint = true;
|
|
283
|
-
defaultTierConfig.reserveFrequency = 0;
|
|
284
|
-
defaultTierConfig.flags.useVotingUnits = true;
|
|
285
|
-
defaultTierConfig.votingUnits = 100;
|
|
286
|
-
|
|
287
|
-
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(1);
|
|
288
|
-
|
|
289
|
-
address user = makeAddr("user");
|
|
290
|
-
|
|
291
|
-
// Mint 2 NFTs (first one deploys CHECKPOINTS lazily).
|
|
292
|
-
uint16[] memory tiersToMint = new uint16[](2);
|
|
293
|
-
tiersToMint[0] = 1;
|
|
294
|
-
tiersToMint[1] = 1;
|
|
295
|
-
vm.prank(owner);
|
|
296
|
-
tiersHook.mintFor(tiersToMint, user);
|
|
297
|
-
|
|
298
|
-
IJB721Checkpoints module = tiersHook.CHECKPOINTS();
|
|
299
|
-
|
|
300
|
-
// User self-delegates.
|
|
301
|
-
vm.prank(user);
|
|
302
|
-
module.delegate(user);
|
|
303
|
-
|
|
304
|
-
assertEq(module.getVotes(user), 200, "User should have 200 votes from 2 NFTs");
|
|
305
|
-
|
|
306
|
-
uint256 blockBeforeBurn = block.number;
|
|
307
|
-
vm.roll(block.number + 1);
|
|
308
|
-
|
|
309
|
-
// Burn one NFT.
|
|
310
|
-
uint256[] memory tokensToBurn = new uint256[](1);
|
|
311
|
-
tokensToBurn[0] = _generateTokenId(1, 1);
|
|
312
|
-
tiersHook.burn(tokensToBurn);
|
|
313
|
-
|
|
314
|
-
assertEq(module.getVotes(user), 100, "User should have 100 votes after burning 1 NFT");
|
|
315
|
-
|
|
316
|
-
vm.roll(block.number + 1);
|
|
317
|
-
|
|
318
|
-
assertEq(module.getPastTotalSupply(blockBeforeBurn), 200, "Past total supply before burn should be 200");
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// -------------------------------------------------------------------
|
|
322
|
-
// Test 9: Unauthorized onTransfer reverts
|
|
323
|
-
// -------------------------------------------------------------------
|
|
324
|
-
function test_unauthorizedOnTransfer_reverts() public {
|
|
325
|
-
defaultTierConfig.flags.allowOwnerMint = true;
|
|
326
|
-
defaultTierConfig.reserveFrequency = 0;
|
|
327
|
-
|
|
328
|
-
ForTest_JB721TiersHook tiersHook = _initializeHookWithCheckpoints(1);
|
|
329
|
-
|
|
330
|
-
// Mint to test checkpoint tracking.
|
|
331
|
-
uint16[] memory tiersToMint = new uint16[](1);
|
|
332
|
-
tiersToMint[0] = 1;
|
|
333
|
-
vm.prank(owner);
|
|
334
|
-
tiersHook.mintFor(tiersToMint, makeAddr("user"));
|
|
335
|
-
|
|
336
|
-
IJB721Checkpoints module = tiersHook.CHECKPOINTS();
|
|
337
|
-
|
|
338
|
-
vm.expectRevert(JB721Checkpoints.JB721Checkpoints_Unauthorized.selector);
|
|
339
|
-
module.onTransfer(address(0), address(1), 1);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
@@ -1,305 +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 {IJB721TiersHookStore} from "../src/interfaces/IJB721TiersHookStore.sol";
|
|
7
|
-
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
8
|
-
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
9
|
-
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
10
|
-
|
|
11
|
-
// =====================================================================
|
|
12
|
-
// Malicious ERC721 receiver that attempts reentrancy via safeTransferFrom
|
|
13
|
-
// =====================================================================
|
|
14
|
-
|
|
15
|
-
/// @notice A malicious ERC721 receiver that attempts to re-enter the hook contract during onERC721Received.
|
|
16
|
-
contract MaliciousReceiver is IERC721Receiver {
|
|
17
|
-
address public target;
|
|
18
|
-
bytes public reentryCalldata;
|
|
19
|
-
bool public reentryAttempted;
|
|
20
|
-
bool public reentrySucceeded;
|
|
21
|
-
|
|
22
|
-
function setReentryTarget(address _target, bytes memory _calldata) external {
|
|
23
|
-
target = _target;
|
|
24
|
-
reentryCalldata = _calldata;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function onERC721Received(address, address, uint256, bytes memory) external override returns (bytes4) {
|
|
28
|
-
if (reentryCalldata.length > 0) {
|
|
29
|
-
reentryAttempted = true;
|
|
30
|
-
(bool success,) = target.call(reentryCalldata);
|
|
31
|
-
reentrySucceeded = success;
|
|
32
|
-
}
|
|
33
|
-
return IERC721Receiver.onERC721Received.selector;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
receive() external payable {}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/// @notice A malicious ERC721 receiver that attempts to transfer the NFT it just received back to itself during
|
|
40
|
-
/// onERC721Received, testing reentrancy via safeTransferFrom.
|
|
41
|
-
contract MaliciousRetransferReceiver is IERC721Receiver {
|
|
42
|
-
address public hookAddress;
|
|
43
|
-
bool public reentryAttempted;
|
|
44
|
-
bool public reentryReverted;
|
|
45
|
-
uint256 public receivedTokenId;
|
|
46
|
-
address public nextRecipient;
|
|
47
|
-
|
|
48
|
-
constructor(address _hook, address _nextRecipient) {
|
|
49
|
-
hookAddress = _hook;
|
|
50
|
-
nextRecipient = _nextRecipient;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function onERC721Received(address, address, uint256 tokenId, bytes memory) external override returns (bytes4) {
|
|
54
|
-
receivedTokenId = tokenId;
|
|
55
|
-
reentryAttempted = true;
|
|
56
|
-
// Attempt to transfer the just-received NFT to another address during the callback.
|
|
57
|
-
try IERC721(hookAddress).safeTransferFrom(address(this), nextRecipient, tokenId) {
|
|
58
|
-
reentryReverted = false;
|
|
59
|
-
} catch {
|
|
60
|
-
reentryReverted = true;
|
|
61
|
-
}
|
|
62
|
-
return IERC721Receiver.onERC721Received.selector;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
receive() external payable {}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// =====================================================================
|
|
69
|
-
// Test Contract: Reentrancy via safeTransferFrom / onERC721Received
|
|
70
|
-
// =====================================================================
|
|
71
|
-
|
|
72
|
-
/// @title TestSafeTransferReentrancy
|
|
73
|
-
/// @notice Tests that malicious ERC721 receivers cannot exploit reentrancy during safeTransferFrom.
|
|
74
|
-
/// @dev The safeTransferFrom flow calls _checkOnERC721Received AFTER the transfer is complete
|
|
75
|
-
/// (state already updated), so the receiver's onERC721Received callback fires with consistent state.
|
|
76
|
-
/// These tests verify that re-entering the hook during that callback does not corrupt state.
|
|
77
|
-
contract TestSafeTransferReentrancy is UnitTestSetup {
|
|
78
|
-
using stdStorage for StdStorage;
|
|
79
|
-
|
|
80
|
-
// ---------------------------------------------------------------
|
|
81
|
-
// Test 1: Malicious receiver tries to re-enter afterPayRecordedWith
|
|
82
|
-
// ---------------------------------------------------------------
|
|
83
|
-
/// @notice A malicious receiver tries to call afterPayRecordedWith when receiving an NFT via safeTransferFrom.
|
|
84
|
-
/// The re-entry fails because the receiver is not registered as a terminal.
|
|
85
|
-
function test_safeTransferFrom_maliciousReceiver_cannotReenterAfterPay() public {
|
|
86
|
-
// Set up hook with tiers and mint an NFT.
|
|
87
|
-
defaultTierConfig.flags.allowOwnerMint = true;
|
|
88
|
-
defaultTierConfig.reserveFrequency = 0;
|
|
89
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(10);
|
|
90
|
-
|
|
91
|
-
// Mint an NFT to the beneficiary.
|
|
92
|
-
uint16[] memory tiersToMint = new uint16[](1);
|
|
93
|
-
tiersToMint[0] = 1;
|
|
94
|
-
vm.prank(owner);
|
|
95
|
-
testHook.mintFor(tiersToMint, beneficiary);
|
|
96
|
-
|
|
97
|
-
uint256 tokenId = _generateTokenId(1, 1);
|
|
98
|
-
assertEq(testHook.ownerOf(tokenId), beneficiary, "Beneficiary should own the NFT");
|
|
99
|
-
|
|
100
|
-
// Deploy the malicious receiver.
|
|
101
|
-
MaliciousReceiver malicious = new MaliciousReceiver();
|
|
102
|
-
|
|
103
|
-
// Build reentrant calldata: try to call afterPayRecordedWith.
|
|
104
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
105
|
-
mintIds[0] = 1;
|
|
106
|
-
|
|
107
|
-
bytes[] memory data = new bytes[](1);
|
|
108
|
-
data[0] = abi.encode(false, mintIds);
|
|
109
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
110
|
-
ids[0] = metadataHelper.getId("pay", address(testHook));
|
|
111
|
-
bytes memory payerMetadata = metadataHelper.createMetadata(ids, data);
|
|
112
|
-
|
|
113
|
-
JBAfterPayRecordedContext memory reentrantContext = JBAfterPayRecordedContext({
|
|
114
|
-
payer: beneficiary,
|
|
115
|
-
projectId: projectId,
|
|
116
|
-
rulesetId: 0,
|
|
117
|
-
amount: JBTokenAmount({
|
|
118
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
119
|
-
value: 1 ether,
|
|
120
|
-
decimals: 18,
|
|
121
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
122
|
-
}),
|
|
123
|
-
forwardedAmount: JBTokenAmount({
|
|
124
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
125
|
-
value: 0,
|
|
126
|
-
decimals: 18,
|
|
127
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
128
|
-
}),
|
|
129
|
-
weight: 10e18,
|
|
130
|
-
newlyIssuedTokenCount: 0,
|
|
131
|
-
beneficiary: address(malicious),
|
|
132
|
-
hookMetadata: bytes(""),
|
|
133
|
-
payerMetadata: payerMetadata
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
malicious.setReentryTarget(address(testHook), abi.encodeCall(testHook.afterPayRecordedWith, (reentrantContext)));
|
|
137
|
-
|
|
138
|
-
// Mock: the malicious receiver is NOT a terminal.
|
|
139
|
-
vm.mockCall(
|
|
140
|
-
address(mockJBDirectory),
|
|
141
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, address(malicious)),
|
|
142
|
-
abi.encode(false)
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
// Transfer the NFT to the malicious receiver via safeTransferFrom.
|
|
146
|
-
vm.prank(beneficiary);
|
|
147
|
-
testHook.safeTransferFrom(beneficiary, address(malicious), tokenId);
|
|
148
|
-
|
|
149
|
-
// Verify the transfer succeeded (state is consistent).
|
|
150
|
-
assertEq(testHook.ownerOf(tokenId), address(malicious), "Malicious receiver should own the NFT");
|
|
151
|
-
|
|
152
|
-
// Verify the reentrancy was attempted but failed.
|
|
153
|
-
assertTrue(malicious.reentryAttempted(), "Reentrancy should have been attempted");
|
|
154
|
-
assertFalse(malicious.reentrySucceeded(), "Reentrancy should have failed");
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ---------------------------------------------------------------
|
|
158
|
-
// Test 2: Malicious receiver tries to re-enter adjustTiers
|
|
159
|
-
// ---------------------------------------------------------------
|
|
160
|
-
/// @notice A malicious receiver tries to call adjustTiers during onERC721Received.
|
|
161
|
-
/// The call is blocked by permission checks.
|
|
162
|
-
function test_safeTransferFrom_maliciousReceiver_cannotReenterAdjustTiers() public {
|
|
163
|
-
// Set up hook with tiers and mint an NFT.
|
|
164
|
-
defaultTierConfig.flags.allowOwnerMint = true;
|
|
165
|
-
defaultTierConfig.reserveFrequency = 0;
|
|
166
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(10);
|
|
167
|
-
|
|
168
|
-
// Mint an NFT to the beneficiary.
|
|
169
|
-
uint16[] memory tiersToMint = new uint16[](1);
|
|
170
|
-
tiersToMint[0] = 1;
|
|
171
|
-
vm.prank(owner);
|
|
172
|
-
testHook.mintFor(tiersToMint, beneficiary);
|
|
173
|
-
|
|
174
|
-
uint256 tokenId = _generateTokenId(1, 1);
|
|
175
|
-
|
|
176
|
-
// Deploy the malicious receiver that tries to remove tiers.
|
|
177
|
-
MaliciousReceiver malicious = new MaliciousReceiver();
|
|
178
|
-
|
|
179
|
-
// Build reentrant calldata: try to remove tier 1 via adjustTiers.
|
|
180
|
-
uint256[] memory tierIdsToRemove = new uint256[](1);
|
|
181
|
-
tierIdsToRemove[0] = 1;
|
|
182
|
-
malicious.setReentryTarget(
|
|
183
|
-
address(testHook), abi.encodeCall(testHook.adjustTiers, (new JB721TierConfig[](0), tierIdsToRemove))
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
// Mock: the malicious receiver does NOT have permission.
|
|
187
|
-
vm.mockCall(mockJBPermissions, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(false));
|
|
188
|
-
|
|
189
|
-
// Transfer the NFT.
|
|
190
|
-
vm.prank(beneficiary);
|
|
191
|
-
testHook.safeTransferFrom(beneficiary, address(malicious), tokenId);
|
|
192
|
-
|
|
193
|
-
// Verify state consistency.
|
|
194
|
-
assertEq(testHook.ownerOf(tokenId), address(malicious), "Malicious receiver should own the NFT");
|
|
195
|
-
assertTrue(malicious.reentryAttempted(), "Reentrancy should have been attempted");
|
|
196
|
-
assertFalse(malicious.reentrySucceeded(), "adjustTiers reentrancy should have failed");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// ---------------------------------------------------------------
|
|
200
|
-
// Test 3: Malicious receiver re-transfers NFT during callback
|
|
201
|
-
// ---------------------------------------------------------------
|
|
202
|
-
/// @notice A malicious receiver tries to transfer the NFT it just received to another address
|
|
203
|
-
/// during onERC721Received. This tests the re-entrant safeTransferFrom scenario.
|
|
204
|
-
/// Since _update completes BEFORE _checkOnERC721Received is called, state is already settled
|
|
205
|
-
/// and a re-transfer should succeed with correct state.
|
|
206
|
-
function test_safeTransferFrom_maliciousReceiver_retransferDuringCallback() public {
|
|
207
|
-
// Set up hook with tiers and mint an NFT.
|
|
208
|
-
defaultTierConfig.flags.allowOwnerMint = true;
|
|
209
|
-
defaultTierConfig.reserveFrequency = 0;
|
|
210
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(10);
|
|
211
|
-
|
|
212
|
-
// Mint an NFT to the beneficiary.
|
|
213
|
-
uint16[] memory tiersToMint = new uint16[](1);
|
|
214
|
-
tiersToMint[0] = 1;
|
|
215
|
-
vm.prank(owner);
|
|
216
|
-
testHook.mintFor(tiersToMint, beneficiary);
|
|
217
|
-
|
|
218
|
-
uint256 tokenId = _generateTokenId(1, 1);
|
|
219
|
-
|
|
220
|
-
// The final recipient is a simple EOA.
|
|
221
|
-
address finalRecipient = makeAddr("finalRecipient");
|
|
222
|
-
|
|
223
|
-
// Deploy the malicious re-transfer receiver.
|
|
224
|
-
MaliciousRetransferReceiver malicious = new MaliciousRetransferReceiver(address(testHook), finalRecipient);
|
|
225
|
-
|
|
226
|
-
// Transfer to the malicious receiver.
|
|
227
|
-
vm.prank(beneficiary);
|
|
228
|
-
testHook.safeTransferFrom(beneficiary, address(malicious), tokenId);
|
|
229
|
-
|
|
230
|
-
// The re-transfer should have succeeded (not reverted) because state was fully settled
|
|
231
|
-
// before onERC721Received was called.
|
|
232
|
-
assertTrue(malicious.reentryAttempted(), "Re-transfer should have been attempted");
|
|
233
|
-
assertFalse(malicious.reentryReverted(), "Re-transfer should have succeeded");
|
|
234
|
-
|
|
235
|
-
// Verify final state: the finalRecipient should own the NFT.
|
|
236
|
-
assertEq(testHook.ownerOf(tokenId), finalRecipient, "Final recipient should own the NFT");
|
|
237
|
-
|
|
238
|
-
// Verify voting units are tracked correctly after the double transfer.
|
|
239
|
-
// Beneficiary should have 0, malicious should have 0, finalRecipient should have the tier's balance.
|
|
240
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
241
|
-
assertEq(
|
|
242
|
-
hookStore.tierBalanceOf(address(testHook), beneficiary, 1), 0, "Beneficiary should have 0 tier balance"
|
|
243
|
-
);
|
|
244
|
-
assertEq(
|
|
245
|
-
hookStore.tierBalanceOf(address(testHook), address(malicious), 1), 0, "Malicious should have 0 tier balance"
|
|
246
|
-
);
|
|
247
|
-
assertEq(
|
|
248
|
-
hookStore.tierBalanceOf(address(testHook), finalRecipient, 1),
|
|
249
|
-
1,
|
|
250
|
-
"Final recipient should have 1 tier balance"
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ---------------------------------------------------------------
|
|
255
|
-
// Test 4: State consistency after safeTransferFrom (no reentrancy)
|
|
256
|
-
// ---------------------------------------------------------------
|
|
257
|
-
/// @notice Verifies that a normal safeTransferFrom to a contract receiver keeps
|
|
258
|
-
/// tier balances, voting units, and firstOwner tracking consistent.
|
|
259
|
-
function test_safeTransferFrom_normalReceiver_stateConsistent() public {
|
|
260
|
-
// Set up hook with tiers and mint an NFT.
|
|
261
|
-
defaultTierConfig.flags.allowOwnerMint = true;
|
|
262
|
-
defaultTierConfig.reserveFrequency = 0;
|
|
263
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(10);
|
|
264
|
-
|
|
265
|
-
// Mint an NFT to the beneficiary.
|
|
266
|
-
uint16[] memory tiersToMint = new uint16[](1);
|
|
267
|
-
tiersToMint[0] = 1;
|
|
268
|
-
vm.prank(owner);
|
|
269
|
-
testHook.mintFor(tiersToMint, beneficiary);
|
|
270
|
-
|
|
271
|
-
uint256 tokenId = _generateTokenId(1, 1);
|
|
272
|
-
|
|
273
|
-
// Deploy a clean receiver (no reentrancy).
|
|
274
|
-
MaliciousReceiver cleanReceiver = new MaliciousReceiver();
|
|
275
|
-
// Don't set any reentrancy target — it will just accept the NFT.
|
|
276
|
-
|
|
277
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
278
|
-
|
|
279
|
-
// Verify pre-transfer state.
|
|
280
|
-
assertEq(
|
|
281
|
-
hookStore.tierBalanceOf(address(testHook), beneficiary, 1), 1, "Beneficiary should have 1 NFT pre-xfer"
|
|
282
|
-
);
|
|
283
|
-
assertEq(
|
|
284
|
-
hookStore.tierBalanceOf(address(testHook), address(cleanReceiver), 1),
|
|
285
|
-
0,
|
|
286
|
-
"Receiver should have 0 NFTs pre-xfer"
|
|
287
|
-
);
|
|
288
|
-
|
|
289
|
-
// Transfer.
|
|
290
|
-
vm.prank(beneficiary);
|
|
291
|
-
testHook.safeTransferFrom(beneficiary, address(cleanReceiver), tokenId);
|
|
292
|
-
|
|
293
|
-
// Verify post-transfer state.
|
|
294
|
-
assertEq(testHook.ownerOf(tokenId), address(cleanReceiver), "Receiver should own the NFT");
|
|
295
|
-
assertEq(
|
|
296
|
-
hookStore.tierBalanceOf(address(testHook), beneficiary, 1), 0, "Beneficiary should have 0 NFTs post-xfer"
|
|
297
|
-
);
|
|
298
|
-
assertEq(
|
|
299
|
-
hookStore.tierBalanceOf(address(testHook), address(cleanReceiver), 1),
|
|
300
|
-
1,
|
|
301
|
-
"Receiver should have 1 NFT post-xfer"
|
|
302
|
-
);
|
|
303
|
-
assertEq(testHook.firstOwnerOf(tokenId), beneficiary, "First owner should still be beneficiary");
|
|
304
|
-
}
|
|
305
|
-
}
|