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",
  "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 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 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 100 = 1%)\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
   \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 InvalidFee();\r
    error NoRouteFound();\r
    error SlippageExceeded(uint256 actual, uint256 expected);\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: 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: 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
        uint256 size;\r
        assembly { size := extcodesize(module) }\r
        if (size == 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 {\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 Quickly returns quotedOut from payload without full decoding.\r
     * @dev    Reads the third word (after module and index) from abi.encode(...).\r
     * @param  payload Full ABI payload of the route.\r
     * @return out_    quotedOut value.\r
     */\r
    function _extractQuotedOut(bytes memory payload) internal pure returns (uint256 out_) {\r
        assembly {\r
            // payload: [len][module][index][amountOut]...\r
            let data := add(payload, 32)\r
            out_ := mload(add(data, 64)) // 64 = 2 * 32\r
        }\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 Peeks common (tokenIn, tokenOut, amountIn) fields from an encoded hop blob.\r
     * @dev    Assumes each hop is ABI-encoded as: (address tokenIn, address tokenOut, ..., uint256 amountIn),\r
     *         so the first two words are addresses and the last word is amountIn.\r
     *         This is a generic helper and makes no assumptions about intermediate fields.\r
     * @param  hop ABI-encoded hop bytes.\r
     * @return tokenIn  First address word of the hop.\r
     * @return tokenOut Second address word of the hop.\r
     * @return amountIn Last 32-byte word of the hop (amountIn).\r
     */\r
    function _peekInOutAmt(bytes memory hop)\r
        internal\r
        pure\r
        returns (address tokenIn, address tokenOut, uint256 amountIn)\r
    {\r
        assembly {\r
            let p := add(hop, 32)             // pointer to data\r
            tokenIn  := shr(96, mload(p))     // first address\r
            tokenOut := shr(96, mload(add(p, 32))) // second address\r
            let len := mload(hop)\r
            amountIn := mload(add(p, sub(len, 32))) // last 32 bytes\r
        }\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
    /* ──────────────────────────────────── Read ───────────────────────────────── */\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 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 absolute best route and a list of all successful routes from the modules.\r
     * @param  tokenIn           Input token.\r
     * @param  tokenOut          Output token.\r
     * @param  amountIn          Input amount.\r
     * @return bestPayload       Payload of the best route.\r
     * @return bestAmountOut     Quote of the best route.\r
     * @return bestModule        Address of the winning module.\r
     * @return bestModuleIndex   Index of the module in the array.\r
     * @return routes            Array of all successful routes (payload + quote).\r
     */\r
    function getBestRouteAndAll(\r
        address tokenIn,\r
        address tokenOut,\r
        uint256 amountIn\r
    )\r
        external\r
        view\r
        returns (\r
            bytes memory,        // bestPayload (overall best)\r
            uint256,             // bestAmountOut\r
            address,             // bestModule\r
            uint256,             // bestModuleIndex\r
            RouteInfo[] memory   // routes (all successful routes, including the best one)\r
        )\r
    {\r
        // Maximum modules * 2 (1-hop + 2-hop) routes\r
        RouteInfo[] memory allRoutes = new RouteInfo[](modules.length * 2);\r
\r
        BestAgg memory best;\r
        QuoteArgs memory qa = QuoteArgs({tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn});\r
        uint256 k = 0; // Counter of successful routes\r
\r
        for (uint256 i = 0; i < modules.length; ) {\r
            ModuleQuotes memory quotes = _getModuleQuotes(modules[i], i, qa);\r
\r
            // 1-hop\r
            if (quotes.amountOut1Hop > 0) {\r
                allRoutes[k] = RouteInfo({\r
                    module: quotes.module,\r
                    index: i,\r
                    payload: quotes.payload1Hop,\r
                    amount: quotes.amountOut1Hop\r
                });\r
                if (quotes.amountOut1Hop > best.amount) {\r
                    best.amount  = quotes.amountOut1Hop;\r
                    best.payload = quotes.payload1Hop;\r
                    best.module  = quotes.module;\r
                    best.idx     = i;\r
                }\r
                unchecked { ++k; }\r
            }\r
            \r
            // 2-hop\r
            if (quotes.amountOut2Hop > 0) {\r
                allRoutes[k] = RouteInfo({\r
                    module: quotes.module,\r
                    index: i,\r
                    payload: quotes.payload2Hop,\r
                    amount: quotes.amountOut2Hop\r
                });\r
                if (quotes.amountOut2Hop > best.amount) {\r
                    best.amount  = quotes.amountOut2Hop;\r
                    best.payload = quotes.payload2Hop;\r
                    best.module  = quotes.module;\r
                    best.idx     = i;\r
                }\r
                unchecked { ++k; }\r
            }\r
\r
            unchecked { ++i; }\r
        }\r
\r
        if (k == 0) revert NoRouteFound();\r
\r
        assembly { mstore(allRoutes, k) } // Shrink array length to k\r
\r
        return (best.payload, best.amount, best.module, best.idx, allRoutes);\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 && percentA <= 99, "UR: percent out of bounds");\r
        require(isModule[A.module]);\r
        require(isModule[B.module]);\r
\r
        IERC20(A.tokenIn).safeTransferFrom(msg.sender, address(this), A.amountIn); // if A.amountIn equals B.amountIn\r
        \r
        _smartApprove(A.tokenIn, A.module, A.amountIn);\r
        _smartApprove(A.tokenIn, B.module, B.amountIn);\r
        \r
        // Perform swaps (call modules)\r
\r
        // Route A (percentA)\r
        // IDexModule.swapRoute passes a percentage (0-100), and the module\r
        // must internally calculate the exact amountIn for this part of the swap.\r
        uint256 outA = IDexModule(A.module).swapRoute(\r
            DexRoute({ data: A.route }), \r
            address(this), \r
            percentA\r
        );\r
\r
        // Route B (100 - percentA)\r
        uint256 outB = IDexModule(B.module).swapRoute(\r
            DexRoute({ data: B.route }), \r
            address(this), \r
            uint16(100 - percentA)\r
        );\r
\r
        // Slip check and return\r
        require((outA + outB) >= minAmountOut, "UR: slippage");\r
\r
        uint256 quotedTotal = (A.quoted * percentA) / 100 + (B.quoted * (uint16(100 - percentA))) / 100;\r
\r
        // Commission + payment to user\r
        netOut = _distributeTokenWithFees(A.tokenOut, to, outA + outB, quotedTotal, minAmountOut);\r
\r
        SplitResult memory r = SplitResult({\r
            moduleA: A.module,\r
            moduleB: B.module,\r
            tokenIn: A.tokenIn,\r
            tokenOut: A.tokenOut,\r
            totalIn: A.amountIn,\r
            amountInA: (A.amountIn * percentA) / 100,\r
            amountInB: (B.amountIn * (uint16(100 - percentA))) / 100,\r
            outA: outA,\r
            outB: outB,\r
            totalOut: outA + outB\r
        });\r
        _emitSwapSplit(r, msg.sender, to, percentA);\r
    }\r
\r
    /* ─────────────── SPLIT: ETH → Token ─────────────── */\r
\r
    /**\r
     * @notice Split-swap ETH→Token via two routes (both via WETH).\r
     * @dev    Converts ETH to WETH; splits input by percentage; holds fees; transfers net amount `to`.\r
     * @param  payloadA          Package A (tokenIn=WETH, amountIn=msg.value).\r
     * @param  payloadB          Package B.\r
     * @param  percentA          Share A (1–99).\r
     * @param  minTotalOut       Minimum total output.\r
     * @param  to                Recipient.\r
     * @return netOut            Net result (ERC20).\r
     */\r
    function swapSplitExactETHForTokens(\r
        bytes calldata payloadA,     // both: tokenIn == WETH, amountIn == msg.value\r
        bytes calldata payloadB,\r
        uint16 percentA,             // 1..99\r
        uint256 minTotalOut,\r
        address to\r
    ) external payable nonReentrant returns (uint256 netOut) {\r
        require(to != address(0), "UR: bad to");\r
        require(msg.value > 0, "UR: no ETH");\r
        require(percentA >= 1 && percentA <= 99, "UR: percent out of bounds");\r
\r
        _requireWethIn(payloadA);\r
        _requireWethIn(payloadB);\r
\r
        LegDecoded memory A = _decodeRouteStructCallData(payloadA);\r
        LegDecoded memory B = _decodeRouteStructCallData(payloadB);\r
        require(A.amountIn == B.amountIn, "UR: split amount mismatch");\r
        require(A.amountIn == msg.value, "UR: value != amountIn");\r
        require(A.tokenOut == B.tokenOut, "UR: out mismatch");\r
        require(A.route.length > 0 && B.route.length > 0, "UR: empty route");\r
        require(isModule[A.module]);\r
        require(isModule[B.module]);\r
\r
        _wrapETH(msg.value);\r
        _smartApprove(WETH, A.module, msg.value);\r
        _smartApprove(WETH, B.module, msg.value);\r
\r
        uint16 percentB = uint16(100 - percentA);\r
\r
        // Route execution → fees → recipient\r
        uint256 outA = IDexModule(A.module).swapRoute(DexRoute({data: A.route}), address(this), percentA);\r
        uint256 outB = IDexModule(B.module).swapRoute(DexRoute({data: B.route}), address(this), percentB);\r
\r
        uint256 grossOut = outA + outB;\r
        require(grossOut >= minTotalOut, "UR: slippage");\r
\r
        uint256 quotedTotal = (A.quoted * percentA) / 100 + (B.quoted * percentB) / 100;\r
\r
        netOut = _distributeTokenWithFees(A.tokenOut, to, grossOut, quotedTotal, minTotalOut);\r
\r
        SplitResult memory r = SplitResult({\r
            moduleA: A.module,\r
            moduleB: B.module,\r
            tokenIn: WETH,\r
            tokenOut: A.tokenOut,\r
            totalIn: msg.value,\r
            amountInA: (uint256(msg.value) * percentA) / 100,\r
            amountInB: (uint256(msg.value) * percentB) / 100,\r
            outA: outA,\r
            outB: outB,\r
            totalOut: grossOut\r
        });\r
        _emitSwapSplit(r, msg.sender, to, percentA);\r
    }\r
\r
    /* ─────────────── SPLIT: Token → ETH ─────────────── */\r
\r
    /**\r
     * @notice Split-swap Token→ETH via two routes (both ending in WETH).\r
     * @dev    Splits input by percentage; converts WETH→ETH; holds fees; transfers net amount `to`.\r
     * @param  payloadA          Package A (tokenOut=WETH).\r
     * @param  payloadB          Package B.\r
     * @param  percentA          Share A (1–99).\r
     * @param  minTotalEthOut    Minimum total output in ETH.\r
     * @param  to                ETH recipient.\r
     * @return netEthOut         Net result (ETH).\r
     */\r
    function swapSplitExactTokensForETH(\r
        bytes calldata payloadA,     // both: tokenOut == WETH, same amountIn\r
        bytes calldata payloadB,\r
        uint16 percentA,             // 1..99\r
        uint256 minTotalEthOut,\r
        address to\r
    ) external nonReentrant returns (uint256 netEthOut) {\r
        require(to != address(0), "UR: bad to");\r
        require(percentA >= 1 && percentA <= 99, "UR: percent out of bounds");\r
\r
        _requireWethOut(payloadA);\r
        _requireWethOut(payloadB);\r
\r
        LegDecoded memory A = _decodeRouteStructCallData(payloadA);\r
        LegDecoded memory B = _decodeRouteStructCallData(payloadB);\r
        require(A.amountIn > 0 && B.amountIn > 0, "UR: zero in");\r
        require(A.amountIn == B.amountIn, "UR: split amount mismatch");\r
        require(A.tokenIn == B.tokenIn, "UR: in mismatch");\r
        require(A.route.length > 0 && B.route.length > 0, "UR: empty route");\r
        require(isModule[A.module]);\r
        require(isModule[B.module]);\r
\r
        IERC20(A.tokenIn).safeTransferFrom(msg.sender, address(this), A.amountIn);\r
        _smartApprove(A.tokenIn, A.module, A.amountIn);\r
        _smartApprove(A.tokenIn, B.module, B.amountIn);\r
\r
        uint16 percentB = uint16(100 - percentA);\r
\r
        uint256 outA = IDexModule(A.module).swapRoute(DexRoute({data: A.route}), address(this), percentA);\r
        uint256 outB = IDexModule(B.module).swapRoute(DexRoute({data: B.route}), address(this), percentB);\r
\r
        uint256 totalWeth = outA + outB;\r
        require(totalWeth >= minTotalEthOut, "UR: slippage");\r
\r
        uint256 quotedTotal = (A.quoted * percentA) / 100 + (B.quoted * percentB) / 100;\r
\r
        _unwrapWETHAndSend(totalWeth, address(this));\r
        netEthOut = _distributeETHWithFees(to, totalWeth, quotedTotal, minTotalEthOut);\r
\r
        SplitResult memory r = SplitResult({\r
            moduleA: A.module,\r
            moduleB: B.module,\r
            tokenIn: A.tokenIn,\r
            tokenOut: WETH,\r
            totalIn: A.amountIn,\r
            amountInA: (A.amountIn * percentA) / 100,\r
            amountInB: (B.amountIn * percentB) / 100,\r
            outA: outA,\r
            outB: outB,\r
            totalOut: totalWeth\r
        });\r
        _emitSwapSplit(r, msg.sender, to, percentA);\r
    }\r
}"
    },
    "@openzeppelin/contracts/security/ReentrancyGuard.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol)

pragma solidity ^0.8.0;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, whic

Tags:
ERC20, ERC165, Multisig, Swap, Upgradeable, Multi-Signature, Factory|addr:0xd6d379bd4235aa6ab10af484116e911161184267|verified:true|block:23587521|tx:0xfe45b0705e79f548449ac0e431c8a3cfd546052f503c9f215a44c8d017d28bf5|first_check:1760604674

Submitted on: 2025-10-16 10:51:17

Comments

Log in to comment.

No comments yet.