@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 +147 -47
- package/foundry.toml +3 -4
- package/package.json +4 -3
- package/script/Deploy.s.sol +26 -20
- package/script/helpers/RouterTerminalDeploymentLib.sol +23 -10
- package/src/JBRouterTerminal.sol +74 -72
- package/src/JBRouterTerminalRegistry.sol +6 -5
- package/src/libraries/JBSwapLib.sol +8 -6
- package/test/RouterTerminal.t.sol +5 -2
- package/test/RouterTerminalFeeCashOutFork.t.sol +1 -6
- package/test/RouterTerminalFork.t.sol +1 -7
- package/test/RouterTerminalSandwichFork.t.sol +1 -6
- package/test/regression/{L30_CashOutLoopLimit.t.sol → CashOutLoopLimit.t.sol} +1 -1
- package/test/regression/{L29_LockTerminalRace.t.sol → LockTerminalRace.t.sol} +1 -1
- package/test/regression/{M3_V4SpotPriceSlippage.t.sol → V4SpotPriceSlippage.t.sol} +1 -1
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.
|
|
147
|
-
2.
|
|
148
|
-
3.
|
|
149
|
-
4.
|
|
150
|
-
5.
|
|
151
|
-
6.
|
|
152
|
-
7.
|
|
153
|
-
8.
|
|
154
|
-
9.
|
|
155
|
-
10.
|
|
156
|
-
11.
|
|
157
|
-
12.
|
|
158
|
-
13.
|
|
159
|
-
14.
|
|
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
|
|
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
|
-
**
|
|
358
|
-
- `
|
|
359
|
-
- `
|
|
360
|
-
|
|
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:
|
|
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/
|
|
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
|
|
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
|
-
|
|
493
|
+
forge-std/=lib/forge-std/src/
|
|
441
494
|
```
|
|
442
495
|
|
|
443
|
-
|
|
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 `
|
|
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.
|
|
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.
|
|
21
|
-
"@
|
|
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",
|
package/script/Deploy.s.sol
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
43
|
+
string memory networkName
|
|
43
44
|
)
|
|
44
45
|
internal
|
|
45
46
|
view
|
|
46
47
|
returns (RouterTerminalDeployment memory deployment)
|
|
47
48
|
{
|
|
48
|
-
deployment.terminal =
|
|
49
|
-
|
|
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(
|
|
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
|
|
64
|
-
string memory
|
|
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
|
-
|
|
73
|
-
|
|
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
|
}
|
package/src/JBRouterTerminal.sol
CHANGED
|
@@ -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
|
-
|
|
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
|
|
396
|
-
address normalizedTokenOut = 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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
|
691
|
-
address nOut = 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
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
1048
|
-
|
|
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
|
-
(
|
|
1070
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
40
|
+
contract V4SpotPriceSlippageTest is Test {
|
|
41
41
|
SwapLibHarness lib;
|
|
42
42
|
|
|
43
43
|
/// @notice The same constants from JBSwapLib / JBRouterTerminal.
|