diff --git a/pkg/balancer-js/src/utils/errors.ts b/pkg/balancer-js/src/utils/errors.ts index 3f4315cf5..8d658b008 100644 --- a/pkg/balancer-js/src/utils/errors.ts +++ b/pkg/balancer-js/src/utils/errors.ts @@ -159,10 +159,7 @@ const balancerErrorCodes: Record = { '525': 'NONZERO_TOKEN_BALANCE', '526': 'BALANCE_TOTAL_OVERFLOW', '527': 'POOL_NO_TOKENS', - '528': 'INSUFFICIENT_FLASH_LOAN_BALANCE', '600': 'SWAP_FEE_PERCENTAGE_TOO_HIGH', - '601': 'FLASH_LOAN_FEE_PERCENTAGE_TOO_HIGH', - '602': 'INSUFFICIENT_FLASH_LOAN_FEE_AMOUNT', '603': 'AUM_FEE_PERCENTAGE_TOO_HIGH', '700': 'SPLITTER_FEE_PERCENTAGE_TOO_HIGH', '998': 'UNIMPLEMENTED', diff --git a/pkg/interfaces/README.md b/pkg/interfaces/README.md index a1650f29c..0a896dae7 100644 --- a/pkg/interfaces/README.md +++ b/pkg/interfaces/README.md @@ -93,37 +93,6 @@ contract SimpleDepositor { } ``` -Sample contract that performs Flash Loans: - -```solidity -pragma solidity ^0.7.0; - -import "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol"; -import "@balancer-labs/v2-interfaces/contracts/vault/IFlashLoanRecipient.sol"; - -contract FlashLoanRecipient is IFlashLoanRecipient { - IVault private constant vault = "0xBA12222222228d8Ba445958a75a0704d566BF2C8"; - - function makeFlashLoan( - IERC20[] memory tokens, - uint256[] memory amounts, - bytes memory userData - ) external { - vault.flashLoan(this, tokens, amounts, userData); - } - - function receiveFlashLoan( - IERC20[] memory tokens, - uint256[] memory amounts, - uint256[] memory feeAmounts, - bytes memory userData - ) external override { - require(msg.sender == vault); - ... - } -} -``` - ### Notes In addition to interfaces, it also includes a small number of libraries that encapsulate enum types for particular pools (e.g., [StablePoolUserData](contracts/pool-stable/StablePoolUserData.sol), and functions for working with encoding and decoding `userData`. (See the `balancer-js` package for TypeScript versions of these utilities.) diff --git a/pkg/interfaces/contracts/pool-range/RangePoolUserData.sol b/pkg/interfaces/contracts/pool-range/RangePoolUserData.sol new file mode 100644 index 000000000..ab45e3b1c --- /dev/null +++ b/pkg/interfaces/contracts/pool-range/RangePoolUserData.sol @@ -0,0 +1,25 @@ +// 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 <0.9.0; + +import "../pool-weighted/WeightedPoolUserData.sol"; + +library RangePoolUserData { + // Joins + + function initialVirtualBalances(bytes memory self) internal pure returns (uint256[] memory virtualBalances) { + (, , virtualBalances) = abi.decode(self, (WeightedPoolUserData.JoinKind, uint256[], uint256[])); + } +} diff --git a/pkg/interfaces/contracts/solidity-utils/helpers/BalancerErrors.sol b/pkg/interfaces/contracts/solidity-utils/helpers/BalancerErrors.sol index fa2df50ce..db5e86db8 100644 --- a/pkg/interfaces/contracts/solidity-utils/helpers/BalancerErrors.sol +++ b/pkg/interfaces/contracts/solidity-utils/helpers/BalancerErrors.sol @@ -276,12 +276,9 @@ library Errors { uint256 internal constant NONZERO_TOKEN_BALANCE = 525; uint256 internal constant BALANCE_TOTAL_OVERFLOW = 526; uint256 internal constant POOL_NO_TOKENS = 527; - uint256 internal constant INSUFFICIENT_FLASH_LOAN_BALANCE = 528; // Fees uint256 internal constant SWAP_FEE_PERCENTAGE_TOO_HIGH = 600; - uint256 internal constant FLASH_LOAN_FEE_PERCENTAGE_TOO_HIGH = 601; - uint256 internal constant INSUFFICIENT_FLASH_LOAN_FEE_AMOUNT = 602; uint256 internal constant AUM_FEE_PERCENTAGE_TOO_HIGH = 603; // FeeSplitter diff --git a/pkg/interfaces/contracts/standalone-utils/IProtocolFeePercentagesProvider.sol b/pkg/interfaces/contracts/standalone-utils/IProtocolFeePercentagesProvider.sol index c8cd432d4..d8c603c44 100644 --- a/pkg/interfaces/contracts/standalone-utils/IProtocolFeePercentagesProvider.sol +++ b/pkg/interfaces/contracts/standalone-utils/IProtocolFeePercentagesProvider.sol @@ -27,7 +27,7 @@ interface IProtocolFeePercentagesProvider { event ProtocolFeeTypeRegistered(uint256 indexed feeType, string name, uint256 maximumPercentage); // Emitted when the value of a fee type changes. - // IMPORTANT: it is possible for a third party to modify the SWAP and FLASH_LOAN fee type values directly in the + // IMPORTANT: it is possible for a third party to modify the SWAP fee type values directly in the // ProtocolFeesCollector, which will result in this event not being emitted despite their value changing. Such usage // of the ProtocolFeesCollector is however discouraged: all state-changing interactions with it should originate in // this contract. @@ -61,11 +61,10 @@ interface IProtocolFeePercentagesProvider { /** * @dev Sets the percentage value for `feeType` to `newValue`. * - * IMPORTANT: it is possible for a third party to modify the SWAP and FLASH_LOAN fee type values directly in the + * IMPORTANT: it is possible for a third party to modify the SWAP fee type values directly in the * ProtocolFeesCollector, without invoking this function. This will result in the `ProtocolFeePercentageChanged` * event not being emitted despite their value changing. Such usage of the ProtocolFeesCollector is however - * discouraged: only this contract should be granted permission to call `setSwapFeePercentage` and - * `setFlashLoanFeePercentage`. + * discouraged: only this contract should be granted permission to call `setSwapFeePercentage`. */ function setFeeTypePercentage(uint256 feeType, uint256 newValue) external; @@ -93,7 +92,6 @@ library ProtocolFeeType { // solhint-disable private-vars-leading-underscore uint256 internal constant SWAP = 0; - uint256 internal constant FLASH_LOAN = 1; uint256 internal constant YIELD = 2; uint256 internal constant AUM = 3; // solhint-enable private-vars-leading-underscore diff --git a/pkg/interfaces/contracts/vault/IFlashLoanRecipient.sol b/pkg/interfaces/contracts/vault/IFlashLoanRecipient.sol deleted file mode 100644 index 10ad894c1..000000000 --- a/pkg/interfaces/contracts/vault/IFlashLoanRecipient.sol +++ /dev/null @@ -1,37 +0,0 @@ -// 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 <0.9.0; - -// Inspired by Aave Protocol's IFlashLoanReceiver. - -import "../solidity-utils/openzeppelin/IERC20.sol"; - -interface IFlashLoanRecipient { - /** - * @dev When `flashLoan` is called on the Vault, it invokes the `receiveFlashLoan` hook on the recipient. - * - * At the time of the call, the Vault will have transferred `amounts` for `tokens` to the recipient. Before this - * call returns, the recipient must have transferred `amounts` plus `feeAmounts` for each token back to the - * Vault, or else the entire flash loan will revert. - * - * `userData` is the same value passed in the `IVault.flashLoan` call. - */ - function receiveFlashLoan( - IERC20[] memory tokens, - uint256[] memory amounts, - uint256[] memory feeAmounts, - bytes memory userData - ) external; -} diff --git a/pkg/interfaces/contracts/vault/IProtocolFeesCollector.sol b/pkg/interfaces/contracts/vault/IProtocolFeesCollector.sol index 7fb4482f7..a5986bcdf 100644 --- a/pkg/interfaces/contracts/vault/IProtocolFeesCollector.sol +++ b/pkg/interfaces/contracts/vault/IProtocolFeesCollector.sol @@ -22,7 +22,6 @@ import "./IAuthorizer.sol"; interface IProtocolFeesCollector { event SwapFeePercentageChanged(uint256 newSwapFeePercentage); - event FlashLoanFeePercentageChanged(uint256 newFlashLoanFeePercentage); function withdrawCollectedFees( IERC20[] calldata tokens, @@ -32,12 +31,8 @@ interface IProtocolFeesCollector { function setSwapFeePercentage(uint256 newSwapFeePercentage) external; - function setFlashLoanFeePercentage(uint256 newFlashLoanFeePercentage) external; - function getSwapFeePercentage() external view returns (uint256); - function getFlashLoanFeePercentage() external view returns (uint256); - function getCollectedFeeAmounts(IERC20[] memory tokens) external view returns (uint256[] memory feeAmounts); function getAuthorizer() external view returns (IAuthorizer); diff --git a/pkg/interfaces/contracts/vault/IVault.sol b/pkg/interfaces/contracts/vault/IVault.sol index c55f9d25c..d217d6f96 100644 --- a/pkg/interfaces/contracts/vault/IVault.sol +++ b/pkg/interfaces/contracts/vault/IVault.sol @@ -22,7 +22,6 @@ import "../solidity-utils/misc/IWETH.sol"; import "./IAsset.sol"; import "./IAuthorizer.sol"; -import "./IFlashLoanRecipient.sol"; import "./IProtocolFeesCollector.sol"; pragma solidity >=0.7.0 <0.9.0; @@ -448,6 +447,13 @@ interface IVault is ISignaturesValidator, ITemporarilyPausable, IAuthentication enum PoolBalanceChangeKind { JOIN, EXIT } + /* Withdraws all pool token balances to caller. The caller should be authorized to call this function. + * The call doesn't burn pool tokens and doesn't change totalSupply. + * + * Emits a `PoolBalanceChanged` event. + */ + function exitPoolEmergency(bytes32 poolId) external; + // Swaps // // Users can swap tokens with Pools by calling the `swap` and `batchSwap` functions. To do this, @@ -649,32 +655,6 @@ interface IVault is ISignaturesValidator, ITemporarilyPausable, IAuthentication FundManagement memory funds ) external returns (int256[] memory assetDeltas); - // Flash Loans - - /** - * @dev Performs a 'flash loan', sending tokens to `recipient`, executing the `receiveFlashLoan` hook on it, - * and then reverting unless the tokens plus a proportional protocol fee have been returned. - * - * The `tokens` and `amounts` arrays must have the same length, and each entry in these indicates the loan amount - * for each token contract. `tokens` must be sorted in ascending order. - * - * The 'userData' field is ignored by the Vault, and forwarded as-is to `recipient` as part of the - * `receiveFlashLoan` call. - * - * Emits `FlashLoan` events. - */ - function flashLoan( - IFlashLoanRecipient recipient, - IERC20[] memory tokens, - uint256[] memory amounts, - bytes memory userData - ) external; - - /** - * @dev Emitted for each individual flash loan performed by `flashLoan`. - */ - event FlashLoan(IFlashLoanRecipient indexed recipient, IERC20 indexed token, uint256 amount, uint256 feeAmount); - // Asset Management // // Each token registered for a Pool can be assigned an Asset Manager, which is able to freely withdraw the Pool's @@ -732,9 +712,7 @@ interface IVault is ISignaturesValidator, ITemporarilyPausable, IAuthentication // Some operations cause the Vault to collect tokens in the form of protocol fees, which can then be withdrawn by // permissioned accounts. // - // There are two kinds of protocol fees: - // - // - flash loan fees: charged on all flash loans, as a percentage of the amounts lent. + // There is one kind of protocol fees: // // - swap fees: a percentage of the fees charged by Pools when performing swaps. For a number of reasons, including // swap gas costs and interface simplicity, protocol swap fees are not charged on each individual swap. Rather, diff --git a/pkg/pool-range/.solhintignore b/pkg/pool-range/.solhintignore new file mode 100644 index 000000000..11d11fbae --- /dev/null +++ b/pkg/pool-range/.solhintignore @@ -0,0 +1 @@ +contracts/test/ diff --git a/pkg/pool-range/README.md b/pkg/pool-range/README.md new file mode 100644 index 000000000..bfd64ac50 --- /dev/null +++ b/pkg/pool-range/README.md @@ -0,0 +1,30 @@ +# Range + +# Range Weighted Pools + + +This package contains the source code for Range Weighted Pools, that is, Pools that swap tokens by enforcing a Constant Weighted Product invariant. + +The pool currently in existence is [`RangePool`](./contracts/RangePool.sol) (basic ten token version). + +Another useful contract is [`RangeMath`](./contracts/RangeMath.sol), which implements the low level calculations required for swaps, joins, exits and price calculations. + +## Overview + +### Installation + +```console +$ git clone --recurse-submodules https://github.com/puzzlenetwork/ranges-sol + +``` + +### Usage + +This package can be used in multiple ways, including interacting with already deployed Pools, performing local testing, or even creating new Pool types that also use the Constant Weighted Product invariant. + +To get the address of deployed contracts in both mainnet and various test networks, see [`range-deployments` repository](https://github.com/puzzlenetwork/range-deployments.git). + + +## Licensing + +[GNU General Public License Version 3 (GPL v3)](../../LICENSE). diff --git a/pkg/pool-range/contracts/BaseRangePool.sol b/pkg/pool-range/contracts/BaseRangePool.sol new file mode 100644 index 000000000..77e3e31d6 --- /dev/null +++ b/pkg/pool-range/contracts/BaseRangePool.sol @@ -0,0 +1,461 @@ +// 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/pool-weighted/WeightedPoolUserData.sol"; +import "@balancer-labs/v2-interfaces/contracts/pool-range/RangePoolUserData.sol"; + +import "@balancer-labs/v2-solidity-utils/contracts/math/FixedPoint.sol"; +import "@balancer-labs/v2-solidity-utils/contracts/helpers/InputHelpers.sol"; + +import "@balancer-labs/v2-pool-utils/contracts/BaseGeneralPool.sol"; +import "@balancer-labs/v2-pool-utils/contracts/lib/BasePoolMath.sol"; +import "@balancer-labs/v2-pool-weighted/contracts/WeightedMath.sol"; + +import "./RangeInputHelpers.sol"; +import "./RangeMath.sol"; + +/** + * @dev Base class for RangePools containing swap, join and exit logic, but leaving storage and management of + * the weights to subclasses. Derived contracts can choose to make weights immutable, mutable, or even dynamic + * based on local or external logic. + */ +abstract contract BaseRangePool is BaseGeneralPool { + using FixedPoint for uint256; + using BasePoolUserData for bytes; + using WeightedPoolUserData for bytes; + using RangePoolUserData for bytes; + + constructor( + IVault vault, + string memory name, + string memory symbol, + IERC20[] memory tokens, + address[] memory assetManagers, + uint256 swapFeePercentage, + uint256 pauseWindowDuration, + uint256 bufferPeriodDuration, + address owner, + bool mutableTokens + ) + BasePool( + vault, + IVault.PoolSpecialization.GENERAL, + name, + symbol, + tokens, + assetManagers, + swapFeePercentage, + pauseWindowDuration, + bufferPeriodDuration, + owner + ) + { + // solhint-disable-previous-line no-empty-blocks + } + + // Virtual functions + + /** + * @dev Returns the index of `token`. + */ + function _getTokenIndex(IERC20 token) internal view virtual returns (uint256); + + /** + * @dev Returns the normalized weight of `token`. Weights are fixed point numbers that sum to FixedPoint.ONE. + */ + function _getNormalizedWeight(IERC20 token) internal view virtual returns (uint256); + + /** + * @dev Returns all normalized weights, in the same order as the Pool's tokens. + */ + function _getNormalizedWeights() internal view virtual returns (uint256[] memory); + + /** + * @dev Returns the virtual balance of `token`. + */ + function _getVirtualBalances() internal view virtual returns (uint256[] memory); + + /** + * @dev Returns the virtual balance of `token`. + */ + function _getVirtualBalance(IERC20 token) internal view virtual returns (uint256); + + /** + * @dev Sets the virtual balances of `initialization`. + */ + function _setVirtualBalances(uint256[] memory deltas) internal virtual; + + /** + * @dev Changes the virtual balance of `token`. + */ + function _changeVirtualBalance(IERC20 token, uint256 delta, bool increase) internal virtual; + + /** + * @dev Changes the virtual balances of `join` by ratioMin. + */ + function _changeVirtualBalancesBy(uint256 ratioMin, bool increase) internal virtual; + + /** + * @dev Returns the current value of the invariant. + * **IMPORTANT NOTE**: calling this function within a Vault context (i.e. in the middle of a join or an exit) is + * potentially unsafe, since the returned value is manipulable. It is up to the caller to ensure safety. + * + * Calculating the invariant requires the state of the pool to be in sync with the state of the Vault. + * That condition may not be true in the middle of a join or an exit. + * + * To call this function safely, attempt to trigger the reentrancy guard in the Vault by calling a non-reentrant + * function before calling `getInvariant`. That will make the transaction revert in an unsafe context. + * (See `VaultReentrancyLib.ensureNotInVaultContext` in pool-utils.) + * + * See https://forum.balancer.fi/t/reentrancy-vulnerability-scope-expanded/4345 for reference. + */ + function getInvariant() public view returns (uint256) { + uint256[] memory virtualBalances = _getVirtualBalances(); + + // Since the Pool hooks always work with upscaled balances, we manually + // upscale here for consistency + _upscaleArray(virtualBalances, _scalingFactors()); + + uint256[] memory normalizedWeights = _getNormalizedWeights(); + return WeightedMath._calculateInvariant(normalizedWeights, virtualBalances); + } + + function getNormalizedWeights() external view returns (uint256[] memory) { + return _getNormalizedWeights(); + } + + function getVirtualBalances() external view returns (uint256[] memory) { + return _getVirtualBalances(); + } + + // Base Pool handlers + + // Swap + + function _onSwapGivenIn( + SwapRequest memory swapRequest, + uint256[] memory factBalances, + uint256 /*indexIn*/, + uint256 /*indexOut*/ + ) internal virtual override returns (uint256) { + uint256 tokenOutIdx = _getTokenIndex(swapRequest.tokenOut); + _require(tokenOutIdx < factBalances.length, Errors.OUT_OF_BOUNDS); + uint256 amountOut = RangeMath._calcOutGivenIn( + _getVirtualBalance(swapRequest.tokenIn), + _getNormalizedWeight(swapRequest.tokenIn), + _getVirtualBalance(swapRequest.tokenOut), + _getNormalizedWeight(swapRequest.tokenOut), + swapRequest.amount, + factBalances[tokenOutIdx] + ); + + _changeVirtualBalance(swapRequest.tokenIn, swapRequest.amount, true); + _changeVirtualBalance(swapRequest.tokenOut, amountOut, false); + return amountOut; + } + + function _onSwapGivenOut( + SwapRequest memory swapRequest, + uint256[] memory factBalances, + uint256 /*indexIn*/, + uint256 /*indexOut*/ + ) internal virtual override returns (uint256) { + uint256 tokenOutIdx = _getTokenIndex(swapRequest.tokenOut); + _require(tokenOutIdx < factBalances.length, Errors.OUT_OF_BOUNDS); + _require(factBalances[tokenOutIdx] >= swapRequest.amount, Errors.INSUFFICIENT_BALANCE); + uint256 amountIn = + RangeMath._calcInGivenOut( + _getVirtualBalance(swapRequest.tokenIn), + _getNormalizedWeight(swapRequest.tokenIn), + _getVirtualBalance(swapRequest.tokenOut), + _getNormalizedWeight(swapRequest.tokenOut), + swapRequest.amount + ); + + _changeVirtualBalance(swapRequest.tokenIn, amountIn, true); + _changeVirtualBalance(swapRequest.tokenOut, swapRequest.amount, false); + return amountIn; + } + + /** + * @dev Called before any join or exit operation. Returns the Pool's total supply by default, but derived contracts + * may choose to add custom behavior at these steps. This often has to do with protocol fee processing. + */ + function _beforeJoinExit(uint256[] memory /*preBalances*/, uint256[] memory /*normalizedWeights*/) + internal + virtual + returns (uint256, uint256); + + /** + * @dev Called after any regular join or exit operation. Empty by default, but derived contracts + * may choose to add custom behavior at these steps. This often has to do with protocol fee processing. + * + * If performing a join operation, balanceDeltas are the amounts in: otherwise they are the amounts out. + * + * This function is free to mutate the `preBalances` array. + */ + function _afterJoinExit( + uint256 preJoinExitInvariant, + uint256[] memory preBalances, // virtual balances + uint256[] memory balanceDeltas, + uint256[] memory normalizedWeights, + uint256 preJoinExitSupply, + uint256 postJoinExitSupply + ) internal virtual; + + // Derived contracts may call this to update state after a join or exit. + function _updatePostJoinExit(uint256 postJoinExitInvariant) internal virtual; + + // Initialize + + function _onInitializePool( + bytes32, + address, + address, + uint256[] memory scalingFactors, + bytes memory userData + ) internal virtual override returns (uint256, uint256[] memory) { + WeightedPoolUserData.JoinKind kind = userData.joinKind(); + _require(kind == WeightedPoolUserData.JoinKind.INIT, Errors.UNINITIALIZED); + + uint256[] memory amountsIn = userData.initialAmountsIn(); + uint256[] memory virtualBalances = userData.initialVirtualBalances(); + InputHelpers.ensureInputLengthMatch(amountsIn.length, scalingFactors.length); + RangeInputHelpers.ensureFactBalanceIsLessOrEqual(amountsIn, virtualBalances); + _upscaleArray(amountsIn, scalingFactors); + + _setVirtualBalances(virtualBalances); + uint256 invariantAfterJoin = getInvariant(); + + // Set the initial BPT to the value of the invariant times the number of tokens. This makes BPT supply more + // consistent in Pools with similar compositions but different number of tokens. + uint256 bptAmountOut = Math.mul(invariantAfterJoin, amountsIn.length); + + // Initialization is still a join, so we need to do post-join work. Since we are not paying protocol fees, + // and all we need to do is update the invariant, call `_updatePostJoinExit` here instead of `_afterJoinExit`. + _updatePostJoinExit(invariantAfterJoin); + + return (bptAmountOut, amountsIn); + } + + // Join + + function _onJoinPool( + bytes32, + address sender, + address, + uint256[] memory factBalances, + uint256, + uint256, + uint256[] memory scalingFactors, + bytes memory userData + ) internal virtual override returns (uint256, uint256[] memory) { + uint256[] memory normalizedWeights = _getNormalizedWeights(); + + (uint256 preJoinExitSupply, uint256 preJoinExitInvariant) = _beforeJoinExit(factBalances, normalizedWeights); + + (uint256 bptAmountOut, uint256[] memory amountsIn) = _doJoin( + sender, + factBalances, + normalizedWeights, + scalingFactors, + preJoinExitSupply, + userData + ); + + _afterJoinExit( + preJoinExitInvariant, + _getVirtualBalances(), + amountsIn, + normalizedWeights, + preJoinExitSupply, + preJoinExitSupply.add(bptAmountOut) + ); + + uint256 minRatio = bptAmountOut.mulUp(FixedPoint.ONE).divDown(preJoinExitSupply); + _changeVirtualBalancesBy(minRatio, true); + + return (bptAmountOut, amountsIn); + } + + /** + * @dev Dispatch code which decodes the provided userdata to perform the specified join type. + * Inheriting contracts may override this function to add additional join types or extra conditions to allow + * or disallow joins under certain circumstances. + */ + function _doJoin( + address, + uint256[] memory factBalances, + uint256[] memory /*normalizedWeights*/, + uint256[] memory scalingFactors, + uint256 totalSupply, + bytes memory userData + ) internal view virtual returns (uint256, uint256[] memory) { + WeightedPoolUserData.JoinKind kind = userData.joinKind(); + + if (kind == WeightedPoolUserData.JoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT) { + return _joinExactTokensInForBPTOut(factBalances, scalingFactors, totalSupply, userData); + } else if (kind == WeightedPoolUserData.JoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT) { + return _joinAllTokensInForExactBPTOut(factBalances, totalSupply, userData); + } else { + _revert(Errors.UNHANDLED_JOIN_KIND); + } + } + + function _joinExactTokensInForBPTOut( + uint256[] memory factBalances, + uint256[] memory scalingFactors, + uint256 totalSupply, + bytes memory userData + ) private pure returns (uint256, uint256[] memory) { + (uint256[] memory amountsIn, uint256 minBPTAmountOut) = userData.exactTokensInForBptOut(); + InputHelpers.ensureInputLengthMatch(factBalances.length, amountsIn.length); + + _upscaleArray(amountsIn, scalingFactors); + + uint256 bptAmountOut = RangeMath._calcBptOutGivenExactTokensIn( + factBalances, + amountsIn, + totalSupply + ); + + _require(bptAmountOut >= minBPTAmountOut, Errors.BPT_OUT_MIN_AMOUNT); + + return (bptAmountOut, amountsIn); + } + + function _joinAllTokensInForExactBPTOut( + uint256[] memory factBalances, + uint256 totalSupply, + bytes memory userData + ) private pure returns (uint256, uint256[] memory) { + uint256 bptAmountOut = userData.allTokensInForExactBptOut(); + // Note that there is no maximum amountsIn parameter: this is handled by `IVault.joinPool`. + + uint256[] memory amountsIn = BasePoolMath.computeProportionalAmountsIn(factBalances, totalSupply, bptAmountOut); + + return (bptAmountOut, amountsIn); + } + + // Exit + + function _onExitPool( + bytes32, + address sender, + address, + uint256[] memory factBalances, + uint256, + uint256, + uint256[] memory scalingFactors, + bytes memory userData + ) internal virtual override returns (uint256, uint256[] memory) { + uint256[] memory normalizedWeights = _getNormalizedWeights(); + + (uint256 preJoinExitSupply, uint256 preJoinExitInvariant) = _beforeJoinExit(factBalances, normalizedWeights); + + (uint256 bptAmountIn, uint256[] memory amountsOut) = _doExit( + sender, + factBalances, + normalizedWeights, + scalingFactors, + preJoinExitSupply, + userData + ); + + _afterJoinExit( + preJoinExitInvariant, + _getVirtualBalances(), + amountsOut, + normalizedWeights, + preJoinExitSupply, + preJoinExitSupply.sub(bptAmountIn) + ); + + uint256 minRatio = bptAmountIn.mulUp(FixedPoint.ONE).divDown(preJoinExitSupply); + _changeVirtualBalancesBy(minRatio, false); + + return (bptAmountIn, amountsOut); + } + + /** + * @dev Dispatch code which decodes the provided userdata to perform the specified exit type. + * Inheriting contracts may override this function to add additional exit types or extra conditions to allow + * or disallow exit under certain circumstances. + */ + function _doExit( + address, + uint256[] memory factBalances, + uint256[] memory /*normalizedWeights*/, + uint256[] memory scalingFactors, + uint256 totalSupply, + bytes memory userData + ) internal view virtual returns (uint256, uint256[] memory) { + WeightedPoolUserData.ExitKind kind = userData.exitKind(); + + if (kind == WeightedPoolUserData.ExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT) { + return _exitExactBPTInForTokensOut(factBalances, totalSupply, userData); + } else if (kind == WeightedPoolUserData.ExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT) { + return _exitBPTInForExactTokensOut(factBalances, scalingFactors, totalSupply, userData); + } else { + _revert(Errors.UNHANDLED_EXIT_KIND); + } + } + + function _exitExactBPTInForTokensOut( + uint256[] memory factBalances, + uint256 totalSupply, + bytes memory userData + ) private pure returns (uint256, uint256[] memory) { + uint256 bptAmountIn = userData.exactBptInForTokensOut(); + // Note that there is no minimum amountOut parameter: this is handled by `IVault.exitPool`. + + uint256[] memory amountsOut = BasePoolMath.computeProportionalAmountsOut(factBalances, totalSupply, bptAmountIn); + return (bptAmountIn, amountsOut); + } + + function _exitBPTInForExactTokensOut( + uint256[] memory factBalances, + uint256[] memory scalingFactors, + uint256 totalSupply, + bytes memory userData + ) private pure returns (uint256, uint256[] memory) { + (uint256[] memory amountsOut, uint256 maxBPTAmountIn) = userData.bptInForExactTokensOut(); + InputHelpers.ensureInputLengthMatch(amountsOut.length, factBalances.length); + _upscaleArray(amountsOut, scalingFactors); + + // This is an exceptional situation in which the fee is charged on a token out instead of a token in. + uint256 bptAmountIn = RangeMath._calcBptInGivenExactTokensOut( + factBalances, + amountsOut, + totalSupply + ); + _require(bptAmountIn <= maxBPTAmountIn, Errors.BPT_IN_MAX_AMOUNT); + + return (bptAmountIn, amountsOut); + } + + // Recovery Mode + + function _doRecoveryModeExit( + uint256[] memory factBalances, + uint256 totalSupply, + bytes memory userData + ) internal pure override returns (uint256 bptAmountIn, uint256[] memory amountsOut) { + bptAmountIn = userData.recoveryModeExit(); + amountsOut = BasePoolMath.computeProportionalAmountsOut(factBalances, totalSupply, bptAmountIn); + } +} diff --git a/pkg/pool-range/contracts/ExternalRangeMath.sol b/pkg/pool-range/contracts/ExternalRangeMath.sol new file mode 100644 index 000000000..08160e3d3 --- /dev/null +++ b/pkg/pool-range/contracts/ExternalRangeMath.sol @@ -0,0 +1,77 @@ +// 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; + +import "./RangeMath.sol"; + +/** + * @notice A contract-wrapper for Range Math. + * @dev Use this contract as an external replacement for RangeMath. + */ +contract ExternalRangeMath { + function calcOutGivenIn( + uint256 virtualBalanceIn, + uint256 weightIn, + uint256 virtualBalanceOut, + uint256 weightOut, + uint256 amountIn, + uint256 factBalance + ) external pure returns (uint256) { + return RangeMath._calcOutGivenIn(virtualBalanceIn, weightIn, virtualBalanceOut, weightOut, amountIn, factBalance); + } + + function calcInGivenOut( + uint256 virtualBalanceIn, + uint256 weightIn, + uint256 virtualBalanceOut, + uint256 weightOut, + uint256 amountOut + ) external pure returns (uint256) { + return RangeMath._calcInGivenOut(virtualBalanceIn, weightIn, virtualBalanceOut, weightOut, amountOut); + } + + function calcBptOutGivenExactTokensIn( + uint256[] memory factBalances, + uint256[] memory amountsIn, + uint256 bptTotalSupply + ) external pure returns (uint256) { + return + RangeMath._calcBptOutGivenExactTokensIn( + factBalances, + amountsIn, + bptTotalSupply + ); + } + + function calcBptInGivenExactTokensOut( + uint256[] memory factBalances, + uint256[] memory amountsOut, + uint256 bptTotalSupply + ) external pure returns (uint256) { + return + RangeMath._calcBptInGivenExactTokensOut( + factBalances, + amountsOut, + bptTotalSupply + ); + } + + function calcRatioMin( + uint256[] memory factBalances, + uint256[] memory amounts + ) external pure returns (uint256) { + return RangeMath._calcRatioMin(factBalances, amounts); + } +} diff --git a/pkg/pool-range/contracts/RangeInputHelpers.sol b/pkg/pool-range/contracts/RangeInputHelpers.sol new file mode 100644 index 000000000..d829b9f25 --- /dev/null +++ b/pkg/pool-range/contracts/RangeInputHelpers.sol @@ -0,0 +1,26 @@ +// 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; + +import "@balancer-labs/v2-solidity-utils/contracts/helpers/InputHelpers.sol"; + +library RangeInputHelpers { + function ensureFactBalanceIsLessOrEqual(uint256[] memory a, uint256[] memory b) internal pure { + InputHelpers.ensureInputLengthMatch(a.length, b.length); + for (uint256 i = 0; i < a.length; ++i) { + _require(a[i] <= b[i], Errors.INSUFFICIENT_BALANCE); + } + } +} diff --git a/pkg/pool-range/contracts/RangeMath.sol b/pkg/pool-range/contracts/RangeMath.sol new file mode 100644 index 000000000..74624c60a --- /dev/null +++ b/pkg/pool-range/contracts/RangeMath.sol @@ -0,0 +1,134 @@ +// 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; + +import "@balancer-labs/v2-solidity-utils/contracts/math/FixedPoint.sol"; +import "@balancer-labs/v2-solidity-utils/contracts/math/Math.sol"; + +// These functions start with an underscore, as if they were part of a contract and not a library. At some point this +// should be fixed. +// solhint-disable private-vars-leading-underscore + +library RangeMath { + using FixedPoint for uint256; + + // Computes how many tokens can be taken out of a pool if `amountIn` are sent, given the + // virtual balances and weights. + function _calcOutGivenIn( + uint256 virtualBalanceIn, + uint256 weightIn, + uint256 virtualBalanceOut, + uint256 weightOut, + uint256 amountIn, + uint256 factBalance + ) internal pure returns (uint256) { + /********************************************************************************************** + // outGivenIn // + // aO = amountOut // + // bO = virtualBalanceOut // + // bI = virtualBalanceIn / / bI \ (wI / wO) \ // + // aI = amountIn aO = bO * | 1 - | -------------------------- | ^ | // + // wI = weightIn \ \ ( bI + aI ) / / // + // wO = weightOut // + // if a0 exceeds factBalance, then a0 = factBalance // // + **********************************************************************************************/ + + // Amount out, so we round down overall. + + // The multiplication rounds down, and the subtrahend (power) rounds up (so the base rounds up too). + // Because bI / (bI + aI) <= 1, the exponent rounds down. + + uint256 denominator = virtualBalanceIn.add(amountIn); + uint256 base = virtualBalanceIn.divUp(denominator); + uint256 exponent = weightIn.divDown(weightOut); + uint256 power = base.powUp(exponent); + + return Math.min(factBalance, virtualBalanceOut.mulDown(power.complement())); + } + + // Computes how many tokens must be sent to a pool in order to take `amountOut`, given the + // virtual balances and weights. + function _calcInGivenOut( + uint256 virtualBalanceIn, + uint256 weightIn, + uint256 virtualBalanceOut, + uint256 weightOut, + uint256 amountOut + ) internal pure returns (uint256) { + /********************************************************************************************** + // inGivenOut // + // aO = amountOut // + // bO = virtualBalanceOut // + // bI = virtualBalanceIn / / bO \ (wO / wI) \ // + // aI = amountIn aI = bI * | | -------------------------- | ^ - 1 | // + // wI = weightIn \ \ ( bO - aO ) / / // + // wO = weightOut // + **********************************************************************************************/ + + // Amount in, so we round up overall. + + // The multiplication rounds up, and the power rounds up (so the base rounds up too). + // Because b0 / (b0 - a0) >= 1, the exponent rounds up. + + uint256 base = virtualBalanceOut.divUp(virtualBalanceOut.sub(amountOut)); + uint256 exponent = weightOut.divUp(weightIn); + uint256 power = base.powUp(exponent); + + // Because the base is larger than one (and the power rounds up), the power should always be larger than one, so + // the following subtraction should never revert. + uint256 ratio = power.sub(FixedPoint.ONE); + + return virtualBalanceIn.mulUp(ratio); + } + + function _calcBptOutGivenExactTokensIn( + uint256[] memory factBalances, + uint256[] memory amountsIn, + uint256 bptTotalSupply + ) internal pure returns (uint256) { + uint256 ratioMin = _calcRatioMin(factBalances, amountsIn); + return bptTotalSupply.mulUp(ratioMin).divDown(FixedPoint.ONE); + } + + function _calcBptInGivenExactTokensOut( + uint256[] memory factBalances, + uint256[] memory amountsOut, + uint256 bptTotalSupply + ) internal pure returns (uint256) { + uint256 ratioMin = _calcRatioMin(factBalances, amountsOut); + return bptTotalSupply.mulUp(ratioMin).divDown(FixedPoint.ONE); + } + + function _calcRatioMin( + uint256[] memory factBalances, + uint256[] memory amounts + ) internal pure returns (uint256) { + uint256 ratioMin = 0; + uint256 i = 0; + while (i < factBalances.length) { + if (factBalances[i] > 0) { + uint256 currentRatio = amounts[i].mulUp(FixedPoint.ONE).divDown(factBalances[i]); + if (ratioMin > 0) { + ratioMin = Math.min(ratioMin, currentRatio); + if (ratioMin == 0) break; + } else { + ratioMin = currentRatio; + } + } + i++; + } + return ratioMin; + } +} diff --git a/pkg/pool-range/contracts/RangePool.sol b/pkg/pool-range/contracts/RangePool.sol new file mode 100644 index 000000000..551c4730b --- /dev/null +++ b/pkg/pool-range/contracts/RangePool.sol @@ -0,0 +1,453 @@ +// 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 "./BaseRangePool.sol"; +import "./RangePoolProtocolFees.sol"; + +/** + * @dev Range Pool with immutable weights. + */ +contract RangePool is BaseRangePool, RangePoolProtocolFees { + using FixedPoint for uint256; + + uint256 private constant _MAX_TOKENS = 10; + + uint256 private immutable _totalTokens; + + IERC20 internal immutable _token0; + IERC20 internal immutable _token1; + IERC20 internal immutable _token2; + IERC20 internal immutable _token3; + IERC20 internal immutable _token4; + IERC20 internal immutable _token5; + IERC20 internal immutable _token6; + IERC20 internal immutable _token7; + IERC20 internal immutable _token8; + IERC20 internal immutable _token9; + + // All token balances are normalized to behave as if the token had 18 decimals. We assume a token's decimals will + // not change throughout its lifetime, and store the corresponding scaling factor for each at construction time. + // These factors are always greater than or equal to one: tokens with more than 18 decimals are not supported. + + uint256 internal immutable _scalingFactor0; + uint256 internal immutable _scalingFactor1; + uint256 internal immutable _scalingFactor2; + uint256 internal immutable _scalingFactor3; + uint256 internal immutable _scalingFactor4; + uint256 internal immutable _scalingFactor5; + uint256 internal immutable _scalingFactor6; + uint256 internal immutable _scalingFactor7; + uint256 internal immutable _scalingFactor8; + uint256 internal immutable _scalingFactor9; + + uint256 internal immutable _normalizedWeight0; + uint256 internal immutable _normalizedWeight1; + uint256 internal immutable _normalizedWeight2; + uint256 internal immutable _normalizedWeight3; + uint256 internal immutable _normalizedWeight4; + uint256 internal immutable _normalizedWeight5; + uint256 internal immutable _normalizedWeight6; + uint256 internal immutable _normalizedWeight7; + uint256 internal immutable _normalizedWeight8; + uint256 internal immutable _normalizedWeight9; + + uint256[] internal _virtualBalances; + + struct NewPoolParams { + string name; + string symbol; + IERC20[] tokens; + uint256[] normalizedWeights; + IRateProvider[] rateProviders; + address[] assetManagers; + uint256 swapFeePercentage; + } + + constructor( + NewPoolParams memory params, + IVault vault, + IProtocolFeePercentagesProvider protocolFeeProvider, + uint256 pauseWindowDuration, + uint256 bufferPeriodDuration, + address owner + ) + BaseRangePool( + vault, + params.name, + params.symbol, + params.tokens, + params.assetManagers, + params.swapFeePercentage, + pauseWindowDuration, + bufferPeriodDuration, + owner, + false + ) + ProtocolFeeCache( + protocolFeeProvider, + ProviderFeeIDs({ swap: ProtocolFeeType.SWAP, yield: ProtocolFeeType.YIELD, aum: ProtocolFeeType.AUM }) + ) + RangePoolProtocolFees(params.tokens.length, params.rateProviders) + { + uint256 numTokens = params.tokens.length; + InputHelpers.ensureInputLengthMatch(numTokens, params.normalizedWeights.length); + + _totalTokens = numTokens; + + // Ensure each normalized weight is above the minimum + uint256 normalizedSum = 0; + for (uint8 i = 0; i < numTokens; i++) { + uint256 normalizedWeight = params.normalizedWeights[i]; + + _require(normalizedWeight >= WeightedMath._MIN_WEIGHT, Errors.MIN_WEIGHT); + normalizedSum = normalizedSum.add(normalizedWeight); + + _virtualBalances.push(0); + } + // Ensure that the normalized weights sum to ONE + _require(normalizedSum == FixedPoint.ONE, Errors.NORMALIZED_WEIGHT_INVARIANT); + + // Immutable variables cannot be initialized inside an if statement, so we must do conditional assignments + _token0 = params.tokens[0]; + _token1 = params.tokens[1]; + _token2 = numTokens > 2 ? params.tokens[2] : IERC20(0); + _token3 = numTokens > 3 ? params.tokens[3] : IERC20(0); + _token4 = numTokens > 4 ? params.tokens[4] : IERC20(0); + _token5 = numTokens > 5 ? params.tokens[5] : IERC20(0); + _token6 = numTokens > 6 ? params.tokens[6] : IERC20(0); + _token7 = numTokens > 7 ? params.tokens[7] : IERC20(0); + _token8 = numTokens > 8 ? params.tokens[8] : IERC20(0); + _token9 = numTokens > 9 ? params.tokens[9] : IERC20(0); + + _scalingFactor0 = _computeScalingFactor(params.tokens[0]); + _scalingFactor1 = _computeScalingFactor(params.tokens[1]); + _scalingFactor2 = numTokens > 2 ? _computeScalingFactor(params.tokens[2]) : 0; + _scalingFactor3 = numTokens > 3 ? _computeScalingFactor(params.tokens[3]) : 0; + _scalingFactor4 = numTokens > 4 ? _computeScalingFactor(params.tokens[4]) : 0; + _scalingFactor5 = numTokens > 5 ? _computeScalingFactor(params.tokens[5]) : 0; + _scalingFactor6 = numTokens > 6 ? _computeScalingFactor(params.tokens[6]) : 0; + _scalingFactor7 = numTokens > 7 ? _computeScalingFactor(params.tokens[7]) : 0; + _scalingFactor8 = numTokens > 8 ? _computeScalingFactor(params.tokens[8]) : 0; + _scalingFactor9 = numTokens > 9 ? _computeScalingFactor(params.tokens[9]) : 0; + + _normalizedWeight0 = params.normalizedWeights[0]; + _normalizedWeight1 = params.normalizedWeights[1]; + _normalizedWeight2 = numTokens > 2 ? params.normalizedWeights[2] : 0; + _normalizedWeight3 = numTokens > 3 ? params.normalizedWeights[3] : 0; + _normalizedWeight4 = numTokens > 4 ? params.normalizedWeights[4] : 0; + _normalizedWeight5 = numTokens > 5 ? params.normalizedWeights[5] : 0; + _normalizedWeight6 = numTokens > 6 ? params.normalizedWeights[6] : 0; + _normalizedWeight7 = numTokens > 7 ? params.normalizedWeights[7] : 0; + _normalizedWeight8 = numTokens > 8 ? params.normalizedWeights[8] : 0; + _normalizedWeight9 = numTokens > 9 ? params.normalizedWeights[9] : 0; + } + + function _getTokenIndex(IERC20 token) internal view virtual override returns (uint256) { + // prettier-ignore + if (token == _token0) { return 0; } + else if (token == _token1) { return 1; } + else if (token == _token2) { return 2; } + else if (token == _token3) { return 3; } + else if (token == _token4) { return 4; } + else if (token == _token5) { return 5; } + else if (token == _token6) { return 6; } + else if (token == _token7) { return 7; } + else if (token == _token8) { return 8; } + else if (token == _token9) { return 9; } + else { + _revert(Errors.INVALID_TOKEN); + } + } + + function _getNormalizedWeight(IERC20 token) internal view virtual override returns (uint256) { + // prettier-ignore + if (token == _token0) { return _normalizedWeight0; } + else if (token == _token1) { return _normalizedWeight1; } + else if (token == _token2) { return _normalizedWeight2; } + else if (token == _token3) { return _normalizedWeight3; } + else if (token == _token4) { return _normalizedWeight4; } + else if (token == _token5) { return _normalizedWeight5; } + else if (token == _token6) { return _normalizedWeight6; } + else if (token == _token7) { return _normalizedWeight7; } + else if (token == _token8) { return _normalizedWeight8; } + else if (token == _token9) { return _normalizedWeight9; } + else { + _revert(Errors.INVALID_TOKEN); + } + } + + function _getNormalizedWeights() internal view virtual override returns (uint256[] memory) { + uint256 totalTokens = _getTotalTokens(); + uint256[] memory normalizedWeights = new uint256[](totalTokens); + + // prettier-ignore + { + normalizedWeights[0] = _normalizedWeight0; + normalizedWeights[1] = _normalizedWeight1; + if (totalTokens > 2) { normalizedWeights[2] = _normalizedWeight2; } else { return normalizedWeights; } + if (totalTokens > 3) { normalizedWeights[3] = _normalizedWeight3; } else { return normalizedWeights; } + if (totalTokens > 4) { normalizedWeights[4] = _normalizedWeight4; } else { return normalizedWeights; } + if (totalTokens > 5) { normalizedWeights[5] = _normalizedWeight5; } else { return normalizedWeights; } + if (totalTokens > 6) { normalizedWeights[6] = _normalizedWeight6; } else { return normalizedWeights; } + if (totalTokens > 7) { normalizedWeights[7] = _normalizedWeight7; } else { return normalizedWeights; } + if (totalTokens > 8) { normalizedWeights[8] = _normalizedWeight8; } else { return normalizedWeights; } + if (totalTokens > 9) { normalizedWeights[9] = _normalizedWeight9; } else { return normalizedWeights; } + } + + return normalizedWeights; + } + + function _getMaxTokens() internal pure virtual override returns (uint256) { + return _MAX_TOKENS; + } + + function _getTotalTokens() internal view virtual override returns (uint256) { + return _totalTokens; + } + + function _getVirtualBalances() internal view virtual override returns (uint256[] memory) { + return _virtualBalances; + } + + function _getVirtualBalance(IERC20 token) internal view virtual override returns (uint256) { + return _virtualBalances[_getTokenIndex(token)]; + } + + function _changeVirtualBalance(IERC20 token, uint256 delta, bool increase) internal virtual override { + uint256 tokenIdx = _getTokenIndex(token); + uint256 downScaledDelta = _downscaleDown(delta, _scalingFactor(token)); + _virtualBalances[tokenIdx] = increase ? _virtualBalances[tokenIdx].add(downScaledDelta) : + _virtualBalances[tokenIdx].sub(downScaledDelta); + } + + function _setVirtualBalances(uint256[] memory balances) internal virtual override { + for (uint256 i = 0; i < _totalTokens; i++) { + _virtualBalances[i] = balances[i]; + } + } + + function _changeVirtualBalancesBy(uint256 ratioMin, bool increase) internal virtual override { + for (uint256 i = 0; i < _totalTokens; i++) { + uint256 value = _virtualBalances[i].mulUp(ratioMin).divDown(FixedPoint.ONE); + _virtualBalances[i] = increase ? _virtualBalances[i].add(value) : _virtualBalances[i].sub(value); + } + } + + /** + * @dev Returns the scaling factor for one of the Pool's tokens. Reverts if `token` is not a token registered by the + * Pool. + */ + function _scalingFactor(IERC20 token) internal view virtual override returns (uint256) { + // prettier-ignore + if (token == _token0) { return _scalingFactor0; } + else if (token == _token1) { return _scalingFactor1; } + else if (token == _token2) { return _scalingFactor2; } + else if (token == _token3) { return _scalingFactor3; } + else if (token == _token4) { return _scalingFactor4; } + else if (token == _token5) { return _scalingFactor5; } + else if (token == _token6) { return _scalingFactor6; } + else if (token == _token7) { return _scalingFactor7; } + else if (token == _token8) { return _scalingFactor8; } + else if (token == _token9) { return _scalingFactor9; } + else { + _revert(Errors.INVALID_TOKEN); + } + } + + function _scalingFactors() internal view virtual override returns (uint256[] memory) { + uint256 totalTokens = _getTotalTokens(); + uint256[] memory scalingFactors = new uint256[](totalTokens); + + // prettier-ignore + { + scalingFactors[0] = _scalingFactor0; + scalingFactors[1] = _scalingFactor1; + if (totalTokens > 2) { scalingFactors[2] = _scalingFactor2; } else { return scalingFactors; } + if (totalTokens > 3) { scalingFactors[3] = _scalingFactor3; } else { return scalingFactors; } + if (totalTokens > 4) { scalingFactors[4] = _scalingFactor4; } else { return scalingFactors; } + if (totalTokens > 5) { scalingFactors[5] = _scalingFactor5; } else { return scalingFactors; } + if (totalTokens > 6) { scalingFactors[6] = _scalingFactor6; } else { return scalingFactors; } + if (totalTokens > 7) { scalingFactors[7] = _scalingFactor7; } else { return scalingFactors; } + if (totalTokens > 8) { scalingFactors[8] = _scalingFactor8; } else { return scalingFactors; } + if (totalTokens > 9) { scalingFactors[9] = _scalingFactor9; } else { return scalingFactors; } + } + + return scalingFactors; + } + + // Initialize + + function _onInitializePool( + bytes32 poolId, + address sender, + address recipient, + uint256[] memory scalingFactors, + bytes memory userData + ) internal virtual override returns (uint256, uint256[] memory) { + // Initialize `_athRateProduct` if the Pool will pay protocol fees on yield. + // Not initializing this here properly will cause all joins/exits to revert. + if (!_isExemptFromYieldProtocolFees()) _updateATHRateProduct(_getRateProduct(_getNormalizedWeights())); + + return super._onInitializePool(poolId, sender, recipient, scalingFactors, userData); + } + + // WeightedPoolProtocolFees functions + + function _beforeJoinExit(uint256[] memory /*preBalances*/, uint256[] memory normalizedWeights) + internal + virtual + override + returns (uint256, uint256) + { + uint256 supplyBeforeFeeCollection = totalSupply(); + uint256 invariant = getInvariant(); + (uint256 protocolFeesToBeMinted, uint256 athRateProduct) = _getPreJoinExitProtocolFees( + invariant, + normalizedWeights, + supplyBeforeFeeCollection + ); + + // We then update the recorded value of `athRateProduct` to ensure we only collect fees on yield once. + // A zero value for `athRateProduct` represents that it is unchanged so we can skip updating it. + if (athRateProduct > 0) { + _updateATHRateProduct(athRateProduct); + } + + _payProtocolFees(protocolFeesToBeMinted); + + return (supplyBeforeFeeCollection.add(protocolFeesToBeMinted), invariant); + } + + function _afterJoinExit( + uint256 preJoinExitInvariant, + uint256[] memory preBalances, + uint256[] memory balanceDeltas, + uint256[] memory normalizedWeights, + uint256 preJoinExitSupply, + uint256 postJoinExitSupply + ) internal virtual override { + uint256 protocolFeesToBeMinted = _getPostJoinExitProtocolFees( + preJoinExitInvariant, + preBalances, + balanceDeltas, + normalizedWeights, + preJoinExitSupply, + postJoinExitSupply + ); + + _payProtocolFees(protocolFeesToBeMinted); + } + + function _updatePostJoinExit(uint256 postJoinExitInvariant) + internal + virtual + override(BaseRangePool, RangePoolProtocolFees) + { + RangePoolProtocolFees._updatePostJoinExit(postJoinExitInvariant); + } + + function _beforeProtocolFeeCacheUpdate() internal override { + // The `getRate()` function depends on the actual supply, which in turn depends on the cached protocol fee + // percentages. Changing these would therefore result in the rate changing, which is not acceptable as this is a + // sensitive value. + // Because of this, we pay any due protocol fees *before* updating the cache, making it so that the new + // percentages only affect future operation of the Pool, and not past fees. As a result, `getRate()` is + // unaffected by the cached protocol fee percentages changing. + + // Given that this operation is state-changing and relatively complex, we only allow it as long as the Pool is + // not paused. + _ensureNotPaused(); + + uint256 invariant = getInvariant(); + + (uint256 protocolFeesToBeMinted, uint256 athRateProduct) = _getPreJoinExitProtocolFees( + invariant, + _getNormalizedWeights(), + totalSupply() + ); + + _payProtocolFees(protocolFeesToBeMinted); + + // With the fees paid, we now store the current invariant and update the ATH rate product (if necessary), + // marking the Pool as free of protocol debt. + + _updatePostJoinExit(invariant); + if (athRateProduct > 0) { + _updateATHRateProduct(athRateProduct); + } + } + + /** + * @notice Returns the effective BPT supply. + * + * @dev This would be the same as `totalSupply` however the Pool owes debt to the Protocol in the form of unminted + * BPT, which will be minted immediately before the next join or exit. We need to take these into account since, + * even if they don't yet exist, they will effectively be included in any Pool operation that involves BPT. + * + * In the vast majority of cases, this function should be used instead of `totalSupply()`. + * + * **IMPORTANT NOTE**: calling this function within a Vault context (i.e. in the middle of a join or an exit) is + * potentially unsafe, since the returned value is manipulable. It is up to the caller to ensure safety. + * + * This is because this function calculates the invariant, which requires the state of the pool to be in sync + * with the state of the Vault. That condition may not be true in the middle of a join or an exit. + * + * To call this function safely, attempt to trigger the reentrancy guard in the Vault by calling a non-reentrant + * function before calling `getActualSupply`. That will make the transaction revert in an unsafe context. + * (See `VaultReentrancyLib.ensureNotInVaultContext` in pool-utils.) + * + * See https://forum.balancer.fi/t/reentrancy-vulnerability-scope-expanded/4345 for reference. + */ + function getActualSupply() external view returns (uint256) { + uint256 supply = totalSupply(); + + (uint256 protocolFeesToBeMinted, ) = _getPreJoinExitProtocolFees( + getInvariant(), + _getNormalizedWeights(), + supply + ); + + return supply.add(protocolFeesToBeMinted); + } + + function _onDisableRecoveryMode() internal override { + // Update the postJoinExitInvariant to the value of the currentInvariant, zeroing out any protocol swap fees. + _updatePostJoinExit(getInvariant()); + + // If the Pool has any protocol yield fees accrued then we update the athRateProduct to zero these out. + // If the current rate product is less than the athRateProduct then we do not perform this update. + // This prevents the Pool from paying protocol fees on the same yield twice if the rate product were to drop. + if (!_isExemptFromYieldProtocolFees()) { + uint256 athRateProduct = getATHRateProduct(); + uint256 rateProduct = _getRateProduct(_getNormalizedWeights()); + + if (rateProduct > athRateProduct) { + _updateATHRateProduct(rateProduct); + } + } + } + + function _isOwnerOnlyAction(bytes32 actionId) + internal + view + virtual + override(BasePool, RangePoolProtocolFees) + returns (bool) + { + return super._isOwnerOnlyAction(actionId); + } +} diff --git a/pkg/pool-range/contracts/RangePoolFactory.sol b/pkg/pool-range/contracts/RangePoolFactory.sol new file mode 100644 index 000000000..7bdd4e58a --- /dev/null +++ b/pkg/pool-range/contracts/RangePoolFactory.sol @@ -0,0 +1,78 @@ +// 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/IVault.sol"; + +import "@balancer-labs/v2-pool-utils/contracts/factories/BasePoolFactory.sol"; + +import "./RangePool.sol"; + +contract RangePoolFactory is BasePoolFactory { + constructor( + IVault vault, + IProtocolFeePercentagesProvider protocolFeeProvider, + uint256 initialPauseWindowDuration, + uint256 bufferPeriodDuration + ) + BasePoolFactory( + vault, + protocolFeeProvider, + initialPauseWindowDuration, + bufferPeriodDuration, + type(RangePool).creationCode + ) + { + // solhint-disable-previous-line no-empty-blocks + } + + /** + * @dev Deploys a new `RangePool`. + */ + function create( + string memory name, + string memory symbol, + IERC20[] memory tokens, + uint256[] memory normalizedWeights, + IRateProvider[] memory rateProviders, + uint256 swapFeePercentage, + address owner, + bytes32 salt + ) external returns (address) { + (uint256 pauseWindowDuration, uint256 bufferPeriodDuration) = getPauseConfiguration(); + + return + _create( + abi.encode( + RangePool.NewPoolParams({ + name: name, + symbol: symbol, + tokens: tokens, + normalizedWeights: normalizedWeights, + rateProviders: rateProviders, + assetManagers: new address[](tokens.length), // Don't allow asset managers, + swapFeePercentage: swapFeePercentage + }), + getVault(), + getProtocolFeePercentagesProvider(), + pauseWindowDuration, + bufferPeriodDuration, + owner + ), + salt + ); + } +} diff --git a/pkg/pool-range/contracts/RangePoolProtocolFees.sol b/pkg/pool-range/contracts/RangePoolProtocolFees.sol new file mode 100644 index 000000000..f868729d3 --- /dev/null +++ b/pkg/pool-range/contracts/RangePoolProtocolFees.sol @@ -0,0 +1,379 @@ +// 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/pool-utils/IRateProviderPool.sol"; +import "@balancer-labs/v2-pool-utils/contracts/external-fees/ProtocolFeeCache.sol"; +import "@balancer-labs/v2-pool-utils/contracts/external-fees/InvariantGrowthProtocolSwapFees.sol"; + +import "./BaseRangePool.sol"; + +abstract contract RangePoolProtocolFees is BaseRangePool, ProtocolFeeCache, IRateProviderPool { + using FixedPoint for uint256; + using WordCodec for bytes32; + + // Rate providers are used only for computing yield fees; they do not inform swap/join/exit. + IRateProvider internal immutable _rateProvider0; + IRateProvider internal immutable _rateProvider1; + IRateProvider internal immutable _rateProvider2; + IRateProvider internal immutable _rateProvider3; + IRateProvider internal immutable _rateProvider4; + IRateProvider internal immutable _rateProvider5; + IRateProvider internal immutable _rateProvider6; + IRateProvider internal immutable _rateProvider7; + IRateProvider internal immutable _rateProvider8; + IRateProvider internal immutable _rateProvider9; + + bool internal immutable _exemptFromYieldFees; + + // All-time high value of the weighted product of the pool's token rates. Comparing such weighted products across + // time provides a measure of the pool's growth resulting from rate changes. The pool also grows due to swap fees, + // but that growth is captured in the invariant; rate growth is not. + uint256 private _athRateProduct; + + // This Pool pays protocol fees by measuring the growth of the invariant between joins and exits. Since weights are + // immutable, the invariant only changes due to accumulated swap fees, which saves gas by freeing the Pool + // from performing any computation or accounting associated with protocol fees during swaps. + // This mechanism requires keeping track of the invariant after the last join or exit. + // + // The maximum value of the invariant is the maximum allowable balance in the Vault (2**112) multiplied by the + // largest possible scaling factor (10**18 for a zero decimals token). The largest invariant is then + // 2**112 * 10**18 ~= 2**172, which means that to save gas we can place this in BasePool's `_miscData`. + uint256 private constant _LAST_POST_JOINEXIT_INVARIANT_OFFSET = 0; + uint256 private constant _LAST_POST_JOINEXIT_INVARIANT_BIT_LENGTH = 192; + + event ATHRateProductUpdated(uint256 oldATHRateProduct, uint256 newATHRateProduct); + + constructor(uint256 numTokens, IRateProvider[] memory rateProviders) { + _require(numTokens <= 10, Errors.MAX_TOKENS); + InputHelpers.ensureInputLengthMatch(numTokens, rateProviders.length); + + _exemptFromYieldFees = _getYieldFeeExemption(rateProviders); + + _rateProvider0 = rateProviders[0]; + _rateProvider1 = rateProviders[1]; + _rateProvider2 = numTokens > 2 ? rateProviders[2] : IRateProvider(0); + _rateProvider3 = numTokens > 3 ? rateProviders[3] : IRateProvider(0); + _rateProvider4 = numTokens > 4 ? rateProviders[4] : IRateProvider(0); + _rateProvider5 = numTokens > 5 ? rateProviders[5] : IRateProvider(0); + _rateProvider6 = numTokens > 6 ? rateProviders[6] : IRateProvider(0); + _rateProvider7 = numTokens > 7 ? rateProviders[7] : IRateProvider(0); + _rateProvider8 = numTokens > 8 ? rateProviders[8] : IRateProvider(0); + _rateProvider9 = numTokens > 9 ? rateProviders[9] : IRateProvider(0); + } + + function _getYieldFeeExemption(IRateProvider[] memory rateProviders) internal pure returns (bool) { + // If we know that no rate providers are set then we can skip yield fees logic. + // If any tokens have rate providers, then set `_exemptFromYieldFees` to false, otherwise leave it true. + for (uint256 i = 0; i < rateProviders.length; i++) { + if (rateProviders[i] != IRateProvider(0)) { + return false; + } + } + return true; + } + + /** + * @dev Returns whether the pool is exempt from protocol fees on yield. + */ + function _isExemptFromYieldProtocolFees() internal view returns (bool) { + return _exemptFromYieldFees; + } + + /** + * @notice Returns the value of the invariant after the last join or exit operation. + */ + function getLastPostJoinExitInvariant() public view returns (uint256) { + return + _getMiscData().decodeUint(_LAST_POST_JOINEXIT_INVARIANT_OFFSET, _LAST_POST_JOINEXIT_INVARIANT_BIT_LENGTH); + } + + /** + * @notice Returns the all time high value for the weighted product of the Pool's tokens' rates. + * @dev Yield protocol fees are only charged when this value is exceeded. + */ + function getATHRateProduct() public view returns (uint256) { + return _athRateProduct; + } + + function getRateProviders() external view override returns (IRateProvider[] memory) { + // commented due to contract size exeeds 24k + /*uint256 totalTokens = _getTotalTokens(); + IRateProvider[] memory providers = new IRateProvider[](totalTokens); + + // prettier-ignore + { + providers[0] = _rateProvider0; + providers[1] = _rateProvider1; + if (totalTokens > 2) { providers[2] = _rateProvider2; } else { return providers; } + if (totalTokens > 3) { providers[3] = _rateProvider3; } else { return providers; } + if (totalTokens > 4) { providers[4] = _rateProvider4; } else { return providers; } + if (totalTokens > 5) { providers[5] = _rateProvider5; } else { return providers; } + if (totalTokens > 6) { providers[6] = _rateProvider6; } else { return providers; } + if (totalTokens > 7) { providers[7] = _rateProvider7; } else { return providers; } + if (totalTokens > 8) { providers[8] = _rateProvider8; } else { return providers; } + if (totalTokens > 9) { providers[9] = _rateProvider9; } else { return providers; } + } + + return providers;*/ + } + + // Protocol Fees + + /** + * @dev Returns the percentage of the Pool's supply which corresponds to protocol fees on swaps accrued by the Pool. + * @param preJoinExitInvariant - The Pool's invariant prior to the join/exit *before* minting protocol fees. + * @param protocolSwapFeePercentage - The percentage of swap fees which are paid to the protocol. + * @return swapProtocolFeesPercentage - The percentage of the Pool which corresponds to protocol fees on swaps. + */ + function _getSwapProtocolFeesPoolPercentage(uint256 preJoinExitInvariant, uint256 protocolSwapFeePercentage) + internal + view + returns (uint256) + { + // Before joins and exits, we measure the growth of the invariant compared to the invariant after the last join + // or exit, which will have been caused by swap fees, and use it to mint BPT as protocol fees. This dilutes all + // LPs, which means that new LPs will join the pool debt-free, and exiting LPs will pay any amounts due + // before leaving. + + return + InvariantGrowthProtocolSwapFees.getProtocolOwnershipPercentage( + preJoinExitInvariant.divDown(getLastPostJoinExitInvariant()), + FixedPoint.ONE, // Supply has not changed so supplyGrowthRatio = 1 + protocolSwapFeePercentage + ); + } + + /** + * @dev Returns the percentage of the Pool's supply which corresponds to protocol fees on yield accrued by the Pool. + * @param normalizedWeights - The Pool's normalized token weights. + * @return yieldProtocolFeesPercentage - The percentage of the Pool which corresponds to protocol fees on yield. + * @return athRateProduct - The new all-time-high rate product if it has increased, otherwise zero. + */ + function _getYieldProtocolFeesPoolPercentage(uint256[] memory normalizedWeights) + internal + view + returns (uint256, uint256) + { + if (_isExemptFromYieldProtocolFees()) return (0, 0); + + // Yield manifests in the Pool by individual tokens becoming more valuable, we convert this into comparable + // units by applying a rate to get the equivalent balance of non-yield-bearing tokens + // + // non-yield-bearing balance = rate * yield-bearing balance + // x'i = ri * xi + // + // To measure the amount of fees to pay due to yield, we take advantage of the fact that scaling the + // Pool's balances results in a scaling factor being applied to the original invariant. + // + // I(r1 * x1, r2 * x2) = (r1 * x1)^w1 * (r2 * x2)^w2 + // = (r1)^w1 * (r2)^w2 * (x1)^w1 * (x2)^w2 + // = I(r1, r2) * I(x1, x2) + // + // We then only need to measure the growth of this scaling factor to measure how the value of the BPT token + // increases due to yield; we can ignore the invariant calculated from the Pool's balances as these cancel. + // We then have the result: + // + // invariantGrowthRatio = I(r1_new, r2_new) / I(r1_old, r2_old) = rateProduct / athRateProduct + + uint256 athRateProduct = _athRateProduct; + uint256 rateProduct = _getRateProduct(normalizedWeights); + + // Only charge yield fees if we've exceeded the all time high of Pool value generated through yield. + // i.e. if the Pool makes a loss through the yield strategies then it shouldn't charge fees until it's + // been recovered. + if (rateProduct <= athRateProduct) return (0, 0); + + return ( + InvariantGrowthProtocolSwapFees.getProtocolOwnershipPercentage( + rateProduct.divDown(athRateProduct), + FixedPoint.ONE, // Supply has not changed so supplyGrowthRatio = 1 + getProtocolFeePercentageCache(ProtocolFeeType.YIELD) + ), + rateProduct + ); + } + + function _updateATHRateProduct(uint256 rateProduct) internal { + emit ATHRateProductUpdated(_athRateProduct, rateProduct); + + _athRateProduct = rateProduct; + } + + /** + * @dev Returns the amount of BPT to be minted as protocol fees prior to processing a join/exit. + * Note that this isn't a view function. This function automatically updates `_athRateProduct` to ensure that + * proper accounting is performed to prevent charging duplicate protocol fees. + * @param preJoinExitInvariant - The Pool's invariant prior to the join/exit. + * @param normalizedWeights - The Pool's normalized token weights. + * @param preJoinExitSupply - The Pool's total supply prior to the join/exit *before* minting protocol fees. + * @return protocolFeesToBeMinted - The amount of BPT to be minted as protocol fees. + * @return athRateProduct - The new all-time-high rate product if it has increased, otherwise zero. + */ + function _getPreJoinExitProtocolFees( + uint256 preJoinExitInvariant, + uint256[] memory normalizedWeights, + uint256 preJoinExitSupply + ) internal view returns (uint256, uint256) { + uint256 protocolSwapFeesPoolPercentage = _getSwapProtocolFeesPoolPercentage( + preJoinExitInvariant, + getProtocolFeePercentageCache(ProtocolFeeType.SWAP) + ); + (uint256 protocolYieldFeesPoolPercentage, uint256 athRateProduct) = _getYieldProtocolFeesPoolPercentage( + normalizedWeights + ); + + return ( + ExternalFees.bptForPoolOwnershipPercentage( + preJoinExitSupply, + protocolSwapFeesPoolPercentage + protocolYieldFeesPoolPercentage + ), + athRateProduct + ); + } + + /** + * @dev Returns the amount of BPT to be minted to pay protocol fees on swap fees accrued during a join/exit. + * Note that this isn't a view function. This function automatically updates `_lastPostJoinExitInvariant` to + * ensure that proper accounting is performed to prevent charging duplicate protocol fees. + * @param preJoinExitInvariant - The Pool's invariant prior to the join/exit. + * @param preBalances - The Pool's balances prior to the join/exit. + * @param balanceDeltas - The changes to the Pool's balances due to the join/exit. + * @param normalizedWeights - The Pool's normalized token weights. + * @param preJoinExitSupply - The Pool's total supply prior to the join/exit *after* minting protocol fees. + * @param postJoinExitSupply - The Pool's total supply after the join/exit. + */ + function _getPostJoinExitProtocolFees( + uint256 preJoinExitInvariant, + uint256[] memory preBalances, + uint256[] memory balanceDeltas, + uint256[] memory normalizedWeights, + uint256 preJoinExitSupply, + uint256 postJoinExitSupply + ) internal returns (uint256) { + bool isJoin = postJoinExitSupply >= preJoinExitSupply; + + // Compute the post balances by adding or removing the deltas. + for (uint256 i = 0; i < preBalances.length; ++i) { + preBalances[i] = isJoin + ? SafeMath.add(preBalances[i], balanceDeltas[i]) + : SafeMath.sub(preBalances[i], balanceDeltas[i]); + } + + // preBalances have now been mutated to reflect the postJoinExit balances. + uint256 postJoinExitInvariant = WeightedMath._calculateInvariant(normalizedWeights, preBalances); + uint256 protocolSwapFeePercentage = getProtocolFeePercentageCache(ProtocolFeeType.SWAP); + + _updatePostJoinExit(postJoinExitInvariant); + // We return immediately if the fee percentage is zero to avoid unnecessary computation. + if (protocolSwapFeePercentage == 0) return 0; + + uint256 protocolFeeAmount = InvariantGrowthProtocolSwapFees.calcDueProtocolFees( + postJoinExitInvariant.divDown(preJoinExitInvariant), + preJoinExitSupply, + postJoinExitSupply, + protocolSwapFeePercentage + ); + + return protocolFeeAmount; + } + + function _updatePostJoinExit(uint256 postJoinExitInvariant) internal virtual override { + // After all joins and exits we store the post join/exit invariant in order to compute growth due to swap fees + // in the next one. + _setMiscData( + _getMiscData().insertUint( + postJoinExitInvariant, + _LAST_POST_JOINEXIT_INVARIANT_OFFSET, + _LAST_POST_JOINEXIT_INVARIANT_BIT_LENGTH + ) + ); + } + + // Helper functions + + /** + * @notice Returns the contribution to the total rate product from a token with the given weight and rate provider. + */ + function _getRateFactor(uint256 normalizedWeight, IRateProvider provider) internal view returns (uint256) { + return provider == IRateProvider(0) ? FixedPoint.ONE : provider.getRate().powDown(normalizedWeight); + } + + /** + * @dev Returns the weighted product of all the token rates. + */ + function _getRateProduct(uint256[] memory normalizedWeights) internal view returns (uint256) { + uint256 totalTokens = normalizedWeights.length; + + uint256 rateProduct = FixedPoint.mulDown( + _getRateFactor(normalizedWeights[0], _rateProvider0), + _getRateFactor(normalizedWeights[1], _rateProvider1) + ); + + if (totalTokens > 2) { + rateProduct = rateProduct.mulDown(_getRateFactor(normalizedWeights[2], _rateProvider2)); + } else { + return rateProduct; + } + if (totalTokens > 3) { + rateProduct = rateProduct.mulDown(_getRateFactor(normalizedWeights[3], _rateProvider3)); + } else { + return rateProduct; + } + if (totalTokens > 4) { + rateProduct = rateProduct.mulDown(_getRateFactor(normalizedWeights[4], _rateProvider4)); + } else { + return rateProduct; + } + if (totalTokens > 5) { + rateProduct = rateProduct.mulDown(_getRateFactor(normalizedWeights[5], _rateProvider5)); + } else { + return rateProduct; + } + if (totalTokens > 6) { + rateProduct = rateProduct.mulDown(_getRateFactor(normalizedWeights[6], _rateProvider6)); + } else { + return rateProduct; + } + if (totalTokens > 7) { + rateProduct = rateProduct.mulDown(_getRateFactor(normalizedWeights[7], _rateProvider7)); + } else { + return rateProduct; + } + if (totalTokens > 8) { + rateProduct = rateProduct.mulDown(_getRateFactor(normalizedWeights[8], _rateProvider8)); + } else { + return rateProduct; + } + if (totalTokens > 9) { + rateProduct = rateProduct.mulDown(_getRateFactor(normalizedWeights[9], _rateProvider9)); + } else { + return rateProduct; + } + + return rateProduct; + } + + function _isOwnerOnlyAction(bytes32 actionId) + internal + view + virtual + override(BasePool, BasePoolAuthorization) + returns (bool) + { + return super._isOwnerOnlyAction(actionId); + } +} diff --git a/pkg/pool-range/foundry.toml b/pkg/pool-range/foundry.toml new file mode 120000 index 000000000..2d554be76 --- /dev/null +++ b/pkg/pool-range/foundry.toml @@ -0,0 +1 @@ +../../foundry.toml \ No newline at end of file diff --git a/pkg/pool-range/hardhat.config.ts b/pkg/pool-range/hardhat.config.ts new file mode 100644 index 000000000..7997b27d6 --- /dev/null +++ b/pkg/pool-range/hardhat.config.ts @@ -0,0 +1,45 @@ +import '@nomiclabs/hardhat-ethers'; +import '@nomiclabs/hardhat-waffle'; +import "hardhat-contract-sizer"; +import 'hardhat-ignore-warnings'; + +import { hardhatBaseConfig } from '@balancer-labs/v2-common'; +import { name } from './package.json'; + +import { task } from 'hardhat/config'; +import { TASK_COMPILE } from 'hardhat/builtin-tasks/task-names'; +import overrideQueryFunctions from '@balancer-labs/v2-helpers/plugins/overrideQueryFunctions'; + +task(TASK_COMPILE).setAction(overrideQueryFunctions); + +export default { + networks: { + hardhat: { + allowUnlimitedContractSize: true, + }, + }, + solidity: { + compilers: [ + { + version: "0.7.3", + settings: { + optimizer: { + enabled: true, + runs: 1, + }, + }, + }, + ], + overrides: { ...hardhatBaseConfig.overrides(name) }, + }, + warnings: hardhatBaseConfig.warnings, + contractSizer: { + alphaSort: true, + disambiguatePaths: false, + runOnCompile: true, + strict: true, + }, + paths: { + sources: "./contracts", + }, +}; diff --git a/pkg/pool-range/package.json b/pkg/pool-range/package.json new file mode 100644 index 000000000..3b4efc23d --- /dev/null +++ b/pkg/pool-range/package.json @@ -0,0 +1,72 @@ +{ + "name": "@balancer-labs/v2-pool-range", + "version": "2.0.1", + "description": "Balancer V2 Range Pools", + "license": "GPL-3.0-only", + "homepage": "https://github.com/balancer-labs/balancer-v2-monorepo/tree/master/pkg/pool-weighted#readme", + "repository": { + "type": "git", + "url": "https://github.com/balancer-labs/balancer-v2-monorepo.git", + "directory": "pkg/pool-range" + }, + "bugs": { + "url": "https://github.com/balancer-labs/balancer-v2-monorepo/issues" + }, + "files": [ + "contracts/**/*", + "!contracts/test/*" + ], + "scripts": { + "build": "yarn compile", + "compile": "hardhat compile && rm -rf artifacts/build-info", + "compile:watch": "nodemon --ext sol --exec yarn compile", + "lint": "yarn lint:solidity && yarn lint:typescript", + "lint:solidity": "solhint 'contracts/**/*.sol'", + "lint:typescript": "NODE_NO_WARNINGS=1 eslint . --ext .ts --ignore-path ../../.eslintignore --max-warnings 0", + "test": "yarn compile && mocha --extension ts --require hardhat/register --require @balancer-labs/v2-common/setupTests --recursive", + "test:fast": "yarn compile && mocha --extension ts --require hardhat/register --require @balancer-labs/v2-common/setupTests --recursive --parallel --exit", + "test:watch": "nodemon --ext js,ts --watch test --watch lib --exec 'clear && yarn test --no-compile'", + "test-fuzz": "forge test" + }, + "devDependencies": { + "@balancer-labs/balancer-js": "workspace:*", + "@balancer-labs/solidity-toolbox": "workspace:*", + "@balancer-labs/v2-common": "workspace:*", + "@balancer-labs/v2-helpers": "workspace:*", + "@balancer-labs/v2-interfaces": "workspace:*", + "@balancer-labs/v2-pool-utils": "workspace:*", + "@balancer-labs/v2-pool-weighted": "workspace:*", + "@balancer-labs/v2-solidity-utils": "workspace:*", + "@nomiclabs/hardhat-ethers": "^2.2.1", + "@nomiclabs/hardhat-waffle": "^2.0.3", + "@types/chai": "^4.3.3", + "@types/lodash": "^4.14.186", + "@types/mocha": "^10.0.0", + "@types/node": "^14.14.31", + "@typescript-eslint/eslint-plugin": "^5.41.0", + "@typescript-eslint/parser": "^5.41.0", + "chai": "^4.3.6", + "decimal.js": "^10.4.2", + "eslint": "^8.26.0", + "eslint-plugin-mocha-no-only": "^1.1.1", + "eslint-plugin-prettier": "^4.2.1", + "ethereum-waffle": "^3.4.4", + "ethers": "^5.7.2", + "hardhat": "^2.12.5", + "hardhat-contract-sizer": "^2.10.0", + "hardhat-ignore-warnings": "^0.2.4", + "lodash.frompairs": "^4.0.1", + "lodash.pick": "^4.4.0", + "lodash.range": "^3.2.0", + "lodash.times": "^4.3.2", + "lodash.zip": "^4.2.0", + "mocha": "^10.1.0", + "nodemon": "^2.0.20", + "prettier": "^2.7.1", + "prettier-plugin-solidity": "v1.0.0-alpha.59", + "solhint": "^3.2.0", + "solhint-plugin-prettier": "^0.0.4", + "ts-node": "^10.9.1", + "typescript": "^4.0.2" + } +} diff --git a/pkg/pool-range/test/BaseRangePool.behavior.ts b/pkg/pool-range/test/BaseRangePool.behavior.ts new file mode 100644 index 000000000..d6f6c1cca --- /dev/null +++ b/pkg/pool-range/test/BaseRangePool.behavior.ts @@ -0,0 +1,841 @@ +import { ethers } from 'hardhat'; +import { expect } from 'chai'; +import { BigNumber, Contract } from 'ethers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; +import { PoolSpecialization, SwapKind } from '@balancer-labs/balancer-js'; +import { BigNumberish, bn, fp, fpMul, pct } from '@balancer-labs/v2-helpers/src/numbers'; + +import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; +import { RawRangePoolDeployment } from './helpers/types'; +import { ZERO_ADDRESS } from '@balancer-labs/v2-helpers/src/constants'; +import { sharedBeforeEach } from '@balancer-labs/v2-common/sharedBeforeEach'; +import { expectBalanceChange } from '@balancer-labs/v2-helpers/src/test/tokenBalance'; +import Vault from '@balancer-labs/v2-helpers/src/models/vault/Vault'; +import BaseRangePool from './helpers/BaseRangePool'; +import RangePool from './helpers/RangePool'; +import { deploy } from './helpers/contract'; + +export function itBehavesAsRangePool(numberOfTokens: number): void { + const POOL_SWAP_FEE_PERCENTAGE = fp(0.01); + const WEIGHTS = [fp(30), fp(70), fp(5), fp(5)]; + const VIRTUAL_BALANCES = [fp(0.2), fp(0.4), fp(0.6), fp(0.8)]; + const INITIAL_BALANCES = [fp(0.1), fp(0.2), fp(0.3), fp(0.4)]; + const GENERAL_POOL_ONSWAP = + 'onSwap((uint8,address,address,uint256,bytes32,uint256,address,address,bytes),uint256[],uint256,uint256)'; + + let recipient: SignerWithAddress, other: SignerWithAddress, lp: SignerWithAddress; + let vault: Vault; + let pool: BaseRangePool, allTokens: TokenList, tokens: TokenList; + let math: Contract; + + const ZEROS = Array(numberOfTokens).fill(bn(0)); + const weights: BigNumberish[] = WEIGHTS.slice(0, numberOfTokens); + const initialVirtualBalances: BigNumberish[] = VIRTUAL_BALANCES.slice(0, numberOfTokens); + const initialBalances = INITIAL_BALANCES.slice(0, numberOfTokens); + + async function deployPool(params: RawRangePoolDeployment = {}): Promise { + pool = await RangePool.create({ + vault, + tokens, + weights, + swapFeePercentage: POOL_SWAP_FEE_PERCENTAGE, + ...params, + }); + } + + before('setup signers', async () => { + [, lp, recipient, other] = await ethers.getSigners(); + math = await deploy('ExternalRangeMath'); + }); + + sharedBeforeEach('deploy tokens and vault', async () => { + vault = await Vault.create(); + + const tokenAmounts = fp(100); + allTokens = await TokenList.create( + [ + { symbol: 'MKR', decimals: 18 }, + { symbol: 'DAI', decimals: 6 }, + { symbol: 'SNX', decimals: 8 }, + { symbol: 'BAT', decimals: 18 }, + { symbol: 'GRT', decimals: 6 }, + ], + { sorted: true } + ); + await allTokens.mint({ to: lp, amount: tokenAmounts }); + await allTokens.approve({ to: vault.address, from: lp, amount: tokenAmounts }); + }); + + beforeEach('define pool tokens', () => { + tokens = allTokens.subset(numberOfTokens); + }); + + describe('creation', () => { + context('when the creation succeeds', () => { + sharedBeforeEach('deploy pool from factory', async () => { + await deployPool({ fromFactory: true }); + }); + + it('sets the vault', async () => { + expect(await pool.getVault()).to.equal(pool.vault.address); + }); + + it('uses the corresponding specialization', async () => { + const expectedSpecialization = PoolSpecialization.GeneralPool; + const { address, specialization } = await pool.getRegisteredInfo(); + expect(address).to.equal(pool.address); + expect(specialization).to.equal(expectedSpecialization); + }); + + it('registers tokens in the vault', async () => { + const poolTokens = await pool.getTokens(); + + expect(poolTokens.tokens).to.have.members(tokens.addresses); + expect(poolTokens.balances).to.be.zeros; + }); + + it('starts with no BPT', async () => { + expect(await pool.totalSupply()).to.be.equal(0); + }); + + it('sets the asset managers to zero', async () => { + await tokens.asyncEach(async (token) => { + const info = await pool.getTokenInfo(token); + expect(info.assetManager).to.equal(ZERO_ADDRESS); + }); + }); + + it('sets swap fee', async () => { + expect(await pool.getSwapFeePercentage()).to.equal(POOL_SWAP_FEE_PERCENTAGE); + }); + + it('sets the name', async () => { + expect(await pool.name()).to.equal('Balancer Pool Token'); + }); + + it('sets the symbol', async () => { + expect(await pool.symbol()).to.equal('BPT'); + }); + + it('sets the decimals', async () => { + expect(await pool.decimals()).to.equal(18); + }); + + it('sets the virtual balances', async () => { + expect((await pool.getVirtualBalances())[0]).to.equal(0); + expect((await pool.getVirtualBalances())[1]).to.equal(0); + }); + }); + + context('when the creation fails', () => { + it('reverts if the number of tokens and weights do not match', async () => { + const badWeights = weights.slice(1); + + await expect(deployPool({ weights: badWeights })).to.be.revertedWith('INPUT_LENGTH_MISMATCH'); + }); + + it('reverts if there are repeated tokens', async () => { + const badTokens = new TokenList(Array(numberOfTokens).fill(tokens.first)); + + await expect(deployPool({ tokens: badTokens, fromFactory: true })).to.be.revertedWith('UNSORTED_ARRAY'); + }); + + it('reverts if the swap fee is too high', async () => { + const badSwapFeePercentage = fp(0.1).add(1); + + await expect(deployPool({ swapFeePercentage: badSwapFeePercentage })).to.be.revertedWith( + 'MAX_SWAP_FEE_PERCENTAGE' + ); + }); + + it('reverts if at least one weight is too low', async () => { + const badWeights = WEIGHTS.slice(0, numberOfTokens); + badWeights[0] = bn(99); + + await expect(deployPool({ weights: badWeights })).to.be.revertedWith('MIN_WEIGHT'); + }); + }); + }); + + describe('onJoinPool', () => { + function itJoins() { + it('fails if caller is not the vault', async () => { + await expect( + pool.instance.connect(lp).onJoinPool(pool.poolId, lp.address, other.address, [0], 0, 0, '0x') + ).to.be.revertedWith('CALLER_NOT_VAULT'); + }); + + it('fails if no user data', async () => { + await expect(pool.join({ data: '0x', from: lp })).to.be.revertedWith('Transaction reverted without a reason'); + }); + + it('fails if wrong user data', async () => { + const wrongUserData = ethers.utils.defaultAbiCoder.encode(['address'], [lp.address]); + + await expect(pool.join({ data: wrongUserData, from: lp })).to.be.revertedWith( + 'Transaction reverted without a reason' + ); + }); + + context('initialization', () => { + it('grants the n * invariant amount of BPT', async () => { + const invariant = await pool.estimateInvariant(initialVirtualBalances); + + const { amountsIn, dueProtocolFeeAmounts } = await pool.init({ + initialBalances, + initialVirtualBalances, + recipient, + from: lp }); + + // Amounts in should be the same as initial ones + expect(amountsIn).to.deep.equal(initialBalances); + + // Protocol fees should be zero + expect(dueProtocolFeeAmounts).to.be.zeros; + + // Initial balances should equal invariant + expect(await pool.balanceOf(recipient)).to.be.equalWithError(invariant.mul(numberOfTokens), 0.001); + }); + + it('fails if already initialized', async () => { + await pool.init({ + initialBalances, + initialVirtualBalances, + recipient, + from: lp }); + + await expect(pool.init({ + initialBalances, + initialVirtualBalances, + from: lp })).to.be.revertedWith('UNHANDLED_JOIN_KIND'); + }); + + it('reverts if paused', async () => { + await pool.pause(); + + await expect(pool.init({ + initialBalances, + initialVirtualBalances, + from: lp })).to.be.revertedWith('PAUSED'); + }); + }); + + context('join exact tokens in for BPT out', () => { + it('fails if not initialized', async () => { + await expect(pool.joinGivenIn({ recipient, amountsIn: initialBalances, from: lp })).to.be.revertedWith( + 'UNINITIALIZED' + ); + }); + + context('once initialized', () => { + let expectedBptOut: BigNumberish; + const amountsIn = ZEROS.map((n, i) => (i === 1 ? fp(0.1) : fp(0.2))); + + sharedBeforeEach('initialize pool', async () => { + await pool.init({ + initialBalances, + initialVirtualBalances, + recipient, + from: lp }); + expectedBptOut = await pool.estimateBptOut(amountsIn, initialBalances); + }); + + it('grants BPT for exact tokens', async () => { + const previousBptBalance = await pool.balanceOf(recipient); + const minimumBptOut = pct(expectedBptOut, 0.99); + + const result = await pool.joinGivenIn({ amountsIn, minimumBptOut, recipient, from: lp }); + + // Amounts in should be the same as initial ones + expect(result.amountsIn).to.deep.equal(amountsIn); + + // Protocol fees should be zero + expect(result.dueProtocolFeeAmounts).to.be.zeros; + + // Make sure received BPT is close to what we expect + const currentBptBalance = await pool.balanceOf(recipient); + expect(currentBptBalance.sub(previousBptBalance)).to.be.equalWithError(expectedBptOut, 0.0001); + + const virtualBalances = await pool.getVirtualBalances(); + expect(virtualBalances[0]).to.be.gt(0); + expect(virtualBalances[1]).to.be.gt(0); + }); + + it('can tell how much BPT it will give in return', async () => { + const minimumBptOut = pct(expectedBptOut, 0.99); + + const result = await pool.queryJoinGivenIn({ amountsIn, minimumBptOut, from: lp }); + + expect(result.amountsIn).to.deep.equal(amountsIn); + expect(result.bptOut).to.be.equalWithError(expectedBptOut, 0.0001); + }); + + it('fails if not enough BPT', async () => { + // This call should fail because we are requesting minimum 1% more + const minimumBptOut = pct(expectedBptOut, 1.01); + + await expect(pool.joinGivenIn({ amountsIn, minimumBptOut, from: lp })).to.be.revertedWith( + 'BPT_OUT_MIN_AMOUNT' + ); + }); + + it('reverts if paused', async () => { + await pool.pause(); + + await expect(pool.joinGivenIn({ amountsIn })).to.be.revertedWith('PAUSED'); + }); + }); + }); + + context('join all tokens in for exact BPT out', () => { + it('fails if not initialized', async () => { + await expect(pool.joinAllGivenOut({ bptOut: fp(2), from: lp })).to.be.revertedWith('UNINITIALIZED'); + }); + + context('once initialized', () => { + sharedBeforeEach('initialize pool', async () => { + await pool.init({ + initialBalances, + initialVirtualBalances, + recipient: recipient, + from: lp }); + }); + + it('grants exact BPT for tokens in', async () => { + const previousBptBalance = await pool.balanceOf(recipient); + // We want to join for half the initial BPT supply, which will require half the initial balances + const bptOut = previousBptBalance.div(2); + + const expectedAmountsIn = initialBalances.map((balance) => balance.div(2)); + + const result = await pool.joinAllGivenOut({ recipient, bptOut, from: lp }); + + for (let i = 0; i < expectedAmountsIn.length; i++) { + expect(result.amountsIn[i]).to.be.equalWithError(expectedAmountsIn[i], 0.001); + } + + // Protocol fees should be zero + expect(result.dueProtocolFeeAmounts).to.be.zeros; + + // Make sure received BPT equals we expect (since bptOut is given) + const currentBptBalance = await pool.balanceOf(recipient); + expect(currentBptBalance.sub(previousBptBalance)).to.be.equal(bptOut); + }); + + it('can tell what token amounts it will have to receive', async () => { + const expectedAmountsIn = initialBalances.map((balance) => balance.div(2)); + const previousBptBalance = await pool.balanceOf(recipient); + // We want to join for half the initial BPT supply, which will require half the initial balances + const bptOut = previousBptBalance.div(2); + + const result = await pool.queryJoinAllGivenOut({ bptOut, from: lp }); + + expect(result.bptOut).to.be.equal(bptOut); + + for (let i = 0; i < expectedAmountsIn.length; i++) { + expect(result.amountsIn[i]).to.be.equalWithError(expectedAmountsIn[i], 0.001); + } + }); + + it('reverts if paused', async () => { + await pool.pause(); + + await expect(pool.joinAllGivenOut({ bptOut: fp(2) })).to.be.revertedWith('PAUSED'); + }); + }); + }); + } + + sharedBeforeEach('deploy pool', async () => { + await deployPool(); + }); + + context('when not in recovery mode', () => { + itJoins(); + }); + + context('when in recovery mode', () => { + sharedBeforeEach(async () => { + await pool.enableRecoveryMode(); + }); + + itJoins(); + }); + }); + + describe('onExitPool', () => { + let previousBptBalance: BigNumber; + + function itExits() { + it('fails if caller is not the vault', async () => { + await expect( + pool.instance.connect(lp).onExitPool(pool.poolId, recipient.address, other.address, [0], 0, 0, '0x') + ).to.be.revertedWith('CALLER_NOT_VAULT'); + }); + + it('fails if no user data', async () => { + await expect(pool.exit({ data: '0x' })).to.be.revertedWith('Transaction reverted without a reason'); + }); + + it('fails if wrong user data', async () => { + const wrongUserData = ethers.utils.defaultAbiCoder.encode(['address'], [lp.address]); + + await expect(pool.exit({ data: wrongUserData })).to.be.revertedWith('Transaction reverted without a reason'); + }); + + context('exit exact BPT in for all tokens out', () => { + it('grants all tokens for exact bpt', async () => { + // Exit with half of the BPT balance + const bptIn = previousBptBalance.div(2); + const expectedAmountsOut = initialBalances.map((balance) => balance.div(2)); + + const result = await pool.multiExitGivenIn({ from: lp, bptIn }); + + // Protocol fees should be zero + expect(result.dueProtocolFeeAmounts).to.be.zeros; + + // Balances are reduced by half because we are returning half of the BPT supply + expect(result.amountsOut).to.be.equalWithError(expectedAmountsOut, 0.001); + + // Current BPT balance should have been reduced by half + expect(await pool.balanceOf(lp)).to.be.equalWithError(bptIn, 0.001); + }); + + it('fully exit', async () => { + // The LP doesn't own all BPT, since some was locked. They will only be able to extract a (large) percentage + // of the Pool's balance: the rest remains there forever. + const totalBPT = await pool.totalSupply(); + const expectedAmountsOut = initialBalances.map((balance) => balance.mul(previousBptBalance).div(totalBPT)); + + const result = await pool.multiExitGivenIn({ from: lp, bptIn: previousBptBalance }); + + // Protocol fees should be zero + expect(result.dueProtocolFeeAmounts).to.be.zeros; + + // All balances are extracted + expect(result.amountsOut).to.be.lteWithError(expectedAmountsOut, 0.00001); + + // Current BPT balances should be zero due to full exit + expect(await pool.balanceOf(lp)).to.equal(0); + }); + + it('can tell what token amounts it will give in return', async () => { + const totalBPT = await pool.totalSupply(); + const expectedAmountsOut = initialBalances.map((balance) => balance.mul(previousBptBalance).div(totalBPT)); + + const result = await pool.queryMultiExitGivenIn({ bptIn: previousBptBalance }); + + expect(result.bptIn).to.equal(previousBptBalance); + expect(result.amountsOut).to.be.lteWithError(expectedAmountsOut, 0.00001); + }); + + it('reverts if paused', async () => { + await pool.pause(); + + const bptIn = previousBptBalance.div(2); + await expect(pool.multiExitGivenIn({ from: lp, bptIn })).to.be.revertedWith('PAUSED'); + }); + }); + + context('exit BPT in for exact tokens out', () => { + it('grants exact tokens for bpt', async () => { + // Request half of the token balances + const amountsOut = initialBalances.map((balance) => balance.div(2)); + const expectedBptIn = previousBptBalance.div(2); + const maximumBptIn = pct(expectedBptIn, 1.01); + + const result = await pool.exitGivenOut({ from: lp, amountsOut, maximumBptIn }); + + // Protocol fees should be zero + expect(result.dueProtocolFeeAmounts).to.be.zeros; + + // Token balances should been reduced as requested + expect(result.amountsOut).to.deep.equal(amountsOut); + + // BPT balance should have been reduced by half because we are returning half of the tokens + expect(await pool.balanceOf(lp)).to.be.equalWithError(previousBptBalance.div(2), 0.001); + }); + + it('can tell how much BPT it will have to receive', async () => { + const amountsOut = initialBalances.map((balance) => balance.div(2)); + const expectedBptIn = previousBptBalance.div(2); + const maximumBptIn = pct(expectedBptIn, 1.01); + + const result = await pool.queryExitGivenOut({ amountsOut, maximumBptIn }); + + expect(result.amountsOut).to.deep.equal(amountsOut); + expect(result.bptIn).to.be.equalWithError(previousBptBalance.div(2), 0.001); + }); + + it('fails if more BPT needed', async () => { + // Call should fail because we are requesting a max amount lower than the actual needed + const amountsOut = initialBalances; + const maximumBptIn = previousBptBalance.div(2); + + await expect(pool.exitGivenOut({ from: lp, amountsOut, maximumBptIn })).to.be.revertedWith( + 'BPT_IN_MAX_AMOUNT' + ); + }); + + it('reverts if paused', async () => { + await pool.pause(); + + const amountsOut = initialBalances; + await expect(pool.exitGivenOut({ from: lp, amountsOut })).to.be.revertedWith('PAUSED'); + }); + }); + } + + sharedBeforeEach('deploy and initialize pool', async () => { + await deployPool(); + await pool.init({ + initialBalances, + initialVirtualBalances, + from: lp }); + previousBptBalance = await pool.balanceOf(lp); + }); + + context('when not in recovery mode', () => { + itExits(); + }); + + context('when in recovery mode', () => { + sharedBeforeEach(async () => { + await pool.enableRecoveryMode(); + }); + + itExits(); + }); + }); + + describe('onSwap', () => { + function itSwaps() { + context('given in', () => { + it('reverts if caller is not the vault', async () => { + await expect( + pool.instance[GENERAL_POOL_ONSWAP]( + { + kind: SwapKind.GivenIn, + tokenIn: tokens.first.address, + tokenOut: tokens.second.address, + amount: 0, + poolId: await pool.getPoolId(), + lastChangeBlock: 0, + from: lp.address, + to: other.address, + userData: '0x', + }, + [], + 0, + 0 + ) + ).to.be.revertedWith('CALLER_NOT_VAULT'); + }); + + it('calculates amount out', async () => { + const amount = fp(0.1); + const amountWithFees = fpMul(amount, POOL_SWAP_FEE_PERCENTAGE.add(fp(1))); + const expectedAmountOut = await pool.estimateGivenIn({ in: 1, out: 0, amount: amountWithFees }); + + const result = await pool.swapGivenIn({ in: 1, out: 0, amount: amountWithFees, from: lp, recipient }); + + expect(result.amount).to.be.equalWithError(expectedAmountOut, 0.01); + }); + + it('calculates max amount out', async () => { + const maxAmountIn = await pool.getMaxIn(1); + const maxAmountInWithFees = fpMul(maxAmountIn, POOL_SWAP_FEE_PERCENTAGE.add(fp(1))); + const expectedAmountOut = await pool.estimateGivenIn({ in: 1, out: 0, amount: maxAmountInWithFees }); + + const result = await pool.swapGivenIn({ in: 1, out: 0, amount: maxAmountInWithFees, from: lp, recipient }); + + expect(result.amount).to.be.equalWithError(expectedAmountOut, 0.05); + }); + + it('swap all amount of token#0 and join tokens again', async () => { + expect((await pool.getBalances())[0]).to.be.gt(0); + + const maxAmountOut = await pool.getMaxOut(0); + await pool.swapGivenOut({ in: 1, out: 0, amount: maxAmountOut, from: lp, recipient }); + + const prevBalances = await pool.getBalances(); + expect(prevBalances[0]).to.be.eq(0); + expect(prevBalances[1]).to.be.gt(0); + + const prevVirtualBalances = await pool.getVirtualBalances(); + + let amountsIn = ZEROS.map((n, i) => (i === 0 ? fp(0) : fp(0.1))); + const minimumBptOut = bn(1); + const ratioMin = await math.calcRatioMin(prevBalances, amountsIn); + await pool.joinGivenIn({ amountsIn, minimumBptOut, from: lp }); + + const newBalances = await pool.getBalances(); + expect(newBalances[0]).to.be.eq(0); + expect(newBalances[1]).to.be.eq(prevBalances[1].add(fp(0.1))); + + // newVirtualBalances[i] - prevVirtualBalances[i] == prevVirtualBalances[i] * rationMin + const newVirtualBalances = await pool.getVirtualBalances(); + expect(prevVirtualBalances[0].mul(ratioMin).div(fp(1))).to.be.equalWithError( + newVirtualBalances[0].sub(prevVirtualBalances[0]), + 0.000001 + ); + expect(prevVirtualBalances[1].mul(ratioMin).div(fp(1))).to.be.equalWithError( + newVirtualBalances[1].sub(prevVirtualBalances[1]), + 0.000001 + ); + + // finally, join all tokens + amountsIn = ZEROS.map((n, i) => (i === 1 ? fp(0.1) : fp(0.2))); + await pool.joinGivenIn({ amountsIn, minimumBptOut, from: lp }); + + expect((await pool.getBalances())[0]).to.be.gt(0); + }); + + it('swap all amount of token#0 and exit', async () => { + expect((await pool.getBalances())[0]).to.be.gt(0); + + const maxAmountOut = await pool.getMaxOut(0); + await pool.swapGivenOut({ in: 1, out: 0, amount: maxAmountOut, from: lp, recipient }); + + expect((await pool.getBalances())[0]).to.be.eq(0); + + const amountsIn = ZEROS.map((n, i) => (i === 0 ? fp(0) : fp(0.1))); + const minimumBptOut = bn(1); + await pool.joinGivenIn({ amountsIn, minimumBptOut, from: lp }); + + const prevVirtualBalances = await pool.getVirtualBalances(); + + const previousBptBalance = await pool.balanceOf(lp); + const totalSupply = await pool.totalSupply(); + const ratioMin = previousBptBalance.mul(fp(1)).div(totalSupply); + await pool.multiExitGivenIn({ from: lp, bptIn: previousBptBalance }); + + const newVirtualBalances = await pool.getVirtualBalances(); + + // prevVirtualBalances[i] - newVirtualBalances[i] == prevVirtualBalances[i] * rationMin + expect(prevVirtualBalances[0].mul(ratioMin).div(fp(1))).to.be.equalWithError( + prevVirtualBalances[0].sub(newVirtualBalances[0]), + 0.000001 + ); + expect(prevVirtualBalances[1].mul(ratioMin).div(fp(1))).to.be.equalWithError( + prevVirtualBalances[1].sub(newVirtualBalances[1]), + 0.000001 + ); + + expect(await pool.balanceOf(lp)).to.equal(0); + }); + + it('reverts if token in is not in the pool', async () => { + await expect(pool.swapGivenIn({ in: allTokens.GRT, out: 0, amount: 1, from: lp })).to.be.revertedWith( + 'TOKEN_NOT_REGISTERED' + ); + }); + + it('reverts if token out is not in the pool', async () => { + await expect(pool.swapGivenIn({ in: 1, out: allTokens.GRT, amount: 1, from: lp })).to.be.revertedWith( + 'TOKEN_NOT_REGISTERED' + ); + }); + + it('reverts if paused', async () => { + await pool.pause(); + + await expect(pool.swapGivenIn({ in: 1, out: 0, amount: 1, from: lp })).to.be.revertedWith('PAUSED'); + }); + }); + + context('given out', () => { + it('reverts if caller is not the vault', async () => { + await expect( + pool.instance[GENERAL_POOL_ONSWAP]( + { + kind: SwapKind.GivenOut, + tokenIn: tokens.first.address, + tokenOut: tokens.second.address, + amount: 0, + poolId: await pool.getPoolId(), + lastChangeBlock: 0, + from: lp.address, + to: other.address, + userData: '0x', + }, + [], + 0, + 0 + ) + ).to.be.revertedWith('CALLER_NOT_VAULT'); + }); + + it('calculates amount in', async () => { + const amount = fp(0.01); + const expectedAmountIn = await pool.estimateGivenOut({ in: 1, out: 0, amount }); + + const result = await pool.swapGivenOut({ in: 1, out: 0, amount, from: lp, recipient }); + + expect(result.amount).to.be.equalWithError(expectedAmountIn, 0.1); + }); + + it('calculates max amount in', async () => { + const amount = await pool.getMaxOut(0); + const expectedAmountIn = await pool.estimateGivenOut({ in: 1, out: 0, amount }); + + const result = await pool.swapGivenOut({ in: 1, out: 0, amount, from: lp, recipient }); + + expect(result.amount).to.be.equalWithError(expectedAmountIn, 0.1); + }); + + // excluded since max out ratio is not checked + /*it('reverts if token in exceeds max out ratio', async () => { + const amount = (await pool.getMaxOut(0)).add(2); + + await expect(pool.swapGivenOut({ in: 1, out: 0, amount, from: lp })).to.be.revertedWith('MAX_OUT_RATIO'); + });*/ + + it('reverts if token in is not in the pool when given out', async () => { + await expect(pool.swapGivenOut({ in: allTokens.GRT, out: 0, amount: 1, from: lp })).to.be.revertedWith( + 'TOKEN_NOT_REGISTERED' + ); + }); + + it('reverts if token out is not in the pool', async () => { + await expect(pool.swapGivenOut({ in: 1, out: allTokens.GRT, amount: 1, from: lp })).to.be.revertedWith( + 'TOKEN_NOT_REGISTERED' + ); + }); + + it('reverts if paused', async () => { + await pool.pause(); + + await expect(pool.swapGivenOut({ in: 1, out: 0, amount: 1, from: lp })).to.be.revertedWith('PAUSED'); + }); + }); + } + + sharedBeforeEach('deploy and join pool', async () => { + await deployPool(); + await pool.init({ + initialBalances, + initialVirtualBalances, + from: lp }); + }); + + context('when not in recovery mode', () => { + itSwaps(); + }); + + context('when in recovery mode', () => { + sharedBeforeEach(async () => { + await pool.enableRecoveryMode(); + }); + + itSwaps(); + }); + }); + + describe('recovery mode', () => { + sharedBeforeEach('deploy pool and enter recovery mode', async () => { + await deployPool(); + await pool.init({ + initialBalances, + initialVirtualBalances, + from: lp }); + await pool.enableRecoveryMode(); + }); + + function itExitsViaRecoveryModeCorrectly() { + it('the recovery mode exit can be used', async () => { + const preExitBPT = await pool.balanceOf(lp.address); + const exitBPT = preExitBPT.div(3); + + // The sole BPT holder is the initial LP, so they own the initial balances + const expectedChanges = tokens.reduce( + (changes, token, i) => ({ ...changes, [token.symbol]: ['very-near', initialBalances[i].div(3)] }), + {} + ); + + await expectBalanceChange( + () => + pool.recoveryModeExit({ + from: lp, + bptIn: exitBPT, + }), + tokens, + { account: lp, changes: expectedChanges } + ); + + // Exit BPT was burned + const afterExitBalance = await pool.balanceOf(lp.address); + expect(afterExitBalance).to.equal(preExitBPT.sub(exitBPT)); + }); + } + + itExitsViaRecoveryModeCorrectly(); + + context('when paused', () => { + sharedBeforeEach('pause pool', async () => { + await pool.pause(); + }); + + itExitsViaRecoveryModeCorrectly(); + }); + }); + + describe('protocol swap fees', () => { + const protocolFeePercentage = fp(0.1); // 10 % + + sharedBeforeEach('deploy and join pool', async () => { + // We will use a mock vault for this one since we'll need to manipulate balances. + await deployPool({ vault: await Vault.create({ mocked: true }) }); + await pool.init({ + initialBalances, + initialVirtualBalances, + from: lp, + protocolFeePercentage }); + }); + + context('without balance changes', () => { + it('no protocol fees on joins and exits', async () => { + let joinResult = await pool.joinGivenIn({ from: lp, amountsIn: fp(100), protocolFeePercentage }); + expect(joinResult.dueProtocolFeeAmounts).to.be.zeros; + + joinResult = await pool.joinAllGivenOut({ from: lp, bptOut: fp(0.1) }); + expect(joinResult.dueProtocolFeeAmounts).to.be.zeros; + + let exitResult = await pool.multiExitGivenIn({ from: lp, bptIn: fp(10), protocolFeePercentage }); + expect(exitResult.dueProtocolFeeAmounts).to.be.zeros; + + joinResult = await pool.joinGivenIn({ from: lp, amountsIn: fp(10), protocolFeePercentage }); + expect(joinResult.dueProtocolFeeAmounts).to.be.zeros; + + exitResult = await pool.exitGivenOut({ from: lp, amountsOut: fp(10), protocolFeePercentage }); + expect(exitResult.dueProtocolFeeAmounts).to.be.zeros; + }); + }); + + context('with balance changes', () => { + let currentBalances: BigNumber[]; + + sharedBeforeEach('simulate doubled initial balances', async () => { + // 4/3 of the initial balances + currentBalances = initialBalances.map((balance) => balance.mul(4).div(3)); + }); + + it('no protocol fees on join exact tokens in for BPT out', async () => { + const result = await pool.joinGivenIn({ from: lp, amountsIn: fp(1), currentBalances, protocolFeePercentage }); + expect(result.dueProtocolFeeAmounts).to.be.zeros; + }); + + it('no protocol fees on exit exact BPT in for all tokens out', async () => { + const bptBalance = await pool.balanceOf(recipient); + const result = await pool.multiExitGivenIn({ + from: lp, + bptIn: bptBalance, + currentBalances, + protocolFeePercentage, + }); + + expect(result.dueProtocolFeeAmounts).to.be.zeros; + }); + }); + }); +} diff --git a/pkg/pool-range/test/BaseRangePool.test.ts b/pkg/pool-range/test/BaseRangePool.test.ts new file mode 100644 index 000000000..669382fb4 --- /dev/null +++ b/pkg/pool-range/test/BaseRangePool.test.ts @@ -0,0 +1,36 @@ +import { expect } from 'chai'; +import { fp } from '@balancer-labs/v2-helpers/src/numbers'; + +import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; +import RangePool from './helpers/RangePool'; + +import { itBehavesAsRangePool } from './BaseRangePool.behavior'; + +describe('BaseRangePool', function () { + context('for a 1 token pool', () => { + it('reverts if there is a single token', async () => { + const tokens = await TokenList.create(1); + const weights = [fp(1)]; + + await expect(RangePool.create({ tokens, weights })).to.be.revertedWith('MIN_TOKENS'); + }); + }); + + context('for a 2 token pool', () => { + itBehavesAsRangePool(2); + }); + + context('for a 3 token pool', () => { + itBehavesAsRangePool(3); + }); + + context('for a too-many token pool', () => { + it('reverts if there are too many tokens', async () => { + // The maximum number of tokens is 20 + const tokens = await TokenList.create(11); + const weights = new Array(21).fill(fp(1)); + + await expect(RangePool.create({ tokens, weights })).to.be.revertedWith('MAX_TOKENS'); + }); + }); +}); diff --git a/pkg/pool-range/test/helpers/BaseRangePool.ts b/pkg/pool-range/test/helpers/BaseRangePool.ts new file mode 100644 index 000000000..edbae7371 --- /dev/null +++ b/pkg/pool-range/test/helpers/BaseRangePool.ts @@ -0,0 +1,511 @@ +import { BigNumber, Contract, ContractFunction, ContractReceipt, ContractTransaction } from 'ethers'; +import { BigNumberish, bn, fp, fromFp, fpMul } from '@balancer-labs/v2-helpers/src/numbers'; +import { MAX_UINT256, ZERO_ADDRESS } from '@balancer-labs/v2-helpers/src/constants'; +import * as expectEvent from '@balancer-labs/v2-helpers/src/test/expectEvent'; +import Vault from '@balancer-labs/v2-helpers/src/models/vault/Vault'; +import Token from '@balancer-labs/v2-helpers/src/models/tokens/Token'; +import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; +import TypesConverter from '@balancer-labs/v2-helpers/src/models/types/TypesConverter'; +import { GeneralSwap } from '@balancer-labs/v2-helpers/src/models/vault/types'; +import { + JoinExitRangePool, + InitRangePool, + JoinGivenInRangePool, + JoinGivenOutRangePool, + JoinAllGivenOutRangePool, + JoinResult, + ExitResult, + SwapResult, + SingleExitGivenInRangePool, + MultiExitGivenInRangePool, + ExitGivenOutRangePool, + SwapRangePool, + ExitQueryResult, + JoinQueryResult, + PoolQueryResult, + GradualWeightUpdateParams, +} from './types'; +import { + calculateInvariant, + calcTokenInGivenExactBptOut, + calcTokenOutGivenExactBptIn, + calcOutGivenIn, + calcInGivenOut, +} from '@balancer-labs/v2-helpers/src/models/pools/weighted/math'; + +import { SwapKind, WeightedPoolEncoder } from '@balancer-labs/balancer-js'; +import { RangePoolEncoder } from './encoder'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import BasePool from '@balancer-labs/v2-helpers/src/models/pools/base/BasePool'; +import { Account } from '@balancer-labs/v2-helpers/src/models/types/types'; + +//const MAX_IN_RATIO = fp(0.3); +//const MAX_OUT_RATIO = fp(0.3); +const MAX_INVARIANT_RATIO = fp(3); +const MIN_INVARIANT_RATIO = fp(0.7); + +export default class BaseRangePool extends BasePool { + weights: BigNumberish[]; + + constructor( + instance: Contract, + poolId: string, + vault: Vault, + tokens: TokenList, + weights: BigNumberish[], + swapFeePercentage: BigNumberish, + owner?: Account + ) { + super(instance, poolId, vault, tokens, swapFeePercentage, owner); + + this.weights = weights; + } + + get normalizedWeights(): BigNumberish[] { + return this.weights; + } + + async getLastPostJoinExitInvariant(): Promise { + return this.instance.getLastPostJoinExitInvariant(); + } + + async getMaxInvariantDecrease(): Promise { + const supply = await this.totalSupply(); + return supply.sub(fpMul(MIN_INVARIANT_RATIO, supply)); + } + + async getMaxInvariantIncrease(): Promise { + const supply = await this.totalSupply(); + return fpMul(MAX_INVARIANT_RATIO, supply).sub(supply); + } + + async getMaxIn(tokenIndex: number, currentBalances?: BigNumber[]): Promise { + if (!currentBalances) currentBalances = await this.getBalances(); + return currentBalances[tokenIndex]; + } + + async getMaxOut(tokenIndex: number, currentBalances?: BigNumber[]): Promise { + if (!currentBalances) currentBalances = await this.getBalances(); + return currentBalances[tokenIndex]; + } + + async getNormalizedWeights(): Promise { + return this.instance.getNormalizedWeights(); + } + + async getVirtualBalances(): Promise { + return this.instance.getVirtualBalances(); + } + + async estimateInvariant(currentBalances?: BigNumberish[]): Promise { + if (!currentBalances) currentBalances = await this.getBalances(); + const scalingFactors = await this.getScalingFactors(); + + return calculateInvariant( + currentBalances.map((x, i) => fpMul(x, scalingFactors[i])), + this.weights + ); + } + + async estimateGivenIn(params: SwapRangePool, virtualBalances?: BigNumberish[]): Promise { + const balances = await this.getBalances(); + if (!virtualBalances) virtualBalances = await this.getVirtualBalances(); + const [tokenIn, tokenOut] = this.tokens.indicesOfTwoTokens(params.in, params.out); + + const amountOut = bn( + calcOutGivenIn( + virtualBalances[tokenIn], + this.weights[tokenIn], + virtualBalances[tokenOut], + this.weights[tokenOut], + params.amount + ) + ); + return amountOut.lt(balances[tokenOut]) ? amountOut : balances[tokenOut]; + } + + async estimateGivenOut(params: SwapRangePool, virtualBalances?: BigNumberish[]): Promise { + if (!virtualBalances) virtualBalances = await this.getVirtualBalances(); + const [tokenIn, tokenOut] = this.tokens.indicesOfTwoTokens(params.in, params.out); + + return bn( + calcInGivenOut( + virtualBalances[tokenIn], + this.weights[tokenIn], + virtualBalances[tokenOut], + this.weights[tokenOut], + params.amount + ) + ); + } + + async calcBptOutGivenExactTokensIn( + fpBalances: BigNumberish[], + fpAmountsIn: BigNumberish[], + fpBptTotalSupply: BigNumberish + ): Promise { + const balances = fpBalances.map(fromFp); + const amountsIn = fpAmountsIn.map(fromFp); + const bptTotalSupply = fromFp(fpBptTotalSupply); + + let ratioMin = amountsIn[0].div(balances[0]); + let i = 1; + while (i < balances.length && ratioMin.gt(0)) { + const tmp = amountsIn[i].div(balances[i]); + ratioMin = tmp.lt(ratioMin) ? tmp : ratioMin; + i++; + } + return fp(bptTotalSupply.mul(ratioMin)); + } + + async estimateBptOut( + amountsIn: BigNumberish[], + currentBalances?: BigNumberish[], + supply?: BigNumberish + ): Promise { + if (!supply) supply = await this.totalSupply(); + if (!currentBalances) currentBalances = await this.getBalances(); + return this.calcBptOutGivenExactTokensIn(currentBalances, amountsIn, supply); + } + + async estimateTokenIn( + token: number | Token, + bptOut: BigNumberish, + currentBalances?: BigNumberish[], + supply?: BigNumberish + ): Promise { + if (!supply) supply = await this.totalSupply(); + if (!currentBalances) currentBalances = await this.getBalances(); + const tokenIndex = this.tokens.indexOf(token); + return calcTokenInGivenExactBptOut( + tokenIndex, + currentBalances, + this.weights, + bptOut, + supply, + this.swapFeePercentage + ); + } + + async estimateTokenOut( + token: number | Token, + bptIn: BigNumberish, + currentBalances?: BigNumberish[], + supply?: BigNumberish + ): Promise { + if (!supply) supply = await this.totalSupply(); + if (!currentBalances) currentBalances = await this.getBalances(); + const tokenIndex = this.tokens.indexOf(token); + return calcTokenOutGivenExactBptIn( + tokenIndex, + currentBalances, + this.weights, + bptIn, + supply, + this.swapFeePercentage + ); + } + + async swapGivenIn(params: SwapRangePool): Promise { + return this.swap(await this._buildSwapParams(SwapKind.GivenIn, params)); + } + + async swapGivenOut(params: SwapRangePool): Promise { + return this.swap(await this._buildSwapParams(SwapKind.GivenOut, params)); + } + + async updateProtocolFeePercentageCache(): Promise { + return this.instance.updateProtocolFeePercentageCache(); + } + + async swap(params: GeneralSwap): Promise { + let receipt: ContractReceipt; + if (this.vault.mocked) { + const tx = await this.vault.generalSwap(params); + receipt = await tx.wait(); + } else { + if (!params.from) throw new Error('No signer provided'); + const tx = await this.vault.instance.connect(params.from).swap( + { + poolId: params.poolId, + kind: params.kind, + assetIn: params.tokenIn, + assetOut: params.tokenOut, + amount: params.amount, + userData: params.data, + }, + { + sender: TypesConverter.toAddress(params.from), + recipient: TypesConverter.toAddress(params.to) ?? ZERO_ADDRESS, + fromInternalBalance: false, + toInternalBalance: false, + }, + params.kind == 0 ? 0 : MAX_UINT256, + MAX_UINT256 + ); + receipt = await tx.wait(); + } + const { amountIn, amountOut } = expectEvent.inReceipt(receipt, 'Swap').args; + const amount = params.kind == SwapKind.GivenIn ? amountOut : amountIn; + + return { amount, receipt }; + } + + async init(params: InitRangePool): Promise { + return this.join(this._buildInitParams(params)); + } + + async joinGivenIn(params: JoinGivenInRangePool): Promise { + return this.join(this._buildJoinGivenInParams(params)); + } + + async queryJoinGivenIn(params: JoinGivenInRangePool): Promise { + return this.queryJoin(this._buildJoinGivenInParams(params)); + } + + async joinGivenOut(params: JoinGivenOutRangePool): Promise { + return this.join(this._buildJoinGivenOutParams(params)); + } + + async queryJoinGivenOut(params: JoinGivenOutRangePool): Promise { + return this.queryJoin(this._buildJoinGivenOutParams(params)); + } + + async joinAllGivenOut(params: JoinAllGivenOutRangePool): Promise { + return this.join(this._buildJoinAllGivenOutParams(params)); + } + + async queryJoinAllGivenOut(params: JoinAllGivenOutRangePool): Promise { + return this.queryJoin(this._buildJoinAllGivenOutParams(params)); + } + + async exitGivenOut(params: ExitGivenOutRangePool): Promise { + return this.exit(this._buildExitGivenOutParams(params)); + } + + async queryExitGivenOut(params: ExitGivenOutRangePool): Promise { + return this.queryExit(this._buildExitGivenOutParams(params)); + } + + async singleExitGivenIn(params: SingleExitGivenInRangePool): Promise { + return this.exit(this._buildSingleExitGivenInParams(params)); + } + + async querySingleExitGivenIn(params: SingleExitGivenInRangePool): Promise { + return this.queryExit(this._buildSingleExitGivenInParams(params)); + } + + async multiExitGivenIn(params: MultiExitGivenInRangePool): Promise { + return this.exit(this._buildMultiExitGivenInParams(params)); + } + + async queryMultiExitGivenIn(params: MultiExitGivenInRangePool): Promise { + return this.queryExit(this._buildMultiExitGivenInParams(params)); + } + + async queryJoin(params: JoinExitRangePool): Promise { + const fn = this.instance.queryJoin; + return (await this._executeQuery(params, fn)) as JoinQueryResult; + } + + async join(params: JoinExitRangePool): Promise { + const currentBalances = params.currentBalances || (await this.getBalances()); + const to = params.recipient ? TypesConverter.toAddress(params.recipient) : params.from?.address ?? ZERO_ADDRESS; + const { tokens } = await this.getTokens(); + + const tx = await this.vault.joinPool({ + poolAddress: this.address, + poolId: this.poolId, + recipient: to, + currentBalances, + tokens, + lastChangeBlock: params.lastChangeBlock ?? 0, + protocolFeePercentage: params.protocolFeePercentage ?? 0, + data: params.data ?? '0x', + from: params.from, + }); + + const receipt = await tx.wait(); + const { deltas, protocolFeeAmounts } = expectEvent.inReceipt(receipt, 'PoolBalanceChanged').args; + return { amountsIn: deltas, dueProtocolFeeAmounts: protocolFeeAmounts, receipt }; + } + + async queryExit(params: JoinExitRangePool): Promise { + const fn = this.instance.queryExit; + return (await this._executeQuery(params, fn)) as ExitQueryResult; + } + + async exit(params: JoinExitRangePool): Promise { + const currentBalances = params.currentBalances || (await this.getBalances()); + const to = params.recipient ? TypesConverter.toAddress(params.recipient) : params.from?.address ?? ZERO_ADDRESS; + const { tokens } = await this.getTokens(); + + const tx = await this.vault.exitPool({ + poolAddress: this.address, + poolId: this.poolId, + recipient: to, + currentBalances, + tokens, + lastChangeBlock: params.lastChangeBlock ?? 0, + protocolFeePercentage: params.protocolFeePercentage ?? 0, + data: params.data ?? '0x', + from: params.from, + }); + + const receipt = await tx.wait(); + const { deltas, protocolFeeAmounts } = expectEvent.inReceipt(receipt, 'PoolBalanceChanged').args; + return { amountsOut: deltas.map((x: BigNumber) => x.mul(-1)), dueProtocolFeeAmounts: protocolFeeAmounts, receipt }; + } + + private async _executeQuery(params: JoinExitRangePool, fn: ContractFunction): Promise { + const currentBalances = params.currentBalances || (await this.getBalances()); + const to = params.recipient ? TypesConverter.toAddress(params.recipient) : params.from?.address ?? ZERO_ADDRESS; + + return fn( + this.poolId, + params.from?.address || ZERO_ADDRESS, + to, + currentBalances, + params.lastChangeBlock ?? 0, + params.protocolFeePercentage ?? 0, + params.data ?? '0x' + ); + } + + private async _buildSwapParams(kind: number, params: SwapRangePool): Promise { + const currentBalances = await this.getBalances(); + const { tokens } = await this.vault.getPoolTokens(this.poolId); + const tokenIn = typeof params.in === 'number' ? tokens[params.in] : params.in.address; + const tokenOut = typeof params.out === 'number' ? tokens[params.out] : params.out.address; + return { + kind, + poolAddress: this.address, + poolId: this.poolId, + from: params.from, + to: params.recipient ?? ZERO_ADDRESS, + tokenIn: tokenIn ?? ZERO_ADDRESS, + tokenOut: tokenOut ?? ZERO_ADDRESS, + balances: currentBalances, + indexIn: typeof params.in === 'number' ? params.in : 0, + indexOut: typeof params.out === 'number' ? params.out : 0, + lastChangeBlock: params.lastChangeBlock ?? 0, + data: params.data ?? '0x', + amount: params.amount, + }; + } + + private _buildInitParams(params: InitRangePool): JoinExitRangePool { + const { initialBalances: balances, initialVirtualBalances: virtualBalances } = params; + const amountsIn = Array.isArray(balances) ? balances : Array(this.tokens.length).fill(balances); + const vBalances = Array.isArray(virtualBalances) ? virtualBalances : Array(this.tokens.length).fill(virtualBalances); + + return { + from: params.from, + recipient: params.recipient, + protocolFeePercentage: params.protocolFeePercentage, + data: RangePoolEncoder.joinInit(amountsIn, vBalances), + }; + } + + private _buildJoinGivenInParams(params: JoinGivenInRangePool): JoinExitRangePool { + const { amountsIn: amounts } = params; + const amountsIn = Array.isArray(amounts) ? amounts : Array(this.tokens.length).fill(amounts); + + return { + from: params.from, + recipient: params.recipient, + lastChangeBlock: params.lastChangeBlock, + currentBalances: params.currentBalances, + protocolFeePercentage: params.protocolFeePercentage, + data: WeightedPoolEncoder.joinExactTokensInForBPTOut(amountsIn, params.minimumBptOut ?? 0), + }; + } + + private _buildJoinGivenOutParams(params: JoinGivenOutRangePool): JoinExitRangePool { + return { + from: params.from, + recipient: params.recipient, + lastChangeBlock: params.lastChangeBlock, + currentBalances: params.currentBalances, + protocolFeePercentage: params.protocolFeePercentage, + data: WeightedPoolEncoder.joinTokenInForExactBPTOut(params.bptOut, this.tokens.indexOf(params.token)), + }; + } + + private _buildJoinAllGivenOutParams(params: JoinAllGivenOutRangePool): JoinExitRangePool { + return { + from: params.from, + recipient: params.recipient, + lastChangeBlock: params.lastChangeBlock, + currentBalances: params.currentBalances, + protocolFeePercentage: params.protocolFeePercentage, + data: WeightedPoolEncoder.joinAllTokensInForExactBPTOut(params.bptOut), + }; + } + + private _buildExitGivenOutParams(params: ExitGivenOutRangePool): JoinExitRangePool { + const { amountsOut: amounts } = params; + const amountsOut = Array.isArray(amounts) ? amounts : Array(this.tokens.length).fill(amounts); + return { + from: params.from, + recipient: params.recipient, + lastChangeBlock: params.lastChangeBlock, + currentBalances: params.currentBalances, + protocolFeePercentage: params.protocolFeePercentage, + data: WeightedPoolEncoder.exitBPTInForExactTokensOut(amountsOut, params.maximumBptIn ?? MAX_UINT256), + }; + } + + private _buildSingleExitGivenInParams(params: SingleExitGivenInRangePool): JoinExitRangePool { + return { + from: params.from, + recipient: params.recipient, + lastChangeBlock: params.lastChangeBlock, + currentBalances: params.currentBalances, + protocolFeePercentage: params.protocolFeePercentage, + data: WeightedPoolEncoder.exitExactBPTInForOneTokenOut(params.bptIn, this.tokens.indexOf(params.token)), + }; + } + + private _buildMultiExitGivenInParams(params: MultiExitGivenInRangePool): JoinExitRangePool { + return { + from: params.from, + recipient: params.recipient, + lastChangeBlock: params.lastChangeBlock, + currentBalances: params.currentBalances, + protocolFeePercentage: params.protocolFeePercentage, + data: WeightedPoolEncoder.exitExactBPTInForTokensOut(params.bptIn), + }; + } + + async setJoinExitEnabled(from: SignerWithAddress, joinExitEnabled: boolean): Promise { + const pool = this.instance.connect(from); + return pool.setJoinExitEnabled(joinExitEnabled); + } + + async setSwapEnabled(from: SignerWithAddress, swapEnabled: boolean): Promise { + const pool = this.instance.connect(from); + return pool.setSwapEnabled(swapEnabled); + } + + async setSwapFeePercentage(from: SignerWithAddress, swapFeePercentage: BigNumberish): Promise { + const pool = this.instance.connect(from); + return pool.setSwapFeePercentage(swapFeePercentage); + } + + async updateWeightsGradually( + from: SignerWithAddress, + startTime: BigNumberish, + endTime: BigNumberish, + endWeights: BigNumberish[] + ): Promise { + const pool = this.instance.connect(from); + + return await pool.updateWeightsGradually(startTime, endTime, endWeights); + } + + async getGradualWeightUpdateParams(from?: SignerWithAddress): Promise { + const pool = from ? this.instance.connect(from) : this.instance; + return await pool.getGradualWeightUpdateParams(); + } +} diff --git a/pkg/pool-range/test/helpers/RangePool.ts b/pkg/pool-range/test/helpers/RangePool.ts new file mode 100644 index 000000000..020ed1b8d --- /dev/null +++ b/pkg/pool-range/test/helpers/RangePool.ts @@ -0,0 +1,111 @@ +import { Contract } from 'ethers'; +import TypesConverter from '@balancer-labs/v2-helpers/src/models/types/TypesConverter'; +import RangeTypesConverter from './TypesConverter'; +import VaultDeployer from '@balancer-labs/v2-helpers/src/models/vault/VaultDeployer'; +import BaseRangePool from './BaseRangePool'; +import { + BUFFER_PERIOD_DURATION, + NAME, + PAUSE_WINDOW_DURATION, + SYMBOL, +} from '@balancer-labs/v2-helpers/src/models/pools/base/BasePool'; +import { RawRangePoolDeployment, RangePoolDeployment } from './types'; +import Vault from '@balancer-labs/v2-helpers/src/models/vault/Vault'; +import { deploy, deployedAt } from './contract'; +import * as expectEvent from '@balancer-labs/v2-helpers/src/test/expectEvent'; +import { Account } from '@balancer-labs/v2-helpers/src/models/types/types'; +import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; +import { BigNumberish } from '@balancer-labs/v2-helpers/src/numbers'; +import { randomBytes } from 'ethers/lib/utils'; + +export default class RangePool extends BaseRangePool { + rateProviders: Account[]; + assetManagers: string[]; + + constructor( + instance: Contract, + poolId: string, + vault: Vault, + tokens: TokenList, + weights: BigNumberish[], + rateProviders: Account[], + assetManagers: string[], + swapFeePercentage: BigNumberish, + owner?: Account + ) { + super(instance, poolId, vault, tokens, weights, swapFeePercentage, owner); + + this.rateProviders = rateProviders; + this.assetManagers = assetManagers; + } + + static async create(params: RawRangePoolDeployment = {}): Promise { + const vault = params?.vault ?? (await VaultDeployer.deploy(TypesConverter.toRawVaultDeployment(params))); + const deployment = RangeTypesConverter.toRangePoolDeployment(params); + const pool = await (params.fromFactory ? this._deployFromFactory : this._deployStandalone)(deployment, vault); + const poolId = await pool.getPoolId(); + + const { tokens, weights, rateProviders, assetManagers, swapFeePercentage, owner } = deployment; + + return new RangePool( + pool, + poolId, + vault, + tokens, + weights, + rateProviders, + assetManagers, + swapFeePercentage, + owner + ); + } + + static async _deployStandalone(params: RangePoolDeployment, vault: Vault): Promise { + const { from } = params; + + return deploy('RangePool', { + args: [ + { + name: NAME, + symbol: SYMBOL, + tokens: params.tokens.addresses, + normalizedWeights: params.weights, + rateProviders: TypesConverter.toAddresses(params.rateProviders), + assetManagers: params.assetManagers, + swapFeePercentage: params.swapFeePercentage, + }, + vault.address, + vault.protocolFeesProvider.address, + params.pauseWindowDuration, + params.bufferPeriodDuration, + params.owner, + ], + from, + }); + } + + static async _deployFromFactory(params: RangePoolDeployment, vault: Vault): Promise { + // Note that we only support asset managers with the standalone deploy method. + + const { tokens, weights, rateProviders, swapFeePercentage, owner, from } = params; + + const factory = await deploy('RangePoolFactory', { + args: [vault.address, vault.getFeesProvider().address, PAUSE_WINDOW_DURATION, BUFFER_PERIOD_DURATION], + from, + }); + + const tx = await factory.create( + NAME, + SYMBOL, + tokens.addresses, + weights, + rateProviders, + swapFeePercentage, + owner, + randomBytes(32) + ); + const receipt = await tx.wait(); + const event = expectEvent.inReceipt(receipt, 'PoolCreated'); + return deployedAt('RangePool', event.args.pool); + } +} diff --git a/pkg/pool-range/test/helpers/TypesConverter.ts b/pkg/pool-range/test/helpers/TypesConverter.ts new file mode 100644 index 000000000..f15efdcb4 --- /dev/null +++ b/pkg/pool-range/test/helpers/TypesConverter.ts @@ -0,0 +1,63 @@ +import { toNormalizedWeights } from '@balancer-labs/balancer-js'; + +import { bn, fp } from '@balancer-labs/v2-helpers/src/numbers'; +import { MONTH } from '@balancer-labs/v2-helpers/src/time'; +import { ZERO_ADDRESS } from '@balancer-labs/v2-helpers/src/constants'; +import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; +import { Account } from '@balancer-labs/v2-helpers/src/models/types/types'; +import { + RawRangePoolDeployment, + RangePoolDeployment, +} from './types'; + +const DEFAULT_PAUSE_WINDOW_DURATION = 3 * MONTH; +const DEFAULT_BUFFER_PERIOD_DURATION = MONTH; + +export function computeDecimalsFromIndex(i: number): number { + // Produces repeating series (0..18) + return i % 19; +} + +export default { + toRangePoolDeployment(params: RawRangePoolDeployment): RangePoolDeployment { + let { + tokens, + weights, + rateProviders, + assetManagers, + swapFeePercentage, + pauseWindowDuration, + bufferPeriodDuration, + } = params; + if (!params.owner) params.owner = ZERO_ADDRESS; + if (!tokens) tokens = new TokenList(); + if (!weights) weights = Array(tokens.length).fill(fp(1)); + weights = toNormalizedWeights(weights.map(bn)); + if (!swapFeePercentage) swapFeePercentage = bn(1e16); + if (!pauseWindowDuration) pauseWindowDuration = DEFAULT_PAUSE_WINDOW_DURATION; + if (!bufferPeriodDuration) bufferPeriodDuration = DEFAULT_BUFFER_PERIOD_DURATION; + if (!rateProviders) rateProviders = Array(tokens.length).fill(ZERO_ADDRESS); + if (!assetManagers) assetManagers = Array(tokens.length).fill(ZERO_ADDRESS); + + return { + tokens, + weights, + rateProviders, + assetManagers, + swapFeePercentage, + pauseWindowDuration, + bufferPeriodDuration, + owner: this.toAddress(params.owner), + from: params.from, + }; + }, + + toAddresses(to: Account[]): string[] { + return to.map(this.toAddress); + }, + + toAddress(to?: Account): string { + if (!to) return ZERO_ADDRESS; + return typeof to === 'string' ? to : to.address; + }, +}; diff --git a/pkg/pool-range/test/helpers/contract.ts b/pkg/pool-range/test/helpers/contract.ts new file mode 100644 index 000000000..00481328a --- /dev/null +++ b/pkg/pool-range/test/helpers/contract.ts @@ -0,0 +1,51 @@ +import { ethers } from 'hardhat'; +import { Contract } from 'ethers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; + +import { Artifacts } from 'hardhat/internal/artifacts'; +import { Artifact } from 'hardhat/types'; +import path from 'path'; +import { Dictionary } from 'lodash'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export type ContractDeploymentParams = { + from?: SignerWithAddress; + args?: Array; + libraries?: Dictionary; +}; + +// Deploys a contract, with optional `from` address and arguments. +// Local contracts are deployed by simply passing the contract name, contracts from other packages must be prefixed by +// the package name, without the @balancer-labs scope. Note that the full path is never required. +// +// For example, to deploy Vault.sol from the package that holds its artifacts, use `deploy('Vault')`. To deploy it from +// a different package, use `deploy('v2-vault/Vault')`, assuming the Vault's package is @balancer-labs/v2-vault. +export async function deploy( + contract: string, + { from, args, libraries }: ContractDeploymentParams = {} +): Promise { + if (!args) args = []; + if (!from) from = (await ethers.getSigners())[0]; + + const artifact = getArtifact(contract); + + const factory = await ethers.getContractFactoryFromArtifact(artifact, { signer: from, libraries }); + const instance = await factory.deploy(...args); + + return instance.deployed(); +} + +// Creates a contract object for a contract deployed at a known address. The `contract` argument follows the same rules +// as in `deploy`. +export async function deployedAt(contract: string, address: string): Promise { + const artifact = getArtifact(contract); + return ethers.getContractAt(artifact.abi, address); +} + +export function getArtifact(contract: string): Artifact { + let artifactsPath: string = path.resolve('./artifacts'); + + const artifacts = new Artifacts(artifactsPath); + return artifacts.readArtifactSync(contract.split('/').slice(-1)[0]); +} diff --git a/pkg/pool-range/test/helpers/encoder.ts b/pkg/pool-range/test/helpers/encoder.ts new file mode 100644 index 000000000..03ad8c003 --- /dev/null +++ b/pkg/pool-range/test/helpers/encoder.ts @@ -0,0 +1,33 @@ +import { defaultAbiCoder } from '@ethersproject/abi'; +import { BigNumberish } from '@ethersproject/bignumber'; + +export enum RangePoolJoinKind { + INIT = 0, + EXACT_TOKENS_IN_FOR_BPT_OUT, + TOKEN_IN_FOR_EXACT_BPT_OUT, + ALL_TOKENS_IN_FOR_EXACT_BPT_OUT, + ADD_TOKEN, +} + +export enum RangePoolExitKind { + EXACT_BPT_IN_FOR_ONE_TOKEN_OUT = 0, + EXACT_BPT_IN_FOR_TOKENS_OUT, + BPT_IN_FOR_EXACT_TOKENS_OUT, + REMOVE_TOKEN, +} + +export class RangePoolEncoder { + /** + * Cannot be constructed. + */ + private constructor() { + // eslint-disable-next-line @typescript-eslint/no-empty-function + } + + /** + * Encodes the userData parameter for providing the initial liquidity to a WeightedPool + * @param initialBalances - the amounts of tokens to send to the pool to form the initial balances + */ + static joinInit = (amountsIn: BigNumberish[], vBalances: BigNumberish[]): string => + defaultAbiCoder.encode(['uint256', 'uint256[]', 'uint256[]'], [RangePoolJoinKind.INIT, amountsIn, vBalances]); +} diff --git a/pkg/pool-range/test/helpers/types.ts b/pkg/pool-range/test/helpers/types.ts new file mode 100644 index 000000000..1301e700e --- /dev/null +++ b/pkg/pool-range/test/helpers/types.ts @@ -0,0 +1,171 @@ +import { BigNumber, ContractReceipt } from 'ethers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; + +import { BigNumberish } from '@balancer-labs/v2-helpers/src//numbers'; + +import Token from '@balancer-labs/v2-helpers/src/models/tokens/Token'; +import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; +import { Account, NAry } from '@balancer-labs/v2-helpers/src/models/types/types'; +import Vault from '@balancer-labs/v2-helpers/src/models/vault/Vault'; + +export type RawRangePoolDeployment = { + tokens?: TokenList; + weights?: BigNumberish[]; + virtualBalances?: BigNumberish[]; + rateProviders?: Account[]; + assetManagers?: string[]; + swapFeePercentage?: BigNumberish; + pauseWindowDuration?: BigNumberish; + bufferPeriodDuration?: BigNumberish; + owner?: Account; + admin?: SignerWithAddress; + from?: SignerWithAddress; + vault?: Vault; + fromFactory?: boolean; +}; + +export type RangePoolDeployment = { + tokens: TokenList; + weights: BigNumberish[]; + rateProviders: Account[]; + assetManagers: string[]; + swapFeePercentage: BigNumberish; + pauseWindowDuration: BigNumberish; + bufferPeriodDuration: BigNumberish; + owner: Account; + admin?: SignerWithAddress; + from?: SignerWithAddress; +}; + +export type SwapRangePool = { + in: number | Token; + out: number | Token; + amount: BigNumberish; + balances?: BigNumberish[]; + recipient?: Account; + from?: SignerWithAddress; + lastChangeBlock?: BigNumberish; + data?: string; +}; + +export type JoinExitRangePool = { + recipient?: Account; + currentBalances?: BigNumberish[]; + lastChangeBlock?: BigNumberish; + protocolFeePercentage?: BigNumberish; + data?: string; + from?: SignerWithAddress; +}; + +export type InitRangePool = { + initialBalances: NAry; + initialVirtualBalances: NAry; + from?: SignerWithAddress; + recipient?: Account; + protocolFeePercentage?: BigNumberish; +}; + +export type JoinGivenInRangePool = { + amountsIn: NAry; + minimumBptOut?: BigNumberish; + from?: SignerWithAddress; + recipient?: Account; + lastChangeBlock?: BigNumberish; + currentBalances?: BigNumberish[]; + protocolFeePercentage?: BigNumberish; +}; + +export type JoinGivenOutRangePool = { + token: number | Token; + bptOut: BigNumberish; + from?: SignerWithAddress; + recipient?: Account; + lastChangeBlock?: BigNumberish; + currentBalances?: BigNumberish[]; + protocolFeePercentage?: BigNumberish; +}; + +export type JoinAllGivenOutRangePool = { + bptOut: BigNumberish; + from?: SignerWithAddress; + recipient?: Account; + lastChangeBlock?: BigNumberish; + currentBalances?: BigNumberish[]; + protocolFeePercentage?: BigNumberish; +}; + +export type ExitGivenOutRangePool = { + amountsOut: NAry; + maximumBptIn?: BigNumberish; + recipient?: Account; + from?: SignerWithAddress; + lastChangeBlock?: BigNumberish; + currentBalances?: BigNumberish[]; + protocolFeePercentage?: BigNumberish; +}; + +export type SingleExitGivenInRangePool = { + bptIn: BigNumberish; + token: number | Token; + recipient?: Account; + from?: SignerWithAddress; + lastChangeBlock?: BigNumberish; + currentBalances?: BigNumberish[]; + protocolFeePercentage?: BigNumberish; +}; + +export type MultiExitGivenInRangePool = { + bptIn: BigNumberish; + recipient?: Account; + from?: SignerWithAddress; + lastChangeBlock?: BigNumberish; + currentBalances?: BigNumberish[]; + protocolFeePercentage?: BigNumberish; +}; + +export type JoinResult = { + amountsIn: BigNumber[]; + dueProtocolFeeAmounts: BigNumber[]; + receipt: ContractReceipt; +}; + +export type ExitResult = { + amountsOut: BigNumber[]; + dueProtocolFeeAmounts: BigNumber[]; + receipt: ContractReceipt; +}; + +export type SwapResult = { + amount: BigNumber; + receipt: ContractReceipt; +}; + +export type JoinQueryResult = { + bptOut: BigNumber; + amountsIn: BigNumber[]; +}; + +export type ExitQueryResult = { + bptIn: BigNumber; + amountsOut: BigNumber[]; +}; + +export type VoidResult = { + receipt: ContractReceipt; +}; + +export type PoolQueryResult = JoinQueryResult | ExitQueryResult; + +export type GradualWeightUpdateParams = { + startTime: BigNumber; + endTime: BigNumber; + startWeights: BigNumber[]; + endWeights: BigNumber[]; +}; + +export type GradualSwapFeeUpdateParams = { + startTime: BigNumber; + endTime: BigNumber; + startSwapFeePercentage: BigNumber; + endSwapFeePercentage: BigNumber; +}; diff --git a/pkg/pool-range/tsconfig.json b/pkg/pool-range/tsconfig.json new file mode 100644 index 000000000..b4e69ae1f --- /dev/null +++ b/pkg/pool-range/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + } +} diff --git a/pkg/pool-stable/test/ComposableStablePoolProtocolFees.test.ts b/pkg/pool-stable/test/ComposableStablePoolProtocolFees.test.ts index 6cc173203..14c7bd0d0 100644 --- a/pkg/pool-stable/test/ComposableStablePoolProtocolFees.test.ts +++ b/pkg/pool-stable/test/ComposableStablePoolProtocolFees.test.ts @@ -65,13 +65,6 @@ describe('ComposableStablePoolProtocolFees', () => { await vault.authorizer .connect(admin) .grantPermission(actionId(feesCollector, 'setSwapFeePercentage'), feesProvider.address, feesCollector.address); - await vault.authorizer - .connect(admin) - .grantPermission( - actionId(feesCollector, 'setFlashLoanFeePercentage'), - feesProvider.address, - feesCollector.address - ); }); context('for a 2 token pool', () => { diff --git a/pkg/pool-utils/contracts/external-fees/ProtocolFeeCache.sol b/pkg/pool-utils/contracts/external-fees/ProtocolFeeCache.sol index 351dd9594..8e771201e 100644 --- a/pkg/pool-utils/contracts/external-fees/ProtocolFeeCache.sol +++ b/pkg/pool-utils/contracts/external-fees/ProtocolFeeCache.sol @@ -67,7 +67,7 @@ abstract contract ProtocolFeeCache is IProtocolFeeCache, RecoveryMode { * This is because some Pools may have different protocol fee values for the same type of underlying operation: * for example, Stable Pools might have a different swap protocol fee than Weighted Pools. * This module does not check at all that the chosen fee types have any sort of relation with the operation they're - * assigned to: it is possible to e.g. set a Pool's swap protocol fee to equal the flash loan protocol fee. + * assigned to: it is possible to e.g. set a Pool's swap protocol fee. */ struct ProviderFeeIDs { uint256 swap; diff --git a/pkg/pool-utils/test/BasePool.test.ts b/pkg/pool-utils/test/BasePool.test.ts index 59e154f0e..a37b508f3 100644 --- a/pkg/pool-utils/test/BasePool.test.ts +++ b/pkg/pool-utils/test/BasePool.test.ts @@ -8,7 +8,7 @@ import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; import { advanceTime, DAY, MONTH } from '@balancer-labs/v2-helpers/src/time'; import { actionId } from '@balancer-labs/v2-helpers/src/models/misc/actions'; import { deploy, deployedAt } from '@balancer-labs/v2-helpers/src/contract'; -import { JoinPoolRequest, ExitPoolRequest, PoolSpecialization, WeightedPoolEncoder } from '@balancer-labs/balancer-js'; +import { JoinPoolRequest, ExitPoolRequest, PoolSpecialization, RangePoolEncoder } from '@balancer-labs/balancer-js'; import { BigNumberish, fp } from '@balancer-labs/v2-helpers/src/numbers'; import { ANY_ADDRESS, DELEGATE_OWNER, ZERO_ADDRESS } from '@balancer-labs/v2-helpers/src/constants'; import { Account } from '@balancer-labs/v2-helpers/src/models/types/types'; @@ -306,12 +306,13 @@ describe('BasePool', function () { sharedBeforeEach('deploy and initialize pool', async () => { initialBalances = Array(tokens.length).fill(fp(1000)); + initialVirtualBalances = Array(tokens.length).fill(fp(2000)); poolId = await pool.getPoolId(); const request: JoinPoolRequest = { assets: tokens.addresses, maxAmountsIn: initialBalances, - userData: WeightedPoolEncoder.joinInit(initialBalances), + userData: RangePoolEncoder.joinInit(initialBalances, initialVirtualBalances), fromInternalBalance: false, }; diff --git a/pkg/pool-utils/test/ProtocolFeeCache.test.ts b/pkg/pool-utils/test/ProtocolFeeCache.test.ts index 78c246dde..fee73c363 100644 --- a/pkg/pool-utils/test/ProtocolFeeCache.test.ts +++ b/pkg/pool-utils/test/ProtocolFeeCache.test.ts @@ -43,13 +43,6 @@ describe('ProtocolFeeCache', () => { vault.protocolFeesProvider.address ); - await vault.authorizer - .connect(admin) - .grantPermission( - actionId(feesCollector, 'setFlashLoanFeePercentage'), - vault.protocolFeesProvider.address, - feesCollector.address - ); await vault.authorizer .connect(admin) .grantPermission( @@ -79,7 +72,7 @@ describe('ProtocolFeeCache', () => { }); context('using custom fee types', () => { - itTestsProtocolFeePercentages({ swap: ProtocolFee.FLASH_LOAN, yield: ProtocolFee.SWAP, aum: ProtocolFee.YIELD }); + itTestsProtocolFeePercentages({ swap: ProtocolFee.SWAP, yield: ProtocolFee.SWAP, aum: ProtocolFee.YIELD }); }); }); diff --git a/pkg/pool-weighted/test/PostJoinExitProtocolFees.test.ts b/pkg/pool-weighted/test/PostJoinExitProtocolFees.test.ts index 3561d0e00..198e9e196 100644 --- a/pkg/pool-weighted/test/PostJoinExitProtocolFees.test.ts +++ b/pkg/pool-weighted/test/PostJoinExitProtocolFees.test.ts @@ -50,13 +50,6 @@ describe('PostJoinExitProtocolFees', () => { .connect(admin) .grantPermission(actionId(feesProvider, 'setFeeTypePercentage'), admin.address, feesProvider.address); - await vault.authorizer - .connect(admin) - .grantPermission( - actionId(feesCollector, 'setFlashLoanFeePercentage'), - feesProvider.address, - feesCollector.address - ); await vault.authorizer .connect(admin) .grantPermission(actionId(feesCollector, 'setSwapFeePercentage'), feesProvider.address, feesCollector.address); diff --git a/pkg/standalone-utils/contracts/ProtocolFeePercentagesProvider.sol b/pkg/standalone-utils/contracts/ProtocolFeePercentagesProvider.sol index c1a1599fc..082e2b7a3 100644 --- a/pkg/standalone-utils/contracts/ProtocolFeePercentagesProvider.sol +++ b/pkg/standalone-utils/contracts/ProtocolFeePercentagesProvider.sol @@ -42,7 +42,6 @@ contract ProtocolFeePercentagesProvider is IProtocolFeePercentagesProvider, Sing // These are copied from ProtocolFeesCollector uint256 private constant _MAX_PROTOCOL_SWAP_FEE_PERCENTAGE = 50e16; // 50% - uint256 private constant _MAX_PROTOCOL_FLASH_LOAN_FEE_PERCENTAGE = 1e16; // 1% constructor( IVault vault, @@ -58,14 +57,11 @@ contract ProtocolFeePercentagesProvider is IProtocolFeePercentagesProvider, Sing _registerFeeType(ProtocolFeeType.YIELD, "Yield", maxYieldValue, 0); _registerFeeType(ProtocolFeeType.AUM, "Assets Under Management", maxAUMValue, 0); - // Swap and Flash loan types are special as their storage is actually located in the ProtocolFeesCollector. We + // Swap type are special as their storage is actually located in the ProtocolFeesCollector. We // therefore simply mark them as registered, but ignore maximum and initial values. Not calling _registerFeeType // also means that ProtocolFeeTypeRegistered nor ProtocolFeePercentageChanged events will be emitted for these. _feeTypeData[ProtocolFeeType.SWAP].registered = true; _feeTypeData[ProtocolFeeType.SWAP].name = "Swap"; - - _feeTypeData[ProtocolFeeType.FLASH_LOAN].registered = true; - _feeTypeData[ProtocolFeeType.FLASH_LOAN].name = "Flash Loan"; } modifier withValidFeeType(uint256 feeType) { @@ -127,8 +123,6 @@ contract ProtocolFeePercentagesProvider is IProtocolFeePercentagesProvider, Sing if (feeType == ProtocolFeeType.SWAP) { _protocolFeesCollector.setSwapFeePercentage(newValue); - } else if (feeType == ProtocolFeeType.FLASH_LOAN) { - _protocolFeesCollector.setFlashLoanFeePercentage(newValue); } else { _feeTypeData[feeType].value = newValue.toUint64(); } @@ -139,8 +133,6 @@ contract ProtocolFeePercentagesProvider is IProtocolFeePercentagesProvider, Sing function getFeeTypePercentage(uint256 feeType) external view override withValidFeeType(feeType) returns (uint256) { if (feeType == ProtocolFeeType.SWAP) { return _protocolFeesCollector.getSwapFeePercentage(); - } else if (feeType == ProtocolFeeType.FLASH_LOAN) { - return _protocolFeesCollector.getFlashLoanFeePercentage(); } else { return _feeTypeData[feeType].value; } @@ -155,8 +147,6 @@ contract ProtocolFeePercentagesProvider is IProtocolFeePercentagesProvider, Sing { if (feeType == ProtocolFeeType.SWAP) { return _MAX_PROTOCOL_SWAP_FEE_PERCENTAGE; - } else if (feeType == ProtocolFeeType.FLASH_LOAN) { - return _MAX_PROTOCOL_FLASH_LOAN_FEE_PERCENTAGE; } else { return _feeTypeData[feeType].maximum; } diff --git a/pkg/standalone-utils/contracts/relayer/special/DoubleEntrypointFixRelayer.sol b/pkg/standalone-utils/contracts/relayer/special/DoubleEntrypointFixRelayer.sol deleted file mode 100644 index 62a79254d..000000000 --- a/pkg/standalone-utils/contracts/relayer/special/DoubleEntrypointFixRelayer.sol +++ /dev/null @@ -1,181 +0,0 @@ -// 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/pool-stable/StablePoolUserData.sol"; -import "@balancer-labs/v2-interfaces/contracts/pool-weighted/WeightedPoolUserData.sol"; -import "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol"; -import "@balancer-labs/v2-interfaces/contracts/vault/IFlashLoanRecipient.sol"; - -import "@balancer-labs/v2-solidity-utils/contracts/helpers/ERC20Helpers.sol"; -import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/SafeERC20.sol"; - -/** - * @title DoubleEntrypointFixRelayer - * @notice This contract performs mitigations to safeguard funds affected by double-entrypoint tokens (mostly Synthetix - * tokens). It doesn't use the standard relayer architecture to simplify the code. - */ -contract DoubleEntrypointFixRelayer is IFlashLoanRecipient { - using SafeERC20 for IERC20; - - // solhint-disable const-name-snakecase - IERC20 public constant BTC_STABLE_POOL_ADDRESS = IERC20(0xFeadd389a5c427952D8fdb8057D6C8ba1156cC56); - bytes32 public constant BTC_STABLE_POOL_ID = 0xfeadd389a5c427952d8fdb8057d6c8ba1156cc56000000000000000000000066; - - // solhint-disable const-name-snakecase - IERC20 public constant wBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); - IERC20 public constant renBTC = IERC20(0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D); - IERC20 public constant sBTC = IERC20(0xfE18be6b3Bd88A2D2A7f928d00292E7a9963CfC6); - IERC20 public constant sBTC_IMPLEMENTATION = IERC20(0x18FcC34bdEaaF9E3b69D2500343527c0c995b1d6); - - IERC20 public constant SNX_WEIGHTED_POOL_ADDRESS = IERC20(0x072f14B85ADd63488DDaD88f855Fda4A99d6aC9B); - bytes32 public constant SNX_WEIGHTED_POOL_ID = 0x072f14b85add63488ddad88f855fda4a99d6ac9b000200000000000000000027; - IERC20 public constant SNX = IERC20(0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F); - IERC20 public constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - IERC20 public constant SNX_IMPLEMENTATION = IERC20(0x639032d3900875a4cf4960aD6b9ee441657aA93C); - // solhint-enable const-name-snakecase - - // This was removed from the StablePoolEncoder along with StablePool. - uint256 private constant _STABLE_POOL_EXIT_KIND_EXACT_BPT_IN_FOR_TOKENS_OUT = 1; - - IVault private immutable _vault; - IProtocolFeesCollector private immutable _protocolFeeCollector; - - constructor(IVault vault) { - _vault = vault; - _protocolFeeCollector = vault.getProtocolFeesCollector(); - } - - function getVault() public view returns (IVault) { - return _vault; - } - - /** - * @notice Fully exit the BTC Stable Pool into its three components (wBTC, renBTC and sBTC), with no price impact - * nor swap fees. This relayer must have been previously approved by the caller, and proper permissions granted by - * Balancer Governance. - */ - function exitBTCStablePool() external { - IERC20[] memory tokens = new IERC20[](3); - tokens[0] = wBTC; - tokens[1] = renBTC; - tokens[2] = sBTC; - uint256 bptAmountIn = BTC_STABLE_POOL_ADDRESS.balanceOf(msg.sender); - - // Pull sBTC out from the Protocol Fee Collector and send it to the Vault ready for the exit. Computing the - // exact amount of sBTC required is a complicated task, as it involves due protocol fees, so we simply send all - // of it. - _withdrawFromProtocolFeeCollector(sBTC, sBTC.balanceOf(address(_protocolFeeCollector))); - - // Perform the exit. - bytes memory userData = abi.encode(_STABLE_POOL_EXIT_KIND_EXACT_BPT_IN_FOR_TOKENS_OUT, bptAmountIn); - IVault.ExitPoolRequest memory request = IVault.ExitPoolRequest( - _asIAsset(tokens), - new uint256[](tokens.length), - userData, - false - ); - getVault().exitPool(BTC_STABLE_POOL_ID, msg.sender, msg.sender, request); - - // Sweep any remaining sBTC back into the Protocol Fee Collector. - IERC20[] memory sBTCEntrypoints = new IERC20[](2); - sBTCEntrypoints[0] = sBTC_IMPLEMENTATION; - sBTCEntrypoints[1] = IERC20(address(sBTC)); - sweepDoubleEntrypointToken(sBTCEntrypoints); - } - - /** - * @notice Fully exit the SNX Weighted Pool into its two components (SNX and WETH), with no price impact nor swap - * fees. This relayer must have been previously approved by the caller, and proper permissions granted by - * Balancer Governance. - */ - function exitSNXWeightedPool() external { - IERC20[] memory tokens = new IERC20[](2); - tokens[0] = SNX; - tokens[1] = WETH; - uint256 bptAmountIn = SNX_WEIGHTED_POOL_ADDRESS.balanceOf(msg.sender); - - // Pull SNX out from the Protocol Fee Collector and send it to the Vault ready for the exit. Computing the - // exact amount of SNX required is a complicated task, as it involves due protocol fees, so we simply send all - // of it. - _withdrawFromProtocolFeeCollector(SNX, SNX.balanceOf(address(_protocolFeeCollector))); - - // Perform the exit. - bytes memory userData = abi.encode(WeightedPoolUserData.ExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT, bptAmountIn); - IVault.ExitPoolRequest memory request = IVault.ExitPoolRequest( - _asIAsset(tokens), - new uint256[](tokens.length), - userData, - false - ); - getVault().exitPool(SNX_WEIGHTED_POOL_ID, msg.sender, msg.sender, request); - - // Sweep any remaining SNX back into the Protocol Fee Collector. - IERC20[] memory snxEntrypoints = new IERC20[](2); - snxEntrypoints[0] = SNX_IMPLEMENTATION; - snxEntrypoints[1] = IERC20(address(SNX)); - sweepDoubleEntrypointToken(snxEntrypoints); - } - - function _withdrawFromProtocolFeeCollector(IERC20 token, uint256 amount) internal { - IERC20[] memory tokens = new IERC20[](1); - tokens[0] = token; - uint256[] memory amounts = new uint256[](1); - amounts[0] = amount; - - _protocolFeeCollector.withdrawCollectedFees(tokens, amounts, address(_vault)); - } - - /** - * @notice Sweep all SNX and sBTC from the Vault into the Protocol Fee Collector. - */ - function sweepSNXsBTC() public { - IERC20[] memory snxEntrypoints = new IERC20[](2); - snxEntrypoints[0] = SNX_IMPLEMENTATION; - snxEntrypoints[1] = IERC20(address(SNX)); - - sweepDoubleEntrypointToken(snxEntrypoints); - - IERC20[] memory sBTCEntrypoints = new IERC20[](2); - sBTCEntrypoints[0] = sBTC_IMPLEMENTATION; - sBTCEntrypoints[1] = IERC20(address(sBTC)); - sweepDoubleEntrypointToken(sBTCEntrypoints); - } - - /** - * @notice Sweep a double-entrypoint token into the Protocol Fee Collector by passing all entrypoints of a given - * token. - */ - function sweepDoubleEntrypointToken(IERC20[] memory tokens) public { - uint256[] memory amounts = new uint256[](tokens.length); - amounts[0] = tokens[0].balanceOf(address(_vault)); - _vault.flashLoan(this, tokens, amounts, "0x"); - } - - /** - * @dev Flash loan callback. Assumes that it receives a flashloan of multiple assets (all entrypoints of a Synthetix - * synth). We only need to repay the first loan as that will automatically all other loans. - */ - function receiveFlashLoan( - IERC20[] memory tokens, - uint256[] memory amounts, - uint256[] memory, - bytes memory - ) external override { - _require(msg.sender == address(_vault), Errors.CALLER_NOT_VAULT); - tokens[0].safeTransfer(address(_vault), amounts[0]); - } -} diff --git a/pkg/standalone-utils/test/ProtocolFeePercentagesProvider.test.ts b/pkg/standalone-utils/test/ProtocolFeePercentagesProvider.test.ts index aca2f07f0..ecb0f04fb 100644 --- a/pkg/standalone-utils/test/ProtocolFeePercentagesProvider.test.ts +++ b/pkg/standalone-utils/test/ProtocolFeePercentagesProvider.test.ts @@ -17,7 +17,6 @@ describe('ProtocolFeePercentagesProvider', function () { enum FeeType { Swap = 0, - FlashLoan = 1, Yield = 2, AUM = 3, } @@ -26,7 +25,6 @@ describe('ProtocolFeePercentagesProvider', function () { // Note that these two values are not passed - they're hardcoded into the ProtocolFeesCollector const MAX_SWAP_VALUE = fp(0.5); - const MAX_FLASH_LOAN_VALUE = fp(0.01); const MAX_AUM_VALUE = fp(0.2); const MAX_YIELD_VALUE = fp(0.8); @@ -128,8 +126,6 @@ describe('ProtocolFeePercentagesProvider', function () { context('native fee types', () => { itReturnsNameAndMaximum(FeeType.Swap, 'Swap', MAX_SWAP_VALUE); - - itReturnsNameAndMaximum(FeeType.FlashLoan, 'Flash Loan', MAX_FLASH_LOAN_VALUE); }); context('custom fee types', () => { @@ -175,8 +171,6 @@ describe('ProtocolFeePercentagesProvider', function () { context('native fee types', () => { itValidatesFeePercentagesCorrectly(FeeType.Swap, MAX_SWAP_VALUE); - - itValidatesFeePercentagesCorrectly(FeeType.FlashLoan, MAX_FLASH_LOAN_VALUE); }); context('custom fee types', () => { @@ -217,13 +211,6 @@ describe('ProtocolFeePercentagesProvider', function () { provider.address, feesCollector.address ); - await authorizer - .connect(admin) - .grantPermission( - actionId(feesCollector, 'setFlashLoanFeePercentage'), - provider.address, - feesCollector.address - ); }); function itSetsTheValueCorrectly(feeType: number, value: BigNumber) { @@ -280,8 +267,6 @@ describe('ProtocolFeePercentagesProvider', function () { } itSetsNativeFeeTypeValueCorrectly(FeeType.Swap, MAX_SWAP_VALUE); - - itSetsNativeFeeTypeValueCorrectly(FeeType.FlashLoan, MAX_FLASH_LOAN_VALUE); }); context('custom fee types', () => { @@ -354,9 +339,6 @@ describe('ProtocolFeePercentagesProvider', function () { await authorizer .connect(admin) .grantPermission(actionId(feesCollector, 'setSwapFeePercentage'), other.address, feesCollector.address); - await authorizer - .connect(admin) - .grantPermission(actionId(feesCollector, 'setFlashLoanFeePercentage'), other.address, feesCollector.address); }); describe('swap fee', () => { @@ -365,13 +347,6 @@ describe('ProtocolFeePercentagesProvider', function () { expect(await provider.getFeeTypePercentage(FeeType.Swap)).to.equal(fp(0.13)); }); }); - - describe('flash loan fee', () => { - it('the provider tracks value changes', async () => { - await feesCollector.connect(other).setFlashLoanFeePercentage(fp(0.0013)); - expect(await provider.getFeeTypePercentage(FeeType.FlashLoan)).to.equal(fp(0.0013)); - }); - }); }); describe('register fee type', () => { @@ -385,14 +360,6 @@ describe('ProtocolFeePercentagesProvider', function () { .grantPermission(actionId(provider, 'registerFeeType'), authorized.address, provider.address); }); - context('when the fee type is already in use', () => { - it('reverts', async () => { - await expect(provider.connect(authorized).registerFeeType(FeeType.FlashLoan, '', 0, 0)).to.be.revertedWith( - 'Fee type already registered' - ); - }); - }); - context('when the maximum value is 0%', () => { it('reverts', async () => { await expect(provider.connect(authorized).registerFeeType(NEW_FEE_TYPE, '', 0, 0)).to.be.revertedWith( diff --git a/pkg/vault/contracts/Fees.sol b/pkg/vault/contracts/Fees.sol index 737108e9d..6f8aed1ef 100644 --- a/pkg/vault/contracts/Fees.sol +++ b/pkg/vault/contracts/Fees.sol @@ -50,16 +50,6 @@ abstract contract Fees is IVault { return getProtocolFeesCollector().getSwapFeePercentage(); } - /** - * @dev Returns the protocol fee amount to charge for a flash loan of `amount`. - */ - function _calculateFlashLoanFeeAmount(uint256 amount) internal view returns (uint256) { - // Fixed point multiplication introduces error: we round up, which means in certain scenarios the charged - // percentage can be slightly higher than intended. - uint256 percentage = getProtocolFeesCollector().getFlashLoanFeePercentage(); - return FixedPoint.mulUp(amount, percentage); - } - function _payFeeAmount(IERC20 token, uint256 amount) internal { if (amount > 0) { token.safeTransfer(address(getProtocolFeesCollector()), amount); diff --git a/pkg/vault/contracts/FlashLoans.sol b/pkg/vault/contracts/FlashLoans.sol deleted file mode 100644 index 15d1f9c18..000000000 --- a/pkg/vault/contracts/FlashLoans.sol +++ /dev/null @@ -1,85 +0,0 @@ -// 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 . - -// This flash loan provider was based on the Aave protocol's open source -// implementation and terminology and interfaces are intentionally kept -// similar - -pragma solidity ^0.7.0; -pragma experimental ABIEncoderV2; - -import "@balancer-labs/v2-interfaces/contracts/solidity-utils/helpers/BalancerErrors.sol"; -import "@balancer-labs/v2-interfaces/contracts/solidity-utils/openzeppelin/IERC20.sol"; -import "@balancer-labs/v2-interfaces/contracts/vault/IFlashLoanRecipient.sol"; - -import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/ReentrancyGuard.sol"; -import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/SafeERC20.sol"; - -import "./Fees.sol"; - -/** - * @dev Handles Flash Loans through the Vault. Calls the `receiveFlashLoan` hook on the flash loan recipient - * contract, which implements the `IFlashLoanRecipient` interface. - */ -abstract contract FlashLoans is Fees, ReentrancyGuard, TemporarilyPausable { - using SafeERC20 for IERC20; - - function flashLoan( - IFlashLoanRecipient recipient, - IERC20[] memory tokens, - uint256[] memory amounts, - bytes memory userData - ) external override nonReentrant whenNotPaused { - InputHelpers.ensureInputLengthMatch(tokens.length, amounts.length); - - uint256[] memory feeAmounts = new uint256[](tokens.length); - uint256[] memory preLoanBalances = new uint256[](tokens.length); - - // Used to ensure `tokens` is sorted in ascending order, which ensures token uniqueness. - IERC20 previousToken = IERC20(0); - - for (uint256 i = 0; i < tokens.length; ++i) { - IERC20 token = tokens[i]; - uint256 amount = amounts[i]; - - _require(token > previousToken, token == IERC20(0) ? Errors.ZERO_TOKEN : Errors.UNSORTED_TOKENS); - previousToken = token; - - preLoanBalances[i] = token.balanceOf(address(this)); - feeAmounts[i] = _calculateFlashLoanFeeAmount(amount); - - _require(preLoanBalances[i] >= amount, Errors.INSUFFICIENT_FLASH_LOAN_BALANCE); - token.safeTransfer(address(recipient), amount); - } - - recipient.receiveFlashLoan(tokens, amounts, feeAmounts, userData); - - for (uint256 i = 0; i < tokens.length; ++i) { - IERC20 token = tokens[i]; - uint256 preLoanBalance = preLoanBalances[i]; - - // Checking for loan repayment first (without accounting for fees) makes for simpler debugging, and results - // in more accurate revert reasons if the flash loan protocol fee percentage is zero. - uint256 postLoanBalance = token.balanceOf(address(this)); - _require(postLoanBalance >= preLoanBalance, Errors.INVALID_POST_LOAN_BALANCE); - - // No need for checked arithmetic since we know the loan was fully repaid. - uint256 receivedFeeAmount = postLoanBalance - preLoanBalance; - _require(receivedFeeAmount >= feeAmounts[i], Errors.INSUFFICIENT_FLASH_LOAN_FEE_AMOUNT); - - _payFeeAmount(token, receivedFeeAmount); - emit FlashLoan(recipient, token, amounts[i], receivedFeeAmount); - } - } -} diff --git a/pkg/vault/contracts/PoolBalances.sol b/pkg/vault/contracts/PoolBalances.sol index f24a5a6d3..f99e10082 100644 --- a/pkg/vault/contracts/PoolBalances.sol +++ b/pkg/vault/contracts/PoolBalances.sol @@ -20,6 +20,7 @@ import "@balancer-labs/v2-interfaces/contracts/solidity-utils/openzeppelin/IERC2 import "@balancer-labs/v2-interfaces/contracts/vault/IBasePool.sol"; import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/ReentrancyGuard.sol"; +import "@balancer-labs/v2-solidity-utils/contracts/helpers/ERC20Helpers.sol"; import "@balancer-labs/v2-solidity-utils/contracts/helpers/InputHelpers.sol"; import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/SafeERC20.sol"; import "@balancer-labs/v2-solidity-utils/contracts/math/Math.sol"; @@ -153,6 +154,34 @@ abstract contract PoolBalances is Fees, ReentrancyGuard, PoolTokens, UserBalance ); } + function _exitPoolEmergency(bytes32 poolId, address payable recipient) internal { + (IERC20[] memory tokens, bytes32[] memory rawBalances) = _getPoolTokens(poolId); + (uint256[] memory balances, ) = rawBalances.totalsAndLastChangeBlock(); + PoolBalanceChange memory change = PoolBalanceChange(_asIAsset(tokens), + new uint256[](tokens.length), new bytes(0), false); + bytes32[] memory finalBalances = _processExitPoolTransfers( + recipient, change, rawBalances, balances, new uint256[](tokens.length)); + + // All that remains is storing the new Pool balances. + PoolSpecialization specialization = _getPoolSpecialization(poolId); + if (specialization == PoolSpecialization.TWO_TOKEN) { + _setTwoTokenPoolCashBalances(poolId, tokens[0], finalBalances[0], tokens[1], finalBalances[1]); + } else if (specialization == PoolSpecialization.MINIMAL_SWAP_INFO) { + _setMinimalSwapInfoPoolBalances(poolId, tokens, finalBalances); + } else { + // PoolSpecialization.GENERAL + _setGeneralPoolBalances(poolId, finalBalances); + } + + emit PoolBalanceChanged( + poolId, + recipient, + tokens, + _unsafeCastToInt256(balances, false), + new uint256[](tokens.length) + ); + } + /** * @dev Calls the corresponding Pool hook to get the amounts in/out plus protocol fee amounts, and performs the * associated token transfers and fee payments, returning the Pool's final balances. diff --git a/pkg/vault/contracts/ProtocolFeesCollector.sol b/pkg/vault/contracts/ProtocolFeesCollector.sol index 5725890d3..29fb90a5c 100644 --- a/pkg/vault/contracts/ProtocolFeesCollector.sol +++ b/pkg/vault/contracts/ProtocolFeesCollector.sol @@ -35,7 +35,6 @@ contract ProtocolFeesCollector is IProtocolFeesCollector, Authentication, Reentr // Absolute maximum fee percentages (1e18 = 100%, 1e16 = 1%). uint256 private constant _MAX_PROTOCOL_SWAP_FEE_PERCENTAGE = 50e16; // 50% - uint256 private constant _MAX_PROTOCOL_FLASH_LOAN_FEE_PERCENTAGE = 1e16; // 1% IVault public immutable override vault; @@ -46,9 +45,6 @@ contract ProtocolFeesCollector is IProtocolFeesCollector, Authentication, Reentr // when users join and exit them. uint256 private _swapFeePercentage; - // The flash loan fee is charged whenever a flash loan occurs, as a percentage of the tokens lent. - uint256 private _flashLoanFeePercentage; - constructor(IVault _vault) // The ProtocolFeesCollector is a singleton, so it simply uses its own address to disambiguate action // identifiers. @@ -77,23 +73,10 @@ contract ProtocolFeesCollector is IProtocolFeesCollector, Authentication, Reentr emit SwapFeePercentageChanged(newSwapFeePercentage); } - function setFlashLoanFeePercentage(uint256 newFlashLoanFeePercentage) external override authenticate { - _require( - newFlashLoanFeePercentage <= _MAX_PROTOCOL_FLASH_LOAN_FEE_PERCENTAGE, - Errors.FLASH_LOAN_FEE_PERCENTAGE_TOO_HIGH - ); - _flashLoanFeePercentage = newFlashLoanFeePercentage; - emit FlashLoanFeePercentageChanged(newFlashLoanFeePercentage); - } - function getSwapFeePercentage() external view override returns (uint256) { return _swapFeePercentage; } - function getFlashLoanFeePercentage() external view override returns (uint256) { - return _flashLoanFeePercentage; - } - function getCollectedFeeAmounts(IERC20[] memory tokens) external view diff --git a/pkg/vault/contracts/Vault.sol b/pkg/vault/contracts/Vault.sol index 182e7b4f2..4390d5ac9 100644 --- a/pkg/vault/contracts/Vault.sol +++ b/pkg/vault/contracts/Vault.sol @@ -19,7 +19,6 @@ import "@balancer-labs/v2-interfaces/contracts/solidity-utils/misc/IWETH.sol"; import "@balancer-labs/v2-interfaces/contracts/vault/IAuthorizer.sol"; import "./VaultAuthorization.sol"; -import "./FlashLoans.sol"; import "./Swaps.sol"; /** @@ -35,7 +34,6 @@ import "./Swaps.sol"; * * - `AssetManagers`: Pool token Asset Manager registry, and Asset Manager interactions. * - `Fees`: set and compute protocol fees. - * - `FlashLoans`: flash loan transfers and fees. * - `PoolBalances`: Pool joins and exits. * - `PoolRegistry`: Pool registration, ID management, and basic queries. * - `PoolTokens`: Pool token registration and registration, and balance queries. @@ -57,7 +55,7 @@ import "./Swaps.sol"; * utilization of `internal` functions (particularly inside modifiers), usage of named return arguments, dedicated * storage access methods, dynamic revert reason generation, and usage of inline assembly, to name a few. */ -contract Vault is VaultAuthorization, FlashLoans, Swaps { +contract Vault is VaultAuthorization, Swaps { constructor( IAuthorizer authorizer, IWETH weth, @@ -75,4 +73,8 @@ contract Vault is VaultAuthorization, FlashLoans, Swaps { function WETH() external view override returns (IWETH) { return _WETH(); } + + function exitPoolEmergency(bytes32 poolId) external override authenticate { + _exitPoolEmergency(poolId, msg.sender); + } } diff --git a/pkg/vault/contracts/test/MockFlashLoanRecipient.sol b/pkg/vault/contracts/test/MockFlashLoanRecipient.sol deleted file mode 100644 index fb31548c7..000000000 --- a/pkg/vault/contracts/test/MockFlashLoanRecipient.sol +++ /dev/null @@ -1,85 +0,0 @@ -// 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; - -import "@balancer-labs/v2-interfaces/contracts/solidity-utils/openzeppelin/IERC20.sol"; -import "@balancer-labs/v2-interfaces/contracts/vault/IFlashLoanRecipient.sol"; -import "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol"; - -import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/SafeERC20.sol"; -import "@balancer-labs/v2-solidity-utils/contracts/test/TestToken.sol"; -import "@balancer-labs/v2-solidity-utils/contracts/math/Math.sol"; - -contract MockFlashLoanRecipient is IFlashLoanRecipient { - using Math for uint256; - using SafeERC20 for IERC20; - - address public immutable vault; - bool public repayLoan; - bool public repayInExcess; - bool public reenter; - - constructor(address _vault) { - vault = _vault; - repayLoan = true; - repayInExcess = false; - reenter = false; - } - - function setRepayLoan(bool _repayLoan) public { - repayLoan = _repayLoan; - } - - function setRepayInExcess(bool _repayInExcess) public { - repayInExcess = _repayInExcess; - } - - function setReenter(bool _reenter) public { - reenter = _reenter; - } - - // Repays loan unless setRepayLoan was called with 'false' - function receiveFlashLoan( - IERC20[] memory tokens, - uint256[] memory amounts, - uint256[] memory feeAmounts, - bytes memory userData - ) external override { - for (uint256 i = 0; i < tokens.length; ++i) { - IERC20 token = tokens[i]; - uint256 amount = amounts[i]; - uint256 feeAmount = feeAmounts[i]; - - require(token.balanceOf(address(this)) == amount, "INVALID_FLASHLOAN_BALANCE"); - - if (reenter) { - IVault(msg.sender).flashLoan(IFlashLoanRecipient(address(this)), tokens, amounts, userData); - } - - // The recipient will mint the fees it pays - TestToken(address(token)).mint(address(this), repayInExcess ? feeAmount.add(1) : feeAmount); - - uint256 totalDebt = amount.add(feeAmount); - - if (!repayLoan) { - totalDebt = totalDebt.sub(1); - } else if (repayInExcess) { - totalDebt = totalDebt.add(1); - } - - token.safeTransfer(vault, totalDebt); - } - } -} diff --git a/pkg/vault/test/ExitPool.test.ts b/pkg/vault/test/ExitPool.test.ts index 4097561bd..b0976b501 100644 --- a/pkg/vault/test/ExitPool.test.ts +++ b/pkg/vault/test/ExitPool.test.ts @@ -170,6 +170,10 @@ describe('Exit Pool', () => { } context('when called incorrectly', () => { + it('reverts if emergency exit with not authorized admin', async () => { + await expect(vault.connect(creator).exitPoolEmergency(poolId)).to.be.revertedWith('SENDER_NOT_ALLOWED'); + }); + it('reverts if the pool ID does not exist', async () => { await expect(exitPool({ poolId: ethers.utils.id('invalid') })).to.be.revertedWith('INVALID_POOL_ID'); }); @@ -239,6 +243,35 @@ describe('Exit Pool', () => { }); }); + context('with authorized admin', () => { + it('exit pool emergency', async () => { + const action = await actionId(vault, 'exitPoolEmergency'); + await authorizer.connect(admin).grantPermission(action, recipient.address, vault.address); + + { + const { tokens, balances } = await vault.getPoolTokens(poolId); + balances.forEach((balance: number, i: number) => { + expect(balance).to.be.gt(0); + }); + tokens.forEach(async (token: Token, i: number) => { + await expect(token.balanceOf(recipient.address)).to.be.eq(0); + }); + } + + await vault.connect(recipient).exitPoolEmergency(poolId); + + { + const { tokens, balances } = await vault.getPoolTokens(poolId); + balances.forEach((balance: number, i: number) => { + expect(balance).to.be.eq(0); + }); + tokens.forEach(async (token: Token, i: number) => { + await expect(token.balanceOf(recipient.address)).to.be.gt(0); + }); + } + }); + }); + context('with correct pool return values', () => { itExitsCorrectlyWithAndWithoutDueProtocolFeesAndInternalBalance(); }); diff --git a/pkg/vault/test/Fees.test.ts b/pkg/vault/test/Fees.test.ts index 898accd20..bc091c1c3 100644 --- a/pkg/vault/test/Fees.test.ts +++ b/pkg/vault/test/Fees.test.ts @@ -36,7 +36,6 @@ describe('Fees', () => { describe('set fees', () => { const MAX_SWAP_FEE_PERCENTAGE = bn(50e16); // 50% - const MAX_FLASH_LOAN_FEE_PERCENTAGE = bn(1e16); // 1% context('when the sender is allowed', () => { context('when the given input is valid', async () => { @@ -56,25 +55,6 @@ describe('Fees', () => { }); }); }); - - describe('flash loan fee', () => { - it('sets the percentage properly', async () => { - await vault.setFlashLoanFeePercentage(MAX_FLASH_LOAN_FEE_PERCENTAGE, { from: admin }); - - const flashLoanFeePercentage = await vault.getFlashLoanFeePercentage(); - expect(flashLoanFeePercentage).to.equal(MAX_FLASH_LOAN_FEE_PERCENTAGE); - }); - - it('emits an event', async () => { - const receipt = await ( - await vault.setFlashLoanFeePercentage(MAX_FLASH_LOAN_FEE_PERCENTAGE, { from: admin }) - ).wait(); - - expectEvent.inReceipt(receipt, 'FlashLoanFeePercentageChanged', { - newFlashLoanFeePercentage: MAX_FLASH_LOAN_FEE_PERCENTAGE, - }); - }); - }); }); context('when the given input is invalid', async () => { @@ -85,14 +65,6 @@ describe('Fees', () => { 'SWAP_FEE_PERCENTAGE_TOO_HIGH' ); }); - - it('reverts if the flash loan fee percentage is above the maximum', async () => { - const badFlashLoanFeePercentage = MAX_FLASH_LOAN_FEE_PERCENTAGE.add(1); - - await expect(vault.setFlashLoanFeePercentage(badFlashLoanFeePercentage, { from: admin })).to.be.revertedWith( - 'FLASH_LOAN_FEE_PERCENTAGE_TOO_HIGH' - ); - }); }); }); @@ -101,9 +73,6 @@ describe('Fees', () => { await expect(vault.setSwapFeePercentage(MAX_SWAP_FEE_PERCENTAGE, { from: other })).to.be.revertedWith( 'SENDER_NOT_ALLOWED' ); - await expect(vault.setFlashLoanFeePercentage(MAX_SWAP_FEE_PERCENTAGE, { from: other })).to.be.revertedWith( - 'SENDER_NOT_ALLOWED' - ); }); }); }); diff --git a/pkg/vault/test/FlashLoan.test.ts b/pkg/vault/test/FlashLoan.test.ts deleted file mode 100644 index b0e264bc8..000000000 --- a/pkg/vault/test/FlashLoan.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { ethers } from 'hardhat'; -import { expect } from 'chai'; -import { Contract, ContractTransaction } from 'ethers'; -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; - -import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; - -import * as expectEvent from '@balancer-labs/v2-helpers/src/test/expectEvent'; -import { deploy, deployedAt } from '@balancer-labs/v2-helpers/src/contract'; -import { actionId } from '@balancer-labs/v2-helpers/src/models/misc/actions'; -import { expectBalanceChange } from '@balancer-labs/v2-helpers/src/test/tokenBalance'; -import { bn, divCeil, fp, fpMul, FP_100_PCT } from '@balancer-labs/v2-helpers/src/numbers'; -import { ANY_ADDRESS, ZERO_ADDRESS } from '@balancer-labs/v2-helpers/src/constants'; -import Vault from '@balancer-labs/v2-helpers/src/models/vault/Vault'; -import { sharedBeforeEach } from '@balancer-labs/v2-common/sharedBeforeEach'; - -describe('Flash Loans', () => { - let admin: SignerWithAddress, minter: SignerWithAddress, feeSetter: SignerWithAddress, other: SignerWithAddress; - let authorizer: Contract, vault: Contract, recipient: Contract, feesCollector: Contract; - let tokens: TokenList; - - before('setup', async () => { - [, admin, minter, feeSetter, other] = await ethers.getSigners(); - }); - - sharedBeforeEach('deploy vault & tokens', async () => { - ({ instance: vault, authorizer } = await Vault.create({ admin })); - feesCollector = await deployedAt('ProtocolFeesCollector', await vault.getProtocolFeesCollector()); - recipient = await deploy('MockFlashLoanRecipient', { from: other, args: [vault.address] }); - - const action = await actionId(feesCollector, 'setFlashLoanFeePercentage'); - await authorizer.connect(admin).grantPermission(action, feeSetter.address, ANY_ADDRESS); - - tokens = await TokenList.create(['DAI', 'MKR'], { from: minter, sorted: true }); - await tokens.mint({ from: minter, to: vault, amount: bn(100e18) }); - }); - - context('with no protocol fees', () => { - sharedBeforeEach(async () => { - await feesCollector.connect(feeSetter).setFlashLoanFeePercentage(0); - }); - - it('causes no net balance change on the Vault', async () => { - await expectBalanceChange( - () => vault.connect(other).flashLoan(recipient.address, [tokens.DAI.address], [bn(1e18)], '0x10'), - tokens, - { account: vault } - ); - }); - - it('all balance can be loaned', async () => { - const tx = await vault.connect(other).flashLoan(recipient.address, [tokens.DAI.address], [bn(100e18)], '0x10'); - const receipt = await tx.wait(); - - expectEvent.inReceipt(receipt, 'FlashLoan', { - recipient: recipient.address, - token: tokens.DAI.address, - amount: bn(100e18), - feeAmount: 0, - }); - }); - - it('reverts if the loan is larger than available balance', async () => { - await expect( - vault.connect(other).flashLoan(recipient.address, [tokens.DAI.address], [bn(100e18).add(1)], '0x10') - ).to.be.revertedWith('INSUFFICIENT_FLASH_LOAN_BALANCE'); - }); - - it('reverts if the borrower does not repay the loan', async () => { - await recipient.setRepayLoan(false); - - await expect( - vault.connect(other).flashLoan(recipient.address, [tokens.DAI.address], [bn(1e18)], '0x10') - ).to.be.revertedWith('INVALID_POST_LOAN_BALANCE'); - }); - }); - - context('with protocol fees', () => { - const feePercentage = fp(0.005); // 0.5% - - sharedBeforeEach(async () => { - await feesCollector.connect(feeSetter).setFlashLoanFeePercentage(feePercentage); - }); - - it('zero loans are possible', async () => { - const loan = 0; - const feeAmount = 0; - - await expectBalanceChange( - () => vault.connect(other).flashLoan(recipient.address, [tokens.DAI.address], [loan], '0x10'), - tokens, - { account: vault } - ); - - expect((await feesCollector.getCollectedFeeAmounts([tokens.DAI.address]))[0]).to.equal(feeAmount); - }); - - it('zero loans are possible', async () => { - const loan = 0; - const feeAmount = 0; - - await expectBalanceChange( - () => vault.connect(other).flashLoan(recipient.address, [tokens.DAI.address], [loan], '0x10'), - tokens, - { account: vault } - ); - - expect((await feesCollector.getCollectedFeeAmounts([tokens.DAI.address]))[0]).to.equal(feeAmount); - }); - - it('the fees module receives protocol fees', async () => { - const loan = bn(1e18); - const feeAmount = divCeil(loan.mul(feePercentage), FP_100_PCT); - - await expectBalanceChange( - () => vault.connect(other).flashLoan(recipient.address, [tokens.DAI.address], [loan], '0x10'), - tokens, - { account: feesCollector, changes: { DAI: feeAmount } } - ); - - expect((await feesCollector.getCollectedFeeAmounts([tokens.DAI.address]))[0]).to.equal(feeAmount); - }); - - it('protocol fees are rounded up', async () => { - const loan = bn(1); - const feeAmount = bn(1); // In this extreme case, fees account for the full loan - - await expectBalanceChange( - () => vault.connect(other).flashLoan(recipient.address, [tokens.DAI.address], [loan], '0x10'), - tokens, - { account: feesCollector, changes: { DAI: feeAmount } } - ); - - expect((await feesCollector.getCollectedFeeAmounts([tokens.DAI.address]))[0]).to.equal(feeAmount); - }); - - it('excess fees can be paid', async () => { - await recipient.setRepayInExcess(true); - - // The recipient pays one extra token - const feeAmount = fpMul(bn(1e18), feePercentage).add(1); - - const tx: ContractTransaction = await expectBalanceChange( - () => vault.connect(other).flashLoan(recipient.address, [tokens.DAI.address], [bn(1e18)], '0x10'), - tokens, - { account: feesCollector, changes: { DAI: feeAmount } } - ); - - expect(await feesCollector.getCollectedFeeAmounts([tokens.DAI.address])).to.deep.equal([feeAmount]); - - expectEvent.inReceipt(await tx.wait(), 'FlashLoan', { - recipient: recipient.address, - token: tokens.DAI.address, - amount: bn(1e18), - feeAmount, - }); - }); - - it('all balance can be loaned', async () => { - await vault.connect(other).flashLoan(recipient.address, [tokens.DAI.address], [bn(100e18)], '0x10'); - }); - - it('reverts if the borrower does not repay the loan', async () => { - await recipient.setRepayLoan(false); - - await expect( - vault.connect(other).flashLoan(recipient.address, [tokens.DAI.address], [bn(1e18)], '0x10') - ).to.be.revertedWith('INSUFFICIENT_FLASH_LOAN_FEE_AMOUNT'); - }); - - it('reverts if the borrower reenters the Vault', async () => { - await recipient.setReenter(true); - - await expect( - vault.connect(other).flashLoan(recipient.address, [tokens.DAI.address], [bn(1e18)], '0x10') - ).to.be.revertedWith('REENTRANCY'); - }); - - describe('multi asset loan', () => { - it('the Vault receives protocol fees proportional to each loan', async () => { - const amounts = [1e18, 2e18].map(bn); - const feeAmounts = amounts.map((amount) => fpMul(amount, feePercentage)); - - await expectBalanceChange( - () => - vault - .connect(other) - .flashLoan(recipient.address, [tokens.DAI.address, tokens.MKR.address], amounts, '0x10'), - tokens, - { account: feesCollector, changes: { DAI: feeAmounts[0], MKR: feeAmounts[1] } } - ); - - expect(await feesCollector.getCollectedFeeAmounts([tokens.DAI.address])).to.deep.equal([feeAmounts[0]]); - expect(await feesCollector.getCollectedFeeAmounts([tokens.MKR.address])).to.deep.equal([feeAmounts[1]]); - }); - - it('all balance can be loaned', async () => { - await vault - .connect(other) - .flashLoan(recipient.address, [tokens.DAI.address, tokens.MKR.address], [bn(100e18), bn(100e18)], '0x10'); - }); - - it('reverts if tokens are not unique', async () => { - await expect( - vault - .connect(other) - .flashLoan(recipient.address, [tokens.DAI.address, tokens.DAI.address], [bn(100e18), bn(100e18)], '0x10') - ).to.be.revertedWith('UNSORTED_TOKENS'); - }); - - it('reverts if tokens are not sorted', async () => { - await expect( - vault - .connect(other) - .flashLoan(recipient.address, [tokens.MKR.address, tokens.DAI.address], [bn(100e18), bn(100e18)], '0x10') - ).to.be.revertedWith('UNSORTED_TOKENS'); - }); - - it('reverts if a token is invalid', async () => { - await expect( - vault - .connect(other) - .flashLoan(recipient.address, [tokens.MKR.address, ZERO_ADDRESS], [bn(100e18), bn(100e18)], '0x10') - ).to.be.revertedWith('ZERO_TOKEN'); - }); - }); - }); -}); diff --git a/pvt/helpers/src/models/vault/Vault.ts b/pvt/helpers/src/models/vault/Vault.ts index 71c70e84e..4006e0e2d 100644 --- a/pvt/helpers/src/models/vault/Vault.ts +++ b/pvt/helpers/src/models/vault/Vault.ts @@ -211,10 +211,9 @@ export default class Vault { return feesCollector.withdrawCollectedFees(tokens, amounts, TypesConverter.toAddress(recipient)); } - async getProtocolFeePercentages(): Promise<{ swapFeePercentage: BigNumber; flashLoanFeePercentage: BigNumber }> { + async getProtocolFeePercentages(): Promise<{ swapFeePercentage: BigNumber }> { return { swapFeePercentage: await this.getSwapFeePercentage(), - flashLoanFeePercentage: await this.getFlashLoanFeePercentage(), }; } @@ -222,10 +221,6 @@ export default class Vault { return this.getFeesProvider().getFeeTypePercentage(ProtocolFee.SWAP); } - async getFlashLoanFeePercentage(): Promise { - return this.getFeesProvider().getFeeTypePercentage(ProtocolFee.FLASH_LOAN); - } - async getFeesCollector(): Promise { if (!this.feesCollector) { const instance = await this.instance.getProtocolFeesCollector(); @@ -253,22 +248,6 @@ export default class Vault { return instance.setSwapFeePercentage(swapFeePercentage); } - async setFlashLoanFeePercentage( - flashLoanFeePercentage: BigNumber, - { from }: TxParams = {} - ): Promise { - const feesCollector = await this.getFeesCollector(); - const id = await actionId(feesCollector, 'setFlashLoanFeePercentage'); - - if (this.authorizer && this.admin && !(await this.hasPermissionGlobally(id, this.admin))) { - await this.grantPermissionGlobally(id, this.admin); - } - - const sender = from || this.admin; - const instance = sender ? feesCollector.connect(sender) : feesCollector; - return instance.setFlashLoanFeePercentage(flashLoanFeePercentage); - } - async setFeeTypePercentage(feeType: number, value: BigNumberish): Promise { if (!this.admin) throw Error("Missing Vault's admin"); @@ -287,12 +266,6 @@ export default class Vault { feeCollector.address ); - await this.grantPermissionIfNeeded( - await actionId(feeCollector, 'setFlashLoanFeePercentage'), - feeProvider.address, - feeCollector.address - ); - await feeProvider.connect(this.admin).setFeeTypePercentage(feeType, bn(value)); } diff --git a/pvt/helpers/src/models/vault/types.ts b/pvt/helpers/src/models/vault/types.ts index b66deba3e..0ffb04515 100644 --- a/pvt/helpers/src/models/vault/types.ts +++ b/pvt/helpers/src/models/vault/types.ts @@ -86,7 +86,6 @@ export type QueryBatchSwap = { export enum ProtocolFee { SWAP = 0, - FLASH_LOAN = 1, YIELD = 2, AUM = 3, } diff --git a/yarn.lock b/yarn.lock index fbc92feee..31746c1cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -302,6 +302,52 @@ __metadata: languageName: unknown linkType: soft +"@balancer-labs/v2-pool-range@workspace:pkg/pool-range": + version: 0.0.0-use.local + resolution: "@balancer-labs/v2-pool-range@workspace:pkg/pool-range" + dependencies: + "@balancer-labs/balancer-js": "workspace:*" + "@balancer-labs/solidity-toolbox": "workspace:*" + "@balancer-labs/v2-common": "workspace:*" + "@balancer-labs/v2-helpers": "workspace:*" + "@balancer-labs/v2-interfaces": "workspace:*" + "@balancer-labs/v2-pool-utils": "workspace:*" + "@balancer-labs/v2-pool-weighted": "workspace:*" + "@balancer-labs/v2-solidity-utils": "workspace:*" + "@nomiclabs/hardhat-ethers": "npm:^2.2.1" + "@nomiclabs/hardhat-waffle": "npm:^2.0.3" + "@types/chai": "npm:^4.3.3" + "@types/lodash": "npm:^4.14.186" + "@types/mocha": "npm:^10.0.0" + "@types/node": "npm:^14.14.31" + "@typescript-eslint/eslint-plugin": "npm:^5.41.0" + "@typescript-eslint/parser": "npm:^5.41.0" + chai: "npm:^4.3.6" + decimal.js: "npm:^10.4.2" + eslint: "npm:^8.26.0" + eslint-plugin-mocha-no-only: "npm:^1.1.1" + eslint-plugin-prettier: "npm:^4.2.1" + ethereum-waffle: "npm:^3.4.4" + ethers: "npm:^5.7.2" + hardhat: "npm:^2.12.5" + hardhat-contract-sizer: "npm:^2.10.0" + hardhat-ignore-warnings: "npm:^0.2.4" + lodash.frompairs: "npm:^4.0.1" + lodash.pick: "npm:^4.4.0" + lodash.range: "npm:^3.2.0" + lodash.times: "npm:^4.3.2" + lodash.zip: "npm:^4.2.0" + mocha: "npm:^10.1.0" + nodemon: "npm:^2.0.20" + prettier: "npm:^2.7.1" + prettier-plugin-solidity: "npm:v1.0.0-alpha.59" + solhint: "npm:^3.2.0" + solhint-plugin-prettier: "npm:^0.0.4" + ts-node: "npm:^10.9.1" + typescript: "npm:^4.0.2" + languageName: unknown + linkType: soft + "@balancer-labs/v2-pool-stable@workspace:*, @balancer-labs/v2-pool-stable@workspace:pkg/pool-stable": version: 0.0.0-use.local resolution: "@balancer-labs/v2-pool-stable@workspace:pkg/pool-stable" @@ -601,6 +647,13 @@ __metadata: languageName: node linkType: hard +"@colors/colors@npm:1.5.0": + version: 1.5.0 + resolution: "@colors/colors@npm:1.5.0" + checksum: 5e08870799494f68e5b3b79e9a337bbf5fd7e634904fbbe642769921bf158fe458c41c888f88edf051b78c5325e3339970f00b24e31421c3480bb58f02687218 + languageName: node + linkType: hard + "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -4153,6 +4206,19 @@ __metadata: languageName: node linkType: hard +"cli-table3@npm:^0.6.0": + version: 0.6.5 + resolution: "cli-table3@npm:0.6.5" + dependencies: + "@colors/colors": "npm:1.5.0" + string-width: "npm:^4.2.0" + dependenciesMeta: + "@colors/colors": + optional: true + checksum: 7004ba985cd532797da5cd286670a717ecc88ce6b6e3f7a3f5ad23cfbc42305e8b0a6f3e91ae5aa520cc762c0f5f1bac2912c900c431ae33e801a8ed8aa68ca0 + languageName: node + linkType: hard + "cliui@npm:^3.2.0": version: 3.2.0 resolution: "cliui@npm:3.2.0" @@ -6779,6 +6845,19 @@ __metadata: languageName: node linkType: hard +"hardhat-contract-sizer@npm:^2.10.0": + version: 2.10.1 + resolution: "hardhat-contract-sizer@npm:2.10.1" + dependencies: + chalk: "npm:^4.0.0" + cli-table3: "npm:^0.6.0" + strip-ansi: "npm:^6.0.0" + peerDependencies: + hardhat: ^2.0.0 + checksum: 2dc683d81aa2292fec137c31c679a9d6ead7e881194fab41de2e0715e40618964917ef0d67e5ed9d881e5ee64d88d9f4cd4a52dc6416412462ded961c492b6b4 + languageName: node + linkType: hard + "hardhat-ignore-warnings@npm:^0.2.4": version: 0.2.8 resolution: "hardhat-ignore-warnings@npm:0.2.8"