From 43ea00e767d1e98a6423b3c83f6b726f7ad98b40 Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Fri, 9 Jun 2023 19:00:56 -0400 Subject: [PATCH 01/19] feat: first cut at queryMulticall method --- .../standalone-utils/IBalancerRelayer.sol | 2 + .../contracts/BatchRelayerQueryLibrary.sol | 31 +++ .../contracts/relayer/BalancerRelayer.sol | 15 +- .../contracts/relayer/BaseRelayerLibrary.sol | 159 +----------- .../relayer/BaseRelayerLibraryCommon.sol | 204 ++++++++++++++++ .../contracts/relayer/VaultActions.sol | 12 +- .../contracts/relayer/VaultQueryActions.sol | 227 ++++++++++++++++++ 7 files changed, 490 insertions(+), 160 deletions(-) create mode 100644 pkg/standalone-utils/contracts/BatchRelayerQueryLibrary.sol create mode 100644 pkg/standalone-utils/contracts/relayer/BaseRelayerLibraryCommon.sol create mode 100644 pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol diff --git a/pkg/interfaces/contracts/standalone-utils/IBalancerRelayer.sol b/pkg/interfaces/contracts/standalone-utils/IBalancerRelayer.sol index 57b93fc6c4..fb67e617df 100644 --- a/pkg/interfaces/contracts/standalone-utils/IBalancerRelayer.sol +++ b/pkg/interfaces/contracts/standalone-utils/IBalancerRelayer.sol @@ -27,4 +27,6 @@ interface IBalancerRelayer { function getVault() external view returns (IVault); function multicall(bytes[] calldata data) external payable returns (bytes[] memory results); + + function queryMulticall(bytes[] calldata data) external returns (bytes[] memory results); } diff --git a/pkg/standalone-utils/contracts/BatchRelayerQueryLibrary.sol b/pkg/standalone-utils/contracts/BatchRelayerQueryLibrary.sol new file mode 100644 index 0000000000..6b63ae97e4 --- /dev/null +++ b/pkg/standalone-utils/contracts/BatchRelayerQueryLibrary.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +import "./relayer/BaseRelayerLibraryCommon.sol"; + +import "./relayer/VaultQueryActions.sol"; + +/** + * @title Batch Relayer Library + * @notice This contract is not a relayer by itself and calls into it directly will fail. + * The associated relayer can be found by calling `getEntrypoint` on this contract. + */ +contract BatchRelayerQueryLibrary is BaseRelayerLibraryCommon, VaultQueryActions { + constructor(IVault vault) BaseRelayerLibraryCommon(vault) { + //solhint-disable-previous-line no-empty-blocks + } +} diff --git a/pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol b/pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol index 32b7f81278..d0204a5a12 100644 --- a/pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol +++ b/pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol @@ -48,14 +48,20 @@ contract BalancerRelayer is IBalancerRelayer, ReentrancyGuard { IVault private immutable _vault; address private immutable _library; + address private immutable _queryLibrary; /** * @dev This contract is not meant to be deployed directly by an EOA, but rather during construction of a contract * derived from `BaseRelayerLibrary`, which will provide its own address as the relayer's library. */ - constructor(IVault vault, address libraryAddress) { + constructor( + IVault vault, + address libraryAddress, + address queryLibrary + ) { _vault = vault; _library = libraryAddress; + _queryLibrary = queryLibrary; } receive() external payable { @@ -83,6 +89,13 @@ contract BalancerRelayer is IBalancerRelayer, ReentrancyGuard { _refundETH(); } + function queryMulticall(bytes[] calldata data) external override nonReentrant returns (bytes[] memory results) { + results = new bytes[](data.length); + for (uint256 i = 0; i < data.length; i++) { + results[i] = _queryLibrary.functionDelegateCall(data[i]); + } + } + function _refundETH() private { uint256 remainingEth = address(this).balance; if (remainingEth > 0) { diff --git a/pkg/standalone-utils/contracts/relayer/BaseRelayerLibrary.sol b/pkg/standalone-utils/contracts/relayer/BaseRelayerLibrary.sol index edf87cc1b6..f45fad6999 100644 --- a/pkg/standalone-utils/contracts/relayer/BaseRelayerLibrary.sol +++ b/pkg/standalone-utils/contracts/relayer/BaseRelayerLibrary.sol @@ -20,6 +20,7 @@ import "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol"; import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/SafeERC20.sol"; import "./IBaseRelayerLibrary.sol"; +import "../BatchRelayerQueryLibrary.sol"; import "./BalancerRelayer.sol"; /** @@ -41,20 +42,19 @@ import "./BalancerRelayer.sol"; * do not revert in a call involving ETH. This also applies to functions that do not alter the state and would be * usually labeled as `view`. */ -contract BaseRelayerLibrary is IBaseRelayerLibrary { +contract BaseRelayerLibrary is BaseRelayerLibraryCommon { using Address for address; using SafeERC20 for IERC20; IVault private immutable _vault; IBalancerRelayer private immutable _entrypoint; - constructor(IVault vault) IBaseRelayerLibrary(vault.WETH()) { + constructor(IVault vault) BaseRelayerLibraryCommon(vault) { _vault = vault; - _entrypoint = new BalancerRelayer(vault, address(this)); - } - function getVault() public view override returns (IVault) { - return _vault; + IBaseRelayerLibrary queryLibrary = new BatchRelayerQueryLibrary(vault); + + _entrypoint = new BalancerRelayer(vault, address(this), address(queryLibrary)); } function getEntrypoint() external view returns (IBalancerRelayer) { @@ -77,151 +77,4 @@ contract BaseRelayerLibrary is IBaseRelayerLibrary { address(_vault).functionCall(data); } - - /** - * @notice Approves the Vault to use tokens held in the relayer - * @dev This is needed to avoid having to send intermediate tokens back to the user - */ - function approveVault(IERC20 token, uint256 amount) external payable override { - if (_isChainedReference(amount)) { - amount = _getChainedReferenceValue(amount); - } - // TODO: gas golf this a bit - token.safeApprove(address(getVault()), amount); - } - - /** - * @notice Returns the amount referenced by chained reference `ref`. - * @dev It does not alter the reference (even if it's marked as temporary). - * - * This function does not alter the state in any way. It is not marked as view because it has to be `payable` - * in order to be used in a batch transaction. - * - * Use a static call to read the state off-chain. - */ - function peekChainedReferenceValue(uint256 ref) external payable override returns (uint256 value) { - (, value) = _peekChainedReferenceValue(ref); - } - - function _pullToken( - address sender, - IERC20 token, - uint256 amount - ) internal override { - if (amount == 0) return; - IERC20[] memory tokens = new IERC20[](1); - tokens[0] = token; - uint256[] memory amounts = new uint256[](1); - amounts[0] = amount; - - _pullTokens(sender, tokens, amounts); - } - - function _pullTokens( - address sender, - IERC20[] memory tokens, - uint256[] memory amounts - ) internal override { - IVault.UserBalanceOp[] memory ops = new IVault.UserBalanceOp[](tokens.length); - for (uint256 i; i < tokens.length; i++) { - ops[i] = IVault.UserBalanceOp({ - asset: IAsset(address(tokens[i])), - amount: amounts[i], - sender: sender, - recipient: payable(address(this)), - kind: IVault.UserBalanceOpKind.TRANSFER_EXTERNAL - }); - } - - getVault().manageUserBalance(ops); - } - - /** - * @dev Returns true if `amount` is not actually an amount, but rather a chained reference. - */ - function _isChainedReference(uint256 amount) internal pure override returns (bool) { - // First 3 nibbles are enough to determine if it's a chained reference. - return - (amount & 0xfff0000000000000000000000000000000000000000000000000000000000000) == - 0xba10000000000000000000000000000000000000000000000000000000000000; - } - - /** - * @dev Returns true if `ref` is temporary reference, i.e. to be deleted after reading it. - */ - function _isTemporaryChainedReference(uint256 amount) internal pure returns (bool) { - // First 3 nibbles determine if it's a chained reference. - // If the 4th nibble is 0 it is temporary; otherwise it is considered read-only. - // In practice, we shall use '0xba11' for read-only references. - return - (amount & 0xffff000000000000000000000000000000000000000000000000000000000000) == - 0xba10000000000000000000000000000000000000000000000000000000000000; - } - - /** - * @dev Stores `value` as the amount referenced by chained reference `ref`. - */ - function _setChainedReferenceValue(uint256 ref, uint256 value) internal override { - bytes32 slot = _getStorageSlot(ref); - - // Since we do manual calculation of storage slots, it is easier (and cheaper) to rely on internal assembly to - // access it. - // solhint-disable-next-line no-inline-assembly - assembly { - sstore(slot, value) - } - } - - /** - * @dev Returns the amount referenced by chained reference `ref`. - * If the reference is temporary, it will be cleared after reading it, so they can each only be read once. - * If the reference is not temporary (i.e. read-only), it will not be cleared after reading it - * (see `_isTemporaryChainedReference` function). - */ - function _getChainedReferenceValue(uint256 ref) internal override returns (uint256) { - (bytes32 slot, uint256 value) = _peekChainedReferenceValue(ref); - - if (_isTemporaryChainedReference(ref)) { - // solhint-disable-next-line no-inline-assembly - assembly { - sstore(slot, 0) - } - } - return value; - } - - /** - * @dev Returns the storage slot for reference `ref` as well as the amount referenced by it. - * It does not alter the reference (even if it's marked as temporary). - */ - function _peekChainedReferenceValue(uint256 ref) private view returns (bytes32 slot, uint256 value) { - slot = _getStorageSlot(ref); - - // Since we do manual calculation of storage slots, it is easier (and cheaper) to rely on internal assembly to - // access it. - // solhint-disable-next-line no-inline-assembly - assembly { - value := sload(slot) - } - } - - // solhint-disable-next-line var-name-mixedcase - bytes32 private immutable _TEMP_STORAGE_SUFFIX = keccak256("balancer.base-relayer-library"); - - function _getStorageSlot(uint256 ref) private view returns (bytes32) { - // This replicates the mechanism Solidity uses to allocate storage slots for mappings, but using a hash as the - // mapping's storage slot, and subtracting 1 at the end. This should be more than enough to prevent collisions - // with other state variables this or derived contracts might use. - // See https://docs.soliditylang.org/en/v0.8.9/internals/layout_in_storage.html - - return bytes32(uint256(keccak256(abi.encodePacked(_removeReferencePrefix(ref), _TEMP_STORAGE_SUFFIX))) - 1); - } - - /** - * @dev Returns a reference without its prefix. - * Use this function to calculate the storage slot so that it's the same for temporary and read-only references. - */ - function _removeReferencePrefix(uint256 ref) private pure returns (uint256) { - return (ref & 0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); - } } diff --git a/pkg/standalone-utils/contracts/relayer/BaseRelayerLibraryCommon.sol b/pkg/standalone-utils/contracts/relayer/BaseRelayerLibraryCommon.sol new file mode 100644 index 0000000000..210f1174bd --- /dev/null +++ b/pkg/standalone-utils/contracts/relayer/BaseRelayerLibraryCommon.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +import "@balancer-labs/v2-interfaces/contracts/standalone-utils/IBalancerRelayer.sol"; +import "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol"; +import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/SafeERC20.sol"; + +import "./IBaseRelayerLibrary.sol"; +import "./BalancerRelayer.sol"; + +/** + * @title Base Relayer Library + * @notice Core functionality of a relayer. Allow users to use a signature to approve this contract + * to take further actions on their behalf. + * @dev + * Relayers are composed of two contracts: + * - A `BalancerRelayer` contract, which acts as a single point of entry into the system through a multicall function + * - A library contract such as this one, which defines the allowed behaviour of the relayer + + * NOTE: Only the entrypoint contract should be allowlisted by Balancer governance as a relayer, so that the Vault + * will reject calls from outside the entrypoint context. + * + * This contract should neither be allowlisted as a relayer, nor called directly by the user. + * No guarantees can be made about fund safety when calling this contract in an improper manner. + * + * All functions that are meant to be called from the entrypoint via `multicall` must be payable so that they + * do not revert in a call involving ETH. This also applies to functions that do not alter the state and would be + * usually labeled as `view`. + */ +abstract contract BaseRelayerLibraryCommon is IBaseRelayerLibrary { + using Address for address; + using SafeERC20 for IERC20; + + IVault private immutable _vault; + + constructor(IVault vault) IBaseRelayerLibrary(vault.WETH()) { + _vault = vault; + } + + function getVault() public view override returns (IVault) { + return _vault; + } + + /** + * @notice Approves the Vault to use tokens held in the relayer + * @dev This is needed to avoid having to send intermediate tokens back to the user + */ + function approveVault(IERC20 token, uint256 amount) external payable override { + if (_isChainedReference(amount)) { + amount = _getChainedReferenceValue(amount); + } + // TODO: gas golf this a bit + token.safeApprove(address(getVault()), amount); + } + + /** + * @notice Returns the amount referenced by chained reference `ref`. + * @dev It does not alter the reference (even if it's marked as temporary). + * + * This function does not alter the state in any way. It is not marked as view because it has to be `payable` + * in order to be used in a batch transaction. + * + * Use a static call to read the state off-chain. + */ + function peekChainedReferenceValue(uint256 ref) external payable override returns (uint256 value) { + (, value) = _peekChainedReferenceValue(ref); + } + + function _pullToken( + address sender, + IERC20 token, + uint256 amount + ) internal override { + if (amount == 0) return; + IERC20[] memory tokens = new IERC20[](1); + tokens[0] = token; + uint256[] memory amounts = new uint256[](1); + amounts[0] = amount; + + _pullTokens(sender, tokens, amounts); + } + + function _pullTokens( + address sender, + IERC20[] memory tokens, + uint256[] memory amounts + ) internal override { + IVault.UserBalanceOp[] memory ops = new IVault.UserBalanceOp[](tokens.length); + for (uint256 i; i < tokens.length; i++) { + ops[i] = IVault.UserBalanceOp({ + asset: IAsset(address(tokens[i])), + amount: amounts[i], + sender: sender, + recipient: payable(address(this)), + kind: IVault.UserBalanceOpKind.TRANSFER_EXTERNAL + }); + } + + getVault().manageUserBalance(ops); + } + + /** + * @dev Returns true if `amount` is not actually an amount, but rather a chained reference. + */ + function _isChainedReference(uint256 amount) internal pure override returns (bool) { + // First 3 nibbles are enough to determine if it's a chained reference. + return + (amount & 0xfff0000000000000000000000000000000000000000000000000000000000000) == + 0xba10000000000000000000000000000000000000000000000000000000000000; + } + + /** + * @dev Returns true if `ref` is temporary reference, i.e. to be deleted after reading it. + */ + function _isTemporaryChainedReference(uint256 amount) internal pure returns (bool) { + // First 3 nibbles determine if it's a chained reference. + // If the 4th nibble is 0 it is temporary; otherwise it is considered read-only. + // In practice, we shall use '0xba11' for read-only references. + return + (amount & 0xffff000000000000000000000000000000000000000000000000000000000000) == + 0xba10000000000000000000000000000000000000000000000000000000000000; + } + + /** + * @dev Stores `value` as the amount referenced by chained reference `ref`. + */ + function _setChainedReferenceValue(uint256 ref, uint256 value) internal override { + bytes32 slot = _getStorageSlot(ref); + + // Since we do manual calculation of storage slots, it is easier (and cheaper) to rely on internal assembly to + // access it. + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, value) + } + } + + /** + * @dev Returns the amount referenced by chained reference `ref`. + * If the reference is temporary, it will be cleared after reading it, so they can each only be read once. + * If the reference is not temporary (i.e. read-only), it will not be cleared after reading it + * (see `_isTemporaryChainedReference` function). + */ + function _getChainedReferenceValue(uint256 ref) internal override returns (uint256) { + (bytes32 slot, uint256 value) = _peekChainedReferenceValue(ref); + + if (_isTemporaryChainedReference(ref)) { + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, 0) + } + } + return value; + } + + /** + * @dev Returns the storage slot for reference `ref` as well as the amount referenced by it. + * It does not alter the reference (even if it's marked as temporary). + */ + function _peekChainedReferenceValue(uint256 ref) private view returns (bytes32 slot, uint256 value) { + slot = _getStorageSlot(ref); + + // Since we do manual calculation of storage slots, it is easier (and cheaper) to rely on internal assembly to + // access it. + // solhint-disable-next-line no-inline-assembly + assembly { + value := sload(slot) + } + } + + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _TEMP_STORAGE_SUFFIX = keccak256("balancer.base-relayer-library"); + + function _getStorageSlot(uint256 ref) private view returns (bytes32) { + // This replicates the mechanism Solidity uses to allocate storage slots for mappings, but using a hash as the + // mapping's storage slot, and subtracting 1 at the end. This should be more than enough to prevent collisions + // with other state variables this or derived contracts might use. + // See https://docs.soliditylang.org/en/v0.8.9/internals/layout_in_storage.html + + return bytes32(uint256(keccak256(abi.encodePacked(_removeReferencePrefix(ref), _TEMP_STORAGE_SUFFIX))) - 1); + } + + /** + * @dev Returns a reference without its prefix. + * Use this function to calculate the storage slot so that it's the same for temporary and read-only references. + */ + function _removeReferencePrefix(uint256 ref) private pure returns (uint256) { + return (ref & 0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + } +} diff --git a/pkg/standalone-utils/contracts/relayer/VaultActions.sol b/pkg/standalone-utils/contracts/relayer/VaultActions.sol index f8fd70b162..901fe37681 100644 --- a/pkg/standalone-utils/contracts/relayer/VaultActions.sol +++ b/pkg/standalone-utils/contracts/relayer/VaultActions.sol @@ -62,7 +62,7 @@ abstract contract VaultActions is IBaseRelayerLibrary { uint256 deadline, uint256 value, uint256 outputReference - ) external payable { + ) external payable virtual { require(funds.sender == msg.sender || funds.sender == address(this), "Incorrect sender"); if (_isChainedReference(singleSwap.amount)) { @@ -85,7 +85,7 @@ abstract contract VaultActions is IBaseRelayerLibrary { uint256 deadline, uint256 value, OutputReference[] calldata outputReferences - ) external payable { + ) external payable virtual { require(funds.sender == msg.sender || funds.sender == address(this), "Incorrect sender"); for (uint256 i = 0; i < swaps.length; ++i) { @@ -144,7 +144,7 @@ abstract contract VaultActions is IBaseRelayerLibrary { IVault.JoinPoolRequest memory request, uint256 value, uint256 outputReference - ) external payable { + ) external payable virtual { require(sender == msg.sender || sender == address(this), "Incorrect sender"); // The output of a join will be the Pool's token contract, typically known as BPT (Balancer Pool Tokens). @@ -171,7 +171,7 @@ abstract contract VaultActions is IBaseRelayerLibrary { * references as necessary. */ function _doJoinPoolChainedReferenceReplacements(PoolKind kind, bytes memory userData) - private + internal returns (bytes memory) { if (kind == PoolKind.WEIGHTED) { @@ -260,7 +260,7 @@ abstract contract VaultActions is IBaseRelayerLibrary { address payable recipient, IVault.ExitPoolRequest memory request, OutputReference[] calldata outputReferences - ) external payable { + ) external payable virtual { require(sender == msg.sender || sender == address(this), "Incorrect sender"); // To track the changes of internal balances, we need an array of token addresses. @@ -314,7 +314,7 @@ abstract contract VaultActions is IBaseRelayerLibrary { * references as necessary. */ function _doExitPoolChainedReferenceReplacements(PoolKind kind, bytes memory userData) - private + internal returns (bytes memory) { if (kind == PoolKind.WEIGHTED) { diff --git a/pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol b/pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol new file mode 100644 index 0000000000..e169111221 --- /dev/null +++ b/pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +import "@balancer-labs/v2-interfaces/contracts/vault/IBasePool.sol"; + +import "@balancer-labs/v2-solidity-utils/contracts/helpers/InputHelpers.sol"; + +import "./VaultActions.sol"; + +/** + * @title VaultQueryActions + * @notice Allows users to simulate the core functions on the Balancer Vault (swaps/joins/exits), using queries instead + * of the actual operations. + */ +abstract contract VaultQueryActions is VaultActions { + function swap( + IVault.SingleSwap memory singleSwap, + IVault.FundManagement calldata funds, + uint256 limit, + uint256, // deadline (could remove, or leave in if we need to preserve the interface) + uint256, // value + uint256 outputReference + ) external payable override { + require(funds.sender == msg.sender || funds.sender == address(this), "Incorrect sender"); + + if (_isChainedReference(singleSwap.amount)) { + singleSwap.amount = _getChainedReferenceValue(singleSwap.amount); + } + + uint256 result = _querySwap(singleSwap, funds); + + _require(singleSwap.kind == IVault.SwapKind.GIVEN_IN ? result >= limit : result <= limit, Errors.SWAP_LIMIT); + + if (_isChainedReference(outputReference)) { + _setChainedReferenceValue(outputReference, result); + } + } + + function _querySwap(IVault.SingleSwap memory singleSwap, IVault.FundManagement memory funds) + private + returns (uint256) + { + // The Vault only supports batch swap queries, so we need to convert the swap call into an equivalent batch + // swap. The result will be identical. + + // The main difference between swaps and batch swaps is that batch swaps require an assets array. We're going + // to place the asset in at index 0, and asset out at index 1. + IAsset[] memory assets = new IAsset[](2); + assets[0] = singleSwap.assetIn; + assets[1] = singleSwap.assetOut; + + IVault.BatchSwapStep[] memory swaps = new IVault.BatchSwapStep[](1); + swaps[0] = IVault.BatchSwapStep({ + poolId: singleSwap.poolId, + assetInIndex: 0, + assetOutIndex: 1, + amount: singleSwap.amount, + userData: singleSwap.userData + }); + + int256[] memory assetDeltas = getVault().queryBatchSwap(singleSwap.kind, swaps, assets, funds); + + // Batch swaps return the full Vault asset deltas, which in the special case of a single step swap contains more + // information than we need (as the amount in is known in a GIVEN_IN swap, and the amount out is known in a + // GIVEN_OUT swap). We extract the information we're interested in. + if (singleSwap.kind == IVault.SwapKind.GIVEN_IN) { + // The asset out will have a negative Vault delta (the assets are coming out of the Pool and the user is + // receiving them), so make it positive to match the `swap` interface. + + _require(assetDeltas[1] <= 0, Errors.SHOULD_NOT_HAPPEN); + return uint256(-assetDeltas[1]); + } else { + // The asset in will have a positive Vault delta (the assets are going into the Pool and the user is + // sending them), so we don't need to do anything. + return uint256(assetDeltas[0]); + } + } + + function batchSwap( + IVault.SwapKind kind, + IVault.BatchSwapStep[] memory swaps, + IAsset[] calldata assets, + IVault.FundManagement calldata funds, + int256[] calldata limits, + uint256, // deadline (could remove, or leave in if we need to preserve the interface) + uint256, // value + OutputReference[] calldata outputReferences + ) external payable override { + require(funds.sender == msg.sender || funds.sender == address(this), "Incorrect sender"); + + for (uint256 i = 0; i < swaps.length; ++i) { + uint256 amount = swaps[i].amount; + if (_isChainedReference(amount)) { + swaps[i].amount = _getChainedReferenceValue(amount); + } + } + + int256[] memory results = getVault().queryBatchSwap(kind, swaps, assets, funds); + + for (uint256 i = 0; i < outputReferences.length; ++i) { + require(_isChainedReference(outputReferences[i].key), "invalid chained reference"); + + _require(results[i] <= limits[i], Errors.SWAP_LIMIT); + + // Batch swap return values are signed, as they are Vault deltas (positive values correspond to assets sent + // to the Vault, and negative values are assets received from the Vault). To simplify the chained reference + // value model, we simply store the absolute value. + // This should be fine for most use cases, as the caller can reason about swap results via the `limits` + // parameter. + _setChainedReferenceValue(outputReferences[i].key, Math.abs(results[outputReferences[i].index])); + } + } + + function joinPool( + bytes32 poolId, + PoolKind kind, + address sender, + address recipient, + IVault.JoinPoolRequest memory request, + uint256, // value (could remove, or leave in if we need to preserve the interface) + uint256 outputReference + ) external payable override { + require(sender == msg.sender || sender == address(this), "Incorrect sender"); + + request.userData = _doJoinPoolChainedReferenceReplacements(kind, request.userData); + + uint256 bptOut = _queryJoin(poolId, sender, recipient, request); + + if (_isChainedReference(outputReference)) { + _setChainedReferenceValue(outputReference, bptOut); + } + } + + function _queryJoin( + bytes32 poolId, + address sender, + address recipient, + IVault.JoinPoolRequest memory request + ) private returns (uint256 bptOut) { + (address pool, ) = getVault().getPool(poolId); + (uint256[] memory balances, uint256 lastChangeBlock) = _validateAssetsAndGetBalances(poolId, request.assets); + IProtocolFeesCollector feesCollector = getVault().getProtocolFeesCollector(); + + (bptOut, ) = IBasePool(pool).queryJoin( + poolId, + sender, + recipient, + balances, + lastChangeBlock, + feesCollector.getSwapFeePercentage(), + request.userData + ); + } + + function exitPool( + bytes32 poolId, + PoolKind kind, + address sender, + address payable recipient, + IVault.ExitPoolRequest memory request, + OutputReference[] calldata outputReferences + ) external payable override { + require(sender == msg.sender || sender == address(this), "Incorrect sender"); + + // Exit the Pool + request.userData = _doExitPoolChainedReferenceReplacements(kind, request.userData); + + uint256[] memory amountsOut = _queryExit(poolId, sender, recipient, request); + + // Save as chained references + for (uint256 i = 0; i < outputReferences.length; i++) { + _setChainedReferenceValue(outputReferences[i].key, amountsOut[i]); + } + } + + function _queryExit( + bytes32 poolId, + address sender, + address recipient, + IVault.ExitPoolRequest memory request + ) private returns (uint256[] memory amountsOut) { + (address pool, ) = getVault().getPool(poolId); + (uint256[] memory balances, uint256 lastChangeBlock) = _validateAssetsAndGetBalances(poolId, request.assets); + IProtocolFeesCollector feesCollector = getVault().getProtocolFeesCollector(); + + (, amountsOut) = IBasePool(pool).queryExit( + poolId, + sender, + recipient, + balances, + lastChangeBlock, + feesCollector.getSwapFeePercentage(), + request.userData + ); + } + + function _validateAssetsAndGetBalances(bytes32 poolId, IAsset[] memory expectedAssets) + private + view + returns (uint256[] memory balances, uint256 lastChangeBlock) + { + IERC20[] memory actualTokens; + IERC20[] memory expectedTokens = _translateToIERC20(expectedAssets); + + (actualTokens, balances, lastChangeBlock) = getVault().getPoolTokens(poolId); + InputHelpers.ensureInputLengthMatch(actualTokens.length, expectedTokens.length); + + for (uint256 i = 0; i < actualTokens.length; ++i) { + IERC20 token = actualTokens[i]; + _require(token == expectedTokens[i], Errors.TOKENS_MISMATCH); + } + } +} From 1c165b2787ca7058a3f24887ae1533c746c77576 Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Sun, 11 Jun 2023 14:40:16 -0400 Subject: [PATCH 02/19] test: add initial test --- .../test/VaultQueryActions.test.ts | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 pkg/standalone-utils/test/VaultQueryActions.test.ts diff --git a/pkg/standalone-utils/test/VaultQueryActions.test.ts b/pkg/standalone-utils/test/VaultQueryActions.test.ts new file mode 100644 index 0000000000..e9ed3d4b1c --- /dev/null +++ b/pkg/standalone-utils/test/VaultQueryActions.test.ts @@ -0,0 +1,134 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; +import Vault from '@balancer-labs/v2-helpers/src/models/vault/Vault'; +import { fp } from '@balancer-labs/v2-helpers/src/numbers'; +import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; +import WeightedPool from '@balancer-labs/v2-helpers/src/models/pools/weighted/WeightedPool'; +import { SwapKind } from '@balancer-labs/balancer-js'; +import { randomAddress } from '@balancer-labs/v2-helpers/src/constants'; +import { Contract } from 'ethers'; +import { expect } from 'chai'; +import { expectChainedReferenceContents, toChainedReference } from './helpers/chainedReferences'; +import TypesConverter from '@balancer-labs/v2-helpers/src/models/types/TypesConverter'; +import { Account } from '@balancer-labs/v2-helpers/src/models/types/types'; +import { setupRelayerEnvironment, encodeSwap, approveVaultForRelayer } from './VaultActionsRelayer.setup'; +import { sharedBeforeEach } from '@balancer-labs/v2-common/sharedBeforeEach'; +import { deploy } from '@balancer-labs/v2-helpers/src/contract'; + +describe('VaultQueryActions', function () { + let queries: Contract; + let vault: Vault; + let tokens: TokenList; + let relayer: Contract, relayerLibrary: Contract; + let user: SignerWithAddress, other: SignerWithAddress; + + let poolIdA: string; + let tokensA: TokenList; + + let recipient: Account; + + before('setup environment', async () => { + ({ user, other, vault, relayer, relayerLibrary } = await setupRelayerEnvironment()); + queries = await deploy('BalancerQueries', { args: [vault.address] }); + }); + + before('setup common recipient', () => { + // All the tests use the same recipient; this is a simple abstraction to improve readability. + recipient = randomAddress(); + }); + + sharedBeforeEach('set up pools', async () => { + tokens = (await TokenList.create(['DAI', 'MKR', 'SNX', 'BAT'])).sort(); + await tokens.mint({ to: user }); + await tokens.approve({ to: vault, from: user }); + + // Pool A: DAI-MKR + tokensA = new TokenList([tokens.DAI, tokens.MKR]).sort(); + const poolA = await WeightedPool.create({ + tokens: tokensA, + vault, + }); + await poolA.init({ initialBalances: fp(1000), from: user }); + + poolIdA = await poolA.getPoolId(); + }); + + describe('simple swap', () => { + const amountIn = fp(2); + + context('when caller is not authorized', () => { + it('reverts', async () => { + expect( + relayer.connect(other).queryMulticall([ + encodeSwap(relayerLibrary, { + poolId: poolIdA, + tokenIn: tokens.DAI, + tokenOut: tokens.MKR, + amount: amountIn, + sender: user.address, + recipient, + }), + ]) + ).to.be.revertedWith('Incorrect sender'); + }); + }); + + context('when caller is authorized', () => { + let sender: Account; + + context('sender = user', () => { + beforeEach(() => { + sender = user; + }); + + itTestsSimpleSwap(); + }); + + context('sender = relayer', () => { + sharedBeforeEach('fund relayer with tokens and approve vault', async () => { + sender = relayer; + await tokens.DAI.transfer(relayer, amountIn, { from: user }); + await approveVaultForRelayer(relayerLibrary, user, tokens); + }); + + itTestsSimpleSwap(); + }); + + function itTestsSimpleSwap() { + it('stores swap output as chained reference', async () => { + const expectedAmountOut = await queries.querySwap( + { + poolId: poolIdA, + kind: SwapKind.GivenIn, + assetIn: tokens.DAI.address, + assetOut: tokens.MKR.address, + amount: amountIn, + userData: '0x', + }, + { + sender: TypesConverter.toAddress(sender), + recipient: TypesConverter.toAddress(recipient), + fromInternalBalance: false, + toInternalBalance: false, + } + ); + + await ( + await relayer.connect(user).multicall([ + encodeSwap(relayerLibrary, { + poolId: poolIdA, + tokenIn: tokens.DAI, + tokenOut: tokens.MKR, + amount: amountIn, + outputReference: toChainedReference(0), + sender, + recipient, + }), + ]) + ).wait(); + + await expectChainedReferenceContents(relayer, toChainedReference(0), expectedAmountOut); + }); + } + }); + }); +}); From f68f4f79df9c14d15e30696d6bfc91eca4e5af74 Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Sun, 11 Jun 2023 15:24:19 -0400 Subject: [PATCH 03/19] fix: relayer benchmark --- pvt/benchmarks/relayer.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pvt/benchmarks/relayer.ts b/pvt/benchmarks/relayer.ts index 671076dfdb..4e75e85ed7 100644 --- a/pvt/benchmarks/relayer.ts +++ b/pvt/benchmarks/relayer.ts @@ -15,7 +15,10 @@ const wordBytesSize = 32; async function main() { ({ vault } = await setupEnvironment()); relayerLibrary = await deploy('v2-standalone-utils/MockBaseRelayerLibrary', { args: [vault.address] }); - relayer = await deploy('v2-standalone-utils/BalancerRelayer', { args: [vault.address, relayerLibrary.address] }); + const queryLibrary = await deploy('v2-standalone-utils/BatchRelayerQueryLibrary', { args: [vault.address] }); + relayer = await deploy('v2-standalone-utils/BalancerRelayer', { + args: [vault.address, relayerLibrary.address, queryLibrary.address], + }); let totalGasUsed = bn(0); console.log('== Measuring multicall gas usage ==\n'); From 64183ca1fd3813135f5fde3c65227ae877623cb8 Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Thu, 15 Jun 2023 19:47:52 -0400 Subject: [PATCH 04/19] fix: always use queryMulticall --- pkg/standalone-utils/test/VaultQueryActions.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/standalone-utils/test/VaultQueryActions.test.ts b/pkg/standalone-utils/test/VaultQueryActions.test.ts index e9ed3d4b1c..97ad8edeca 100644 --- a/pkg/standalone-utils/test/VaultQueryActions.test.ts +++ b/pkg/standalone-utils/test/VaultQueryActions.test.ts @@ -113,7 +113,7 @@ describe('VaultQueryActions', function () { ); await ( - await relayer.connect(user).multicall([ + await relayer.connect(user).queryMulticall([ encodeSwap(relayerLibrary, { poolId: poolIdA, tokenIn: tokens.DAI, From 9f847071f368b5909942e6b8cc7e0ea822726ddc Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Mon, 3 Jul 2023 15:21:23 -0400 Subject: [PATCH 05/19] docs: retain full interface --- .../contracts/relayer/VaultQueryActions.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol b/pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol index e169111221..f106edcdcb 100644 --- a/pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol +++ b/pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol @@ -31,7 +31,7 @@ abstract contract VaultQueryActions is VaultActions { IVault.SingleSwap memory singleSwap, IVault.FundManagement calldata funds, uint256 limit, - uint256, // deadline (could remove, or leave in if we need to preserve the interface) + uint256, // deadline uint256, // value uint256 outputReference ) external payable override { @@ -96,7 +96,7 @@ abstract contract VaultQueryActions is VaultActions { IAsset[] calldata assets, IVault.FundManagement calldata funds, int256[] calldata limits, - uint256, // deadline (could remove, or leave in if we need to preserve the interface) + uint256, // deadline uint256, // value OutputReference[] calldata outputReferences ) external payable override { @@ -131,7 +131,7 @@ abstract contract VaultQueryActions is VaultActions { address sender, address recipient, IVault.JoinPoolRequest memory request, - uint256, // value (could remove, or leave in if we need to preserve the interface) + uint256, // value uint256 outputReference ) external payable override { require(sender == msg.sender || sender == address(this), "Incorrect sender"); From 8a88635f3c4246210de2e6bd58fa131cbb698dcd Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Mon, 3 Jul 2023 15:22:48 -0400 Subject: [PATCH 06/19] refactor: rename queryMulticall to vaultActionsQueryMulticall, to clarify scope --- .../contracts/standalone-utils/IBalancerRelayer.sol | 2 +- pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol | 7 ++++++- pkg/standalone-utils/test/VaultQueryActions.test.ts | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/pkg/interfaces/contracts/standalone-utils/IBalancerRelayer.sol b/pkg/interfaces/contracts/standalone-utils/IBalancerRelayer.sol index fb67e617df..b3ab5d7e98 100644 --- a/pkg/interfaces/contracts/standalone-utils/IBalancerRelayer.sol +++ b/pkg/interfaces/contracts/standalone-utils/IBalancerRelayer.sol @@ -28,5 +28,5 @@ interface IBalancerRelayer { function multicall(bytes[] calldata data) external payable returns (bytes[] memory results); - function queryMulticall(bytes[] calldata data) external returns (bytes[] memory results); + function vaultActionsQueryMulticall(bytes[] calldata data) external returns (bytes[] memory results); } diff --git a/pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol b/pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol index c9ada75602..dc026c73ec 100644 --- a/pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol +++ b/pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol @@ -91,7 +91,12 @@ contract BalancerRelayer is IBalancerRelayer, Version, ReentrancyGuard { _refundETH(); } - function queryMulticall(bytes[] calldata data) external override nonReentrant returns (bytes[] memory results) { + function vaultActionsQueryMulticall(bytes[] calldata data) + external + override + nonReentrant + returns (bytes[] memory results) + { results = new bytes[](data.length); for (uint256 i = 0; i < data.length; i++) { results[i] = _queryLibrary.functionDelegateCall(data[i]); diff --git a/pkg/standalone-utils/test/VaultQueryActions.test.ts b/pkg/standalone-utils/test/VaultQueryActions.test.ts index 97ad8edeca..db0856a003 100644 --- a/pkg/standalone-utils/test/VaultQueryActions.test.ts +++ b/pkg/standalone-utils/test/VaultQueryActions.test.ts @@ -58,7 +58,7 @@ describe('VaultQueryActions', function () { context('when caller is not authorized', () => { it('reverts', async () => { expect( - relayer.connect(other).queryMulticall([ + relayer.connect(other).vaultActionsQueryMulticall([ encodeSwap(relayerLibrary, { poolId: poolIdA, tokenIn: tokens.DAI, @@ -113,7 +113,7 @@ describe('VaultQueryActions', function () { ); await ( - await relayer.connect(user).queryMulticall([ + await relayer.connect(user).vaultActionsQueryMulticall([ encodeSwap(relayerLibrary, { poolId: poolIdA, tokenIn: tokens.DAI, From 5ce552d1cd92e4f9af203c057ed74e72c000db29 Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Mon, 3 Jul 2023 16:12:37 -0400 Subject: [PATCH 07/19] refactor: move encodeBatchSwap up to setup so it can be shared --- .../test/VaultActions.test.ts | 63 ++++++------------- .../test/VaultActionsRelayer.setup.ts | 47 +++++++++++++- 2 files changed, 64 insertions(+), 46 deletions(-) diff --git a/pkg/standalone-utils/test/VaultActions.test.ts b/pkg/standalone-utils/test/VaultActions.test.ts index b4a841af2d..a0e6930f90 100644 --- a/pkg/standalone-utils/test/VaultActions.test.ts +++ b/pkg/standalone-utils/test/VaultActions.test.ts @@ -25,6 +25,7 @@ import { encodeJoinPool, encodeExitPool, encodeSwap, + encodeBatchSwap, getJoinExitAmounts, approveVaultForRelayer, PoolKind, @@ -88,50 +89,6 @@ describe('VaultActions', function () { poolIdC = await poolC.getPoolId(); }); - function encodeBatchSwap(params: { - swaps: Array<{ - poolId: string; - tokenIn: Token; - tokenOut: Token; - amount: BigNumberish; - }>; - outputReferences?: Dictionary; - sender: Account; - recipient?: Account; - useInternalBalance?: boolean; - }): string { - const outputReferences = Object.entries(params.outputReferences ?? {}).map(([symbol, key]) => ({ - index: tokens.findIndexBySymbol(symbol), - key, - })); - - if (params.useInternalBalance == undefined) { - params.useInternalBalance = false; - } - - return relayerLibrary.interface.encodeFunctionData('batchSwap', [ - SwapKind.GivenIn, - params.swaps.map((swap) => ({ - poolId: swap.poolId, - assetInIndex: tokens.indexOf(swap.tokenIn), - assetOutIndex: tokens.indexOf(swap.tokenOut), - amount: swap.amount, - userData: '0x', - })), - tokens.addresses, - { - sender: TypesConverter.toAddress(params.sender), - recipient: params.recipient ?? TypesConverter.toAddress(recipient), - fromInternalBalance: params.useInternalBalance, - toInternalBalance: params.useInternalBalance, - }, - new Array(tokens.length).fill(MAX_INT256), - MAX_UINT256, - 0, - outputReferences, - ]); - } - function encodeManageUserBalance(params: { ops: Array<{ kind: UserBalanceOpKind; @@ -332,7 +289,7 @@ describe('VaultActions', function () { context('when caller is not authorized', () => { it('reverts', async () => { await expect( - relayer.connect(other).multicall([encodeBatchSwap({ swaps: [], sender: user.address })]) + relayer.connect(other).multicall([encodeBatchSwap({ relayerLibrary, tokens, swaps: [], sender: user.address, recipient })]) ).to.be.revertedWith('Incorrect sender'); }); }); @@ -365,11 +322,14 @@ describe('VaultActions', function () { () => relayer.connect(user).multicall([ encodeBatchSwap({ + relayerLibrary, + tokens, swaps: [ { poolId: poolIdA, tokenIn: tokens.DAI, tokenOut: tokens.MKR, amount: amountInA }, { poolId: poolIdC, tokenIn: tokens.SNX, tokenOut: tokens.BAT, amount: amountInC }, ], sender, + recipient, }), ]), tokens, @@ -406,11 +366,14 @@ describe('VaultActions', function () { const receipt = await ( await relayer.connect(user).multicall([ encodeBatchSwap({ + relayerLibrary, + tokens, swaps: [ { poolId: poolIdA, tokenIn: tokens.DAI, tokenOut: tokens.MKR, amount: amountInA }, { poolId: poolIdC, tokenIn: tokens.SNX, tokenOut: tokens.BAT, amount: amountInC }, ], sender, + recipient, outputReferences: { MKR: toChainedReference(0), SNX: toChainedReference(1), @@ -440,6 +403,8 @@ describe('VaultActions', function () { const receipt = await ( await relayer.connect(user).multicall([ encodeBatchSwap({ + relayerLibrary, + tokens, swaps: [ { poolId: poolIdA, tokenIn: tokens.DAI, tokenOut: tokens.MKR, amount: amountInA }, { @@ -450,6 +415,7 @@ describe('VaultActions', function () { }, ], sender, + recipient, }), ]) ).wait(); @@ -466,6 +432,8 @@ describe('VaultActions', function () { () => relayer.connect(user).multicall([ encodeBatchSwap({ + relayerLibrary, + tokens, swaps: [ { poolId: poolIdA, tokenIn: tokens.DAI, tokenOut: tokens.MKR, amount: amountInA }, { poolId: poolIdC, tokenIn: tokens.SNX, tokenOut: tokens.BAT, amount: amountInC }, @@ -478,6 +446,8 @@ describe('VaultActions', function () { recipient: TypesConverter.toAddress(sender), // Override default recipient to chain the output with the next swap. }), encodeBatchSwap({ + relayerLibrary, + tokens, swaps: [ // Swap previously acquired MKR for SNX { @@ -495,6 +465,7 @@ describe('VaultActions', function () { }, ], sender, + recipient, }), ]), tokens, @@ -1473,6 +1444,8 @@ describe('VaultActions', function () { recipient: relayer.address, }), encodeBatchSwap({ + relayerLibrary, + tokens, swaps: [{ poolId: poolIdB, tokenIn: tokens.MKR, tokenOut: tokens.SNX, amount: toChainedReference(1) }], outputReferences: { SNX: toChainedReference(1), diff --git a/pkg/standalone-utils/test/VaultActionsRelayer.setup.ts b/pkg/standalone-utils/test/VaultActionsRelayer.setup.ts index 8f6b325672..5eec3fdb2d 100644 --- a/pkg/standalone-utils/test/VaultActionsRelayer.setup.ts +++ b/pkg/standalone-utils/test/VaultActionsRelayer.setup.ts @@ -2,7 +2,7 @@ import { ethers } from 'hardhat'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; import Vault from '@balancer-labs/v2-helpers/src/models/vault/Vault'; import { BigNumber, Contract } from 'ethers'; -import { MAX_UINT256, ZERO_ADDRESS } from '@balancer-labs/v2-helpers/src/constants'; +import { MAX_INT256, MAX_UINT256, ZERO_ADDRESS } from '@balancer-labs/v2-helpers/src/constants'; import { deploy, deployedAt } from '@balancer-labs/v2-helpers/src/contract'; import { actionId } from '@balancer-labs/v2-helpers/src/models/misc/actions'; import { BigNumberish } from '@balancer-labs/v2-helpers/src/numbers'; @@ -158,6 +158,51 @@ export function encodeSwap( ]); } +export function encodeBatchSwap(params: { + relayerLibrary: Contract, + tokens: TokenList, + swaps: Array<{ + poolId: string; + tokenIn: Token; + tokenOut: Token; + amount: BigNumberish; + }>; + outputReferences?: Dictionary; + sender: Account; + recipient?: Account; + useInternalBalance?: boolean; +}): string { + const outputReferences = Object.entries(params.outputReferences ?? {}).map(([symbol, key]) => ({ + index: params.tokens.findIndexBySymbol(symbol), + key, + })); + + if (params.useInternalBalance == undefined) { + params.useInternalBalance = false; + } + + return params.relayerLibrary.interface.encodeFunctionData('batchSwap', [ + SwapKind.GivenIn, + params.swaps.map((swap) => ({ + poolId: swap.poolId, + assetInIndex: params.tokens.indexOf(swap.tokenIn), + assetOutIndex: params.tokens.indexOf(swap.tokenOut), + amount: swap.amount, + userData: '0x', + })), + params.tokens.addresses, + { + sender: TypesConverter.toAddress(params.sender), + recipient: params.recipient ?? TypesConverter.toAddress(recipient), + fromInternalBalance: params.useInternalBalance, + toInternalBalance: params.useInternalBalance, + }, + new Array(params.tokens.length).fill(MAX_INT256), + MAX_UINT256, + 0, + outputReferences, + ]); +} export function getJoinExitAmounts(poolTokens: TokenList, tokenAmounts: Dictionary): Array { return poolTokens.map((token) => tokenAmounts[token.symbol] ?? 0); } From d0f45bbe7b01a66de4b83e394caefa20c8c73fdc Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Mon, 3 Jul 2023 16:51:39 -0400 Subject: [PATCH 08/19] feat: add batchSwap test --- .../test/VaultActions.test.ts | 9 +- .../test/VaultActionsRelayer.setup.ts | 4 +- .../test/VaultQueryActions.test.ts | 90 ++++++++++++++++++- 3 files changed, 96 insertions(+), 7 deletions(-) diff --git a/pkg/standalone-utils/test/VaultActions.test.ts b/pkg/standalone-utils/test/VaultActions.test.ts index a0e6930f90..ca645354d3 100644 --- a/pkg/standalone-utils/test/VaultActions.test.ts +++ b/pkg/standalone-utils/test/VaultActions.test.ts @@ -3,15 +3,14 @@ import Vault from '@balancer-labs/v2-helpers/src/models/vault/Vault'; import { BigNumberish, fp } from '@balancer-labs/v2-helpers/src/numbers'; import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; import WeightedPool from '@balancer-labs/v2-helpers/src/models/pools/weighted/WeightedPool'; -import { getPoolAddress, SwapKind, UserBalanceOpKind, WeightedPoolEncoder } from '@balancer-labs/balancer-js'; -import { MAX_INT256, MAX_UINT256, randomAddress, ZERO_ADDRESS } from '@balancer-labs/v2-helpers/src/constants'; +import { getPoolAddress, UserBalanceOpKind, WeightedPoolEncoder } from '@balancer-labs/balancer-js'; +import { MAX_UINT256, randomAddress, ZERO_ADDRESS } from '@balancer-labs/v2-helpers/src/constants'; import { expectBalanceChange } from '@balancer-labs/v2-helpers/src/test/tokenBalance'; import * as expectEvent from '@balancer-labs/v2-helpers/src/test/expectEvent'; import { expectTransferEvent } from '@balancer-labs/v2-helpers/src/test/expectTransfer'; import { Contract } from 'ethers'; import { expect } from 'chai'; import Token from '@balancer-labs/v2-helpers/src/models/tokens/Token'; -import { Dictionary } from 'lodash'; import { Zero } from '@ethersproject/constants'; import { expectChainedReferenceContents, @@ -289,7 +288,9 @@ describe('VaultActions', function () { context('when caller is not authorized', () => { it('reverts', async () => { await expect( - relayer.connect(other).multicall([encodeBatchSwap({ relayerLibrary, tokens, swaps: [], sender: user.address, recipient })]) + relayer + .connect(other) + .multicall([encodeBatchSwap({ relayerLibrary, tokens, swaps: [], sender: user.address, recipient })]) ).to.be.revertedWith('Incorrect sender'); }); }); diff --git a/pkg/standalone-utils/test/VaultActionsRelayer.setup.ts b/pkg/standalone-utils/test/VaultActionsRelayer.setup.ts index 5eec3fdb2d..4a9ab042cd 100644 --- a/pkg/standalone-utils/test/VaultActionsRelayer.setup.ts +++ b/pkg/standalone-utils/test/VaultActionsRelayer.setup.ts @@ -159,8 +159,8 @@ export function encodeSwap( } export function encodeBatchSwap(params: { - relayerLibrary: Contract, - tokens: TokenList, + relayerLibrary: Contract; + tokens: TokenList; swaps: Array<{ poolId: string; tokenIn: Token; diff --git a/pkg/standalone-utils/test/VaultQueryActions.test.ts b/pkg/standalone-utils/test/VaultQueryActions.test.ts index db0856a003..5d0bfc60d4 100644 --- a/pkg/standalone-utils/test/VaultQueryActions.test.ts +++ b/pkg/standalone-utils/test/VaultQueryActions.test.ts @@ -10,7 +10,12 @@ import { expect } from 'chai'; import { expectChainedReferenceContents, toChainedReference } from './helpers/chainedReferences'; import TypesConverter from '@balancer-labs/v2-helpers/src/models/types/TypesConverter'; import { Account } from '@balancer-labs/v2-helpers/src/models/types/types'; -import { setupRelayerEnvironment, encodeSwap, approveVaultForRelayer } from './VaultActionsRelayer.setup'; +import { + setupRelayerEnvironment, + encodeSwap, + encodeBatchSwap, + approveVaultForRelayer, +} from './VaultActionsRelayer.setup'; import { sharedBeforeEach } from '@balancer-labs/v2-common/sharedBeforeEach'; import { deploy } from '@balancer-labs/v2-helpers/src/contract'; @@ -131,4 +136,87 @@ describe('VaultQueryActions', function () { } }); }); + + describe('batch swap', () => { + const amountIn = fp(5); + + context('when caller is not authorized', () => { + it('reverts', async () => { + expect( + relayer.connect(other).vaultActionsQueryMulticall([ + encodeBatchSwap({ + relayerLibrary, + tokens, + swaps: [ + { poolId: poolIdA, tokenIn: tokens.DAI, tokenOut: tokens.MKR, amount: amountIn }, + { poolId: poolIdA, tokenIn: tokens.MKR, tokenOut: tokens.DAI, amount: 0 }, + ], + sender: other, + recipient, + }), + ]) + ).to.be.revertedWith('Incorrect sender'); + }); + }); + + context('when caller is authorized', () => { + let sender: Account; + + context('sender = user', () => { + beforeEach(() => { + sender = user; + }); + + itTestsBatchSwap(); + }); + + context('sender = relayer', () => { + sharedBeforeEach('fund relayer with tokens and approve vault', async () => { + sender = relayer; + await tokens.DAI.transfer(relayer, amountIn, { from: user }); + await approveVaultForRelayer(relayerLibrary, user, tokens); + }); + + itTestsBatchSwap(); + }); + + function itTestsBatchSwap() { + it('stores batch swap output as chained reference', async () => { + const amount = fp(1); + const indexIn = tokens.indexOf(tokens.DAI); + const indexOut = tokens.indexOf(tokens.MKR); + + const result = await queries.queryBatchSwap( + SwapKind.GivenIn, + [{ poolId: poolIdA, assetInIndex: indexIn, assetOutIndex: indexOut, amount, userData: '0x' }], + tokens.addresses, + { + sender: TypesConverter.toAddress(sender), + recipient, + fromInternalBalance: false, + toInternalBalance: false, + } + ); + + expect(result[indexIn]).to.deep.equal(amount); + const expectedAmountOut = result[indexOut].mul(-1); + + await ( + await relayer.connect(user).vaultActionsQueryMulticall([ + encodeBatchSwap({ + relayerLibrary, + tokens, + swaps: [{ poolId: poolIdA, tokenIn: tokens.DAI, tokenOut: tokens.MKR, amount }], + sender, + recipient, + outputReferences: { MKR: toChainedReference(0) }, + }), + ]) + ).wait(); + + await expectChainedReferenceContents(relayer, toChainedReference(0), expectedAmountOut); + }); + } + }); + }); }); From 7eaed85f5dfa1c3e9c5ead6d52257c2a3267544c Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Mon, 3 Jul 2023 16:57:17 -0400 Subject: [PATCH 09/19] refactor: no need for approvals --- .../test/VaultQueryActions.test.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/pkg/standalone-utils/test/VaultQueryActions.test.ts b/pkg/standalone-utils/test/VaultQueryActions.test.ts index 5d0bfc60d4..716febae1f 100644 --- a/pkg/standalone-utils/test/VaultQueryActions.test.ts +++ b/pkg/standalone-utils/test/VaultQueryActions.test.ts @@ -10,12 +10,7 @@ import { expect } from 'chai'; import { expectChainedReferenceContents, toChainedReference } from './helpers/chainedReferences'; import TypesConverter from '@balancer-labs/v2-helpers/src/models/types/TypesConverter'; import { Account } from '@balancer-labs/v2-helpers/src/models/types/types'; -import { - setupRelayerEnvironment, - encodeSwap, - encodeBatchSwap, - approveVaultForRelayer, -} from './VaultActionsRelayer.setup'; +import { setupRelayerEnvironment, encodeSwap, encodeBatchSwap } from './VaultActionsRelayer.setup'; import { sharedBeforeEach } from '@balancer-labs/v2-common/sharedBeforeEach'; import { deploy } from '@balancer-labs/v2-helpers/src/contract'; @@ -89,10 +84,8 @@ describe('VaultQueryActions', function () { }); context('sender = relayer', () => { - sharedBeforeEach('fund relayer with tokens and approve vault', async () => { + beforeEach(() => { sender = relayer; - await tokens.DAI.transfer(relayer, amountIn, { from: user }); - await approveVaultForRelayer(relayerLibrary, user, tokens); }); itTestsSimpleSwap(); @@ -171,10 +164,8 @@ describe('VaultQueryActions', function () { }); context('sender = relayer', () => { - sharedBeforeEach('fund relayer with tokens and approve vault', async () => { + beforeEach(() => { sender = relayer; - await tokens.DAI.transfer(relayer, amountIn, { from: user }); - await approveVaultForRelayer(relayerLibrary, user, tokens); }); itTestsBatchSwap(); From ad60bebc9528698678c1f6280c849c9be0c925ac Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Mon, 3 Jul 2023 17:22:42 -0400 Subject: [PATCH 10/19] feat: add join test --- .../test/VaultQueryActions.test.ts | 89 ++++++++++++++++++- 1 file changed, 85 insertions(+), 4 deletions(-) diff --git a/pkg/standalone-utils/test/VaultQueryActions.test.ts b/pkg/standalone-utils/test/VaultQueryActions.test.ts index 716febae1f..9889540b7c 100644 --- a/pkg/standalone-utils/test/VaultQueryActions.test.ts +++ b/pkg/standalone-utils/test/VaultQueryActions.test.ts @@ -3,14 +3,20 @@ import Vault from '@balancer-labs/v2-helpers/src/models/vault/Vault'; import { fp } from '@balancer-labs/v2-helpers/src/numbers'; import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; import WeightedPool from '@balancer-labs/v2-helpers/src/models/pools/weighted/WeightedPool'; -import { SwapKind } from '@balancer-labs/balancer-js'; -import { randomAddress } from '@balancer-labs/v2-helpers/src/constants'; -import { Contract } from 'ethers'; +import { SwapKind, WeightedPoolEncoder } from '@balancer-labs/balancer-js'; +import { MAX_UINT112, randomAddress } from '@balancer-labs/v2-helpers/src/constants'; +import { Contract, BigNumber } from 'ethers'; import { expect } from 'chai'; import { expectChainedReferenceContents, toChainedReference } from './helpers/chainedReferences'; import TypesConverter from '@balancer-labs/v2-helpers/src/models/types/TypesConverter'; import { Account } from '@balancer-labs/v2-helpers/src/models/types/types'; -import { setupRelayerEnvironment, encodeSwap, encodeBatchSwap } from './VaultActionsRelayer.setup'; +import { + setupRelayerEnvironment, + encodeSwap, + encodeBatchSwap, + encodeJoinPool, + PoolKind, +} from './VaultActionsRelayer.setup'; import { sharedBeforeEach } from '@balancer-labs/v2-common/sharedBeforeEach'; import { deploy } from '@balancer-labs/v2-helpers/src/contract'; @@ -210,4 +216,79 @@ describe('VaultQueryActions', function () { } }); }); + + describe('join', () => { + let expectedBptOut: BigNumber, amountsIn: BigNumber[], data: string; + const maxAmountsIn: BigNumber[] = [MAX_UINT112, MAX_UINT112]; + + sharedBeforeEach('estimate expected bpt out', async () => { + amountsIn = [fp(1), fp(0)]; + data = WeightedPoolEncoder.joinExactTokensInForBPTOut(amountsIn, 0); + }); + + context('when caller is not authorized', () => { + it('reverts', async () => { + expect( + relayer.connect(other).vaultActionsQueryMulticall([ + encodeJoinPool(vault, relayerLibrary, { + poolId: poolIdA, + userData: data, + sender: user.address, + recipient, + poolKind: PoolKind.WEIGHTED, + }), + ]) + ).to.be.revertedWith('Incorrect sender'); + }); + }); + + context('when caller is authorized', () => { + let sender: Account; + + context('sender = user', () => { + beforeEach(() => { + sender = user; + }); + + itTestsJoin(); + }); + + context('sender = relayer', () => { + beforeEach(() => { + sender = relayer; + }); + + itTestsJoin(); + }); + + function itTestsJoin() { + it('stores join as chained reference', async () => { + const result = await queries.queryJoin(poolIdA, TypesConverter.toAddress(sender), recipient, { + assets: tokensA.addresses, + maxAmountsIn, + fromInternalBalance: false, + userData: data, + }); + + expect(result.amountsIn).to.deep.equal(amountsIn); + expectedBptOut = result.bptOut; + + await ( + await relayer.connect(user).vaultActionsQueryMulticall([ + encodeJoinPool(vault, relayerLibrary, { + poolId: poolIdA, + userData: data, + outputReference: toChainedReference(0), + sender, + recipient, + poolKind: PoolKind.WEIGHTED, + }), + ]) + ).wait(); + + await expectChainedReferenceContents(relayer, toChainedReference(0), expectedBptOut); + }); + } + }); + }); }); From 19fc49714a7d8988a09fb5bdab92af4f5c82ec1c Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Mon, 3 Jul 2023 17:42:12 -0400 Subject: [PATCH 11/19] feat: add exitPool test --- .../test/VaultQueryActions.test.ts | 94 ++++++++++++++++++- 1 file changed, 91 insertions(+), 3 deletions(-) diff --git a/pkg/standalone-utils/test/VaultQueryActions.test.ts b/pkg/standalone-utils/test/VaultQueryActions.test.ts index 9889540b7c..9991c69fdd 100644 --- a/pkg/standalone-utils/test/VaultQueryActions.test.ts +++ b/pkg/standalone-utils/test/VaultQueryActions.test.ts @@ -15,17 +15,21 @@ import { encodeSwap, encodeBatchSwap, encodeJoinPool, + encodeExitPool, PoolKind, } from './VaultActionsRelayer.setup'; import { sharedBeforeEach } from '@balancer-labs/v2-common/sharedBeforeEach'; import { deploy } from '@balancer-labs/v2-helpers/src/contract'; describe('VaultQueryActions', function () { + const INITIAL_BALANCE = fp(1000); + let queries: Contract; let vault: Vault; let tokens: TokenList; let relayer: Contract, relayerLibrary: Contract; let user: SignerWithAddress, other: SignerWithAddress; + let poolA: WeightedPool; let poolIdA: string; let tokensA: TokenList; @@ -49,11 +53,11 @@ describe('VaultQueryActions', function () { // Pool A: DAI-MKR tokensA = new TokenList([tokens.DAI, tokens.MKR]).sort(); - const poolA = await WeightedPool.create({ + poolA = await WeightedPool.create({ tokens: tokensA, vault, }); - await poolA.init({ initialBalances: fp(1000), from: user }); + await poolA.init({ initialBalances: INITIAL_BALANCE, from: user }); poolIdA = await poolA.getPoolId(); }); @@ -262,7 +266,7 @@ describe('VaultQueryActions', function () { }); function itTestsJoin() { - it('stores join as chained reference', async () => { + it('stores join result as chained reference', async () => { const result = await queries.queryJoin(poolIdA, TypesConverter.toAddress(sender), recipient, { assets: tokensA.addresses, maxAmountsIn, @@ -291,4 +295,88 @@ describe('VaultQueryActions', function () { } }); }); + + describe('exit', () => { + let bptIn: BigNumber, calculatedAmountsOut: BigNumber[], data: string; + const minAmountsOut: BigNumber[] = []; + + sharedBeforeEach('estimate expected amounts out', async () => { + bptIn = (await poolA.totalSupply()).div(2); + calculatedAmountsOut = tokensA.map(() => INITIAL_BALANCE.div(2)); + data = WeightedPoolEncoder.exitExactBPTInForTokensOut(bptIn); + }); + + context('when caller is not authorized', () => { + it('reverts', async () => { + expect( + relayer.connect(other).vaultActionsQueryMulticall([ + encodeExitPool(vault, relayerLibrary, tokensA, { + poolId: poolIdA, + userData: data, + toInternalBalance: false, + sender: user.address, + recipient, + poolKind: PoolKind.WEIGHTED, + }), + ]) + ).to.be.revertedWith('Incorrect sender'); + }); + }); + + context('when caller is authorized', () => { + let sender: Account; + + context('sender = user', () => { + beforeEach(() => { + sender = user; + }); + + itTestsExit(); + }); + + context('sender = relayer', () => { + beforeEach(() => { + sender = relayer; + }); + + itTestsExit(); + }); + + function itTestsExit() { + it('stores exit result as chained reference', async () => { + const result = await queries.queryExit(poolIdA, TypesConverter.toAddress(sender), recipient, { + assets: tokensA.addresses, + minAmountsOut, + toInternalBalance: false, + userData: data, + }); + + expect(result.bptIn).to.equal(bptIn); + const expectedAmountsOut = result.amountsOut; + // sanity check + expect(expectedAmountsOut).to.deep.equal(calculatedAmountsOut); + + await ( + await relayer.connect(user).vaultActionsQueryMulticall([ + encodeExitPool(vault, relayerLibrary, tokensA, { + poolId: poolIdA, + userData: data, + outputReferences: { + DAI: toChainedReference(0), + MKR: toChainedReference(1), + }, + sender, + recipient, + toInternalBalance: false, + poolKind: PoolKind.WEIGHTED, + }), + ]) + ).wait(); + + await expectChainedReferenceContents(relayer, toChainedReference(0), expectedAmountsOut[0]); + await expectChainedReferenceContents(relayer, toChainedReference(1), expectedAmountsOut[1]); + }); + } + }); + }); }); From af9e6fe6bf943b6dce66991e1492ea7241b97397 Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Thu, 10 Aug 2023 23:49:44 -0400 Subject: [PATCH 12/19] lint --- pkg/standalone-utils/test/VaultActions.test.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pkg/standalone-utils/test/VaultActions.test.ts b/pkg/standalone-utils/test/VaultActions.test.ts index a73f7c9335..912c94b95e 100644 --- a/pkg/standalone-utils/test/VaultActions.test.ts +++ b/pkg/standalone-utils/test/VaultActions.test.ts @@ -3,12 +3,7 @@ import Vault from '@balancer-labs/v2-helpers/src/models/vault/Vault'; import { BigNumberish, fp } from '@balancer-labs/v2-helpers/src/numbers'; import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; import WeightedPool from '@balancer-labs/v2-helpers/src/models/pools/weighted/WeightedPool'; -import { - BasePoolEncoder, - getPoolAddress, - UserBalanceOpKind, - WeightedPoolEncoder, -} from '@balancer-labs/balancer-js'; +import { BasePoolEncoder, getPoolAddress, UserBalanceOpKind, WeightedPoolEncoder } from '@balancer-labs/balancer-js'; import { MAX_UINT256, randomAddress, ZERO_ADDRESS } from '@balancer-labs/v2-helpers/src/constants'; import { expectBalanceChange } from '@balancer-labs/v2-helpers/src/test/tokenBalance'; import * as expectEvent from '@balancer-labs/v2-helpers/src/test/expectEvent'; From 9c9306ebaa3133dc5f53e015c4268a44220a16af Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Fri, 11 Aug 2023 12:13:49 -0400 Subject: [PATCH 13/19] refactor: optimize multicall --- pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol b/pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol index dc026c73ec..1086890a24 100644 --- a/pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol +++ b/pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol @@ -83,8 +83,10 @@ contract BalancerRelayer is IBalancerRelayer, Version, ReentrancyGuard { } function multicall(bytes[] calldata data) external payable override nonReentrant returns (bytes[] memory results) { - results = new bytes[](data.length); - for (uint256 i = 0; i < data.length; i++) { + uint256 numData = data.length; + + results = new bytes[](numData); + for (uint256 i = 0; i < numData; i++) { results[i] = _library.functionDelegateCall(data[i]); } From a958b17adbe060204697c392002e1262ae956336 Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Wed, 18 Oct 2023 17:43:29 -0400 Subject: [PATCH 14/19] fix: set chained references properly on exits --- pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol b/pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol index f106edcdcb..b66b9e5856 100644 --- a/pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol +++ b/pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol @@ -183,7 +183,7 @@ abstract contract VaultQueryActions is VaultActions { // Save as chained references for (uint256 i = 0; i < outputReferences.length; i++) { - _setChainedReferenceValue(outputReferences[i].key, amountsOut[i]); + _setChainedReferenceValue(outputReferences[i].key, amountsOut[outputReferences[i].index]); } } From d240cd311e4f087cd9542f37aaaf260e970cec27 Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Tue, 24 Oct 2023 15:13:37 -0400 Subject: [PATCH 15/19] fix: restore compile command --- pkg/liquidity-mining/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/liquidity-mining/package.json b/pkg/liquidity-mining/package.json index 60e727e892..5bebc89208 100644 --- a/pkg/liquidity-mining/package.json +++ b/pkg/liquidity-mining/package.json @@ -18,7 +18,7 @@ ], "scripts": { "build": "yarn compile", - "compile": "hardhat compile ", + "compile": "hardhat compile && rm -rf artifacts/build-info", "compile:watch": "nodemon --ext sol --exec yarn compile", "lint": "yarn lint:typescript && yarn lint:solidity", "lint:typescript": "NODE_NO_WARNINGS=1 eslint . --ext .ts --ignore-path ../../.eslintignore --max-warnings 0", From 52b31a3374dd899e0700b905eb49abc7d4e9a259 Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Tue, 24 Oct 2023 15:19:24 -0400 Subject: [PATCH 16/19] refactor: propagate multicall changes to query multicall --- pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol b/pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol index 1086890a24..c8dba9c648 100644 --- a/pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol +++ b/pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol @@ -99,8 +99,10 @@ contract BalancerRelayer is IBalancerRelayer, Version, ReentrancyGuard { nonReentrant returns (bytes[] memory results) { - results = new bytes[](data.length); - for (uint256 i = 0; i < data.length; i++) { + uint256 numData = data.length; + + results = new bytes[](numData); + for (uint256 i = 0; i < numData; i++) { results[i] = _queryLibrary.functionDelegateCall(data[i]); } } From 019ea02d91cfa85962681bc94386569bbe478bb7 Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Tue, 24 Oct 2023 17:30:44 -0400 Subject: [PATCH 17/19] test: test exit indexes on VaultQueryActions --- .../test/VaultQueryActions.test.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/pkg/standalone-utils/test/VaultQueryActions.test.ts b/pkg/standalone-utils/test/VaultQueryActions.test.ts index 9991c69fdd..4beab94893 100644 --- a/pkg/standalone-utils/test/VaultQueryActions.test.ts +++ b/pkg/standalone-utils/test/VaultQueryActions.test.ts @@ -1,6 +1,6 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; import Vault from '@balancer-labs/v2-helpers/src/models/vault/Vault'; -import { fp } from '@balancer-labs/v2-helpers/src/numbers'; +import { FP_ZERO, fp } from '@balancer-labs/v2-helpers/src/numbers'; import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; import WeightedPool from '@balancer-labs/v2-helpers/src/models/pools/weighted/WeightedPool'; import { SwapKind, WeightedPoolEncoder } from '@balancer-labs/balancer-js'; @@ -20,6 +20,7 @@ import { } from './VaultActionsRelayer.setup'; import { sharedBeforeEach } from '@balancer-labs/v2-common/sharedBeforeEach'; import { deploy } from '@balancer-labs/v2-helpers/src/contract'; +import { expectArrayEqualWithError } from '@balancer-labs/v2-helpers/src/test/relativeError'; describe('VaultQueryActions', function () { const INITIAL_BALANCE = fp(1000); @@ -301,9 +302,12 @@ describe('VaultQueryActions', function () { const minAmountsOut: BigNumber[] = []; sharedBeforeEach('estimate expected amounts out', async () => { - bptIn = (await poolA.totalSupply()).div(2); - calculatedAmountsOut = tokensA.map(() => INITIAL_BALANCE.div(2)); - data = WeightedPoolEncoder.exitExactBPTInForTokensOut(bptIn); + bptIn = (await poolA.totalSupply()).div(5); + const tokenIn = await poolA.estimateTokenOut(0, bptIn); + calculatedAmountsOut = [BigNumber.from(tokenIn), FP_ZERO]; + // Use a non-proportional exit so that the token amounts are different + // (so that we can see whether indexes are used) + data = WeightedPoolEncoder.exitExactBPTInForOneTokenOut(bptIn, 0); }); context('when caller is not authorized', () => { @@ -354,8 +358,9 @@ describe('VaultQueryActions', function () { expect(result.bptIn).to.equal(bptIn); const expectedAmountsOut = result.amountsOut; // sanity check - expect(expectedAmountsOut).to.deep.equal(calculatedAmountsOut); + expectArrayEqualWithError(expectedAmountsOut, calculatedAmountsOut); + // Pass an "out of order" reference, to ensure it it is using the index values await ( await relayer.connect(user).vaultActionsQueryMulticall([ encodeExitPool(vault, relayerLibrary, tokensA, { @@ -363,7 +368,7 @@ describe('VaultQueryActions', function () { userData: data, outputReferences: { DAI: toChainedReference(0), - MKR: toChainedReference(1), + MKR: toChainedReference(3), }, sender, recipient, @@ -373,8 +378,8 @@ describe('VaultQueryActions', function () { ]) ).wait(); - await expectChainedReferenceContents(relayer, toChainedReference(0), expectedAmountsOut[0]); - await expectChainedReferenceContents(relayer, toChainedReference(1), expectedAmountsOut[1]); + await expectChainedReferenceContents(relayer, toChainedReference(0), expectedAmountsOut[tokensA.indexOf(tokensA.DAI)]); + await expectChainedReferenceContents(relayer, toChainedReference(3), expectedAmountsOut[tokensA.indexOf(tokensA.MKR)]); }); } }); From 9893712b74339d1d2602e380475c40e61da13920 Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Tue, 24 Oct 2023 17:35:48 -0400 Subject: [PATCH 18/19] lint --- pkg/standalone-utils/test/VaultQueryActions.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/standalone-utils/test/VaultQueryActions.test.ts b/pkg/standalone-utils/test/VaultQueryActions.test.ts index 4beab94893..1ca1e2ecb5 100644 --- a/pkg/standalone-utils/test/VaultQueryActions.test.ts +++ b/pkg/standalone-utils/test/VaultQueryActions.test.ts @@ -378,8 +378,16 @@ describe('VaultQueryActions', function () { ]) ).wait(); - await expectChainedReferenceContents(relayer, toChainedReference(0), expectedAmountsOut[tokensA.indexOf(tokensA.DAI)]); - await expectChainedReferenceContents(relayer, toChainedReference(3), expectedAmountsOut[tokensA.indexOf(tokensA.MKR)]); + await expectChainedReferenceContents( + relayer, + toChainedReference(0), + expectedAmountsOut[tokensA.indexOf(tokensA.DAI)] + ); + await expectChainedReferenceContents( + relayer, + toChainedReference(3), + expectedAmountsOut[tokensA.indexOf(tokensA.MKR)] + ); }); } }); From 5a511be09252994822353dea809bc14fbe2eed9d Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Wed, 25 Oct 2023 10:42:38 -0400 Subject: [PATCH 19/19] refactor: disable manageUserBalance, as inconsistent with query operations --- .../contracts/relayer/VaultActions.sol | 7 ++- .../contracts/relayer/VaultQueryActions.sol | 12 +++++ .../test/VaultQueryActions.test.ts | 50 ++++++++++++++++++- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/pkg/standalone-utils/contracts/relayer/VaultActions.sol b/pkg/standalone-utils/contracts/relayer/VaultActions.sol index eed5203d29..8fbbf19aae 100644 --- a/pkg/standalone-utils/contracts/relayer/VaultActions.sol +++ b/pkg/standalone-utils/contracts/relayer/VaultActions.sol @@ -32,7 +32,10 @@ import "./IBaseRelayerLibrary.sol"; * @dev Since the relayer is not expected to hold user funds, we expect the user to be the recipient of any token * transfers from the Vault. * - * All functions must be payable so they can be called from a multicall involving ETH + * All functions must be payable so they can be called from a multicall involving ETH. + * + * Note that this is a base contract for VaultQueryActions. Any functions that should not be called in a query context + * (e.g., `manageUserBalance`), should be virtual here, and overridden to revert in VaultQueryActions. */ abstract contract VaultActions is IBaseRelayerLibrary { using Math for uint256; @@ -114,7 +117,7 @@ abstract contract VaultActions is IBaseRelayerLibrary { IVault.UserBalanceOp[] memory ops, uint256 value, OutputReference[] calldata outputReferences - ) external payable { + ) external payable virtual { for (uint256 i = 0; i < ops.length; i++) { require(ops[i].sender == msg.sender || ops[i].sender == address(this), "Incorrect sender"); diff --git a/pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol b/pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol index b66b9e5856..7ee00ad8a1 100644 --- a/pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol +++ b/pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol @@ -25,6 +25,9 @@ import "./VaultActions.sol"; * @title VaultQueryActions * @notice Allows users to simulate the core functions on the Balancer Vault (swaps/joins/exits), using queries instead * of the actual operations. + * @dev Inherits from VaultActions to maximize reuse - but also pulls in `manageUserBalance`. This might not hurt + * anything, but isn't intended behavior in a query context, so we override and disable it. Anything else added to the + * base contract that isn't query-friendly should likewise be disabled. */ abstract contract VaultQueryActions is VaultActions { function swap( @@ -224,4 +227,13 @@ abstract contract VaultQueryActions is VaultActions { _require(token == expectedTokens[i], Errors.TOKENS_MISMATCH); } } + + /// @dev Prevent `vaultActionsQueryMulticall` from calling manageUserBalance. + function manageUserBalance( + IVault.UserBalanceOp[] memory, + uint256, + OutputReference[] calldata + ) external payable override { + _revert(Errors.UNIMPLEMENTED); + } } diff --git a/pkg/standalone-utils/test/VaultQueryActions.test.ts b/pkg/standalone-utils/test/VaultQueryActions.test.ts index 1ca1e2ecb5..4e020e0378 100644 --- a/pkg/standalone-utils/test/VaultQueryActions.test.ts +++ b/pkg/standalone-utils/test/VaultQueryActions.test.ts @@ -1,9 +1,9 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; import Vault from '@balancer-labs/v2-helpers/src/models/vault/Vault'; -import { FP_ZERO, fp } from '@balancer-labs/v2-helpers/src/numbers'; +import { BigNumberish, FP_ZERO, fp } from '@balancer-labs/v2-helpers/src/numbers'; import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; import WeightedPool from '@balancer-labs/v2-helpers/src/models/pools/weighted/WeightedPool'; -import { SwapKind, WeightedPoolEncoder } from '@balancer-labs/balancer-js'; +import { SwapKind, UserBalanceOpKind, WeightedPoolEncoder } from '@balancer-labs/balancer-js'; import { MAX_UINT112, randomAddress } from '@balancer-labs/v2-helpers/src/constants'; import { Contract, BigNumber } from 'ethers'; import { expect } from 'chai'; @@ -17,6 +17,7 @@ import { encodeJoinPool, encodeExitPool, PoolKind, + OutputReference, } from './VaultActionsRelayer.setup'; import { sharedBeforeEach } from '@balancer-labs/v2-common/sharedBeforeEach'; import { deploy } from '@balancer-labs/v2-helpers/src/contract'; @@ -392,4 +393,49 @@ describe('VaultQueryActions', function () { } }); }); + + describe('user balance ops', () => { + const amountDAI = fp(2); + const amountSNX = fp(5); + + function encodeManageUserBalance(params: { + ops: Array<{ + kind: UserBalanceOpKind; + asset: string; + amount: BigNumberish; + sender: Account; + recipient?: Account; + }>; + outputReferences?: OutputReference[]; + }): string { + return relayerLibrary.interface.encodeFunctionData('manageUserBalance', [ + params.ops.map((op) => ({ + kind: op.kind, + asset: op.asset, + amount: op.amount, + sender: TypesConverter.toAddress(op.sender), + recipient: op.recipient ?? TypesConverter.toAddress(recipient), + })), + 0, + params.outputReferences ?? [], + ]); + } + + it('does not allow calls to manageUserBalance', async () => { + await expect( + relayer.connect(user).vaultActionsQueryMulticall([ + encodeManageUserBalance({ + ops: [ + { kind: UserBalanceOpKind.DepositInternal, asset: tokens.DAI.address, amount: amountDAI, sender: user }, + { kind: UserBalanceOpKind.DepositInternal, asset: tokens.SNX.address, amount: amountSNX, sender: user }, + ], + outputReferences: [ + { index: 0, key: toChainedReference(0) }, + { index: 1, key: toChainedReference(1) }, + ], + }), + ]) + ).to.be.revertedWith('UNIMPLEMENTED'); + }); + }); });