@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,116 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.28;
|
|
3
|
-
|
|
4
|
-
import {Test} from "forge-std/Test.sol";
|
|
5
|
-
import {JB721CheckpointsDeployer} from "../../src/JB721CheckpointsDeployer.sol";
|
|
6
|
-
import {IJB721CheckpointsDeployer} from "../../src/interfaces/IJB721CheckpointsDeployer.sol";
|
|
7
|
-
import {IJB721Checkpoints} from "../../src/interfaces/IJB721Checkpoints.sol";
|
|
8
|
-
import {IJB721TiersHookStore} from "../../src/interfaces/IJB721TiersHookStore.sol";
|
|
9
|
-
|
|
10
|
-
/// @notice H-1 audit fix: access control tests for JB721CheckpointsDeployer.deploy().
|
|
11
|
-
/// @dev The deployer must only allow the hook address itself (msg.sender == hook) to call deploy().
|
|
12
|
-
contract Test_JB721CheckpointsDeployer_AccessControl is Test {
|
|
13
|
-
JB721CheckpointsDeployer deployer;
|
|
14
|
-
address mockStore;
|
|
15
|
-
|
|
16
|
-
function setUp() public {
|
|
17
|
-
deployer = new JB721CheckpointsDeployer();
|
|
18
|
-
mockStore = makeAddr("mockStore");
|
|
19
|
-
vm.etch(mockStore, new bytes(0x69));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// ═══════════════════════════════════════════════════════════
|
|
23
|
-
// Test 1: Calling deploy() from the hook address succeeds
|
|
24
|
-
// ═══════════════════════════════════════════════════════════
|
|
25
|
-
|
|
26
|
-
/// @notice deploy() succeeds when msg.sender == hook parameter.
|
|
27
|
-
function test_deploy_succeedsWhenCallerIsHook() public {
|
|
28
|
-
address hookAddr = makeAddr("hook");
|
|
29
|
-
|
|
30
|
-
// Call deploy from the hook address, passing itself as the hook parameter.
|
|
31
|
-
vm.prank(hookAddr);
|
|
32
|
-
IJB721Checkpoints module = deployer.deploy(hookAddr, IJB721TiersHookStore(mockStore));
|
|
33
|
-
|
|
34
|
-
// Verify the module was deployed (non-zero address).
|
|
35
|
-
assertTrue(address(module) != address(0), "Module should be deployed");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ═══════════════════════════════════════════════════════════
|
|
39
|
-
// Test 2: Calling deploy() from any other address reverts
|
|
40
|
-
// ═══════════════════════════════════════════════════════════
|
|
41
|
-
|
|
42
|
-
/// @notice deploy() reverts with Unauthorized when msg.sender != hook.
|
|
43
|
-
function test_deploy_revertsWhenCallerIsNotHook() public {
|
|
44
|
-
address hookAddr = makeAddr("hook");
|
|
45
|
-
address attacker = makeAddr("attacker");
|
|
46
|
-
|
|
47
|
-
vm.prank(attacker);
|
|
48
|
-
vm.expectRevert(IJB721CheckpointsDeployer.JB721CheckpointsDeployer_Unauthorized.selector);
|
|
49
|
-
deployer.deploy(hookAddr, IJB721TiersHookStore(mockStore));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/// @notice deploy() reverts when caller passes their own address as hook but is not the actual hook.
|
|
53
|
-
function test_deploy_revertsWhenCallerPassesSelfButIsNotHook() public {
|
|
54
|
-
address attacker = makeAddr("attacker");
|
|
55
|
-
address realHook = makeAddr("realHook");
|
|
56
|
-
|
|
57
|
-
// Attacker tries to pass realHook as the hook parameter but calls from their own address.
|
|
58
|
-
vm.prank(attacker);
|
|
59
|
-
vm.expectRevert(IJB721CheckpointsDeployer.JB721CheckpointsDeployer_Unauthorized.selector);
|
|
60
|
-
deployer.deploy(realHook, IJB721TiersHookStore(mockStore));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ═══════════════════════════════════════════════════════════
|
|
64
|
-
// Test 3: Front-running scenario
|
|
65
|
-
// ═══════════════════════════════════════════════════════════
|
|
66
|
-
|
|
67
|
-
/// @notice An attacker cannot front-run the hook by calling deploy() with the hook's address.
|
|
68
|
-
/// Since msg.sender must equal hook, the attacker's tx reverts.
|
|
69
|
-
function test_deploy_frontRunningRevertsForAttacker() public {
|
|
70
|
-
address hookAddr = makeAddr("hook");
|
|
71
|
-
address frontRunner = makeAddr("frontRunner");
|
|
72
|
-
|
|
73
|
-
// Front-runner attempts to call deploy with the hook's address before the hook does.
|
|
74
|
-
vm.prank(frontRunner);
|
|
75
|
-
vm.expectRevert(IJB721CheckpointsDeployer.JB721CheckpointsDeployer_Unauthorized.selector);
|
|
76
|
-
deployer.deploy(hookAddr, IJB721TiersHookStore(mockStore));
|
|
77
|
-
|
|
78
|
-
// The legitimate hook can still deploy successfully after the failed front-run attempt.
|
|
79
|
-
vm.prank(hookAddr);
|
|
80
|
-
IJB721Checkpoints module = deployer.deploy(hookAddr, IJB721TiersHookStore(mockStore));
|
|
81
|
-
assertTrue(address(module) != address(0), "Hook should deploy successfully after failed front-run");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/// @notice An attacker who calls deploy(attacker, store) from their own address gets a clone
|
|
85
|
-
/// for themselves, but this does NOT affect the real hook's future deployment.
|
|
86
|
-
function test_deploy_attackerDeploysOwnClone_doesNotAffectRealHook() public {
|
|
87
|
-
address hookAddr = makeAddr("hook");
|
|
88
|
-
address attacker = makeAddr("attacker");
|
|
89
|
-
|
|
90
|
-
// Attacker deploys their own clone (msg.sender == attacker == hook param).
|
|
91
|
-
// This succeeds because msg.sender == hook parameter.
|
|
92
|
-
vm.prank(attacker);
|
|
93
|
-
IJB721Checkpoints attackerModule = deployer.deploy(attacker, IJB721TiersHookStore(mockStore));
|
|
94
|
-
assertTrue(address(attackerModule) != address(0), "Attacker can deploy their own clone");
|
|
95
|
-
|
|
96
|
-
// The real hook can still deploy its own clone (different salt = different CREATE2 address).
|
|
97
|
-
vm.prank(hookAddr);
|
|
98
|
-
IJB721Checkpoints hookModule = deployer.deploy(hookAddr, IJB721TiersHookStore(mockStore));
|
|
99
|
-
assertTrue(address(hookModule) != address(0), "Real hook can still deploy its own clone");
|
|
100
|
-
|
|
101
|
-
// The two clones are at different addresses.
|
|
102
|
-
assertTrue(
|
|
103
|
-
address(attackerModule) != address(hookModule),
|
|
104
|
-
"Attacker clone and hook clone should be different addresses"
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/// @notice Fuzz: any random caller that is not the hook reverts.
|
|
109
|
-
function testFuzz_deploy_revertsForArbitraryCaller(address caller, address hookAddr) public {
|
|
110
|
-
vm.assume(caller != hookAddr);
|
|
111
|
-
|
|
112
|
-
vm.prank(caller);
|
|
113
|
-
vm.expectRevert(IJB721CheckpointsDeployer.JB721CheckpointsDeployer_Unauthorized.selector);
|
|
114
|
-
deployer.deploy(hookAddr, IJB721TiersHookStore(mockStore));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
@@ -1,144 +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 "forge-std/Test.sol";
|
|
6
|
-
|
|
7
|
-
import {JB721TiersRulesetMetadataResolver} from "../../src/libraries/JB721TiersRulesetMetadataResolver.sol";
|
|
8
|
-
import {JB721TiersRulesetMetadata} from "../../src/structs/JB721TiersRulesetMetadata.sol";
|
|
9
|
-
|
|
10
|
-
/// @notice Unit + fuzz tests for `JB721TiersRulesetMetadataResolver`.
|
|
11
|
-
contract TestJB721TiersRulesetMetadataResolver is Test {
|
|
12
|
-
//*********************************************************************//
|
|
13
|
-
// --- pack: individual flags ---------------------------------------- //
|
|
14
|
-
//*********************************************************************//
|
|
15
|
-
|
|
16
|
-
function test_pack_allFalse() public {
|
|
17
|
-
JB721TiersRulesetMetadata memory meta =
|
|
18
|
-
JB721TiersRulesetMetadata({pauseTransfers: false, pauseMintPendingReserves: false});
|
|
19
|
-
uint256 packed = JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(meta);
|
|
20
|
-
assertEq(packed, 0, "both false should pack to 0");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function test_pack_pauseTransfersOnly() public {
|
|
24
|
-
JB721TiersRulesetMetadata memory meta =
|
|
25
|
-
JB721TiersRulesetMetadata({pauseTransfers: true, pauseMintPendingReserves: false});
|
|
26
|
-
uint256 packed = JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(meta);
|
|
27
|
-
assertEq(packed, 1, "pauseTransfers only should pack to 1");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function test_pack_pauseMintPendingReservesOnly() public {
|
|
31
|
-
JB721TiersRulesetMetadata memory meta =
|
|
32
|
-
JB721TiersRulesetMetadata({pauseTransfers: false, pauseMintPendingReserves: true});
|
|
33
|
-
uint256 packed = JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(meta);
|
|
34
|
-
assertEq(packed, 2, "pauseMintPendingReserves only should pack to 2");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function test_pack_bothTrue() public {
|
|
38
|
-
JB721TiersRulesetMetadata memory meta =
|
|
39
|
-
JB721TiersRulesetMetadata({pauseTransfers: true, pauseMintPendingReserves: true});
|
|
40
|
-
uint256 packed = JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(meta);
|
|
41
|
-
assertEq(packed, 3, "both true should pack to 3");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
//*********************************************************************//
|
|
45
|
-
// --- transfersPaused / mintPendingReservesPaused -------------------- //
|
|
46
|
-
//*********************************************************************//
|
|
47
|
-
|
|
48
|
-
function test_transfersPaused() public {
|
|
49
|
-
assertFalse(JB721TiersRulesetMetadataResolver.transfersPaused(0));
|
|
50
|
-
assertTrue(JB721TiersRulesetMetadataResolver.transfersPaused(1));
|
|
51
|
-
assertFalse(JB721TiersRulesetMetadataResolver.transfersPaused(2));
|
|
52
|
-
assertTrue(JB721TiersRulesetMetadataResolver.transfersPaused(3));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function test_mintPendingReservesPaused() public {
|
|
56
|
-
assertFalse(JB721TiersRulesetMetadataResolver.mintPendingReservesPaused(0));
|
|
57
|
-
assertFalse(JB721TiersRulesetMetadataResolver.mintPendingReservesPaused(1));
|
|
58
|
-
assertTrue(JB721TiersRulesetMetadataResolver.mintPendingReservesPaused(2));
|
|
59
|
-
assertTrue(JB721TiersRulesetMetadataResolver.mintPendingReservesPaused(3));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
//*********************************************************************//
|
|
63
|
-
// --- expandMetadata ------------------------------------------------ //
|
|
64
|
-
//*********************************************************************//
|
|
65
|
-
|
|
66
|
-
function test_expandMetadata_zero() public {
|
|
67
|
-
JB721TiersRulesetMetadata memory meta = JB721TiersRulesetMetadataResolver.expandMetadata(0);
|
|
68
|
-
assertFalse(meta.pauseTransfers);
|
|
69
|
-
assertFalse(meta.pauseMintPendingReserves);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function test_expandMetadata_one() public {
|
|
73
|
-
JB721TiersRulesetMetadata memory meta = JB721TiersRulesetMetadataResolver.expandMetadata(1);
|
|
74
|
-
assertTrue(meta.pauseTransfers);
|
|
75
|
-
assertFalse(meta.pauseMintPendingReserves);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function test_expandMetadata_two() public {
|
|
79
|
-
JB721TiersRulesetMetadata memory meta = JB721TiersRulesetMetadataResolver.expandMetadata(2);
|
|
80
|
-
assertFalse(meta.pauseTransfers);
|
|
81
|
-
assertTrue(meta.pauseMintPendingReserves);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function test_expandMetadata_three() public {
|
|
85
|
-
JB721TiersRulesetMetadata memory meta = JB721TiersRulesetMetadataResolver.expandMetadata(3);
|
|
86
|
-
assertTrue(meta.pauseTransfers);
|
|
87
|
-
assertTrue(meta.pauseMintPendingReserves);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
//*********************************************************************//
|
|
91
|
-
// --- Round-Trip ----------------------------------------------------- //
|
|
92
|
-
//*********************************************************************//
|
|
93
|
-
|
|
94
|
-
function test_packExpandRoundTrip_allCombinations() public {
|
|
95
|
-
for (uint256 i; i < 4; i++) {
|
|
96
|
-
bool transfers = (i & 1) == 1;
|
|
97
|
-
bool reserves = (i & 2) == 2;
|
|
98
|
-
|
|
99
|
-
JB721TiersRulesetMetadata memory meta =
|
|
100
|
-
JB721TiersRulesetMetadata({pauseTransfers: transfers, pauseMintPendingReserves: reserves});
|
|
101
|
-
|
|
102
|
-
uint256 packed = JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(meta);
|
|
103
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
104
|
-
JB721TiersRulesetMetadata memory expanded = JB721TiersRulesetMetadataResolver.expandMetadata(uint16(packed));
|
|
105
|
-
|
|
106
|
-
assertEq(expanded.pauseTransfers, transfers, "transfers round-trip");
|
|
107
|
-
assertEq(expanded.pauseMintPendingReserves, reserves, "reserves round-trip");
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
//*********************************************************************//
|
|
112
|
-
// --- Fuzz ---------------------------------------------------------- //
|
|
113
|
-
//*********************************************************************//
|
|
114
|
-
|
|
115
|
-
function testFuzz_packExpandRoundTrip(bool pauseTransfers, bool pauseMintPendingReserves) public {
|
|
116
|
-
JB721TiersRulesetMetadata memory meta = JB721TiersRulesetMetadata({
|
|
117
|
-
pauseTransfers: pauseTransfers, pauseMintPendingReserves: pauseMintPendingReserves
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
uint256 packed = JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(meta);
|
|
121
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
122
|
-
JB721TiersRulesetMetadata memory expanded = JB721TiersRulesetMetadataResolver.expandMetadata(uint16(packed));
|
|
123
|
-
|
|
124
|
-
assertEq(expanded.pauseTransfers, pauseTransfers, "fuzz transfers round-trip");
|
|
125
|
-
assertEq(expanded.pauseMintPendingReserves, pauseMintPendingReserves, "fuzz reserves round-trip");
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function testFuzz_transfersPaused_bitIsolation(uint256 data) public {
|
|
129
|
-
bool result = JB721TiersRulesetMetadataResolver.transfersPaused(data);
|
|
130
|
-
assertEq(result, (data & 1) == 1, "transfersPaused should check bit 0");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function testFuzz_mintPendingReservesPaused_bitIsolation(uint256 data) public {
|
|
134
|
-
bool result = JB721TiersRulesetMetadataResolver.mintPendingReservesPaused(data);
|
|
135
|
-
assertEq(result, ((data >> 1) & 1) == 1, "mintPendingReservesPaused should check bit 1");
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function testFuzz_pack_onlyUsesLow2Bits(bool a, bool b) public {
|
|
139
|
-
JB721TiersRulesetMetadata memory meta =
|
|
140
|
-
JB721TiersRulesetMetadata({pauseTransfers: a, pauseMintPendingReserves: b});
|
|
141
|
-
uint256 packed = JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(meta);
|
|
142
|
-
assertEq(packed & ~uint256(3), 0, "packed value should only use bits 0 and 1");
|
|
143
|
-
}
|
|
144
|
-
}
|
package/test/unit/JBBitmap.t.sol
DELETED
|
@@ -1,170 +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 "forge-std/Test.sol";
|
|
6
|
-
|
|
7
|
-
import {JBBitmap} from "../../src/libraries/JBBitmap.sol";
|
|
8
|
-
import {JBBitmapWord} from "../../src/structs/JBBitmapWord.sol";
|
|
9
|
-
|
|
10
|
-
/// @notice Unit + fuzz tests for `JBBitmap`.
|
|
11
|
-
contract TestJBBitmap is Test {
|
|
12
|
-
using JBBitmap for mapping(uint256 => uint256);
|
|
13
|
-
using JBBitmap for JBBitmapWord;
|
|
14
|
-
|
|
15
|
-
mapping(uint256 => uint256) internal bitmap;
|
|
16
|
-
|
|
17
|
-
//*********************************************************************//
|
|
18
|
-
// --- readId -------------------------------------------------------- //
|
|
19
|
-
//*********************************************************************//
|
|
20
|
-
|
|
21
|
-
function test_readId_initiallyZero() public {
|
|
22
|
-
JBBitmapWord memory word = bitmap.readId(0);
|
|
23
|
-
assertEq(word.currentWord, 0, "initial word should be 0");
|
|
24
|
-
assertEq(word.currentDepth, 0, "depth for index 0 should be 0");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function test_readId_depthCalculation() public {
|
|
28
|
-
// Index 255 is in depth 0, index 256 is in depth 1.
|
|
29
|
-
JBBitmapWord memory word0 = bitmap.readId(255);
|
|
30
|
-
assertEq(word0.currentDepth, 0, "index 255 should be depth 0");
|
|
31
|
-
|
|
32
|
-
JBBitmapWord memory word1 = bitmap.readId(256);
|
|
33
|
-
assertEq(word1.currentDepth, 1, "index 256 should be depth 1");
|
|
34
|
-
|
|
35
|
-
JBBitmapWord memory word2 = bitmap.readId(512);
|
|
36
|
-
assertEq(word2.currentDepth, 2, "index 512 should be depth 2");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
//*********************************************************************//
|
|
40
|
-
// --- removeTier / isTierIdRemoved ---------------------------------- //
|
|
41
|
-
//*********************************************************************//
|
|
42
|
-
|
|
43
|
-
function test_removeTier_setsbit() public {
|
|
44
|
-
assertFalse(bitmap.isTierIdRemoved(5), "should not be removed initially");
|
|
45
|
-
|
|
46
|
-
bitmap.removeTier(5);
|
|
47
|
-
|
|
48
|
-
assertTrue(bitmap.isTierIdRemoved(5), "should be removed after removeTier");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function test_removeTier_doesNotAffectOtherBits() public {
|
|
52
|
-
bitmap.removeTier(5);
|
|
53
|
-
|
|
54
|
-
assertFalse(bitmap.isTierIdRemoved(4), "adjacent bit should not be affected");
|
|
55
|
-
assertFalse(bitmap.isTierIdRemoved(6), "adjacent bit should not be affected");
|
|
56
|
-
assertFalse(bitmap.isTierIdRemoved(0), "index 0 should not be affected");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function test_removeTier_multipleBitsInSameWord() public {
|
|
60
|
-
bitmap.removeTier(0);
|
|
61
|
-
bitmap.removeTier(1);
|
|
62
|
-
bitmap.removeTier(255);
|
|
63
|
-
|
|
64
|
-
assertTrue(bitmap.isTierIdRemoved(0));
|
|
65
|
-
assertTrue(bitmap.isTierIdRemoved(1));
|
|
66
|
-
assertTrue(bitmap.isTierIdRemoved(255));
|
|
67
|
-
assertFalse(bitmap.isTierIdRemoved(2));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function test_removeTier_acrossWords() public {
|
|
71
|
-
bitmap.removeTier(0); // depth 0
|
|
72
|
-
bitmap.removeTier(256); // depth 1
|
|
73
|
-
bitmap.removeTier(512); // depth 2
|
|
74
|
-
|
|
75
|
-
assertTrue(bitmap.isTierIdRemoved(0));
|
|
76
|
-
assertTrue(bitmap.isTierIdRemoved(256));
|
|
77
|
-
assertTrue(bitmap.isTierIdRemoved(512));
|
|
78
|
-
|
|
79
|
-
assertFalse(bitmap.isTierIdRemoved(1));
|
|
80
|
-
assertFalse(bitmap.isTierIdRemoved(257));
|
|
81
|
-
assertFalse(bitmap.isTierIdRemoved(513));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function test_removeTier_idempotent() public {
|
|
85
|
-
bitmap.removeTier(10);
|
|
86
|
-
bitmap.removeTier(10); // Remove again.
|
|
87
|
-
assertTrue(bitmap.isTierIdRemoved(10), "should still be removed");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
//*********************************************************************//
|
|
91
|
-
// --- isTierIdRemoved (memory struct variant) ----------------------- //
|
|
92
|
-
//*********************************************************************//
|
|
93
|
-
|
|
94
|
-
function test_isTierIdRemoved_memoryStruct() public {
|
|
95
|
-
bitmap.removeTier(3);
|
|
96
|
-
|
|
97
|
-
JBBitmapWord memory word = bitmap.readId(3);
|
|
98
|
-
assertTrue(word.isTierIdRemoved(3), "memory struct should read removed bit");
|
|
99
|
-
assertFalse(word.isTierIdRemoved(4), "memory struct should read non-removed bit");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function test_isTierIdRemoved_wrongDepthReturnsWrong() public {
|
|
103
|
-
bitmap.removeTier(3); // depth 0
|
|
104
|
-
|
|
105
|
-
// Read a word from depth 1 — should not see index 3's removal.
|
|
106
|
-
JBBitmapWord memory word = bitmap.readId(256);
|
|
107
|
-
assertFalse(word.isTierIdRemoved(3), "wrong depth should not see bit");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
//*********************************************************************//
|
|
111
|
-
// --- refreshBitmapNeeded ------------------------------------------- //
|
|
112
|
-
//*********************************************************************//
|
|
113
|
-
|
|
114
|
-
function test_refreshBitmapNeeded_sameDepth() public {
|
|
115
|
-
JBBitmapWord memory word = bitmap.readId(0);
|
|
116
|
-
assertFalse(word.refreshBitmapNeeded(100), "same depth should not need refresh");
|
|
117
|
-
assertFalse(word.refreshBitmapNeeded(255), "still depth 0, no refresh needed");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function test_refreshBitmapNeeded_differentDepth() public {
|
|
121
|
-
JBBitmapWord memory word = bitmap.readId(0);
|
|
122
|
-
assertTrue(word.refreshBitmapNeeded(256), "depth 1 should need refresh from depth 0");
|
|
123
|
-
assertTrue(word.refreshBitmapNeeded(512), "depth 2 should need refresh from depth 0");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
//*********************************************************************//
|
|
127
|
-
// --- Fuzz Tests ---------------------------------------------------- //
|
|
128
|
-
//*********************************************************************//
|
|
129
|
-
|
|
130
|
-
function testFuzz_removeTier_roundTrip(uint16 index) public {
|
|
131
|
-
assertFalse(bitmap.isTierIdRemoved(index), "should start unremoved");
|
|
132
|
-
|
|
133
|
-
bitmap.removeTier(index);
|
|
134
|
-
|
|
135
|
-
assertTrue(bitmap.isTierIdRemoved(index), "should be removed after removeTier");
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function testFuzz_removeTier_isolatedBit(uint16 indexA, uint16 indexB) public {
|
|
139
|
-
vm.assume(indexA != indexB);
|
|
140
|
-
|
|
141
|
-
bitmap.removeTier(indexA);
|
|
142
|
-
|
|
143
|
-
assertTrue(bitmap.isTierIdRemoved(indexA), "A should be removed");
|
|
144
|
-
assertFalse(bitmap.isTierIdRemoved(indexB), "B should not be removed");
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function testFuzz_readId_depthMatchesIndex(uint16 index) public {
|
|
148
|
-
JBBitmapWord memory word = bitmap.readId(index);
|
|
149
|
-
assertEq(word.currentDepth, uint256(index) >> 8, "depth should be index / 256");
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function testFuzz_refreshBitmapNeeded_consistency(uint16 indexA, uint16 indexB) public {
|
|
153
|
-
JBBitmapWord memory word = bitmap.readId(indexA);
|
|
154
|
-
bool needed = word.refreshBitmapNeeded(indexB);
|
|
155
|
-
// Refresh is needed iff depths differ.
|
|
156
|
-
assertEq(needed, (uint256(indexA) >> 8) != (uint256(indexB) >> 8), "refresh iff different depth");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function testFuzz_removeTier_multipleBits(uint8 a, uint8 b, uint8 c) public {
|
|
160
|
-
vm.assume(a != b && b != c && a != c);
|
|
161
|
-
|
|
162
|
-
bitmap.removeTier(a);
|
|
163
|
-
bitmap.removeTier(b);
|
|
164
|
-
bitmap.removeTier(c);
|
|
165
|
-
|
|
166
|
-
assertTrue(bitmap.isTierIdRemoved(a));
|
|
167
|
-
assertTrue(bitmap.isTierIdRemoved(b));
|
|
168
|
-
assertTrue(bitmap.isTierIdRemoved(c));
|
|
169
|
-
}
|
|
170
|
-
}
|
|
@@ -1,136 +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 "forge-std/Test.sol";
|
|
6
|
-
|
|
7
|
-
import {JBIpfsDecoder} from "../../src/libraries/JBIpfsDecoder.sol";
|
|
8
|
-
|
|
9
|
-
/// @notice Unit tests for `JBIpfsDecoder`.
|
|
10
|
-
contract TestJBIpfsDecoder is Test {
|
|
11
|
-
/// @notice Known IPFS CID v0 hash for testing.
|
|
12
|
-
/// @dev Obtained by hashing a known payload: the CID for the hex 0x1220... prefix + this hash should decode to a
|
|
13
|
-
/// valid base58 string starting with "Qm".
|
|
14
|
-
bytes32 constant TEST_HASH = 0x7465737468617368000000000000000000000000000000000000000000000000;
|
|
15
|
-
|
|
16
|
-
//*********************************************************************//
|
|
17
|
-
// --- decode: basic output ------------------------------------------ //
|
|
18
|
-
//*********************************************************************//
|
|
19
|
-
|
|
20
|
-
function test_decode_prependsBaseUri() public {
|
|
21
|
-
string memory result = JBIpfsDecoder.decode("ipfs://", TEST_HASH);
|
|
22
|
-
// Result must start with the base URI.
|
|
23
|
-
bytes memory resultBytes = bytes(result);
|
|
24
|
-
bytes memory prefix = bytes("ipfs://");
|
|
25
|
-
for (uint256 i; i < prefix.length; i++) {
|
|
26
|
-
assertEq(resultBytes[i], prefix[i], "prefix mismatch");
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function test_decode_emptyBaseUri() public {
|
|
31
|
-
string memory result = JBIpfsDecoder.decode("", TEST_HASH);
|
|
32
|
-
// Should still produce a non-empty base58 hash.
|
|
33
|
-
assertTrue(bytes(result).length > 0, "should produce output with empty base URI");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function test_decode_outputStartsWithQm() public {
|
|
37
|
-
// All CIDv0 hashes start with "Qm" because the 0x1220 prefix encodes to "Qm" in base58.
|
|
38
|
-
string memory result = JBIpfsDecoder.decode("", TEST_HASH);
|
|
39
|
-
bytes memory resultBytes = bytes(result);
|
|
40
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
41
|
-
assertEq(resultBytes[0], bytes1("Q"), "first char should be Q");
|
|
42
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
43
|
-
assertEq(resultBytes[1], bytes1("m"), "second char should be m");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function test_decode_outputLength() public {
|
|
47
|
-
// CIDv0 hashes are always 46 characters in base58.
|
|
48
|
-
string memory result = JBIpfsDecoder.decode("", TEST_HASH);
|
|
49
|
-
assertEq(bytes(result).length, 46, "CIDv0 base58 hash should be 46 characters");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
//*********************************************************************//
|
|
53
|
-
// --- decode: determinism ------------------------------------------- //
|
|
54
|
-
//*********************************************************************//
|
|
55
|
-
|
|
56
|
-
function test_decode_deterministic() public {
|
|
57
|
-
string memory a = JBIpfsDecoder.decode("ipfs://", TEST_HASH);
|
|
58
|
-
string memory b = JBIpfsDecoder.decode("ipfs://", TEST_HASH);
|
|
59
|
-
assertEq(keccak256(bytes(a)), keccak256(bytes(b)), "same input should produce same output");
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function test_decode_differentHashesDifferentOutput() public {
|
|
63
|
-
bytes32 hashA = bytes32(uint256(1));
|
|
64
|
-
bytes32 hashB = bytes32(uint256(2));
|
|
65
|
-
string memory a = JBIpfsDecoder.decode("", hashA);
|
|
66
|
-
string memory b = JBIpfsDecoder.decode("", hashB);
|
|
67
|
-
assertTrue(keccak256(bytes(a)) != keccak256(bytes(b)), "different hashes should produce different output");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
//*********************************************************************//
|
|
71
|
-
// --- decode: base58 alphabet --------------------------------------- //
|
|
72
|
-
//*********************************************************************//
|
|
73
|
-
|
|
74
|
-
function test_decode_onlyBase58Chars() public {
|
|
75
|
-
string memory result = JBIpfsDecoder.decode("", TEST_HASH);
|
|
76
|
-
bytes memory resultBytes = bytes(result);
|
|
77
|
-
bytes memory alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
78
|
-
|
|
79
|
-
for (uint256 i; i < resultBytes.length; i++) {
|
|
80
|
-
bool found;
|
|
81
|
-
for (uint256 j; j < alphabet.length; j++) {
|
|
82
|
-
if (resultBytes[i] == alphabet[j]) {
|
|
83
|
-
found = true;
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
assertTrue(found, "output should only contain base58 characters");
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
//*********************************************************************//
|
|
92
|
-
// --- decode: fuzz -------------------------------------------------- //
|
|
93
|
-
//*********************************************************************//
|
|
94
|
-
|
|
95
|
-
function testFuzz_decode_alwaysProduces46Chars(bytes32 hash) public {
|
|
96
|
-
string memory result = JBIpfsDecoder.decode("", hash);
|
|
97
|
-
assertEq(bytes(result).length, 46, "any hash should produce 46-char CIDv0");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function testFuzz_decode_alwaysStartsWithQm(bytes32 hash) public {
|
|
101
|
-
string memory result = JBIpfsDecoder.decode("", hash);
|
|
102
|
-
bytes memory resultBytes = bytes(result);
|
|
103
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
104
|
-
assertEq(resultBytes[0], bytes1("Q"), "first char should be Q");
|
|
105
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
106
|
-
assertEq(resultBytes[1], bytes1("m"), "second char should be m");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function testFuzz_decode_prependsBaseUri(bytes32 hash, uint8 baseLen) public {
|
|
110
|
-
// Create a base URI of varying length (0-255 chars).
|
|
111
|
-
bytes memory base = new bytes(baseLen);
|
|
112
|
-
for (uint256 i; i < baseLen; i++) {
|
|
113
|
-
base[i] = "x";
|
|
114
|
-
}
|
|
115
|
-
string memory baseUri = string(base);
|
|
116
|
-
string memory result = JBIpfsDecoder.decode(baseUri, hash);
|
|
117
|
-
assertEq(bytes(result).length, uint256(baseLen) + 46, "output length = base + 46");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function testFuzz_decode_onlyBase58Chars(bytes32 hash) public {
|
|
121
|
-
string memory result = JBIpfsDecoder.decode("", hash);
|
|
122
|
-
bytes memory resultBytes = bytes(result);
|
|
123
|
-
bytes memory alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
124
|
-
|
|
125
|
-
for (uint256 i; i < resultBytes.length; i++) {
|
|
126
|
-
bool found;
|
|
127
|
-
for (uint256 j; j < alphabet.length; j++) {
|
|
128
|
-
if (resultBytes[i] == alphabet[j]) {
|
|
129
|
-
found = true;
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
assertTrue(found, "all chars should be in base58 alphabet");
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|