@bananapus/distributor-v6 0.0.6 → 0.0.8
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 +55 -18
- package/src/JBTokenDistributor.sol +10 -0
- 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 -2054
- package/test/JBTokenDistributor.t.sol +0 -503
- package/test/audit/CodexNemesisAccountingPoC.t.sol +0 -339
- package/test/audit/CodexNemesisPoC.t.sol +0 -191
- package/test/audit/H26VotingPowerCap.t.sol +0 -338
- package/test/audit/Pass12Fixes.t.sol +0 -344
- package/test/fork/TokenDistributorFork.t.sol +0 -603
- package/test/invariant/JB721DistributorInvariant.t.sol +0 -409
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.28;
|
|
3
|
-
|
|
4
|
-
import {Test} from "forge-std/Test.sol";
|
|
5
|
-
import {stdError} from "forge-std/StdError.sol";
|
|
6
|
-
|
|
7
|
-
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
8
|
-
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
9
|
-
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
|
|
10
|
-
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
|
|
11
|
-
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
12
|
-
|
|
13
|
-
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
14
|
-
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
15
|
-
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
16
|
-
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
17
|
-
import {JBSplitHookContext} from "@bananapus/core-v6/src/structs/JBSplitHookContext.sol";
|
|
18
|
-
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
19
|
-
import {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
|
|
20
|
-
|
|
21
|
-
import {JBDistributor} from "../../src/JBDistributor.sol";
|
|
22
|
-
import {JB721Distributor} from "../../src/JB721Distributor.sol";
|
|
23
|
-
import {JBTokenDistributor} from "../../src/JBTokenDistributor.sol";
|
|
24
|
-
|
|
25
|
-
contract CodexNemesisDirectory {
|
|
26
|
-
mapping(uint256 projectId => mapping(address terminal => bool)) public terminals;
|
|
27
|
-
mapping(uint256 projectId => address controller) public controllers;
|
|
28
|
-
|
|
29
|
-
function setTerminal(uint256 projectId, address terminal, bool isTerminal) external {
|
|
30
|
-
terminals[projectId][terminal] = isTerminal;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function setController(uint256 projectId, address controller) external {
|
|
34
|
-
controllers[projectId] = controller;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function isTerminalOf(uint256 projectId, IJBTerminal terminal) external view returns (bool) {
|
|
38
|
-
return terminals[projectId][address(terminal)];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function controllerOf(uint256 projectId) external view returns (IERC165) {
|
|
42
|
-
return IERC165(controllers[projectId]);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
contract CodexNemesisRewardToken is ERC20 {
|
|
47
|
-
constructor() ERC20("Reward", "RWD") {}
|
|
48
|
-
|
|
49
|
-
function mint(address to, uint256 amount) external {
|
|
50
|
-
_mint(to, amount);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
contract CodexNemesisVotesToken is ERC20, ERC20Votes {
|
|
55
|
-
constructor() ERC20("Votes", "VOTE") EIP712("Votes", "1") {}
|
|
56
|
-
|
|
57
|
-
function mint(address to, uint256 amount) external {
|
|
58
|
-
_mint(to, amount);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function _update(address from, address to, uint256 value) internal override(ERC20, ERC20Votes) {
|
|
62
|
-
super._update(from, to, value);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
contract CodexNemesisStore {
|
|
67
|
-
uint256 public maxTier;
|
|
68
|
-
mapping(uint256 tierId => JB721Tier) public tiers;
|
|
69
|
-
mapping(uint256 tokenId => uint256 tierId) public tokenTiers;
|
|
70
|
-
|
|
71
|
-
function setMaxTierIdOf(uint256 maxTierId) external {
|
|
72
|
-
maxTier = maxTierId;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function setTier(uint256 tierId, JB721Tier memory tier) external {
|
|
76
|
-
tiers[tierId] = tier;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function setTokenTier(uint256 tokenId, uint256 tierId) external {
|
|
80
|
-
tokenTiers[tokenId] = tierId;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function tierOfTokenId(address, uint256 tokenId, bool) external view returns (JB721Tier memory) {
|
|
84
|
-
return tiers[tokenTiers[tokenId]];
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
contract CodexNemesisCheckpoints {
|
|
89
|
-
uint256 public totalSupplyAtSnapshot;
|
|
90
|
-
mapping(address account => uint256 votes) public votesAtSnapshot;
|
|
91
|
-
|
|
92
|
-
function setTotalSupply(uint256 totalSupply) external {
|
|
93
|
-
totalSupplyAtSnapshot = totalSupply;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function setVotes(address account, uint256 votes) external {
|
|
97
|
-
votesAtSnapshot[account] = votes;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function getPastTotalSupply(uint256) external view returns (uint256) {
|
|
101
|
-
return totalSupplyAtSnapshot;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function getPastVotes(address account, uint256) external view returns (uint256) {
|
|
105
|
-
return votesAtSnapshot[account];
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
contract CodexNemesisHook {
|
|
110
|
-
CodexNemesisStore public immutable STORE;
|
|
111
|
-
CodexNemesisCheckpoints public immutable CHECKPOINTS;
|
|
112
|
-
|
|
113
|
-
mapping(uint256 tokenId => address owner) public owners;
|
|
114
|
-
|
|
115
|
-
constructor(CodexNemesisStore store, CodexNemesisCheckpoints checkpoints) {
|
|
116
|
-
STORE = store;
|
|
117
|
-
CHECKPOINTS = checkpoints;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function ownerOf(uint256 tokenId) external view returns (address) {
|
|
121
|
-
address owner = owners[tokenId];
|
|
122
|
-
require(owner != address(0), "NO_OWNER");
|
|
123
|
-
return owner;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function setOwner(uint256 tokenId, address owner) external {
|
|
127
|
-
owners[tokenId] = owner;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
contract CodexNemesisAccountingPoCTest is Test {
|
|
132
|
-
uint256 internal constant PROJECT_ID = 1;
|
|
133
|
-
uint256 internal constant ROUND_DURATION = 100;
|
|
134
|
-
uint256 internal constant VESTING_ROUNDS = 4;
|
|
135
|
-
|
|
136
|
-
address internal attacker = makeAddr("attacker");
|
|
137
|
-
address internal honest = makeAddr("honest");
|
|
138
|
-
address internal maliciousController = makeAddr("maliciousController");
|
|
139
|
-
|
|
140
|
-
CodexNemesisDirectory internal directory;
|
|
141
|
-
CodexNemesisRewardToken internal rewardToken;
|
|
142
|
-
|
|
143
|
-
function setUp() public {
|
|
144
|
-
directory = new CodexNemesisDirectory();
|
|
145
|
-
rewardToken = new CodexNemesisRewardToken();
|
|
146
|
-
directory.setController(PROJECT_ID, maliciousController);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function test_controllerCanCreditUndeliveredTokensAndDrainRealInventory() public {
|
|
150
|
-
JBTokenDistributor distributor =
|
|
151
|
-
new JBTokenDistributor(IJBDirectory(address(directory)), ROUND_DURATION, VESTING_ROUNDS);
|
|
152
|
-
CodexNemesisVotesToken votesToken = new CodexNemesisVotesToken();
|
|
153
|
-
|
|
154
|
-
votesToken.mint(attacker, 10 ether);
|
|
155
|
-
votesToken.mint(honest, 990 ether);
|
|
156
|
-
|
|
157
|
-
vm.prank(attacker);
|
|
158
|
-
votesToken.delegate(attacker);
|
|
159
|
-
vm.prank(honest);
|
|
160
|
-
votesToken.delegate(honest);
|
|
161
|
-
vm.roll(block.number + 1);
|
|
162
|
-
|
|
163
|
-
rewardToken.mint(address(this), 1000 ether);
|
|
164
|
-
rewardToken.approve(address(distributor), 1000 ether);
|
|
165
|
-
distributor.fund(address(votesToken), IERC20(address(rewardToken)), 1000 ether);
|
|
166
|
-
|
|
167
|
-
JBSplit memory split = JBSplit({
|
|
168
|
-
percent: 1_000_000_000,
|
|
169
|
-
projectId: 0,
|
|
170
|
-
beneficiary: payable(address(votesToken)),
|
|
171
|
-
preferAddToBalance: false,
|
|
172
|
-
lockedUntil: 0,
|
|
173
|
-
hook: IJBSplitHook(address(distributor))
|
|
174
|
-
});
|
|
175
|
-
JBSplitHookContext memory fakeContext = JBSplitHookContext({
|
|
176
|
-
token: address(rewardToken),
|
|
177
|
-
amount: 99_000 ether,
|
|
178
|
-
decimals: 18,
|
|
179
|
-
projectId: PROJECT_ID,
|
|
180
|
-
groupId: 0,
|
|
181
|
-
split: split
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// C-6 FIX: The malicious controller did not actually transfer tokens, so the
|
|
185
|
-
// balance-delta check reverts with UnfundedSplitCredit.
|
|
186
|
-
vm.prank(maliciousController);
|
|
187
|
-
vm.expectRevert(JBDistributor.JBDistributor_UnfundedSplitCredit.selector);
|
|
188
|
-
distributor.processSplitWith(fakeContext);
|
|
189
|
-
|
|
190
|
-
// The real balance should remain intact — the attack was blocked.
|
|
191
|
-
assertEq(rewardToken.balanceOf(address(distributor)), 1000 ether, "balance unchanged after blocked attack");
|
|
192
|
-
assertEq(
|
|
193
|
-
distributor.balanceOf(address(votesToken), IERC20(address(rewardToken))),
|
|
194
|
-
1000 ether,
|
|
195
|
-
"tracked balance was not inflated"
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function test_721LateMintCanUseOwnersPastVotesAndDrainRound() public {
|
|
200
|
-
JB721Distributor distributor =
|
|
201
|
-
new JB721Distributor(IJBDirectory(address(directory)), ROUND_DURATION, VESTING_ROUNDS);
|
|
202
|
-
CodexNemesisStore store = new CodexNemesisStore();
|
|
203
|
-
CodexNemesisCheckpoints checkpoints = new CodexNemesisCheckpoints();
|
|
204
|
-
CodexNemesisHook hook = new CodexNemesisHook(store, checkpoints);
|
|
205
|
-
|
|
206
|
-
JB721TierFlags memory flags;
|
|
207
|
-
store.setMaxTierIdOf(1);
|
|
208
|
-
store.setTier({
|
|
209
|
-
tierId: 1,
|
|
210
|
-
tier: JB721Tier({
|
|
211
|
-
id: 1,
|
|
212
|
-
price: 1 ether,
|
|
213
|
-
remainingSupply: 97,
|
|
214
|
-
initialSupply: 100,
|
|
215
|
-
votingUnits: 100,
|
|
216
|
-
reserveFrequency: 0,
|
|
217
|
-
reserveBeneficiary: address(0),
|
|
218
|
-
encodedIPFSUri: bytes32(0),
|
|
219
|
-
category: 0,
|
|
220
|
-
discountPercent: 0,
|
|
221
|
-
flags: flags,
|
|
222
|
-
splitPercent: 0,
|
|
223
|
-
resolvedUri: ""
|
|
224
|
-
})
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
store.setTokenTier(1, 1);
|
|
228
|
-
store.setTokenTier(2, 1);
|
|
229
|
-
store.setTokenTier(3, 1);
|
|
230
|
-
hook.setOwner(1, attacker);
|
|
231
|
-
hook.setOwner(2, attacker);
|
|
232
|
-
hook.setOwner(3, attacker);
|
|
233
|
-
|
|
234
|
-
checkpoints.setTotalSupply(100);
|
|
235
|
-
checkpoints.setVotes(attacker, 100);
|
|
236
|
-
|
|
237
|
-
rewardToken.mint(address(this), 1000 ether);
|
|
238
|
-
rewardToken.approve(address(distributor), 1000 ether);
|
|
239
|
-
distributor.fund(address(hook), IERC20(address(rewardToken)), 1000 ether);
|
|
240
|
-
|
|
241
|
-
IERC20[] memory tokens = new IERC20[](1);
|
|
242
|
-
tokens[0] = IERC20(address(rewardToken));
|
|
243
|
-
|
|
244
|
-
uint256[] memory firstLateMint = new uint256[](1);
|
|
245
|
-
firstLateMint[0] = 2;
|
|
246
|
-
distributor.beginVesting(address(hook), firstLateMint, tokens);
|
|
247
|
-
|
|
248
|
-
assertEq(distributor.claimedFor(address(hook), 2, tokens[0]), 1000 ether);
|
|
249
|
-
assertEq(
|
|
250
|
-
distributor.totalVestingAmountOf(address(hook), tokens[0]),
|
|
251
|
-
distributor.balanceOf(address(hook), tokens[0]),
|
|
252
|
-
"one post-snapshot token consumed the whole snapshot"
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
vm.warp(distributor.roundStartTimestamp(VESTING_ROUNDS) + 1);
|
|
256
|
-
vm.roll(block.number + 1);
|
|
257
|
-
vm.prank(attacker);
|
|
258
|
-
distributor.collectVestedRewards(address(hook), firstLateMint, tokens, attacker);
|
|
259
|
-
|
|
260
|
-
assertEq(rewardToken.balanceOf(attacker), 1000 ether, "one late-minted token drained the funded balance");
|
|
261
|
-
assertEq(rewardToken.balanceOf(address(distributor)), 0, "honest snapshot stake is left unfunded");
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function test_721SnapshotVotesCanBeReusedAcrossSeparateLateMintClaims() public {
|
|
265
|
-
JB721Distributor distributor =
|
|
266
|
-
new JB721Distributor(IJBDirectory(address(directory)), ROUND_DURATION, VESTING_ROUNDS);
|
|
267
|
-
CodexNemesisStore store = new CodexNemesisStore();
|
|
268
|
-
CodexNemesisCheckpoints checkpoints = new CodexNemesisCheckpoints();
|
|
269
|
-
CodexNemesisHook hook = new CodexNemesisHook(store, checkpoints);
|
|
270
|
-
|
|
271
|
-
JB721TierFlags memory flags;
|
|
272
|
-
store.setMaxTierIdOf(1);
|
|
273
|
-
store.setTier({
|
|
274
|
-
tierId: 1,
|
|
275
|
-
tier: JB721Tier({
|
|
276
|
-
id: 1,
|
|
277
|
-
price: 1 ether,
|
|
278
|
-
remainingSupply: 97,
|
|
279
|
-
initialSupply: 100,
|
|
280
|
-
votingUnits: 100,
|
|
281
|
-
reserveFrequency: 0,
|
|
282
|
-
reserveBeneficiary: address(0),
|
|
283
|
-
encodedIPFSUri: bytes32(0),
|
|
284
|
-
category: 0,
|
|
285
|
-
discountPercent: 0,
|
|
286
|
-
flags: flags,
|
|
287
|
-
splitPercent: 0,
|
|
288
|
-
resolvedUri: ""
|
|
289
|
-
})
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
store.setTokenTier(1, 1);
|
|
293
|
-
store.setTokenTier(2, 1);
|
|
294
|
-
store.setTokenTier(3, 1);
|
|
295
|
-
hook.setOwner(1, attacker);
|
|
296
|
-
hook.setOwner(2, attacker);
|
|
297
|
-
hook.setOwner(3, attacker);
|
|
298
|
-
|
|
299
|
-
checkpoints.setTotalSupply(100);
|
|
300
|
-
checkpoints.setVotes(attacker, 100);
|
|
301
|
-
|
|
302
|
-
rewardToken.mint(address(this), 1000 ether);
|
|
303
|
-
rewardToken.approve(address(distributor), 1000 ether);
|
|
304
|
-
distributor.fund(address(hook), IERC20(address(rewardToken)), 1000 ether);
|
|
305
|
-
|
|
306
|
-
IERC20[] memory tokens = new IERC20[](1);
|
|
307
|
-
tokens[0] = IERC20(address(rewardToken));
|
|
308
|
-
|
|
309
|
-
uint256[] memory firstLateMint = new uint256[](1);
|
|
310
|
-
firstLateMint[0] = 2;
|
|
311
|
-
distributor.beginVesting(address(hook), firstLateMint, tokens);
|
|
312
|
-
|
|
313
|
-
uint256[] memory secondLateMint = new uint256[](1);
|
|
314
|
-
secondLateMint[0] = 3;
|
|
315
|
-
distributor.beginVesting(address(hook), secondLateMint, tokens);
|
|
316
|
-
|
|
317
|
-
// H-24 FIX: With persistent consumed-votes tracking, the second beginVesting sees
|
|
318
|
-
// that all 100 votes are already consumed, so token 3 gets 0 reward.
|
|
319
|
-
assertEq(distributor.claimedFor(address(hook), 2, tokens[0]), 1000 ether);
|
|
320
|
-
assertEq(distributor.claimedFor(address(hook), 3, tokens[0]), 0, "votes already consumed, no double-claim");
|
|
321
|
-
assertEq(
|
|
322
|
-
distributor.totalVestingAmountOf(address(hook), tokens[0]),
|
|
323
|
-
1000 ether,
|
|
324
|
-
"total vesting capped at funded balance"
|
|
325
|
-
);
|
|
326
|
-
assertLe(
|
|
327
|
-
distributor.totalVestingAmountOf(address(hook), tokens[0]),
|
|
328
|
-
distributor.balanceOf(address(hook), tokens[0]),
|
|
329
|
-
"vesting obligations do not exceed funded balance"
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
// Collection should succeed without underflow.
|
|
333
|
-
vm.warp(distributor.roundStartTimestamp(VESTING_ROUNDS) + 1);
|
|
334
|
-
vm.roll(block.number + 1);
|
|
335
|
-
vm.prank(attacker);
|
|
336
|
-
distributor.collectVestedRewards(address(hook), firstLateMint, tokens, attacker);
|
|
337
|
-
assertEq(rewardToken.balanceOf(attacker), 1000 ether, "attacker gets only their fair share");
|
|
338
|
-
}
|
|
339
|
-
}
|
|
@@ -1,191 +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 {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
18
|
-
import {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
|
|
19
|
-
|
|
20
|
-
import {JB721Distributor} from "../../src/JB721Distributor.sol";
|
|
21
|
-
import {JBDistributor} from "../../src/JBDistributor.sol";
|
|
22
|
-
import {JBTokenDistributor} from "../../src/JBTokenDistributor.sol";
|
|
23
|
-
|
|
24
|
-
import {H26MockDirectory, H26MockHook, H26MockRewardToken, H26MockStore} from "./H26VotingPowerCap.t.sol";
|
|
25
|
-
|
|
26
|
-
contract CodexNemesisDirectory {
|
|
27
|
-
mapping(uint256 projectId => mapping(address terminal => bool)) public terminals;
|
|
28
|
-
mapping(uint256 projectId => address controller) public controllers;
|
|
29
|
-
|
|
30
|
-
function setTerminal(uint256 projectId, address terminal, bool isTerminal) external {
|
|
31
|
-
terminals[projectId][terminal] = isTerminal;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function setController(uint256 projectId, address controller) external {
|
|
35
|
-
controllers[projectId] = controller;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function isTerminalOf(uint256 projectId, IJBTerminal terminal) external view returns (bool) {
|
|
39
|
-
return terminals[projectId][address(terminal)];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function controllerOf(uint256 projectId) external view returns (IERC165) {
|
|
43
|
-
return IERC165(controllers[projectId]);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
contract CodexNemesisRewardToken is ERC20 {
|
|
48
|
-
constructor() ERC20("Reward", "RWD") {}
|
|
49
|
-
|
|
50
|
-
function mint(address to, uint256 amount) external {
|
|
51
|
-
_mint(to, amount);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
contract CodexNemesisVotesToken is ERC20, ERC20Votes {
|
|
56
|
-
constructor() ERC20("StakeToken", "STK") EIP712("StakeToken", "1") {}
|
|
57
|
-
|
|
58
|
-
function mint(address to, uint256 amount) external {
|
|
59
|
-
_mint(to, amount);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function _update(address from, address to, uint256 value) internal override(ERC20, ERC20Votes) {
|
|
63
|
-
super._update(from, to, value);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
contract CodexNemesisPoCTest is Test {
|
|
68
|
-
uint256 constant ROUND_DURATION = 100;
|
|
69
|
-
uint256 constant VESTING_ROUNDS = 1;
|
|
70
|
-
|
|
71
|
-
function test_721OwnerVotingCapResetsAcrossBeginVestingCalls() public {
|
|
72
|
-
H26MockStore store = new H26MockStore();
|
|
73
|
-
H26MockHook hook = new H26MockHook(store);
|
|
74
|
-
H26MockDirectory directory = new H26MockDirectory();
|
|
75
|
-
JB721Distributor distributor =
|
|
76
|
-
new JB721Distributor(IJBDirectory(address(directory)), ROUND_DURATION, VESTING_ROUNDS);
|
|
77
|
-
H26MockRewardToken rewardToken = new H26MockRewardToken();
|
|
78
|
-
|
|
79
|
-
address alice = makeAddr("alice");
|
|
80
|
-
|
|
81
|
-
store.setMaxTierIdOf(1);
|
|
82
|
-
store.setTier(
|
|
83
|
-
1,
|
|
84
|
-
JB721Tier({
|
|
85
|
-
id: 1,
|
|
86
|
-
price: 1 ether,
|
|
87
|
-
remainingSupply: 7,
|
|
88
|
-
initialSupply: 10,
|
|
89
|
-
votingUnits: 50,
|
|
90
|
-
reserveFrequency: 0,
|
|
91
|
-
reserveBeneficiary: address(0),
|
|
92
|
-
encodedIPFSUri: bytes32(0),
|
|
93
|
-
category: 0,
|
|
94
|
-
discountPercent: 0,
|
|
95
|
-
flags: JB721TierFlags({
|
|
96
|
-
allowOwnerMint: false,
|
|
97
|
-
transfersPausable: false,
|
|
98
|
-
cantBeRemoved: false,
|
|
99
|
-
cantIncreaseDiscountPercent: false,
|
|
100
|
-
cantBuyWithCredits: false
|
|
101
|
-
}),
|
|
102
|
-
splitPercent: 0,
|
|
103
|
-
resolvedUri: ""
|
|
104
|
-
})
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
store.setTokenTier(1, 1);
|
|
108
|
-
store.setTokenTier(2, 1);
|
|
109
|
-
store.setTokenTier(3, 1);
|
|
110
|
-
hook.setOwner(1, alice);
|
|
111
|
-
hook.setOwner(2, alice);
|
|
112
|
-
hook.setOwner(3, alice);
|
|
113
|
-
hook._checkpoints().setVotesOverride(alice, 100);
|
|
114
|
-
|
|
115
|
-
rewardToken.mint(address(this), 1500 ether);
|
|
116
|
-
rewardToken.approve(address(distributor), 1500 ether);
|
|
117
|
-
distributor.fund(address(hook), IERC20(address(rewardToken)), 1500 ether);
|
|
118
|
-
|
|
119
|
-
IERC20[] memory tokens = new IERC20[](1);
|
|
120
|
-
tokens[0] = IERC20(address(rewardToken));
|
|
121
|
-
|
|
122
|
-
uint256[] memory oneTokenId = new uint256[](1);
|
|
123
|
-
oneTokenId[0] = 1;
|
|
124
|
-
distributor.beginVesting(address(hook), oneTokenId, tokens);
|
|
125
|
-
oneTokenId[0] = 2;
|
|
126
|
-
distributor.beginVesting(address(hook), oneTokenId, tokens);
|
|
127
|
-
oneTokenId[0] = 3;
|
|
128
|
-
distributor.beginVesting(address(hook), oneTokenId, tokens);
|
|
129
|
-
|
|
130
|
-
uint256 claimed1 = distributor.claimedFor(address(hook), 1, IERC20(address(rewardToken)));
|
|
131
|
-
uint256 claimed2 = distributor.claimedFor(address(hook), 2, IERC20(address(rewardToken)));
|
|
132
|
-
uint256 claimed3 = distributor.claimedFor(address(hook), 3, IERC20(address(rewardToken)));
|
|
133
|
-
|
|
134
|
-
// H-24 FIX: With persistent consumed-votes tracking, the total claimed is now correctly
|
|
135
|
-
// capped at the owner's voting power (100 votes / 150 total stake * 1500 = 1000 ether).
|
|
136
|
-
assertEq(claimed1 + claimed2 + claimed3, 1000 ether);
|
|
137
|
-
assertLe(claimed1 + claimed2 + claimed3, 1000 ether);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function test_processSplitWithControllerPathCanCreditUnfundedBalanceAndDrainOtherHook() public {
|
|
141
|
-
CodexNemesisDirectory directory = new CodexNemesisDirectory();
|
|
142
|
-
JBTokenDistributor distributor =
|
|
143
|
-
new JBTokenDistributor(IJBDirectory(address(directory)), ROUND_DURATION, VESTING_ROUNDS);
|
|
144
|
-
CodexNemesisRewardToken rewardToken = new CodexNemesisRewardToken();
|
|
145
|
-
CodexNemesisVotesToken attackerVotes = new CodexNemesisVotesToken();
|
|
146
|
-
|
|
147
|
-
address attacker = makeAddr("attacker");
|
|
148
|
-
address maliciousController = makeAddr("maliciousController");
|
|
149
|
-
address victimHook = makeAddr("victimHook");
|
|
150
|
-
uint256 projectId = 1;
|
|
151
|
-
|
|
152
|
-
directory.setController(projectId, maliciousController);
|
|
153
|
-
|
|
154
|
-
rewardToken.mint(address(this), 1000 ether);
|
|
155
|
-
rewardToken.approve(address(distributor), 1000 ether);
|
|
156
|
-
distributor.fund(victimHook, IERC20(address(rewardToken)), 1000 ether);
|
|
157
|
-
|
|
158
|
-
attackerVotes.mint(attacker, 1000 ether);
|
|
159
|
-
vm.prank(attacker);
|
|
160
|
-
attackerVotes.delegate(attacker);
|
|
161
|
-
vm.roll(block.number + 1);
|
|
162
|
-
|
|
163
|
-
JBSplit memory split = JBSplit({
|
|
164
|
-
percent: 1_000_000_000,
|
|
165
|
-
projectId: 0,
|
|
166
|
-
beneficiary: payable(address(attackerVotes)),
|
|
167
|
-
preferAddToBalance: false,
|
|
168
|
-
lockedUntil: 0,
|
|
169
|
-
hook: IJBSplitHook(address(distributor))
|
|
170
|
-
});
|
|
171
|
-
JBSplitHookContext memory context = JBSplitHookContext({
|
|
172
|
-
token: address(rewardToken),
|
|
173
|
-
amount: 1000 ether,
|
|
174
|
-
decimals: 18,
|
|
175
|
-
projectId: projectId,
|
|
176
|
-
groupId: uint256(uint160(address(rewardToken))),
|
|
177
|
-
split: split
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// C-6 FIX: The malicious controller did not actually transfer tokens, so the
|
|
181
|
-
// balance-delta check should revert with UnfundedSplitCredit.
|
|
182
|
-
vm.prank(maliciousController);
|
|
183
|
-
vm.expectRevert(JBDistributor.JBDistributor_UnfundedSplitCredit.selector);
|
|
184
|
-
distributor.processSplitWith(context);
|
|
185
|
-
|
|
186
|
-
// The victim hook's balance should remain intact.
|
|
187
|
-
assertEq(distributor.balanceOf(victimHook, IERC20(address(rewardToken))), 1000 ether);
|
|
188
|
-
// No balance should be credited to the attacker's hook.
|
|
189
|
-
assertEq(distributor.balanceOf(address(attackerVotes), IERC20(address(rewardToken))), 0);
|
|
190
|
-
}
|
|
191
|
-
}
|