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 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
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
    // Store the registry address and maximum price “freshness”\r
    address public feedRegistry = 0x47Fb2585D2C56Fe188D0E6ec628a38b74fCeeeDf;\r
\r
    address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;\r
    address[] public modules;                                // list of modules (Curve, UniV2, Sushi, UniV3)\r
    mapping(address => bool)     public isModule;\r
    mapping(address => uint256)  private moduleIndexPlusOne; // 1-based for O(1) remove\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
    /**\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
    /* ───────────────────────────────── 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
        // (опционально) минимальная проверка кода\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
     * @dev    Reads the address from slot 3 of the payload ABI header (see _loadAddressFromPayload).\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 = _loadAddressFromPayload(payload, 3);\r
        require(tokenIn == WETH, "UR: payload tokenIn != WETH");\r
    }\r
\r
    /**\r
     * @notice Ensures that tokenOut == WETH in the output payload.\r
     * @dev    Reads the address from the ABI header payload in slot 4 (see _loadAddressFromPayload).\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 = _loadAddressFromPayload(payload, 4);\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 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 the balance and allowance of a token for a pair (wallet, spender).\r
     * @dev For address(0), we treat it as ETH: we return the ETH balance of the wallet and allowance = 0.\r
     * Uses low-level staticcall to handle non-standard ERC-20 tokens (e.g., USDT).\r
     */\r
    function _safeBalanceAndAllowance(\r
        address token,\r
        address wallet,\r
        address spender\r
    ) internal view returns (uint256 bal, uint256 allow_) {\r
        if (token == address(0)) {\r
            // ETH: allowance not applicable\r
            return (wallet.balance, 0);\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
\r
    /**\r
     * @notice Returns the price of 1 token in USD (normalized to 1e18) via Chainlink Feed Registry.\r
     * @dev    Does not check for data “freshness” and does not revert.\r
     *         ETH is passed as address(0) and mapped internally to Chainlink DENOM_ETH.\r
     * @param  token     Token address (or address(0) for ETH).\r
     * @param  asEth     If true, use the ETH/USD price (e.g., for WETH, stETH, etc.).\r
     * @return price1e18 The price of 1 token in USD (1e18).\r
     * @return updatedAt The time of the last feed update.\r
     */\r
    function _tryTokenUsdPrice1e18(address token, bool asEth)\r
        internal\r
        view\r
        returns (uint256 price1e18, uint256 updatedAt)\r
    {\r
        address reg = feedRegistry;\r
        if (reg == address(0)) return (0, 0);\r
\r
        address base = asEth\r
            ? DENOM_ETH\r
            : (token == address(0) ? DENOM_ETH : token);\r
\r
        // latestRoundData may throw an error, but as a rule, it works fine on mainnet for known tokens.\r
        ( , int256 answer, , uint256 upd, ) = IFeedRegistry(reg).latestRoundData(base, DENOM_USD);\r
        if (answer <= 0) return (0, upd);\r
\r
        uint8 pdec = IFeedRegistry(reg).decimals(base, DENOM_USD);\r
        uint256 u = uint256(answer);\r
\r
        // Normalization to 1e18\r
        if (pdec < 18)      price1e18 = u * 10 ** (18 - pdec);\r
        else if (pdec > 18) price1e18 = u / 10 ** (pdec - 18);\r
        else                price1e18 = u;\r
\r
        return (price1e18, upd);\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 the balance of `wallet` in `token` and the allowance of this token for `spender`.\r
     * @param  token           ERC-20 token address (or address(0) for ETH)\r
     * @param  wallet          address of the owner of the funds\r
     * @param  spender         address of the contract for which we are checking the allowance\r
     * @return balance         wallet balance in token (or ETH balance if token==address(0))\r
     * @return allowance_      current token allowance for spender\r
     */\r
    function balanceAndAllowanceOf(\r
        address token,\r
        address wallet,\r
        address spender\r
    ) external view returns (uint256 balance, uint256 allowance_) {\r
        (balance, allowance_) = _safeBalanceAndAllowance(token, wallet, spender);\r
    }\r
\r
    /**\r
     * @notice Returns (balance, allowance) and the price of 1 token in USD (1e18) without a validity flag.\r
     * @dev    ETH = address(0). The price is taken from Chainlink Feed Registry/_tryTokenUsdPrice1e18.\r
     * @param  token           ERC-20 token address (or address(0) for ETH)\r
     * @param  wallet          Address of the owner of the funds\r
     * @param  spender         Address of the contract for which we are checking the assignment\r
     * @param  asEth           If true, force the ETH/USD price (for WETH, stETH, etc.)\r
     * @return balance         Wallet balance in tokens (or ETH if token==address(0))\r
     * @return allowance_      Token allowance for spender (0 for ETH)\r
     * @return usdPerToken1e18 Price of 1 token in USD (1e18), 0 if no feed/answer<=0\r
     * @return updatedAt       Last price update in Chainlink\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
        )\r
    {\r
        // 1) safely read balance/allowance (do not revert to non-standard ERC-20)\r
        (balance, allowance_) = _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
\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 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
     * @param  wallet             address of the owner of the funds\r
     * @param  spender            address of the contract for which we are checking the allowance\r
     * @param  asEthIn            If true — use the ETH/USD price for tokenIn (ignoring its own feed)\r
     * @param  asEthOut           If true — use the ETH/USD price for tokenOut (ignoring its own feed)\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
     * @return balance            wallet balance in tokens (ETH, if token==address(0))\r
     * @return allowance_         token allowance for spender (0 for ETH)\r
     * @return usdPerToken1e18In  price of 1 token in USD, normalized to 1e18 (0 if hasUsd=false)\r
     * @return updatedAtIn        time of the last price update from Chainlink\r
     * @return usdPerToken1e18Out price of 1 token in USD, normalized to 1e18 (0 if hasUsd=false)\r
     * @return updatedAtOut time  of the last price update from Chainlink\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 balance,\r
        uint256 allowance_,\r
\r
        uint256 usdPerToken1e18In, uint256 updatedAtIn,\r
        uint256 usdPerToken1e18Out, uint256 updatedAtOut\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
        (balance, allowance_) = _safeBalanceAndAllowance(tokenIn, 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
\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 total quote.\r
     * @return bestPercentA      Share of A (0–100) giving the maximum.\r
     */\r
    function findBestSplit(\r
        bytes memory payloadA, // ИЗМЕНЕНИЕ: bytes memory\r
        bytes memory payloadB\r
    )\r
        internal \r
        view\r
        returns (\r
            uint256 bestAmountOut,\r
            uint16 bestPercentA\r
        )\r
    {\r
        // Decode and verify\r
        LegDecoded memory A = _decodeRouteStruct(payloadA);\r
        LegDecoded memory B = _decodeRouteStruct(payloadB);\r
\r
        require(A.amountIn > 0 && B.amountIn > 0, "UR: zero amounts");\r
        require(A.tokenIn == B.tokenIn, "UR: in mismatch");\r
        require(A.tokenOut == B.tokenOut, "UR: out mismatch");\r
        require(A.amountIn == B.amountIn, "UR: totalIn mismatch"); \r
\r
        address moduleA = A.module;\r
        address moduleB = B.module;\r
        \r
        // --- Step 1: Initialization (50%) ---\r
        uint16 initialPercent = 50; // 50%\r
        \r
        uint256 currentMaxOut = _calculateTotalOut(\r
            moduleA, A.route, moduleB, B.route, initialPercent\r
        );\r
        uint16 currentBestPercent = initialPercent;\r
\r
        // --- Step 2: Main sparse search: 10% to 90% in 10% increments ---\r
        // Check 10, 20, 30, 40, 60, 70, 80, 90. (50% already checked).\r
        for (uint16 percent = 10; percent <= 90; percent += 10) {\r
            if (percent == 50) continue; \r
\r
            uint256 totalOut = _calculateTotalOut(\r
                moduleA, A.route, moduleB, B.route, percent\r
            );\r
\r
            if (totalOut > currentMaxOut) {\r
                currentMaxOut = totalOut;\r
                currentBestPercent = percent;\r
            }\r
        }\r
        \r
        // --- Step 3: Refinement (Local search, +/- 5% step) ---\r
        uint16 bestPercentFound = currentBestPercent;\r
        \r
        // Array of offsets for refinement: [-5, +5] Percent\r
        int16[] memory offsets = new int16[](2);\r
        offsets[0] = -5; // Checking -5% from the best point\r
        offsets[1] = 5;  // Checking +5% from the best point\r
\r
        for (uint256 i = 0; i < offsets.length; ) {\r
            int16 offset = offsets[i];\r
            \r
            // Protection against values exceeding the limits (e.g., below 1% or above 99%)\r
            // Condition: bestPercentFound <= 5 (for -5) or bestPercentFound >= 95 (for +5)\r
            if (\r
                (offset < 0 && bestPercentFound <= uint16(-offset)) || \r
                (offset > 0 && bestPercentFound >= 100 - uint16(offset))\r
            ) {\r
                 unchecked { ++i; }\r
                 continue;\r
            }\r
            \r
            uint16 checkPercent;\r
            if (offset < 0) {\r
                checkPercent = bestPercentFound - uint16(-offset);\r
            } else {\r
                checkPercent = bestPercentFound + uint16(offset);\r
            }\r
            \r
            // Check that the point is within a reasonable range for swap [1, 99]\r
            if (checkPercent >= 1 && checkPercent <= 99) { \r
                uint256 totalOut = _calculateTotalOut(\r
                    moduleA, A.route, moduleB, B.route, checkPercent\r
                );\r
\r
                if (totalOut > currentMaxOut) {\r
                    currentMaxOut = totalOut;\r
                    currentBestPercent = checkPercent;\r
                }\r
            }\r
            unchecked { ++i; }\r
        }\r
        \r
        // 4. Return the result\r
        bestAmountOut = currentMaxOut;\r
        bestPercentA = currentBestPercent;\r
    }\r
\r
    /**\r
     * @notice Decodes the route payload.\r
     * @param  payload ABI-encoded packet.\r
     * @return module            Module address.\r
     * @return moduleIndex       Module index.\r
     * @return quotedOut         Output quote.\r
     * @return tokenIn           Input token.\r
     * @return tokenOut          Output token.\r
     * @return amountIn          Input amount.\r
     * @return routeData         Route byte hops.\r
     */\r
    function decodeRoute(bytes calldata payload)\r
        public\r
        pure\r
        returns (\r
            address module,\r
            uint256 moduleIndex,\r
            uint256 quotedOut,\r
            address tokenIn,\r
            address tokenOut,\r
            uint256 amountIn,\r
            bytes[] memory routeData\r
        )\r
    {\r
        (module, moduleIndex, quotedOut, tokenIn, tokenOut, amountIn, routeData) =\r
            abi.decode(payload, (address, uint256, uint256, address, address, uint256, bytes[]));\r
    }   \r
\r
    /* ──────────────────────────────────── Swap ───────────────────────────────── */\r
\r
    /* ─────────────── ROUTE: Token → Token ───────────── */\r
\r
    /**\r
     * @notice Execute a swap based on a pre-prepared payload.\r
     * @dev    Takes a commission from the swap and positive slippage; checks minAmountOut; transfers the net amount to `to`.\r
     * @param  payload           ABI-encoded route (see decodeRoute).\r
     * @param  to                Recipient of the final tokens.\r
     * @param  minAmountOut      Minimum allowable output.\r
     * @return netOut            Net amount after commissions are deducted.\r
     */\r
    function swapRoute(bytes calldata payload, address to, uint256 minAmountOut)\r
        external\r
        nonReentrant\r
        returns (uint256 netOut)\r
    {\r
        require(to != address(0), "UR: bad to");\r
\r
        (\r
            address module, , uint256 quotedOut,\r
            address tokenIn, address tokenOut, uint256 amountIn,\r
            bytes[] memory routeData\r
        ) = decodeRoute(payload);\r
\r
        require(isModule[module], "UR: unknown module");\r
        require(amountIn > 0, "UR: zero amountIn");\r
        require(routeData.length > 0, "UR: empty route");\r
\r
        IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);\r
        _smartApprove(tokenIn, module, amountIn);\r
\r
        uint256 amountOut = IDexModule(module).swapRoute(DexRoute({ data: routeData }), address(this), 100);\r
        require(amountOut >= minAmountOut, "UR: slippage");\r
\r
        // Pay the user minus the fee — and return immediately\r
        netOut = _distributeTokenWithFees(tokenOut, to, amountOut, quotedOut, minAmountOut);\r
\r
        emit SwapExecuted(module, msg.sender, to, tokenIn, tokenOut, amountIn, netOut, quotedOut);\r
    }\r
\r
    /* ─────────────── ROUTE: ETH → Token ─────────────── */\r
\r
    /**\r
     * @notice Swap ETH→Token by payload with WETH as tokenIn.\r
     * @dev    Wraps ETH in WETH, calls the module, holds commissions, sends net amount `to`.\r
     * @param  payload           Route packet (tokenIn=WETH, amountIn=msg.value).\r
     * @param  to                Recipient.\r
     * @param  minAmountOut      Minimum output.\r
     * @return netOut            Net amount (ERC20).\r
     */\r
    function swapRouteExactETHForTokens(\r
        bytes calldata payload,      // payload with tokenIn == WETH and amountIn == msg.value\r
        address to,\r
        uint256 minAmountOut\r
    ) external payable nonReentrant returns (uint256 netOut) {\r
        require(to != address(0), "UR: bad to");\r
        require(msg.value > 0, "UR: no ETH");\r
        _requireWethIn(payload);\r
\r
        (\r
            address module, , uint256 quotedOut,\r
            , address tokenOut, uint256 amountIn,\r
            bytes[] memory routeData\r
        ) = decodeRoute(payload);\r
\r
        require(isModule[module], "UR: unknown module");\r
        require(routeData.length > 0, "UR: empty route");\r
        require(amountIn == msg.value, "UR: value != amountIn");\r
\r
        _wrapETH(msg.value);                       // ETH -> WETH\r
        _smartApprove(WETH, module, msg.value);    // approve\r
\r
        // Send to router → calculate commission → pay customer\r
        uint256 amountOut = IDexModule(module).swapRoute(DexRoute({data: routeData}), address(this), 100);\r
        require(amountOut >= minAmountOut, "UR: slippage");\r
\r
        netOut = _distributeTokenWithFees(tokenOut, to, amountOut, quotedOut, minAmountOut);\r
\r
        emit SwapExecuted(module, msg.sender, to, WETH, tokenOut, amountIn, netOut, quotedOut);\r
    }\r
\r
    /* ─────────────── ROUTE: Token → ETH ─────────────── */\r
\r
    /**\r
     * @notice Swap Token→ETH by payload with WETH as tokenOut.\r
     * @dev    Calls the module before WETH, converts to ETH, holds commissions, sends net amount `to`.\r
     * @param  payload           Route package (tokenOut=WETH).\r
     * @param  to                ETH recipient.\r
     * @param  minAmountOut      Minimum output.\r
     * @return netEthOut         Net amount (ETH).\r
     */\r
    function swapRouteExactTokensForETH(\r
        bytes calldata payload,      // payload: tokenOut == WETH\r
        address to,\r
        uint256 minAmountOut\r
    ) external nonReentrant returns (uint256 netEthOut) {\r
        require(to != address(0), "UR: bad to");\r
        _requireWethOut(payload);\r
\r
        (\r
            address module, , uint256 quotedOut,\r
            address tokenIn, , uint256 amountIn,\r
            bytes[] memory routeData\r
        ) = decodeRoute(payload);\r
\r
        require(isModule[module], "UR: unknown module");\r
        require(amountIn > 0, "UR: zero in");\r
        require(routeData.length > 0, "UR: empty route");\r
\r
        IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);\r
        _smartApprove(tokenIn, module, amountIn);\r
\r
        uint256 outWeth = IDexModule(module).swapRoute(DexRoute({data: routeData}), address(this), 100);\r
        require(outWeth >= minAmountOut, "UR: slippage");\r
\r
        // Unwrap and distribute with fees\r
        _unwrapWETHAndSend(outWeth, address(this));\r
\r
        netEthOut = _distributeETHWithFees(to, outWeth, quotedOut, minAmountOut);\r
\r
        emit SwapExecuted(module, msg.sender, to, tokenIn, WETH, amountIn, netEthOut, quotedOut);\r
    }\r
\r
    /* ─────────────── SPLIT: Token → Token ───────────── */\r
\r
    /**\r
     * @notice Perform a split swap with two token→token routes.\r
     * @dev    Splits the input by `percentA`/`100-percentA`; checks minAmountOut; holds commissions; forwards the net amount.\r
     * @param  payloadA          Route A package.\r
     * @param  payloadB          Route B package.\r
     * @param  percentA          Share A (1–99).\r
     * @param  minAmountOut      Minimum total output.\r
     * @param  to                Recipient.\r
     * @return netOut            Net amount after fees.\r
     */\r
    function swapSplit(\r
        bytes calldata payloadA,\r
        bytes calldata payloadB,\r
        uint16 percentA,\r
        uint256 minAmountOut,\r
        address to\r
    ) external nonReentrant returns (uint256 netOut) {       \r
        // Decode and verify\r
        LegDecoded memory A = _decodeRouteStructCallData(payloadA);\r
        LegDecoded memory B = _decodeRouteStructCallData(payloadB);\r
        \r
        require(A.amountIn > 0 && B.amountIn > 0, "UR: zero amounts");\r
        require(A.tokenIn == B.tokenIn, "UR: in mismatch");\r
        require(A.tokenOut == B.tokenOut, "UR: out mismatch");\r
        require(A.amountIn == B.amountIn, "UR: totalIn mismatch");\r
        require(A.route.length > 0 && B.route.length > 0, "UR: empty route");\r
        require(percentA >= 1 && perce

Tags:
ERC20, ERC165, Multisig, Swap, Upgradeable, Multi-Signature, Factory, Oracle|addr:0xc01827b606b6d9843caa2919b51587eeb30c2e7b|verified:true|block:23595366|tx:0xdbec8c6a1159fc5547b8e056f042623e48b952a2d7ac317d4b99ace7be926c06|first_check:1760693032

Submitted on: 2025-10-17 11:23:52

Comments

Log in to comment.

No comments yet.