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
Submitted on: 2025-09-19 10:43:01
Comments
Log in to comment.
No comments yet.