@bananapus/router-terminal-v6 0.0.51 → 0.0.54
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/README.md +1 -1
- package/package.json +4 -4
- package/references/operations.md +2 -2
- package/references/runtime.md +4 -4
- package/script/Deploy.s.sol +1 -1
- package/src/JBPayRouteResolver.sol +3 -3
- package/src/JBRouterTerminal.sol +30 -16
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ script/
|
|
|
126
126
|
- swap previews are best-effort estimates and depend on current pool state plus caller-supplied quote data
|
|
127
127
|
- recursive cash-out routing increases complexity when the input token is itself a Juicebox project token
|
|
128
128
|
- slippage and sandwich resistance depend on the quality of the chosen quote path
|
|
129
|
-
-
|
|
129
|
+
- `addToBalanceOf` rejects lossy terminal pulls; `pay` cannot reliably detect final-hop fee-on-transfer loss because pay hooks can consume tokens during settlement
|
|
130
130
|
|
|
131
131
|
The most common reader mistake here is to stop at the router and forget to inspect the terminal that actually receives the value.
|
|
132
132
|
|
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.54",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-router-terminal-v6'"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@bananapus/buyback-hook-v6": "^0.0.
|
|
28
|
-
"@bananapus/core-v6": "^0.0.
|
|
27
|
+
"@bananapus/buyback-hook-v6": "^0.0.55",
|
|
28
|
+
"@bananapus/core-v6": "^0.0.68",
|
|
29
29
|
"@bananapus/permission-ids-v6": "^0.0.27",
|
|
30
|
-
"@bananapus/univ4-router-v6": "^0.0.
|
|
30
|
+
"@bananapus/univ4-router-v6": "^0.0.40",
|
|
31
31
|
"@openzeppelin/contracts": "5.6.1",
|
|
32
32
|
"@uniswap/permit2": "github:Uniswap/permit2#cc56ad0f3439c502c246fc5cfcc3db92bb8b7219",
|
|
33
33
|
"@uniswap/v3-core": "github:Uniswap/v3-core#6562c52e8f75f0c10f9deaf44861847585fc8129",
|
package/references/operations.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
## Change Checklist
|
|
10
10
|
|
|
11
11
|
- If you edit route discovery, verify both direct acceptance and swap-based routes.
|
|
12
|
-
- If you edit the cash-out loop, check
|
|
12
|
+
- If you edit the cash-out loop, check project-token cash-out flows and fork tests, not only simple payments.
|
|
13
13
|
- If you edit slippage or quote logic, inspect [`src/JBPayRouteResolver.sol`](../src/JBPayRouteResolver.sol) and the preview tests together.
|
|
14
14
|
- If you edit preview behavior, verify route ranking still normalizes buyback-hook hints and still agrees with execution.
|
|
15
15
|
- If you edit refund or partial-fill handling, verify baseline snapshots and destination-terminal receipt enforcement together.
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
- Preview output drifts from execution because quote and execution paths were edited independently.
|
|
22
22
|
- Registry state makes a project use a different router than expected.
|
|
23
23
|
- Metadata overrides force an output token or cash-out source that the caller did not intend.
|
|
24
|
-
-
|
|
24
|
+
- On `addToBalanceOf` paths, a terminal-facing ERC-20 receipt mismatch indicates a non-standard final-hop token path.
|
|
25
25
|
|
|
26
26
|
## Useful Proof Points
|
|
27
27
|
|
package/references/runtime.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
## Runtime Path
|
|
10
10
|
|
|
11
|
-
1. The router accepts
|
|
11
|
+
1. The router accepts native tokens, ERC-20s, or claimed Juicebox project-token ERC-20s.
|
|
12
12
|
2. If the input is a Juicebox project token, the router may enter a cash-out loop first.
|
|
13
13
|
3. The router resolves the desired output token using direct acceptance, wrap/unwrap equivalence, metadata overrides, or pool discovery.
|
|
14
14
|
4. It converts value through direct forwarding, wrap/unwrap, Uniswap V3, or Uniswap V4.
|
|
@@ -17,18 +17,18 @@
|
|
|
17
17
|
## High-Risk Areas
|
|
18
18
|
|
|
19
19
|
- Preview and execution parity: changes to quote selection or route discovery should be checked against both preview and mutative paths.
|
|
20
|
-
- V4 discovery scope: the router
|
|
20
|
+
- V4 discovery scope: the router searches both vanilla V4 pools and pools using the configured canonical `UNIV4_HOOK`.
|
|
21
21
|
- Cash-out loop behavior: recursive routing through project tokens can create subtle loop or slippage issues.
|
|
22
22
|
- Callback validation: V3 and V4 callback guards are security-critical and should not drift.
|
|
23
23
|
- Leftover/refund handling: refunds can route to the original payer or fallback recipient depending on context.
|
|
24
24
|
- Dynamic accounting contexts: this repo intentionally synthesizes accounting contexts instead of storing a static token list.
|
|
25
|
-
- Final terminal-facing ERC-20 receipt enforcement:
|
|
25
|
+
- Final terminal-facing ERC-20 receipt enforcement: `addToBalanceOf` rejects lossy terminal pulls, while `pay` does not enforce receipt deltas because pay hooks can consume tokens during settlement. The registry does not independently enforce receipts; it relies on the router path it forwards into.
|
|
26
26
|
- Preview normalization: buyback-hook metadata can improve the user-visible preview outcome, so route ranking must normalize hook-returned hints consistently across candidates.
|
|
27
27
|
|
|
28
28
|
## Tests To Trust First
|
|
29
29
|
|
|
30
30
|
- [`test/RouterTerminalPreviewFork.t.sol`](../test/RouterTerminalPreviewFork.t.sol) for preview-path behavior.
|
|
31
|
-
- [`test/RouterTerminalCashOutFork.t.sol`](../test/RouterTerminalCashOutFork.t.sol) and [`test/
|
|
31
|
+
- [`test/RouterTerminalCashOutFork.t.sol`](../test/RouterTerminalCashOutFork.t.sol) and [`test/RouterTerminalFeeCashOutFork.t.sol`](../test/RouterTerminalFeeCashOutFork.t.sol) for project-token cash-out routing.
|
|
32
32
|
- [`test/RouterTerminalReentrancy.t.sol`](../test/RouterTerminalReentrancy.t.sol) for callback and reentrancy-sensitive behavior.
|
|
33
33
|
- [`test/RouterTerminalFork.t.sol`](../test/RouterTerminalFork.t.sol), [`test/RouterTerminalMultihopFork.t.sol`](../test/RouterTerminalMultihopFork.t.sol), and [`test/invariant/RouterTerminalInvariant.t.sol`](../test/invariant/RouterTerminalInvariant.t.sol) for live routing assumptions.
|
|
34
34
|
- [`test/regression/CashOutCircularPrimaryTerminal.t.sol`](../test/regression/CashOutCircularPrimaryTerminal.t.sol), [`test/regression/CashOutFallbackPrefersRecursiveLoop.t.sol`](../test/regression/CashOutFallbackPrefersRecursiveLoop.t.sol), [`test/regression/LeftoverRefund.t.sol`](../test/regression/LeftoverRefund.t.sol), and [`test/regression/PreviewPrimaryTerminalMismatch.t.sol`](../test/regression/PreviewPrimaryTerminalMismatch.t.sol) for the misdiagnosis-prone edge cases.
|
package/script/Deploy.s.sol
CHANGED
|
@@ -172,7 +172,7 @@ contract DeployScript is Script, Sphinx {
|
|
|
172
172
|
trustedForwarder: trustedForwarder
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
-
// Deploy the router terminal with chain-
|
|
175
|
+
// Deploy the router terminal with chain-identical CREATE2 inputs; chain-specific constants
|
|
176
176
|
// (WETH + Uniswap V3 factory + V4 PoolManager + V4 hook) are wired afterwards via the
|
|
177
177
|
// DEPLOYER-gated one-shot setChainSpecificConstants setter on the terminal.
|
|
178
178
|
require(address(buyback.hook) != address(0), "RouterTerminal: missing buyback hook");
|
|
@@ -39,9 +39,9 @@ contract JBPayRouteResolver is IJBPayRouteResolver {
|
|
|
39
39
|
/// @param directory The directory storing project terminal relationships.
|
|
40
40
|
/// @dev The wrapped-native-token address is intentionally NOT cached here. The router passes it in as a parameter
|
|
41
41
|
/// (`address wrappedNativeToken`) on every external resolver call and the resolver threads it through internal
|
|
42
|
-
/// helpers. This keeps the resolver's constructor inputs
|
|
43
|
-
///
|
|
44
|
-
/// step inside loops like `_discoverAcceptedToken`.
|
|
42
|
+
/// helpers. This keeps the resolver's constructor inputs byte-identical across chains (no chain-specific WETH
|
|
43
|
+
/// baked in), so its deployed address (router + nonce 1) stays unified and avoids an extra external call per
|
|
44
|
+
/// normalization step inside loops like `_discoverAcceptedToken`.
|
|
45
45
|
constructor(IJBDirectory directory) {
|
|
46
46
|
DIRECTORY = directory;
|
|
47
47
|
}
|
package/src/JBRouterTerminal.sol
CHANGED
|
@@ -87,6 +87,7 @@ contract JBRouterTerminal is
|
|
|
87
87
|
address terminal, address token, uint256 expectedAmount, uint256 actualAmount
|
|
88
88
|
);
|
|
89
89
|
error JBRouterTerminal_PermitAllowanceNotEnough(uint256 amount, uint256 allowance);
|
|
90
|
+
error JBRouterTerminal_QuoteTokenMismatch(address quotedTokenOut, address expectedTokenOut);
|
|
90
91
|
error JBRouterTerminal_SlippageExceeded(uint256 amountOut, uint256 minAmountOut);
|
|
91
92
|
error JBRouterTerminal_Unauthorized(address caller);
|
|
92
93
|
|
|
@@ -121,8 +122,8 @@ contract JBRouterTerminal is
|
|
|
121
122
|
//*********************************************************************//
|
|
122
123
|
|
|
123
124
|
/// @notice The canonical buyback hook whose preview hook specification metadata this router understands.
|
|
124
|
-
/// @dev
|
|
125
|
-
/// `immutable` without breaking the router's own chain-
|
|
125
|
+
/// @dev `JBBuybackHook` is deployed via CREATE2 to a unified address on every chain, so this stays
|
|
126
|
+
/// `immutable` without breaking the router's own chain-identical CREATE2 address.
|
|
126
127
|
address public immutable BUYBACK_HOOK;
|
|
127
128
|
|
|
128
129
|
/// @notice The directory of terminals and controllers for projects.
|
|
@@ -144,10 +145,10 @@ contract JBRouterTerminal is
|
|
|
144
145
|
address internal immutable _DEPLOYER;
|
|
145
146
|
|
|
146
147
|
/// @notice The helper contract used to resolve best pay-route previews without bloating router runtime size.
|
|
147
|
-
/// @dev Deployed in the constructor with chain-
|
|
148
|
+
/// @dev Deployed in the constructor with chain-identical inputs (only `directory` — the resolver does NOT cache
|
|
148
149
|
/// `wrappedNativeToken` 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
|
|
150
|
-
/// CREATE2 and the resolver is deployed at the router's nonce 1, the resolver's address is
|
|
150
|
+
/// avoid an extra external call on each normalization step). Because this router's address is unified via
|
|
151
|
+
/// CREATE2 and the resolver is deployed at the router's nonce 1, the resolver's address is unified too.
|
|
151
152
|
IJBPayRouteResolver internal immutable _PAY_ROUTE_RESOLVER;
|
|
152
153
|
|
|
153
154
|
/// @notice Pre-computed metadata ID for "permit2".
|
|
@@ -196,7 +197,7 @@ contract JBRouterTerminal is
|
|
|
196
197
|
/// @param directory A contract storing directories of terminals and controllers for each project.
|
|
197
198
|
/// @param tokens A contract managing project token balances.
|
|
198
199
|
/// @param permit2 A permit2 utility.
|
|
199
|
-
/// @param buybackHook The canonical buyback hook
|
|
200
|
+
/// @param buybackHook The canonical buyback hook, deployed to the same address on each supported chain.
|
|
200
201
|
/// @param trustedForwarder The trusted forwarder for the contract.
|
|
201
202
|
/// @param deployer The address authorized to call `setChainSpecificConstants` exactly once. Held immutable so the
|
|
202
203
|
/// constructor inputs are byte-identical across chains and the CREATE2 address is unified.
|
|
@@ -909,7 +910,7 @@ contract JBRouterTerminal is
|
|
|
909
910
|
/// @param tokenIn The token currently available to swap.
|
|
910
911
|
/// @param tokenOut The token the swap should deliver.
|
|
911
912
|
/// @param amount The amount of `tokenIn` to preview.
|
|
912
|
-
/// @param metadata Metadata that can provide an explicit quote
|
|
913
|
+
/// @param metadata Metadata that can provide an explicit `(tokenOut, minAmountOut)` quote for the swap.
|
|
913
914
|
/// @return amountOut The predicted amount of `tokenOut` the router would receive.
|
|
914
915
|
function _previewSwapAmountOut(
|
|
915
916
|
address tokenIn,
|
|
@@ -1096,7 +1097,7 @@ contract JBRouterTerminal is
|
|
|
1096
1097
|
}
|
|
1097
1098
|
|
|
1098
1099
|
/// @notice Run the common post-transfer cleanup after forwarding funds into a destination terminal.
|
|
1099
|
-
/// @param destTerminal The terminal that
|
|
1100
|
+
/// @param destTerminal The terminal that received the forwarded call.
|
|
1100
1101
|
/// @param token The token that was forwarded into the destination terminal.
|
|
1101
1102
|
function _afterTransferFor(IJBTerminal destTerminal, address token) internal {
|
|
1102
1103
|
// Revoke any leftover allowance the destination terminal did not pull so routed calls do not leave approvals
|
|
@@ -1188,6 +1189,9 @@ contract JBRouterTerminal is
|
|
|
1188
1189
|
// Pass minTokensReclaimed=0 to the terminal because the buyback hook's sell-side delivers tokens via
|
|
1189
1190
|
// callback (reclaimAmount=0 from the terminal's perspective), which would fail the terminal's own min
|
|
1190
1191
|
// check. The router enforces the user's minimum via the balance-delta check below instead.
|
|
1192
|
+
// Still forward the original metadata on the first hop so the source hook can use the same user floor
|
|
1193
|
+
// when choosing between direct cash-out and its own routed cash-out path.
|
|
1194
|
+
bytes memory hopMetadata = i == 0 ? metadata : bytes("");
|
|
1191
1195
|
cashOutTerminal.cashOutTokensOf({
|
|
1192
1196
|
holder: address(this),
|
|
1193
1197
|
projectId: sourceProjectId,
|
|
@@ -1195,7 +1199,7 @@ contract JBRouterTerminal is
|
|
|
1195
1199
|
tokenToReclaim: tokenToReclaim,
|
|
1196
1200
|
minTokensReclaimed: 0,
|
|
1197
1201
|
beneficiary: payable(address(this)),
|
|
1198
|
-
metadata:
|
|
1202
|
+
metadata: hopMetadata,
|
|
1199
1203
|
referralProjectId: 0
|
|
1200
1204
|
});
|
|
1201
1205
|
|
|
@@ -1258,7 +1262,7 @@ contract JBRouterTerminal is
|
|
|
1258
1262
|
address nOut = _normalize(tokenOut);
|
|
1259
1263
|
|
|
1260
1264
|
if (nIn == nOut) {
|
|
1261
|
-
// Same underlying token
|
|
1265
|
+
// Same underlying token; wrap or unwrap.
|
|
1262
1266
|
if (tokenIn == JBConstants.NATIVE_TOKEN) _wrapNativeToken(amount);
|
|
1263
1267
|
else _unwrapNativeToken(amount);
|
|
1264
1268
|
return amount;
|
|
@@ -2260,7 +2264,8 @@ contract JBRouterTerminal is
|
|
|
2260
2264
|
///
|
|
2261
2265
|
/// Mitigations in place:
|
|
2262
2266
|
/// 1. Users SHOULD provide a `quoteForSwap` value in the payment metadata (obtained from an off-chain
|
|
2263
|
-
/// quoter or RPC simulation).
|
|
2267
|
+
/// quoter or RPC simulation). The quote must encode the output token and minimum output amount. When present,
|
|
2268
|
+
/// this function is bypassed entirely — see `_pickPoolAndQuote`.
|
|
2264
2269
|
/// 2. When a hook implements `IGeomeanOracle.observe(...)`, this function uses that oracle-derived tick instead
|
|
2265
2270
|
/// of spot.
|
|
2266
2271
|
/// 3. The sigmoid slippage formula (`JBSwapLib.getSlippageTolerance`) enforces a minimum 2% slippage floor
|
|
@@ -2299,7 +2304,7 @@ contract JBRouterTerminal is
|
|
|
2299
2304
|
|
|
2300
2305
|
// If the pool has a hook, try querying it as a geomean oracle (e.g., JBUniswapV4Hook implements this).
|
|
2301
2306
|
if (address(key.hooks) != address(0)) {
|
|
2302
|
-
// Build the two-element lookback array: [_TWAP_WINDOW seconds ago,
|
|
2307
|
+
// Build the two-element lookback array: [_TWAP_WINDOW seconds ago, current block time].
|
|
2303
2308
|
uint32[] memory secondsAgos = new uint32[](2);
|
|
2304
2309
|
secondsAgos[0] = _TWAP_WINDOW; // Start of the window (120 seconds ago).
|
|
2305
2310
|
secondsAgos[1] = 0; // End of the window (current block).
|
|
@@ -2416,8 +2421,9 @@ contract JBRouterTerminal is
|
|
|
2416
2421
|
/// protection. Integrators should still supply `quoteForSwap` metadata whenever they can.
|
|
2417
2422
|
///
|
|
2418
2423
|
/// Priority for `minAmountOut`:
|
|
2419
|
-
/// 1. **User-provided quote** — If `quoteForSwap` is present in `metadata`, it is used
|
|
2420
|
-
/// This is the recommended path for MEV protection,
|
|
2424
|
+
/// 1. **User-provided quote** — If `quoteForSwap` is present in `metadata`, it is used after confirming the
|
|
2425
|
+
/// quote's output token matches the selected route. This is the recommended path for MEV protection,
|
|
2426
|
+
/// especially for V4 pools.
|
|
2421
2427
|
/// 2. **V3 TWAP** — If the best pool is V3, uses a manipulation-resistant time-weighted average price.
|
|
2422
2428
|
/// 3. **V4 automatic quote** — If the best pool is V4, first attempts a hook-provided oracle quote and
|
|
2423
2429
|
/// otherwise falls back to the instantaneous `getSlot0` tick. The spot fallback is manipulable within the
|
|
@@ -2446,11 +2452,19 @@ contract JBRouterTerminal is
|
|
|
2446
2452
|
revert JBRouterTerminal_NoPoolFound({tokenIn: normalizedTokenIn, tokenOut: normalizedTokenOut});
|
|
2447
2453
|
}
|
|
2448
2454
|
|
|
2449
|
-
//
|
|
2455
|
+
// `quoteForSwap` is encoded as `(tokenOut, minAmountOut)`. Binding the quote to its output token prevents
|
|
2456
|
+
// metadata quoted for one route from being replayed against another route with a weaker floor.
|
|
2450
2457
|
(bool exists, bytes memory quote) = _getDataFor({metadata: metadata, id: _QUOTE_FOR_SWAP_ID});
|
|
2451
2458
|
|
|
2452
2459
|
if (exists) {
|
|
2453
|
-
(
|
|
2460
|
+
(address quotedTokenOut, uint256 quotedMinAmountOut) = abi.decode(quote, (address, uint256));
|
|
2461
|
+
// Normalize ETH/WETH before comparing because pool routes use WETH internally for native-token swaps.
|
|
2462
|
+
if (_normalize(quotedTokenOut) != normalizedTokenOut) {
|
|
2463
|
+
revert JBRouterTerminal_QuoteTokenMismatch({
|
|
2464
|
+
quotedTokenOut: quotedTokenOut, expectedTokenOut: normalizedTokenOut
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2467
|
+
minAmountOut = quotedMinAmountOut;
|
|
2454
2468
|
}
|
|
2455
2469
|
|
|
2456
2470
|
// Treat a decoded value of 0 the same as "not provided" so that a stale or default-zero quote
|