@bananapus/router-terminal-v6 0.0.21 → 0.0.22
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 +0 -1
- package/src/JBRouterTerminal.sol +154 -58
- 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});
|
|
@@ -638,14 +636,13 @@ contract JBRouterTerminal is
|
|
|
638
636
|
// Check for a user-provided minimum cashout reclaim amount (slippage protection).
|
|
639
637
|
uint256 minTokensReclaimed = _minReclaimedFrom(metadata);
|
|
640
638
|
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
// checks would add gas and complexity without improving the overall guarantee.
|
|
639
|
+
// Propagate proportional slippage protection across multi-hop cashouts. Each intermediate step scales the
|
|
640
|
+
// minimum by the ratio of actual output to expected input, maintaining end-to-end slippage guarantees.
|
|
644
641
|
for (uint256 i; i < _MAX_CASHOUT_ITERATIONS; i++) {
|
|
645
642
|
// Skip the destination check on the first iteration if we have a credit override.
|
|
646
643
|
if (sourceProjectIdOverride == 0) {
|
|
647
644
|
// slither-disable-next-line calls-loop
|
|
648
|
-
destTerminal =
|
|
645
|
+
destTerminal = _primaryTerminalOf({projectId: destProjectId, token: token});
|
|
649
646
|
if (address(destTerminal) != address(0) && address(destTerminal) != address(this)) {
|
|
650
647
|
return (destTerminal, token, amount);
|
|
651
648
|
}
|
|
@@ -653,8 +650,7 @@ contract JBRouterTerminal is
|
|
|
653
650
|
|
|
654
651
|
// Use the override if provided, otherwise look up the project ID from the token.
|
|
655
652
|
// slither-disable-next-line calls-loop
|
|
656
|
-
uint256 sourceProjectId =
|
|
657
|
-
sourceProjectIdOverride != 0 ? sourceProjectIdOverride : TOKENS.projectIdOf(IJBToken(token));
|
|
653
|
+
uint256 sourceProjectId = sourceProjectIdOverride != 0 ? sourceProjectIdOverride : _projectIdOf(token);
|
|
658
654
|
|
|
659
655
|
// If it's not a JB project token, return as-is (caller handles the swap).
|
|
660
656
|
if (sourceProjectId == 0) return (IJBTerminal(address(0)), token, amount);
|
|
@@ -663,20 +659,26 @@ contract JBRouterTerminal is
|
|
|
663
659
|
(address tokenToReclaim, IJBCashOutTerminal cashOutTerminal) =
|
|
664
660
|
_findCashOutPath({sourceProjectId: sourceProjectId, destProjectId: destProjectId});
|
|
665
661
|
|
|
662
|
+
// Track the expected amount before cashout so we can scale the minimum proportionally.
|
|
663
|
+
uint256 previousExpectedAmount = amount;
|
|
664
|
+
|
|
666
665
|
// Cash out the source project's tokens.
|
|
667
666
|
// slither-disable-next-line calls-loop
|
|
668
667
|
amount = cashOutTerminal.cashOutTokensOf({
|
|
669
668
|
holder: address(this),
|
|
670
669
|
projectId: sourceProjectId,
|
|
671
|
-
cashOutCount:
|
|
670
|
+
cashOutCount: previousExpectedAmount,
|
|
672
671
|
tokenToReclaim: tokenToReclaim,
|
|
673
672
|
minTokensReclaimed: minTokensReclaimed,
|
|
674
673
|
beneficiary: payable(address(this)),
|
|
675
674
|
metadata: ""
|
|
676
675
|
});
|
|
677
676
|
|
|
678
|
-
//
|
|
679
|
-
|
|
677
|
+
// Scale the minimum proportionally for the next step based on the actual cashout ratio.
|
|
678
|
+
// This propagates slippage protection through multi-hop cashouts instead of dropping it.
|
|
679
|
+
if (minTokensReclaimed != 0 && previousExpectedAmount != 0) {
|
|
680
|
+
minTokensReclaimed = mulDiv(minTokensReclaimed, amount, previousExpectedAmount);
|
|
681
|
+
}
|
|
680
682
|
|
|
681
683
|
// Update for next iteration.
|
|
682
684
|
token = tokenToReclaim;
|
|
@@ -714,8 +716,8 @@ contract JBRouterTerminal is
|
|
|
714
716
|
|
|
715
717
|
if (nIn == nOut) {
|
|
716
718
|
// Same underlying token — just wrap or unwrap.
|
|
717
|
-
if (tokenIn == JBConstants.NATIVE_TOKEN)
|
|
718
|
-
else
|
|
719
|
+
if (tokenIn == JBConstants.NATIVE_TOKEN) _wethDeposit(amount);
|
|
720
|
+
else _wethWithdraw(amount);
|
|
719
721
|
return amount;
|
|
720
722
|
}
|
|
721
723
|
|
|
@@ -851,24 +853,21 @@ contract JBRouterTerminal is
|
|
|
851
853
|
callbackData: abi.encode(projectId, tokenIn, tokenOut)
|
|
852
854
|
});
|
|
853
855
|
|
|
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
856
|
// For native token inputs, wrap any raw ETH remaining from partial fills so the leftover check catches it.
|
|
858
857
|
// In partial fills, the swap callback only wraps the amount the pool consumed, leaving excess as raw ETH.
|
|
859
858
|
if (tokenIn == JBConstants.NATIVE_TOKEN && address(this).balance != 0) {
|
|
860
|
-
|
|
859
|
+
_wethDeposit(address(this).balance);
|
|
861
860
|
}
|
|
862
861
|
|
|
863
862
|
// Unwrap if output is native token.
|
|
864
|
-
if (tokenOut == JBConstants.NATIVE_TOKEN)
|
|
863
|
+
if (tokenOut == JBConstants.NATIVE_TOKEN) _wethWithdraw(amountOut);
|
|
865
864
|
|
|
866
|
-
// Return leftover input tokens to payer. The router is stateless —
|
|
867
|
-
//
|
|
868
|
-
//
|
|
865
|
+
// Return leftover input tokens to payer. The router is stateless — it should never hold funds between
|
|
866
|
+
// transactions. The full remaining balance is refunded (not a delta) so that any accidentally-sent tokens
|
|
867
|
+
// are recovered rather than permanently stuck. This is intentional: recovery > lockup.
|
|
869
868
|
uint256 balanceAfter = IERC20(normalizedTokenIn).balanceOf(address(this));
|
|
870
869
|
if (balanceAfter > 0) {
|
|
871
|
-
if (tokenIn == JBConstants.NATIVE_TOKEN)
|
|
870
|
+
if (tokenIn == JBConstants.NATIVE_TOKEN) _wethWithdraw(balanceAfter);
|
|
872
871
|
_transferFrom({from: address(this), to: refundTo, token: tokenIn, amount: balanceAfter});
|
|
873
872
|
}
|
|
874
873
|
}
|
|
@@ -899,7 +898,7 @@ contract JBRouterTerminal is
|
|
|
899
898
|
// Check if input is a JB project token (credit or ERC-20).
|
|
900
899
|
uint256 sourceProjectId = sourceProjectIdOverride;
|
|
901
900
|
if (sourceProjectId == 0 && tokenIn != JBConstants.NATIVE_TOKEN) {
|
|
902
|
-
sourceProjectId =
|
|
901
|
+
sourceProjectId = _projectIdOf(tokenIn);
|
|
903
902
|
}
|
|
904
903
|
|
|
905
904
|
if (sourceProjectId != 0) {
|
|
@@ -939,7 +938,7 @@ contract JBRouterTerminal is
|
|
|
939
938
|
if (Currency.unwrap(currency) == address(0)) {
|
|
940
939
|
// Unwrap only the WETH deficit (caller may hold partial ETH + partial WETH).
|
|
941
940
|
uint256 deficit = amount > address(this).balance ? amount - address(this).balance : 0;
|
|
942
|
-
if (deficit > 0)
|
|
941
|
+
if (deficit > 0) _wethWithdraw(deficit);
|
|
943
942
|
// slither-disable-next-line unused-return
|
|
944
943
|
POOL_MANAGER.settle{value: amount}();
|
|
945
944
|
} else {
|
|
@@ -956,7 +955,7 @@ contract JBRouterTerminal is
|
|
|
956
955
|
POOL_MANAGER.take({currency: currency, to: address(this), amount: amount});
|
|
957
956
|
|
|
958
957
|
// If native ETH output, wrap to WETH (downstream _handleSwap unwraps if needed).
|
|
959
|
-
if (Currency.unwrap(currency) == address(0))
|
|
958
|
+
if (Currency.unwrap(currency) == address(0)) _wethDeposit(amount);
|
|
960
959
|
}
|
|
961
960
|
|
|
962
961
|
/// @notice Transfers tokens.
|
|
@@ -984,17 +983,86 @@ contract JBRouterTerminal is
|
|
|
984
983
|
PERMIT2.transferFrom({from: from, to: to, amount: uint160(amount), token: token});
|
|
985
984
|
}
|
|
986
985
|
|
|
986
|
+
/// @notice Deposit native tokens into WETH.
|
|
987
|
+
/// @param amount The amount of native tokens to wrap.
|
|
988
|
+
function _wethDeposit(uint256 amount) internal {
|
|
989
|
+
WETH.deposit{value: amount}();
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/// @notice Withdraw native tokens from WETH.
|
|
993
|
+
/// @param amount The amount of WETH to unwrap.
|
|
994
|
+
function _wethWithdraw(uint256 amount) internal {
|
|
995
|
+
WETH.withdraw(amount);
|
|
996
|
+
}
|
|
997
|
+
|
|
987
998
|
//*********************************************************************//
|
|
988
999
|
// ------------------------- internal views -------------------------- //
|
|
989
1000
|
//*********************************************************************//
|
|
990
1001
|
|
|
1002
|
+
/// @notice Look up the best pool address from the V3 factory.
|
|
1003
|
+
/// @param tokenA One token in the pair.
|
|
1004
|
+
/// @param tokenB The other token in the pair.
|
|
1005
|
+
/// @param fee The fee tier to query.
|
|
1006
|
+
/// @return The pool address, or address(0) if none exists.
|
|
1007
|
+
// slither-disable-next-line calls-loop
|
|
1008
|
+
function _getPool(address tokenA, address tokenB, uint24 fee) internal view returns (address) {
|
|
1009
|
+
return FACTORY.getPool({tokenA: tokenA, tokenB: tokenB, fee: fee});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/// @notice Look up the in-range liquidity for a V4 pool.
|
|
1013
|
+
/// @param id The pool ID.
|
|
1014
|
+
/// @return The pool's current in-range liquidity.
|
|
1015
|
+
function _getLiquidity(PoolId id) internal view returns (uint128) {
|
|
1016
|
+
return POOL_MANAGER.getLiquidity(id);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/// @notice Read slot0 from a V4 pool.
|
|
1020
|
+
/// @param id The pool ID.
|
|
1021
|
+
/// @return sqrtPriceX96 The current sqrt price.
|
|
1022
|
+
/// @return tick The current tick.
|
|
1023
|
+
/// @return protocolFee The protocol fee.
|
|
1024
|
+
/// @return lpFee The LP fee.
|
|
1025
|
+
// slither-disable-next-line unused-return
|
|
1026
|
+
function _getSlot0(PoolId id)
|
|
1027
|
+
internal
|
|
1028
|
+
view
|
|
1029
|
+
returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee)
|
|
1030
|
+
{
|
|
1031
|
+
return POOL_MANAGER.getSlot0(id);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/// @notice Look up the primary terminal for a project and token.
|
|
1035
|
+
/// @param projectId The ID of the project.
|
|
1036
|
+
/// @param token The token to look up.
|
|
1037
|
+
/// @return The primary terminal, or IJBTerminal(address(0)) if none.
|
|
1038
|
+
// slither-disable-next-line calls-loop
|
|
1039
|
+
function _primaryTerminalOf(uint256 projectId, address token) internal view returns (IJBTerminal) {
|
|
1040
|
+
return DIRECTORY.primaryTerminalOf({projectId: projectId, token: token});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/// @notice Look up the project ID for a token.
|
|
1044
|
+
/// @param token The token address to query.
|
|
1045
|
+
/// @return The project ID, or 0 if the token is not a JB project token.
|
|
1046
|
+
function _projectIdOf(address token) internal view returns (uint256) {
|
|
1047
|
+
return TOKENS.projectIdOf(IJBToken(token));
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/// @notice Look up all terminals for a project.
|
|
1051
|
+
/// @param projectId The ID of the project.
|
|
1052
|
+
/// @return The array of terminals.
|
|
1053
|
+
// slither-disable-next-line calls-loop
|
|
1054
|
+
function _terminalsOf(uint256 projectId) internal view returns (IJBTerminal[] memory) {
|
|
1055
|
+
return DIRECTORY.terminalsOf(projectId);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
991
1058
|
/// @notice Find the highest liquidity across all V3 fee tiers and V4 pools for a token pair.
|
|
992
1059
|
/// @param tokenA One token in the pair.
|
|
993
1060
|
/// @param tokenB The other token in the pair.
|
|
994
1061
|
/// @return bestLiquidity The highest liquidity found, or 0 if no pool exists.
|
|
1062
|
+
// slither-disable-next-line calls-loop
|
|
995
1063
|
function _bestPoolLiquidity(address tokenA, address tokenB) internal view returns (uint128 bestLiquidity) {
|
|
996
1064
|
PoolInfo memory pool = _discoverPool(tokenA, tokenB);
|
|
997
|
-
if (pool.isV4) return
|
|
1065
|
+
if (pool.isV4) return _getLiquidity(pool.v4Key.toId());
|
|
998
1066
|
if (address(pool.v3Pool) != address(0)) return pool.v3Pool.liquidity();
|
|
999
1067
|
}
|
|
1000
1068
|
|
|
@@ -1035,7 +1103,7 @@ contract JBRouterTerminal is
|
|
|
1035
1103
|
returns (address tokenOut, IJBTerminal destTerminal)
|
|
1036
1104
|
{
|
|
1037
1105
|
address normalizedTokenIn = _normalize(tokenIn);
|
|
1038
|
-
IJBTerminal[] memory terminals =
|
|
1106
|
+
IJBTerminal[] memory terminals = _terminalsOf(projectId);
|
|
1039
1107
|
|
|
1040
1108
|
uint128 bestLiquidity;
|
|
1041
1109
|
bool hasFallback;
|
|
@@ -1058,7 +1126,8 @@ contract JBRouterTerminal is
|
|
|
1058
1126
|
}
|
|
1059
1127
|
|
|
1060
1128
|
// Search for pool with best liquidity (V3 + V4).
|
|
1061
|
-
uint128 candidateLiquidity =
|
|
1129
|
+
uint128 candidateLiquidity =
|
|
1130
|
+
_bestPoolLiquidity({tokenA: normalizedTokenIn, tokenB: normalizedCandidate});
|
|
1062
1131
|
if (candidateLiquidity > bestLiquidity) {
|
|
1063
1132
|
bestLiquidity = candidateLiquidity;
|
|
1064
1133
|
tokenOut = candidateToken;
|
|
@@ -1088,8 +1157,7 @@ contract JBRouterTerminal is
|
|
|
1088
1157
|
// Search V3.
|
|
1089
1158
|
for (uint256 i; i < 4; i++) {
|
|
1090
1159
|
// slither-disable-next-line calls-loop
|
|
1091
|
-
address poolAddr =
|
|
1092
|
-
FACTORY.getPool({tokenA: normalizedTokenIn, tokenB: normalizedTokenOut, fee: _FEE_TIERS[i]});
|
|
1160
|
+
address poolAddr = _getPool({tokenA: normalizedTokenIn, tokenB: normalizedTokenOut, fee: _FEE_TIERS[i]});
|
|
1093
1161
|
|
|
1094
1162
|
if (poolAddr == address(0)) continue;
|
|
1095
1163
|
|
|
@@ -1146,12 +1214,12 @@ contract JBRouterTerminal is
|
|
|
1146
1214
|
});
|
|
1147
1215
|
|
|
1148
1216
|
// slither-disable-next-line unused-return,calls-loop
|
|
1149
|
-
(uint160 sqrtPriceX96,,,) =
|
|
1217
|
+
(uint160 sqrtPriceX96,,,) = _getSlot0(key.toId());
|
|
1150
1218
|
// slither-disable-next-line incorrect-equality
|
|
1151
1219
|
if (sqrtPriceX96 == 0) continue;
|
|
1152
1220
|
|
|
1153
1221
|
// slither-disable-next-line calls-loop
|
|
1154
|
-
uint128 poolLiquidity =
|
|
1222
|
+
uint128 poolLiquidity = _getLiquidity(key.toId());
|
|
1155
1223
|
if (poolLiquidity > currentBestLiquidity) {
|
|
1156
1224
|
currentBestLiquidity = poolLiquidity;
|
|
1157
1225
|
bestPool = PoolInfo({isV4: true, v3Pool: IUniswapV3Pool(address(0)), v4Key: key});
|
|
@@ -1182,7 +1250,7 @@ contract JBRouterTerminal is
|
|
|
1182
1250
|
IJBCashOutTerminal baseFallbackTerminal;
|
|
1183
1251
|
|
|
1184
1252
|
// slither-disable-next-line calls-loop
|
|
1185
|
-
IJBTerminal[] memory terminals =
|
|
1253
|
+
IJBTerminal[] memory terminals = _terminalsOf(sourceProjectId);
|
|
1186
1254
|
|
|
1187
1255
|
for (uint256 i; i < terminals.length; i++) {
|
|
1188
1256
|
// Check if this terminal supports the IJBCashOutTerminal interface.
|
|
@@ -1204,13 +1272,13 @@ contract JBRouterTerminal is
|
|
|
1204
1272
|
|
|
1205
1273
|
// Priority 1: Does the destination project directly accept this token?
|
|
1206
1274
|
// slither-disable-next-line calls-loop
|
|
1207
|
-
IJBTerminal destTerminal =
|
|
1275
|
+
IJBTerminal destTerminal = _primaryTerminalOf({projectId: destProjectId, token: contextToken});
|
|
1208
1276
|
if (address(destTerminal) != address(0)) return (contextToken, terminal);
|
|
1209
1277
|
|
|
1210
1278
|
// Priority 2: Is this a JB project token (so we can recurse)?
|
|
1211
1279
|
if (address(fallbackTerminal) == address(0) && contextToken != JBConstants.NATIVE_TOKEN) {
|
|
1212
1280
|
// slither-disable-next-line calls-loop
|
|
1213
|
-
if (
|
|
1281
|
+
if (_projectIdOf(contextToken) != 0) {
|
|
1214
1282
|
fallbackToken = contextToken;
|
|
1215
1283
|
fallbackTerminal = terminal;
|
|
1216
1284
|
}
|
|
@@ -1332,9 +1400,32 @@ contract JBRouterTerminal is
|
|
|
1332
1400
|
returns (uint256 minAmountOut)
|
|
1333
1401
|
{
|
|
1334
1402
|
PoolId id = key.toId();
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1403
|
+
int24 tick;
|
|
1404
|
+
bool usedTwap;
|
|
1405
|
+
|
|
1406
|
+
// Try to use TWAP from the pool's oracle hook (if available) for manipulation resistance.
|
|
1407
|
+
if (address(key.hooks) != address(0)) {
|
|
1408
|
+
uint32[] memory secondsAgos = new uint32[](2);
|
|
1409
|
+
secondsAgos[0] = _TWAP_WINDOW;
|
|
1410
|
+
secondsAgos[1] = 0;
|
|
1411
|
+
// slither-disable-next-line unused-return
|
|
1412
|
+
try IGeomeanOracle(address(key.hooks)).observe(key, secondsAgos) returns (
|
|
1413
|
+
int56[] memory tickCumulatives, uint160[] memory
|
|
1414
|
+
) {
|
|
1415
|
+
// Compute the arithmetic mean tick from the TWAP window.
|
|
1416
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
1417
|
+
tick = int24((tickCumulatives[1] - tickCumulatives[0]) / int56(int32(_TWAP_WINDOW)));
|
|
1418
|
+
usedTwap = true;
|
|
1419
|
+
} catch {}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// Fall back to spot price if TWAP was not available.
|
|
1423
|
+
if (!usedTwap) {
|
|
1424
|
+
// slither-disable-next-line unused-return
|
|
1425
|
+
(, tick,,) = _getSlot0(id);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
uint128 liquidity = _getLiquidity(id);
|
|
1338
1429
|
|
|
1339
1430
|
if (liquidity == 0) revert JBRouterTerminal_NoLiquidity();
|
|
1340
1431
|
|
|
@@ -1479,7 +1570,7 @@ contract JBRouterTerminal is
|
|
|
1479
1570
|
// Only probe direct destination acceptance when there is no one-shot source override to consume first.
|
|
1480
1571
|
if (sourceProjectIdOverride == 0) {
|
|
1481
1572
|
// Ask the directory whether the destination already has a primary terminal for the current token.
|
|
1482
|
-
destTerminal =
|
|
1573
|
+
destTerminal = _primaryTerminalOf({projectId: destProjectId, token: token});
|
|
1483
1574
|
|
|
1484
1575
|
// If a real external terminal accepts this token, the preview route is complete and exact.
|
|
1485
1576
|
if (address(destTerminal) != address(0) && address(destTerminal) != address(this)) {
|
|
@@ -1488,8 +1579,7 @@ contract JBRouterTerminal is
|
|
|
1488
1579
|
}
|
|
1489
1580
|
|
|
1490
1581
|
// 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));
|
|
1582
|
+
uint256 sourceProjectId = sourceProjectIdOverride != 0 ? sourceProjectIdOverride : _projectIdOf(token);
|
|
1493
1583
|
|
|
1494
1584
|
// If this is no longer a JB project token, stop cashing out and let the caller continue routing from it.
|
|
1495
1585
|
if (sourceProjectId == 0) return (IJBTerminal(address(0)), token, amount);
|
|
@@ -1497,15 +1587,21 @@ contract JBRouterTerminal is
|
|
|
1497
1587
|
// Hold the token produced by the next previewed cashout hop.
|
|
1498
1588
|
address tokenToReclaim;
|
|
1499
1589
|
|
|
1590
|
+
// Track the expected amount before cashout so we can scale the minimum proportionally.
|
|
1591
|
+
uint256 previousExpectedAmount = amount;
|
|
1592
|
+
|
|
1500
1593
|
// Preview the next cashout hop to learn which base token and amount would come out.
|
|
1501
|
-
(tokenToReclaim, amount) =
|
|
1502
|
-
|
|
1594
|
+
(tokenToReclaim, amount) = _previewCashOutStep({
|
|
1595
|
+
sourceProjectId: sourceProjectId, destProjectId: destProjectId, amount: previousExpectedAmount
|
|
1596
|
+
});
|
|
1503
1597
|
|
|
1504
|
-
// Enforce the caller's minimum reclaim amount on
|
|
1598
|
+
// Enforce the caller's minimum reclaim amount on this hop.
|
|
1505
1599
|
if (amount < minTokensReclaimed) revert JBRouterTerminal_SlippageExceeded(amount, minTokensReclaimed);
|
|
1506
1600
|
|
|
1507
|
-
//
|
|
1508
|
-
minTokensReclaimed
|
|
1601
|
+
// Scale the minimum proportionally for the next step based on the actual cashout ratio.
|
|
1602
|
+
if (minTokensReclaimed != 0 && previousExpectedAmount != 0) {
|
|
1603
|
+
minTokensReclaimed = mulDiv(minTokensReclaimed, amount, previousExpectedAmount);
|
|
1604
|
+
}
|
|
1509
1605
|
|
|
1510
1606
|
// Continue previewing from the token reclaimed in this hop.
|
|
1511
1607
|
token = tokenToReclaim;
|
|
@@ -1541,7 +1637,7 @@ contract JBRouterTerminal is
|
|
|
1541
1637
|
_findCashOutPath({sourceProjectId: sourceProjectId, destProjectId: destProjectId});
|
|
1542
1638
|
|
|
1543
1639
|
// Ask that terminal how much of the reclaim token this cashout count would return.
|
|
1544
|
-
// slither-disable-next-line unused-return
|
|
1640
|
+
// slither-disable-next-line unused-return,calls-loop
|
|
1545
1641
|
(, reclaimAmount,,) = cashOutTerminal.previewCashOutFrom({
|
|
1546
1642
|
holder: address(this),
|
|
1547
1643
|
projectId: sourceProjectId,
|
|
@@ -1581,7 +1677,7 @@ contract JBRouterTerminal is
|
|
|
1581
1677
|
|
|
1582
1678
|
// Otherwise, infer the source project from the input token when it is not the native-token sentinel.
|
|
1583
1679
|
if (sourceProjectId == 0 && tokenIn != JBConstants.NATIVE_TOKEN) {
|
|
1584
|
-
sourceProjectId =
|
|
1680
|
+
sourceProjectId = _projectIdOf(tokenIn);
|
|
1585
1681
|
}
|
|
1586
1682
|
|
|
1587
1683
|
// JB project tokens and credit routes must be previewed through the cash-out path first.
|
|
@@ -1689,13 +1785,13 @@ contract JBRouterTerminal is
|
|
|
1689
1785
|
JBMetadataResolver.getDataFor({id: JBMetadataResolver.getId("routeTokenOut"), metadata: metadata});
|
|
1690
1786
|
if (exists) {
|
|
1691
1787
|
(tokenOut) = abi.decode(routeData, (address));
|
|
1692
|
-
destTerminal =
|
|
1788
|
+
destTerminal = _primaryTerminalOf({projectId: projectId, token: tokenOut});
|
|
1693
1789
|
if (address(destTerminal) == address(0)) revert JBRouterTerminal_NoRouteFound(projectId, tokenOut);
|
|
1694
1790
|
return (tokenOut, destTerminal);
|
|
1695
1791
|
}
|
|
1696
1792
|
|
|
1697
1793
|
// 2. Direct acceptance — project accepts tokenIn as-is.
|
|
1698
|
-
destTerminal =
|
|
1794
|
+
destTerminal = _primaryTerminalOf({projectId: projectId, token: tokenIn});
|
|
1699
1795
|
if (address(destTerminal) != address(0) && destTerminal.accountingContextsOf(projectId).length != 0) {
|
|
1700
1796
|
return (tokenIn, destTerminal);
|
|
1701
1797
|
}
|
|
@@ -1703,7 +1799,7 @@ contract JBRouterTerminal is
|
|
|
1703
1799
|
// 2b. Check NATIVE_TOKEN <-> WETH equivalence.
|
|
1704
1800
|
if (tokenIn == JBConstants.NATIVE_TOKEN || tokenIn == address(WETH)) {
|
|
1705
1801
|
tokenOut = tokenIn == JBConstants.NATIVE_TOKEN ? address(WETH) : JBConstants.NATIVE_TOKEN;
|
|
1706
|
-
destTerminal =
|
|
1802
|
+
destTerminal = _primaryTerminalOf({projectId: projectId, token: tokenOut});
|
|
1707
1803
|
if (address(destTerminal) != address(0)) return (tokenOut, destTerminal);
|
|
1708
1804
|
}
|
|
1709
1805
|
|
|
@@ -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
|
+
}
|