Contract Name:
UniversalRouter
Contract Source Code:
<i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
/**
* @title DSF UniversalRouter
* @author Andrei Averin — CTO dsf.finance
* @notice A universal router that combines DEX modules (Curve, UniswapV2, SushiSwap, UniswapV3, etc.)
* and performs single and split swaps with automatic commission withholding.
* @dev Router:
* - Requests the best quotes (1-hop and 2-hop) from all modules;
* - Selects the optimal route or split between the two best;
* - Pulls tokens from the user, approves modules, and executes the swap;
* - Charges a fee from the swap (feeBpsSwap) and from positive slippage (feeBpsPositive);
* - Supports ETH↔WETH, secure calls, and module list management.
*
* Uses low-level staticcall to IDexModule.getBestRoute(address,address,uint256)
* and a unified payload format:
* abi.encode(module,index,quotedOut,tokenIn,tokenOut,amountIn,bytes[] route).
*/
/* ─────────────────────────────── Interfaces / Types ─────────────────────────────── */
struct DexRoute { bytes[] data; }
struct Quote {
address pool;
int128 i;
int128 j;
bool useUnderlying;
uint256 amountOut;
}
struct BestAgg {
bytes payload;
uint256 amount;
address module;
uint256 idx;
}
struct RouteInfo {
address module;
uint256 index;
bytes payload; // the same format as in getBestRoute/decodeRoute
uint256 amount; // quotedOut
}
struct QuoteArgs {
address tokenIn;
address tokenOut;
uint256 amountIn;
}
struct LegDecoded {
address module;
uint256 index;
uint256 quoted; // quotedOut from payload
address tokenIn;
address tokenOut;
uint256 amountIn;
bytes[] route;
}
struct SplitResult {
address moduleA;
address moduleB;
address tokenIn;
address tokenOut;
uint256 totalIn;
uint256 amountInA;
uint256 amountInB;
uint256 outA;
uint256 outB;
uint256 totalOut;
}
struct TrackedRoute {
bytes payload;
uint256 amountOut;
address module;
uint256 moduleIndex;
}
struct BestQuotes {
TrackedRoute top1Hop;
TrackedRoute second1Hop;
TrackedRoute top2Hop;
TrackedRoute second2Hop;
}
struct ModuleQuotes {
address module;
uint256 moduleIndex;
bytes payload1Hop;
uint256 amountOut1Hop;
bytes payload2Hop;
uint256 amountOut2Hop;
}
interface IFeedRegistry {
function latestRoundData(address base, address quote)
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
function decimals(address base, address quote) external view returns (uint8);
}
interface IDexModule {
/**
* @notice Compute the best 1-hop and 2-hop routes.
* @param tokenIn Input token
* @param tokenOut Output token
* @param amountIn Input amount
* @return best1HopRoute Serialized 1-hop route
* @return amountOut1Hop Quoted 1-hop output
* @return best2HopRoute Serialized 2-hop route
* @return amountOut2Hop Quoted 2-hop output
*/
function getBestRoute(
address tokenIn,
address tokenOut,
uint256 amountIn
) external view returns (
DexRoute memory best1HopRoute,
uint256 amountOut1Hop,
DexRoute memory best2HopRoute,
uint256 amountOut2Hop
);
/**
* @notice Execute a previously returned route with a slippage check based on a percentage.
* @param route Serialized route
* @param to Recipient of the final tokens
* @param percent Percentage (0-100) of amountIn from the route to be swapped. 100 = 100%.
* @return amountOut Actual output received
*/
function swapRoute(
DexRoute calldata route,
address to,
uint256 percent
) external returns (uint256 amountOut);
/**
* @notice Simulate a route (1–2 hops) encoded as {DexRoute}.
* @param route Serialized route
* @param percent Percentage (0-100)
* @return amountOut Quoted total output amount
*/
function simulateRoute(
DexRoute calldata route,
uint256 percent
) external view returns (uint256 amountOut);
}
interface IWETH {
function deposit() external payable;
function withdraw(uint256 amount) external;
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address) external view returns (uint256);
}
contract UniversalRouter is Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
/* ─────────────────────────────── Storage ─────────────────────────────── */
address constant DENOM_ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
address constant DENOM_USD = 0x0000000000000000000000000000000000000348;
// Chainlink Feed Registry (mainnet)
address public feedRegistry = 0x47Fb2585D2C56Fe188D0E6ec628a38b74fCeeeDf;
address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address[] public modules; // list of modules (Curve, UniV2, Sushi, UniV3)
mapping(address => bool) public isModule;
mapping(address => uint256) private moduleIndexPlusOne; // 1-based for O(1) remove
/* ───────────────────────────── Fees config ───────────────────────────── */
address public feeRecipient; // commission recipient address
uint16 public feeBpsSwap; // commission in bps (max 10000 = 100%)
uint16 public feeBpsPositive; // commission with positive slippage, bps (max 10000 = 100%)
/* ────────────────────────────── Fees caps ────────────────────────────── */
uint16 public constant MAX_FEE_SWAP_BPS = 100; // 1%
uint16 public constant MAX_FEE_POSITIVE_BPS = 10_000; // 100%
/* ─────────────────────────────── Events ──────────────────────────────── */
event ModuleAdded(address indexed module);
event ModuleRemoved(address indexed module);
event ModulesReset(uint256 newCount);
event FeeConfigUpdated(address indexed recipient, uint16 bpsSwap, uint16 bpsPositive);
event ERC20Recovered(address indexed token, address indexed to, uint256 amount);
event ETHSwept(address indexed to, uint256 amount);
/**
* @notice Execution of a single swap.
* @param module Module that executed the route.
* @param user Initiator (msg.sender).
* @param to Recipient of the final funds.
* @param tokenIn Input token.
* @param tokenOut Output token.
* @param amountIn Input amount (withdrawn from the user).
* @param amountOut Final amount after fees (net).
* @param quotedOut Expected output (quota from payload).
*/
event SwapExecuted(
address indexed module,
address indexed user,
address indexed to,
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 amountOut,
uint256 quotedOut
);
/**
* @notice Execution of a split swap via two routes.
* @param user Initiator (msg.sender).
* @param to Recipient of the final funds.
* @param moduleA Module A.
* @param moduleB Module B.
* @param tokenIn Input token (or WETH for ETH route).
* @param tokenOut Output token.
* @param totalIn Total input.
* @param totalOut Total output (after fees — if the event is emitted after distribution).
* @param bpsA Share A in percent (0–100).
*/
event SwapSplitExecuted(
address indexed user,
address indexed to,
address moduleA,
address moduleB,
address tokenIn,
address tokenOut,
uint256 totalIn,
uint256 totalOut,
uint16 bpsA
);
/* ─────────────────────────────── Errors ─────────────────────────────── */
error ZeroAddress();
error DuplicateModule();
error NotAModule();
error NoRouteFound();
/* ─────────────────────────────── Modifiers ─────────────────────────────── */
modifier onlyERC20(address token) {
require(token != address(0) && token.code.length > 0, "not ERC20");
_;
}
/* ──────────────────────────────── receive ───────────────────────────────── */
/// @notice Needed to get native ETH (e.g., with IWETH.withdraw()).
receive() external payable {}
/* ─────────────────────────────── Constructor ─────────────────────────────── */
/**
* @notice Deploys the router and configures modules and commission parameters.
* @param _modules List of module addresses (Curve/UniV2/UniV3/…).
* @param _feeRecipient Address of the commission recipient.
* @param _feeBpsSwap Swap fee, bps (max. limited by require inside).
* @param _feeBpsPositive Positive slippage fee, bps (max. limited by require inside).
*/
constructor(
address[] memory _modules,
address _feeRecipient,
uint16 _feeBpsSwap,
uint16 _feeBpsPositive
) Ownable(msg.sender) {
_setModules(_modules);
require(_feeBpsSwap <= MAX_FEE_SWAP_BPS, "UR: swap fee too high");
require(_feeBpsPositive <= MAX_FEE_POSITIVE_BPS, "UR: pos fee too high");
feeRecipient = _feeRecipient;
feeBpsSwap = _feeBpsSwap;
feeBpsPositive = _feeBpsPositive;
emit FeeConfigUpdated(feeRecipient, feeBpsSwap, feeBpsPositive);
}
/* ────────────────────────────── Admin: Registry ──────────────────────────── */
function setFeedRegistry(address reg) external onlyOwner {
require(reg != address(0), "UR: bad registry");
require(reg.code.length > 0, "UR: registry not a contract");
feedRegistry = reg;
}
/* ──────────────────────────── Admin: modules mgmt ────────────────────────── */
/**
* @notice Complete reset of the module list.
* @dev Clears old ones, adds new ones, emits ModulesReset.
* @param _modules New list of modules.
*/
function setModules(address[] calldata _modules) external onlyOwner {
_clearModules();
_addModules(_modules);
emit ModulesReset(_modules.length);
}
/**
* @notice Add module to allowlist.
* @param module Address of IDexModule module.
*/
function addModule(address module) external onlyOwner {
_addModule(module);
}
/**
* @notice Remove module from allowlist.
* @param module Address of IDexModule module.
*/
function removeModule(address module) external onlyOwner {
_removeModule(module);
}
/**
* @notice Returns the number of connected modules.
* @return The length of the modules array.
*/
function modulesLength() external view returns (uint256) {
return modules.length;
}
/**
* @notice Returns the current list of modules.
* @dev The array is returned in memory (a copy of the state).
* @return An array of module addresses.
*/
function getModules() external view returns (address[] memory) {
return modules;
}
/* ────────────────────────────────── Admin ────────────────────────────────── */
/**
* @notice Rescue all stuck ERC-20 tokens to `to` or owner.
* @dev Owner-only. Uses SafeERC20. Always sends the entire token balance
* held by this contract. If `to` is zero, defaults to owner().
* @param token ERC-20 token address to rescue (must be non-zero).
* @param to Recipient; if zero address, defaults to owner().
*/
function recoverERC20(address token, address to)
external
onlyOwner
nonReentrant
{
require(token != address(0), "UR: token=0");
address recipient = (to == address(0)) ? owner() : to;
uint256 amt = IERC20(token).balanceOf(address(this));
if (amt == 0) return; // nothing to do
IERC20(token).safeTransfer(recipient, amt);
emit ERC20Recovered(token, recipient, amt);
}
/**
* @notice Transfers all remaining ETH from the contract to the owner or specified address.
* @dev Only for the owner (onlyOwner). Added nonReentrant to protect against repeated calls.
* If the `to` parameter is equal to a zero address, the funds are sent to the contract owner.
* @param to The address of the ETH recipient (if 0x0 — send to the owner).
*/
function sweepETH(address to)
external
onlyOwner
nonReentrant
{
address recipient = (to == address(0)) ? owner() : to;
uint256 bal = address(this).balance;
(bool ok,) = recipient.call{value: bal}("");
require(ok, "ETH sweep failed");
emit ETHSwept(recipient, bal);
}
/* ───────────────────────────────── Admin: fee ────────────────────────────── */
/**
* @notice Update the address of the commission recipient.
* @param _recipient New address of the fee recipient.
*/
function setFeeRecipient(address _recipient) external onlyOwner {
feeRecipient = _recipient;
emit FeeConfigUpdated(feeRecipient, feeBpsSwap, feeBpsPositive);
}
/**
* @notice Update commission percentages.
* @dev Upper limit checked via require; emits FeeConfigUpdated.
* @param _feeBpsSwap Swap commission, bps.
* @param _feeBpsPositive Positive slippage commission, bps.
*/
function setFeePercents(uint16 _feeBpsSwap, uint16 _feeBpsPositive) external onlyOwner {
require(_feeBpsSwap <= MAX_FEE_SWAP_BPS, "UR: swap fee too high");
require(_feeBpsPositive <= MAX_FEE_POSITIVE_BPS, "UR: pos fee too high");
feeBpsSwap = _feeBpsSwap;
feeBpsPositive = _feeBpsPositive;
emit FeeConfigUpdated(feeRecipient, feeBpsSwap, feeBpsPositive);
}
/**
* @notice Completely reinstalls the list of modules.
* @dev Clears the current modules, then adds new ones. Emits ModulesReset.
* @param _modules New list of modules.
*/
function _setModules(address[] memory _modules) internal {
_clearModules();
uint256 n = _modules.length;
for (uint256 i; i < n; ) {
_addModule(_modules[i]);
unchecked { ++i; }
}
emit ModulesReset(n);
}
/**
* @notice Resets (clears) all modules.
* @dev Resets isModule and indexes; clears the modules array.
*/
function _clearModules() internal {
uint256 n = modules.length;
for (uint256 i; i < n; ) {
address m = modules[i];
isModule[m] = false;
moduleIndexPlusOne[m] = 0;
unchecked { ++i; }
}
delete modules;
}
/**
* @notice Adds modules in bulk.
* @dev Calls _addModule for each address.
* @param _modules List of module addresses.
*/
function _addModules(address[] calldata _modules) internal {
uint256 n = _modules.length;
for (uint256 i; i < n; ) {
_addModule(_modules[i]);
unchecked { ++i; }
}
}
/**
* @notice Adds one module to the allowlist.
* @dev Checks for a non-zero address, the presence of code, and the absence of duplicates.
* Updates isModule, modules, and moduleIndexPlusOne. Emits ModuleAdded.
* @param module The module contract address.
*/
function _addModule(address module) internal {
if (module == address(0)) revert ZeroAddress();
if (isModule[module]) revert DuplicateModule();
// (опционально) минимальная проверка кода
if (module.code.length == 0) revert ZeroAddress();
isModule[module] = true;
modules.push(module);
moduleIndexPlusOne[module] = modules.length; // 1-based
emit ModuleAdded(module);
}
/**
* @notice Removes a module from the allowlist.
* @dev Performs O(1) removal via swap-pop, supporting 1-based indexing.
* Emit ModuleRemoved.
* @param module Address of the module to be removed.
*/
function _removeModule(address module) internal {
uint256 idxPlusOne = moduleIndexPlusOne[module];
if (idxPlusOne == 0) revert NotAModule();
uint256 idx = idxPlusOne - 1;
uint256 lastIdx = modules.length - 1;
if (idx != lastIdx) {
address last = modules[lastIdx];
modules[idx] = last;
moduleIndexPlusOne[last] = idx + 1;
}
modules.pop();
isModule[module] = false;
moduleIndexPlusOne[module] = 0;
emit ModuleRemoved(module);
}
/* ─────────────────────────────── WETH Helpers ────────────────────────────── */
/**
* @dev Wraps incoming native ETH into WETH.
* @param amount Amount of ETH to wrap (msg.value).
*/
function _wrapETH(uint256 amount) internal {
IWETH(WETH).deposit{value: amount}();
}
/**
* @dev Converts WETH back to ETH and sends it to the recipient.
* @param amount Amount of WETH to convert.
* @param to Recipient's native ETH address.
*/
function _unwrapWETHAndSend(uint256 amount, address to) internal {
require(IWETH(WETH).balanceOf(address(this)) >= amount, "UR: insufficient WETH");
IWETH(WETH).withdraw(amount);
// Send native ETH
(bool success,) = to.call{value: amount}("");
require(success, "UR: ETH transfer failed");
}
/* ───────────────────────────── ETH payout/guards ─────────────────────────── */
/**
* @notice Ensures that tokenIn == WETH in the input payload.
* @dev Reads the address from slot 3 of the payload ABI header (see _loadAddressFromPayload).
* @param payload ABI-encoded route: (module,index,quotedOut,tokenIn,tokenOut,amountIn,bytes[]).
*/
function _requireWethIn(bytes calldata payload) internal pure {
address tokenIn = _loadAddressFromPayload(payload, 3);
require(tokenIn == WETH, "UR: payload tokenIn != WETH");
}
/**
* @notice Ensures that tokenOut == WETH in the output payload.
* @dev Reads the address from the ABI header payload in slot 4 (see _loadAddressFromPayload).
* @param payload ABI-encoded route: (module,index,quotedOut,tokenIn,tokenOut,amountIn,bytes[]).
*/
function _requireWethOut(bytes calldata payload) internal pure {
address tokenOut = _loadAddressFromPayload(payload, 4);
require(tokenOut == WETH, "UR: payload tokenOut != WETH");
}
/**
* @notice Quick reading of the address from the ABI header payload.
* @dev The slot corresponds to the position of a 32-byte word in abi.encode(...).
* 0: module, 1: index, 2: quotedOut, 3: tokenIn, 4: tokenOut, 5: amountIn, 6: offset(bytes[]).
* @param payload Full ABI payload.
* @param slot Slot number (0-based).
* @return a Address read from the specified slot.
*/
function _loadAddressFromPayload(bytes calldata payload, uint256 slot) internal pure returns (address a) {
assembly ("memory-safe") {
a := shr(96, calldataload(add(payload.offset, mul(slot, 32))))
}
}
/* ────────────────────────────────── Helpers ──────────────────────────────── */
/**
* @notice Updates the best quotes in 1-hop and 2-hop segments.
* @dev Supports “top-1” and “top-2” for each category.
* @param currentBest Current best routes.
* @param newRoute Candidate for inclusion.
* @param is1Hop 1-hop (true) or 2-hop (false) flag.
*/
function _updateBestQuotes(BestQuotes memory currentBest, TrackedRoute memory newRoute, bool is1Hop) private pure {
if (is1Hop) {
if (newRoute.amountOut > currentBest.top1Hop.amountOut) {
currentBest.second1Hop = currentBest.top1Hop;
currentBest.top1Hop = newRoute;
} else if (newRoute.amountOut > currentBest.second1Hop.amountOut) {
currentBest.second1Hop = newRoute;
}
} else { // 2-hop
if (newRoute.amountOut > currentBest.top2Hop.amountOut) {
currentBest.second2Hop = currentBest.top2Hop;
currentBest.top2Hop = newRoute;
} else if (newRoute.amountOut > currentBest.second2Hop.amountOut) {
currentBest.second2Hop = newRoute;
}
}
}
/**
* @notice Updates the two absolute best routes found so far (overall Top-1 and Top-2).
* @dev If the new route beats Top-1, it becomes Top-1 and the old Top-1 shifts to Top-2.
* Otherwise, if it only beats Top-2, it replaces Top-2.
* @param top1 Current absolute best route (Top-1).
* @param top2 Current second absolute best route (Top-2).
* @param newRoute Newly observed candidate route to compare against the tops.
* @return Updated Top-1 and Top-2 routes (in this order).
*/
function _updateTopOverall(
TrackedRoute memory top1,
TrackedRoute memory top2,
TrackedRoute memory newRoute
) private pure returns (TrackedRoute memory, TrackedRoute memory) {
if (newRoute.amountOut > top1.amountOut) {
top2 = top1;
top1 = newRoute;
} else if (newRoute.amountOut > top2.amountOut) {
top2 = newRoute;
}
return (top1, top2);
}
/**
* @notice Queries a module for the best 1-hop and 2-hop quotes and packages them as payloads.
* @dev Calls IDexModule.getBestRoute via staticcall and, if non-zero quotes are returned,
* encodes payloads as abi.encode(module, index, quotedOut, tokenIn, tokenOut, amountIn, route.data).
* If the module is not registered or the call fails/returns empty, the struct remains zeroed.
* @param m Module address being queried.
* @param idx Module index (stored for payload bookkeeping).
* @param a Quote arguments (tokenIn, tokenOut, amountIn).
* @return quotes Struct holding module info, 1-hop/2-hop amounts and payloads (if any).
*/
function _getModuleQuotes(
address m,
uint256 idx,
QuoteArgs memory a
) internal view returns (ModuleQuotes memory quotes) {
quotes.module = m;
quotes.moduleIndex = idx;
if (!isModule[m]) return quotes;
bytes memory cd = abi.encodeWithSelector(
IDexModule.getBestRoute.selector,
a.tokenIn,
a.tokenOut,
a.amountIn
);
(bool success, bytes memory ret) = m.staticcall(cd);
if (!success || ret.length == 0) return quotes;
(
DexRoute memory route1, uint256 out1,
DexRoute memory route2, uint256 out2
) = abi.decode(ret, (DexRoute, uint256, DexRoute, uint256));
// Build payloads only for non-zero, non-empty routes.
if (out1 > 0 && route1.data.length > 0) {
quotes.amountOut1Hop = out1;
quotes.payload1Hop = abi.encode(
m, idx, out1, a.tokenIn, a.tokenOut, a.amountIn, route1.data
);
}
if (out2 > 0 && route2.data.length > 0) {
quotes.amountOut2Hop = out2;
quotes.payload2Hop = abi.encode(
m, idx, out2, a.tokenIn, a.tokenOut, a.amountIn, route2.data
);
}
}
/**
* @dev Private helper function for calculating the total output amount.
* @param percentA Percentage of amountIn for Route A (0-100).
*/
function _calculateTotalOut(
address moduleA,
bytes[] memory routeA,
address moduleB,
bytes[] memory routeB,
uint16 percentA // 0-100
) internal view returns (uint256 totalOut) {
uint16 percentB = 100 - percentA;
// simulateRoute for A (percent 0–100)
uint256 outA = IDexModule(moduleA).simulateRoute(DexRoute({ data: routeA }), percentA);
// simulateRoute for B (percent 0–100)
uint256 outB = IDexModule(moduleB).simulateRoute(DexRoute({ data: routeB }), percentB);
return outA + outB;
}
/**
* @notice Safely sets the allowance to the required minimum.
* @dev If the current allowance < amount, first set it to zero (if >0), then set it to type(uint256).max.
* Uses SafeERC20.forceApprove for maximum compatibility.
* @param token ERC20 token address.
* @param spender Contract address to which we issue the allowance.
* @param amount Minimum required limit.
*/
function _smartApprove(address token, address spender, uint256 amount) internal {
uint256 cur = IERC20(token).allowance(address(this), spender);
if (cur < amount) {
if (cur > 0) IERC20(token).forceApprove(spender, 0);
IERC20(token).forceApprove(spender, type(uint256).max);
}
}
/**
* @notice Emits the consolidated split-swap execution event.
* @dev Packs the essential split data into a single event for off-chain indexing/analytics.
* @param r Split result struct (modules, tokens, totals).
* @param user Original caller (initiator).
* @param to Final receiver of the swapped tokens/ETH.
* @param bpsA Portion routed through module A, in percent (0–100).
*/
function _emitSwapSplit(
SplitResult memory r,
address user,
address to,
uint16 bpsA
) internal {
emit SwapSplitExecuted(
user,
to,
r.moduleA,
r.moduleB,
r.tokenIn,
r.tokenOut,
r.totalIn,
r.totalOut,
bpsA
);
}
/**
* @notice Decodes a route payload (in memory) into a typed struct used by the router.
* @dev Expects payload encoded as:
* (address module, uint256 index, uint256 quoted, address tokenIn, address tokenOut, uint256 amountIn, bytes[] route)
* @param payload ABI-encoded payload stored in memory.
* @return d Decoded LegDecoded struct.
*/
function _decodeRouteStruct(bytes memory payload)
internal
pure
returns (LegDecoded memory d)
{
(d.module, d.index, d.quoted, d.tokenIn, d.tokenOut, d.amountIn, d.route) =
abi.decode(payload, (address, uint256, uint256, address, address, uint256, bytes[]));
}
/**
* @notice Decodes a route payload (in calldata) into a typed struct used by the router.
* @dev Same layout as the memory version, but reads directly from calldata to save gas.
* @param payload ABI-encoded payload residing in calldata.
* @return d Decoded LegDecoded struct.
*/
function _decodeRouteStructCallData(bytes calldata payload)
internal
pure
returns (LegDecoded memory d)
{
(d.module, d.index, d.quoted, d.tokenIn, d.tokenOut, d.amountIn, d.route) =
abi.decode(payload, (address, uint256, uint256, address, address, uint256, bytes[]));
}
/**
* @notice Distribution of commissions and ERC20 transfer.
* @dev Retains fix-fee (feeBpsSwap) and % of positive slippage (feeBpsPositive).
* @param token ERC20 address.
* @param to Recipient.
* @param grossOut Actual output after swap(s).
* @param quotedOut Quote (expectation).
* @param minOut Minimum acceptable output.
* @return netOut Amount after commissions.
*/
function _distributeTokenWithFees(
address token,
address to,
uint256 grossOut, // actual output after swap(s)
uint256 quotedOut, // quoted (expected) output
uint256 minOut
) internal returns (uint256 netOut) {
if (grossOut == 0) return 0;
uint256 baseline = quotedOut > minOut ? quotedOut : minOut;
uint256 feeSwap = 0;
uint256 feePos = 0;
// take fees only if recipient is set and bps > 0
if (feeRecipient != address(0)) {
if (feeBpsSwap > 0) {
unchecked { feeSwap = (grossOut * feeBpsSwap) / 10_000; }
}
if (feeBpsPositive > 0 && grossOut > baseline) {
unchecked { feePos = ((grossOut - baseline) * feeBpsPositive) / 10_000; }
}
}
uint256 totalFee = feeSwap + feePos;
// safety guard against overflow/rounding:
if (totalFee > grossOut) totalFee = grossOut;
netOut = grossOut - totalFee;
// Payouts: send fee to feeRecipient first, then net to user
if (totalFee > 0) {
IERC20(token).safeTransfer(feeRecipient, totalFee);
}
IERC20(token).safeTransfer(to, netOut);
}
/**
* @notice Distribution of fees and transfer of ETH.
* @dev Similar to _distributeTokenWithFees, but for ETH.
* @param to Recipient.
* @param grossEth Actual ETH output.
* @param quotedOutEth Expected output.
* @param minOutEth Minimum allowable output.
* @return netOut Amount after fees.
*/
function _distributeETHWithFees(
address to,
uint256 grossEth, // actual ETH output
uint256 quotedOutEth, // expected output (WETH==ETH)
uint256 minOutEth
) internal returns (uint256 netOut) {
if (grossEth == 0) return 0;
uint256 baseline = quotedOutEth > minOutEth ? quotedOutEth : minOutEth;
uint256 feeSwap = 0;
uint256 feePos = 0;
if (feeRecipient != address(0)) {
if (feeBpsSwap > 0) {
unchecked { feeSwap = (grossEth * feeBpsSwap) / 10_000; }
}
if (feeBpsPositive > 0 && grossEth > baseline) {
unchecked { feePos = ((grossEth - baseline) * feeBpsPositive) / 10_000; }
}
}
uint256 totalFee = feeSwap + feePos;
if (totalFee > grossEth) totalFee = grossEth;
netOut = grossEth - totalFee;
if (totalFee > 0) {
(bool fs, ) = feeRecipient.call{value: totalFee}("");
require(fs, "fee ETH xfer failed");
}
(bool ok, ) = to.call{value: netOut}("");
require(ok, "ETH xfer failed");
}
/**
* @notice Safely reads balance, allowance, and decimals for a (token, wallet, spender).
* @dev For ETH (token==address(0)): returns (wallet.balance, 0, 18).
* Uses low-level staticcall to tolerate non-standard ERC-20s (e.g., USDT).
* If {decimals()} cannot be read, returns 0 as a sentinel value.
* @param token ERC-20 token address (or address(0) for ETH).
* @param wallet Address whose balance is queried.
* @param spender Address whose allowance is checked.
* @return bal Token balance of {wallet} (or ETH balance if token==address(0)).
* @return allow_ Current allowance from {wallet} to {spender} (0 for ETH).
* @return decs Token decimals (18 for ETH; 0 if unreadable).
*/
function _safeBalanceAndAllowance(
address token,
address wallet,
address spender
) internal view returns (uint256 bal, uint256 allow_, uint8 decs) {
if (token == address(0)) {
// ETH: allowance not applicable
return (wallet.balance, 0, 18);
}
// balanceOf(wallet)
(bool ok1, bytes memory data1) =
token.staticcall(abi.encodeWithSelector(IERC20.balanceOf.selector, wallet));
if (ok1 && data1.length >= 32) {
bal = abi.decode(data1, (uint256));
} else {
bal = 0;
}
// allowance(wallet, spender)
(bool ok2, bytes memory data2) =
token.staticcall(abi.encodeWithSelector(IERC20.allowance.selector, wallet, spender));
if (ok2 && data2.length >= 32) {
allow_ = abi.decode(data2, (uint256));
} else {
allow_ = 0;
}
// decimals() selector = 0x313ce567
(bool ok3, bytes memory data3) =
token.staticcall(abi.encodeWithSelector(bytes4(0x313ce567)));
if (ok3 && data3.length >= 32) {
// Some tokens return uint256, some return uint8. Read as uint256 and cast safely.
uint256 d = abi.decode(data3, (uint256));
decs = d > 255 ? uint8(255) : uint8(d);
} else {
// if reading failed — 0, so that the caller understands that it is undefined
decs = 0;
}
}
/**
* @notice Reads the base/quote price from the Chainlink Feed Registry and normalizes it to 1e18.
* @dev Never reverts. Returns (price1e18=0, updatedAt=0) if the feed is missing or invalid.
* No freshness check is performed here; callers may validate staleness separately.
* @param base Address of the base asset (token address or Chainlink denomination, e.g. DENOM_ETH).
* @param quote Address of the quote asset (typically DENOM_USD).
* @return price1e18 Base/quote price scaled to 1e18 (0 if unavailable).
* @return updatedAt Timestamp of the last feed update (0 if unavailable).
*/
function _pairPrice1e18(address base, address quote)
private
view
returns (uint256 price1e18, uint256 updatedAt)
{
address reg = feedRegistry;
if (reg == address(0)) return (0, 0);
try IFeedRegistry(reg).latestRoundData(base, quote) returns (
uint80, int256 answer, uint256, uint256 upd, uint80
) {
if (answer <= 0) return (0, upd);
uint8 dec = IFeedRegistry(reg).decimals(base, quote);
uint256 u = uint256(answer);
if (dec < 18) price1e18 = u * 10 ** (18 - dec);
else if (dec > 18) price1e18 = u / 10 ** (dec - 18);
else price1e18 = u;
return (price1e18, upd);
} catch {
return (0, 0);
}
}
/**
* @notice Returns the price of 1 token in USD (1e18) via Chainlink Feed Registry.
* @dev Never reverts. **Does not** check data freshness; returns whatever the registry holds.
* Resolution order:
* - If `asEth == true` or `token == address(0)`: use ETH/USD;
* - Else try direct TOKEN/USD;
* - If `token == WETH`: use ETH/USD;
* - Else try TOKEN/ETH × ETH/USD.
* @param token Token address (or address(0) for ETH).
* @param asEth If true, force using the ETH/USD price (e.g., for WETH, stETH, etc.).
* @return price1e18 The price of 1 token in USD (1e18), or 0 if unavailable.
* @return updatedAt The timestamp of the last feed update used for the computation.
*/
function _tryTokenUsdPrice1e18(address token, bool asEth)
internal
view
returns (uint256 price1e18, uint256 updatedAt)
{
// ETH or forcibly as ETH
if (asEth || token == address(0)) {
(price1e18, updatedAt) = _pairPrice1e18(DENOM_ETH, DENOM_USD);
if (price1e18 != 0) return (price1e18, updatedAt);
}
// Direct TOKEN/USD
(price1e18, updatedAt) = _pairPrice1e18(token, DENOM_USD);
if (price1e18 != 0) return (price1e18, updatedAt);
// WETH → ETH/USD
if (token == WETH) {
(price1e18, updatedAt) = _pairPrice1e18(DENOM_ETH, DENOM_USD);
return (price1e18, updatedAt);
}
// Attempt via TOKEN/ETH × ETH/USD (relevant for LST, etc.)
(uint256 tEth, uint256 updA) = _pairPrice1e18(token, DENOM_ETH);
if (tEth == 0) return (0, updA);
(uint256 ethUsd, uint256 updB) = _pairPrice1e18(DENOM_ETH, DENOM_USD);
if (ethUsd == 0) return (0, updB);
uint256 minUpd = updA < updB ? updA : updB;
return ((tEth * ethUsd) / 1e18, minUpd);
}
/* ──────────────────────────────────── Read ───────────────────────────────── */
/**
* @notice Reads token→USD price (1e18) via Chainlink Feed Registry without checking “freshness”.
* @dev Returns 0 if the feed is missing or `answer <= 0`. ETH is passed as address(0).
* @param token Token address (or address(0) for ETH).
* @param asEth If true, use ETH/USD price (for WETH, stETH, etc.).
* @return usdPerToken1e18 Token price in USD (1e18) or 0.
* @return updatedAt Time of the last feed update.
*/
function tryTokenUsdPrice1e18(address token, bool asEth)
external
view
returns (uint256 usdPerToken1e18, uint256 updatedAt)
{
(usdPerToken1e18, updatedAt) = _tryTokenUsdPrice1e18(token, asEth);
return (usdPerToken1e18, updatedAt);
}
/**
* @notice Returns balance, allowance, and decimals for a (token, wallet, spender).
* @dev For ETH (token==address(0)): returns (wallet.balance, 0, 18).
* Decimals is 0 if the token's {decimals()} cannot be read.
* @param token ERC-20 token address (or address(0) for ETH).
* @param wallet Address whose balance is queried.
* @param spender Address whose allowance is checked.
* @return balance Token balance of {wallet} (or ETH balance if token==address(0)).
* @return allowance_ Current allowance from {wallet} to {spender} (0 for ETH).
* @return decimals_ Token decimals (18 for ETH; 0 if unreadable).
*/
function balanceAndAllowanceOf(
address token,
address wallet,
address spender
) external view returns (uint256 balance, uint256 allowance_, uint8 decimals_) {
(balance, allowance_, decimals_) = _safeBalanceAndAllowance(token, wallet, spender);
}
/**
* @notice Returns balance, allowance, decimals, and USD price for a single token.
* @dev For ETH (token==address(0)): returns (wallet.balance, 0, 18) and uses ETH/USD price when {asEth} is true.
* Decimals is 0 if the token's {decimals()} cannot be read.
* @param token ERC-20 token address (or address(0) for ETH).
* @param wallet Address whose balance is queried.
* @param spender Address whose allowance is checked.
* @param asEth If true, forces the ETH/USD feed for this token.
* @return balance Token balance of {wallet} (or ETH balance if token==address(0)).
* @return allowance_ Current allowance from {wallet} to {spender} (0 for ETH).
* @return usdPerToken1e18 Price of 1 token in USD (scaled to 1e18), 0 if no feed/<=0.
* @return updatedAt Timestamp of the last price update.
* @return decimals_ Token decimals (18 for ETH; 0 if unreadable).
*/
function balanceAllowanceAndUsd(
address token,
address wallet,
address spender,
bool asEth
)
external
view
returns (
uint256 balance,
uint256 allowance_,
uint256 usdPerToken1e18,
uint256 updatedAt,
uint8 decimals_
)
{
// 1) safely read balance/allowance (do not revert to non-standard ERC-20)
(balance, allowance_, decimals_) = _safeBalanceAndAllowance(token, wallet, spender);
// 2) Let's try to get the price in USD (gently, without revert)
(usdPerToken1e18, updatedAt) = _tryTokenUsdPrice1e18(token, asEth);
}
/**
* @notice Returns balance, allowance, decimals, and USD price for two tokens at once.
* @dev For ETH (token==address(0)): returns (wallet.balance, 0, 18) and uses ETH/USD price if requested via {asEthIn}/{asEthOut}.
* Decimals is 0 if a token's {decimals()} cannot be read.
* @param wallet Address whose balances are queried.
* @param spender Address whose allowances are checked.
* @param tokenIn First token address (or address(0) for ETH).
* @param asEthIn If true, forces ETH/USD feed for tokenIn.
* @param tokenOut Second token address (or address(0) for ETH).
* @param asEthOut If true, forces ETH/USD feed for tokenOut.
* @return balanceIn {wallet} balance of tokenIn (ETH if tokenIn==address(0)).
* @return allowance_In Allowance of tokenIn to {spender} (0 for ETH).
* @return usdPerToken1e18In TokenIn USD price normalized to 1e18 (0 if no feed/<=0).
* @return updatedAtIn Timestamp of the last price update for tokenIn.
* @return decimals_In Decimals of tokenIn (18 for ETH; 0 if unreadable).
* @return balanceOut {wallet} balance of tokenOut (ETH if tokenOut==address(0)).
* @return allowance_Out Allowance of tokenOut to {spender} (0 for ETH).
* @return usdPerToken1e18Out TokenOut USD price normalized to 1e18 (0 if no feed/<=0).
* @return updatedAtOut Timestamp of the last price update for tokenOut.
* @return decimals_Out Decimals of tokenOut (18 for ETH; 0 if unreadable).
*/
function balanceAllowanceAndUsdDouble(
address wallet,
address spender,
address tokenIn,
bool asEthIn,
address tokenOut,
bool asEthOut
)
external
view
returns (
uint256 balanceIn,
uint256 allowance_In,
uint256 usdPerToken1e18In,
uint256 updatedAtIn,
uint8 decimals_In,
uint256 balanceOut,
uint256 allowance_Out,
uint256 usdPerToken1e18Out,
uint256 updatedAtOut,
uint8 decimals_Out
)
{
// 1) safely read balance/allowance (do not revert to non-standard ERC-20)
(balanceIn, allowance_In, decimals_In) = _safeBalanceAndAllowance(tokenIn, wallet, spender);
(balanceOut, allowance_Out, decimals_Out) = _safeBalanceAndAllowance(tokenOut, wallet, spender);
// 2) Let's try to get the price in USD (gently, without revert)
(usdPerToken1e18In, updatedAtIn) = _tryTokenUsdPrice1e18(tokenIn, asEthIn);
(usdPerToken1e18Out, updatedAtOut) = _tryTokenUsdPrice1e18(tokenOut, asEthOut);
}
/**
* @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.
* @param tokenIn Input token.
* @param tokenOut Output token.
* @param amountIn Input amount.
* @return best1HopRouteTop1 Payload of the best 1-hop.
* @return amountOut1HopTop1 Quote of the best 1-hop.
* @return best2HopRouteTop1 Payload of the best 2-hop.
* @return amountOut2HopTop1 Quote for the best 2-hop.
* @return best1HopRouteTop2 Payload of the second 1-hop.
* @return amountOut1HopTop2 Quote for the second 1-hop.
* @return best2HopRouteTop2 Payload of the second 2-hop.
* @return amountOut2HopTop2 Quote for the second 2-hop.
* @return splitAmountOut Best split quote between two absolute tops (0 if split does not improve).
* @return splitPercentA Share for route A (in percent, 0–100) for split (0 if split is not applicable).
*/
function getBestRoute(
address tokenIn,
address tokenOut,
uint256 amountIn
) external view returns (
bytes memory best1HopRouteTop1, uint256 amountOut1HopTop1,
bytes memory best2HopRouteTop1, uint256 amountOut2HopTop1,
bytes memory best1HopRouteTop2, uint256 amountOut1HopTop2,
bytes memory best2HopRouteTop2, uint256 amountOut2HopTop2,
uint256 splitAmountOut, uint16 splitPercentA
) {
QuoteArgs memory qa = QuoteArgs({
tokenIn: tokenIn,
tokenOut: tokenOut,
amountIn: amountIn
});
BestQuotes memory best;
TrackedRoute memory top1Overall; // Absolute best route
TrackedRoute memory top2Overall; // Second best route
for (uint256 i = 0; i < modules.length; ) {
ModuleQuotes memory quotes = _getModuleQuotes(modules[i], i, qa);
if (quotes.amountOut1Hop > 0) {
TrackedRoute memory r1 = TrackedRoute({
payload: quotes.payload1Hop,
amountOut: quotes.amountOut1Hop,
module: quotes.module,
moduleIndex: quotes.moduleIndex
});
_updateBestQuotes(best, r1, true);
(top1Overall, top2Overall) = _updateTopOverall(top1Overall, top2Overall, r1);
}
if (quotes.amountOut2Hop > 0) {
TrackedRoute memory r2 = TrackedRoute({
payload: quotes.payload2Hop,
amountOut: quotes.amountOut2Hop,
module: quotes.module,
moduleIndex: quotes.moduleIndex
});
_updateBestQuotes(best, r2, false);
(top1Overall, top2Overall) = _updateTopOverall(top1Overall, top2Overall, r2);
}
unchecked { ++i; }
}
if (top1Overall.amountOut == 0) revert NoRouteFound();
// Return the standard 8 fields
best1HopRouteTop1 = best.top1Hop.payload; amountOut1HopTop1 = best.top1Hop.amountOut;
best2HopRouteTop1 = best.top2Hop.payload; amountOut2HopTop1 = best.top2Hop.amountOut;
best1HopRouteTop2 = best.second1Hop.payload; amountOut1HopTop2 = best.second1Hop.amountOut;
best2HopRouteTop2 = best.second2Hop.payload; amountOut2HopTop2 = best.second2Hop.amountOut;
// Compute split between the two overall best routes (T1 and T2)
if (top2Overall.amountOut > 0 && keccak256(top1Overall.payload) != keccak256(top2Overall.payload)) {
(splitAmountOut, splitPercentA) = findBestSplit(
top1Overall.payload,
top2Overall.payload
);
// If split provides no improvement, do not return it,
// since the best will be either T1 or T2 (T1.amountOut >= T2.amountOut).
if (splitAmountOut <= top1Overall.amountOut) {
splitAmountOut = 0;
splitPercentA = 0;
}
} else {
// If only one route found, or T1 == T2, split is not applicable
splitAmountOut = 0;
splitPercentA = 0;
}
}
/**
* @notice Returns top quotes (1-hop & 2-hop), an optional optimal split of the two best routes,
* and wallet allowances/balances/decimals plus Chainlink USD prices.
* @dev Scans all registered modules and tracks Top-1/Top-2 for both 1-hop and 2-hop.
* Also tracks the two overall best routes and probes a split between them.
* If no route is found, reverts with {NoRouteFound}.
* For ETH inputs/outputs, balances/allowances refer to ETH and decimals=18.
* @param tokenIn Input token.
* @param tokenOut Output token.
* @param amountIn Input amount.
* @param wallet Wallet to read balances/allowances from.
* @param spender Spender to check allowances against.
* @param asEthIn If true, forces ETH/USD feed for tokenIn (WETH, stETH, etc.).
* @param asEthOut If true, forces ETH/USD feed for tokenOut (WETH, stETH, etc.).
* @return best1HopRouteTop1 Serialized payload of best 1-hop route.
* @return amountOut1HopTop1 Quoted output of best 1-hop route.
* @return best2HopRouteTop1 Serialized payload of best 2-hop route.
* @return amountOut2HopTop1 Quoted output of best 2-hop route.
* @return best1HopRouteTop2 Serialized payload of second-best 1-hop route.
* @return amountOut1HopTop2 Quoted output of second-best 1-hop route.
* @return best2HopRouteTop2 Serialized payload of second-best 2-hop route.
* @return amountOut2HopTop2 Quoted output of second-best 2-hop route.
* @return splitAmountOut Quote for the best split of the two overall leaders (0 if not improving).
* @return splitPercentA Percent for route A (0–100) in the split (0 if split not applicable).
*
* @return balanceIn {wallet} balance of tokenIn (ETH if tokenIn==address(0)).
* @return allowance_In Allowance of tokenIn to {spender} (0 for ETH).
* @return decimals_In Decimals of tokenIn (18 for ETH; 0 if unreadable).
* @return balanceOut {wallet} balance of tokenOut (ETH if tokenOut==address(0)).
* @return allowance_Out Allowance of tokenOut to {spender} (0 for ETH).
* @return decimals_Out Decimals of tokenOut (18 for ETH; 0 if unreadable).
*
* @return usdPerToken1e18In TokenIn USD price normalized to 1e18 (0 if no feed/<=0).
* @return updatedAtIn Timestamp of the last price update for tokenIn.
* @return usdPerToken1e18Out TokenOut USD price normalized to 1e18 (0 if no feed/<=0).
* @return updatedAtOut Timestamp of the last price update for tokenOut.
*/
function getBestRouteSuper(
address tokenIn,
address tokenOut,
uint256 amountIn,
address wallet,
address spender,
bool asEthIn,
bool asEthOut
) external view returns (
bytes memory best1HopRouteTop1, uint256 amountOut1HopTop1,
bytes memory best2HopRouteTop1, uint256 amountOut2HopTop1,
bytes memory best1HopRouteTop2, uint256 amountOut1HopTop2,
bytes memory best2HopRouteTop2, uint256 amountOut2HopTop2,
uint256 splitAmountOut, uint16 splitPercentA,
uint256 balanceIn, uint256 allowance_In, uint8 decimals_In,
uint256 balanceOut, uint256 allowance_Out, uint8 decimals_Out,
uint256 usdPerToken1e18In, uint256 updatedAtIn,
uint256 usdPerToken1e18Out, uint256 updatedAtOut
) {
QuoteArgs memory qa = QuoteArgs({
tokenIn: tokenIn,
tokenOut: tokenOut,
amountIn: amountIn
});
BestQuotes memory best;
TrackedRoute memory top1Overall; // Absolute best route
TrackedRoute memory top2Overall; // Second best route
for (uint256 i = 0; i < modules.length; ) {
ModuleQuotes memory quotes = _getModuleQuotes(modules[i], i, qa);
if (quotes.amountOut1Hop > 0) {
TrackedRoute memory r1 = TrackedRoute({
payload: quotes.payload1Hop,
amountOut: quotes.amountOut1Hop,
module: quotes.module,
moduleIndex: quotes.moduleIndex
});
_updateBestQuotes(best, r1, true);
(top1Overall, top2Overall) = _updateTopOverall(top1Overall, top2Overall, r1);
}
if (quotes.amountOut2Hop > 0) {
TrackedRoute memory r2 = TrackedRoute({
payload: quotes.payload2Hop,
amountOut: quotes.amountOut2Hop,
module: quotes.module,
moduleIndex: quotes.moduleIndex
});
_updateBestQuotes(best, r2, false);
(top1Overall, top2Overall) = _updateTopOverall(top1Overall, top2Overall, r2);
}
unchecked { ++i; }
}
if (top1Overall.amountOut == 0) revert NoRouteFound();
// Return the standard 8 fields
best1HopRouteTop1 = best.top1Hop.payload; amountOut1HopTop1 = best.top1Hop.amountOut;
best2HopRouteTop1 = best.top2Hop.payload; amountOut2HopTop1 = best.top2Hop.amountOut;
best1HopRouteTop2 = best.second1Hop.payload; amountOut1HopTop2 = best.second1Hop.amountOut;
best2HopRouteTop2 = best.second2Hop.payload; amountOut2HopTop2 = best.second2Hop.amountOut;
// Compute split between the two overall best routes (T1 and T2)
if (top2Overall.amountOut > 0 && keccak256(top1Overall.payload) != keccak256(top2Overall.payload)) {
(splitAmountOut, splitPercentA) = findBestSplit(
top1Overall.payload,
top2Overall.payload
);
// If split provides no improvement, do not return it,
// since the best will be either T1 or T2 (T1.amountOut >= T2.amountOut).
if (splitAmountOut <= top1Overall.amountOut) {
splitAmountOut = 0;
splitPercentA = 0;
}
} else {
// If only one route found, or T1 == T2, split is not applicable
splitAmountOut = 0;
splitPercentA = 0;
}
// safely read balance/allowance (do not revert on non-standard ERC-20)
(balanceIn, allowance_In, decimals_In) = _safeBalanceAndAllowance(tokenIn, wallet, spender);
(balanceOut, allowance_Out, decimals_Out) = _safeBalanceAndAllowance(tokenOut, wallet, spender);
// Let's try to get the price in USD (gently, without revert)
(usdPerToken1e18In, updatedAtIn) = _tryTokenUsdPrice1e18(tokenIn, asEthIn);
(usdPerToken1e18Out, updatedAtOut) = _tryTokenUsdPrice1e18(tokenOut, asEthOut);
}
/**
* @notice Find the best split ratio between two route payloads.
* @dev Discrete search by simulateRoute + local fine-tuning.
* @param payloadA Route A.
* @param payloadB Route B.
* @return bestAmountOut Best total quote.
* @return bestPercentA Share of A (0–100) giving the maximum.
*/
function findBestSplit(
bytes memory payloadA, // ИЗМЕНЕНИЕ: bytes memory
bytes memory payloadB
)
internal
view
returns (
uint256 bestAmountOut,
uint16 bestPercentA
)
{
// Decode and verify
LegDecoded memory A = _decodeRouteStruct(payloadA);
LegDecoded memory B = _decodeRouteStruct(payloadB);
require(A.amountIn > 0 && B.amountIn > 0, "UR: zero amounts");
require(A.tokenIn == B.tokenIn, "UR: in mismatch");
require(A.tokenOut == B.tokenOut, "UR: out mismatch");
require(A.amountIn == B.amountIn, "UR: totalIn mismatch");
address moduleA = A.module;
address moduleB = B.module;
// --- Step 1: Initialization (50%) ---
uint16 initialPercent = 50; // 50%
uint256 currentMaxOut = _calculateTotalOut(
moduleA, A.route, moduleB, B.route, initialPercent
);
uint16 currentBestPercent = initialPercent;
// --- Step 2: Main sparse search: 10% to 90% in 10% increments ---
// Check 10, 20, 30, 40, 60, 70, 80, 90. (50% already checked).
for (uint16 percent = 10; percent <= 90; percent += 10) {
if (percent == 50) continue;
uint256 totalOut = _calculateTotalOut(
moduleA, A.route, moduleB, B.route, percent
);
if (totalOut > currentMaxOut) {
currentMaxOut = totalOut;
currentBestPercent = percent;
}
}
// --- Step 3: Refinement (Local search, +/- 5% step) ---
uint16 bestPercentFound = currentBestPercent;
// Array of offsets for refinement: [-5, +5] Percent
int16[] memory offsets = new int16[](2);
offsets[0] = -5; // Checking -5% from the best point
offsets[1] = 5; // Checking +5% from the best point
for (uint256 i = 0; i < offsets.length; ) {
int16 offset = offsets[i];
// Protection against values exceeding the limits (e.g., below 1% or above 99%)
// Condition: bestPercentFound <= 5 (for -5) or bestPercentFound >= 95 (for +5)
if (
(offset < 0 && bestPercentFound <= uint16(-offset)) ||
(offset > 0 && bestPercentFound >= 100 - uint16(offset))
) {
unchecked { ++i; }
continue;
}
uint16 checkPercent;
if (offset < 0) {
checkPercent = bestPercentFound - uint16(-offset);
} else {
checkPercent = bestPercentFound + uint16(offset);
}
// Check that the point is within a reasonable range for swap [1, 99]
if (checkPercent >= 1 && checkPercent <= 99) {
uint256 totalOut = _calculateTotalOut(
moduleA, A.route, moduleB, B.route, checkPercent
);
if (totalOut > currentMaxOut) {
currentMaxOut = totalOut;
currentBestPercent = checkPercent;
}
}
unchecked { ++i; }
}
// 4. Return the result
bestAmountOut = currentMaxOut;
bestPercentA = currentBestPercent;
}
/**
* @notice Decodes the route payload.
* @param payload ABI-encoded packet.
* @return module Module address.
* @return moduleIndex Module index.
* @return quotedOut Output quote.
* @return tokenIn Input token.
* @return tokenOut Output token.
* @return amountIn Input amount.
* @return routeData Route byte hops.
*/
function decodeRoute(bytes calldata payload)
public
pure
returns (
address module,
uint256 moduleIndex,
uint256 quotedOut,
address tokenIn,
address tokenOut,
uint256 amountIn,
bytes[] memory routeData
)
{
(module, moduleIndex, quotedOut, tokenIn, tokenOut, amountIn, routeData) =
abi.decode(payload, (address, uint256, uint256, address, address, uint256, bytes[]));
}
/* ──────────────────────────────────── Swap ───────────────────────────────── */
/* ─────────────── ROUTE: Token → Token ───────────── */
/**
* @notice Execute a swap based on a pre-prepared payload.
* @dev Takes a commission from the swap and positive slippage; checks minAmountOut; transfers the net amount to `to`.
* @param payload ABI-encoded route (see decodeRoute).
* @param to Recipient of the final tokens.
* @param minAmountOut Minimum allowable output.
* @return netOut Net amount after commissions are deducted.
*/
function swapRoute(bytes calldata payload, address to, uint256 minAmountOut)
external
nonReentrant
returns (uint256 netOut)
{
require(to != address(0), "UR: bad to");
(
address module, , uint256 quotedOut,
address tokenIn, address tokenOut, uint256 amountIn,
bytes[] memory routeData
) = decodeRoute(payload);
require(isModule[module], "UR: unknown module");
require(amountIn > 0, "UR: zero amountIn");
require(routeData.length > 0, "UR: empty route");
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
_smartApprove(tokenIn, module, amountIn);
uint256 amountOut = IDexModule(module).swapRoute(DexRoute({ data: routeData }), address(this), 100);
require(amountOut >= minAmountOut, "UR: slippage");
// Pay the user minus the fee — and return immediately
netOut = _distributeTokenWithFees(tokenOut, to, amountOut, quotedOut, minAmountOut);
emit SwapExecuted(module, msg.sender, to, tokenIn, tokenOut, amountIn, netOut, quotedOut);
}
/* ─────────────── ROUTE: ETH → Token ─────────────── */
/**
* @notice Swap ETH→Token by payload with WETH as tokenIn.
* @dev Wraps ETH in WETH, calls the module, holds commissions, sends net amount `to`.
* @param payload Route packet (tokenIn=WETH, amountIn=msg.value).
* @param to Recipient.
* @param minAmountOut Minimum output.
* @return netOut Net amount (ERC20).
*/
function swapRouteExactETHForTokens(
bytes calldata payload, // payload with tokenIn == WETH and amountIn == msg.value
address to,
uint256 minAmountOut
) external payable nonReentrant returns (uint256 netOut) {
require(to != address(0), "UR: bad to");
require(msg.value > 0, "UR: no ETH");
_requireWethIn(payload);
(
address module, , uint256 quotedOut,
, address tokenOut, uint256 amountIn,
bytes[] memory routeData
) = decodeRoute(payload);
require(isModule[module], "UR: unknown module");
require(routeData.length > 0, "UR: empty route");
require(amountIn == msg.value, "UR: value != amountIn");
_wrapETH(msg.value); // ETH -> WETH
_smartApprove(WETH, module, msg.value); // approve
// Send to router → calculate commission → pay customer
uint256 amountOut = IDexModule(module).swapRoute(DexRoute({data: routeData}), address(this), 100);
require(amountOut >= minAmountOut, "UR: slippage");
netOut = _distributeTokenWithFees(tokenOut, to, amountOut, quotedOut, minAmountOut);
emit SwapExecuted(module, msg.sender, to, WETH, tokenOut, amountIn, netOut, quotedOut);
}
/* ─────────────── ROUTE: Token → ETH ─────────────── */
/**
* @notice Swap Token→ETH by payload with WETH as tokenOut.
* @dev Calls the module before WETH, converts to ETH, holds commissions, sends net amount `to`.
* @param payload Route package (tokenOut=WETH).
* @param to ETH recipient.
* @param minAmountOut Minimum output.
* @return netEthOut Net amount (ETH).
*/
function swapRouteExactTokensForETH(
bytes calldata payload, // payload: tokenOut == WETH
address to,
uint256 minAmountOut
) external nonReentrant returns (uint256 netEthOut) {
require(to != address(0), "UR: bad to");
_requireWethOut(payload);
(
address module, , uint256 quotedOut,
address tokenIn, , uint256 amountIn,
bytes[] memory routeData
) = decodeRoute(payload);
require(isModule[module], "UR: unknown module");
require(amountIn > 0, "UR: zero in");
require(routeData.length > 0, "UR: empty route");
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
_smartApprove(tokenIn, module, amountIn);
uint256 outWeth = IDexModule(module).swapRoute(DexRoute({data: routeData}), address(this), 100);
require(outWeth >= minAmountOut, "UR: slippage");
// Unwrap and distribute with fees
_unwrapWETHAndSend(outWeth, address(this));
netEthOut = _distributeETHWithFees(to, outWeth, quotedOut, minAmountOut);
emit SwapExecuted(module, msg.sender, to, tokenIn, WETH, amountIn, netEthOut, quotedOut);
}
/* ─────────────── SPLIT: Token → Token ───────────── */
/**
* @notice Perform a split swap with two token→token routes.
* @dev Splits the input by `percentA`/`100-percentA`; checks minAmountOut; holds commissions; forwards the net amount.
* @param payloadA Route A package.
* @param payloadB Route B package.
* @param percentA Share A (1–99).
* @param minAmountOut Minimum total output.
* @param to Recipient.
* @return netOut Net amount after fees.
*/
function swapSplit(
bytes calldata payloadA,
bytes calldata payloadB,
uint16 percentA,
uint256 minAmountOut,
address to
) external nonReentrant returns (uint256 netOut) {
// Decode and verify
LegDecoded memory A = _decodeRouteStructCallData(payloadA);
LegDecoded memory B = _decodeRouteStructCallData(payloadB);
require(A.amountIn > 0 && B.amountIn > 0, "UR: zero amounts");
require(A.tokenIn == B.tokenIn, "UR: in mismatch");
require(A.tokenOut == B.tokenOut, "UR: out mismatch");
require(A.amountIn == B.amountIn, "UR: totalIn mismatch");
require(A.route.length > 0 && B.route.length > 0, "UR: empty route");
require(percentA >= 1 && percentA <= 99, "UR: percent out of bounds");
require(isModule[A.module], "UR: unknown module");
require(isModule[B.module], "UR: unknown module");
IERC20(A.tokenIn).safeTransferFrom(msg.sender, address(this), A.amountIn); // if A.amountIn equals B.amountIn
_smartApprove(A.tokenIn, A.module, A.amountIn);
_smartApprove(A.tokenIn, B.module, B.amountIn);
// Perform swaps (call modules)
// Route A (percentA)
// IDexModule.swapRoute passes a percentage (0-100), and the module
// must internally calculate the exact amountIn for this part of the swap.
uint256 outA = IDexModule(A.module).swapRoute(
DexRoute({ data: A.route }),
address(this),
percentA
);
// Route B (100 - percentA)
uint256 outB = IDexModule(B.module).swapRoute(
DexRoute({ data: B.route }),
address(this),
uint16(100 - percentA)
);
// Slip check and return
require((outA + outB) >= minAmountOut, "UR: slippage");
uint256 quotedTotal = (A.quoted * percentA) / 100 + (B.quoted * (uint16(100 - percentA))) / 100;
// Commission + payment to user
netOut = _distributeTokenWithFees(A.tokenOut, to, outA + outB, quotedTotal, minAmountOut);
SplitResult memory r = SplitResult({
moduleA: A.module,
moduleB: B.module,
tokenIn: A.tokenIn,
tokenOut: A.tokenOut,
totalIn: A.amountIn,
amountInA: (A.amountIn * percentA) / 100,
amountInB: (B.amountIn * (uint16(100 - percentA))) / 100,
outA: outA,
outB: outB,
totalOut: outA + outB
});
_emitSwapSplit(r, msg.sender, to, percentA);
}
/* ─────────────── SPLIT: ETH → Token ─────────────── */
/**
* @notice Split-swap ETH→Token via two routes (both via WETH).
* @dev Converts ETH to WETH; splits input by percentage; holds fees; transfers net amount `to`.
* @param payloadA Package A (tokenIn=WETH, amountIn=msg.value).
* @param payloadB Package B.
* @param percentA Share A (1–99).
* @param minTotalOut Minimum total output.
* @param to Recipient.
* @return netOut Net result (ERC20).
*/
function swapSplitExactETHForTokens(
bytes calldata payloadA, // both: tokenIn == WETH, amountIn == msg.value
bytes calldata payloadB,
uint16 percentA, // 1..99
uint256 minTotalOut,
address to
) external payable nonReentrant returns (uint256 netOut) {
require(to != address(0), "UR: bad to");
require(msg.value > 0, "UR: no ETH");
require(percentA >= 1 && percentA <= 99, "UR: percent out of bounds");
_requireWethIn(payloadA);
_requireWethIn(payloadB);
LegDecoded memory A = _decodeRouteStructCallData(payloadA);
LegDecoded memory B = _decodeRouteStructCallData(payloadB);
require(A.amountIn == B.amountIn, "UR: split amount mismatch");
require(A.amountIn == msg.value, "UR: value != amountIn");
require(A.tokenOut == B.tokenOut, "UR: out mismatch");
require(A.route.length > 0 && B.route.length > 0, "UR: empty route");
require(isModule[A.module], "UR: unknown module");
require(isModule[B.module], "UR: unknown module");
_wrapETH(msg.value);
_smartApprove(WETH, A.module, msg.value);
_smartApprove(WETH, B.module, msg.value);
uint16 percentB = uint16(100 - percentA);
// Route execution → fees → recipient
uint256 outA = IDexModule(A.module).swapRoute(DexRoute({data: A.route}), address(this), percentA);
uint256 outB = IDexModule(B.module).swapRoute(DexRoute({data: B.route}), address(this), percentB);
uint256 grossOut = outA + outB;
require(grossOut >= minTotalOut, "UR: slippage");
uint256 quotedTotal = (A.quoted * percentA) / 100 + (B.quoted * percentB) / 100;
netOut = _distributeTokenWithFees(A.tokenOut, to, grossOut, quotedTotal, minTotalOut);
SplitResult memory r = SplitResult({
moduleA: A.module,
moduleB: B.module,
tokenIn: WETH,
tokenOut: A.tokenOut,
totalIn: msg.value,
amountInA: (uint256(msg.value) * percentA) / 100,
amountInB: (uint256(msg.value) * percentB) / 100,
outA: outA,
outB: outB,
totalOut: grossOut
});
_emitSwapSplit(r, msg.sender, to, percentA);
}
/* ─────────────── SPLIT: Token → ETH ─────────────── */
/**
* @notice Split-swap Token→ETH via two routes (both ending in WETH).
* @dev Splits input by percentage; converts WETH→ETH; holds fees; transfers net amount `to`.
* @param payloadA Package A (tokenOut=WETH).
* @param payloadB Package B.
* @param percentA Share A (1–99).
* @param minTotalEthOut Minimum total output in ETH.
* @param to ETH recipient.
* @return netEthOut Net result (ETH).
*/
function swapSplitExactTokensForETH(
bytes calldata payloadA, // both: tokenOut == WETH, same amountIn
bytes calldata payloadB,
uint16 percentA, // 1..99
uint256 minTotalEthOut,
address to
) external nonReentrant returns (uint256 netEthOut) {
require(to != address(0), "UR: bad to");
require(percentA >= 1 && percentA <= 99, "UR: percent out of bounds");
_requireWethOut(payloadA);
_requireWethOut(payloadB);
LegDecoded memory A = _decodeRouteStructCallData(payloadA);
LegDecoded memory B = _decodeRouteStructCallData(payloadB);
require(A.amountIn > 0 && B.amountIn > 0, "UR: zero in");
require(A.amountIn == B.amountIn, "UR: split amount mismatch");
require(A.tokenIn == B.tokenIn, "UR: in mismatch");
require(A.route.length > 0 && B.route.length > 0, "UR: empty route");
require(isModule[A.module], "UR: unknown module");
require(isModule[B.module], "UR: unknown module");
IERC20(A.tokenIn).safeTransferFrom(msg.sender, address(this), A.amountIn);
_smartApprove(A.tokenIn, A.module, A.amountIn);
_smartApprove(A.tokenIn, B.module, B.amountIn);
uint16 percentB = uint16(100 - percentA);
uint256 outA = IDexModule(A.module).swapRoute(DexRoute({data: A.route}), address(this), percentA);
uint256 outB = IDexModule(B.module).swapRoute(DexRoute({data: B.route}), address(this), percentB);
uint256 totalWeth = outA + outB;
require(totalWeth >= minTotalEthOut, "UR: slippage");
uint256 quotedTotal = (A.quoted * percentA) / 100 + (B.quoted * percentB) / 100;
_unwrapWETHAndSend(totalWeth, address(this));
netEthOut = _distributeETHWithFees(to, totalWeth, quotedTotal, minTotalEthOut);
SplitResult memory r = SplitResult({
moduleA: A.module,
moduleB: B.module,
tokenIn: A.tokenIn,
tokenOut: WETH,
totalIn: A.amountIn,
amountInA: (A.amountIn * percentA) / 100,
amountInB: (B.amountIn * percentB) / 100,
outA: outA,
outB: outB,
totalOut: totalWeth
});
_emitSwapSplit(r, msg.sender, to, percentA);
}
} <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// 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, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single `nonReentrant` guard, functions marked as
* `nonReentrant` may not call one another. This can be worked around by making
* those functions `private`, and then adding `external` `nonReentrant` entry
* points to them.
*
* TIP: If you would like to learn more about reentrancy and alternative ways
* to protect against it, check out our blog post
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuard {
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
// but in exchange the refund on every call to nonReentrant will be lower in
// amount. Since refunds are capped to a percentage of the total
// transaction's gas, it is best to keep them low in cases like this one, to
// increase the likelihood of the full refund coming into effect.
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
constructor() {
_status = _NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and making it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
// On the first call to nonReentrant, _status will be _NOT_ENTERED
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
// Any calls to nonReentrant after this point will fail
_status = _ENTERED;
}
function _nonReentrantAfter() private {
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_status = _NOT_ENTERED;
}
/**
* @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
* `nonReentrant` function in the call stack.
*/
function _reentrancyGuardEntered() internal view returns (bool) {
return _status == _ENTERED;
}
} <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
pragma solidity ^0.8.20;
import {Context} from "../utils/Context.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* The initial owner is set to the address provided by the deployer. This can
* later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
/**
* @dev The caller account is not authorized to perform an operation.
*/
error OwnableUnauthorizedAccount(address account);
/**
* @dev The owner is not a valid owner account. (eg. `address(0)`)
*/
error OwnableInvalidOwner(address owner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the address provided by the deployer as the initial owner.
*/
constructor(address initialOwner) {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby disabling any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
} <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.3.0) (token/ERC20/utils/SafeERC20.sol)
pragma solidity ^0.8.20;
import {IERC20} from "../IERC20.sol";
import {IERC1363} from "../../../interfaces/IERC1363.sol";
/**
* @title SafeERC20
* @dev Wrappers around ERC-20 operations that throw on failure (when the token
* contract returns false). Tokens that return no value (and instead revert or
* throw on failure) are also supported, non-reverting calls are assumed to be
* successful.
* To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
*/
library SafeERC20 {
/**
* @dev An operation with an ERC-20 token failed.
*/
error SafeERC20FailedOperation(address token);
/**
* @dev Indicates a failed `decreaseAllowance` request.
*/
error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);
/**
* @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeTransfer(IERC20 token, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
}
/**
* @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
* calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
*/
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
}
/**
* @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful.
*/
function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {
return _callOptionalReturnBool(token, abi.encodeCall(token.transfer, (to, value)));
}
/**
* @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful.
*/
function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) {
return _callOptionalReturnBool(token, abi.encodeCall(token.transferFrom, (from, to, value)));
}
/**
* @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*
* IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
* smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
* this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
* that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
*/
function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
uint256 oldAllowance = token.allowance(address(this), spender);
forceApprove(token, spender, oldAllowance + value);
}
/**
* @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no
* value, non-reverting calls are assumed to be successful.
*
* IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
* smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
* this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
* that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
*/
function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {
unchecked {
uint256 currentAllowance = token.allowance(address(this), spender);
if (currentAllowance < requestedDecrease) {
revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);
}
forceApprove(token, spender, currentAllowance - requestedDecrease);
}
}
/**
* @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
* to be set to zero before setting it to a non-zero value, such as USDT.
*
* NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function
* only sets the "standard" allowance. Any temporary allowance will remain active, in addition to the value being
* set here.
*/
function forceApprove(IERC20 token, address spender, uint256 value) internal {
bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));
if (!_callOptionalReturnBool(token, approvalCall)) {
_callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));
_callOptionalReturn(token, approvalCall);
}
}
/**
* @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no
* code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
* targeting contracts.
*
* Reverts if the returned value is other than `true`.
*/
function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
if (to.code.length == 0) {
safeTransfer(token, to, value);
} else if (!token.transferAndCall(to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target
* has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
* targeting contracts.
*
* Reverts if the returned value is other than `true`.
*/
function transferFromAndCallRelaxed(
IERC1363 token,
address from,
address to,
uint256 value,
bytes memory data
) internal {
if (to.code.length == 0) {
safeTransferFrom(token, from, to, value);
} else if (!token.transferFromAndCall(from, to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no
* code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
* targeting contracts.
*
* NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.
* Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}
* once without retrying, and relies on the returned value to be true.
*
* Reverts if the returned value is other than `true`.
*/
function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
if (to.code.length == 0) {
forceApprove(token, to, value);
} else if (!token.approveAndCall(to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.
*/
function _callOptionalReturn(IERC20 token, bytes memory data) private {
uint256 returnSize;
uint256 returnValue;
assembly ("memory-safe") {
let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
// bubble errors
if iszero(success) {
let ptr := mload(0x40)
returndatacopy(ptr, 0, returndatasize())
revert(ptr, returndatasize())
}
returnSize := returndatasize()
returnValue := mload(0)
}
if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.
*/
function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
bool success;
uint256 returnSize;
uint256 returnValue;
assembly ("memory-safe") {
success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
returnSize := returndatasize()
returnValue := mload(0)
}
return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);
}
} <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/IERC20.sol)
pragma solidity >=0.4.16;
/**
* @dev Interface of the ERC-20 standard as defined in the ERC.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
} <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (interfaces/IERC1363.sol)
pragma solidity >=0.6.2;
import {IERC20} from "./IERC20.sol";
import {IERC165} from "./IERC165.sol";
/**
* @title IERC1363
* @dev Interface of the ERC-1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363].
*
* Defines an extension interface for ERC-20 tokens that supports executing code on a recipient contract
* after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction.
*/
interface IERC1363 is IERC20, IERC165 {
/*
* Note: the ERC-165 identifier for this interface is 0xb0202a11.
* 0xb0202a11 ===
* bytes4(keccak256('transferAndCall(address,uint256)')) ^
* bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^
* bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^
* bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^
* bytes4(keccak256('approveAndCall(address,uint256)')) ^
* bytes4(keccak256('approveAndCall(address,uint256,bytes)'))
*/
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
* @param to The address which you want to transfer to.
* @param value The amount of tokens to be transferred.
* @return A boolean value indicating whether the operation succeeded unless throwing.
*/
function transferAndCall(address to, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
* @param to The address which you want to transfer to.
* @param value The amount of tokens to be transferred.
* @param data Additional data with no specified format, sent in call to `to`.
* @return A boolean value indicating whether the operation succeeded unless throwing.
*/
function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
* @param from The address which you want to send tokens from.
* @param to The address which you want to transfer to.
* @param value The amount of tokens to be transferred.
* @return A boolean value indicating whether the operation succeeded unless throwing.
*/
function transferFromAndCall(address from, address to, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
* @param from The address which you want to send tokens from.
* @param to The address which you want to transfer to.
* @param value The amount of tokens to be transferred.
* @param data Additional data with no specified format, sent in call to `to`.
* @return A boolean value indicating whether the operation succeeded unless throwing.
*/
function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
* @param spender The address which will spend the funds.
* @param value The amount of tokens to be spent.
* @return A boolean value indicating whether the operation succeeded unless throwing.
*/
function approveAndCall(address spender, uint256 value) external returns (bool);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
* @param spender The address which will spend the funds.
* @param value The amount of tokens to be spent.
* @param data Additional data with no specified format, sent in call to `spender`.
* @return A boolean value indicating whether the operation succeeded unless throwing.
*/
function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool);
} <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
pragma solidity ^0.8.20;
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
} <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (interfaces/IERC165.sol)
pragma solidity >=0.4.16;
import {IERC165} from "../utils/introspection/IERC165.sol"; <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (interfaces/IERC20.sol)
pragma solidity >=0.4.16;
import {IERC20} from "../token/ERC20/IERC20.sol"; <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (utils/introspection/IERC165.sol)
pragma solidity >=0.4.16;
/**
* @dev Interface of the ERC-165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[ERC].
*
* Implementers can declare support of contract interfaces, which can then be
* queried by others ({ERC165Checker}).
*
* For an implementation, see {ERC165}.
*/
interface IERC165 {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}