@bananapus/721-hook-v6 0.0.36 → 0.0.38
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/ADMINISTRATION.md +47 -47
- package/ARCHITECTURE.md +53 -46
- package/AUDIT_INSTRUCTIONS.md +35 -75
- package/CHANGELOG.md +4 -0
- package/README.md +19 -26
- package/RISKS.md +82 -89
- package/SKILLS.md +17 -22
- package/USER_JOURNEYS.md +61 -133
- package/package.json +1 -1
- package/src/JB721CheckpointsDeployer.sol +2 -0
- package/src/JB721TiersHookProjectDeployer.sol +3 -3
- package/src/interfaces/IJB721CheckpointsDeployer.sol +3 -0
- package/test/audit/CodexNemesisProjectDeployerAuth.t.sol +273 -0
- package/test/unit/JB721CheckpointsDeployer_AccessControl.t.sol +116 -0
|
@@ -24,7 +24,7 @@ import {JBQueueRulesetsConfig} from "./structs/JBQueueRulesetsConfig.sol";
|
|
|
24
24
|
|
|
25
25
|
/// @title JB721TiersHookProjectDeployer
|
|
26
26
|
/// @notice Deploys a project and a 721 tiers hook for it. Can be used to queue rulesets for the project if given
|
|
27
|
-
/// `JBPermissionIds.QUEUE_RULESETS`.
|
|
27
|
+
/// `JBPermissionIds.QUEUE_RULESETS` or `JBPermissionIds.LAUNCH_RULESETS`.
|
|
28
28
|
contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721TiersHookProjectDeployer {
|
|
29
29
|
//*********************************************************************//
|
|
30
30
|
// --------------- public immutable stored properties ---------------- //
|
|
@@ -102,7 +102,7 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/// @notice Launches rulesets for a project with an attached 721 tiers hook.
|
|
105
|
-
/// @dev Only a project's owner or an operator with the `
|
|
105
|
+
/// @dev Only a project's owner or an operator with the `LAUNCH_RULESETS & SET_TERMINALS` permission can launch its
|
|
106
106
|
/// rulesets.
|
|
107
107
|
/// @param projectId The ID of the project that rulesets are being launched for.
|
|
108
108
|
/// @param deployTiersHookConfig Configuration which dictates the behavior of the 721 tiers hook which is being
|
|
@@ -128,7 +128,7 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
|
|
|
128
128
|
|
|
129
129
|
// Enforce permissions.
|
|
130
130
|
_requirePermissionFrom({
|
|
131
|
-
account: PROJECTS.ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.
|
|
131
|
+
account: PROJECTS.ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.LAUNCH_RULESETS
|
|
132
132
|
});
|
|
133
133
|
|
|
134
134
|
_requirePermissionFrom({
|
|
@@ -6,6 +6,9 @@ import {IJB721TiersHookStore} from "./IJB721TiersHookStore.sol";
|
|
|
6
6
|
|
|
7
7
|
/// @notice Deploys JB721Checkpoints clones for JB721TiersHook instances.
|
|
8
8
|
interface IJB721CheckpointsDeployer {
|
|
9
|
+
/// @notice Thrown when the caller is not the hook that the checkpoint module is being deployed for.
|
|
10
|
+
error JB721CheckpointsDeployer_Unauthorized();
|
|
11
|
+
|
|
9
12
|
/// @notice The implementation contract that clones are based on.
|
|
10
13
|
/// @return The implementation address.
|
|
11
14
|
// forge-lint: disable-next-line(mixed-case-function)
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import "../utils/UnitTestSetup.sol";
|
|
5
|
+
|
|
6
|
+
import {JB721TiersHookProjectDeployer} from "../../src/JB721TiersHookProjectDeployer.sol";
|
|
7
|
+
import {JBDeploy721TiersHookConfig} from "../../src/structs/JBDeploy721TiersHookConfig.sol";
|
|
8
|
+
import {JBLaunchRulesetsConfig} from "../../src/structs/JBLaunchRulesetsConfig.sol";
|
|
9
|
+
import {JBQueueRulesetsConfig} from "../../src/structs/JBQueueRulesetsConfig.sol";
|
|
10
|
+
import {JBPayDataHookRulesetConfig} from "../../src/structs/JBPayDataHookRulesetConfig.sol";
|
|
11
|
+
import {JBPayDataHookRulesetMetadata} from "../../src/structs/JBPayDataHookRulesetMetadata.sol";
|
|
12
|
+
import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
|
|
13
|
+
import {JB721InitTiersConfig} from "../../src/structs/JB721InitTiersConfig.sol";
|
|
14
|
+
import {JB721TiersHookFlags} from "../../src/structs/JB721TiersHookFlags.sol";
|
|
15
|
+
import {IJB721TiersHook} from "../../src/interfaces/IJB721TiersHook.sol";
|
|
16
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
17
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
18
|
+
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
19
|
+
import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
|
|
20
|
+
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
21
|
+
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
22
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
23
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
24
|
+
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
25
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
26
|
+
|
|
27
|
+
contract MockProjectsForNemesis {
|
|
28
|
+
uint256 internal _count;
|
|
29
|
+
address internal _owner;
|
|
30
|
+
|
|
31
|
+
function setup(uint256 count_, address owner_) external {
|
|
32
|
+
_count = count_;
|
|
33
|
+
_owner = owner_;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function count() external view returns (uint256) {
|
|
37
|
+
return _count;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ownerOf(uint256) external view returns (address) {
|
|
41
|
+
return _owner;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fallback() external {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
contract StrictControllerForNemesis {
|
|
48
|
+
address internal immutable EXPECTED_CALLER;
|
|
49
|
+
|
|
50
|
+
error UnexpectedCaller(address caller);
|
|
51
|
+
|
|
52
|
+
constructor(address expectedCaller) {
|
|
53
|
+
EXPECTED_CALLER = expectedCaller;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fallback() external payable {
|
|
57
|
+
if (msg.sender != EXPECTED_CALLER) revert UnexpectedCaller(msg.sender);
|
|
58
|
+
bytes memory result = abi.encode(uint256(42));
|
|
59
|
+
assembly {
|
|
60
|
+
return(add(result, 32), mload(result))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
contract Test_CodexNemesisProjectDeployerAuth is UnitTestSetup {
|
|
66
|
+
JB721TiersHookProjectDeployer internal projectDeployer;
|
|
67
|
+
address internal operator = address(0xBEEF);
|
|
68
|
+
uint256 internal testProjectId = 5;
|
|
69
|
+
|
|
70
|
+
function setUp() public override {
|
|
71
|
+
super.setUp();
|
|
72
|
+
|
|
73
|
+
MockProjectsForNemesis projects = new MockProjectsForNemesis();
|
|
74
|
+
vm.etch(mockJBProjects, address(projects).code);
|
|
75
|
+
MockProjectsForNemesis(mockJBProjects).setup(testProjectId, owner);
|
|
76
|
+
|
|
77
|
+
vm.mockCall(mockJBDirectory, abi.encodeWithSelector(IJBDirectory.PROJECTS.selector), abi.encode(mockJBProjects));
|
|
78
|
+
|
|
79
|
+
projectDeployer = new JB721TiersHookProjectDeployer(
|
|
80
|
+
IJBDirectory(mockJBDirectory), IJBPermissions(mockJBPermissions), jbHookDeployer, address(0)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function test_launchRulesetsFor_revertsBecauseControllerSeesProjectDeployerAsCaller() external {
|
|
85
|
+
(JBDeploy721TiersHookConfig memory hookConfig, JBLaunchRulesetsConfig memory launchConfig) =
|
|
86
|
+
_launchConfig(testProjectId);
|
|
87
|
+
|
|
88
|
+
StrictControllerForNemesis controller = new StrictControllerForNemesis(owner);
|
|
89
|
+
|
|
90
|
+
vm.prank(owner);
|
|
91
|
+
vm.expectRevert(
|
|
92
|
+
abi.encodeWithSelector(StrictControllerForNemesis.UnexpectedCaller.selector, address(projectDeployer))
|
|
93
|
+
);
|
|
94
|
+
projectDeployer.launchRulesetsFor(
|
|
95
|
+
testProjectId, hookConfig, launchConfig, IJBController(address(controller)), bytes32(0)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function test_queueRulesetsOf_revertsBecauseControllerSeesProjectDeployerAsCaller() external {
|
|
100
|
+
(JBDeploy721TiersHookConfig memory hookConfig, JBQueueRulesetsConfig memory queueConfig) =
|
|
101
|
+
_queueConfig(testProjectId);
|
|
102
|
+
|
|
103
|
+
StrictControllerForNemesis controller = new StrictControllerForNemesis(owner);
|
|
104
|
+
|
|
105
|
+
vm.prank(owner);
|
|
106
|
+
vm.expectRevert(
|
|
107
|
+
abi.encodeWithSelector(StrictControllerForNemesis.UnexpectedCaller.selector, address(projectDeployer))
|
|
108
|
+
);
|
|
109
|
+
projectDeployer.queueRulesetsOf(
|
|
110
|
+
testProjectId, hookConfig, queueConfig, IJBController(address(controller)), bytes32(0)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function test_launchRulesetsFor_checksLaunchPermission() external {
|
|
115
|
+
(JBDeploy721TiersHookConfig memory hookConfig, JBLaunchRulesetsConfig memory launchConfig) =
|
|
116
|
+
_launchConfig(testProjectId);
|
|
117
|
+
|
|
118
|
+
address account = owner;
|
|
119
|
+
StrictControllerForNemesis controller = new StrictControllerForNemesis(owner);
|
|
120
|
+
|
|
121
|
+
// Grant LAUNCH_RULESETS and SET_TERMINALS, deny QUEUE_RULESETS.
|
|
122
|
+
vm.mockCall(
|
|
123
|
+
mockJBPermissions,
|
|
124
|
+
abi.encodeWithSelector(
|
|
125
|
+
IJBPermissions.hasPermission.selector,
|
|
126
|
+
operator,
|
|
127
|
+
account,
|
|
128
|
+
testProjectId,
|
|
129
|
+
JBPermissionIds.QUEUE_RULESETS,
|
|
130
|
+
true,
|
|
131
|
+
true
|
|
132
|
+
),
|
|
133
|
+
abi.encode(false)
|
|
134
|
+
);
|
|
135
|
+
vm.mockCall(
|
|
136
|
+
mockJBPermissions,
|
|
137
|
+
abi.encodeWithSelector(
|
|
138
|
+
IJBPermissions.hasPermission.selector,
|
|
139
|
+
operator,
|
|
140
|
+
account,
|
|
141
|
+
testProjectId,
|
|
142
|
+
JBPermissionIds.LAUNCH_RULESETS,
|
|
143
|
+
true,
|
|
144
|
+
true
|
|
145
|
+
),
|
|
146
|
+
abi.encode(true)
|
|
147
|
+
);
|
|
148
|
+
vm.mockCall(
|
|
149
|
+
mockJBPermissions,
|
|
150
|
+
abi.encodeWithSelector(
|
|
151
|
+
IJBPermissions.hasPermission.selector,
|
|
152
|
+
operator,
|
|
153
|
+
account,
|
|
154
|
+
testProjectId,
|
|
155
|
+
JBPermissionIds.SET_TERMINALS,
|
|
156
|
+
true,
|
|
157
|
+
true
|
|
158
|
+
),
|
|
159
|
+
abi.encode(true)
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// The permission check passes with LAUNCH_RULESETS. The call proceeds to the controller, which reverts
|
|
163
|
+
// because it sees the deployer contract as the caller rather than the original operator.
|
|
164
|
+
vm.prank(operator);
|
|
165
|
+
vm.expectRevert(
|
|
166
|
+
abi.encodeWithSelector(StrictControllerForNemesis.UnexpectedCaller.selector, address(projectDeployer))
|
|
167
|
+
);
|
|
168
|
+
projectDeployer.launchRulesetsFor(
|
|
169
|
+
testProjectId, hookConfig, launchConfig, IJBController(address(controller)), bytes32(0)
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function _launchConfig(uint256 projectId_)
|
|
174
|
+
internal
|
|
175
|
+
view
|
|
176
|
+
returns (JBDeploy721TiersHookConfig memory hookConfig, JBLaunchRulesetsConfig memory launchConfig)
|
|
177
|
+
{
|
|
178
|
+
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
179
|
+
tierConfigs[0] = JB721TierConfig({
|
|
180
|
+
price: uint104(10),
|
|
181
|
+
initialSupply: uint32(100),
|
|
182
|
+
votingUnits: uint16(0),
|
|
183
|
+
reserveFrequency: uint16(0),
|
|
184
|
+
reserveBeneficiary: reserveBeneficiary,
|
|
185
|
+
encodedIPFSUri: tokenUris[0],
|
|
186
|
+
category: uint24(1),
|
|
187
|
+
discountPercent: uint8(0),
|
|
188
|
+
flags: JB721TierConfigFlags({
|
|
189
|
+
allowOwnerMint: false,
|
|
190
|
+
useReserveBeneficiaryAsDefault: false,
|
|
191
|
+
transfersPausable: false,
|
|
192
|
+
useVotingUnits: false,
|
|
193
|
+
cantBeRemoved: false,
|
|
194
|
+
cantIncreaseDiscountPercent: false,
|
|
195
|
+
cantBuyWithCredits: false
|
|
196
|
+
}),
|
|
197
|
+
splitPercent: 0,
|
|
198
|
+
splits: new JBSplit[](0)
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
hookConfig = JBDeploy721TiersHookConfig({
|
|
202
|
+
name: name,
|
|
203
|
+
symbol: symbol,
|
|
204
|
+
baseUri: baseUri,
|
|
205
|
+
tokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
206
|
+
contractUri: contractUri,
|
|
207
|
+
tiersConfig: JB721InitTiersConfig({
|
|
208
|
+
tiers: tierConfigs, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
|
|
209
|
+
}),
|
|
210
|
+
flags: JB721TiersHookFlags({
|
|
211
|
+
preventOverspending: false,
|
|
212
|
+
issueTokensForSplits: false,
|
|
213
|
+
noNewTiersWithReserves: true,
|
|
214
|
+
noNewTiersWithVotes: false,
|
|
215
|
+
noNewTiersWithOwnerMinting: true
|
|
216
|
+
})
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
JBPayDataHookRulesetConfig[] memory rulesetConfigs = new JBPayDataHookRulesetConfig[](1);
|
|
220
|
+
rulesetConfigs[0].mustStartAtOrAfter = 0;
|
|
221
|
+
rulesetConfigs[0].duration = 14;
|
|
222
|
+
rulesetConfigs[0].weight = 1e18;
|
|
223
|
+
rulesetConfigs[0].weightCutPercent = 0;
|
|
224
|
+
rulesetConfigs[0].approvalHook = IJBRulesetApprovalHook(address(0));
|
|
225
|
+
rulesetConfigs[0].metadata = JBPayDataHookRulesetMetadata({
|
|
226
|
+
reservedPercent: 5000,
|
|
227
|
+
cashOutTaxRate: 5000,
|
|
228
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
229
|
+
pausePay: false,
|
|
230
|
+
pauseCreditTransfers: false,
|
|
231
|
+
allowOwnerMinting: false,
|
|
232
|
+
allowSetCustomToken: false,
|
|
233
|
+
allowTerminalMigration: false,
|
|
234
|
+
allowSetTerminals: false,
|
|
235
|
+
allowSetController: false,
|
|
236
|
+
ownerMustSendPayouts: false,
|
|
237
|
+
allowAddAccountingContext: false,
|
|
238
|
+
allowAddPriceFeed: false,
|
|
239
|
+
holdFees: false,
|
|
240
|
+
useTotalSurplusForCashOuts: false,
|
|
241
|
+
useDataHookForCashOut: false,
|
|
242
|
+
metadata: 0x00
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
JBAccountingContext[] memory accountingContexts = new JBAccountingContext[](1);
|
|
246
|
+
accountingContexts[0] = JBAccountingContext({
|
|
247
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18, token: JBConstants.NATIVE_TOKEN
|
|
248
|
+
});
|
|
249
|
+
JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
|
|
250
|
+
terminalConfigs[0] = JBTerminalConfig({
|
|
251
|
+
terminal: IJBTerminal(mockTerminalAddress), accountingContextsToAccept: accountingContexts
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
launchConfig = JBLaunchRulesetsConfig({
|
|
255
|
+
projectId: uint56(projectId_),
|
|
256
|
+
rulesetConfigurations: rulesetConfigs,
|
|
257
|
+
terminalConfigurations: terminalConfigs,
|
|
258
|
+
memo: "launch"
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function _queueConfig(uint256 projectId_)
|
|
263
|
+
internal
|
|
264
|
+
view
|
|
265
|
+
returns (JBDeploy721TiersHookConfig memory hookConfig, JBQueueRulesetsConfig memory queueConfig)
|
|
266
|
+
{
|
|
267
|
+
JBLaunchRulesetsConfig memory launchConfig;
|
|
268
|
+
(hookConfig, launchConfig) = _launchConfig(projectId_);
|
|
269
|
+
queueConfig = JBQueueRulesetsConfig({
|
|
270
|
+
projectId: uint56(projectId_), rulesetConfigurations: launchConfig.rulesetConfigurations, memo: "queue"
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
}
|