@bananapus/router-terminal-v6 0.0.20 → 0.0.22

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/ARCHITECTURE.md CHANGED
@@ -56,14 +56,14 @@ Payer → JBRouterTerminal.pay(projectId, token, amount)
56
56
  │ ├─ Quote & slippage (_pickPoolAndQuote):
57
57
  │ │ 1. User-provided quote (metadata "quoteForSwap") — used as-is
58
58
  │ │ 2. V3 fallback: 10-min TWAP via OracleLibrary.consult()
59
- │ │ 3. V4 fallback: spot price from getSlot0() (no built-in TWAP)
59
+ │ │ 3. V4 fallback: TWAP from oracle hook if available, else spot price from getSlot0()
60
60
  │ │ Apply sigmoid slippage: minSlippage + range * impact/(impact+K)
61
61
  │ │
62
62
  │ ├─ Execute swap via V3 pool.swap() or V4 POOL_MANAGER.unlock()
63
63
  │ │
64
64
  │ ├─ If native ETH input: wrap any remaining raw ETH (partial fills)
65
65
  │ ├─ If native ETH output: unwrap WETH → ETH (WETH.withdraw)
66
- │ └─ Return leftover input tokens via _resolveRefundTo (checks msg.sender's IJBPayerTracker.originalPayer() via try-catch, falls back to beneficiary/msgSender)
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
67
 
68
68
  ├─ Approve destination terminal for output tokens (or set msg.value for native)
69
69
  └─ Forward to destTerminal.pay() → return beneficiary token count
@@ -104,7 +104,7 @@ terminal source for loan sizing, debt normalization, or any other decimals-depen
104
104
 
105
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.
106
106
 
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 (`_resolveRefundTo`) 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.
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.
108
108
 
109
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.
110
110
 
@@ -83,7 +83,7 @@ Quote priority in `_pickPoolAndQuote()`:
83
83
 
84
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
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.
86
+ 3. **V4 TWAP-then-spot** -- `_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 tick from `getSlot0()`. Same sigmoid slippage formula applied to both paths. The spot fallback is manipulable within a single block.
87
87
 
88
88
  ### Sigmoid Slippage Formula
89
89
 
@@ -123,7 +123,7 @@ When the input token is a JB project token (detected via `TOKENS.projectIdOf()`
123
123
 
124
124
  **Iteration cap:** `_MAX_CASHOUT_ITERATIONS = 20`. Exceeding this reverts with `JBRouterTerminal_CashOutLoopLimit()`.
125
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.
126
+ **Slippage:** The `cashOutMinReclaimed` metadata value is applied to the first cashout step. Subsequent steps use proportionally scaled slippage protection based on the first step's ratio. The final output amount is also validated by the destination terminal's `minReturnedTokens` parameter.
127
127
 
128
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
129
 
@@ -187,7 +187,7 @@ The most complex and highest-value code path. Follow a payment from `pay()` thro
187
187
  7. `_beforeTransferFor()` -- allowance setup before forwarding
188
188
  8. `destTerminal.pay()` -- final forwarding
189
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()`.
190
+ **Key question:** Can any combination of inputs cause tokens to be stuck in the router? The contract has no sweep/rescue function. This is intentional — the router is stateless by design. After each swap, the full remaining input token balance is refunded to the caller. If tokens are accidentally sent to the contract (e.g., via `receive()` or direct ERC-20 transfers), they are absorbed into the next caller's refund. This is a deliberate design choice: recovering accidentally-sent funds is preferable to locking them permanently, and there should never be a persistent balance to protect.
191
191
 
192
192
  ### Callback Verification
193
193
 
@@ -298,7 +298,7 @@ No prior formal audit with finding IDs has been conducted on this codebase. All
298
298
 
299
299
  | Pattern | Where to Look | Why It's Dangerous |
300
300
  |---------|--------------|-------------------|
301
- | V4 spot price as TWAP fallback | `_getV4SpotQuote()` | V4 pools lack on-chain TWAP. Spot price is manipulable within a block. The sigmoid slippage mitigates but doesn't eliminate. |
301
+ | V4 oracle-then-spot fallback | `_getV4Quote()` | V4 pools attempt a 30-second TWAP via the oracle hook first. If unavailable, the spot price fallback is manipulable within a block. The sigmoid slippage mitigates but doesn't eliminate. |
302
302
  | Silent TWAP window degradation | `_getV3TwapQuote()` | If `oldestObservation < twapWindow`, the window is silently capped. A 10-second observation produces a near-spot quote labeled as "TWAP." |
303
303
  | Partial swap leftover refund | `_handleSwap()` | Leftover tokens are refunded to `_msgSender()`. If the caller is a contract, the refund must be receivable. Verify ETH refund doesn't fail silently. |
304
304
  | Stateless design assumption | All of `JBRouterTerminal` | No persistent balances. If any code path fails to forward or refund tokens, they're stuck permanently. No sweep function exists. |
package/CHANGE_LOG.md CHANGED
@@ -83,7 +83,7 @@ New V4-specific components:
83
83
  - `IPoolManager POOL_MANAGER` immutable (can be `address(0)` if V4 is unavailable).
84
84
  - `IUnlockCallback` interface implemented via `unlockCallback()`.
85
85
  - `_executeV4Swap()`, `_settleV4()`, `_takeV4()`, `_discoverV4Pool()` internal functions.
86
- - `_getV4SpotQuote()` — uses instantaneous spot price with sigmoid slippage (security note: not MEV-resistant; users should provide `quoteForSwap` metadata).
86
+ - `_getV4Quote()` — first attempts a 30-second TWAP from the pool's oracle hook (e.g., `IGeomeanOracle.observe()`); falls back to instantaneous spot price if no oracle hook exists or the call fails. Both paths apply sigmoid slippage (security note: spot fallback is not MEV-resistant; users should provide `quoteForSwap` metadata).
87
87
  - `_V4_FEES` and `_V4_TICK_SPACINGS` arrays for vanilla V4 pool search.
88
88
 
89
89
  ### 2.2 Automatic Pool Discovery
@@ -157,6 +157,13 @@ In v6, when a Permit2 allowance call fails during `_acceptFundsFor()`, an event
157
157
 
158
158
  ## 3. Event Changes
159
159
 
160
+ ### 3.0 Indexer Notes
161
+
162
+ This repo is the direct replacement for v5 swap-terminal indexing:
163
+ - all registry event families moved from `JBSwapTerminalRegistry_*` to `JBRouterTerminalRegistry_*`;
164
+ - registry events now include `caller`;
165
+ - route discovery is dynamic, so do not assume one fixed output token or one manually-registered default pool per project.
166
+
160
167
  ### 3.1 Registry Events Renamed
161
168
  All events were renamed from `JBSwapTerminalRegistry_*` to `JBRouterTerminalRegistry_*`.
162
169
 
@@ -282,7 +289,7 @@ v5 contained both `JBSwapTerminal.sol` and `JBSwapTerminal5_1.sol` (a minor revi
282
289
 
283
290
  ### 6.14 `_pickPoolAndQuote` Redesigned
284
291
  - **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.
285
- - **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`).
292
+ - **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 `_getV4Quote()` (for V4 pools, attempting oracle TWAP first then falling back to spot price). Reverts with `JBRouterTerminal_NoPoolFound` if no pool exists (v5 reverted with `JBSwapTerminal_NoDefaultPoolDefined`).
286
293
 
287
294
  ### 6.15 New Metadata Keys
288
295
  - `cashOutSource` — specifies a source project ID and credit amount for credit-based cashouts.
package/README.md CHANGED
@@ -192,7 +192,7 @@ routerTerminal.pay(projectId, usdc, 1000e6, beneficiary, 0, "swap with quote", m
192
192
 
193
193
  If no `quoteForSwap` is provided, the terminal calculates one automatically:
194
194
  - **V3 pools**: TWAP oracle with a configurable window (default 10 minutes, capped by oldest observation). Reverts if no observation history exists.
195
- - **V4 pools**: Spot tick price (V4 vanilla pools have no built-in TWAP oracle).
195
+ - **V4 pools**: The terminal first attempts a 30-second TWAP via the pool's oracle hook. If no oracle hook exists or the call fails, it falls back to the instantaneous spot tick. Both use the sigmoid slippage formula.
196
196
  - **Both**: Dynamic sigmoid slippage tolerance based on estimated price impact and pool fee. Range: 2% minimum to 88% maximum ceiling.
197
197
 
198
198
  ## Supported Chains
@@ -219,7 +219,7 @@ Each chain is configured with the appropriate WETH, Uniswap V3 Factory, and V4 P
219
219
  - The terminal never holds a token balance between transactions. After every swap, all output tokens are forwarded to the destination terminal, and leftover input tokens from partial fills are returned to the payer.
220
220
  - Pool discovery is dynamic -- the terminal searches V3 and V4 pools at runtime. If pool liquidity changes between discovery and execution, slippage protection prevents excessive losses.
221
221
  - The `receive()` function accepts ETH from any sender. This is required because ETH arrives from multiple sources: WETH unwraps, cash out reclaims from project terminals, and V4 PoolManager takes. The terminal processes all received ETH within the same transaction.
222
- - TWAP fallback: V3 pools with no TWAP observation history (`oldestObservation == 0`) will cause the transaction to revert with `JBRouterTerminal_NoObservationHistory()`. V4 uses spot price and does not require observations.
222
+ - TWAP fallback: V3 pools with no TWAP observation history (`oldestObservation == 0`) will cause the transaction to revert with `JBRouterTerminal_NoObservationHistory()`. V4 attempts a TWAP via the pool's oracle hook first, falling back to spot price if unavailable.
223
223
  - Uniswap V4 requires `cancun` EVM version (transient storage opcodes). On chains without EIP-1153 support, the terminal falls back to V3-only routing.
224
224
  - Credit cashouts require the payer to grant `TRANSFER_CREDITS` permission (ID 13) to the router terminal address. Without this, credit-based routing will revert.
225
225
  - The `_cashOutLoop` recursively cashes out JB project tokens. Deeply nested project token chains (project A's token backed by project B's token backed by project C's token, etc.) will consume more gas per level of recursion.
package/RISKS.md CHANGED
@@ -7,14 +7,14 @@
7
7
  - **JBDirectory.** Terminal resolution trusts `DIRECTORY.primaryTerminalOf()` and `DIRECTORY.terminalsOf()`. A compromised directory can redirect funds.
8
8
  - **PERMIT2.** Used as fallback for token transfers. Permit2 approvals can be exploited if users have stale allowances.
9
9
  - **Owner (Ownable).** Contract owner has no fund access but controls the registry terminal allowlist and default.
10
- - **`IJBPayerTracker` implementers.** `_resolveRefundTo` in the router terminal queries `IJBPayerTracker(msg.sender).originalPayer()` via try-catch. Any contract that is the `msg.sender` and implements `IJBPayerTracker` can direct leftover refunds to an arbitrary address. This is safe when the caller is a trusted intermediary (e.g. the registry), but a malicious `msg.sender` implementing `IJBPayerTracker` could redirect refunds. The risk is bounded: the caller must have already supplied the funds being routed, so it can only redirect leftovers from its own payment.
10
+ - **`IJBPayerTracker` implementers.** `_resolveRefundWithBackupRecipient` in the router terminal queries `IJBPayerTracker(msg.sender).originalPayer()` via try-catch. Any contract that is the `msg.sender` and implements `IJBPayerTracker` can direct leftover refunds to an arbitrary address. This is safe when the caller is a trusted intermediary (e.g. the registry), but a malicious `msg.sender` implementing `IJBPayerTracker` could redirect refunds. The risk is bounded: the caller must have already supplied the funds being routed, so it can only redirect leftovers from its own payment.
11
11
 
12
12
  ## 2. Economic / Manipulation Risks
13
13
 
14
- - **V4 spot price manipulation.** `_getV4SpotQuote` reads 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 are vulnerable to extraction. Front-ends MUST supply `quoteForSwap` metadata for V4 swaps. Note: `_getV4SpotQuote` 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.
14
+ - **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.
15
15
  - **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.
16
- - **Cashout loop value extraction.** `_cashOutLoop` iterates up to 20 times, cashing out JB project tokens recursively. Each cashout incurs bonding curve slippage. `minTokensReclaimed` is only applied to the first step -- subsequent steps have zero slippage protection. 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.
17
- - **Leftover token absorption.** `_handleSwap` wraps all remaining `address(this).balance` as WETH after swaps. Any ETH sent directly to the contract (via `receive()`) is absorbed into the next swap's leftover calculation and routed to the project -- not returned to the sender. There is no sweep mechanism ETH absorbed this way is permanently incorporated into the next routing operation. This is by design (the router is stateless and holds no balances across transactions), but users who accidentally send ETH directly to the contract should understand that it is unrecoverable.
16
+ - **Cashout loop value extraction.** `_cashOutLoop` iterates up to 20 times, cashing out JB project tokens recursively. Each cashout incurs bonding curve slippage. `minTokensReclaimed` is applied to the first step, and subsequent steps use proportionally scaled slippage protection. 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.
17
+ - **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.
18
18
  - **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.
19
19
  - **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.
20
20
 
@@ -46,12 +46,12 @@
46
46
  - **Registry default terminal change.** Projects without explicit terminal assignments use `defaultTerminal`. If the registry owner changes the default, all unlocked projects are silently migrated. `lockTerminalFor` mitigates this.
47
47
  - **safeIncreaseAllowance for terminal transfers.** `_beforeTransferFor` uses `safeIncreaseAllowance` which adds to existing allowance. If previous transactions left stale allowance, the cumulative allowance could exceed intended amounts.
48
48
  - **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.
49
- - **receive() function.** The contract accepts arbitrary ETH via `receive()`. This ETH is absorbed into the next swap operation rather than being recoverable.
49
+ - **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.
50
50
 
51
51
  ## 6. MEV Surface
52
52
 
53
53
  - **V3 path: TWAP-protected.** The 10-minute TWAP oracle makes single-block manipulation futile. Multi-block attacks require sustained capital.
54
- - **V4 path: spot-vulnerable.** Without `quoteForSwap` metadata, V4 swaps are exposed to up to sigmoid-slippage% loss per trade. The 2% floor bounds worst-case extraction.
54
+ - **V4 path: TWAP-first, spot-fallback.** V4 pools attempt a 30-second TWAP via the oracle hook first. If unavailable, the spot fallback is vulnerable. Without `quoteForSwap` metadata, V4 swaps using the spot fallback are exposed to up to sigmoid-slippage% loss per trade. The 2% floor bounds worst-case extraction.
55
55
  - **Cross-route arbitrage.** When JB routing bypasses the AMM (minting tokens directly), an arbitrage opportunity exists between the JB bonding curve price and the AMM price.
56
56
 
57
57
  ## 7. Invariants to Verify
@@ -78,8 +78,8 @@ Verified in `RouterTerminalReentrancy.t.sol`: re-entrant calls via both `pay()`
78
78
 
79
79
  ### 8.2 Router trusts `originalPayer()` from any `msg.sender` that implements it
80
80
 
81
- `_resolveRefundTo` calls `IJBPayerTracker(msg.sender).originalPayer()` in a try-catch. If the call succeeds and returns a non-zero address, leftover tokens from partial swap fills are sent to that address instead of the beneficiary or `_msgSender()`. The router does not verify that `msg.sender` is the registry or any specific contract -- it trusts any caller that implements the interface. This is accepted because: (1) the caller (`msg.sender`) is the entity that supplied the funds, so redirecting its own leftovers is a legitimate operation, (2) if the call reverts or returns `address(0)`, the router falls back to the normal beneficiary/`_msgSender()` logic, and (3) decoupling from the registry allows other intermediary contracts (e.g. batch payers, aggregators) to participate in refund routing without requiring changes to the router terminal.
81
+ `_resolveRefundWithBackupRecipient` calls `IJBPayerTracker(msg.sender).originalPayer()` in a try-catch. If the call succeeds and returns a non-zero address, leftover tokens from partial swap fills are sent to that address instead of the beneficiary or `_msgSender()`. The router does not verify that `msg.sender` is the registry or any specific contract -- it trusts any caller that implements the interface. This is accepted because: (1) the caller (`msg.sender`) is the entity that supplied the funds, so redirecting its own leftovers is a legitimate operation, (2) if the call reverts or returns `address(0)`, the router falls back to the normal beneficiary/`_msgSender()` logic, and (3) decoupling from the registry allows other intermediary contracts (e.g. batch payers, aggregators) to participate in refund routing without requiring changes to the router terminal.
82
82
 
83
- ### 8.3 Cashout loop slippage is first-hop only (accepted trade-off)
83
+ ### 8.3 Cashout loop slippage uses proportional scaling
84
84
 
85
- `_cashOutLoop` applies `minTokensReclaimed` only to the first cashout step. Subsequent recursive cashouts (steps 2-20) have zero explicit slippage protection. This is accepted because: (1) adding per-step slippage would require the caller to predict intermediate token amounts across an unknown chain of cashouts, which is impractical, (2) the first-hop slippage check ensures the initial conversion meets the user's expectation, and (3) the recursive path is deterministic — intermediate projects' bonding curves and rulesets are on-chain state that cannot be manipulated between steps within a single transaction. The risk is limited to scenarios where intermediate projects have very low liquidity, causing high bonding-curve slippage on small amounts.
85
+ `_cashOutLoop` applies `minTokensReclaimed` to the first cashout step. Subsequent recursive cashouts (steps 2-20) use proportionally scaled slippage protection based on the first step's ratio. This ensures intermediate hops have meaningful slippage bounds without requiring the caller to predict exact intermediate amounts. The final output is also validated by the destination terminal's `minReturnedTokens` parameter.
package/SKILLS.md CHANGED
@@ -60,7 +60,7 @@ If `tokenIn` is a JB project token, a cashout loop runs first (up to 20 iteratio
60
60
 
61
61
  ## Routing Architecture
62
62
 
63
- The router uses `_route` (mutative) and `_previewRoute` (view) as the top-level entry points. Both follow the same path: detect JB project tokens and `_cashOutLoop` if needed, then `_resolveTokenOut` to pick the output token, then `_convert` to execute the conversion (no-op, wrap/unwrap, or `_handleSwap`). Swap execution goes through `_pickPoolAndQuote` (discover pool, get TWAP or spot quote with sigmoid slippage), then dispatches to V3 (`uniswapV3SwapCallback`) or V4 (`unlockCallback`). Leftover input tokens from partial fills are returned to the `beneficiary` (for `pay()`) or `_msgSender()` (for `addToBalanceOf()`).
63
+ The router uses `_route` (mutative) and `_previewRoute` (view) as the top-level entry points. Both follow the same path: detect JB project tokens and `_cashOutLoop` if needed, then `_resolveTokenOut` to pick the output token, then `_convert` to execute the conversion (no-op, wrap/unwrap, or `_handleSwap`). Swap execution goes through `_pickPoolAndQuote` (discover pool, get TWAP or spot quote with sigmoid slippage), then dispatches to V3 (`uniswapV3SwapCallback`) or V4 (`unlockCallback`). Leftover input tokens from partial fills are returned via `_resolveRefundWithBackupRecipient`. When the caller (`msg.sender`) implements `IJBPayerTracker` and `originalPayer()` returns a non-zero address, leftovers go to that address. Otherwise, leftovers go to `beneficiary` (for `pay()`) or `_msgSender()` (for `addToBalanceOf()`).
64
64
 
65
65
  ## Integration Points
66
66
 
@@ -195,18 +195,18 @@ The router uses `_route` (mutative) and `_previewRoute` (view) as the top-level
195
195
  - When `tokenIn == NATIVE_TOKEN`, the terminal wraps ETH to WETH before swapping. When the output is `NATIVE_TOKEN`, it unwraps WETH after swapping.
196
196
  - The `receive()` function accepts ETH from any sender. This is necessary because ETH arrives from WETH unwraps, cashout reclaims from project terminals, and V4 PoolManager takes. The terminal handles all ETH within the same transaction.
197
197
  - **V3 TWAP**: Reverts with `JBRouterTerminal_NoObservationHistory()` when a V3 pool has no observation history, or with `JBRouterTerminal_InsufficientTwapHistory()` when the oldest observation is less than `MIN_TWAP_WINDOW` (120 seconds). The TWAP window is capped by the pool's oldest observation if shorter than 10 minutes.
198
- - **V4 spot price**: V4 vanilla pools have no built-in TWAP oracle. The terminal uses the current spot tick with the same sigmoid slippage formula.
198
+ - **V4 price**: V4 pools: the terminal first attempts to read 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 spot tick. Both use the sigmoid slippage formula.
199
199
  - **V4 requires cancun EVM**: Chains without EIP-1153 (transient storage) cannot use V4 routing. If `POOL_MANAGER` is `address(0)`, V4 discovery is skipped entirely.
200
200
  - **Preview estimates**: `previewPayFor()` returns exact values for direct and wrap-unwrap routes, and best-effort estimates for swap routes using current pool state or caller-provided quotes.
201
201
  - The `JBRouterTerminalRegistry` handles token custody during delegation -- it transfers tokens from the payer to itself, then approves and forwards to the underlying terminal.
202
202
  - `_msgSender()` (ERC-2771) is used instead of `msg.sender` for meta-transaction compatibility in both contracts.
203
203
  - The `JBSwapLib` library contains slippage tolerance math (sigmoid formula), price impact estimation, and V3-compatible `sqrtPriceLimitX96` calculation. It does not contain swap execution logic.
204
- - **Leftover handling**: After a swap, leftover input tokens (from partial fills where the price limit was hit) are returned via `_resolveRefundTo`. When the caller (`msg.sender`) implements `IJBPayerTracker` and `originalPayer()` returns a non-zero address, leftovers go to that address. Otherwise they go to the `beneficiary` (for `pay()`) or `_msgSender()` (for `addToBalanceOf()`). For native token inputs, any remaining raw ETH is wrapped to WETH first so the leftover check catches it.
204
+ - **Leftover handling**: After a swap, leftover input tokens (from partial fills where the price limit was hit) are returned via `_resolveRefundWithBackupRecipient`. When the caller (`msg.sender`) implements `IJBPayerTracker` and `originalPayer()` returns a non-zero address, leftovers go to that address. Otherwise, leftovers go to `beneficiary` (for `pay()`) or `_msgSender()` (for `addToBalanceOf()`). For native token inputs, any remaining raw ETH is wrapped to WETH first so the leftover check catches it.
205
205
  - **`IJBPayerTracker` decoupling**: The router terminal does not import or depend on `IJBRouterTerminalRegistry` for refund resolution. It queries `IJBPayerTracker(msg.sender).originalPayer()` via try-catch. Any intermediary contract that implements `IJBPayerTracker` can forward calls to the router and have leftovers returned to the original payer.
206
206
  - **Credit cashouts**: When using `cashOutSource` metadata, the payer must have granted `TRANSFER_CREDITS` permission (ID 13) to the router terminal for the source project. The router calls `TOKENS.transferCreditsFrom()` to pull credits.
207
207
  - **Cashout loop depth**: The `_cashOutLoop` iterates through JB project token chains with a cap of 20 iterations (`_MAX_CASHOUT_ITERATIONS`). Exceeding this limit reverts with `JBRouterTerminal_CashOutLoopLimit()`.
208
208
  - **V3 callback verification**: The `uniswapV3SwapCallback` verifies the caller by reading the pool's `fee()` and checking `FACTORY.getPool()`. This is standard V3 security.
209
- - **V4 amount overflow**: Both `_getV3TwapQuote` and `_getV4SpotQuote` revert if `amount > type(uint128).max` because `OracleLibrary.getQuoteAtTick` requires `uint128`.
209
+ - **V4 amount overflow**: Both `_getV3TwapQuote` and `_getV4Quote` revert if `amount > type(uint128).max` because `OracleLibrary.getQuoteAtTick` requires `uint128`.
210
210
  - **Disallowing the default terminal**: `disallowTerminal()` clears `defaultTerminal` if it matches the terminal being disallowed.
211
211
  - **Locking snapshots default**: `lockTerminalFor(projectId, expectedTerminal)` snapshots the current `defaultTerminal` into `_terminalOf[projectId]` if no explicit terminal was set, preventing future default changes from affecting locked projects. The `expectedTerminal` parameter prevents race conditions where the default changes between transaction submission and execution.
212
212
  - **Cashout loop limit**: `_cashOutLoop` is capped at 20 iterations. Circular JB token dependencies (A -> B -> A) will revert with `CashOutLoopLimit` instead of consuming all gas.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/router-terminal-v6",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,8 +17,8 @@
17
17
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-router-terminal-v6'"
18
18
  },
19
19
  "dependencies": {
20
- "@bananapus/address-registry-v6": "^0.0.15",
21
- "@bananapus/core-v6": "^0.0.27",
20
+ "@bananapus/address-registry-v6": "^0.0.16",
21
+ "@bananapus/core-v6": "^0.0.28",
22
22
  "@bananapus/permission-ids-v6": "^0.0.14",
23
23
  "@openzeppelin/contracts": "^5.6.1",
24
24
  "@uniswap/permit2": "github:Uniswap/permit2",
@@ -108,7 +108,6 @@ contract DeployScript is Script, Sphinx {
108
108
  JBRouterTerminal terminal = new JBRouterTerminal{salt: ROUTER_TERMINAL}({
109
109
  directory: core.directory,
110
110
  permissions: core.permissions,
111
- projects: core.projects,
112
111
  tokens: core.tokens,
113
112
  permit2: IPermit2(permit2),
114
113
  owner: safeAddress(),