Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"outputSelection": {
"*": {
"*": [
"evm.bytecode",
"evm.deployedBytecode",
"abi"
]
}
},
"remappings": []
},
"sources": {
"contracts/swap/UniversalRouter.sol": {
"content": "// SPDX-License-Identifier: MIT\r
pragma solidity ^0.8.30;\r
\r
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";\r
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";\r
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";\r
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";\r
\r
/**\r
* @title DSF UniversalRouter\r
* @author Andrei Averin — CTO dsf.finance\r
* @notice A universal router that combines DEX modules (Curve, UniswapV2, SushiSwap, UniswapV3, etc.)\r
* and performs single and split swaps with automatic commission withholding\r
* @dev Router:\r
* - Requests the best quotes (1-hop and 2-hop) from all modules;\r
* - Selects the optimal route or split between the two best;\r
* - Pulls tokens from the user, approves modules, and executes the swap;\r
* - Charges a fee from the swap (feeBpsSwap) and from positive slippage (feeBpsPositive);\r
* - Supports ETH↔WETH, secure calls, and module list management\r
*\r
* Uses low-level staticcall to IDexModule.getBestRoute(address,address,uint256)\r
* and a unified payload format:\r
* abi.encode(module,index,quotedOut,tokenIn,tokenOut,amountIn,bytes[] route)\r
*/\r
\r
/* ─────────────────────────────── Interfaces / Types ─────────────────────────────── */\r
\r
struct DexRoute { bytes[] data; }\r
\r
struct Quote {\r
address pool;\r
int128 i;\r
int128 j;\r
bool useUnderlying;\r
uint256 amountOut;\r
}\r
\r
struct BestAgg {\r
bytes payload;\r
uint256 amount;\r
address module;\r
uint256 idx;\r
}\r
\r
struct RouteInfo {\r
address module;\r
uint256 index;\r
bytes payload; // the same format as in getBestRoute/decodeRoute\r
uint256 amount; // quotedOut\r
}\r
\r
struct QuoteArgs {\r
address tokenIn;\r
address tokenOut;\r
uint256 amountIn;\r
}\r
\r
struct LegDecoded {\r
address module;\r
uint256 index;\r
uint256 quoted; // quotedOut from payload\r
address tokenIn;\r
address tokenOut;\r
uint256 amountIn;\r
bytes[] route;\r
}\r
\r
struct SplitResult {\r
address moduleA;\r
address moduleB;\r
address tokenIn;\r
address tokenOut;\r
uint256 totalIn;\r
uint256 amountInA;\r
uint256 amountInB;\r
uint256 outA;\r
uint256 outB;\r
uint256 totalOut;\r
}\r
\r
struct TrackedRoute {\r
bytes payload;\r
uint256 amountOut;\r
address module;\r
uint256 moduleIndex;\r
}\r
\r
struct BestQuotes {\r
TrackedRoute top1Hop;\r
TrackedRoute second1Hop;\r
TrackedRoute top2Hop;\r
TrackedRoute second2Hop;\r
}\r
\r
struct ModuleQuotes {\r
address module;\r
uint256 moduleIndex;\r
bytes payload1Hop;\r
uint256 amountOut1Hop;\r
bytes payload2Hop;\r
uint256 amountOut2Hop;\r
}\r
\r
interface IUniswapRouter {\r
function getAmountsOut(uint256 amountIn, address[] calldata path)\r
external\r
view\r
returns (uint256[] memory amounts);\r
}\r
\r
interface IFeedRegistry {\r
function latestRoundData(address base, address quote)\r
external\r
view\r
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);\r
\r
function decimals(address base, address quote) external view returns (uint8);\r
}\r
\r
interface IDexModule {\r
/**\r
* @notice Compute the best 1-hop and 2-hop routes.\r
* @param tokenIn Input token\r
* @param tokenOut Output token\r
* @param amountIn Input amount\r
* @return best1HopRoute Serialized 1-hop route\r
* @return amountOut1Hop Quoted 1-hop output\r
* @return best2HopRoute Serialized 2-hop route\r
* @return amountOut2Hop Quoted 2-hop output\r
*/\r
function getBestRoute(\r
address tokenIn,\r
address tokenOut,\r
uint256 amountIn\r
) external view returns (\r
DexRoute memory best1HopRoute,\r
uint256 amountOut1Hop,\r
DexRoute memory best2HopRoute,\r
uint256 amountOut2Hop\r
);\r
\r
/**\r
* @notice Execute a previously returned route with a slippage check based on a percentage\r
* @param route Serialized route\r
* @param to Recipient of the final tokens\r
* @param percent Percentage (0-100) of amountIn from the route to be swapped. 100 = 100%\r
* @return amountOut Actual output received\r
*/\r
function swapRoute(\r
DexRoute calldata route,\r
address to,\r
uint256 percent\r
) external returns (uint256 amountOut);\r
\r
/**\r
* @notice Simulate a route (1–2 hops) encoded as {DexRoute}\r
* @param route Serialized route\r
* @param percent Percentage (0-100)\r
* @return amountOut Quoted total output amount\r
*/\r
function simulateRoute(\r
DexRoute calldata route,\r
uint256 percent\r
) external view returns (uint256 amountOut);\r
}\r
\r
interface IWETH {\r
function deposit() external payable;\r
function withdraw(uint256 amount) external;\r
function transfer(address to, uint256 amount) external returns (bool);\r
function balanceOf(address) external view returns (uint256);\r
}\r
\r
interface IDexV2AggregatorModule {\r
function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts);\r
\r
function swapExactTokensForTokens(\r
uint amountIn,\r
uint amountOutMin,\r
address[] calldata path,\r
address to,\r
uint deadline\r
) external returns (uint[] memory amounts);\r
}\r
\r
contract UniversalRouter is Ownable, ReentrancyGuard {\r
using SafeERC20 for IERC20;\r
\r
/* ─────────────────────────────── Storage ─────────────────────────────── */\r
\r
address constant DENOM_ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;\r
address constant DENOM_USD = 0x0000000000000000000000000000000000000348;\r
\r
// Chainlink Feed Registry (mainnet)\r
address public feedRegistry = 0x47Fb2585D2C56Fe188D0E6ec628a38b74fCeeeDf;\r
\r
address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;\r
\r
address public constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;\r
address[] public modules; // list of modules (Curve, UniV2, Sushi, UniV3 ...)\r
\r
// Optional: DSF Dex V2 Aggregator (UniV2/Sushi/Pancake aggregator with split)\r
address public dexV2Aggregator;\r
\r
mapping(address => bool) public isModule;\r
mapping(address => uint256) private moduleIndexPlusOne; // 1-based for O(1) remove\r
\r
// Uniswap V2 Router (mainnet)\r
IUniswapRouter public constant V2_ROUTER = IUniswapRouter(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);\r
\r
/* ───────────────────────────── Fees config ───────────────────────────── */\r
\r
address public feeRecipient; // commission recipient address\r
uint16 public feeBpsSwap; // commission in bps (max 10000 = 100%)\r
uint16 public feeBpsPositive; // commission with positive slippage, bps (max 10000 = 100%)\r
\r
/* ────────────────────────────── Fees caps ────────────────────────────── */\r
\r
uint16 public constant MAX_FEE_SWAP_BPS = 100; // 1%\r
uint16 public constant MAX_FEE_POSITIVE_BPS = 10_000; // 100%\r
\r
/* ─────────────────────────────── Events ──────────────────────────────── */\r
\r
event ModuleAdded(address indexed module);\r
event ModuleRemoved(address indexed module);\r
event ModulesReset(uint256 newCount);\r
event FeeConfigUpdated(address indexed recipient, uint16 bpsSwap, uint16 bpsPositive);\r
event ERC20Recovered(address indexed token, address indexed to, uint256 amount);\r
event ETHSwept(address indexed to, uint256 amount);\r
\r
event DexV2AggregatorUpdated(address indexed oldAgg, address indexed newAgg);\r
\r
/**\r
* @notice Execution of a single swap\r
* @param module Module that executed the route\r
* @param user Initiator (msg.sender)\r
* @param to Recipient of the final funds\r
* @param tokenIn Input token\r
* @param tokenOut Output token\r
* @param amountIn Input amount (withdrawn from the user)\r
* @param amountOut Final amount after fees (net)\r
* @param quotedOut Expected output (quota from payload)\r
*/\r
event SwapExecuted(\r
address indexed module,\r
address indexed user,\r
address indexed to,\r
address tokenIn,\r
address tokenOut,\r
uint256 amountIn,\r
uint256 amountOut,\r
uint256 quotedOut\r
);\r
\r
/**\r
* @notice Execution of a split swap via two routes\r
* @param user Initiator (msg.sender)\r
* @param to Recipient of the final funds\r
* @param moduleA Module A\r
* @param moduleB Module B\r
* @param tokenIn Input token (or WETH for ETH route)\r
* @param tokenOut Output token\r
* @param totalIn Total input\r
* @param totalOut Total output (after fees — if the event is emitted after distribution)\r
* @param bpsA Share A in percent (0–100)\r
*/\r
event SwapSplitExecuted(\r
address indexed user,\r
address indexed to,\r
address moduleA,\r
address moduleB,\r
address tokenIn,\r
address tokenOut,\r
uint256 totalIn,\r
uint256 totalOut,\r
uint16 bpsA\r
);\r
\r
/* ─────────────────────────────── Errors ─────────────────────────────── */\r
\r
error ZeroAddress();\r
error DuplicateModule();\r
error NotAModule();\r
error NoRouteFound();\r
\r
/* ─────────────────────────────── Modifiers ─────────────────────────────── */\r
\r
modifier onlyERC20(address token) {\r
require(token != address(0) && token.code.length > 0, "not ERC20");\r
_;\r
}\r
\r
/* ──────────────────────────────── receive ───────────────────────────────── */\r
\r
/// @notice Needed to get native ETH (e.g., with IWETH.withdraw())\r
receive() external payable {}\r
\r
/* ─────────────────────────────── Constructor ─────────────────────────────── */\r
\r
/**\r
* @notice Deploys the router and configures modules and commission parameters\r
* @param _modules List of module addresses (Curve/UniV2/UniV3/…)\r
* @param _feeRecipient Address of the commission recipient\r
* @param _feeBpsSwap Swap fee, bps (max. limited by require inside)\r
* @param _feeBpsPositive Positive slippage fee, bps (max. limited by require inside)\r
*/\r
constructor(\r
address[] memory _modules,\r
address _feeRecipient,\r
uint16 _feeBpsSwap,\r
uint16 _feeBpsPositive\r
) Ownable(msg.sender) {\r
_setModules(_modules);\r
require(_feeBpsSwap <= MAX_FEE_SWAP_BPS, "UR: swap fee too high");\r
require(_feeBpsPositive <= MAX_FEE_POSITIVE_BPS, "UR: pos fee too high");\r
feeRecipient = _feeRecipient;\r
feeBpsSwap = _feeBpsSwap;\r
feeBpsPositive = _feeBpsPositive;\r
\r
emit FeeConfigUpdated(feeRecipient, feeBpsSwap, feeBpsPositive);\r
}\r
\r
/* ────────────────────────────── Admin: Registry ──────────────────────────── */\r
\r
function setFeedRegistry(address reg) external onlyOwner {\r
require(reg != address(0), "UR: bad registry");\r
require(reg.code.length > 0, "UR: registry not a contract");\r
feedRegistry = reg;\r
}\r
\r
/* ──────────────────────────── Admin: modules mgmt ────────────────────────── */\r
\r
/**\r
* @notice Complete reset of the module list\r
* @dev Clears old ones, adds new ones, emits ModulesReset\r
* @param _modules New list of modules\r
*/\r
function setModules(address[] calldata _modules) external onlyOwner {\r
_clearModules();\r
_addModules(_modules);\r
emit ModulesReset(_modules.length);\r
}\r
\r
/**\r
* @notice Add module to allowlist\r
* @param module Address of IDexModule module\r
*/\r
function addModule(address module) external onlyOwner {\r
_addModule(module);\r
}\r
\r
/**\r
* @notice Remove module from allowlist\r
* @param module Address of IDexModule module\r
*/\r
function removeModule(address module) external onlyOwner {\r
_removeModule(module);\r
}\r
\r
/**\r
* @notice Returns the number of connected modules\r
* @return The length of the modules array\r
*/\r
function modulesLength() external view returns (uint256) {\r
return modules.length;\r
}\r
\r
/**\r
* @notice Returns the current list of modules\r
* @dev The array is returned in memory (a copy of the state)\r
* @return An array of module addresses\r
*/\r
function getModules() external view returns (address[] memory) {\r
return modules;\r
}\r
\r
/* ────────────────────────────────── Admin ────────────────────────────────── */\r
\r
/**\r
* @notice Rescue all stuck ERC-20 tokens to `to` or owner\r
* @dev Owner-only. Uses SafeERC20. Always sends the entire token balance\r
* held by this contract. If `to` is zero, defaults to owner()\r
* @param token ERC-20 token address to rescue (must be non-zero)\r
* @param to Recipient; if zero address, defaults to owner()\r
*/\r
function recoverERC20(address token, address to)\r
external\r
onlyOwner\r
nonReentrant\r
{\r
require(token != address(0), "UR: token=0");\r
address recipient = (to == address(0)) ? owner() : to;\r
\r
uint256 amt = IERC20(token).balanceOf(address(this));\r
if (amt == 0) return; // nothing to do\r
\r
IERC20(token).safeTransfer(recipient, amt);\r
emit ERC20Recovered(token, recipient, amt);\r
}\r
\r
/**\r
* @notice Transfers all remaining ETH from the contract to the owner or specified address\r
* @dev Only for the owner (onlyOwner). Added nonReentrant to protect against repeated calls\r
* If the `to` parameter is equal to a zero address, the funds are sent to the contract owner\r
* @param to The address of the ETH recipient (if 0x0 — send to the owner)\r
*/\r
function sweepETH(address to)\r
external\r
onlyOwner\r
nonReentrant\r
{\r
address recipient = (to == address(0)) ? owner() : to;\r
uint256 bal = address(this).balance;\r
(bool ok,) = recipient.call{value: bal}("");\r
require(ok, "ETH sweep failed");\r
emit ETHSwept(recipient, bal);\r
}\r
\r
/**\r
* @notice Sets the address of the DexV2 aggregator (UniV2/Sushi/Pancake... aggregator with split).\r
* @param agg New aggregator address or 0x0 to reset/disable\r
*/\r
function setDexV2Aggregator(address agg) external onlyOwner {\r
address old = dexV2Aggregator;\r
if (agg != address(0)) {\r
require(agg.code.length > 0, "UR: agg not a contract");\r
}\r
dexV2Aggregator = agg;\r
emit DexV2AggregatorUpdated(old, agg);\r
}\r
\r
/* ───────────────────────────────── Admin: fee ────────────────────────────── */\r
\r
/**\r
* @notice Update the address of the commission recipient\r
* @param _recipient New address of the fee recipient\r
*/\r
function setFeeRecipient(address _recipient) external onlyOwner {\r
feeRecipient = _recipient;\r
emit FeeConfigUpdated(feeRecipient, feeBpsSwap, feeBpsPositive);\r
}\r
\r
/**\r
* @notice Update commission percentages\r
* @dev Upper limit checked via require; emits FeeConfigUpdated\r
* @param _feeBpsSwap Swap commission, bps\r
* @param _feeBpsPositive Positive slippage commission, bps\r
*/\r
function setFeePercents(uint16 _feeBpsSwap, uint16 _feeBpsPositive) external onlyOwner {\r
require(_feeBpsSwap <= MAX_FEE_SWAP_BPS, "UR: swap fee too high");\r
require(_feeBpsPositive <= MAX_FEE_POSITIVE_BPS, "UR: pos fee too high");\r
feeBpsSwap = _feeBpsSwap;\r
feeBpsPositive = _feeBpsPositive;\r
emit FeeConfigUpdated(feeRecipient, feeBpsSwap, feeBpsPositive);\r
}\r
\r
/**\r
* @notice Completely reinstalls the list of modules\r
* @dev Clears the current modules, then adds new ones. Emits ModulesReset\r
* @param _modules New list of modules\r
*/\r
function _setModules(address[] memory _modules) internal {\r
_clearModules();\r
uint256 n = _modules.length;\r
for (uint256 i; i < n; ) {\r
_addModule(_modules[i]);\r
unchecked { ++i; }\r
}\r
emit ModulesReset(n);\r
}\r
\r
/**\r
* @notice Resets (clears) all modules\r
* @dev Resets isModule and indexes; clears the modules array\r
*/\r
function _clearModules() internal {\r
uint256 n = modules.length;\r
for (uint256 i; i < n; ) {\r
address m = modules[i];\r
isModule[m] = false;\r
moduleIndexPlusOne[m] = 0;\r
unchecked { ++i; }\r
}\r
delete modules;\r
}\r
\r
/**\r
* @notice Adds modules in bulk\r
* @dev Calls _addModule for each address\r
* @param _modules List of module addresses\r
*/\r
function _addModules(address[] calldata _modules) internal {\r
uint256 n = _modules.length;\r
for (uint256 i; i < n; ) {\r
_addModule(_modules[i]);\r
unchecked { ++i; }\r
}\r
}\r
\r
/**\r
* @notice Adds one module to the allowlist\r
* @dev Checks for a non-zero address, the presence of code, and the absence of duplicates\r
* Updates isModule, modules, and moduleIndexPlusOne. Emits ModuleAdded\r
* @param module The module contract address\r
*/\r
function _addModule(address module) internal {\r
if (module == address(0)) revert ZeroAddress();\r
if (isModule[module]) revert DuplicateModule();\r
\r
if (module.code.length == 0) revert ZeroAddress();\r
\r
isModule[module] = true;\r
modules.push(module);\r
moduleIndexPlusOne[module] = modules.length; // 1-based\r
emit ModuleAdded(module);\r
}\r
\r
/**\r
* @notice Removes a module from the allowlist\r
* @dev Performs O(1) removal via swap-pop, supporting 1-based indexing\r
* Emit ModuleRemoved\r
* @param module Address of the module to be removed\r
*/\r
function _removeModule(address module) internal {\r
uint256 idxPlusOne = moduleIndexPlusOne[module];\r
if (idxPlusOne == 0) revert NotAModule();\r
\r
uint256 idx = idxPlusOne - 1;\r
uint256 lastIdx = modules.length - 1;\r
\r
if (idx != lastIdx) {\r
address last = modules[lastIdx];\r
modules[idx] = last;\r
moduleIndexPlusOne[last] = idx + 1;\r
}\r
modules.pop();\r
\r
isModule[module] = false;\r
moduleIndexPlusOne[module] = 0;\r
emit ModuleRemoved(module);\r
}\r
\r
/* ─────────────────────────────── WETH Helpers ────────────────────────────── */\r
\r
/**\r
* @dev Wraps incoming native ETH into WETH\r
* @param amount Amount of ETH to wrap (msg.value)\r
*/\r
function _wrapETH(uint256 amount) internal {\r
IWETH(WETH).deposit{value: amount}();\r
}\r
\r
/**\r
* @dev Converts WETH back to ETH and sends it to the recipient\r
* @param amount Amount of WETH to convert\r
* @param to Recipient's native ETH address\r
*/\r
function _unwrapWETHAndSend(uint256 amount, address to) internal {\r
require(IWETH(WETH).balanceOf(address(this)) >= amount, "UR: insufficient WETH");\r
IWETH(WETH).withdraw(amount);\r
// Send native ETH\r
(bool success,) = to.call{value: amount}("");\r
require(success, "UR: ETH transfer failed");\r
}\r
\r
/* ───────────────────────────── ETH payout/guards ─────────────────────────── */\r
\r
/**\r
* @notice Ensures that tokenIn == WETH in the input payload\r
* @param payload ABI-encoded route: (module,index,quotedOut,tokenIn,tokenOut,amountIn,bytes[])\r
*/\r
function _requireWethIn(bytes calldata payload) internal pure {\r
(, , , address tokenIn, , , ) =\r
abi.decode(payload, (address,uint256,uint256,address,address,uint256,bytes[]));\r
require(tokenIn == WETH, "UR: payload tokenIn != WETH");\r
}\r
\r
/**\r
* @notice Ensures that tokenOut == WETH in the output payload\r
* @param payload ABI-encoded route: (module,index,quotedOut,tokenIn,tokenOut,amountIn,bytes[])\r
*/\r
function _requireWethOut(bytes calldata payload) internal pure {\r
(, , , , address tokenOut, , ) =\r
abi.decode(payload, (address,uint256,uint256,address,address,uint256,bytes[]));\r
require(tokenOut == WETH, "UR: payload tokenOut != WETH");\r
}\r
\r
/**\r
* @notice Quick reading of the address from the ABI header payload\r
* @dev The slot corresponds to the position of a 32-byte word in abi.encode(...)\r
* 0: module, 1: index, 2: quotedOut, 3: tokenIn, 4: tokenOut, 5: amountIn, 6: offset(bytes[])\r
* @param payload Full ABI payload\r
* @param slot Slot number (0-based)\r
* @return a Address read from the specified slot\r
*/\r
function _loadAddressFromPayload(bytes calldata payload, uint256 slot) internal pure returns (address a) {\r
assembly ("memory-safe") {\r
a := shr(96, calldataload(add(payload.offset, mul(slot, 32))))\r
}\r
}\r
\r
/* ────────────────────────────────── Helpers ──────────────────────────────── */\r
\r
/**\r
* @notice Returns the exit quote of the last token on the V2 route via the UniswapV2 public router.\r
* @param amountIn Input token amount.\r
* @param path Path of the form [tokenIn, ..., tokenOut]; must be ≥ 2 in length.\r
* @return Expected amountOut for the last element of `path`, or 0 on error/illiquidity.\r
*/\r
function _v2GetOut(uint256 amountIn, address[] memory path)\r
internal\r
view\r
returns (uint256)\r
{\r
if (amountIn == 0 || path.length < 2) return 0;\r
try V2_ROUTER.getAmountsOut(amountIn, path) returns (uint256[] memory amounts) {\r
return amounts[amounts.length - 1];\r
} catch {\r
return 0; // no liquidity/pairs — simply 0\r
}\r
}\r
\r
/**\r
* @notice Quick estimate of the TOKEN price in USDT via the USDT→WETH→TOKEN (V2 router) connection.\r
* @param token Address of the token for which the output is estimated.\r
* @param amountUsdt Amount of USDT at the input (e.g., 1_000_000 for 1 USDT with 6 digits).\r
* @return Expected output in TOKEN (in its minimum units) or 0.\r
*/\r
function _v2OutUsdtWethToken(address token, uint256 amountUsdt) internal view returns (uint256) {\r
address[] memory path = new address[](3);\r
path[0] = USDT;\r
path[1] = WETH;\r
path[2] = token;\r
return _v2GetOut(amountUsdt, path);\r
}\r
\r
/**\r
* @notice Updates the best quotes in 1-hop and 2-hop segments\r
* @dev Supports “top-1” and “top-2” for each category\r
* @param currentBest Current best routes\r
* @param newRoute Candidate for inclusion\r
* @param is1Hop 1-hop (true) or 2-hop (false) flag\r
*/\r
function _updateBestQuotes(BestQuotes memory currentBest, TrackedRoute memory newRoute, bool is1Hop) private pure {\r
if (is1Hop) {\r
if (newRoute.amountOut > currentBest.top1Hop.amountOut) {\r
currentBest.second1Hop = currentBest.top1Hop;\r
currentBest.top1Hop = newRoute;\r
} else if (newRoute.amountOut > currentBest.second1Hop.amountOut) {\r
currentBest.second1Hop = newRoute;\r
}\r
} else { // 2-hop\r
if (newRoute.amountOut > currentBest.top2Hop.amountOut) {\r
currentBest.second2Hop = currentBest.top2Hop;\r
currentBest.top2Hop = newRoute;\r
} else if (newRoute.amountOut > currentBest.second2Hop.amountOut) {\r
currentBest.second2Hop = newRoute;\r
}\r
}\r
}\r
\r
/**\r
* @notice Updates the two absolute best routes found so far (overall Top-1 and Top-2)\r
* @dev If the new route beats Top-1, it becomes Top-1 and the old Top-1 shifts to Top-2\r
* Otherwise, if it only beats Top-2, it replaces Top-2\r
* @param top1 Current absolute best route (Top-1)\r
* @param top2 Current second absolute best route (Top-2)\r
* @param newRoute Newly observed candidate route to compare against the tops\r
* @return Updated Top-1 and Top-2 routes (in this order)\r
*/\r
function _updateTopOverall(\r
TrackedRoute memory top1,\r
TrackedRoute memory top2,\r
TrackedRoute memory newRoute\r
) private pure returns (TrackedRoute memory, TrackedRoute memory) {\r
if (newRoute.amountOut > top1.amountOut) {\r
top2 = top1;\r
top1 = newRoute;\r
} else if (newRoute.amountOut > top2.amountOut) {\r
top2 = newRoute;\r
}\r
return (top1, top2);\r
}\r
\r
/**\r
* @notice Queries a module for the best 1-hop and 2-hop quotes and packages them as payloads\r
* @dev Calls IDexModule.getBestRoute via staticcall and, if non-zero quotes are returned,\r
* encodes payloads as abi.encode(module, index, quotedOut, tokenIn, tokenOut, amountIn, route.data)\r
* If the module is not registered or the call fails/returns empty, the struct remains zeroed\r
* @param m Module address being queried\r
* @param idx Module index (stored for payload bookkeeping)\r
* @param a Quote arguments (tokenIn, tokenOut, amountIn)\r
* @return quotes Struct holding module info, 1-hop/2-hop amounts and payloads (if any)\r
*/\r
function _getModuleQuotes(\r
address m,\r
uint256 idx,\r
QuoteArgs memory a\r
) internal view returns (ModuleQuotes memory quotes) {\r
quotes.module = m;\r
quotes.moduleIndex = idx;\r
\r
if (!isModule[m]) return quotes;\r
\r
bytes memory cd = abi.encodeWithSelector(\r
IDexModule.getBestRoute.selector,\r
a.tokenIn,\r
a.tokenOut,\r
a.amountIn\r
);\r
\r
(bool success, bytes memory ret) = m.staticcall(cd);\r
if (!success || ret.length == 0) return quotes;\r
\r
(\r
DexRoute memory route1, uint256 out1,\r
DexRoute memory route2, uint256 out2\r
) = abi.decode(ret, (DexRoute, uint256, DexRoute, uint256));\r
\r
// Build payloads only for non-zero, non-empty routes\r
if (out1 > 0 && route1.data.length > 0) {\r
quotes.amountOut1Hop = out1;\r
quotes.payload1Hop = abi.encode(\r
m, idx, out1, a.tokenIn, a.tokenOut, a.amountIn, route1.data\r
);\r
}\r
\r
if (out2 > 0 && route2.data.length > 0) {\r
quotes.amountOut2Hop = out2;\r
quotes.payload2Hop = abi.encode(\r
m, idx, out2, a.tokenIn, a.tokenOut, a.amountIn, route2.data\r
);\r
}\r
}\r
\r
/**\r
* @dev Private helper function for calculating the total output amount\r
* @param percentA Percentage of amountIn for Route A (0-100)\r
*/\r
function _calculateTotalOut(\r
address moduleA,\r
bytes[] memory routeA,\r
address moduleB,\r
bytes[] memory routeB,\r
uint16 percentA // 0-100\r
) internal view returns (uint256 totalOut) {\r
uint16 percentB = 100 - percentA;\r
\r
// simulateRoute for A (percent 0–100)\r
uint256 outA = IDexModule(moduleA).simulateRoute(DexRoute({ data: routeA }), percentA);\r
\r
// simulateRoute for B (percent 0–100)\r
uint256 outB = IDexModule(moduleB).simulateRoute(DexRoute({ data: routeB }), percentB);\r
\r
return outA + outB;\r
}\r
\r
/**\r
* @notice Safely sets the allowance to the required minimum\r
* @dev If the current allowance < amount, first set it to zero (if >0), then set it to type(uint256).max\r
* Uses SafeERC20.forceApprove for maximum compatibility\r
* @param token ERC20 token address\r
* @param spender Contract address to which we issue the allowance\r
* @param amount Minimum required limit\r
*/\r
function _smartApprove(address token, address spender, uint256 amount) internal {\r
uint256 cur = IERC20(token).allowance(address(this), spender);\r
if (cur < amount) {\r
if (cur > 0) IERC20(token).forceApprove(spender, 0);\r
IERC20(token).forceApprove(spender, type(uint256).max);\r
}\r
}\r
\r
/**\r
* @notice Emits the consolidated split-swap execution event\r
* @dev Packs the essential split data into a single event for off-chain indexing/analytics\r
* @param r Split result struct (modules, tokens, totals)\r
* @param user Original caller (initiator)\r
* @param to Final receiver of the swapped tokens/ETH\r
* @param bpsA Portion routed through module A, in percent (0–100)\r
*/\r
function _emitSwapSplit(\r
SplitResult memory r,\r
address user,\r
address to,\r
uint16 bpsA\r
) internal {\r
emit SwapSplitExecuted(\r
user,\r
to,\r
r.moduleA,\r
r.moduleB,\r
r.tokenIn,\r
r.tokenOut,\r
r.totalIn,\r
r.totalOut,\r
bpsA\r
);\r
}\r
\r
/**\r
* @notice Decodes a route payload (in memory) into a typed struct used by the router\r
* @dev Expects payload encoded as:\r
* (address module, uint256 index, uint256 quoted, address tokenIn, address tokenOut, uint256 amountIn, bytes[] route)\r
* @param payload ABI-encoded payload stored in memory\r
* @return d Decoded LegDecoded struct\r
*/\r
function _decodeRouteStruct(bytes memory payload)\r
internal\r
pure\r
returns (LegDecoded memory d)\r
{\r
(d.module, d.index, d.quoted, d.tokenIn, d.tokenOut, d.amountIn, d.route) =\r
abi.decode(payload, (address, uint256, uint256, address, address, uint256, bytes[]));\r
}\r
\r
/**\r
* @notice Decodes a route payload (in calldata) into a typed struct used by the router\r
* @dev Same layout as the memory version, but reads directly from calldata to save gas\r
* @param payload ABI-encoded payload residing in calldata\r
* @return d Decoded LegDecoded struct\r
*/\r
function _decodeRouteStructCallData(bytes calldata payload)\r
internal\r
pure\r
returns (LegDecoded memory d)\r
{\r
(d.module, d.index, d.quoted, d.tokenIn, d.tokenOut, d.amountIn, d.route) =\r
abi.decode(payload, (address, uint256, uint256, address, address, uint256, bytes[]));\r
}\r
\r
/**\r
* @notice Distribution of commissions and ERC20 transfer\r
* @dev Retains fix-fee (feeBpsSwap) and % of positive slippage (feeBpsPositive)\r
* @param token ERC20 address\r
* @param to Recipient\r
* @param grossOut Actual output after swap(s)\r
* @param quotedOut Quote (expectation)\r
* @param minOut Minimum acceptable output\r
* @return netOut Amount after commissions\r
*/\r
function _distributeTokenWithFees(\r
address token,\r
address to,\r
uint256 grossOut, // actual output after swap(s)\r
uint256 quotedOut, // quoted (expected) output\r
uint256 minOut\r
) internal returns (uint256 netOut) {\r
if (grossOut == 0) return 0;\r
\r
uint256 baseline = quotedOut > minOut ? quotedOut : minOut;\r
\r
uint256 feeSwap = 0;\r
uint256 feePos = 0;\r
\r
// take fees only if recipient is set and bps > 0\r
if (feeRecipient != address(0)) {\r
if (feeBpsSwap > 0) {\r
unchecked { feeSwap = (grossOut * feeBpsSwap) / 10_000; }\r
}\r
if (feeBpsPositive > 0 && grossOut > baseline) {\r
unchecked { feePos = ((grossOut - baseline) * feeBpsPositive) / 10_000; }\r
}\r
}\r
\r
uint256 totalFee = feeSwap + feePos;\r
// safety guard against overflow/rounding:\r
if (totalFee > grossOut) totalFee = grossOut;\r
\r
netOut = grossOut - totalFee;\r
\r
// Payouts: send fee to feeRecipient first, then net to user\r
if (totalFee > 0) {\r
IERC20(token).safeTransfer(feeRecipient, totalFee);\r
}\r
IERC20(token).safeTransfer(to, netOut);\r
}\r
\r
/**\r
* @notice Distribution of fees and transfer of ETH\r
* @dev Similar to _distributeTokenWithFees, but for ETH\r
* @param to Recipient\r
* @param grossEth Actual ETH output\r
* @param quotedOutEth Expected output\r
* @param minOutEth Minimum allowable output\r
* @return netOut Amount after fees\r
*/\r
function _distributeETHWithFees(\r
address to,\r
uint256 grossEth, // actual ETH output\r
uint256 quotedOutEth, // expected output (WETH==ETH)\r
uint256 minOutEth\r
) internal returns (uint256 netOut) {\r
if (grossEth == 0) return 0;\r
\r
uint256 baseline = quotedOutEth > minOutEth ? quotedOutEth : minOutEth;\r
\r
uint256 feeSwap = 0;\r
uint256 feePos = 0;\r
\r
if (feeRecipient != address(0)) {\r
if (feeBpsSwap > 0) {\r
unchecked { feeSwap = (grossEth * feeBpsSwap) / 10_000; }\r
}\r
if (feeBpsPositive > 0 && grossEth > baseline) {\r
unchecked { feePos = ((grossEth - baseline) * feeBpsPositive) / 10_000; }\r
}\r
}\r
\r
uint256 totalFee = feeSwap + feePos;\r
if (totalFee > grossEth) totalFee = grossEth;\r
\r
netOut = grossEth - totalFee;\r
\r
if (totalFee > 0) {\r
(bool fs, ) = feeRecipient.call{value: totalFee}("");\r
require(fs, "fee ETH xfer failed");\r
}\r
(bool ok, ) = to.call{value: netOut}("");\r
require(ok, "ETH xfer failed");\r
}\r
\r
/**\r
* @notice Safely reads balance, allowance, and decimals for a (token, wallet, spender)\r
* @dev For ETH (token==address(0)): returns (wallet.balance, 0, 18)\r
* Uses low-level staticcall to tolerate non-standard ERC-20s (e.g., USDT)\r
* If {decimals()} cannot be read, returns 0 as a sentinel value\r
* @param token ERC-20 token address (or address(0) for ETH)\r
* @param wallet Address whose balance is queried\r
* @param spender Address whose allowance is checked\r
* @return bal Token balance of {wallet} (or ETH balance if token==address(0))\r
* @return allow_ Current allowance from {wallet} to {spender} (0 for ETH)\r
* @return decs Token decimals (18 for ETH; 0 if unreadable)\r
*/\r
function _safeBalanceAndAllowance(\r
address token,\r
address wallet,\r
address spender\r
) internal view returns (uint256 bal, uint256 allow_, uint8 decs) {\r
if (token == address(0)) {\r
// ETH: allowance not applicable\r
return (wallet.balance, 0, 18);\r
}\r
\r
// balanceOf(wallet)\r
(bool ok1, bytes memory data1) =\r
token.staticcall(abi.encodeWithSelector(IERC20.balanceOf.selector, wallet));\r
if (ok1 && data1.length >= 32) {\r
bal = abi.decode(data1, (uint256));\r
} else {\r
bal = 0;\r
}\r
\r
// allowance(wallet, spender)\r
(bool ok2, bytes memory data2) =\r
token.staticcall(abi.encodeWithSelector(IERC20.allowance.selector, wallet, spender));\r
if (ok2 && data2.length >= 32) {\r
allow_ = abi.decode(data2, (uint256));\r
} else {\r
allow_ = 0;\r
}\r
\r
// decimals() selector = 0x313ce567\r
(bool ok3, bytes memory data3) =\r
token.staticcall(abi.encodeWithSelector(bytes4(0x313ce567)));\r
if (ok3 && data3.length >= 32) {\r
// Some tokens return uint256, some return uint8. Read as uint256 and cast safely.\r
uint256 d = abi.decode(data3, (uint256));\r
decs = d > 255 ? uint8(255) : uint8(d);\r
} else {\r
// if reading failed — 0, so that the caller understands that it is undefined\r
decs = 0;\r
}\r
}\r
\r
/**\r
* @notice Reads the base/quote price from the Chainlink Feed Registry and normalizes it to 1e18\r
* @dev Never reverts. Returns (price1e18=0, updatedAt=0) if the feed is missing or invalid\r
* No freshness check is performed here; callers may validate staleness separately\r
* @param base Address of the base asset (token address or Chainlink denomination, e.g. DENOM_ETH)\r
* @param quote Address of the quote asset (typically DENOM_USD)\r
* @return price1e18 Base/quote price scaled to 1e18 (0 if unavailable)\r
* @return updatedAt Timestamp of the last feed update (0 if unavailable)\r
*/\r
function _pairPrice1e18(address base, address quote)\r
private\r
view\r
returns (uint256 price1e18, uint256 updatedAt)\r
{\r
address reg = feedRegistry;\r
if (reg == address(0)) return (0, 0);\r
\r
try IFeedRegistry(reg).latestRoundData(base, quote) returns (\r
uint80, int256 answer, uint256, uint256 upd, uint80\r
) {\r
if (answer <= 0) return (0, upd);\r
uint8 dec = IFeedRegistry(reg).decimals(base, quote);\r
uint256 u = uint256(answer);\r
if (dec < 18) price1e18 = u * 10 ** (18 - dec);\r
else if (dec > 18) price1e18 = u / 10 ** (dec - 18);\r
else price1e18 = u;\r
return (price1e18, upd);\r
} catch {\r
return (0, 0);\r
}\r
}\r
\r
/**\r
* @notice Returns the price of 1 token in USD (1e18) via Chainlink Feed Registry\r
* @dev Never reverts. **Does not** check data freshness; returns whatever the registry holds\r
* Resolution order:\r
* - If `asEth == true` or `token == address(0)`: use ETH/USD;\r
* - Else try direct TOKEN/USD;\r
* - If `token == WETH`: use ETH/USD;\r
* - Else try TOKEN/ETH × ETH/USD\r
* @param token Token address (or address(0) for ETH)\r
* @param asEth If true, force using the ETH/USD price (e.g., for WETH, stETH, etc.)\r
* @return price1e18 The price of 1 token in USD (1e18), or 0 if unavailable\r
* @return updatedAt The timestamp of the last feed update used for the computation\r
*/\r
function _tryTokenUsdPrice1e18(address token, bool asEth)\r
internal\r
view\r
returns (uint256 price1e18, uint256 updatedAt)\r
{\r
// ETH or forcibly as ETH & WETH → ETH/USD\r
if (asEth || token == address(0) || token == WETH) {\r
(price1e18, updatedAt) = _pairPrice1e18(DENOM_ETH, DENOM_USD);\r
if (price1e18 != 0) return (price1e18, updatedAt);\r
}\r
\r
// Direct TOKEN/USD\r
(price1e18, updatedAt) = _pairPrice1e18(token, DENOM_USD);\r
if (price1e18 != 0) return (price1e18, updatedAt);\r
\r
// Attempt via TOKEN/ETH × ETH/USD (relevant for LST, etc.)\r
(uint256 tEth, uint256 updA) = _pairPrice1e18(token, DENOM_ETH);\r
if (tEth != 0) {\r
(uint256 ethUsd, uint256 updB) = _pairPrice1e18(DENOM_ETH, DENOM_USD);\r
if (ethUsd != 0) {\r
uint256 minUpd = updA < updB ? updA : updB;\r
return ((tEth * ethUsd) / 1e18, minUpd);\r
}\r
}\r
\r
// Fallback from DEX (UniswapV2): USDT -> (WETH?) -> TOKEN\r
(uint256 usdtUsd1e18, uint256 updU) = _pairPrice1e18(USDT, DENOM_USD);\r
if (usdtUsd1e18 == 0) return (0, 0);\r
\r
uint256 tokenOutRaw = _v2OutUsdtWethToken(token, 1_000_000);\r
\r
uint8 decs;\r
\r
// decimals() selector = 0x313ce567\r
(bool ok3, bytes memory data3) =\r
token.staticcall(abi.encodeWithSelector(bytes4(0x313ce567)));\r
if (ok3 && data3.length >= 32) {\r
// Some tokens return uint256, some return uint8. Read as uint256 and cast safely.\r
uint256 d = abi.decode(data3, (uint256));\r
decs = d > 255 ? uint8(255) : uint8(d);\r
} else {\r
// if reading failed — 0, so that the caller understands that it is undefined\r
decs = 0;\r
}\r
\r
if (tokenOutRaw == 0) return (0, 0);\r
\r
uint256 tenPow = (decs == 0) ? 1e18 : _safePow10(decs);\r
price1e18 = (usdtUsd1e18 * tenPow) / tokenOutRaw;\r
return (price1e18, updU);\r
}\r
\r
/**\r
* @notice Safely computes 10**{decs} without risking overflow\r
* @dev The maximum exponent that fits in uint256 is 77, since 10**78 > 2**256\r
* Values above 77 are clamped down to 77\r
* @param decs Requested number of decimals (e.g. ERC-20 decimals)\r
* @return power 10 raised to the safe exponent, i.e. 10**min(decs,77)\r
*/\r
function _safePow10(uint8 decs) internal pure returns (uint256) {\r
// 10**78 no longer fits in uint256; 10**77 < 2**256\r
uint8 safeDecs = decs > 77 ? 77 : decs;\r
unchecked { \r
return 10 ** uint256(safeDecs);\r
}\r
}\r
\r
/* ──────────────────────────────────── Read ───────────────────────────────── */\r
\r
/**\r
* @notice Reads token→USD price (1e18) via Chainlink Feed Registry without checking “freshness”\r
* @dev Returns 0 if the feed is missing or `answer <= 0`. ETH is passed as address(0)\r
* @param token Token address (or address(0) for ETH)\r
* @param asEth If true, use ETH/USD price (for WETH, stETH, etc.)\r
* @return usdPerToken1e18 Token price in USD (1e18) or 0\r
* @return updatedAt Time of the last feed update\r
*/\r
function tryTokenUsdPrice1e18(address token, bool asEth)\r
external\r
view\r
returns (uint256 usdPerToken1e18, uint256 updatedAt)\r
{\r
(usdPerToken1e18, updatedAt) = _tryTokenUsdPrice1e18(token, asEth);\r
return (usdPerToken1e18, updatedAt);\r
}\r
\r
/**\r
* @notice Returns balance, allowance, and decimals for a (token, wallet, spender)\r
* @dev For ETH (token==address(0)): returns (wallet.balance, 0, 18)\r
* Decimals is 0 if the token's {decimals()} cannot be read\r
* @param token ERC-20 token address (or address(0) for ETH)\r
* @param wallet Address whose balance is queried\r
* @param spender Address whose allowance is checked\r
* @return balance Token balance of {wallet} (or ETH balance if token==address(0))\r
* @return allowance_ Current allowance from {wallet} to {spender} (0 for ETH)\r
* @return decimals_ Token decimals (18 for ETH; 0 if unreadable)\r
*/\r
function balanceAndAllowanceOf(\r
address token,\r
address wallet,\r
address spender\r
) external view returns (uint256 balance, uint256 allowance_, uint8 decimals_) {\r
(balance, allowance_, decimals_) = _safeBalanceAndAllowance(token, wallet, spender);\r
}\r
\r
/**\r
* @notice Returns balance, allowance, decimals, and USD price for a single token\r
* @dev For ETH (token==address(0)): returns (wallet.balance, 0, 18) and uses ETH/USD price when {asEth} is true\r
* Decimals is 0 if the token's {decimals()} cannot be read\r
* @param token ERC-20 token address (or address(0) for ETH)\r
* @param wallet Address whose balance is queried\r
* @param spender Address whose allowance is checked\r
* @param asEth If true, forces the ETH/USD feed for this token\r
* @return balance Token balance of {wallet} (or ETH balance if token==address(0))\r
* @return allowance_ Current allowance from {wallet} to {spender} (0 for ETH)\r
* @return usdPerToken1e18 Price of 1 token in USD (scaled to 1e18), 0 if no feed/<=0\r
* @return updatedAt Timestamp of the last price update\r
* @return decimals_ Token decimals (18 for ETH; 0 if unreadable)\r
* @return ethBalance Native ETH balance of {wallet} (wei)\r
*/\r
function balanceAllowanceAndUsd(\r
address token,\r
address wallet,\r
address spender,\r
bool asEth\r
)\r
external\r
view\r
returns (\r
uint256 balance,\r
uint256 allowance_,\r
uint256 usdPerToken1e18,\r
uint256 updatedAt,\r
uint8 decimals_,\r
uint256 ethBalance\r
)\r
{\r
// 1) safely read balance/allowance (do not revert to non-standard ERC-20)\r
(balance, allowance_, decimals_) = _safeBalanceAndAllowance(token, wallet, spender);\r
\r
// 2) Let's try to get the price in USD (gently, without revert)\r
(usdPerToken1e18, updatedAt) = _tryTokenUsdPrice1e18(token, asEth);\r
\r
// 3) Always return native ETH balance of the wallet\r
ethBalance = wallet.balance;\r
}\r
\r
/**\r
* @notice Returns balance, allowance, decimals, and USD price for two tokens at once\r
* @dev For ETH (token==address(0)): returns (wallet.balance, 0, 18) and uses ETH/USD price if requested via {asEthIn}/{asEthOut}\r
* Decimals is 0 if a token's {decimals()} cannot be read\r
* @param wallet Address whose balances are queried\r
* @param spender Address whose allowances are checked\r
* @param tokenIn First token address (or address(0) for ETH)\r
* @param asEthIn If true, forces ETH/USD feed for tokenIn\r
* @param tokenOut Second token address (or address(0) for ETH)\r
* @param asEthOut If true, forces ETH/USD feed for tokenOut\r
* @return balanceIn {wallet} balance of tokenIn (ETH if tokenIn==address(0))\r
* @return allowance_In Allowance of tokenIn to {spender} (0 for ETH)\r
* @return usdPerToken1e18In TokenIn USD price normalized to 1e18 (0 if no feed/<=0)\r
* @return updatedAtIn Timestamp of the last price update for tokenIn\r
* @return decimals_In Decimals of tokenIn (18 for ETH; 0 if unreadable)\r
* @return balanceOut {wallet} balance of tokenOut (ETH if tokenOut==address(0))\r
* @return allowance_Out Allowance of tokenOut to {spender} (0 for ETH)\r
* @return usdPerToken1e18Out TokenOut USD price normalized to 1e18 (0 if no feed/<=0)\r
* @return updatedAtOut Timestamp of the last price update for tokenOut\r
* @return decimals_Out Decimals of tokenOut (18 for ETH; 0 if unreadable)\r
* @return ethBalance Native ETH balance of {wallet} (wei)\r
*/\r
function balanceAllowanceAndUsdDouble(\r
address wallet,\r
address spender,\r
address tokenIn,\r
bool asEthIn,\r
address tokenOut,\r
bool asEthOut\r
)\r
external\r
view\r
returns (\r
uint256 balanceIn,\r
uint256 allowance_In,\r
uint256 usdPerToken1e18In,\r
uint256 updatedAtIn,\r
uint8 decimals_In,\r
uint256 balanceOut,\r
uint256 allowance_Out,\r
uint256 usdPerToken1e18Out,\r
uint256 updatedAtOut,\r
uint8 decimals_Out,\r
uint256 ethBalance\r
)\r
{\r
// 1) safely read balance/allowance (do not revert to non-standard ERC-20)\r
(balanceIn, allowance_In, decimals_In) = _safeBalanceAndAllowance(tokenIn, wallet, spender);\r
(balanceOut, allowance_Out, decimals_Out) = _safeBalanceAndAllowance(tokenOut, wallet, spender);\r
\r
// 2) Let's try to get the price in USD (gently, without revert)\r
(usdPerToken1e18In, updatedAtIn) = _tryTokenUsdPrice1e18(tokenIn, asEthIn);\r
(usdPerToken1e18Out, updatedAtOut) = _tryTokenUsdPrice1e18(tokenOut, asEthOut);\r
\r
// 3) Native ETH balance\r
ethBalance = wallet.balance;\r
}\r
\r
/**\r
* @notice Return the 4 best routes (Top-1/Top-2 for 1-hop and 2-hop) and (optionally) the optimal split of the two absolute leaders\r
* @param tokenIn Input token\r
* @param tokenOut Output token\r
* @param amountIn Input amount\r
* @return best1HopRouteTop1 Payload of the best 1-hop\r
* @return amountOut1HopTop1 Quote of the best 1-hop\r
* @return best2HopRouteTop1 Payload of the best 2-hop\r
* @return amountOut2HopTop1 Quote for the best 2-hop\r
* @return best1HopRouteTop2 Payload of the second 1-hop\r
* @return amountOut1HopTop2 Quote for the second 1-hop\r
* @return best2HopRouteTop2 Payload of the second 2-hop\r
* @return amountOut2HopTop2 Quote for the second 2-hop\r
* @return splitAmountOut Best split quote between two absolute tops (0 if split does not improve)\r
* @return splitPercentA Share for route A (in percent, 0–100) for split (0 if split is not applicable)\r
*/\r
function getBestRoute(\r
address tokenIn,\r
address tokenOut,\r
uint256 amountIn\r
) external view returns (\r
bytes memory best1HopRouteTop1, uint256 amountOut1HopTop1,\r
bytes memory best2HopRouteTop1, uint256 amountOut2HopTop1,\r
bytes memory best1HopRouteTop2, uint256 amountOut1HopTop2,\r
bytes memory best2HopRouteTop2, uint256 amountOut2HopTop2,\r
uint256 splitAmountOut, uint16 splitPercentA\r
) {\r
QuoteArgs memory qa = QuoteArgs({\r
tokenIn: tokenIn,\r
tokenOut: tokenOut,\r
amountIn: amountIn\r
});\r
\r
BestQuotes memory best;\r
TrackedRoute memory top1Overall; // Absolute best route\r
TrackedRoute memory top2Overall; // Second best route\r
\r
for (uint256 i = 0; i < modules.length; ) {\r
ModuleQuotes memory quotes = _getModuleQuotes(modules[i], i, qa);\r
\r
if (quotes.amountOut1Hop > 0) {\r
TrackedRoute memory r1 = TrackedRoute({\r
payload: quotes.payload1Hop,\r
amountOut: quotes.amountOut1Hop,\r
module: quotes.module,\r
moduleIndex: quotes.moduleIndex\r
});\r
_updateBestQuotes(best, r1, true);\r
(top1Overall, top2Overall) = _updateTopOverall(top1Overall, top2Overall, r1);\r
}\r
\r
if (quotes.amountOut2Hop > 0) {\r
TrackedRoute memory r2 = TrackedRoute({\r
payload: quotes.payload2Hop,\r
amountOut: quotes.amountOut2Hop,\r
module: quotes.module,\r
moduleIndex: quotes.moduleIndex\r
});\r
_updateBestQuotes(best, r2, false);\r
(top1Overall, top2Overall) = _updateTopOverall(top1Overall, top2Overall, r2);\r
}\r
\r
unchecked { ++i; }\r
}\r
\r
if (top1Overall.amountOut == 0) revert NoRouteFound();\r
\r
// Return the standard 8 fields\r
best1HopRouteTop1 = best.top1Hop.payload; amountOut1HopTop1 = best.top1Hop.amountOut;\r
best2HopRouteTop1 = best.top2Hop.payload; amountOut2HopTop1 = best.top2Hop.amountOut;\r
best1HopRouteTop2 = best.second1Hop.payload; amountOut1HopTop2 = best.second1Hop.amountOut;\r
best2HopRouteTop2 = best.second2Hop.payload; amountOut2HopTop2 = best.second2Hop.amountOut;\r
\r
// Compute split between the two overall best routes (T1 and T2)\r
if (top2Overall.amountOut > 0 && keccak256(top1Overall.payload) != keccak256(top2Overall.payload)) {\r
(splitAmountOut, splitPercentA) = findBestSplit(\r
top1Overall.payload,\r
top2Overall.payload\r
);\r
\r
// If split provides no improvement, do not return it,\r
// since the best will be either T1 or T2 (T1.amountOut >= T2.amountOut).\r
if (splitAmountOut <= top1Overall.amountOut) {\r
splitAmountOut = 0;\r
splitPercentA = 0;\r
}\r
} else {\r
// If only one route found, or T1 == T2, split is not applicable\r
splitAmountOut = 0;\r
splitPercentA = 0;\r
}\r
}\r
\r
/**\r
* @notice Returns top quotes (1-hop & 2-hop), an optional optimal split of the two best routes,\r
* and wallet allowances/balances/decimals plus Chainlink USD prices\r
* @dev Scans all registered modules and tracks Top-1/Top-2 for both 1-hop and 2-hop\r
* Also tracks the two overall best routes and probes a split between them\r
* If no route is found, reverts with {NoRouteFound}\r
* For ETH inputs/outputs, balances/allowances refer to ETH and decimals=18\r
* @param tokenIn Input token\r
* @param tokenOut Output token\r
* @param amountIn Input amount\r
* @param wallet Wallet to read balances/allowances from\r
* @param spender Spender to check allowances against\r
* @param asEthIn If true, forces ETH/USD feed for tokenIn (WETH, stETH, etc.)\r
* @param asEthOut If true, forces ETH/USD feed for tokenOut (WETH, stETH, etc.)\r
* @return best1HopRouteTop1 Serialized payload of best 1-hop route\r
* @return amountOut1HopTop1 Quoted output of best 1-hop route\r
* @return best2HopRouteTop1 Serialized payload of best 2-hop route\r
* @return amountOut2HopTop1 Quoted output of best 2-hop route\r
* @return best1HopRouteTop2 Serialized payload of second-best 1-hop route\r
* @return amountOut1HopTop2 Quoted output of second-best 1-hop route\r
* @return best2HopRouteTop2 Serialized payload of second-best 2-hop route\r
* @return amountOut2HopTop2 Quoted output of second-best 2-hop route\r
* @return splitAmountOut Quote for the best split of the two overall leaders (0 if not improving)\r
* @return splitPercentA Percent for route A (0–100) in the split (0 if split not applicable)\r
*\r
* @return balanceIn {wallet} balance of tokenIn (ETH if tokenIn==address(0))\r
* @return allowance_In Allowance of tokenIn to {spender} (0 for ETH)\r
* @return decimals_In Decimals of tokenIn (18 for ETH; 0 if unreadable)\r
* @return balanceOut {wallet} balance of tokenOut (ETH if tokenOut==address(0))\r
* @return allowance_Out Allowance of tokenOut to {spender} (0 for ETH)\r
* @return decimals_Out Decimals of tokenOut (18 for ETH; 0 if unreadable)\r
*\r
* @return usdPerToken1e18In TokenIn USD price normalized to 1e18 (0 if no feed/<=0)\r
* @return updatedAtIn Timestamp of the last price update for tokenIn\r
* @return usdPerToken1e18Out TokenOut USD price normalized to 1e18 (0 if no feed/<=0)\r
* @return updatedAtOut Timestamp of the last price update for tokenOut\r
*/\r
function getBestRouteSuper(\r
address tokenIn,\r
address tokenOut,\r
uint256 amountIn,\r
address wallet,\r
address spender,\r
bool asEthIn,\r
bool asEthOut\r
) external view returns (\r
bytes memory best1HopRouteTop1, uint256 amountOut1HopTop1,\r
bytes memory best2HopRouteTop1, uint256 amountOut2HopTop1,\r
bytes memory best1HopRouteTop2, uint256 amountOut1HopTop2,\r
bytes memory best2HopRouteTop2, uint256 amountOut2HopTop2,\r
uint256 splitAmountOut, uint16 splitPercentA,\r
\r
uint256 balanceIn, uint256 allowance_In, uint8 decimals_In,\r
uint256 balanceOut, uint256 allowance_Out, uint8 decimals_Out,\r
\r
uint256 usdPerToken1e18In, uint256 updatedAtIn,\r
uint256 usdPerToken1e18Out, uint256 updatedAtOut,\r
uint256 ethBalance\r
) {\r
QuoteArgs memory qa = QuoteArgs({\r
tokenIn: tokenIn,\r
tokenOut: tokenOut,\r
amountIn: amountIn\r
});\r
\r
BestQuotes memory best;\r
TrackedRoute memory top1Overall; // Absolute best route\r
TrackedRoute memory top2Overall; // Second best route\r
\r
for (uint256 i = 0; i < modules.length; ) {\r
ModuleQuotes memory quotes = _getModuleQuotes(modules[i], i, qa);\r
\r
if (quotes.amountOut1Hop > 0) {\r
TrackedRoute memory r1 = TrackedRoute({\r
payload: quotes.payload1Hop,\r
amountOut: quotes.amountOut1Hop,\r
module: quotes.module,\r
moduleIndex: quotes.moduleIndex\r
});\r
_updateBestQuotes(best, r1, true);\r
(top1Overall, top2Overall) = _updateTopOverall(top1Overall, top2Overall, r1);\r
}\r
\r
if (quotes.amountOut2Hop > 0) {\r
TrackedRoute memory r2 = TrackedRoute({\r
payload: quotes.payload2Hop,\r
amountOut: quotes.amountOut2Hop,\r
module: quotes.module,\r
moduleIndex: quotes.moduleIndex\r
});\r
_updateBestQuotes(best, r2, false);\r
(top1Overall, top2Overall) = _updateTopOverall(top1Overall, top2Overall, r2);\r
}\r
\r
unchecked { ++i; }\r
}\r
\r
if (top1Overall.amountOut == 0) revert NoRouteFound();\r
\r
// Return the standard 8 fields\r
best1HopRouteTop1 = best.top1Hop.payload; amountOut1HopTop1 = best.top1Hop.amountOut;\r
best2HopRouteTop1 = best.top2Hop.payload; amountOut2HopTop1 = best.top2Hop.amountOut;\r
best1HopRouteTop2 = best.second1Hop.payload; amountOut1HopTop2 = best.second1Hop.amountOut;\r
best2HopRouteTop2 = best.second2Hop.payload; amountOut2HopTop2 = best.second2Hop.amountOut;\r
\r
// Compute split between the two overall best routes (T1 and T2)\r
if (top2Overall.amountOut > 0 && keccak256(top1Overall.payload) != keccak256(top2Overall.payload)) {\r
(splitAmountOut, splitPercentA) = findBestSplit(\r
top1Overall.payload,\r
top2Overall.payload\r
);\r
\r
// If split provides no improvement, do not return it,\r
// Since the best will be either T1 or T2 (T1.amountOut >= T2.amountOut)\r
if (splitAmountOut <= top1Overall.amountOut) {\r
splitAmountOut = 0;\r
splitPercentA = 0;\r
}\r
} else {\r
// If only one route found, or T1 == T2, split is not applicable\r
splitAmountOut = 0;\r
splitPercentA = 0;\r
}\r
\r
// Safely read balance/allowance (do not revert on non-standard ERC-20)\r
(balanceIn, allowance_In, decimals_In) = _safeBalanceAndAllowance(tokenIn, wallet, spender);\r
(balanceOut, allowance_Out, decimals_Out) = _safeBalanceAndAllowance(tokenOut, wallet, spender);\r
\r
// Let's try to get the price in USD (gently, without revert)\r
(usdPerToken1e18In, updatedAtIn) = _tryTokenUsdPrice1e18(tokenIn, asEthIn);\r
(usdPerToken1e18Out, updatedAtOut) = _tryTokenUsdPrice1e18(tokenOut, asEthOut);\r
\r
// Native ETH balance\r
ethBalance = wallet.balance;\r
}\r
\r
/**\r
* @notice Find the best split ratio between two route payloads\r
* @dev Discrete search by simulateRoute + local fine-tuning\r
* @param payloadA Route A\r
* @param payloadB Route B\r
* @return bestAmountOut Best to
Submitted on: 2025-11-06 13:06:28
Comments
Log in to comment.
No comments yet.