@bananapus/router-terminal-v6 0.0.9 → 0.0.11

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/STYLE_GUIDE.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  How we write Solidity and organize repos across the Juicebox V6 ecosystem. `nana-core-v6` is the gold standard — when in doubt, match what it does.
4
4
 
5
- **This repo's deviations:** `evm_version = 'cancun'`, `via_ir = true` in ci_sizes profile.
6
-
7
5
  ## File Organization
8
6
 
9
7
  ```
@@ -19,8 +17,6 @@ src/
19
17
 
20
18
  One contract/interface/struct/enum per file. Name the file after the type it contains.
21
19
 
22
- **Structs, enums, libraries, and interfaces always go in their subdirectories** (`src/structs/`, `src/enums/`, `src/libraries/`, `src/interfaces/`) — never inline in contract files or placed in `src/` root. This keeps type definitions discoverable and import paths consistent across repos.
23
-
24
20
  ## Pragma Versions
25
21
 
26
22
  ```solidity
@@ -108,14 +104,6 @@ contract JBExample is JBPermissioned, IJBExample {
108
104
  // -------------------------- constructor ---------------------------- //
109
105
  //*********************************************************************//
110
106
 
111
- //*********************************************************************//
112
- // ---------------------- receive / fallback ------------------------- //
113
- //*********************************************************************//
114
-
115
- //*********************************************************************//
116
- // --------------------------- modifiers ----------------------------- //
117
- //*********************************************************************//
118
-
119
107
  //*********************************************************************//
120
108
  // ---------------------- external transactions ---------------------- //
121
109
  //*********************************************************************//
@@ -143,28 +131,23 @@ contract JBExample is JBPermissioned, IJBExample {
143
131
  ```
144
132
 
145
133
  **Section order:**
146
- 1. `using` declarations
147
- 2. Custom errors
148
- 3. Public constants
149
- 4. Internal constants
150
- 5. Public immutable stored properties
151
- 6. Internal immutable stored properties
152
- 7. Public stored properties
153
- 8. Internal stored properties
154
- 9. Constructor
155
- 10. `receive` / `fallback`
156
- 11. Modifiers
157
- 12. External transactions
158
- 13. External views
159
- 14. Public transactions
160
- 15. Internal helpers
161
- 16. Internal views
162
- 17. Private helpers
134
+ 1. Custom errors
135
+ 2. Public constants
136
+ 3. Internal constants
137
+ 4. Public immutable stored properties
138
+ 5. Internal immutable stored properties
139
+ 6. Public stored properties
140
+ 7. Internal stored properties
141
+ 8. Constructor
142
+ 9. External transactions
143
+ 10. External views
144
+ 11. Public transactions
145
+ 12. Internal helpers
146
+ 13. Internal views
147
+ 14. Private helpers
163
148
 
164
149
  Functions are alphabetized within each section.
165
150
 
166
- **Events:** Events are declared in interfaces only, never in implementation contracts. Implementations inherit events from their interface and emit them unqualified. This keeps the ABI definition in one place and allows tests to use interface-qualified event expectations (e.g., `emit IJBController.LaunchProject(...)`).
167
-
168
151
  ## Interface Structure
169
152
 
170
153
  ```solidity
@@ -214,7 +197,7 @@ interface IJBExample is IJBBase {
214
197
  | Public/external function | `camelCase` | `cashOutTokensOf` |
215
198
  | Internal/private function | `_camelCase` | `_processFee` |
216
199
  | Internal storage | `_camelCase` | `_accountingContextForTokenOf` |
217
- | Function parameter | `camelCase` | `projectId`, `cashOutCount` |
200
+ | Function parameter | `camelCase` (no underscores) | `projectId`, `cashOutCount` |
218
201
 
219
202
  ## NatSpec
220
203
 
@@ -270,9 +253,12 @@ uint256 public constant MAX_RESERVED_PERCENT = 10_000;
270
253
 
271
254
  ## Function Calls
272
255
 
273
- Use named parameters for readability when calling functions with 3+ arguments:
256
+ Use named arguments for all function calls with 2 or more arguments — in both `src/` and `script/`:
274
257
 
275
258
  ```solidity
259
+ // Good — named arguments
260
+ token.mint({account: beneficiary, amount: count});
261
+ _transferOwnership({newOwner: address(0), projectId: 0});
276
262
  PERMISSIONS.hasPermission({
277
263
  operator: sender,
278
264
  account: account,
@@ -281,8 +267,18 @@ PERMISSIONS.hasPermission({
281
267
  includeRoot: true,
282
268
  includeWildcardProjectId: true
283
269
  });
270
+
271
+ // Bad — positional arguments with 2+ args
272
+ token.mint(beneficiary, count);
273
+ _transferOwnership(address(0), 0);
284
274
  ```
285
275
 
276
+ Single-argument calls use positional style: `_burn(amount)`.
277
+
278
+ This also applies to constructor calls, struct literals, and inherited/library calls (e.g., OZ `_mint`, `_safeMint`, `safeTransfer`, `allowance`, `Clones.cloneDeterministic`).
279
+
280
+ Named argument keys must use **camelCase** — never underscores. If a function's parameter names use underscores, rename them to camelCase first.
281
+
286
282
  ## Multiline Signatures
287
283
 
288
284
  ```solidity
@@ -336,10 +332,6 @@ optimizer_runs = 200
336
332
  libs = ["node_modules", "lib"]
337
333
  fs_permissions = [{ access = "read-write", path = "./"}]
338
334
 
339
- [profile.ci_sizes]
340
- via_ir = true
341
- optimizer_runs = 200
342
-
343
335
  [fuzz]
344
336
  runs = 4096
345
337
 
@@ -354,10 +346,14 @@ multiline_func_header = "all"
354
346
  wrap_comments = true
355
347
  ```
356
348
 
357
- **Variations:**
358
- - `evm_version = 'cancun'` for repos using transient storage (buyback-hook, router-terminal, univ4-router)
359
- - `via_ir = true` for repos hitting stack-too-deep (buyback-hook, banny-retail, univ4-lp-split-hook, deploy-all)
360
- - `optimizer = false` only for deploy-all-v6 (stack-too-deep with optimization)
349
+ **Optional sections (add only when needed):**
350
+ - `[rpc_endpoints]` repos with fork tests. Maps named endpoints to env vars (e.g. `ethereum = "${RPC_ETHEREUM_MAINNET}"`).
351
+ - `[profile.ci_sizes]` only when CI needs different optimizer settings than defaults for the size check step (e.g. `optimizer_runs = 200` when the default profile uses a lower value).
352
+
353
+ **Common variations:**
354
+ - `via_ir = true` when hitting stack-too-deep
355
+ - `optimizer = false` when optimization causes stack-too-deep
356
+ - `optimizer_runs` reduced when deep struct nesting causes stack-too-deep at 200 runs
361
357
 
362
358
  ### CI Workflows
363
359
 
@@ -387,8 +383,10 @@ jobs:
387
383
  uses: foundry-rs/foundry-toolchain@v1
388
384
  - name: Run tests
389
385
  run: forge test --fail-fast --summary --detailed --skip "*/script/**"
386
+ env:
387
+ RPC_ETHEREUM_MAINNET: ${{ secrets.RPC_ETHEREUM_MAINNET }}
390
388
  - name: Check contract sizes
391
- run: FOUNDRY_PROFILE=ci_sizes forge build --sizes --skip "*/test/**" --skip "*/script/**" --skip SphinxUtils
389
+ run: forge build --sizes --skip "*/test/**" --skip "*/script/**" --skip SphinxUtils
392
390
  ```
393
391
 
394
392
  **lint.yml:**
@@ -410,11 +408,60 @@ jobs:
410
408
  run: forge fmt --check
411
409
  ```
412
410
 
411
+ **slither.yml** (repos with `src/` contracts only):
412
+ ```yaml
413
+ name: slither
414
+ on:
415
+ pull_request:
416
+ branches:
417
+ - main
418
+ push:
419
+ branches:
420
+ - main
421
+ jobs:
422
+ analyze:
423
+ runs-on: ubuntu-latest
424
+ steps:
425
+ - uses: actions/checkout@v4
426
+ with:
427
+ submodules: recursive
428
+ - uses: actions/setup-node@v4
429
+ with:
430
+ node-version: latest
431
+ - name: Install npm dependencies
432
+ run: npm install --omit=dev
433
+ - name: Install Foundry
434
+ uses: foundry-rs/foundry-toolchain@v1
435
+ - name: Run slither
436
+ uses: crytic/slither-action@v0.3.1
437
+ with:
438
+ slither-config: slither-ci.config.json
439
+ fail-on: medium
440
+ ```
441
+
442
+ **slither-ci.config.json:**
443
+ ```json
444
+ {
445
+ "detectors_to_exclude": "timestamp,uninitialized-local,naming-convention,solc-version,shadowing-local",
446
+ "exclude_informational": true,
447
+ "exclude_low": false,
448
+ "exclude_medium": false,
449
+ "exclude_high": false,
450
+ "disable_color": false,
451
+ "filter_paths": "(mocks/|test/|node_modules/|lib/)",
452
+ "legacy_ast": false
453
+ }
454
+ ```
455
+
456
+ **Variations:**
457
+ - Deployer-only repos (no `src/`, only `script/`) skip slither entirely — the action's internal `forge build` skips `test/` and `script/` by default, leaving nothing to compile.
458
+ - Use inline `// slither-disable-next-line <detector>` to suppress known false positives rather than adding to `detectors_to_exclude` in the config. The comment must be on the line immediately before the flagged expression.
459
+
413
460
  ### package.json
414
461
 
415
462
  ```json
416
463
  {
417
- "name": "@bananapus/router-terminal-v6",
464
+ "name": "@bananapus/package-name-v6",
418
465
  "version": "x.x.x",
419
466
  "license": "MIT",
420
467
  "repository": { "type": "git", "url": "git+https://github.com/Org/repo.git" },
@@ -434,13 +481,62 @@ jobs:
434
481
 
435
482
  ### remappings.txt
436
483
 
437
- Every repo has a `remappings.txt`. Minimal content:
484
+ Every repo has a `remappings.txt` as the **single source of truth** for import remappings. Never add remappings to `foundry.toml`.
485
+
486
+ **Principle:** Import paths in Solidity source must match npm package names exactly. With `libs = ["node_modules", "lib"]`, Foundry auto-resolves `@scope/package/path/File.sol` → `node_modules/@scope/package/path/File.sol`. No remapping needed for packages installed as real directories.
487
+
488
+ **Note:** Auto-resolution does **not** work for symlinked packages (e.g. npm workspace links). Workspace repos like `deploy-all-v6` and `nana-cli-v6` need explicit `@scope/package/=node_modules/@scope/package/` remappings for each symlinked dependency.
489
+
490
+ **Minimal content** (most repos):
438
491
 
439
492
  ```
440
- @sphinx-labs/contracts/=lib/sphinx/packages/contracts/contracts/foundry
493
+ forge-std/=lib/forge-std/src/
441
494
  ```
442
495
 
443
- Additional mappings as needed for repo-specific dependencies.
496
+ Only add extra remappings for:
497
+ - **`forge-std`** — always needed (git submodule with `src/` subdirectory)
498
+ - **Repo-specific `lib/` submodules** that have no npm package (e.g., `hookmate/=lib/hookmate/src/`)
499
+ - **Symlinked npm packages** — need explicit `@scope/package/=node_modules/@scope/package/` entries
500
+ - **Nested transitive deps** — e.g., `@chainlink/contracts-ccip/` nested inside `@bananapus/suckers-v6/node_modules/`
501
+
502
+ **Never add remappings for:**
503
+ - npm packages that match their import path and are installed as real directories — they auto-resolve
504
+ - Short-form aliases (e.g., `@bananapus/core/` → `@bananapus/core-v6/src/`) — fix the import instead
505
+ - Packages available via npm that are also git submodules — remove the submodule, use npm
506
+
507
+ **Import path convention:**
508
+
509
+ | Package | Import path | Resolves to |
510
+ |---------|------------|-------------|
511
+ | `@bananapus/core-v6` | `@bananapus/core-v6/src/libraries/JBConstants.sol` | `node_modules/@bananapus/core-v6/src/...` |
512
+ | `@openzeppelin/contracts` | `@openzeppelin/contracts/token/ERC20/IERC20.sol` | `node_modules/@openzeppelin/contracts/...` |
513
+ | `@uniswap/v4-core` | `@uniswap/v4-core/src/interfaces/IPoolManager.sol` | `node_modules/@uniswap/v4-core/src/...` |
514
+
515
+ ### Linting
516
+
517
+ Solar (Foundry's built-in linter) runs automatically during `forge build`. It scans all `.sol` files in `libs` directories, including `node_modules`.
518
+
519
+ **All test helpers must use relative imports** (e.g. `../../src/structs/JBRuleset.sol`), not bare `src/` imports. This ensures solar can resolve paths when the helper is consumed via npm in downstream repos.
520
+
521
+ ### Fork Tests
522
+
523
+ Fork tests use named RPC endpoints defined in `[rpc_endpoints]` of `foundry.toml`. No skip guards — fork tests should hard-fail if the RPC endpoint is unavailable, making CI failures explicit.
524
+
525
+ ```solidity
526
+ function setUp() public {
527
+ vm.createSelectFork("ethereum");
528
+ // ... setup code
529
+ }
530
+ ```
531
+
532
+ The endpoint name (e.g. `"ethereum"`) maps to an env var via `foundry.toml`:
533
+
534
+ ```toml
535
+ [rpc_endpoints]
536
+ ethereum = "${RPC_ETHEREUM_MAINNET}"
537
+ ```
538
+
539
+ For multi-chain fork tests, add all needed endpoints.
444
540
 
445
541
  ### Formatting
446
542
 
@@ -468,4 +564,8 @@ CI checks formatting via `forge fmt --check`.
468
564
 
469
565
  ### Contract Size Checks
470
566
 
471
- CI runs `FOUNDRY_PROFILE=ci_sizes forge build --sizes` to catch contracts approaching the 24KB limit. The `ci_sizes` profile uses `optimizer_runs = 200` for realistic size measurement even when the default profile has different optimizer settings.
567
+ CI runs `forge build --sizes` to catch contracts approaching the 24KB limit. When the repo's default `optimizer_runs` differs from what you want for size checking, use `FOUNDRY_PROFILE=ci_sizes forge build --sizes` with a `[profile.ci_sizes]` section in `foundry.toml`.
568
+
569
+ ## Repo-Specific Deviations
570
+
571
+ None. This repo follows the standard configuration exactly.
package/foundry.toml CHANGED
@@ -5,10 +5,6 @@ optimizer_runs = 200
5
5
  libs = ["node_modules", "lib"]
6
6
  fs_permissions = [{ access = "read-write", path = "./"}]
7
7
 
8
- [profile.ci_sizes]
9
- via_ir = true
10
- optimizer_runs = 200
11
-
12
8
  [fuzz]
13
9
  runs = 4096
14
10
 
@@ -21,3 +17,6 @@ fail_on_revert = false
21
17
  number_underscore = "thousands"
22
18
  multiline_func_header = "all"
23
19
  wrap_comments = true
20
+
21
+ [rpc_endpoints]
22
+ ethereum = "${RPC_ETHEREUM_MAINNET}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/router-terminal-v6",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,8 +17,9 @@
17
17
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-router-terminal-v6'"
18
18
  },
19
19
  "dependencies": {
20
- "@bananapus/core-v6": "^0.0.11",
21
- "@openzeppelin/contracts": "^5.2.0",
20
+ "@bananapus/core-v6": "^0.0.16",
21
+ "@bananapus/permission-ids-v6": "^0.0.9",
22
+ "@openzeppelin/contracts": "^5.6.1",
22
23
  "@uniswap/permit2": "github:Uniswap/permit2",
23
24
  "@uniswap/v3-core": "github:Uniswap/v3-core#0.8",
24
25
  "@uniswap/v3-periphery": "github:Uniswap/v3-periphery#0.8",
@@ -1,9 +1,9 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
- import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
4
+ import {CoreDeployment, CoreDeploymentLib} from "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
5
5
 
6
- import {Sphinx} from "@sphinx-labs/contracts/SphinxPlugin.sol";
6
+ import {Sphinx} from "@sphinx-labs/contracts/contracts/foundry/SphinxPlugin.sol";
7
7
  import {Script} from "forge-std/Script.sol";
8
8
 
9
9
  import {JBRouterTerminal} from "../src/JBRouterTerminal.sol";
@@ -18,8 +18,8 @@ contract DeployScript is Script, Sphinx {
18
18
  CoreDeployment core;
19
19
 
20
20
  /// @notice the salts that are used to deploy the contracts.
21
- bytes32 ROUTER_TERMINAL = "JBRouterTerminalV6";
22
- bytes32 ROUTER_TERMINAL_REGISTRY = "JBRouterTerminalRegistryV6";
21
+ bytes32 constant ROUTER_TERMINAL = "JBRouterTerminalV6";
22
+ bytes32 constant ROUTER_TERMINAL_REGISTRY = "JBRouterTerminalRegistryV6";
23
23
 
24
24
  /// @notice tracks the addresses that are required for the chain we are deploying to.
25
25
  address weth;
@@ -38,7 +38,9 @@ contract DeployScript is Script, Sphinx {
38
38
  // Get the deployment addresses for the nana CORE for this chain.
39
39
  // We want to do this outside of the `sphinx` modifier.
40
40
  core = CoreDeploymentLib.getDeployment(
41
- vm.envOr("NANA_CORE_DEPLOYMENT_PATH", string("node_modules/@bananapus/core-v6/deployments/"))
41
+ vm.envOr({
42
+ name: "NANA_CORE_DEPLOYMENT_PATH", defaultValue: string("node_modules/@bananapus/core-v6/deployments/")
43
+ })
42
44
  );
43
45
 
44
46
  trustedForwarder = core.permissions.trustedForwarder();
@@ -95,22 +97,26 @@ contract DeployScript is Script, Sphinx {
95
97
  }
96
98
 
97
99
  function deploy() public sphinx {
98
- JBRouterTerminalRegistry registry = new JBRouterTerminalRegistry{salt: ROUTER_TERMINAL_REGISTRY}(
99
- core.permissions, core.projects, IPermit2(permit2), safeAddress(), trustedForwarder
100
- );
100
+ JBRouterTerminalRegistry registry = new JBRouterTerminalRegistry{salt: ROUTER_TERMINAL_REGISTRY}({
101
+ permissions: core.permissions,
102
+ projects: core.projects,
103
+ permit2: IPermit2(permit2),
104
+ owner: safeAddress(),
105
+ trustedForwarder: trustedForwarder
106
+ });
101
107
 
102
- JBRouterTerminal terminal = new JBRouterTerminal{salt: ROUTER_TERMINAL}(
103
- core.directory,
104
- core.permissions,
105
- core.projects,
106
- core.tokens,
107
- IPermit2(permit2),
108
- safeAddress(),
109
- IWETH9(weth),
110
- IUniswapV3Factory(factory),
111
- IPoolManager(poolManager),
112
- trustedForwarder
113
- );
108
+ JBRouterTerminal terminal = new JBRouterTerminal{salt: ROUTER_TERMINAL}({
109
+ directory: core.directory,
110
+ permissions: core.permissions,
111
+ projects: core.projects,
112
+ tokens: core.tokens,
113
+ permit2: IPermit2(permit2),
114
+ owner: safeAddress(),
115
+ weth: IWETH9(weth),
116
+ factory: IUniswapV3Factory(factory),
117
+ poolManager: IPoolManager(poolManager),
118
+ trustedForwarder: trustedForwarder
119
+ });
114
120
 
115
121
  // Set the terminal as the default for the registry.
116
122
  registry.setDefaultTerminal(terminal);
@@ -4,7 +4,7 @@ pragma solidity 0.8.26;
4
4
  import {stdJson} from "forge-std/Script.sol";
5
5
  import {Vm} from "forge-std/Vm.sol";
6
6
 
7
- import {SphinxConstants, NetworkInfo} from "@sphinx-labs/contracts/SphinxConstants.sol";
7
+ import {SphinxConstants, NetworkInfo} from "@sphinx-labs/contracts/contracts/foundry/SphinxConstants.sol";
8
8
 
9
9
  import {IJBRouterTerminal} from "../../src/interfaces/IJBRouterTerminal.sol";
10
10
  import {IJBRouterTerminalRegistry} from "../../src/interfaces/IJBRouterTerminalRegistry.sol";
@@ -17,6 +17,7 @@ struct RouterTerminalDeployment {
17
17
  library RouterTerminalDeploymentLib {
18
18
  // Cheat code address, 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D.
19
19
  address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code"))));
20
+ // forge-lint: disable-next-line(screaming-snake-case-const)
20
21
  Vm internal constant vm = Vm(VM_ADDRESS);
21
22
 
22
23
  function getDeployment(string memory path) internal returns (RouterTerminalDeployment memory deployment) {
@@ -30,7 +31,7 @@ library RouterTerminalDeploymentLib {
30
31
 
31
32
  for (uint256 _i; _i < networks.length; _i++) {
32
33
  if (networks[_i].chainId == chainId) {
33
- return getDeployment(path, networks[_i].name);
34
+ return getDeployment({path: path, networkName: networks[_i].name});
34
35
  }
35
36
  }
36
37
 
@@ -39,17 +40,28 @@ library RouterTerminalDeploymentLib {
39
40
 
40
41
  function getDeployment(
41
42
  string memory path,
42
- string memory network_name
43
+ string memory networkName
43
44
  )
44
45
  internal
45
46
  view
46
47
  returns (RouterTerminalDeployment memory deployment)
47
48
  {
48
- deployment.terminal =
49
- IJBRouterTerminal(_getDeploymentAddress(path, "nana-router-terminal-v6", network_name, "JBRouterTerminal"));
49
+ deployment.terminal = IJBRouterTerminal(
50
+ _getDeploymentAddress({
51
+ path: path,
52
+ projectName: "nana-router-terminal-v6",
53
+ networkName: networkName,
54
+ contractName: "JBRouterTerminal"
55
+ })
56
+ );
50
57
 
51
58
  deployment.registry = IJBRouterTerminalRegistry(
52
- _getDeploymentAddress(path, "nana-router-terminal-v6", network_name, "JBRouterTerminalRegistry")
59
+ _getDeploymentAddress({
60
+ path: path,
61
+ projectName: "nana-router-terminal-v6",
62
+ networkName: networkName,
63
+ contractName: "JBRouterTerminalRegistry"
64
+ })
53
65
  );
54
66
  }
55
67
 
@@ -60,8 +72,8 @@ library RouterTerminalDeploymentLib {
60
72
  /// @return The address of the contract.
61
73
  function _getDeploymentAddress(
62
74
  string memory path,
63
- string memory project_name,
64
- string memory network_name,
75
+ string memory projectName,
76
+ string memory networkName,
65
77
  string memory contractName
66
78
  )
67
79
  internal
@@ -69,7 +81,8 @@ library RouterTerminalDeploymentLib {
69
81
  returns (address)
70
82
  {
71
83
  string memory deploymentJson =
72
- vm.readFile(string.concat(path, project_name, "/", network_name, "/", contractName, ".json"));
73
- return stdJson.readAddress(deploymentJson, ".address");
84
+ // forge-lint: disable-next-line(unsafe-cheatcode)
85
+ vm.readFile(string.concat(path, projectName, "/", networkName, "/", contractName, ".json"));
86
+ return stdJson.readAddress({json: deploymentJson, key: ".address"});
74
87
  }
75
88
  }
@@ -98,10 +98,13 @@ contract JBRouterTerminal is
98
98
 
99
99
  /// @notice The fee tiers to search when auto-discovering V3 pools, ordered by commonality.
100
100
  /// 3000 = 0.3%, 500 = 0.05%, 10000 = 1%, 100 = 0.01%.
101
+ // forge-lint: disable-next-line(mixed-case-variable)
101
102
  uint24[4] internal _FEE_TIERS = [uint24(3000), uint24(500), uint24(10_000), uint24(100)];
102
103
 
103
104
  /// @notice The fee/tickSpacing pairings to search for V4 vanilla pools.
105
+ // forge-lint: disable-next-line(mixed-case-variable)
104
106
  uint24[4] internal _V4_FEES = [uint24(3000), uint24(500), uint24(10_000), uint24(100)];
107
+ // forge-lint: disable-next-line(mixed-case-variable)
105
108
  int24[4] internal _V4_TICK_SPACINGS = [int24(60), int24(10), int24(200), int24(1)];
106
109
 
107
110
  //*********************************************************************//
@@ -186,6 +189,7 @@ contract JBRouterTerminal is
186
189
  override
187
190
  returns (JBAccountingContext memory context)
188
191
  {
192
+ // forge-lint: disable-next-line(unsafe-typecast)
189
193
  context = JBAccountingContext({token: token, decimals: 18, currency: uint32(uint160(token))});
190
194
  }
191
195
 
@@ -223,7 +227,10 @@ contract JBRouterTerminal is
223
227
  override
224
228
  returns (PoolInfo memory pool)
225
229
  {
226
- return _discoverPool(normalizedTokenIn, normalizedTokenOut);
230
+ pool = _discoverPool(normalizedTokenIn, normalizedTokenOut);
231
+ if (!pool.isV4 && address(pool.v3Pool) == address(0)) {
232
+ revert JBRouterTerminal_NoPoolFound(normalizedTokenIn, normalizedTokenOut);
233
+ }
227
234
  }
228
235
 
229
236
  /// @notice Public wrapper for V3-only _discoverPool, useful for off-chain queries.
@@ -240,6 +247,9 @@ contract JBRouterTerminal is
240
247
  returns (IUniswapV3Pool pool)
241
248
  {
242
249
  PoolInfo memory info = _discoverPool(normalizedTokenIn, normalizedTokenOut);
250
+ if (!info.isV4 && address(info.v3Pool) == address(0)) {
251
+ revert JBRouterTerminal_NoPoolFound(normalizedTokenIn, normalizedTokenOut);
252
+ }
243
253
  if (!info.isV4) pool = info.v3Pool;
244
254
  }
245
255
 
@@ -277,6 +287,11 @@ contract JBRouterTerminal is
277
287
  return ERC2771Context._msgSender();
278
288
  }
279
289
 
290
+ /// @notice Normalize a token address by replacing the native token sentinel with WETH.
291
+ function _normalize(address token) internal view returns (address) {
292
+ return token == JBConstants.NATIVE_TOKEN ? address(WETH) : token;
293
+ }
294
+
280
295
  //*********************************************************************//
281
296
  // ---------------------- external transactions ---------------------- //
282
297
  //*********************************************************************//
@@ -328,7 +343,7 @@ contract JBRouterTerminal is
328
343
  }
329
344
 
330
345
  /// @notice Empty implementation to satisfy the interface. This terminal has no balance to migrate.
331
- function migrateBalanceOf(uint256, address, IJBTerminal) external override returns (uint256 balance) {
346
+ function migrateBalanceOf(uint256, address, IJBTerminal) external pure override returns (uint256 balance) {
332
347
  return 0;
333
348
  }
334
349
 
@@ -392,22 +407,23 @@ contract JBRouterTerminal is
392
407
  (, address tokenIn, address tokenOut) = abi.decode(data, (uint256, address, address));
393
408
 
394
409
  // Normalize tokens (wrap native token if needed).
395
- address normalizedTokenIn = tokenIn == JBConstants.NATIVE_TOKEN ? address(WETH) : tokenIn;
396
- address normalizedTokenOut = tokenOut == JBConstants.NATIVE_TOKEN ? address(WETH) : tokenOut;
410
+ address normalizedTokenIn = _normalize(tokenIn);
411
+ address normalizedTokenOut = _normalize(tokenOut);
397
412
 
398
413
  // Verify caller is a legitimate pool via the factory.
399
414
  uint24 fee = IUniswapV3Pool(msg.sender).fee();
400
- address expectedPool = FACTORY.getPool(normalizedTokenIn, normalizedTokenOut, fee);
415
+ address expectedPool = FACTORY.getPool({tokenA: normalizedTokenIn, tokenB: normalizedTokenOut, fee: fee});
401
416
  if (msg.sender != expectedPool) revert JBRouterTerminal_CallerNotPool(msg.sender);
402
417
 
403
418
  // Calculate the amount of tokens to send to the pool (the positive delta).
419
+ // forge-lint: disable-next-line(unsafe-typecast)
404
420
  uint256 amountToSendToPool = amount0Delta < 0 ? uint256(amount1Delta) : uint256(amount0Delta);
405
421
 
406
422
  // Wrap native tokens if needed.
407
423
  if (tokenIn == JBConstants.NATIVE_TOKEN) WETH.deposit{value: amountToSendToPool}();
408
424
 
409
425
  // Transfer the tokens to the pool.
410
- IERC20(normalizedTokenIn).safeTransfer(msg.sender, amountToSendToPool);
426
+ IERC20(normalizedTokenIn).safeTransfer({to: msg.sender, value: amountToSendToPool});
411
427
  }
412
428
 
413
429
  /// @notice The Uniswap V4 unlock callback. Called by the PoolManager during `unlock()`.
@@ -436,10 +452,14 @@ contract JBRouterTerminal is
436
452
  int128 delta0 = delta.amount0();
437
453
  int128 delta1 = delta.amount1();
438
454
  if (zeroForOne) {
455
+ // forge-lint: disable-next-line(unsafe-typecast)
439
456
  amountIn = uint256(uint128(-delta0));
457
+ // forge-lint: disable-next-line(unsafe-typecast)
440
458
  amountOut = uint256(uint128(delta1));
441
459
  } else {
460
+ // forge-lint: disable-next-line(unsafe-typecast)
442
461
  amountIn = uint256(uint128(-delta1));
462
+ // forge-lint: disable-next-line(unsafe-typecast)
443
463
  amountOut = uint256(uint128(delta0));
444
464
  }
445
465
  }
@@ -448,11 +468,11 @@ contract JBRouterTerminal is
448
468
 
449
469
  // Settle input (pay what we owe to the PoolManager).
450
470
  Currency inputCurrency = zeroForOne ? key.currency0 : key.currency1;
451
- _settleV4(inputCurrency, amountIn);
471
+ _settleV4({currency: inputCurrency, amount: amountIn});
452
472
 
453
473
  // Take output (receive what the PoolManager owes us).
454
474
  Currency outputCurrency = zeroForOne ? key.currency1 : key.currency0;
455
- _takeV4(outputCurrency, amountOut);
475
+ _takeV4({currency: outputCurrency, amount: amountOut});
456
476
 
457
477
  return abi.encode(amountOut);
458
478
  }
@@ -543,7 +563,7 @@ contract JBRouterTerminal is
543
563
  if (token == JBConstants.NATIVE_TOKEN) return amount;
544
564
 
545
565
  // Otherwise, set the appropriate allowance for the recipient.
546
- IERC20(token).safeIncreaseAllowance(to, amount);
566
+ IERC20(token).safeIncreaseAllowance({spender: to, value: amount});
547
567
 
548
568
  return 0;
549
569
  }
@@ -553,37 +573,9 @@ contract JBRouterTerminal is
553
573
  /// @param tokenB The other token in the pair.
554
574
  /// @return bestLiquidity The highest liquidity found, or 0 if no pool exists.
555
575
  function _bestPoolLiquidity(address tokenA, address tokenB) internal view returns (uint128 bestLiquidity) {
556
- // Search V3.
557
- for (uint256 i; i < 4; i++) {
558
- // slither-disable-next-line calls-loop
559
- address poolAddr = FACTORY.getPool(tokenA, tokenB, _FEE_TIERS[i]);
560
- if (poolAddr == address(0)) continue;
561
-
562
- // slither-disable-next-line calls-loop
563
- uint128 liquidity = IUniswapV3Pool(poolAddr).liquidity();
564
- if (liquidity > bestLiquidity) bestLiquidity = liquidity;
565
- }
566
-
567
- // Search V4 — reuse _discoverV4Pool with current best.
568
- PoolInfo memory v4Result = _discoverV4Pool(
569
- tokenA,
570
- tokenB,
571
- bestLiquidity,
572
- PoolInfo({
573
- isV4: false,
574
- v3Pool: IUniswapV3Pool(address(0)),
575
- v4Key: PoolKey({
576
- currency0: Currency.wrap(address(0)),
577
- currency1: Currency.wrap(address(0)),
578
- fee: 0,
579
- tickSpacing: 0,
580
- hooks: IHooks(address(0))
581
- })
582
- })
583
- );
584
- if (v4Result.isV4) {
585
- bestLiquidity = POOL_MANAGER.getLiquidity(v4Result.v4Key.toId());
586
- }
576
+ PoolInfo memory pool = _discoverPool(tokenA, tokenB);
577
+ if (pool.isV4) return POOL_MANAGER.getLiquidity(pool.v4Key.toId());
578
+ if (address(pool.v3Pool) != address(0)) return pool.v3Pool.liquidity();
587
579
  }
588
580
 
589
581
  /// @notice The maximum number of cashout iterations before reverting. Prevents infinite loops from circular
@@ -687,8 +679,8 @@ contract JBRouterTerminal is
687
679
  // Exact same token — no conversion needed.
688
680
  if (tokenIn == tokenOut) return amount;
689
681
 
690
- address nIn = tokenIn == JBConstants.NATIVE_TOKEN ? address(WETH) : tokenIn;
691
- address nOut = tokenOut == JBConstants.NATIVE_TOKEN ? address(WETH) : tokenOut;
682
+ address nIn = _normalize(tokenIn);
683
+ address nOut = _normalize(tokenOut);
692
684
 
693
685
  if (nIn == nOut) {
694
686
  // Same underlying token — just wrap or unwrap.
@@ -721,7 +713,7 @@ contract JBRouterTerminal is
721
713
  view
722
714
  returns (address tokenOut, IJBTerminal destTerminal)
723
715
  {
724
- address normalizedTokenIn = tokenIn == JBConstants.NATIVE_TOKEN ? address(WETH) : tokenIn;
716
+ address normalizedTokenIn = _normalize(tokenIn);
725
717
  IJBTerminal[] memory terminals = DIRECTORY.terminalsOf(projectId);
726
718
 
727
719
  uint128 bestLiquidity;
@@ -733,8 +725,7 @@ contract JBRouterTerminal is
733
725
 
734
726
  for (uint256 j; j < contexts.length; j++) {
735
727
  address candidateToken = contexts[j].token;
736
- address normalizedCandidate =
737
- candidateToken == JBConstants.NATIVE_TOKEN ? address(WETH) : candidateToken;
728
+ address normalizedCandidate = _normalize(candidateToken);
738
729
 
739
730
  if (normalizedCandidate == normalizedTokenIn) continue;
740
731
 
@@ -776,7 +767,8 @@ contract JBRouterTerminal is
776
767
  // Search V3.
777
768
  for (uint256 i; i < 4; i++) {
778
769
  // slither-disable-next-line calls-loop
779
- address poolAddr = FACTORY.getPool(normalizedTokenIn, normalizedTokenOut, _FEE_TIERS[i]);
770
+ address poolAddr =
771
+ FACTORY.getPool({tokenA: normalizedTokenIn, tokenB: normalizedTokenOut, fee: _FEE_TIERS[i]});
780
772
 
781
773
  if (poolAddr == address(0)) continue;
782
774
 
@@ -801,10 +793,6 @@ contract JBRouterTerminal is
801
793
 
802
794
  // Search V4.
803
795
  bestPool = _discoverV4Pool(normalizedTokenIn, normalizedTokenOut, bestLiquidity, bestPool);
804
-
805
- if (!bestPool.isV4 && address(bestPool.v3Pool) == address(0)) {
806
- revert JBRouterTerminal_NoPoolFound(normalizedTokenIn, normalizedTokenOut);
807
- }
808
796
  }
809
797
 
810
798
  /// @notice Search V4 vanilla pools and update bestPool if a V4 pool has higher liquidity.
@@ -873,11 +861,7 @@ contract JBRouterTerminal is
873
861
 
874
862
  if (pool.isV4) {
875
863
  return _executeV4Swap({
876
- key: pool.v4Key,
877
- normalizedTokenIn: normalizedTokenIn,
878
- normalizedTokenOut: normalizedTokenOut,
879
- amount: amount,
880
- minAmountOut: minAmountOut
864
+ key: pool.v4Key, normalizedTokenIn: normalizedTokenIn, amount: amount, minAmountOut: minAmountOut
881
865
  });
882
866
  } else {
883
867
  return _executeV3Swap({
@@ -908,8 +892,11 @@ contract JBRouterTerminal is
908
892
  (int256 amount0, int256 amount1) = pool.swap({
909
893
  recipient: address(this),
910
894
  zeroForOne: zeroForOne,
895
+ // forge-lint: disable-next-line(unsafe-typecast)
911
896
  amountSpecified: int256(amount),
912
- sqrtPriceLimitX96: JBSwapLib.sqrtPriceLimitFromAmounts(amount, minAmountOut, zeroForOne),
897
+ sqrtPriceLimitX96: JBSwapLib.sqrtPriceLimitFromAmounts({
898
+ amountIn: amount, minimumAmountOut: minAmountOut, zeroForOne: zeroForOne
899
+ }),
913
900
  data: callbackData
914
901
  });
915
902
 
@@ -921,7 +908,6 @@ contract JBRouterTerminal is
921
908
  function _executeV4Swap(
922
909
  PoolKey memory key,
923
910
  address normalizedTokenIn,
924
- address normalizedTokenOut,
925
911
  uint256 amount,
926
912
  uint256 minAmountOut
927
913
  )
@@ -933,11 +919,14 @@ contract JBRouterTerminal is
933
919
  bool zeroForOne = Currency.unwrap(key.currency0) == v4In;
934
920
 
935
921
  // Use sqrtPriceLimitFromAmounts for partial-fill protection, consistent with V3 path.
936
- uint160 sqrtPriceLimitX96 = JBSwapLib.sqrtPriceLimitFromAmounts(amount, minAmountOut, zeroForOne);
922
+ uint160 sqrtPriceLimitX96 = JBSwapLib.sqrtPriceLimitFromAmounts({
923
+ amountIn: amount, minimumAmountOut: minAmountOut, zeroForOne: zeroForOne
924
+ });
937
925
 
938
926
  // V4 sign convention: negative = exact input, positive = exact output.
939
927
  bytes memory result =
940
- POOL_MANAGER.unlock(abi.encode(key, zeroForOne, -int256(amount), sqrtPriceLimitX96, minAmountOut));
928
+ // forge-lint: disable-next-line(unsafe-typecast)
929
+ POOL_MANAGER.unlock(abi.encode(key, zeroForOne, -int256(amount), sqrtPriceLimitX96, minAmountOut));
941
930
 
942
931
  amountOut = abi.decode(result, (uint256));
943
932
  }
@@ -1044,8 +1033,9 @@ contract JBRouterTerminal is
1044
1033
  uint160 sqrtP = TickMath.getSqrtRatioAtTick(arithmeticMeanTick);
1045
1034
  if (sqrtP == 0) return SLIPPAGE_DENOMINATOR;
1046
1035
 
1047
- uint256 impact = JBSwapLib.calculateImpact(amountIn, liquidity, sqrtP, zeroForOne);
1048
- return JBSwapLib.getSlippageTolerance(impact, poolFeeBps);
1036
+ uint256 impact =
1037
+ JBSwapLib.calculateImpact({amountIn: amountIn, liquidity: liquidity, sqrtP: sqrtP, zeroForOne: zeroForOne});
1038
+ return JBSwapLib.getSlippageTolerance({impact: impact, poolFeeBps: poolFeeBps});
1049
1039
  }
1050
1040
 
1051
1041
  /// @notice Get a TWAP-based quote with dynamic slippage for a V3 pool.
@@ -1066,8 +1056,11 @@ contract JBRouterTerminal is
1066
1056
  uint256 twapWindow = DEFAULT_TWAP_WINDOW;
1067
1057
  if (oldestObservation < twapWindow) twapWindow = oldestObservation;
1068
1058
 
1069
- (int24 arithmeticMeanTick, uint128 liquidity) =
1070
- OracleLibrary.consult({pool: address(pool), secondsAgo: uint32(twapWindow)});
1059
+ (
1060
+ int24 arithmeticMeanTick,
1061
+ uint128 liquidity
1062
+ // forge-lint: disable-next-line(unsafe-typecast)
1063
+ ) = OracleLibrary.consult({pool: address(pool), secondsAgo: uint32(twapWindow)});
1071
1064
 
1072
1065
  if (liquidity == 0) revert JBRouterTerminal_NoLiquidity();
1073
1066
 
@@ -1086,6 +1079,7 @@ contract JBRouterTerminal is
1086
1079
  if (amount > type(uint128).max) revert JBRouterTerminal_AmountOverflow(amount);
1087
1080
  minAmountOut = OracleLibrary.getQuoteAtTick({
1088
1081
  tick: arithmeticMeanTick,
1082
+ // forge-lint: disable-next-line(unsafe-typecast)
1089
1083
  baseAmount: uint128(amount),
1090
1084
  baseToken: normalizedTokenIn,
1091
1085
  quoteToken: normalizedTokenOut
@@ -1146,7 +1140,11 @@ contract JBRouterTerminal is
1146
1140
 
1147
1141
  if (amount > type(uint128).max) revert JBRouterTerminal_AmountOverflow(amount);
1148
1142
  minAmountOut = OracleLibrary.getQuoteAtTick({
1149
- tick: tick, baseAmount: uint128(amount), baseToken: normalizedTokenIn, quoteToken: normalizedTokenOut
1143
+ tick: tick,
1144
+ // forge-lint: disable-next-line(unsafe-typecast)
1145
+ baseAmount: uint128(amount),
1146
+ baseToken: normalizedTokenIn,
1147
+ quoteToken: normalizedTokenOut
1150
1148
  });
1151
1149
 
1152
1150
  minAmountOut -= (minAmountOut * slippageTolerance) / SLIPPAGE_DENOMINATOR;
@@ -1169,7 +1167,7 @@ contract JBRouterTerminal is
1169
1167
  internal
1170
1168
  returns (uint256 amountOut)
1171
1169
  {
1172
- address normalizedTokenIn = tokenIn == JBConstants.NATIVE_TOKEN ? address(WETH) : tokenIn;
1170
+ address normalizedTokenIn = _normalize(tokenIn);
1173
1171
 
1174
1172
  // Snapshot the input token balance before the swap to compute the leftover delta accurately.
1175
1173
  uint256 balanceBefore = IERC20(normalizedTokenIn).balanceOf(address(this));
@@ -1177,7 +1175,7 @@ contract JBRouterTerminal is
1177
1175
  // Execute the swap in a scoped block to manage stack depth.
1178
1176
  amountOut = _executeSwap({
1179
1177
  normalizedTokenIn: normalizedTokenIn,
1180
- normalizedTokenOut: tokenOut == JBConstants.NATIVE_TOKEN ? address(WETH) : tokenOut,
1178
+ normalizedTokenOut: _normalize(tokenOut),
1181
1179
  amount: amount,
1182
1180
  metadata: metadata,
1183
1181
  callbackData: abi.encode(projectId, tokenIn, tokenOut)
@@ -1231,6 +1229,9 @@ contract JBRouterTerminal is
1231
1229
  {
1232
1230
  // Discover the best pool across V3 and V4 fee tiers.
1233
1231
  pool = _discoverPool(normalizedTokenIn, normalizedTokenOut);
1232
+ if (!pool.isV4 && address(pool.v3Pool) == address(0)) {
1233
+ revert JBRouterTerminal_NoPoolFound(normalizedTokenIn, normalizedTokenOut);
1234
+ }
1234
1235
 
1235
1236
  // Check for a user-provided quote.
1236
1237
  (bool exists, bytes memory quote) =
@@ -1376,7 +1377,7 @@ contract JBRouterTerminal is
1376
1377
  } else {
1377
1378
  // ERC20: sync then transfer then settle.
1378
1379
  POOL_MANAGER.sync(currency);
1379
- IERC20(Currency.unwrap(currency)).safeTransfer(address(POOL_MANAGER), amount);
1380
+ IERC20(Currency.unwrap(currency)).safeTransfer({to: address(POOL_MANAGER), value: amount});
1380
1381
  // slither-disable-next-line unused-return
1381
1382
  POOL_MANAGER.settle();
1382
1383
  }
@@ -1384,7 +1385,7 @@ contract JBRouterTerminal is
1384
1385
 
1385
1386
  /// @notice Take the output side of a V4 swap (receive tokens from PoolManager).
1386
1387
  function _takeV4(Currency currency, uint256 amount) internal {
1387
- POOL_MANAGER.take(currency, address(this), amount);
1388
+ POOL_MANAGER.take({currency: currency, to: address(this), amount: amount});
1388
1389
 
1389
1390
  // If native ETH output, wrap to WETH (downstream _handleSwap unwraps if needed).
1390
1391
  if (Currency.unwrap(currency) == address(0)) {
@@ -1400,19 +1401,20 @@ contract JBRouterTerminal is
1400
1401
  function _transferFrom(address from, address payable to, address token, uint256 amount) internal {
1401
1402
  if (from == address(this)) {
1402
1403
  // If the token is native token, assume the `sendValue` standard.
1403
- if (token == JBConstants.NATIVE_TOKEN) return Address.sendValue(to, amount);
1404
+ if (token == JBConstants.NATIVE_TOKEN) return Address.sendValue({recipient: to, amount: amount});
1404
1405
 
1405
1406
  // If the transfer is from this terminal, use `safeTransfer`.
1406
- return IERC20(token).safeTransfer(to, amount);
1407
+ return IERC20(token).safeTransfer({to: to, value: amount});
1407
1408
  }
1408
1409
 
1409
1410
  // If there's sufficient approval, transfer normally.
1410
1411
  if (IERC20(token).allowance({owner: address(from), spender: address(this)}) >= amount) {
1411
- return IERC20(token).safeTransferFrom(from, to, amount);
1412
+ return IERC20(token).safeTransferFrom({from: from, to: to, value: amount});
1412
1413
  }
1413
1414
 
1414
1415
  // Otherwise, attempt to use the `permit2` method.
1415
- PERMIT2.transferFrom(from, to, uint160(amount), token);
1416
+ // forge-lint: disable-next-line(unsafe-typecast)
1417
+ PERMIT2.transferFrom({from: from, to: to, amount: uint160(amount), token: token});
1416
1418
  }
1417
1419
 
1418
1420
  //*********************************************************************//
@@ -442,7 +442,7 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
442
442
  if (token == JBConstants.NATIVE_TOKEN) return amount;
443
443
 
444
444
  // Otherwise, set the appropriate allowance for the recipient.
445
- IERC20(token).safeIncreaseAllowance(to, amount);
445
+ IERC20(token).safeIncreaseAllowance({spender: to, value: amount});
446
446
 
447
447
  return 0;
448
448
  }
@@ -455,18 +455,19 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
455
455
  function _transferFrom(address from, address payable to, address token, uint256 amount) internal virtual {
456
456
  if (from == address(this)) {
457
457
  // If the token is native token, assume the `sendValue` standard.
458
- if (token == JBConstants.NATIVE_TOKEN) return Address.sendValue(to, amount);
458
+ if (token == JBConstants.NATIVE_TOKEN) return Address.sendValue({recipient: to, amount: amount});
459
459
 
460
460
  // If the transfer is from this terminal, use `safeTransfer`.
461
- return IERC20(token).safeTransfer(to, amount);
461
+ return IERC20(token).safeTransfer({to: to, value: amount});
462
462
  }
463
463
 
464
464
  // If there's sufficient approval, transfer normally.
465
465
  if (IERC20(token).allowance({owner: address(from), spender: address(this)}) >= amount) {
466
- return IERC20(token).safeTransferFrom(from, to, amount);
466
+ return IERC20(token).safeTransferFrom({from: from, to: to, value: amount});
467
467
  }
468
468
 
469
469
  // Otherwise, attempt to use the `permit2` method.
470
- PERMIT2.transferFrom(from, to, uint160(amount), token);
470
+ // forge-lint: disable-next-line(unsafe-typecast)
471
+ PERMIT2.transferFrom({from: from, to: to, amount: uint160(amount), token: token});
471
472
  }
472
473
  }
@@ -54,7 +54,7 @@ library JBSwapLib {
54
54
 
55
55
  // Sigmoid: minSlippage + (maxSlippage - minSlippage) * impact / (impact + K)
56
56
  uint256 range = MAX_SLIPPAGE - minSlippage;
57
- uint256 tolerance = minSlippage + mulDiv(range, impact, impact + SIGMOID_K);
57
+ uint256 tolerance = minSlippage + mulDiv({x: range, y: impact, denominator: impact + SIGMOID_K});
58
58
 
59
59
  return tolerance;
60
60
  }
@@ -82,11 +82,11 @@ library JBSwapLib {
82
82
  {
83
83
  if (liquidity == 0 || sqrtP == 0) return 0;
84
84
 
85
- uint256 base = mulDiv(amountIn, IMPACT_PRECISION, uint256(liquidity));
85
+ uint256 base = mulDiv({x: amountIn, y: IMPACT_PRECISION, denominator: uint256(liquidity)});
86
86
 
87
87
  impact = zeroForOne
88
- ? mulDiv(base, uint256(sqrtP), uint256(1) << 96)
89
- : mulDiv(base, uint256(1) << 96, uint256(sqrtP));
88
+ ? mulDiv({x: base, y: uint256(sqrtP), denominator: uint256(1) << 96})
89
+ : mulDiv({x: base, y: uint256(1) << 96, denominator: uint256(sqrtP)});
90
90
  }
91
91
 
92
92
  //*********************************************************************//
@@ -130,11 +130,11 @@ library JBSwapLib {
130
130
  return zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1;
131
131
  } else if (num / den >= (uint256(1) << 64)) {
132
132
  // Extended range: use ratioX128 to avoid mulDiv overflow, then shift.
133
- uint256 ratioX128 = mulDiv(num, uint256(1) << 128, den);
133
+ uint256 ratioX128 = mulDiv({x: num, y: uint256(1) << 128, denominator: den});
134
134
  sqrtResult = Math.sqrt(ratioX128) * (uint256(1) << 32);
135
135
  } else {
136
136
  // Normal range: full precision via ratioX192.
137
- uint256 ratioX192 = mulDiv(num, uint256(1) << 192, den);
137
+ uint256 ratioX192 = mulDiv({x: num, y: uint256(1) << 192, denominator: den});
138
138
  sqrtResult = Math.sqrt(ratioX192);
139
139
  }
140
140
 
@@ -145,6 +145,7 @@ library JBSwapLib {
145
145
  if (sqrtResult >= uint256(TickMath.MAX_SQRT_RATIO)) {
146
146
  return TickMath.MAX_SQRT_RATIO - 1;
147
147
  }
148
+ // forge-lint: disable-next-line(unsafe-typecast)
148
149
  return uint160(sqrtResult);
149
150
  } else {
150
151
  if (sqrtResult >= uint256(TickMath.MAX_SQRT_RATIO)) {
@@ -153,6 +154,7 @@ library JBSwapLib {
153
154
  if (sqrtResult <= uint256(TickMath.MIN_SQRT_RATIO)) {
154
155
  return TickMath.MIN_SQRT_RATIO + 1;
155
156
  }
157
+ // forge-lint: disable-next-line(unsafe-typecast)
156
158
  return uint160(sqrtResult);
157
159
  }
158
160
  }
@@ -94,9 +94,12 @@ contract RouterTerminalHarness is JBRouterTerminal {
94
94
  )
95
95
  external
96
96
  view
97
- returns (PoolInfo memory)
97
+ returns (PoolInfo memory pool)
98
98
  {
99
- return _discoverPool(normalizedTokenIn, normalizedTokenOut);
99
+ pool = _discoverPool(normalizedTokenIn, normalizedTokenOut);
100
+ if (!pool.isV4 && address(pool.v3Pool) == address(0)) {
101
+ revert JBRouterTerminal_NoPoolFound(normalizedTokenIn, normalizedTokenOut);
102
+ }
100
103
  }
101
104
  }
102
105
 
@@ -111,12 +111,7 @@ contract RouterTerminalFeeCashOutForkTest is Test {
111
111
  // ──────────────────────────
112
112
 
113
113
  function setUp() public {
114
- string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
115
- if (bytes(rpcUrl).length == 0) {
116
- vm.skip(true);
117
- return;
118
- }
119
- vm.createSelectFork(rpcUrl, BLOCK_NUMBER);
114
+ vm.createSelectFork("ethereum", BLOCK_NUMBER);
120
115
 
121
116
  _deployJBCore();
122
117
 
@@ -94,13 +94,7 @@ contract RouterTerminalForkTest is Test {
94
94
  // ──────────────────────────────────────
95
95
 
96
96
  function setUp() public {
97
- // Skip fork tests in CI when no RPC URL is configured.
98
- string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
99
- if (bytes(rpcUrl).length == 0) {
100
- vm.skip(true);
101
- return;
102
- }
103
- vm.createSelectFork(rpcUrl, BLOCK_NUMBER);
97
+ vm.createSelectFork("ethereum", BLOCK_NUMBER);
104
98
 
105
99
  // Deploy all JB core contracts fresh within the fork.
106
100
  _deployJBCore();
@@ -305,12 +305,7 @@ contract RouterTerminalSandwichForkTest is Test {
305
305
  //*********************************************************************//
306
306
 
307
307
  function setUp() public {
308
- string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
309
- if (bytes(rpcUrl).length == 0) {
310
- vm.skip(true);
311
- return;
312
- }
313
- vm.createSelectFork(rpcUrl, BLOCK_NUMBER);
308
+ vm.createSelectFork("ethereum", BLOCK_NUMBER);
314
309
 
315
310
  _deployJBCore();
316
311
 
@@ -51,7 +51,7 @@ contract MockToken {
51
51
 
52
52
  /// @notice _cashOutLoop should revert with CashOutLoopLimit when circular token
53
53
  /// dependencies cause more than 20 iterations, instead of consuming all gas.
54
- contract L30_CashOutLoopLimitTest is Test {
54
+ contract CashOutLoopLimitTest is Test {
55
55
  JBRouterTerminal routerTerminal;
56
56
 
57
57
  IJBDirectory directory = IJBDirectory(makeAddr("directory"));
@@ -13,7 +13,7 @@ import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
13
13
  import {JBRouterTerminalRegistry} from "../../src/JBRouterTerminalRegistry.sol";
14
14
 
15
15
  /// @notice lockTerminalFor should revert if current terminal doesn't match expected.
16
- contract L29_LockTerminalRaceTest is Test {
16
+ contract LockTerminalRaceTest is Test {
17
17
  JBRouterTerminalRegistry registry;
18
18
 
19
19
  IJBPermissions permissions = IJBPermissions(makeAddr("permissions"));
@@ -37,7 +37,7 @@ contract SwapLibHarness {
37
37
  /// Because setting up a full V4 PoolManager in unit tests requires significant infrastructure, these tests
38
38
  /// exercise the slippage math in isolation via JBSwapLib, which is the same code path used by
39
39
  /// `_getV4SpotQuote` and `_getV3TwapQuote`.
40
- contract M3_V4SpotPriceSlippageTest is Test {
40
+ contract V4SpotPriceSlippageTest is Test {
41
41
  SwapLibHarness lib;
42
42
 
43
43
  /// @notice The same constants from JBSwapLib / JBRouterTerminal.