From f1284cc874aee7e1fc81cae29dfe007cc5b7bf80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bruno?= Date: Thu, 19 Dec 2024 18:04:44 -0300 Subject: [PATCH] Creating MEV Router --- pkg/interfaces/contracts/vault/IMevRouter.sol | 34 +++ .../contracts/vault/IMevTaxCollector.sol | 7 + pkg/interfaces/contracts/vault/IRouter.sol | 117 +-------- .../contracts/vault/IRouterSwap.sol | 127 ++++++++++ pkg/vault/contracts/MevRouter.sol | 97 ++++++++ pkg/vault/contracts/Router.sol | 202 +--------------- pkg/vault/contracts/RouterSwap.sol | 224 ++++++++++++++++++ pkg/vault/contracts/test/RouterMock.sol | 3 +- .../test/foundry/mutation/router/Router.t.sol | 5 +- 9 files changed, 499 insertions(+), 317 deletions(-) create mode 100644 pkg/interfaces/contracts/vault/IMevRouter.sol create mode 100644 pkg/interfaces/contracts/vault/IMevTaxCollector.sol create mode 100644 pkg/interfaces/contracts/vault/IRouterSwap.sol create mode 100644 pkg/vault/contracts/MevRouter.sol create mode 100644 pkg/vault/contracts/RouterSwap.sol diff --git a/pkg/interfaces/contracts/vault/IMevRouter.sol b/pkg/interfaces/contracts/vault/IMevRouter.sol new file mode 100644 index 000000000..5493c951d --- /dev/null +++ b/pkg/interfaces/contracts/vault/IMevRouter.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IRouterSwap } from "./IRouterSwap.sol"; +import { IMevTaxCollector } from "./IMevTaxCollector.sol"; + +interface IMevRouter is IRouterSwap { + struct MevRouterParams { + IMevTaxCollector mevTaxCollector; + uint256 mevTaxMultiplier; + uint256 priorityGasThreshold; + } + + event MevTaxCharged(address pool, uint256 mevTax); + + function isMevTaxEnabled() external view returns (bool isMevTaxEnabled); + + function enableMevTax() external; + + function disableMevTax() external; + + function getMevTaxCollector() external view returns (IMevTaxCollector mevTaxCollector); + + function setMevTaxCollector(IMevTaxCollector mevTaxCollector) external; + + function getMevTaxMultiplier() external view returns (uint256 mevTaxMultiplier); + + function setMevTaxMultiplier(uint256 mevTaxMultiplier) external; + + function getPriorityGasThreshold() external view returns (uint256 priorityGasThreshold); + + function setPriorityGasThreshold(uint256 priorityGasThreshold) external; +} diff --git a/pkg/interfaces/contracts/vault/IMevTaxCollector.sol b/pkg/interfaces/contracts/vault/IMevTaxCollector.sol new file mode 100644 index 000000000..213b62c19 --- /dev/null +++ b/pkg/interfaces/contracts/vault/IMevTaxCollector.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +interface IMevTaxCollector { + function chargeMevTax(address pool) external payable; +} diff --git a/pkg/interfaces/contracts/vault/IRouter.sol b/pkg/interfaces/contracts/vault/IRouter.sol index 1cab5179e..9000a0621 100644 --- a/pkg/interfaces/contracts/vault/IRouter.sol +++ b/pkg/interfaces/contracts/vault/IRouter.sol @@ -5,10 +5,11 @@ pragma solidity ^0.8.24; import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IRouterSwap } from "./IRouterSwap.sol"; import { AddLiquidityKind, RemoveLiquidityKind, SwapKind } from "./VaultTypes.sol"; /// @notice User-friendly interface to basic Vault operations: swap, add/remove liquidity, and associated queries. -interface IRouter { +interface IRouter is IRouterSwap { /*************************************************************************** Pool Initialization ***************************************************************************/ @@ -235,82 +236,6 @@ interface IRouter { uint256[] memory minAmountsOut ) external payable returns (uint256[] memory amountsOut); - /*************************************************************************** - Swaps - ***************************************************************************/ - - /** - * @notice Data for the swap hook. - * @param sender Account initiating the swap operation - * @param kind Type of swap (exact in or exact out) - * @param pool Address of the liquidity pool - * @param tokenIn Token to be swapped from - * @param tokenOut Token to be swapped to - * @param amountGiven Amount given based on kind of the swap (e.g., tokenIn for exact in) - * @param limit Maximum or minimum amount based on the kind of swap (e.g., maxAmountIn for exact out) - * @param deadline Deadline for the swap, after which it will revert - * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH - * @param userData Additional (optional) data sent with the swap request - */ - struct SwapSingleTokenHookParams { - address sender; - SwapKind kind; - address pool; - IERC20 tokenIn; - IERC20 tokenOut; - uint256 amountGiven; - uint256 limit; - uint256 deadline; - bool wethIsEth; - bytes userData; - } - - /** - * @notice Executes a swap operation specifying an exact input token amount. - * @param pool Address of the liquidity pool - * @param tokenIn Token to be swapped from - * @param tokenOut Token to be swapped to - * @param exactAmountIn Exact amounts of input tokens to send - * @param minAmountOut Minimum amount of tokens to be received - * @param deadline Deadline for the swap, after which it will revert - * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH - * @param userData Additional (optional) data sent with the swap request - * @return amountOut Calculated amount of output tokens to be received in exchange for the given input tokens - */ - function swapSingleTokenExactIn( - address pool, - IERC20 tokenIn, - IERC20 tokenOut, - uint256 exactAmountIn, - uint256 minAmountOut, - uint256 deadline, - bool wethIsEth, - bytes calldata userData - ) external payable returns (uint256 amountOut); - - /** - * @notice Executes a swap operation specifying an exact output token amount. - * @param pool Address of the liquidity pool - * @param tokenIn Token to be swapped from - * @param tokenOut Token to be swapped to - * @param exactAmountOut Exact amounts of input tokens to receive - * @param maxAmountIn Maximum amount of tokens to be sent - * @param deadline Deadline for the swap, after which it will revert - * @param userData Additional (optional) data sent with the swap request - * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH - * @return amountIn Calculated amount of input tokens to be sent in exchange for the requested output tokens - */ - function swapSingleTokenExactOut( - address pool, - IERC20 tokenIn, - IERC20 tokenOut, - uint256 exactAmountOut, - uint256 maxAmountIn, - uint256 deadline, - bool wethIsEth, - bytes calldata userData - ) external payable returns (uint256 amountIn); - /*************************************************************************** Queries ***************************************************************************/ @@ -459,42 +384,4 @@ interface IRouter { address pool, uint256 exactBptAmountIn ) external returns (uint256[] memory amountsOut); - - /** - * @notice Queries a swap operation specifying an exact input token amount without actually executing it. - * @param pool Address of the liquidity pool - * @param tokenIn Token to be swapped from - * @param tokenOut Token to be swapped to - * @param exactAmountIn Exact amounts of input tokens to send - * @param sender The sender passed to the operation. It can influence results (e.g., with user-dependent hooks) - * @param userData Additional (optional) data sent with the query request - * @return amountOut Calculated amount of output tokens to be received in exchange for the given input tokens - */ - function querySwapSingleTokenExactIn( - address pool, - IERC20 tokenIn, - IERC20 tokenOut, - uint256 exactAmountIn, - address sender, - bytes calldata userData - ) external returns (uint256 amountOut); - - /** - * @notice Queries a swap operation specifying an exact output token amount without actually executing it. - * @param pool Address of the liquidity pool - * @param tokenIn Token to be swapped from - * @param tokenOut Token to be swapped to - * @param exactAmountOut Exact amounts of input tokens to receive - * @param sender The sender passed to the operation. It can influence results (e.g., with user-dependent hooks) - * @param userData Additional (optional) data sent with the query request - * @return amountIn Calculated amount of input tokens to be sent in exchange for the requested output tokens - */ - function querySwapSingleTokenExactOut( - address pool, - IERC20 tokenIn, - IERC20 tokenOut, - uint256 exactAmountOut, - address sender, - bytes calldata userData - ) external returns (uint256 amountIn); } diff --git a/pkg/interfaces/contracts/vault/IRouterSwap.sol b/pkg/interfaces/contracts/vault/IRouterSwap.sol new file mode 100644 index 000000000..d22676cfa --- /dev/null +++ b/pkg/interfaces/contracts/vault/IRouterSwap.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { SwapKind } from "./VaultTypes.sol"; + +interface IRouterSwap { + /*************************************************************************** + Swaps + ***************************************************************************/ + + /** + * @notice Data for the swap hook. + * @param sender Account initiating the swap operation + * @param kind Type of swap (exact in or exact out) + * @param pool Address of the liquidity pool + * @param tokenIn Token to be swapped from + * @param tokenOut Token to be swapped to + * @param amountGiven Amount given based on kind of the swap (e.g., tokenIn for exact in) + * @param limit Maximum or minimum amount based on the kind of swap (e.g., maxAmountIn for exact out) + * @param deadline Deadline for the swap, after which it will revert + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @param userData Additional (optional) data sent with the swap request + */ + struct SwapSingleTokenHookParams { + address sender; + SwapKind kind; + address pool; + IERC20 tokenIn; + IERC20 tokenOut; + uint256 amountGiven; + uint256 limit; + uint256 deadline; + bool wethIsEth; + bytes userData; + } + + /** + * @notice Executes a swap operation specifying an exact input token amount. + * @param pool Address of the liquidity pool + * @param tokenIn Token to be swapped from + * @param tokenOut Token to be swapped to + * @param exactAmountIn Exact amounts of input tokens to send + * @param minAmountOut Minimum amount of tokens to be received + * @param deadline Deadline for the swap, after which it will revert + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @param userData Additional (optional) data sent with the swap request + * @return amountOut Calculated amount of output tokens to be received in exchange for the given input tokens + */ + function swapSingleTokenExactIn( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountIn, + uint256 minAmountOut, + uint256 deadline, + bool wethIsEth, + bytes calldata userData + ) external payable returns (uint256 amountOut); + + /** + * @notice Executes a swap operation specifying an exact output token amount. + * @param pool Address of the liquidity pool + * @param tokenIn Token to be swapped from + * @param tokenOut Token to be swapped to + * @param exactAmountOut Exact amounts of input tokens to receive + * @param maxAmountIn Maximum amount of tokens to be sent + * @param deadline Deadline for the swap, after which it will revert + * @param userData Additional (optional) data sent with the swap request + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @return amountIn Calculated amount of input tokens to be sent in exchange for the requested output tokens + */ + function swapSingleTokenExactOut( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountOut, + uint256 maxAmountIn, + uint256 deadline, + bool wethIsEth, + bytes calldata userData + ) external payable returns (uint256 amountIn); + + /*************************************************************************** + Queries + ***************************************************************************/ + + /** + * @notice Queries a swap operation specifying an exact input token amount without actually executing it. + * @param pool Address of the liquidity pool + * @param tokenIn Token to be swapped from + * @param tokenOut Token to be swapped to + * @param exactAmountIn Exact amounts of input tokens to send + * @param sender The sender passed to the operation. It can influence results (e.g., with user-dependent hooks) + * @param userData Additional (optional) data sent with the query request + * @return amountOut Calculated amount of output tokens to be received in exchange for the given input tokens + */ + function querySwapSingleTokenExactIn( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountIn, + address sender, + bytes calldata userData + ) external returns (uint256 amountOut); + + /** + * @notice Queries a swap operation specifying an exact output token amount without actually executing it. + * @param pool Address of the liquidity pool + * @param tokenIn Token to be swapped from + * @param tokenOut Token to be swapped to + * @param exactAmountOut Exact amounts of input tokens to receive + * @param sender The sender passed to the operation. It can influence results (e.g., with user-dependent hooks) + * @param userData Additional (optional) data sent with the query request + * @return amountIn Calculated amount of input tokens to be sent in exchange for the requested output tokens + */ + function querySwapSingleTokenExactOut( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountOut, + address sender, + bytes calldata userData + ) external returns (uint256 amountIn); +} diff --git a/pkg/vault/contracts/MevRouter.sol b/pkg/vault/contracts/MevRouter.sol new file mode 100644 index 000000000..23fe8ef9d --- /dev/null +++ b/pkg/vault/contracts/MevRouter.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IPermit2 } from "permit2/src/interfaces/IPermit2.sol"; + +import { IWETH } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/misc/IWETH.sol"; +import { IMevRouter } from "@balancer-labs/v3-interfaces/contracts/vault/IMevRouter.sol"; +import { IMevTaxCollector } from "@balancer-labs/v3-interfaces/contracts/vault/IMevTaxCollector.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; + +import { SingletonAuthentication } from "./SingletonAuthentication.sol"; +import { RouterSwap } from "./RouterSwap.sol"; + +contract MevRouter is IMevRouter, SingletonAuthentication, RouterSwap { + IMevTaxCollector internal mevTaxCollector; + + uint256 internal mevTaxMultiplier; + uint256 internal priorityGasThreshold; + + bool private _isMevTaxEnabled; + + constructor( + IVault vault, + IWETH weth, + IPermit2 permit2, + string memory routerVersion, + MevRouterParams memory params + ) SingletonAuthentication(vault) RouterSwap(vault, weth, permit2, routerVersion) { + mevTaxCollector = params.mevTaxCollector; + mevTaxMultiplier = params.mevTaxMultiplier; + priorityGasThreshold = params.priorityGasThreshold; + _isMevTaxEnabled = true; + } + + /// @inheritdoc IMevRouter + function isMevTaxEnabled() external view returns (bool) { + return _isMevTaxEnabled; + } + + /// @inheritdoc IMevRouter + function enableMevTax() external authenticate { + _isMevTaxEnabled = true; + } + + /// @inheritdoc IMevRouter + function disableMevTax() external authenticate { + _isMevTaxEnabled = false; + } + + /// @inheritdoc IMevRouter + function getMevTaxCollector() external view returns (IMevTaxCollector) { + return mevTaxCollector; + } + + /// @inheritdoc IMevRouter + function setMevTaxCollector(IMevTaxCollector newMevTaxCollector) external authenticate { + mevTaxCollector = newMevTaxCollector; + } + + /// @inheritdoc IMevRouter + function getMevTaxMultiplier() external view returns (uint256) { + return mevTaxMultiplier; + } + + /// @inheritdoc IMevRouter + function setMevTaxMultiplier(uint256 newMevTaxMultiplier) external authenticate { + mevTaxMultiplier = newMevTaxMultiplier; + } + + /// @inheritdoc IMevRouter + function getPriorityGasThreshold() external view returns (uint256) { + return priorityGasThreshold; + } + + /// @inheritdoc IMevRouter + function setPriorityGasThreshold(uint256 newPriorityGasThreshold) external authenticate { + priorityGasThreshold = newPriorityGasThreshold; + } + + function chargeMevTax(address pool) internal { + if (_isMevTaxEnabled == false) { + return; + } + + uint256 priorityGasPrice = tx.gasprice - block.basefee; + + if (priorityGasPrice < priorityGasThreshold) { + return; + } + + uint256 mevTax = priorityGasPrice * mevTaxMultiplier; + mevTaxCollector.chargeMevTax{ value: mevTax }(pool); + + emit MevTaxCharged(pool, mevTax); + } +} diff --git a/pkg/vault/contracts/Router.sol b/pkg/vault/contracts/Router.sol index 172f31280..821896fff 100644 --- a/pkg/vault/contracts/Router.sol +++ b/pkg/vault/contracts/Router.sol @@ -13,14 +13,14 @@ import { IRouter } from "@balancer-labs/v3-interfaces/contracts/vault/IRouter.so import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; -import { RouterCommon } from "./RouterCommon.sol"; +import { RouterSwap } from "./RouterSwap.sol"; /** * @notice Entrypoint for swaps, liquidity operations, and corresponding queries. * @dev The external API functions unlock the Vault, which calls back into the corresponding hook functions. * These interact with the Vault, transfer tokens, settle accounting, and handle wrapping and unwrapping ETH. */ -contract Router is IRouter, RouterCommon { +contract Router is IRouter, RouterSwap { using Address for address payable; using SafeCast for *; @@ -29,7 +29,7 @@ contract Router is IRouter, RouterCommon { IWETH weth, IPermit2 permit2, string memory routerVersion - ) RouterCommon(vault, weth, permit2, routerVersion) { + ) RouterSwap(vault, weth, permit2, routerVersion) { // solhint-disable-previous-line no-empty-blocks } @@ -554,124 +554,6 @@ contract Router is IRouter, RouterCommon { _returnEth(sender); } - /*************************************************************************** - Swaps - ***************************************************************************/ - - /// @inheritdoc IRouter - function swapSingleTokenExactIn( - address pool, - IERC20 tokenIn, - IERC20 tokenOut, - uint256 exactAmountIn, - uint256 minAmountOut, - uint256 deadline, - bool wethIsEth, - bytes calldata userData - ) external payable saveSender(msg.sender) returns (uint256) { - return - abi.decode( - _vault.unlock( - abi.encodeCall( - Router.swapSingleTokenHook, - SwapSingleTokenHookParams({ - sender: msg.sender, - kind: SwapKind.EXACT_IN, - pool: pool, - tokenIn: tokenIn, - tokenOut: tokenOut, - amountGiven: exactAmountIn, - limit: minAmountOut, - deadline: deadline, - wethIsEth: wethIsEth, - userData: userData - }) - ) - ), - (uint256) - ); - } - - /// @inheritdoc IRouter - function swapSingleTokenExactOut( - address pool, - IERC20 tokenIn, - IERC20 tokenOut, - uint256 exactAmountOut, - uint256 maxAmountIn, - uint256 deadline, - bool wethIsEth, - bytes calldata userData - ) external payable saveSender(msg.sender) returns (uint256) { - return - abi.decode( - _vault.unlock( - abi.encodeCall( - Router.swapSingleTokenHook, - SwapSingleTokenHookParams({ - sender: msg.sender, - kind: SwapKind.EXACT_OUT, - pool: pool, - tokenIn: tokenIn, - tokenOut: tokenOut, - amountGiven: exactAmountOut, - limit: maxAmountIn, - deadline: deadline, - wethIsEth: wethIsEth, - userData: userData - }) - ) - ), - (uint256) - ); - } - - /** - * @notice Hook for swaps. - * @dev Can only be called by the Vault. Also handles native ETH. - * @param params Swap parameters (see IRouter for struct definition) - * @return amountCalculated Token amount calculated by the pool math (e.g., amountOut for a exact in swap) - */ - function swapSingleTokenHook( - SwapSingleTokenHookParams calldata params - ) external nonReentrant onlyVault returns (uint256) { - (uint256 amountCalculated, uint256 amountIn, uint256 amountOut) = _swapHook(params); - - IERC20 tokenIn = params.tokenIn; - - _takeTokenIn(params.sender, tokenIn, amountIn, params.wethIsEth); - _sendTokenOut(params.sender, params.tokenOut, amountOut, params.wethIsEth); - - if (tokenIn == _weth) { - // Return the rest of ETH to sender - _returnEth(params.sender); - } - - return amountCalculated; - } - - function _swapHook( - SwapSingleTokenHookParams calldata params - ) internal returns (uint256 amountCalculated, uint256 amountIn, uint256 amountOut) { - // The deadline is timestamp-based: it should not be relied upon for sub-minute accuracy. - // solhint-disable-next-line not-rely-on-time - if (block.timestamp > params.deadline) { - revert SwapDeadline(); - } - - (amountCalculated, amountIn, amountOut) = _vault.swap( - VaultSwapParams({ - kind: params.kind, - pool: params.pool, - tokenIn: params.tokenIn, - tokenOut: params.tokenOut, - amountGivenRaw: params.amountGiven, - limitRaw: params.limit, - userData: params.userData - }) - ); - } - /******************************************************************************* Queries *******************************************************************************/ @@ -1002,82 +884,4 @@ contract Router is IRouter, RouterCommon { uint256[] memory minAmountsOut = new uint256[](_vault.getPoolTokens(pool).length); return _vault.removeLiquidityRecovery(pool, sender, exactBptAmountIn, minAmountsOut); } - - /// @inheritdoc IRouter - function querySwapSingleTokenExactIn( - address pool, - IERC20 tokenIn, - IERC20 tokenOut, - uint256 exactAmountIn, - address sender, - bytes memory userData - ) external saveSender(sender) returns (uint256 amountCalculated) { - return - abi.decode( - _vault.quote( - abi.encodeCall( - Router.querySwapHook, - SwapSingleTokenHookParams({ - sender: msg.sender, - kind: SwapKind.EXACT_IN, - pool: pool, - tokenIn: tokenIn, - tokenOut: tokenOut, - amountGiven: exactAmountIn, - limit: 0, - deadline: _MAX_AMOUNT, - wethIsEth: false, - userData: userData - }) - ) - ), - (uint256) - ); - } - - /// @inheritdoc IRouter - function querySwapSingleTokenExactOut( - address pool, - IERC20 tokenIn, - IERC20 tokenOut, - uint256 exactAmountOut, - address sender, - bytes memory userData - ) external saveSender(sender) returns (uint256 amountCalculated) { - return - abi.decode( - _vault.quote( - abi.encodeCall( - Router.querySwapHook, - SwapSingleTokenHookParams({ - sender: msg.sender, - kind: SwapKind.EXACT_OUT, - pool: pool, - tokenIn: tokenIn, - tokenOut: tokenOut, - amountGiven: exactAmountOut, - limit: _MAX_AMOUNT, - deadline: type(uint256).max, - wethIsEth: false, - userData: userData - }) - ) - ), - (uint256) - ); - } - - /** - * @notice Hook for swap queries. - * @dev Can only be called by the Vault. Also handles native ETH. - * @param params Swap parameters (see IRouter for struct definition) - * @return amountCalculated Token amount calculated by the pool math (e.g., amountOut for a exact in swap) - */ - function querySwapHook( - SwapSingleTokenHookParams calldata params - ) external nonReentrant onlyVault returns (uint256) { - (uint256 amountCalculated, , ) = _swapHook(params); - - return amountCalculated; - } } diff --git a/pkg/vault/contracts/RouterSwap.sol b/pkg/vault/contracts/RouterSwap.sol new file mode 100644 index 000000000..fef189fe7 --- /dev/null +++ b/pkg/vault/contracts/RouterSwap.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IPermit2 } from "permit2/src/interfaces/IPermit2.sol"; + +import { IWETH } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/misc/IWETH.sol"; +import { IRouterSwap } from "@balancer-labs/v3-interfaces/contracts/vault/IRouterSwap.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { SwapKind, VaultSwapParams } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { RouterCommon } from "./RouterCommon.sol"; + +contract RouterSwap is IRouterSwap, RouterCommon { + constructor( + IVault vault, + IWETH weth, + IPermit2 permit2, + string memory routerVersion + ) RouterCommon(vault, weth, permit2, routerVersion) { + // solhint-disable-previous-line no-empty-blocks + } + + /*************************************************************************** + Swaps + ***************************************************************************/ + + /// @inheritdoc IRouterSwap + function swapSingleTokenExactIn( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountIn, + uint256 minAmountOut, + uint256 deadline, + bool wethIsEth, + bytes calldata userData + ) external payable saveSender(msg.sender) returns (uint256) { + return + abi.decode( + _vault.unlock( + abi.encodeCall( + RouterSwap.swapSingleTokenHook, + SwapSingleTokenHookParams({ + sender: msg.sender, + kind: SwapKind.EXACT_IN, + pool: pool, + tokenIn: tokenIn, + tokenOut: tokenOut, + amountGiven: exactAmountIn, + limit: minAmountOut, + deadline: deadline, + wethIsEth: wethIsEth, + userData: userData + }) + ) + ), + (uint256) + ); + } + + /// @inheritdoc IRouterSwap + function swapSingleTokenExactOut( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountOut, + uint256 maxAmountIn, + uint256 deadline, + bool wethIsEth, + bytes calldata userData + ) external payable saveSender(msg.sender) returns (uint256) { + return + abi.decode( + _vault.unlock( + abi.encodeCall( + RouterSwap.swapSingleTokenHook, + SwapSingleTokenHookParams({ + sender: msg.sender, + kind: SwapKind.EXACT_OUT, + pool: pool, + tokenIn: tokenIn, + tokenOut: tokenOut, + amountGiven: exactAmountOut, + limit: maxAmountIn, + deadline: deadline, + wethIsEth: wethIsEth, + userData: userData + }) + ) + ), + (uint256) + ); + } + + /** + * @notice Hook for swaps. + * @dev Can only be called by the Vault. Also handles native ETH. + * @param params Swap parameters (see IRouter for struct definition) + * @return amountCalculated Token amount calculated by the pool math (e.g., amountOut for a exact in swap) + */ + function swapSingleTokenHook( + SwapSingleTokenHookParams calldata params + ) external nonReentrant onlyVault returns (uint256) { + (uint256 amountCalculated, uint256 amountIn, uint256 amountOut) = _swapHook(params); + + IERC20 tokenIn = params.tokenIn; + + _takeTokenIn(params.sender, tokenIn, amountIn, params.wethIsEth); + _sendTokenOut(params.sender, params.tokenOut, amountOut, params.wethIsEth); + + if (tokenIn == _weth) { + // Return the rest of ETH to sender + _returnEth(params.sender); + } + + return amountCalculated; + } + + function _swapHook( + SwapSingleTokenHookParams calldata params + ) internal returns (uint256 amountCalculated, uint256 amountIn, uint256 amountOut) { + // The deadline is timestamp-based: it should not be relied upon for sub-minute accuracy. + // solhint-disable-next-line not-rely-on-time + if (block.timestamp > params.deadline) { + revert SwapDeadline(); + } + + (amountCalculated, amountIn, amountOut) = _vault.swap( + VaultSwapParams({ + kind: params.kind, + pool: params.pool, + tokenIn: params.tokenIn, + tokenOut: params.tokenOut, + amountGivenRaw: params.amountGiven, + limitRaw: params.limit, + userData: params.userData + }) + ); + } + + /******************************************************************************* + Queries + *******************************************************************************/ + + /// @inheritdoc IRouterSwap + function querySwapSingleTokenExactIn( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountIn, + address sender, + bytes memory userData + ) external saveSender(sender) returns (uint256 amountCalculated) { + return + abi.decode( + _vault.quote( + abi.encodeCall( + RouterSwap.querySwapHook, + SwapSingleTokenHookParams({ + sender: msg.sender, + kind: SwapKind.EXACT_IN, + pool: pool, + tokenIn: tokenIn, + tokenOut: tokenOut, + amountGiven: exactAmountIn, + limit: 0, + deadline: _MAX_AMOUNT, + wethIsEth: false, + userData: userData + }) + ) + ), + (uint256) + ); + } + + /// @inheritdoc IRouterSwap + function querySwapSingleTokenExactOut( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountOut, + address sender, + bytes memory userData + ) external saveSender(sender) returns (uint256 amountCalculated) { + return + abi.decode( + _vault.quote( + abi.encodeCall( + RouterSwap.querySwapHook, + SwapSingleTokenHookParams({ + sender: msg.sender, + kind: SwapKind.EXACT_OUT, + pool: pool, + tokenIn: tokenIn, + tokenOut: tokenOut, + amountGiven: exactAmountOut, + limit: _MAX_AMOUNT, + deadline: type(uint256).max, + wethIsEth: false, + userData: userData + }) + ) + ), + (uint256) + ); + } + + /** + * @notice Hook for swap queries. + * @dev Can only be called by the Vault. Also handles native ETH. + * @param params Swap parameters (see IRouter for struct definition) + * @return amountCalculated Token amount calculated by the pool math (e.g., amountOut for a exact in swap) + */ + function querySwapHook( + SwapSingleTokenHookParams calldata params + ) external nonReentrant onlyVault returns (uint256) { + (uint256 amountCalculated, , ) = _swapHook(params); + + return amountCalculated; + } +} diff --git a/pkg/vault/contracts/test/RouterMock.sol b/pkg/vault/contracts/test/RouterMock.sol index 10c1084f3..0396b5653 100644 --- a/pkg/vault/contracts/test/RouterMock.sol +++ b/pkg/vault/contracts/test/RouterMock.sol @@ -16,6 +16,7 @@ import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; import { RevertCodec } from "@balancer-labs/v3-solidity-utils/contracts/helpers/RevertCodec.sol"; import { Router } from "../Router.sol"; +import { RouterSwap } from "../RouterSwap.sol"; string constant MOCK_ROUTER_VERSION = "Mock Router v1"; @@ -75,7 +76,7 @@ contract RouterMock is Router { try _vault.quoteAndRevert( abi.encodeCall( - Router.querySwapHook, + RouterSwap.querySwapHook, SwapSingleTokenHookParams({ sender: msg.sender, kind: SwapKind.EXACT_IN, diff --git a/pkg/vault/test/foundry/mutation/router/Router.t.sol b/pkg/vault/test/foundry/mutation/router/Router.t.sol index 0bc8d0765..4f71c5963 100644 --- a/pkg/vault/test/foundry/mutation/router/Router.t.sol +++ b/pkg/vault/test/foundry/mutation/router/Router.t.sol @@ -10,6 +10,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IRouterCommon } from "@balancer-labs/v3-interfaces/contracts/vault/IRouterCommon.sol"; import { IVaultErrors } from "@balancer-labs/v3-interfaces/contracts/vault/IVaultErrors.sol"; import { IRouter } from "@balancer-labs/v3-interfaces/contracts/vault/IRouter.sol"; +import { IRouterSwap } from "@balancer-labs/v3-interfaces/contracts/vault/IRouterSwap.sol"; import { AddLiquidityKind, RemoveLiquidityKind, @@ -109,7 +110,7 @@ contract RouterMutationTest is BaseVaultTest { } function testSwapSingleTokenHookWhenNotVault() public { - IRouter.SwapSingleTokenHookParams memory params = IRouter.SwapSingleTokenHookParams( + IRouterSwap.SwapSingleTokenHookParams memory params = IRouterSwap.SwapSingleTokenHookParams( msg.sender, SwapKind.EXACT_IN, pool, @@ -152,7 +153,7 @@ contract RouterMutationTest is BaseVaultTest { } function testQuerySwapHookWhenNotVault() public { - IRouter.SwapSingleTokenHookParams memory params = IRouter.SwapSingleTokenHookParams( + IRouterSwap.SwapSingleTokenHookParams memory params = IRouterSwap.SwapSingleTokenHookParams( msg.sender, SwapKind.EXACT_IN, pool,