diff --git a/pkg/interfaces/contracts/standalone-utils/IBalancerRelayer.sol b/pkg/interfaces/contracts/standalone-utils/IBalancerRelayer.sol index 57b93fc6c4..b3ab5d7e98 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 vaultActionsQueryMulticall(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 8a52852fc2..c8dba9c648 100644 --- a/pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol +++ b/pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol @@ -49,6 +49,7 @@ contract BalancerRelayer is IBalancerRelayer, Version, 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 @@ -57,10 +58,12 @@ contract BalancerRelayer is IBalancerRelayer, Version, ReentrancyGuard { constructor( IVault vault, address libraryAddress, + address queryLibrary, string memory version ) Version(version) { _vault = vault; _library = libraryAddress; + _queryLibrary = queryLibrary; } receive() external payable { @@ -80,14 +83,30 @@ 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]); } _refundETH(); } + function vaultActionsQueryMulticall(bytes[] calldata data) + external + override + nonReentrant + returns (bytes[] memory results) + { + uint256 numData = data.length; + + results = new bytes[](numData); + for (uint256 i = 0; i < numData; 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 dd1feda3b3..6ed1e6bf11 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, string memory version) IBaseRelayerLibrary(vault.WETH()) { + constructor(IVault vault, string memory version) BaseRelayerLibraryCommon(vault) { _vault = vault; - _entrypoint = new BalancerRelayer(vault, address(this), version); - } - function getVault() public view override returns (IVault) { - return _vault; + IBaseRelayerLibrary queryLibrary = new BatchRelayerQueryLibrary(vault); + + _entrypoint = new BalancerRelayer(vault, address(this), address(queryLibrary), version); } 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 3818e01a8b..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; @@ -63,7 +66,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)) { @@ -86,7 +89,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) { @@ -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"); @@ -145,7 +148,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). @@ -172,7 +175,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) { @@ -261,7 +264,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. @@ -315,7 +318,7 @@ abstract contract VaultActions is IBaseRelayerLibrary { * references as necessary. */ function _doExitPoolChainedReferenceReplacements(PoolKind kind, bytes memory userData) - private + internal returns (bytes memory) { // Must check for the recovery mode ExitKind first, which is common to all pool types. diff --git a/pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol b/pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol new file mode 100644 index 0000000000..7ee00ad8a1 --- /dev/null +++ b/pkg/standalone-utils/contracts/relayer/VaultQueryActions.sol @@ -0,0 +1,239 @@ +// 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. + * @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( + IVault.SingleSwap memory singleSwap, + IVault.FundManagement calldata funds, + uint256 limit, + uint256, // deadline + 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 + 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 + 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[outputReferences[i].index]); + } + } + + 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); + } + } + + /// @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/VaultActions.test.ts b/pkg/standalone-utils/test/VaultActions.test.ts index f022ddcced..912c94b95e 100644 --- a/pkg/standalone-utils/test/VaultActions.test.ts +++ b/pkg/standalone-utils/test/VaultActions.test.ts @@ -3,21 +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 { - BasePoolEncoder, - 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 { 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'; 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, @@ -31,6 +24,7 @@ import { encodeJoinPool, encodeExitPool, encodeSwap, + encodeBatchSwap, getJoinExitAmounts, approveVaultForRelayer, PoolKind, @@ -96,50 +90,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; @@ -340,7 +290,9 @@ 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'); }); }); @@ -373,11 +325,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, @@ -414,11 +369,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), @@ -448,6 +406,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 }, { @@ -458,6 +418,7 @@ describe('VaultActions', function () { }, ], sender, + recipient, }), ]) ).wait(); @@ -474,6 +435,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 }, @@ -486,6 +449,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 { @@ -503,6 +468,7 @@ describe('VaultActions', function () { }, ], sender, + recipient, }), ]), tokens, @@ -1711,6 +1677,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 a15493e2c6..b237b24899 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'; @@ -159,6 +159,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); } diff --git a/pkg/standalone-utils/test/VaultQueryActions.test.ts b/pkg/standalone-utils/test/VaultQueryActions.test.ts new file mode 100644 index 0000000000..4e020e0378 --- /dev/null +++ b/pkg/standalone-utils/test/VaultQueryActions.test.ts @@ -0,0 +1,441 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; +import Vault from '@balancer-labs/v2-helpers/src/models/vault/Vault'; +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, 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'; +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, + encodeJoinPool, + encodeExitPool, + PoolKind, + OutputReference, +} 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); + + 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; + + 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(); + poolA = await WeightedPool.create({ + tokens: tokensA, + vault, + }); + await poolA.init({ initialBalances: INITIAL_BALANCE, 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).vaultActionsQueryMulticall([ + 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', () => { + beforeEach(() => { + sender = relayer; + }); + + 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).vaultActionsQueryMulticall([ + encodeSwap(relayerLibrary, { + poolId: poolIdA, + tokenIn: tokens.DAI, + tokenOut: tokens.MKR, + amount: amountIn, + outputReference: toChainedReference(0), + sender, + recipient, + }), + ]) + ).wait(); + + await expectChainedReferenceContents(relayer, toChainedReference(0), expectedAmountOut); + }); + } + }); + }); + + 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', () => { + beforeEach(() => { + sender = relayer; + }); + + 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); + }); + } + }); + }); + + 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 result 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); + }); + } + }); + }); + + describe('exit', () => { + let bptIn: BigNumber, calculatedAmountsOut: BigNumber[], data: string; + const minAmountsOut: BigNumber[] = []; + + sharedBeforeEach('estimate expected amounts out', async () => { + 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', () => { + 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 + 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, { + poolId: poolIdA, + userData: data, + outputReferences: { + DAI: toChainedReference(0), + MKR: toChainedReference(3), + }, + sender, + recipient, + toInternalBalance: false, + poolKind: PoolKind.WEIGHTED, + }), + ]) + ).wait(); + + await expectChainedReferenceContents( + relayer, + toChainedReference(0), + expectedAmountsOut[tokensA.indexOf(tokensA.DAI)] + ); + await expectChainedReferenceContents( + relayer, + toChainedReference(3), + expectedAmountsOut[tokensA.indexOf(tokensA.MKR)] + ); + }); + } + }); + }); + + 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'); + }); + }); +}); diff --git a/pvt/benchmarks/relayer.ts b/pvt/benchmarks/relayer.ts index 6e2501d545..d5ce523f2a 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');