@bananapus/router-terminal-v6 0.0.10 → 0.0.12

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.
@@ -0,0 +1,283 @@
1
+ # nana-router-terminal-v6 -- Audit Instructions
2
+
3
+ Target: experienced Solidity auditors reviewing the Juicebox V6 router terminal.
4
+
5
+ ## Architecture Overview
6
+
7
+ The router terminal accepts any token and dynamically discovers what each destination Juicebox project accepts. It then routes the payment there via direct forwarding, Uniswap swap (V3 or V4), JB token cashout, or a combination. It is stateless between transactions -- it holds no persistent balances, no queued operations, no pending state.
8
+
9
+ Two contracts. One library. One struct.
10
+
11
+ ### Contract Table
12
+
13
+ | Contract | Lines | Role |
14
+ |----------|-------|------|
15
+ | `JBRouterTerminal` | ~1,437 | Core routing engine. Accepts any token, discovers destination project's accepted token, converts via wrap/unwrap, Uniswap V3 swap, Uniswap V4 swap, or JB token cashout chain, then forwards to destination terminal. Implements `IJBTerminal`, `IJBPermitTerminal`, `IUniswapV3SwapCallback`, `IUnlockCallback`. |
16
+ | `JBRouterTerminalRegistry` | ~481 | Maps projects to their preferred router terminal instance. Owner-managed allowlist of terminals. Default terminal fallback. Lock terminal pattern for immutability. Implements `IJBTerminal`. |
17
+ | `JBSwapLib` (library) | ~161 | Continuous sigmoid slippage tolerance calculation. Price impact estimation. `sqrtPriceLimitX96` computation from input/output amounts. |
18
+ | `PoolInfo` (struct) | ~14 | Tagged union: `{isV4, v3Pool, v4Key}`. Carries the winning pool from discovery. |
19
+
20
+ ### Dependency Graph
21
+
22
+ ```
23
+ JBRouterTerminal
24
+ ├── JBPermissioned (nana-core-v6) -- permission checks
25
+ ├── Ownable (OZ) -- owner management (unused operationally)
26
+ ├── ERC2771Context (OZ) -- meta-transactions
27
+ ├── IJBTerminal -- terminal interface
28
+ ├── IJBPermitTerminal -- Permit2 support
29
+ ├── IUniswapV3SwapCallback -- V3 swap callback
30
+ ├── IUnlockCallback -- V4 swap callback
31
+ ├── JBSwapLib -- slippage math
32
+ ├── JBDirectory -- terminal/controller lookup
33
+ ├── JBTokens -- credit transfers, projectIdOf lookups
34
+ ├── IUniswapV3Factory -- V3 pool discovery & verification
35
+ ├── IPoolManager -- V4 swap execution & pool state reads
36
+ ├── IPermit2 -- token transfer with Permit2
37
+ └── IWETH9 -- wrap/unwrap native token
38
+
39
+ JBRouterTerminalRegistry
40
+ ├── JBPermissioned -- permission checks
41
+ ├── Ownable -- owner-only admin (allow/disallow/setDefault)
42
+ ├── ERC2771Context -- meta-transactions
43
+ ├── IJBTerminal -- forwards pay/addToBalanceOf
44
+ └── IPermit2 -- token transfer with Permit2
45
+ ```
46
+
47
+ ### Immutable State (JBRouterTerminal)
48
+
49
+ | Name | Type | Purpose |
50
+ |------|------|---------|
51
+ | `DIRECTORY` | `IJBDirectory` | Project terminal/controller lookup |
52
+ | `PROJECTS` | `IJBProjects` | ERC-721 project ownership |
53
+ | `TOKENS` | `IJBTokens` | Credit transfers, token-to-project mapping |
54
+ | `FACTORY` | `IUniswapV3Factory` | V3 pool discovery and callback verification |
55
+ | `POOL_MANAGER` | `IPoolManager` | V4 swap execution (can be `address(0)` if V4 unavailable) |
56
+ | `PERMIT2` | `IPermit2` | Token transfer utility |
57
+ | `WETH` | `IWETH9` | Native token wrapping/unwrapping |
58
+
59
+ ### Mutable State (JBRouterTerminalRegistry)
60
+
61
+ | Name | Visibility | Purpose |
62
+ |------|-----------|---------|
63
+ | `defaultTerminal` | `public` | Fallback terminal for projects without explicit assignment |
64
+ | `hasLockedTerminal` | `public mapping` | Per-project lock flag (irreversible) |
65
+ | `isTerminalAllowed` | `public mapping` | Allowlist of terminals the owner has approved |
66
+ | `_terminalOf` | `internal mapping` | Per-project explicit terminal assignment |
67
+
68
+ ## V3/V4 Pool Routing Mechanics
69
+
70
+ ### Pool Discovery
71
+
72
+ `_discoverPool(normalizedTokenIn, normalizedTokenOut)` searches 8 pools total:
73
+
74
+ **V3 (4 fee tiers):** 3000 (0.3%), 500 (0.05%), 10000 (1%), 100 (0.01%). Each is looked up via `FACTORY.getPool()`. Liquidity read via `pool.liquidity()`.
75
+
76
+ **V4 (4 fee/tickSpacing pairs):** 3000/60, 500/10, 10000/200, 100/1. Each is looked up via `POOL_MANAGER.getSlot0(key.toId())` -- a zero `sqrtPriceX96` means the pool does not exist. Liquidity read via `POOL_MANAGER.getLiquidity()`.
77
+
78
+ The pool with the highest in-range liquidity wins. If `POOL_MANAGER == address(0)`, V4 search is skipped entirely.
79
+
80
+ ### Quote Computation
81
+
82
+ Quote priority in `_pickPoolAndQuote()`:
83
+
84
+ 1. **User-provided quote** -- If `quoteForSwap` metadata key is present, its value is used as `minAmountOut` directly. This is the recommended path for all swaps, especially V4.
85
+ 2. **V3 TWAP** -- `_getV3TwapQuote()` uses `OracleLibrary.consult()` with a 10-minute window (capped to oldest available observation). Computes `minAmountOut = twapQuote * (1 - sigmoidSlippage)`.
86
+ 3. **V4 spot price** -- `_getV4SpotQuote()` reads instantaneous tick from `getSlot0()`. Same sigmoid slippage formula applied. This is manipulable within a single block.
87
+
88
+ ### Sigmoid Slippage Formula
89
+
90
+ ```
91
+ impact = amountIn * PRECISION / liquidity * (sqrtP or 1/sqrtP)
92
+ tolerance = minSlippage + (MAX_SLIPPAGE - minSlippage) * impact / (impact + SIGMOID_K)
93
+ ```
94
+
95
+ Where:
96
+ - `minSlippage = max(poolFee + 100 bps, 200 bps)` -- floor of 2%
97
+ - `MAX_SLIPPAGE = 8800 bps` -- ceiling of 88%
98
+ - `SIGMOID_K = 5e16` -- steepness parameter
99
+ - `IMPACT_PRECISION = 1e18`
100
+
101
+ ### Swap Execution
102
+
103
+ **V3 path:** Direct `pool.swap()` call. Callback `uniswapV3SwapCallback()` wraps ETH if needed and transfers input tokens to the pool. Callback is verified by computing `FACTORY.getPool(tokenA, tokenB, fee)` and checking it matches `msg.sender`.
104
+
105
+ **V4 path:** `POOL_MANAGER.unlock(data)` triggers `unlockCallback()`. Inside the callback: `POOL_MANAGER.swap()` executes the trade, `_settleV4()` pays the input, `_takeV4()` receives the output. For native ETH output, received ETH is immediately wrapped to WETH (downstream `_handleSwap` unwraps if the destination needs native token). Callback verified by `msg.sender == address(POOL_MANAGER)`.
106
+
107
+ ### Leftover Handling
108
+
109
+ After a swap, `_handleSwap()` measures the balance delta of the input token. If the swap was a partial fill (sqrtPriceLimit hit), leftover input tokens are returned to `_msgSender()`. For native token inputs, any remaining raw ETH is wrapped to WETH before the delta check, then unwrapped for the refund.
110
+
111
+ ## Cashout Loop
112
+
113
+ When the input token is a JB project token (detected via `TOKENS.projectIdOf()` or `cashOutSource` metadata), the router enters `_cashOutLoop()`:
114
+
115
+ 1. Check if the destination project directly accepts the current token. If yes, return.
116
+ 2. Determine the source project ID (from metadata override or token lookup).
117
+ 3. Call `_findCashOutPath()` which searches source project terminals for:
118
+ - **Priority 1:** A reclaimable token the destination directly accepts.
119
+ - **Priority 2:** A reclaimable JB project token (can recurse).
120
+ - **Priority 3:** Any base token (the router will swap it later).
121
+ 4. Execute `cashOutTerminal.cashOutTokensOf()` to reclaim tokens.
122
+ 5. Loop back to step 1 with the reclaimed token.
123
+
124
+ **Iteration cap:** `_MAX_CASHOUT_ITERATIONS = 20`. Exceeding this reverts with `JBRouterTerminal_CashOutLoopLimit()`.
125
+
126
+ **Slippage:** The `cashOutMinReclaimed` metadata value is applied only to the first cashout step. Subsequent steps have zero per-step minimum. The final output amount is validated by the destination terminal's `minReturnedTokens` parameter.
127
+
128
+ **Circular dependency:** If token A cashes out to token B and token B cashes out to token A, the loop hits the 20-iteration cap and reverts cleanly (no fund loss, only gas wasted).
129
+
130
+ ## Lock Terminal Pattern
131
+
132
+ The registry implements a two-phase terminal assignment:
133
+
134
+ 1. **Set:** `setTerminalFor(projectId, terminal)` -- requires `SET_ROUTER_TERMINAL` permission and terminal must be in the allowlist. Can be changed freely while unlocked.
135
+
136
+ 2. **Lock:** `lockTerminalFor(projectId, expectedTerminal)` -- requires `SET_ROUTER_TERMINAL` permission. The `expectedTerminal` parameter is a race condition guard: if the resolved terminal (explicit or default) does not match, the call reverts with `TerminalMismatch`. Once locked, `hasLockedTerminal[projectId] = true` and `setTerminalFor()` reverts permanently.
137
+
138
+ **Default snapshot on lock:** If a project has no explicit terminal at lock time, the current `defaultTerminal` is snapshotted into `_terminalOf[projectId]` before locking. This insulates the project from future default changes.
139
+
140
+ **Irreversibility:** There is no `unlockTerminalFor()`. Locking is permanent.
141
+
142
+ ## Fee-on-Transfer Token Risks
143
+
144
+ **JBRouterTerminal:** Uses balance-delta accounting in `_acceptFundsFor()` (lines 550-556). Measures `balanceBefore` and `balanceAfter` the transfer, returning the actual amount received. This correctly handles fee-on-transfer tokens at the acceptance stage. However, the amount forwarded to the destination terminal is the delta, which may differ from what the destination terminal expects if it also performs balance-delta checks.
145
+
146
+ **JBRouterTerminalRegistry:** Does NOT use balance-delta accounting in `_acceptFundsFor()` (line 437). It returns the user-supplied `amount` directly. If a fee-on-transfer token is used through the registry, the forwarded amount will exceed actual tokens received, causing a downstream revert or incorrect accounting.
147
+
148
+ **Audit focus:** Verify that the registry's `_acceptFundsFor` cannot be exploited with fee-on-transfer tokens. The comment on line 433-435 states these are "not supported by design" -- confirm this is documented clearly enough and that no path silently loses funds.
149
+
150
+ ## uint160 Permit2 Truncation Risk
151
+
152
+ Both contracts cast `amount` to `uint160` when falling through to `PERMIT2.transferFrom()`:
153
+
154
+ - `JBRouterTerminal._transferFrom()` line 1425: `if (amount > type(uint160).max) revert JBRouterTerminal_AmountOverflow(amount);`
155
+ - `JBRouterTerminalRegistry._transferFrom()` line 477: `if (amount > type(uint160).max) revert JBRouterTerminalRegistry_AmountOverflow();`
156
+
157
+ Both contracts now revert before truncation occurs. Verify these overflow checks are complete and that no code path can reach the `uint160()` cast without hitting the guard.
158
+
159
+ **Practical risk:** Low. ERC-20 supplies rarely approach `type(uint160).max` (~1.46e48). But the check is there -- verify it.
160
+
161
+ ## Short TWAP Window Concerns
162
+
163
+ `_getV3TwapQuote()` defaults to a 10-minute TWAP window (`DEFAULT_TWAP_WINDOW = 600 seconds`). If the pool's oldest observation is younger than 10 minutes, the window is silently capped:
164
+
165
+ ```solidity
166
+ if (oldestObservation < twapWindow) twapWindow = oldestObservation;
167
+ ```
168
+
169
+ If `oldestObservation == 0`, the function reverts with `JBRouterTerminal_NoObservationHistory()`.
170
+
171
+ **Risk:** A pool with 30 seconds of observation history produces a "TWAP" that is functionally a spot price. This is the same vulnerability as the V4 spot price path, but without the V4 label warning users.
172
+
173
+ **Audit focus:** Consider whether the contract should enforce a minimum observation age (e.g., revert if `oldestObservation < 5 minutes`). Currently, any non-zero observation age is accepted.
174
+
175
+ ## Priority Audit Areas
176
+
177
+ ### Critical Path: Payment Routing
178
+
179
+ The most complex and highest-value code path. Follow a payment from `pay()` through:
180
+
181
+ 1. `_acceptFundsFor()` -- token acceptance (native, ERC-20, Permit2, credit cashout)
182
+ 2. `_route()` -- routing decision (JB token cashout path vs. resolve+convert)
183
+ 3. `_cashOutLoop()` -- recursive cashout when input is a JB project token
184
+ 4. `_resolveTokenOut()` -- discover what the destination project accepts
185
+ 5. `_convert()` -- same-token no-op, wrap/unwrap, or Uniswap swap
186
+ 6. `_handleSwap()` -> `_executeSwap()` -> V3 or V4 execution
187
+ 7. `_beforeTransferFor()` -- allowance setup before forwarding
188
+ 8. `destTerminal.pay()` -- final forwarding
189
+
190
+ **Key question:** Can any combination of inputs cause tokens to be stuck in the router? The contract has no sweep/rescue function and an empty `receive()`.
191
+
192
+ ### Callback Verification
193
+
194
+ - **V3:** `uniswapV3SwapCallback()` reads `fee` from caller, computes expected pool via `FACTORY.getPool()`, checks `msg.sender == expectedPool`. Standard pattern but verify a malicious contract cannot satisfy this check.
195
+ - **V4:** `unlockCallback()` checks `msg.sender == address(POOL_MANAGER)`. The PoolManager's unlock pattern prevents reentrancy by design. Verify no path allows a reentrant call through `POOL_MANAGER.unlock()`.
196
+
197
+ ### Slippage Math
198
+
199
+ The sigmoid formula in `JBSwapLib` is novel. Verify:
200
+ - Floor enforcement: `getSlippageTolerance(0, feeBps)` always returns `>= 200 bps`.
201
+ - Ceiling enforcement: result never exceeds `MAX_SLIPPAGE (8800)`.
202
+ - Monotonicity: higher impact always yields higher tolerance.
203
+ - No overflow: `impact + SIGMOID_K` cannot overflow `uint256`.
204
+ - `sqrtPriceLimitFromAmounts()` correctly handles edge cases (zero amounts, extreme ratios, boundary tick values).
205
+
206
+ ### Registry Trust Model
207
+
208
+ - Owner can `setDefaultTerminal()` and redirect all unlocked projects.
209
+ - Owner can `disallowTerminal()` which clears default if it matches.
210
+ - `lockTerminalFor()` race condition guard: verify the `expectedTerminal` check is correct.
211
+ - Verify `setTerminalFor()` correctly blocks when `hasLockedTerminal[projectId]` is true.
212
+
213
+ ## Invariants
214
+
215
+ These should hold for every transaction and can be used as fuzzing properties:
216
+
217
+ 1. **Zero balance after pay/addToBalanceOf**: `address(routerTerminal).balance == 0` and `IERC20(anyToken).balanceOf(address(routerTerminal)) == 0` after every `pay()` or `addToBalanceOf()` call completes.
218
+
219
+ 2. **No silent truncation**: No code path reaches a `uint160()` cast with a value exceeding `type(uint160).max` without reverting first.
220
+
221
+ 3. **Cashout loop termination**: `_cashOutLoop()` always terminates in at most 20 iterations (revert or return).
222
+
223
+ 4. **Callback caller verification**: `uniswapV3SwapCallback()` only executes token transfers when `msg.sender` is a legitimate V3 pool. `unlockCallback()` only executes when `msg.sender == address(POOL_MANAGER)`.
224
+
225
+ 5. **Locked terminal immutability**: Once `hasLockedTerminal[projectId]` is true, `_terminalOf[projectId]` never changes.
226
+
227
+ 6. **Slippage floor**: `JBSwapLib.getSlippageTolerance(impact, feeBps)` always returns `>= max(feeBps + 100, 200)` (unless the ceiling applies).
228
+
229
+ 7. **TWAP window lower bound**: `_getV3TwapQuote()` reverts when `oldestObservation == 0`.
230
+
231
+ 8. **Leftover refund correctness**: After `_handleSwap()`, the balance delta of the input token (relative to the pre-swap snapshot) is returned to `_msgSender()`. No input tokens remain in the contract.
232
+
233
+ ## Testing Setup
234
+
235
+ ### Running Tests
236
+
237
+ ```bash
238
+ # Unit tests (mocked dependencies, no RPC needed)
239
+ forge test --match-path "test/RouterTerminal.t.sol"
240
+ forge test --match-path "test/RouterTerminalRegistry.t.sol"
241
+ forge test --match-path "test/regression/*.t.sol"
242
+
243
+ # Fork tests (requires Ethereum mainnet RPC)
244
+ # Set RPC_ETHEREUM_MAINNET in .env or foundry.toml
245
+ forge test --match-path "test/RouterTerminalFork.t.sol" --fork-url $RPC_ETHEREUM_MAINNET
246
+ forge test --match-path "test/RouterTerminalSandwichFork.t.sol" --fork-url $RPC_ETHEREUM_MAINNET
247
+ forge test --match-path "test/RouterTerminalFeeCashOutFork.t.sol" --fork-url $RPC_ETHEREUM_MAINNET
248
+
249
+ # All tests
250
+ forge test
251
+ ```
252
+
253
+ ### Foundry Configuration
254
+
255
+ - Solidity 0.8.26, EVM target `cancun`, optimizer 200 runs
256
+ - Fuzz runs: 4,096 per test
257
+ - Invariant runs: 1,024 with depth 100
258
+ - Fork tests pinned to Ethereum mainnet block 21,700,000 (post-V4 deployment)
259
+
260
+ ### Test Coverage Summary
261
+
262
+ | Test File | Type | Coverage Area | Count |
263
+ |-----------|------|---------------|-------|
264
+ | `RouterTerminal.t.sol` | Unit (mocked) | Core routing, swap paths, cashout, V4, pool discovery, TWAP, errors | ~35 |
265
+ | `RouterTerminalRegistry.t.sol` | Unit (mocked) | Allow/disallow, set/lock, forwarding, permissions | ~12 |
266
+ | `RouterTerminalFork.t.sol` | Fork (mainnet) | End-to-end swaps: ETH->USDC, USDC->ETH, ETH->DAI, addToBalance, quote metadata | ~12 |
267
+ | `RouterTerminalFeeCashOutFork.t.sol` | Fork (mainnet) | Fee routing through cashout: project 3 payouts -> fee -> cashout -> project 1 | 1 |
268
+ | `RouterTerminalSandwichFork.t.sol` | Fork (mainnet) | MEV/sandwich: V3 TWAP resistance, V4 spot manipulation, user quote | 4 |
269
+ | `regression/LockTerminalRace.t.sol` | Unit | Race condition in `lockTerminalFor` with `expectedTerminal` | 3 |
270
+ | `regression/CashOutLoopLimit.t.sol` | Unit | Circular cashout loop cap at 20 iterations | 2 |
271
+ | `regression/V4SpotPriceSlippage.t.sol` | Unit + fuzz | Sigmoid math: floor, ceiling, monotonicity, bounded range, user quote | 14 |
272
+
273
+ ### Coverage Gaps Worth Investigating
274
+
275
+ | Area | Status | Why It Matters |
276
+ |------|--------|----------------|
277
+ | Fee-on-transfer tokens | NOT TESTED | Registry `_acceptFundsFor` does not use balance-delta |
278
+ | Short TWAP windows (<60s) | NOT TESTED | Silently degrades to near-spot-price |
279
+ | Multi-hop cashout chains (>1 step) | NOT TESTED | Only the circular case and single-step are tested |
280
+ | V4 pools with custom hooks | NOT TESTED | All V4 tests use `hooks: IHooks(address(0))` |
281
+ | Concurrent pay + addToBalance | NOT TESTED | Mitigated by stateless design but worth verifying |
282
+ | Credit cashout path (fork) | NOT TESTED | Only unit-mocked, no fork test with real credits |
283
+ | addToBalanceOf with cashout routing | PARTIALLY | Fork covers ETH->USDC, not cashout or credit paths |
package/CHANGE_LOG.md ADDED
@@ -0,0 +1,290 @@
1
+ # nana-router-terminal-v6 Changelog (v5 → v6)
2
+
3
+ This document describes all changes between `nana-swap-terminal` (v5) and `nana-router-terminal-v6` (v6).
4
+
5
+ **Note:** This repo was renamed from `nana-swap-terminal` to `nana-router-terminal` in v6.
6
+
7
+ ---
8
+
9
+ ## 1. Breaking Changes
10
+
11
+ ### 1.1 Contract Renamed: `JBSwapTerminal` → `JBRouterTerminal`
12
+ The main contract was renamed from `JBSwapTerminal` (and `JBSwapTerminal5_1`) to `JBRouterTerminal`. All error prefixes, interface names, and references changed accordingly.
13
+
14
+ ### 1.2 No Fixed `TOKEN_OUT` — Dynamic Token Discovery
15
+ - **v5:** The terminal had an immutable `TOKEN_OUT` address set at construction. All incoming tokens were swapped to this single output token. Projects had to explicitly configure pools via `addDefaultPool()`.
16
+ - **v6:** The terminal dynamically discovers what token each destination project accepts by querying `DIRECTORY.primaryTerminalOf()` and iterating the project's terminal accounting contexts. There is no `TOKEN_OUT` immutable. The `_OUT_IS_NATIVE_TOKEN` flag is also removed.
17
+
18
+ ### 1.3 Constructor Parameters Changed
19
+ - **v5:** `constructor(directory, permissions, projects, permit2, owner, weth, tokenOut, factory, trustedForwarder)` — required a fixed `tokenOut` and reverted on `address(0)`.
20
+ - **v6:** `constructor(directory, permissions, projects, tokens, permit2, owner, weth, factory, poolManager, trustedForwarder)` — added `IJBTokens tokens` and `IPoolManager poolManager`; removed `tokenOut`.
21
+
22
+ ### 1.4 `IJBSwapTerminal` Interface Removed, Replaced by `IJBRouterTerminal`
23
+ - **v5 `IJBSwapTerminal`** exposed: `DEFAULT_PROJECT_ID()`, `MAX_TWAP_WINDOW()`, `MIN_TWAP_WINDOW()`, `MIN_DEFAULT_POOL_CARDINALITY()`, `UNCERTAIN_SLIPPAGE_TOLERANCE()`, `SLIPPAGE_DENOMINATOR()`, `twapWindowOf()`, `addDefaultPool()`, `addTwapParamsFor()`.
24
+ - **v6 `IJBRouterTerminal`** exposes only: `discoverBestPool()`, `discoverPool()`.
25
+ - All pool/TWAP configuration functions are removed (see section 1.5).
26
+
27
+ ### 1.5 Removed: Per-Project Pool Configuration and TWAP Management
28
+ The following functions and storage were removed entirely in v6:
29
+ - `addDefaultPool(uint256 projectId, address token, IUniswapV3Pool pool)` — projects no longer manually configure pools.
30
+ - `addTwapParamsFor(uint256 projectId, IUniswapV3Pool pool, uint256 secondsAgo)` — TWAP windows are no longer project-configurable.
31
+ - `getPoolFor(uint256 projectId, address tokenIn)` — pool lookup is now fully automatic.
32
+ - `twapWindowOf(uint256 projectId, IUniswapV3Pool pool)` — replaced by a fixed `DEFAULT_TWAP_WINDOW = 10 minutes`.
33
+ - Storage mappings `_poolFor`, `_accountingContextFor`, `_tokensWithAContext`, `_twapWindowOf` — all removed.
34
+
35
+ ### 1.6 Removed: Constants
36
+ The following public constants from v5 were removed:
37
+ - `DEFAULT_PROJECT_ID` (was `0`)
38
+ - `MAX_TWAP_WINDOW` (was `2 days`)
39
+ - `MIN_TWAP_WINDOW` (was `2 minutes`)
40
+ - `MIN_DEFAULT_POOL_CARDINALITY` (was `10`)
41
+ - `UNCERTAIN_SLIPPAGE_TOLERANCE` (was `1050`)
42
+
43
+ The `SLIPPAGE_DENOMINATOR` constant was kept but changed from `uint160` to `uint256`.
44
+
45
+ ### 1.7 `supportsInterface` Changed
46
+ - **v5:** Reported support for `IJBTerminal`, `IJBPermitTerminal`, `IERC165`, `IUniswapV3SwapCallback`, `IJBPermissioned`, `IJBSwapTerminal`.
47
+ - **v6:** Reports support for `IJBTerminal`, `IJBPermitTerminal`, `IERC165`, `IJBPermissioned` only. No longer advertises `IUniswapV3SwapCallback` or the custom interface.
48
+
49
+ ### 1.8 Permission ID Changed (Registry)
50
+ - **v5:** `lockTerminalFor()` and `setTerminalFor()` used `JBPermissionIds.ADD_SWAP_TERMINAL_POOL`.
51
+ - **v6:** These functions use `JBPermissionIds.SET_ROUTER_TERMINAL`.
52
+
53
+ ### 1.9 `lockTerminalFor` Signature Changed (Registry)
54
+ - **v5:** `lockTerminalFor(uint256 projectId)` — no confirmation of which terminal is being locked.
55
+ - **v6:** `lockTerminalFor(uint256 projectId, IJBTerminal expectedTerminal)` — requires the caller to confirm the expected terminal, preventing race conditions where the default changes between transaction submission and execution.
56
+
57
+ ### 1.10 Accounting Contexts Are Now Dynamic
58
+ - **v5:** `accountingContextForTokenOf()` looked up stored contexts from `_accountingContextFor` mappings (set via `addDefaultPool`). `accountingContextsOf()` merged project-specific and default-project contexts.
59
+ - **v6:** `accountingContextForTokenOf()` is `pure` and returns a synthetic context with 18 decimals for any token. `accountingContextsOf()` returns an empty array since the terminal accepts any token dynamically.
60
+
61
+ ### 1.11 Solidity Version
62
+ - **v5:** `pragma solidity 0.8.23`
63
+ - **v6:** `pragma solidity 0.8.26`
64
+
65
+ ---
66
+
67
+ ## 2. New Features
68
+
69
+ ### 2.1 Uniswap V4 Support
70
+ The terminal now supports swapping via both Uniswap V3 and V4 pools. Pool discovery (`_discoverPool`) searches across all V3 fee tiers and V4 fee/tickSpacing combinations, selecting whichever pool has the highest in-range liquidity.
71
+
72
+ New V4-specific components:
73
+ - `IPoolManager POOL_MANAGER` immutable (can be `address(0)` if V4 is unavailable).
74
+ - `IUnlockCallback` interface implemented via `unlockCallback()`.
75
+ - `_executeV4Swap()`, `_settleV4()`, `_takeV4()`, `_discoverV4Pool()` internal functions.
76
+ - `_getV4SpotQuote()` — uses instantaneous spot price with sigmoid slippage (security note: not MEV-resistant; users should provide `quoteForSwap` metadata).
77
+ - `_V4_FEES` and `_V4_TICK_SPACINGS` arrays for vanilla V4 pool search.
78
+
79
+ ### 2.2 Automatic Pool Discovery
80
+ - **v5:** Pools had to be manually registered per project via `addDefaultPool()`.
81
+ - **v6:** Pools are auto-discovered at swap time by scanning all V3 fee tiers (3000, 500, 10000, 100 bps) and V4 pools, selecting the one with the highest liquidity. No manual configuration needed.
82
+
83
+ ### 2.3 Automatic Token Route Discovery
84
+ New `_resolveTokenOut()` function determines what token a destination project accepts, with the following priority:
85
+ 1. Metadata override (`routeTokenOut` key).
86
+ 2. Direct acceptance (project accepts `tokenIn`).
87
+ 3. NATIVE/WETH equivalence check.
88
+ 4. Dynamic discovery — iterate all project terminals and their accounting contexts, find the accepted token with the best Uniswap pool against `tokenIn`.
89
+
90
+ ### 2.4 JB Token Cash Out Routing
91
+ New `_cashOutLoop()` function enables recursive cashout of JB project tokens:
92
+ - If `tokenIn` is a JB project token (ERC-20 or credit), the terminal can cash it out from the source project's terminal, then recursively process the reclaimed token.
93
+ - Up to `_MAX_CASHOUT_ITERATIONS = 20` iterations to prevent infinite loops.
94
+ - Supports `cashOutSource` metadata key for credit-based cashouts.
95
+ - Supports `cashOutMinReclaimed` metadata key for slippage protection on the first cashout step.
96
+
97
+ ### 2.5 Credit Transfer Support
98
+ `_acceptFundsFor()` now checks for `cashOutSource` metadata. If present, instead of transferring ERC-20 tokens, it pulls JB token credits from the payer via `TOKENS.transferCreditsFrom()`.
99
+
100
+ ### 2.6 New `IJBTokens TOKENS` Immutable
101
+ The terminal now has a reference to the `IJBTokens` contract for looking up project IDs from token addresses and transferring credits.
102
+
103
+ ### 2.7 `PoolInfo` Struct (New File)
104
+ New struct `src/structs/PoolInfo.sol` that represents either a V3 or V4 pool:
105
+ ```solidity
106
+ struct PoolInfo {
107
+ bool isV4;
108
+ IUniswapV3Pool v3Pool;
109
+ PoolKey v4Key;
110
+ }
111
+ ```
112
+
113
+ ### 2.8 `JBSwapLib` Library (New File)
114
+ New library `src/libraries/JBSwapLib.sol` containing:
115
+ - `getSlippageTolerance(impact, poolFeeBps)` — continuous sigmoid formula replacing v5's stepped if/else brackets. Returns slippage in basis points with a minimum of `poolFee + 1%` (floor 2%) and a ceiling of 88%.
116
+ - `calculateImpact(amountIn, liquidity, sqrtP, zeroForOne)` — estimates price impact at 1e18 precision.
117
+ - `sqrtPriceLimitFromAmounts(amountIn, minimumAmountOut, zeroForOne)` — computes a `sqrtPriceLimitX96` for partial-fill protection (V3 and V4). This replaces v5's approach of using extreme price limits (`MIN_SQRT_RATIO + 1` / `MAX_SQRT_RATIO - 1`).
118
+
119
+ ### 2.9 `discoverBestPool()` and `discoverPool()` Public Views
120
+ New external view functions on `IJBRouterTerminal` for off-chain queries:
121
+ - `discoverBestPool(tokenIn, tokenOut)` — returns a `PoolInfo` (V3 or V4).
122
+ - `discoverPool(tokenIn, tokenOut)` — returns only the V3 pool (backwards-compatible helper).
123
+
124
+ ### 2.10 `Permit2AllowanceFailed` Event
125
+ In v6, when a Permit2 allowance call fails during `_acceptFundsFor()`, an event `Permit2AllowanceFailed(token, owner, reason)` is emitted (inherited from `IJBPermitTerminal`), and the payment continues using fallback transfer. In v5, the failure was silently swallowed with an empty `catch`.
126
+
127
+ ### 2.11 Fee-on-Transfer Token Handling
128
+ - **v5 `_acceptFundsFor`:** Returned `IERC20(token).balanceOf(address(this))` after transfer — would include any pre-existing balance.
129
+ - **v6 `_acceptFundsFor`:** Uses balance-delta pattern (`balanceAfter - balanceBefore`) to accurately measure tokens received.
130
+
131
+ ### 2.12 Partial-Fill Leftover Handling via Balance Delta
132
+ - **v5 `_handleTokenTransfersAndSwap`:** Measured leftovers as the full `balanceOf(normalizedTokenIn)` — could include pre-existing balances.
133
+ - **v6 `_handleSwap`:** Snapshots `balanceBefore` and uses `balanceAfter - balanceBefore` for accurate leftover calculation.
134
+
135
+ ### 2.13 `PERMIT2` Exposed on `IJBRouterTerminalRegistry` Interface
136
+ - **v5:** `PERMIT2` was an immutable on the registry contract but not exposed on the `IJBSwapTerminalRegistry` interface.
137
+ - **v6:** `PERMIT2()` is declared on the `IJBRouterTerminalRegistry` interface.
138
+
139
+ ---
140
+
141
+ ## 3. Event Changes
142
+
143
+ ### 3.1 Registry Events Renamed
144
+ All events were renamed from `JBSwapTerminalRegistry_*` to `JBRouterTerminalRegistry_*`.
145
+
146
+ ### 3.2 Registry Events Now Include `caller`
147
+ All registry events now include an `address caller` parameter:
148
+ | v5 | v6 |
149
+ |---|---|
150
+ | `JBSwapTerminalRegistry_AllowTerminal(IJBTerminal terminal)` | `JBRouterTerminalRegistry_AllowTerminal(IJBTerminal terminal, address caller)` |
151
+ | `JBSwapTerminalRegistry_DisallowTerminal(IJBTerminal terminal)` | `JBRouterTerminalRegistry_DisallowTerminal(IJBTerminal terminal, address caller)` |
152
+ | `JBSwapTerminalRegistry_LockTerminal(uint256 projectId)` | `JBRouterTerminalRegistry_LockTerminal(uint256 indexed projectId, address caller)` |
153
+ | `JBSwapTerminalRegistry_SetDefaultTerminal(IJBTerminal terminal)` | `JBRouterTerminalRegistry_SetDefaultTerminal(IJBTerminal terminal, address caller)` |
154
+ | `JBSwapTerminalRegistry_SetTerminal(uint256 indexed projectId, IJBTerminal terminal)` | `JBRouterTerminalRegistry_SetTerminal(uint256 indexed projectId, IJBTerminal terminal, address caller)` |
155
+
156
+ ### 3.3 New Event on Main Terminal
157
+ - `Permit2AllowanceFailed(address indexed token, address indexed owner, bytes reason)` — emitted when Permit2 allowance fails (from `IJBPermitTerminal`).
158
+
159
+ ---
160
+
161
+ ## 4. Error Changes
162
+
163
+ ### 4.1 Main Terminal Errors Renamed and Restructured
164
+ | v5 Error | v6 Error | Notes |
165
+ |---|---|---|
166
+ | `JBSwapTerminal_CallerNotPool(address)` | `JBRouterTerminal_CallerNotPool(address)` | Renamed |
167
+ | `JBSwapTerminal_InvalidTwapWindow(uint256, uint256, uint256)` | *Removed* | TWAP windows are no longer configurable |
168
+ | `JBSwapTerminal_SpecifiedSlippageExceeded(uint256, uint256)` | `JBRouterTerminal_SlippageExceeded(uint256 amountOut, uint256 minAmountOut)` | Renamed |
169
+ | `JBSwapTerminal_NoDefaultPoolDefined(uint256, address)` | *Removed* | Pools are auto-discovered |
170
+ | `JBSwapTerminal_NoMsgValueAllowed(uint256)` | `JBRouterTerminal_NoMsgValueAllowed(uint256)` | Renamed |
171
+ | `JBSwapTerminal_PermitAllowanceNotEnough(uint256, uint256)` | `JBRouterTerminal_PermitAllowanceNotEnough(uint256, uint256)` | Renamed |
172
+ | `JBSwapTerminal_TokenNotAccepted(uint256, address)` | `JBRouterTerminal_TokenNotAccepted(uint256, address)` | Renamed |
173
+ | `JBSwapTerminal_UnexpectedCall(address)` | *Removed* | `receive()` is now unrestricted |
174
+ | `JBSwapTerminal_WrongPool(address, address)` | *Removed* | No manual pool registration |
175
+ | `JBSwapTerminal_ZeroToken()` | *Removed* | No fixed `tokenOut` constructor param |
176
+ | *N/A* | `JBRouterTerminal_AmountOverflow(uint256)` | New — guards `uint160` cast in `_transferFrom` and `uint128` cast in TWAP quote |
177
+ | *N/A* | `JBRouterTerminal_CallerNotPoolManager(address)` | New — V4 callback verification |
178
+ | *N/A* | `JBRouterTerminal_CashOutLoopLimit()` | New — exceeded `_MAX_CASHOUT_ITERATIONS` |
179
+ | *N/A* | `JBRouterTerminal_NoCashOutPath(uint256, uint256)` | New — no cashout terminal found |
180
+ | *N/A* | `JBRouterTerminal_NoLiquidity()` | New — pool has zero liquidity |
181
+ | *N/A* | `JBRouterTerminal_NoObservationHistory()` | New — V3 pool has no observation history for TWAP |
182
+ | *N/A* | `JBRouterTerminal_NoPoolFound(address, address)` | New — no V3 or V4 pool exists |
183
+ | *N/A* | `JBRouterTerminal_NoRouteFound(uint256, address)` | New — no accepted token found for project |
184
+
185
+ ### 4.2 Registry Errors Renamed and New
186
+ | v5 Error | v6 Error | Notes |
187
+ |---|---|---|
188
+ | `JBSwapTerminalRegistry_NoMsgValueAllowed(uint256)` | `JBRouterTerminalRegistry_NoMsgValueAllowed(uint256)` | Renamed |
189
+ | `JBSwapTerminalRegistry_PermitAllowanceNotEnough(uint256, uint256)` | `JBRouterTerminalRegistry_PermitAllowanceNotEnough(uint256, uint256)` | Renamed |
190
+ | `JBSwapTerminalRegistry_TerminalLocked(uint256)` | `JBRouterTerminalRegistry_TerminalLocked(uint256)` | Renamed |
191
+ | `JBSwapTerminalRegistry_TerminalNotAllowed(IJBTerminal)` | `JBRouterTerminalRegistry_TerminalNotAllowed(IJBTerminal)` | Renamed |
192
+ | `JBSwapTerminalRegistry_TerminalNotSet(uint256)` | `JBRouterTerminalRegistry_TerminalNotSet(uint256)` | Renamed |
193
+ | *N/A* | `JBRouterTerminalRegistry_AmountOverflow()` | New — guards `uint160` cast |
194
+ | *N/A* | `JBRouterTerminalRegistry_TerminalMismatch(IJBTerminal, IJBTerminal)` | New — `lockTerminalFor` safety check |
195
+ | *N/A* | `JBRouterTerminalRegistry_ZeroAddress()` | New — prevents setting `address(0)` as default terminal |
196
+
197
+ ---
198
+
199
+ ## 5. Struct Changes
200
+
201
+ ### 5.1 New: `PoolInfo` (`src/structs/PoolInfo.sol`)
202
+ ```solidity
203
+ struct PoolInfo {
204
+ bool isV4;
205
+ IUniswapV3Pool v3Pool;
206
+ PoolKey v4Key;
207
+ }
208
+ ```
209
+ Represents either a Uniswap V3 or V4 pool. Used throughout the v6 pool discovery and swap execution flow.
210
+
211
+ ---
212
+
213
+ ## 6. Implementation Changes (Non-Interface)
214
+
215
+ ### 6.1 Slippage Tolerance: Stepped → Continuous Sigmoid
216
+ - **v5:** Used a series of `if/else` brackets to map impact ranges to slippage tolerances (9 discrete brackets). Impact precision was `10 * SLIPPAGE_DENOMINATOR` (1e5).
217
+ - **v6:** Uses a continuous sigmoid formula in `JBSwapLib.getSlippageTolerance()`: `minSlippage + (MAX_SLIPPAGE - minSlippage) * impact / (impact + K)`. Impact precision is 1e18. Minimum slippage is `poolFee + 1%` with a 2% floor. Maximum is 88%.
218
+
219
+ ### 6.2 `sqrtPriceLimitX96` Computation
220
+ - **v5 `_swap`:** Always used extreme price limits (`MIN_SQRT_RATIO + 1` or `MAX_SQRT_RATIO - 1`), providing no partial-fill protection.
221
+ - **v6 `_executeV3Swap` / `_executeV4Swap`:** Uses `JBSwapLib.sqrtPriceLimitFromAmounts()` to compute a meaningful price limit from `amountIn` and `minAmountOut`, providing partial-fill protection so the swap stops if execution price worsens beyond the minimum acceptable rate.
222
+
223
+ ### 6.3 `uniswapV3SwapCallback` Pool Verification
224
+ - **v5:** Verified the caller by looking up the stored pool from `_poolFor[projectId][normalizedTokenIn]` and comparing `msg.sender` against it.
225
+ - **v6:** Verifies the caller by querying the factory directly with the callback data's `tokenIn`/`tokenOut` and the pool's `fee()`. This is necessary because pools are auto-discovered rather than stored.
226
+
227
+ ### 6.4 `uniswapV3SwapCallback` Data Format
228
+ - **v5:** Callback data was `abi.encode(projectId, tokenIn)`.
229
+ - **v6:** Callback data is `abi.encode(projectId, tokenIn, tokenOut)` — includes `tokenOut` for factory verification.
230
+
231
+ ### 6.5 `receive()` Function
232
+ - **v5:** Restricted to only accept ETH from the `WETH` contract, reverting with `JBSwapTerminal_UnexpectedCall` otherwise.
233
+ - **v6:** Unrestricted — accepts ETH from cashout reclaims, WETH unwraps, and V4 PoolManager takes.
234
+
235
+ ### 6.6 `_acceptFundsFor` Uses `_msgSender()` Consistently
236
+ - **v5:** Mixed `msg.sender` and `_msgSender()` — used `msg.sender` in the Permit2 permit call and `_transferFrom`.
237
+ - **v6:** Uses `_msgSender()` consistently throughout, respecting ERC-2771 meta-transactions.
238
+
239
+ ### 6.7 `pay()` and `addToBalanceOf()` Routing
240
+ - **v5:** Both functions looked up `DIRECTORY.primaryTerminalOf(projectId, TOKEN_OUT)` for a fixed output token, then swapped via `_handleTokenTransfersAndSwap()`.
241
+ - **v6:** Both functions call `_route()` which handles the full routing pipeline: credit detection, JB token cashout, token resolution, and conversion (direct/wrap/swap).
242
+
243
+ ### 6.8 `_beforeTransferFor` Simplified
244
+ - **v5:** Checked the `_OUT_IS_NATIVE_TOKEN` flag.
245
+ - **v6:** Checks `token == JBConstants.NATIVE_TOKEN` directly (since there's no fixed output token).
246
+
247
+ ### 6.9 `_transferFrom` Amount Overflow Guard
248
+ - **v5:** Cast `amount` to `uint160` unchecked when calling `PERMIT2.transferFrom`.
249
+ - **v6:** Checks `amount > type(uint160).max` and reverts with `JBRouterTerminal_AmountOverflow` before the cast.
250
+
251
+ ### 6.10 Registry `terminalOf` Storage Encapsulation
252
+ - **v5:** `terminalOf` was a `public` mapping directly on the interface.
253
+ - **v6:** The storage mapping is `internal _terminalOf`, with a public `terminalOf(projectId)` view function that applies the default fallback. This prevents direct mapping access that would bypass the fallback logic.
254
+
255
+ ### 6.11 Registry `disallowTerminal` Clears Default
256
+ - **v5:** `disallowTerminal()` only set `isTerminalAllowed[terminal] = false`.
257
+ - **v6:** Additionally clears `defaultTerminal` if it matches the terminal being disallowed.
258
+
259
+ ### 6.12 Registry `setDefaultTerminal` Zero Address Check
260
+ - **v5:** No validation on the terminal address.
261
+ - **v6:** Reverts with `JBRouterTerminalRegistry_ZeroAddress()` if `address(terminal) == address(0)`.
262
+
263
+ ### 6.13 Removed: `JBSwapTerminal5_1`
264
+ v5 contained both `JBSwapTerminal.sol` and `JBSwapTerminal5_1.sol` (a minor revision). v6 has a single `JBRouterTerminal.sol`.
265
+
266
+ ### 6.14 `_pickPoolAndQuote` Redesigned
267
+ - **v5:** Looked up stored pools from `_poolFor` mappings. If no user quote was provided, used project-specific TWAP windows with fallback to slot0 for pools with no observations.
268
+ - **v6:** Auto-discovers pools via `_discoverPool()`. If no user quote is provided, dispatches to `_getV3TwapQuote()` (for V3 pools, using a fixed 10-minute TWAP window) or `_getV4SpotQuote()` (for V4 pools, using spot price). Reverts with `JBRouterTerminal_NoPoolFound` if no pool exists (v5 reverted with `JBSwapTerminal_NoDefaultPoolDefined`).
269
+
270
+ ### 6.15 New Metadata Keys
271
+ - `cashOutSource` — specifies a source project ID and credit amount for credit-based cashouts.
272
+ - `cashOutMinReclaimed` — minimum tokens to reclaim from the first cashout step.
273
+ - `routeTokenOut` — payer-specified output token override.
274
+ - `quoteForSwap` — retained from v5 (user-provided minimum output quote).
275
+ - `permit2` — retained from v5.
276
+
277
+ ---
278
+
279
+ ## 7. Migration Table
280
+
281
+ | v5 File | v6 File | Status |
282
+ |---|---|---|
283
+ | `src/JBSwapTerminal.sol` | `src/JBRouterTerminal.sol` | **Renamed + Rewritten** |
284
+ | `src/JBSwapTerminal5_1.sol` | *N/A* | **Removed** (consolidated into `JBRouterTerminal`) |
285
+ | `src/JBSwapTerminalRegistry.sol` | `src/JBRouterTerminalRegistry.sol` | **Renamed + Updated** |
286
+ | `src/interfaces/IJBSwapTerminal.sol` | `src/interfaces/IJBRouterTerminal.sol` | **Renamed + Rewritten** (entirely different surface) |
287
+ | `src/interfaces/IJBSwapTerminalRegistry.sol` | `src/interfaces/IJBRouterTerminalRegistry.sol` | **Renamed + Updated** (added `PERMIT2()`, `caller` on events, new `lockTerminalFor` sig) |
288
+ | `src/interfaces/IWETH9.sol` | `src/interfaces/IWETH9.sol` | **Unchanged** (import path updated to OZ) |
289
+ | *N/A* | `src/structs/PoolInfo.sol` | **New** |
290
+ | *N/A* | `src/libraries/JBSwapLib.sol` | **New** |