Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relayer: query swap/join/exit (V2) #2521

Merged
merged 24 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
43ea00e
feat: first cut at queryMulticall method
EndymionJkb Jun 9, 2023
1c165b2
test: add initial test
EndymionJkb Jun 11, 2023
f68f4f7
fix: relayer benchmark
EndymionJkb Jun 11, 2023
6f51954
Merge branch 'master' into query-relayer-v2
EndymionJkb Jun 15, 2023
64183ca
fix: always use queryMulticall
EndymionJkb Jun 15, 2023
bcca196
Merge branch 'master' into query-relayer-v2
EndymionJkb Jul 3, 2023
9f84707
docs: retain full interface
EndymionJkb Jul 3, 2023
8a88635
refactor: rename queryMulticall to vaultActionsQueryMulticall, to cla…
EndymionJkb Jul 3, 2023
5ce552d
refactor: move encodeBatchSwap up to setup so it can be shared
EndymionJkb Jul 3, 2023
d0f45bb
feat: add batchSwap test
EndymionJkb Jul 3, 2023
7eaed85
refactor: no need for approvals
EndymionJkb Jul 3, 2023
ad60beb
feat: add join test
EndymionJkb Jul 3, 2023
19fc497
feat: add exitPool test
EndymionJkb Jul 3, 2023
ff1b081
Merge branch 'master' into query-relayer-v2
EndymionJkb Aug 11, 2023
af9e6fe
lint
EndymionJkb Aug 11, 2023
9c9306e
refactor: optimize multicall
EndymionJkb Aug 11, 2023
b2801b7
Merge branch 'master' into query-relayer-v2
EndymionJkb Sep 14, 2023
a958b17
fix: set chained references properly on exits
EndymionJkb Oct 18, 2023
d240cd3
fix: restore compile command
EndymionJkb Oct 24, 2023
52b31a3
refactor: propagate multicall changes to query multicall
EndymionJkb Oct 24, 2023
019ea02
test: test exit indexes on VaultQueryActions
EndymionJkb Oct 24, 2023
9893712
lint
EndymionJkb Oct 24, 2023
5a511be
refactor: disable manageUserBalance, as inconsistent with query opera…
EndymionJkb Oct 25, 2023
f12a4d2
Merge branch 'master' into query-relayer-v2
EndymionJkb Oct 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
31 changes: 31 additions & 0 deletions pkg/standalone-utils/contracts/BatchRelayerQueryLibrary.sol
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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
}
}
23 changes: 21 additions & 2 deletions pkg/standalone-utils/contracts/relayer/BalancerRelayer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down
159 changes: 6 additions & 153 deletions pkg/standalone-utils/contracts/relayer/BaseRelayerLibrary.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -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) {
Expand All @@ -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);
}
}
Loading
Loading