@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 +3 -3
- package/AUDIT_INSTRUCTIONS.md +4 -4
- package/CHANGE_LOG.md +9 -2
- package/README.md +2 -2
- package/RISKS.md +9 -9
- package/SKILLS.md +4 -4
- package/package.json +3 -3
- package/script/Deploy.s.sol +0 -1
- package/src/JBRouterTerminal.sol +157 -61
- package/src/JBRouterTerminalRegistry.sol +3 -7
- package/src/interfaces/IGeomeanOracle.sol +21 -0
- package/test/RouterTerminal.t.sol +56 -36
- package/test/RouterTerminalCashOutFork.t.sol +0 -1
- package/test/RouterTerminalCreditCashout.t.sol +0 -5
- package/test/RouterTerminalERC2771.t.sol +0 -5
- package/test/RouterTerminalFeeCashOutFork.t.sol +0 -1
- package/test/RouterTerminalFork.t.sol +0 -1
- package/test/RouterTerminalMultihopFork.t.sol +0 -1
- package/test/RouterTerminalPreviewFork.t.sol +7 -5
- package/test/RouterTerminalReentrancy.t.sol +0 -5
- package/test/RouterTerminalRegistry.t.sol +3 -1
- package/test/RouterTerminalSandwichFork.t.sol +0 -1
- package/test/TestAuditGaps.sol +5 -7
- package/test/audit/LeftoverRefund.t.sol +444 -0
- package/test/audit/PayerTrackerRefund.t.sol +14 -10
- package/test/audit/RefundToBeneficiary.t.sol +102 -0
- package/test/audit/{CodexRegistryAddToBalancePartialFill.t.sol → RegistryAddToBalancePartialFill.t.sol} +49 -38
- package/test/fork/V4QuoteAndSettlementFork.t.sol +0 -1
- package/test/invariant/RouterTerminalInvariant.t.sol +1 -5
- package/test/regression/CashOutLoopLimit.t.sol +1 -4
- package/test/regression/RouterTerminalEdgeCases.t.sol +49 -27
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()
|
|
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
|
|
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 (`
|
|
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
|
|
package/AUDIT_INSTRUCTIONS.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
- `
|
|
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 `
|
|
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**:
|
|
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
|
|
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.** `
|
|
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
|
|
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
|
|
17
|
-
- **Leftover token
|
|
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
|
|
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-
|
|
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
|
-
`
|
|
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
|
|
83
|
+
### 8.3 Cashout loop slippage uses proportional scaling
|
|
84
84
|
|
|
85
|
-
`_cashOutLoop` applies `minTokensReclaimed`
|
|
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
|
|
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
|
|
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 `
|
|
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 `
|
|
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.
|
|
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.
|
|
21
|
-
"@bananapus/core-v6": "^0.0.
|
|
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",
|
package/script/Deploy.s.sol
CHANGED
|
@@ -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(),
|