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');