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