@bananapus/router-terminal-v6 0.0.17 → 0.0.19
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 +21 -3
- package/ARCHITECTURE.md +57 -7
- package/AUDIT_INSTRUCTIONS.md +84 -9
- package/CHANGE_LOG.md +13 -1
- package/README.md +18 -12
- package/RISKS.md +13 -3
- package/SKILLS.md +43 -23
- package/STYLE_GUIDE.md +2 -2
- package/USER_JOURNEYS.md +6 -5
- package/foundry.toml +1 -1
- package/package.json +3 -3
- package/script/Deploy.s.sol +1 -1
- package/script/helpers/RouterTerminalDeploymentLib.sol +1 -1
- package/src/JBRouterTerminal.sol +60 -19
- package/src/JBRouterTerminalRegistry.sol +47 -9
- package/src/interfaces/IJBPayerTracker.sol +11 -0
- package/src/interfaces/IJBRouterTerminalRegistry.sol +3 -1
- package/src/libraries/JBSwapLib.sol +1 -1
- package/test/RouterTerminal.t.sol +1 -1
- package/test/RouterTerminalCashOutFork.t.sol +1 -1
- package/test/RouterTerminalCreditCashout.t.sol +1 -1
- package/test/RouterTerminalERC2771.t.sol +1 -1
- package/test/RouterTerminalFeeCashOutFork.t.sol +1 -1
- package/test/RouterTerminalFork.t.sol +1 -1
- package/test/RouterTerminalMultihopFork.t.sol +1 -1
- package/test/RouterTerminalPreviewFork.t.sol +1 -1
- package/test/RouterTerminalReentrancy.t.sol +1 -1
- package/test/RouterTerminalRegistry.t.sol +8 -4
- package/test/RouterTerminalSandwichFork.t.sol +1 -1
- package/test/TestAuditGaps.sol +298 -8
- package/test/audit/CodexRegistryAddToBalancePartialFill.t.sol +463 -0
- package/test/audit/PayerTrackerRefund.t.sol +177 -0
- package/test/audit/Permit2AllowanceFailed.t.sol +158 -0
- package/test/fork/V4QuoteAndSettlementFork.t.sol +1 -1
- package/test/invariant/RouterTerminalInvariant.t.sol +1 -1
- package/test/regression/CashOutLoopLimit.t.sol +1 -1
- package/test/regression/LockTerminalRace.t.sol +1 -1
- package/test/regression/RouterTerminalEdgeCases.t.sol +465 -0
- package/test/regression/V4SpotPriceSlippage.t.sol +1 -1
package/ADMINISTRATION.md
CHANGED
|
@@ -13,7 +13,7 @@ Admin privileges and their scope in nana-router-terminal-v6.
|
|
|
13
13
|
### 2. Project Owner / SET_ROUTER_TERMINAL Delegate
|
|
14
14
|
|
|
15
15
|
**Contract**: `JBRouterTerminalRegistry`
|
|
16
|
-
**Assigned via**: Ownership of the project's ERC-721 NFT (via `JBProjects.ownerOf(projectId)`), or delegation via `JBPermissions` with permission ID `SET_ROUTER_TERMINAL` (
|
|
16
|
+
**Assigned via**: Ownership of the project's ERC-721 NFT (via `JBProjects.ownerOf(projectId)`), or delegation via `JBPermissions` with permission ID `SET_ROUTER_TERMINAL` (29).
|
|
17
17
|
**Scope**: Per-project -- controls which router terminal a specific project uses, and can permanently lock that choice.
|
|
18
18
|
|
|
19
19
|
### 3. Router Terminal Owner (Ownable)
|
|
@@ -22,12 +22,26 @@ Admin privileges and their scope in nana-router-terminal-v6.
|
|
|
22
22
|
**Assigned via**: Constructor parameter `owner`, transferable via `Ownable.transferOwnership()`.
|
|
23
23
|
**Scope**: Currently unused. `JBRouterTerminal` inherits `Ownable` but does not gate any functions behind `onlyOwner`. The owner exists for potential future use or subclass extensions. The `Ownable.renounceOwnership()` and `Ownable.transferOwnership()` functions are inherited but have no practical effect on the terminal's operation.
|
|
24
24
|
|
|
25
|
+
**Risk note:** While the owner has no current powers over the terminal's operation, the `Ownable` inheritance means a future code change or subclass could introduce `onlyOwner`-gated functions. If the terminal is deployed with a specific owner address, that address retains transfer rights indefinitely.
|
|
26
|
+
|
|
25
27
|
### 4. Credit Cashout Payer (Implicit)
|
|
26
28
|
|
|
27
29
|
**Contract**: `JBRouterTerminal`
|
|
28
30
|
**Required permission**: `TRANSFER_CREDITS` (permission ID 13) -- must be granted by the payer to the router terminal address for the source project via `JBPermissions`.
|
|
29
31
|
**Scope**: Per-transaction. Required only when using the `cashOutSource` metadata key to route payments through credit cashouts.
|
|
30
32
|
|
|
33
|
+
## Terminal Resolution
|
|
34
|
+
|
|
35
|
+
When a payment is forwarded through the registry, the terminal is resolved as follows:
|
|
36
|
+
|
|
37
|
+
1. If the project has called `setTerminalFor(projectId, terminal)`, that explicit terminal is used.
|
|
38
|
+
2. If no explicit terminal is set, the registry's `defaultTerminal` is used.
|
|
39
|
+
3. If neither exists, the forwarding reverts.
|
|
40
|
+
|
|
41
|
+
**Lock semantics:** When `lockTerminalFor()` is called on a project with no explicit terminal, the current default is snapshot into `_terminalOf[projectId]` before locking. The project becomes independent of future default changes.
|
|
42
|
+
|
|
43
|
+
**Disallow interaction:** If the registry owner calls `disallowTerminal()` on the current default terminal, the `defaultTerminal` is automatically cleared (set to `address(0)`). Projects relying on the default (without locking) would lose their terminal resolution until a new default is set. Projects should lock their terminal to avoid disruption.
|
|
44
|
+
|
|
31
45
|
## Privileged Functions
|
|
32
46
|
|
|
33
47
|
### JBRouterTerminalRegistry
|
|
@@ -37,8 +51,8 @@ Admin privileges and their scope in nana-router-terminal-v6.
|
|
|
37
51
|
| `allowTerminal(terminal)` | Registry Owner | `onlyOwner` | Global | Adds a terminal to the allowlist (`isTerminalAllowed[terminal] = true`). Projects can only use allowlisted terminals. |
|
|
38
52
|
| `disallowTerminal(terminal)` | Registry Owner | `onlyOwner` | Global | Removes a terminal from the allowlist. Also clears `defaultTerminal` if it matches the disallowed terminal. Does NOT affect projects that have already locked their terminal. |
|
|
39
53
|
| `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. |
|
|
40
|
-
| `setTerminalFor(projectId, terminal)` | Project Owner or Delegate | `SET_ROUTER_TERMINAL` (
|
|
41
|
-
| `lockTerminalFor(projectId, expectedTerminal)` | Project Owner or Delegate | `SET_ROUTER_TERMINAL` (
|
|
54
|
+
| `setTerminalFor(projectId, terminal)` | Project Owner or Delegate | `SET_ROUTER_TERMINAL` (29) | Per-project | Routes a specific project to a specific allowed terminal. Reverts if the terminal is not allowlisted or if the project's terminal is locked. |
|
|
55
|
+
| `lockTerminalFor(projectId, expectedTerminal)` | Project Owner or Delegate | `SET_ROUTER_TERMINAL` (29) | Per-project | Permanently locks the terminal choice for a project. If no explicit terminal is set, snapshots the current default into `_terminalOf[projectId]`. Reverts with `TerminalMismatch` if the resolved terminal differs from `expectedTerminal` (race condition protection). **Irreversible.** |
|
|
42
56
|
|
|
43
57
|
### JBRouterTerminal
|
|
44
58
|
|
|
@@ -62,6 +76,8 @@ The following values are set at deploy time and cannot be changed:
|
|
|
62
76
|
|
|
63
77
|
| Property | Set At | Mutable? | Description |
|
|
64
78
|
|----------|--------|----------|-------------|
|
|
79
|
+
| `PERMISSIONS` | Constructor | No | JB permissions registry for permission checks |
|
|
80
|
+
| `Trusted forwarder` | Constructor | No | ERC-2771 trusted forwarder for meta-transactions |
|
|
65
81
|
| `DIRECTORY` | Constructor | No | JB directory for terminal/controller lookups |
|
|
66
82
|
| `PROJECTS` | Constructor | No | JB project NFT registry |
|
|
67
83
|
| `TOKENS` | Constructor | No | JB token manager for credit transfers and project token lookups |
|
|
@@ -79,6 +95,8 @@ The following values are set at deploy time and cannot be changed:
|
|
|
79
95
|
|
|
80
96
|
| Property | Set At | Mutable? | Description |
|
|
81
97
|
|----------|--------|----------|-------------|
|
|
98
|
+
| `PERMISSIONS` | Constructor | No | JB permissions registry for permission checks |
|
|
99
|
+
| `Trusted forwarder` | Constructor | No | ERC-2771 trusted forwarder for meta-transactions |
|
|
82
100
|
| `PROJECTS` | Constructor | No | JB project NFT registry |
|
|
83
101
|
| `PERMIT2` | Constructor | No | Permit2 contract for gasless approvals |
|
|
84
102
|
|
package/ARCHITECTURE.md
CHANGED
|
@@ -11,6 +11,7 @@ src/
|
|
|
11
11
|
├── JBRouterTerminal.sol — Payment routing: swap + forward to destination terminal
|
|
12
12
|
├── JBRouterTerminalRegistry.sol — Registry mapping projects to router terminal configs
|
|
13
13
|
├── interfaces/
|
|
14
|
+
│ ├── IJBPayerTracker.sol
|
|
14
15
|
│ ├── IJBRouterTerminal.sol
|
|
15
16
|
│ ├── IJBRouterTerminalRegistry.sol
|
|
16
17
|
│ └── IWETH9.sol
|
|
@@ -25,13 +26,47 @@ src/
|
|
|
25
26
|
### Payment Routing
|
|
26
27
|
```
|
|
27
28
|
Payer → JBRouterTerminal.pay(projectId, token, amount)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
→
|
|
33
|
-
→
|
|
34
|
-
|
|
29
|
+
│
|
|
30
|
+
├─ Accept funds (msg.value for native, Permit2 pull for ERC-20, or credit transfer)
|
|
31
|
+
│
|
|
32
|
+
├─ If input is a JB project token (credit or ERC-20):
|
|
33
|
+
│ → Cash out recursively via _cashOutLoop (up to 20 hops)
|
|
34
|
+
│ → Reclaimed token becomes the new tokenIn
|
|
35
|
+
│
|
|
36
|
+
├─ Resolve tokenOut: what does the destination project accept?
|
|
37
|
+
│ 1. Metadata override ("routeTokenOut")
|
|
38
|
+
│ 2. Direct acceptance — project already accepts tokenIn
|
|
39
|
+
│ 3. NATIVE ↔ WETH equivalence check
|
|
40
|
+
│ 4. Dynamic discovery — iterate project terminals, find swappable token
|
|
41
|
+
│
|
|
42
|
+
├─ Convert tokenIn → tokenOut via _convert:
|
|
43
|
+
│ ├─ Same token: no-op
|
|
44
|
+
│ ├─ NATIVE ↔ WETH: wrap (WETH.deposit) or unwrap (WETH.withdraw)
|
|
45
|
+
│ └─ Different tokens: Uniswap swap
|
|
46
|
+
│ │
|
|
47
|
+
│ ├─ If native ETH input: held as raw ETH until V3 callback
|
|
48
|
+
│ │ wraps (WETH.deposit) only the amount the pool consumes
|
|
49
|
+
│ │
|
|
50
|
+
│ ├─ Pool discovery (_discoverPool):
|
|
51
|
+
│ │ Search V3 pools across 4 fee tiers (0.3%, 0.05%, 1%, 0.01%)
|
|
52
|
+
│ │ Search V4 pools across same fee tiers (if PoolManager deployed)
|
|
53
|
+
│ │ Compare in-range liquidity across all candidates
|
|
54
|
+
│ │ Select the single pool with highest liquidity
|
|
55
|
+
│ │
|
|
56
|
+
│ ├─ Quote & slippage (_pickPoolAndQuote):
|
|
57
|
+
│ │ 1. User-provided quote (metadata "quoteForSwap") — used as-is
|
|
58
|
+
│ │ 2. V3 fallback: 10-min TWAP via OracleLibrary.consult()
|
|
59
|
+
│ │ 3. V4 fallback: spot price from getSlot0() (no built-in TWAP)
|
|
60
|
+
│ │ Apply sigmoid slippage: minSlippage + range * impact/(impact+K)
|
|
61
|
+
│ │
|
|
62
|
+
│ ├─ Execute swap via V3 pool.swap() or V4 POOL_MANAGER.unlock()
|
|
63
|
+
│ │
|
|
64
|
+
│ ├─ If native ETH input: wrap any remaining raw ETH (partial fills)
|
|
65
|
+
│ ├─ If native ETH output: unwrap WETH → ETH (WETH.withdraw)
|
|
66
|
+
│ └─ Return leftover input tokens via _resolveRefundTo (checks msg.sender's IJBPayerTracker.originalPayer() via try-catch, falls back to beneficiary/msgSender)
|
|
67
|
+
│
|
|
68
|
+
├─ Approve destination terminal for output tokens (or set msg.value for native)
|
|
69
|
+
└─ Forward to destTerminal.pay() → return beneficiary token count
|
|
35
70
|
```
|
|
36
71
|
|
|
37
72
|
### Preview Routing
|
|
@@ -48,6 +83,7 @@ Caller → JBRouterTerminal.previewPayFor(projectId, token, amount)
|
|
|
48
83
|
|-------|-----------|---------|
|
|
49
84
|
| Terminal | `IJBTerminal` | Acts as a terminal that routes payments |
|
|
50
85
|
| Registry | `IJBRouterTerminalRegistry` | Maps projects to routing configs |
|
|
86
|
+
| Payer tracker | `IJBPayerTracker` | Exposes the original payer of a forwarded call for refund resolution |
|
|
51
87
|
| Permit | `IJBPermitTerminal` | Permit2 token approval support |
|
|
52
88
|
|
|
53
89
|
## Composition Boundary
|
|
@@ -58,6 +94,20 @@ tokens and probes `IERC20Metadata.decimals()` for ERC-20s (falling back to `18`
|
|
|
58
94
|
forwards that value unchanged. Treat the router layer as a payment router only, not as an accounting-sensitive
|
|
59
95
|
terminal source for loan sizing, debt normalization, or any other decimals-dependent logic.
|
|
60
96
|
|
|
97
|
+
## Design Decisions
|
|
98
|
+
|
|
99
|
+
**Why the router is a terminal, not a standalone contract.** By implementing `IJBTerminal`, the router can be set as a project's terminal in the directory. Payers and frontend integrations call the same `pay()` / `addToBalanceOf()` interface they use for any terminal — no special routing code required on the caller side. The router accepts funds, converts them, and forwards to the real destination terminal in a single transaction.
|
|
100
|
+
|
|
101
|
+
**Why both Uniswap V3 and V4 support.** V4 pools may offer deeper liquidity for certain pairs (especially native-ETH pairs that V4 handles natively), while V3 pools have years of established liquidity. The router searches both and picks the pool with the highest in-range liquidity, giving payers the best available execution without requiring them to know which protocol version has the better pool.
|
|
102
|
+
|
|
103
|
+
**Why synthetic accounting contexts.** The router accepts any token and converts it before forwarding. It never holds balances between transactions, so it has no meaningful accounting of its own. `accountingContextForTokenOf()` returns a best-effort context (probing `decimals()` with an 18-decimal fallback) purely so the directory can register it. The real accounting happens at the destination terminal.
|
|
104
|
+
|
|
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
|
+
|
|
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.
|
|
108
|
+
|
|
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
|
+
|
|
61
111
|
## Dependencies
|
|
62
112
|
- `@bananapus/core-v6` — Terminal, directory, permissions
|
|
63
113
|
- `@uniswap/v3-core` + `v3-periphery` — V3 swap routing
|
package/AUDIT_INSTRUCTIONS.md
CHANGED
|
@@ -12,8 +12,8 @@ Two contracts. One library. One struct.
|
|
|
12
12
|
|
|
13
13
|
| Contract | Lines | Role |
|
|
14
14
|
|----------|-------|------|
|
|
15
|
-
| `JBRouterTerminal` | ~1,
|
|
16
|
-
| `JBRouterTerminalRegistry` | ~
|
|
15
|
+
| `JBRouterTerminal` | ~1,672 | Core routing engine. Accepts any token, discovers destination project's accepted token, converts via wrap/unwrap, Uniswap V3 swap, Uniswap V4 swap, or JB token cashout chain, then forwards to destination terminal. Implements `IJBTerminal`, `IJBPermitTerminal`, `IUniswapV3SwapCallback`, `IUnlockCallback`. |
|
|
16
|
+
| `JBRouterTerminalRegistry` | ~514 | Maps projects to their preferred router terminal instance. Owner-managed allowlist of terminals. Default terminal fallback. Lock terminal pattern for immutability. Implements `IJBTerminal`. |
|
|
17
17
|
| `JBSwapLib` (library) | ~161 | Continuous sigmoid slippage tolerance calculation. Price impact estimation. `sqrtPriceLimitX96` computation from input/output amounts. |
|
|
18
18
|
| `PoolInfo` (struct) | ~14 | Tagged union: `{isV4, v3Pool, v4Key}`. Carries the winning pool from discovery. |
|
|
19
19
|
|
|
@@ -143,9 +143,9 @@ The registry implements a two-phase terminal assignment:
|
|
|
143
143
|
|
|
144
144
|
**JBRouterTerminal:** Uses balance-delta accounting in `_acceptFundsFor()` (lines 569-575). Measures `balanceBefore` and `balanceAfter` the transfer, returning the actual amount received. This correctly handles fee-on-transfer tokens at the acceptance stage. However, the amount forwarded to the destination terminal is the delta, which may differ from what the destination terminal expects if it also performs balance-delta checks.
|
|
145
145
|
|
|
146
|
-
**JBRouterTerminalRegistry:** Does NOT use balance-delta accounting in `_acceptFundsFor()` (line
|
|
146
|
+
**JBRouterTerminalRegistry:** Does NOT use balance-delta accounting in `_acceptFundsFor()` (line 432). It returns the user-supplied `amount` directly. If a fee-on-transfer token is used through the registry, the forwarded amount will exceed actual tokens received, causing a downstream revert or incorrect accounting.
|
|
147
147
|
|
|
148
|
-
**Audit focus:** Verify that the registry's `_acceptFundsFor` cannot be exploited with fee-on-transfer tokens. The comment on lines
|
|
148
|
+
**Audit focus:** Verify that the registry's `_acceptFundsFor` cannot be exploited with fee-on-transfer tokens. The comment on lines 466-468 states these are "not supported by design" -- confirm this is documented clearly enough and that no path silently loses funds.
|
|
149
149
|
|
|
150
150
|
## uint160 Permit2 Truncation Risk
|
|
151
151
|
|
|
@@ -252,7 +252,7 @@ forge test
|
|
|
252
252
|
|
|
253
253
|
### Foundry Configuration
|
|
254
254
|
|
|
255
|
-
- Solidity 0.8.26, EVM target `cancun`, optimizer 200 runs
|
|
255
|
+
- Solidity ^0.8.26 (^0.8.28 for JBRouterTerminalRegistry), EVM target `cancun`, optimizer 200 runs
|
|
256
256
|
- Fuzz runs: 4,096 per test
|
|
257
257
|
- Invariant runs: 1,024 with depth 100
|
|
258
258
|
- Fork tests pinned to Ethereum mainnet block 21,700,000 (post-V4 deployment)
|
|
@@ -261,14 +261,22 @@ forge test
|
|
|
261
261
|
|
|
262
262
|
| Test File | Type | Coverage Area | Count |
|
|
263
263
|
|-----------|------|---------------|-------|
|
|
264
|
-
| `RouterTerminal.t.sol` | Unit (mocked) | Core routing, swap paths, cashout, V4, pool discovery, TWAP, errors | ~
|
|
265
|
-
| `RouterTerminalRegistry.t.sol` | Unit (mocked) | Allow/disallow, set/lock, forwarding, permissions | ~
|
|
264
|
+
| `RouterTerminal.t.sol` | Unit (mocked) | Core routing, swap paths, cashout, V4, pool discovery, TWAP, errors | ~39 |
|
|
265
|
+
| `RouterTerminalRegistry.t.sol` | Unit (mocked) | Allow/disallow, set/lock, forwarding, permissions | ~18 |
|
|
266
266
|
| `RouterTerminalFork.t.sol` | Fork (mainnet) | End-to-end swaps: ETH->USDC, USDC->ETH, ETH->DAI, addToBalance, quote metadata | ~12 |
|
|
267
|
+
| `RouterTerminalCashOutFork.t.sol` | Fork (mainnet) | Cashout routing through terminals | ~5 |
|
|
268
|
+
| `RouterTerminalCreditCashout.t.sol` | Unit (mocked) | Credit cashout path | ~6 |
|
|
269
|
+
| `RouterTerminalERC2771.t.sol` | Unit (mocked) | ERC-2771 meta-transaction forwarding | ~3 |
|
|
267
270
|
| `RouterTerminalFeeCashOutFork.t.sol` | Fork (mainnet) | Fee routing through cashout: project 3 payouts -> fee -> cashout -> project 1 | 1 |
|
|
271
|
+
| `RouterTerminalMultihopFork.t.sol` | Fork (mainnet) | Multi-hop cashout chains across projects | ~3 |
|
|
272
|
+
| `RouterTerminalPreviewFork.t.sol` | Fork (mainnet) | Preview functions for pay and cashout routing | ~4 |
|
|
273
|
+
| `RouterTerminalReentrancy.t.sol` | Unit (mocked) | Reentrancy scenarios through callbacks | ~3 |
|
|
268
274
|
| `RouterTerminalSandwichFork.t.sol` | Fork (mainnet) | MEV/sandwich: V3 TWAP resistance, V4 spot manipulation, user quote | 4 |
|
|
275
|
+
| `fork/V4QuoteAndSettlementFork.t.sol` | Fork (mainnet) | V4 quote computation and settlement | ~4 |
|
|
276
|
+
| `invariant/RouterTerminalInvariant.t.sol` | Invariant | Zero-balance, callback verification, loop termination (9 invariants) | 9 |
|
|
269
277
|
| `regression/LockTerminalRace.t.sol` | Unit | Race condition in `lockTerminalFor` with `expectedTerminal` | 3 |
|
|
270
278
|
| `regression/CashOutLoopLimit.t.sol` | Unit | Circular cashout loop cap at 20 iterations | 2 |
|
|
271
|
-
| `regression/V4SpotPriceSlippage.t.sol` | Unit + fuzz | Sigmoid math: floor, ceiling, monotonicity, bounded range, user quote |
|
|
279
|
+
| `regression/V4SpotPriceSlippage.t.sol` | Unit + fuzz | Sigmoid math: floor, ceiling, monotonicity, bounded range, user quote | ~17 |
|
|
272
280
|
|
|
273
281
|
### Coverage Gaps Worth Investigating
|
|
274
282
|
|
|
@@ -276,8 +284,75 @@ forge test
|
|
|
276
284
|
|------|--------|----------------|
|
|
277
285
|
| Fee-on-transfer tokens | NOT TESTED | Registry `_acceptFundsFor` does not use balance-delta |
|
|
278
286
|
| Short TWAP windows (<60s) | NOT TESTED | Silently degrades to near-spot-price |
|
|
279
|
-
| Multi-hop cashout chains (>1 step) |
|
|
287
|
+
| Multi-hop cashout chains (>1 step) | TESTED | `RouterTerminalMultihopFork.t.sol` covers multi-hop cashout chains (~3 tests) |
|
|
280
288
|
| V4 pools with custom hooks | NOT TESTED | All V4 tests use `hooks: IHooks(address(0))` |
|
|
281
289
|
| Concurrent pay + addToBalance | NOT TESTED | Mitigated by stateless design but worth verifying |
|
|
282
290
|
| Credit cashout path (fork) | NOT TESTED | Only unit-mocked, no fork test with real credits |
|
|
283
291
|
| addToBalanceOf with cashout routing | PARTIALLY | Fork covers ETH->USDC, not cashout or credit paths |
|
|
292
|
+
|
|
293
|
+
## Previous Audit Findings
|
|
294
|
+
|
|
295
|
+
No prior formal audit with finding IDs has been conducted on this codebase. All risk analysis is internal. See [RISKS.md](./RISKS.md) for known risks.
|
|
296
|
+
|
|
297
|
+
## Anti-Patterns to Hunt
|
|
298
|
+
|
|
299
|
+
| Pattern | Where to Look | Why It's Dangerous |
|
|
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. |
|
|
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
|
+
| 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
|
+
| 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. |
|
|
305
|
+
| Registry default terminal | `JBRouterTerminalRegistry` | Owner changing `defaultTerminal` redirects all unlocked projects. A compromised owner could redirect payments. |
|
|
306
|
+
| `receive()` is empty | `JBRouterTerminal` | Accepts ETH but has no recovery mechanism. ETH sent directly (not through `pay()`) is permanently lost. |
|
|
307
|
+
| Fee-on-transfer in registry | `JBRouterTerminalRegistry._acceptFundsFor()` | Returns user-supplied `amount`, not actual balance delta. Fee-on-transfer tokens cause downstream accounting mismatch. |
|
|
308
|
+
|
|
309
|
+
## Error Reference
|
|
310
|
+
|
|
311
|
+
| Error | Contract | Trigger |
|
|
312
|
+
|-------|----------|---------|
|
|
313
|
+
| `JBRouterTerminal_AmountOverflow(uint256)` | JBRouterTerminal | Amount exceeds `type(uint160).max` during Permit2 transfer, or exceeds `type(uint128).max` in V3 TWAP quote computation. |
|
|
314
|
+
| `JBRouterTerminal_CallerNotPool(address)` | JBRouterTerminal | `uniswapV3SwapCallback()` called by an address that does not match the expected V3 pool computed via `FACTORY.getPool()`. |
|
|
315
|
+
| `JBRouterTerminal_CallerNotPoolManager(address)` | JBRouterTerminal | `unlockCallback()` called by an address other than `POOL_MANAGER`. |
|
|
316
|
+
| `JBRouterTerminal_CashOutLoopLimit()` | JBRouterTerminal | `_cashOutLoop()` or `_previewCashOutLoop()` exceeds 20 iterations without resolving to an accepted token. |
|
|
317
|
+
| `JBRouterTerminal_NoCashOutPath(uint256, uint256)` | JBRouterTerminal | `_findCashOutPath()` cannot find any terminal on the source project that reclaims a usable token for the destination. |
|
|
318
|
+
| `JBRouterTerminal_NoLiquidity()` | JBRouterTerminal | V3 or V4 pool selected for quoting has zero in-range liquidity. |
|
|
319
|
+
| `JBRouterTerminal_NoMsgValueAllowed(uint256)` | JBRouterTerminal | `msg.value != 0` when paying with ERC-20 tokens or credits (ETH not expected on these paths). |
|
|
320
|
+
| `JBRouterTerminal_NoObservationHistory()` | JBRouterTerminal | V3 pool's oldest observation is 0 seconds old -- no TWAP data available. |
|
|
321
|
+
| `JBRouterTerminal_NoPoolFound(address, address)` | JBRouterTerminal | `_discoverPool()` or `_pickPoolAndQuote()` found no V3 or V4 pool with non-zero liquidity for the given token pair. |
|
|
322
|
+
| `JBRouterTerminal_NoRouteFound(uint256, address)` | JBRouterTerminal | `_resolveTokenOut()` cannot find any terminal on the destination project that accepts the input token or a swappable alternative. |
|
|
323
|
+
| `JBRouterTerminal_PermitAllowanceNotEnough(uint256, uint256)` | JBRouterTerminal | The Permit2 single-use allowance provided in metadata is less than the payment amount. |
|
|
324
|
+
| `JBRouterTerminal_SlippageExceeded(uint256, uint256)` | JBRouterTerminal | Swap output or cashout reclaim amount is below the required minimum (`minAmountOut` or `minTokensReclaimed`). |
|
|
325
|
+
| `JBRouterTerminalRegistry_AmountOverflow()` | JBRouterTerminalRegistry | Amount exceeds `type(uint160).max` during Permit2 transfer. |
|
|
326
|
+
| `JBRouterTerminalRegistry_NoMsgValueAllowed(uint256)` | JBRouterTerminalRegistry | `msg.value != 0` when paying with ERC-20 tokens through the registry. |
|
|
327
|
+
| `JBRouterTerminalRegistry_PermitAllowanceNotEnough(uint256, uint256)` | JBRouterTerminalRegistry | Permit2 single-use allowance in metadata is less than the payment amount. |
|
|
328
|
+
| `JBRouterTerminalRegistry_TerminalLocked(uint256)` | JBRouterTerminalRegistry | `setTerminalFor()` called for a project whose terminal has already been locked via `lockTerminalFor()`. |
|
|
329
|
+
| `JBRouterTerminalRegistry_TerminalMismatch(IJBTerminal, IJBTerminal)` | JBRouterTerminalRegistry | `lockTerminalFor()` called with an `expectedTerminal` that does not match the project's currently resolved terminal. |
|
|
330
|
+
| `JBRouterTerminalRegistry_TerminalNotAllowed(IJBTerminal)` | JBRouterTerminalRegistry | `setTerminalFor()` called with a terminal not on the owner's allowlist. |
|
|
331
|
+
| `JBRouterTerminalRegistry_TerminalNotSet(uint256)` | JBRouterTerminalRegistry | `lockTerminalFor()` called for a project with no explicit terminal and no default terminal configured. |
|
|
332
|
+
| `JBRouterTerminalRegistry_ZeroAddress()` | JBRouterTerminalRegistry | `setDefaultTerminal()` called with `address(0)`. |
|
|
333
|
+
|
|
334
|
+
## Compiler and Version Info
|
|
335
|
+
|
|
336
|
+
- **Solidity**: ^0.8.26 (^0.8.28 for JBRouterTerminalRegistry)
|
|
337
|
+
- **EVM target**: Cancun
|
|
338
|
+
- **Optimizer**: via-IR, 200 runs
|
|
339
|
+
- **Dependencies**: OpenZeppelin 5.x, Uniswap V3/V4, nana-core-v6
|
|
340
|
+
- **Build**: `forge build` (Foundry)
|
|
341
|
+
|
|
342
|
+
## How to Report Findings
|
|
343
|
+
|
|
344
|
+
For each finding:
|
|
345
|
+
|
|
346
|
+
1. **Title** -- one line, starts with severity (CRITICAL/HIGH/MEDIUM/LOW)
|
|
347
|
+
2. **Affected contract(s)** -- exact file path and line numbers
|
|
348
|
+
3. **Description** -- what is wrong, in plain language
|
|
349
|
+
4. **Trigger sequence** -- step-by-step, minimal steps to reproduce
|
|
350
|
+
5. **Impact** -- what an attacker gains, what a user loses (with numbers if possible)
|
|
351
|
+
6. **Proof** -- code trace showing the exact execution path, or a Foundry test
|
|
352
|
+
7. **Fix** -- minimal code change that resolves the issue
|
|
353
|
+
|
|
354
|
+
**Severity guide:**
|
|
355
|
+
- **CRITICAL**: Direct fund loss, tokens stuck permanently, or system insolvency.
|
|
356
|
+
- **HIGH**: Conditional fund loss, routing bypass, or broken invariant.
|
|
357
|
+
- **MEDIUM**: Value leakage, suboptimal routing, griefing.
|
|
358
|
+
- **LOW**: Informational, edge-case-only with no material impact.
|
package/CHANGE_LOG.md
CHANGED
|
@@ -4,6 +4,16 @@ This document describes all changes between `nana-swap-terminal` (v5) and `nana-
|
|
|
4
4
|
|
|
5
5
|
**Note:** This repo was renamed from `nana-swap-terminal` to `nana-router-terminal` in v6.
|
|
6
6
|
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
This release represents a **philosophical shift from configured to automatic**: the swap terminal required manual pool registration and per-project TWAP configuration, while the router terminal automatically discovers the best route for any payment.
|
|
10
|
+
|
|
11
|
+
- **Renamed `JBSwapTerminal` → `JBRouterTerminal`**: Reflects the broader scope — not just swapping, but full payment routing including JB token cashouts, credit transfers, and multi-hop resolution.
|
|
12
|
+
- **Automatic pool discovery**: Pools are auto-discovered across both Uniswap V3 and V4 by scanning all fee tiers and selecting the highest-liquidity pool. No manual `addDefaultPool()` needed.
|
|
13
|
+
- **Automatic token route discovery**: The terminal dynamically determines what token a destination project accepts by querying terminals and accounting contexts.
|
|
14
|
+
- **JB token cashout routing**: Can recursively cash out JB project tokens (ERC-20 or credits) from source projects before routing the reclaimed tokens to the destination.
|
|
15
|
+
- **Shared `JBSwapLib` library**: Swap math extracted for reuse with `nana-buyback-hook-v6` — includes continuous sigmoid slippage and V4 price limit computation.
|
|
16
|
+
|
|
7
17
|
---
|
|
8
18
|
|
|
9
19
|
## 1. Breaking Changes
|
|
@@ -60,7 +70,7 @@ The `SLIPPAGE_DENOMINATOR` constant was kept but changed from `uint160` to `uint
|
|
|
60
70
|
|
|
61
71
|
### 1.11 Solidity Version
|
|
62
72
|
- **v5:** `pragma solidity 0.8.23`
|
|
63
|
-
- **v6:** `pragma solidity 0.8.26`
|
|
73
|
+
- **v6:** `pragma solidity ^0.8.26` (^0.8.28 for JBRouterTerminalRegistry)
|
|
64
74
|
|
|
65
75
|
---
|
|
66
76
|
|
|
@@ -295,3 +305,5 @@ v5 contained both `JBSwapTerminal.sol` and `JBSwapTerminal5_1.sol` (a minor revi
|
|
|
295
305
|
| `src/interfaces/IWETH9.sol` | `src/interfaces/IWETH9.sol` | **Unchanged** (import path updated to OZ) |
|
|
296
306
|
| *N/A* | `src/structs/PoolInfo.sol` | **New** |
|
|
297
307
|
| *N/A* | `src/libraries/JBSwapLib.sol` | **New** |
|
|
308
|
+
|
|
309
|
+
> **Cross-repo impact**: `nana-fee-project-deployer-v6` replaced `SwapTerminalDeploymentLib` with `RouterTerminalDeploymentLib`. `nana-permission-ids-v6` replaced `ADD_SWAP_TERMINAL_POOL`/`ADD_SWAP_TERMINAL_TWAP_PARAMS` with `SET_ROUTER_TERMINAL` (29). `nana-buyback-hook-v6` shares the `JBSwapLib` library for sigmoid slippage and V4 swap math.
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ _If you're having trouble understanding this contract, take a look at the [core
|
|
|
15
15
|
| Contract | Description |
|
|
16
16
|
|----------|-------------|
|
|
17
17
|
| `JBRouterTerminal` | Core terminal. Accepts any token via `pay` or `addToBalanceOf`, previews payment routes via `previewPayFor`, discovers the destination project's accepted token, and routes there -- swapping through Uniswap V3 or V4 pools if needed, cashing out JB project tokens if the input is a project token, or forwarding directly if the token is already accepted. Uses TWAP oracle (V3) or spot price (V4) for automatic slippage protection when the caller does not provide a quote. Implements `IJBTerminal`, `IJBPermitTerminal`, `IUniswapV3SwapCallback`, `IUnlockCallback`, and `IJBRouterTerminal`. |
|
|
18
|
-
| `JBRouterTerminalRegistry` | A proxy terminal that delegates `pay`, `previewPayFor`, and `addToBalanceOf` to a per-project or default `JBRouterTerminal` instance. Project owners can choose which router terminal they use, and optionally lock that choice permanently. Implements `IJBTerminal` and the extra registry management surface via `IJBRouterTerminalRegistry`. |
|
|
18
|
+
| `JBRouterTerminalRegistry` | A proxy terminal that delegates `pay`, `previewPayFor`, and `addToBalanceOf` to a per-project or default `JBRouterTerminal` instance. Project owners can choose which router terminal they use, and optionally lock that choice permanently. Implements `IJBTerminal`, `IJBPayerTracker`, and the extra registry management surface via `IJBRouterTerminalRegistry`. |
|
|
19
19
|
|
|
20
20
|
## How It Works
|
|
21
21
|
|
|
@@ -116,7 +116,7 @@ npm ci && forge install
|
|
|
116
116
|
|
|
117
117
|
Key `foundry.toml` settings:
|
|
118
118
|
|
|
119
|
-
- `solc = '0.8.
|
|
119
|
+
- `solc = '0.8.28'`
|
|
120
120
|
- `evm_version = 'cancun'` (required for Uniswap V4's transient storage)
|
|
121
121
|
- `optimizer_runs = 200`
|
|
122
122
|
- `via_ir = true` (required for `JBRouterTerminal` to fit under EIP-170)
|
|
@@ -131,8 +131,9 @@ nana-router-terminal-v6/
|
|
|
131
131
|
│ ├── JBRouterTerminal.sol # Core router terminal
|
|
132
132
|
│ ├── JBRouterTerminalRegistry.sol # Per-project terminal routing
|
|
133
133
|
│ ├── interfaces/
|
|
134
|
+
│ │ ├── IJBPayerTracker.sol # Original-payer tracking for refund resolution
|
|
134
135
|
│ │ ├── IJBRouterTerminal.sol # Router terminal interface
|
|
135
|
-
│ │ ├── IJBRouterTerminalRegistry.sol # Registry interface (extends IJBTerminal)
|
|
136
|
+
│ │ ├── IJBRouterTerminalRegistry.sol # Registry interface (extends IJBTerminal, IJBPayerTracker)
|
|
136
137
|
│ │ └── IWETH9.sol # WETH wrapper interface
|
|
137
138
|
│ ├── libraries/
|
|
138
139
|
│ │ └── JBSwapLib.sol # Slippage tolerance, impact, and price limit math
|
|
@@ -196,17 +197,22 @@ If no `quoteForSwap` is provided, the terminal calculates one automatically:
|
|
|
196
197
|
|
|
197
198
|
## Supported Chains
|
|
198
199
|
|
|
199
|
-
The deployment script supports:
|
|
200
|
+
The deployment script (`script/Deploy.s.sol`) supports:
|
|
200
201
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
| Arbitrum | `0x82aF...b1DD` | `0x1F98...F984` | `0x0000...8A90` |
|
|
207
|
-
| + Sepolia testnets for each | | | |
|
|
202
|
+
- Ethereum Mainnet
|
|
203
|
+
- Optimism
|
|
204
|
+
- Base
|
|
205
|
+
- Arbitrum
|
|
206
|
+
- Sepolia testnets for each
|
|
208
207
|
|
|
209
|
-
Permit2 is deployed at `0x000000000022D473030F116dDEE9F6B43aC78BA3` on all chains.
|
|
208
|
+
Each chain is configured with the appropriate WETH, Uniswap V3 Factory, and V4 PoolManager addresses. Permit2 is deployed at `0x000000000022D473030F116dDEE9F6B43aC78BA3` on all chains. See `script/Deploy.s.sol` for the full address list.
|
|
209
|
+
|
|
210
|
+
## Permissions
|
|
211
|
+
|
|
212
|
+
| Permission | ID | Contract | Purpose |
|
|
213
|
+
|------------|----|----------|---------|
|
|
214
|
+
| `SET_ROUTER_TERMINAL` | 29 | `JBRouterTerminalRegistry` | Allows a project owner or delegate to call `setTerminalFor` (choose which router terminal a project uses) and `lockTerminalFor` (permanently lock that choice). |
|
|
215
|
+
| `TRANSFER_CREDITS` | 13 | `JBRouterTerminal` | Must be granted by the payer to the router terminal address for the source project. Required when using `cashOutSource` metadata to cash out credits -- the router calls `TOKENS.transferCreditsFrom()` to pull credits from the payer. |
|
|
210
216
|
|
|
211
217
|
## Risks
|
|
212
218
|
|
package/RISKS.md
CHANGED
|
@@ -7,13 +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
11
|
|
|
11
12
|
## 2. Economic / Manipulation Risks
|
|
12
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
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.
|
|
15
|
-
- **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.
|
|
16
|
-
- **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.
|
|
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.
|
|
17
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.
|
|
18
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.
|
|
19
20
|
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
- **No access control on `pay` / `addToBalanceOf`.** Anyone can route payments. This is by design but means the contract processes arbitrary token types and amounts.
|
|
23
24
|
- **Fee-on-transfer tokens unsupported.** `_acceptFundsFor` in the terminal uses balance-delta, but the registry does NOT. Fee-on-transfer tokens through the registry will mismatch.
|
|
24
25
|
- **Credit cashout path.** `_acceptFundsFor` processes `cashOutSource` metadata to transfer credits from `_msgSender()`. Requires `TRANSFER_CREDITS` permission. If a user has this permission set broadly, any caller through the trusted forwarder could drain their credits.
|
|
25
|
-
- **Registry owner.** Controls which terminals are allowlisted and sets the global default. Disallowing
|
|
26
|
+
- **Registry owner.** Controls which terminals are allowlisted and sets the global default. Disallowing the current default terminal now reverts with `CannotDisallowDefaultTerminal` instead of silently clearing it. Per-project terminal settings already set to a disallowed terminal are NOT cleared.
|
|
26
27
|
- **Synthetic accounting contexts.** `JBRouterTerminal.accountingContextForTokenOf()` uses best-effort decimals for
|
|
27
28
|
routing discovery: native tokens use `18`, ERC-20s probe `IERC20Metadata.decimals()` when available, and broken or
|
|
28
29
|
non-standard tokens fall back to `18`. `JBRouterTerminalRegistry` simply forwards that context. This is safe for
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
non-18-decimal assets. Lending and debt-normalization flows must point at a real terminal, not the router layer.
|
|
31
32
|
The router now refuses to treat a primary terminal as direct acceptance unless that terminal also exposes non-empty
|
|
32
33
|
accounting contexts for the project, so router-stack terminals do not win the direct-forward fast path.
|
|
34
|
+
For example, a USDC terminal (6 decimals) routed through the router reports `decimals: 6` correctly. But if the router cannot probe `IERC20Metadata.decimals()` (non-standard token, or reverting `decimals()` function), it falls back to 18 decimals — a 1e12 scaling error. This only affects routing discovery heuristics, not actual fund transfers, but could cause suboptimal pool selection in `_discoverPool`.
|
|
33
35
|
|
|
34
36
|
## 4. DoS Vectors
|
|
35
37
|
|
|
@@ -73,3 +75,11 @@ The router terminal has no `ReentrancyGuard` or `_routing` flag. This is a consc
|
|
|
73
75
|
- **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.
|
|
74
76
|
|
|
75
77
|
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`).
|
|
78
|
+
|
|
79
|
+
### 8.2 Router trusts `originalPayer()` from any `msg.sender` that implements it
|
|
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.
|
|
82
|
+
|
|
83
|
+
### 8.3 Cashout loop slippage is first-hop only (accepted trade-off)
|
|
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.
|