@bananapus/router-terminal-v6 0.0.41 → 0.0.43
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 +29 -0
- package/foundry.toml +1 -0
- package/package.json +1 -1
- package/script/Deploy.s.sol +10 -5
- package/src/JBPayRouteResolver.sol +90 -60
- package/src/JBRouterTerminal.sol +123 -195
- package/src/JBRouterTerminalRegistry.sol +47 -19
- package/src/interfaces/IJBPayRoutePreviewer.sol +0 -2
- package/src/interfaces/IJBPayRouteResolver.sol +9 -0
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.
|
package/foundry.toml
CHANGED
package/package.json
CHANGED
package/script/Deploy.s.sol
CHANGED
|
@@ -172,19 +172,24 @@ contract DeployScript is Script, Sphinx {
|
|
|
172
172
|
trustedForwarder: trustedForwarder
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
-
// Deploy the router terminal
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/// @
|
|
44
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
403
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
594
|
-
(
|
|
595
|
-
|
|
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 ==
|
|
662
|
-
tokenOut = tokenIn == 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({
|
|
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,
|
|
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(
|
|
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,
|
|
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({
|
|
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
|
package/src/JBRouterTerminal.sol
CHANGED
|
@@ -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
|
|
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
|
|
190
|
-
address
|
|
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
|
-
|
|
202
|
-
_PAY_ROUTE_RESOLVER = IJBPayRouteResolver(address(new JBPayRouteResolver({directory: directory
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
|
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
|
-
|
|
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`
|
|
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
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
(
|
|
1176
|
-
|
|
1177
|
-
|
|
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
|
-
|
|
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 =
|
|
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)),
|
|
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
|
|
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
|
-
//
|
|
1614
|
-
(
|
|
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 (
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/// @
|
|
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(
|
|
2162
|
-
|
|
2163
|
-
|
|
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(
|
|
2169
|
-
|
|
2170
|
-
|
|
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(
|
|
2175
|
-
|
|
2176
|
-
|
|
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
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
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
|
-
|
|
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%.
|
|
@@ -26,6 +26,8 @@ import {IJBForwardingTerminal} from "./interfaces/IJBForwardingTerminal.sol";
|
|
|
26
26
|
import {IJBPayerTracker} from "./interfaces/IJBPayerTracker.sol";
|
|
27
27
|
import {IJBRouterTerminalRegistry} from "./interfaces/IJBRouterTerminalRegistry.sol";
|
|
28
28
|
|
|
29
|
+
import {JBForwardingCheck} from "./libraries/JBForwardingCheck.sol";
|
|
30
|
+
|
|
29
31
|
import {DefaultTerminalSegment} from "./structs/DefaultTerminalSegment.sol";
|
|
30
32
|
|
|
31
33
|
/// @notice A forwarding layer that lets each project choose which router terminal receives its payments, with an
|
|
@@ -306,26 +308,45 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
306
308
|
return ERC2771Context._msgSender();
|
|
307
309
|
}
|
|
308
310
|
|
|
309
|
-
/// @notice
|
|
311
|
+
/// @notice Returns the original payer to record in transient storage. If `_msgSender()` is a
|
|
312
|
+
/// contract that exposes `IJBPayerTracker.originalPayer()` and that getter returns a non-zero
|
|
313
|
+
/// value, the upstream payer is propagated so a forwarding chain (project payer -> registry
|
|
314
|
+
/// -> router) refunds the true originator instead of the intermediary. Otherwise the direct
|
|
315
|
+
/// caller is recorded — the common direct-call case.
|
|
316
|
+
/// @return originalPayerOrSender The upstream payer if the resolved sender is a forwarding
|
|
317
|
+
/// tracker that returns a non-zero address; otherwise the resolved sender itself.
|
|
318
|
+
function _originalPayerOrSender() internal view returns (address originalPayerOrSender) {
|
|
319
|
+
// Resolve through ERC-2771 so a trusted meta-tx forwarder is transparently unwrapped.
|
|
320
|
+
address sender = _msgSender();
|
|
321
|
+
|
|
322
|
+
// EOAs and contracts without code can't implement IJBPayerTracker — record them directly.
|
|
323
|
+
if (sender.code.length == 0) return sender;
|
|
324
|
+
|
|
325
|
+
// Probe the caller for IJBPayerTracker.originalPayer() via staticcall so a reverting or
|
|
326
|
+
// non-conformant caller does not bubble up — fall back to the resolved sender on failure.
|
|
327
|
+
(bool ok, bytes memory data) = sender.staticcall(abi.encodeWithSelector(IJBPayerTracker.originalPayer.selector));
|
|
328
|
+
|
|
329
|
+
// Caller doesn't implement the interface (revert) or returned a truncated payload — treat
|
|
330
|
+
// it as a direct payment from the caller itself.
|
|
331
|
+
if (!ok || data.length < 32) return sender;
|
|
332
|
+
|
|
333
|
+
// Decode the upstream payer the caller advertised. A zero value means "no upstream tracked",
|
|
334
|
+
// which only happens when the caller is itself receiving a direct call — record the caller.
|
|
335
|
+
address upstream = abi.decode(data, (address));
|
|
336
|
+
return upstream == address(0) ? sender : upstream;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/// @notice Reject terminal choices that would forward the project back into this registry,
|
|
340
|
+
/// directly or transitively. Walks up to the depth limit `JBForwardingCheck` enforces so a
|
|
341
|
+
/// chain registry -> A -> B -> registry is caught (the previous one-hop probe missed those
|
|
342
|
+
/// transitive cycles, letting a project lock itself into a route that always loops).
|
|
310
343
|
/// @param projectId The project to validate forwarding for.
|
|
311
344
|
/// @param terminal The terminal to validate.
|
|
312
345
|
function _requireNonCircularTerminalFor(uint256 projectId, IJBTerminal terminal) internal view {
|
|
313
346
|
// Reject direct self-selection so the registry cannot forward a project to itself.
|
|
314
347
|
if (address(terminal) == address(this)) revert JBRouterTerminalRegistry_CircularForward(terminal);
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (address(terminal).code.length == 0) return;
|
|
318
|
-
|
|
319
|
-
// If the candidate is another forwarding terminal, ask where this project would end up.
|
|
320
|
-
try IJBForwardingTerminal(address(terminal)).terminalOf({projectId: projectId}) returns (
|
|
321
|
-
IJBTerminal downstreamTerminal
|
|
322
|
-
) {
|
|
323
|
-
// Reject one-hop forwarding cycles that bounce this project back into the registry.
|
|
324
|
-
if (address(downstreamTerminal) == address(this)) {
|
|
325
|
-
revert JBRouterTerminalRegistry_CircularForward({terminal: terminal});
|
|
326
|
-
}
|
|
327
|
-
} catch {
|
|
328
|
-
// Non-forwarding terminals are valid choices; failed interface probes should not block them.
|
|
348
|
+
if (JBForwardingCheck.isCircularTerminal({target: address(this), projectId: projectId, terminal: terminal})) {
|
|
349
|
+
revert JBRouterTerminalRegistry_CircularForward({terminal: terminal});
|
|
329
350
|
}
|
|
330
351
|
}
|
|
331
352
|
|
|
@@ -409,8 +430,9 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
409
430
|
address previousPayer = originalPayer;
|
|
410
431
|
|
|
411
432
|
// Store the original payer in transient storage so downstream router terminals can refund partial-fill
|
|
412
|
-
// leftovers to the true payer.
|
|
413
|
-
|
|
433
|
+
// leftovers to the true payer. If the immediate caller is itself a forwarding intermediary that exposes
|
|
434
|
+
// its own original payer, propagate that — otherwise refunds in nested forwards stop at the intermediary.
|
|
435
|
+
originalPayer = _originalPayerOrSender();
|
|
414
436
|
|
|
415
437
|
// Reject forwards that would bounce straight back into this call's immediate caller.
|
|
416
438
|
_enforceNoCircularForward(terminal);
|
|
@@ -559,8 +581,9 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
559
581
|
address previousPayer = originalPayer;
|
|
560
582
|
|
|
561
583
|
// Store the original payer in transient storage so downstream router terminals can refund partial-fill
|
|
562
|
-
// leftovers to the true payer.
|
|
563
|
-
|
|
584
|
+
// leftovers to the true payer. If the immediate caller is itself a forwarding intermediary that exposes
|
|
585
|
+
// its own original payer, propagate that — otherwise refunds in nested forwards stop at the intermediary.
|
|
586
|
+
originalPayer = _originalPayerOrSender();
|
|
564
587
|
|
|
565
588
|
// Reject forwards that would bounce straight back into this call's immediate caller.
|
|
566
589
|
_enforceNoCircularForward(terminal);
|
|
@@ -598,6 +621,11 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
598
621
|
|
|
599
622
|
uint256 count = PROJECTS.count();
|
|
600
623
|
|
|
624
|
+
// Reject defaults that would route any current project back into the registry through a
|
|
625
|
+
// transitive forwarding chain. Probed at the current project-count snapshot since the
|
|
626
|
+
// default is what unconfigured (and all-future) projects will resolve to.
|
|
627
|
+
_requireNonCircularTerminalFor({projectId: count, terminal: terminal});
|
|
628
|
+
|
|
601
629
|
// Snapshot the OUTGOING default so existing projects (ID <= count) continue resolving to
|
|
602
630
|
// it, not to the new default. First-ever call has defaultTerminal == 0 and nothing to snapshot.
|
|
603
631
|
if (address(defaultTerminal) != address(0)) {
|
|
@@ -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,
|