UniswapStrategy

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": {
    "src/strategy/UniswapStrategy.sol": {
      "content": "// SPDX-License-Identifier: LicenseRef-CICADA-Proprietary
// SPDX-FileCopyrightText: (c) 2024 Cicada Software, CICADA DMCC. All rights reserved.
pragma solidity ^0.8.28;

import {Vault} from "../../src/Vault.sol";
import {SafeERC20} from "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
import {Enums, Exact} from "../lib/Common.sol";
import {UniV2} from "../lib/UniV2.sol";
import {UniV3} from "../lib/UniV3.sol";
import {IUniversalRouter} from "@uniswap-universal-router/contracts/interfaces/IUniversalRouter.sol";
import {IPermit2} from "@uniswap/briefcase/protocols/permit2/interfaces/IPermit2.sol";
import {Commands} from "@uniswap-universal-router/contracts/libraries/Commands.sol";
import {IERC20} from "@openzeppelin-contracts/token/ERC20/IERC20.sol";
import {IV4Router} from "@uniswap/briefcase/protocols/v4-periphery/interfaces/IV4Router.sol";
import {IV4Quoter} from "@uniswap/briefcase/protocols/v4-periphery/interfaces/IV4Quoter.sol";
import {SwapParamsUniV4, IUniswapStrategy, IStrategy} from "../../src/interfaces/IUniswapStrategy.sol";
import {Actions} from "@uniswap/briefcase/protocols/v4-periphery/libraries/Actions.sol";
import {Ownable} from "@openzeppelin-contracts/access/Ownable.sol";
import {Currency} from "@uniswap/briefcase/protocols/v4-core/types/Currency.sol";
import {IHasPoolManager} from "../interfaces/IHasPoolManager.sol";

/**
 * @title UniswapStrategy
 * @notice A strategy for swapping tokens on Uniswap V2/V3/V4 via Universal Router
 * @dev Also can be used for PancakeSwap
 */
contract UniswapStrategy is IUniswapStrategy, Ownable {
    using SafeERC20 for IERC20;
    using UniV2 for UniV2.UniV2PathVia;
    using UniV3 for UniV3.UniV3PathVia;
    using UniV3 for UniV3.UniV3PathFull;

    address public immutable UNIVERSAL_ROUTER;
    address public immutable UNI_V4_QUOTER;
    string public constant version = "v0.1.6";

    error InvalidFlavor(Enums.Flavor flavor);
    error UnableToDecodeSwapParams(bytes flavorParams);
    error EmptyPath();
    error QuoteTokenIsNotInPathOrIncorrectlyPlaced();
    error BaseTokenIsNotExactCurrency();

    // A bit of a risky move, but right now there are no Permit2 contracts on other addresses, across UniV4 supported chains
    address public constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;

    /**
     * @notice Initializes the Strategy with the provided ISwapRouter V3.
     * @param _universalRouter Address of the Uniswap V3 router contract used for asset swaps.
     */
    constructor(address _universalRouter, address uniV4Quoter) Ownable(msg.sender) {
        if (_universalRouter == address(0)) revert("UniswapStrategy: ZERO_ROUTER");
        if (_universalRouter.code.length == 0) revert("UniswapStrategy: ROUTER_NO_CODE");
        if (uniV4Quoter == address(0)) revert("UniswapStrategy: ZERO_QUOTER");
        if (uniV4Quoter.code.length == 0) revert("UniswapStrategy: QUOTER_NO_CODE");

        UNIVERSAL_ROUTER = _universalRouter;
        UNI_V4_QUOTER = uniV4Quoter;
    }

    /// @inheritdoc IStrategy
    function buy(uint256 amountOutBase, uint256 amountInMaxQuote, Enums.Flavor flavor, bytes calldata flavorParams)
        public
        virtual
    {
        Vault vault = Vault(payable(msg.sender));
        IERC20 quoteToken = vault.quoteToken();

        Enums.Direction direction = Enums.Direction.QuoteToBase;

        _buyOrSell(
            address(quoteToken), address(vault), amountOutBase, amountInMaxQuote, direction, flavor, flavorParams
        );
    }

    /// @inheritdoc IStrategy
    function sell(uint256 amountInBase, uint256 amountOutMinQuote, Enums.Flavor flavor, bytes calldata flavorParams)
        public
        virtual
    {
        Vault vault = Vault(payable(msg.sender));
        IERC20 baseToken = vault.baseToken();

        Enums.Direction direction = Enums.Direction.BaseToQuote;

        _buyOrSell(address(baseToken), address(vault), amountInBase, amountOutMinQuote, direction, flavor, flavorParams);
    }

    /// @inheritdoc IUniswapStrategy
    function quote(
        address vault,
        uint256 baseTokenAmount,
        Enums.Direction direction,
        Enums.Flavor flavor,
        bytes calldata flavorParams
    ) public virtual returns (uint256 baseAmount, uint256 quoteAmount) {
        if (flavor != Enums.Flavor.UniV4) revert InvalidFlavor(flavor);

        SwapParamsUniV4 memory swapParams = _parseSwapParamsUniV4(vault, direction, flavorParams);

        Exact exactType = _isBuy(direction) ? Exact.Out : Exact.In;

        (uint256 amountIn, uint256 amountOut) = quoteV4(baseTokenAmount, exactType, flavor, swapParams);

        if (exactType == Exact.In) {
            baseAmount = amountIn;
            quoteAmount = amountOut;
        } else {
            baseAmount = amountOut;
            quoteAmount = amountIn;
        }
    }

    /// @notice General purpose quote for both Quote Exact and Base Exact quotes
    function quoteV4(uint256 amount, Exact exactType, Enums.Flavor flavor, SwapParamsUniV4 memory swapParams)
        public
        virtual
        returns (uint256 amountIn, uint256 amountOut)
    {
        if (flavor != Enums.Flavor.UniV4) revert InvalidFlavor(flavor);

        IV4Quoter.QuoteExactParams memory params = IV4Quoter.QuoteExactParams({
            exactCurrency: swapParams.currency,
            path: swapParams.path,
            exactAmount: uint128(amount)
        });

        if (exactType == Exact.In) {
            (amountOut,) = IV4Quoter(UNI_V4_QUOTER).quoteExactInput(params);
            amountIn = amount;
        } else {
            (amountIn,) = IV4Quoter(UNI_V4_QUOTER).quoteExactOutput(params);
            amountOut = amount;
        }
    }

    /**
     * @notice Executes a buy or sell operation by swapping a specified amount of tokens
     * @param tokenIn The address of the token to swap from
     * @param recipient The address of the recipient of the swapped tokens
     * @param amount0 First amount parameter in Command
     * @param amount1 Second amount parameter in Command
     * @param direction The direction of the swap
     * @param flavor The flavor of the strategy
     * @param flavorParams encoded path parameters in Cicada's proprietary format
     */
    function _buyOrSell(
        address tokenIn,
        address recipient,
        uint256 amount0,
        uint256 amount1,
        Enums.Direction direction,
        Enums.Flavor flavor,
        bytes calldata flavorParams
    ) internal {
        (bytes memory commands, bytes[] memory inputs) =
            _getExecuteArgs(recipient, amount0, amount1, direction, flavor, flavorParams);

        uint256 amount = direction == Enums.Direction.QuoteToBase ? amount1 : amount0;

        if (flavor == Enums.Flavor.UniV4) {
            IERC20(tokenIn).safeTransferFrom(recipient, address(this), amount);
            _approveTokenWithPermit2(tokenIn, uint160(amount), uint48(block.timestamp + 1000));
        } else {
            IERC20(tokenIn).safeTransferFrom(recipient, UNIVERSAL_ROUTER, amount);
        }

        IUniversalRouter(UNIVERSAL_ROUTER).execute(commands, inputs, block.timestamp);

        _returnFundsToVault();
    }

    /**
     * @dev Returns the arguments for the execute function
     * @param recipient The address of the recipient of the swapped tokens
     * @param amount0 First amount parameter in Command
     * @param amount1 Second amount parameter in Command
     * @param flavor The flavor of the strategy
     * @param flavorParams encoded path parameters in Cicada's proprietary format
     */
    function _getExecuteArgs(
        address recipient,
        uint256 amount0,
        uint256 amount1,
        Enums.Direction direction,
        Enums.Flavor flavor,
        bytes calldata flavorParams
    ) internal view returns (bytes memory commands, bytes[] memory inputs) {
        uint256 command = _getExecuteCommand(flavor, direction);

        commands = abi.encodePacked(bytes1(uint8(command)));

        inputs = _getExecuteInputArg(recipient, amount0, amount1, direction, flavor, flavorParams);

        return (commands, inputs);
    }

    /**
     * @notice Returns the command for the execute function
     * @param flavor The flavor of the strategy
     * @param direction The direction of the swap
     */
    function _getExecuteCommand(Enums.Flavor flavor, Enums.Direction direction) internal pure returns (uint256) {
        bool isQuoteToBase = direction == Enums.Direction.QuoteToBase;

        if (flavor == Enums.Flavor.UniV3QuoterV2) {
            if (isQuoteToBase) {
                return Commands.V3_SWAP_EXACT_OUT;
            } else {
                return Commands.V3_SWAP_EXACT_IN;
            }
        } else if (flavor == Enums.Flavor.UniV2Legacy) {
            if (isQuoteToBase) {
                return Commands.V2_SWAP_EXACT_OUT;
            } else {
                return Commands.V2_SWAP_EXACT_IN;
            }
        } else if (flavor == Enums.Flavor.UniV4) {
            return Commands.V4_SWAP;
        } else {
            revert InvalidFlavor(flavor);
        }
    }

    /**
     * @dev Returns the input argument for the execute function
     * @param vault The address of the Vault and the recipient of the swapped tokens
     * @param amount0 First amount parameter in Command
     * @param amount1 Second amount parameter in Command
     * @param flavor The flavor of the strategy
     * @param flavorParams encoded path parameters in Cicada's proprietary format
     */
    function _getExecuteInputArg(
        address vault,
        uint256 amount0,
        uint256 amount1,
        Enums.Direction direction,
        Enums.Flavor flavor,
        bytes calldata flavorParams
    ) internal view returns (bytes[] memory inputs) {
        inputs = new bytes[](1);

        if (flavor == Enums.Flavor.UniV3QuoterV2) {
            UniV3.UniV3PathFull memory pathFull = _getV3Path(flavorParams);
            bytes memory path = pathFull.encode();

            inputs[0] = abi.encode(vault, amount0, amount1, path, false);
        } else if (flavor == Enums.Flavor.UniV2Legacy) {
            address[] memory path = _getV2Path(direction, flavorParams);

            inputs[0] = abi.encode(vault, amount0, amount1, path, false);
        } else if (flavor == Enums.Flavor.UniV4) {
            inputs[0] = _getV4SwapActionsInputs(vault, direction, amount0, amount1, flavorParams);
        } else {
            revert InvalidFlavor(flavor);
        }
    }

    /**
     * @param flavorParams encoded path parameters in Cicada's proprietary format
     */
    function _getV2Path(Enums.Direction direction, bytes calldata flavorParams)
        internal
        view
        returns (address[] memory path)
    {
        Vault vault = Vault(payable(msg.sender));
        IERC20 baseToken = vault.baseToken();
        IERC20 quoteToken = vault.quoteToken();

        UniV2.UniV2PathVia memory pathVia = abi.decode(flavorParams, (UniV2.UniV2PathVia));

        path = pathVia.toPathFull(direction, address(baseToken), address(quoteToken));
    }

    /**
     * @dev in SWAP_EXACT_OUT command path is reversed, so path for both directions,
     * that utilize different IN/OUT commands (QuoteToBase + EXACT_OUT and BaseToQuote + EXACT_IN) is the same
     * @param flavorParams encoded path parameters in Cicada's proprietary format
     */
    function _getV3Path(bytes calldata flavorParams) internal view returns (UniV3.UniV3PathFull memory path) {
        Vault vault = Vault(payable(msg.sender));
        IERC20 baseToken = vault.baseToken();
        IERC20 quoteToken = vault.quoteToken();

        UniV3.UniV3SwapParams memory swapParams = abi.decode(flavorParams, (UniV3.UniV3SwapParams));
        UniV3.UniV3PathVia memory pathVia = swapParams.pathVia;

        path = pathVia.toPathFull(Enums.Direction.BaseToQuote, address(baseToken), address(quoteToken));
    }

    /**
     * @dev Returns the input argument for the execute function
     * @param direction The direction of the swap
     * @param amount0 Always a base token amount
     * @param amount1 Second amount parameter in Command (amountInMaximum or amountOutMinimum)
     * @param flavorParams encoded path parameters in Cicada's proprietary format
     */
    function _getV4SwapActionsInputs(
        address vault,
        Enums.Direction direction,
        uint256 amount0,
        uint256 amount1,
        bytes calldata flavorParams
    ) internal view returns (bytes memory input) {
        SwapParamsUniV4 memory swapParams = _parseSwapParamsUniV4(vault, direction, flavorParams);

        bytes memory actions = abi.encodePacked(
            _isBuy(direction) ? uint8(Actions.SWAP_EXACT_OUT) : uint8(Actions.SWAP_EXACT_IN),
            uint8(Actions.SETTLE_ALL),
            uint8(Actions.TAKE_ALL)
        );

        bytes[] memory params = new bytes[](3);

        Currency currencyIn;
        Currency currencyOut;

        uint256 amountIn;
        uint256 amountOut;

        if (_isBuy(direction)) {
            currencyIn = swapParams.path[0].intermediateCurrency;
            currencyOut = swapParams.currency;

            amountOut = amount0;
            amountIn = amount1;

            params[0] = abi.encode(
                IV4Router.ExactOutputParams({
                    currencyOut: currencyOut,
                    path: swapParams.path,
                    amountOut: uint128(amount0),
                    amountInMaximum: uint128(amount1)
                })
            );
        } else {
            currencyIn = swapParams.currency;
            currencyOut = swapParams.path[swapParams.path.length - 1].intermediateCurrency;

            amountIn = amount0;
            amountOut = amount1;

            params[0] = abi.encode(
                IV4Router.ExactInputParams({
                    currencyIn: currencyIn,
                    path: swapParams.path,
                    amountIn: uint128(amount0),
                    amountOutMinimum: uint128(amount1)
                })
            );
        }

        // First parameter: swap configuration
        // params[0] = abi.encode(swapParams.currency, swapParams.path, amount0, amount1);

        // Second parameter: specify input tokens for the swap
        // encode SETTLE_ALL parameters
        params[1] = abi.encode(currencyIn, amountIn);

        // Third parameter: specify output tokens from the swap
        params[2] = abi.encode(currencyOut, amountOut);

        return abi.encode(actions, params);
    }

    function _parseSwapParamsUniV4(address vault, Enums.Direction direction, bytes calldata flavorParams)
        internal
        view
        returns (SwapParamsUniV4 memory swapParams)
    {
        swapParams = _decodeUniV4SwapParams(flavorParams);

        if (swapParams.path.length == 0) revert EmptyPath();

        address baseToken = address(Vault(payable(vault)).baseToken());
        address quoteToken = address(Vault(payable(vault)).quoteToken());

        // require is used instead of revert, cos != operator is not overloaded for Currency type
        require(swapParams.currency == Currency.wrap(baseToken), BaseTokenIsNotExactCurrency());

        // TODO: add quoteToken check in path
        if (_isBuy(direction)) {
            bool quoteTokenIsFirstInPath = swapParams.path[0].intermediateCurrency == Currency.wrap(quoteToken);
            if (!quoteTokenIsFirstInPath) revert QuoteTokenIsNotInPathOrIncorrectlyPlaced();
        } else {
            bool quoteTokenIsLastInPath =
                swapParams.path[swapParams.path.length - 1].intermediateCurrency == Currency.wrap(quoteToken);
            if (!quoteTokenIsLastInPath) revert QuoteTokenIsNotInPathOrIncorrectlyPlaced();
        }
    }

    function _decodeUniV4SwapParams(bytes calldata flavorParams) internal view returns (SwapParamsUniV4 memory) {
        try this.decodeUniV4SwapParams(flavorParams) returns (SwapParamsUniV4 memory _swapParams) {
            return _swapParams;
        } catch {
            revert UnableToDecodeSwapParams(flavorParams);
        }
    }

    /// @dev workaround function to try/catch decode (try/catch is not supported for external functions)
    function decodeUniV4SwapParams(bytes calldata flavorParams) external pure returns (SwapParamsUniV4 memory) {
        return abi.decode(flavorParams, (SwapParamsUniV4));
    }

    /// @inheritdoc IUniswapStrategy
    function withdraw(address token, address recipient) external onlyOwner {
        if (token == address(0)) {
            uint256 amount = address(this).balance;
            payable(recipient).transfer(amount);
        } else {
            uint256 amount = IERC20(token).balanceOf(address(this));
            IERC20(token).safeTransfer(recipient, amount);
        }
    }

    /**
     * @dev Transfers any remaining balances of `baseToken` and `quoteToken` from the strategy contract
     * to the `vault`. Intended to avoid keeping idle funds on the strategy contract and returns them
     * to the vault in gas-efficient manner.
     * Internal function that checks the contract's current balances of `baseToken` and `quoteToken`,
     * and transfers them to the `vault` if the balances are greater than zero.
     */
    function _returnFundsToVault() internal {
        Vault vault = Vault(payable(msg.sender));
        IERC20 baseToken = vault.baseToken();
        IERC20 quoteToken = vault.quoteToken();

        uint256 baseTokenBalance = baseToken.balanceOf(address(this));
        if (baseTokenBalance > 0) {
            baseToken.safeTransfer(address(vault), baseTokenBalance);
        }
        uint256 quoteTokenBalance = quoteToken.balanceOf(address(this));
        if (quoteTokenBalance > 0) {
            quoteToken.safeTransfer(address(vault), quoteTokenBalance);
        }

        uint256 quoteTokenBalanceOnRouter = quoteToken.balanceOf(UNIVERSAL_ROUTER);

        if (quoteTokenBalanceOnRouter > 0) {
            bytes memory sweepCommand = abi.encodePacked(bytes1(uint8(Commands.SWEEP)));
            bytes[] memory sweepInputs = new bytes[](1);
            sweepInputs[0] = abi.encode(IERC20(quoteToken), address(vault), 0);

            IUniversalRouter(UNIVERSAL_ROUTER).execute(sweepCommand, sweepInputs, block.timestamp);
        }
    }

    function _approveTokenWithPermit2(address token, uint160 amount, uint48 expiration) internal {
        IERC20(token).forceApprove(PERMIT2, amount);
        IPermit2(PERMIT2).approve(token, UNIVERSAL_ROUTER, amount, expiration);
    }

    function _isBuy(Enums.Direction direction) internal pure returns (bool) {
        return direction == Enums.Direction.QuoteToBase;
    }
}
"
    },
    "src/Vault.sol": {
      "content": "// SPDX-License-Identifier: LicenseRef-CICADA-Proprietary
// SPDX-FileCopyrightText: (c) 2024 Cicada Software, CICADA DMCC. All rights reserved.
pragma solidity ^0.8.29;

import {SafeERC20} from "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
import {Address} from "@openzeppelin-contracts/utils/Address.sol";
import {IUniswapV2Router02} from "@uniswap-v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import {AccessControl} from "@openzeppelin-contracts/access/AccessControl.sol";
import {Math} from "@openzeppelin-contracts/utils/math/Math.sol";
import {IERC20} from "@openzeppelin-contracts/token/ERC20/IERC20.sol";

/**
 * @title Vault
 * @notice Manages funds and executes arbitrage operations on behalf of the DeGate arbitrage engine.
 * Handles asset approvals, delegates execution to the strategy, and enforces a USD TVL loss threshold to safeguard funds.
 * @dev The Vault interacts with a strategy contract to perform swaps and can be upgraded to integrate new strategies and DeFi protocols.
 */
contract Vault is AccessControl {
    using SafeERC20 for IERC20;
    using Address for address;

    struct VaultReserves {
        uint256 baseTokenBalance;
        uint256 baseTokenUsdValue;
        uint256 quoteTokenBalance;
        uint256 quoteTokenUsdValue;
        uint256 totalUsdValue;
    }

    // MANAGER_ROLE: Responsible for managing and upgrading the strategy contract.
    // Can grant MANAGER_ROLE and GATEWAY_EXECUTOR_ROLE roles.
    // Typically held by the platform staff via a multisignature wallet. This role allows
    // for the introduction of new methods and integrations with DeFi protocols.
    bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");

    // GATEWAY_EXECUTOR_ROLE: Held by the arbitrage engine gateway process, this EOA is responsible
    // for triggering arbitrage operations and covering the gas costs associated with these transactions.
    bytes32 public constant GATEWAY_EXECUTOR_ROLE = keccak256("GATEWAY_EXECUTOR_ROLE");

    // FINANCIER_ROLE: Held by the owner of the funds, which could be an externally owned
    // account (EOA) or a multisignature wallet. The holder of this role is responsible for
    // providing the funding for operations and has the exclusive ability to withdraw funds
    // from the vault.
    // Can grant FINANCIER_ROLE to other accounts.
    bytes32 public constant FINANCIER_ROLE = keccak256("FINANCIER_ROLE");

    // MAX_ALLOWED_LOSS_BPS represents the maximum allowed loss in basis points (bps).
    // A value of 1000 bps is equivalent to a 10% maximum loss allowed per single action
    uint256 public constant MAX_ALLOWED_SINGLE_LOSS_BPS = 100;
    uint256 public constant MAX_ALLOWED_CUM_LOSS_BPS = 1000;

    address public strategy;
    IERC20 public baseToken;
    IERC20 public quoteToken;
    address[] public reservesEvaluationPath;
    address[] public gatewayExecutors;
    IUniswapV2Router02 public router;
    string public constant version = "v0.1.3-flex";
    uint256 public allowedSingleLossBps;
    uint256 public allowedCumLossBps;
    uint256 public currentCumLossBps;
    uint256 public expectedGasBurn;
    bool public isFlex;

    // These events are emitted during the arbitrage execution process to track the state and results of operations.
    // allowing the arbitrage engine to index, fetch, and analyze the outcomes.
    // `PreExecState` captures the Vault's initial balances and USD TVL before execution.
    // `PostExecState` records the final balances and resulting USD TVL
    // `ExecResult` provides the net changes in balances and USD value.
    event PreExecState(VaultReserves);
    event PostExecState(VaultReserves);
    event ExecResult(int256 baseTokenBalanceChange, int256 quoteTokenBalanceChange, int256 totalUsdValueChange);

    event AllowedLossUpdated(uint256 allowedSingleLossBps, uint256 allowedCumLossBps);
    event StrategyUpdated(address previousStrategy, address newStrategy);
    event GatewayExecutorAdded(address executor);
    event GatewayExecutorRemoved(address executor);
    event ExpectedGasBurnUpdated(uint256 newExpectedGasBurn);

    /**
     * @dev Initializes the Vault with the provided parameters.
     * @param _baseToken Address of the base token (first token in the pair).
     * @param _quoteToken Address of the quote token (second token in the pair).
     * @param _reservesEvaluationPath Route to the USD stablecoin token (for USD valuation).
     * @param _router Address of the Uniswap V2 router (for price impact and USD value calculations).
     * @param _allowedSingleLossBps Allowed single-tx loss in 1/10000 basis points (modifiable by FINANCIER_ROLE).
     * @param _allowedCumLossBps Allowed cumulative loss loss in 1/10000 basis points (modifiable by FINANCIER_ROLE).
     * @param _strategy Address of the strategy contract (modifiable by MANAGER_ROLE).
     * @param _financier Address granted the FINANCIER_ROLE (responsible for funding operations).
     * @param _gatewayExecutors Addresses granted the GATEWAY_EXECUTOR_ROLE (able to initiate buy/sell operations).
     * @param _manager Address granted the MANAGER_ROLE (manages strategy upgrades).
     * @param _isFlex Flag to enable/disable flex mode
     */
    constructor(
        IERC20 _baseToken,
        IERC20 _quoteToken,
        address[] memory _reservesEvaluationPath,
        IUniswapV2Router02 _router,
        uint256 _allowedSingleLossBps,
        uint256 _allowedCumLossBps,
        uint256 _expectedGasBurn,
        address _strategy,
        address _financier,
        address[] memory _gatewayExecutors,
        address _manager,
        bool _isFlex
    ) {
        isFlex = _isFlex;
        _validateTokensAndRouter(address(_baseToken), address(_quoteToken), address(_router));
        baseToken = _baseToken;
        quoteToken = _quoteToken;
        router = _router;
        _validateEvaluationPath(_reservesEvaluationPath);
        reservesEvaluationPath = _reservesEvaluationPath;
        require(_allowedSingleLossBps <= MAX_ALLOWED_SINGLE_LOSS_BPS, "ALLOWED_SINGLE_LOSS_OVER_MAX");
        require(_allowedCumLossBps <= MAX_ALLOWED_CUM_LOSS_BPS, "ALLOWED_CUM_LOSS_OVER_MAX");
        allowedSingleLossBps = _allowedSingleLossBps;
        allowedCumLossBps = _allowedCumLossBps;
        strategy = _strategy;
        require(_expectedGasBurn > 200_000, "EXPECTED_GAS_BURN_TOO_LOW");
        expectedGasBurn = _expectedGasBurn;
        _setRoleAdmin(GATEWAY_EXECUTOR_ROLE, MANAGER_ROLE);
        _setRoleAdmin(MANAGER_ROLE, MANAGER_ROLE);
        _setRoleAdmin(FINANCIER_ROLE, FINANCIER_ROLE);
        _grantRole(FINANCIER_ROLE, _financier);
        for (uint256 i = 0; i < _gatewayExecutors.length; i++) {
            _addGatewayExecutor(_gatewayExecutors[i]);
        }
        _grantRole(MANAGER_ROLE, _manager);
    }

    function updateConfig(
        IERC20 _baseToken,
        IERC20 _quoteToken,
        address[] memory _reservesEvaluationPath,
        IUniswapV2Router02 _router,
        bool _isFlex
    ) external onlyRole(MANAGER_ROLE) {
        _validateTokensAndRouter(address(_baseToken), address(_quoteToken), address(_router));
        baseToken = _baseToken;
        quoteToken = _quoteToken;
        router = _router;
        _validateEvaluationPath(_reservesEvaluationPath);
        reservesEvaluationPath = _reservesEvaluationPath;
        isFlex = _isFlex;
    }

    receive() external payable {}

    /**
     * @notice Sets the expected gas burn value.
     * @param _expectedGasBurn The new expected gas burn value.
     */
    function setExpectedGasBurn(uint256 _expectedGasBurn) external onlyRole(MANAGER_ROLE) {
        require(_expectedGasBurn > 200_000, "EXPECTED_GAS_BURN_TOO_LOW");
        expectedGasBurn = _expectedGasBurn;
        emit ExpectedGasBurnUpdated(_expectedGasBurn);
    }

    /**
     * @notice Updates the allowed USD loss thresholds in basis points (bps).
     * @param _allowedSingleLossBps The new allowed single-operation loss in bps (1 bps = 0.01%).
     * @param _allowedCumLossBps The new allowed cumulative loss in bps (1 bps = 0.01%).
     *
     * Example: `setAllowedLoss(100);` sets the allowed loss to 1%.
     */
    function setAllowedLoss(uint256 _allowedSingleLossBps, uint256 _allowedCumLossBps)
        external
        onlyRole(FINANCIER_ROLE)
    {
        require(_allowedSingleLossBps <= MAX_ALLOWED_SINGLE_LOSS_BPS, "ALLOWED_SINGLE_LOSS_OVER_MAX");
        require(_allowedCumLossBps <= MAX_ALLOWED_CUM_LOSS_BPS, "ALLOWED_CUM_LOSS_OVER_MAX");
        allowedSingleLossBps = _allowedSingleLossBps;
        allowedCumLossBps = _allowedCumLossBps;
        currentCumLossBps = 0;
        emit AllowedLossUpdated(allowedSingleLossBps, allowedCumLossBps);
    }

    /**
     * @notice Updates the strategy contract used by the Vault.
     * @dev This function is used to update the strategy contract that the Vault interacts with for executing arbitrage operations.
     * Can only be called by an account with the MANAGER_ROLE (Typically held by the platform staff via a multisignature wallet)
     * @param _strategy The address of the new strategy contract.
     */
    function setStrategy(address _strategy) external onlyRole(MANAGER_ROLE) {
        require(_strategy != address(0), "STRATEGY_NOTSET");
        require(_strategy != strategy, "SAME_STRATEGY");
        // Revoke approvals from previous strategy for safety
        if (strategy != address(0)) {
            baseToken.forceApprove(strategy, 0);
            quoteToken.forceApprove(strategy, 0);
        }
        strategy = _strategy;
        emit StrategyUpdated(strategy, _strategy);
    }

    /**
     * @dev Adds a new gateway executor.
     * @param _executor The address of the executor to add.
     */
    function addGatewayExecutor(address _executor) external onlyRole(MANAGER_ROLE) {
        _addGatewayExecutor(_executor);
    }

    /**
     * @dev Removes an existing gateway executor.
     * @param _executor The address of the executor to remove.
     */
    function removeGatewayExecutor(address _executor) external onlyRole(MANAGER_ROLE) {
        _removeGatewayExecutor(_executor);
    }

    /**
     * @dev Ensures each gateway executor has at least _minWei wei on its baance.
     * @param _minWei The minimum balance units each executor should have.
     */
    function replenishExecutorsWei(uint256 _minWei) external onlyRole(MANAGER_ROLE) {
        _replenishExecutorsWei(_minWei);
    }

    /**
     * @dev Executes swap operations initiated by an external DeGate process with GATEWAY_EXECUTOR_ROLE.
     * Approves and flash-loans specified amounts of base and quote tokens from the Vault to the Strategy contract,
     * delegating control for execution. The Strategy completes the operation and returns the funds to the Vault.
     * While temporary USD losses due to slippage and DEX fees are expected, the modifier ensures losses
     * stay within the allowed threshold, mitigating potential mistakes.
     * @param _baseTokenAmount The amount of base tokens to approve and flash-loan to the Strategy.
     * @param _quoteTokenAmount The amount of quote tokens to approve and flash-loan to the Strategy.
     * @param _params Encoded function selector and parameters for the Strategy contract's execution.
     */
    function execute(uint256 _baseTokenAmount, uint256 _quoteTokenAmount, bytes calldata _params)
        external
        onlyRole(GATEWAY_EXECUTOR_ROLE)
    {
        baseToken.forceApprove(strategy, _baseTokenAmount);
        quoteToken.forceApprove(strategy, _quoteTokenAmount);
        VaultReserves memory vaultReservesBefore = getVaultReserves();

        emit PreExecState(vaultReservesBefore);

        strategy.functionCall(_params);

        VaultReserves memory vaultReservesAfter = getVaultReserves();

        if (!isFlex) {
            _trackAndEnforceLossLimits(vaultReservesBefore, vaultReservesAfter);
        }

        emit PostExecState(vaultReservesAfter);

        emit ExecResult(
            int256(vaultReservesAfter.baseTokenBalance) - int256(vaultReservesBefore.baseTokenBalance),
            int256(vaultReservesAfter.quoteTokenBalance) - int256(vaultReservesBefore.quoteTokenBalance),
            int256(vaultReservesAfter.totalUsdValue) - int256(vaultReservesBefore.totalUsdValue)
        );
        _replenishExecutorsWei(0);
    }

    /**
     * @notice Withdraws a specified amount of a given token from the Vault.
     * @dev Allows the funds' owner to withdraw baseToken, quoteToken, or recover any other tokens (e.g., mistakenly sent) from the Vault.
     * This function can only be called by an account with the FINANCIER_ROLE, representing the owner of the funds.
     * @param token The ERC20 token to withdraw. If the address is zero, withdraw native token (ETH).
     * @param amount The amount of the token to withdraw.
     */
    function withdraw(address token, uint256 amount) external onlyRole(FINANCIER_ROLE) {
        if (token == address(0)) {
            payable(msg.sender).transfer(amount);
        } else {
            IERC20(token).safeTransfer(msg.sender, amount);
        }
    }

    /**
     * @notice Calculates and returns the total USD value of the vault's reserves.
     * @dev This function retrieves the balances of two tokens (baseToken and quoteToken),
     *      converts their respective balances into USD value using a Uniswap-like router,
     *      and sums the USD values to return the total value of the reserves.
     *
     * @return usdValue The total USD value of the vault's reserves.
     */
    function getVaultReserves() public view returns (VaultReserves memory) {
        uint256 baseTokenBalance = baseToken.balanceOf(address(this));
        uint256 quoteTokenBalance = quoteToken.balanceOf(address(this));

        uint256 baseTokenUsdValue;
        uint256 quoteTokenUsdValue;

        if (!isFlex) {
            // If the route is not explicitly specified, evaluate baseToken by the direct route to quoteToken
            // and use theraw quote token balance as USD value
            if (reservesEvaluationPath.length == 0) {
                address[] memory fullEvaluationPath = new address[](2);
                fullEvaluationPath[0] = address(baseToken);
                fullEvaluationPath[1] = address(quoteToken);

                if (baseTokenBalance > 0) {
                    fullEvaluationPath[0] = address(baseToken);
                    baseTokenUsdValue = router.getAmountsOut(baseTokenBalance, fullEvaluationPath)[1];
                }

                quoteTokenUsdValue = quoteTokenBalance;
            } else {
                address[] memory fullEvaluationPath = new address[](reservesEvaluationPath.length + 1);
                for (uint256 i = 0; i < reservesEvaluationPath.length; i++) {
                    fullEvaluationPath[i + 1] = reservesEvaluationPath[i];
                }

                if (baseTokenBalance > 0) {
                    fullEvaluationPath[0] = address(baseToken);
                    baseTokenUsdValue = router.getAmountsOut(baseTokenBalance, fullEvaluationPath)[1];
                }

                if (quoteTokenBalance > 0) {
                    fullEvaluationPath[0] = address(quoteToken);
                    quoteTokenUsdValue = router.getAmountsOut(quoteTokenBalance, fullEvaluationPath)[1];
                }
            }
        }

        uint256 totalUsdValue = baseTokenUsdValue + quoteTokenUsdValue;

        VaultReserves memory reserves = VaultReserves({
            baseTokenBalance: baseTokenBalance,
            baseTokenUsdValue: baseTokenUsdValue,
            quoteTokenBalance: quoteTokenBalance,
            quoteTokenUsdValue: quoteTokenUsdValue,
            totalUsdValue: totalUsdValue
        });

        return reserves;
    }

    /**
     * @dev Adds a new gateway executor.
     * @param _executor The address of the executor to add.
     */
    function _addGatewayExecutor(address _executor) internal {
        require(_executor != address(0), "INVALID_EXECUTOR_ADDRESS");

        for (uint256 i = 0; i < gatewayExecutors.length; i++) {
            require(gatewayExecutors[i] != _executor, "EXECUTOR_ALREADY_EXISTS");
        }

        _grantRole(GATEWAY_EXECUTOR_ROLE, _executor);
        gatewayExecutors.push(_executor);
        emit GatewayExecutorAdded(_executor);
    }

    /**
     * @dev Removes an existing gateway executor.
     * @param _executor The address of the executor to remove.
     */
    function _removeGatewayExecutor(address _executor) internal {
        require(_executor != address(0), "INVALID_EXECUTOR_ADDRESS");
        _revokeRole(GATEWAY_EXECUTOR_ROLE, _executor);

        bool removed = false;

        // Remove executor from the array
        for (uint256 i = 0; i < gatewayExecutors.length; i++) {
            if (gatewayExecutors[i] == _executor) {
                gatewayExecutors[i] = gatewayExecutors[gatewayExecutors.length - 1];
                gatewayExecutors.pop();
                removed = true;
                break;
            }
        }

        require(removed, "EXECUTOR_NOT_FOUND");
        emit GatewayExecutorRemoved(_executor);
    }

    /**
     * @dev Ensures each gateway executor has at least _minWei.
     * @param _minWei The minimum balance each executor should have. If it's 0, it's auto-calculated
     */
    function _replenishExecutorsWei(uint256 _minWei) internal {
        if (_minWei == 0) {
            _minWei = expectedGasBurn * tx.gasprice;
        }
        for (uint256 i = 0; i < gatewayExecutors.length; i++) {
            address executor = gatewayExecutors[i];
            uint256 balance = executor.balance;
            if (balance < _minWei) {
                uint256 amountToSend = _minWei - balance;
                if (address(this).balance < amountToSend) {
                    // Exit the loop and function if the vault's balance is not enough
                    break;
                }
                payable(executor).transfer(amountToSend);
            }
        }
    }

    /**
     * @dev Tracks and enforces loss limits based on the change in reserves.
     * This function calculates the loss in USD value between the reserves before and after an operation.
     * It then converts this loss to basis points (bps) and ensures that the loss does not exceed the allowed single loss
     * and cumulative loss limits. If the loss exceeds these limits, the function reverts.
     *
     * @param reservesBefore The reserves before the operation.
     * @param reservesAfter The reserves after the operation.
     */
    function _trackAndEnforceLossLimits(VaultReserves memory reservesBefore, VaultReserves memory reservesAfter)
        internal
    {
        if (isFlex) return;
        if (reservesBefore.totalUsdValue > reservesAfter.totalUsdValue) {
            uint256 singleLossUsd = reservesBefore.totalUsdValue - reservesAfter.totalUsdValue;
            // Ensure singleLossBps is at least 1 if a loss occurred to prevent exploiting rounding errors
            uint256 singleLossBps = Math.max((singleLossUsd * 10000) / reservesBefore.totalUsdValue, 1);
            currentCumLossBps += singleLossBps;
            require(singleLossBps <= allowedSingleLossBps, "SINGLE_LOSS_EXCEEDS_ALLOWED");
            require(currentCumLossBps <= allowedCumLossBps, "CUM_LOSS_EXCEEDS_ALLOWED");
        }
    }

    function _validateTokensAndRouter(address _baseToken, address _quoteToken, address _router) internal pure {
        require(_baseToken != address(0), "BASE_TOKEN_NOT_SET");
        require(_quoteToken != address(0), "QUOTE_TOKEN_NOT_SET");
        require(_router != address(0), "ROUTER_NOT_SET");
        require(_baseToken != _quoteToken, "BASE_AND_QUOTE_EQUAL");
    }

    function _validateEvaluationPath(address[] memory _reservesEvaluationPath) internal view {
        if (isFlex) return;
        for (uint256 i = 0; i < _reservesEvaluationPath.length; i++) {
            require(_reservesEvaluationPath[i] != address(0), "EVALUATION_PATH_CONTAINS_ZERO");
            require(_reservesEvaluationPath[i] != address(baseToken), "EVALUATION_PATH_CONTAINS_BASE");
            require(_reservesEvaluationPath[i] != address(quoteToken), "EVALUATION_PATH_CONTAINS_QUOTE");
        }
    }
}
"
    },
    "dependencies/@openzeppelin-contracts-5.2.0/token/ERC20/utils/SafeERC20.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.2.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 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);
    }
}
"
    },
    "src/lib/Common.sol": {
      "content": "// SPDX-License-Identifier: LicenseRef-CICADA-Proprietary
// SPDX-FileCopyrightText: (c) 2024 Cicada Software, CICADA DMCC. All rights reserved.
pragma solidity ^0.8.29;

enum Exact {
    In,
    Out
}

library Enums {
    enum Direction {
        QuoteToBase,
        BaseToQuote
    }

    enum Flavor {
        UniV2Legacy,
        UniV3QuoterV2,
        BalancerV2,
        Curve,
        UniV4
    }
}
"
    },
    "src/lib/UniV2.sol": {
      "content": "// SPDX-License-Identifier: LicenseRef-CICADA-Proprietary
// SPDX-FileCopyrightText: (c) 2024 Cicada Software, CICADA DMCC. All rights reserved.
pragma solidity ^0.8.29;

import {Enums} from "./Common.sol";

/**
 * @title UniV2
 * @notice Library for handling Uniswap V2 specifics
 * @dev Provides a standard way to handle Uniswap V2 paths
 */
library UniV2 {
    /// @notice Represents a TRANSIT path (without in and out tokens) used internally in Cicada software
    struct UniV2PathVia {
        address[] tokens;
    }

    /**
     * @notice Converts a transit path (internal form of route used inside Cicada Perimeter)
     *   to a full path by adding in and out tokens
     * @param self Transit path struct containing intermediate tokens
     * @param direction The direction of the swap
     * @param baseToken Address of the base token
     * @param quoteToken Address of the quote token
     * @return Uniswap V2-compatible path
     */
    function toPathFull(UniV2PathVia memory self, Enums.Direction direction, address baseToken, address quoteToken)
        internal
        pure
        returns (address[] memory)
    {
        address[] memory path = new address[](self.tokens.length + 2);

        if (direction == Enums.Direction.BaseToQuote) {
            path[0] = baseToken;
            for (uint256 i = 0; i < self.tokens.length; i++) {
                path[i + 1] = self.tokens[i];
            }
            path[path.length - 1] = quoteToken;
        } else {
            path[0] = quoteToken;
            for (uint256 i = 0; i < self.tokens.length; i++) {
                path[i + 1] = self.tokens[self.tokens.length - 1 - i];
            }
            path[path.length - 1] = baseToken;
        }

        return path;
    }
}
"
    },
    "src/lib/UniV3.sol": {
      "content": "// SPDX-License-Identifier: LicenseRef-CICADA-Proprietary
// SPDX-FileCopyrightText: (c) 2024 Cicada Software, CICADA DMCC. All rights reserved.
pragma solidity ^0.8.29;

import {Enums} from "./Common.sol";

/**
 * @title UniV3
 * @notice Library for handling Uniswap V3 specifics
 * @dev Encodes token addresses and fee tiers into a bytes path for Uniswap V3 router
 */
library UniV3 {
    /// @notice Represents a FULL Uniswap V3 swap path with tokens and their corresponding pool fees
    struct UniV3PathFull {
        address[] tokens;
        uint24[] fees;
    }

    /// @notice Represents a TRANSIT path (without base and quote tokens) used internally in Cicada software
    struct UniV3PathVia {
        address[] tokens;
        uint24[] fees;
    }

    struct UniV3SwapParams {
        UniV3PathVia pathVia;
    }

    /**
     * @notice Encodes a path into bytes format required by Uniswap V3 router
     * @param self Path struct containing tokens and fees
     * @return Encoded path as bytes: token0 + fee0 + token1 + fee1 + ... + tokenN
     */
    function encode(UniV3PathFull memory self) internal pure returns (bytes memory) {
        require(validatePathFull(self), "UniV3Path: BAD_PFULL");

        bytes memory path = new bytes(0);
        for (uint256 i = 0; i < self.fees.length; i++) {
            path = bytes.concat(path, abi.encodePacked(self.tokens[i], self.fees[i]));
        }
        path = bytes.concat(path, abi.encodePacked(self.tokens[self.tokens.length - 1]));

        return path;
    }

    /**
     * @notice Converts a transit path (internal form of route used inside Cicada Perimeter)
     *   to a full path by adding base and quote tokens
     * @param self Transit path struct containing intermediate tokens and fees
     * @param baseToken Address of the base token
     * @param quoteToken Address of the quote token
     * @return UniV3-compatible Full path struct with tokens ordered based on swap direction
     */
    function toPathFull(UniV3PathVia memory self, Enums.Direction direction, address baseToken, address quoteToken)
        internal
        pure
        returns (UniV3PathFull memory)
    {
        require(validatePathVia(self), "UniV3Path: BAD_PVIA");
        address[] memory tokens = new address[](self.tokens.length + 2);
        if (direction == Enums.Direction.QuoteToBase) {
            self = reverse(self);
            tokens[0] = quoteToken;
            tokens[tokens.length - 1] = baseToken;
        } else if (direction == Enums.Direction.BaseToQuote) {
            tokens[0] = baseToken;
            tokens[tokens.length - 1] = quoteToken;
        } else {
            revert("UniV3Path: BAD_DIRECTION");
        }

        for (uint256 i = 0; i < self.tokens.length; i++) {
            tokens[i + 1] = self.tokens[i];
        }

        UniV3PathFull memory result = UniV3PathFull({tokens: tokens, fees: self.fees});
        require(validatePathFull(result), "UniV3Path: BAD_PFULL");
        return result;
    }

    /**
     * @notice Validates that the path has correct token and fee array lengths
     * @return true if tokens.length - 1 == fees.length and tokens not empty
     */
    function validatePathFull(UniV3PathFull memory self) internal pure returns (bool) {
        return self.fees.length > 0 && self.tokens.length == self.fees.length + 1;
    }

    /**
     * @notice Validates that the transit path has correct token and fee array lengths
     */
    function validatePathVia(UniV3PathVia memory self) internal pure returns (bool) {
        return self.fees.length > 0 && self.tokens.length == self.fees.length - 1;
    }

    /**
     * @notice Reverses the order of tokens and fees in a UniV3PathVia
     * @param self The UniV3PathVia struct to reverse
     * @return path UniV3PathVia with reversed tokens and fees arrays
     * @dev Used when converting Buy direction paths to maintain correct token ordering
     * @dev Requires fees.length == tokens.length + 1 to maintain valid path structure
     */
    function reverse(UniV3PathVia memory self) internal pure returns (UniV3PathVia memory) {
        uint256 tokensLength = self.tokens.length;
        uint256 feesLength = self.fees.length;
        require(feesLength == tokensLength + 1, "UniV3: BAD_PATH_LENGTHS");

        address[] memory reversedTokens = new address[](tokensLength);
        uint24[] memory reversedFees = new uint24[](feesLength);

        // Reverse tokens
        for (uint256 i = 0; i < tokensLength; i++) {
            reversedTokens[i] = self.tokens[tokensLength - 1 - i];
        }

        // Reverse fees
        for (uint256 i = 0; i < feesLength; i++) {
            reversedFees[i] = self.fees[feesLength - 1 - i];
        }

        return (UniV3PathVia({tokens: reversedTokens, fees: reversedFees}));
    }
}
"
    },
    "dependencies/@uniswap-universal-router-2.0.0/contracts/interfaces/IUniversalRouter.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;

interface IUniversalRouter {
    /// @notice Thrown when a required command has failed
    error ExecutionFailed(uint256 commandIndex, bytes message);

    /// @notice Thrown when attempting to send ETH directly to the contract
    error ETHNotAccepted();

    /// @notice Thrown when executing commands with an expired deadline
    error TransactionDeadlinePassed();

    /// @notice Thrown when attempting to execute commands and an incorrect number of inputs are provided
    error LengthMismatch();

    // @notice Thrown when an address that isn't WETH tries to send ETH to the router without calldata
    error InvalidEthSender();

    /// @notice Executes encoded commands along with provided inputs. Reverts if deadline has expired.
    /// @param commands A set of concatenated commands, each 1 byte in length
    /// @param inputs An array of byte strings containing abi encoded inputs for each command
    /// @param deadline The deadline by which the transaction must be executed
    function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) external payable;
}
"
    },
    "dependencies/uniswap-briefcase-0.1.29/src/protocols/permit2/interfaces/IPermit2.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity >=0.6.2;

import {IAllowanceTransfer} from './IAllowanceTransfer.sol';
import {ISignatureTransfer} from './ISignatureTransfer.sol';

/// @notice Permit2 handles signature-based transfers in SignatureTransfer and allowance-based transfers in AllowanceTransfer.
/// @dev Users must approve Permit2 before calling any of the transfer functions.
interface IPermit2 is ISignatureTransfer, IAllowanceTransfer {
// IPermit2 unifies the two interfaces so users have maximal flexibility with their approval.
}
"
    },
    "dependencies/@uniswap-universal-router-2.0.0/contracts/libraries/Commands.sol": {
      "content": "// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;

/// @title Commands
/// @notice Command Flags used to decode commands
library Commands {
    // Masks to extract certain bits of commands
    bytes1 internal constant FLAG_ALLOW_REVERT = 0x80;
    bytes1 internal constant COMMAND_TYPE_MASK = 0x3f;

    // Command Types. Maximum supported command at this moment is 0x3f.
    // The commands are executed in nested if blocks to minimise gas consumption

    // Command Types where value<=0x07, executed in the first nested-if block
    uint256 constant V3_SWAP_EXACT_IN = 0x00;
    uint256 constant V3_SWAP_EXACT_OUT = 0x01;
    uint256 constant PERMIT2_TRANSFER_FROM = 0x02;
    uint256 constant PERMIT2_PERMIT_BATCH = 0x03;
    uint256 constant SWEEP = 0x04;
    uint256 constant TRANSFER = 0x05;
    uint256 constant PAY_PORTION = 0x06;
    // COMMAND_PLACEHOLDER = 0x07;

    // Command Types where 0x08<=value<=0x0f, executed in the second nested-if block
    uint256 constant V2_SWAP_EXACT_IN = 0x08;
    uint256 constant V2_SWAP_EXACT_OUT = 0x09;
    uint256 constant PERMIT2_PERMIT = 0x0a;
    uint256 constant WRAP_ETH = 0x0b;
    uint256 constant UNWRAP_WETH = 0x0c;
    uint256 constant PERMIT2_TRANSFER_FROM_BATCH = 0x0d;
    uint256 constant BALANCE_CHECK_ERC20 = 0x0e;
    // COMMAND_PLACEHOLDER = 0x0f;

    // Command Types where 0x10<=value<=0x20, executed in the third nested-if block
    uint256 constant V4_SWAP = 0x10;
    uint256 constant V3_POSITION_MANAGER_PERMIT = 0x11;
    uint256 constant V3_POSITION_MANAGER_CALL = 0x12;
    uint256 constant V4_INITIALIZE_POOL = 0x13;
    uint256 constant V4_POSITION_MANAGER_CALL = 0x14;
    // COMMAND_PLACEHOLDER = 0x15 -> 0x20

    // Command Types where 0x21<=value<=0x3f
    uint256 constant EXECUTE_SUB_PLAN = 0x21;
    // COMMAND_PLACEHOLDER for 0x22 to 0x3f
}
"
    },
    "dependencies/@openzeppelin-contracts-5.2.0/token/ERC20/IERC20.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.20;

/**
 * @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);
}
"
    },
    "dependencies/uniswap-briefcase-0.1.29/src/protocols/v4-periphery/interfaces/IV4Router.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity >=0.6.2;

import {Currency} from '../../v4-core/types/Currency.sol';
import {PoolKey} from '../../v4-core/types/PoolKey.sol';
import {PathKey} from '../libraries/PathKey.sol';
import {IImmutableState} from './IImmutableState.sol';

/// @title IV4Router
/// @notice Interface for the V4Router contract
interface IV4Router is IImmutableState {
    /// @notice Emitted when an exactInput swap does not receive its minAmountOut
    error V4TooLittleReceived(uint256 minAmountOutReceived, uint256 amountReceived);
    /// @notice Emitted when an exactOutput is asked for more than its maxAmountIn
    error V4TooMuchRequested(uint256 maxAmountInRequested, uint256 amountRequested);

    /// @notice Parameters for a single-hop exact-input swap
    struct ExactInputSingleParams {
        PoolKey poolKey;
        bool zeroForOne;
        uint128 amountIn;
        uint128 amountOutMinimum;
        bytes hookData;
    }

    /// @notice Parameters for a multi-hop exact-input swap
    struct ExactInputParams {
        Currency currencyIn;
        PathKey[] path;
        uint128 amountIn;
        uint128 amountOutMinimum;
    }

    /// @notice Parameters for a single-hop exact-output swap
    struct ExactOutputS

Tags:
ERC20, ERC165, Multisig, Mintable, Burnable, Swap, Liquidity, Upgradeable, Multi-Signature, Factory|addr:0x6ae5b4ef5b97d1258f9352e07e34d04f29465cee|verified:true|block:23391104|tx:0x2d13da0c69afdc480a0d133b52d5f665482e5b03dc5e8dbf07bf505352ef1134|first_check:1758271380

Submitted on: 2025-09-19 10:43:01

Comments

Log in to comment.

No comments yet.