@bananapus/omnichain-deployers-v6 0.0.23 → 0.0.25
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/ARCHITECTURE.md +10 -0
- package/CHANGELOG.md +5 -0
- package/RISKS.md +1 -1
- package/USER_JOURNEYS.md +5 -4
- package/package.json +7 -7
- package/src/JBOmnichainDeployer.sol +50 -14
- package/test/JBOmnichainDeployer.t.sol +41 -6
- package/test/JBOmnichainDeployerGuard.t.sol +12 -0
- package/test/OmnichainDeployerAttacks.t.sol +20 -6
- package/test/OmnichainDeployerEdgeCases.t.sol +45 -15
- package/test/TestAuditGaps.sol +58 -25
- package/test/Tiered721HookComposition.t.sol +22 -8
- package/test/audit/CarryForwardRejectedHook.t.sol +24 -2
- package/test/audit/CodexNemesisAudit.t.sol +158 -0
- package/test/audit/JBOmnichainDeployer.t.sol +31 -6
- package/test/audit/WeightScalingComparison.t.sol +12 -0
- package/test/fork/OmnichainForkTestBase.sol +9 -1
- package/test/fork/TestSuckerDeploymentFork.t.sol +1 -1
package/ARCHITECTURE.md
CHANGED
|
@@ -31,6 +31,15 @@ caller
|
|
|
31
31
|
-> project ownership is transferred to the intended owner
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
### 721 Hook Carry-Forward (Queue Path)
|
|
35
|
+
|
|
36
|
+
When `queueRulesetsOf` is called without new tiers, the deployer carries the existing 721 hook forward. The source ruleset is chosen with this precedence:
|
|
37
|
+
|
|
38
|
+
1. **Latest queued ruleset** — if its approval status is `Approved` or `Empty` (no approval hook) and it has a hook config stored in the deployer.
|
|
39
|
+
2. **Current active ruleset** — fallback when no qualifying queued ruleset exists.
|
|
40
|
+
|
|
41
|
+
This ensures that a recently queued (and approved) ruleset's hook config takes precedence over a potentially stale active ruleset. The `useDataHookForCashOut` flag is also preserved from whichever source ruleset is selected.
|
|
42
|
+
|
|
34
43
|
### Pay And Cash-Out Wrapping
|
|
35
44
|
|
|
36
45
|
```text
|
|
@@ -47,6 +56,7 @@ runtime callback
|
|
|
47
56
|
- Hook order matters: the 721 hook runs first, and the extra hook receives the updated context.
|
|
48
57
|
- The deployer's predicted ruleset IDs must stay aligned with `JBRulesets` behavior; the storage keys depend on it.
|
|
49
58
|
- Every project launched through this repo gets a 721 hook surface, even if it starts with zero tiers.
|
|
59
|
+
- Carry-forward must prefer the latest approved queued ruleset over the current ruleset to avoid losing hook config from a recently queued update.
|
|
50
60
|
|
|
51
61
|
## Where Complexity Lives
|
|
52
62
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v6 post-audit (M-11 fix)
|
|
4
|
+
|
|
5
|
+
- **Carry-forward hook selection improved.** `queueRulesetsOf` now checks `latestQueuedOf(projectId)` before falling back to `currentOf(projectId)` when carrying forward a 721 hook. Previously it only read `currentOf`, which could miss a recently queued (and approved) ruleset's hook config. The source ruleset must have approval status `Approved` or `Empty` and a stored hook config in the deployer.
|
|
6
|
+
- The `useDataHookForCashOut` flag is preserved from whichever source ruleset is selected during carry-forward.
|
|
7
|
+
|
|
3
8
|
## Scope
|
|
4
9
|
|
|
5
10
|
This file describes the verified change from `nana-omnichain-deployers-v5` to the current `nana-omnichain-deployers-v6` repo.
|
package/RISKS.md
CHANGED
|
@@ -52,7 +52,7 @@ This file focuses on the risks in the deployer layer that launches Juicebox proj
|
|
|
52
52
|
## 6. Integration Risks
|
|
53
53
|
|
|
54
54
|
- **Hook config keyed by predicted rulesetId.** Configs stored at `block.timestamp + i` must match the actual rulesetId assigned by the controller. If the controller assigns different IDs (e.g., due to approval hook delays), the stored configs become unreachable -- payments/cashouts fall through to default behavior (no 721 handling, no extra hook).
|
|
55
|
-
- **Carried-forward 721 hook on queue.** When `tiers.length == 0`, `queueRulesetsOf` carries forward the hook from `
|
|
55
|
+
- **Carried-forward 721 hook on queue.** When `tiers.length == 0`, `queueRulesetsOf` carries forward the hook from a previous ruleset. The source is selected by first checking `latestQueuedOf(projectId)` — if the queued ruleset's approval status is `Approved` or `Empty` and it has a stored hook config, that config is used. Otherwise it falls back to `currentOf(projectId)`. If neither has a hook deployed through this deployer, the mapping is empty and the call reverts with `JBOmnichainDeployer_InvalidHook`. The `useDataHookForCashOut` flag is also preserved from whichever source ruleset is selected.
|
|
56
56
|
- **ERC721Receiver restriction.** `onERC721Received` only accepts from `PROJECTS`. Any other NFTs sent to this contract are permanently lost.
|
|
57
57
|
- **Cross-reference: sucker registration.** The deployer grants `MAP_SUCKER_TOKEN` to `SUCKER_REGISTRY` with `projectId=0` (wildcard). This means the registry can map tokens for ALL projects deployed through this deployer. See [nana-suckers-v6 RISKS.md](../nana-suckers-v6/RISKS.md) for the full sucker lifecycle risks.
|
|
58
58
|
- **Cross-reference: core reentrancy.** The deployer delegates to `JBController` and `JBMultiTerminal` for all fund operations. See [nana-core-v6 RISKS.md](../nana-core-v6/RISKS.md) section 3 for the reentrancy surface of these contracts.
|
package/USER_JOURNEYS.md
CHANGED
|
@@ -36,10 +36,11 @@
|
|
|
36
36
|
**Success:** the deployer carries the existing hook forward, stores it against the queued ruleset, and preserves the ownership and wrapper assumptions bridge flows need.
|
|
37
37
|
|
|
38
38
|
**Flow**
|
|
39
|
-
1. Queue the next ruleset through the deployer using the path that reuses the existing 721 hook.
|
|
40
|
-
2.
|
|
41
|
-
3.
|
|
42
|
-
4.
|
|
39
|
+
1. Queue the next ruleset through the deployer using the path that reuses the existing 721 hook (pass zero tiers).
|
|
40
|
+
2. The deployer selects the source hook: it first checks the latest queued ruleset (if approved or with no approval hook), then falls back to the current active ruleset. This prevents losing a recently queued hook config.
|
|
41
|
+
3. The `useDataHookForCashOut` flag is preserved from whichever source ruleset is selected.
|
|
42
|
+
4. Validate the controller and queued ruleset inputs before relying on the result.
|
|
43
|
+
5. Confirm the queued ruleset now points at the carried-forward hook rather than accidentally dropping the 721 layer.
|
|
43
44
|
|
|
44
45
|
## Journey 4: Compose A Tiered 721 Hook With A Custom Extra Hook
|
|
45
46
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/omnichain-deployers-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.25",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-omnichain-deployers-v6'"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@bananapus/721-hook-v6": "^0.0.
|
|
21
|
-
"@bananapus/buyback-hook-v6": "^0.0.
|
|
22
|
-
"@bananapus/core-v6": "^0.0.
|
|
20
|
+
"@bananapus/721-hook-v6": "^0.0.33",
|
|
21
|
+
"@bananapus/buyback-hook-v6": "^0.0.27",
|
|
22
|
+
"@bananapus/core-v6": "^0.0.34",
|
|
23
23
|
"@bananapus/ownable-v6": "^0.0.17",
|
|
24
|
-
"@bananapus/permission-ids-v6": "^0.0.
|
|
25
|
-
"@bananapus/suckers-v6": "^0.0.
|
|
24
|
+
"@bananapus/permission-ids-v6": "^0.0.17",
|
|
25
|
+
"@bananapus/suckers-v6": "^0.0.25",
|
|
26
26
|
"@openzeppelin/contracts": "^5.6.1",
|
|
27
27
|
"@uniswap/v4-core": "^1.0.2"
|
|
28
28
|
},
|
|
@@ -30,4 +30,4 @@
|
|
|
30
30
|
"@bananapus/address-registry-v6": "^0.0.17",
|
|
31
31
|
"@sphinx-labs/plugins": "^0.33.2"
|
|
32
32
|
}
|
|
33
|
-
}
|
|
33
|
+
}
|
|
@@ -3,6 +3,7 @@ pragma solidity 0.8.28;
|
|
|
3
3
|
|
|
4
4
|
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
5
5
|
import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookProjectDeployer.sol";
|
|
6
|
+
import {JBApprovalStatus} from "@bananapus/core-v6/src/enums/JBApprovalStatus.sol";
|
|
6
7
|
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
7
8
|
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
8
9
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
@@ -132,13 +133,14 @@ contract JBOmnichainDeployer is
|
|
|
132
133
|
// ------------------------- external views -------------------------- //
|
|
133
134
|
//*********************************************************************//
|
|
134
135
|
|
|
135
|
-
/// @notice Allow cash outs from suckers without a tax.
|
|
136
|
+
/// @notice Allow cash outs from suckers without a tax, and compute cross-chain tax supply for non-sucker cash outs.
|
|
136
137
|
/// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a cash out.
|
|
137
138
|
/// @param context Standard Juicebox cash out context. See `JBBeforeCashOutRecordedContext`.
|
|
138
139
|
/// @return cashOutTaxRate The cash out tax rate, which influences the amount of terminal tokens which get cashed
|
|
139
140
|
/// out.
|
|
140
141
|
/// @return cashOutCount The number of project tokens that are cashed out.
|
|
141
|
-
/// @return totalSupply The total
|
|
142
|
+
/// @return totalSupply The total token supply across all chains (for both proportional reclaim and tax).
|
|
143
|
+
/// @return effectiveSurplusValue The global surplus across all chains for proportional reclaim.
|
|
142
144
|
/// @return hookSpecifications The amount of funds and the data to send to cash out hooks (this contract).
|
|
143
145
|
function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
|
|
144
146
|
external
|
|
@@ -148,18 +150,28 @@ contract JBOmnichainDeployer is
|
|
|
148
150
|
uint256 cashOutTaxRate,
|
|
149
151
|
uint256 cashOutCount,
|
|
150
152
|
uint256 totalSupply,
|
|
153
|
+
uint256 effectiveSurplusValue,
|
|
151
154
|
JBCashOutHookSpecification[] memory hookSpecifications
|
|
152
155
|
)
|
|
153
156
|
{
|
|
154
157
|
// If the cash out is from a sucker, bypass all taxes and fees.
|
|
158
|
+
// Pass through the local surplus so the reclaim calculation can compute the pro-rata share.
|
|
155
159
|
if (SUCKER_REGISTRY.isSuckerOf({projectId: context.projectId, addr: context.holder})) {
|
|
156
|
-
return (0, context.cashOutCount, context.totalSupply, hookSpecifications);
|
|
160
|
+
return (0, context.cashOutCount, context.totalSupply, context.surplus.value, hookSpecifications);
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
// Start with the values from the context. Hooks below may override these.
|
|
160
164
|
cashOutTaxRate = context.cashOutTaxRate;
|
|
161
165
|
cashOutCount = context.cashOutCount;
|
|
162
|
-
|
|
166
|
+
|
|
167
|
+
// Compute the cross-chain total supply: local supply + sum of known peer chain supplies.
|
|
168
|
+
// This prevents the cash out tax from vanishing when a holder dominates the local supply.
|
|
169
|
+
totalSupply = context.totalSupply + SUCKER_REGISTRY.remoteTotalSupplyOf(context.projectId);
|
|
170
|
+
|
|
171
|
+
// Compute the cross-chain surplus: local surplus + sum of known peer chain surpluses.
|
|
172
|
+
// This prevents disproportionate reclaim when tokens bridge away but surplus stays.
|
|
173
|
+
effectiveSurplusValue = context.surplus.value
|
|
174
|
+
+ SUCKER_REGISTRY.remoteSurplusOf(context.projectId, 18, uint256(uint160(context.surplus.token)));
|
|
163
175
|
|
|
164
176
|
// Will hold the 721 hook's cash out specifications (always 0 or 1 element).
|
|
165
177
|
JBCashOutHookSpecification[] memory tiered721HookSpecifications;
|
|
@@ -169,8 +181,11 @@ contract JBOmnichainDeployer is
|
|
|
169
181
|
|
|
170
182
|
// If a 721 hook is set and opted into cash out handling, let it adjust the cash out parameters.
|
|
171
183
|
if (address(tiered721Config.hook) != address(0) && tiered721Config.useDataHookForCashOut) {
|
|
172
|
-
// Forward to the 721 hook. It may change the tax rate, count,
|
|
173
|
-
|
|
184
|
+
// Forward to the 721 hook. It may change the tax rate, count, and return hook specs.
|
|
185
|
+
// We discard the inner hook's effectiveSurplusValue — this contract computes the cross-chain values.
|
|
186
|
+
// We also discard its totalSupply since this contract computes the cross-chain supply.
|
|
187
|
+
// slither-disable-next-line unused-return
|
|
188
|
+
(cashOutTaxRate, cashOutCount,,, tiered721HookSpecifications) =
|
|
174
189
|
IJBRulesetDataHook(address(tiered721Config.hook)).beforeCashOutRecordedWith(context);
|
|
175
190
|
}
|
|
176
191
|
|
|
@@ -188,14 +203,17 @@ contract JBOmnichainDeployer is
|
|
|
188
203
|
hookContext.cashOutCount = cashOutCount;
|
|
189
204
|
hookContext.totalSupply = totalSupply;
|
|
190
205
|
|
|
191
|
-
// Forward to the extra hook. It may further change the tax rate, count,
|
|
192
|
-
|
|
206
|
+
// Forward to the extra hook. It may further change the tax rate, count, and return hook specs.
|
|
207
|
+
// We discard the inner hook's effectiveSurplusValue — this contract computes the cross-chain values.
|
|
208
|
+
// We also discard its totalSupply since this contract computes the cross-chain supply.
|
|
209
|
+
// slither-disable-next-line unused-return
|
|
210
|
+
(cashOutTaxRate, cashOutCount,,, extraHookSpecifications) =
|
|
193
211
|
extraHook.dataHook.beforeCashOutRecordedWith(hookContext);
|
|
194
212
|
}
|
|
195
213
|
|
|
196
214
|
// If neither hook returned any specifications, return the adjusted values with no hook specs.
|
|
197
215
|
if (tiered721HookSpecifications.length == 0 && extraHookSpecifications.length == 0) {
|
|
198
|
-
return (cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications);
|
|
216
|
+
return (cashOutTaxRate, cashOutCount, totalSupply, effectiveSurplusValue, hookSpecifications);
|
|
199
217
|
}
|
|
200
218
|
|
|
201
219
|
// Merge both hooks' specifications: 721 spec (if any) first, then extra hook specs.
|
|
@@ -214,7 +232,7 @@ contract JBOmnichainDeployer is
|
|
|
214
232
|
hookSpecifications = extraHookSpecifications;
|
|
215
233
|
}
|
|
216
234
|
|
|
217
|
-
return (cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications);
|
|
235
|
+
return (cashOutTaxRate, cashOutCount, totalSupply, effectiveSurplusValue, hookSpecifications);
|
|
218
236
|
}
|
|
219
237
|
|
|
220
238
|
/// @notice Forward the call to the original data hook.
|
|
@@ -613,6 +631,10 @@ contract JBOmnichainDeployer is
|
|
|
613
631
|
});
|
|
614
632
|
}
|
|
615
633
|
|
|
634
|
+
//*********************************************************************//
|
|
635
|
+
// -------------------------- internal views ------------------------ //
|
|
636
|
+
//*********************************************************************//
|
|
637
|
+
|
|
616
638
|
//*********************************************************************//
|
|
617
639
|
// ------------------------ internal functions ----------------------- //
|
|
618
640
|
//*********************************************************************//
|
|
@@ -808,10 +830,24 @@ contract JBOmnichainDeployer is
|
|
|
808
830
|
// Use the caller-provided flag when deploying a new hook.
|
|
809
831
|
use721ForCashOut = deploy721Config.useDataHookForCashOut;
|
|
810
832
|
} else {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
833
|
+
uint256 sourceRulesetId;
|
|
834
|
+
{
|
|
835
|
+
// First try the latest queued ruleset — if it's been explicitly approved
|
|
836
|
+
// (or has no approval hook), its hook config should take precedence.
|
|
837
|
+
(JBRuleset memory latestQueued, JBApprovalStatus approvalStatus) =
|
|
838
|
+
controller.RULESETS().latestQueuedOf(projectId);
|
|
839
|
+
if (
|
|
840
|
+
latestQueued.id != 0
|
|
841
|
+
&& (approvalStatus == JBApprovalStatus.Approved || approvalStatus == JBApprovalStatus.Empty)
|
|
842
|
+
&& address(_tiered721HookOf[projectId][latestQueued.id].hook) != address(0)
|
|
843
|
+
) {
|
|
844
|
+
sourceRulesetId = latestQueued.id;
|
|
845
|
+
} else {
|
|
846
|
+
// Fall back to the current (active, approved) ruleset.
|
|
847
|
+
sourceRulesetId = controller.RULESETS().currentOf(projectId).id;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
JBTiered721HookConfig memory previousConfig = _tiered721HookOf[projectId][sourceRulesetId];
|
|
815
851
|
hook = previousConfig.hook;
|
|
816
852
|
// Revert if no hook exists to carry forward — this means no tiers were provided and
|
|
817
853
|
// no previous ruleset had a 721 hook deployed through this contract.
|
|
@@ -3,6 +3,7 @@ pragma solidity 0.8.28;
|
|
|
3
3
|
|
|
4
4
|
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
|
|
6
|
+
import {JBApprovalStatus} from "@bananapus/core-v6/src/enums/JBApprovalStatus.sol";
|
|
6
7
|
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
7
8
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
8
9
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
@@ -88,6 +89,18 @@ contract TestJBOmnichainDeployer is Test {
|
|
|
88
89
|
abi.encodeWithSelector(IJBRulesetDataHook.beforePayRecordedWith.selector),
|
|
89
90
|
abi.encode(uint256(1000), new JBPayHookSpecification[](0))
|
|
90
91
|
);
|
|
92
|
+
|
|
93
|
+
// Default: no remote supply or surplus (non-omnichain project).
|
|
94
|
+
vm.mockCall(
|
|
95
|
+
address(suckerRegistry),
|
|
96
|
+
abi.encodeWithSelector(IJBSuckerRegistry.remoteTotalSupplyOf.selector),
|
|
97
|
+
abi.encode(uint256(0))
|
|
98
|
+
);
|
|
99
|
+
vm.mockCall(
|
|
100
|
+
address(suckerRegistry),
|
|
101
|
+
abi.encodeWithSelector(IJBSuckerRegistry.remoteSurplusOf.selector),
|
|
102
|
+
abi.encode(uint256(0))
|
|
103
|
+
);
|
|
91
104
|
}
|
|
92
105
|
|
|
93
106
|
//*********************************************************************//
|
|
@@ -206,7 +219,7 @@ contract TestJBOmnichainDeployer is Test {
|
|
|
206
219
|
|
|
207
220
|
JBBeforeCashOutRecordedContext memory context = _makeCashOutContext(projectId, rulesetId, sucker);
|
|
208
221
|
|
|
209
|
-
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply
|
|
222
|
+
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) =
|
|
210
223
|
deployer.beforeCashOutRecordedWith(context);
|
|
211
224
|
|
|
212
225
|
assertEq(cashOutTaxRate, 0, "sucker should get 0 tax");
|
|
@@ -222,7 +235,7 @@ contract TestJBOmnichainDeployer is Test {
|
|
|
222
235
|
|
|
223
236
|
JBBeforeCashOutRecordedContext memory context = _makeCashOutContext(projectId, rulesetId, randomAddr);
|
|
224
237
|
|
|
225
|
-
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply
|
|
238
|
+
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) =
|
|
226
239
|
deployer.beforeCashOutRecordedWith(context);
|
|
227
240
|
|
|
228
241
|
assertEq(cashOutTaxRate, context.cashOutTaxRate, "non-sucker should get original tax");
|
|
@@ -393,10 +406,22 @@ contract TestJBOmnichainDeployer is Test {
|
|
|
393
406
|
abi.encode(launchTimestamp)
|
|
394
407
|
);
|
|
395
408
|
|
|
396
|
-
// Mock
|
|
397
|
-
|
|
409
|
+
// Mock latestQueuedOf to return the launch ruleset with Empty approval status.
|
|
410
|
+
{
|
|
411
|
+
JBRuleset memory latestQueuedRuleset;
|
|
412
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
413
|
+
latestQueuedRuleset.id = uint48(launchTimestamp);
|
|
414
|
+
vm.mockCall(
|
|
415
|
+
address(rulesets),
|
|
416
|
+
abi.encodeWithSelector(IJBRulesets.latestQueuedOf.selector, projectId),
|
|
417
|
+
abi.encode(latestQueuedRuleset, JBApprovalStatus.Empty)
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Mock currentOf as a fallback (not reached when latestQueuedOf succeeds).
|
|
398
422
|
{
|
|
399
423
|
JBRuleset memory currentRuleset;
|
|
424
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
400
425
|
currentRuleset.id = uint48(launchTimestamp);
|
|
401
426
|
vm.mockCall(
|
|
402
427
|
address(rulesets),
|
|
@@ -485,9 +510,19 @@ contract TestJBOmnichainDeployer is Test {
|
|
|
485
510
|
abi.encode(launchTimestamp)
|
|
486
511
|
);
|
|
487
512
|
|
|
488
|
-
// Mock
|
|
489
|
-
|
|
513
|
+
// Mock latestQueuedOf to return the launch ruleset with Empty approval status.
|
|
514
|
+
JBRuleset memory latestQueuedRuleset;
|
|
515
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
516
|
+
latestQueuedRuleset.id = uint48(launchTimestamp);
|
|
517
|
+
vm.mockCall(
|
|
518
|
+
address(rulesets),
|
|
519
|
+
abi.encodeWithSelector(IJBRulesets.latestQueuedOf.selector, projectId),
|
|
520
|
+
abi.encode(latestQueuedRuleset, JBApprovalStatus.Empty)
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
// Mock currentOf as a fallback (not reached when latestQueuedOf succeeds).
|
|
490
524
|
JBRuleset memory currentRuleset;
|
|
525
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
491
526
|
currentRuleset.id = uint48(launchTimestamp);
|
|
492
527
|
vm.mockCall(
|
|
493
528
|
address(rulesets),
|
|
@@ -82,6 +82,18 @@ contract MockSuckerRegistry is IJBSuckerRegistry {
|
|
|
82
82
|
return new address[](0);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
function remoteTotalSupplyOf(uint256) external pure override returns (uint256) {
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function remoteBalanceOf(uint256, uint256, uint256) external pure override returns (uint256) {
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function remoteSurplusOf(uint256, uint256, uint256) external pure override returns (uint256) {
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
85
97
|
function removeDeprecatedSucker(uint256, address) external override {}
|
|
86
98
|
function removeSuckerDeployer(address) external override {}
|
|
87
99
|
}
|
|
@@ -43,7 +43,7 @@ contract RevertingDataHook is IJBRulesetDataHook {
|
|
|
43
43
|
external
|
|
44
44
|
pure
|
|
45
45
|
override
|
|
46
|
-
returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
46
|
+
returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
47
47
|
{
|
|
48
48
|
revert("Hook always reverts");
|
|
49
49
|
}
|
|
@@ -72,9 +72,11 @@ contract InflatingDataHook is IJBRulesetDataHook {
|
|
|
72
72
|
external
|
|
73
73
|
pure
|
|
74
74
|
override
|
|
75
|
-
returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
75
|
+
returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
76
76
|
{
|
|
77
|
-
return (
|
|
77
|
+
return (
|
|
78
|
+
context.cashOutTaxRate, context.cashOutCount, context.totalSupply, 0, new JBCashOutHookSpecification[](0)
|
|
79
|
+
);
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
function hasMintPermissionFor(uint256, JBRuleset calldata, address) external pure override returns (bool) {
|
|
@@ -142,6 +144,18 @@ contract OmnichainDeployerAttacks is Test {
|
|
|
142
144
|
abi.encodeWithSelector(IJBRulesetDataHook.beforePayRecordedWith.selector),
|
|
143
145
|
abi.encode(uint256(1000), new JBPayHookSpecification[](0))
|
|
144
146
|
);
|
|
147
|
+
|
|
148
|
+
// Default: no remote supply or surplus (non-omnichain project).
|
|
149
|
+
vm.mockCall(
|
|
150
|
+
address(suckerRegistry),
|
|
151
|
+
abi.encodeWithSelector(IJBSuckerRegistry.remoteTotalSupplyOf.selector),
|
|
152
|
+
abi.encode(uint256(0))
|
|
153
|
+
);
|
|
154
|
+
vm.mockCall(
|
|
155
|
+
address(suckerRegistry),
|
|
156
|
+
abi.encodeWithSelector(IJBSuckerRegistry.remoteSurplusOf.selector),
|
|
157
|
+
abi.encode(uint256(0))
|
|
158
|
+
);
|
|
145
159
|
}
|
|
146
160
|
|
|
147
161
|
// =========================================================================
|
|
@@ -166,7 +180,7 @@ contract OmnichainDeployerAttacks is Test {
|
|
|
166
180
|
|
|
167
181
|
JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, sucker);
|
|
168
182
|
|
|
169
|
-
(uint256 cashOutTaxRate
|
|
183
|
+
(uint256 cashOutTaxRate,,,,) = deployer.beforeCashOutRecordedWith(ctx);
|
|
170
184
|
assertEq(cashOutTaxRate, 0, "Sucker should get 0 tax");
|
|
171
185
|
}
|
|
172
186
|
|
|
@@ -182,7 +196,7 @@ contract OmnichainDeployerAttacks is Test {
|
|
|
182
196
|
|
|
183
197
|
JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, attacker);
|
|
184
198
|
|
|
185
|
-
(uint256 cashOutTaxRate
|
|
199
|
+
(uint256 cashOutTaxRate,,,,) = deployer.beforeCashOutRecordedWith(ctx);
|
|
186
200
|
assertEq(cashOutTaxRate, 5000, "Non-sucker should get original tax");
|
|
187
201
|
}
|
|
188
202
|
|
|
@@ -291,7 +305,7 @@ contract OmnichainDeployerAttacks is Test {
|
|
|
291
305
|
JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, storedRulesetId, sucker);
|
|
292
306
|
|
|
293
307
|
// Sucker gets early return with 0 tax — never hits the reverting hook.
|
|
294
|
-
(uint256 cashOutTaxRate
|
|
308
|
+
(uint256 cashOutTaxRate,,,,) = deployer.beforeCashOutRecordedWith(ctx);
|
|
295
309
|
assertEq(cashOutTaxRate, 0, "Sucker bypasses hook and gets 0 tax");
|
|
296
310
|
}
|
|
297
311
|
|
|
@@ -3,6 +3,7 @@ pragma solidity 0.8.28;
|
|
|
3
3
|
|
|
4
4
|
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
|
|
6
|
+
import {JBApprovalStatus} from "@bananapus/core-v6/src/enums/JBApprovalStatus.sol";
|
|
6
7
|
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
7
8
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
8
9
|
import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
|
|
@@ -77,7 +78,7 @@ contract CustomCashOutHook is IJBRulesetDataHook {
|
|
|
77
78
|
external
|
|
78
79
|
view
|
|
79
80
|
override
|
|
80
|
-
returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
81
|
+
returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
81
82
|
{
|
|
82
83
|
JBCashOutHookSpecification[] memory hookSpecifications;
|
|
83
84
|
|
|
@@ -91,7 +92,7 @@ contract CustomCashOutHook is IJBRulesetDataHook {
|
|
|
91
92
|
});
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
return (cashOutTaxRateReturn, cashOutCountReturn, totalSupplyReturn, hookSpecifications);
|
|
95
|
+
return (cashOutTaxRateReturn, cashOutCountReturn, totalSupplyReturn, 0, hookSpecifications);
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
function hasMintPermissionFor(uint256, JBRuleset calldata, address) external view override returns (bool) {
|
|
@@ -144,6 +145,18 @@ contract OmnichainDeployerEdgeCases is Test {
|
|
|
144
145
|
address(suckerRegistry), abi.encodeWithSelector(IJBSuckerRegistry.isSuckerOf.selector), abi.encode(false)
|
|
145
146
|
);
|
|
146
147
|
|
|
148
|
+
// Default: no remote supply or surplus (non-omnichain project).
|
|
149
|
+
vm.mockCall(
|
|
150
|
+
address(suckerRegistry),
|
|
151
|
+
abi.encodeWithSelector(IJBSuckerRegistry.remoteTotalSupplyOf.selector),
|
|
152
|
+
abi.encode(uint256(0))
|
|
153
|
+
);
|
|
154
|
+
vm.mockCall(
|
|
155
|
+
address(suckerRegistry),
|
|
156
|
+
abi.encodeWithSelector(IJBSuckerRegistry.remoteSurplusOf.selector),
|
|
157
|
+
abi.encode(uint256(0))
|
|
158
|
+
);
|
|
159
|
+
|
|
147
160
|
// Hook deployer mocks (every path now deploys a 721 hook).
|
|
148
161
|
vm.mockCall(
|
|
149
162
|
address(hookDeployer),
|
|
@@ -323,7 +336,7 @@ contract OmnichainDeployerEdgeCases is Test {
|
|
|
323
336
|
|
|
324
337
|
JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, attacker);
|
|
325
338
|
|
|
326
|
-
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply
|
|
339
|
+
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
|
|
327
340
|
assertEq(cashOutTaxRate, 5000, "Should return original tax rate");
|
|
328
341
|
assertEq(cashOutCount, 1000, "Should return original cashOutCount");
|
|
329
342
|
assertEq(totalSupply, 10_000, "Should return original totalSupply");
|
|
@@ -340,10 +353,12 @@ contract OmnichainDeployerEdgeCases is Test {
|
|
|
340
353
|
|
|
341
354
|
JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, attacker);
|
|
342
355
|
|
|
343
|
-
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply
|
|
356
|
+
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
|
|
344
357
|
assertEq(cashOutTaxRate, 2000, "Should return custom hook tax rate");
|
|
345
358
|
assertEq(cashOutCount, 500, "Should return custom hook cashOutCount");
|
|
346
|
-
|
|
359
|
+
// The deployer discards the inner hook's totalSupply and computes cross-chain supply instead.
|
|
360
|
+
// With no suckers, this equals context.totalSupply.
|
|
361
|
+
assertEq(totalSupply, ctx.totalSupply, "Should return cross-chain totalSupply (context value with no suckers)");
|
|
347
362
|
}
|
|
348
363
|
|
|
349
364
|
// =========================================================================
|
|
@@ -369,7 +384,7 @@ contract OmnichainDeployerEdgeCases is Test {
|
|
|
369
384
|
|
|
370
385
|
JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, sucker);
|
|
371
386
|
|
|
372
|
-
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply
|
|
387
|
+
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
|
|
373
388
|
assertEq(cashOutTaxRate, 0, "Sucker should get 0 tax regardless of hooks");
|
|
374
389
|
assertEq(cashOutCount, 1000, "Sucker should get original cashOutCount");
|
|
375
390
|
assertEq(totalSupply, 10_000, "Sucker should get original totalSupply");
|
|
@@ -390,14 +405,14 @@ contract OmnichainDeployerEdgeCases is Test {
|
|
|
390
405
|
vm.mockCall(
|
|
391
406
|
mock721,
|
|
392
407
|
abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
|
|
393
|
-
abi.encode(uint256(9999), uint256(1), uint256(1), new JBCashOutHookSpecification[](0))
|
|
408
|
+
abi.encode(uint256(9999), uint256(1), uint256(1), uint256(0), new JBCashOutHookSpecification[](0))
|
|
394
409
|
);
|
|
395
410
|
|
|
396
411
|
JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, attacker);
|
|
397
412
|
|
|
398
413
|
// Since useDataHookForCashOut is false, the 721 hook should NOT be called.
|
|
399
414
|
// Original values should be returned.
|
|
400
|
-
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply
|
|
415
|
+
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
|
|
401
416
|
assertEq(cashOutTaxRate, 5000, "Should return original tax rate, not 721 hook's 9999");
|
|
402
417
|
assertEq(cashOutCount, 1000, "Should return original cashOutCount, not 721 hook's 1");
|
|
403
418
|
assertEq(totalSupply, 10_000, "Should return original totalSupply, not 721 hook's 1");
|
|
@@ -416,10 +431,12 @@ contract OmnichainDeployerEdgeCases is Test {
|
|
|
416
431
|
JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, attacker);
|
|
417
432
|
|
|
418
433
|
// Since useDataHookForCashOut is true, the custom hook SHOULD be called.
|
|
419
|
-
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply
|
|
434
|
+
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
|
|
420
435
|
assertEq(cashOutTaxRate, 9999, "Should return custom hook's tax rate");
|
|
421
436
|
assertEq(cashOutCount, 1, "Should return custom hook's cashOutCount");
|
|
422
|
-
|
|
437
|
+
// The deployer discards the inner hook's totalSupply and computes cross-chain supply instead.
|
|
438
|
+
// With no suckers, this equals context.totalSupply.
|
|
439
|
+
assertEq(totalSupply, ctx.totalSupply, "Should return cross-chain totalSupply (context value with no suckers)");
|
|
423
440
|
}
|
|
424
441
|
|
|
425
442
|
function test_beforeCashOut_merges721AndCustomHookSpecifications() public {
|
|
@@ -440,7 +457,7 @@ contract OmnichainDeployerEdgeCases is Test {
|
|
|
440
457
|
vm.mockCall(
|
|
441
458
|
mock721,
|
|
442
459
|
abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
|
|
443
|
-
abi.encode(uint256(4000), uint256(700), uint256(9000), specs)
|
|
460
|
+
abi.encode(uint256(4000), uint256(700), uint256(9000), uint256(0), specs)
|
|
444
461
|
);
|
|
445
462
|
|
|
446
463
|
JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, attacker);
|
|
@@ -448,13 +465,15 @@ contract OmnichainDeployerEdgeCases is Test {
|
|
|
448
465
|
(
|
|
449
466
|
uint256 cashOutTaxRate,
|
|
450
467
|
uint256 cashOutCount,
|
|
451
|
-
uint256 totalSupply
|
|
468
|
+
uint256 totalSupply,,
|
|
452
469
|
JBCashOutHookSpecification[] memory hookSpecifications
|
|
453
470
|
) = deployer.beforeCashOutRecordedWith(ctx);
|
|
454
471
|
|
|
455
472
|
assertEq(cashOutTaxRate, 2000, "Custom hook should receive and override 721-adjusted tax rate");
|
|
456
473
|
assertEq(cashOutCount, 500, "Custom hook should receive and override 721-adjusted cashOutCount");
|
|
457
|
-
|
|
474
|
+
// The deployer discards the inner hook's totalSupply and computes cross-chain supply instead.
|
|
475
|
+
// With no suckers, this equals context.totalSupply.
|
|
476
|
+
assertEq(totalSupply, ctx.totalSupply, "Should return cross-chain totalSupply (context value with no suckers)");
|
|
458
477
|
assertEq(hookSpecifications.length, 2, "721 and custom cash out specs should both be returned");
|
|
459
478
|
assertEq(address(hookSpecifications[0].hook), mock721, "721 hook spec should come first");
|
|
460
479
|
assertEq(hookSpecifications[0].amount, 11, "721 hook spec amount should be preserved");
|
|
@@ -476,7 +495,7 @@ contract OmnichainDeployerEdgeCases is Test {
|
|
|
476
495
|
JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, attacker);
|
|
477
496
|
|
|
478
497
|
// Since useDataHookForCashOut is false, the custom hook should NOT be called.
|
|
479
|
-
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply
|
|
498
|
+
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
|
|
480
499
|
assertEq(cashOutTaxRate, 5000, "Should return original tax rate, not custom hook's 2000");
|
|
481
500
|
assertEq(cashOutCount, 1000, "Should return original cashOutCount, not custom hook's 1");
|
|
482
501
|
assertEq(totalSupply, 10_000, "Should return original totalSupply, not custom hook's 1");
|
|
@@ -493,7 +512,7 @@ contract OmnichainDeployerEdgeCases is Test {
|
|
|
493
512
|
JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, rulesetId, attacker);
|
|
494
513
|
|
|
495
514
|
// No 721 hook, no custom hook — should fall through to original values.
|
|
496
|
-
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply
|
|
515
|
+
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
|
|
497
516
|
assertEq(cashOutTaxRate, 5000, "Should return original tax rate");
|
|
498
517
|
assertEq(cashOutCount, 1000, "Should return original cashOutCount");
|
|
499
518
|
assertEq(totalSupply, 10_000, "Should return original totalSupply");
|
|
@@ -614,6 +633,17 @@ contract OmnichainDeployerEdgeCases is Test {
|
|
|
614
633
|
abi.encode(uint256(50)) // A past ruleset ID — no hook stored at this ID
|
|
615
634
|
);
|
|
616
635
|
|
|
636
|
+
// Mock latestQueuedOf to return a ruleset with id=50 (no hook stored for this id via the deployer).
|
|
637
|
+
// The carry-forward logic checks latestQueuedOf first; since no hook was stored at id=50,
|
|
638
|
+
// it falls through to currentOf.
|
|
639
|
+
JBRuleset memory latestQueuedRuleset;
|
|
640
|
+
latestQueuedRuleset.id = uint48(50);
|
|
641
|
+
vm.mockCall(
|
|
642
|
+
address(rulesets),
|
|
643
|
+
abi.encodeWithSelector(IJBRulesets.latestQueuedOf.selector, projectId),
|
|
644
|
+
abi.encode(latestQueuedRuleset, JBApprovalStatus.Empty)
|
|
645
|
+
);
|
|
646
|
+
|
|
617
647
|
// Mock currentOf to return a ruleset with id=50 (no hook stored for this id via the deployer).
|
|
618
648
|
JBRuleset memory currentRuleset;
|
|
619
649
|
currentRuleset.id = uint48(50);
|
package/test/TestAuditGaps.sol
CHANGED
|
@@ -3,6 +3,7 @@ pragma solidity 0.8.28;
|
|
|
3
3
|
|
|
4
4
|
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
|
|
6
|
+
import {JBApprovalStatus} from "@bananapus/core-v6/src/enums/JBApprovalStatus.sol";
|
|
6
7
|
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
7
8
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
8
9
|
import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
|
|
@@ -54,9 +55,11 @@ contract PayRevertingHook is IJBRulesetDataHook {
|
|
|
54
55
|
external
|
|
55
56
|
pure
|
|
56
57
|
override
|
|
57
|
-
returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
58
|
+
returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
58
59
|
{
|
|
59
|
-
return (
|
|
60
|
+
return (
|
|
61
|
+
context.cashOutTaxRate, context.cashOutCount, context.totalSupply, 0, new JBCashOutHookSpecification[](0)
|
|
62
|
+
);
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
function hasMintPermissionFor(uint256, JBRuleset calldata, address) external pure override returns (bool) {
|
|
@@ -85,7 +88,7 @@ contract CashOutRevertingHook is IJBRulesetDataHook {
|
|
|
85
88
|
external
|
|
86
89
|
pure
|
|
87
90
|
override
|
|
88
|
-
returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
91
|
+
returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
89
92
|
{
|
|
90
93
|
revert CustomCashOutError();
|
|
91
94
|
}
|
|
@@ -116,9 +119,11 @@ contract MintPermissionRevertingHook is IJBRulesetDataHook {
|
|
|
116
119
|
external
|
|
117
120
|
pure
|
|
118
121
|
override
|
|
119
|
-
returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
122
|
+
returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
120
123
|
{
|
|
121
|
-
return (
|
|
124
|
+
return (
|
|
125
|
+
context.cashOutTaxRate, context.cashOutCount, context.totalSupply, 0, new JBCashOutHookSpecification[](0)
|
|
126
|
+
);
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
function hasMintPermissionFor(uint256, JBRuleset calldata, address) external pure override returns (bool) {
|
|
@@ -145,10 +150,10 @@ contract ExtremeCashOutHook is IJBRulesetDataHook {
|
|
|
145
150
|
external
|
|
146
151
|
pure
|
|
147
152
|
override
|
|
148
|
-
returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
153
|
+
returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
149
154
|
{
|
|
150
155
|
// Return extreme values: max tax rate, zero count, max supply.
|
|
151
|
-
return (type(uint256).max, 0, type(uint256).max, new JBCashOutHookSpecification[](0));
|
|
156
|
+
return (type(uint256).max, 0, type(uint256).max, 0, new JBCashOutHookSpecification[](0));
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
function hasMintPermissionFor(uint256, JBRuleset calldata, address) external pure override returns (bool) {
|
|
@@ -188,9 +193,11 @@ contract ManySpecsHook is IJBRulesetDataHook {
|
|
|
188
193
|
external
|
|
189
194
|
pure
|
|
190
195
|
override
|
|
191
|
-
returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
196
|
+
returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
192
197
|
{
|
|
193
|
-
return (
|
|
198
|
+
return (
|
|
199
|
+
context.cashOutTaxRate, context.cashOutCount, context.totalSupply, 0, new JBCashOutHookSpecification[](0)
|
|
200
|
+
);
|
|
194
201
|
}
|
|
195
202
|
|
|
196
203
|
function hasMintPermissionFor(uint256, JBRuleset calldata, address) external pure override returns (bool) {
|
|
@@ -218,9 +225,11 @@ contract ZeroWeightHook is IJBRulesetDataHook {
|
|
|
218
225
|
external
|
|
219
226
|
pure
|
|
220
227
|
override
|
|
221
|
-
returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
228
|
+
returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
222
229
|
{
|
|
223
|
-
return (
|
|
230
|
+
return (
|
|
231
|
+
context.cashOutTaxRate, context.cashOutCount, context.totalSupply, 0, new JBCashOutHookSpecification[](0)
|
|
232
|
+
);
|
|
224
233
|
}
|
|
225
234
|
|
|
226
235
|
function hasMintPermissionFor(uint256, JBRuleset calldata, address) external pure override returns (bool) {
|
|
@@ -309,6 +318,18 @@ contract TestAuditGaps is Test {
|
|
|
309
318
|
address(suckerRegistry), abi.encodeWithSelector(IJBSuckerRegistry.isSuckerOf.selector), abi.encode(false)
|
|
310
319
|
);
|
|
311
320
|
|
|
321
|
+
// Default: no remote supply or surplus (non-omnichain project).
|
|
322
|
+
vm.mockCall(
|
|
323
|
+
address(suckerRegistry),
|
|
324
|
+
abi.encodeWithSelector(IJBSuckerRegistry.remoteTotalSupplyOf.selector),
|
|
325
|
+
abi.encode(uint256(0))
|
|
326
|
+
);
|
|
327
|
+
vm.mockCall(
|
|
328
|
+
address(suckerRegistry),
|
|
329
|
+
abi.encodeWithSelector(IJBSuckerRegistry.remoteSurplusOf.selector),
|
|
330
|
+
abi.encode(uint256(0))
|
|
331
|
+
);
|
|
332
|
+
|
|
312
333
|
// Default 721 hook mock: returns context weight and empty specs (0 tiers, no splits).
|
|
313
334
|
// A real 721 hook with no tiers returns contextWeight unchanged.
|
|
314
335
|
vm.mockCall(
|
|
@@ -368,7 +389,7 @@ contract TestAuditGaps is Test {
|
|
|
368
389
|
JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, storedRulesetId, sucker);
|
|
369
390
|
|
|
370
391
|
// Sucker gets early return -- never hits the reverting hook.
|
|
371
|
-
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply
|
|
392
|
+
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
|
|
372
393
|
assertEq(cashOutTaxRate, 0, "Sucker should get 0 tax even with reverting hook");
|
|
373
394
|
assertEq(cashOutCount, ctx.cashOutCount, "Sucker cashOutCount should pass through");
|
|
374
395
|
assertEq(totalSupply, ctx.totalSupply, "Sucker totalSupply should pass through");
|
|
@@ -426,10 +447,12 @@ contract TestAuditGaps is Test {
|
|
|
426
447
|
|
|
427
448
|
JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, storedRulesetId, attacker);
|
|
428
449
|
|
|
429
|
-
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply
|
|
450
|
+
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
|
|
430
451
|
assertEq(cashOutTaxRate, type(uint256).max, "Should pass through max tax rate from hook");
|
|
431
452
|
assertEq(cashOutCount, 0, "Should pass through 0 count from hook");
|
|
432
|
-
|
|
453
|
+
// The deployer discards the inner hook's totalSupply and computes cross-chain supply instead.
|
|
454
|
+
// With no suckers, this equals context.totalSupply.
|
|
455
|
+
assertEq(totalSupply, ctx.totalSupply, "Should return cross-chain totalSupply (context value with no suckers)");
|
|
433
456
|
}
|
|
434
457
|
|
|
435
458
|
// -------------------------------------------------------------------------
|
|
@@ -481,7 +504,7 @@ contract TestAuditGaps is Test {
|
|
|
481
504
|
// Cash out should work since the hook only reverts for pay (useDataHookForCashOut = false).
|
|
482
505
|
JBBeforeCashOutRecordedContext memory ctx = _makeCashOutContext(projectId, storedRulesetId, attacker);
|
|
483
506
|
|
|
484
|
-
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply
|
|
507
|
+
(uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(ctx);
|
|
485
508
|
assertEq(cashOutTaxRate, ctx.cashOutTaxRate, "Cash out should return original tax rate");
|
|
486
509
|
assertEq(cashOutCount, ctx.cashOutCount, "Cash out should return original count");
|
|
487
510
|
assertEq(totalSupply, ctx.totalSupply, "Cash out should return original supply");
|
|
@@ -539,6 +562,7 @@ contract TestAuditGaps is Test {
|
|
|
539
562
|
// latestRulesetIdOf still = BASE_TIME (from launch).
|
|
540
563
|
// Guard: BASE_TIME >= BASE_TIME + 1 -> false -> passes.
|
|
541
564
|
_mockLatestRulesetId(BASE_TIME);
|
|
565
|
+
_mockLatestQueuedRuleset(BASE_TIME, JBApprovalStatus.Empty);
|
|
542
566
|
_mockCurrentRulesetId(BASE_TIME);
|
|
543
567
|
|
|
544
568
|
uint256 expectedQueuedId = BASE_TIME + 1;
|
|
@@ -566,6 +590,7 @@ contract TestAuditGaps is Test {
|
|
|
566
590
|
// Warp forward 1 second for first queue.
|
|
567
591
|
vm.warp(BASE_TIME + 1);
|
|
568
592
|
_mockLatestRulesetId(BASE_TIME);
|
|
593
|
+
_mockLatestQueuedRuleset(BASE_TIME, JBApprovalStatus.Empty);
|
|
569
594
|
_mockCurrentRulesetId(BASE_TIME);
|
|
570
595
|
|
|
571
596
|
uint256 firstQueueTime = BASE_TIME + 1;
|
|
@@ -596,6 +621,7 @@ contract TestAuditGaps is Test {
|
|
|
596
621
|
|
|
597
622
|
// Warp forward 1 more second. Now block.timestamp = BASE_TIME + 2.
|
|
598
623
|
vm.warp(BASE_TIME + 2);
|
|
624
|
+
_mockLatestQueuedRuleset(firstQueueTime, JBApprovalStatus.Empty);
|
|
599
625
|
|
|
600
626
|
uint256 secondQueueTime = BASE_TIME + 2;
|
|
601
627
|
vm.mockCall(
|
|
@@ -674,6 +700,7 @@ contract TestAuditGaps is Test {
|
|
|
674
700
|
// Warp past the latestRulesetId: block.timestamp = BASE_TIME + 3.
|
|
675
701
|
vm.warp(BASE_TIME + 3);
|
|
676
702
|
_mockLatestRulesetId(latestRulesetId);
|
|
703
|
+
_mockLatestQueuedRuleset(BASE_TIME, JBApprovalStatus.Empty);
|
|
677
704
|
_mockCurrentRulesetId(BASE_TIME);
|
|
678
705
|
|
|
679
706
|
uint256 expectedQueuedId = BASE_TIME + 3;
|
|
@@ -702,6 +729,7 @@ contract TestAuditGaps is Test {
|
|
|
702
729
|
// Warp forward.
|
|
703
730
|
vm.warp(BASE_TIME + 100);
|
|
704
731
|
_mockLatestRulesetId(BASE_TIME);
|
|
732
|
+
_mockLatestQueuedRuleset(BASE_TIME, JBApprovalStatus.Empty);
|
|
705
733
|
_mockCurrentRulesetId(BASE_TIME);
|
|
706
734
|
|
|
707
735
|
uint256 expectedQueuedId = BASE_TIME + 100;
|
|
@@ -742,16 +770,8 @@ contract TestAuditGaps is Test {
|
|
|
742
770
|
// Warp forward.
|
|
743
771
|
vm.warp(BASE_TIME + 50);
|
|
744
772
|
_mockLatestRulesetId(BASE_TIME);
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
);
|
|
773
|
+
_mockLatestQueuedRuleset(BASE_TIME, JBApprovalStatus.Empty);
|
|
774
|
+
_mockCurrentRulesetId(BASE_TIME);
|
|
755
775
|
|
|
756
776
|
uint256 expectedQueuedId = BASE_TIME + 50;
|
|
757
777
|
vm.mockCall(
|
|
@@ -834,6 +854,7 @@ contract TestAuditGaps is Test {
|
|
|
834
854
|
|
|
835
855
|
vm.warp(BASE_TIME + 10);
|
|
836
856
|
_mockLatestRulesetId(BASE_TIME);
|
|
857
|
+
_mockLatestQueuedRuleset(BASE_TIME, JBApprovalStatus.Empty);
|
|
837
858
|
_mockCurrentRulesetId(BASE_TIME);
|
|
838
859
|
|
|
839
860
|
uint256 expectedQueuedId = BASE_TIME + 10;
|
|
@@ -883,12 +904,24 @@ contract TestAuditGaps is Test {
|
|
|
883
904
|
|
|
884
905
|
function _mockCurrentRulesetId(uint256 currentRulesetId) internal {
|
|
885
906
|
JBRuleset memory r;
|
|
907
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
886
908
|
r.id = uint48(currentRulesetId);
|
|
887
909
|
vm.mockCall(
|
|
888
910
|
address(rulesetsContract), abi.encodeWithSelector(IJBRulesets.currentOf.selector, projectId), abi.encode(r)
|
|
889
911
|
);
|
|
890
912
|
}
|
|
891
913
|
|
|
914
|
+
function _mockLatestQueuedRuleset(uint256 rulesetId, JBApprovalStatus status) internal {
|
|
915
|
+
JBRuleset memory r;
|
|
916
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
917
|
+
r.id = uint48(rulesetId);
|
|
918
|
+
vm.mockCall(
|
|
919
|
+
address(rulesetsContract),
|
|
920
|
+
abi.encodeWithSelector(IJBRulesets.latestQueuedOf.selector, projectId),
|
|
921
|
+
abi.encode(r, status)
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
|
|
892
925
|
function _makePayContext(uint256 pid, uint256 rid) internal returns (JBBeforePayRecordedContext memory) {
|
|
893
926
|
return JBBeforePayRecordedContext({
|
|
894
927
|
terminal: makeAddr("terminal"),
|
|
@@ -106,6 +106,16 @@ contract Tiered721HookComposition is Test {
|
|
|
106
106
|
vm.mockCall(
|
|
107
107
|
address(suckerRegistry), abi.encodeWithSelector(IJBSuckerRegistry.isSuckerOf.selector), abi.encode(false)
|
|
108
108
|
);
|
|
109
|
+
vm.mockCall(
|
|
110
|
+
address(suckerRegistry),
|
|
111
|
+
abi.encodeWithSelector(IJBSuckerRegistry.remoteTotalSupplyOf.selector),
|
|
112
|
+
abi.encode(uint256(0))
|
|
113
|
+
);
|
|
114
|
+
vm.mockCall(
|
|
115
|
+
address(suckerRegistry),
|
|
116
|
+
abi.encodeWithSelector(IJBSuckerRegistry.remoteSurplusOf.selector),
|
|
117
|
+
abi.encode(uint256(0))
|
|
118
|
+
);
|
|
109
119
|
JBPayHookSpecification[] memory default721Specs = new JBPayHookSpecification[](1);
|
|
110
120
|
default721Specs[0] =
|
|
111
121
|
JBPayHookSpecification({hook: IJBPayHook(hookAddr), noop: false, amount: 0, metadata: bytes("")});
|
|
@@ -330,7 +340,7 @@ contract Tiered721HookComposition is Test {
|
|
|
330
340
|
abi.encode(true)
|
|
331
341
|
);
|
|
332
342
|
JBBeforeCashOutRecordedContext memory context = _makeCashOutContext(projectId, block.timestamp, sucker);
|
|
333
|
-
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply
|
|
343
|
+
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(context);
|
|
334
344
|
assertEq(taxRate, 0, "sucker gets 0 tax");
|
|
335
345
|
assertEq(cashOutCount, context.cashOutCount);
|
|
336
346
|
assertEq(totalSupply, context.totalSupply);
|
|
@@ -349,13 +359,15 @@ contract Tiered721HookComposition is Test {
|
|
|
349
359
|
vm.mockCall(
|
|
350
360
|
buybackHookAddr,
|
|
351
361
|
abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
|
|
352
|
-
abi.encode(uint256(3000), uint256(500), uint256(5000), cashOutSpecs)
|
|
362
|
+
abi.encode(uint256(3000), uint256(500), uint256(5000), uint256(0), cashOutSpecs)
|
|
353
363
|
);
|
|
354
364
|
JBBeforeCashOutRecordedContext memory context = _makeCashOutContext(projectId, block.timestamp, randomAddr);
|
|
355
|
-
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply
|
|
365
|
+
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(context);
|
|
356
366
|
assertEq(taxRate, 3000, "buyback hook's tax rate");
|
|
357
367
|
assertEq(cashOutCount, 500, "buyback hook's cashOutCount");
|
|
358
|
-
|
|
368
|
+
// The deployer discards the inner hook's totalSupply and computes cross-chain supply instead.
|
|
369
|
+
// With no suckers, this equals context.totalSupply.
|
|
370
|
+
assertEq(totalSupply, context.totalSupply, "cross-chain totalSupply (context value with no suckers)");
|
|
359
371
|
}
|
|
360
372
|
|
|
361
373
|
function test_beforeCashOut_zeroTiers_forwardsToUserHook() public {
|
|
@@ -375,13 +387,15 @@ contract Tiered721HookComposition is Test {
|
|
|
375
387
|
vm.mockCall(
|
|
376
388
|
customHookAddr,
|
|
377
389
|
abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
|
|
378
|
-
abi.encode(uint256(2000), uint256(100), uint256(1000), cashOutSpecs)
|
|
390
|
+
abi.encode(uint256(2000), uint256(100), uint256(1000), uint256(0), cashOutSpecs)
|
|
379
391
|
);
|
|
380
392
|
JBBeforeCashOutRecordedContext memory context = _makeCashOutContext(projectId, block.timestamp, randomAddr);
|
|
381
|
-
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply
|
|
393
|
+
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(context);
|
|
382
394
|
assertEq(taxRate, 2000, "user hook's tax rate");
|
|
383
395
|
assertEq(cashOutCount, 100, "user hook's cashOutCount");
|
|
384
|
-
|
|
396
|
+
// The deployer discards the inner hook's totalSupply and computes cross-chain supply instead.
|
|
397
|
+
// With no suckers, this equals context.totalSupply.
|
|
398
|
+
assertEq(totalSupply, context.totalSupply, "cross-chain totalSupply (context value with no suckers)");
|
|
385
399
|
}
|
|
386
400
|
|
|
387
401
|
function test_beforeCashOut_zeroTiers_noUserHook_returnsOriginal() public {
|
|
@@ -398,7 +412,7 @@ contract Tiered721HookComposition is Test {
|
|
|
398
412
|
controller
|
|
399
413
|
);
|
|
400
414
|
JBBeforeCashOutRecordedContext memory context = _makeCashOutContext(projectId, block.timestamp, randomAddr);
|
|
401
|
-
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply
|
|
415
|
+
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(context);
|
|
402
416
|
assertEq(taxRate, context.cashOutTaxRate, "original tax rate");
|
|
403
417
|
assertEq(cashOutCount, context.cashOutCount);
|
|
404
418
|
assertEq(totalSupply, context.totalSupply);
|
|
@@ -30,6 +30,7 @@ import {JBOmnichain721Config} from "../../src/structs/JBOmnichain721Config.sol";
|
|
|
30
30
|
import {JBSuckerDeploymentConfig} from "../../src/structs/JBSuckerDeploymentConfig.sol";
|
|
31
31
|
|
|
32
32
|
contract RejectingApprovalHook is ERC165, IJBRulesetApprovalHook {
|
|
33
|
+
// forge-lint: disable-next-line(mixed-case-function)
|
|
33
34
|
function DURATION() external pure override returns (uint256) {
|
|
34
35
|
return 0;
|
|
35
36
|
}
|
|
@@ -73,10 +74,12 @@ contract SequentialHookDeployer is IJB721TiersHookDeployer {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
contract MockSuckerRegistryCarryForward is IJBSuckerRegistry {
|
|
77
|
+
// forge-lint: disable-next-line(mixed-case-function)
|
|
76
78
|
function DIRECTORY() external pure override returns (IJBDirectory) {
|
|
77
79
|
return IJBDirectory(address(0));
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
// forge-lint: disable-next-line(mixed-case-function)
|
|
80
83
|
function PROJECTS() external pure override returns (IJBProjects) {
|
|
81
84
|
return IJBProjects(address(0));
|
|
82
85
|
}
|
|
@@ -97,6 +100,7 @@ contract MockSuckerRegistryCarryForward is IJBSuckerRegistry {
|
|
|
97
100
|
return new address[](0);
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
// forge-lint: disable-next-line(mixed-case-function)
|
|
100
104
|
function MAX_TO_REMOTE_FEE() external pure override returns (uint256) {
|
|
101
105
|
return 0;
|
|
102
106
|
}
|
|
@@ -122,6 +126,18 @@ contract MockSuckerRegistryCarryForward is IJBSuckerRegistry {
|
|
|
122
126
|
return new address[](0);
|
|
123
127
|
}
|
|
124
128
|
|
|
129
|
+
function remoteTotalSupplyOf(uint256) external pure override returns (uint256) {
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function remoteBalanceOf(uint256, uint256, uint256) external pure override returns (uint256) {
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function remoteSurplusOf(uint256, uint256, uint256) external pure override returns (uint256) {
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
125
141
|
function removeDeprecatedSucker(uint256, address) external override {}
|
|
126
142
|
function removeSuckerDeployer(address) external override {}
|
|
127
143
|
}
|
|
@@ -236,7 +252,8 @@ contract CarryForwardRejectedHookTest is TestBaseWorkflow {
|
|
|
236
252
|
|
|
237
253
|
function _default721Config() internal pure returns (JBOmnichain721Config memory config) {
|
|
238
254
|
config.deployTiersHookConfig.tiersConfig.currency =
|
|
239
|
-
|
|
255
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
256
|
+
uint32(uint160(address(0x000000000000000000000000000000000000EEEe)));
|
|
240
257
|
config.deployTiersHookConfig.tiersConfig.decimals = 18;
|
|
241
258
|
}
|
|
242
259
|
|
|
@@ -255,6 +272,7 @@ contract CarryForwardRejectedHookTest is TestBaseWorkflow {
|
|
|
255
272
|
metadata: JBRulesetMetadata({
|
|
256
273
|
reservedPercent: 0,
|
|
257
274
|
cashOutTaxRate: 0,
|
|
275
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
258
276
|
baseCurrency: uint32(uint160(address(0x000000000000000000000000000000000000EEEe))),
|
|
259
277
|
pausePay: false,
|
|
260
278
|
pauseCreditTransfers: false,
|
|
@@ -290,6 +308,7 @@ contract CarryForwardRejectedHookTest is TestBaseWorkflow {
|
|
|
290
308
|
jbPermissions()
|
|
291
309
|
.setPermissionsFor(
|
|
292
310
|
owner,
|
|
311
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
293
312
|
JBPermissionsData({operator: address(this), projectId: uint64(projectId), permissionIds: permissionIds})
|
|
294
313
|
);
|
|
295
314
|
|
|
@@ -298,7 +317,10 @@ contract CarryForwardRejectedHookTest is TestBaseWorkflow {
|
|
|
298
317
|
.setPermissionsFor(
|
|
299
318
|
owner,
|
|
300
319
|
JBPermissionsData({
|
|
301
|
-
operator: address(deployer),
|
|
320
|
+
operator: address(deployer),
|
|
321
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
322
|
+
projectId: uint64(projectId),
|
|
323
|
+
permissionIds: permissionIds
|
|
302
324
|
})
|
|
303
325
|
);
|
|
304
326
|
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
7
|
+
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
8
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
9
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
10
|
+
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
11
|
+
import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
|
|
12
|
+
import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
|
|
13
|
+
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
14
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
15
|
+
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
16
|
+
import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookProjectDeployer.sol";
|
|
17
|
+
import {IJBOwnable} from "@bananapus/ownable-v6/src/interfaces/IJBOwnable.sol";
|
|
18
|
+
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
19
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
20
|
+
import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
|
|
21
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
22
|
+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
23
|
+
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
24
|
+
|
|
25
|
+
import {JBOmnichainDeployer} from "../../src/JBOmnichainDeployer.sol";
|
|
26
|
+
import {JBOmnichain721Config} from "../../src/structs/JBOmnichain721Config.sol";
|
|
27
|
+
import {JBSuckerDeploymentConfig} from "../../src/structs/JBSuckerDeploymentConfig.sol";
|
|
28
|
+
|
|
29
|
+
contract CodexNemesisAudit is Test {
|
|
30
|
+
IJBPermissions internal permissions = IJBPermissions(makeAddr("permissions"));
|
|
31
|
+
IJBProjects internal projects = IJBProjects(makeAddr("projects"));
|
|
32
|
+
IJB721TiersHookDeployer internal hookDeployer = IJB721TiersHookDeployer(makeAddr("hookDeployer"));
|
|
33
|
+
IJBSuckerRegistry internal mockSuckerRegistry = IJBSuckerRegistry(makeAddr("suckerRegistry"));
|
|
34
|
+
IJBController internal controller = IJBController(makeAddr("controller"));
|
|
35
|
+
IJBDirectory internal directory = IJBDirectory(makeAddr("directory"));
|
|
36
|
+
address internal hookAddr = makeAddr("hook721");
|
|
37
|
+
address internal projectOwner = makeAddr("projectOwner");
|
|
38
|
+
address internal operator = makeAddr("operator");
|
|
39
|
+
|
|
40
|
+
uint256 internal constant PROJECT_ID = 42;
|
|
41
|
+
|
|
42
|
+
function setUp() public {
|
|
43
|
+
vm.mockCall(
|
|
44
|
+
address(permissions), abi.encodeWithSelector(IJBPermissions.setPermissionsFor.selector), abi.encode()
|
|
45
|
+
);
|
|
46
|
+
vm.mockCall(
|
|
47
|
+
address(permissions), abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true)
|
|
48
|
+
);
|
|
49
|
+
vm.mockCall(
|
|
50
|
+
address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, PROJECT_ID), abi.encode(projectOwner)
|
|
51
|
+
);
|
|
52
|
+
vm.mockCall(
|
|
53
|
+
address(hookDeployer),
|
|
54
|
+
abi.encodeWithSelector(IJB721TiersHookDeployer.deployHookFor.selector),
|
|
55
|
+
abi.encode(IJB721TiersHook(hookAddr))
|
|
56
|
+
);
|
|
57
|
+
vm.mockCall(hookAddr, abi.encodeWithSelector(IJBOwnable.transferOwnershipToProject.selector), abi.encode());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function test_poc_launchRulesetsFor_revertsWhenProjectHasNoControllerYet() public {
|
|
61
|
+
JBOmnichainDeployer deployer =
|
|
62
|
+
new JBOmnichainDeployer(mockSuckerRegistry, hookDeployer, permissions, projects, address(0));
|
|
63
|
+
|
|
64
|
+
vm.mockCall(
|
|
65
|
+
address(controller), abi.encodeWithSelector(IJBController.DIRECTORY.selector), abi.encode(directory)
|
|
66
|
+
);
|
|
67
|
+
// A freshly created project with no controller yet is valid for core launchRulesetsFor,
|
|
68
|
+
// but the deployer rejects it before the controller can set itself as first controller.
|
|
69
|
+
vm.mockCall(
|
|
70
|
+
address(directory),
|
|
71
|
+
abi.encodeWithSelector(IJBDirectory.controllerOf.selector, PROJECT_ID),
|
|
72
|
+
abi.encode(IERC165(address(0)))
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
JBRulesetConfig[] memory configs = new JBRulesetConfig[](1);
|
|
76
|
+
configs[0] = _makeRulesetConfig();
|
|
77
|
+
|
|
78
|
+
vm.prank(projectOwner);
|
|
79
|
+
vm.expectRevert(JBOmnichainDeployer.JBOmnichainDeployer_ControllerMismatch.selector);
|
|
80
|
+
deployer.launchRulesetsFor(PROJECT_ID, configs, new JBTerminalConfig[](0), "memo", controller);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function test_poc_deploySuckersFor_requiresHiddenPermissionForDeployerItself() public {
|
|
84
|
+
vm.mockCall(address(directory), abi.encodeWithSelector(IJBDirectory.PROJECTS.selector), abi.encode(projects));
|
|
85
|
+
|
|
86
|
+
JBSuckerRegistry registry = new JBSuckerRegistry(directory, permissions, address(this), address(0));
|
|
87
|
+
JBOmnichainDeployer deployer = new JBOmnichainDeployer(
|
|
88
|
+
IJBSuckerRegistry(address(registry)), hookDeployer, permissions, projects, address(0)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
vm.mockCall(
|
|
92
|
+
address(permissions),
|
|
93
|
+
abi.encodeWithSelector(
|
|
94
|
+
IJBPermissions.hasPermission.selector,
|
|
95
|
+
operator,
|
|
96
|
+
projectOwner,
|
|
97
|
+
PROJECT_ID,
|
|
98
|
+
JBPermissionIds.DEPLOY_SUCKERS,
|
|
99
|
+
true,
|
|
100
|
+
true
|
|
101
|
+
),
|
|
102
|
+
abi.encode(true)
|
|
103
|
+
);
|
|
104
|
+
vm.mockCall(
|
|
105
|
+
address(permissions),
|
|
106
|
+
abi.encodeWithSelector(
|
|
107
|
+
IJBPermissions.hasPermission.selector,
|
|
108
|
+
address(deployer),
|
|
109
|
+
projectOwner,
|
|
110
|
+
PROJECT_ID,
|
|
111
|
+
JBPermissionIds.DEPLOY_SUCKERS,
|
|
112
|
+
true,
|
|
113
|
+
true
|
|
114
|
+
),
|
|
115
|
+
abi.encode(false)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
JBSuckerDeploymentConfig memory config = JBSuckerDeploymentConfig({
|
|
119
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32("SUCKER_SALT")
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
vm.prank(operator);
|
|
123
|
+
vm.expectRevert(
|
|
124
|
+
abi.encodeWithSelector(
|
|
125
|
+
JBPermissioned.JBPermissioned_Unauthorized.selector,
|
|
126
|
+
projectOwner,
|
|
127
|
+
address(deployer),
|
|
128
|
+
PROJECT_ID,
|
|
129
|
+
JBPermissionIds.DEPLOY_SUCKERS
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
deployer.deploySuckersFor(PROJECT_ID, config);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function _makeRulesetConfig() internal pure returns (JBRulesetConfig memory config) {
|
|
136
|
+
config.metadata = JBRulesetMetadata({
|
|
137
|
+
reservedPercent: 0,
|
|
138
|
+
cashOutTaxRate: 0,
|
|
139
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
140
|
+
pausePay: false,
|
|
141
|
+
pauseCreditTransfers: false,
|
|
142
|
+
allowOwnerMinting: false,
|
|
143
|
+
allowSetCustomToken: false,
|
|
144
|
+
allowTerminalMigration: false,
|
|
145
|
+
allowSetTerminals: false,
|
|
146
|
+
allowSetController: false,
|
|
147
|
+
allowAddAccountingContext: false,
|
|
148
|
+
allowAddPriceFeed: false,
|
|
149
|
+
ownerMustSendPayouts: false,
|
|
150
|
+
holdFees: false,
|
|
151
|
+
useTotalSurplusForCashOuts: false,
|
|
152
|
+
useDataHookForPay: false,
|
|
153
|
+
useDataHookForCashOut: false,
|
|
154
|
+
dataHook: address(0),
|
|
155
|
+
metadata: 0
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -3,6 +3,7 @@ pragma solidity 0.8.28;
|
|
|
3
3
|
|
|
4
4
|
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
|
|
6
|
+
import {JBApprovalStatus} from "@bananapus/core-v6/src/enums/JBApprovalStatus.sol";
|
|
6
7
|
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
7
8
|
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
8
9
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
@@ -144,6 +145,14 @@ contract JBOmnichainDeployerTest is Test {
|
|
|
144
145
|
abi.encode()
|
|
145
146
|
);
|
|
146
147
|
vm.mockCall(suckerRegistry, abi.encodeWithSelector(IJBSuckerRegistry.isSuckerOf.selector), abi.encode(false));
|
|
148
|
+
vm.mockCall(
|
|
149
|
+
suckerRegistry,
|
|
150
|
+
abi.encodeWithSelector(IJBSuckerRegistry.remoteTotalSupplyOf.selector),
|
|
151
|
+
abi.encode(uint256(0))
|
|
152
|
+
);
|
|
153
|
+
vm.mockCall(
|
|
154
|
+
suckerRegistry, abi.encodeWithSelector(IJBSuckerRegistry.remoteSurplusOf.selector), abi.encode(uint256(0))
|
|
155
|
+
);
|
|
147
156
|
|
|
148
157
|
JBOmnichainDeployer deployer =
|
|
149
158
|
new JBOmnichainDeployer(IJBSuckerRegistry(suckerRegistry), hookDeployer, permissions, projects, address(0));
|
|
@@ -152,7 +161,7 @@ contract JBOmnichainDeployerTest is Test {
|
|
|
152
161
|
vm.mockCall(
|
|
153
162
|
hookAddr,
|
|
154
163
|
abi.encodeWithSelector(IJBRulesetDataHook.beforeCashOutRecordedWith.selector),
|
|
155
|
-
abi.encode(uint256(1234), uint256(55), uint256(999), emptySpecs)
|
|
164
|
+
abi.encode(uint256(1234), uint256(55), uint256(999), uint256(0), emptySpecs)
|
|
156
165
|
);
|
|
157
166
|
|
|
158
167
|
JBRulesetConfig[] memory launchConfigs = new JBRulesetConfig[](1);
|
|
@@ -176,11 +185,13 @@ contract JBOmnichainDeployerTest is Test {
|
|
|
176
185
|
assertTrue(initialUseForCashOut, "launch should store the initial cash-out flag");
|
|
177
186
|
|
|
178
187
|
JBBeforeCashOutRecordedContext memory initialContext = _cashOutContext(initialRulesetId);
|
|
179
|
-
(uint256 initialTaxRate, uint256 initialCashOutCount, uint256 initialTotalSupply
|
|
188
|
+
(uint256 initialTaxRate, uint256 initialCashOutCount, uint256 initialTotalSupply,,) =
|
|
180
189
|
deployer.beforeCashOutRecordedWith(initialContext);
|
|
181
190
|
assertEq(initialTaxRate, 1234, "initial ruleset should forward cash-outs into the 721 hook");
|
|
182
191
|
assertEq(initialCashOutCount, 55, "initial ruleset should use the 721 hook cash-out count");
|
|
183
|
-
|
|
192
|
+
// The deployer discards the inner hook's totalSupply and computes cross-chain supply instead.
|
|
193
|
+
// With no suckers, this equals context.totalSupply (777).
|
|
194
|
+
assertEq(initialTotalSupply, 777, "initial ruleset should use cross-chain totalSupply (context value)");
|
|
184
195
|
|
|
185
196
|
vm.mockCall(
|
|
186
197
|
address(controller), abi.encodeWithSelector(IJBController.DIRECTORY.selector), abi.encode(directory)
|
|
@@ -197,8 +208,20 @@ contract JBOmnichainDeployerTest is Test {
|
|
|
197
208
|
abi.encode(initialRulesetId)
|
|
198
209
|
);
|
|
199
210
|
|
|
200
|
-
// Mock
|
|
211
|
+
// Mock latestQueuedOf to return the initial ruleset with Empty approval status,
|
|
212
|
+
// so the carry-forward logic finds the 721 hook from the latest queued ruleset.
|
|
213
|
+
JBRuleset memory latestQueuedRuleset;
|
|
214
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
215
|
+
latestQueuedRuleset.id = uint48(initialRulesetId);
|
|
216
|
+
vm.mockCall(
|
|
217
|
+
address(rulesets),
|
|
218
|
+
abi.encodeWithSelector(IJBRulesets.latestQueuedOf.selector, PROJECT_ID),
|
|
219
|
+
abi.encode(latestQueuedRuleset, JBApprovalStatus.Empty)
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// Mock currentOf as a fallback (not reached in this scenario since latestQueuedOf succeeds).
|
|
201
223
|
JBRuleset memory currentRuleset;
|
|
224
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
202
225
|
currentRuleset.id = uint48(initialRulesetId);
|
|
203
226
|
vm.mockCall(
|
|
204
227
|
address(rulesets),
|
|
@@ -226,13 +249,15 @@ contract JBOmnichainDeployerTest is Test {
|
|
|
226
249
|
assertTrue(queuedUseForCashOut, "queue should preserve the existing 721 cash-out flag");
|
|
227
250
|
|
|
228
251
|
JBBeforeCashOutRecordedContext memory queuedContext = _cashOutContext(queuedRulesetId);
|
|
229
|
-
(uint256 queuedTaxRate, uint256 queuedCashOutCount, uint256 queuedTotalSupply
|
|
252
|
+
(uint256 queuedTaxRate, uint256 queuedCashOutCount, uint256 queuedTotalSupply,,) =
|
|
230
253
|
deployer.beforeCashOutRecordedWith(queuedContext);
|
|
231
254
|
|
|
232
255
|
// The 721 hook is properly consulted for cash-outs in the queued ruleset.
|
|
233
256
|
assertEq(queuedTaxRate, 1234, "queued ruleset should forward cash-outs into the 721 hook");
|
|
234
257
|
assertEq(queuedCashOutCount, 55, "queued ruleset should use the 721 hook cash-out count");
|
|
235
|
-
|
|
258
|
+
// The deployer discards the inner hook's totalSupply and computes cross-chain supply instead.
|
|
259
|
+
// With no suckers, this equals context.totalSupply (777).
|
|
260
|
+
assertEq(queuedTotalSupply, 777, "queued ruleset should use cross-chain totalSupply (context value)");
|
|
236
261
|
}
|
|
237
262
|
|
|
238
263
|
function _cashOutContext(uint256 rulesetId) internal pure returns (JBBeforeCashOutRecordedContext memory context) {
|
|
@@ -70,6 +70,18 @@ contract WeightScalingComparisonTest is Test {
|
|
|
70
70
|
address(suckerRegistry), abi.encodeWithSelector(IJBSuckerRegistry.isSuckerOf.selector), abi.encode(false)
|
|
71
71
|
);
|
|
72
72
|
|
|
73
|
+
// Default: no remote supply or surplus (non-omnichain project).
|
|
74
|
+
vm.mockCall(
|
|
75
|
+
address(suckerRegistry),
|
|
76
|
+
abi.encodeWithSelector(IJBSuckerRegistry.remoteTotalSupplyOf.selector),
|
|
77
|
+
abi.encode(uint256(0))
|
|
78
|
+
);
|
|
79
|
+
vm.mockCall(
|
|
80
|
+
address(suckerRegistry),
|
|
81
|
+
abi.encodeWithSelector(IJBSuckerRegistry.remoteSurplusOf.selector),
|
|
82
|
+
abi.encode(uint256(0))
|
|
83
|
+
);
|
|
84
|
+
|
|
73
85
|
// Mock hook deployer to return our mock hook address.
|
|
74
86
|
vm.mockCall(
|
|
75
87
|
address(hookDeployer),
|
|
@@ -27,6 +27,7 @@ import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
|
27
27
|
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
28
28
|
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
29
29
|
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
30
|
+
import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
|
|
30
31
|
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
31
32
|
import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookProjectDeployer.sol";
|
|
32
33
|
import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
|
|
@@ -181,7 +182,14 @@ abstract contract OmnichainForkTestBase is TestBaseWorkflow {
|
|
|
181
182
|
suckerRegistry = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
182
183
|
hookStore = new JB721TiersHookStore();
|
|
183
184
|
exampleHook = new JB721TiersHook(
|
|
184
|
-
jbDirectory(),
|
|
185
|
+
jbDirectory(),
|
|
186
|
+
jbPermissions(),
|
|
187
|
+
jbPrices(),
|
|
188
|
+
jbRulesets(),
|
|
189
|
+
hookStore,
|
|
190
|
+
jbSplits(),
|
|
191
|
+
new JB721CheckpointsDeployer(),
|
|
192
|
+
address(0)
|
|
185
193
|
);
|
|
186
194
|
addressRegistry = new JBAddressRegistry();
|
|
187
195
|
hookDeployer721 = new JB721TiersHookDeployer(exampleHook, hookStore, addressRegistry, multisig());
|
|
@@ -179,7 +179,7 @@ contract TestSuckerDeploymentFork is OmnichainForkTestBase {
|
|
|
179
179
|
});
|
|
180
180
|
|
|
181
181
|
// Call beforeCashOutRecordedWith directly on the omnichain deployer.
|
|
182
|
-
(uint256 returnedTaxRate, uint256 returnedCashOutCount, uint256 returnedTotalSupply
|
|
182
|
+
(uint256 returnedTaxRate, uint256 returnedCashOutCount, uint256 returnedTotalSupply,,) =
|
|
183
183
|
omnichainDeployer.beforeCashOutRecordedWith(context);
|
|
184
184
|
|
|
185
185
|
// Sucker should get 0% tax.
|