@bananapus/router-terminal-v6 0.0.21 → 0.0.23
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/ARCHITECTURE.md +1 -1
- package/AUDIT_INSTRUCTIONS.md +4 -4
- package/CHANGE_LOG.md +2 -2
- package/README.md +2 -2
- package/RISKS.md +7 -7
- package/SKILLS.md +2 -2
- package/package.json +1 -1
- package/script/Deploy.s.sol +12 -7
- package/src/JBRouterTerminal.sol +182 -60
- package/src/JBRouterTerminalRegistry.sol +3 -7
- package/src/interfaces/IGeomeanOracle.sol +21 -0
- package/test/RouterTerminal.t.sol +56 -36
- package/test/RouterTerminalCashOutFork.t.sol +0 -1
- package/test/RouterTerminalCreditCashout.t.sol +0 -5
- package/test/RouterTerminalERC2771.t.sol +0 -5
- package/test/RouterTerminalFeeCashOutFork.t.sol +0 -1
- package/test/RouterTerminalFork.t.sol +0 -1
- package/test/RouterTerminalMultihopFork.t.sol +0 -1
- package/test/RouterTerminalPreviewFork.t.sol +7 -5
- package/test/RouterTerminalReentrancy.t.sol +0 -5
- package/test/RouterTerminalRegistry.t.sol +3 -1
- package/test/RouterTerminalSandwichFork.t.sol +0 -1
- package/test/TestAuditGaps.sol +5 -7
- package/test/audit/LeftoverRefund.t.sol +45 -25
- package/test/audit/PayerTrackerRefund.t.sol +12 -8
- package/test/audit/RefundToBeneficiary.t.sol +0 -2
- package/test/audit/RegistryAddToBalancePartialFill.t.sol +48 -37
- package/test/fork/V4QuoteAndSettlementFork.t.sol +0 -1
- package/test/invariant/RouterTerminalInvariant.t.sol +1 -5
- package/test/regression/CashOutLoopLimit.t.sol +1 -4
- package/test/regression/RouterTerminalEdgeCases.t.sol +49 -27
package/src/JBRouterTerminal.sol
CHANGED
|
@@ -7,7 +7,6 @@ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
|
7
7
|
import {IJBPermissioned} from "@bananapus/core-v6/src/interfaces/IJBPermissioned.sol";
|
|
8
8
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
9
9
|
import {IJBPermitTerminal} from "@bananapus/core-v6/src/interfaces/IJBPermitTerminal.sol";
|
|
10
|
-
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
11
10
|
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
12
11
|
import {IJBToken} from "@bananapus/core-v6/src/interfaces/IJBToken.sol";
|
|
13
12
|
import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
|
|
@@ -17,6 +16,7 @@ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingCo
|
|
|
17
16
|
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
18
17
|
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
19
18
|
import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
|
|
19
|
+
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
20
20
|
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
21
21
|
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
22
22
|
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
@@ -41,6 +41,7 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
|
41
41
|
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
|
|
42
42
|
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
|
|
43
43
|
|
|
44
|
+
import {IGeomeanOracle} from "./interfaces/IGeomeanOracle.sol";
|
|
44
45
|
import {IJBRouterTerminal} from "./interfaces/IJBRouterTerminal.sol";
|
|
45
46
|
import {IJBPayerTracker} from "./interfaces/IJBPayerTracker.sol";
|
|
46
47
|
import {IWETH9} from "./interfaces/IWETH9.sol";
|
|
@@ -106,6 +107,9 @@ contract JBRouterTerminal is
|
|
|
106
107
|
/// @notice The denominator used for slippage tolerance basis points.
|
|
107
108
|
uint256 internal constant _SLIPPAGE_DENOMINATOR = 10_000;
|
|
108
109
|
|
|
110
|
+
/// @notice The TWAP window (in seconds) used when querying a V4 oracle hook.
|
|
111
|
+
uint32 private constant _TWAP_WINDOW = 30;
|
|
112
|
+
|
|
109
113
|
//*********************************************************************//
|
|
110
114
|
// ---------------- public immutable stored properties --------------- //
|
|
111
115
|
//*********************************************************************//
|
|
@@ -113,9 +117,6 @@ contract JBRouterTerminal is
|
|
|
113
117
|
/// @notice The directory of terminals and controllers for projects.
|
|
114
118
|
IJBDirectory public immutable DIRECTORY;
|
|
115
119
|
|
|
116
|
-
/// @notice Mints ERC-721s that represent project ownership and transfers.
|
|
117
|
-
IJBProjects public immutable PROJECTS;
|
|
118
|
-
|
|
119
120
|
/// @notice Manages minting, burning, and balances of projects' tokens and token credits.
|
|
120
121
|
IJBTokens public immutable TOKENS;
|
|
121
122
|
|
|
@@ -152,7 +153,6 @@ contract JBRouterTerminal is
|
|
|
152
153
|
|
|
153
154
|
/// @param directory A contract storing directories of terminals and controllers for each project.
|
|
154
155
|
/// @param permissions A contract storing permissions.
|
|
155
|
-
/// @param projects A contract which mints ERC-721s that represent project ownership and transfers.
|
|
156
156
|
/// @param tokens A contract managing project token balances.
|
|
157
157
|
/// @param permit2 A permit2 utility.
|
|
158
158
|
/// @param owner The owner of the contract.
|
|
@@ -163,7 +163,6 @@ contract JBRouterTerminal is
|
|
|
163
163
|
constructor(
|
|
164
164
|
IJBDirectory directory,
|
|
165
165
|
IJBPermissions permissions,
|
|
166
|
-
IJBProjects projects,
|
|
167
166
|
IJBTokens tokens,
|
|
168
167
|
IPermit2 permit2,
|
|
169
168
|
address owner,
|
|
@@ -177,7 +176,6 @@ contract JBRouterTerminal is
|
|
|
177
176
|
Ownable(owner)
|
|
178
177
|
{
|
|
179
178
|
DIRECTORY = directory;
|
|
180
|
-
PROJECTS = projects;
|
|
181
179
|
TOKENS = tokens;
|
|
182
180
|
FACTORY = factory;
|
|
183
181
|
POOL_MANAGER = poolManager;
|
|
@@ -306,7 +304,7 @@ contract JBRouterTerminal is
|
|
|
306
304
|
|
|
307
305
|
// Verify caller is a legitimate pool via the factory.
|
|
308
306
|
uint24 fee = IUniswapV3Pool(msg.sender).fee();
|
|
309
|
-
address expectedPool =
|
|
307
|
+
address expectedPool = _getPool({tokenA: normalizedTokenIn, tokenB: normalizedTokenOut, fee: fee});
|
|
310
308
|
if (msg.sender != expectedPool) revert JBRouterTerminal_CallerNotPool(msg.sender);
|
|
311
309
|
|
|
312
310
|
// Calculate the amount of tokens to send to the pool (the positive delta).
|
|
@@ -314,7 +312,7 @@ contract JBRouterTerminal is
|
|
|
314
312
|
uint256 amountToSendToPool = amount0Delta < 0 ? uint256(amount1Delta) : uint256(amount0Delta);
|
|
315
313
|
|
|
316
314
|
// Wrap native tokens if needed.
|
|
317
|
-
if (tokenIn == JBConstants.NATIVE_TOKEN)
|
|
315
|
+
if (tokenIn == JBConstants.NATIVE_TOKEN) _wethDeposit(amountToSendToPool);
|
|
318
316
|
|
|
319
317
|
// Transfer the tokens to the pool.
|
|
320
318
|
IERC20(normalizedTokenIn).safeTransfer({to: msg.sender, value: amountToSendToPool});
|
|
@@ -545,9 +543,18 @@ contract JBRouterTerminal is
|
|
|
545
543
|
|
|
546
544
|
(uint256 sourceProjectId, uint256 creditAmount) = abi.decode(creditData, (uint256, uint256));
|
|
547
545
|
|
|
548
|
-
//
|
|
546
|
+
// Resolve the actual credit holder. When called via an intermediary (e.g. JBRouterTerminalRegistry),
|
|
547
|
+
// _msgSender() is the intermediary — use its originalPayer to find the true holder.
|
|
548
|
+
address holder = _msgSender();
|
|
549
|
+
if (msg.sender.code.length > 0) {
|
|
550
|
+
try IJBPayerTracker(msg.sender).originalPayer() returns (address payer) {
|
|
551
|
+
if (payer != address(0)) holder = payer;
|
|
552
|
+
} catch {}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Pull credits from the holder (requires holder to have granted TRANSFER_CREDITS to this contract).
|
|
549
556
|
TOKENS.transferCreditsFrom({
|
|
550
|
-
holder:
|
|
557
|
+
holder: holder, projectId: sourceProjectId, recipient: address(this), count: creditAmount
|
|
551
558
|
});
|
|
552
559
|
|
|
553
560
|
return creditAmount;
|
|
@@ -638,14 +645,13 @@ contract JBRouterTerminal is
|
|
|
638
645
|
// Check for a user-provided minimum cashout reclaim amount (slippage protection).
|
|
639
646
|
uint256 minTokensReclaimed = _minReclaimedFrom(metadata);
|
|
640
647
|
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
// checks would add gas and complexity without improving the overall guarantee.
|
|
648
|
+
// Propagate proportional slippage protection across multi-hop cashouts. Each intermediate step scales the
|
|
649
|
+
// minimum by the ratio of actual output to expected input, maintaining end-to-end slippage guarantees.
|
|
644
650
|
for (uint256 i; i < _MAX_CASHOUT_ITERATIONS; i++) {
|
|
645
651
|
// Skip the destination check on the first iteration if we have a credit override.
|
|
646
652
|
if (sourceProjectIdOverride == 0) {
|
|
647
653
|
// slither-disable-next-line calls-loop
|
|
648
|
-
destTerminal =
|
|
654
|
+
destTerminal = _primaryTerminalOf({projectId: destProjectId, token: token});
|
|
649
655
|
if (address(destTerminal) != address(0) && address(destTerminal) != address(this)) {
|
|
650
656
|
return (destTerminal, token, amount);
|
|
651
657
|
}
|
|
@@ -653,8 +659,7 @@ contract JBRouterTerminal is
|
|
|
653
659
|
|
|
654
660
|
// Use the override if provided, otherwise look up the project ID from the token.
|
|
655
661
|
// slither-disable-next-line calls-loop
|
|
656
|
-
uint256 sourceProjectId =
|
|
657
|
-
sourceProjectIdOverride != 0 ? sourceProjectIdOverride : TOKENS.projectIdOf(IJBToken(token));
|
|
662
|
+
uint256 sourceProjectId = sourceProjectIdOverride != 0 ? sourceProjectIdOverride : _projectIdOf(token);
|
|
658
663
|
|
|
659
664
|
// If it's not a JB project token, return as-is (caller handles the swap).
|
|
660
665
|
if (sourceProjectId == 0) return (IJBTerminal(address(0)), token, amount);
|
|
@@ -663,20 +668,28 @@ contract JBRouterTerminal is
|
|
|
663
668
|
(address tokenToReclaim, IJBCashOutTerminal cashOutTerminal) =
|
|
664
669
|
_findCashOutPath({sourceProjectId: sourceProjectId, destProjectId: destProjectId});
|
|
665
670
|
|
|
671
|
+
// Track the expected amount before cashout so we can scale the minimum proportionally.
|
|
672
|
+
uint256 previousExpectedAmount = amount;
|
|
673
|
+
|
|
666
674
|
// Cash out the source project's tokens.
|
|
667
675
|
// slither-disable-next-line calls-loop
|
|
668
676
|
amount = cashOutTerminal.cashOutTokensOf({
|
|
669
677
|
holder: address(this),
|
|
670
678
|
projectId: sourceProjectId,
|
|
671
|
-
cashOutCount:
|
|
679
|
+
cashOutCount: previousExpectedAmount,
|
|
672
680
|
tokenToReclaim: tokenToReclaim,
|
|
673
681
|
minTokensReclaimed: minTokensReclaimed,
|
|
674
682
|
beneficiary: payable(address(this)),
|
|
675
683
|
metadata: ""
|
|
676
684
|
});
|
|
677
685
|
|
|
678
|
-
//
|
|
679
|
-
|
|
686
|
+
// Scale the minimum proportionally for the next step based on the actual cashout ratio.
|
|
687
|
+
// This propagates slippage protection through multi-hop cashouts instead of dropping it.
|
|
688
|
+
if (minTokensReclaimed != 0 && previousExpectedAmount != 0) {
|
|
689
|
+
minTokensReclaimed = mulDiv(minTokensReclaimed, amount, previousExpectedAmount);
|
|
690
|
+
// minTokensReclaimed may round to 0 here — that is intentional.
|
|
691
|
+
// A 0 minimum is valid and means no slippage protection for this hop.
|
|
692
|
+
}
|
|
680
693
|
|
|
681
694
|
// Update for next iteration.
|
|
682
695
|
token = tokenToReclaim;
|
|
@@ -714,8 +727,8 @@ contract JBRouterTerminal is
|
|
|
714
727
|
|
|
715
728
|
if (nIn == nOut) {
|
|
716
729
|
// Same underlying token — just wrap or unwrap.
|
|
717
|
-
if (tokenIn == JBConstants.NATIVE_TOKEN)
|
|
718
|
-
else
|
|
730
|
+
if (tokenIn == JBConstants.NATIVE_TOKEN) _wethDeposit(amount);
|
|
731
|
+
else _wethWithdraw(amount);
|
|
719
732
|
return amount;
|
|
720
733
|
}
|
|
721
734
|
|
|
@@ -851,24 +864,21 @@ contract JBRouterTerminal is
|
|
|
851
864
|
callbackData: abi.encode(projectId, tokenIn, tokenOut)
|
|
852
865
|
});
|
|
853
866
|
|
|
854
|
-
// Any pre-existing ETH in the contract is absorbed into swap leftovers and routed to
|
|
855
|
-
// the project. This is acceptable because the contract should not hold ETH between transactions. The
|
|
856
|
-
// receive() function or any direct ETH sends are treated as donations.
|
|
857
867
|
// For native token inputs, wrap any raw ETH remaining from partial fills so the leftover check catches it.
|
|
858
868
|
// In partial fills, the swap callback only wraps the amount the pool consumed, leaving excess as raw ETH.
|
|
859
869
|
if (tokenIn == JBConstants.NATIVE_TOKEN && address(this).balance != 0) {
|
|
860
|
-
|
|
870
|
+
_wethDeposit(address(this).balance);
|
|
861
871
|
}
|
|
862
872
|
|
|
863
873
|
// Unwrap if output is native token.
|
|
864
|
-
if (tokenOut == JBConstants.NATIVE_TOKEN)
|
|
874
|
+
if (tokenOut == JBConstants.NATIVE_TOKEN) _wethWithdraw(amountOut);
|
|
865
875
|
|
|
866
|
-
// Return leftover input tokens to payer. The router is stateless —
|
|
867
|
-
//
|
|
868
|
-
//
|
|
876
|
+
// Return leftover input tokens to payer. The router is stateless — it should never hold funds between
|
|
877
|
+
// transactions. The full remaining balance is refunded (not a delta) so that any accidentally-sent tokens
|
|
878
|
+
// are recovered rather than permanently stuck. This is intentional: recovery > lockup.
|
|
869
879
|
uint256 balanceAfter = IERC20(normalizedTokenIn).balanceOf(address(this));
|
|
870
880
|
if (balanceAfter > 0) {
|
|
871
|
-
if (tokenIn == JBConstants.NATIVE_TOKEN)
|
|
881
|
+
if (tokenIn == JBConstants.NATIVE_TOKEN) _wethWithdraw(balanceAfter);
|
|
872
882
|
_transferFrom({from: address(this), to: refundTo, token: tokenIn, amount: balanceAfter});
|
|
873
883
|
}
|
|
874
884
|
}
|
|
@@ -899,7 +909,7 @@ contract JBRouterTerminal is
|
|
|
899
909
|
// Check if input is a JB project token (credit or ERC-20).
|
|
900
910
|
uint256 sourceProjectId = sourceProjectIdOverride;
|
|
901
911
|
if (sourceProjectId == 0 && tokenIn != JBConstants.NATIVE_TOKEN) {
|
|
902
|
-
sourceProjectId =
|
|
912
|
+
sourceProjectId = _projectIdOf(tokenIn);
|
|
903
913
|
}
|
|
904
914
|
|
|
905
915
|
if (sourceProjectId != 0) {
|
|
@@ -939,7 +949,7 @@ contract JBRouterTerminal is
|
|
|
939
949
|
if (Currency.unwrap(currency) == address(0)) {
|
|
940
950
|
// Unwrap only the WETH deficit (caller may hold partial ETH + partial WETH).
|
|
941
951
|
uint256 deficit = amount > address(this).balance ? amount - address(this).balance : 0;
|
|
942
|
-
if (deficit > 0)
|
|
952
|
+
if (deficit > 0) _wethWithdraw(deficit);
|
|
943
953
|
// slither-disable-next-line unused-return
|
|
944
954
|
POOL_MANAGER.settle{value: amount}();
|
|
945
955
|
} else {
|
|
@@ -956,7 +966,7 @@ contract JBRouterTerminal is
|
|
|
956
966
|
POOL_MANAGER.take({currency: currency, to: address(this), amount: amount});
|
|
957
967
|
|
|
958
968
|
// If native ETH output, wrap to WETH (downstream _handleSwap unwraps if needed).
|
|
959
|
-
if (Currency.unwrap(currency) == address(0))
|
|
969
|
+
if (Currency.unwrap(currency) == address(0)) _wethDeposit(amount);
|
|
960
970
|
}
|
|
961
971
|
|
|
962
972
|
/// @notice Transfers tokens.
|
|
@@ -984,17 +994,86 @@ contract JBRouterTerminal is
|
|
|
984
994
|
PERMIT2.transferFrom({from: from, to: to, amount: uint160(amount), token: token});
|
|
985
995
|
}
|
|
986
996
|
|
|
997
|
+
/// @notice Deposit native tokens into WETH.
|
|
998
|
+
/// @param amount The amount of native tokens to wrap.
|
|
999
|
+
function _wethDeposit(uint256 amount) internal {
|
|
1000
|
+
WETH.deposit{value: amount}();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/// @notice Withdraw native tokens from WETH.
|
|
1004
|
+
/// @param amount The amount of WETH to unwrap.
|
|
1005
|
+
function _wethWithdraw(uint256 amount) internal {
|
|
1006
|
+
WETH.withdraw(amount);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
987
1009
|
//*********************************************************************//
|
|
988
1010
|
// ------------------------- internal views -------------------------- //
|
|
989
1011
|
//*********************************************************************//
|
|
990
1012
|
|
|
1013
|
+
/// @notice Look up the best pool address from the V3 factory.
|
|
1014
|
+
/// @param tokenA One token in the pair.
|
|
1015
|
+
/// @param tokenB The other token in the pair.
|
|
1016
|
+
/// @param fee The fee tier to query.
|
|
1017
|
+
/// @return The pool address, or address(0) if none exists.
|
|
1018
|
+
// slither-disable-next-line calls-loop
|
|
1019
|
+
function _getPool(address tokenA, address tokenB, uint24 fee) internal view returns (address) {
|
|
1020
|
+
return FACTORY.getPool({tokenA: tokenA, tokenB: tokenB, fee: fee});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/// @notice Look up the in-range liquidity for a V4 pool.
|
|
1024
|
+
/// @param id The pool ID.
|
|
1025
|
+
/// @return The pool's current in-range liquidity.
|
|
1026
|
+
function _getLiquidity(PoolId id) internal view returns (uint128) {
|
|
1027
|
+
return POOL_MANAGER.getLiquidity(id);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/// @notice Read slot0 from a V4 pool.
|
|
1031
|
+
/// @param id The pool ID.
|
|
1032
|
+
/// @return sqrtPriceX96 The current sqrt price.
|
|
1033
|
+
/// @return tick The current tick.
|
|
1034
|
+
/// @return protocolFee The protocol fee.
|
|
1035
|
+
/// @return lpFee The LP fee.
|
|
1036
|
+
// slither-disable-next-line unused-return
|
|
1037
|
+
function _getSlot0(PoolId id)
|
|
1038
|
+
internal
|
|
1039
|
+
view
|
|
1040
|
+
returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee)
|
|
1041
|
+
{
|
|
1042
|
+
return POOL_MANAGER.getSlot0(id);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/// @notice Look up the primary terminal for a project and token.
|
|
1046
|
+
/// @param projectId The ID of the project.
|
|
1047
|
+
/// @param token The token to look up.
|
|
1048
|
+
/// @return The primary terminal, or IJBTerminal(address(0)) if none.
|
|
1049
|
+
// slither-disable-next-line calls-loop
|
|
1050
|
+
function _primaryTerminalOf(uint256 projectId, address token) internal view returns (IJBTerminal) {
|
|
1051
|
+
return DIRECTORY.primaryTerminalOf({projectId: projectId, token: token});
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/// @notice Look up the project ID for a token.
|
|
1055
|
+
/// @param token The token address to query.
|
|
1056
|
+
/// @return The project ID, or 0 if the token is not a JB project token.
|
|
1057
|
+
function _projectIdOf(address token) internal view returns (uint256) {
|
|
1058
|
+
return TOKENS.projectIdOf(IJBToken(token));
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/// @notice Look up all terminals for a project.
|
|
1062
|
+
/// @param projectId The ID of the project.
|
|
1063
|
+
/// @return The array of terminals.
|
|
1064
|
+
// slither-disable-next-line calls-loop
|
|
1065
|
+
function _terminalsOf(uint256 projectId) internal view returns (IJBTerminal[] memory) {
|
|
1066
|
+
return DIRECTORY.terminalsOf(projectId);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
991
1069
|
/// @notice Find the highest liquidity across all V3 fee tiers and V4 pools for a token pair.
|
|
992
1070
|
/// @param tokenA One token in the pair.
|
|
993
1071
|
/// @param tokenB The other token in the pair.
|
|
994
1072
|
/// @return bestLiquidity The highest liquidity found, or 0 if no pool exists.
|
|
1073
|
+
// slither-disable-next-line calls-loop
|
|
995
1074
|
function _bestPoolLiquidity(address tokenA, address tokenB) internal view returns (uint128 bestLiquidity) {
|
|
996
1075
|
PoolInfo memory pool = _discoverPool(tokenA, tokenB);
|
|
997
|
-
if (pool.isV4) return
|
|
1076
|
+
if (pool.isV4) return _getLiquidity(pool.v4Key.toId());
|
|
998
1077
|
if (address(pool.v3Pool) != address(0)) return pool.v3Pool.liquidity();
|
|
999
1078
|
}
|
|
1000
1079
|
|
|
@@ -1035,7 +1114,7 @@ contract JBRouterTerminal is
|
|
|
1035
1114
|
returns (address tokenOut, IJBTerminal destTerminal)
|
|
1036
1115
|
{
|
|
1037
1116
|
address normalizedTokenIn = _normalize(tokenIn);
|
|
1038
|
-
IJBTerminal[] memory terminals =
|
|
1117
|
+
IJBTerminal[] memory terminals = _terminalsOf(projectId);
|
|
1039
1118
|
|
|
1040
1119
|
uint128 bestLiquidity;
|
|
1041
1120
|
bool hasFallback;
|
|
@@ -1058,7 +1137,8 @@ contract JBRouterTerminal is
|
|
|
1058
1137
|
}
|
|
1059
1138
|
|
|
1060
1139
|
// Search for pool with best liquidity (V3 + V4).
|
|
1061
|
-
uint128 candidateLiquidity =
|
|
1140
|
+
uint128 candidateLiquidity =
|
|
1141
|
+
_bestPoolLiquidity({tokenA: normalizedTokenIn, tokenB: normalizedCandidate});
|
|
1062
1142
|
if (candidateLiquidity > bestLiquidity) {
|
|
1063
1143
|
bestLiquidity = candidateLiquidity;
|
|
1064
1144
|
tokenOut = candidateToken;
|
|
@@ -1088,8 +1168,7 @@ contract JBRouterTerminal is
|
|
|
1088
1168
|
// Search V3.
|
|
1089
1169
|
for (uint256 i; i < 4; i++) {
|
|
1090
1170
|
// slither-disable-next-line calls-loop
|
|
1091
|
-
address poolAddr =
|
|
1092
|
-
FACTORY.getPool({tokenA: normalizedTokenIn, tokenB: normalizedTokenOut, fee: _FEE_TIERS[i]});
|
|
1171
|
+
address poolAddr = _getPool({tokenA: normalizedTokenIn, tokenB: normalizedTokenOut, fee: _FEE_TIERS[i]});
|
|
1093
1172
|
|
|
1094
1173
|
if (poolAddr == address(0)) continue;
|
|
1095
1174
|
|
|
@@ -1146,12 +1225,12 @@ contract JBRouterTerminal is
|
|
|
1146
1225
|
});
|
|
1147
1226
|
|
|
1148
1227
|
// slither-disable-next-line unused-return,calls-loop
|
|
1149
|
-
(uint160 sqrtPriceX96,,,) =
|
|
1228
|
+
(uint160 sqrtPriceX96,,,) = _getSlot0(key.toId());
|
|
1150
1229
|
// slither-disable-next-line incorrect-equality
|
|
1151
1230
|
if (sqrtPriceX96 == 0) continue;
|
|
1152
1231
|
|
|
1153
1232
|
// slither-disable-next-line calls-loop
|
|
1154
|
-
uint128 poolLiquidity =
|
|
1233
|
+
uint128 poolLiquidity = _getLiquidity(key.toId());
|
|
1155
1234
|
if (poolLiquidity > currentBestLiquidity) {
|
|
1156
1235
|
currentBestLiquidity = poolLiquidity;
|
|
1157
1236
|
bestPool = PoolInfo({isV4: true, v3Pool: IUniswapV3Pool(address(0)), v4Key: key});
|
|
@@ -1182,7 +1261,7 @@ contract JBRouterTerminal is
|
|
|
1182
1261
|
IJBCashOutTerminal baseFallbackTerminal;
|
|
1183
1262
|
|
|
1184
1263
|
// slither-disable-next-line calls-loop
|
|
1185
|
-
IJBTerminal[] memory terminals =
|
|
1264
|
+
IJBTerminal[] memory terminals = _terminalsOf(sourceProjectId);
|
|
1186
1265
|
|
|
1187
1266
|
for (uint256 i; i < terminals.length; i++) {
|
|
1188
1267
|
// Check if this terminal supports the IJBCashOutTerminal interface.
|
|
@@ -1204,13 +1283,13 @@ contract JBRouterTerminal is
|
|
|
1204
1283
|
|
|
1205
1284
|
// Priority 1: Does the destination project directly accept this token?
|
|
1206
1285
|
// slither-disable-next-line calls-loop
|
|
1207
|
-
IJBTerminal destTerminal =
|
|
1286
|
+
IJBTerminal destTerminal = _primaryTerminalOf({projectId: destProjectId, token: contextToken});
|
|
1208
1287
|
if (address(destTerminal) != address(0)) return (contextToken, terminal);
|
|
1209
1288
|
|
|
1210
1289
|
// Priority 2: Is this a JB project token (so we can recurse)?
|
|
1211
1290
|
if (address(fallbackTerminal) == address(0) && contextToken != JBConstants.NATIVE_TOKEN) {
|
|
1212
1291
|
// slither-disable-next-line calls-loop
|
|
1213
|
-
if (
|
|
1292
|
+
if (_projectIdOf(contextToken) != 0) {
|
|
1214
1293
|
fallbackToken = contextToken;
|
|
1215
1294
|
fallbackTerminal = terminal;
|
|
1216
1295
|
}
|
|
@@ -1332,9 +1411,39 @@ contract JBRouterTerminal is
|
|
|
1332
1411
|
returns (uint256 minAmountOut)
|
|
1333
1412
|
{
|
|
1334
1413
|
PoolId id = key.toId();
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1414
|
+
|
|
1415
|
+
// The tick used for quoting — prefer TWAP over spot for MEV resistance.
|
|
1416
|
+
int24 tick;
|
|
1417
|
+
|
|
1418
|
+
// Track whether the oracle hook provided a TWAP so we know whether to fall back to spot.
|
|
1419
|
+
bool usedTwap;
|
|
1420
|
+
|
|
1421
|
+
// If the pool has a hook, try querying it as a geomean oracle (e.g., JBUniswapV4Hook implements this).
|
|
1422
|
+
if (address(key.hooks) != address(0)) {
|
|
1423
|
+
// Build the two-element lookback array: [_TWAP_WINDOW seconds ago, now].
|
|
1424
|
+
uint32[] memory secondsAgos = new uint32[](2);
|
|
1425
|
+
secondsAgos[0] = _TWAP_WINDOW; // Start of the window (30 seconds ago).
|
|
1426
|
+
secondsAgos[1] = 0; // End of the window (current block).
|
|
1427
|
+
|
|
1428
|
+
// Ask the hook for cumulative tick data over the window. Silently catch if it doesn't support it.
|
|
1429
|
+
// slither-disable-next-line unused-return
|
|
1430
|
+
try IGeomeanOracle(address(key.hooks)).observe(key, secondsAgos) returns (
|
|
1431
|
+
int56[] memory tickCumulatives, uint160[] memory
|
|
1432
|
+
) {
|
|
1433
|
+
// Derive the arithmetic mean tick: (cumulative_now - cumulative_start) / elapsed_seconds.
|
|
1434
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
1435
|
+
tick = int24((tickCumulatives[1] - tickCumulatives[0]) / int56(int32(_TWAP_WINDOW)));
|
|
1436
|
+
usedTwap = true;
|
|
1437
|
+
} catch {}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// If no TWAP was available (no hook, or hook doesn't implement observe), use the instantaneous spot tick.
|
|
1441
|
+
if (!usedTwap) {
|
|
1442
|
+
// slither-disable-next-line unused-return
|
|
1443
|
+
(, tick,,) = _getSlot0(id);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
uint128 liquidity = _getLiquidity(id);
|
|
1338
1447
|
|
|
1339
1448
|
if (liquidity == 0) revert JBRouterTerminal_NoLiquidity();
|
|
1340
1449
|
|
|
@@ -1418,6 +1527,12 @@ contract JBRouterTerminal is
|
|
|
1418
1527
|
|
|
1419
1528
|
if (exists) {
|
|
1420
1529
|
(minAmountOut) = abi.decode(quote, (uint256));
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Treat a decoded value of 0 the same as "not provided" so that a stale or default-zero quote
|
|
1533
|
+
// does not silently disable slippage protection. Fall through to automatic quoting.
|
|
1534
|
+
if (minAmountOut != 0) {
|
|
1535
|
+
// User-provided quote is valid; skip automatic quoting.
|
|
1421
1536
|
} else if (pool.isV4) {
|
|
1422
1537
|
minAmountOut = _getV4SpotQuote({
|
|
1423
1538
|
key: pool.v4Key,
|
|
@@ -1479,7 +1594,7 @@ contract JBRouterTerminal is
|
|
|
1479
1594
|
// Only probe direct destination acceptance when there is no one-shot source override to consume first.
|
|
1480
1595
|
if (sourceProjectIdOverride == 0) {
|
|
1481
1596
|
// Ask the directory whether the destination already has a primary terminal for the current token.
|
|
1482
|
-
destTerminal =
|
|
1597
|
+
destTerminal = _primaryTerminalOf({projectId: destProjectId, token: token});
|
|
1483
1598
|
|
|
1484
1599
|
// If a real external terminal accepts this token, the preview route is complete and exact.
|
|
1485
1600
|
if (address(destTerminal) != address(0) && address(destTerminal) != address(this)) {
|
|
@@ -1488,8 +1603,7 @@ contract JBRouterTerminal is
|
|
|
1488
1603
|
}
|
|
1489
1604
|
|
|
1490
1605
|
// Use the override once when present; otherwise infer the source project from the current JB token.
|
|
1491
|
-
uint256 sourceProjectId =
|
|
1492
|
-
sourceProjectIdOverride != 0 ? sourceProjectIdOverride : TOKENS.projectIdOf(IJBToken(token));
|
|
1606
|
+
uint256 sourceProjectId = sourceProjectIdOverride != 0 ? sourceProjectIdOverride : _projectIdOf(token);
|
|
1493
1607
|
|
|
1494
1608
|
// If this is no longer a JB project token, stop cashing out and let the caller continue routing from it.
|
|
1495
1609
|
if (sourceProjectId == 0) return (IJBTerminal(address(0)), token, amount);
|
|
@@ -1497,15 +1611,23 @@ contract JBRouterTerminal is
|
|
|
1497
1611
|
// Hold the token produced by the next previewed cashout hop.
|
|
1498
1612
|
address tokenToReclaim;
|
|
1499
1613
|
|
|
1614
|
+
// Track the expected amount before cashout so we can scale the minimum proportionally.
|
|
1615
|
+
uint256 previousExpectedAmount = amount;
|
|
1616
|
+
|
|
1500
1617
|
// Preview the next cashout hop to learn which base token and amount would come out.
|
|
1501
|
-
(tokenToReclaim, amount) =
|
|
1502
|
-
|
|
1618
|
+
(tokenToReclaim, amount) = _previewCashOutStep({
|
|
1619
|
+
sourceProjectId: sourceProjectId, destProjectId: destProjectId, amount: previousExpectedAmount
|
|
1620
|
+
});
|
|
1503
1621
|
|
|
1504
|
-
// Enforce the caller's minimum reclaim amount on
|
|
1622
|
+
// Enforce the caller's minimum reclaim amount on this hop.
|
|
1505
1623
|
if (amount < minTokensReclaimed) revert JBRouterTerminal_SlippageExceeded(amount, minTokensReclaimed);
|
|
1506
1624
|
|
|
1507
|
-
//
|
|
1508
|
-
minTokensReclaimed
|
|
1625
|
+
// Scale the minimum proportionally for the next step based on the actual cashout ratio.
|
|
1626
|
+
if (minTokensReclaimed != 0 && previousExpectedAmount != 0) {
|
|
1627
|
+
minTokensReclaimed = mulDiv(minTokensReclaimed, amount, previousExpectedAmount);
|
|
1628
|
+
// minTokensReclaimed may round to 0 here — that is intentional.
|
|
1629
|
+
// A 0 minimum is valid and means no slippage protection for this hop.
|
|
1630
|
+
}
|
|
1509
1631
|
|
|
1510
1632
|
// Continue previewing from the token reclaimed in this hop.
|
|
1511
1633
|
token = tokenToReclaim;
|
|
@@ -1541,7 +1663,7 @@ contract JBRouterTerminal is
|
|
|
1541
1663
|
_findCashOutPath({sourceProjectId: sourceProjectId, destProjectId: destProjectId});
|
|
1542
1664
|
|
|
1543
1665
|
// Ask that terminal how much of the reclaim token this cashout count would return.
|
|
1544
|
-
// slither-disable-next-line unused-return
|
|
1666
|
+
// slither-disable-next-line unused-return,calls-loop
|
|
1545
1667
|
(, reclaimAmount,,) = cashOutTerminal.previewCashOutFrom({
|
|
1546
1668
|
holder: address(this),
|
|
1547
1669
|
projectId: sourceProjectId,
|
|
@@ -1581,7 +1703,7 @@ contract JBRouterTerminal is
|
|
|
1581
1703
|
|
|
1582
1704
|
// Otherwise, infer the source project from the input token when it is not the native-token sentinel.
|
|
1583
1705
|
if (sourceProjectId == 0 && tokenIn != JBConstants.NATIVE_TOKEN) {
|
|
1584
|
-
sourceProjectId =
|
|
1706
|
+
sourceProjectId = _projectIdOf(tokenIn);
|
|
1585
1707
|
}
|
|
1586
1708
|
|
|
1587
1709
|
// JB project tokens and credit routes must be previewed through the cash-out path first.
|
|
@@ -1689,13 +1811,13 @@ contract JBRouterTerminal is
|
|
|
1689
1811
|
JBMetadataResolver.getDataFor({id: JBMetadataResolver.getId("routeTokenOut"), metadata: metadata});
|
|
1690
1812
|
if (exists) {
|
|
1691
1813
|
(tokenOut) = abi.decode(routeData, (address));
|
|
1692
|
-
destTerminal =
|
|
1814
|
+
destTerminal = _primaryTerminalOf({projectId: projectId, token: tokenOut});
|
|
1693
1815
|
if (address(destTerminal) == address(0)) revert JBRouterTerminal_NoRouteFound(projectId, tokenOut);
|
|
1694
1816
|
return (tokenOut, destTerminal);
|
|
1695
1817
|
}
|
|
1696
1818
|
|
|
1697
1819
|
// 2. Direct acceptance — project accepts tokenIn as-is.
|
|
1698
|
-
destTerminal =
|
|
1820
|
+
destTerminal = _primaryTerminalOf({projectId: projectId, token: tokenIn});
|
|
1699
1821
|
if (address(destTerminal) != address(0) && destTerminal.accountingContextsOf(projectId).length != 0) {
|
|
1700
1822
|
return (tokenIn, destTerminal);
|
|
1701
1823
|
}
|
|
@@ -1703,7 +1825,7 @@ contract JBRouterTerminal is
|
|
|
1703
1825
|
// 2b. Check NATIVE_TOKEN <-> WETH equivalence.
|
|
1704
1826
|
if (tokenIn == JBConstants.NATIVE_TOKEN || tokenIn == address(WETH)) {
|
|
1705
1827
|
tokenOut = tokenIn == JBConstants.NATIVE_TOKEN ? address(WETH) : JBConstants.NATIVE_TOKEN;
|
|
1706
|
-
destTerminal =
|
|
1828
|
+
destTerminal = _primaryTerminalOf({projectId: projectId, token: tokenOut});
|
|
1707
1829
|
if (address(destTerminal) != address(0)) return (tokenOut, destTerminal);
|
|
1708
1830
|
}
|
|
1709
1831
|
|
|
@@ -108,13 +108,6 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
108
108
|
PERMIT2 = permit2;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
//*********************************************************************//
|
|
112
|
-
// ------------------------- receive -------------------------------- //
|
|
113
|
-
//*********************************************************************//
|
|
114
|
-
|
|
115
|
-
/// @notice Accept native token refunds from the router on partial swap fills.
|
|
116
|
-
receive() external payable {}
|
|
117
|
-
|
|
118
111
|
//*********************************************************************//
|
|
119
112
|
// ------------------------- external views -------------------------- //
|
|
120
113
|
//*********************************************************************//
|
|
@@ -259,6 +252,7 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
259
252
|
/// @param shouldReturnHeldFees A boolean to indicate whether held fees should be returned.
|
|
260
253
|
/// @param memo A memo to pass along to the emitted event.
|
|
261
254
|
/// @param metadata Bytes in `JBMetadataResolver`'s format.
|
|
255
|
+
// slither-disable-next-line reentrancy-benign
|
|
262
256
|
function addToBalanceOf(
|
|
263
257
|
uint256 projectId,
|
|
264
258
|
address token,
|
|
@@ -374,6 +368,7 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
374
368
|
/// @param memo A memo to pass along to the emitted event.
|
|
375
369
|
/// @param metadata Bytes in `JBMetadataResolver`'s format.
|
|
376
370
|
/// @return result The number of tokens received.
|
|
371
|
+
// slither-disable-next-line reentrancy-benign
|
|
377
372
|
function pay(
|
|
378
373
|
uint256 projectId,
|
|
379
374
|
address token,
|
|
@@ -464,6 +459,7 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
|
|
|
464
459
|
/// @param amount The amount of tokens being paid in.
|
|
465
460
|
/// @param metadata The metadata in which `permit2` context is provided.
|
|
466
461
|
/// @return amount The amount of tokens that have been accepted.
|
|
462
|
+
// slither-disable-next-line reentrancy-events
|
|
467
463
|
function _acceptFundsFor(address token, uint256 amount, bytes calldata metadata) internal returns (uint256) {
|
|
468
464
|
// If native tokens are being paid in, return the `msg.value`.
|
|
469
465
|
if (token == JBConstants.NATIVE_TOKEN) return msg.value;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
5
|
+
|
|
6
|
+
/// @notice Minimal interface for a Uniswap V4 TWAP oracle hook (e.g. GeomeanOracle).
|
|
7
|
+
/// @dev Used by JBRouterTerminal to attempt TWAP-based quoting on V4 pools whose hook supports it.
|
|
8
|
+
interface IGeomeanOracle {
|
|
9
|
+
/// @notice Returns cumulative tick and seconds-per-liquidity values for the given observation timestamps.
|
|
10
|
+
/// @param key The pool key to observe.
|
|
11
|
+
/// @param secondsAgos An array of seconds-ago offsets (e.g., [30, 0] for a 30-second TWAP).
|
|
12
|
+
/// @return tickCumulatives The cumulative tick values at each observation point.
|
|
13
|
+
/// @return secondsPerLiquidityCumulativeX128s The cumulative seconds-per-liquidity values.
|
|
14
|
+
function observe(
|
|
15
|
+
PoolKey calldata key,
|
|
16
|
+
uint32[] calldata secondsAgos
|
|
17
|
+
)
|
|
18
|
+
external
|
|
19
|
+
view
|
|
20
|
+
returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s);
|
|
21
|
+
}
|