@bananapus/router-terminal-v6 0.0.26 → 0.0.27

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/ADMINISTRATION.md CHANGED
@@ -1,152 +1,80 @@
1
1
  # Administration
2
2
 
3
- Admin privileges and their scope in nana-router-terminal-v6.
4
-
5
3
  ## At A Glance
6
4
 
7
5
  | 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.
6
+ | --- | --- |
7
+ | Scope | Global router terminal allowlisting and project-local terminal selection |
8
+ | Control posture | Mixed registry-owner and project-local delegated control |
9
+ | Highest-risk actions | Changing the default terminal, locking a project to the wrong terminal, and relying on misconfigured credit-cashout routing |
10
+ | Recovery posture | Unlocked projects can move; locked projects and immutable router wiring limit recovery |
20
11
 
21
- ## One-Way Or High-Risk Actions
12
+ ## Purpose
22
13
 
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.
14
+ `nana-router-terminal-v6` splits administration between a global registry and project-local terminal selection. The router logic itself is mostly immutable; the mutable control plane lives in `JBRouterTerminalRegistry`.
25
15
 
26
- ## Recovery Notes
16
+ ## Control Model
27
17
 
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.
18
+ - `JBRouterTerminalRegistry` is globally `Ownable`.
19
+ - Project owners or delegates choose and can lock their router terminal.
20
+ - `JBRouterTerminal` has immutable routing dependencies and no owner-controlled strategy knobs.
21
+ - Some transaction paths depend on project-local `JBPermissions`, such as `TRANSFER_CREDITS`.
30
22
 
31
23
  ## Roles
32
24
 
33
- ### 1. Registry Owner (Ownable)
34
-
35
- **Contract**: `JBRouterTerminalRegistry`
36
- **Assigned via**: Constructor parameter `owner`, transferable via `Ownable.transferOwnership()`.
37
- **Scope**: Global -- controls which router terminals can be used by any project and sets the system-wide default terminal.
38
-
39
- ### 2. Project Owner / SET_ROUTER_TERMINAL Delegate
40
-
41
- **Contract**: `JBRouterTerminalRegistry`
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).
43
- **Scope**: Per-project -- controls which router terminal a specific project uses, and can permanently lock that choice.
44
-
45
- ### 3. Credit Cashout Payer (Implicit)
46
-
47
- **Contract**: `JBRouterTerminal`
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`.
49
- **Scope**: Per-transaction. Required only when using the `cashOutSource` metadata key to route payments through credit cashouts.
50
-
51
- ## Terminal Resolution
25
+ | Role | How Assigned | Scope | Notes |
26
+ | --- | --- | --- | --- |
27
+ | Registry owner | `Ownable(owner)` | Global | Controls allowlist and default terminal |
28
+ | Project owner | `JBProjects.ownerOf(projectId)` | Per project | May delegate `SET_ROUTER_TERMINAL` |
29
+ | Terminal delegate | `JBPermissions` grant | Per project | Usually `SET_ROUTER_TERMINAL` |
30
+ | Payer | Per transaction | Per payment | May need `TRANSFER_CREDITS` for credit-cashout routing |
52
31
 
53
- When a payment is forwarded through the registry, the terminal is resolved as follows:
32
+ ## Privileged Surfaces
54
33
 
55
- 1. If the project has called `setTerminalFor(projectId, terminal)`, that explicit terminal is used.
56
- 2. If no explicit terminal is set, the registry's `defaultTerminal` is used.
57
- 3. If neither exists, the forwarding reverts.
34
+ | Contract | Function | Who Can Call | Effect |
35
+ | --- | --- | --- | --- |
36
+ | `JBRouterTerminalRegistry` | `allowTerminal(...)`, `disallowTerminal(...)`, `setDefaultTerminal(...)` | Registry owner | Controls global terminal availability and fallback |
37
+ | `JBRouterTerminalRegistry` | `setTerminalFor(...)` | Project owner or `SET_ROUTER_TERMINAL` delegate | Sets a project's explicit router terminal |
38
+ | `JBRouterTerminalRegistry` | `lockTerminalFor(...)` | Project owner or `SET_ROUTER_TERMINAL` delegate | Irreversibly locks the resolved terminal for a project |
58
39
 
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.
40
+ ## Immutable And One-Way
60
41
 
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.
42
+ - `lockTerminalFor(...)` is irreversible.
43
+ - Constructor dependencies on the router are immutable.
44
+ - The current default terminal must move before the old default can be disallowed.
62
45
 
63
- ## Privileged Functions
46
+ ## Operational Notes
64
47
 
65
- ### JBRouterTerminalRegistry
48
+ - Keep the terminal allowlist small and explicit.
49
+ - Change the default terminal carefully because unconfigured projects inherit it.
50
+ - Encourage projects to lock only after validating the resolved terminal and routing behavior.
51
+ - Review credit-cashout routing permissions before relying on that path operationally.
52
+ - Distinguish configuration risk from quote-quality risk: some route-discovery paths are best-effort, and some V4 quote paths can rely on weaker spot-style assumptions when robust history is unavailable.
66
53
 
67
- | Function | Required Role | Permission ID | Scope | What It Does |
68
- |----------|--------------|---------------|-------|--------------|
69
- | `allowTerminal(terminal)` | Registry Owner | `onlyOwner` | Global | Adds a terminal to the allowlist (`isTerminalAllowed[terminal] = true`). Projects can only use allowlisted terminals. |
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. |
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. |
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.** |
54
+ ## Machine Notes
74
55
 
75
- ### Implicit Permission Requirements (not `onlyOwner`, but enforced by external contracts)
56
+ - Do not treat registry ownership as authority to override locked project choice.
57
+ - Inspect `src/JBRouterTerminalRegistry.sol` and `src/JBRouterTerminal.sol` separately; they govern different control boundaries.
58
+ - If the effective terminal resolution and the documented default differ, stop and resolve the registry state before further actions.
59
+ - If route previews are falling back to weaker discovery or quote paths, do not describe the router as offering uniform oracle-quality guarantees across all pools and states.
76
60
 
77
- | Operation | Required By | Permission | What Happens |
78
- |-----------|------------|------------|--------------|
79
- | Credit cashout via `cashOutSource` metadata | Payer | `TRANSFER_CREDITS` (13) granted to router terminal | `TOKENS.transferCreditsFrom()` pulls credits from payer. Reverts if payer has not granted the permission. |
80
- | Cashout execution | Router terminal (as holder) | None (terminal holds the tokens) | `IJBCashOutTerminal.cashOutTokensOf()` is called with the router as the holder. The router already holds the tokens from the credit transfer or prior cashout step. |
61
+ ## Recovery
81
62
 
82
- ## Immutable Configuration
63
+ - Unlocked projects can switch to another allowlisted terminal.
64
+ - Locked projects cannot be unlocked by the registry.
65
+ - Bad immutable router behavior means replacement infrastructure, not in-place edits.
66
+ - Quote-path weakness is usually mitigated operationally with better pool choice, external quoting, or replacement routing infrastructure, not with an owner-only hotfix.
83
67
 
84
- The following values are set at deploy time and cannot be changed:
85
-
86
- ### JBRouterTerminal
87
-
88
- | Property | Set At | Mutable? | Description |
89
- |----------|--------|----------|-------------|
90
- | `Trusted forwarder` | Constructor | No | ERC-2771 trusted forwarder for meta-transactions |
91
- | `DIRECTORY` | Constructor | No | JB directory for terminal/controller lookups |
92
- | `TOKENS` | Constructor | No | JB token manager for credit transfers and project token lookups |
93
- | `FACTORY` | Constructor | No | Uniswap V3 factory for pool discovery and callback verification |
94
- | `POOL_MANAGER` | Constructor | No | Uniswap V4 PoolManager (can be `address(0)` to disable V4) |
95
- | `PERMIT2` | Constructor | No | Permit2 contract for gasless approvals |
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 |
99
- | `DEFAULT_TWAP_WINDOW` | Compile-time constant | No | 10 minutes (600 seconds) |
100
- | `SLIPPAGE_DENOMINATOR` | Compile-time constant | No | 10,000 (basis points) |
101
- | `_FEE_TIERS` | Storage (initialized) | No | `[3000, 500, 10000, 100]` -- V3 fee tiers |
102
- | `_V4_FEES` / `_V4_TICK_SPACINGS` | Storage (initialized) | No | V4 pool parameters |
103
- | `_MAX_CASHOUT_ITERATIONS` | Compile-time constant | No | 20 iterations |
104
-
105
- ### JBRouterTerminalRegistry
106
-
107
- | Property | Set At | Mutable? | Description |
108
- |----------|--------|----------|-------------|
109
- | `PERMISSIONS` | Constructor | No | JB permissions registry for permission checks |
110
- | `Trusted forwarder` | Constructor | No | ERC-2771 trusted forwarder for meta-transactions |
111
- | `PROJECTS` | Constructor | No | JB project NFT registry |
112
- | `PERMIT2` | Constructor | No | Permit2 contract for gasless approvals |
113
-
114
- ### JBPayRouteResolver
115
-
116
- | Property | Set At | Mutable? | Description |
117
- |----------|--------|----------|-------------|
118
- | `DIRECTORY` | Constructor | No | JB directory for terminal and accounting-context lookups, cached from the router at construction time |
119
- | `WETH` | Constructor | No | Wrapped native token used for token normalization, cached from the router at construction time |
120
-
121
- ### JBSwapLib
68
+ ## Admin Boundaries
122
69
 
123
- | Constant | Value | Description |
124
- |----------|-------|-------------|
125
- | `MAX_SLIPPAGE` | 8,800 (88%) | Maximum slippage tolerance ceiling |
126
- | `IMPACT_PRECISION` | 1e18 | Precision for impact calculations |
127
- | `SIGMOID_K` | 5e16 | Sigmoid curve shape parameter |
70
+ - The registry owner cannot unlock or override a locked project terminal.
71
+ - Project operators cannot set a terminal that the registry does not allow.
72
+ - Router maintainers cannot tune routing heuristics or constructor immutables post-deploy.
73
+ - There is no pause surface in the registry or router.
128
74
 
129
- ## Admin Boundaries
75
+ ## Source Map
130
76
 
131
- What admins **cannot** do:
132
-
133
- ### Registry Owner Cannot:
134
- - **Unlock a locked terminal.** `lockTerminalFor` is irreversible -- there is no `unlockTerminalFor` function.
135
- - **Override a project's locked terminal choice.** Once locked, the terminal is permanently stored in `_terminalOf[projectId]` and `hasLockedTerminal[projectId]` is permanently `true`.
136
- - **Force a project to use a specific terminal.** Only the project owner (or delegate) can call `setTerminalFor`.
137
- - **Access project funds.** The registry is a pass-through; it holds funds transiently during forwarding only.
138
- - **Modify swap parameters, slippage, or routing logic.** These are controlled by the `JBRouterTerminal` contract, not the registry.
139
- - **Pause payments.** There is no pause mechanism.
140
-
141
- ### Router Terminal Maintainers Cannot:
142
- - **Modify swap slippage parameters.** The TWAP window, sigmoid constants, fee tiers, and max slippage are all immutable.
143
- - **Redirect funds.** The terminal is stateless between transactions and routes payments to whichever terminal the JB directory specifies.
144
- - **Change the Uniswap factory or PoolManager.** These are immutable constructor parameters.
145
- - **Override user-provided quotes.** The `quoteForSwap` metadata is decoded and used as-is.
146
- - **Prevent specific users from paying.** There is no blocklist mechanism.
147
- - **Extract stuck funds.** There is no sweep or rescue function. The terminal relies on completing all token movements within a single transaction.
148
-
149
- ### Project Owner / Delegate Cannot:
150
- - **Change the terminal after locking.** The `setTerminalFor` function reverts with `TerminalLocked` if the terminal is locked.
151
- - **Set a disallowed terminal.** `setTerminalFor` reverts with `TerminalNotAllowed` if the terminal is not on the registry owner's allowlist.
152
- - **Affect other projects' routing.** Permission checks are scoped to the specific `projectId`.
77
+ - `src/JBRouterTerminalRegistry.sol`
78
+ - `src/JBRouterTerminal.sol`
79
+ - `src/JBPayRouteResolver.sol`
80
+ - `test/`
package/ARCHITECTURE.md CHANGED
@@ -2,67 +2,92 @@
2
2
 
3
3
  ## Purpose
4
4
 
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.
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, can recursively cash out upstream JB project tokens, and swaps through bounded Uniswap V3 or V4 routes before forwarding value to the destination terminal.
7
6
 
8
- ## Boundaries
7
+ The router is intentionally heuristic. It does not search every possible route for a globally optimal price.
9
8
 
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.
9
+ ## System Overview
14
10
 
15
- ## Main Components
11
+ `JBRouterTerminal` is a terminal-shaped adapter, not an accounting source of truth. `JBRouterTerminalRegistry` is both a registry and a stable project-facing proxy surface: projects can point at the registry while the registry resolves, and can later lock, the actual router terminal implementation to use. `JBPayRouteResolver` expands preview candidates without forcing the main router contract to carry all preview complexity inline. Final accounting still occurs in the downstream terminal selected through `nana-core-v6`.
16
12
 
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 |
13
+ ## Core Invariants
24
14
 
25
- ## Runtime Model
15
+ - The router's own accounting context is synthetic and must not be treated as the project ledger.
16
+ - Preview route discovery and live execution must stay aligned.
17
+ - Refund behavior is part of correctness, not UX.
18
+ - Registry locking prevents silent migration to untrusted router implementations.
19
+ - Final terminal-facing ERC-20 hops only support standard, non-lossy transfers.
20
+ - Recursive project-token cashout routing is intentionally bounded; non-converging paths should fail instead of looping.
21
+ - Caller reclaim minima only apply to the first cashout hop, because later hops may change token units.
22
+ - Circular `router -> registry -> same router` forwarding remains blocked in the registry.
23
+
24
+ ## Modules
25
+
26
+ | Module | Responsibility | Notes |
27
+ | --- | --- | --- |
28
+ | `JBRouterTerminal` | Intake, route discovery, swap execution, forwarding, and refunds | Main runtime surface |
29
+ | `JBRouterTerminalRegistry` | Project-level router selection, locking, and proxy forwarding to the resolved router terminal | Governance, safety, and proxy surface |
30
+ | `JBPayRouteResolver` | Preview candidate evaluation | Helper to keep runtime size bounded |
31
+ | `JBSwapLib` and routing structs | Pool discovery, quoting, and route metadata | Shared routing logic |
32
+
33
+ ## Trust Boundaries
34
+
35
+ - Final accounting remains in the downstream terminal selected through `JBDirectory`.
36
+ - The router trusts Uniswap V3, Uniswap V4, Permit2, and optional payer trackers for routing-side behavior.
37
+ - Fee-on-transfer tokens are only tolerated on ingress where received-balance deltas can be reconciled.
38
+ - The registry is trusted to resolve and forward into the intended router terminal implementation for a project.
39
+
40
+ ## Critical Flows
41
+
42
+ ### Route And Pay
26
43
 
27
44
  ```text
28
45
  router pay call
29
46
  -> accept native, ERC-20, or JB-token-like input
30
47
  -> 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
48
+ -> resolve the destination token the project terminal actually accepts
49
+ -> choose the best direct, wrap/unwrap, or swap path under the router's bounded candidate-discovery heuristic
50
+ -> execute the route and forward the result to the downstream terminal
51
+ -> refund leftover input when possible
35
52
  ```
36
53
 
37
- ## Critical Invariants
38
-
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.
54
+ ## Accounting Model
45
55
 
46
- ## Where Complexity Lives
56
+ The router does not own project balances. It owns transient route accounting: input reconciliation, swap execution, forwarded amount, and refund resolution.
47
57
 
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.
58
+ Preview and execution share the same conceptual route shape: optional recursive cashout first, then destination-token resolution, then final conversion and forwarding. `JBPayRouteResolver` narrows candidate tokens and usable external terminals so the live router does not need to brute-force every possibility inline.
55
59
 
56
- ## Dependencies
60
+ ## Security Model
57
61
 
58
- - `nana-core-v6` terminal and directory surfaces
59
- - Uniswap V3, Uniswap V4, and Permit2
60
- - Optional `IJBPayerTracker` intermediaries for refund attribution
62
+ - Native-asset handling and refunds are the most failure-prone paths.
63
+ - V3 and V4 discovery must stay synchronized between preview and live execution.
64
+ - V4 discovery intentionally considers both vanilla pools and pools using the canonical `UNIV4_HOOK`.
65
+ - The router's “best route” claim is only as strong as its bounded discovery set and external-terminal safety checks. It is not a global optimizer.
66
+ - Recursive cashout behavior, preferred-token handling, and one-shot source overrides are tightly coupled; changing one can silently desynchronize preview from execution.
67
+ - “Best route” means best under the bounded discovery heuristic, not globally optimal routing.
61
68
 
62
69
  ## Safe Change Guide
63
70
 
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.
71
+ - Keep route discovery and route execution semantics paired.
72
+ - Be conservative with native wrapping, unwrapping, and refund behavior.
73
+ - If recursive cash-out logic changes, review hop limits and failure handling together.
74
+ - If metadata semantics change, re-check first-hop reclaim minima, one-shot source overrides, and preferred-token routing together.
75
+ - Do not turn the router into a persistent treasury layer.
76
+
77
+ ## Canonical Checks
78
+
79
+ - bounded recursive cash-out behavior:
80
+ `test/regression/CashOutLoopLimit.t.sol`
81
+ - preview versus execution terminal alignment:
82
+ `test/audit/PreviewPrimaryTerminalMismatch.t.sol`
83
+ - router-wide route and refund invariants:
84
+ `test/invariant/RouterTerminalInvariant.t.sol`
85
+
86
+ ## Source Map
87
+
88
+ - `src/JBRouterTerminal.sol`
89
+ - `src/JBRouterTerminalRegistry.sol`
90
+ - `src/JBPayRouteResolver.sol`
91
+ - `test/regression/CashOutLoopLimit.t.sol`
92
+ - `test/audit/PreviewPrimaryTerminalMismatch.t.sol`
93
+ - `test/invariant/RouterTerminalInvariant.t.sol`
@@ -2,7 +2,7 @@
2
2
 
3
3
  This repo accepts one token and routes value into whatever token a destination project actually accepts. Audit it as a stateless router whose mistakes show up as lost value, bad slippage control, or wrong-route accounting.
4
4
 
5
- ## Objective
5
+ ## Audit Objective
6
6
 
7
7
  Find issues that:
8
8
  - route user funds through an incorrect pool or protocol path
@@ -25,7 +25,13 @@ Key dependencies:
25
25
  - `nana-core-v6`
26
26
  - Uniswap V3 and V4 integration surfaces
27
27
 
28
- ## System Model
28
+ ## Start Here
29
+
30
+ 1. `src/JBRouterTerminal.sol`
31
+ 2. `src/JBRouterTerminalRegistry.sol`
32
+ 3. `src/libraries/JBSwapLib.sol`
33
+
34
+ ## Security Model
29
35
 
30
36
  The router terminal:
31
37
  - discovers what token a project’s terminal accepts
@@ -35,6 +41,22 @@ The router terminal:
35
41
 
36
42
  The registry chooses which router terminal instance a project uses and whether that choice is locked.
37
43
 
44
+ ## Roles And Privileges
45
+
46
+ | Role | Powers | How constrained |
47
+ |------|--------|-----------------|
48
+ | User or relayer | Initiate routed payment with beneficiary and slippage intent | Must receive exact refund semantics requested |
49
+ | Registry controller | Set default or allowed router terminals | Must not redirect projects unexpectedly |
50
+ | Router terminal | Hold funds only transiently during routing | Must not retain leftovers across flows |
51
+
52
+ ## Integration Assumptions
53
+
54
+ | Dependency | Assumption | What breaks if wrong |
55
+ |------------|------------|----------------------|
56
+ | `nana-core-v6` | Terminal discovery and pay semantics are accurate | Routed value lands in the wrong place |
57
+ | Uniswap V3 and V4 | Callback settlement and pool discovery are authentic | Slippage and final forwarded amount diverge |
58
+ | Permit2 | Allowances and deadlines reflect user intent | Unauthorized transfer or stuck routing behavior |
59
+
38
60
  ## Critical Invariants
39
61
 
40
62
  1. User intent is preserved
@@ -49,16 +71,7 @@ The quoted path, callback settlement, and final forwarded amount must all descri
49
71
  4. Registry controls stay narrow
50
72
  Default terminals, allowed terminals, and lock semantics must not let an unexpected router instance take over project routing.
51
73
 
52
- ## Threat Model
53
-
54
- Prioritize:
55
- - V3 or V4 callback reentrancy
56
- - sandwiching around discovered pool liquidity
57
- - beneficiary versus payer refund mismatches
58
- - Permit2 allowance or deadline misuse
59
- - races around registry terminal locking
60
-
61
- ## Hotspots
74
+ ## Attack Surfaces
62
75
 
63
76
  - payment entrypoints and refund logic
64
77
  - V3 callback verification
@@ -66,18 +79,8 @@ Prioritize:
66
79
  - pool discovery and best-path selection
67
80
  - registry allowlist and lock behavior
68
81
 
69
- ## Build And Verification
82
+ ## Verification
70
83
 
71
- Standard workflow:
72
84
  - `npm install`
73
85
  - `forge build`
74
86
  - `forge test`
75
-
76
- Current tests focus on:
77
- - refund edge cases
78
- - payer tracking
79
- - Permit2 failure handling
80
- - cash-out-assisted routes
81
- - reentrancy and sandwich-sensitive fork cases
82
-
83
- Strong findings in this repo usually show the router holding onto value or satisfying user slippage checks with the wrong sign, recipient, or output token.
package/README.md CHANGED
@@ -1,9 +1,14 @@
1
1
  # Juicebox Router Terminal
2
2
 
3
- `@bananapus/router-terminal-v6` is a routing terminal for Juicebox V6. It accepts value in many input tokens, discovers what token the destination project actually accepts, and forwards the payment through the best available route.
3
+ `@bananapus/router-terminal-v6` is a routing terminal for Juicebox V6. It accepts value in many input tokens, discovers what token the destination project actually accepts, and forwards the payment through the best previewed route it can resolve from the configured candidates.
4
4
 
5
- Docs: <https://docs.juicebox.money>
6
- Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
5
+ Docs: <https://docs.juicebox.money>
6
+ Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
7
+ User journeys: [USER_JOURNEYS.md](./USER_JOURNEYS.md)
8
+ Skills: [SKILLS.md](./SKILLS.md)
9
+ Risks: [RISKS.md](./RISKS.md)
10
+ Administration: [ADMINISTRATION.md](./ADMINISTRATION.md)
11
+ Audit instructions: [AUDIT_INSTRUCTIONS.md](./AUDIT_INSTRUCTIONS.md)
7
12
 
8
13
  ## Overview
9
14
 
@@ -16,7 +21,7 @@ It can route through:
16
21
  - Uniswap V3 or V4 swaps
17
22
  - recursive Juicebox token cash outs when the input itself is a project token
18
23
 
19
- Projects can use the registry contract to choose a project-specific router terminal or fall back to a default.
24
+ Projects can use the registry to choose, and optionally lock, a project-specific router terminal or fall back to the registry's default terminal.
20
25
 
21
26
  Use this repo when UX requires "pay with many tokens, settle into the right one." Do not use it as a replacement for downstream terminal accounting or as an authoritative decimal source.
22
27
 
@@ -28,7 +33,7 @@ This repo is best understood as an execution router attached to Juicebox, not as
28
33
  | --- | --- |
29
34
  | `JBRouterTerminal` | Main routing terminal that accepts many token types and forwards value to the destination terminal. |
30
35
  | `JBRouterTerminalRegistry` | Registry and proxy surface that lets a project choose and optionally lock its preferred router terminal. |
31
- | `JBPayRouteResolver` | Helper that evaluates pay-route candidates and selects the best route preview for the router terminal. |
36
+ | `JBPayRouteResolver` | Helper that evaluates pay-route candidates and selects the strongest route preview the router can resolve. |
32
37
 
33
38
  ## Mental Model
34
39
 
@@ -68,6 +73,14 @@ The shortest useful reading order is:
68
73
 
69
74
  That separation is the reason a successful route can still end in a downstream terminal behavior you did not expect.
70
75
 
76
+ ## High-Signal Tests
77
+
78
+ 1. `test/RouterTerminal.t.sol`
79
+ 2. `test/RouterTerminalPreviewFork.t.sol`
80
+ 3. `test/RouterTerminalCashOutFork.t.sol`
81
+ 4. `test/audit/PreviewPrimaryTerminalMismatch.t.sol`
82
+ 5. `test/codex/CashOutCircularPrimaryTerminal.t.sol`
83
+
71
84
  ## Install
72
85
 
73
86
  ```bash
@@ -116,3 +129,9 @@ script/
116
129
  - final terminal-facing ERC-20 hops must be standard tokens; lossy terminal pulls are rejected on both router and registry paths
117
130
 
118
131
  The most common reader mistake here is to stop at the router and forget to inspect the terminal that actually receives the value.
132
+
133
+ ## For AI Agents
134
+
135
+ - Do not claim the router is the accounting source of truth after forwarding.
136
+ - Read the preview, recursive cash-out, and registry tests before summarizing path selection behavior.
137
+ - If the route ends in surprising accounting, move to the downstream terminal in `nana-core-v6`.
package/RISKS.md CHANGED
@@ -29,11 +29,11 @@ This file focuses on the routing, accounting-context, and liquidity-selection ri
29
29
 
30
30
  ## 2. Economic / Manipulation Risks
31
31
 
32
- - **V4 price manipulation.** `_getV4Quote` first attempts a 30-second TWAP from the pool's oracle hook (e.g., `IGeomeanOracle.observe()`). If no oracle hook exists or the call fails, it falls back to the instantaneous `getSlot0` tick, which is manipulable via sandwich attacks or flash loans. The sigmoid slippage formula provides a floor (min 2%) but does NOT provide full MEV protection. Without user-supplied `quoteForSwap` metadata, V4 swaps using the spot fallback are vulnerable to extraction. Front-ends MUST supply `quoteForSwap` metadata for V4 swaps without oracle hooks. Note: `_getV4Quote` normalizes WETH to `address(0)` before calling OracleLibrary, since V4 uses `address(0)` for native ETH -- without this normalization, token sorting would mismatch the pool's currency ordering and produce inverted quotes.
32
+ - **V4 price manipulation.** `_getV4Quote` first attempts a 30-second TWAP from the pool's oracle hook (e.g., `IGeomeanOracle.observe()`). If no oracle hook exists or the call fails, it falls back to the instantaneous `getSlot0` tick, which is manipulable via sandwich attacks or flash loans. The sigmoid slippage formula provides a floor (min 2%) but does NOT provide full MEV protection. This spot-fallback path is an accepted risk for integrations that cannot source external quotes, particularly when routing routine flow through sufficiently deep pools where manipulation cost is expected to dominate likely extractable value. Deep liquidity reduces practical risk but does not eliminate the same-block manipulation surface. Thin pools, fresh pools, and unusually large swaps should not rely on this fallback. Front-ends SHOULD supply `quoteForSwap` metadata for V4 swaps whenever possible. Note: `_getV4Quote` normalizes WETH to `address(0)` before calling OracleLibrary, since V4 uses `address(0)` for native ETH -- without this normalization, token sorting would mismatch the pool's currency ordering and produce inverted quotes.
33
33
  - **Hooked V4 discovery scope.** Auto-discovery checks both vanilla V4 pools and pools using the configured canonical `UNIV4_HOOK`. That keeps buyback-hook and LP-split-hook routes discoverable, but it also means deployment misconfiguration of `UNIV4_HOOK` changes which hooked pools are even visible to the router.
34
34
  - **V3 TWAP manipulation.** Short TWAP windows (falls back to `oldestObservation` if < 10 minutes) reduce manipulation resistance. A newly created pool with minimal history can be manipulated within the TWAP window.
35
35
  - **Cashout loop value extraction.** `_cashOutLoop` iterates up to 20 times, cashing out JB project tokens recursively. Each cashout incurs bonding curve slippage. `cashOutMinReclaimed` is enforced only on the first concrete cashout hop. Later hops intentionally do not reuse or rescale that minimum, because multi-hop cashouts can change token units and a single metadata amount cannot be propagated safely across different assets. Gas cost: each cashout iteration involves `terminal.cashOutTokensOf` (external call, ~100-200k gas) plus token transfer and balance accounting. At 20 iterations maximum, the worst case is ~4M gas for the loop alone, leaving headroom within a 30M block but consuming a significant portion.
36
- - **Leftover token handling.** `_handleSwap` refunds the full remaining input token balance after a swap. The router is stateless and should never hold funds between transactions. If tokens are accidentally sent to the contract, they are absorbed into the next caller's refund rather than being permanently stuck this is intentional, as recovering stuck funds is preferable to locking them forever. There is no sweep mechanism because there should be no persistent balance to sweep.
36
+ - **Pre-existing balances are intentionally not swept into refunds.** `_handleSwap` snapshots a per-route refund baseline and only refunds the leftover input balance attributable to the current route. Tokens or ETH that were already sitting in the router before the route began are excluded from that refund calculation. This avoids gifting prior stray balances to the next caller, but it also means accidentally sent balances can remain stranded because the router has no sweep mechanism.
37
37
  - **V4 native ETH settlement.** `_settleV4` unwraps WETH to native ETH when settling a `Currency.wrap(address(0))` debt with PoolManager. This is necessary because the router may hold WETH (from ERC-20 transfers or prior wrapping) but V4 native pools require `msg.value` settlement. If `address(this).balance` is already sufficient, no unwrap occurs.
38
38
  - **Pool selection by liquidity.** `_discoverPool` selects the pool with the highest `liquidity()` value. An attacker can deploy a pool with high but concentrated (out-of-range) liquidity to win selection, then manipulate the actual swap execution at worse prices.
39
39
  - **Heuristic route selection, not best execution.** Automatic routing chooses among discovered paths using bounded heuristics, and pool discovery prefers the deepest discovered pool rather than exhaustively quoting every viable pool. This is an intentional tradeoff to keep routing predictable and gas-bounded. Integrators should treat router-selected execution as best-effort convenience, not a guarantee of the globally best obtainable output. When execution quality matters more than convenience, frontends should supply `quoteForSwap` metadata.
@@ -67,7 +67,7 @@ This file focuses on the routing, accounting-context, and liquidity-selection ri
67
67
  - **Locked bad-terminal risk.** `lockTerminalFor` protects projects from silent migrations, but it also freezes the current resolved terminal exactly as-is. If the locked terminal is malformed, recursively forwards, reverts on use, or otherwise cannot actually process routed payments, the project can be left permanently unroutable through the registry.
68
68
  - **forceApprove for terminal transfers.** `_beforeTransferFor` uses `forceApprove`, which resets the allowance to zero before setting the new value. This mitigates stale-allowance accumulation: even if a previous transaction left a non-zero allowance, `forceApprove` overwrites it rather than adding to it. The reset-then-set pattern prevents cumulative allowance drift.
69
69
  - **Callback data trust.** `uniswapV3SwapCallback` validates the caller by reconstructing the pool address from `(tokenIn, tokenOut, fee)`. The factory `getPool` lookup makes spoofing infeasible in practice.
70
- - **receive() function.** The contract accepts arbitrary ETH via `receive()`. This is necessary for WETH unwraps, cashout reclaims, and V4 PoolManager takes. Any ETH received is absorbed into the next swap's leftover refund. Since the router is stateless by design, this is acceptable — funds should not persist between transactions, and recovering accidentally-sent ETH is preferable to locking it permanently.
70
+ - **receive() function.** The contract accepts arbitrary ETH via `receive()`. This is necessary for WETH unwraps, cashout reclaims, and V4 PoolManager takes. Route-specific leftover refunds only return ETH attributable to the active route; arbitrary ETH already sitting in the router before that route is not swept out automatically and can remain stranded.
71
71
 
72
72
  ## 6. MEV Surface
73
73
 
@@ -77,7 +77,7 @@ This file focuses on the routing, accounting-context, and liquidity-selection ri
77
77
 
78
78
  ## 7. Invariants to Verify
79
79
 
80
- - After any `pay()` or `addToBalanceOf()`, the contract should hold zero tokens (all routed to destination terminal or returned as leftovers).
80
+ - After any `pay()` or `addToBalanceOf()`, the contract should not retain balances attributable to the just-processed route. Pre-existing stray balances may still remain because refund logic intentionally excludes them.
81
81
  - `minAmountOut` in swaps is never zero when TWAP/spot price is available (the sigmoid formula has a 2% floor).
82
82
  - Callback validation: `uniswapV3SwapCallback` only transfers tokens to verified pool addresses.
83
83
  - `unlockCallback` only executes when called by `POOL_MANAGER`.
@@ -95,7 +95,7 @@ The router terminal has no `ReentrancyGuard` or `_routing` flag. This is a consc
95
95
  - **Each call uses its own funds.** The re-entrant call would need to supply its own tokens/ETH (via `_acceptFundsFor`). It cannot consume funds belonging to the outer call because those funds are already committed to the routing pipeline.
96
96
  - **A reentrancy guard would block legitimate composition.** Projects may have terminal chains where terminal A routes through this router, which cashes out into terminal B, which itself routes through this router for a different project. A blanket reentrancy guard would break such flows.
97
97
 
98
- Verified in `RouterTerminalReentrancy.t.sol`: re-entrant calls via both `pay()` and `addToBalanceOf()` succeed without corrupting the outer call's ETH forwarding. The invariant suite (`RouterTerminalInvariant.t.sol`) further confirms that the router holds zero tokens and zero ETH after every operation including operations that exercise the cashout recursion loop (`_cashOutLoop`, up to `_MAX_CASHOUT_ITERATIONS = 20`).
98
+ Verified in `RouterTerminalReentrancy.t.sol`: re-entrant calls via both `pay()` and `addToBalanceOf()` succeed without corrupting the outer call's ETH forwarding. The invariant suite (`RouterTerminalInvariant.t.sol`) confirms that routed operations do not strand balances attributable to the active route, including operations that exercise the cashout recursion loop (`_cashOutLoop`, up to `_MAX_CASHOUT_ITERATIONS = 20`). That does not imply the router can never hold pre-existing stray balances, because refund logic intentionally excludes balances that were already present before the route began.
99
99
 
100
100
  ### 8.2 Router trusts `originalPayer()` from any `msg.sender` that implements it
101
101
 
@@ -112,3 +112,7 @@ The router and resolver no longer contain registry-specific circular-resolution
112
112
  ### 8.4 Liquidity-first pool selection is intentional
113
113
 
114
114
  The router does not attempt full best-execution search across every viable V3 and V4 pool. `_discoverPool` prefers the deepest discovered pool, and the selected pool is then quoted and executed. This can underperform an alternative pool in some market states, but it is an accepted product tradeoff: bounded route discovery, lower complexity, and predictable behavior are prioritized over exhaustive output maximization. Users who need tighter execution guarantees should provide `quoteForSwap` metadata or route externally.
115
+
116
+ ### 8.6 V4 spot fallback is an accepted risk for programmatic integrations
117
+
118
+ Automatic V4 quoting first tries a hook-provided oracle observation and only falls back to spot when no such quote is available. That fallback remains manipulable within the block and is not equivalent to an external quote or a trusted TWAP. It is nevertheless accepted in this terminal because some programmatic integrations cannot provide `quoteForSwap` metadata and still need a bounded on-chain quoting path. The intended operating envelope is routine routing through sufficiently deep pools with moderate swap sizes, where manipulation is possible in principle but expected to be uneconomic in practice. This is a risk reduction argument, not a safety proof: deep liquidity raises attack cost, but does not remove the vulnerability class.
package/SKILLS.md CHANGED
@@ -3,7 +3,7 @@
3
3
  ## Use This File For
4
4
 
5
5
  - Use this file when the task involves routed payments or cash-outs, swap-path metadata, dynamic accepted-token discovery, route-registry selection, or router-terminal fee and slippage behavior.
6
- - Start here, then open the terminal, registry, swap helpers, or tests that own the exact behavior in question.
6
+ - Start here, then decide whether the problem is route discovery, swap execution, cash-out recursion, or registry selection. Those are distinct failure modes in this repo.
7
7
 
8
8
  ## Read This Next
9
9
 
@@ -11,9 +11,11 @@
11
11
  |---|---|
12
12
  | Repo overview and routing model | [`README.md`](./README.md), [`ARCHITECTURE.md`](./ARCHITECTURE.md) |
13
13
  | Terminal execution path | [`src/JBRouterTerminal.sol`](./src/JBRouterTerminal.sol) |
14
+ | Pay-route resolution helpers | [`src/JBPayRouteResolver.sol`](./src/JBPayRouteResolver.sol) |
14
15
  | Registry behavior and terminal selection | [`src/JBRouterTerminalRegistry.sol`](./src/JBRouterTerminalRegistry.sol) |
15
16
  | Shared libraries, interfaces, and metadata structs | [`src/libraries/`](./src/libraries/), [`src/interfaces/`](./src/interfaces/), [`src/structs/`](./src/structs/) |
16
- | Forked routing behavior, preview parity, and regressions | [`test/RouterTerminalPreviewFork.t.sol`](./test/RouterTerminalPreviewFork.t.sol), [`test/RouterTerminalCashOutFork.t.sol`](./test/RouterTerminalCashOutFork.t.sol), [`test/RouterTerminalReentrancy.t.sol`](./test/RouterTerminalReentrancy.t.sol), [`test/regression/`](./test/regression/) |
17
+ | Preview, cash-out, and buyback composition | [`test/RouterTerminalPreviewFork.t.sol`](./test/RouterTerminalPreviewFork.t.sol), [`test/RouterTerminalCashOutFork.t.sol`](./test/RouterTerminalCashOutFork.t.sol), [`test/RouterTerminalBuybackHookFork.t.sol`](./test/RouterTerminalBuybackHookFork.t.sol), [`test/RouterTerminalFeeCashOutFork.t.sol`](./test/RouterTerminalFeeCashOutFork.t.sol) |
18
+ | Registry, multihop, and adversarial coverage | [`test/RouterTerminalRegistry.t.sol`](./test/RouterTerminalRegistry.t.sol), [`test/RouterTerminalMultihopFork.t.sol`](./test/RouterTerminalMultihopFork.t.sol), [`test/RouterTerminalReentrancy.t.sol`](./test/RouterTerminalReentrancy.t.sol), [`test/RouterTerminalSandwichFork.t.sol`](./test/RouterTerminalSandwichFork.t.sol), [`test/TestAuditGaps.sol`](./test/TestAuditGaps.sol) |
17
19
 
18
20
  ## Repo Map
19
21
 
@@ -36,6 +38,11 @@ Universal routing terminal for Juicebox V6. This repo accepts many input tokens,
36
38
  ## Working Rules
37
39
 
38
40
  - Start in [`src/JBRouterTerminal.sol`](./src/JBRouterTerminal.sol) for execution behavior, but verify downstream semantics in the destination terminal before treating the router as the source of truth.
41
+ - The router intentionally synthesizes accounting contexts instead of storing a static accepted-token list. If token acceptance looks wrong, verify discovery logic before touching registry state.
39
42
  - Treat preview behavior, quote selection, and execution callbacks as tightly coupled. Changes in one usually need verification in the others.
40
43
  - When the input token is itself a Juicebox project token, follow the cash-out loop carefully. Recursive routing assumptions are where subtle bugs hide.
44
+ - Multi-hop and buyback-assisted routes are first-class behavior here, not edge cases. Verify them explicitly when changing route selection.
45
+ - Refund handling is route-specific state, not cleanup garnish. Baseline snapshots and partial-fill leftovers are part of correctness.
46
+ - Final terminal-facing receipt enforcement is a real boundary. If a terminal pull or forwarding model is non-standard, prove receipt semantics still hold before weakening guards.
47
+ - Callback guards and final-hop receipt checks are security boundaries. Do not weaken them to accommodate non-standard token paths.
41
48
  - If you touch registry behavior, verify project-specific overrides, allowlisting, and terminal locking all still match the intended governance model.