@bananapus/router-terminal-v6 0.0.24 → 0.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/ADMINISTRATION.md +35 -24
  2. package/ARCHITECTURE.md +51 -99
  3. package/AUDIT_INSTRUCTIONS.md +61 -336
  4. package/CHANGELOG.md +59 -0
  5. package/README.md +78 -186
  6. package/RISKS.md +36 -7
  7. package/SKILLS.md +28 -260
  8. package/STYLE_GUIDE.md +56 -17
  9. package/USER_JOURNEYS.md +63 -475
  10. package/package.json +3 -3
  11. package/references/operations.md +29 -0
  12. package/references/runtime.md +32 -0
  13. package/script/Deploy.s.sol +88 -15
  14. package/script/helpers/RouterTerminalDeploymentLib.sol +27 -8
  15. package/src/JBPayRouteResolver.sol +888 -0
  16. package/src/JBRouterTerminal.sol +1024 -378
  17. package/src/JBRouterTerminalRegistry.sol +77 -35
  18. package/src/interfaces/IJBForwardingTerminal.sol +13 -0
  19. package/src/interfaces/IJBPayRoutePreviewer.sol +100 -0
  20. package/src/interfaces/IJBPayRouteResolver.sol +114 -0
  21. package/src/interfaces/IJBPayerTracker.sol +2 -2
  22. package/src/interfaces/IJBRouterTerminal.sol +2 -2
  23. package/src/interfaces/IJBRouterTerminalRegistry.sol +14 -13
  24. package/src/interfaces/IWETH9.sol +3 -1
  25. package/src/libraries/JBSwapLib.sol +10 -2
  26. package/src/structs/CashOutPathCandidates.sol +19 -0
  27. package/test/RouterTerminal.t.sol +712 -52
  28. package/test/RouterTerminalBuybackHookFork.t.sol +402 -0
  29. package/test/RouterTerminalCashOutFork.t.sol +2 -2
  30. package/test/RouterTerminalCreditCashout.t.sol +650 -419
  31. package/test/RouterTerminalERC2771.t.sol +98 -3
  32. package/test/RouterTerminalFeeCashOutFork.t.sol +2 -2
  33. package/test/RouterTerminalFork.t.sol +2 -2
  34. package/test/RouterTerminalMultihopFork.t.sol +2 -2
  35. package/test/RouterTerminalPreviewFork.t.sol +4 -3
  36. package/test/RouterTerminalReentrancy.t.sol +16 -18
  37. package/test/RouterTerminalSandwichFork.t.sol +2 -2
  38. package/test/TestAuditGaps.sol +84 -20
  39. package/test/audit/DeployBuybackHookZero.t.sol +419 -0
  40. package/test/audit/LeftoverRefund.t.sol +100 -19
  41. package/test/audit/PayerTrackerRefund.t.sol +9 -3
  42. package/test/audit/Permit2AllowanceFailed.t.sol +37 -10
  43. package/test/audit/PreviewPrimaryTerminalMismatch.t.sol +239 -0
  44. package/test/audit/RefundToBeneficiary.t.sol +3 -4
  45. package/test/audit/RegistryAddToBalancePartialFill.t.sol +9 -1
  46. package/test/codex/CashOutCircularPrimaryTerminal.t.sol +326 -0
  47. package/test/codex/CashOutFallbackPrefersRecursiveLoop.t.sol +363 -0
  48. package/test/codex/RegistrySelfLockDoS.t.sol +72 -0
  49. package/test/codex/RouterRegistryReceiptMismatch.t.sol +207 -0
  50. package/test/codex/V4HookedPoolIgnored.t.sol +101 -0
  51. package/test/codex/V4WethInputUsesStuckEth.t.sol +330 -0
  52. package/test/fork/V4QuoteAndSettlementFork.t.sol +2 -2
  53. package/test/invariant/RouterTerminalInvariant.t.sol +2 -3
  54. package/test/regression/CashOutLoopLimit.t.sol +10 -2
  55. package/test/regression/RouterTerminalEdgeCases.t.sol +9 -1
  56. package/CHANGE_LOG.md +0 -316
package/ADMINISTRATION.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  Admin privileges and their scope in nana-router-terminal-v6.
4
4
 
5
+ ## At A Glance
6
+
7
+ | Item | Details |
8
+ |------|---------|
9
+ | Scope | Registry-level selection of router terminal implementations plus immutable router-terminal swap and forwarding behavior. |
10
+ | Operators | Registry owner for the global allowlist/default, project owners or `SET_ROUTER_TERMINAL` delegates for per-project selection, and users who must supply valid routing metadata. |
11
+ | Highest-risk actions | Locking a project to the wrong terminal, changing the default terminal without understanding who inherits it, or assuming the router is an accounting-truth surface when it is not. |
12
+ | Recovery posture | Unlocked projects can switch terminals. Locked projects keep the stored terminal choice, so recovery requires moving the project to a different admin path outside the registry. |
13
+
14
+ ## Routine Operations
15
+
16
+ - Keep the registry allowlist limited to router terminal implementations you actually want projects to choose from.
17
+ - Change the default terminal only when you are comfortable affecting every project that still relies on fallback resolution.
18
+ - Encourage projects to lock their terminal only after verifying the resolved terminal address and expected routing behavior.
19
+ - For credit-cashout routing, verify the payer has granted the router terminal the needed `TRANSFER_CREDITS` permission before relying on that path.
20
+
21
+ ## One-Way Or High-Risk Actions
22
+
23
+ - `lockTerminalFor` is irreversible.
24
+ - The current default terminal cannot be disallowed; the registry owner must move the default first before removing the old implementation from the allowlist.
25
+
26
+ ## Recovery Notes
27
+
28
+ - If the default terminal is wrong, update the registry quickly before more projects snapshot it through `lockTerminalFor`.
29
+ - If a project already locked the wrong terminal, the registry cannot unlock it; recovery has to happen by migrating the project's broader terminal setup elsewhere.
30
+
5
31
  ## Roles
6
32
 
7
33
  ### 1. Registry Owner (Ownable)
@@ -13,18 +39,10 @@ Admin privileges and their scope in nana-router-terminal-v6.
13
39
  ### 2. Project Owner / SET_ROUTER_TERMINAL Delegate
14
40
 
15
41
  **Contract**: `JBRouterTerminalRegistry`
16
- **Assigned via**: Ownership of the project's ERC-721 NFT (via `JBProjects.ownerOf(projectId)`), or delegation via `JBPermissions` with permission ID `SET_ROUTER_TERMINAL` (29).
42
+ **Assigned via**: Ownership of the project's ERC-721 NFT (via `JBProjects.ownerOf(projectId)`), or delegation via `JBPermissions` with permission ID `SET_ROUTER_TERMINAL` (30).
17
43
  **Scope**: Per-project -- controls which router terminal a specific project uses, and can permanently lock that choice.
18
44
 
19
- ### 3. Router Terminal Owner (Ownable)
20
-
21
- **Contract**: `JBRouterTerminal`
22
- **Assigned via**: Constructor parameter `owner`, transferable via `Ownable.transferOwnership()`.
23
- **Scope**: Currently unused. `JBRouterTerminal` inherits `Ownable` but does not gate any functions behind `onlyOwner`. The owner exists for potential future use or subclass extensions. The `Ownable.renounceOwnership()` and `Ownable.transferOwnership()` functions are inherited but have no practical effect on the terminal's operation.
24
-
25
- **Risk note:** While the owner has no current powers over the terminal's operation, the `Ownable` inheritance means a future code change or subclass could introduce `onlyOwner`-gated functions. If the terminal is deployed with a specific owner address, that address retains transfer rights indefinitely.
26
-
27
- ### 4. Credit Cashout Payer (Implicit)
45
+ ### 3. Credit Cashout Payer (Implicit)
28
46
 
29
47
  **Contract**: `JBRouterTerminal`
30
48
  **Required permission**: `TRANSFER_CREDITS` (permission ID 13) -- must be granted by the payer to the router terminal address for the source project via `JBPermissions`.
@@ -40,7 +58,7 @@ When a payment is forwarded through the registry, the terminal is resolved as fo
40
58
 
41
59
  **Lock semantics:** When `lockTerminalFor()` is called on a project with no explicit terminal, the current default is snapshot into `_terminalOf[projectId]` before locking. The project becomes independent of future default changes.
42
60
 
43
- **Disallow interaction:** If the registry owner calls `disallowTerminal()` on the current default terminal, the `defaultTerminal` is automatically cleared (set to `address(0)`). Projects relying on the default (without locking) would lose their terminal resolution until a new default is set. Projects should lock their terminal to avoid disruption.
61
+ **Disallow interaction:** The registry owner cannot disallow the current default terminal. `disallowTerminal()` reverts with `JBRouterTerminalRegistry_CannotDisallowDefaultTerminal` until `setDefaultTerminal()` has moved the default elsewhere first. Projects relying on the default therefore keep a valid fallback terminal unless the owner explicitly changes that default.
44
62
 
45
63
  ## Privileged Functions
46
64
 
@@ -49,17 +67,10 @@ When a payment is forwarded through the registry, the terminal is resolved as fo
49
67
  | Function | Required Role | Permission ID | Scope | What It Does |
50
68
  |----------|--------------|---------------|-------|--------------|
51
69
  | `allowTerminal(terminal)` | Registry Owner | `onlyOwner` | Global | Adds a terminal to the allowlist (`isTerminalAllowed[terminal] = true`). Projects can only use allowlisted terminals. |
52
- | `disallowTerminal(terminal)` | Registry Owner | `onlyOwner` | Global | Removes a terminal from the allowlist. Also clears `defaultTerminal` if it matches the disallowed terminal. Does NOT affect projects that have already locked their terminal. |
70
+ | `disallowTerminal(terminal)` | Registry Owner | `onlyOwner` | Global | Removes a terminal from the allowlist. Reverts if `terminal` is the current `defaultTerminal`, so the owner must move the default first. Does NOT affect projects that have already locked their terminal or explicitly set another terminal. |
53
71
  | `setDefaultTerminal(terminal)` | Registry Owner | `onlyOwner` | Global | Sets the default terminal for all projects that have not set a project-specific terminal. Also auto-allows the terminal. |
54
- | `setTerminalFor(projectId, terminal)` | Project Owner or Delegate | `SET_ROUTER_TERMINAL` (29) | Per-project | Routes a specific project to a specific allowed terminal. Reverts if the terminal is not allowlisted or if the project's terminal is locked. |
55
- | `lockTerminalFor(projectId, expectedTerminal)` | Project Owner or Delegate | `SET_ROUTER_TERMINAL` (29) | Per-project | Permanently locks the terminal choice for a project. If no explicit terminal is set, snapshots the current default into `_terminalOf[projectId]`. Reverts with `TerminalMismatch` if the resolved terminal differs from `expectedTerminal` (race condition protection). **Irreversible.** |
56
-
57
- ### JBRouterTerminal
58
-
59
- | Function | Required Role | Permission ID | Scope | What It Does |
60
- |----------|--------------|---------------|-------|--------------|
61
- | `transferOwnership(newOwner)` | Owner | `onlyOwner` (inherited) | Global | Transfers contract ownership. No functions currently gated by ownership. |
62
- | `renounceOwnership()` | Owner | `onlyOwner` (inherited) | Global | Renounces contract ownership. No functions currently gated by ownership. |
72
+ | `setTerminalFor(projectId, terminal)` | Project Owner or Delegate | `SET_ROUTER_TERMINAL` (30) | Per-project | Routes a specific project to a specific allowed terminal. Reverts if the terminal is not allowlisted or if the project's terminal is locked. |
73
+ | `lockTerminalFor(projectId, expectedTerminal)` | Project Owner or Delegate | `SET_ROUTER_TERMINAL` (30) | Per-project | Permanently locks the terminal choice for a project. If no explicit terminal is set, snapshots the current default into `_terminalOf[projectId]`. Reverts with `TerminalMismatch` if the resolved terminal differs from `expectedTerminal` (race condition protection). **Irreversible.** |
63
74
 
64
75
  ### Implicit Permission Requirements (not `onlyOwner`, but enforced by external contracts)
65
76
 
@@ -76,15 +87,15 @@ The following values are set at deploy time and cannot be changed:
76
87
 
77
88
  | Property | Set At | Mutable? | Description |
78
89
  |----------|--------|----------|-------------|
79
- | `PERMISSIONS` | Constructor | No | JB permissions registry for permission checks |
80
90
  | `Trusted forwarder` | Constructor | No | ERC-2771 trusted forwarder for meta-transactions |
81
91
  | `DIRECTORY` | Constructor | No | JB directory for terminal/controller lookups |
82
- | `PROJECTS` | Constructor | No | JB project NFT registry |
83
92
  | `TOKENS` | Constructor | No | JB token manager for credit transfers and project token lookups |
84
93
  | `FACTORY` | Constructor | No | Uniswap V3 factory for pool discovery and callback verification |
85
94
  | `POOL_MANAGER` | Constructor | No | Uniswap V4 PoolManager (can be `address(0)` to disable V4) |
86
95
  | `PERMIT2` | Constructor | No | Permit2 contract for gasless approvals |
87
96
  | `WETH` | Constructor | No | Wrapped ETH contract |
97
+ | `BUYBACK_HOOK` | Constructor | No | Canonical buyback hook whose metadata this router understands |
98
+ | `UNIV4_HOOK` | Constructor | No | Canonical Uniswap V4 hook address searched during hooked-pool discovery |
88
99
  | `DEFAULT_TWAP_WINDOW` | Compile-time constant | No | 10 minutes (600 seconds) |
89
100
  | `SLIPPAGE_DENOMINATOR` | Compile-time constant | No | 10,000 (basis points) |
90
101
  | `_FEE_TIERS` | Storage (initialized) | No | `[3000, 500, 10000, 100]` -- V3 fee tiers |
@@ -120,7 +131,7 @@ What admins **cannot** do:
120
131
  - **Modify swap parameters, slippage, or routing logic.** These are controlled by the `JBRouterTerminal` contract, not the registry.
121
132
  - **Pause payments.** There is no pause mechanism.
122
133
 
123
- ### Router Terminal Owner Cannot:
134
+ ### Router Terminal Maintainers Cannot:
124
135
  - **Modify swap slippage parameters.** The TWAP window, sigmoid constants, fee tiers, and max slippage are all immutable.
125
136
  - **Redirect funds.** The terminal is stateless between transactions and routes payments to whichever terminal the JB directory specifies.
126
137
  - **Change the Uniswap factory or PoolManager.** These are immutable constructor parameters.
package/ARCHITECTURE.md CHANGED
@@ -1,116 +1,68 @@
1
- # nana-router-terminal-v6 — Architecture
1
+ # Architecture
2
2
 
3
3
  ## Purpose
4
4
 
5
- Payment routing terminal for Juicebox V6. Accepts any token and dynamically discovers what each destination project accepts, then routes payment via direct forwarding, Uniswap swap (V3 or V4), JB token cashout, or a combination.
5
+ `nana-router-terminal-v6` lets a payer fund a Juicebox project with a token the project does not directly accept. It discovers the destination token, unwraps or wraps native assets when needed, optionally cashes out upstream JB project tokens, and swaps through the deepest available Uniswap V3 or V4 route before forwarding the final asset to the destination terminal.
6
+ The router is intentionally heuristic: it does not exhaustively search every viable pool for the absolute best execution price.
6
7
 
7
- ## Contract Map
8
+ ## Boundaries
8
9
 
9
- ```
10
- src/
11
- ├── JBRouterTerminal.sol — Payment routing: swap + forward to destination terminal
12
- ├── JBRouterTerminalRegistry.sol Registry mapping projects to router terminal configs
13
- ├── interfaces/
14
- │ ├── IJBPayerTracker.sol
15
- │ ├── IJBRouterTerminal.sol
16
- │ ├── IJBRouterTerminalRegistry.sol
17
- │ └── IWETH9.sol
18
- ├── libraries/
19
- │ └── JBSwapLib.sol — Uniswap V3/V4 swap helpers, pool discovery
20
- └── structs/
21
- └── PoolInfo.sol — Cached pool configuration
22
- ```
23
-
24
- ## Key Data Flow
25
-
26
- ### Payment Routing
27
- ```
28
- Payer → JBRouterTerminal.pay(projectId, token, amount)
29
-
30
- ├─ Accept funds (msg.value for native, Permit2 pull for ERC-20, or credit transfer)
31
-
32
- ├─ If input is a JB project token (credit or ERC-20):
33
- │ → Cash out recursively via _cashOutLoop (up to 20 hops)
34
- │ → Reclaimed token becomes the new tokenIn
35
-
36
- ├─ Resolve tokenOut: what does the destination project accept?
37
- │ 1. Metadata override ("routeTokenOut")
38
- │ 2. Direct acceptance — project already accepts tokenIn
39
- │ 3. NATIVE ↔ WETH equivalence check
40
- │ 4. Dynamic discovery — iterate project terminals, find swappable token
41
-
42
- ├─ Convert tokenIn → tokenOut via _convert:
43
- │ ├─ Same token: no-op
44
- │ ├─ NATIVE ↔ WETH: wrap (WETH.deposit) or unwrap (WETH.withdraw)
45
- │ └─ Different tokens: Uniswap swap
46
- │ │
47
- │ ├─ If native ETH input: held as raw ETH until V3 callback
48
- │ │ wraps (WETH.deposit) only the amount the pool consumes
49
- │ │
50
- │ ├─ Pool discovery (_discoverPool):
51
- │ │ Search V3 pools across 4 fee tiers (0.3%, 0.05%, 1%, 0.01%)
52
- │ │ Search V4 pools across same fee tiers (if PoolManager deployed)
53
- │ │ Compare in-range liquidity across all candidates
54
- │ │ Select the single pool with highest liquidity
55
- │ │
56
- │ ├─ Quote & slippage (_pickPoolAndQuote):
57
- │ │ 1. User-provided quote (metadata "quoteForSwap") — used as-is
58
- │ │ 2. V3 fallback: 10-min TWAP via OracleLibrary.consult()
59
- │ │ 3. V4 fallback: TWAP from oracle hook if available, else spot price from getSlot0()
60
- │ │ Apply sigmoid slippage: minSlippage + range * impact/(impact+K)
61
- │ │
62
- │ ├─ Execute swap via V3 pool.swap() or V4 POOL_MANAGER.unlock()
63
- │ │
64
- │ ├─ If native ETH input: wrap any remaining raw ETH (partial fills)
65
- │ ├─ If native ETH output: unwrap WETH → ETH (WETH.withdraw)
66
- │ └─ Return leftover input tokens via _resolveRefundWithBackupRecipient (checks msg.sender's IJBPayerTracker.originalPayer() via try-catch, falls back to beneficiary for pay() or _msgSender() for addToBalanceOf())
67
-
68
- ├─ Approve destination terminal for output tokens (or set msg.value for native)
69
- └─ Forward to destTerminal.pay() → return beneficiary token count
70
- ```
10
+ - The router is a terminal-shaped payment adapter, not a canonical accounting terminal.
11
+ - It owns routing, swapping, quoting, and refund behavior.
12
+ - Final accounting still occurs at the destination terminal that actually accepts the routed token.
13
+ - Pool selection is optimized for simple, bounded route discovery, not full best-execution search across all candidate pools.
71
14
 
72
- ### Preview Routing
73
- ```
74
- Caller → JBRouterTerminal.previewPayFor(projectId, token, amount)
75
- → Mirror source-of-funds and routing logic in view context
76
- → If direct, wrap-unwrap, or exact cashout route: forward preview to destination terminal
77
- → If swap route: estimate output using the same quote-selection logic used for execution bounds
78
- ```
79
-
80
- ## Extension Points
15
+ ## Main Components
81
16
 
82
- | Point | Interface | Purpose |
83
- |-------|-----------|---------|
84
- | Terminal | `IJBTerminal` | Acts as a terminal that routes payments |
85
- | Registry | `IJBRouterTerminalRegistry` | Maps projects to routing configs |
86
- | Payer tracker | `IJBPayerTracker` | Exposes the original payer of a forwarded call for refund resolution |
87
- | Permit | `IJBPermitTerminal` | Permit2 token approval support |
17
+ | Component | Responsibility |
18
+ | --- | --- |
19
+ | `JBRouterTerminal` | Source-token intake, route discovery, swapping, and forwarding |
20
+ | `JBRouterTerminalRegistry` | Project-level selection and locking of router terminal instances |
21
+ | `JBPayRouteResolver` | Helper contract that evaluates pay-route preview candidates without bloating router runtime size |
22
+ | `JBSwapLib` | Pool discovery, quoting, and slippage helpers |
23
+ | `PoolInfo`, `CashOutPathCandidates`, and interfaces | Typed routing metadata and registry/payer integration surfaces |
88
24
 
89
- ## Composition Boundary
25
+ ## Runtime Model
90
26
 
91
- The router terminal exposes the `IJBTerminal` surface because it needs to participate in Juicebox routing, but its
92
- accounting context is intentionally synthetic. `accountingContextForTokenOf()` returns `decimals = 18` for native
93
- tokens and probes `IERC20Metadata.decimals()` for ERC-20s (falling back to `18` if the call fails). The registry
94
- forwards that value unchanged. Treat the router layer as a payment router only, not as an accounting-sensitive
95
- terminal source for loan sizing, debt normalization, or any other decimals-dependent logic.
27
+ ```text
28
+ router pay call
29
+ -> accept native, ERC-20, or JB-token-like input
30
+ -> if input is a project token, recursively cash it out first
31
+ -> resolve the destination token the project can actually receive
32
+ -> pick the best direct, wrap/unwrap, or swap route
33
+ -> execute the route and forward the result to the destination terminal
34
+ -> return any leftover input to the original payer when possible
35
+ ```
96
36
 
97
- ## Design Decisions
37
+ ## Critical Invariants
98
38
 
99
- **Why the router is a terminal, not a standalone contract.** By implementing `IJBTerminal`, the router can be set as a project's terminal in the directory. Payers and frontend integrations call the same `pay()` / `addToBalanceOf()` interface they use for any terminal — no special routing code required on the caller side. The router accepts funds, converts them, and forwards to the real destination terminal in a single transaction.
39
+ - The router's own accounting context is synthetic. Consumers should not treat it as the source of truth for project accounting.
40
+ - Pool discovery and quote logic must stay aligned between preview and execution paths.
41
+ - Refund resolution is part of correctness, not ergonomics. Partial fills without correct refunds create value leaks.
42
+ - Registry locking is a security feature; it prevents projects from being silently switched to untrusted router implementations.
43
+ - Final forwarded ERC-20 hops are only supported for standard tokens whose destination-terminal pull transfers the full nominal amount without transfer fees or burns.
44
+ - Circular `router -> registry -> same router` forwarding is blocked in the registry, not by teaching the router about registry internals.
100
45
 
101
- **Why both Uniswap V3 and V4 support.** V4 pools may offer deeper liquidity for certain pairs (especially native-ETH pairs that V4 handles natively), while V3 pools have years of established liquidity. The router searches both and picks the pool with the highest in-range liquidity, giving payers the best available execution without requiring them to know which protocol version has the better pool.
46
+ ## Where Complexity Lives
102
47
 
103
- **Why synthetic accounting contexts.** The router accepts any token and converts it before forwarding. It never holds balances between transactions, so it has no meaningful accounting of its own. `accountingContextForTokenOf()` returns a best-effort context (probing `decimals()` with an 18-decimal fallback) purely so the directory can register it. The real accounting happens at the destination terminal.
48
+ - The router composes multiple route families: direct, wrap/unwrap, recursive JB cash-out, and DEX swaps.
49
+ - Native-asset handling and refund handling are the most failure-prone parts of the implementation.
50
+ - Liquidity discovery across V3 and V4 is simple to describe but easy to desynchronize between preview and live execution.
51
+ - V4 discovery intentionally searches both vanilla pools and pools using the canonical `UNIV4_HOOK`, because buyback-hook and LP-split integrations rely on that hook-backed oracle path.
52
+ - “Best route” in this system means the best route under the router's discovery heuristic, not a guarantee of globally optimal output across every live pool.
53
+ - Fee-on-transfer or otherwise lossy ERC-20s are only tolerated on ingress where the router can reconcile the received balance delta. They are rejected on the router's final terminal-facing hop. The registry does not perform independent receipt enforcement; it relies on the downstream router terminal to reject lossy transfers.
54
+ - The preview candidate fanout lives in `JBPayRouteResolver`, but downstream `previewPayFor(...)` calls still originate from the router so payer-sensitive previews match execution context.
104
55
 
105
- **Why the registry pattern.** `JBRouterTerminalRegistry` lets project owners lock a specific `JBRouterTerminal` instance for their project and manage Permit2 approvals in one place. This provides a stable entry point: if the router implementation is upgraded, the registry can be pointed to the new instance without changing the project's directory entry. It also gates which router terminals are allowed, preventing untrusted implementations from being set.
56
+ ## Dependencies
106
57
 
107
- **Why `IJBPayerTracker` is a separate interface.** The router terminal needs to know the original payer when called through an intermediary so it can return leftover tokens from partial swap fills to the right address. Rather than coupling the router to `IJBRouterTerminalRegistry` specifically, the refund resolution logic (`_resolveRefundWithBackupRecipient`) queries `IJBPayerTracker(msg.sender).originalPayer()` via a try-catch. This means any contract that implements `IJBPayerTracker` -- not just the registry -- can act as a forwarding intermediary. The registry inherits `IJBPayerTracker` through `IJBRouterTerminalRegistry`, keeping backward compatibility while opening the door for other intermediary patterns.
58
+ - `nana-core-v6` terminal and directory surfaces
59
+ - Uniswap V3, Uniswap V4, and Permit2
60
+ - Optional `IJBPayerTracker` intermediaries for refund attribution
108
61
 
109
- **Why liquidity-based pool selection instead of quote comparison.** Comparing actual output quotes across V3 and V4 would require executing (or simulating) swaps on both — expensive on-chain and complex for V4 where swaps must go through `PoolManager.unlock()`. Comparing in-range liquidity is a single `liquidity()` or `getLiquidity()` read per pool, is gas-cheap, and strongly correlates with execution quality for typical swap sizes.
62
+ ## Safe Change Guide
110
63
 
111
- ## Dependencies
112
- - `@bananapus/core-v6` Terminal, directory, permissions
113
- - `@uniswap/v3-core` + `v3-periphery` V3 swap routing
114
- - `@uniswap/v4-core` V4 pool manager
115
- - `@uniswap/permit2` Token approvals
116
- - `@openzeppelin/contracts` — SafeERC20, ERC2771, Ownable
64
+ - Keep route selection and execution semantics paired. If preview and execution diverge, frontends will misprice user flows.
65
+ - Be cautious with native-token handling; wrap and unwrap edge cases are where routers usually leak value.
66
+ - If you change recursive cash-out behavior, inspect the hop limit and failure modes together.
67
+ - Do not promote the router into a stateful treasury layer.
68
+ - Treat any new convenience path as a new asset-conservation proof obligation.