@bananapus/router-terminal-v6 0.0.30 → 0.0.32

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/RISKS.md CHANGED
@@ -1,113 +1,44 @@
1
- # Router Terminal Risk Register
1
+ # Accepted Security Risks
2
2
 
3
- This file covers the routing, accounting-context, and liquidity-selection risks in the terminal that accepts arbitrary tokens and forwards them into a project's real accounting surface.
3
+ Documented risks that were reviewed and accepted.
4
4
 
5
- ## How To Use This File
5
+ ## Oracle & Slippage Risks
6
6
 
7
- - Read `Priority risks` first. They explain where routing convenience can diverge from accounting truth.
8
- - Use the later sections for token-decimal synthesis, swap-path, and integration reasoning.
9
- - Treat `Accepted behaviors` as explicit statements about what this terminal does not guarantee.
7
+ **Pool-local V3 TWAP trusted as swap floor for permissionless pools.** *(Medium)*
8
+ An attacker could deploy a manipulable pool with higher liquidity to become the selected candidate. Users should provide `quoteForSwap` metadata from off-chain sources. Mitigated by 120s minimum TWAP window and sigmoid slippage formula.
10
9
 
11
- ## Priority Risks
10
+ **Liquidity-based pool selection enables unsafe spot quoting.** *(Medium)*
11
+ Pool discovery ranks candidates by instantaneous liquidity, so an attacker could inflate liquidity to force selection of a manipulable pool. Mitigated by V4 TWAP hardening and sigmoid slippage formula. Users should provide off-chain quotes for high-value swaps.
12
12
 
13
- | Priority | Risk | Why it matters | Primary controls |
14
- |----------|------|----------------|------------------|
15
- | P0 | Synthetic accounting context misuse | The router synthesizes best-effort decimals and routing context. If downstream systems treat it as accounting truth, they can misprice or mis-lend. | Clear docs, registry scoping, and explicit prohibition on accounting-sensitive reuse. |
16
- | P1 | Wrong-route or low-liquidity execution | The router chooses among direct forwarding, V3, V4, and cash-out paths. A bad route can degrade user execution. | Route-selection tests, liquidity checks, and user-specified minimum returns. |
17
- | P1 | Integration fragility with broken tokens | Non-standard ERC-20 metadata or transfer behavior can distort decimal inference or swap execution. | Fallback defaults, defensive probing, receipt checks on terminal-facing hops, and hostile-token testing. |
13
+ **Harmonic-mean liquidity inflates V3 slippage tolerance.** *(Medium)*
14
+ `OracleLibrary.consult` returns harmonic-mean liquidity, which can be deflated by brief low-liquidity periods. However, harmonic mean is more resistant to manipulation than spot liquidity. Mitigated by 120s TWAP minimum and 10-minute default observation window.
18
15
 
19
- ## 1. Trust Assumptions
16
+ **`quoteForSwap` / auto-selected tokenOut mismatch.** *(Minor)*
17
+ When a user provides `quoteForSwap` metadata, the quote may not match the auto-selected output token. Frontends should set `quoteForSwap` per the expected output token.
20
18
 
21
- - **Uniswap V3 factory and V4 PoolManager behave correctly.** Pool discovery trusts those external systems to point at real pools.
22
- - **Canonical V4 hook configuration is correct.** If deployers set the wrong `UNIV4_HOOK`, the router can miss intended hooked pools.
23
- - **The trusted forwarder is trustworthy.** A compromised forwarder can initiate transfers on behalf of any user.
24
- - **`JBDirectory` resolves the right terminals.** A compromised directory can redirect funds.
25
- - **Permit2 allowances are intentional.** Stale approvals can be abused.
26
- - **The registry owner acts correctly.** The owner controls allowlisting and the default router terminal.
27
- - **`IJBPayerTracker` callers are trusted to name their own refund recipient.** A caller implementing `originalPayer()` can redirect leftovers from its own route.
19
+ **Multi-hop cashout slippage cleared after first hop.** *(Minor)*
20
+ Only the final output matters; the outer function enforces end-to-end minimum via `minReclaimed`. Intermediate per-hop slippage checks are intentionally omitted. Maximum 20 recursive cashout iterations allowed (`_MAX_CASHOUT_ITERATIONS`); beyond that the operation reverts.
28
21
 
29
- ## 2. Economic And Manipulation Risks
22
+ **Zero oracle quote disables swap protection.** *(Minor)*
23
+ When the oracle returns zero (no liquidity), slippage tolerance becomes zero. The swap would fail anyway due to lack of liquidity, so this has no practical impact.
30
24
 
31
- - **V4 price manipulation.** `_getV4Quote` tries a 30-second TWAP first. If that fails, it falls back to spot pricing, which is manipulable within the block.
32
- - **Hooked V4 discovery scope.** Auto-discovery checks both vanilla V4 pools and pools using the configured canonical `UNIV4_HOOK`.
33
- - **V3 TWAP manipulation.** Short history reduces manipulation resistance, especially in new pools.
34
- - **Cash-out loop value extraction.** `_cashOutLoop` is capped at 20 iterations. `cashOutMinReclaimed` only applies on the first real cash-out hop.
35
- - **Pre-existing balances are intentionally excluded from route refunds.** Stray balances already sitting in the router are not swept into the next caller's refund.
36
- - **V4 native ETH settlement is special-cased.** `_settleV4` unwraps WETH when the pool manager expects native ETH.
37
- - **Pool selection is liquidity-first.** `_discoverPool` picks the deepest discovered pool, not the globally best execution path.
38
- - **Route selection is heuristic, not best execution.** The router bounds discovery for predictability and gas, not exhaustive optimization.
25
+ > **Note:** The V4 TWAP window was hardened from 30s to 120s. This is no longer an accepted risk -- it was fixed.
39
26
 
40
- ## 3. Access Control
27
+ ## Registry & Forwarding Risks
41
28
 
42
- - **`pay` and `addToBalanceOf` are permissionless.** Anyone can route payments.
43
- - **Lossy terminal-facing ERC-20s are unsupported.** Final forwarded ERC-20 hops must settle exactly on the router path.
44
- - **Credit cash-out path depends on `TRANSFER_CREDITS`.** Broad grants of that permission widen the attack surface.
45
- - **The registry owner controls allowlisting and the global default.** Disallowing the current default now reverts instead of silently clearing it.
46
- - **Registry terminal locking can freeze a bad choice.** `lockTerminalFor` is a one-way commitment.
47
- - **Router accounting contexts are synthetic.** They are safe for discovery, but unsafe as accounting truth for lending, debt, or balance normalization.
29
+ **Registry forwarding uses registry as credit holder.** *(Medium)*
30
+ When payments flow through the registry, credits accrue to the registry address, not the original user. Credit-based cashouts must go directly to the router terminal, not through the registry. This is intentional to prevent `originalPayer()` spoofing attacks.
48
31
 
49
- ## 4. DoS Vectors
32
+ **Forwarding-terminal receipt bypass.** *(Minor)*
33
+ `_isForwardingTerminal` bypasses receipt validation on incoming transfers. Forwarding terminals are registered by project owners and therefore trusted to handle receipts correctly.
50
34
 
51
- - **No pool exists.** If no V3 or V4 pool exists for a token pair, automatic swap routing fails.
52
- - **No observation history.** V3 TWAP quoting can revert on fresh pools.
53
- - **Cash-out loop limit.** Circular or deep token dependency chains hit `_MAX_CASHOUT_ITERATIONS = 20` and revert.
54
- - **Zero-liquidity pools.** Pools with no usable liquidity revert.
55
- - **External terminal reverts.** Final terminal calls are not wrapped in `try/catch`.
56
- - **Non-standard final ERC-20 transfer behavior.** Lossy terminal-facing tokens revert on the final forwarded hop.
35
+ **Forwarder claim disables receipt check.** *(Minor)*
36
+ Forwarding terminals registered by project owners are trusted to handle receipts correctly, so receipt validation is skipped for these callers.
57
37
 
58
- ## 5. Integration Risks
38
+ ## Minor Configuration Risks
59
39
 
60
- - **Registry default terminal changes affect unlocked projects.** Projects without explicit assignments follow `defaultTerminal`.
61
- - **Locked bad-terminal risk remains.** Locking protects against silent migration, but also freezes any mistake.
62
- - **`forceApprove` is used for terminal transfers.** This resets allowance before setting a new value and avoids stale-allowance accumulation.
63
- - **Callback data trust matters.** `uniswapV3SwapCallback` validates the pool by reconstructing its address from the expected parameters.
64
- - **The contract accepts arbitrary ETH.** That is necessary for unwraps and V4 settlement, but stray ETH can remain stranded.
40
+ **Unbounded quadratic candidate enumeration.** *(Minor)*
41
+ `_candidatePayRouteTokens` can enumerate O(n^2) candidates in theory. Bounded in practice to ~5-10 terminals per project, keeping gas costs manageable.
65
42
 
66
- ## 6. MEV Surface
67
-
68
- - **V3 path is TWAP-protected.** A 10-minute TWAP makes single-block manipulation much harder.
69
- - **V4 path is TWAP-first, spot-fallback.** When no oracle quote is available, the spot fallback is vulnerable.
70
- - **Cross-route arbitrage exists.** When JB routing bypasses the AMM, differences between bonding-curve price and AMM price create arbitrage opportunities.
71
-
72
- ## 7. Invariants To Verify
73
-
74
- - after any `pay()` or `addToBalanceOf()`, the router should not retain balances attributable to the just-processed route
75
- - `minAmountOut` in swaps is never zero when TWAP or spot price is available
76
- - `uniswapV3SwapCallback` only transfers tokens to verified pool addresses
77
- - `unlockCallback` only executes when called by `POOL_MANAGER`
78
- - credit cash-out only transfers credits from `_msgSender()`
79
- - the cash-out loop always terminates: it finds a terminal, hits the loop limit, or reverts for lack of path
80
-
81
- ## 8. Accepted Behaviors
82
-
83
- ### 8.1 No reentrancy guard
84
-
85
- The router has no `ReentrancyGuard` or `_routing` flag. This is intentional because the router is designed as a stateless routing layer, not a persistent accounting surface. Each call must fund and resolve its own route, and a blanket reentrancy guard would break legitimate composed routing flows.
86
-
87
- ### 8.2 Router trusts `originalPayer()` from any caller that implements it
88
-
89
- `_resolveRefundWithBackupRecipient` calls `IJBPayerTracker(msg.sender).originalPayer()` in a `try/catch`. If the caller returns a non-zero address, leftovers can be sent there. This is accepted because the caller already supplied the funds being routed.
90
-
91
- ### 8.3 Cash-out loop slippage is first-hop only
92
-
93
- `_cashOutLoop` applies `cashOutMinReclaimed` to the first cash-out step only. Later recursive hops may reclaim different assets with different units, so reusing one minimum across the full loop would be unsound.
94
-
95
- ### 8.4 Liquidity-first pool selection is intentional
96
-
97
- The router does not do an exhaustive best-execution search across every viable V3 and V4 pool. It prefers bounded discovery, lower complexity, and predictable behavior.
98
-
99
- ### 8.5 Registry owns immediate circular-forward protection
100
-
101
- The router and resolver no longer contain registry-specific circular-resolution logic. `JBRouterTerminalRegistry` rejects forwarding back into its immediate caller instead.
102
-
103
- ### 8.6 V4 spot fallback is an accepted risk for programmatic integrations
104
-
105
- Automatic V4 quoting first tries a hook-provided oracle observation and falls back to spot only when that quote is unavailable. The fallback is still manipulable and is accepted only as a bounded on-chain quoting path for integrations that cannot provide `quoteForSwap` metadata.
106
-
107
- ### 8.7 Credit cash-outs only work when calling the router terminal directly
108
-
109
- The credit-cashout path in `_acceptFundsFor` uses `holder = _msgSender()` — the direct caller — as the credit holder. This means credit cash-outs **do not work through the `JBRouterTerminalRegistry`**, because when the registry forwards a `pay()` call, `_msgSender()` inside the router terminal resolves to the registry's address, not the original user. The registry has no credits, so `transferCreditsFrom` would fail.
110
-
111
- This is intentional. The previous design used `_resolveOriginalPayer(sender)` to recover the original user from the registry's transient `originalPayer` storage. However, any contract implementing `IJBPayerTracker.originalPayer()` could spoof a victim's address and steal their credits (H-24). The fix uses the direct sender unconditionally for credit transfers, closing the spoofing vector at the cost of registry-mediated credit flows.
112
-
113
- Users who need to cash out credits through the router should call `JBRouterTerminal.pay()` directly with `cashOutSource` metadata, not through the registry.
43
+ **Permit2 try/catch falls through to ERC20 allowance.** *(Minor)*
44
+ Standard Permit2 fallback pattern. If Permit2 signature verification fails, the contract falls back to standard ERC20 `transferFrom` using existing allowance.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/router-terminal-v6",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,9 +17,9 @@
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/buyback-hook-v6": "^0.0.27",
21
- "@bananapus/core-v6": "^0.0.34",
22
- "@bananapus/permission-ids-v6": "^0.0.17",
20
+ "@bananapus/buyback-hook-v6": "^0.0.30",
21
+ "@bananapus/core-v6": "^0.0.36",
22
+ "@bananapus/permission-ids-v6": "^0.0.19",
23
23
  "@openzeppelin/contracts": "^5.6.1",
24
24
  "@uniswap/permit2": "github:Uniswap/permit2",
25
25
  "@uniswap/v3-core": "github:Uniswap/v3-core#0.8",
@@ -274,9 +274,23 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
274
274
  }
275
275
 
276
276
  // Decode only the minimum token-count commitments needed to score the buyback-enhanced preview.
277
- (,,,,,,,,, uint256 minimumBeneficiaryTokenCount, uint256 minimumReservedTokenCount,) = abi.decode(
277
+ (,,,,,,,,,, uint256 minimumBeneficiaryTokenCount, uint256 minimumReservedTokenCount,) = abi.decode(
278
278
  specification.metadata,
279
- (bool, uint256, uint256, bool, address, uint256, int24, uint128, bytes32, uint256, uint256, uint256)
279
+ (
280
+ bool,
281
+ uint256,
282
+ uint256,
283
+ bool,
284
+ address,
285
+ uint256,
286
+ uint256,
287
+ int24,
288
+ uint128,
289
+ bytes32,
290
+ uint256,
291
+ uint256,
292
+ uint256
293
+ )
280
294
  );
281
295
 
282
296
  // Keep whichever decoded hook commitment implies the stronger user-visible preview outcome.
@@ -340,20 +354,30 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
340
354
  view
341
355
  returns (bool isCircular)
342
356
  {
343
- // Treat direct self-routes as circular immediately.
344
- if (address(terminal) == address(router)) return true;
345
-
346
- // Probe via staticcall so plain terminals degrade cleanly.
347
- (bool success, bytes memory data) =
348
- address(terminal).staticcall(abi.encodeCall(IJBForwardingTerminal.terminalOf, (projectId)));
349
-
350
- // Non-forwarding terminals (call fails or returns zero) are not circular.
351
- if (!success || data.length < 32) return false;
352
- IJBTerminal forwardingTarget = abi.decode(data, (IJBTerminal));
353
- if (address(forwardingTarget) == address(0)) return false;
357
+ // Follow the forwarding chain up to 5 hops to detect circular routes back to the router.
358
+ // A bounded loop prevents infinite gas consumption from longer chains while catching realistic cycles.
359
+ IJBTerminal current = terminal;
360
+ for (uint256 i; i < 5; i++) {
361
+ // Treat routes back to the router as circular.
362
+ if (address(current) == address(router)) return true;
363
+
364
+ // Probe via staticcall so plain terminals degrade cleanly.
365
+ // slither-disable-next-line calls-loop
366
+ (bool success, bytes memory data) =
367
+ address(current).staticcall(abi.encodeCall(IJBForwardingTerminal.terminalOf, (projectId)));
368
+
369
+ // Non-forwarding terminals (call fails or returns zero) end the chain — not circular.
370
+ if (!success || data.length < 32) return false;
371
+ IJBTerminal forwardingTarget = abi.decode(data, (IJBTerminal));
372
+ if (address(forwardingTarget) == address(0)) return false;
373
+
374
+ // Follow the forwarding chain one more hop.
375
+ current = forwardingTarget;
376
+ }
354
377
 
355
- // Forwarding terminals that route back into the router are circular.
356
- return address(forwardingTarget) == address(router);
378
+ // If we followed 5 hops without finding a non-forwarding terminal or the router,
379
+ // treat this as a suspicious deep chain and mark it as circular to be safe.
380
+ return true;
357
381
  }
358
382
 
359
383
  /// @notice Normalize a token into the form the router uses for routing comparisons.
@@ -481,56 +505,6 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
481
505
  routedAmountOut = routedAmountIn;
482
506
  }
483
507
 
484
- /// @notice External wrapper so candidate previews can be isolated with `try/catch`.
485
- /// @param router The router terminal whose preview helpers should be used.
486
- /// @param projectId The destination project that would receive the payment.
487
- /// @param tokenIn The token currently available to route.
488
- /// @param amount The amount of `tokenIn` being previewed.
489
- /// @param beneficiary The address whose beneficiary token count is being measured.
490
- /// @param metadata Metadata forwarded into both the routing preview and terminal preview.
491
- /// @param tokenOut The candidate destination token to preview.
492
- /// @param destTerminal The terminal that accepts `tokenOut` for the destination project.
493
- /// @return routedDestTerminal The terminal chosen for this candidate route.
494
- /// @return routedTokenOut The routed token that would be paid into the destination terminal.
495
- /// @return routedAmountOut The routed amount that would be paid into the destination terminal.
496
- /// @return ruleset The ruleset returned by the terminal preview.
497
- /// @return beneficiaryTokenCount The effective beneficiary token count for this candidate route.
498
- /// @return reservedTokenCount The effective reserved token count for this candidate route.
499
- /// @return hookSpecifications The hook specifications returned by the terminal preview.
500
- function previewPayRouteForCandidate(
501
- IJBPayRoutePreviewer router,
502
- uint256 projectId,
503
- address tokenIn,
504
- uint256 amount,
505
- address beneficiary,
506
- bytes calldata metadata,
507
- address tokenOut,
508
- IJBTerminal destTerminal
509
- )
510
- external
511
- view
512
- returns (
513
- IJBTerminal routedDestTerminal,
514
- address routedTokenOut,
515
- uint256 routedAmountOut,
516
- JBRuleset memory ruleset,
517
- uint256 beneficiaryTokenCount,
518
- uint256 reservedTokenCount,
519
- JBPayHookSpecification[] memory hookSpecifications
520
- )
521
- {
522
- return _previewPayRouteForCandidate({
523
- router: router,
524
- projectId: projectId,
525
- tokenIn: tokenIn,
526
- amount: amount,
527
- beneficiary: beneficiary,
528
- metadata: metadata,
529
- tokenOut: tokenOut,
530
- destTerminal: destTerminal
531
- });
532
- }
533
-
534
508
  /// @notice Preview the fallback route that would be used when no candidate token can be scored directly.
535
509
  /// @param router The router terminal whose preview helpers should be used.
536
510
  /// @param destProjectId The destination project being paid.
@@ -762,6 +736,7 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
762
736
  returns (IJBTerminal candidateTerminal)
763
737
  {
764
738
  // Resolve the primary terminal for the candidate token so fallback discovery agrees with preview/execution.
739
+ // slither-disable-next-line calls-loop
765
740
  candidateTerminal = directory.primaryTerminalOf({projectId: projectId, token: candidateToken});
766
741
 
767
742
  // Drop candidates whose primary terminal disappeared or would route straight back into the router.
@@ -773,39 +748,10 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
773
748
  }
774
749
  }
775
750
 
776
- /// @inheritdoc IJBPayRouteResolver
777
- function usablePrimaryTerminalOf(
778
- IJBPayRoutePreviewer router,
779
- uint256 projectId,
780
- address token
781
- )
782
- external
783
- view
784
- returns (IJBTerminal terminal)
785
- {
786
- return _usablePrimaryTerminalForCandidate({
787
- router: router, directory: DIRECTORY, projectId: projectId, candidateToken: token
788
- });
789
- }
790
-
791
751
  //*********************************************************************//
792
752
  // ------------------------- external views -------------------------- //
793
753
  //*********************************************************************//
794
754
 
795
- /// @inheritdoc IJBPayRouteResolver
796
- function resolveTokenOut(
797
- IJBPayRoutePreviewer router,
798
- uint256 projectId,
799
- address tokenIn,
800
- bytes calldata metadata
801
- )
802
- external
803
- view
804
- returns (address tokenOut, IJBTerminal destTerminal)
805
- {
806
- return _resolveTokenOut({router: router, projectId: projectId, tokenIn: tokenIn, metadata: metadata});
807
- }
808
-
809
755
  /// @inheritdoc IJBPayRouteResolver
810
756
  // slither-disable-next-line calls-loop
811
757
  function previewBestPayRoute(
@@ -930,27 +876,168 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
930
876
  );
931
877
  }
932
878
 
933
- // Fall back to the router's generic route resolution when no candidate token could be scored directly.
879
+ // No candidate token could be scored — fall back to the router's generic route resolution.
880
+ // Uses an external self-call (`self.previewFallbackRoute`) so Solidity's try/catch can isolate
881
+ // reverts from broken terminals or price feeds without bricking the entire best-route preview.
882
+ try self.previewFallbackRoute(router, projectId, tokenIn, amount, beneficiary, metadata) returns (
883
+ IJBTerminal fallbackDestTerminal,
884
+ address fallbackTokenOut,
885
+ uint256 fallbackAmountOut,
886
+ JBRuleset memory fallbackRuleset,
887
+ uint256 fallbackBeneficiaryTokenCount,
888
+ uint256 fallbackReservedTokenCount,
889
+ JBPayHookSpecification[] memory fallbackHookSpecifications
890
+ ) {
891
+ destTerminal = fallbackDestTerminal;
892
+ tokenOut = fallbackTokenOut;
893
+ amountOut = fallbackAmountOut;
894
+ ruleset = fallbackRuleset;
895
+ beneficiaryTokenCount = fallbackBeneficiaryTokenCount;
896
+ reservedTokenCount = fallbackReservedTokenCount;
897
+ hookSpecifications = fallbackHookSpecifications;
898
+ } catch {
899
+ // If the fallback also fails, return default zero values — the caller gets "no route found".
900
+ }
901
+ }
902
+
903
+ /// @notice External self-call wrapper that previews the fallback route in an isolated context.
904
+ /// @dev Solidity's `try/catch` only works on external calls. `previewBestPayRoute` calls
905
+ /// `self.previewFallbackRoute(...)` so that a revert in the fallback path (e.g. a broken terminal or
906
+ /// price feed) is caught instead of bricking the entire best-route preview.
907
+ /// @dev This function should only be called by this contract itself — external callers have no reason to use it.
908
+ /// @param routePreviewer The router terminal whose preview helpers are used to simulate the route.
909
+ /// @param destProjectId The project being paid through the fallback route.
910
+ /// @param tokenIn The token the payer is sending.
911
+ /// @param amountIn The amount of `tokenIn` being routed.
912
+ /// @param beneficiary The address that would receive minted project tokens.
913
+ /// @param metadata Arbitrary bytes forwarded into route and terminal pay previews.
914
+ /// @return destTerminal The terminal the fallback route would deliver funds to.
915
+ /// @return tokenOut The token `destTerminal` would receive after any intermediate swaps.
916
+ /// @return amountOut The amount of `tokenOut` that would arrive at `destTerminal`.
917
+ /// @return ruleset The ruleset that would govern the terminal pay.
918
+ /// @return beneficiaryTokenCount The number of project tokens `beneficiary` would receive.
919
+ /// @return reservedTokenCount The number of project tokens that would be reserved.
920
+ /// @return hookSpecifications Any pay-hook specifications returned by the terminal preview.
921
+ function previewFallbackRoute(
922
+ IJBPayRoutePreviewer routePreviewer,
923
+ uint256 destProjectId,
924
+ address tokenIn,
925
+ uint256 amountIn,
926
+ address beneficiary,
927
+ bytes calldata metadata
928
+ )
929
+ external
930
+ view
931
+ returns (
932
+ IJBTerminal destTerminal,
933
+ address tokenOut,
934
+ uint256 amountOut,
935
+ JBRuleset memory ruleset,
936
+ uint256 beneficiaryTokenCount,
937
+ uint256 reservedTokenCount,
938
+ JBPayHookSpecification[] memory hookSpecifications
939
+ )
940
+ {
941
+ // Resolve which terminal and token the fallback route would use.
934
942
  (destTerminal, tokenOut, amountOut) = _previewRoute({
935
- router: router, destProjectId: projectId, tokenIn: tokenIn, amount: amount, metadata: metadata
943
+ router: routePreviewer, destProjectId: destProjectId, tokenIn: tokenIn, amount: amountIn, metadata: metadata
936
944
  });
937
945
 
938
- // Preview the final terminal pay for that fallback route.
939
- (ruleset, beneficiaryTokenCount, reservedTokenCount, hookSpecifications) = router.previewTerminalPayOf({
946
+ // Simulate the terminal pay to get token counts and hook specs.
947
+ (ruleset, beneficiaryTokenCount, reservedTokenCount, hookSpecifications) = routePreviewer.previewTerminalPayOf({
940
948
  destTerminal: destTerminal,
941
- projectId: projectId,
949
+ projectId: destProjectId,
942
950
  token: tokenOut,
943
951
  amount: amountOut,
944
952
  beneficiary: beneficiary,
945
953
  metadata: metadata
946
954
  });
947
955
 
948
- // Normalize the fallback preview counts so buyback-hook metadata still affects route ranking consistently.
956
+ // Normalize counts to account for buyback-hook overrides.
949
957
  (beneficiaryTokenCount, reservedTokenCount) = _effectivePreviewPayTokenCounts({
950
- buybackHook: router.BUYBACK_HOOK(),
958
+ buybackHook: routePreviewer.BUYBACK_HOOK(),
951
959
  beneficiaryTokenCount: beneficiaryTokenCount,
952
960
  reservedTokenCount: reservedTokenCount,
953
961
  hookSpecifications: hookSpecifications
954
962
  });
955
963
  }
964
+
965
+ /// @notice External wrapper so candidate previews can be isolated with `try/catch`.
966
+ /// @param router The router terminal whose preview helpers should be used.
967
+ /// @param projectId The destination project that would receive the payment.
968
+ /// @param tokenIn The token currently available to route.
969
+ /// @param amount The amount of `tokenIn` being previewed.
970
+ /// @param beneficiary The address whose beneficiary token count is being measured.
971
+ /// @param metadata Metadata forwarded into both the routing preview and terminal preview.
972
+ /// @param tokenOut The candidate destination token to preview.
973
+ /// @param destTerminal The terminal that accepts `tokenOut` for the destination project.
974
+ /// @return routedDestTerminal The terminal chosen for this candidate route.
975
+ /// @return routedTokenOut The routed token that would be paid into the destination terminal.
976
+ /// @return routedAmountOut The routed amount that would be paid into the destination terminal.
977
+ /// @return ruleset The ruleset returned by the terminal preview.
978
+ /// @return beneficiaryTokenCount The effective beneficiary token count for this candidate route.
979
+ /// @return reservedTokenCount The effective reserved token count for this candidate route.
980
+ /// @return hookSpecifications The hook specifications returned by the terminal preview.
981
+ function previewPayRouteForCandidate(
982
+ IJBPayRoutePreviewer router,
983
+ uint256 projectId,
984
+ address tokenIn,
985
+ uint256 amount,
986
+ address beneficiary,
987
+ bytes calldata metadata,
988
+ address tokenOut,
989
+ IJBTerminal destTerminal
990
+ )
991
+ external
992
+ view
993
+ returns (
994
+ IJBTerminal routedDestTerminal,
995
+ address routedTokenOut,
996
+ uint256 routedAmountOut,
997
+ JBRuleset memory ruleset,
998
+ uint256 beneficiaryTokenCount,
999
+ uint256 reservedTokenCount,
1000
+ JBPayHookSpecification[] memory hookSpecifications
1001
+ )
1002
+ {
1003
+ return _previewPayRouteForCandidate({
1004
+ router: router,
1005
+ projectId: projectId,
1006
+ tokenIn: tokenIn,
1007
+ amount: amount,
1008
+ beneficiary: beneficiary,
1009
+ metadata: metadata,
1010
+ tokenOut: tokenOut,
1011
+ destTerminal: destTerminal
1012
+ });
1013
+ }
1014
+
1015
+ /// @inheritdoc IJBPayRouteResolver
1016
+ function resolveTokenOut(
1017
+ IJBPayRoutePreviewer router,
1018
+ uint256 projectId,
1019
+ address tokenIn,
1020
+ bytes calldata metadata
1021
+ )
1022
+ external
1023
+ view
1024
+ returns (address tokenOut, IJBTerminal destTerminal)
1025
+ {
1026
+ return _resolveTokenOut({router: router, projectId: projectId, tokenIn: tokenIn, metadata: metadata});
1027
+ }
1028
+
1029
+ /// @inheritdoc IJBPayRouteResolver
1030
+ function usablePrimaryTerminalOf(
1031
+ IJBPayRoutePreviewer router,
1032
+ uint256 projectId,
1033
+ address token
1034
+ )
1035
+ external
1036
+ view
1037
+ returns (IJBTerminal terminal)
1038
+ {
1039
+ return _usablePrimaryTerminalForCandidate({
1040
+ router: router, directory: DIRECTORY, projectId: projectId, candidateToken: token
1041
+ });
1042
+ }
956
1043
  }