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
Submitted on: 2025-10-16 10:51:17
Comments
Log in to comment.
No comments yet.