@bananapus/router-terminal-v6 0.0.40 → 0.0.42

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/CHANGELOG.md CHANGED
@@ -6,6 +6,35 @@ This file describes the verified change from `nana-swap-terminal-v5` to the curr
6
6
 
7
7
  ## In-v6 changes
8
8
 
9
+ ### Chain-same CREATE2 address for `JBRouterTerminal`
10
+
11
+ `JBRouterTerminal` now deploys to the same address on every chain via CREATE2. The four chain-specific
12
+ immutables (`WRAPPED_NATIVE_TOKEN`, `FACTORY`, `POOL_MANAGER`, `UNIV4_HOOK`) moved from `immutable` to public
13
+ storage and are wired in after deployment via a new one-shot `setChainSpecificConstants(wrappedNativeToken, factory, poolManager, univ4Hook)` setter, gated by a `_DEPLOYER` internal immutable (same pattern as `JBBuybackHook` and `JBUniswapV4LPSplitHookDeployer`).
14
+
15
+ - Constructor signature changed: `(IJBDirectory directory, IJBTokens tokens, IPermit2 permit2, address buybackHook, address trustedForwarder, address deployer)` — was 9 args, now 6. The four chain-different dependencies are no longer ctor inputs.
16
+ - New external function: `setChainSpecificConstants(IWETH9 wrappedNativeToken, IUniswapV3Factory factory, IPoolManager poolManager, address univ4Hook)`. Reverts with `JBRouterTerminal_Unauthorized(caller)` if msg.sender != `_DEPLOYER`; reverts with `JBRouterTerminal_AlreadyConfigured()` if `WRAPPED_NATIVE_TOKEN` has already been set.
17
+ - `BUYBACK_HOOK` stays as `public immutable` because `JBBuybackHook` is itself chain-same as of `@bananapus/buyback-hook-v6@0.0.44`.
18
+
19
+ `JBPayRouteResolver` also lost its `WRAPPED_NATIVE_TOKEN` immutable but does NOT call back into the router for it. Instead, the router passes its `WRAPPED_NATIVE_TOKEN` storage value as a parameter (`address wrappedNativeToken`) on every external resolver call (`previewBestPayRoute`, `previewPayRouteForCandidate`, `previewFallbackRoute`, `resolveTokenOut`), and the resolver threads it through internal helpers (`_normalizedTokenOf`, `_hasSameRoutingAsset`, `_discoverAcceptedToken`, `_resolveTokenOut`, `_previewAmountToToken`, `_previewRoute`). `_normalizedTokenOf` and `_hasSameRoutingAsset` are `pure` again. This avoids an extra external call per normalization step (which would compound inside the loops in `_discoverAcceptedToken`).
20
+
21
+ The resolver is still deployed in the router's constructor (chain-same input: just `directory`); its CREATE address is `router.address + nonce 1`, which is chain-same once the router itself is chain-same.
22
+
23
+ Integrator impact: deployers must call `setChainSpecificConstants` once after construction (the script in `script/Deploy.s.sol` does this in the same transaction as the deploy). Tests and the local deploy script have been updated accordingly.
24
+
25
+ Size: `JBRouterTerminal` 23,706 → 23,468 B (-238 B; headroom 870 → 1,108 B against the EIP-170 24,576 B limit). `JBPayRouteResolver` 10,438 → 10,398 B (-40 B).
26
+
27
+ ### Removed: credit cash-out input path
28
+
29
+ The router no longer accepts unclaimed Juicebox credits as a payment input. The `cashOutSource` metadata key, the `sourceProjectIdOverride` parameter on `previewCashOutLoopOf`, the `IJBController.transferCreditsFrom` pull in `_acceptFundsFor`, and the `_cashOutSourceFrom` helper have all been removed. Credit holders should call `JBTokens.claimFor` to materialize their credits as an ERC-20 first, then route through the router as a normal ERC-20 payment.
30
+
31
+ - Removed: `IJBController` import + `_CASH_OUT_SOURCE_ID` immutable.
32
+ - Removed: 3 test files (`RouterTerminalCreditCashout.t.sol`, `regression/CreditCashoutSpoofedPayer.t.sol`, `regression/CreditCashoutPreferredTokenBypass.t.sol`, `regression/PreviewCashOutShortcircuitDivergence.t.sol`).
33
+ - Changed: `IJBPayRoutePreviewer.previewCashOutLoopOf` signature — dropped the `uint256 sourceProjectIdOverride` parameter (now 5 args).
34
+ - Frees ~580 B of runtime size; reduces attack surface (no msg.sender-vs-originalPayer ambiguity in the credit pull).
35
+
36
+ Integrator impact: any frontend or backend that constructs the `cashOutSource` metadata key and routes JB credits via the router must switch to a two-step flow (`claimFor` → `router.pay`).
37
+
9
38
  ### Threshold-protected `setDefaultTerminal`
10
39
 
11
40
  The registry owner's `setDefaultTerminal(IJBTerminal)` call now applies only to projects created AFTER the call. Existing projects without an explicit `setTerminalFor` override keep resolving to the default that was current when their project-ID cohort was active. The outgoing default is snapshotted into an append-only `_defaultTerminalHistory` array on every `setDefaultTerminal` call.
@@ -20,7 +49,13 @@ Indexer impact: read `defaultTerminalFor(projectId)` rather than `defaultTermina
20
49
 
21
50
  Admin impact: the registry owner can no longer silently reroute payments for already-deployed projects by changing the default. See `ADMINISTRATION.md` for the updated boundary description.
22
51
 
52
+ ### `0.0.41` — Document multi-hop forwarding-cycle as accepted risk
53
+
54
+ `JBRouterTerminalRegistry._requireNonCircularTerminalFor` only walks one hop of `IJBForwardingTerminal.terminalOf` when admitting a new explicit or default terminal. A multi-hop `A → B → registry` chain passes admission (the registry only sees `downstream == B ≠ self`), but once locked in, a subsequent `pay`/`addToBalanceOf` recurses through the registry until OOG. The `JBPayRouteResolver` swap-routing path already uses the bounded multi-hop helper `JBForwardingCheck.isCircularTerminal`; the registry admission path does not.
55
+
56
+ This is documented as accepted in `RISKS.md` (§Registry & Forwarding Risks). Impact is bounded to a self-locking DoS on the project that constructs the multi-hop chain — external actors cannot trigger it, and the project owner can rotate the registry default to recover. Per-PR retrofit cost was judged non-trivial relative to that impact. Project owners installing chained forwarding terminals should run a manual `JBForwardingCheck.isCircularTerminal({target: registry, projectId: …, terminal: candidate})` simulation before approving the candidate.
23
57
 
58
+ No runtime code change in this release — documentation only.
24
59
 
25
60
  ## Current v6 surface
26
61
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/router-terminal-v6",
3
- "version": "0.0.40",
3
+ "version": "0.0.42",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -172,19 +172,24 @@ contract DeployScript is Script, Sphinx {
172
172
  trustedForwarder: trustedForwarder
173
173
  });
174
174
 
175
- // Deploy the router terminal using the resolved network-specific Uniswap and buyback-hook addresses.
175
+ // Deploy the router terminal with chain-same CREATE2 inputs; chain-specific constants
176
+ // (WETH + Uniswap V3 factory + V4 PoolManager + V4 hook) are wired afterwards via the
177
+ // DEPLOYER-gated one-shot setChainSpecificConstants setter on the terminal.
176
178
  require(address(buyback.hook) != address(0), "RouterTerminal: missing buyback hook");
177
179
  require(address(univ4Router.hook) != address(0), "RouterTerminal: missing v4 hook");
178
180
  JBRouterTerminal terminal = new JBRouterTerminal{salt: ROUTER_TERMINAL}({
179
181
  directory: core.directory,
180
182
  tokens: core.tokens,
181
183
  permit2: IPermit2(permit2),
182
- weth: IWETH9(weth),
184
+ buybackHook: address(buyback.hook),
185
+ trustedForwarder: trustedForwarder,
186
+ deployer: safeAddress()
187
+ });
188
+ terminal.setChainSpecificConstants({
189
+ wrappedNativeToken: IWETH9(weth),
183
190
  factory: IUniswapV3Factory(factory),
184
191
  poolManager: IPoolManager(poolManager),
185
- buybackHook: address(buyback.hook),
186
- univ4Hook: address(univ4Router.hook),
187
- trustedForwarder: trustedForwarder
192
+ univ4Hook: address(univ4Router.hook)
188
193
  });
189
194
 
190
195
  // Set the terminal as the default for the registry.
@@ -32,18 +32,18 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
32
32
  /// @notice The directory storing project terminal relationships, cached from the router at construction time.
33
33
  IJBDirectory public immutable DIRECTORY;
34
34
 
35
- /// @notice The ERC-20 wrapper for the chain's native token, cached from the router at construction time.
36
- IWETH9 public immutable WRAPPED_NATIVE_TOKEN;
37
-
38
35
  //*********************************************************************//
39
36
  // -------------------------- constructor ---------------------------- //
40
37
  //*********************************************************************//
41
38
 
42
39
  /// @param directory The directory storing project terminal relationships.
43
- /// @param weth The ERC-20 wrapper for the chain's native token, used for router token normalization.
44
- constructor(IJBDirectory directory, IWETH9 weth) {
40
+ /// @dev The wrapped-native-token address is intentionally NOT cached here. The router passes it in as a parameter
41
+ /// (`address wrappedNativeToken`) on every external resolver call and the resolver threads it through internal
42
+ /// helpers. This keeps the resolver's constructor inputs chain-same (no chain-specific WETH baked in) so its
43
+ /// CREATE address (router + nonce 1) stays unified, AND avoids paying an extra external call per normalization
44
+ /// step inside loops like `_discoverAcceptedToken`.
45
+ constructor(IJBDirectory directory) {
45
46
  DIRECTORY = directory;
46
- WRAPPED_NATIVE_TOKEN = weth;
47
47
  }
48
48
 
49
49
  //*********************************************************************//
@@ -140,12 +140,15 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
140
140
  /// @notice Search a project's terminals for an accepted token that has a Uniswap pool with `tokenIn`.
141
141
  /// @dev Falls back to the first accepted token if no pool exists.
142
142
  /// @param router The router whose normalization and pool-discovery helpers should be used.
143
+ /// @param wrappedNativeToken The router's wrapped-native-token address (threaded from the caller to avoid an extra
144
+ /// external call on every normalization in this loop).
143
145
  /// @param projectId The destination project whose accepted tokens should be searched.
144
146
  /// @param tokenIn The input token to find a route from.
145
147
  /// @return tokenOut The best accepted token found.
146
148
  /// @return destTerminal The terminal that accepts `tokenOut`.
147
149
  function _discoverAcceptedToken(
148
150
  IJBPayRoutePreviewer router,
151
+ address wrappedNativeToken,
149
152
  uint256 projectId,
150
153
  address tokenIn
151
154
  )
@@ -157,7 +160,7 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
157
160
  IJBDirectory directory = DIRECTORY;
158
161
 
159
162
  // Normalize the input token once so liquidity comparisons use the router's canonical token form.
160
- address normalizedTokenIn = _normalizedTokenOf(tokenIn);
163
+ address normalizedTokenIn = _normalizedTokenOf({wrappedNativeToken: wrappedNativeToken, token: tokenIn});
161
164
 
162
165
  // Read the destination project's currently known terminals directly from the directory.
163
166
  IJBTerminal[] memory terminals = directory.terminalsOf(projectId);
@@ -187,7 +190,8 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
187
190
  address candidateToken = contexts[j].token;
188
191
 
189
192
  // Normalize the candidate so native-vs-wrapped comparisons behave the same as the router.
190
- address normalizedCandidate = _normalizedTokenOf(candidateToken);
193
+ address normalizedCandidate =
194
+ _normalizedTokenOf({wrappedNativeToken: wrappedNativeToken, token: candidateToken});
191
195
 
192
196
  // Skip tokens that are equivalent to the input token because they do not require route discovery.
193
197
  if (normalizedCandidate == normalizedTokenIn) {
@@ -366,15 +370,25 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
366
370
  }
367
371
 
368
372
  /// @notice Check whether two tokens share the same routing representation for the router.
373
+ /// @param wrappedNativeToken The router's wrapped-native-token address, used to normalize native vs wrapped.
369
374
  /// @param tokenA The first token to compare.
370
375
  /// @param tokenB The second token to compare.
371
376
  /// @return hasSameAsset A flag indicating whether the router would treat both tokens as the same asset.
372
- function _hasSameRoutingAsset(address tokenA, address tokenB) internal view returns (bool hasSameAsset) {
377
+ function _hasSameRoutingAsset(
378
+ address wrappedNativeToken,
379
+ address tokenA,
380
+ address tokenB
381
+ )
382
+ internal
383
+ pure
384
+ returns (bool hasSameAsset)
385
+ {
373
386
  // Treat exact-token matches as the same routing asset without extra normalization work.
374
387
  if (tokenA == tokenB) return true;
375
388
 
376
389
  // Otherwise compare normalized representations so native and wrapped native tokens share one routing identity.
377
- return _normalizedTokenOf(tokenA) == _normalizedTokenOf(tokenB);
390
+ return _normalizedTokenOf({wrappedNativeToken: wrappedNativeToken, token: tokenA})
391
+ == _normalizedTokenOf({wrappedNativeToken: wrappedNativeToken, token: tokenB});
378
392
  }
379
393
 
380
394
  /// @notice Whether previewing through a terminal would cycle back into the router.
@@ -397,14 +411,23 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
397
411
  }
398
412
 
399
413
  /// @notice Normalize a token into the form the router uses for routing comparisons.
414
+ /// @param wrappedNativeToken The router's wrapped-native-token address.
400
415
  /// @param token The token to normalize.
401
416
  /// @return normalizedToken The normalized token address.
402
- function _normalizedTokenOf(address token) internal view returns (address normalizedToken) {
403
- return token == JBConstants.NATIVE_TOKEN ? address(WRAPPED_NATIVE_TOKEN) : token;
417
+ function _normalizedTokenOf(
418
+ address wrappedNativeToken,
419
+ address token
420
+ )
421
+ internal
422
+ pure
423
+ returns (address normalizedToken)
424
+ {
425
+ return token == JBConstants.NATIVE_TOKEN ? wrappedNativeToken : token;
404
426
  }
405
427
 
406
428
  /// @notice Preview the amount that would be routed into a specific destination token.
407
429
  /// @param router The router terminal whose preview helpers to use.
430
+ /// @param wrappedNativeToken The router's wrapped-native-token address.
408
431
  /// @param destProjectId The destination project the router is trying to pay.
409
432
  /// @param tokenIn The token currently available to route.
410
433
  /// @param amount The amount of `tokenIn` to preview.
@@ -414,6 +437,7 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
414
437
  /// @return routedAmountIn The amount of `routedTokenIn` that would reach the destination terminal.
415
438
  function _previewAmountToToken(
416
439
  IJBPayRoutePreviewer router,
440
+ address wrappedNativeToken,
417
441
  uint256 destProjectId,
418
442
  address tokenIn,
419
443
  uint256 amount,
@@ -436,7 +460,7 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
436
460
  });
437
461
 
438
462
  // Return early when the routed token already matches the desired destination token.
439
- if (_hasSameRoutingAsset({tokenA: routedTokenIn, tokenB: tokenOut})) {
463
+ if (_hasSameRoutingAsset({wrappedNativeToken: wrappedNativeToken, tokenA: routedTokenIn, tokenB: tokenOut})) {
440
464
  return (tokenOut, routedAmountIn);
441
465
  }
442
466
 
@@ -467,6 +491,7 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
467
491
  /// @return hookSpecifications The hook specifications returned by the terminal preview.
468
492
  function _previewPayRouteForCandidate(
469
493
  IJBPayRoutePreviewer router,
494
+ address wrappedNativeToken,
470
495
  uint256 projectId,
471
496
  address tokenIn,
472
497
  uint256 amount,
@@ -490,6 +515,7 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
490
515
  // First preview the route into the candidate destination token so the terminal is scored on post-route inputs.
491
516
  (address routedTokenIn, uint256 routedAmountIn) = _previewAmountToToken({
492
517
  router: router,
518
+ wrappedNativeToken: wrappedNativeToken,
493
519
  destProjectId: projectId,
494
520
  tokenIn: tokenIn,
495
521
  amount: amount,
@@ -532,6 +558,7 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
532
558
  /// @return amountOut The amount of `tokenOut` that would be routed.
533
559
  function _previewRoute(
534
560
  IJBPayRoutePreviewer router,
561
+ address wrappedNativeToken,
535
562
  uint256 destProjectId,
536
563
  address tokenIn,
537
564
  uint256 amount,
@@ -555,11 +582,16 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
555
582
  if (address(destTerminal) != address(0)) return (destTerminal, tokenIn, amount);
556
583
 
557
584
  // Resolve the destination token and terminal that the project would accept from the remaining input.
558
- (tokenOut, destTerminal) =
559
- _resolveTokenOut({router: router, projectId: destProjectId, tokenIn: tokenIn, metadata: metadata});
585
+ (tokenOut, destTerminal) = _resolveTokenOut({
586
+ router: router,
587
+ wrappedNativeToken: wrappedNativeToken,
588
+ projectId: destProjectId,
589
+ tokenIn: tokenIn,
590
+ metadata: metadata
591
+ });
560
592
 
561
593
  // Return the current amount unchanged when no swap is needed after token resolution.
562
- if (_hasSameRoutingAsset({tokenA: tokenIn, tokenB: tokenOut})) {
594
+ if (_hasSameRoutingAsset({wrappedNativeToken: wrappedNativeToken, tokenA: tokenIn, tokenB: tokenOut})) {
563
595
  return (destTerminal, tokenOut, amount);
564
596
  }
565
597
 
@@ -590,19 +622,16 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
590
622
  view
591
623
  returns (IJBTerminal resolvedTerminal, address routedTokenIn, uint256 routedAmountIn)
592
624
  {
593
- // Resolve whether this preview should first treat the input token as a JB project-token cashout source.
594
- (uint256 sourceProjectIdOverride, uint256 sourceProjectId) =
595
- _sourceProjectIdOf({router: router, tokenIn: tokenIn, metadata: metadata});
596
-
597
- // When there is no project-token source, the current input already is the routed input.
598
- if (sourceProjectId == 0) return (resolvedTerminal, tokenIn, amount);
625
+ // When the input is not a JB project token, the current input already is the routed input.
626
+ if (tokenIn == JBConstants.NATIVE_TOKEN || router.TOKENS().projectIdOf(IJBToken(tokenIn)) == 0) {
627
+ return (resolvedTerminal, tokenIn, amount);
628
+ }
599
629
 
600
630
  // Otherwise reuse the router's own preview cashout loop so preview and execution stay aligned.
601
631
  return router.previewCashOutLoopOf({
602
632
  destProjectId: destProjectId,
603
633
  token: tokenIn,
604
634
  amount: amount,
605
- sourceProjectIdOverride: sourceProjectIdOverride,
606
635
  metadata: metadata,
607
636
  preferredToken: preferredToken
608
637
  });
@@ -610,6 +639,7 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
610
639
 
611
640
  /// @notice Resolve what output token a project accepts for a given input token.
612
641
  /// @param router The router whose view helpers to use.
642
+ /// @param wrappedNativeToken The router's wrapped-native-token address.
613
643
  /// @param projectId The destination project to pay.
614
644
  /// @param tokenIn The input token to route.
615
645
  /// @param metadata Metadata forwarded into route-token resolution.
@@ -617,6 +647,7 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
617
647
  /// @return destTerminal The terminal that accepts `tokenOut`.
618
648
  function _resolveTokenOut(
619
649
  IJBPayRoutePreviewer router,
650
+ address wrappedNativeToken,
620
651
  uint256 projectId,
621
652
  address tokenIn,
622
653
  bytes calldata metadata
@@ -658,8 +689,8 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
658
689
  }
659
690
 
660
691
  // Then try the native-token and wrapped-native-token equivalent form before falling back to pool discovery.
661
- if (tokenIn == JBConstants.NATIVE_TOKEN || tokenIn == address(WRAPPED_NATIVE_TOKEN)) {
662
- tokenOut = tokenIn == JBConstants.NATIVE_TOKEN ? address(WRAPPED_NATIVE_TOKEN) : JBConstants.NATIVE_TOKEN;
692
+ if (tokenIn == JBConstants.NATIVE_TOKEN || tokenIn == wrappedNativeToken) {
693
+ tokenOut = tokenIn == JBConstants.NATIVE_TOKEN ? wrappedNativeToken : JBConstants.NATIVE_TOKEN;
663
694
  destTerminal = directory.primaryTerminalOf({projectId: projectId, token: tokenOut});
664
695
  if (
665
696
  address(destTerminal) != address(0)
@@ -670,7 +701,9 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
670
701
  }
671
702
 
672
703
  // Finally discover the best accepted token using the router's liquidity heuristic.
673
- (tokenOut, destTerminal) = _discoverAcceptedToken({router: router, projectId: projectId, tokenIn: tokenIn});
704
+ (tokenOut, destTerminal) = _discoverAcceptedToken({
705
+ router: router, wrappedNativeToken: wrappedNativeToken, projectId: projectId, tokenIn: tokenIn
706
+ });
674
707
 
675
708
  // Revert when discovery failed entirely or only found a circular route.
676
709
  if (
@@ -743,36 +776,6 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
743
776
  reservedTokenCount = tokenCount - beneficiaryTokenCount;
744
777
  }
745
778
 
746
- /// @notice Resolve whether the current route input should first be treated as a project-token cashout source.
747
- /// @param router The router terminal whose project-token lookup should be used.
748
- /// @param tokenIn The current route input token.
749
- /// @param metadata Metadata that may include an explicit cashout-source override.
750
- /// @return sourceProjectIdOverride The source project ID encoded in metadata, or 0 if none was provided.
751
- /// @return sourceProjectId The effective source project ID inferred from `metadata` and `tokenIn`.
752
- function _sourceProjectIdOf(
753
- IJBPayRoutePreviewer router,
754
- address tokenIn,
755
- bytes calldata metadata
756
- )
757
- internal
758
- view
759
- returns (uint256 sourceProjectIdOverride, uint256 sourceProjectId)
760
- {
761
- // Read the router-scoped cashout-source metadata so preview matches the router's own metadata namespace.
762
- (bool exists, bytes memory creditData) = _getDataFor({router: router, metadata: metadata, key: "cashOutSource"});
763
-
764
- // Decode the explicit source-project override when the caller supplied one.
765
- if (exists) (sourceProjectIdOverride,) = abi.decode(creditData, (uint256, uint256));
766
-
767
- // Start from the explicit override.
768
- sourceProjectId = sourceProjectIdOverride;
769
-
770
- // Fall back to inferring the project ID from the input token whenever the token is not the native sentinel.
771
- if (sourceProjectId == 0 && tokenIn != JBConstants.NATIVE_TOKEN) {
772
- sourceProjectId = router.TOKENS().projectIdOf(IJBToken(tokenIn));
773
- }
774
- }
775
-
776
779
  /// @notice Choose the stronger preview outcome using beneficiary tokens first and reserved tokens as a tie-break.
777
780
  /// @param currentBeneficiaryTokenCount The beneficiary token count from the strongest route so far.
778
781
  /// @param currentReservedTokenCount The reserved token count from the strongest route so far.
@@ -841,6 +844,7 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
841
844
  /// @inheritdoc IJBPayRouteResolver
842
845
  function previewBestPayRoute(
843
846
  IJBPayRoutePreviewer router,
847
+ address wrappedNativeToken,
844
848
  uint256 projectId,
845
849
  address tokenIn,
846
850
  uint256 amount,
@@ -884,6 +888,7 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
884
888
  // Score the explicitly requested route directly instead of scanning every accepted token.
885
889
  return _previewPayRouteForCandidate({
886
890
  router: router,
891
+ wrappedNativeToken: wrappedNativeToken,
887
892
  projectId: projectId,
888
893
  tokenIn: tokenIn,
889
894
  amount: amount,
@@ -914,7 +919,15 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
914
919
 
915
920
  // Isolate each candidate preview so one broken route does not brick the whole search.
916
921
  try self.previewPayRouteForCandidate(
917
- router, projectId, tokenIn, amount, beneficiary, metadata, candidateTokens[i], candidateTerminal
922
+ router,
923
+ wrappedNativeToken,
924
+ projectId,
925
+ tokenIn,
926
+ amount,
927
+ beneficiary,
928
+ metadata,
929
+ candidateTokens[i],
930
+ candidateTerminal
918
931
  ) returns (
919
932
  IJBTerminal candidateDestTerminal,
920
933
  address candidateTokenOut,
@@ -964,7 +977,9 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
964
977
  // No candidate token could be scored — fall back to the router's generic route resolution.
965
978
  // Uses an external self-call (`self.previewFallbackRoute`) so Solidity's try/catch can isolate
966
979
  // reverts from broken terminals or price feeds without bricking the entire best-route preview.
967
- try self.previewFallbackRoute(router, projectId, tokenIn, amount, beneficiary, metadata) returns (
980
+ try self.previewFallbackRoute(
981
+ router, wrappedNativeToken, projectId, tokenIn, amount, beneficiary, metadata
982
+ ) returns (
968
983
  IJBTerminal fallbackDestTerminal,
969
984
  address fallbackTokenOut,
970
985
  uint256 fallbackAmountOut,
@@ -1005,6 +1020,7 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
1005
1020
  /// @return hookSpecifications Any pay-hook specifications returned by the terminal preview.
1006
1021
  function previewFallbackRoute(
1007
1022
  IJBPayRoutePreviewer routePreviewer,
1023
+ address wrappedNativeToken,
1008
1024
  uint256 destProjectId,
1009
1025
  address tokenIn,
1010
1026
  uint256 amountIn,
@@ -1025,7 +1041,12 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
1025
1041
  {
1026
1042
  // Resolve which terminal and token the fallback route would use.
1027
1043
  (destTerminal, tokenOut, amountOut) = _previewRoute({
1028
- router: routePreviewer, destProjectId: destProjectId, tokenIn: tokenIn, amount: amountIn, metadata: metadata
1044
+ router: routePreviewer,
1045
+ wrappedNativeToken: wrappedNativeToken,
1046
+ destProjectId: destProjectId,
1047
+ tokenIn: tokenIn,
1048
+ amount: amountIn,
1049
+ metadata: metadata
1029
1050
  });
1030
1051
 
1031
1052
  // Simulate the terminal pay to get token counts and hook specs.
@@ -1065,6 +1086,7 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
1065
1086
  /// @return hookSpecifications The hook specifications returned by the terminal preview.
1066
1087
  function previewPayRouteForCandidate(
1067
1088
  IJBPayRoutePreviewer router,
1089
+ address wrappedNativeToken,
1068
1090
  uint256 projectId,
1069
1091
  address tokenIn,
1070
1092
  uint256 amount,
@@ -1087,6 +1109,7 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
1087
1109
  {
1088
1110
  return _previewPayRouteForCandidate({
1089
1111
  router: router,
1112
+ wrappedNativeToken: wrappedNativeToken,
1090
1113
  projectId: projectId,
1091
1114
  tokenIn: tokenIn,
1092
1115
  amount: amount,
@@ -1100,6 +1123,7 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
1100
1123
  /// @inheritdoc IJBPayRouteResolver
1101
1124
  function resolveTokenOut(
1102
1125
  IJBPayRoutePreviewer router,
1126
+ address wrappedNativeToken,
1103
1127
  uint256 projectId,
1104
1128
  address tokenIn,
1105
1129
  bytes calldata metadata
@@ -1108,7 +1132,13 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
1108
1132
  view
1109
1133
  returns (address tokenOut, IJBTerminal destTerminal)
1110
1134
  {
1111
- return _resolveTokenOut({router: router, projectId: projectId, tokenIn: tokenIn, metadata: metadata});
1135
+ return _resolveTokenOut({
1136
+ router: router,
1137
+ wrappedNativeToken: wrappedNativeToken,
1138
+ projectId: projectId,
1139
+ tokenIn: tokenIn,
1140
+ metadata: metadata
1141
+ });
1112
1142
  }
1113
1143
 
1114
1144
  /// @inheritdoc IJBPayRouteResolver
@@ -2,7 +2,6 @@
2
2
  pragma solidity 0.8.28;
3
3
 
4
4
  import {IJBCashOutTerminal} from "@bananapus/core-v6/src/interfaces/IJBCashOutTerminal.sol";
5
- import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
6
5
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
7
6
  import {IJBPermitTerminal} from "@bananapus/core-v6/src/interfaces/IJBPermitTerminal.sol";
8
7
  import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
@@ -72,6 +71,7 @@ contract JBRouterTerminal is
72
71
  // --------------------------- custom errors ------------------------- //
73
72
  //*********************************************************************//
74
73
 
74
+ error JBRouterTerminal_AlreadyConfigured();
75
75
  error JBRouterTerminal_AmountOverflow(uint256 amount);
76
76
  error JBRouterTerminal_CallerNotPool(address caller);
77
77
  error JBRouterTerminal_CallerNotPoolManager(address caller);
@@ -88,6 +88,7 @@ contract JBRouterTerminal is
88
88
  );
89
89
  error JBRouterTerminal_PermitAllowanceNotEnough(uint256 amount, uint256 allowance);
90
90
  error JBRouterTerminal_SlippageExceeded(uint256 amountOut, uint256 minAmountOut);
91
+ error JBRouterTerminal_Unauthorized(address caller);
91
92
 
92
93
  //*********************************************************************//
93
94
  // ------------------------- public constants ------------------------ //
@@ -120,39 +121,35 @@ contract JBRouterTerminal is
120
121
  //*********************************************************************//
121
122
 
122
123
  /// @notice The canonical buyback hook whose preview hook specification metadata this router understands.
124
+ /// @dev Chain-same: `JBBuybackHook` is deployed via CREATE2 to a unified address on every chain, so this stays
125
+ /// `immutable` without breaking the router's own chain-same CREATE2 address.
123
126
  address public immutable BUYBACK_HOOK;
124
127
 
125
- /// @notice The canonical Uniswap V4 router hook address used by supported hooked pools.
126
- address public immutable UNIV4_HOOK;
127
-
128
128
  /// @notice The directory of terminals and controllers for projects.
129
129
  IJBDirectory public immutable DIRECTORY;
130
130
 
131
- /// @notice The Uniswap V3 factory used for pool discovery and verification.
132
- IUniswapV3Factory public immutable FACTORY;
133
-
134
131
  /// @notice The Permit2 contract used for token approvals and transfers.
135
132
  IPermit2 public immutable override PERMIT2;
136
133
 
137
- /// @notice The Uniswap V4 PoolManager. Can be address(0) if V4 is not deployed on this chain.
138
- IPoolManager public immutable POOL_MANAGER;
139
-
140
134
  /// @notice Manages minting, burning, and balances of projects' tokens and token credits.
141
135
  IJBTokens public immutable TOKENS;
142
136
 
143
- /// @notice The ERC-20 wrapper for the native token.
144
- IWETH9 public immutable WRAPPED_NATIVE_TOKEN;
145
-
146
137
  //*********************************************************************//
147
138
  // -------------- internal immutable stored properties -------------- //
148
139
  //*********************************************************************//
149
140
 
141
+ /// @notice The deployer authorized to call `setChainSpecificConstants` exactly once.
142
+ /// @dev Held immutable so the constructor inputs are byte-identical across chains and the CREATE2 address is
143
+ /// unified. Mirrors the `JBOptimismSuckerDeployer.setChainSpecificConstants` pattern in nana-suckers-v6.
144
+ address internal immutable _DEPLOYER;
145
+
150
146
  /// @notice The helper contract used to resolve best pay-route previews without bloating router runtime size.
147
+ /// @dev Deployed in the constructor with chain-same inputs (just `directory` — the resolver does NOT cache
148
+ /// `WRAPPED_NATIVE_TOKEN` locally; the router passes it in on every external resolver call as a parameter to
149
+ /// avoid an extra external call on each normalization step). Because this router's address is chain-same via
150
+ /// CREATE2 and the resolver is deployed at the router's nonce 1, the resolver's address is chain-same too.
151
151
  IJBPayRouteResolver internal immutable _PAY_ROUTE_RESOLVER;
152
152
 
153
- /// @notice Pre-computed metadata ID for "cashOutSource".
154
- bytes4 internal immutable _CASH_OUT_SOURCE_ID;
155
-
156
153
  /// @notice Pre-computed metadata ID for "permit2".
157
154
  bytes4 internal immutable _PERMIT2_ID;
158
155
 
@@ -162,6 +159,32 @@ contract JBRouterTerminal is
162
159
  /// @notice Pre-computed metadata ID for "quoteForSwap".
163
160
  bytes4 internal immutable _QUOTE_FOR_SWAP_ID;
164
161
 
162
+ //*********************************************************************//
163
+ // --------------------- public stored properties -------------------- //
164
+ //*********************************************************************//
165
+
166
+ /// @notice The Uniswap V3 factory used for pool discovery and verification.
167
+ /// @dev Set once by `_DEPLOYER` via `setChainSpecificConstants`. Held as storage rather than immutable so the
168
+ /// constructor inputs are byte-identical on every chain (Uniswap V3 deploys to a different factory address per
169
+ /// chain).
170
+ IUniswapV3Factory public FACTORY;
171
+
172
+ /// @notice The Uniswap V4 PoolManager. Can be `address(0)` if V4 is not deployed on this chain.
173
+ /// @dev Set once by `_DEPLOYER` via `setChainSpecificConstants`. Held as storage rather than immutable so the
174
+ /// constructor inputs are byte-identical on every chain.
175
+ IPoolManager public POOL_MANAGER;
176
+
177
+ /// @notice The canonical Uniswap V4 router hook address used by supported hooked pools.
178
+ /// @dev Set once by `_DEPLOYER` via `setChainSpecificConstants`. Held as storage rather than immutable because
179
+ /// `JBUniswapV4Hook` inherits Uniswap's `BaseHook -> ImmutableState`, which forces a chain-specific PoolManager
180
+ /// immutable inside the hook itself — making the hook chain-different by design.
181
+ address public UNIV4_HOOK;
182
+
183
+ /// @notice The ERC-20 wrapper for the native token.
184
+ /// @dev Set once by `_DEPLOYER` via `setChainSpecificConstants`. Held as storage rather than immutable so the
185
+ /// constructor inputs are byte-identical on every chain (WETH/WCELO/etc. differ per chain).
186
+ IWETH9 public override WRAPPED_NATIVE_TOKEN;
187
+
165
188
  //*********************************************************************//
166
189
  // ---------------------- internal stored properties ----------------- //
167
190
  //*********************************************************************//
@@ -173,36 +196,28 @@ contract JBRouterTerminal is
173
196
  /// @param directory A contract storing directories of terminals and controllers for each project.
174
197
  /// @param tokens A contract managing project token balances.
175
198
  /// @param permit2 A permit2 utility.
176
- /// @param weth The ERC-20 wrapper for the chain's native token (e.g. WETH on Ethereum, WCELO on Celo).
177
- /// @param factory The Uniswap V3 factory for pool discovery.
178
- /// @param poolManager The Uniswap V4 PoolManager (address(0) if V4 not available).
179
- /// @param univ4Hook The canonical Uniswap V4 router hook used by supported hooked pools.
199
+ /// @param buybackHook The canonical buyback hook (chain-same across all chains).
180
200
  /// @param trustedForwarder The trusted forwarder for the contract.
201
+ /// @param deployer The address authorized to call `setChainSpecificConstants` exactly once. Held immutable so the
202
+ /// constructor inputs are byte-identical across chains and the CREATE2 address is unified.
181
203
  constructor(
182
204
  IJBDirectory directory,
183
205
  IJBTokens tokens,
184
206
  IPermit2 permit2,
185
- IWETH9 weth,
186
- IUniswapV3Factory factory,
187
- IPoolManager poolManager,
188
207
  address buybackHook,
189
- address univ4Hook,
190
- address trustedForwarder
208
+ address trustedForwarder,
209
+ address deployer
191
210
  )
192
211
  ERC2771Context(trustedForwarder)
193
212
  {
194
213
  DIRECTORY = directory;
195
214
  TOKENS = tokens;
196
- FACTORY = factory;
197
- POOL_MANAGER = poolManager;
198
215
  PERMIT2 = permit2;
199
- WRAPPED_NATIVE_TOKEN = weth;
200
216
  BUYBACK_HOOK = buybackHook;
201
- UNIV4_HOOK = univ4Hook;
202
- _PAY_ROUTE_RESOLVER = IJBPayRouteResolver(address(new JBPayRouteResolver({directory: directory, weth: weth})));
217
+ _DEPLOYER = deployer;
218
+ _PAY_ROUTE_RESOLVER = IJBPayRouteResolver(address(new JBPayRouteResolver({directory: directory})));
203
219
 
204
220
  // Pre-compute metadata IDs to avoid hashing string literals on every call.
205
- _CASH_OUT_SOURCE_ID = JBMetadataResolver.getId("cashOutSource");
206
221
  _PERMIT2_ID = JBMetadataResolver.getId("permit2");
207
222
  _CASH_OUT_MIN_RECLAIMED_ID = JBMetadataResolver.getId("cashOutMinReclaimed");
208
223
  _QUOTE_FOR_SWAP_ID = JBMetadataResolver.getId("quoteForSwap");
@@ -377,6 +392,32 @@ contract JBRouterTerminal is
377
392
  // than `amount` but the router cannot detect or prevent this. See RISKS.md for details.
378
393
  }
379
394
 
395
+ /// @notice One-shot setter for the chain-specific Uniswap and wrapped-native addresses.
396
+ /// @dev Callable only by `_DEPLOYER` and only once (when `WRAPPED_NATIVE_TOKEN` is still `address(0)`). After this
397
+ /// call all four values are effectively immutable for the contract's lifetime. Mirrors the
398
+ /// `JBOptimismSuckerDeployer.setChainSpecificConstants` pattern so the contract's CREATE2 inputs stay
399
+ /// byte-identical across chains and its deployed address is unified.
400
+ /// @param wrappedNativeToken The ERC-20 wrapper for the chain's native token (e.g. WETH on Ethereum,
401
+ /// WCELO on Celo).
402
+ /// @param factory The Uniswap V3 factory for pool discovery on this chain.
403
+ /// @param poolManager The Uniswap V4 PoolManager on this chain (may be `address(0)` if V4 is not deployed there).
404
+ /// @param univ4Hook The canonical Uniswap V4 router hook on this chain.
405
+ function setChainSpecificConstants(
406
+ IWETH9 wrappedNativeToken,
407
+ IUniswapV3Factory factory,
408
+ IPoolManager poolManager,
409
+ address univ4Hook
410
+ )
411
+ external
412
+ {
413
+ if (msg.sender != _DEPLOYER) revert JBRouterTerminal_Unauthorized({caller: msg.sender});
414
+ if (address(WRAPPED_NATIVE_TOKEN) != address(0)) revert JBRouterTerminal_AlreadyConfigured();
415
+ WRAPPED_NATIVE_TOKEN = wrappedNativeToken;
416
+ FACTORY = factory;
417
+ POOL_MANAGER = poolManager;
418
+ UNIV4_HOOK = univ4Hook;
419
+ }
420
+
380
421
  /// @notice The Uniswap v3 pool callback where the token transfer is expected to happen.
381
422
  /// @dev Verifies the caller is a legitimate pool via the factory using the encoded tokenIn/tokenOut pair.
382
423
  /// @param amount0Delta The amount of token 0 used for the swap.
@@ -603,18 +644,16 @@ contract JBRouterTerminal is
603
644
  JBPayHookSpecification[] memory hookSpecifications
604
645
  )
605
646
  {
606
- // Simulate how the router would normalize the incoming funds before routing.
607
- amount = _previewAcceptFundsFor({amount: amount, metadata: metadata});
608
- (,,, ruleset, beneficiaryTokenCount, reservedTokenCount, hookSpecifications) = _previewBestPayRoute({
609
- projectId: projectId, tokenIn: token, amount: amount, beneficiary: beneficiary, metadata: metadata
610
- });
647
+ (,,, ruleset, beneficiaryTokenCount, reservedTokenCount, hookSpecifications) =
648
+ _previewBestPayRoute({
649
+ projectId: projectId, tokenIn: token, amount: amount, beneficiary: beneficiary, metadata: metadata
650
+ });
611
651
  }
612
652
 
613
653
  /// @notice Preview the recursive cashout loop the router would use for a project-token input.
614
654
  /// @param destProjectId The destination project the router is trying to pay.
615
655
  /// @param token The current token to route.
616
656
  /// @param amount The amount of `token` to preview.
617
- /// @param sourceProjectIdOverride The one-shot source project override encoded in metadata, if any.
618
657
  /// @param metadata Metadata forwarded into preview helpers.
619
658
  /// @param preferredToken The token the cashout loop should prefer to land on, or `address(0)` for no preference.
620
659
  /// @return destTerminal The terminal reached by the cashout loop, or address(0) if routing should continue.
@@ -624,7 +663,6 @@ contract JBRouterTerminal is
624
663
  uint256 destProjectId,
625
664
  address token,
626
665
  uint256 amount,
627
- uint256 sourceProjectIdOverride,
628
666
  bytes calldata metadata,
629
667
  address preferredToken
630
668
  )
@@ -636,7 +674,6 @@ contract JBRouterTerminal is
636
674
  destProjectId: destProjectId,
637
675
  token: token,
638
676
  amount: amount,
639
- sourceProjectIdOverride: sourceProjectIdOverride,
640
677
  metadata: metadata,
641
678
  preferredToken: preferredToken
642
679
  });
@@ -716,7 +753,7 @@ contract JBRouterTerminal is
716
753
 
717
754
  /// @notice Resolve the original payer when called through an intermediary.
718
755
  /// @dev Registry-style forwarders record the original payer in transient storage via `IJBPayerTracker`. When
719
- /// present, that address is used for refunds and credit cashouts instead of the intermediary.
756
+ /// present, that address is used for refunds instead of the intermediary.
720
757
  /// @param fallback_ The default address to use when no original payer is available.
721
758
  /// @return The original payer, or `fallback_` if none is available.
722
759
  function _resolveOriginalPayer(address fallback_) internal view returns (address) {
@@ -854,8 +891,11 @@ contract JBRouterTerminal is
854
891
  )
855
892
  {
856
893
  // Delegate the heavy preview-selection logic to the helper contract so the router stays within runtime size.
894
+ // Pass `wrappedNativeToken` once (single SLOAD) so the resolver does not have to call back into the router for
895
+ // it on every normalization step.
857
896
  return _PAY_ROUTE_RESOLVER.previewBestPayRoute({
858
897
  router: IJBPayRoutePreviewer(address(this)),
898
+ wrappedNativeToken: address(WRAPPED_NATIVE_TOKEN),
859
899
  projectId: projectId,
860
900
  tokenIn: tokenIn,
861
901
  amount: amount,
@@ -891,18 +931,6 @@ contract JBRouterTerminal is
891
931
  });
892
932
  }
893
933
 
894
- /// @notice Check whether two tokens share the same routing representation.
895
- /// @param tokenA The first token to compare.
896
- /// @param tokenB The second token to compare.
897
- /// @return hasSameAsset A flag indicating whether the router treats both tokens as the same asset.
898
- function _hasSameRoutingAsset(address tokenA, address tokenB) internal view returns (bool hasSameAsset) {
899
- // Treat exact-token matches as the same asset without doing any normalization work.
900
- if (tokenA == tokenB) return true;
901
-
902
- // Otherwise compare normalized representations so native and wrapped native tokens share one routing identity.
903
- return _normalize(tokenA) == _normalize(tokenB);
904
- }
905
-
906
934
  /// @notice Snapshot a destination terminal's pre-call token balance and check forwarding status.
907
935
  /// @dev Combines both the balance snapshot and forwarding probe into a single helper to avoid duplicate
908
936
  /// `_isForwardingTerminal` calls in the pay/addToBalance flows.
@@ -1006,57 +1034,21 @@ contract JBRouterTerminal is
1006
1034
  }
1007
1035
 
1008
1036
  /// @notice Resolve which source project a routed token should cash out from.
1009
- /// @param sourceProjectIdOverride A one-shot source-project override decoded from routing metadata.
1010
1037
  /// @param token The current route token that may be a JB project token.
1011
1038
  /// @return sourceProjectId The project to cash out from, or 0 if the token is not a JB project token.
1012
- function _sourceProjectIdOf(
1013
- uint256 sourceProjectIdOverride,
1014
- address token
1015
- )
1016
- internal
1017
- view
1018
- returns (uint256 sourceProjectId)
1019
- {
1020
- // Prefer the explicit one-shot override when metadata supplied one.
1021
- sourceProjectId = sourceProjectIdOverride;
1022
-
1023
- // Otherwise infer the source project from the current token unless it is the native-token sentinel.
1024
- if (sourceProjectId == 0 && token != JBConstants.NATIVE_TOKEN) {
1025
- sourceProjectId = _projectIdOf(token);
1026
- }
1039
+ function _sourceProjectIdOf(address token) internal view returns (uint256 sourceProjectId) {
1040
+ if (token != JBConstants.NATIVE_TOKEN) sourceProjectId = _projectIdOf(token);
1027
1041
  }
1028
1042
 
1029
1043
  /// @notice Accept a token paid in by the caller.
1030
1044
  /// @param token The address of the token to accept.
1031
1045
  /// @param amount The amount of tokens to accept.
1032
- /// @param metadata The metadata in which `permit2` and credit context is provided.
1046
+ /// @param metadata The metadata in which `permit2` context is provided.
1033
1047
  /// @return The amount of tokens accepted.
1034
1048
  function _acceptFundsFor(address token, uint256 amount, bytes calldata metadata) internal returns (uint256) {
1035
1049
  // Cache _msgSender() once to avoid repeated ERC-2771 context resolution.
1036
1050
  address sender = _msgSender();
1037
1051
 
1038
- // Check for credit cash-out metadata.
1039
- (bool creditExists, bytes memory creditData) = _getDataFor({metadata: metadata, id: _CASH_OUT_SOURCE_ID});
1040
-
1041
- if (creditExists) {
1042
- // Credit cashouts don't use msg.value — revert if ETH was sent to prevent it being trapped.
1043
- if (msg.value != 0) revert JBRouterTerminal_NoMsgValueAllowed(msg.value);
1044
-
1045
- (uint256 sourceProjectId, uint256 creditAmount) = abi.decode(creditData, (uint256, uint256));
1046
-
1047
- // Use the direct sender as the credit holder. Do NOT resolve via _resolveOriginalPayer here,
1048
- // because a malicious contract could spoof originalPayer() to steal another user's credits.
1049
- address holder = sender;
1050
-
1051
- // Pull credits through the project's controller, which enforces holder permissions for credit transfers.
1052
- IJBController controller = IJBController(address(DIRECTORY.controllerOf(sourceProjectId)));
1053
- controller.transferCreditsFrom({
1054
- holder: holder, projectId: sourceProjectId, recipient: address(this), creditCount: creditAmount
1055
- });
1056
-
1057
- return creditAmount;
1058
- }
1059
-
1060
1052
  // If native tokens are being paid in, return the `msg.value`.
1061
1053
  if (token == JBConstants.NATIVE_TOKEN) return msg.value;
1062
1054
 
@@ -1142,8 +1134,6 @@ contract JBRouterTerminal is
1142
1134
  /// @param destProjectId The ID of the destination project.
1143
1135
  /// @param token The current token to process.
1144
1136
  /// @param amount The amount of the current token.
1145
- /// @param sourceProjectIdOverride When non-zero, use this as the source project ID instead of looking up via
1146
- /// `TOKENS.projectIdOf()`. Reset to 0 after first use.
1147
1137
  /// @param metadata Bytes in `JBMetadataResolver`'s format (may contain cashOutMinReclaimed).
1148
1138
  /// @return destTerminal The terminal that accepts the final token (address(0) if no direct acceptance found).
1149
1139
  /// @return finalToken The token after all cashouts.
@@ -1152,7 +1142,6 @@ contract JBRouterTerminal is
1152
1142
  uint256 destProjectId,
1153
1143
  address token,
1154
1144
  uint256 amount,
1155
- uint256 sourceProjectIdOverride,
1156
1145
  bytes calldata metadata,
1157
1146
  address preferredToken
1158
1147
  )
@@ -1168,24 +1157,18 @@ contract JBRouterTerminal is
1168
1157
  // Walk the cashout path hop by hop until we reach a directly acceptable destination asset or exhaust the
1169
1158
  // bounded iteration limit.
1170
1159
  for (uint256 i; i < _MAX_CASHOUT_ITERATIONS;) {
1171
- // Skip the destination check on the first iteration if we have a credit override — the forced
1172
- // cashout must happen before any early return.
1173
- if (sourceProjectIdOverride == 0) {
1174
- address routeToken;
1175
- (destTerminal, routeToken) =
1176
- _findRouteTerminal({destProjectId: destProjectId, token: token, preferredToken: preferredToken});
1177
- if (address(destTerminal) != address(0)) {
1178
- if (preferredToken != address(0)) {
1179
- (routeToken, amount) =
1180
- _alignTokenToPreferredToken({token: token, amount: amount, preferredToken: preferredToken});
1181
- }
1182
- return (destTerminal, routeToken, amount);
1160
+ address routeToken;
1161
+ (destTerminal, routeToken) =
1162
+ _findRouteTerminal({destProjectId: destProjectId, token: token, preferredToken: preferredToken});
1163
+ if (address(destTerminal) != address(0)) {
1164
+ if (preferredToken != address(0)) {
1165
+ (routeToken, amount) =
1166
+ _alignTokenToPreferredToken({token: token, amount: amount, preferredToken: preferredToken});
1183
1167
  }
1168
+ return (destTerminal, routeToken, amount);
1184
1169
  }
1185
1170
 
1186
- // Use the override if provided, otherwise look up the project ID from the token.
1187
- uint256 sourceProjectId =
1188
- _sourceProjectIdOf({sourceProjectIdOverride: sourceProjectIdOverride, token: token});
1171
+ uint256 sourceProjectId = _sourceProjectIdOf(token);
1189
1172
 
1190
1173
  // If it's not a JB project token, return as-is (caller handles the swap).
1191
1174
  if (sourceProjectId == 0) return (IJBTerminal(address(0)), token, amount);
@@ -1236,7 +1219,6 @@ contract JBRouterTerminal is
1236
1219
 
1237
1220
  // Update for next iteration.
1238
1221
  token = tokenToReclaim;
1239
- sourceProjectIdOverride = 0;
1240
1222
 
1241
1223
  unchecked {
1242
1224
  ++i;
@@ -1410,7 +1392,7 @@ contract JBRouterTerminal is
1410
1392
  address v4In = normalizedTokenIn == address(WRAPPED_NATIVE_TOKEN) ? address(0) : normalizedTokenIn;
1411
1393
 
1412
1394
  // Determine the V4 swap direction by comparing the input token to currency0 in the pool key.
1413
- bool zeroForOne = _unwrapCurrency(key.currency0) == v4In;
1395
+ bool zeroForOne = Currency.unwrap(key.currency0) == v4In;
1414
1396
 
1415
1397
  // Use extreme sqrtPriceLimitX96 to allow full swap execution. Slippage is enforced by
1416
1398
  // the post-swap minAmountOut check in the unlock callback.
@@ -1525,7 +1507,11 @@ contract JBRouterTerminal is
1525
1507
 
1526
1508
  // Resolve what token the destination project accepts and which terminal to use.
1527
1509
  (tokenOut, destTerminal) = _PAY_ROUTE_RESOLVER.resolveTokenOut({
1528
- router: IJBPayRoutePreviewer(address(this)), projectId: destProjectId, tokenIn: tokenIn, metadata: metadata
1510
+ router: IJBPayRoutePreviewer(address(this)),
1511
+ wrappedNativeToken: address(WRAPPED_NATIVE_TOKEN),
1512
+ projectId: destProjectId,
1513
+ tokenIn: tokenIn,
1514
+ metadata: metadata
1529
1515
  });
1530
1516
 
1531
1517
  // Convert the post-cashout route input into the resolved destination token and refund any leftover input.
@@ -1595,7 +1581,7 @@ contract JBRouterTerminal is
1595
1581
  /// @param destProjectId The destination project to reach.
1596
1582
  /// @param tokenIn The current route input token.
1597
1583
  /// @param amount The current route input amount.
1598
- /// @param metadata Metadata that may include a cashout-source override.
1584
+ /// @param metadata Metadata that may include a cashOutMinReclaimed floor.
1599
1585
  /// @param preferredToken The preferred token to target during any cashout loop.
1600
1586
  /// @return resolvedTerminal The terminal found by the cashout loop, or address(0) if conversion should continue.
1601
1587
  /// @return routedTokenIn The token that remains to be routed after the cashout step.
@@ -1610,20 +1596,14 @@ contract JBRouterTerminal is
1610
1596
  internal
1611
1597
  returns (IJBTerminal resolvedTerminal, address routedTokenIn, uint256 routedAmountIn)
1612
1598
  {
1613
- // Read any one-shot source-project override that should force the first cashout hop.
1614
- (uint256 sourceProjectIdOverride,) = _cashOutSourceFrom(metadata);
1615
- // Start from the explicit override, then fall back to inferring the source project from the input token.
1616
- uint256 sourceProjectId = _sourceProjectIdOf({sourceProjectIdOverride: sourceProjectIdOverride, token: tokenIn});
1617
-
1618
- // Leave the route unchanged when the input is not a JB project token and no override was provided.
1619
- if (sourceProjectId == 0) return (resolvedTerminal, tokenIn, amount);
1599
+ // Leave the route unchanged when the input is not a JB project token.
1600
+ if (_sourceProjectIdOf(tokenIn) == 0) return (resolvedTerminal, tokenIn, amount);
1620
1601
 
1621
1602
  // Cash out through the discovered source project before the caller continues with direct routing or swaps.
1622
1603
  return _cashOutLoop({
1623
1604
  destProjectId: destProjectId,
1624
1605
  token: tokenIn,
1625
1606
  amount: amount,
1626
- sourceProjectIdOverride: sourceProjectIdOverride,
1627
1607
  metadata: metadata,
1628
1608
  preferredToken: preferredToken
1629
1609
  });
@@ -1667,7 +1647,7 @@ contract JBRouterTerminal is
1667
1647
  /// @param amount The amount of `currency` to settle.
1668
1648
  /// @param canUseExistingNativeBalance Whether already-held raw native tokens can be used before unwrapping.
1669
1649
  function _settleV4(Currency currency, uint256 amount, bool canUseExistingNativeBalance) internal {
1670
- if (_unwrapCurrency(currency) == address(0)) {
1650
+ if (Currency.unwrap(currency) == address(0)) {
1671
1651
  // Native-funded routes may spend the ETH they already hold.
1672
1652
  // Wrapped-native-funded routes must not consume unrelated raw native tokens already sitting on the router.
1673
1653
  if (canUseExistingNativeBalance) {
@@ -1686,7 +1666,7 @@ contract JBRouterTerminal is
1686
1666
  // ERC20 settlement requires PoolManager to observe the token first (`sync`), then receive the transfer,
1687
1667
  // then finalize the accounting with `settle`.
1688
1668
  POOL_MANAGER.sync(currency);
1689
- IERC20(_unwrapCurrency(currency)).safeTransfer({to: address(POOL_MANAGER), value: amount});
1669
+ IERC20(Currency.unwrap(currency)).safeTransfer({to: address(POOL_MANAGER), value: amount});
1690
1670
  POOL_MANAGER.settle();
1691
1671
  }
1692
1672
  }
@@ -1699,7 +1679,7 @@ contract JBRouterTerminal is
1699
1679
  POOL_MANAGER.take({currency: currency, to: address(this), amount: amount});
1700
1680
 
1701
1681
  // If native token output, wrap it (downstream _handleSwap unwraps if needed).
1702
- if (_unwrapCurrency(currency) == address(0)) _wrapNativeToken(amount);
1682
+ if (Currency.unwrap(currency) == address(0)) _wrapNativeToken(amount);
1703
1683
  }
1704
1684
 
1705
1685
  /// @notice Transfer tokens from one address to another using direct approval, `safeTransfer`, or Permit2 as a
@@ -1859,22 +1839,6 @@ contract JBRouterTerminal is
1859
1839
  if (address(pool.v3Pool) != address(0)) return pool.v3Pool.liquidity();
1860
1840
  }
1861
1841
 
1862
- /// @notice Parse the optional `cashOutSource` metadata.
1863
- /// @param metadata The metadata to inspect for credit cashout overrides.
1864
- /// @return sourceProjectId The source project override, or 0 if none is specified.
1865
- /// @return amount The credit amount, or 0 if none is specified.
1866
- function _cashOutSourceFrom(bytes calldata metadata)
1867
- internal
1868
- view
1869
- returns (uint256 sourceProjectId, uint256 amount)
1870
- {
1871
- // Read the optional cash-out source payload from the metadata blob.
1872
- (bool exists, bytes memory creditData) = _getDataFor({metadata: metadata, id: _CASH_OUT_SOURCE_ID});
1873
-
1874
- // Decode the source project and credit amount if the payload is present.
1875
- if (exists) (sourceProjectId, amount) = abi.decode(creditData, (uint256, uint256));
1876
- }
1877
-
1878
1842
  /// @notice Return the ERC-2771 context suffix length used by the inherited forwarder-aware context.
1879
1843
  /// @return suffixLength The number of bytes appended to calldata for the forwarded sender.
1880
1844
  /// @dev ERC-2771 specifies the context as a single address, which is always 20 bytes.
@@ -1899,7 +1863,8 @@ contract JBRouterTerminal is
1899
1863
  returns (IJBTerminal terminal, address resultToken)
1900
1864
  {
1901
1865
  if (preferredToken != address(0)) {
1902
- if (_hasSameRoutingAsset({tokenA: token, tokenB: preferredToken})) {
1866
+ // Same-routing-asset check: exact match, or both normalize to the same wrapped-native form.
1867
+ if (token == preferredToken || _normalize(token) == _normalize(preferredToken)) {
1903
1868
  terminal = _usablePrimaryTerminalOf({projectId: destProjectId, token: preferredToken});
1904
1869
  if (address(terminal) != address(0)) return (terminal, preferredToken);
1905
1870
  }
@@ -2099,7 +2064,7 @@ contract JBRouterTerminal is
2099
2064
 
2100
2065
  // Priority 1: Does the destination project directly accept this token through a usable terminal?
2101
2066
  IJBTerminal destTerminal = _usablePrimaryTerminalOf({projectId: destProjectId, token: contextToken});
2102
- candidates = _recordCashOutPathCandidate({
2067
+ _recordCashOutPathCandidate({
2103
2068
  candidates: candidates, contextToken: contextToken, terminal: terminal, destTerminal: destTerminal
2104
2069
  });
2105
2070
 
@@ -2133,11 +2098,11 @@ contract JBRouterTerminal is
2133
2098
  }
2134
2099
 
2135
2100
  /// @notice Record a reclaim token as a direct, recursive, or base fallback during cashout-path discovery.
2136
- /// @param candidates The current fallback candidates accumulated so far.
2101
+ /// @dev Mutates `candidates` in place (memory struct passed by reference) — no return value needed.
2102
+ /// @param candidates The current fallback candidates accumulated so far. Updated in-place by this call.
2137
2103
  /// @param contextToken The token exposed by the current cashout terminal accounting context.
2138
2104
  /// @param terminal The cashout terminal that can reclaim `contextToken`.
2139
2105
  /// @param destTerminal The destination project's direct terminal for `contextToken`, if any.
2140
- /// @return updatedCandidates The fallback set after considering `contextToken`.
2141
2106
  function _recordCashOutPathCandidate(
2142
2107
  CashOutPathCandidates memory candidates,
2143
2108
  address contextToken,
@@ -2146,10 +2111,7 @@ contract JBRouterTerminal is
2146
2111
  )
2147
2112
  internal
2148
2113
  view
2149
- returns (CashOutPathCandidates memory updatedCandidates)
2150
2114
  {
2151
- updatedCandidates = candidates;
2152
-
2153
2115
  // Treat native ETH as a non-recursive base asset. For ERC-20s, detect whether the token is itself a JB
2154
2116
  // project token so recursive and base fallbacks stay disjoint.
2155
2117
  bool isJbProjectToken;
@@ -2158,22 +2120,22 @@ contract JBRouterTerminal is
2158
2120
  }
2159
2121
 
2160
2122
  // Record the first directly accepted token only when its destination terminal is actually usable.
2161
- if (address(destTerminal) != address(0) && address(updatedCandidates.directFallbackTerminal) == address(0)) {
2162
- updatedCandidates.directFallbackToken = contextToken;
2163
- updatedCandidates.directFallbackTerminal = terminal;
2123
+ if (address(destTerminal) != address(0) && address(candidates.directFallbackTerminal) == address(0)) {
2124
+ candidates.directFallbackToken = contextToken;
2125
+ candidates.directFallbackTerminal = terminal;
2164
2126
  }
2165
2127
 
2166
2128
  // Record the first JB project token so the router can recurse through another cashout hop if no direct or
2167
2129
  // base-token exit ends up existing.
2168
- if (address(updatedCandidates.fallbackTerminal) == address(0) && isJbProjectToken) {
2169
- updatedCandidates.fallbackToken = contextToken;
2170
- updatedCandidates.fallbackTerminal = terminal;
2130
+ if (address(candidates.fallbackTerminal) == address(0) && isJbProjectToken) {
2131
+ candidates.fallbackToken = contextToken;
2132
+ candidates.fallbackTerminal = terminal;
2171
2133
  }
2172
2134
 
2173
2135
  // Record the first non-JB base-token fallback so the router can at least continue via a swap route.
2174
- if (address(updatedCandidates.baseFallbackTerminal) == address(0) && !isJbProjectToken) {
2175
- updatedCandidates.baseFallbackToken = contextToken;
2176
- updatedCandidates.baseFallbackTerminal = terminal;
2136
+ if (address(candidates.baseFallbackTerminal) == address(0) && !isJbProjectToken) {
2137
+ candidates.baseFallbackToken = contextToken;
2138
+ candidates.baseFallbackTerminal = terminal;
2177
2139
  }
2178
2140
  }
2179
2141
 
@@ -2510,28 +2472,10 @@ contract JBRouterTerminal is
2510
2472
  }
2511
2473
  }
2512
2474
 
2513
- /// @notice A view-only mirror of `_acceptFundsFor` used for previews.
2514
- /// @dev Preview semantics use the caller-supplied `amount` as the intended input amount.
2515
- /// @param amount The caller-supplied payment amount.
2516
- /// @param metadata The metadata to inspect for credit cashout overrides.
2517
- /// @return The effective amount that routing should use.
2518
- function _previewAcceptFundsFor(uint256 amount, bytes calldata metadata) internal view returns (uint256) {
2519
- // Credit cashouts use the credit amount encoded in metadata rather than the raw token amount.
2520
- (uint256 sourceProjectId, uint256 creditAmount) = _cashOutSourceFrom(metadata);
2521
-
2522
- // Mirror execution semantics exactly: the presence of a source override means the decoded
2523
- // credit amount, even `0`, is the effective routed amount.
2524
- if (sourceProjectId != 0) return creditAmount;
2525
-
2526
- // Otherwise, use the caller-specified amount unchanged.
2527
- return amount;
2528
- }
2529
-
2530
2475
  /// @notice A view-only mirror of `_cashOutLoop`.
2531
2476
  /// @param destProjectId The ID of the destination project.
2532
2477
  /// @param token The current token to process.
2533
2478
  /// @param amount The amount of the current token.
2534
- /// @param sourceProjectIdOverride An optional source project override from metadata.
2535
2479
  /// @param metadata Bytes in `JBMetadataResolver`'s format.
2536
2480
  /// @return destTerminal The terminal that accepts the final token, if found.
2537
2481
  /// @return finalToken The token after all cash-out steps.
@@ -2540,7 +2484,6 @@ contract JBRouterTerminal is
2540
2484
  uint256 destProjectId,
2541
2485
  address token,
2542
2486
  uint256 amount,
2543
- uint256 sourceProjectIdOverride,
2544
2487
  bytes calldata metadata,
2545
2488
  address preferredToken
2546
2489
  )
@@ -2555,17 +2498,12 @@ contract JBRouterTerminal is
2555
2498
 
2556
2499
  // Walk the same cash-out path execution would take, bounded to prevent circular routes.
2557
2500
  for (uint256 i; i < _MAX_CASHOUT_ITERATIONS;) {
2558
- // Skip destination checks when the forced first cashout hasn't happened yet (mirrors _cashOutLoop).
2559
- if (sourceProjectIdOverride == 0) {
2560
- address routeToken;
2561
- (destTerminal, routeToken) =
2562
- _findRouteTerminal({destProjectId: destProjectId, token: token, preferredToken: preferredToken});
2563
- if (address(destTerminal) != address(0)) return (destTerminal, routeToken, amount);
2564
- }
2501
+ address routeToken;
2502
+ (destTerminal, routeToken) =
2503
+ _findRouteTerminal({destProjectId: destProjectId, token: token, preferredToken: preferredToken});
2504
+ if (address(destTerminal) != address(0)) return (destTerminal, routeToken, amount);
2565
2505
 
2566
- // Use the override once when present; otherwise infer the source project from the current JB token.
2567
- uint256 sourceProjectId =
2568
- _sourceProjectIdOf({sourceProjectIdOverride: sourceProjectIdOverride, token: token});
2506
+ uint256 sourceProjectId = _sourceProjectIdOf(token);
2569
2507
 
2570
2508
  // If this is no longer a JB project token, stop cashing out and let the caller continue routing from it.
2571
2509
  if (sourceProjectId == 0) return (IJBTerminal(address(0)), token, amount);
@@ -2592,9 +2530,6 @@ contract JBRouterTerminal is
2592
2530
  // Continue previewing from the token reclaimed in this hop.
2593
2531
  token = tokenToReclaim;
2594
2532
 
2595
- // Consume the one-shot override so later hops derive their project from the reclaimed token itself.
2596
- sourceProjectIdOverride = 0;
2597
-
2598
2533
  unchecked {
2599
2534
  ++i;
2600
2535
  }
@@ -2710,13 +2645,6 @@ contract JBRouterTerminal is
2710
2645
  return Currency.wrap(token);
2711
2646
  }
2712
2647
 
2713
- /// @notice Unwrap a Uniswap V4 `Currency` back into an address.
2714
- /// @param currency The currency value to unwrap.
2715
- /// @return token The unwrapped token address.
2716
- function _unwrapCurrency(Currency currency) internal pure returns (address token) {
2717
- return Currency.unwrap(currency);
2718
- }
2719
-
2720
2648
  /// @notice Return the V3 fee tier at the given index.
2721
2649
  /// @dev Replaces the storage array `_FEE_TIERS` with a pure function (no SLOAD).
2722
2650
  /// @param index The tier index (0-3), ordered by commonality: 0.3%, 0.05%, 1%, 0.01%.
@@ -30,7 +30,6 @@ interface IJBPayRoutePreviewer {
30
30
  /// @param destProjectId The destination project the router is trying to pay.
31
31
  /// @param token The current token to route.
32
32
  /// @param amount The amount of `token` to preview.
33
- /// @param sourceProjectIdOverride The one-shot source project override encoded in metadata, if any.
34
33
  /// @param metadata Metadata forwarded into preview helpers.
35
34
  /// @param preferredToken The token the cashout loop should prefer to land on, or `address(0)` for no preference.
36
35
  /// @return destTerminal The terminal reached by the cashout loop, or address(0) if routing should continue.
@@ -40,7 +39,6 @@ interface IJBPayRoutePreviewer {
40
39
  uint256 destProjectId,
41
40
  address token,
42
41
  uint256 amount,
43
- uint256 sourceProjectIdOverride,
44
42
  bytes calldata metadata,
45
43
  address preferredToken
46
44
  )
@@ -11,6 +11,8 @@ import {IJBPayRoutePreviewer} from "./IJBPayRoutePreviewer.sol";
11
11
  interface IJBPayRouteResolver {
12
12
  /// @notice Preview the best pay route for a router terminal.
13
13
  /// @param router The router terminal whose preview helpers to use.
14
+ /// @param wrappedNativeToken The router's wrapped-native-token address, passed in so the resolver does not have to
15
+ /// call back into the router for it on every normalization step.
14
16
  /// @param projectId The destination project that would receive the payment.
15
17
  /// @param tokenIn The token currently available to route.
16
18
  /// @param amount The amount of `tokenIn` to preview.
@@ -25,6 +27,7 @@ interface IJBPayRouteResolver {
25
27
  /// @return hookSpecifications The hook specifications returned by the chosen terminal preview.
26
28
  function previewBestPayRoute(
27
29
  IJBPayRoutePreviewer router,
30
+ address wrappedNativeToken,
28
31
  uint256 projectId,
29
32
  address tokenIn,
30
33
  uint256 amount,
@@ -45,6 +48,7 @@ interface IJBPayRouteResolver {
45
48
 
46
49
  /// @notice Preview a specific candidate pay route so callers can isolate revert-prone candidates with `try/catch`.
47
50
  /// @param router The router terminal whose preview helpers to use.
51
+ /// @param wrappedNativeToken The router's wrapped-native-token address.
48
52
  /// @param projectId The destination project that would receive the payment.
49
53
  /// @param tokenIn The token currently available to route.
50
54
  /// @param amount The amount of `tokenIn` to preview.
@@ -61,6 +65,7 @@ interface IJBPayRouteResolver {
61
65
  /// @return hookSpecifications The hook specifications returned by the terminal preview.
62
66
  function previewPayRouteForCandidate(
63
67
  IJBPayRoutePreviewer router,
68
+ address wrappedNativeToken,
64
69
  uint256 projectId,
65
70
  address tokenIn,
66
71
  uint256 amount,
@@ -83,6 +88,7 @@ interface IJBPayRouteResolver {
83
88
 
84
89
  /// @notice Determine what output token a project accepts for a given input token.
85
90
  /// @param router The router whose view helpers to use.
91
+ /// @param wrappedNativeToken The router's wrapped-native-token address.
86
92
  /// @param projectId The destination project to pay.
87
93
  /// @param tokenIn The input token to route.
88
94
  /// @param metadata Metadata forwarded into route-token resolution.
@@ -90,6 +96,7 @@ interface IJBPayRouteResolver {
90
96
  /// @return destTerminal The terminal that accepts `tokenOut`.
91
97
  function resolveTokenOut(
92
98
  IJBPayRoutePreviewer router,
99
+ address wrappedNativeToken,
93
100
  uint256 projectId,
94
101
  address tokenIn,
95
102
  bytes calldata metadata
@@ -102,6 +109,7 @@ interface IJBPayRouteResolver {
102
109
  /// @dev Called via `self.previewFallbackRoute(...)` so `try/catch` can absorb reverts from broken
103
110
  /// terminals or price feeds without bricking the entire best-route preview.
104
111
  /// @param routePreviewer The router terminal whose preview helpers to use for simulating the route.
112
+ /// @param wrappedNativeToken The router's wrapped-native-token address.
105
113
  /// @param destProjectId The project to pay through the fallback route.
106
114
  /// @param tokenIn The token the payer is sending.
107
115
  /// @param amountIn The amount of `tokenIn` to route.
@@ -116,6 +124,7 @@ interface IJBPayRouteResolver {
116
124
  /// @return hookSpecifications Any pay-hook specifications returned by the terminal preview.
117
125
  function previewFallbackRoute(
118
126
  IJBPayRoutePreviewer routePreviewer,
127
+ address wrappedNativeToken,
119
128
  uint256 destProjectId,
120
129
  address tokenIn,
121
130
  uint256 amountIn,