@bananapus/distributor-v6 0.0.7 → 0.0.9
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/README.md +2 -2
- package/package.json +16 -7
- package/src/JB721Distributor.sol +46 -12
- package/src/JBDistributor.sol +59 -37
- package/src/JBTokenDistributor.sol +4 -0
- package/src/interfaces/IJBDistributor.sol +3 -1
- package/src/structs/JBTokenSnapshotData.sol +4 -2
- package/src/structs/JBVestingData.sol +6 -3
- package/.github/pull_request_template.md +0 -33
- package/.github/workflows/lint.yml +0 -19
- package/.github/workflows/publish.yml +0 -19
- package/.github/workflows/slither.yml +0 -23
- package/.github/workflows/test.yml +0 -28
- package/.gitmodules +0 -3
- package/ADMINISTRATION.md +0 -65
- package/ARCHITECTURE.md +0 -89
- package/AUDIT_INSTRUCTIONS.md +0 -52
- package/RISKS.md +0 -78
- package/SKILLS.md +0 -36
- package/USER_JOURNEYS.md +0 -122
- package/slither-ci.config.json +0 -10
- package/test/AuditFixes.t.sol +0 -429
- package/test/JB721Distributor.t.sol +0 -2059
- package/test/JBTokenDistributor.t.sol +0 -503
- package/test/audit/CodexNemesisAccountingPoC.t.sol +0 -344
- package/test/audit/CodexNemesisFreshSplitTokenMismatch.t.sol +0 -133
- package/test/audit/CodexNemesisFreshVerification.t.sol +0 -218
- package/test/audit/CodexNemesisPoC.t.sol +0 -191
- package/test/audit/H26VotingPowerCap.t.sol +0 -343
- package/test/audit/Pass12Fixes.t.sol +0 -344
- package/test/audit/PostSnapshotMintTheft.t.sol +0 -413
- package/test/audit/TokenMismatchFix.t.sol +0 -295
- package/test/fork/TokenDistributorFork.t.sol +0 -603
- package/test/invariant/JB721DistributorInvariant.t.sol +0 -414
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.28;
|
|
3
|
-
|
|
4
|
-
import {Test} from "forge-std/Test.sol";
|
|
5
|
-
|
|
6
|
-
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
7
|
-
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
8
|
-
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
|
|
9
|
-
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
|
|
10
|
-
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
11
|
-
|
|
12
|
-
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
13
|
-
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
14
|
-
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
15
|
-
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
16
|
-
import {JBSplitHookContext} from "@bananapus/core-v6/src/structs/JBSplitHookContext.sol";
|
|
17
|
-
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
18
|
-
|
|
19
|
-
import {JBTokenDistributor} from "../../src/JBTokenDistributor.sol";
|
|
20
|
-
import {JB721Distributor} from "../../src/JB721Distributor.sol";
|
|
21
|
-
import {JBDistributor} from "../../src/JBDistributor.sol";
|
|
22
|
-
|
|
23
|
-
// =========================================================================
|
|
24
|
-
// Mock contracts
|
|
25
|
-
// =========================================================================
|
|
26
|
-
|
|
27
|
-
/// @notice Mock JB directory for token mismatch tests.
|
|
28
|
-
contract TMMockDirectory {
|
|
29
|
-
mapping(uint256 projectId => mapping(address terminal => bool)) public terminals;
|
|
30
|
-
mapping(uint256 projectId => address controller) public controllers;
|
|
31
|
-
|
|
32
|
-
function setTerminal(uint256 projectId, address terminal, bool isTerminal) external {
|
|
33
|
-
terminals[projectId][terminal] = isTerminal;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function setController(uint256 projectId, address controller) external {
|
|
37
|
-
controllers[projectId] = controller;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function isTerminalOf(uint256 projectId, IJBTerminal terminal) external view returns (bool) {
|
|
41
|
-
return terminals[projectId][address(terminal)];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function controllerOf(uint256 projectId) external view returns (IERC165) {
|
|
45
|
-
return IERC165(controllers[projectId]);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/// @notice Simple ERC20 token representing a victim's deposited ERC-20.
|
|
50
|
-
contract TMVictimToken is ERC20 {
|
|
51
|
-
constructor() ERC20("VictimToken", "VICTIM") {}
|
|
52
|
-
|
|
53
|
-
function mint(address to, uint256 amount) external {
|
|
54
|
-
_mint(to, amount);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/// @notice ERC20Votes token for staking.
|
|
59
|
-
contract TMVotesToken is ERC20, ERC20Votes {
|
|
60
|
-
constructor() ERC20("StakeToken", "STK") EIP712("StakeToken", "1") {}
|
|
61
|
-
|
|
62
|
-
function mint(address to, uint256 amount) external {
|
|
63
|
-
_mint(to, amount);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function _update(address from, address to, uint256 value) internal override(ERC20, ERC20Votes) {
|
|
67
|
-
super._update(from, to, value);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// =========================================================================
|
|
72
|
-
// Test: JBTokenDistributor token mismatch vulnerability
|
|
73
|
-
// =========================================================================
|
|
74
|
-
|
|
75
|
-
/// @notice Proves the ETH-to-ERC20 cross-booking vulnerability is fixed in JBTokenDistributor.
|
|
76
|
-
/// @dev The attack: a malicious terminal sends ETH (msg.value != 0) but sets context.token to an
|
|
77
|
-
/// arbitrary ERC-20 address. Without the fix, the ETH amount would be credited under that ERC-20's
|
|
78
|
-
/// balance mapping, effectively stealing another hook's ERC-20 balance.
|
|
79
|
-
contract TokenMismatchTokenDistributorTest is Test {
|
|
80
|
-
TMMockDirectory directory;
|
|
81
|
-
TMVictimToken victimToken;
|
|
82
|
-
TMVotesToken votesToken;
|
|
83
|
-
JBTokenDistributor distributor;
|
|
84
|
-
|
|
85
|
-
address attacker = makeAddr("attacker");
|
|
86
|
-
address victim = makeAddr("victim");
|
|
87
|
-
address terminal = makeAddr("terminal");
|
|
88
|
-
address hook;
|
|
89
|
-
uint256 projectId = 1;
|
|
90
|
-
|
|
91
|
-
uint256 constant ROUND_DURATION = 100;
|
|
92
|
-
uint256 constant VESTING_ROUNDS = 4;
|
|
93
|
-
|
|
94
|
-
function setUp() public {
|
|
95
|
-
directory = new TMMockDirectory();
|
|
96
|
-
victimToken = new TMVictimToken();
|
|
97
|
-
votesToken = new TMVotesToken();
|
|
98
|
-
|
|
99
|
-
directory.setTerminal(projectId, terminal, true);
|
|
100
|
-
|
|
101
|
-
distributor = new JBTokenDistributor(IJBDirectory(address(directory)), ROUND_DURATION, VESTING_ROUNDS);
|
|
102
|
-
|
|
103
|
-
hook = address(votesToken);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/// @notice Helper to build a JBSplitHookContext.
|
|
107
|
-
function _buildContext(address token, uint256 amount) internal view returns (JBSplitHookContext memory) {
|
|
108
|
-
JBSplit memory split = JBSplit({
|
|
109
|
-
percent: 1_000_000_000,
|
|
110
|
-
projectId: 0,
|
|
111
|
-
beneficiary: payable(hook),
|
|
112
|
-
preferAddToBalance: false,
|
|
113
|
-
lockedUntil: 0,
|
|
114
|
-
hook: IJBSplitHook(address(distributor))
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
return JBSplitHookContext({
|
|
118
|
-
token: token, amount: amount, decimals: 18, projectId: projectId, groupId: 0, split: split
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/// @notice Proves the fix: sending ETH with context.token set to an ERC-20 address reverts.
|
|
123
|
-
function test_tokenMismatch_ethWithErc20Token_reverts() public {
|
|
124
|
-
// Attacker constructs a context claiming the token is victimToken (an ERC-20),
|
|
125
|
-
// but sends ETH as msg.value.
|
|
126
|
-
JBSplitHookContext memory context = _buildContext(address(victimToken), 1 ether);
|
|
127
|
-
|
|
128
|
-
// Fund the terminal with ETH for the attack.
|
|
129
|
-
vm.deal(terminal, 10 ether);
|
|
130
|
-
|
|
131
|
-
// The attack should now revert with TokenMismatch.
|
|
132
|
-
vm.prank(terminal);
|
|
133
|
-
vm.expectRevert(JBTokenDistributor.JBTokenDistributor_TokenMismatch.selector);
|
|
134
|
-
distributor.processSplitWith{value: 1 ether}(context);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/// @notice Proves that legitimate native ETH splits (context.token == NATIVE_TOKEN) still work.
|
|
138
|
-
function test_tokenMismatch_ethWithNativeToken_succeeds() public {
|
|
139
|
-
JBSplitHookContext memory context = _buildContext(JBConstants.NATIVE_TOKEN, 1 ether);
|
|
140
|
-
|
|
141
|
-
vm.deal(terminal, 10 ether);
|
|
142
|
-
|
|
143
|
-
vm.prank(terminal);
|
|
144
|
-
distributor.processSplitWith{value: 1 ether}(context);
|
|
145
|
-
|
|
146
|
-
// Balance should be credited under NATIVE_TOKEN.
|
|
147
|
-
assertEq(
|
|
148
|
-
distributor.balanceOf(hook, IERC20(JBConstants.NATIVE_TOKEN)),
|
|
149
|
-
1 ether,
|
|
150
|
-
"Native ETH split should credit balance under NATIVE_TOKEN"
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/// @notice Demonstrates the attack scenario that the fix prevents: without the fix,
|
|
155
|
-
/// an attacker could steal the victim's ERC-20 balance by sending ETH with a fake token.
|
|
156
|
-
function test_tokenMismatch_attackScenario_cannotStealErc20Balance() public {
|
|
157
|
-
// Step 1: Victim legitimately deposits ERC-20 tokens via the terminal.
|
|
158
|
-
uint256 victimAmount = 100 ether;
|
|
159
|
-
victimToken.mint(terminal, victimAmount);
|
|
160
|
-
|
|
161
|
-
JBSplitHookContext memory legitimateContext = _buildContext(address(victimToken), victimAmount);
|
|
162
|
-
|
|
163
|
-
vm.startPrank(terminal);
|
|
164
|
-
victimToken.approve(address(distributor), victimAmount);
|
|
165
|
-
distributor.processSplitWith(legitimateContext);
|
|
166
|
-
vm.stopPrank();
|
|
167
|
-
|
|
168
|
-
// Verify victim's ERC-20 balance is properly recorded.
|
|
169
|
-
assertEq(
|
|
170
|
-
distributor.balanceOf(hook, IERC20(address(victimToken))),
|
|
171
|
-
victimAmount,
|
|
172
|
-
"Victim's ERC-20 balance should be credited"
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
// Step 2: Attacker tries to send 1 ETH but have it credited as victimToken.
|
|
176
|
-
// This would allow the attacker to inflate the victimToken balance and steal from others.
|
|
177
|
-
JBSplitHookContext memory attackContext = _buildContext(address(victimToken), 1 ether);
|
|
178
|
-
|
|
179
|
-
vm.deal(terminal, 10 ether);
|
|
180
|
-
vm.prank(terminal);
|
|
181
|
-
vm.expectRevert(JBTokenDistributor.JBTokenDistributor_TokenMismatch.selector);
|
|
182
|
-
distributor.processSplitWith{value: 1 ether}(attackContext);
|
|
183
|
-
|
|
184
|
-
// Balance remains unchanged — attack blocked.
|
|
185
|
-
assertEq(
|
|
186
|
-
distributor.balanceOf(hook, IERC20(address(victimToken))),
|
|
187
|
-
victimAmount,
|
|
188
|
-
"Victim's ERC-20 balance must not change after blocked attack"
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// =========================================================================
|
|
194
|
-
// Test: JB721Distributor token mismatch vulnerability
|
|
195
|
-
// =========================================================================
|
|
196
|
-
|
|
197
|
-
/// @notice Proves the ETH-to-ERC20 cross-booking vulnerability is fixed in JB721Distributor.
|
|
198
|
-
contract TokenMismatch721DistributorTest is Test {
|
|
199
|
-
TMMockDirectory directory;
|
|
200
|
-
TMVictimToken victimToken;
|
|
201
|
-
JB721Distributor distributor;
|
|
202
|
-
|
|
203
|
-
address terminal = makeAddr("terminal");
|
|
204
|
-
address hook = makeAddr("nft-hook");
|
|
205
|
-
uint256 projectId = 1;
|
|
206
|
-
|
|
207
|
-
uint256 constant ROUND_DURATION = 100;
|
|
208
|
-
uint256 constant VESTING_ROUNDS = 4;
|
|
209
|
-
|
|
210
|
-
function setUp() public {
|
|
211
|
-
directory = new TMMockDirectory();
|
|
212
|
-
victimToken = new TMVictimToken();
|
|
213
|
-
|
|
214
|
-
directory.setTerminal(projectId, terminal, true);
|
|
215
|
-
|
|
216
|
-
distributor = new JB721Distributor(IJBDirectory(address(directory)), ROUND_DURATION, VESTING_ROUNDS);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/// @notice Helper to build a JBSplitHookContext.
|
|
220
|
-
function _buildContext(address token, uint256 amount) internal view returns (JBSplitHookContext memory) {
|
|
221
|
-
JBSplit memory split = JBSplit({
|
|
222
|
-
percent: 1_000_000_000,
|
|
223
|
-
projectId: 0,
|
|
224
|
-
beneficiary: payable(hook),
|
|
225
|
-
preferAddToBalance: false,
|
|
226
|
-
lockedUntil: 0,
|
|
227
|
-
hook: IJBSplitHook(address(distributor))
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
return JBSplitHookContext({
|
|
231
|
-
token: token, amount: amount, decimals: 18, projectId: projectId, groupId: 0, split: split
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/// @notice Proves the fix: sending ETH with context.token set to an ERC-20 address reverts.
|
|
236
|
-
function test_tokenMismatch_721_ethWithErc20Token_reverts() public {
|
|
237
|
-
JBSplitHookContext memory context = _buildContext(address(victimToken), 1 ether);
|
|
238
|
-
|
|
239
|
-
vm.deal(terminal, 10 ether);
|
|
240
|
-
|
|
241
|
-
vm.prank(terminal);
|
|
242
|
-
vm.expectRevert(JB721Distributor.JB721Distributor_TokenMismatch.selector);
|
|
243
|
-
distributor.processSplitWith{value: 1 ether}(context);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/// @notice Proves that legitimate native ETH splits still work for JB721Distributor.
|
|
247
|
-
function test_tokenMismatch_721_ethWithNativeToken_succeeds() public {
|
|
248
|
-
JBSplitHookContext memory context = _buildContext(JBConstants.NATIVE_TOKEN, 1 ether);
|
|
249
|
-
|
|
250
|
-
vm.deal(terminal, 10 ether);
|
|
251
|
-
|
|
252
|
-
vm.prank(terminal);
|
|
253
|
-
distributor.processSplitWith{value: 1 ether}(context);
|
|
254
|
-
|
|
255
|
-
// Balance should be credited under NATIVE_TOKEN.
|
|
256
|
-
assertEq(
|
|
257
|
-
distributor.balanceOf(hook, IERC20(JBConstants.NATIVE_TOKEN)),
|
|
258
|
-
1 ether,
|
|
259
|
-
"Native ETH split should credit balance under NATIVE_TOKEN"
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/// @notice Full attack scenario blocked on JB721Distributor.
|
|
264
|
-
function test_tokenMismatch_721_attackScenario_cannotStealErc20Balance() public {
|
|
265
|
-
// Step 1: Legitimate ERC-20 deposit.
|
|
266
|
-
uint256 victimAmount = 50 ether;
|
|
267
|
-
victimToken.mint(terminal, victimAmount);
|
|
268
|
-
|
|
269
|
-
JBSplitHookContext memory legitimateContext = _buildContext(address(victimToken), victimAmount);
|
|
270
|
-
|
|
271
|
-
vm.startPrank(terminal);
|
|
272
|
-
victimToken.approve(address(distributor), victimAmount);
|
|
273
|
-
distributor.processSplitWith(legitimateContext);
|
|
274
|
-
vm.stopPrank();
|
|
275
|
-
|
|
276
|
-
assertEq(
|
|
277
|
-
distributor.balanceOf(hook, IERC20(address(victimToken))), victimAmount, "Victim balance should be credited"
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
// Step 2: Attack — send ETH but claim it as victimToken.
|
|
281
|
-
JBSplitHookContext memory attackContext = _buildContext(address(victimToken), 1 ether);
|
|
282
|
-
|
|
283
|
-
vm.deal(terminal, 10 ether);
|
|
284
|
-
vm.prank(terminal);
|
|
285
|
-
vm.expectRevert(JB721Distributor.JB721Distributor_TokenMismatch.selector);
|
|
286
|
-
distributor.processSplitWith{value: 1 ether}(attackContext);
|
|
287
|
-
|
|
288
|
-
// Balance unaffected.
|
|
289
|
-
assertEq(
|
|
290
|
-
distributor.balanceOf(hook, IERC20(address(victimToken))),
|
|
291
|
-
victimAmount,
|
|
292
|
-
"Balance must remain unchanged after blocked attack"
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
}
|