From 3389027faffc22e3b91cdcbcebee81732c7ee53a Mon Sep 17 00:00:00 2001 From: elshan_eth <22689890+elshan-eth@users.noreply.github.com> Date: Thu, 9 Jan 2025 19:21:28 +0100 Subject: [PATCH 1/4] first iteration --- pkg/interfaces/contracts/vault/IRouter.sol | 119 +---------- .../contracts/vault/IRouterSwap.sol | 128 +++++++++++ pkg/vault/contracts/AggregatorsRouter.sol | 201 ++++++++++++++++++ pkg/vault/contracts/Router.sol | 13 +- pkg/vault/contracts/test/RouterMock.sol | 5 +- .../test/foundry/AggregatorsRouter.t.sol | 108 ++++++++++ .../test/foundry/mutation/router/Router.t.sol | 5 +- .../foundry/utils/VaultContractsDeployer.sol | 18 ++ 8 files changed, 471 insertions(+), 126 deletions(-) create mode 100644 pkg/interfaces/contracts/vault/IRouterSwap.sol create mode 100644 pkg/vault/contracts/AggregatorsRouter.sol create mode 100644 pkg/vault/test/foundry/AggregatorsRouter.t.sol diff --git a/pkg/interfaces/contracts/vault/IRouter.sol b/pkg/interfaces/contracts/vault/IRouter.sol index 1cab5179e..af337e98f 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 { AddLiquidityKind, RemoveLiquidityKind, SwapKind } from "./VaultTypes.sol"; +import { AddLiquidityKind, RemoveLiquidityKind } from "./VaultTypes.sol"; +import { IRouterSwap } from "./IRouterSwap.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..14b78436e --- /dev/null +++ b/pkg/interfaces/contracts/vault/IRouterSwap.sol @@ -0,0 +1,128 @@ +// 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"; + +/// @notice User-friendly interface to basic Vault operations: swap, add/remove liquidity, and associated queries. +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/AggregatorsRouter.sol b/pkg/vault/contracts/AggregatorsRouter.sol new file mode 100644 index 000000000..6a934f891 --- /dev/null +++ b/pkg/vault/contracts/AggregatorsRouter.sol @@ -0,0 +1,201 @@ +// 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 { IAllowanceTransfer } from "permit2/src/interfaces/IAllowanceTransfer.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 "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { RouterCommon } from "./RouterCommon.sol"; + +/** + * @notice Entrypoint for aggregators who want make swaps without build in payment logic. + * @dev The external API functions unlock the Vault, which calls back into the corresponding hook functions. + * These interact with the Vault and settle accounting. + */ +contract AggregatorsRouter is IRouterSwap, RouterCommon { + error OperationNotSupported(string detail); + + constructor( + IVault vault, + IWETH weth, + string memory routerVersion + ) RouterCommon(vault, weth, IPermit2(address(0x00)), 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) { + if (wethIsEth) { + revert OperationNotSupported("ETH operations are not supported"); + } + + return + abi.decode( + _vault.unlock( + abi.encodeCall( + AggregatorsRouter.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, + IERC20, + IERC20, + uint256, + uint256, + uint256, + bool, + bytes calldata + ) external payable returns (uint256) { + revert OperationNotSupported("swapSingleTokenExactOut is not supported"); // TODO: will implement this in next PR + } + + /** + * @notice Hook for swaps. + * @dev Can only be called by the Vault. Also handles native ETH. + * @param params Swap parameters (see IRouterSwap 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); + + _vault.settle(params.tokenIn, amountIn); + _sendTokenOut(params.sender, params.tokenOut, amountOut, false); + + 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( + AggregatorsRouter.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, + IERC20, + IERC20, + uint256, + address, + bytes memory + ) external pure returns (uint256) { + revert OperationNotSupported("swapSingleTokenExactOut is not supported"); // TODO: will implement this in next PR + } + + /** + * @notice Hook for swap queries. + * @dev Can only be called by the Vault. Also handles native ETH. + * @param params Swap parameters (see IRouterSwap 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; + } + + /*************************************************************************** + Overrides + ***************************************************************************/ + function permitBatchAndCall( + PermitApproval[] calldata, + bytes[] calldata, + IAllowanceTransfer.PermitBatch calldata, + bytes calldata, + bytes[] calldata + ) external payable override returns (bytes[] memory) { + revert OperationNotSupported("permit2 is not supported"); + } +} diff --git a/pkg/vault/contracts/Router.sol b/pkg/vault/contracts/Router.sol index 23bb64f47..aed3ffa22 100644 --- a/pkg/vault/contracts/Router.sol +++ b/pkg/vault/contracts/Router.sol @@ -10,6 +10,7 @@ import { IPermit2 } from "permit2/src/interfaces/IPermit2.sol"; import { IWETH } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/misc/IWETH.sol"; import { IRouter } from "@balancer-labs/v3-interfaces/contracts/vault/IRouter.sol"; +import { IRouterSwap } from "@balancer-labs/v3-interfaces/contracts/vault/IRouterSwap.sol"; import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; @@ -558,7 +559,7 @@ contract Router is IRouter, RouterCommon { Swaps ***************************************************************************/ - /// @inheritdoc IRouter + /// @inheritdoc IRouterSwap function swapSingleTokenExactIn( address pool, IERC20 tokenIn, @@ -592,7 +593,7 @@ contract Router is IRouter, RouterCommon { ); } - /// @inheritdoc IRouter + /// @inheritdoc IRouterSwap function swapSingleTokenExactOut( address pool, IERC20 tokenIn, @@ -629,7 +630,7 @@ contract Router is IRouter, RouterCommon { /** * @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) + * @param params Swap parameters (see IRouterSwap for struct definition) * @return amountCalculated Token amount calculated by the pool math (e.g., amountOut for a exact in swap) */ function swapSingleTokenHook( @@ -1003,7 +1004,7 @@ contract Router is IRouter, RouterCommon { return _vault.removeLiquidityRecovery(pool, sender, exactBptAmountIn, minAmountsOut); } - /// @inheritdoc IRouter + /// @inheritdoc IRouterSwap function querySwapSingleTokenExactIn( address pool, IERC20 tokenIn, @@ -1035,7 +1036,7 @@ contract Router is IRouter, RouterCommon { ); } - /// @inheritdoc IRouter + /// @inheritdoc IRouterSwap function querySwapSingleTokenExactOut( address pool, IERC20 tokenIn, @@ -1070,7 +1071,7 @@ contract Router is IRouter, RouterCommon { /** * @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) + * @param params Swap parameters (see IRouterSwap for struct definition) * @return amountCalculated Token amount calculated by the pool math (e.g., amountOut for a exact in swap) */ function querySwapHook( diff --git a/pkg/vault/contracts/test/RouterMock.sol b/pkg/vault/contracts/test/RouterMock.sol index 10c1084f3..3ab6f38a5 100644 --- a/pkg/vault/contracts/test/RouterMock.sol +++ b/pkg/vault/contracts/test/RouterMock.sol @@ -10,6 +10,7 @@ import { IPermit2 } from "permit2/src/interfaces/IPermit2.sol"; import { IWETH } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/misc/IWETH.sol"; import { SwapKind } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; import { IRouter } from "@balancer-labs/v3-interfaces/contracts/vault/IRouter.sol"; +import { IRouterSwap } from "@balancer-labs/v3-interfaces/contracts/vault/IRouterSwap.sol"; import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; @@ -48,12 +49,12 @@ contract RouterMock is Router { } function manualReentrancySwapSingleTokenHook() external nonReentrant { - IRouter.SwapSingleTokenHookParams memory params; + IRouterSwap.SwapSingleTokenHookParams memory params; Router(payable(this)).swapSingleTokenHook(params); } function manualReentrancyQuerySwapHook() external nonReentrant { - IRouter.SwapSingleTokenHookParams memory params; + IRouterSwap.SwapSingleTokenHookParams memory params; Router(payable(this)).querySwapHook(params); } diff --git a/pkg/vault/test/foundry/AggregatorsRouter.t.sol b/pkg/vault/test/foundry/AggregatorsRouter.t.sol new file mode 100644 index 000000000..e8ba68dc7 --- /dev/null +++ b/pkg/vault/test/foundry/AggregatorsRouter.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; +import { IVaultErrors } from "@balancer-labs/v3-interfaces/contracts/vault/IVaultErrors.sol"; +import { TokenConfig } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; + +import { EVMCallModeHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/EVMCallModeHelpers.sol"; +import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; +import { ArrayHelpers } from "@balancer-labs/v3-solidity-utils/contracts/test/ArrayHelpers.sol"; + +import { RateProviderMock } from "../../contracts/test/RateProviderMock.sol"; +import { AggregatorsRouter } from "../../contracts/AggregatorsRouter.sol"; +import { PoolMock } from "../../contracts/test/PoolMock.sol"; + +import { PoolFactoryMock, BaseVaultTest } from "./utils/BaseVaultTest.sol"; + +contract AggregatorsRouterTest is BaseVaultTest { + using CastingHelpers for address[]; + using ArrayHelpers for *; + + string version = "test"; + AggregatorsRouter internal aggregatorsRouter; + + // Track the indices for the standard dai/usdc pool. + uint256 internal daiIdx; + uint256 internal usdcIdx; + + function setUp() public virtual override { + rateProvider = deployRateProviderMock(); + + BaseVaultTest.setUp(); + aggregatorsRouter = deployAggregatorsRouter(IVault(address(vault)), weth, version); + } + + function createPool() internal override returns (address newPool, bytes memory poolArgs) { + string memory name = "ERC20 Pool"; + string memory symbol = "ERC20POOL"; + + newPool = address(deployPoolMock(IVault(address(vault)), name, symbol)); + vm.label(newPool, "pool"); + + IRateProvider[] memory rateProviders = new IRateProvider[](2); + rateProviders[0] = rateProvider; + rateProviders[1] = rateProvider; + bool[] memory paysYieldFees = new bool[](2); + paysYieldFees[0] = true; + paysYieldFees[1] = true; + + PoolFactoryMock(poolFactory).registerTestPool( + newPool, + vault.buildTokenConfig( + [address(dai), address(usdc)].toMemoryArray().asIERC20(), + rateProviders, + paysYieldFees + ), + poolHooksContract, + lp + ); + (daiIdx, usdcIdx) = getSortedIndexes(address(dai), address(usdc)); + + poolArgs = abi.encode(vault, name, symbol); + } + + function testQuerySwap() public { + vm.prank(bob); + vm.expectRevert(EVMCallModeHelpers.NotStaticCall.selector); + aggregatorsRouter.querySwapSingleTokenExactIn(pool, usdc, dai, 1e18, address(this), bytes("")); + } + + function testSwapExactInWithoutPayment() public { + vm.prank(alice); + vm.expectRevert(IVaultErrors.BalanceNotSettled.selector); + aggregatorsRouter.swapSingleTokenExactIn(address(pool), usdc, dai, 1e18, 0, MAX_UINT256, false, bytes("")); + } + + function testSwapExactIn_Fuzz(uint256 swapAmount) public { + swapAmount = bound(swapAmount, 1e18, vault.getPoolData(address(pool)).balancesLiveScaled18[daiIdx]); + + vm.startPrank(alice); + usdc.transfer(address(vault), swapAmount); + + uint256 outputTokenAmount = aggregatorsRouter.swapSingleTokenExactIn( + address(pool), + usdc, + dai, + swapAmount, + 0, + MAX_UINT256, + false, + bytes("") + ); + vm.stopPrank(); + + assertEq(usdc.balanceOf(alice), defaultAccountBalance() - swapAmount, "Wrong WETH balance"); + assertEq(dai.balanceOf(alice), defaultAccountBalance() + outputTokenAmount, "Wrong DAI balance"); + } + + function testRouterVersion() public view { + assertEq(aggregatorsRouter.version(), version, "Router version mismatch"); + } +} 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, diff --git a/pkg/vault/test/foundry/utils/VaultContractsDeployer.sol b/pkg/vault/test/foundry/utils/VaultContractsDeployer.sol index 264ad2eff..0b4edea00 100644 --- a/pkg/vault/test/foundry/utils/VaultContractsDeployer.sol +++ b/pkg/vault/test/foundry/utils/VaultContractsDeployer.sol @@ -23,6 +23,7 @@ import { BatchRouterMock } from "../../../contracts/test/BatchRouterMock.sol"; import { ERC20MultiTokenMock } from "../../../contracts/test/ERC20MultiTokenMock.sol"; import { LinearBasePoolMathMock } from "../../../contracts/test/LinearBasePoolMathMock.sol"; import { ProtocolFeeController } from "../../../contracts/ProtocolFeeController.sol"; +import { AggregatorsRouter } from "../../../contracts/AggregatorsRouter.sol"; import { VaultExtensionMock } from "../../../contracts/test/VaultExtensionMock.sol"; import { VaultAdminMock } from "../../../contracts/test/VaultAdminMock.sol"; import { VaultMock } from "../../../contracts/test/VaultMock.sol"; @@ -274,6 +275,23 @@ contract VaultContractsDeployer is BaseContractsDeployer { } } + function deployAggregatorsRouter( + IVault vault, + IWETH weth, + string memory version + ) internal returns (AggregatorsRouter) { + if (reusingArtifacts) { + return + AggregatorsRouter( + payable( + deployCode(_computeVaultPath(type(AggregatorsRouter).name), abi.encode(vault, weth, version)) + ) + ); + } else { + return new AggregatorsRouter(vault, weth, version); + } + } + function deployBufferRouterMock(IVault vault, IWETH weth, IPermit2 permit2) internal returns (BufferRouterMock) { if (reusingArtifacts) { return From 7d61bf347ecb30defaf6593edb64a29e25900372 Mon Sep 17 00:00:00 2001 From: elshan_eth <22689890+elshan-eth@users.noreply.github.com> Date: Thu, 9 Jan 2025 19:26:10 +0100 Subject: [PATCH 2/4] fix codestyle --- pkg/vault/contracts/AggregatorsRouter.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/vault/contracts/AggregatorsRouter.sol b/pkg/vault/contracts/AggregatorsRouter.sol index 6a934f891..edd271db8 100644 --- a/pkg/vault/contracts/AggregatorsRouter.sol +++ b/pkg/vault/contracts/AggregatorsRouter.sol @@ -82,7 +82,8 @@ contract AggregatorsRouter is IRouterSwap, RouterCommon { bool, bytes calldata ) external payable returns (uint256) { - revert OperationNotSupported("swapSingleTokenExactOut is not supported"); // TODO: will implement this in next PR + // TODO: will implement this in next PR + revert OperationNotSupported("swapSingleTokenExactOut is not supported"); } /** @@ -169,7 +170,8 @@ contract AggregatorsRouter is IRouterSwap, RouterCommon { address, bytes memory ) external pure returns (uint256) { - revert OperationNotSupported("swapSingleTokenExactOut is not supported"); // TODO: will implement this in next PR + // TODO: will implement this in next PR + revert OperationNotSupported("swapSingleTokenExactOut is not supported"); } /** From 6fc3692bbad8b7640ee760dc1228a0d816ee7da0 Mon Sep 17 00:00:00 2001 From: elshan_eth <22689890+elshan-eth@users.noreply.github.com> Date: Thu, 9 Jan 2025 20:01:10 +0100 Subject: [PATCH 3/4] fix hooks tests --- pkg/pool-hooks/test/foundry/LotteryHookExample.t.sol | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/pool-hooks/test/foundry/LotteryHookExample.t.sol b/pkg/pool-hooks/test/foundry/LotteryHookExample.t.sol index ebf8015cc..5748eacf0 100644 --- a/pkg/pool-hooks/test/foundry/LotteryHookExample.t.sol +++ b/pkg/pool-hooks/test/foundry/LotteryHookExample.t.sol @@ -7,6 +7,7 @@ import "forge-std/Test.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IRouter } from "@balancer-labs/v3-interfaces/contracts/vault/IRouter.sol"; +import { IRouterSwap } from "@balancer-labs/v3-interfaces/contracts/vault/IRouterSwap.sol"; import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; import { LiquidityManagement, @@ -254,15 +255,15 @@ contract LotteryHookExampleTest is BaseVaultTest { bytes4 routerMethod; // If kind is BOTH, odd iterations are EXACT_IN and even iterations are EXACT_OUT. if (kind == SwapKindLottery.EXACT_IN || (kind == SwapKindLottery.BOTH && iterations % 2 == 1)) { - routerMethod = IRouter.swapSingleTokenExactIn.selector; + routerMethod = IRouterSwap.swapSingleTokenExactIn.selector; } else { - routerMethod = IRouter.swapSingleTokenExactOut.selector; + routerMethod = IRouterSwap.swapSingleTokenExactOut.selector; } uint8 randomNumber = LotteryHookExample(poolHooksContract).getRandomNumber(); uint256 amountGiven = swapAmount; - uint256 amountCalculated = routerMethod == IRouter.swapSingleTokenExactIn.selector + uint256 amountCalculated = routerMethod == IRouterSwap.swapSingleTokenExactIn.selector ? swapAmount - hookFee // If EXACT_IN, amount calculated is amount out; user receives less : swapAmount + hookFee; // If EXACT_IN, amount calculated is amount in; user pays more @@ -280,7 +281,7 @@ contract LotteryHookExampleTest is BaseVaultTest { emit LotteryHookExample.LotteryWinningsPaid(poolHooksContract, alice, IERC20(usdc), usdcWinnings); } } else { - if (routerMethod == IRouter.swapSingleTokenExactIn.selector) { + if (routerMethod == IRouterSwap.swapSingleTokenExactIn.selector) { vm.expectEmit(); emit LotteryHookExample.LotteryFeeCollected(poolHooksContract, IERC20(usdc), hookFee); } else { @@ -311,7 +312,7 @@ contract LotteryHookExampleTest is BaseVaultTest { if (randomNumber == LotteryHookExample(poolHooksContract).LUCKY_NUMBER()) { break; } else { - if (routerMethod == IRouter.swapSingleTokenExactIn.selector) { + if (routerMethod == IRouterSwap.swapSingleTokenExactIn.selector) { accruedFees[usdcIdx] += hookFee; } else { accruedFees[daiIdx] += hookFee; From 1f95ff3aa2fc3bfc287b1d553f1aa8d2a141f0ea Mon Sep 17 00:00:00 2001 From: elshan_eth <22689890+elshan-eth@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:49:10 +0100 Subject: [PATCH 4/4] add payment hook --- .../contracts/vault/IRouterPaymentHooks.sol | 9 +++ pkg/vault/contracts/AggregatorsRouter.sol | 81 ++++++++++++++----- pkg/vault/contracts/test/AggregatorMock.sol | 78 ++++++++++++++++++ .../test/foundry/AggregatorsRouter.t.sol | 72 +++++++++++++++-- 4 files changed, 212 insertions(+), 28 deletions(-) create mode 100644 pkg/interfaces/contracts/vault/IRouterPaymentHooks.sol create mode 100644 pkg/vault/contracts/test/AggregatorMock.sol diff --git a/pkg/interfaces/contracts/vault/IRouterPaymentHooks.sol b/pkg/interfaces/contracts/vault/IRouterPaymentHooks.sol new file mode 100644 index 000000000..7ce1804c2 --- /dev/null +++ b/pkg/interfaces/contracts/vault/IRouterPaymentHooks.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IRouterPaymentHooks { + function onPay(IERC20 token, uint256 amount, bytes calldata userData) external; +} diff --git a/pkg/vault/contracts/AggregatorsRouter.sol b/pkg/vault/contracts/AggregatorsRouter.sol index edd271db8..ce1b65f07 100644 --- a/pkg/vault/contracts/AggregatorsRouter.sol +++ b/pkg/vault/contracts/AggregatorsRouter.sol @@ -8,6 +8,7 @@ import { IAllowanceTransfer } from "permit2/src/interfaces/IAllowanceTransfer.so 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 { IRouterPaymentHooks } from "@balancer-labs/v3-interfaces/contracts/vault/IRouterPaymentHooks.sol"; import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; @@ -73,17 +74,36 @@ contract AggregatorsRouter is IRouterSwap, RouterCommon { /// @inheritdoc IRouterSwap function swapSingleTokenExactOut( - address, - IERC20, - IERC20, - uint256, - uint256, - uint256, - bool, - bytes calldata - ) external payable returns (uint256) { - // TODO: will implement this in next PR - revert OperationNotSupported("swapSingleTokenExactOut is not supported"); + 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( + AggregatorsRouter.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) + ); } /** @@ -97,6 +117,8 @@ contract AggregatorsRouter is IRouterSwap, RouterCommon { ) external nonReentrant onlyVault returns (uint256) { (uint256 amountCalculated, uint256 amountIn, uint256 amountOut) = _swapHook(params); + IRouterPaymentHooks(params.sender).onPay(params.tokenIn, amountIn, params.userData); + _vault.settle(params.tokenIn, amountIn); _sendTokenOut(params.sender, params.tokenOut, amountOut, false); @@ -163,15 +185,34 @@ contract AggregatorsRouter is IRouterSwap, RouterCommon { /// @inheritdoc IRouterSwap function querySwapSingleTokenExactOut( - address, - IERC20, - IERC20, - uint256, - address, - bytes memory - ) external pure returns (uint256) { - // TODO: will implement this in next PR - revert OperationNotSupported("swapSingleTokenExactOut is not supported"); + 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( + AggregatorsRouter.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) + ); } /** diff --git a/pkg/vault/contracts/test/AggregatorMock.sol b/pkg/vault/contracts/test/AggregatorMock.sol new file mode 100644 index 000000000..be0c44fa2 --- /dev/null +++ b/pkg/vault/contracts/test/AggregatorMock.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IRouterSwap } from "@balancer-labs/v3-interfaces/contracts/vault/IRouterSwap.sol"; +import { IRouterPaymentHooks } from "@balancer-labs/v3-interfaces/contracts/vault/IRouterPaymentHooks.sol"; + +contract AggregatorMock is IRouterPaymentHooks { + address internal vault; + IRouterSwap internal router; + bool public isPaymentHookActive; + + constructor(address vault_, IRouterSwap router_) { + vault = vault_; + router = router_; + isPaymentHookActive = true; + } + + function onPay(IERC20 token, uint256 amount, bytes calldata) external { + if (!isPaymentHookActive) { + return; + } + + token.transfer(address(vault), amount); + } + + function setPaymentHookActive(bool active) external { + isPaymentHookActive = active; + } + + function swapSingleTokenExactIn( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountIn, + uint256 minAmountOut, + uint256 deadline, + bool wethIsEth, + bytes calldata userData + ) external returns (uint256 amountOut) { + return + router.swapSingleTokenExactIn( + pool, + tokenIn, + tokenOut, + exactAmountIn, + minAmountOut, + deadline, + wethIsEth, + userData + ); + } + + function swapSingleTokenExactOut( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountOut, + uint256 maxAmountIn, + uint256 deadline, + bool wethIsEth, + bytes calldata userData + ) external returns (uint256) { + return + router.swapSingleTokenExactOut( + pool, + tokenIn, + tokenOut, + exactAmountOut, + maxAmountIn, + deadline, + wethIsEth, + userData + ); + } +} diff --git a/pkg/vault/test/foundry/AggregatorsRouter.t.sol b/pkg/vault/test/foundry/AggregatorsRouter.t.sol index e8ba68dc7..3324d81ad 100644 --- a/pkg/vault/test/foundry/AggregatorsRouter.t.sol +++ b/pkg/vault/test/foundry/AggregatorsRouter.t.sol @@ -10,6 +10,8 @@ import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-u import { IVaultErrors } from "@balancer-labs/v3-interfaces/contracts/vault/IVaultErrors.sol"; import { TokenConfig } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { IRouterSwap } from "@balancer-labs/v3-interfaces/contracts/vault/IRouterSwap.sol"; +import { IRouterPaymentHooks } from "@balancer-labs/v3-interfaces/contracts/vault/IRouterPaymentHooks.sol"; import { EVMCallModeHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/EVMCallModeHelpers.sol"; import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; @@ -17,6 +19,7 @@ import { ArrayHelpers } from "@balancer-labs/v3-solidity-utils/contracts/test/Ar import { RateProviderMock } from "../../contracts/test/RateProviderMock.sol"; import { AggregatorsRouter } from "../../contracts/AggregatorsRouter.sol"; +import { AggregatorMock } from "../../contracts/test/AggregatorMock.sol"; import { PoolMock } from "../../contracts/test/PoolMock.sol"; import { PoolFactoryMock, BaseVaultTest } from "./utils/BaseVaultTest.sol"; @@ -27,6 +30,10 @@ contract AggregatorsRouterTest is BaseVaultTest { string version = "test"; AggregatorsRouter internal aggregatorsRouter; + AggregatorMock internal aggregatorMock; + + uint256 aggregatorUDCInitialBalance; + uint256 aggregatorDAIInitialBalance; // Track the indices for the standard dai/usdc pool. uint256 internal daiIdx; @@ -37,6 +44,13 @@ contract AggregatorsRouterTest is BaseVaultTest { BaseVaultTest.setUp(); aggregatorsRouter = deployAggregatorsRouter(IVault(address(vault)), weth, version); + aggregatorMock = new AggregatorMock(address(vault), IRouterSwap(address(aggregatorsRouter))); + + uint256 aliceBalance = usdc.balanceOf(alice); + aggregatorUDCInitialBalance = aliceBalance; + aggregatorDAIInitialBalance = 0; + vm.prank(alice); + usdc.transfer(address(aggregatorMock), aliceBalance); } function createPool() internal override returns (address newPool, bytes memory poolArgs) { @@ -75,18 +89,18 @@ contract AggregatorsRouterTest is BaseVaultTest { } function testSwapExactInWithoutPayment() public { + aggregatorMock.setPaymentHookActive(false); + vm.prank(alice); vm.expectRevert(IVaultErrors.BalanceNotSettled.selector); - aggregatorsRouter.swapSingleTokenExactIn(address(pool), usdc, dai, 1e18, 0, MAX_UINT256, false, bytes("")); + aggregatorMock.swapSingleTokenExactIn(address(pool), usdc, dai, 1e18, 0, MAX_UINT256, false, bytes("")); } function testSwapExactIn_Fuzz(uint256 swapAmount) public { swapAmount = bound(swapAmount, 1e18, vault.getPoolData(address(pool)).balancesLiveScaled18[daiIdx]); - vm.startPrank(alice); - usdc.transfer(address(vault), swapAmount); - - uint256 outputTokenAmount = aggregatorsRouter.swapSingleTokenExactIn( + vm.prank(alice); + uint256 outputTokenAmount = aggregatorMock.swapSingleTokenExactIn( address(pool), usdc, dai, @@ -96,10 +110,52 @@ contract AggregatorsRouterTest is BaseVaultTest { false, bytes("") ); - vm.stopPrank(); + assertEq( + usdc.balanceOf(address(aggregatorMock)), + aggregatorUDCInitialBalance - swapAmount, + "Wrong usdc balance" + ); + assertEq( + dai.balanceOf(address(aggregatorMock)), + aggregatorDAIInitialBalance + outputTokenAmount, + "Wrong dai balance" + ); + } + + function testSwapExactOut_Fuzz(uint256 swapAmount) public { + swapAmount = bound(swapAmount, 1e18, vault.getPoolData(address(pool)).balancesLiveScaled18[daiIdx]); + + uint256 snapshotId = vm.snapshot(); - assertEq(usdc.balanceOf(alice), defaultAccountBalance() - swapAmount, "Wrong WETH balance"); - assertEq(dai.balanceOf(alice), defaultAccountBalance() + outputTokenAmount, "Wrong DAI balance"); + _prankStaticCall(); + uint256 exactAmountOut = router.querySwapSingleTokenExactIn( + pool, + usdc, + dai, + swapAmount, + address(this), + bytes("") + ); + vm.revertTo(snapshotId); + + vm.prank(alice); + uint256 amountIn = aggregatorMock.swapSingleTokenExactOut( + address(pool), + usdc, + dai, + exactAmountOut, + MAX_UINT256, + MAX_UINT256, + false, + bytes("") + ); + + assertEq(usdc.balanceOf(address(aggregatorMock)), aggregatorUDCInitialBalance - amountIn, "Wrong usdc balance"); + assertEq( + dai.balanceOf(address(aggregatorMock)), + aggregatorDAIInitialBalance + exactAmountOut, + "Wrong dai balance" + ); } function testRouterVersion() public view {