@bananapus/omnichain-deployers-v6 0.0.20 → 0.0.21
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 +4 -2
- package/package.json +3 -4
- package/references/operations.md +1 -0
- package/src/JBOmnichainDeployer.sol +7 -2
- package/src/interfaces/IJBOmnichainDeployer.sol +3 -0
- package/test/JBOmnichainDeployer.t.sol +23 -0
- package/test/OmnichainDeployerEdgeCases.t.sol +10 -1
- package/test/TestAuditGaps.sol +23 -0
- package/test/audit/CarryForwardRejectedHook.t.sol +305 -0
- package/test/audit/JBOmnichainDeployer.t.sol +10 -0
- package/assets/findings/nana-omnichain-deployers-v6-pashov-ai-audit-report-20260330-103536.md +0 -39
package/ADMINISTRATION.md
CHANGED
|
@@ -43,7 +43,7 @@ Admin privileges and their scope in nana-omnichain-deployers-v6.
|
|
|
43
43
|
|
|
44
44
|
| Function | Required Role | Permission ID | Scope | What It Does |
|
|
45
45
|
|----------|--------------|---------------|-------|--------------|
|
|
46
|
-
| `deploySuckersFor` | Project owner or operator | `DEPLOY_SUCKERS` | Per-project | Deploys new cross-chain suckers for an existing project via the sucker registry. |
|
|
46
|
+
| `deploySuckersFor` | Project owner or operator | `DEPLOY_SUCKERS` | Per-project | Deploys new cross-chain suckers for an existing project via the sucker registry. The same operation also applies token mappings on the new suckers, so existing projects must already have the registry arranged as an authorized `MAP_SUCKER_TOKEN` operator for that project. |
|
|
47
47
|
| `launchRulesetsFor` | Project owner or operator | `LAUNCH_RULESETS` + `SET_TERMINALS` | Per-project | Deploys a 721 tiers hook, launches new rulesets with terminal configuration for an existing project. Has a simplified overload without `deploy721Config`. |
|
|
48
48
|
| `queueRulesetsOf` | Project owner or operator | `QUEUE_RULESETS` | Per-project | Queues new rulesets for an existing project. If tiers provided, deploys a new 721 hook. Otherwise, carries forward the 721 hook from the latest ruleset. Has a simplified overload without `deploy721Config`. |
|
|
49
49
|
|
|
@@ -81,12 +81,14 @@ Admin privileges and their scope in nana-omnichain-deployers-v6.
|
|
|
81
81
|
|
|
82
82
|
| Action | Who | Mechanism |
|
|
83
83
|
|--------|-----|-----------|
|
|
84
|
-
| Deploy suckers for existing project | Project owner or DEPLOY_SUCKERS operator | `deploySuckersFor` calls `SUCKER_REGISTRY.deploySuckersFor` |
|
|
84
|
+
| Deploy suckers for existing project | Project owner or DEPLOY_SUCKERS operator | `deploySuckersFor` calls `SUCKER_REGISTRY.deploySuckersFor`; because the registry also applies the initial mappings, existing projects must pair this with a project-level `MAP_SUCKER_TOKEN` arrangement for the registry |
|
|
85
85
|
| Deploy suckers during project launch | Project deployer (anyone) | Included in `launchProjectFor` if `salt != bytes32(0)` |
|
|
86
86
|
| Map sucker tokens | JBSuckerRegistry | Granted MAP_SUCKER_TOKEN at construction with projectId=0 wildcard |
|
|
87
87
|
| Grant 0% cash-out tax to suckers | Automatic | `beforeCashOutRecordedWith` checks `SUCKER_REGISTRY.isSuckerOf` |
|
|
88
88
|
| Grant mint permission to suckers | Automatic | `hasMintPermissionFor` checks `SUCKER_REGISTRY.isSuckerOf` |
|
|
89
89
|
|
|
90
|
+
**Existing-project operator note:** `deploySuckersFor` looks like a deployment-only action at the top level, but it is intentionally a deploy-and-map flow. If an existing project delegates `DEPLOY_SUCKERS` without also arranging `MAP_SUCKER_TOKEN` for the registry, the transaction will fail once the registry reaches the mapping step.
|
|
91
|
+
|
|
90
92
|
**Cross-chain determinism:** The salt for sucker deployment is combined with `_msgSender()` (`keccak256(abi.encode(salt, _msgSender()))`). Deploying from the same sender address with the same salt on each chain produces matching sucker addresses.
|
|
91
93
|
|
|
92
94
|
## Data Hook Proxy Pattern
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/omnichain-deployers-v6",
|
|
3
|
-
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,9 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@bananapus/721-hook-v6": "^0.0.30",
|
|
21
|
-
"@bananapus/
|
|
22
|
-
"@bananapus/
|
|
23
|
-
"@bananapus/core-v6": "^0.0.30",
|
|
21
|
+
"@bananapus/buyback-hook-v6": "^0.0.25",
|
|
22
|
+
"@bananapus/core-v6": "^0.0.31",
|
|
24
23
|
"@bananapus/ownable-v6": "^0.0.16",
|
|
25
24
|
"@bananapus/permission-ids-v6": "^0.0.15",
|
|
26
25
|
"@bananapus/suckers-v6": "^0.0.20",
|
package/references/operations.md
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
- A same-block queue assumption breaks predicted ruleset IDs and silently strands stored config.
|
|
20
20
|
- A project expects custom-hook behavior on cash-out, but the wrapper flags disable it or the sucker exemption bypasses it.
|
|
21
21
|
- Deterministic deployment assumptions fail because sender or salt composition changed.
|
|
22
|
+
- Existing-project sucker deployment is treated as a pure `DEPLOY_SUCKERS` flow even though the registry also performs token mapping, so missing `MAP_SUCKER_TOKEN` authority for the registry shows up only when the deploy path reaches `mapTokens`.
|
|
22
23
|
|
|
23
24
|
## Useful Proof Points
|
|
24
25
|
|
|
@@ -378,7 +378,9 @@ contract JBOmnichainDeployer is
|
|
|
378
378
|
//*********************************************************************//
|
|
379
379
|
|
|
380
380
|
/// @notice Deploy new suckers for an existing project.
|
|
381
|
-
/// @dev Only the juicebox's owner can
|
|
381
|
+
/// @dev Only the juicebox's owner or an operator with `JBPermissionIds.DEPLOY_SUCKERS` can call this entrypoint.
|
|
382
|
+
/// The downstream registry call also maps the configured tokens on each newly created sucker, so the same
|
|
383
|
+
/// end-to-end operation depends on the project's token-mapping authority being arranged for the registry.
|
|
382
384
|
/// @param projectId The ID of the project to deploy suckers for.
|
|
383
385
|
/// @param suckerDeploymentConfiguration The suckers to set up for the project.
|
|
384
386
|
function deploySuckersFor(
|
|
@@ -806,7 +808,10 @@ contract JBOmnichainDeployer is
|
|
|
806
808
|
// Use the caller-provided flag when deploying a new hook.
|
|
807
809
|
use721ForCashOut = deploy721Config.useDataHookForCashOut;
|
|
808
810
|
} else {
|
|
809
|
-
|
|
811
|
+
// Read the *current* (approved) ruleset — not `latestRulesetId` — because a queued-but-unapproved
|
|
812
|
+
// ruleset may have been rejected by the approval hook, so its hook config should not be carried forward.
|
|
813
|
+
uint256 currentRulesetId = controller.RULESETS().currentOf(projectId).id;
|
|
814
|
+
JBTiered721HookConfig memory previousConfig = _tiered721HookOf[projectId][currentRulesetId];
|
|
810
815
|
hook = previousConfig.hook;
|
|
811
816
|
// Revert if no hook exists to carry forward — this means no tiers were provided and
|
|
812
817
|
// no previous ruleset had a 721 hook deployed through this contract.
|
|
@@ -37,6 +37,9 @@ interface IJBOmnichainDeployer {
|
|
|
37
37
|
returns (IJB721TiersHook hook, bool useDataHookForCashOut);
|
|
38
38
|
|
|
39
39
|
/// @notice Deploy new suckers for an existing project.
|
|
40
|
+
/// @dev This flow forwards to `JBSuckerRegistry.deploySuckersFor`, which also applies the supplied token mappings
|
|
41
|
+
/// on each newly deployed sucker. Existing projects therefore need both deployment authority and a project-level
|
|
42
|
+
/// arrangement that lets the registry perform `MAP_SUCKER_TOKEN` for the same operation.
|
|
40
43
|
/// @param projectId The ID of the project to deploy suckers for.
|
|
41
44
|
/// @param suckerDeploymentConfiguration The suckers to set up for the project.
|
|
42
45
|
/// @return suckers The addresses of the deployed suckers.
|
|
@@ -392,6 +392,19 @@ contract TestJBOmnichainDeployer is Test {
|
|
|
392
392
|
abi.encodeWithSelector(IJBRulesets.latestRulesetIdOf.selector, projectId),
|
|
393
393
|
abi.encode(launchTimestamp)
|
|
394
394
|
);
|
|
395
|
+
|
|
396
|
+
// Mock currentOf to return a ruleset whose id matches the launch so carry-forward can look up the stored 721
|
|
397
|
+
// hook.
|
|
398
|
+
{
|
|
399
|
+
JBRuleset memory currentRuleset;
|
|
400
|
+
currentRuleset.id = uint48(launchTimestamp);
|
|
401
|
+
vm.mockCall(
|
|
402
|
+
address(rulesets),
|
|
403
|
+
abi.encodeWithSelector(IJBRulesets.currentOf.selector, projectId),
|
|
404
|
+
abi.encode(currentRuleset)
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
395
408
|
// Warp forward so latestRulesetId < block.timestamp.
|
|
396
409
|
vm.warp(block.timestamp + 1);
|
|
397
410
|
|
|
@@ -472,6 +485,16 @@ contract TestJBOmnichainDeployer is Test {
|
|
|
472
485
|
abi.encode(launchTimestamp)
|
|
473
486
|
);
|
|
474
487
|
|
|
488
|
+
// Mock currentOf to return a ruleset whose id matches the launch so carry-forward can look up the stored 721
|
|
489
|
+
// hook.
|
|
490
|
+
JBRuleset memory currentRuleset;
|
|
491
|
+
currentRuleset.id = uint48(launchTimestamp);
|
|
492
|
+
vm.mockCall(
|
|
493
|
+
address(rulesets),
|
|
494
|
+
abi.encodeWithSelector(IJBRulesets.currentOf.selector, projectId),
|
|
495
|
+
abi.encode(currentRuleset)
|
|
496
|
+
);
|
|
497
|
+
|
|
475
498
|
// Warp forward so latestRulesetId < block.timestamp.
|
|
476
499
|
vm.warp(block.timestamp + 1);
|
|
477
500
|
|
|
@@ -614,11 +614,20 @@ contract OmnichainDeployerEdgeCases is Test {
|
|
|
614
614
|
abi.encode(uint256(50)) // A past ruleset ID — no hook stored at this ID
|
|
615
615
|
);
|
|
616
616
|
|
|
617
|
+
// Mock currentOf to return a ruleset with id=50 (no hook stored for this id via the deployer).
|
|
618
|
+
JBRuleset memory currentRuleset;
|
|
619
|
+
currentRuleset.id = uint48(50);
|
|
620
|
+
vm.mockCall(
|
|
621
|
+
address(rulesets),
|
|
622
|
+
abi.encodeWithSelector(IJBRulesets.currentOf.selector, projectId),
|
|
623
|
+
abi.encode(currentRuleset)
|
|
624
|
+
);
|
|
625
|
+
|
|
617
626
|
JBRulesetConfig[] memory configs = new JBRulesetConfig[](1);
|
|
618
627
|
configs[0] = _makeRulesetConfig(address(0), true, false);
|
|
619
628
|
|
|
620
629
|
// Empty 721 config — no tiers, so it tries to carry forward an existing hook.
|
|
621
|
-
// But no hook was stored for rulesetId=
|
|
630
|
+
// But no hook was stored for rulesetId=50 via this deployer.
|
|
622
631
|
JBOmnichain721Config memory empty721Config;
|
|
623
632
|
vm.prank(projectOwner);
|
|
624
633
|
vm.expectRevert(JBOmnichainDeployer.JBOmnichainDeployer_InvalidHook.selector);
|
package/test/TestAuditGaps.sol
CHANGED
|
@@ -539,6 +539,7 @@ contract TestAuditGaps is Test {
|
|
|
539
539
|
// latestRulesetIdOf still = BASE_TIME (from launch).
|
|
540
540
|
// Guard: BASE_TIME >= BASE_TIME + 1 -> false -> passes.
|
|
541
541
|
_mockLatestRulesetId(BASE_TIME);
|
|
542
|
+
_mockCurrentRulesetId(BASE_TIME);
|
|
542
543
|
|
|
543
544
|
uint256 expectedQueuedId = BASE_TIME + 1;
|
|
544
545
|
vm.mockCall(
|
|
@@ -565,6 +566,7 @@ contract TestAuditGaps is Test {
|
|
|
565
566
|
// Warp forward 1 second for first queue.
|
|
566
567
|
vm.warp(BASE_TIME + 1);
|
|
567
568
|
_mockLatestRulesetId(BASE_TIME);
|
|
569
|
+
_mockCurrentRulesetId(BASE_TIME);
|
|
568
570
|
|
|
569
571
|
uint256 firstQueueTime = BASE_TIME + 1;
|
|
570
572
|
vm.mockCall(
|
|
@@ -672,6 +674,7 @@ contract TestAuditGaps is Test {
|
|
|
672
674
|
// Warp past the latestRulesetId: block.timestamp = BASE_TIME + 3.
|
|
673
675
|
vm.warp(BASE_TIME + 3);
|
|
674
676
|
_mockLatestRulesetId(latestRulesetId);
|
|
677
|
+
_mockCurrentRulesetId(BASE_TIME);
|
|
675
678
|
|
|
676
679
|
uint256 expectedQueuedId = BASE_TIME + 3;
|
|
677
680
|
vm.mockCall(
|
|
@@ -699,6 +702,7 @@ contract TestAuditGaps is Test {
|
|
|
699
702
|
// Warp forward.
|
|
700
703
|
vm.warp(BASE_TIME + 100);
|
|
701
704
|
_mockLatestRulesetId(BASE_TIME);
|
|
705
|
+
_mockCurrentRulesetId(BASE_TIME);
|
|
702
706
|
|
|
703
707
|
uint256 expectedQueuedId = BASE_TIME + 100;
|
|
704
708
|
vm.mockCall(
|
|
@@ -739,6 +743,16 @@ contract TestAuditGaps is Test {
|
|
|
739
743
|
vm.warp(BASE_TIME + 50);
|
|
740
744
|
_mockLatestRulesetId(BASE_TIME);
|
|
741
745
|
|
|
746
|
+
// Mock currentOf to return a ruleset whose id matches the launch ruleset so carry-forward can look up the
|
|
747
|
+
// stored 721 hook.
|
|
748
|
+
JBRuleset memory currentRuleset;
|
|
749
|
+
currentRuleset.id = uint48(BASE_TIME);
|
|
750
|
+
vm.mockCall(
|
|
751
|
+
address(rulesetsContract),
|
|
752
|
+
abi.encodeWithSelector(IJBRulesets.currentOf.selector, projectId),
|
|
753
|
+
abi.encode(currentRuleset)
|
|
754
|
+
);
|
|
755
|
+
|
|
742
756
|
uint256 expectedQueuedId = BASE_TIME + 50;
|
|
743
757
|
vm.mockCall(
|
|
744
758
|
address(controller),
|
|
@@ -820,6 +834,7 @@ contract TestAuditGaps is Test {
|
|
|
820
834
|
|
|
821
835
|
vm.warp(BASE_TIME + 10);
|
|
822
836
|
_mockLatestRulesetId(BASE_TIME);
|
|
837
|
+
_mockCurrentRulesetId(BASE_TIME);
|
|
823
838
|
|
|
824
839
|
uint256 expectedQueuedId = BASE_TIME + 10;
|
|
825
840
|
vm.mockCall(
|
|
@@ -866,6 +881,14 @@ contract TestAuditGaps is Test {
|
|
|
866
881
|
);
|
|
867
882
|
}
|
|
868
883
|
|
|
884
|
+
function _mockCurrentRulesetId(uint256 currentRulesetId) internal {
|
|
885
|
+
JBRuleset memory r;
|
|
886
|
+
r.id = uint48(currentRulesetId);
|
|
887
|
+
vm.mockCall(
|
|
888
|
+
address(rulesetsContract), abi.encodeWithSelector(IJBRulesets.currentOf.selector, projectId), abi.encode(r)
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
869
892
|
function _makePayContext(uint256 pid, uint256 rid) internal returns (JBBeforePayRecordedContext memory) {
|
|
870
893
|
return JBBeforePayRecordedContext({
|
|
871
894
|
terminal: makeAddr("terminal"),
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {TestBaseWorkflow} from "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
5
|
+
import {JBApprovalStatus} from "@bananapus/core-v6/src/enums/JBApprovalStatus.sol";
|
|
6
|
+
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
7
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
8
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
9
|
+
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
10
|
+
import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
|
|
11
|
+
import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
|
|
12
|
+
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
13
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
14
|
+
import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
|
|
15
|
+
import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
|
|
16
|
+
import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
|
|
17
|
+
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
18
|
+
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
19
|
+
import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
|
|
20
|
+
import {JBDeploy721TiersHookConfig} from "@bananapus/721-hook-v6/src/structs/JBDeploy721TiersHookConfig.sol";
|
|
21
|
+
import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
|
|
22
|
+
import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
|
|
23
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
24
|
+
import {JBSuckersPair} from "@bananapus/suckers-v6/src/structs/JBSuckersPair.sol";
|
|
25
|
+
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
26
|
+
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
|
|
27
|
+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
28
|
+
import {JBOmnichainDeployer} from "../../src/JBOmnichainDeployer.sol";
|
|
29
|
+
import {JBOmnichain721Config} from "../../src/structs/JBOmnichain721Config.sol";
|
|
30
|
+
import {JBSuckerDeploymentConfig} from "../../src/structs/JBSuckerDeploymentConfig.sol";
|
|
31
|
+
|
|
32
|
+
contract RejectingApprovalHook is ERC165, IJBRulesetApprovalHook {
|
|
33
|
+
function DURATION() external pure override returns (uint256) {
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function approvalStatusOf(uint256, JBRuleset calldata) external pure override returns (JBApprovalStatus) {
|
|
38
|
+
return JBApprovalStatus.Failed;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
|
|
42
|
+
return interfaceId == type(IJBRulesetApprovalHook).interfaceId || super.supportsInterface(interfaceId);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
contract HookStub {
|
|
47
|
+
uint256 public lastProjectId;
|
|
48
|
+
|
|
49
|
+
function transferOwnershipToProject(uint256 projectId) external {
|
|
50
|
+
lastProjectId = projectId;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
contract SequentialHookDeployer is IJB721TiersHookDeployer {
|
|
55
|
+
HookStub[] internal _hooks;
|
|
56
|
+
uint256 internal _index;
|
|
57
|
+
|
|
58
|
+
constructor(HookStub[] memory hooks) {
|
|
59
|
+
_hooks = hooks;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function deployHookFor(
|
|
63
|
+
uint256,
|
|
64
|
+
JBDeploy721TiersHookConfig calldata,
|
|
65
|
+
bytes32
|
|
66
|
+
)
|
|
67
|
+
external
|
|
68
|
+
override
|
|
69
|
+
returns (IJB721TiersHook)
|
|
70
|
+
{
|
|
71
|
+
return IJB721TiersHook(address(_hooks[_index++]));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
contract MockSuckerRegistryCarryForward is IJBSuckerRegistry {
|
|
76
|
+
function DIRECTORY() external pure override returns (IJBDirectory) {
|
|
77
|
+
return IJBDirectory(address(0));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function PROJECTS() external pure override returns (IJBProjects) {
|
|
81
|
+
return IJBProjects(address(0));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isSuckerOf(uint256, address) external pure override returns (bool) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function suckerDeployerIsAllowed(address) external pure override returns (bool) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function suckerPairsOf(uint256) external pure override returns (JBSuckersPair[] memory) {
|
|
93
|
+
return new JBSuckersPair[](0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function suckersOf(uint256) external pure override returns (address[] memory) {
|
|
97
|
+
return new address[](0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function MAX_TO_REMOTE_FEE() external pure override returns (uint256) {
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function toRemoteFee() external pure override returns (uint256) {
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function setToRemoteFee(uint256) external override {}
|
|
109
|
+
function allowSuckerDeployer(address) external override {}
|
|
110
|
+
function allowSuckerDeployers(address[] calldata) external override {}
|
|
111
|
+
|
|
112
|
+
function deploySuckersFor(
|
|
113
|
+
uint256,
|
|
114
|
+
bytes32,
|
|
115
|
+
JBSuckerDeployerConfig[] memory
|
|
116
|
+
)
|
|
117
|
+
external
|
|
118
|
+
pure
|
|
119
|
+
override
|
|
120
|
+
returns (address[] memory)
|
|
121
|
+
{
|
|
122
|
+
return new address[](0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function removeDeprecatedSucker(uint256, address) external override {}
|
|
126
|
+
function removeSuckerDeployer(address) external override {}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
contract CarryForwardRejectedHookTest is TestBaseWorkflow {
|
|
130
|
+
JBOmnichainDeployer internal deployer;
|
|
131
|
+
RejectingApprovalHook internal rejectHook;
|
|
132
|
+
SequentialHookDeployer internal hookDeployer;
|
|
133
|
+
HookStub internal initialHook;
|
|
134
|
+
HookStub internal rejectedHook;
|
|
135
|
+
MockSuckerRegistryCarryForward internal suckerRegistry;
|
|
136
|
+
|
|
137
|
+
address internal owner;
|
|
138
|
+
|
|
139
|
+
function setUp() public override {
|
|
140
|
+
super.setUp();
|
|
141
|
+
|
|
142
|
+
owner = multisig();
|
|
143
|
+
rejectHook = new RejectingApprovalHook();
|
|
144
|
+
initialHook = new HookStub();
|
|
145
|
+
rejectedHook = new HookStub();
|
|
146
|
+
HookStub[] memory hooks = new HookStub[](2);
|
|
147
|
+
hooks[0] = initialHook;
|
|
148
|
+
hooks[1] = rejectedHook;
|
|
149
|
+
hookDeployer = new SequentialHookDeployer(hooks);
|
|
150
|
+
suckerRegistry = new MockSuckerRegistryCarryForward();
|
|
151
|
+
|
|
152
|
+
deployer = new JBOmnichainDeployer(
|
|
153
|
+
IJBSuckerRegistry(address(suckerRegistry)),
|
|
154
|
+
IJB721TiersHookDeployer(address(hookDeployer)),
|
|
155
|
+
IJBPermissions(address(jbPermissions())),
|
|
156
|
+
IJBProjects(address(jbProjects())),
|
|
157
|
+
trustedForwarder()
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
vm.prank(multisig());
|
|
161
|
+
jbDirectory().setIsAllowedToSetFirstController(address(deployer), true);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function test_queueCarryForward_usesRejectedRulesetHook_notCurrentRulesetHook() public {
|
|
165
|
+
JBRulesetConfig[] memory initialRulesets = _makeRulesets(rejectHook);
|
|
166
|
+
JBTerminalConfig[] memory terminals = new JBTerminalConfig[](0);
|
|
167
|
+
JBOmnichain721Config memory configWithTiers = _configWithTiers(false);
|
|
168
|
+
|
|
169
|
+
(uint256 projectId, IJB721TiersHook launchedHook,) = deployer.launchProjectFor(
|
|
170
|
+
owner,
|
|
171
|
+
"ipfs://project",
|
|
172
|
+
configWithTiers,
|
|
173
|
+
initialRulesets,
|
|
174
|
+
terminals,
|
|
175
|
+
"launch",
|
|
176
|
+
_emptySuckerConfig(),
|
|
177
|
+
IJBController(address(jbController()))
|
|
178
|
+
);
|
|
179
|
+
uint256 initialRulesetId = jbRulesets().latestRulesetIdOf(projectId);
|
|
180
|
+
|
|
181
|
+
assertEq(address(launchedHook), address(initialHook), "launch should use the first hook");
|
|
182
|
+
|
|
183
|
+
_grantQueuePermissions(projectId);
|
|
184
|
+
|
|
185
|
+
vm.warp(block.timestamp + 1);
|
|
186
|
+
|
|
187
|
+
JBOmnichain721Config memory rejectedConfig = _configWithTiers(true);
|
|
188
|
+
(uint256 rejectedRulesetId,) = deployer.queueRulesetsOf(
|
|
189
|
+
projectId,
|
|
190
|
+
rejectedConfig,
|
|
191
|
+
_makeRulesets(IJBRulesetApprovalHook(address(0))),
|
|
192
|
+
"rejected",
|
|
193
|
+
IJBController(address(jbController()))
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
JBRuleset memory currentRuleset = jbRulesets().currentOf(projectId);
|
|
197
|
+
assertEq(currentRuleset.id, initialRulesetId, "core should still use the last approved ruleset");
|
|
198
|
+
|
|
199
|
+
vm.warp(block.timestamp + 2);
|
|
200
|
+
|
|
201
|
+
(uint256 carriedRulesetId,) = deployer.queueRulesetsOf(
|
|
202
|
+
projectId,
|
|
203
|
+
_default721Config(),
|
|
204
|
+
_makeRulesets(IJBRulesetApprovalHook(address(0))),
|
|
205
|
+
"carry",
|
|
206
|
+
IJBController(address(jbController()))
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
(IJB721TiersHook carriedHook, bool carriedCashOutFlag) = deployer.tiered721HookOf(projectId, carriedRulesetId);
|
|
210
|
+
(IJB721TiersHook currentHook, bool currentCashOutFlag) = deployer.tiered721HookOf(projectId, initialRulesetId);
|
|
211
|
+
(IJB721TiersHook rejectedStoredHook, bool rejectedCashOutFlag) =
|
|
212
|
+
deployer.tiered721HookOf(projectId, rejectedRulesetId);
|
|
213
|
+
|
|
214
|
+
assertEq(address(currentHook), address(initialHook), "approved ruleset should point to the initial hook");
|
|
215
|
+
assertFalse(currentCashOutFlag, "approved ruleset should preserve its original cash-out flag");
|
|
216
|
+
|
|
217
|
+
assertEq(address(rejectedStoredHook), address(rejectedHook), "rejected ruleset should store the second hook");
|
|
218
|
+
assertTrue(rejectedCashOutFlag, "rejected ruleset should store the second cash-out flag");
|
|
219
|
+
|
|
220
|
+
// The carry-forward uses currentOf (the approved ruleset), not the rejected queued one.
|
|
221
|
+
assertEq(
|
|
222
|
+
address(carriedHook),
|
|
223
|
+
address(initialHook),
|
|
224
|
+
"carry-forward should inherit the current (approved) hook, not the rejected one"
|
|
225
|
+
);
|
|
226
|
+
assertFalse(carriedCashOutFlag, "carry-forward should inherit the current (approved) cash-out flag");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function _configWithTiers(bool useDataHookForCashOut) internal pure returns (JBOmnichain721Config memory config) {
|
|
230
|
+
config = _default721Config();
|
|
231
|
+
config.useDataHookForCashOut = useDataHookForCashOut;
|
|
232
|
+
config.deployTiersHookConfig.tiersConfig.tiers = new JB721TierConfig[](1);
|
|
233
|
+
config.deployTiersHookConfig.tiersConfig.tiers[0].price = 1 ether;
|
|
234
|
+
config.deployTiersHookConfig.tiersConfig.tiers[0].initialSupply = 10;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function _default721Config() internal pure returns (JBOmnichain721Config memory config) {
|
|
238
|
+
config.deployTiersHookConfig.tiersConfig.currency =
|
|
239
|
+
uint32(uint160(address(0x000000000000000000000000000000000000EEEe)));
|
|
240
|
+
config.deployTiersHookConfig.tiersConfig.decimals = 18;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function _makeRulesets(IJBRulesetApprovalHook approvalHook)
|
|
244
|
+
internal
|
|
245
|
+
pure
|
|
246
|
+
returns (JBRulesetConfig[] memory configs)
|
|
247
|
+
{
|
|
248
|
+
configs = new JBRulesetConfig[](1);
|
|
249
|
+
configs[0] = JBRulesetConfig({
|
|
250
|
+
mustStartAtOrAfter: uint48(0),
|
|
251
|
+
duration: uint32(0),
|
|
252
|
+
weight: uint112(1e18),
|
|
253
|
+
weightCutPercent: uint32(0),
|
|
254
|
+
approvalHook: approvalHook,
|
|
255
|
+
metadata: JBRulesetMetadata({
|
|
256
|
+
reservedPercent: 0,
|
|
257
|
+
cashOutTaxRate: 0,
|
|
258
|
+
baseCurrency: uint32(uint160(address(0x000000000000000000000000000000000000EEEe))),
|
|
259
|
+
pausePay: false,
|
|
260
|
+
pauseCreditTransfers: false,
|
|
261
|
+
allowOwnerMinting: false,
|
|
262
|
+
allowSetCustomToken: false,
|
|
263
|
+
allowTerminalMigration: false,
|
|
264
|
+
allowSetController: false,
|
|
265
|
+
allowSetTerminals: false,
|
|
266
|
+
allowAddAccountingContext: false,
|
|
267
|
+
allowAddPriceFeed: false,
|
|
268
|
+
ownerMustSendPayouts: false,
|
|
269
|
+
holdFees: false,
|
|
270
|
+
useTotalSurplusForCashOuts: false,
|
|
271
|
+
useDataHookForPay: false,
|
|
272
|
+
useDataHookForCashOut: false,
|
|
273
|
+
dataHook: address(0),
|
|
274
|
+
metadata: 0
|
|
275
|
+
}),
|
|
276
|
+
splitGroups: new JBSplitGroup[](0),
|
|
277
|
+
fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function _emptySuckerConfig() internal pure returns (JBSuckerDeploymentConfig memory) {
|
|
282
|
+
return JBSuckerDeploymentConfig({deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32(0)});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function _grantQueuePermissions(uint256 projectId) internal {
|
|
286
|
+
uint8[] memory permissionIds = new uint8[](1);
|
|
287
|
+
permissionIds[0] = JBPermissionIds.QUEUE_RULESETS;
|
|
288
|
+
|
|
289
|
+
vm.prank(owner);
|
|
290
|
+
jbPermissions()
|
|
291
|
+
.setPermissionsFor(
|
|
292
|
+
owner,
|
|
293
|
+
JBPermissionsData({operator: address(this), projectId: uint64(projectId), permissionIds: permissionIds})
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
vm.prank(owner);
|
|
297
|
+
jbPermissions()
|
|
298
|
+
.setPermissionsFor(
|
|
299
|
+
owner,
|
|
300
|
+
JBPermissionsData({
|
|
301
|
+
operator: address(deployer), projectId: uint64(projectId), permissionIds: permissionIds
|
|
302
|
+
})
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -11,6 +11,7 @@ import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
|
11
11
|
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
12
12
|
import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
|
|
13
13
|
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
14
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
14
15
|
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|
|
15
16
|
import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
|
|
16
17
|
import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
|
|
@@ -196,6 +197,15 @@ contract JBOmnichainDeployerTest is Test {
|
|
|
196
197
|
abi.encode(initialRulesetId)
|
|
197
198
|
);
|
|
198
199
|
|
|
200
|
+
// Mock currentOf to return a JBRuleset with id = initialRulesetId so the carry-forward lookup succeeds.
|
|
201
|
+
JBRuleset memory currentRuleset;
|
|
202
|
+
currentRuleset.id = uint48(initialRulesetId);
|
|
203
|
+
vm.mockCall(
|
|
204
|
+
address(rulesets),
|
|
205
|
+
abi.encodeWithSelector(IJBRulesets.currentOf.selector, PROJECT_ID),
|
|
206
|
+
abi.encode(currentRuleset)
|
|
207
|
+
);
|
|
208
|
+
|
|
199
209
|
vm.warp(block.timestamp + 1);
|
|
200
210
|
uint256 queuedRulesetId = block.timestamp;
|
|
201
211
|
vm.mockCall(
|
package/assets/findings/nana-omnichain-deployers-v6-pashov-ai-audit-report-20260330-103536.md
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# 🔐 Security Review — nana-omnichain-deployers-v6
|
|
2
|
-
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
## Scope
|
|
6
|
-
|
|
7
|
-
| | |
|
|
8
|
-
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
9
|
-
| **Mode** | ALL / default |
|
|
10
|
-
| **Files reviewed** | `script/Deploy.s.sol` · `script/helpers/DeployersDeploymentLib.sol` · `src/JBOmnichainDeployer.sol`<br>`src/structs/JBDeployerHookConfig.sol` · `src/structs/JBOmnichain721Config.sol` · `src/structs/JBSuckerDeploymentConfig.sol`<br>`src/structs/JBTiered721HookConfig.sol` |
|
|
11
|
-
| **Confidence threshold (1-100)** | 75 |
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## Findings
|
|
16
|
-
|
|
17
|
-
None.
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
Findings List
|
|
22
|
-
|
|
23
|
-
| # | Confidence | Title |
|
|
24
|
-
|---|---|---|
|
|
25
|
-
| - | - | None |
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## Leads
|
|
30
|
-
|
|
31
|
-
_Vulnerability trails with concrete code smells where the full exploit path could not be completed in one analysis pass. These are not false positives — they are high-signal leads for manual review. Not scored._
|
|
32
|
-
|
|
33
|
-
- **Reflexive controller validation overstates what it proves** — `JBOmnichainDeployer._validateController` — Code smells: validation is anchored to `controller.DIRECTORY()` supplied by the same controller being checked, rather than to an immutable trusted directory — This did not survive to a finding because the path does not update the canonical project rulesets or directory, so I could not complete a live exploit that changes real terminal execution. It is still worth tightening or clarifying because the current comment implies stronger authenticity guarantees than the code actually enforces.
|
|
34
|
-
- **`launchRulesetsFor` semantics depend on upstream “first launch only” behavior** — `JBOmnichainDeployer._launchRulesetsFor` — Code smells: the wrapper presents a generic launch entrypoint, but upstream `JBController.launchRulesetsFor` reverts once a project already has rulesets — I did not confirm a security impact, but the API/documentation boundary is easy to misuse and should remain regression-tested so integrations do not assume it can relaunch arbitrary existing projects.
|
|
35
|
-
- **Pay-hook composition assumes the 721 hook preserves its current return-shape invariants** — `JBOmnichainDeployer.beforePayRecordedWith` — Code smells: only `tiered721HookSpecs[0]` is consumed and `projectAmount` is clamped to zero if the returned split amount exceeds the payment amount — I could not prove an exploitable path in the current dependency set because the bundled 721 hook maintains the expected single-spec invariant. This remains a dependency-sensitive integration trail if the upstream hook contract or interface expectations ever change.
|
|
36
|
-
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
> ⚠️ This review was performed by an AI assistant. AI analysis can never verify the complete absence of vulnerabilities and no guarantee of security is given. Team security reviews, bug bounty programs, and on-chain monitoring are strongly recommended. For a consultation regarding your projects' security, visit [https://www.pashov.com](https://www.pashov.com)
|