@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,165 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.28;
|
|
3
|
-
|
|
4
|
-
import {CommonBase} from "forge-std/Base.sol";
|
|
5
|
-
import {StdCheats} from "forge-std/StdCheats.sol";
|
|
6
|
-
import {StdUtils} from "forge-std/StdUtils.sol";
|
|
7
|
-
|
|
8
|
-
import {JB721TiersHookStore} from "../../../src/JB721TiersHookStore.sol";
|
|
9
|
-
import {JB721TierConfig} from "../../../src/structs/JB721TierConfig.sol";
|
|
10
|
-
import {JB721TierConfigFlags} from "../../../src/structs/JB721TierConfigFlags.sol";
|
|
11
|
-
// forge-lint: disable-next-line(unused-import)
|
|
12
|
-
import {JB721TiersHookFlags} from "../../../src/structs/JB721TiersHookFlags.sol";
|
|
13
|
-
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
14
|
-
|
|
15
|
-
/// @notice Handler for JB721TiersHookStore invariant tests.
|
|
16
|
-
/// @dev Acts as the "hook" address itself, so msg.sender (this) == hook in the store.
|
|
17
|
-
contract TierStoreHandler is CommonBase, StdCheats, StdUtils {
|
|
18
|
-
JB721TiersHookStore public immutable STORE;
|
|
19
|
-
|
|
20
|
-
// This contract acts as the hook.
|
|
21
|
-
// forge-lint: disable-next-line(mixed-case-variable)
|
|
22
|
-
address public HOOK;
|
|
23
|
-
|
|
24
|
-
// Ghost variable tracking the max tier ID seen.
|
|
25
|
-
uint256 public lowestMaxTierIdSeen;
|
|
26
|
-
|
|
27
|
-
// Track how many tiers we've added.
|
|
28
|
-
uint256 public tiersAdded;
|
|
29
|
-
|
|
30
|
-
// Track minted token IDs so we can only burn minted tokens.
|
|
31
|
-
uint256[] public mintedTokenIds;
|
|
32
|
-
|
|
33
|
-
// Track burned token IDs to avoid double-burn.
|
|
34
|
-
mapping(uint256 => bool) public wasBurned;
|
|
35
|
-
|
|
36
|
-
constructor(JB721TiersHookStore store) {
|
|
37
|
-
STORE = store;
|
|
38
|
-
HOOK = address(this);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/// @notice Add a new tier.
|
|
42
|
-
function addTier(uint104 price, uint32 initialSupply, uint16 reserveFrequency, uint24 category) public {
|
|
43
|
-
// Bound inputs to valid ranges.
|
|
44
|
-
initialSupply = uint32(bound(initialSupply, 1, 1_000_000));
|
|
45
|
-
price = uint104(bound(price, 1, type(uint104).max));
|
|
46
|
-
category = uint24(bound(category, 0, 100));
|
|
47
|
-
|
|
48
|
-
// Reserve frequency must be <= initialSupply if non-zero.
|
|
49
|
-
if (reserveFrequency > 0) {
|
|
50
|
-
reserveFrequency = uint16(bound(reserveFrequency, 1, 200));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
JB721TierConfig[] memory configs = new JB721TierConfig[](1);
|
|
54
|
-
configs[0] = JB721TierConfig({
|
|
55
|
-
price: price,
|
|
56
|
-
initialSupply: initialSupply,
|
|
57
|
-
votingUnits: 0,
|
|
58
|
-
reserveFrequency: reserveFrequency,
|
|
59
|
-
reserveBeneficiary: address(0),
|
|
60
|
-
encodedIPFSUri: bytes32(0),
|
|
61
|
-
category: category,
|
|
62
|
-
discountPercent: 0,
|
|
63
|
-
flags: JB721TierConfigFlags({
|
|
64
|
-
allowOwnerMint: true,
|
|
65
|
-
useReserveBeneficiaryAsDefault: false,
|
|
66
|
-
transfersPausable: false,
|
|
67
|
-
useVotingUnits: false,
|
|
68
|
-
cantBeRemoved: false,
|
|
69
|
-
cantIncreaseDiscountPercent: false,
|
|
70
|
-
cantBuyWithCredits: false
|
|
71
|
-
}),
|
|
72
|
-
splitPercent: 0,
|
|
73
|
-
splits: new JBSplit[](0)
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
try this._doAddTiers(configs) {
|
|
77
|
-
tiersAdded++;
|
|
78
|
-
_updateMaxTierIdSeen();
|
|
79
|
-
} catch {}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/// @dev External wrapper so calldata encoding is correct for the store.
|
|
83
|
-
function _doAddTiers(JB721TierConfig[] calldata configs) external {
|
|
84
|
-
STORE.recordAddTiers(configs);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/// @notice Remove a tier.
|
|
88
|
-
function removeTier(uint256 tierId) public {
|
|
89
|
-
uint256 maxId = STORE.maxTierIdOf(HOOK);
|
|
90
|
-
if (maxId == 0) return;
|
|
91
|
-
|
|
92
|
-
tierId = bound(tierId, 1, maxId);
|
|
93
|
-
|
|
94
|
-
uint256[] memory ids = new uint256[](1);
|
|
95
|
-
ids[0] = tierId;
|
|
96
|
-
|
|
97
|
-
try this._doRemoveTiers(ids) {} catch {}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/// @dev External wrapper for calldata.
|
|
101
|
-
function _doRemoveTiers(uint256[] calldata ids) external {
|
|
102
|
-
STORE.recordRemoveTierIds(ids);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/// @notice Mint from a tier (simulates store recording a mint).
|
|
106
|
-
function mint(uint256 tierId) public {
|
|
107
|
-
uint256 maxId = STORE.maxTierIdOf(HOOK);
|
|
108
|
-
if (maxId == 0) return;
|
|
109
|
-
|
|
110
|
-
tierId = bound(tierId, 1, maxId);
|
|
111
|
-
|
|
112
|
-
uint16[] memory tierIds = new uint16[](1);
|
|
113
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
114
|
-
tierIds[0] = uint16(tierId);
|
|
115
|
-
|
|
116
|
-
try this._doMint(type(uint256).max, tierIds) returns (uint256[] memory tokenIds) {
|
|
117
|
-
for (uint256 i; i < tokenIds.length; i++) {
|
|
118
|
-
if (tokenIds[i] != 0) {
|
|
119
|
-
mintedTokenIds.push(tokenIds[i]);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
} catch {}
|
|
123
|
-
|
|
124
|
-
_updateMaxTierIdSeen();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/// @dev External wrapper for calldata.
|
|
128
|
-
function _doMint(uint256 amount, uint16[] calldata tierIds) external returns (uint256[] memory tokenIds) {
|
|
129
|
-
(tokenIds,,) = STORE.recordMint(amount, tierIds, true);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/// @notice Burn a minted token.
|
|
133
|
-
function burn(uint256 indexSeed) public {
|
|
134
|
-
if (mintedTokenIds.length == 0) return;
|
|
135
|
-
|
|
136
|
-
uint256 index = bound(indexSeed, 0, mintedTokenIds.length - 1);
|
|
137
|
-
uint256 tokenId = mintedTokenIds[index];
|
|
138
|
-
|
|
139
|
-
// Skip if already burned.
|
|
140
|
-
if (wasBurned[tokenId]) return;
|
|
141
|
-
|
|
142
|
-
uint256[] memory tokenIds = new uint256[](1);
|
|
143
|
-
tokenIds[0] = tokenId;
|
|
144
|
-
|
|
145
|
-
try this._doBurn(tokenIds) {
|
|
146
|
-
wasBurned[tokenId] = true;
|
|
147
|
-
} catch {}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/// @dev External wrapper for calldata.
|
|
151
|
-
function _doBurn(uint256[] calldata tokenIds) external {
|
|
152
|
-
STORE.recordBurn(tokenIds);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function mintedTokenCount() external view returns (uint256) {
|
|
156
|
-
return mintedTokenIds.length;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function _updateMaxTierIdSeen() internal {
|
|
160
|
-
uint256 current = STORE.maxTierIdOf(HOOK);
|
|
161
|
-
if (lowestMaxTierIdSeen == 0 || current > lowestMaxTierIdSeen) {
|
|
162
|
-
lowestMaxTierIdSeen = current;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
@@ -1,277 +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
|
-
// forge-lint: disable-next-line(unused-import)
|
|
8
|
-
import {JB721TiersHookLib} from "../../src/libraries/JB721TiersHookLib.sol";
|
|
9
|
-
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
10
|
-
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
11
|
-
import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
|
|
12
|
-
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
13
|
-
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
14
|
-
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
15
|
-
|
|
16
|
-
/// @notice Regression tests: a broken project terminal in _addToBalance now reverts the payment (M-4 fix).
|
|
17
|
-
contract Test_BrokenTerminalDoesNotDos is UnitTestSetup {
|
|
18
|
-
using stdStorage for StdStorage;
|
|
19
|
-
|
|
20
|
-
function setUp() public override {
|
|
21
|
-
super.setUp();
|
|
22
|
-
vm.etch(mockJBSplits, new bytes(0x69));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Helper: build payer metadata for tier IDs.
|
|
26
|
-
function _buildPayerMetadata(
|
|
27
|
-
address hookAddress,
|
|
28
|
-
uint16[] memory tierIdsToMint
|
|
29
|
-
)
|
|
30
|
-
internal
|
|
31
|
-
view
|
|
32
|
-
returns (bytes memory)
|
|
33
|
-
{
|
|
34
|
-
bytes[] memory data = new bytes[](1);
|
|
35
|
-
data[0] = abi.encode(false, tierIdsToMint);
|
|
36
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
37
|
-
ids[0] = metadataHelper.getId("pay", hookAddress);
|
|
38
|
-
return metadataHelper.createMetadata(ids, data);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/// @notice Helper to build an ERC-20 afterPay context (reduces stack depth).
|
|
42
|
-
function _buildErc20PayContext(
|
|
43
|
-
address hookAddress,
|
|
44
|
-
address token,
|
|
45
|
-
uint32 currency,
|
|
46
|
-
uint8 decimals,
|
|
47
|
-
uint256[] memory tierIds,
|
|
48
|
-
uint256 amount
|
|
49
|
-
)
|
|
50
|
-
internal
|
|
51
|
-
view
|
|
52
|
-
returns (JBAfterPayRecordedContext memory)
|
|
53
|
-
{
|
|
54
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
55
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
56
|
-
bytes memory payerMetadata = _buildPayerMetadata(hookAddress, mintIds);
|
|
57
|
-
|
|
58
|
-
uint16[] memory splitTierIds = new uint16[](1);
|
|
59
|
-
splitTierIds[0] = uint16(tierIds[0]);
|
|
60
|
-
uint256[] memory splitAmounts = new uint256[](1);
|
|
61
|
-
splitAmounts[0] = amount;
|
|
62
|
-
|
|
63
|
-
return JBAfterPayRecordedContext({
|
|
64
|
-
payer: beneficiary,
|
|
65
|
-
projectId: projectId,
|
|
66
|
-
rulesetId: 0,
|
|
67
|
-
amount: JBTokenAmount({token: token, value: amount, decimals: decimals, currency: currency}),
|
|
68
|
-
forwardedAmount: JBTokenAmount({token: token, value: amount, decimals: decimals, currency: currency}),
|
|
69
|
-
weight: 10e18,
|
|
70
|
-
newlyIssuedTokenCount: 0,
|
|
71
|
-
beneficiary: beneficiary,
|
|
72
|
-
hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
|
|
73
|
-
payerMetadata: payerMetadata
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
78
|
-
// ETH: broken own-project terminal in _addToBalance should revert (M-4)
|
|
79
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
/// @notice When a split has no valid recipient (projectId==0, beneficiary==address(0)),
|
|
82
|
-
/// funds route to the project's own terminal via _addToBalance. If that terminal reverts,
|
|
83
|
-
/// the payment now reverts with JB721TiersHookLib_SplitFallbackFailed (M-4 fix).
|
|
84
|
-
function test_brokenOwnTerminal_eth_reverts() public {
|
|
85
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
86
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
87
|
-
|
|
88
|
-
// Add a tier with 100% split, priced at 1 ETH.
|
|
89
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
90
|
-
tierConfigs[0].price = 1 ether;
|
|
91
|
-
tierConfigs[0].initialSupply = uint32(100);
|
|
92
|
-
tierConfigs[0].category = uint24(1);
|
|
93
|
-
tierConfigs[0].encodedIPFSUri = bytes32(uint256(0x1234));
|
|
94
|
-
tierConfigs[0].splitPercent = 1_000_000_000; // 100%
|
|
95
|
-
|
|
96
|
-
vm.prank(address(testHook));
|
|
97
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
98
|
-
|
|
99
|
-
// Mock directory checks.
|
|
100
|
-
mockAndExpect(
|
|
101
|
-
address(mockJBDirectory),
|
|
102
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
103
|
-
abi.encode(true)
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
// Mock splits: a split with no valid recipient (projectId==0, beneficiary==address(0)),
|
|
107
|
-
// so funds route to _addToBalance.
|
|
108
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
109
|
-
splits[0] = JBSplit({
|
|
110
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
111
|
-
projectId: 0,
|
|
112
|
-
beneficiary: payable(address(0)),
|
|
113
|
-
preferAddToBalance: false,
|
|
114
|
-
lockedUntil: 0,
|
|
115
|
-
hook: IJBSplitHook(address(0))
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
119
|
-
mockAndExpect(
|
|
120
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
// Mock the project's primary terminal to a contract that reverts on addToBalanceOf.
|
|
124
|
-
address brokenTerminal = makeAddr("brokenTerminal");
|
|
125
|
-
vm.etch(brokenTerminal, new bytes(0x69));
|
|
126
|
-
mockAndExpect(
|
|
127
|
-
address(mockJBDirectory),
|
|
128
|
-
abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, projectId, JBConstants.NATIVE_TOKEN),
|
|
129
|
-
abi.encode(brokenTerminal)
|
|
130
|
-
);
|
|
131
|
-
// Make addToBalanceOf revert.
|
|
132
|
-
vm.mockCallRevert(
|
|
133
|
-
brokenTerminal, abi.encodeWithSelector(IJBTerminal.addToBalanceOf.selector), "terminal broken"
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
// Build payer metadata.
|
|
137
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
138
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
139
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
140
|
-
|
|
141
|
-
// Build hook metadata (per-tier split breakdown).
|
|
142
|
-
uint16[] memory splitTierIds = new uint16[](1);
|
|
143
|
-
splitTierIds[0] = uint16(tierIds[0]);
|
|
144
|
-
uint256[] memory splitAmounts = new uint256[](1);
|
|
145
|
-
splitAmounts[0] = 1 ether;
|
|
146
|
-
|
|
147
|
-
JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
|
|
148
|
-
payer: beneficiary,
|
|
149
|
-
projectId: projectId,
|
|
150
|
-
rulesetId: 0,
|
|
151
|
-
amount: JBTokenAmount({
|
|
152
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
153
|
-
value: 1 ether,
|
|
154
|
-
decimals: 18,
|
|
155
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
156
|
-
}),
|
|
157
|
-
forwardedAmount: JBTokenAmount({
|
|
158
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
159
|
-
value: 1 ether,
|
|
160
|
-
decimals: 18,
|
|
161
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
162
|
-
}),
|
|
163
|
-
weight: 10e18,
|
|
164
|
-
newlyIssuedTokenCount: 0,
|
|
165
|
-
beneficiary: beneficiary,
|
|
166
|
-
hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
|
|
167
|
-
payerMetadata: payerMetadata
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
vm.deal(mockTerminalAddress, 2 ether);
|
|
171
|
-
|
|
172
|
-
vm.prank(mockTerminalAddress);
|
|
173
|
-
// The payment should now revert when the fallback addToBalanceOf call fails (M-4).
|
|
174
|
-
vm.expectRevert();
|
|
175
|
-
testHook.afterPayRecordedWith{value: 1 ether}(payContext);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
179
|
-
// ERC-20: broken own-project terminal in _addToBalance should revert (M-4)
|
|
180
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
181
|
-
|
|
182
|
-
/// @notice Same scenario as above but with ERC-20 tokens. On terminal failure, the
|
|
183
|
-
/// approval is reset to 0 for safety and the payment now reverts (M-4 fix).
|
|
184
|
-
function test_brokenOwnTerminal_erc20_reverts() public {
|
|
185
|
-
BrokenTerminalERC20 usdc = new BrokenTerminalERC20("USD Coin", "USDC", 6);
|
|
186
|
-
uint32 usdcCurrency = uint32(uint160(address(usdc)));
|
|
187
|
-
|
|
188
|
-
JB721TiersHook testHook = _initHookDefaultTiers(0, false, usdcCurrency, 6);
|
|
189
|
-
|
|
190
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
191
|
-
tierConfigs[0].price = 100e6;
|
|
192
|
-
tierConfigs[0].initialSupply = uint32(100);
|
|
193
|
-
tierConfigs[0].category = uint24(1);
|
|
194
|
-
tierConfigs[0].encodedIPFSUri = bytes32(uint256(0x1234));
|
|
195
|
-
tierConfigs[0].splitPercent = 1_000_000_000; // 100%
|
|
196
|
-
|
|
197
|
-
vm.prank(address(testHook));
|
|
198
|
-
uint256[] memory tierIds = testHook.STORE().recordAddTiers(tierConfigs);
|
|
199
|
-
|
|
200
|
-
_setupErc20Mocks(testHook, tierIds, address(usdc), usdcCurrency);
|
|
201
|
-
|
|
202
|
-
JBAfterPayRecordedContext memory payContext =
|
|
203
|
-
_buildErc20PayContext(address(testHook), address(usdc), usdcCurrency, 6, tierIds, 100e6);
|
|
204
|
-
|
|
205
|
-
// Fund the terminal and approve the hook to pull tokens.
|
|
206
|
-
usdc.mint(mockTerminalAddress, 100e6);
|
|
207
|
-
vm.prank(mockTerminalAddress);
|
|
208
|
-
usdc.approve(address(testHook), 100e6);
|
|
209
|
-
|
|
210
|
-
vm.prank(mockTerminalAddress);
|
|
211
|
-
// The payment should now revert when the fallback addToBalanceOf call fails (M-4).
|
|
212
|
-
vm.expectRevert();
|
|
213
|
-
testHook.afterPayRecordedWith(payContext);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/// @notice Sets up mocks for the ERC-20 broken terminal test (extracted to avoid stack-too-deep).
|
|
217
|
-
function _setupErc20Mocks(
|
|
218
|
-
JB721TiersHook testHook,
|
|
219
|
-
uint256[] memory tierIds,
|
|
220
|
-
address token,
|
|
221
|
-
uint32 /* usdcCurrency */
|
|
222
|
-
)
|
|
223
|
-
internal
|
|
224
|
-
{
|
|
225
|
-
mockAndExpect(
|
|
226
|
-
address(mockJBDirectory),
|
|
227
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
228
|
-
abi.encode(true)
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
// Mock splits: no valid recipient, so funds route to _addToBalance.
|
|
232
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
233
|
-
splits[0] = JBSplit({
|
|
234
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
235
|
-
projectId: 0,
|
|
236
|
-
beneficiary: payable(address(0)),
|
|
237
|
-
preferAddToBalance: false,
|
|
238
|
-
lockedUntil: 0,
|
|
239
|
-
hook: IJBSplitHook(address(0))
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
243
|
-
mockAndExpect(
|
|
244
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
245
|
-
);
|
|
246
|
-
|
|
247
|
-
// Mock the project's primary terminal to a contract that reverts on addToBalanceOf.
|
|
248
|
-
address brokenTerminal = makeAddr("brokenTerminal");
|
|
249
|
-
vm.etch(brokenTerminal, new bytes(0x69));
|
|
250
|
-
mockAndExpect(
|
|
251
|
-
address(mockJBDirectory),
|
|
252
|
-
abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, projectId, token),
|
|
253
|
-
abi.encode(brokenTerminal)
|
|
254
|
-
);
|
|
255
|
-
// Make addToBalanceOf revert.
|
|
256
|
-
vm.mockCallRevert(
|
|
257
|
-
brokenTerminal, abi.encodeWithSelector(IJBTerminal.addToBalanceOf.selector), "terminal broken"
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/// @notice A simple mintable ERC20 for testing.
|
|
263
|
-
contract BrokenTerminalERC20 is ERC20 {
|
|
264
|
-
uint8 internal _decimals;
|
|
265
|
-
|
|
266
|
-
constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_) {
|
|
267
|
-
_decimals = decimals_;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function decimals() public view override returns (uint8) {
|
|
271
|
-
return _decimals;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function mint(address to, uint256 amount) external {
|
|
275
|
-
_mint(to, amount);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
@@ -1,190 +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
|
-
// forge-lint: disable-next-line(unused-import)
|
|
8
|
-
import {JB721TiersHookLib} from "../../src/libraries/JB721TiersHookLib.sol";
|
|
9
|
-
|
|
10
|
-
/// @notice calculateSplitAmounts caches the tierOf result to avoid a duplicate external call.
|
|
11
|
-
/// Verifies that the cached tier lookup returns the same split amounts as reading price and splitPercent individually.
|
|
12
|
-
contract Test_L35_CacheTierLookup is UnitTestSetup {
|
|
13
|
-
using stdStorage for StdStorage;
|
|
14
|
-
|
|
15
|
-
/// @notice Verify that calculateSplitAmounts returns correct per-tier amounts when multiple tiers have different
|
|
16
|
-
/// prices and split percentages. This exercises the cached `tier` variable.
|
|
17
|
-
function test_calculateSplitAmounts_multiTier_correctAmounts() public {
|
|
18
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
19
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
20
|
-
|
|
21
|
-
// Add 3 tiers with different prices and split percents.
|
|
22
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](3);
|
|
23
|
-
|
|
24
|
-
// Tier A: 1 ETH, 25% split -> 0.25 ETH
|
|
25
|
-
tierConfigs[0].price = 1 ether;
|
|
26
|
-
tierConfigs[0].initialSupply = uint32(100);
|
|
27
|
-
tierConfigs[0].category = uint24(1);
|
|
28
|
-
tierConfigs[0].encodedIPFSUri = bytes32(uint256(0x1111));
|
|
29
|
-
tierConfigs[0].splitPercent = 250_000_000; // 25%
|
|
30
|
-
|
|
31
|
-
// Tier B: 2 ETH, 50% split -> 1 ETH
|
|
32
|
-
tierConfigs[1].price = 2 ether;
|
|
33
|
-
tierConfigs[1].initialSupply = uint32(100);
|
|
34
|
-
tierConfigs[1].category = uint24(2);
|
|
35
|
-
tierConfigs[1].encodedIPFSUri = bytes32(uint256(0x2222));
|
|
36
|
-
tierConfigs[1].splitPercent = 500_000_000; // 50%
|
|
37
|
-
|
|
38
|
-
// Tier C: 0.5 ETH, 100% split -> 0.5 ETH
|
|
39
|
-
tierConfigs[2].price = 0.5 ether;
|
|
40
|
-
tierConfigs[2].initialSupply = uint32(100);
|
|
41
|
-
tierConfigs[2].category = uint24(3);
|
|
42
|
-
tierConfigs[2].encodedIPFSUri = bytes32(uint256(0x3333));
|
|
43
|
-
tierConfigs[2].splitPercent = 1_000_000_000; // 100%
|
|
44
|
-
|
|
45
|
-
vm.prank(address(testHook));
|
|
46
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
47
|
-
|
|
48
|
-
// Build payer metadata requesting all 3 tiers.
|
|
49
|
-
uint16[] memory mintIds = new uint16[](3);
|
|
50
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
51
|
-
mintIds[1] = uint16(tierIds[1]);
|
|
52
|
-
mintIds[2] = uint16(tierIds[2]);
|
|
53
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
54
|
-
|
|
55
|
-
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
56
|
-
terminal: mockTerminalAddress,
|
|
57
|
-
payer: beneficiary,
|
|
58
|
-
amount: JBTokenAmount({
|
|
59
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
60
|
-
value: 3.5 ether,
|
|
61
|
-
decimals: 18,
|
|
62
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
63
|
-
}),
|
|
64
|
-
projectId: projectId,
|
|
65
|
-
rulesetId: 0,
|
|
66
|
-
beneficiary: beneficiary,
|
|
67
|
-
weight: 10e18,
|
|
68
|
-
reservedPercent: 5000,
|
|
69
|
-
metadata: payerMetadata
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
(, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
|
|
73
|
-
|
|
74
|
-
// Total split = 0.25 + 1.0 + 0.5 = 1.75 ETH
|
|
75
|
-
assertEq(specs[0].amount, 1.75 ether, "Total split amount should be 1.75 ETH");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/// @notice Verify that a tier with splitPercent == 0 contributes nothing to the total, even when cached.
|
|
79
|
-
function test_calculateSplitAmounts_zeroSplitSkipped() public {
|
|
80
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
81
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
82
|
-
|
|
83
|
-
// Tier A: 1 ETH, 50% split
|
|
84
|
-
// Tier B: 3 ETH, 0% split (should be skipped)
|
|
85
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](2);
|
|
86
|
-
tierConfigs[0].price = 1 ether;
|
|
87
|
-
tierConfigs[0].initialSupply = uint32(100);
|
|
88
|
-
tierConfigs[0].category = uint24(1);
|
|
89
|
-
tierConfigs[0].encodedIPFSUri = bytes32(uint256(0xAAAA));
|
|
90
|
-
tierConfigs[0].splitPercent = 500_000_000; // 50%
|
|
91
|
-
|
|
92
|
-
tierConfigs[1].price = 3 ether;
|
|
93
|
-
tierConfigs[1].initialSupply = uint32(100);
|
|
94
|
-
tierConfigs[1].category = uint24(2);
|
|
95
|
-
tierConfigs[1].encodedIPFSUri = bytes32(uint256(0xBBBB));
|
|
96
|
-
tierConfigs[1].splitPercent = 0; // 0%
|
|
97
|
-
|
|
98
|
-
vm.prank(address(testHook));
|
|
99
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
100
|
-
|
|
101
|
-
uint16[] memory mintIds = new uint16[](2);
|
|
102
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
103
|
-
mintIds[1] = uint16(tierIds[1]);
|
|
104
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
105
|
-
|
|
106
|
-
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
107
|
-
terminal: mockTerminalAddress,
|
|
108
|
-
payer: beneficiary,
|
|
109
|
-
amount: JBTokenAmount({
|
|
110
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
111
|
-
value: 4 ether,
|
|
112
|
-
decimals: 18,
|
|
113
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
114
|
-
}),
|
|
115
|
-
projectId: projectId,
|
|
116
|
-
rulesetId: 0,
|
|
117
|
-
beneficiary: beneficiary,
|
|
118
|
-
weight: 10e18,
|
|
119
|
-
reservedPercent: 5000,
|
|
120
|
-
metadata: payerMetadata
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
(, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
|
|
124
|
-
|
|
125
|
-
// Only tier A contributes: 1 ETH * 50% = 0.5 ETH
|
|
126
|
-
assertEq(specs[0].amount, 0.5 ether, "Only non-zero split tiers should contribute");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/// @notice Verify that duplicate tier IDs in metadata produce correct cumulative split amounts.
|
|
130
|
-
/// The cached tier lookup must handle the same tier appearing multiple times.
|
|
131
|
-
function test_calculateSplitAmounts_duplicateTierIds() public {
|
|
132
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
133
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
134
|
-
|
|
135
|
-
// Single tier: 1 ETH, 30% split
|
|
136
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
137
|
-
tierConfigs[0].price = 1 ether;
|
|
138
|
-
tierConfigs[0].initialSupply = uint32(100);
|
|
139
|
-
tierConfigs[0].category = uint24(1);
|
|
140
|
-
tierConfigs[0].encodedIPFSUri = bytes32(uint256(0xCCCC));
|
|
141
|
-
tierConfigs[0].splitPercent = 300_000_000; // 30%
|
|
142
|
-
|
|
143
|
-
vm.prank(address(testHook));
|
|
144
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
145
|
-
|
|
146
|
-
// Request the same tier twice in metadata.
|
|
147
|
-
uint16[] memory mintIds = new uint16[](2);
|
|
148
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
149
|
-
mintIds[1] = uint16(tierIds[0]);
|
|
150
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
151
|
-
|
|
152
|
-
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
153
|
-
terminal: mockTerminalAddress,
|
|
154
|
-
payer: beneficiary,
|
|
155
|
-
amount: JBTokenAmount({
|
|
156
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
157
|
-
value: 2 ether,
|
|
158
|
-
decimals: 18,
|
|
159
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
160
|
-
}),
|
|
161
|
-
projectId: projectId,
|
|
162
|
-
rulesetId: 0,
|
|
163
|
-
beneficiary: beneficiary,
|
|
164
|
-
weight: 10e18,
|
|
165
|
-
reservedPercent: 5000,
|
|
166
|
-
metadata: payerMetadata
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
(, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
|
|
170
|
-
|
|
171
|
-
// 2 x (1 ETH * 30%) = 0.6 ETH
|
|
172
|
-
assertEq(specs[0].amount, 0.6 ether, "Duplicate tier IDs should each contribute their split amount");
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Helper: build payer metadata for tier IDs.
|
|
176
|
-
function _buildPayerMetadata(
|
|
177
|
-
address hookAddress,
|
|
178
|
-
uint16[] memory tierIdsToMint
|
|
179
|
-
)
|
|
180
|
-
internal
|
|
181
|
-
view
|
|
182
|
-
returns (bytes memory)
|
|
183
|
-
{
|
|
184
|
-
bytes[] memory data = new bytes[](1);
|
|
185
|
-
data[0] = abi.encode(false, tierIdsToMint);
|
|
186
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
187
|
-
ids[0] = metadataHelper.getId("pay", hookAddress);
|
|
188
|
-
return metadataHelper.createMetadata(ids, data);
|
|
189
|
-
}
|
|
190
|
-
}
|