UniversalRouter

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

Tags:
ERC20, ERC165, Multisig, Swap, Liquidity, Upgradeable, Multi-Signature, Factory, Oracle|addr:0x2f5a9b2b43578c8a53917dbdf16ffbbc2fe48d5f|verified:true|block:23739675|tx:0x78075b6941e2d5a18c87cdfb078f65d0a1cd851cc476bed60aa108983839a274|first_check:1762430788

Submitted on: 2025-11-06 13:06:28

Comments

Log in to comment.

No comments yet.