From 0a154f6bd8749de3e55a079661cf3f06d78bb6b9 Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Wed, 26 Oct 2022 21:38:28 -0400 Subject: [PATCH 1/3] refactor: rename BasePool to LegacyBasePool --- pkg/pool-linear/contracts/LinearPool.sol | 14 +++++++------- pkg/pool-stable/contracts/ComposableStablePool.sol | 4 ++-- .../contracts/ComposableStablePoolProtocolFees.sol | 2 +- .../contracts/ComposableStablePoolStorage.sol | 4 ++-- .../test/MockComposableStablePoolProtocolFees.sol | 4 ++-- .../test/MockComposableStablePoolRates.sol | 4 ++-- .../test/MockComposableStablePoolStorage.sol | 4 ++-- pkg/pool-utils/contracts/BaseGeneralPool.sol | 12 ++++++------ .../contracts/BaseMinimalSwapInfoPool.sol | 12 ++++++------ .../contracts/{BasePool.sol => LegacyBasePool.sol} | 2 +- pkg/pool-utils/contracts/lib/ComposablePoolLib.sol | 2 ++ .../{MockBasePool.sol => MockLegacyBasePool.sol} | 6 +++--- .../{BasePool.test.ts => LegacyBasePool.test.ts} | 4 ++-- pkg/pool-weighted/contracts/BaseWeightedPool.sol | 2 +- pkg/pool-weighted/contracts/WeightedPool.sol | 2 +- .../contracts/WeightedPoolProtocolFees.sol | 2 +- 16 files changed, 41 insertions(+), 39 deletions(-) rename pkg/pool-utils/contracts/{BasePool.sol => LegacyBasePool.sol} (99%) rename pkg/pool-utils/contracts/test/{MockBasePool.sol => MockLegacyBasePool.sol} (98%) rename pkg/pool-utils/test/{BasePool.test.ts => LegacyBasePool.test.ts} (99%) diff --git a/pkg/pool-linear/contracts/LinearPool.sol b/pkg/pool-linear/contracts/LinearPool.sol index 4c796012c4..6d61926bd1 100644 --- a/pkg/pool-linear/contracts/LinearPool.sol +++ b/pkg/pool-linear/contracts/LinearPool.sol @@ -21,7 +21,7 @@ import "@balancer-labs/v2-interfaces/contracts/pool-utils/IRateProvider.sol"; import "@balancer-labs/v2-interfaces/contracts/pool-linear/ILinearPool.sol"; import "@balancer-labs/v2-interfaces/contracts/vault/IGeneralPool.sol"; -import "@balancer-labs/v2-pool-utils/contracts/BasePool.sol"; +import "@balancer-labs/v2-pool-utils/contracts/LegacyBasePool.sol"; import "@balancer-labs/v2-pool-utils/contracts/rates/PriceRateCache.sol"; import "@balancer-labs/v2-solidity-utils/contracts/helpers/ERC20Helpers.sol"; @@ -38,8 +38,8 @@ import "./LinearMath.sol"; * The Pool will register three tokens in the Vault however: the two assets and the BPT itself, * so that BPT can be exchanged (effectively joining and exiting) via swaps. * - * Despite inheriting from BasePool, much of the basic behavior changes. This Pool does not support regular joins and - * exits, as the initial BPT supply is 'preminted' during initialization. No further BPT can be minted, and BPT can + * Despite inheriting from LegacyBasePool, much of the basic behavior changes. This Pool does not support regular joins + * and exits, as the initial BPT supply is 'preminted' during initialization. No further BPT can be minted, and BPT can * only be burned if governance enables Recovery Mode and LPs use it to exit proportionally. * * Unlike most other Pools, this one does not attempt to create revenue by charging fees: value is derived by holding @@ -51,7 +51,7 @@ import "./LinearMath.sol"; * The net revenue via fees is expected to be zero: all collected fees are used to pay for this 'rebalancing'. * Accordingly, this Pool does not pay any protocol fees. */ -abstract contract LinearPool is ILinearPool, IGeneralPool, IRateProvider, BasePool { +abstract contract LinearPool is ILinearPool, IGeneralPool, IRateProvider, LegacyBasePool { using WordCodec for bytes32; using FixedPoint for uint256; using PriceRateCache for bytes32; @@ -86,8 +86,8 @@ abstract contract LinearPool is ILinearPool, IGeneralPool, IRateProvider, BasePo uint256 private immutable _scalingFactorMainToken; uint256 private immutable _scalingFactorWrappedToken; - // The lower and upper targets are in BasePool's misc data field, which has 192 bits available (as it shares the - // same storage slot as the swap fee percentage and recovery mode flag, which together take up 64 bits). + // The lower and upper targets are in LegacyBasePool's misc data field, which has 192 bits available (as it shares + // the same storage slot as the swap fee percentage and recovery mode flag, which together take up 64 bits). // We use 64 of these 192 for the targets (32 for each). // // The targets are already scaled by the main token's scaling factor (which makes the token behave as if it had 18 @@ -124,7 +124,7 @@ abstract contract LinearPool is ILinearPool, IGeneralPool, IRateProvider, BasePo uint256 bufferPeriodDuration, address owner ) - BasePool( + LegacyBasePool( vault, IVault.PoolSpecialization.GENERAL, name, diff --git a/pkg/pool-stable/contracts/ComposableStablePool.sol b/pkg/pool-stable/contracts/ComposableStablePool.sol index a1be4bd5e3..481e45d234 100644 --- a/pkg/pool-stable/contracts/ComposableStablePool.sol +++ b/pkg/pool-stable/contracts/ComposableStablePool.sol @@ -82,7 +82,7 @@ contract ComposableStablePool is } constructor(NewPoolParams memory params) - BasePool( + LegacyBasePool( params.vault, IVault.PoolSpecialization.GENERAL, params.name, @@ -1136,7 +1136,7 @@ contract ComposableStablePool is override( // Our inheritance pattern creates a small diamond that requires explicitly listing the parents here. // Each parent calls the `super` version, so linearization ensures all implementations are called. - BasePool, + LegacyBasePool, ComposableStablePoolProtocolFees, StablePoolAmplification, ComposableStablePoolRates diff --git a/pkg/pool-stable/contracts/ComposableStablePoolProtocolFees.sol b/pkg/pool-stable/contracts/ComposableStablePoolProtocolFees.sol index 9c8e5814b3..c048bd907a 100644 --- a/pkg/pool-stable/contracts/ComposableStablePoolProtocolFees.sol +++ b/pkg/pool-stable/contracts/ComposableStablePoolProtocolFees.sol @@ -317,7 +317,7 @@ abstract contract ComposableStablePoolProtocolFees is override( // Our inheritance pattern creates a small diamond that requires explicitly listing the parents here. // Each parent calls the `super` version, so linearization ensures all implementations are called. - BasePool, + LegacyBasePool, BasePoolAuthorization, ComposableStablePoolRates ) diff --git a/pkg/pool-stable/contracts/ComposableStablePoolStorage.sol b/pkg/pool-stable/contracts/ComposableStablePoolStorage.sol index ae9396e3f9..f65d572098 100644 --- a/pkg/pool-stable/contracts/ComposableStablePoolStorage.sol +++ b/pkg/pool-stable/contracts/ComposableStablePoolStorage.sol @@ -18,11 +18,11 @@ import "@balancer-labs/v2-interfaces/contracts/solidity-utils/helpers/BalancerEr import "@balancer-labs/v2-interfaces/contracts/solidity-utils/openzeppelin/IERC20.sol"; import "@balancer-labs/v2-interfaces/contracts/pool-utils/IRateProvider.sol"; -import "@balancer-labs/v2-pool-utils/contracts/BasePool.sol"; +import "@balancer-labs/v2-pool-utils/contracts/LegacyBasePool.sol"; import "./StableMath.sol"; -abstract contract ComposableStablePoolStorage is BasePool { +abstract contract ComposableStablePoolStorage is LegacyBasePool { using FixedPoint for uint256; using WordCodec for bytes32; diff --git a/pkg/pool-stable/contracts/test/MockComposableStablePoolProtocolFees.sol b/pkg/pool-stable/contracts/test/MockComposableStablePoolProtocolFees.sol index e3165e45cc..ccfe5bca38 100644 --- a/pkg/pool-stable/contracts/test/MockComposableStablePoolProtocolFees.sol +++ b/pkg/pool-stable/contracts/test/MockComposableStablePoolProtocolFees.sol @@ -46,14 +46,14 @@ contract MockComposableStablePoolProtocolFees is ComposableStablePoolProtocolFee protocolFeeProvider, ProviderFeeIDs({ swap: ProtocolFeeType.SWAP, yield: ProtocolFeeType.YIELD, aum: ProtocolFeeType.AUM }) ) - BasePool( + LegacyBasePool( vault, IVault.PoolSpecialization.GENERAL, "MockStablePoolStorage", "MOCK_BPT", _insertSorted(tokens, IERC20(this)), new address[](tokens.length + 1), - 1e12, // BasePool._MIN_SWAP_FEE_PERCENTAGE + 1e12, // LegacyBasePool._MIN_SWAP_FEE_PERCENTAGE 0, 0, address(0) diff --git a/pkg/pool-stable/contracts/test/MockComposableStablePoolRates.sol b/pkg/pool-stable/contracts/test/MockComposableStablePoolRates.sol index 2e8a786e2a..f6d467d7ef 100644 --- a/pkg/pool-stable/contracts/test/MockComposableStablePoolRates.sol +++ b/pkg/pool-stable/contracts/test/MockComposableStablePoolRates.sol @@ -31,14 +31,14 @@ contract MockComposableStablePoolRates is ComposableStablePoolRates { ComposableStablePoolStorage( StorageParams(_insertSorted(tokens, IERC20(this)), tokenRateProviders, exemptFromYieldProtocolFeeFlags) ) - BasePool( + LegacyBasePool( vault, IVault.PoolSpecialization.GENERAL, "MockStablePoolStorage", "MOCK_BPT", _insertSorted(tokens, IERC20(this)), new address[](tokens.length + 1), - 1e12, // BasePool._MIN_SWAP_FEE_PERCENTAGE + 1e12, // LegacyBasePool._MIN_SWAP_FEE_PERCENTAGE 0, 0, owner diff --git a/pkg/pool-stable/contracts/test/MockComposableStablePoolStorage.sol b/pkg/pool-stable/contracts/test/MockComposableStablePoolStorage.sol index e5d2ac631b..029e1e3fd0 100644 --- a/pkg/pool-stable/contracts/test/MockComposableStablePoolStorage.sol +++ b/pkg/pool-stable/contracts/test/MockComposableStablePoolStorage.sol @@ -33,14 +33,14 @@ contract MockComposableStablePoolStorage is ComposableStablePoolStorage { exemptFromYieldProtocolFeeFlags: exemptFromYieldProtocolFeeFlags }) ) - BasePool( + LegacyBasePool( vault, IVault.PoolSpecialization.GENERAL, "MockComposableStablePoolStorage", "MOCK_BPT", _insertSorted(tokens, IERC20(this)), new address[](tokens.length + 1), - 1e12, // BasePool._MIN_SWAP_FEE_PERCENTAGE + 1e12, // LegacyBasePool._MIN_SWAP_FEE_PERCENTAGE 0, 0, address(0) diff --git a/pkg/pool-utils/contracts/BaseGeneralPool.sol b/pkg/pool-utils/contracts/BaseGeneralPool.sol index 4f6f25789f..94aed60cd7 100644 --- a/pkg/pool-utils/contracts/BaseGeneralPool.sol +++ b/pkg/pool-utils/contracts/BaseGeneralPool.sol @@ -17,16 +17,16 @@ pragma experimental ABIEncoderV2; import "@balancer-labs/v2-interfaces/contracts/vault/IGeneralPool.sol"; -import "./BasePool.sol"; +import "./LegacyBasePool.sol"; /** - * @dev Extension of `BasePool`, adding a handler for `IGeneralPool.onSwap`. + * @dev Extension of `LegacyBasePool`, adding a handler for `IGeneralPool.onSwap`. * - * Derived contracts must call `BasePool`'s constructor, and implement `_onSwapGivenIn` and `_onSwapGivenOut` along with - * `BasePool`'s virtual functions. Inheriting from this contract lets derived contracts choose the General - * specialization setting. + * Derived contracts must call `LegacyBasePool`'s constructor, and implement `_onSwapGivenIn` and `_onSwapGivenOut` + * along with `LegacyBasePool`'s virtual functions. Inheriting from this contract lets derived contracts choose the + * General specialization setting. */ -abstract contract BaseGeneralPool is IGeneralPool, BasePool { +abstract contract BaseGeneralPool is IGeneralPool, LegacyBasePool { // Swap Hooks function onSwap( diff --git a/pkg/pool-utils/contracts/BaseMinimalSwapInfoPool.sol b/pkg/pool-utils/contracts/BaseMinimalSwapInfoPool.sol index 702b14a151..e988e26a7a 100644 --- a/pkg/pool-utils/contracts/BaseMinimalSwapInfoPool.sol +++ b/pkg/pool-utils/contracts/BaseMinimalSwapInfoPool.sol @@ -17,16 +17,16 @@ pragma experimental ABIEncoderV2; import "@balancer-labs/v2-interfaces/contracts/vault/IMinimalSwapInfoPool.sol"; -import "./BasePool.sol"; +import "./LegacyBasePool.sol"; /** - * @dev Extension of `BasePool`, adding a handler for `IMinimalSwapInfoPool.onSwap`. + * @dev Extension of `LegacyBasePool`, adding a handler for `IMinimalSwapInfoPool.onSwap`. * - * Derived contracts must call `BasePool`'s constructor, and implement `_onSwapGivenIn` and `_onSwapGivenOut` along with - * `BasePool`'s virtual functions. Inheriting from this contract lets derived contracts choose the Two Token or Minimal - * Swap Info specialization settings. + * Derived contracts must call `LegacyBasePool`'s constructor, and implement `_onSwapGivenIn` and `_onSwapGivenOut` + * along with `LegacyBasePool`'s virtual functions. Inheriting from this contract lets derived contracts choose the + * Two Token or Minimal Swap Info specialization settings. */ -abstract contract BaseMinimalSwapInfoPool is IMinimalSwapInfoPool, BasePool { +abstract contract BaseMinimalSwapInfoPool is IMinimalSwapInfoPool, LegacyBasePool { // Swap Hooks function onSwap( diff --git a/pkg/pool-utils/contracts/BasePool.sol b/pkg/pool-utils/contracts/LegacyBasePool.sol similarity index 99% rename from pkg/pool-utils/contracts/BasePool.sol rename to pkg/pool-utils/contracts/LegacyBasePool.sol index 8fcc35e2f9..1e4ad92128 100644 --- a/pkg/pool-utils/contracts/BasePool.sol +++ b/pkg/pool-utils/contracts/LegacyBasePool.sol @@ -54,7 +54,7 @@ import "./RecoveryMode.sol"; * BaseGeneralPool or BaseMinimalSwapInfoPool. Otherwise, subclasses must inherit from the corresponding interfaces * and implement the swap callbacks themselves. */ -abstract contract BasePool is +abstract contract LegacyBasePool is IBasePool, IControlledPool, BasePoolAuthorization, diff --git a/pkg/pool-utils/contracts/lib/ComposablePoolLib.sol b/pkg/pool-utils/contracts/lib/ComposablePoolLib.sol index 9e4ead9467..caea0fa294 100644 --- a/pkg/pool-utils/contracts/lib/ComposablePoolLib.sol +++ b/pkg/pool-utils/contracts/lib/ComposablePoolLib.sol @@ -18,6 +18,8 @@ import "@balancer-labs/v2-interfaces/contracts/solidity-utils/openzeppelin/IERC2 import "@balancer-labs/v2-solidity-utils/contracts/math/FixedPoint.sol"; +// solhint-disable no-inline-assembly + library ComposablePoolLib { using FixedPoint for uint256; diff --git a/pkg/pool-utils/contracts/test/MockBasePool.sol b/pkg/pool-utils/contracts/test/MockLegacyBasePool.sol similarity index 98% rename from pkg/pool-utils/contracts/test/MockBasePool.sol rename to pkg/pool-utils/contracts/test/MockLegacyBasePool.sol index 397a5bb1c9..6bd692562f 100644 --- a/pkg/pool-utils/contracts/test/MockBasePool.sol +++ b/pkg/pool-utils/contracts/test/MockLegacyBasePool.sol @@ -17,9 +17,9 @@ pragma experimental ABIEncoderV2; import "@balancer-labs/v2-interfaces/contracts/pool-weighted/WeightedPoolUserData.sol"; -import "../BasePool.sol"; +import "../LegacyBasePool.sol"; -contract MockBasePool is BasePool { +contract MockLegacyBasePool is LegacyBasePool { using BasePoolUserData for bytes; using WeightedPoolUserData for bytes; @@ -43,7 +43,7 @@ contract MockBasePool is BasePool { uint256 bufferPeriodDuration, address owner ) - BasePool( + LegacyBasePool( vault, specialization, name, diff --git a/pkg/pool-utils/test/BasePool.test.ts b/pkg/pool-utils/test/LegacyBasePool.test.ts similarity index 99% rename from pkg/pool-utils/test/BasePool.test.ts rename to pkg/pool-utils/test/LegacyBasePool.test.ts index 51df41dbfb..cd166bd93d 100644 --- a/pkg/pool-utils/test/BasePool.test.ts +++ b/pkg/pool-utils/test/LegacyBasePool.test.ts @@ -16,7 +16,7 @@ import TypesConverter from '@balancer-labs/v2-helpers/src/models/types/TypesConv import { random } from 'lodash'; import { defaultAbiCoder } from 'ethers/lib/utils'; -describe('BasePool', function () { +describe('LegacyBasePool', function () { let admin: SignerWithAddress, poolOwner: SignerWithAddress, deployer: SignerWithAddress, other: SignerWithAddress; let authorizer: Contract, vault: Contract; let tokens: TokenList; @@ -63,7 +63,7 @@ describe('BasePool', function () { if (!bufferPeriodDuration) bufferPeriodDuration = 0; if (!owner) owner = ZERO_ADDRESS; - return deploy('MockBasePool', { + return deploy('MockLegacyBasePool', { from: params.from, args: [ vault.address, diff --git a/pkg/pool-weighted/contracts/BaseWeightedPool.sol b/pkg/pool-weighted/contracts/BaseWeightedPool.sol index cdf838d1e0..8ec10c86da 100644 --- a/pkg/pool-weighted/contracts/BaseWeightedPool.sol +++ b/pkg/pool-weighted/contracts/BaseWeightedPool.sol @@ -46,7 +46,7 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool { address owner, bool mutableTokens ) - BasePool( + LegacyBasePool( vault, // Given BaseMinimalSwapInfoPool supports both of these specializations, and this Pool never registers // or deregisters any tokens after construction, picking Two Token when the Pool only has two tokens is free diff --git a/pkg/pool-weighted/contracts/WeightedPool.sol b/pkg/pool-weighted/contracts/WeightedPool.sol index efa78788f6..3a42677d9c 100644 --- a/pkg/pool-weighted/contracts/WeightedPool.sol +++ b/pkg/pool-weighted/contracts/WeightedPool.sol @@ -396,7 +396,7 @@ contract WeightedPool is BaseWeightedPool, WeightedPoolProtocolFees { internal view virtual - override(BasePool, WeightedPoolProtocolFees) + override(LegacyBasePool, WeightedPoolProtocolFees) returns (bool) { return super._isOwnerOnlyAction(actionId); diff --git a/pkg/pool-weighted/contracts/WeightedPoolProtocolFees.sol b/pkg/pool-weighted/contracts/WeightedPoolProtocolFees.sol index 8a4c537fbb..8257357e4d 100644 --- a/pkg/pool-weighted/contracts/WeightedPoolProtocolFees.sol +++ b/pkg/pool-weighted/contracts/WeightedPoolProtocolFees.sol @@ -351,7 +351,7 @@ abstract contract WeightedPoolProtocolFees is BaseWeightedPool, ProtocolFeeCache internal view virtual - override(BasePool, BasePoolAuthorization) + override(LegacyBasePool, BasePoolAuthorization) returns (bool) { return super._isOwnerOnlyAction(actionId); From 0b1df67af4588d0cddb1fd7af92c967d6108998d Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Wed, 26 Oct 2022 21:56:43 -0400 Subject: [PATCH 2/3] refactor: move new BasePool out of managed pool --- .../managed/vendor => pool-utils/contracts}/BasePool.sol | 6 +++--- .../contracts/test/MockBasePool.sol | 5 ++--- .../test/managed => pool-utils/test}/BasePool.test.ts | 2 +- pkg/pool-weighted/contracts/managed/ManagedPoolSettings.sol | 3 +-- 4 files changed, 7 insertions(+), 9 deletions(-) rename pkg/{pool-weighted/contracts/managed/vendor => pool-utils/contracts}/BasePool.sol (99%) rename pkg/{pool-weighted => pool-utils}/contracts/test/MockBasePool.sol (97%) rename pkg/{pool-weighted/test/managed => pool-utils/test}/BasePool.test.ts (99%) diff --git a/pkg/pool-weighted/contracts/managed/vendor/BasePool.sol b/pkg/pool-utils/contracts/BasePool.sol similarity index 99% rename from pkg/pool-weighted/contracts/managed/vendor/BasePool.sol rename to pkg/pool-utils/contracts/BasePool.sol index ee9c0d71ec..c3636fbd3c 100644 --- a/pkg/pool-weighted/contracts/managed/vendor/BasePool.sol +++ b/pkg/pool-utils/contracts/BasePool.sol @@ -22,9 +22,9 @@ import "@balancer-labs/v2-interfaces/contracts/vault/IMinimalSwapInfoPool.sol"; import "@balancer-labs/v2-solidity-utils/contracts/helpers/TemporarilyPausable.sol"; -import "@balancer-labs/v2-pool-utils/contracts/BalancerPoolToken.sol"; -import "@balancer-labs/v2-pool-utils/contracts/BasePoolAuthorization.sol"; -import "@balancer-labs/v2-pool-utils/contracts/RecoveryMode.sol"; +import "./BalancerPoolToken.sol"; +import "./BasePoolAuthorization.sol"; +import "./RecoveryMode.sol"; // solhint-disable max-states-count diff --git a/pkg/pool-weighted/contracts/test/MockBasePool.sol b/pkg/pool-utils/contracts/test/MockBasePool.sol similarity index 97% rename from pkg/pool-weighted/contracts/test/MockBasePool.sol rename to pkg/pool-utils/contracts/test/MockBasePool.sol index 4326cf403c..83067e0324 100644 --- a/pkg/pool-weighted/contracts/test/MockBasePool.sol +++ b/pkg/pool-utils/contracts/test/MockBasePool.sol @@ -17,9 +17,8 @@ pragma experimental ABIEncoderV2; import "@balancer-labs/v2-interfaces/contracts/pool-weighted/WeightedPoolUserData.sol"; -import "@balancer-labs/v2-pool-utils/contracts/lib/PoolRegistrationLib.sol"; - -import "../managed/vendor/BasePool.sol"; +import "../lib/PoolRegistrationLib.sol"; +import "../BasePool.sol"; contract MockBasePool is BasePool { uint256 public constant ON_SWAP_MINIMAL_RETURN = 0xa987654321; diff --git a/pkg/pool-weighted/test/managed/BasePool.test.ts b/pkg/pool-utils/test/BasePool.test.ts similarity index 99% rename from pkg/pool-weighted/test/managed/BasePool.test.ts rename to pkg/pool-utils/test/BasePool.test.ts index e94546ba95..464740ff06 100644 --- a/pkg/pool-weighted/test/managed/BasePool.test.ts +++ b/pkg/pool-utils/test/BasePool.test.ts @@ -85,7 +85,7 @@ describe('BasePool', function () { if (!owner) owner = ZERO_ADDRESS; if (!from) from = deployer; - return deploy('v2-pool-weighted/MockBasePool', { + return deploy('MockBasePool', { from, args: [ vault.address, diff --git a/pkg/pool-weighted/contracts/managed/ManagedPoolSettings.sol b/pkg/pool-weighted/contracts/managed/ManagedPoolSettings.sol index 44086ec5b4..619362fb95 100644 --- a/pkg/pool-weighted/contracts/managed/ManagedPoolSettings.sol +++ b/pkg/pool-weighted/contracts/managed/ManagedPoolSettings.sol @@ -27,13 +27,12 @@ import "@balancer-labs/v2-pool-utils/contracts/lib/PoolRegistrationLib.sol"; import "@balancer-labs/v2-pool-utils/contracts/external-fees/InvariantGrowthProtocolSwapFees.sol"; import "@balancer-labs/v2-pool-utils/contracts/external-fees/ProtocolFeeCache.sol"; import "@balancer-labs/v2-pool-utils/contracts/external-fees/ExternalAUMFees.sol"; +import "@balancer-labs/v2-pool-utils/contracts/BasePool.sol"; import "../lib/GradualValueChange.sol"; import "../managed/CircuitBreakerStorageLib.sol"; import "../WeightedMath.sol"; -import "./vendor/BasePool.sol"; - import "./ManagedPoolStorageLib.sol"; import "./ManagedPoolAumStorageLib.sol"; import "./ManagedPoolTokenStorageLib.sol"; From cdcf6d23d036e4c5c1b53385c3175c5087bbbc56 Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Thu, 27 Oct 2022 10:44:04 -0400 Subject: [PATCH 3/3] refactor: rename again to reduce diff --- pkg/pool-linear/contracts/LinearPool.sol | 10 +- .../contracts/ComposableStablePool.sol | 4 +- .../ComposableStablePoolProtocolFees.sol | 2 +- .../contracts/ComposableStablePoolStorage.sol | 4 +- .../MockComposableStablePoolProtocolFees.sol | 4 +- .../test/MockComposableStablePoolRates.sol | 4 +- .../test/MockComposableStablePoolStorage.sol | 4 +- pkg/pool-utils/contracts/BaseGeneralPool.sol | 10 +- .../contracts/BaseMinimalSwapInfoPool.sol | 10 +- pkg/pool-utils/contracts/BasePool.sol | 461 ++++++-- .../{LegacyBasePool.sol => NewBasePool.sol} | 463 ++------ .../contracts/test/MockBasePool.sol | 141 +-- .../contracts/test/MockLegacyBasePool.sol | 173 --- .../contracts/test/MockNewBasePool.sol | 160 +++ pkg/pool-utils/test/BasePool.test.ts | 891 +++++++--------- pkg/pool-utils/test/LegacyBasePool.test.ts | 850 --------------- pkg/pool-utils/test/NewBasePool.test.ts | 997 ++++++++++++++++++ .../contracts/BaseWeightedPool.sol | 2 +- pkg/pool-weighted/contracts/WeightedPool.sol | 2 +- .../contracts/WeightedPoolProtocolFees.sol | 2 +- .../contracts/managed/ManagedPool.sol | 2 +- .../contracts/managed/ManagedPoolSettings.sol | 4 +- .../test/MockManagedPoolSettings.sol | 2 +- 23 files changed, 2101 insertions(+), 2101 deletions(-) rename pkg/pool-utils/contracts/{LegacyBasePool.sol => NewBasePool.sol} (58%) delete mode 100644 pkg/pool-utils/contracts/test/MockLegacyBasePool.sol create mode 100644 pkg/pool-utils/contracts/test/MockNewBasePool.sol delete mode 100644 pkg/pool-utils/test/LegacyBasePool.test.ts create mode 100644 pkg/pool-utils/test/NewBasePool.test.ts diff --git a/pkg/pool-linear/contracts/LinearPool.sol b/pkg/pool-linear/contracts/LinearPool.sol index 6d61926bd1..b6a05ca669 100644 --- a/pkg/pool-linear/contracts/LinearPool.sol +++ b/pkg/pool-linear/contracts/LinearPool.sol @@ -21,7 +21,7 @@ import "@balancer-labs/v2-interfaces/contracts/pool-utils/IRateProvider.sol"; import "@balancer-labs/v2-interfaces/contracts/pool-linear/ILinearPool.sol"; import "@balancer-labs/v2-interfaces/contracts/vault/IGeneralPool.sol"; -import "@balancer-labs/v2-pool-utils/contracts/LegacyBasePool.sol"; +import "@balancer-labs/v2-pool-utils/contracts/BasePool.sol"; import "@balancer-labs/v2-pool-utils/contracts/rates/PriceRateCache.sol"; import "@balancer-labs/v2-solidity-utils/contracts/helpers/ERC20Helpers.sol"; @@ -38,7 +38,7 @@ import "./LinearMath.sol"; * The Pool will register three tokens in the Vault however: the two assets and the BPT itself, * so that BPT can be exchanged (effectively joining and exiting) via swaps. * - * Despite inheriting from LegacyBasePool, much of the basic behavior changes. This Pool does not support regular joins + * Despite inheriting from BasePool, much of the basic behavior changes. This Pool does not support regular joins * and exits, as the initial BPT supply is 'preminted' during initialization. No further BPT can be minted, and BPT can * only be burned if governance enables Recovery Mode and LPs use it to exit proportionally. * @@ -51,7 +51,7 @@ import "./LinearMath.sol"; * The net revenue via fees is expected to be zero: all collected fees are used to pay for this 'rebalancing'. * Accordingly, this Pool does not pay any protocol fees. */ -abstract contract LinearPool is ILinearPool, IGeneralPool, IRateProvider, LegacyBasePool { +abstract contract LinearPool is ILinearPool, IGeneralPool, IRateProvider, BasePool { using WordCodec for bytes32; using FixedPoint for uint256; using PriceRateCache for bytes32; @@ -86,7 +86,7 @@ abstract contract LinearPool is ILinearPool, IGeneralPool, IRateProvider, Legacy uint256 private immutable _scalingFactorMainToken; uint256 private immutable _scalingFactorWrappedToken; - // The lower and upper targets are in LegacyBasePool's misc data field, which has 192 bits available (as it shares + // The lower and upper targets are in BasePool's misc data field, which has 192 bits available (as it shares // the same storage slot as the swap fee percentage and recovery mode flag, which together take up 64 bits). // We use 64 of these 192 for the targets (32 for each). // @@ -124,7 +124,7 @@ abstract contract LinearPool is ILinearPool, IGeneralPool, IRateProvider, Legacy uint256 bufferPeriodDuration, address owner ) - LegacyBasePool( + BasePool( vault, IVault.PoolSpecialization.GENERAL, name, diff --git a/pkg/pool-stable/contracts/ComposableStablePool.sol b/pkg/pool-stable/contracts/ComposableStablePool.sol index 481e45d234..a1be4bd5e3 100644 --- a/pkg/pool-stable/contracts/ComposableStablePool.sol +++ b/pkg/pool-stable/contracts/ComposableStablePool.sol @@ -82,7 +82,7 @@ contract ComposableStablePool is } constructor(NewPoolParams memory params) - LegacyBasePool( + BasePool( params.vault, IVault.PoolSpecialization.GENERAL, params.name, @@ -1136,7 +1136,7 @@ contract ComposableStablePool is override( // Our inheritance pattern creates a small diamond that requires explicitly listing the parents here. // Each parent calls the `super` version, so linearization ensures all implementations are called. - LegacyBasePool, + BasePool, ComposableStablePoolProtocolFees, StablePoolAmplification, ComposableStablePoolRates diff --git a/pkg/pool-stable/contracts/ComposableStablePoolProtocolFees.sol b/pkg/pool-stable/contracts/ComposableStablePoolProtocolFees.sol index c048bd907a..9c8e5814b3 100644 --- a/pkg/pool-stable/contracts/ComposableStablePoolProtocolFees.sol +++ b/pkg/pool-stable/contracts/ComposableStablePoolProtocolFees.sol @@ -317,7 +317,7 @@ abstract contract ComposableStablePoolProtocolFees is override( // Our inheritance pattern creates a small diamond that requires explicitly listing the parents here. // Each parent calls the `super` version, so linearization ensures all implementations are called. - LegacyBasePool, + BasePool, BasePoolAuthorization, ComposableStablePoolRates ) diff --git a/pkg/pool-stable/contracts/ComposableStablePoolStorage.sol b/pkg/pool-stable/contracts/ComposableStablePoolStorage.sol index f65d572098..ae9396e3f9 100644 --- a/pkg/pool-stable/contracts/ComposableStablePoolStorage.sol +++ b/pkg/pool-stable/contracts/ComposableStablePoolStorage.sol @@ -18,11 +18,11 @@ import "@balancer-labs/v2-interfaces/contracts/solidity-utils/helpers/BalancerEr import "@balancer-labs/v2-interfaces/contracts/solidity-utils/openzeppelin/IERC20.sol"; import "@balancer-labs/v2-interfaces/contracts/pool-utils/IRateProvider.sol"; -import "@balancer-labs/v2-pool-utils/contracts/LegacyBasePool.sol"; +import "@balancer-labs/v2-pool-utils/contracts/BasePool.sol"; import "./StableMath.sol"; -abstract contract ComposableStablePoolStorage is LegacyBasePool { +abstract contract ComposableStablePoolStorage is BasePool { using FixedPoint for uint256; using WordCodec for bytes32; diff --git a/pkg/pool-stable/contracts/test/MockComposableStablePoolProtocolFees.sol b/pkg/pool-stable/contracts/test/MockComposableStablePoolProtocolFees.sol index ccfe5bca38..e3165e45cc 100644 --- a/pkg/pool-stable/contracts/test/MockComposableStablePoolProtocolFees.sol +++ b/pkg/pool-stable/contracts/test/MockComposableStablePoolProtocolFees.sol @@ -46,14 +46,14 @@ contract MockComposableStablePoolProtocolFees is ComposableStablePoolProtocolFee protocolFeeProvider, ProviderFeeIDs({ swap: ProtocolFeeType.SWAP, yield: ProtocolFeeType.YIELD, aum: ProtocolFeeType.AUM }) ) - LegacyBasePool( + BasePool( vault, IVault.PoolSpecialization.GENERAL, "MockStablePoolStorage", "MOCK_BPT", _insertSorted(tokens, IERC20(this)), new address[](tokens.length + 1), - 1e12, // LegacyBasePool._MIN_SWAP_FEE_PERCENTAGE + 1e12, // BasePool._MIN_SWAP_FEE_PERCENTAGE 0, 0, address(0) diff --git a/pkg/pool-stable/contracts/test/MockComposableStablePoolRates.sol b/pkg/pool-stable/contracts/test/MockComposableStablePoolRates.sol index f6d467d7ef..2e8a786e2a 100644 --- a/pkg/pool-stable/contracts/test/MockComposableStablePoolRates.sol +++ b/pkg/pool-stable/contracts/test/MockComposableStablePoolRates.sol @@ -31,14 +31,14 @@ contract MockComposableStablePoolRates is ComposableStablePoolRates { ComposableStablePoolStorage( StorageParams(_insertSorted(tokens, IERC20(this)), tokenRateProviders, exemptFromYieldProtocolFeeFlags) ) - LegacyBasePool( + BasePool( vault, IVault.PoolSpecialization.GENERAL, "MockStablePoolStorage", "MOCK_BPT", _insertSorted(tokens, IERC20(this)), new address[](tokens.length + 1), - 1e12, // LegacyBasePool._MIN_SWAP_FEE_PERCENTAGE + 1e12, // BasePool._MIN_SWAP_FEE_PERCENTAGE 0, 0, owner diff --git a/pkg/pool-stable/contracts/test/MockComposableStablePoolStorage.sol b/pkg/pool-stable/contracts/test/MockComposableStablePoolStorage.sol index 029e1e3fd0..e5d2ac631b 100644 --- a/pkg/pool-stable/contracts/test/MockComposableStablePoolStorage.sol +++ b/pkg/pool-stable/contracts/test/MockComposableStablePoolStorage.sol @@ -33,14 +33,14 @@ contract MockComposableStablePoolStorage is ComposableStablePoolStorage { exemptFromYieldProtocolFeeFlags: exemptFromYieldProtocolFeeFlags }) ) - LegacyBasePool( + BasePool( vault, IVault.PoolSpecialization.GENERAL, "MockComposableStablePoolStorage", "MOCK_BPT", _insertSorted(tokens, IERC20(this)), new address[](tokens.length + 1), - 1e12, // LegacyBasePool._MIN_SWAP_FEE_PERCENTAGE + 1e12, // BasePool._MIN_SWAP_FEE_PERCENTAGE 0, 0, address(0) diff --git a/pkg/pool-utils/contracts/BaseGeneralPool.sol b/pkg/pool-utils/contracts/BaseGeneralPool.sol index 94aed60cd7..bfbd43a3c1 100644 --- a/pkg/pool-utils/contracts/BaseGeneralPool.sol +++ b/pkg/pool-utils/contracts/BaseGeneralPool.sol @@ -17,16 +17,16 @@ pragma experimental ABIEncoderV2; import "@balancer-labs/v2-interfaces/contracts/vault/IGeneralPool.sol"; -import "./LegacyBasePool.sol"; +import "./BasePool.sol"; /** - * @dev Extension of `LegacyBasePool`, adding a handler for `IGeneralPool.onSwap`. + * @dev Extension of `BasePool`, adding a handler for `IGeneralPool.onSwap`. * - * Derived contracts must call `LegacyBasePool`'s constructor, and implement `_onSwapGivenIn` and `_onSwapGivenOut` - * along with `LegacyBasePool`'s virtual functions. Inheriting from this contract lets derived contracts choose the + * Derived contracts must call `BasePool`'s constructor, and implement `_onSwapGivenIn` and `_onSwapGivenOut` + * along with `BasePool`'s virtual functions. Inheriting from this contract lets derived contracts choose the * General specialization setting. */ -abstract contract BaseGeneralPool is IGeneralPool, LegacyBasePool { +abstract contract BaseGeneralPool is IGeneralPool, BasePool { // Swap Hooks function onSwap( diff --git a/pkg/pool-utils/contracts/BaseMinimalSwapInfoPool.sol b/pkg/pool-utils/contracts/BaseMinimalSwapInfoPool.sol index e988e26a7a..7828e5eae0 100644 --- a/pkg/pool-utils/contracts/BaseMinimalSwapInfoPool.sol +++ b/pkg/pool-utils/contracts/BaseMinimalSwapInfoPool.sol @@ -17,16 +17,16 @@ pragma experimental ABIEncoderV2; import "@balancer-labs/v2-interfaces/contracts/vault/IMinimalSwapInfoPool.sol"; -import "./LegacyBasePool.sol"; +import "./BasePool.sol"; /** - * @dev Extension of `LegacyBasePool`, adding a handler for `IMinimalSwapInfoPool.onSwap`. + * @dev Extension of `BasePool`, adding a handler for `IMinimalSwapInfoPool.onSwap`. * - * Derived contracts must call `LegacyBasePool`'s constructor, and implement `_onSwapGivenIn` and `_onSwapGivenOut` - * along with `LegacyBasePool`'s virtual functions. Inheriting from this contract lets derived contracts choose the + * Derived contracts must call `BasePool`'s constructor, and implement `_onSwapGivenIn` and `_onSwapGivenOut` + * along with `BasePool`'s virtual functions. Inheriting from this contract lets derived contracts choose the * Two Token or Minimal Swap Info specialization settings. */ -abstract contract BaseMinimalSwapInfoPool is IMinimalSwapInfoPool, LegacyBasePool { +abstract contract BaseMinimalSwapInfoPool is IMinimalSwapInfoPool, BasePool { // Swap Hooks function onSwap( diff --git a/pkg/pool-utils/contracts/BasePool.sol b/pkg/pool-utils/contracts/BasePool.sol index c3636fbd3c..8fcc35e2f9 100644 --- a/pkg/pool-utils/contracts/BasePool.sol +++ b/pkg/pool-utils/contracts/BasePool.sol @@ -15,12 +15,19 @@ pragma solidity ^0.7.0; pragma experimental ABIEncoderV2; +import "@balancer-labs/v2-interfaces/contracts/pool-utils/IControlledPool.sol"; import "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol"; import "@balancer-labs/v2-interfaces/contracts/vault/IBasePool.sol"; -import "@balancer-labs/v2-interfaces/contracts/vault/IGeneralPool.sol"; -import "@balancer-labs/v2-interfaces/contracts/vault/IMinimalSwapInfoPool.sol"; +import "@balancer-labs/v2-solidity-utils/contracts/helpers/InputHelpers.sol"; +import "@balancer-labs/v2-solidity-utils/contracts/helpers/WordCodec.sol"; +import "@balancer-labs/v2-solidity-utils/contracts/helpers/ScalingHelpers.sol"; import "@balancer-labs/v2-solidity-utils/contracts/helpers/TemporarilyPausable.sol"; +import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/ERC20.sol"; +import "@balancer-labs/v2-solidity-utils/contracts/math/FixedPoint.sol"; +import "@balancer-labs/v2-solidity-utils/contracts/math/Math.sol"; + +import "./lib/PoolRegistrationLib.sol"; import "./BalancerPoolToken.sol"; import "./BasePoolAuthorization.sol"; @@ -49,27 +56,59 @@ import "./RecoveryMode.sol"; */ abstract contract BasePool is IBasePool, - IGeneralPool, - IMinimalSwapInfoPool, + IControlledPool, BasePoolAuthorization, BalancerPoolToken, TemporarilyPausable, RecoveryMode { + using WordCodec for bytes32; + using FixedPoint for uint256; using BasePoolUserData for bytes; + uint256 private constant _MIN_TOKENS = 2; + uint256 private constant _DEFAULT_MINIMUM_BPT = 1e6; + // 1e18 corresponds to 1.0, or a 100% fee + uint256 private constant _MIN_SWAP_FEE_PERCENTAGE = 1e12; // 0.0001% + uint256 private constant _MAX_SWAP_FEE_PERCENTAGE = 1e17; // 10% - this fits in 64 bits + + // `_miscData` is a storage slot that can be used to store unrelated pieces of information. All pools store the + // recovery mode flag and swap fee percentage, but `miscData` can be extended to store more pieces of information. + // The most signficant bit is reserved for the recovery mode flag, and the swap fee percentage is stored in + // the next most significant 63 bits, leaving the remaining 192 bits free to store any other information derived + // pools might need. + // + // This slot is preferred for gas-sensitive operations as it is read in all joins, swaps and exits, + // and therefore warm. + + // [ recovery | swap fee | available ] + // [ 1 bit | 63 bits | 192 bits ] + // [ MSB LSB ] + bytes32 private _miscData; + + uint256 private constant _SWAP_FEE_PERCENTAGE_OFFSET = 192; + uint256 private constant _RECOVERY_MODE_BIT_OFFSET = 255; + + // A fee can never be larger than FixedPoint.ONE, which fits in 60 bits, so 63 is more than enough. + uint256 private constant _SWAP_FEE_PERCENTAGE_BIT_LENGTH = 63; + bytes32 private immutable _poolId; // Note that this value is immutable in the Vault, so we can make it immutable here and save gas IProtocolFeesCollector private immutable _protocolFeesCollector; + event SwapFeePercentageChanged(uint256 swapFeePercentage); + constructor( IVault vault, - bytes32 poolId, + IVault.PoolSpecialization specialization, string memory name, string memory symbol, + IERC20[] memory tokens, + address[] memory assetManagers, + uint256 swapFeePercentage, uint256 pauseWindowDuration, uint256 bufferPeriodDuration, address owner @@ -84,12 +123,24 @@ abstract contract BasePool is BasePoolAuthorization(owner) TemporarilyPausable(pauseWindowDuration, bufferPeriodDuration) { + _require(tokens.length >= _MIN_TOKENS, Errors.MIN_TOKENS); + _require(tokens.length <= _getMaxTokens(), Errors.MAX_TOKENS); + + _setSwapFeePercentage(swapFeePercentage); + + bytes32 poolId = PoolRegistrationLib.registerPoolWithAssetManagers( + vault, + specialization, + tokens, + assetManagers + ); + // Set immutable state variables - these cannot be read from during construction _poolId = poolId; _protocolFeesCollector = vault.getProtocolFeesCollector(); } - // Getters + // Getters / Setters /** * @notice Return the pool id. @@ -98,13 +149,9 @@ abstract contract BasePool is return _poolId; } - function _getAuthorizer() internal view override returns (IAuthorizer) { - // Access control management is delegated to the Vault's Authorizer. This lets Balancer Governance manage which - // accounts can call permissioned functions: for example, to perform emergency pauses. - // If the owner is delegated, then *all* permissioned functions, including `updateSwapFeeGradually`, will be - // under Governance control. - return getVault().getAuthorizer(); - } + function _getTotalTokens() internal view virtual returns (uint256); + + function _getMaxTokens() internal pure virtual returns (uint256); /** * @dev Returns the minimum BPT supply. This amount is minted to the zero address during initialization, effectively @@ -113,11 +160,17 @@ abstract contract BasePool is * This is useful to make sure Pool initialization happens only once, but derived Pools can change this value (even * to zero) by overriding this function. */ - function _getMinimumBpt() internal pure returns (uint256) { + function _getMinimumBpt() internal pure virtual returns (uint256) { return _DEFAULT_MINIMUM_BPT; } - // Protocol Fees + /** + * @notice Return the current value of the swap fee percentage. + * @dev This is stored in `_miscData`. + */ + function getSwapFeePercentage() public view virtual override returns (uint256) { + return _miscData.decodeUint(_SWAP_FEE_PERCENTAGE_OFFSET, _SWAP_FEE_PERCENTAGE_BIT_LENGTH); + } /** * @notice Return the ProtocolFeesCollector contract. @@ -128,12 +181,61 @@ abstract contract BasePool is } /** - * @dev Pays protocol fees by minting `bptAmount` to the Protocol Fee Collector. + * @notice Set the swap fee percentage. + * @dev This is a permissioned function, and disabled if the pool is paused. The swap fee must be within the + * bounds set by MIN_SWAP_FEE_PERCENTAGE/MAX_SWAP_FEE_PERCENTAGE. Emits the SwapFeePercentageChanged event. */ - function _payProtocolFees(uint256 bptAmount) internal { - if (bptAmount > 0) { - _mintPoolTokens(address(getProtocolFeesCollector()), bptAmount); - } + function setSwapFeePercentage(uint256 swapFeePercentage) public virtual override authenticate whenNotPaused { + _setSwapFeePercentage(swapFeePercentage); + } + + function _setSwapFeePercentage(uint256 swapFeePercentage) internal virtual { + _require(swapFeePercentage >= _getMinSwapFeePercentage(), Errors.MIN_SWAP_FEE_PERCENTAGE); + _require(swapFeePercentage <= _getMaxSwapFeePercentage(), Errors.MAX_SWAP_FEE_PERCENTAGE); + + _miscData = _miscData.insertUint( + swapFeePercentage, + _SWAP_FEE_PERCENTAGE_OFFSET, + _SWAP_FEE_PERCENTAGE_BIT_LENGTH + ); + + emit SwapFeePercentageChanged(swapFeePercentage); + } + + function _getMinSwapFeePercentage() internal pure virtual returns (uint256) { + return _MIN_SWAP_FEE_PERCENTAGE; + } + + function _getMaxSwapFeePercentage() internal pure virtual returns (uint256) { + return _MAX_SWAP_FEE_PERCENTAGE; + } + + /** + * @notice Returns whether the pool is in Recovery Mode. + */ + function inRecoveryMode() public view override returns (bool) { + return _miscData.decodeBool(_RECOVERY_MODE_BIT_OFFSET); + } + + /** + * @dev Sets the recoveryMode state, and emits the corresponding event. + */ + function _setRecoveryMode(bool enabled) internal virtual override { + _miscData = _miscData.insertBool(enabled, _RECOVERY_MODE_BIT_OFFSET); + + emit RecoveryModeStateChanged(enabled); + + // Some pools need to update their state when leaving recovery mode to ensure proper functioning of the Pool. + // We do not allow an `_onEnableRecoveryMode()` hook as this may jeopardize the ability to enable Recovery mode. + if (!enabled) _onDisableRecoveryMode(); + } + + /** + * @dev Performs any necessary actions on the disabling of Recovery Mode. + * This is usually to reset any fee collection mechanisms to ensure that they operate correctly going forward. + */ + function _onDisableRecoveryMode() internal virtual { + // solhint-disable-previous-line no-empty-blocks } /** @@ -155,48 +257,30 @@ abstract contract BasePool is _setPaused(false); } - modifier onlyVault(bytes32 poolId) { - _require(msg.sender == address(getVault()), Errors.CALLER_NOT_VAULT); - _require(poolId == getPoolId(), Errors.INVALID_POOL_ID); - _; + function _isOwnerOnlyAction(bytes32 actionId) internal view virtual override returns (bool) { + return (actionId == getActionId(this.setSwapFeePercentage.selector)) || super._isOwnerOnlyAction(actionId); } - // Swap / Join / Exit Hooks - - function onSwap( - SwapRequest memory request, - uint256 balanceTokenIn, - uint256 balanceTokenOut - ) external override onlyVault(request.poolId) returns (uint256) { - _ensureNotPaused(); + function _getMiscData() internal view returns (bytes32) { + return _miscData; + } - return _onSwapMinimal(request, balanceTokenIn, balanceTokenOut); + /** + * @dev Inserts data into the least-significant 192 bits of the misc data storage slot. + * Note that the remaining 64 bits are used for the swap fee percentage and cannot be overloaded. + */ + function _setMiscData(bytes32 newData) internal { + _miscData = _miscData.insertBits192(newData, 0); } - function _onSwapMinimal( - SwapRequest memory request, - uint256 balanceTokenIn, - uint256 balanceTokenOut - ) internal virtual returns (uint256); + // Join / Exit Hooks - function onSwap( - SwapRequest memory request, - uint256[] memory balances, - uint256 indexIn, - uint256 indexOut - ) external override onlyVault(request.poolId) returns (uint256) { - _ensureNotPaused(); - - return _onSwapGeneral(request, balances, indexIn, indexOut); + modifier onlyVault(bytes32 poolId) { + _require(msg.sender == address(getVault()), Errors.CALLER_NOT_VAULT); + _require(poolId == getPoolId(), Errors.INVALID_POOL_ID); + _; } - function _onSwapGeneral( - SwapRequest memory request, - uint256[] memory balances, - uint256 indexIn, - uint256 indexOut - ) internal virtual returns (uint256); - /** * @notice Vault hook for adding liquidity to a pool (including the first time, "initializing" the pool). * @dev This function can only be called from the Vault, from `joinPool`. @@ -206,15 +290,22 @@ abstract contract BasePool is address sender, address recipient, uint256[] memory balances, - uint256, - uint256, + uint256 lastChangeBlock, + uint256 protocolSwapFeePercentage, bytes memory userData - ) external override onlyVault(poolId) returns (uint256[] memory amountsIn, uint256[] memory dueProtocolFees) { - uint256 bptAmountOut; + ) external override onlyVault(poolId) returns (uint256[] memory, uint256[] memory) { + _beforeSwapJoinExit(); + + uint256[] memory scalingFactors = _scalingFactors(); - _ensureNotPaused(); if (totalSupply() == 0) { - (bptAmountOut, amountsIn) = _onInitializePool(sender, userData); + (uint256 bptAmountOut, uint256[] memory amountsIn) = _onInitializePool( + poolId, + sender, + recipient, + scalingFactors, + userData + ); // On initialization, we lock _getMinimumBpt() by minting it for the zero address. This BPT acts as a // minimum as it will never be burned, which reduces potential issues with rounding, and also prevents the @@ -222,16 +313,34 @@ abstract contract BasePool is _require(bptAmountOut >= _getMinimumBpt(), Errors.MINIMUM_BPT); _mintPoolTokens(address(0), _getMinimumBpt()); _mintPoolTokens(recipient, bptAmountOut - _getMinimumBpt()); + + // amountsIn are amounts entering the Pool, so we round up. + _downscaleUpArray(amountsIn, scalingFactors); + + return (amountsIn, new uint256[](balances.length)); } else { - (bptAmountOut, amountsIn) = _onJoinPool(sender, balances, userData); + _upscaleArray(balances, scalingFactors); + (uint256 bptAmountOut, uint256[] memory amountsIn) = _onJoinPool( + poolId, + sender, + recipient, + balances, + lastChangeBlock, + inRecoveryMode() ? 0 : protocolSwapFeePercentage, // Protocol fees are disabled while in recovery mode + scalingFactors, + userData + ); // Note we no longer use `balances` after calling `_onJoinPool`, which may mutate it. _mintPoolTokens(recipient, bptAmountOut); - } - // This Pool ignores the `dueProtocolFees` return value, so we simply return a zeroed-out array. - dueProtocolFees = new uint256[](amountsIn.length); + // amountsIn are amounts entering the Pool, so we round up. + _downscaleUpArray(amountsIn, scalingFactors); + + // This Pool ignores the `dueProtocolFees` return value, so we simply return a zeroed-out array. + return (amountsIn, new uint256[](balances.length)); + } } /** @@ -241,12 +350,13 @@ abstract contract BasePool is function onExitPool( bytes32 poolId, address sender, - address, + address recipient, uint256[] memory balances, - uint256, - uint256, + uint256 lastChangeBlock, + uint256 protocolSwapFeePercentage, bytes memory userData - ) external override onlyVault(poolId) returns (uint256[] memory amountsOut, uint256[] memory dueProtocolFees) { + ) external override onlyVault(poolId) returns (uint256[] memory, uint256[] memory) { + uint256[] memory amountsOut; uint256 bptAmountIn; // When a user calls `exitPool`, this is the first point of entry from the Vault. @@ -262,9 +372,24 @@ abstract contract BasePool is (bptAmountIn, amountsOut) = _doRecoveryModeExit(balances, totalSupply(), userData); } else { // Note that we only call this if we're not in a recovery mode exit. - _ensureNotPaused(); - - (bptAmountIn, amountsOut) = _onExitPool(sender, balances, userData); + _beforeSwapJoinExit(); + + uint256[] memory scalingFactors = _scalingFactors(); + _upscaleArray(balances, scalingFactors); + + (bptAmountIn, amountsOut) = _onExitPool( + poolId, + sender, + recipient, + balances, + lastChangeBlock, + inRecoveryMode() ? 0 : protocolSwapFeePercentage, // Protocol fees are disabled while in recovery mode + scalingFactors, + userData + ); + + // amountsOut are amounts exiting the Pool, so we round down. + _downscaleDownArray(amountsOut, scalingFactors); } // Note we no longer use `balances` after calling `_onExitPool`, which may mutate it. @@ -272,7 +397,7 @@ abstract contract BasePool is _burnPoolTokens(sender, bptAmountIn); // This Pool ignores the `dueProtocolFees` return value, so we simply return a zeroed-out array. - dueProtocolFees = new uint256[](amountsOut.length); + return (amountsOut, new uint256[](balances.length)); } // Query functions @@ -289,15 +414,27 @@ abstract contract BasePool is * explicitly use eth_call instead of eth_sendTransaction. */ function queryJoin( - bytes32, + bytes32 poolId, address sender, - address, + address recipient, uint256[] memory balances, - uint256, - uint256, + uint256 lastChangeBlock, + uint256 protocolSwapFeePercentage, bytes memory userData ) external override returns (uint256 bptOut, uint256[] memory amountsIn) { - _queryAction(sender, balances, userData, _onJoinPool); + InputHelpers.ensureInputLengthMatch(balances.length, _getTotalTokens()); + + _queryAction( + poolId, + sender, + recipient, + balances, + lastChangeBlock, + protocolSwapFeePercentage, + userData, + _onJoinPool, + _downscaleUpArray + ); // The `return` opcode is executed directly inside `_queryAction`, so execution never reaches this statement, // and we don't need to return anything here - it just silences compiler warnings. @@ -316,15 +453,27 @@ abstract contract BasePool is * explicitly use eth_call instead of eth_sendTransaction. */ function queryExit( - bytes32, + bytes32 poolId, address sender, - address, + address recipient, uint256[] memory balances, - uint256, - uint256, + uint256 lastChangeBlock, + uint256 protocolSwapFeePercentage, bytes memory userData ) external override returns (uint256 bptIn, uint256[] memory amountsOut) { - _queryAction(sender, balances, userData, _onExitPool); + InputHelpers.ensureInputLengthMatch(balances.length, _getTotalTokens()); + + _queryAction( + poolId, + sender, + recipient, + balances, + lastChangeBlock, + protocolSwapFeePercentage, + userData, + _onExitPool, + _downscaleDownArray + ); // The `return` opcode is executed directly inside `_queryAction`, so execution never reaches this statement, // and we don't need to return anything here - it just silences compiler warnings. @@ -347,10 +496,13 @@ abstract contract BasePool is * The tokens granted to the Pool will be transferred from `sender`. These amounts are considered upscaled and will * be downscaled (rounding up) before being returned to the Vault. */ - function _onInitializePool(address sender, bytes memory userData) - internal - virtual - returns (uint256 bptAmountOut, uint256[] memory amountsIn); + function _onInitializePool( + bytes32 poolId, + address sender, + address recipient, + uint256[] memory scalingFactors, + bytes memory userData + ) internal virtual returns (uint256 bptAmountOut, uint256[] memory amountsIn); /** * @dev Called whenever the Pool is joined after the first initialization join (see `_onInitializePool`). @@ -370,8 +522,13 @@ abstract contract BasePool is * amounts are considered upscaled and will be downscaled (rounding down) before being returned to the Vault. */ function _onJoinPool( + bytes32 poolId, address sender, + address recipient, uint256[] memory balances, + uint256 lastChangeBlock, + uint256 protocolSwapFeePercentage, + uint256[] memory scalingFactors, bytes memory userData ) internal virtual returns (uint256 bptAmountOut, uint256[] memory amountsIn); @@ -393,16 +550,125 @@ abstract contract BasePool is * amounts are considered upscaled and will be downscaled (rounding down) before being returned to the Vault. */ function _onExitPool( + bytes32 poolId, address sender, + address recipient, uint256[] memory balances, + uint256 lastChangeBlock, + uint256 protocolSwapFeePercentage, + uint256[] memory scalingFactors, bytes memory userData ) internal virtual returns (uint256 bptAmountIn, uint256[] memory amountsOut); + /** + * @dev Called at the very beginning of swaps, joins and exits, even before the scaling factors are read. Derived + * contracts can extend this implementation to perform any state-changing operations they might need (including e.g. + * updating the scaling factors), + * + * The only scenario in which this function is not called is during a recovery mode exit. This makes it safe to + * perform non-trivial computations or interact with external dependencies here, as recovery mode will not be + * affected. + * + * Since this contract does not implement swaps, derived contracts must also make sure this function is called on + * swap handlers. + */ + function _beforeSwapJoinExit() internal virtual { + // All joins, exits and swaps are disabled (except recovery mode exits). + _ensureNotPaused(); + } + + // Internal functions + + /** + * @dev Pays protocol fees by minting `bptAmount` to the Protocol Fee Collector. + */ + function _payProtocolFees(uint256 bptAmount) internal { + if (bptAmount > 0) { + _mintPoolTokens(address(getProtocolFeesCollector()), bptAmount); + } + } + + /** + * @dev Adds swap fee amount to `amount`, returning a higher value. + */ + function _addSwapFeeAmount(uint256 amount) internal view returns (uint256) { + // This returns amount + fee amount, so we round up (favoring a higher fee amount). + return amount.divUp(getSwapFeePercentage().complement()); + } + + /** + * @dev Subtracts swap fee amount from `amount`, returning a lower value. + */ + function _subtractSwapFeeAmount(uint256 amount) internal view returns (uint256) { + // This returns amount - fee amount, so we round up (favoring a higher fee amount). + uint256 feeAmount = amount.mulUp(getSwapFeePercentage()); + return amount.sub(feeAmount); + } + + // Scaling + + /** + * @dev Returns a scaling factor that, when multiplied to a token amount for `token`, normalizes its balance as if + * it had 18 decimals. + */ + function _computeScalingFactor(IERC20 token) internal view returns (uint256) { + if (address(token) == address(this)) { + return FixedPoint.ONE; + } + + // Tokens that don't implement the `decimals` method are not supported. + uint256 tokenDecimals = ERC20(address(token)).decimals(); + + // Tokens with more than 18 decimals are not supported. + uint256 decimalsDifference = Math.sub(18, tokenDecimals); + return FixedPoint.ONE * 10**decimalsDifference; + } + + /** + * @dev Returns the scaling factor for one of the Pool's tokens. Reverts if `token` is not a token registered by the + * Pool. + * + * All scaling factors are fixed-point values with 18 decimals, to allow for this function to be overridden by + * derived contracts that need to apply further scaling, making these factors potentially non-integer. + * + * The largest 'base' scaling factor (i.e. in tokens with less than 18 decimals) is 10**18, which in fixed-point is + * 10**36. This value can be multiplied with a 112 bit Vault balance with no overflow by a factor of ~1e7, making + * even relatively 'large' factors safe to use. + * + * The 1e7 figure is the result of 2**256 / (1e18 * 1e18 * 2**112). + */ + function _scalingFactor(IERC20 token) internal view virtual returns (uint256); + + /** + * @dev Same as `_scalingFactor()`, except for all registered tokens (in the same order as registered). The Vault + * will always pass balances in this order when calling any of the Pool hooks. + */ + function _scalingFactors() internal view virtual returns (uint256[] memory); + + function getScalingFactors() external view override returns (uint256[] memory) { + return _scalingFactors(); + } + + function _getAuthorizer() internal view override returns (IAuthorizer) { + // Access control management is delegated to the Vault's Authorizer. This lets Balancer Governance manage which + // accounts can call permissioned functions: for example, to perform emergency pauses. + // If the owner is delegated, then *all* permissioned functions, including `setSwapFeePercentage`, will be under + // Governance control. + return getVault().getAuthorizer(); + } + function _queryAction( + bytes32 poolId, address sender, + address recipient, uint256[] memory balances, + uint256 lastChangeBlock, + uint256 protocolSwapFeePercentage, bytes memory userData, - function(address, uint256[] memory, bytes memory) internal returns (uint256, uint256[] memory) _action + function(bytes32, address, address, uint256[] memory, uint256, uint256, uint256[] memory, bytes memory) + internal + returns (uint256, uint256[] memory) _action, + function(uint256[] memory, uint256[] memory) internal view _downscaleArray ) private { // This uses the same technique used by the Vault in queryBatchSwap. Refer to that function for a detailed // explanation. @@ -472,7 +738,26 @@ abstract contract BasePool is } } } else { - (uint256 bptAmount, uint256[] memory tokenAmounts) = _action(sender, balances, userData); + // This imitates the relevant parts of the bodies of onJoin and onExit. Since they're not virtual, we know + // that their implementations will match this regardless of what derived contracts might do. + + _beforeSwapJoinExit(); + + uint256[] memory scalingFactors = _scalingFactors(); + _upscaleArray(balances, scalingFactors); + + (uint256 bptAmount, uint256[] memory tokenAmounts) = _action( + poolId, + sender, + recipient, + balances, + lastChangeBlock, + protocolSwapFeePercentage, + scalingFactors, + userData + ); + + _downscaleArray(tokenAmounts, scalingFactors); // solhint-disable-next-line no-inline-assembly assembly { diff --git a/pkg/pool-utils/contracts/LegacyBasePool.sol b/pkg/pool-utils/contracts/NewBasePool.sol similarity index 58% rename from pkg/pool-utils/contracts/LegacyBasePool.sol rename to pkg/pool-utils/contracts/NewBasePool.sol index 1e4ad92128..b4bb2eade5 100644 --- a/pkg/pool-utils/contracts/LegacyBasePool.sol +++ b/pkg/pool-utils/contracts/NewBasePool.sol @@ -15,19 +15,12 @@ pragma solidity ^0.7.0; pragma experimental ABIEncoderV2; -import "@balancer-labs/v2-interfaces/contracts/pool-utils/IControlledPool.sol"; import "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol"; import "@balancer-labs/v2-interfaces/contracts/vault/IBasePool.sol"; +import "@balancer-labs/v2-interfaces/contracts/vault/IGeneralPool.sol"; +import "@balancer-labs/v2-interfaces/contracts/vault/IMinimalSwapInfoPool.sol"; -import "@balancer-labs/v2-solidity-utils/contracts/helpers/InputHelpers.sol"; -import "@balancer-labs/v2-solidity-utils/contracts/helpers/WordCodec.sol"; -import "@balancer-labs/v2-solidity-utils/contracts/helpers/ScalingHelpers.sol"; import "@balancer-labs/v2-solidity-utils/contracts/helpers/TemporarilyPausable.sol"; -import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/ERC20.sol"; -import "@balancer-labs/v2-solidity-utils/contracts/math/FixedPoint.sol"; -import "@balancer-labs/v2-solidity-utils/contracts/math/Math.sol"; - -import "./lib/PoolRegistrationLib.sol"; import "./BalancerPoolToken.sol"; import "./BasePoolAuthorization.sol"; @@ -54,61 +47,29 @@ import "./RecoveryMode.sol"; * BaseGeneralPool or BaseMinimalSwapInfoPool. Otherwise, subclasses must inherit from the corresponding interfaces * and implement the swap callbacks themselves. */ -abstract contract LegacyBasePool is +abstract contract NewBasePool is IBasePool, - IControlledPool, + IGeneralPool, + IMinimalSwapInfoPool, BasePoolAuthorization, BalancerPoolToken, TemporarilyPausable, RecoveryMode { - using WordCodec for bytes32; - using FixedPoint for uint256; using BasePoolUserData for bytes; - uint256 private constant _MIN_TOKENS = 2; - uint256 private constant _DEFAULT_MINIMUM_BPT = 1e6; - // 1e18 corresponds to 1.0, or a 100% fee - uint256 private constant _MIN_SWAP_FEE_PERCENTAGE = 1e12; // 0.0001% - uint256 private constant _MAX_SWAP_FEE_PERCENTAGE = 1e17; // 10% - this fits in 64 bits - - // `_miscData` is a storage slot that can be used to store unrelated pieces of information. All pools store the - // recovery mode flag and swap fee percentage, but `miscData` can be extended to store more pieces of information. - // The most signficant bit is reserved for the recovery mode flag, and the swap fee percentage is stored in - // the next most significant 63 bits, leaving the remaining 192 bits free to store any other information derived - // pools might need. - // - // This slot is preferred for gas-sensitive operations as it is read in all joins, swaps and exits, - // and therefore warm. - - // [ recovery | swap fee | available ] - // [ 1 bit | 63 bits | 192 bits ] - // [ MSB LSB ] - bytes32 private _miscData; - - uint256 private constant _SWAP_FEE_PERCENTAGE_OFFSET = 192; - uint256 private constant _RECOVERY_MODE_BIT_OFFSET = 255; - - // A fee can never be larger than FixedPoint.ONE, which fits in 60 bits, so 63 is more than enough. - uint256 private constant _SWAP_FEE_PERCENTAGE_BIT_LENGTH = 63; - bytes32 private immutable _poolId; // Note that this value is immutable in the Vault, so we can make it immutable here and save gas IProtocolFeesCollector private immutable _protocolFeesCollector; - event SwapFeePercentageChanged(uint256 swapFeePercentage); - constructor( IVault vault, - IVault.PoolSpecialization specialization, + bytes32 poolId, string memory name, string memory symbol, - IERC20[] memory tokens, - address[] memory assetManagers, - uint256 swapFeePercentage, uint256 pauseWindowDuration, uint256 bufferPeriodDuration, address owner @@ -123,24 +84,12 @@ abstract contract LegacyBasePool is BasePoolAuthorization(owner) TemporarilyPausable(pauseWindowDuration, bufferPeriodDuration) { - _require(tokens.length >= _MIN_TOKENS, Errors.MIN_TOKENS); - _require(tokens.length <= _getMaxTokens(), Errors.MAX_TOKENS); - - _setSwapFeePercentage(swapFeePercentage); - - bytes32 poolId = PoolRegistrationLib.registerPoolWithAssetManagers( - vault, - specialization, - tokens, - assetManagers - ); - // Set immutable state variables - these cannot be read from during construction _poolId = poolId; _protocolFeesCollector = vault.getProtocolFeesCollector(); } - // Getters / Setters + // Getters /** * @notice Return the pool id. @@ -149,9 +98,13 @@ abstract contract LegacyBasePool is return _poolId; } - function _getTotalTokens() internal view virtual returns (uint256); - - function _getMaxTokens() internal pure virtual returns (uint256); + function _getAuthorizer() internal view override returns (IAuthorizer) { + // Access control management is delegated to the Vault's Authorizer. This lets Balancer Governance manage which + // accounts can call permissioned functions: for example, to perform emergency pauses. + // If the owner is delegated, then *all* permissioned functions, including `updateSwapFeeGradually`, will be + // under Governance control. + return getVault().getAuthorizer(); + } /** * @dev Returns the minimum BPT supply. This amount is minted to the zero address during initialization, effectively @@ -160,17 +113,11 @@ abstract contract LegacyBasePool is * This is useful to make sure Pool initialization happens only once, but derived Pools can change this value (even * to zero) by overriding this function. */ - function _getMinimumBpt() internal pure virtual returns (uint256) { + function _getMinimumBpt() internal pure returns (uint256) { return _DEFAULT_MINIMUM_BPT; } - /** - * @notice Return the current value of the swap fee percentage. - * @dev This is stored in `_miscData`. - */ - function getSwapFeePercentage() public view virtual override returns (uint256) { - return _miscData.decodeUint(_SWAP_FEE_PERCENTAGE_OFFSET, _SWAP_FEE_PERCENTAGE_BIT_LENGTH); - } + // Protocol Fees /** * @notice Return the ProtocolFeesCollector contract. @@ -181,61 +128,12 @@ abstract contract LegacyBasePool is } /** - * @notice Set the swap fee percentage. - * @dev This is a permissioned function, and disabled if the pool is paused. The swap fee must be within the - * bounds set by MIN_SWAP_FEE_PERCENTAGE/MAX_SWAP_FEE_PERCENTAGE. Emits the SwapFeePercentageChanged event. - */ - function setSwapFeePercentage(uint256 swapFeePercentage) public virtual override authenticate whenNotPaused { - _setSwapFeePercentage(swapFeePercentage); - } - - function _setSwapFeePercentage(uint256 swapFeePercentage) internal virtual { - _require(swapFeePercentage >= _getMinSwapFeePercentage(), Errors.MIN_SWAP_FEE_PERCENTAGE); - _require(swapFeePercentage <= _getMaxSwapFeePercentage(), Errors.MAX_SWAP_FEE_PERCENTAGE); - - _miscData = _miscData.insertUint( - swapFeePercentage, - _SWAP_FEE_PERCENTAGE_OFFSET, - _SWAP_FEE_PERCENTAGE_BIT_LENGTH - ); - - emit SwapFeePercentageChanged(swapFeePercentage); - } - - function _getMinSwapFeePercentage() internal pure virtual returns (uint256) { - return _MIN_SWAP_FEE_PERCENTAGE; - } - - function _getMaxSwapFeePercentage() internal pure virtual returns (uint256) { - return _MAX_SWAP_FEE_PERCENTAGE; - } - - /** - * @notice Returns whether the pool is in Recovery Mode. - */ - function inRecoveryMode() public view override returns (bool) { - return _miscData.decodeBool(_RECOVERY_MODE_BIT_OFFSET); - } - - /** - * @dev Sets the recoveryMode state, and emits the corresponding event. - */ - function _setRecoveryMode(bool enabled) internal virtual override { - _miscData = _miscData.insertBool(enabled, _RECOVERY_MODE_BIT_OFFSET); - - emit RecoveryModeStateChanged(enabled); - - // Some pools need to update their state when leaving recovery mode to ensure proper functioning of the Pool. - // We do not allow an `_onEnableRecoveryMode()` hook as this may jeopardize the ability to enable Recovery mode. - if (!enabled) _onDisableRecoveryMode(); - } - - /** - * @dev Performs any necessary actions on the disabling of Recovery Mode. - * This is usually to reset any fee collection mechanisms to ensure that they operate correctly going forward. + * @dev Pays protocol fees by minting `bptAmount` to the Protocol Fee Collector. */ - function _onDisableRecoveryMode() internal virtual { - // solhint-disable-previous-line no-empty-blocks + function _payProtocolFees(uint256 bptAmount) internal { + if (bptAmount > 0) { + _mintPoolTokens(address(getProtocolFeesCollector()), bptAmount); + } } /** @@ -257,30 +155,48 @@ abstract contract LegacyBasePool is _setPaused(false); } - function _isOwnerOnlyAction(bytes32 actionId) internal view virtual override returns (bool) { - return (actionId == getActionId(this.setSwapFeePercentage.selector)) || super._isOwnerOnlyAction(actionId); + modifier onlyVault(bytes32 poolId) { + _require(msg.sender == address(getVault()), Errors.CALLER_NOT_VAULT); + _require(poolId == getPoolId(), Errors.INVALID_POOL_ID); + _; } - function _getMiscData() internal view returns (bytes32) { - return _miscData; - } + // Swap / Join / Exit Hooks - /** - * @dev Inserts data into the least-significant 192 bits of the misc data storage slot. - * Note that the remaining 64 bits are used for the swap fee percentage and cannot be overloaded. - */ - function _setMiscData(bytes32 newData) internal { - _miscData = _miscData.insertBits192(newData, 0); + function onSwap( + SwapRequest memory request, + uint256 balanceTokenIn, + uint256 balanceTokenOut + ) external override onlyVault(request.poolId) returns (uint256) { + _ensureNotPaused(); + + return _onSwapMinimal(request, balanceTokenIn, balanceTokenOut); } - // Join / Exit Hooks + function _onSwapMinimal( + SwapRequest memory request, + uint256 balanceTokenIn, + uint256 balanceTokenOut + ) internal virtual returns (uint256); - modifier onlyVault(bytes32 poolId) { - _require(msg.sender == address(getVault()), Errors.CALLER_NOT_VAULT); - _require(poolId == getPoolId(), Errors.INVALID_POOL_ID); - _; + function onSwap( + SwapRequest memory request, + uint256[] memory balances, + uint256 indexIn, + uint256 indexOut + ) external override onlyVault(request.poolId) returns (uint256) { + _ensureNotPaused(); + + return _onSwapGeneral(request, balances, indexIn, indexOut); } + function _onSwapGeneral( + SwapRequest memory request, + uint256[] memory balances, + uint256 indexIn, + uint256 indexOut + ) internal virtual returns (uint256); + /** * @notice Vault hook for adding liquidity to a pool (including the first time, "initializing" the pool). * @dev This function can only be called from the Vault, from `joinPool`. @@ -290,22 +206,15 @@ abstract contract LegacyBasePool is address sender, address recipient, uint256[] memory balances, - uint256 lastChangeBlock, - uint256 protocolSwapFeePercentage, + uint256, + uint256, bytes memory userData - ) external override onlyVault(poolId) returns (uint256[] memory, uint256[] memory) { - _beforeSwapJoinExit(); - - uint256[] memory scalingFactors = _scalingFactors(); + ) external override onlyVault(poolId) returns (uint256[] memory amountsIn, uint256[] memory dueProtocolFees) { + uint256 bptAmountOut; + _ensureNotPaused(); if (totalSupply() == 0) { - (uint256 bptAmountOut, uint256[] memory amountsIn) = _onInitializePool( - poolId, - sender, - recipient, - scalingFactors, - userData - ); + (bptAmountOut, amountsIn) = _onInitializePool(sender, userData); // On initialization, we lock _getMinimumBpt() by minting it for the zero address. This BPT acts as a // minimum as it will never be burned, which reduces potential issues with rounding, and also prevents the @@ -313,34 +222,16 @@ abstract contract LegacyBasePool is _require(bptAmountOut >= _getMinimumBpt(), Errors.MINIMUM_BPT); _mintPoolTokens(address(0), _getMinimumBpt()); _mintPoolTokens(recipient, bptAmountOut - _getMinimumBpt()); - - // amountsIn are amounts entering the Pool, so we round up. - _downscaleUpArray(amountsIn, scalingFactors); - - return (amountsIn, new uint256[](balances.length)); } else { - _upscaleArray(balances, scalingFactors); - (uint256 bptAmountOut, uint256[] memory amountsIn) = _onJoinPool( - poolId, - sender, - recipient, - balances, - lastChangeBlock, - inRecoveryMode() ? 0 : protocolSwapFeePercentage, // Protocol fees are disabled while in recovery mode - scalingFactors, - userData - ); + (bptAmountOut, amountsIn) = _onJoinPool(sender, balances, userData); // Note we no longer use `balances` after calling `_onJoinPool`, which may mutate it. _mintPoolTokens(recipient, bptAmountOut); - - // amountsIn are amounts entering the Pool, so we round up. - _downscaleUpArray(amountsIn, scalingFactors); - - // This Pool ignores the `dueProtocolFees` return value, so we simply return a zeroed-out array. - return (amountsIn, new uint256[](balances.length)); } + + // This Pool ignores the `dueProtocolFees` return value, so we simply return a zeroed-out array. + dueProtocolFees = new uint256[](amountsIn.length); } /** @@ -350,13 +241,12 @@ abstract contract LegacyBasePool is function onExitPool( bytes32 poolId, address sender, - address recipient, + address, uint256[] memory balances, - uint256 lastChangeBlock, - uint256 protocolSwapFeePercentage, + uint256, + uint256, bytes memory userData - ) external override onlyVault(poolId) returns (uint256[] memory, uint256[] memory) { - uint256[] memory amountsOut; + ) external override onlyVault(poolId) returns (uint256[] memory amountsOut, uint256[] memory dueProtocolFees) { uint256 bptAmountIn; // When a user calls `exitPool`, this is the first point of entry from the Vault. @@ -372,24 +262,9 @@ abstract contract LegacyBasePool is (bptAmountIn, amountsOut) = _doRecoveryModeExit(balances, totalSupply(), userData); } else { // Note that we only call this if we're not in a recovery mode exit. - _beforeSwapJoinExit(); - - uint256[] memory scalingFactors = _scalingFactors(); - _upscaleArray(balances, scalingFactors); - - (bptAmountIn, amountsOut) = _onExitPool( - poolId, - sender, - recipient, - balances, - lastChangeBlock, - inRecoveryMode() ? 0 : protocolSwapFeePercentage, // Protocol fees are disabled while in recovery mode - scalingFactors, - userData - ); - - // amountsOut are amounts exiting the Pool, so we round down. - _downscaleDownArray(amountsOut, scalingFactors); + _ensureNotPaused(); + + (bptAmountIn, amountsOut) = _onExitPool(sender, balances, userData); } // Note we no longer use `balances` after calling `_onExitPool`, which may mutate it. @@ -397,7 +272,7 @@ abstract contract LegacyBasePool is _burnPoolTokens(sender, bptAmountIn); // This Pool ignores the `dueProtocolFees` return value, so we simply return a zeroed-out array. - return (amountsOut, new uint256[](balances.length)); + dueProtocolFees = new uint256[](amountsOut.length); } // Query functions @@ -414,27 +289,15 @@ abstract contract LegacyBasePool is * explicitly use eth_call instead of eth_sendTransaction. */ function queryJoin( - bytes32 poolId, + bytes32, address sender, - address recipient, + address, uint256[] memory balances, - uint256 lastChangeBlock, - uint256 protocolSwapFeePercentage, + uint256, + uint256, bytes memory userData ) external override returns (uint256 bptOut, uint256[] memory amountsIn) { - InputHelpers.ensureInputLengthMatch(balances.length, _getTotalTokens()); - - _queryAction( - poolId, - sender, - recipient, - balances, - lastChangeBlock, - protocolSwapFeePercentage, - userData, - _onJoinPool, - _downscaleUpArray - ); + _queryAction(sender, balances, userData, _onJoinPool); // The `return` opcode is executed directly inside `_queryAction`, so execution never reaches this statement, // and we don't need to return anything here - it just silences compiler warnings. @@ -453,27 +316,15 @@ abstract contract LegacyBasePool is * explicitly use eth_call instead of eth_sendTransaction. */ function queryExit( - bytes32 poolId, + bytes32, address sender, - address recipient, + address, uint256[] memory balances, - uint256 lastChangeBlock, - uint256 protocolSwapFeePercentage, + uint256, + uint256, bytes memory userData ) external override returns (uint256 bptIn, uint256[] memory amountsOut) { - InputHelpers.ensureInputLengthMatch(balances.length, _getTotalTokens()); - - _queryAction( - poolId, - sender, - recipient, - balances, - lastChangeBlock, - protocolSwapFeePercentage, - userData, - _onExitPool, - _downscaleDownArray - ); + _queryAction(sender, balances, userData, _onExitPool); // The `return` opcode is executed directly inside `_queryAction`, so execution never reaches this statement, // and we don't need to return anything here - it just silences compiler warnings. @@ -496,13 +347,10 @@ abstract contract LegacyBasePool is * The tokens granted to the Pool will be transferred from `sender`. These amounts are considered upscaled and will * be downscaled (rounding up) before being returned to the Vault. */ - function _onInitializePool( - bytes32 poolId, - address sender, - address recipient, - uint256[] memory scalingFactors, - bytes memory userData - ) internal virtual returns (uint256 bptAmountOut, uint256[] memory amountsIn); + function _onInitializePool(address sender, bytes memory userData) + internal + virtual + returns (uint256 bptAmountOut, uint256[] memory amountsIn); /** * @dev Called whenever the Pool is joined after the first initialization join (see `_onInitializePool`). @@ -522,13 +370,8 @@ abstract contract LegacyBasePool is * amounts are considered upscaled and will be downscaled (rounding down) before being returned to the Vault. */ function _onJoinPool( - bytes32 poolId, address sender, - address recipient, uint256[] memory balances, - uint256 lastChangeBlock, - uint256 protocolSwapFeePercentage, - uint256[] memory scalingFactors, bytes memory userData ) internal virtual returns (uint256 bptAmountOut, uint256[] memory amountsIn); @@ -550,125 +393,16 @@ abstract contract LegacyBasePool is * amounts are considered upscaled and will be downscaled (rounding down) before being returned to the Vault. */ function _onExitPool( - bytes32 poolId, address sender, - address recipient, uint256[] memory balances, - uint256 lastChangeBlock, - uint256 protocolSwapFeePercentage, - uint256[] memory scalingFactors, bytes memory userData ) internal virtual returns (uint256 bptAmountIn, uint256[] memory amountsOut); - /** - * @dev Called at the very beginning of swaps, joins and exits, even before the scaling factors are read. Derived - * contracts can extend this implementation to perform any state-changing operations they might need (including e.g. - * updating the scaling factors), - * - * The only scenario in which this function is not called is during a recovery mode exit. This makes it safe to - * perform non-trivial computations or interact with external dependencies here, as recovery mode will not be - * affected. - * - * Since this contract does not implement swaps, derived contracts must also make sure this function is called on - * swap handlers. - */ - function _beforeSwapJoinExit() internal virtual { - // All joins, exits and swaps are disabled (except recovery mode exits). - _ensureNotPaused(); - } - - // Internal functions - - /** - * @dev Pays protocol fees by minting `bptAmount` to the Protocol Fee Collector. - */ - function _payProtocolFees(uint256 bptAmount) internal { - if (bptAmount > 0) { - _mintPoolTokens(address(getProtocolFeesCollector()), bptAmount); - } - } - - /** - * @dev Adds swap fee amount to `amount`, returning a higher value. - */ - function _addSwapFeeAmount(uint256 amount) internal view returns (uint256) { - // This returns amount + fee amount, so we round up (favoring a higher fee amount). - return amount.divUp(getSwapFeePercentage().complement()); - } - - /** - * @dev Subtracts swap fee amount from `amount`, returning a lower value. - */ - function _subtractSwapFeeAmount(uint256 amount) internal view returns (uint256) { - // This returns amount - fee amount, so we round up (favoring a higher fee amount). - uint256 feeAmount = amount.mulUp(getSwapFeePercentage()); - return amount.sub(feeAmount); - } - - // Scaling - - /** - * @dev Returns a scaling factor that, when multiplied to a token amount for `token`, normalizes its balance as if - * it had 18 decimals. - */ - function _computeScalingFactor(IERC20 token) internal view returns (uint256) { - if (address(token) == address(this)) { - return FixedPoint.ONE; - } - - // Tokens that don't implement the `decimals` method are not supported. - uint256 tokenDecimals = ERC20(address(token)).decimals(); - - // Tokens with more than 18 decimals are not supported. - uint256 decimalsDifference = Math.sub(18, tokenDecimals); - return FixedPoint.ONE * 10**decimalsDifference; - } - - /** - * @dev Returns the scaling factor for one of the Pool's tokens. Reverts if `token` is not a token registered by the - * Pool. - * - * All scaling factors are fixed-point values with 18 decimals, to allow for this function to be overridden by - * derived contracts that need to apply further scaling, making these factors potentially non-integer. - * - * The largest 'base' scaling factor (i.e. in tokens with less than 18 decimals) is 10**18, which in fixed-point is - * 10**36. This value can be multiplied with a 112 bit Vault balance with no overflow by a factor of ~1e7, making - * even relatively 'large' factors safe to use. - * - * The 1e7 figure is the result of 2**256 / (1e18 * 1e18 * 2**112). - */ - function _scalingFactor(IERC20 token) internal view virtual returns (uint256); - - /** - * @dev Same as `_scalingFactor()`, except for all registered tokens (in the same order as registered). The Vault - * will always pass balances in this order when calling any of the Pool hooks. - */ - function _scalingFactors() internal view virtual returns (uint256[] memory); - - function getScalingFactors() external view override returns (uint256[] memory) { - return _scalingFactors(); - } - - function _getAuthorizer() internal view override returns (IAuthorizer) { - // Access control management is delegated to the Vault's Authorizer. This lets Balancer Governance manage which - // accounts can call permissioned functions: for example, to perform emergency pauses. - // If the owner is delegated, then *all* permissioned functions, including `setSwapFeePercentage`, will be under - // Governance control. - return getVault().getAuthorizer(); - } - function _queryAction( - bytes32 poolId, address sender, - address recipient, uint256[] memory balances, - uint256 lastChangeBlock, - uint256 protocolSwapFeePercentage, bytes memory userData, - function(bytes32, address, address, uint256[] memory, uint256, uint256, uint256[] memory, bytes memory) - internal - returns (uint256, uint256[] memory) _action, - function(uint256[] memory, uint256[] memory) internal view _downscaleArray + function(address, uint256[] memory, bytes memory) internal returns (uint256, uint256[] memory) _action ) private { // This uses the same technique used by the Vault in queryBatchSwap. Refer to that function for a detailed // explanation. @@ -738,26 +472,7 @@ abstract contract LegacyBasePool is } } } else { - // This imitates the relevant parts of the bodies of onJoin and onExit. Since they're not virtual, we know - // that their implementations will match this regardless of what derived contracts might do. - - _beforeSwapJoinExit(); - - uint256[] memory scalingFactors = _scalingFactors(); - _upscaleArray(balances, scalingFactors); - - (uint256 bptAmount, uint256[] memory tokenAmounts) = _action( - poolId, - sender, - recipient, - balances, - lastChangeBlock, - protocolSwapFeePercentage, - scalingFactors, - userData - ); - - _downscaleArray(tokenAmounts, scalingFactors); + (uint256 bptAmount, uint256[] memory tokenAmounts) = _action(sender, balances, userData); // solhint-disable-next-line no-inline-assembly assembly { diff --git a/pkg/pool-utils/contracts/test/MockBasePool.sol b/pkg/pool-utils/contracts/test/MockBasePool.sol index 83067e0324..397a5bb1c9 100644 --- a/pkg/pool-utils/contracts/test/MockBasePool.sol +++ b/pkg/pool-utils/contracts/test/MockBasePool.sol @@ -17,25 +17,18 @@ pragma experimental ABIEncoderV2; import "@balancer-labs/v2-interfaces/contracts/pool-weighted/WeightedPoolUserData.sol"; -import "../lib/PoolRegistrationLib.sol"; import "../BasePool.sol"; contract MockBasePool is BasePool { - uint256 public constant ON_SWAP_MINIMAL_RETURN = 0xa987654321; - uint256 public constant ON_SWAP_GENERAL_RETURN = 0x123456789a; - uint256 public constant ON_JOIN_RETURN = 0xbbaa11; - uint256 public constant ON_EXIT_RETURN = 0x11aabb; - using BasePoolUserData for bytes; using WeightedPoolUserData for bytes; - bool private _inRecoveryMode; + uint256 private immutable _totalTokens; + + bool private _failBeforeSwapJoinExit; - event InnerOnInitializePoolCalled(bytes userData); - event InnerOnSwapMinimalCalled(SwapRequest request, uint256 balanceTokenIn, uint256 balanceTokenOut); - event InnerOnSwapGeneralCalled(SwapRequest request, uint256[] balances, uint256 indexIn, uint256 indexOut); - event InnerOnJoinPoolCalled(address sender, uint256[] balances, bytes userData); - event InnerOnExitPoolCalled(address sender, uint256[] balances, bytes userData); + event InnerOnJoinPoolCalled(uint256 protocolSwapFeePercentage); + event InnerOnExitPoolCalled(uint256 protocolSwapFeePercentage); event RecoveryModeExit(uint256 totalSupply, uint256[] balances, uint256 bptAmountIn); constructor( @@ -45,24 +38,43 @@ contract MockBasePool is BasePool { string memory symbol, IERC20[] memory tokens, address[] memory assetManagers, + uint256 swapFeePercentage, uint256 pauseWindowDuration, uint256 bufferPeriodDuration, address owner ) BasePool( vault, - PoolRegistrationLib.registerPoolWithAssetManagers(vault, specialization, tokens, assetManagers), + specialization, name, symbol, + tokens, + assetManagers, + swapFeePercentage, pauseWindowDuration, bufferPeriodDuration, owner ) - {} + { + _failBeforeSwapJoinExit = false; + _totalTokens = tokens.length; + } - function _onInitializePool(address, bytes memory userData) internal override returns (uint256, uint256[] memory) { - emit InnerOnInitializePoolCalled(userData); + function setMiscData(bytes32 data) external { + _setMiscData(data); + } + function getMiscData() external view returns (bytes32) { + return _getMiscData(); + } + + function _onInitializePool( + bytes32, + address, + address, + uint256[] memory, + bytes memory userData + ) internal pure override returns (uint256, uint256[] memory) { uint256[] memory amountsIn = userData.initialAmountsIn(); uint256 bptAmountOut; @@ -73,81 +85,82 @@ contract MockBasePool is BasePool { return (bptAmountOut, amountsIn); } - function _onSwapMinimal( - SwapRequest memory request, - uint256 balanceTokenIn, - uint256 balanceTokenOut - ) internal override returns (uint256) { - emit InnerOnSwapMinimalCalled(request, balanceTokenIn, balanceTokenOut); - return ON_SWAP_MINIMAL_RETURN; - } - - function _onSwapGeneral( - SwapRequest memory request, - uint256[] memory balances, - uint256 indexIn, - uint256 indexOut - ) internal override returns (uint256) { - emit InnerOnSwapGeneralCalled(request, balances, indexIn, indexOut); - return ON_SWAP_GENERAL_RETURN; - } - function _onJoinPool( - address sender, + bytes32, + address, + address, uint256[] memory balances, - bytes memory userData + uint256, + uint256 protocolSwapFeePercentage, + uint256[] memory, + bytes memory ) internal override returns (uint256, uint256[] memory) { - emit InnerOnJoinPoolCalled(sender, balances, userData); + emit InnerOnJoinPoolCalled(protocolSwapFeePercentage); - uint256[] memory amountsIn = new uint256[](balances.length); - for (uint256 i = 0; i < amountsIn.length; ++i) { - amountsIn[i] = ON_JOIN_RETURN; - } - return (0, amountsIn); + return (0, new uint256[](balances.length)); } function _onExitPool( - address sender, + bytes32, + address, + address, uint256[] memory balances, - bytes memory userData + uint256, + uint256 protocolSwapFeePercentage, + uint256[] memory, + bytes memory ) internal override returns (uint256, uint256[] memory) { - emit InnerOnExitPoolCalled(sender, balances, userData); + emit InnerOnExitPoolCalled(protocolSwapFeePercentage); - uint256[] memory amountsOut = new uint256[](balances.length); - for (uint256 i = 0; i < amountsOut.length; ++i) { - amountsOut[i] = ON_EXIT_RETURN; - } - return (0, amountsOut); + return (0, new uint256[](balances.length)); } - function inRecoveryMode() public view override returns (bool) { - return _inRecoveryMode; + function setFailBeforeSwapJoinExit(bool fail) external { + _failBeforeSwapJoinExit = fail; } - function _setRecoveryMode(bool enabled) internal override { - _inRecoveryMode = enabled; + function _beforeSwapJoinExit() internal override { + require(!_failBeforeSwapJoinExit, "FAIL_BEFORE_SWAP_JOIN_EXIT"); + super._beforeSwapJoinExit(); } - function getScalingFactors() external pure override returns (uint256[] memory) { - _revert(Errors.UNIMPLEMENTED); + function payProtocolFees(uint256 bptAmount) public { + _payProtocolFees(bptAmount); } - function getSwapFeePercentage() external pure override returns (uint256) { - _revert(Errors.UNIMPLEMENTED); + function _getMaxTokens() internal pure override returns (uint256) { + return 8; } - function payProtocolFees(uint256 bptAmount) public { - _payProtocolFees(bptAmount); + function _getTotalTokens() internal view override returns (uint256) { + return _totalTokens; + } + + function _scalingFactor(IERC20) internal pure override returns (uint256) { + return FixedPoint.ONE; } - function getMinimumBpt() external pure returns (uint256) { - return _getMinimumBpt(); + function _scalingFactors() internal view override returns (uint256[] memory scalingFactors) { + uint256 numTokens = _getTotalTokens(); + + scalingFactors = new uint256[](numTokens); + for (uint256 i = 0; i < numTokens; i++) { + scalingFactors[i] = FixedPoint.ONE; + } } - function onlyVaultCallable(bytes32 poolId) public view onlyVault(poolId) { + function doNotCallInRecovery() external view whenNotInRecoveryMode { // solhint-disable-previous-line no-empty-blocks } + function notCallableInRecovery() external view { + _ensureNotInRecoveryMode(); + } + + function onlyCallableInRecovery() external view { + _ensureInRecoveryMode(); + } + function _doRecoveryModeExit( uint256[] memory balances, uint256 totalSupply, diff --git a/pkg/pool-utils/contracts/test/MockLegacyBasePool.sol b/pkg/pool-utils/contracts/test/MockLegacyBasePool.sol deleted file mode 100644 index 6bd692562f..0000000000 --- a/pkg/pool-utils/contracts/test/MockLegacyBasePool.sol +++ /dev/null @@ -1,173 +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-weighted/WeightedPoolUserData.sol"; - -import "../LegacyBasePool.sol"; - -contract MockLegacyBasePool is LegacyBasePool { - using BasePoolUserData for bytes; - using WeightedPoolUserData for bytes; - - uint256 private immutable _totalTokens; - - bool private _failBeforeSwapJoinExit; - - event InnerOnJoinPoolCalled(uint256 protocolSwapFeePercentage); - event InnerOnExitPoolCalled(uint256 protocolSwapFeePercentage); - event RecoveryModeExit(uint256 totalSupply, uint256[] balances, uint256 bptAmountIn); - - constructor( - IVault vault, - IVault.PoolSpecialization specialization, - string memory name, - string memory symbol, - IERC20[] memory tokens, - address[] memory assetManagers, - uint256 swapFeePercentage, - uint256 pauseWindowDuration, - uint256 bufferPeriodDuration, - address owner - ) - LegacyBasePool( - vault, - specialization, - name, - symbol, - tokens, - assetManagers, - swapFeePercentage, - pauseWindowDuration, - bufferPeriodDuration, - owner - ) - { - _failBeforeSwapJoinExit = false; - _totalTokens = tokens.length; - } - - function setMiscData(bytes32 data) external { - _setMiscData(data); - } - - function getMiscData() external view returns (bytes32) { - return _getMiscData(); - } - - function _onInitializePool( - bytes32, - address, - address, - uint256[] memory, - bytes memory userData - ) internal pure override returns (uint256, uint256[] memory) { - uint256[] memory amountsIn = userData.initialAmountsIn(); - uint256 bptAmountOut; - - for (uint256 i = 0; i < amountsIn.length; i++) { - bptAmountOut += amountsIn[i]; - } - - return (bptAmountOut, amountsIn); - } - - function _onJoinPool( - bytes32, - address, - address, - uint256[] memory balances, - uint256, - uint256 protocolSwapFeePercentage, - uint256[] memory, - bytes memory - ) internal override returns (uint256, uint256[] memory) { - emit InnerOnJoinPoolCalled(protocolSwapFeePercentage); - - return (0, new uint256[](balances.length)); - } - - function _onExitPool( - bytes32, - address, - address, - uint256[] memory balances, - uint256, - uint256 protocolSwapFeePercentage, - uint256[] memory, - bytes memory - ) internal override returns (uint256, uint256[] memory) { - emit InnerOnExitPoolCalled(protocolSwapFeePercentage); - - return (0, new uint256[](balances.length)); - } - - function setFailBeforeSwapJoinExit(bool fail) external { - _failBeforeSwapJoinExit = fail; - } - - function _beforeSwapJoinExit() internal override { - require(!_failBeforeSwapJoinExit, "FAIL_BEFORE_SWAP_JOIN_EXIT"); - super._beforeSwapJoinExit(); - } - - function payProtocolFees(uint256 bptAmount) public { - _payProtocolFees(bptAmount); - } - - function _getMaxTokens() internal pure override returns (uint256) { - return 8; - } - - function _getTotalTokens() internal view override returns (uint256) { - return _totalTokens; - } - - function _scalingFactor(IERC20) internal pure override returns (uint256) { - return FixedPoint.ONE; - } - - function _scalingFactors() internal view override returns (uint256[] memory scalingFactors) { - uint256 numTokens = _getTotalTokens(); - - scalingFactors = new uint256[](numTokens); - for (uint256 i = 0; i < numTokens; i++) { - scalingFactors[i] = FixedPoint.ONE; - } - } - - function doNotCallInRecovery() external view whenNotInRecoveryMode { - // solhint-disable-previous-line no-empty-blocks - } - - function notCallableInRecovery() external view { - _ensureNotInRecoveryMode(); - } - - function onlyCallableInRecovery() external view { - _ensureInRecoveryMode(); - } - - function _doRecoveryModeExit( - uint256[] memory balances, - uint256 totalSupply, - bytes memory userData - ) internal override returns (uint256, uint256[] memory) { - uint256 bptAmountIn = userData.recoveryModeExit(); - emit RecoveryModeExit(totalSupply, balances, bptAmountIn); - return (bptAmountIn, balances); - } -} diff --git a/pkg/pool-utils/contracts/test/MockNewBasePool.sol b/pkg/pool-utils/contracts/test/MockNewBasePool.sol new file mode 100644 index 0000000000..165aacba3c --- /dev/null +++ b/pkg/pool-utils/contracts/test/MockNewBasePool.sol @@ -0,0 +1,160 @@ +// 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 "../lib/PoolRegistrationLib.sol"; +import "../NewBasePool.sol"; + +contract MockNewBasePool is NewBasePool { + uint256 public constant ON_SWAP_MINIMAL_RETURN = 0xa987654321; + uint256 public constant ON_SWAP_GENERAL_RETURN = 0x123456789a; + uint256 public constant ON_JOIN_RETURN = 0xbbaa11; + uint256 public constant ON_EXIT_RETURN = 0x11aabb; + + using BasePoolUserData for bytes; + using WeightedPoolUserData for bytes; + + bool private _inRecoveryMode; + + event InnerOnInitializePoolCalled(bytes userData); + event InnerOnSwapMinimalCalled(SwapRequest request, uint256 balanceTokenIn, uint256 balanceTokenOut); + event InnerOnSwapGeneralCalled(SwapRequest request, uint256[] balances, uint256 indexIn, uint256 indexOut); + event InnerOnJoinPoolCalled(address sender, uint256[] balances, bytes userData); + event InnerOnExitPoolCalled(address sender, uint256[] balances, bytes userData); + event RecoveryModeExit(uint256 totalSupply, uint256[] balances, uint256 bptAmountIn); + + constructor( + IVault vault, + IVault.PoolSpecialization specialization, + string memory name, + string memory symbol, + IERC20[] memory tokens, + address[] memory assetManagers, + uint256 pauseWindowDuration, + uint256 bufferPeriodDuration, + address owner + ) + NewBasePool( + vault, + PoolRegistrationLib.registerPoolWithAssetManagers(vault, specialization, tokens, assetManagers), + name, + symbol, + pauseWindowDuration, + bufferPeriodDuration, + owner + ) + {} + + function _onInitializePool(address, bytes memory userData) internal override returns (uint256, uint256[] memory) { + emit InnerOnInitializePoolCalled(userData); + + uint256[] memory amountsIn = userData.initialAmountsIn(); + uint256 bptAmountOut; + + for (uint256 i = 0; i < amountsIn.length; i++) { + bptAmountOut += amountsIn[i]; + } + + return (bptAmountOut, amountsIn); + } + + function _onSwapMinimal( + SwapRequest memory request, + uint256 balanceTokenIn, + uint256 balanceTokenOut + ) internal override returns (uint256) { + emit InnerOnSwapMinimalCalled(request, balanceTokenIn, balanceTokenOut); + return ON_SWAP_MINIMAL_RETURN; + } + + function _onSwapGeneral( + SwapRequest memory request, + uint256[] memory balances, + uint256 indexIn, + uint256 indexOut + ) internal override returns (uint256) { + emit InnerOnSwapGeneralCalled(request, balances, indexIn, indexOut); + return ON_SWAP_GENERAL_RETURN; + } + + function _onJoinPool( + address sender, + uint256[] memory balances, + bytes memory userData + ) internal override returns (uint256, uint256[] memory) { + emit InnerOnJoinPoolCalled(sender, balances, userData); + + uint256[] memory amountsIn = new uint256[](balances.length); + for (uint256 i = 0; i < amountsIn.length; ++i) { + amountsIn[i] = ON_JOIN_RETURN; + } + return (0, amountsIn); + } + + function _onExitPool( + address sender, + uint256[] memory balances, + bytes memory userData + ) internal override returns (uint256, uint256[] memory) { + emit InnerOnExitPoolCalled(sender, balances, userData); + + uint256[] memory amountsOut = new uint256[](balances.length); + for (uint256 i = 0; i < amountsOut.length; ++i) { + amountsOut[i] = ON_EXIT_RETURN; + } + return (0, amountsOut); + } + + function inRecoveryMode() public view override returns (bool) { + return _inRecoveryMode; + } + + function _setRecoveryMode(bool enabled) internal override { + _inRecoveryMode = enabled; + } + + function getScalingFactors() external pure override returns (uint256[] memory) { + _revert(Errors.UNIMPLEMENTED); + } + + function getSwapFeePercentage() external pure override returns (uint256) { + _revert(Errors.UNIMPLEMENTED); + } + + function payProtocolFees(uint256 bptAmount) public { + _payProtocolFees(bptAmount); + } + + function getMinimumBpt() external pure returns (uint256) { + return _getMinimumBpt(); + } + + function onlyVaultCallable(bytes32 poolId) public view onlyVault(poolId) { + // solhint-disable-previous-line no-empty-blocks + } + + function _doRecoveryModeExit( + uint256[] memory balances, + uint256 totalSupply, + bytes memory userData + ) internal override returns (uint256, uint256[] memory) { + uint256 bptAmountIn = userData.recoveryModeExit(); + emit RecoveryModeExit(totalSupply, balances, bptAmountIn); + return (bptAmountIn, balances); + } +} diff --git a/pkg/pool-utils/test/BasePool.test.ts b/pkg/pool-utils/test/BasePool.test.ts index 464740ff06..51df41dbfb 100644 --- a/pkg/pool-utils/test/BasePool.test.ts +++ b/pkg/pool-utils/test/BasePool.test.ts @@ -4,41 +4,25 @@ import { BigNumber, Contract, ContractReceipt } from 'ethers'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; import * as expectEvent from '@balancer-labs/v2-helpers/src/test/expectEvent'; -import { expectTransferEvent } from '@balancer-labs/v2-helpers/src/test/expectTransfer'; import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; -import { advanceTime, MONTH } from '@balancer-labs/v2-helpers/src/time'; +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, - SwapRequest, - PoolSpecialization, - WeightedPoolEncoder, - SingleSwap, - SwapKind, - FundManagement, -} from '@balancer-labs/balancer-js'; -import { BigNumberish, bn, fp } from '@balancer-labs/v2-helpers/src/numbers'; -import { ANY_ADDRESS, DELEGATE_OWNER, MAX_UINT256, ZERO_ADDRESS } from '@balancer-labs/v2-helpers/src/constants'; +import { JoinPoolRequest, ExitPoolRequest, PoolSpecialization, WeightedPoolEncoder } 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'; import TypesConverter from '@balancer-labs/v2-helpers/src/models/types/TypesConverter'; -import { impersonate } from '@balancer-labs/v2-deployments/src/signers'; import { random } from 'lodash'; import { defaultAbiCoder } from 'ethers/lib/utils'; -import { sharedBeforeEach } from '@balancer-labs/v2-common/sharedBeforeEach'; describe('BasePool', function () { - let admin: SignerWithAddress, - poolOwner: SignerWithAddress, - deployer: SignerWithAddress, - other: SignerWithAddress, - vaultSigner: SignerWithAddress; - + let admin: SignerWithAddress, poolOwner: SignerWithAddress, deployer: SignerWithAddress, other: SignerWithAddress; let authorizer: Contract, vault: Contract; let tokens: TokenList; const MIN_SWAP_FEE_PERCENTAGE = fp(0.000001); + const MAX_SWAP_FEE_PERCENTAGE = fp(0.1); const PAUSE_WINDOW_DURATION = MONTH * 3; const BUFFER_PERIOD_DURATION = MONTH; @@ -50,13 +34,11 @@ describe('BasePool', function () { sharedBeforeEach(async () => { authorizer = await deploy('v2-vault/TimelockAuthorizer', { args: [admin.address, ZERO_ADDRESS, MONTH] }); vault = await deploy('v2-vault/Vault', { args: [authorizer.address, ZERO_ADDRESS, 0, 0] }); - vaultSigner = await impersonate(vault.address, fp(100)); tokens = await TokenList.create(['DAI', 'MKR', 'SNX'], { sorted: true }); }); function deployBasePool( params: { - specialization?: PoolSpecialization; tokens?: TokenList | string[]; assetManagers?: string[]; swapFeePercentage?: BigNumberish; @@ -67,33 +49,30 @@ describe('BasePool', function () { } = {} ): Promise { let { - specialization, tokens: poolTokens, assetManagers, swapFeePercentage, pauseWindowDuration, bufferPeriodDuration, owner, - from, } = params; - if (!specialization) specialization = PoolSpecialization.GeneralPool; if (!poolTokens) poolTokens = tokens; if (!assetManagers) assetManagers = Array(poolTokens.length).fill(ZERO_ADDRESS); if (!swapFeePercentage) swapFeePercentage = MIN_SWAP_FEE_PERCENTAGE; - if (!pauseWindowDuration) pauseWindowDuration = 0; + if (!pauseWindowDuration) pauseWindowDuration = MONTH; if (!bufferPeriodDuration) bufferPeriodDuration = 0; if (!owner) owner = ZERO_ADDRESS; - if (!from) from = deployer; return deploy('MockBasePool', { - from, + from: params.from, args: [ vault.address, - specialization, + PoolSpecialization.GeneralPool, 'Balancer Pool Token', 'BPT', Array.isArray(poolTokens) ? poolTokens : poolTokens.addresses, assetManagers, + swapFeePercentage, pauseWindowDuration, bufferPeriodDuration, TypesConverter.toAddress(owner), @@ -101,44 +80,10 @@ describe('BasePool', function () { }); } - describe('only vault modifier', () => { - let pool: Contract; - let poolId: string; - - sharedBeforeEach(async () => { - pool = await deployBasePool(); - poolId = await pool.getPoolId(); - }); - - context('when caller is vault', () => { - it('does not revert with the correct pool ID', async () => { - await expect(pool.connect(vaultSigner).onlyVaultCallable(poolId)).to.not.be.reverted; - }); - - it('reverts with any pool ID', async () => { - await expect(pool.connect(vaultSigner).onlyVaultCallable(ethers.utils.randomBytes(32))).to.be.revertedWith( - 'INVALID_POOL_ID' - ); - }); - }); - - context('when caller is other', () => { - it('reverts with the correct pool ID', async () => { - await expect(pool.connect(other).onlyVaultCallable(poolId)).to.be.revertedWith('CALLER_NOT_VAULT'); - }); - - it('reverts with any pool ID', async () => { - await expect(pool.connect(other).onlyVaultCallable(ethers.utils.randomBytes(32))).to.be.revertedWith( - 'CALLER_NOT_VAULT' - ); - }); - }); - }); - describe('authorizer', () => { let pool: Contract; - sharedBeforeEach(async () => { + sharedBeforeEach('deploy pool', async () => { pool = await deployBasePool(); }); @@ -191,10 +136,6 @@ describe('BasePool', function () { expectEvent.notEmitted(await tx.wait(), 'Transfer'); }); - it('shares protocol fees collector with the vault', async () => { - expect(await pool.getProtocolFeesCollector()).to.be.eq(await vault.getProtocolFeesCollector()); - }); - it('mints bpt to the protocol fee collector', async () => { const feeCollector = await pool.getProtocolFeesCollector(); @@ -206,8 +147,145 @@ describe('BasePool', function () { }); }); + describe('swap fee', () => { + context('initialization', () => { + it('has an initial swap fee', async () => { + const swapFeePercentage = fp(0.003); + const pool = await deployBasePool({ swapFeePercentage }); + + expect(await pool.getSwapFeePercentage()).to.equal(swapFeePercentage); + }); + }); + + context('set swap fee percentage', () => { + let pool: Contract; + let sender: SignerWithAddress; + + function itSetsSwapFeePercentage() { + context('when the new swap fee percentage is within bounds', () => { + const newSwapFeePercentage = MAX_SWAP_FEE_PERCENTAGE.sub(1); + + it('can change the swap fee', async () => { + await pool.connect(sender).setSwapFeePercentage(newSwapFeePercentage); + + expect(await pool.getSwapFeePercentage()).to.equal(newSwapFeePercentage); + }); + + it('emits an event', async () => { + const receipt = await (await pool.connect(sender).setSwapFeePercentage(newSwapFeePercentage)).wait(); + + expectEvent.inReceipt(receipt, 'SwapFeePercentageChanged', { swapFeePercentage: newSwapFeePercentage }); + }); + + context('when paused', () => { + sharedBeforeEach('pause pool', async () => { + const action = await actionId(pool, 'pause'); + await authorizer.connect(admin).grantPermissions([action], admin.address, [ANY_ADDRESS]); + await pool.connect(admin).pause(); + }); + + it('reverts', async () => { + await expect(pool.connect(sender).setSwapFeePercentage(newSwapFeePercentage)).to.be.revertedWith( + 'PAUSED' + ); + }); + }); + }); + + context('when the new swap fee percentage is above the maximum', () => { + const swapFeePercentage = MAX_SWAP_FEE_PERCENTAGE.add(1); + + it('reverts', async () => { + await expect(pool.connect(sender).setSwapFeePercentage(swapFeePercentage)).to.be.revertedWith( + 'MAX_SWAP_FEE_PERCENTAGE' + ); + }); + }); + + context('when the new swap fee percentage is below the minimum', () => { + const swapFeePercentage = MIN_SWAP_FEE_PERCENTAGE.sub(1); + + it('reverts', async () => { + await expect(pool.connect(sender).setSwapFeePercentage(swapFeePercentage)).to.be.revertedWith( + 'MIN_SWAP_FEE_PERCENTAGE' + ); + }); + }); + } + + function itRevertsWithUnallowedSender() { + it('reverts', async () => { + await expect(pool.connect(sender).setSwapFeePercentage(MIN_SWAP_FEE_PERCENTAGE)).to.be.revertedWith( + 'SENDER_NOT_ALLOWED' + ); + }); + } + + context('with a delegated owner', () => { + const owner = DELEGATE_OWNER; + + sharedBeforeEach('deploy pool', async () => { + pool = await deployBasePool({ swapFeePercentage: fp(0.01), owner }); + }); + + beforeEach('set sender', () => { + sender = other; + }); + + context('when the sender has the set fee permission in the authorizer', () => { + sharedBeforeEach('grant permission', async () => { + const action = await actionId(pool, 'setSwapFeePercentage'); + await authorizer.connect(admin).grantPermissions([action], sender.address, [ANY_ADDRESS]); + }); + + itSetsSwapFeePercentage(); + }); + + context('when the sender does not have the set fee permission in the authorizer', () => { + itRevertsWithUnallowedSender(); + }); + }); + + context('with an owner', () => { + let owner: SignerWithAddress; + + sharedBeforeEach('deploy pool', async () => { + owner = poolOwner; + pool = await deployBasePool({ swapFeePercentage: fp(0.01), owner }); + }); + + context('when the sender is the owner', () => { + beforeEach(() => { + sender = owner; + }); + + itSetsSwapFeePercentage(); + }); + + context('when the sender is not the owner', () => { + beforeEach(() => { + sender = other; + }); + + context('when the sender does not have the set fee permission in the authorizer', () => { + itRevertsWithUnallowedSender(); + }); + + context('when the sender has the set fee permission in the authorizer', () => { + sharedBeforeEach(async () => { + const action = await actionId(pool, 'setSwapFeePercentage'); + await authorizer.connect(admin).grantPermissions([action], sender.address, [ANY_ADDRESS]); + }); + + itRevertsWithUnallowedSender(); + }); + }); + }); + }); + }); + describe('pause', () => { - let pool: Contract, minimalPool: Contract; + let pool: Contract; const PAUSE_WINDOW_DURATION = MONTH * 3; const BUFFER_PERIOD_DURATION = MONTH; @@ -222,14 +300,12 @@ describe('BasePool', function () { }); context('when paused', () => { - let poolId: string, minimalPoolId: string; + let poolId: string; let initialBalances: BigNumber[]; sharedBeforeEach('deploy and initialize pool', async () => { - const initialBalancePerToken = 1000; - initialBalances = Array(tokens.length).fill(fp(initialBalancePerToken)); + initialBalances = Array(tokens.length).fill(fp(1000)); poolId = await pool.getPoolId(); - minimalPoolId = await minimalPool.getPoolId(); const request: JoinPoolRequest = { assets: tokens.addresses, @@ -238,58 +314,14 @@ describe('BasePool', function () { fromInternalBalance: false, }; - await tokens.mint({ to: poolOwner, amount: fp(2 * initialBalancePerToken + random(1000)) }); + await tokens.mint({ to: poolOwner, amount: fp(1000 + random(1000)) }); await tokens.approve({ from: poolOwner, to: vault }); await vault.connect(poolOwner).joinPool(poolId, poolOwner.address, poolOwner.address, request); - await vault.connect(poolOwner).joinPool(minimalPoolId, poolOwner.address, poolOwner.address, request); }); sharedBeforeEach('pause pool', async () => { await pool.connect(sender).pause(); - await minimalPool.connect(sender).pause(); - }); - - it('swaps revert in general pool', async () => { - const singleSwap: SingleSwap = { - poolId, - kind: SwapKind.GivenIn, - assetIn: tokens.get(0).instance.address, - assetOut: tokens.get(1).instance.address, - amount: 1, // Needs to be > 0 - userData: '0x', - }; - - const funds: FundManagement = { - sender: poolOwner.address, - recipient: poolOwner.address, - fromInternalBalance: false, - toInternalBalance: false, - }; - - // min amount: 0, deadline: max. - await expect(vault.connect(poolOwner).swap(singleSwap, funds, 0, MAX_UINT256)).to.be.revertedWith('PAUSED'); - }); - - it('swaps revert in minimal pool', async () => { - const singleSwap: SingleSwap = { - poolId: minimalPoolId, - kind: SwapKind.GivenIn, - assetIn: tokens.get(0).instance.address, - assetOut: tokens.get(1).instance.address, - amount: 1, // Needs to be > 0 - userData: '0x', - }; - - const funds: FundManagement = { - sender: poolOwner.address, - recipient: poolOwner.address, - fromInternalBalance: false, - toInternalBalance: false, - }; - - // min amount: 0, deadline: max. - await expect(vault.connect(poolOwner).swap(singleSwap, funds, 0, MAX_UINT256)).to.be.revertedWith('PAUSED'); }); it('joins revert', async () => { @@ -331,7 +363,7 @@ describe('BasePool', function () { }); it('cannot unpause after the pause window', async () => { - await advanceTime(PAUSE_WINDOW_DURATION + 1); + await advanceTime(PAUSE_WINDOW_DURATION + DAY); await expect(pool.connect(sender).pause()).to.be.revertedWith('PAUSE_WINDOW_EXPIRED'); }); } @@ -346,19 +378,12 @@ describe('BasePool', function () { context('with a delegated owner', () => { const owner = DELEGATE_OWNER; - sharedBeforeEach('deploy pools', async () => { + sharedBeforeEach('deploy pool', async () => { pool = await deployBasePool({ pauseWindowDuration: PAUSE_WINDOW_DURATION, bufferPeriodDuration: BUFFER_PERIOD_DURATION, owner, }); - - minimalPool = await deployBasePool({ - specialization: PoolSpecialization.MinimalSwapInfoPool, - pauseWindowDuration: PAUSE_WINDOW_DURATION, - bufferPeriodDuration: BUFFER_PERIOD_DURATION, - owner, - }); }); beforeEach('set sender', () => { @@ -376,9 +401,6 @@ describe('BasePool', function () { await authorizer .connect(admin) .grantPermissions([pauseAction, unpauseAction], sender.address, [ANY_ADDRESS, ANY_ADDRESS]); - await authorizer - .connect(admin) - .grantPermissions([await actionId(minimalPool, 'pause')], sender.address, [ANY_ADDRESS]); }); itCanPause(); @@ -388,20 +410,13 @@ describe('BasePool', function () { context('with an owner', () => { let owner: SignerWithAddress; - sharedBeforeEach('deploy pools', async () => { + sharedBeforeEach('deploy pool', async () => { owner = poolOwner; pool = await deployBasePool({ pauseWindowDuration: PAUSE_WINDOW_DURATION, bufferPeriodDuration: BUFFER_PERIOD_DURATION, owner, }); - - minimalPool = await deployBasePool({ - specialization: PoolSpecialization.MinimalSwapInfoPool, - pauseWindowDuration: PAUSE_WINDOW_DURATION, - bufferPeriodDuration: BUFFER_PERIOD_DURATION, - owner, - }); }); context('when the sender is the owner', () => { @@ -422,7 +437,7 @@ describe('BasePool', function () { }); context('when the sender has the pause permission in the authorizer', () => { - sharedBeforeEach('grant permission', async () => { + sharedBeforeEach(async () => { const pauseAction = await actionId(pool, 'pause'); const unpauseAction = await actionId(pool, 'unpause'); await authorizer @@ -448,12 +463,56 @@ describe('BasePool', function () { expect(recoveryMode).to.be.true; }); - it('can disable recovery mode', async () => { + it('enabling recovery mode emits an event', async () => { + const tx = await pool.connect(sender).enableRecoveryMode(); + const receipt = await tx.wait(); + expectEvent.inReceipt(receipt, 'RecoveryModeStateChanged', { enabled: true }); + }); + + context('when recovery mode is enabled', () => { + sharedBeforeEach('enable recovery mode', async () => { + await pool.connect(sender).enableRecoveryMode(); + }); + + it('can disable recovery mode', async () => { + await pool.connect(sender).disableRecoveryMode(); + + const recoveryMode = await pool.inRecoveryMode(); + expect(recoveryMode).to.be.false; + }); + + it('cannot enable recovery mode again', async () => { + await expect(pool.connect(sender).enableRecoveryMode()).to.be.revertedWith('IN_RECOVERY_MODE'); + }); + + it('disabling recovery mode emits an event', async () => { + const tx = await pool.connect(sender).disableRecoveryMode(); + const receipt = await tx.wait(); + expectEvent.inReceipt(receipt, 'RecoveryModeStateChanged', { enabled: false }); + + const recoveryMode = await pool.inRecoveryMode(); + expect(recoveryMode).to.be.false; + }); + }); + + context('when recovery mode is disabled', () => { + it('cannot be disabled again', async () => { + const recoveryMode = await pool.inRecoveryMode(); + expect(recoveryMode).to.be.false; + + await expect(pool.connect(sender).disableRecoveryMode()).to.be.revertedWith('NOT_IN_RECOVERY_MODE'); + }); + }); + + it('reverts when calling functions in the wrong mode', async () => { + await expect(pool.notCallableInRecovery()).to.not.be.reverted; + await expect(pool.onlyCallableInRecovery()).to.be.revertedWith('NOT_IN_RECOVERY_MODE'); + await pool.connect(sender).enableRecoveryMode(); - await pool.connect(sender).disableRecoveryMode(); - const recoveryMode = await pool.inRecoveryMode(); - expect(recoveryMode).to.be.false; + await expect(pool.doNotCallInRecovery()).to.be.revertedWith('IN_RECOVERY_MODE'); + await expect(pool.notCallableInRecovery()).to.be.revertedWith('IN_RECOVERY_MODE'); + await expect(pool.onlyCallableInRecovery()).to.not.be.reverted; }); } @@ -467,7 +526,7 @@ describe('BasePool', function () { context('with a delegated owner', () => { const owner = DELEGATE_OWNER; - sharedBeforeEach(async () => { + sharedBeforeEach('deploy pool', async () => { pool = await deployBasePool({ pauseWindowDuration: PAUSE_WINDOW_DURATION, bufferPeriodDuration: BUFFER_PERIOD_DURATION, @@ -502,7 +561,7 @@ describe('BasePool', function () { context('with an owner', () => { let owner: SignerWithAddress; - sharedBeforeEach(async () => { + sharedBeforeEach('deploy pool', async () => { owner = poolOwner; pool = await deployBasePool({ pauseWindowDuration: PAUSE_WINDOW_DURATION, @@ -544,454 +603,248 @@ describe('BasePool', function () { }); }); }); - }); - describe('swap join exit', () => { - const RECOVERY_MODE_EXIT_KIND = 255; - let pool: Contract, minimalPool: Contract; - let poolId: string, minimalPoolId: string; - let initialBalances: BigNumber[]; + context('exit', () => { + const RECOVERY_MODE_EXIT_KIND = 255; + let poolId: string; + let initialBalances: BigNumber[]; + let pool: Contract; - let sender: SignerWithAddress, recipient: SignerWithAddress; + let normalJoin: () => Promise; + let normalExit: () => Promise; - let normalSwap: (singleSwap: SingleSwap) => Promise; - let normalJoin: () => Promise; - let normalExit: () => Promise; + const PROTOCOL_SWAP_FEE_PERCENTAGE = fp(0.3); - const PROTOCOL_SWAP_FEE_PERCENTAGE = fp(0.3); - const OTHER_EXIT_KIND = 1; - const OTHER_JOIN_KIND = 1; + sharedBeforeEach('deploy and initialize pool', async () => { + initialBalances = Array(tokens.length).fill(fp(1000)); + pool = await deployBasePool({ pauseWindowDuration: MONTH }); + poolId = await pool.getPoolId(); - sharedBeforeEach('deploy and initialize pool', async () => { - sender = poolOwner; - recipient = poolOwner; - const initialBalancePerToken = 1000; + const request: JoinPoolRequest = { + assets: tokens.addresses, + maxAmountsIn: initialBalances, + userData: WeightedPoolEncoder.joinInit(initialBalances), + fromInternalBalance: false, + }; - initialBalances = Array(tokens.length).fill(fp(initialBalancePerToken)); - pool = await deployBasePool({ pauseWindowDuration: MONTH }); - poolId = await pool.getPoolId(); + await tokens.mint({ to: poolOwner, amount: fp(1000 + random(1000)) }); + await tokens.approve({ from: poolOwner, to: vault }); - minimalPool = await deployBasePool({ - pauseWindowDuration: MONTH, - specialization: PoolSpecialization.MinimalSwapInfoPool, + await vault.connect(poolOwner).joinPool(poolId, poolOwner.address, poolOwner.address, request); }); - minimalPoolId = await minimalPool.getPoolId(); - - const request: JoinPoolRequest = { - assets: tokens.addresses, - maxAmountsIn: initialBalances, - userData: WeightedPoolEncoder.joinInit(initialBalances), - fromInternalBalance: false, - }; - - // We mint twice the initial pool balance to fund two pools. - await tokens.mint({ to: sender, amount: fp(2 * initialBalancePerToken + random(1000)) }); - await tokens.approve({ from: sender, to: vault }); - - await vault.connect(sender).joinPool(poolId, sender.address, recipient.address, request); - await vault.connect(sender).joinPool(minimalPoolId, sender.address, recipient.address, request); - }); - - sharedBeforeEach('prepare normal swaps', () => { - const funds: FundManagement = { - sender: poolOwner.address, - recipient: poolOwner.address, - fromInternalBalance: false, - toInternalBalance: false, - }; - - // min amount: 0, deadline: max. - normalSwap = async (singleSwap: SingleSwap) => - (await vault.connect(sender).swap(singleSwap, funds, 0, MAX_UINT256)).wait(); - }); - - sharedBeforeEach('prepare normal join and exit', () => { - const joinRequest: JoinPoolRequest = { - assets: tokens.addresses, - maxAmountsIn: Array(tokens.length).fill(fp(1)), - userData: defaultAbiCoder.encode(['uint256'], [OTHER_JOIN_KIND]), - fromInternalBalance: false, - }; - normalJoin = async () => - (await vault.connect(sender).joinPool(poolId, sender.address, recipient.address, joinRequest)).wait(); + sharedBeforeEach('set a non-zero protocol swap fee percentage', async () => { + const feesCollector = await deployedAt( + 'v2-vault/ProtocolFeesCollector', + await vault.getProtocolFeesCollector() + ); - const exitRequest: ExitPoolRequest = { - assets: tokens.addresses, - minAmountsOut: Array(tokens.length).fill(0), - userData: defaultAbiCoder.encode(['uint256'], [OTHER_EXIT_KIND]), - toInternalBalance: false, - }; + await authorizer + .connect(admin) + .grantPermissions([await actionId(feesCollector, 'setSwapFeePercentage')], admin.address, [ANY_ADDRESS]); - normalExit = async () => - (await vault.connect(sender).exitPool(poolId, sender.address, recipient.address, exitRequest)).wait(); - }); + await feesCollector.connect(admin).setSwapFeePercentage(PROTOCOL_SWAP_FEE_PERCENTAGE); - sharedBeforeEach('set a non-zero protocol swap fee percentage', async () => { - const feesCollector = await deployedAt('v2-vault/ProtocolFeesCollector', await vault.getProtocolFeesCollector()); + expect(await feesCollector.getSwapFeePercentage()).to.equal(PROTOCOL_SWAP_FEE_PERCENTAGE); + }); - await authorizer - .connect(admin) - .grantPermissions([await actionId(feesCollector, 'setSwapFeePercentage')], admin.address, [ANY_ADDRESS]); + before('prepare normal join and exit', () => { + const OTHER_JOIN_KIND = 1; - await feesCollector.connect(admin).setSwapFeePercentage(PROTOCOL_SWAP_FEE_PERCENTAGE); + const joinRequest: JoinPoolRequest = { + assets: tokens.addresses, + maxAmountsIn: Array(tokens.length).fill(0), + userData: defaultAbiCoder.encode(['uint256'], [OTHER_JOIN_KIND]), + fromInternalBalance: false, + }; - expect(await feesCollector.getSwapFeePercentage()).to.equal(PROTOCOL_SWAP_FEE_PERCENTAGE); - }); + normalJoin = async () => + (await vault.connect(poolOwner).joinPool(poolId, poolOwner.address, poolOwner.address, joinRequest)).wait(); - context('when not in recovery mode', () => { - it('the recovery mode exit reverts', async () => { - const preExitBPT = await pool.balanceOf(sender.address); - const exitBPT = preExitBPT.div(3); + const OTHER_EXIT_KIND = 1; - const request: ExitPoolRequest = { + const exitRequest: ExitPoolRequest = { assets: tokens.addresses, minAmountsOut: Array(tokens.length).fill(0), - userData: defaultAbiCoder.encode(['uint256', 'uint256'], [RECOVERY_MODE_EXIT_KIND, exitBPT]), + userData: defaultAbiCoder.encode(['uint256'], [OTHER_EXIT_KIND]), toInternalBalance: false, }; - await expect( - vault.connect(sender).exitPool(poolId, sender.address, recipient.address, request) - ).to.be.revertedWith('NOT_IN_RECOVERY_MODE'); + normalExit = async () => + (await vault.connect(poolOwner).exitPool(poolId, poolOwner.address, poolOwner.address, exitRequest)).wait(); }); - itSwaps(); + context('when not in recovery mode', () => { + it('the recovery mode exit reverts', async () => { + const preExitBPT = await pool.balanceOf(poolOwner.address); + const exitBPT = preExitBPT.div(3); - itJoins(); - - itExits(); - }); - - context('when in recovery mode', () => { - sharedBeforeEach('enable recovery mode', async () => { - const enableRecoveryAction = await actionId(pool, 'enableRecoveryMode'); - const disableRecoveryAction = await actionId(pool, 'disableRecoveryMode'); - await authorizer - .connect(admin) - .grantPermissions([enableRecoveryAction, disableRecoveryAction], admin.address, [ANY_ADDRESS, ANY_ADDRESS]); - - await pool.connect(admin).enableRecoveryMode(); - }); - - itSwaps(); - - itJoins(); - - itExits(); - - function itExitsViaRecoveryModeCorrectly() { - let request: ExitPoolRequest; - let preExitBPT: BigNumber, exitBPT: BigNumber; - - sharedBeforeEach(async () => { - preExitBPT = await pool.balanceOf(sender.address); - exitBPT = preExitBPT.div(3); - - request = { + const request: ExitPoolRequest = { assets: tokens.addresses, minAmountsOut: Array(tokens.length).fill(0), userData: defaultAbiCoder.encode(['uint256', 'uint256'], [RECOVERY_MODE_EXIT_KIND, exitBPT]), toInternalBalance: false, }; - }); - it('passes the correct arguments to `_doRecoveryModeExit`', async () => { - const totalSupply = await pool.totalSupply(); - const tx = await vault.connect(sender).exitPool(poolId, sender.address, recipient.address, request); - expectEvent.inIndirectReceipt(await tx.wait(), pool.interface, 'RecoveryModeExit', { - totalSupply, - balances: initialBalances, - bptAmountIn: exitBPT, - }); + await expect( + vault.connect(poolOwner).exitPool(poolId, poolOwner.address, poolOwner.address, request) + ).to.be.revertedWith('NOT_IN_RECOVERY_MODE'); }); - it('burns the expected amount of BPT', async () => { - await vault.connect(sender).exitPool(poolId, sender.address, recipient.address, request); + describe('normal joins', () => { + it('do not revert', async () => { + await expect(normalJoin()).to.not.be.reverted; + }); - const afterExitBalance = await pool.balanceOf(sender.address); - expect(afterExitBalance).to.equal(preExitBPT.sub(exitBPT)); + it('receive the real protocol swap fee percentage value', async () => { + const receipt = await normalJoin(); + expectEvent.inIndirectReceipt(receipt, pool.interface, 'InnerOnJoinPoolCalled', { + protocolSwapFeePercentage: PROTOCOL_SWAP_FEE_PERCENTAGE, + }); + }); }); - it('returns 0 due protocol fees', async () => { - const onExitReturn = await pool - .connect(vaultSigner) - .callStatic.onExitPool(poolId, sender.address, recipient.address, initialBalances, 0, 0, request.userData); + describe('normal exits', () => { + it('do not revert', async () => { + await expect(normalExit()).to.not.be.reverted; + }); - expect(onExitReturn.length).to.be.eq(2); - expect(onExitReturn[1]).to.deep.eq(Array(tokens.length).fill(bn(0))); + it('receive the real protocol swap value fee percentage value', async () => { + const receipt = await normalExit(); + expectEvent.inIndirectReceipt(receipt, pool.interface, 'InnerOnExitPoolCalled', { + protocolSwapFeePercentage: PROTOCOL_SWAP_FEE_PERCENTAGE, + }); + }); }); - } - - itExitsViaRecoveryModeCorrectly(); + }); - context('when paused', () => { - sharedBeforeEach('pause pool', async () => { + context('when in recovery mode', () => { + sharedBeforeEach('enable recovery mode', async () => { + const enableRecoveryAction = await actionId(pool, 'enableRecoveryMode'); + const disableRecoveryAction = await actionId(pool, 'disableRecoveryMode'); await authorizer .connect(admin) - .grantPermissions([await actionId(pool, 'pause')], admin.address, [ANY_ADDRESS]); + .grantPermissions([enableRecoveryAction, disableRecoveryAction], admin.address, [ANY_ADDRESS, ANY_ADDRESS]); - await pool.connect(admin).pause(); + await pool.connect(admin).enableRecoveryMode(); }); - itExitsViaRecoveryModeCorrectly(); - }); - }); - - function itSwaps() { - let singleSwap: SingleSwap; - let swapRequest: SwapRequest; - - describe('minimal swaps', () => { - sharedBeforeEach('prepare swap request', async () => { - singleSwap = { - poolId: minimalPoolId, - kind: SwapKind.GivenIn, - assetIn: tokens.get(0).instance.address, - assetOut: tokens.get(1).instance.address, - amount: 1, // Needs to be > 0 - userData: '0xdeadbeef', - }; - - const lastChangeBlock = (await vault.getPoolTokens(minimalPoolId)).lastChangeBlock; - swapRequest = { - kind: singleSwap.kind, - tokenIn: singleSwap.assetIn, - tokenOut: singleSwap.assetOut, - amount: singleSwap.amount, - poolId: singleSwap.poolId, - lastChangeBlock: lastChangeBlock, - from: sender.address, - to: recipient.address, - userData: singleSwap.userData, - }; - }); - - it('do not revert', async () => { - await expect(normalSwap(singleSwap)).to.not.be.reverted; - }); - - it('calls inner onSwapMinimal hook with swap parameters', async () => { - const receipt = await normalSwap(singleSwap); + describe('normal joins', () => { + it('do not revert', async () => { + await expect(normalJoin()).to.not.be.reverted; + }); - expectEvent.inIndirectReceipt(receipt, minimalPool.interface, 'InnerOnSwapMinimalCalled', { - request: Object.values(swapRequest), - balanceTokenIn: initialBalances[0], - balanceTokenOut: initialBalances[1], + it('receive 0 as the protocol swap fee percentage value', async () => { + const receipt = await normalJoin(); + expectEvent.inIndirectReceipt(receipt, pool.interface, 'InnerOnJoinPoolCalled', { + protocolSwapFeePercentage: 0, + }); }); }); - it('returns the output of the inner onSwapMinimal hook', async () => { - const onSwap = - 'onSwap((uint8,address,address,uint256,bytes32,uint256,address,address,bytes),uint256,uint256)'; - const onSwapReturn = await minimalPool.connect(vaultSigner).callStatic[onSwap](swapRequest, 0, 0); - expect(onSwapReturn).to.be.eq(await minimalPool.ON_SWAP_MINIMAL_RETURN()); - }); + describe('normal exits', () => { + it('do not revert', async () => { + await expect(normalExit()).to.not.be.reverted; + }); - it('reverts if swap hook caller is not the vault', async () => { - const onSwap = - 'onSwap((uint8,address,address,uint256,bytes32,uint256,address,address,bytes),uint256,uint256)'; - await expect(minimalPool.connect(other)[onSwap](swapRequest, 0, 0)).to.be.revertedWith('CALLER_NOT_VAULT'); + it('receive 0 as the protocol swap fee percentage value', async () => { + const receipt = await normalExit(); + expectEvent.inIndirectReceipt(receipt, pool.interface, 'InnerOnExitPoolCalled', { + protocolSwapFeePercentage: 0, + }); + }); }); - }); - - describe('general swaps', () => { - sharedBeforeEach('prepare swap request', async () => { - singleSwap = { - poolId, - kind: SwapKind.GivenIn, - assetIn: tokens.get(1).instance.address, - assetOut: tokens.get(2).instance.address, - amount: 1, // Needs to be > 0 - userData: '0xdeadbeef', - }; - const lastChangeBlock = (await vault.getPoolTokens(poolId)).lastChangeBlock; - swapRequest = { - kind: singleSwap.kind, - tokenIn: singleSwap.assetIn, - tokenOut: singleSwap.assetOut, - amount: singleSwap.amount, - poolId: singleSwap.poolId, - lastChangeBlock: lastChangeBlock, - from: sender.address, - to: recipient.address, - userData: singleSwap.userData, - }; - }); + function itExitsViaRecoveryModeCorrectly() { + it('the recovery mode exit can be used', async () => { + const preExitBPT = await pool.balanceOf(poolOwner.address); + const exitBPT = preExitBPT.div(3); + + const request: ExitPoolRequest = { + assets: tokens.addresses, + minAmountsOut: Array(tokens.length).fill(0), + userData: defaultAbiCoder.encode(['uint256', 'uint256'], [RECOVERY_MODE_EXIT_KIND, exitBPT]), + toInternalBalance: false, + }; + + const totalSupply = await pool.totalSupply(); + const tx = await vault.connect(poolOwner).exitPool(poolId, poolOwner.address, poolOwner.address, request); + expectEvent.inIndirectReceipt(await tx.wait(), pool.interface, 'RecoveryModeExit', { + totalSupply, + balances: initialBalances, + bptAmountIn: exitBPT, + }); + + // Exit BPT was burned + const afterExitBalance = await pool.balanceOf(poolOwner.address); + expect(afterExitBalance).to.equal(preExitBPT.sub(exitBPT)); + }); + } - it('do not revert', async () => { - await expect(normalSwap(singleSwap)).to.not.be.reverted; - }); + itExitsViaRecoveryModeCorrectly(); - it('calls inner onSwapGeneral hook with swap parameters', async () => { - const receipt = await normalSwap(singleSwap); + context('when paused', () => { + sharedBeforeEach('pause pool', async () => { + await authorizer + .connect(admin) + .grantPermissions([await actionId(pool, 'pause')], admin.address, [ANY_ADDRESS]); - expectEvent.inIndirectReceipt(receipt, pool.interface, 'InnerOnSwapGeneralCalled', { - request: Object.values(swapRequest), - balances: initialBalances, - indexIn: 1, - indexOut: 2, + await pool.connect(admin).pause(); }); - }); - - it('returns the output of the inner onSwapGeneral hook', async () => { - const onSwap = - 'onSwap((uint8,address,address,uint256,bytes32,uint256,address,address,bytes),uint256[],uint256,uint256)'; - const onSwapReturn = await pool.connect(vaultSigner).callStatic[onSwap](swapRequest, [], 0, 0); - expect(onSwapReturn).to.be.eq(await pool.ON_SWAP_GENERAL_RETURN()); - }); - - it('reverts if swap hook caller is not the vault', async () => { - const onSwap = - 'onSwap((uint8,address,address,uint256,bytes32,uint256,address,address,bytes),uint256[],uint256,uint256)'; - await expect(minimalPool.connect(other)[onSwap](swapRequest, [], 0, 0)).to.be.revertedWith( - 'CALLER_NOT_VAULT' - ); - }); - }); - } - function itJoins() { - describe('normal joins', () => { - it('do not revert', async () => { - await expect(normalJoin()).to.not.be.reverted; + itExitsViaRecoveryModeCorrectly(); }); - it('calls inner onJoin hook with join parameters', async () => { - const receipt = await normalJoin(); - expectEvent.inIndirectReceipt(receipt, pool.interface, 'InnerOnJoinPoolCalled', { - sender: sender.address, - balances: initialBalances, - userData: defaultAbiCoder.encode(['uint256'], [OTHER_JOIN_KIND]), + context('when _beforeSwapJoinExit() reverts', () => { + sharedBeforeEach(async () => { + await pool.setFailBeforeSwapJoinExit(true); }); - }); - - it('returns the output of the inner onJoin hook and 0 due protocol fees', async () => { - const onJoinReturn = await pool - .connect(vaultSigner) - .callStatic.onJoinPool(poolId, sender.address, recipient.address, initialBalances, 0, 0, '0x'); - expect(onJoinReturn).to.be.deep.eq([ - Array(tokens.length).fill(await pool.ON_JOIN_RETURN()), - Array(tokens.length).fill(bn(0)), - ]); - }); - }); - } - function itExits() { - describe('normal exits', () => { - it('do not revert', async () => { - await expect(normalExit()).to.not.be.reverted; - }); + it('normal joins revert', async () => { + await expect(normalJoin()).to.be.revertedWith('FAIL_BEFORE_SWAP_JOIN_EXIT'); + }); - it('calls inner onExit hook with exit parameters', async () => { - const receipt = await normalExit(); - expectEvent.inIndirectReceipt(receipt, pool.interface, 'InnerOnExitPoolCalled', { - sender: sender.address, - balances: initialBalances, - userData: defaultAbiCoder.encode(['uint256'], [OTHER_EXIT_KIND]), + it('normal exits revert', async () => { + await expect(normalExit()).to.be.revertedWith('FAIL_BEFORE_SWAP_JOIN_EXIT'); }); - }); - it('returns the output of the inner onExit hook and 0 due protocol fees', async () => { - const onExitReturn = await pool - .connect(vaultSigner) - .callStatic.onExitPool(poolId, sender.address, recipient.address, initialBalances, 0, 0, '0x'); - expect(onExitReturn).to.be.deep.eq([ - Array(tokens.length).fill(await pool.ON_EXIT_RETURN()), - Array(tokens.length).fill(bn(0)), - ]); + itExitsViaRecoveryModeCorrectly(); }); }); - } + }); }); - describe('pool initialization', () => { + describe('misc data', () => { let pool: Contract; - let sender: SignerWithAddress, recipient: SignerWithAddress; - let poolId: string, userData: string; - let request: JoinPoolRequest; - let initialBalances: Array; - - sharedBeforeEach('set up pool and initial join request', async () => { - sender = poolOwner; - recipient = other; - pool = await deployBasePool({ - pauseWindowDuration: PAUSE_WINDOW_DURATION, - }); - poolId = await pool.getPoolId(); - - const initialBalancePerToken = 1000; - - await tokens.mint({ to: sender, amount: fp(initialBalancePerToken) }); - await tokens.approve({ from: sender, to: vault }); - - initialBalances = Array(tokens.length).fill(fp(initialBalancePerToken)); - userData = WeightedPoolEncoder.joinInit(initialBalances); - request = { - assets: tokens.addresses, - maxAmountsIn: initialBalances, - userData, - fromInternalBalance: false, - }; - }); + const swapFeePercentage = fp(0.02); - context('when paused', () => { - sharedBeforeEach(async () => { - await authorizer - .connect(admin) - .grantPermissions([await actionId(pool, 'pause')], sender.address, [ANY_ADDRESS]); - await pool.connect(sender).pause(); - }); - - it('reverts', async () => { - await expect( - vault.connect(sender).joinPool(poolId, sender.address, recipient.address, request) - ).to.be.revertedWith('PAUSED'); - }); + sharedBeforeEach('deploy pool', async () => { + pool = await deployBasePool({ swapFeePercentage }); }); - context('when not paused', () => { - it('calls inner initialization hook', async () => { - const receipt = await ( - await vault.connect(sender).joinPool(poolId, sender.address, recipient.address, request) - ).wait(); - - expectEvent.inIndirectReceipt(receipt, pool.interface, 'InnerOnInitializePoolCalled', { - userData, - }); - }); + it('stores the swap fee pct in the most-significant 64 bits', async () => { + expect(await pool.getSwapFeePercentage()).to.equal(swapFeePercentage); - it('locks the minimum bpt in the zero address', async () => { - const receipt = await ( - await vault.connect(sender).joinPool(poolId, sender.address, recipient.address, request) - ).wait(); + const swapFeeHex = swapFeePercentage.toHexString().slice(2); // remove 0x + const expectedMiscData = swapFeeHex.padStart(16, '0').padEnd(64, '0'); // pad first 8 bytes and fill with zeros - expectTransferEvent(receipt, { from: ZERO_ADDRESS, to: ZERO_ADDRESS, value: await pool.getMinimumBpt() }, pool); - }); + const miscData = await pool.getMiscData(); + expect(miscData).to.be.equal(`0x${expectedMiscData}`); + }); - it('mints bpt to recipient', async () => { - const receipt = await ( - await vault.connect(sender).joinPool(poolId, sender.address, recipient.address, request) - ).wait(); + it('can store up-to 192 bits of extra data', async () => { + const swapFeeHex = `0x${swapFeePercentage.toHexString().slice(2).padStart(16, '0')}`; - // total BPT is calculated by the mock initial hook; base pool mint it after substracting the minimum BPT amount. - const minimumBpt = await pool.getMinimumBpt(); - const totalBptOut = initialBalances.reduce((previous, current) => previous.add(current)); - expectTransferEvent( - receipt, - { from: ZERO_ADDRESS, to: recipient.address, value: totalBptOut.sub(minimumBpt) }, - pool - ); - }); + const assertMiscData = async (data: string): Promise => { + await pool.setMiscData(data); + const expectedMiscData = `${swapFeeHex}${data.slice(18)}`; // 0x + 16 bits + expect(await pool.getMiscData()).to.be.equal(expectedMiscData); + }; - it('returns the output of the inner onInitialize hook and 0 due protocol fees', async () => { - const onInitReturn = await pool - .connect(vaultSigner) - .callStatic.onJoinPool(poolId, sender.address, recipient.address, initialBalances, 0, 0, request.userData); - expect(onInitReturn).to.be.deep.eq([initialBalances, Array(tokens.length).fill(bn(0))]); - }); + for (let i = 0; i <= 64; i++) { + const data = `0x${'1'.repeat(i).padStart(64, '0')}`; + await assertMiscData(data); + } }); }); }); diff --git a/pkg/pool-utils/test/LegacyBasePool.test.ts b/pkg/pool-utils/test/LegacyBasePool.test.ts deleted file mode 100644 index cd166bd93d..0000000000 --- a/pkg/pool-utils/test/LegacyBasePool.test.ts +++ /dev/null @@ -1,850 +0,0 @@ -import { ethers } from 'hardhat'; -import { expect } from 'chai'; -import { BigNumber, Contract, ContractReceipt } from 'ethers'; -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; - -import * as expectEvent from '@balancer-labs/v2-helpers/src/test/expectEvent'; -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 { 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'; -import TypesConverter from '@balancer-labs/v2-helpers/src/models/types/TypesConverter'; -import { random } from 'lodash'; -import { defaultAbiCoder } from 'ethers/lib/utils'; - -describe('LegacyBasePool', function () { - let admin: SignerWithAddress, poolOwner: SignerWithAddress, deployer: SignerWithAddress, other: SignerWithAddress; - let authorizer: Contract, vault: Contract; - let tokens: TokenList; - - const MIN_SWAP_FEE_PERCENTAGE = fp(0.000001); - const MAX_SWAP_FEE_PERCENTAGE = fp(0.1); - - const PAUSE_WINDOW_DURATION = MONTH * 3; - const BUFFER_PERIOD_DURATION = MONTH; - - before(async () => { - [, admin, poolOwner, deployer, other] = await ethers.getSigners(); - }); - - sharedBeforeEach(async () => { - authorizer = await deploy('v2-vault/TimelockAuthorizer', { args: [admin.address, ZERO_ADDRESS, MONTH] }); - vault = await deploy('v2-vault/Vault', { args: [authorizer.address, ZERO_ADDRESS, 0, 0] }); - tokens = await TokenList.create(['DAI', 'MKR', 'SNX'], { sorted: true }); - }); - - function deployBasePool( - params: { - tokens?: TokenList | string[]; - assetManagers?: string[]; - swapFeePercentage?: BigNumberish; - pauseWindowDuration?: number; - bufferPeriodDuration?: number; - owner?: Account; - from?: SignerWithAddress; - } = {} - ): Promise { - let { - tokens: poolTokens, - assetManagers, - swapFeePercentage, - pauseWindowDuration, - bufferPeriodDuration, - owner, - } = params; - if (!poolTokens) poolTokens = tokens; - if (!assetManagers) assetManagers = Array(poolTokens.length).fill(ZERO_ADDRESS); - if (!swapFeePercentage) swapFeePercentage = MIN_SWAP_FEE_PERCENTAGE; - if (!pauseWindowDuration) pauseWindowDuration = MONTH; - if (!bufferPeriodDuration) bufferPeriodDuration = 0; - if (!owner) owner = ZERO_ADDRESS; - - return deploy('MockLegacyBasePool', { - from: params.from, - args: [ - vault.address, - PoolSpecialization.GeneralPool, - 'Balancer Pool Token', - 'BPT', - Array.isArray(poolTokens) ? poolTokens : poolTokens.addresses, - assetManagers, - swapFeePercentage, - pauseWindowDuration, - bufferPeriodDuration, - TypesConverter.toAddress(owner), - ], - }); - } - - describe('authorizer', () => { - let pool: Contract; - - sharedBeforeEach('deploy pool', async () => { - pool = await deployBasePool(); - }); - - it('uses the authorizer of the vault', async () => { - expect(await pool.getAuthorizer()).to.equal(authorizer.address); - }); - - it('tracks authorizer changes in the vault', async () => { - const action = await actionId(vault, 'setAuthorizer'); - await authorizer.connect(admin).grantPermissions([action], admin.address, [ANY_ADDRESS]); - - await vault.connect(admin).setAuthorizer(other.address); - - expect(await pool.getAuthorizer()).to.equal(other.address); - }); - - describe('action identifiers', () => { - const selector = '0x12345678'; - - context('with same pool creator', () => { - it('pools share action identifiers', async () => { - const pool = await deployBasePool({ tokens, from: deployer }); - const otherPool = await deployBasePool({ tokens, from: deployer }); - - expect(await pool.getActionId(selector)).to.equal(await otherPool.getActionId(selector)); - }); - }); - - context('with different pool creators', () => { - it('pools have unique action identifiers', async () => { - const pool = await deployBasePool({ tokens, from: deployer }); - const otherPool = await deployBasePool({ tokens, from: other }); - - expect(await pool.getActionId(selector)).to.not.equal(await otherPool.getActionId(selector)); - }); - }); - }); - }); - - describe('protocol fees', () => { - let pool: Contract; - - sharedBeforeEach(async () => { - pool = await deployBasePool(); - }); - - it('skips zero value mints', async () => { - const tx = await pool.payProtocolFees(0); - - expectEvent.notEmitted(await tx.wait(), 'Transfer'); - }); - - it('mints bpt to the protocol fee collector', async () => { - const feeCollector = await pool.getProtocolFeesCollector(); - - const balanceBefore = await pool.balanceOf(feeCollector); - await pool.payProtocolFees(fp(42)); - const balanceAfter = await pool.balanceOf(feeCollector); - - expect(balanceAfter.sub(balanceBefore)).to.equal(fp(42)); - }); - }); - - describe('swap fee', () => { - context('initialization', () => { - it('has an initial swap fee', async () => { - const swapFeePercentage = fp(0.003); - const pool = await deployBasePool({ swapFeePercentage }); - - expect(await pool.getSwapFeePercentage()).to.equal(swapFeePercentage); - }); - }); - - context('set swap fee percentage', () => { - let pool: Contract; - let sender: SignerWithAddress; - - function itSetsSwapFeePercentage() { - context('when the new swap fee percentage is within bounds', () => { - const newSwapFeePercentage = MAX_SWAP_FEE_PERCENTAGE.sub(1); - - it('can change the swap fee', async () => { - await pool.connect(sender).setSwapFeePercentage(newSwapFeePercentage); - - expect(await pool.getSwapFeePercentage()).to.equal(newSwapFeePercentage); - }); - - it('emits an event', async () => { - const receipt = await (await pool.connect(sender).setSwapFeePercentage(newSwapFeePercentage)).wait(); - - expectEvent.inReceipt(receipt, 'SwapFeePercentageChanged', { swapFeePercentage: newSwapFeePercentage }); - }); - - context('when paused', () => { - sharedBeforeEach('pause pool', async () => { - const action = await actionId(pool, 'pause'); - await authorizer.connect(admin).grantPermissions([action], admin.address, [ANY_ADDRESS]); - await pool.connect(admin).pause(); - }); - - it('reverts', async () => { - await expect(pool.connect(sender).setSwapFeePercentage(newSwapFeePercentage)).to.be.revertedWith( - 'PAUSED' - ); - }); - }); - }); - - context('when the new swap fee percentage is above the maximum', () => { - const swapFeePercentage = MAX_SWAP_FEE_PERCENTAGE.add(1); - - it('reverts', async () => { - await expect(pool.connect(sender).setSwapFeePercentage(swapFeePercentage)).to.be.revertedWith( - 'MAX_SWAP_FEE_PERCENTAGE' - ); - }); - }); - - context('when the new swap fee percentage is below the minimum', () => { - const swapFeePercentage = MIN_SWAP_FEE_PERCENTAGE.sub(1); - - it('reverts', async () => { - await expect(pool.connect(sender).setSwapFeePercentage(swapFeePercentage)).to.be.revertedWith( - 'MIN_SWAP_FEE_PERCENTAGE' - ); - }); - }); - } - - function itRevertsWithUnallowedSender() { - it('reverts', async () => { - await expect(pool.connect(sender).setSwapFeePercentage(MIN_SWAP_FEE_PERCENTAGE)).to.be.revertedWith( - 'SENDER_NOT_ALLOWED' - ); - }); - } - - context('with a delegated owner', () => { - const owner = DELEGATE_OWNER; - - sharedBeforeEach('deploy pool', async () => { - pool = await deployBasePool({ swapFeePercentage: fp(0.01), owner }); - }); - - beforeEach('set sender', () => { - sender = other; - }); - - context('when the sender has the set fee permission in the authorizer', () => { - sharedBeforeEach('grant permission', async () => { - const action = await actionId(pool, 'setSwapFeePercentage'); - await authorizer.connect(admin).grantPermissions([action], sender.address, [ANY_ADDRESS]); - }); - - itSetsSwapFeePercentage(); - }); - - context('when the sender does not have the set fee permission in the authorizer', () => { - itRevertsWithUnallowedSender(); - }); - }); - - context('with an owner', () => { - let owner: SignerWithAddress; - - sharedBeforeEach('deploy pool', async () => { - owner = poolOwner; - pool = await deployBasePool({ swapFeePercentage: fp(0.01), owner }); - }); - - context('when the sender is the owner', () => { - beforeEach(() => { - sender = owner; - }); - - itSetsSwapFeePercentage(); - }); - - context('when the sender is not the owner', () => { - beforeEach(() => { - sender = other; - }); - - context('when the sender does not have the set fee permission in the authorizer', () => { - itRevertsWithUnallowedSender(); - }); - - context('when the sender has the set fee permission in the authorizer', () => { - sharedBeforeEach(async () => { - const action = await actionId(pool, 'setSwapFeePercentage'); - await authorizer.connect(admin).grantPermissions([action], sender.address, [ANY_ADDRESS]); - }); - - itRevertsWithUnallowedSender(); - }); - }); - }); - }); - }); - - describe('pause', () => { - let pool: Contract; - const PAUSE_WINDOW_DURATION = MONTH * 3; - const BUFFER_PERIOD_DURATION = MONTH; - - let sender: SignerWithAddress; - - function itCanPause() { - it('can pause', async () => { - await pool.connect(sender).pause(); - - const { paused } = await pool.getPausedState(); - expect(paused).to.be.true; - }); - - context('when paused', () => { - let poolId: string; - let initialBalances: BigNumber[]; - - sharedBeforeEach('deploy and initialize pool', async () => { - initialBalances = Array(tokens.length).fill(fp(1000)); - poolId = await pool.getPoolId(); - - const request: JoinPoolRequest = { - assets: tokens.addresses, - maxAmountsIn: initialBalances, - userData: WeightedPoolEncoder.joinInit(initialBalances), - fromInternalBalance: false, - }; - - await tokens.mint({ to: poolOwner, amount: fp(1000 + random(1000)) }); - await tokens.approve({ from: poolOwner, to: vault }); - - await vault.connect(poolOwner).joinPool(poolId, poolOwner.address, poolOwner.address, request); - }); - - sharedBeforeEach('pause pool', async () => { - await pool.connect(sender).pause(); - }); - - it('joins revert', async () => { - const OTHER_JOIN_KIND = 1; - - const request: JoinPoolRequest = { - assets: tokens.addresses, - maxAmountsIn: Array(tokens.length).fill(0), - userData: defaultAbiCoder.encode(['uint256'], [OTHER_JOIN_KIND]), - fromInternalBalance: false, - }; - - await expect( - vault.connect(poolOwner).joinPool(poolId, poolOwner.address, poolOwner.address, request) - ).to.be.revertedWith('PAUSED'); - }); - - it('exits revert', async () => { - const OTHER_EXIT_KIND = 1; - - const request: ExitPoolRequest = { - assets: tokens.addresses, - minAmountsOut: Array(tokens.length).fill(0), - userData: defaultAbiCoder.encode(['uint256'], [OTHER_EXIT_KIND]), - toInternalBalance: false, - }; - - await expect( - vault.connect(poolOwner).exitPool(poolId, poolOwner.address, poolOwner.address, request) - ).to.be.revertedWith('PAUSED'); - }); - }); - - it('can unpause', async () => { - await pool.connect(sender).unpause(); - - const { paused } = await pool.getPausedState(); - expect(paused).to.be.false; - }); - - it('cannot unpause after the pause window', async () => { - await advanceTime(PAUSE_WINDOW_DURATION + DAY); - await expect(pool.connect(sender).pause()).to.be.revertedWith('PAUSE_WINDOW_EXPIRED'); - }); - } - - function itRevertsWithUnallowedSender() { - it('reverts', async () => { - await expect(pool.connect(sender).pause()).to.be.revertedWith('SENDER_NOT_ALLOWED'); - await expect(pool.connect(sender).unpause()).to.be.revertedWith('SENDER_NOT_ALLOWED'); - }); - } - - context('with a delegated owner', () => { - const owner = DELEGATE_OWNER; - - sharedBeforeEach('deploy pool', async () => { - pool = await deployBasePool({ - pauseWindowDuration: PAUSE_WINDOW_DURATION, - bufferPeriodDuration: BUFFER_PERIOD_DURATION, - owner, - }); - }); - - beforeEach('set sender', () => { - sender = other; - }); - - context('when the sender does not have the pause permission in the authorizer', () => { - itRevertsWithUnallowedSender(); - }); - - context('when the sender has the pause permission in the authorizer', () => { - sharedBeforeEach('grant permission', async () => { - const pauseAction = await actionId(pool, 'pause'); - const unpauseAction = await actionId(pool, 'unpause'); - await authorizer - .connect(admin) - .grantPermissions([pauseAction, unpauseAction], sender.address, [ANY_ADDRESS, ANY_ADDRESS]); - }); - - itCanPause(); - }); - }); - - context('with an owner', () => { - let owner: SignerWithAddress; - - sharedBeforeEach('deploy pool', async () => { - owner = poolOwner; - pool = await deployBasePool({ - pauseWindowDuration: PAUSE_WINDOW_DURATION, - bufferPeriodDuration: BUFFER_PERIOD_DURATION, - owner, - }); - }); - - context('when the sender is the owner', () => { - beforeEach('set sender', () => { - sender = owner; - }); - - itRevertsWithUnallowedSender(); - }); - - context('when the sender is not the owner', () => { - beforeEach('set sender', () => { - sender = other; - }); - - context('when the sender does not have the pause permission in the authorizer', () => { - itRevertsWithUnallowedSender(); - }); - - context('when the sender has the pause permission in the authorizer', () => { - sharedBeforeEach(async () => { - const pauseAction = await actionId(pool, 'pause'); - const unpauseAction = await actionId(pool, 'unpause'); - await authorizer - .connect(admin) - .grantPermissions([pauseAction, unpauseAction], sender.address, [ANY_ADDRESS, ANY_ADDRESS]); - }); - - itCanPause(); - }); - }); - }); - }); - - describe('recovery mode', () => { - let pool: Contract; - let sender: SignerWithAddress; - - function itCanEnableRecoveryMode() { - it('can enable recovery mode', async () => { - await pool.connect(sender).enableRecoveryMode(); - - const recoveryMode = await pool.inRecoveryMode(); - expect(recoveryMode).to.be.true; - }); - - it('enabling recovery mode emits an event', async () => { - const tx = await pool.connect(sender).enableRecoveryMode(); - const receipt = await tx.wait(); - expectEvent.inReceipt(receipt, 'RecoveryModeStateChanged', { enabled: true }); - }); - - context('when recovery mode is enabled', () => { - sharedBeforeEach('enable recovery mode', async () => { - await pool.connect(sender).enableRecoveryMode(); - }); - - it('can disable recovery mode', async () => { - await pool.connect(sender).disableRecoveryMode(); - - const recoveryMode = await pool.inRecoveryMode(); - expect(recoveryMode).to.be.false; - }); - - it('cannot enable recovery mode again', async () => { - await expect(pool.connect(sender).enableRecoveryMode()).to.be.revertedWith('IN_RECOVERY_MODE'); - }); - - it('disabling recovery mode emits an event', async () => { - const tx = await pool.connect(sender).disableRecoveryMode(); - const receipt = await tx.wait(); - expectEvent.inReceipt(receipt, 'RecoveryModeStateChanged', { enabled: false }); - - const recoveryMode = await pool.inRecoveryMode(); - expect(recoveryMode).to.be.false; - }); - }); - - context('when recovery mode is disabled', () => { - it('cannot be disabled again', async () => { - const recoveryMode = await pool.inRecoveryMode(); - expect(recoveryMode).to.be.false; - - await expect(pool.connect(sender).disableRecoveryMode()).to.be.revertedWith('NOT_IN_RECOVERY_MODE'); - }); - }); - - it('reverts when calling functions in the wrong mode', async () => { - await expect(pool.notCallableInRecovery()).to.not.be.reverted; - await expect(pool.onlyCallableInRecovery()).to.be.revertedWith('NOT_IN_RECOVERY_MODE'); - - await pool.connect(sender).enableRecoveryMode(); - - await expect(pool.doNotCallInRecovery()).to.be.revertedWith('IN_RECOVERY_MODE'); - await expect(pool.notCallableInRecovery()).to.be.revertedWith('IN_RECOVERY_MODE'); - await expect(pool.onlyCallableInRecovery()).to.not.be.reverted; - }); - } - - function itRevertsWithUnallowedSender() { - it('reverts', async () => { - await expect(pool.connect(sender).enableRecoveryMode()).to.be.revertedWith('SENDER_NOT_ALLOWED'); - await expect(pool.connect(sender).disableRecoveryMode()).to.be.revertedWith('SENDER_NOT_ALLOWED'); - }); - } - - context('with a delegated owner', () => { - const owner = DELEGATE_OWNER; - - sharedBeforeEach('deploy pool', async () => { - pool = await deployBasePool({ - pauseWindowDuration: PAUSE_WINDOW_DURATION, - bufferPeriodDuration: BUFFER_PERIOD_DURATION, - owner, - }); - }); - - beforeEach('set sender', () => { - sender = other; - }); - - context('when the sender does not have the recovery mode permission in the authorizer', () => { - itRevertsWithUnallowedSender(); - }); - - context('when the sender has the recovery mode permission in the authorizer', () => { - sharedBeforeEach('grant permission', async () => { - const enableRecoveryAction = await actionId(pool, 'enableRecoveryMode'); - const disableRecoveryAction = await actionId(pool, 'disableRecoveryMode'); - await authorizer - .connect(admin) - .grantPermissions([enableRecoveryAction, disableRecoveryAction], sender.address, [ - ANY_ADDRESS, - ANY_ADDRESS, - ]); - }); - - itCanEnableRecoveryMode(); - }); - }); - - context('with an owner', () => { - let owner: SignerWithAddress; - - sharedBeforeEach('deploy pool', async () => { - owner = poolOwner; - pool = await deployBasePool({ - pauseWindowDuration: PAUSE_WINDOW_DURATION, - bufferPeriodDuration: BUFFER_PERIOD_DURATION, - owner, - }); - }); - - context('when the sender is the owner', () => { - beforeEach('set sender', () => { - sender = owner; - }); - - itRevertsWithUnallowedSender(); - }); - - context('when the sender is not the owner', () => { - beforeEach('set sender', () => { - sender = other; - }); - - context('when the sender does not have the recovery mode permission in the authorizer', () => { - itRevertsWithUnallowedSender(); - }); - - context('when the sender has the recovery mode permission in the authorizer', () => { - sharedBeforeEach('grant permission', async () => { - const enableRecoveryAction = await actionId(pool, 'enableRecoveryMode'); - const disableRecoveryAction = await actionId(pool, 'disableRecoveryMode'); - await authorizer - .connect(admin) - .grantPermissions([enableRecoveryAction, disableRecoveryAction], sender.address, [ - ANY_ADDRESS, - ANY_ADDRESS, - ]); - }); - - itCanEnableRecoveryMode(); - }); - }); - }); - - context('exit', () => { - const RECOVERY_MODE_EXIT_KIND = 255; - let poolId: string; - let initialBalances: BigNumber[]; - let pool: Contract; - - let normalJoin: () => Promise; - let normalExit: () => Promise; - - const PROTOCOL_SWAP_FEE_PERCENTAGE = fp(0.3); - - sharedBeforeEach('deploy and initialize pool', async () => { - initialBalances = Array(tokens.length).fill(fp(1000)); - pool = await deployBasePool({ pauseWindowDuration: MONTH }); - poolId = await pool.getPoolId(); - - const request: JoinPoolRequest = { - assets: tokens.addresses, - maxAmountsIn: initialBalances, - userData: WeightedPoolEncoder.joinInit(initialBalances), - fromInternalBalance: false, - }; - - await tokens.mint({ to: poolOwner, amount: fp(1000 + random(1000)) }); - await tokens.approve({ from: poolOwner, to: vault }); - - await vault.connect(poolOwner).joinPool(poolId, poolOwner.address, poolOwner.address, request); - }); - - sharedBeforeEach('set a non-zero protocol swap fee percentage', async () => { - const feesCollector = await deployedAt( - 'v2-vault/ProtocolFeesCollector', - await vault.getProtocolFeesCollector() - ); - - await authorizer - .connect(admin) - .grantPermissions([await actionId(feesCollector, 'setSwapFeePercentage')], admin.address, [ANY_ADDRESS]); - - await feesCollector.connect(admin).setSwapFeePercentage(PROTOCOL_SWAP_FEE_PERCENTAGE); - - expect(await feesCollector.getSwapFeePercentage()).to.equal(PROTOCOL_SWAP_FEE_PERCENTAGE); - }); - - before('prepare normal join and exit', () => { - const OTHER_JOIN_KIND = 1; - - const joinRequest: JoinPoolRequest = { - assets: tokens.addresses, - maxAmountsIn: Array(tokens.length).fill(0), - userData: defaultAbiCoder.encode(['uint256'], [OTHER_JOIN_KIND]), - fromInternalBalance: false, - }; - - normalJoin = async () => - (await vault.connect(poolOwner).joinPool(poolId, poolOwner.address, poolOwner.address, joinRequest)).wait(); - - const OTHER_EXIT_KIND = 1; - - const exitRequest: ExitPoolRequest = { - assets: tokens.addresses, - minAmountsOut: Array(tokens.length).fill(0), - userData: defaultAbiCoder.encode(['uint256'], [OTHER_EXIT_KIND]), - toInternalBalance: false, - }; - - normalExit = async () => - (await vault.connect(poolOwner).exitPool(poolId, poolOwner.address, poolOwner.address, exitRequest)).wait(); - }); - - context('when not in recovery mode', () => { - it('the recovery mode exit reverts', async () => { - const preExitBPT = await pool.balanceOf(poolOwner.address); - const exitBPT = preExitBPT.div(3); - - const request: ExitPoolRequest = { - assets: tokens.addresses, - minAmountsOut: Array(tokens.length).fill(0), - userData: defaultAbiCoder.encode(['uint256', 'uint256'], [RECOVERY_MODE_EXIT_KIND, exitBPT]), - toInternalBalance: false, - }; - - await expect( - vault.connect(poolOwner).exitPool(poolId, poolOwner.address, poolOwner.address, request) - ).to.be.revertedWith('NOT_IN_RECOVERY_MODE'); - }); - - describe('normal joins', () => { - it('do not revert', async () => { - await expect(normalJoin()).to.not.be.reverted; - }); - - it('receive the real protocol swap fee percentage value', async () => { - const receipt = await normalJoin(); - expectEvent.inIndirectReceipt(receipt, pool.interface, 'InnerOnJoinPoolCalled', { - protocolSwapFeePercentage: PROTOCOL_SWAP_FEE_PERCENTAGE, - }); - }); - }); - - describe('normal exits', () => { - it('do not revert', async () => { - await expect(normalExit()).to.not.be.reverted; - }); - - it('receive the real protocol swap value fee percentage value', async () => { - const receipt = await normalExit(); - expectEvent.inIndirectReceipt(receipt, pool.interface, 'InnerOnExitPoolCalled', { - protocolSwapFeePercentage: PROTOCOL_SWAP_FEE_PERCENTAGE, - }); - }); - }); - }); - - context('when in recovery mode', () => { - sharedBeforeEach('enable recovery mode', async () => { - const enableRecoveryAction = await actionId(pool, 'enableRecoveryMode'); - const disableRecoveryAction = await actionId(pool, 'disableRecoveryMode'); - await authorizer - .connect(admin) - .grantPermissions([enableRecoveryAction, disableRecoveryAction], admin.address, [ANY_ADDRESS, ANY_ADDRESS]); - - await pool.connect(admin).enableRecoveryMode(); - }); - - describe('normal joins', () => { - it('do not revert', async () => { - await expect(normalJoin()).to.not.be.reverted; - }); - - it('receive 0 as the protocol swap fee percentage value', async () => { - const receipt = await normalJoin(); - expectEvent.inIndirectReceipt(receipt, pool.interface, 'InnerOnJoinPoolCalled', { - protocolSwapFeePercentage: 0, - }); - }); - }); - - describe('normal exits', () => { - it('do not revert', async () => { - await expect(normalExit()).to.not.be.reverted; - }); - - it('receive 0 as the protocol swap fee percentage value', async () => { - const receipt = await normalExit(); - expectEvent.inIndirectReceipt(receipt, pool.interface, 'InnerOnExitPoolCalled', { - protocolSwapFeePercentage: 0, - }); - }); - }); - - function itExitsViaRecoveryModeCorrectly() { - it('the recovery mode exit can be used', async () => { - const preExitBPT = await pool.balanceOf(poolOwner.address); - const exitBPT = preExitBPT.div(3); - - const request: ExitPoolRequest = { - assets: tokens.addresses, - minAmountsOut: Array(tokens.length).fill(0), - userData: defaultAbiCoder.encode(['uint256', 'uint256'], [RECOVERY_MODE_EXIT_KIND, exitBPT]), - toInternalBalance: false, - }; - - const totalSupply = await pool.totalSupply(); - const tx = await vault.connect(poolOwner).exitPool(poolId, poolOwner.address, poolOwner.address, request); - expectEvent.inIndirectReceipt(await tx.wait(), pool.interface, 'RecoveryModeExit', { - totalSupply, - balances: initialBalances, - bptAmountIn: exitBPT, - }); - - // Exit BPT was burned - const afterExitBalance = await pool.balanceOf(poolOwner.address); - expect(afterExitBalance).to.equal(preExitBPT.sub(exitBPT)); - }); - } - - itExitsViaRecoveryModeCorrectly(); - - context('when paused', () => { - sharedBeforeEach('pause pool', async () => { - await authorizer - .connect(admin) - .grantPermissions([await actionId(pool, 'pause')], admin.address, [ANY_ADDRESS]); - - await pool.connect(admin).pause(); - }); - - itExitsViaRecoveryModeCorrectly(); - }); - - context('when _beforeSwapJoinExit() reverts', () => { - sharedBeforeEach(async () => { - await pool.setFailBeforeSwapJoinExit(true); - }); - - it('normal joins revert', async () => { - await expect(normalJoin()).to.be.revertedWith('FAIL_BEFORE_SWAP_JOIN_EXIT'); - }); - - it('normal exits revert', async () => { - await expect(normalExit()).to.be.revertedWith('FAIL_BEFORE_SWAP_JOIN_EXIT'); - }); - - itExitsViaRecoveryModeCorrectly(); - }); - }); - }); - }); - - describe('misc data', () => { - let pool: Contract; - const swapFeePercentage = fp(0.02); - - sharedBeforeEach('deploy pool', async () => { - pool = await deployBasePool({ swapFeePercentage }); - }); - - it('stores the swap fee pct in the most-significant 64 bits', async () => { - expect(await pool.getSwapFeePercentage()).to.equal(swapFeePercentage); - - const swapFeeHex = swapFeePercentage.toHexString().slice(2); // remove 0x - const expectedMiscData = swapFeeHex.padStart(16, '0').padEnd(64, '0'); // pad first 8 bytes and fill with zeros - - const miscData = await pool.getMiscData(); - expect(miscData).to.be.equal(`0x${expectedMiscData}`); - }); - - it('can store up-to 192 bits of extra data', async () => { - const swapFeeHex = `0x${swapFeePercentage.toHexString().slice(2).padStart(16, '0')}`; - - const assertMiscData = async (data: string): Promise => { - await pool.setMiscData(data); - const expectedMiscData = `${swapFeeHex}${data.slice(18)}`; // 0x + 16 bits - expect(await pool.getMiscData()).to.be.equal(expectedMiscData); - }; - - for (let i = 0; i <= 64; i++) { - const data = `0x${'1'.repeat(i).padStart(64, '0')}`; - await assertMiscData(data); - } - }); - }); -}); diff --git a/pkg/pool-utils/test/NewBasePool.test.ts b/pkg/pool-utils/test/NewBasePool.test.ts new file mode 100644 index 0000000000..3cf217ebbd --- /dev/null +++ b/pkg/pool-utils/test/NewBasePool.test.ts @@ -0,0 +1,997 @@ +import { ethers } from 'hardhat'; +import { expect } from 'chai'; +import { BigNumber, Contract, ContractReceipt } from 'ethers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; + +import * as expectEvent from '@balancer-labs/v2-helpers/src/test/expectEvent'; +import { expectTransferEvent } from '@balancer-labs/v2-helpers/src/test/expectTransfer'; +import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; +import { advanceTime, 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, + SwapRequest, + PoolSpecialization, + WeightedPoolEncoder, + SingleSwap, + SwapKind, + FundManagement, +} from '@balancer-labs/balancer-js'; +import { BigNumberish, bn, fp } from '@balancer-labs/v2-helpers/src/numbers'; +import { ANY_ADDRESS, DELEGATE_OWNER, MAX_UINT256, ZERO_ADDRESS } from '@balancer-labs/v2-helpers/src/constants'; +import { Account } from '@balancer-labs/v2-helpers/src/models/types/types'; +import TypesConverter from '@balancer-labs/v2-helpers/src/models/types/TypesConverter'; +import { impersonate } from '@balancer-labs/v2-deployments/src/signers'; +import { random } from 'lodash'; +import { defaultAbiCoder } from 'ethers/lib/utils'; +import { sharedBeforeEach } from '@balancer-labs/v2-common/sharedBeforeEach'; + +describe('NewBasePool', function () { + let admin: SignerWithAddress, + poolOwner: SignerWithAddress, + deployer: SignerWithAddress, + other: SignerWithAddress, + vaultSigner: SignerWithAddress; + + let authorizer: Contract, vault: Contract; + let tokens: TokenList; + + const MIN_SWAP_FEE_PERCENTAGE = fp(0.000001); + + const PAUSE_WINDOW_DURATION = MONTH * 3; + const BUFFER_PERIOD_DURATION = MONTH; + + before(async () => { + [, admin, poolOwner, deployer, other] = await ethers.getSigners(); + }); + + sharedBeforeEach(async () => { + authorizer = await deploy('v2-vault/TimelockAuthorizer', { args: [admin.address, ZERO_ADDRESS, MONTH] }); + vault = await deploy('v2-vault/Vault', { args: [authorizer.address, ZERO_ADDRESS, 0, 0] }); + vaultSigner = await impersonate(vault.address, fp(100)); + tokens = await TokenList.create(['DAI', 'MKR', 'SNX'], { sorted: true }); + }); + + function deployBasePool( + params: { + specialization?: PoolSpecialization; + tokens?: TokenList | string[]; + assetManagers?: string[]; + swapFeePercentage?: BigNumberish; + pauseWindowDuration?: number; + bufferPeriodDuration?: number; + owner?: Account; + from?: SignerWithAddress; + } = {} + ): Promise { + let { + specialization, + tokens: poolTokens, + assetManagers, + swapFeePercentage, + pauseWindowDuration, + bufferPeriodDuration, + owner, + from, + } = params; + if (!specialization) specialization = PoolSpecialization.GeneralPool; + if (!poolTokens) poolTokens = tokens; + if (!assetManagers) assetManagers = Array(poolTokens.length).fill(ZERO_ADDRESS); + if (!swapFeePercentage) swapFeePercentage = MIN_SWAP_FEE_PERCENTAGE; + if (!pauseWindowDuration) pauseWindowDuration = 0; + if (!bufferPeriodDuration) bufferPeriodDuration = 0; + if (!owner) owner = ZERO_ADDRESS; + if (!from) from = deployer; + + return deploy('MockNewBasePool', { + from, + args: [ + vault.address, + specialization, + 'Balancer Pool Token', + 'BPT', + Array.isArray(poolTokens) ? poolTokens : poolTokens.addresses, + assetManagers, + pauseWindowDuration, + bufferPeriodDuration, + TypesConverter.toAddress(owner), + ], + }); + } + + describe('only vault modifier', () => { + let pool: Contract; + let poolId: string; + + sharedBeforeEach(async () => { + pool = await deployBasePool(); + poolId = await pool.getPoolId(); + }); + + context('when caller is vault', () => { + it('does not revert with the correct pool ID', async () => { + await expect(pool.connect(vaultSigner).onlyVaultCallable(poolId)).to.not.be.reverted; + }); + + it('reverts with any pool ID', async () => { + await expect(pool.connect(vaultSigner).onlyVaultCallable(ethers.utils.randomBytes(32))).to.be.revertedWith( + 'INVALID_POOL_ID' + ); + }); + }); + + context('when caller is other', () => { + it('reverts with the correct pool ID', async () => { + await expect(pool.connect(other).onlyVaultCallable(poolId)).to.be.revertedWith('CALLER_NOT_VAULT'); + }); + + it('reverts with any pool ID', async () => { + await expect(pool.connect(other).onlyVaultCallable(ethers.utils.randomBytes(32))).to.be.revertedWith( + 'CALLER_NOT_VAULT' + ); + }); + }); + }); + + describe('authorizer', () => { + let pool: Contract; + + sharedBeforeEach(async () => { + pool = await deployBasePool(); + }); + + it('uses the authorizer of the vault', async () => { + expect(await pool.getAuthorizer()).to.equal(authorizer.address); + }); + + it('tracks authorizer changes in the vault', async () => { + const action = await actionId(vault, 'setAuthorizer'); + await authorizer.connect(admin).grantPermissions([action], admin.address, [ANY_ADDRESS]); + + await vault.connect(admin).setAuthorizer(other.address); + + expect(await pool.getAuthorizer()).to.equal(other.address); + }); + + describe('action identifiers', () => { + const selector = '0x12345678'; + + context('with same pool creator', () => { + it('pools share action identifiers', async () => { + const pool = await deployBasePool({ tokens, from: deployer }); + const otherPool = await deployBasePool({ tokens, from: deployer }); + + expect(await pool.getActionId(selector)).to.equal(await otherPool.getActionId(selector)); + }); + }); + + context('with different pool creators', () => { + it('pools have unique action identifiers', async () => { + const pool = await deployBasePool({ tokens, from: deployer }); + const otherPool = await deployBasePool({ tokens, from: other }); + + expect(await pool.getActionId(selector)).to.not.equal(await otherPool.getActionId(selector)); + }); + }); + }); + }); + + describe('protocol fees', () => { + let pool: Contract; + + sharedBeforeEach(async () => { + pool = await deployBasePool(); + }); + + it('skips zero value mints', async () => { + const tx = await pool.payProtocolFees(0); + + expectEvent.notEmitted(await tx.wait(), 'Transfer'); + }); + + it('shares protocol fees collector with the vault', async () => { + expect(await pool.getProtocolFeesCollector()).to.be.eq(await vault.getProtocolFeesCollector()); + }); + + it('mints bpt to the protocol fee collector', async () => { + const feeCollector = await pool.getProtocolFeesCollector(); + + const balanceBefore = await pool.balanceOf(feeCollector); + await pool.payProtocolFees(fp(42)); + const balanceAfter = await pool.balanceOf(feeCollector); + + expect(balanceAfter.sub(balanceBefore)).to.equal(fp(42)); + }); + }); + + describe('pause', () => { + let pool: Contract, minimalPool: Contract; + const PAUSE_WINDOW_DURATION = MONTH * 3; + const BUFFER_PERIOD_DURATION = MONTH; + + let sender: SignerWithAddress; + + function itCanPause() { + it('can pause', async () => { + await pool.connect(sender).pause(); + + const { paused } = await pool.getPausedState(); + expect(paused).to.be.true; + }); + + context('when paused', () => { + let poolId: string, minimalPoolId: string; + let initialBalances: BigNumber[]; + + sharedBeforeEach('deploy and initialize pool', async () => { + const initialBalancePerToken = 1000; + initialBalances = Array(tokens.length).fill(fp(initialBalancePerToken)); + poolId = await pool.getPoolId(); + minimalPoolId = await minimalPool.getPoolId(); + + const request: JoinPoolRequest = { + assets: tokens.addresses, + maxAmountsIn: initialBalances, + userData: WeightedPoolEncoder.joinInit(initialBalances), + fromInternalBalance: false, + }; + + await tokens.mint({ to: poolOwner, amount: fp(2 * initialBalancePerToken + random(1000)) }); + await tokens.approve({ from: poolOwner, to: vault }); + + await vault.connect(poolOwner).joinPool(poolId, poolOwner.address, poolOwner.address, request); + await vault.connect(poolOwner).joinPool(minimalPoolId, poolOwner.address, poolOwner.address, request); + }); + + sharedBeforeEach('pause pool', async () => { + await pool.connect(sender).pause(); + await minimalPool.connect(sender).pause(); + }); + + it('swaps revert in general pool', async () => { + const singleSwap: SingleSwap = { + poolId, + kind: SwapKind.GivenIn, + assetIn: tokens.get(0).instance.address, + assetOut: tokens.get(1).instance.address, + amount: 1, // Needs to be > 0 + userData: '0x', + }; + + const funds: FundManagement = { + sender: poolOwner.address, + recipient: poolOwner.address, + fromInternalBalance: false, + toInternalBalance: false, + }; + + // min amount: 0, deadline: max. + await expect(vault.connect(poolOwner).swap(singleSwap, funds, 0, MAX_UINT256)).to.be.revertedWith('PAUSED'); + }); + + it('swaps revert in minimal pool', async () => { + const singleSwap: SingleSwap = { + poolId: minimalPoolId, + kind: SwapKind.GivenIn, + assetIn: tokens.get(0).instance.address, + assetOut: tokens.get(1).instance.address, + amount: 1, // Needs to be > 0 + userData: '0x', + }; + + const funds: FundManagement = { + sender: poolOwner.address, + recipient: poolOwner.address, + fromInternalBalance: false, + toInternalBalance: false, + }; + + // min amount: 0, deadline: max. + await expect(vault.connect(poolOwner).swap(singleSwap, funds, 0, MAX_UINT256)).to.be.revertedWith('PAUSED'); + }); + + it('joins revert', async () => { + const OTHER_JOIN_KIND = 1; + + const request: JoinPoolRequest = { + assets: tokens.addresses, + maxAmountsIn: Array(tokens.length).fill(0), + userData: defaultAbiCoder.encode(['uint256'], [OTHER_JOIN_KIND]), + fromInternalBalance: false, + }; + + await expect( + vault.connect(poolOwner).joinPool(poolId, poolOwner.address, poolOwner.address, request) + ).to.be.revertedWith('PAUSED'); + }); + + it('exits revert', async () => { + const OTHER_EXIT_KIND = 1; + + const request: ExitPoolRequest = { + assets: tokens.addresses, + minAmountsOut: Array(tokens.length).fill(0), + userData: defaultAbiCoder.encode(['uint256'], [OTHER_EXIT_KIND]), + toInternalBalance: false, + }; + + await expect( + vault.connect(poolOwner).exitPool(poolId, poolOwner.address, poolOwner.address, request) + ).to.be.revertedWith('PAUSED'); + }); + }); + + it('can unpause', async () => { + await pool.connect(sender).unpause(); + + const { paused } = await pool.getPausedState(); + expect(paused).to.be.false; + }); + + it('cannot unpause after the pause window', async () => { + await advanceTime(PAUSE_WINDOW_DURATION + 1); + await expect(pool.connect(sender).pause()).to.be.revertedWith('PAUSE_WINDOW_EXPIRED'); + }); + } + + function itRevertsWithUnallowedSender() { + it('reverts', async () => { + await expect(pool.connect(sender).pause()).to.be.revertedWith('SENDER_NOT_ALLOWED'); + await expect(pool.connect(sender).unpause()).to.be.revertedWith('SENDER_NOT_ALLOWED'); + }); + } + + context('with a delegated owner', () => { + const owner = DELEGATE_OWNER; + + sharedBeforeEach('deploy pools', async () => { + pool = await deployBasePool({ + pauseWindowDuration: PAUSE_WINDOW_DURATION, + bufferPeriodDuration: BUFFER_PERIOD_DURATION, + owner, + }); + + minimalPool = await deployBasePool({ + specialization: PoolSpecialization.MinimalSwapInfoPool, + pauseWindowDuration: PAUSE_WINDOW_DURATION, + bufferPeriodDuration: BUFFER_PERIOD_DURATION, + owner, + }); + }); + + beforeEach('set sender', () => { + sender = other; + }); + + context('when the sender does not have the pause permission in the authorizer', () => { + itRevertsWithUnallowedSender(); + }); + + context('when the sender has the pause permission in the authorizer', () => { + sharedBeforeEach('grant permission', async () => { + const pauseAction = await actionId(pool, 'pause'); + const unpauseAction = await actionId(pool, 'unpause'); + await authorizer + .connect(admin) + .grantPermissions([pauseAction, unpauseAction], sender.address, [ANY_ADDRESS, ANY_ADDRESS]); + await authorizer + .connect(admin) + .grantPermissions([await actionId(minimalPool, 'pause')], sender.address, [ANY_ADDRESS]); + }); + + itCanPause(); + }); + }); + + context('with an owner', () => { + let owner: SignerWithAddress; + + sharedBeforeEach('deploy pools', async () => { + owner = poolOwner; + pool = await deployBasePool({ + pauseWindowDuration: PAUSE_WINDOW_DURATION, + bufferPeriodDuration: BUFFER_PERIOD_DURATION, + owner, + }); + + minimalPool = await deployBasePool({ + specialization: PoolSpecialization.MinimalSwapInfoPool, + pauseWindowDuration: PAUSE_WINDOW_DURATION, + bufferPeriodDuration: BUFFER_PERIOD_DURATION, + owner, + }); + }); + + context('when the sender is the owner', () => { + beforeEach('set sender', () => { + sender = owner; + }); + + itRevertsWithUnallowedSender(); + }); + + context('when the sender is not the owner', () => { + beforeEach('set sender', () => { + sender = other; + }); + + context('when the sender does not have the pause permission in the authorizer', () => { + itRevertsWithUnallowedSender(); + }); + + context('when the sender has the pause permission in the authorizer', () => { + sharedBeforeEach('grant permission', async () => { + const pauseAction = await actionId(pool, 'pause'); + const unpauseAction = await actionId(pool, 'unpause'); + await authorizer + .connect(admin) + .grantPermissions([pauseAction, unpauseAction], sender.address, [ANY_ADDRESS, ANY_ADDRESS]); + }); + + itCanPause(); + }); + }); + }); + }); + + describe('recovery mode', () => { + let pool: Contract; + let sender: SignerWithAddress; + + function itCanEnableRecoveryMode() { + it('can enable recovery mode', async () => { + await pool.connect(sender).enableRecoveryMode(); + + const recoveryMode = await pool.inRecoveryMode(); + expect(recoveryMode).to.be.true; + }); + + it('can disable recovery mode', async () => { + await pool.connect(sender).enableRecoveryMode(); + await pool.connect(sender).disableRecoveryMode(); + + const recoveryMode = await pool.inRecoveryMode(); + expect(recoveryMode).to.be.false; + }); + } + + function itRevertsWithUnallowedSender() { + it('reverts', async () => { + await expect(pool.connect(sender).enableRecoveryMode()).to.be.revertedWith('SENDER_NOT_ALLOWED'); + await expect(pool.connect(sender).disableRecoveryMode()).to.be.revertedWith('SENDER_NOT_ALLOWED'); + }); + } + + context('with a delegated owner', () => { + const owner = DELEGATE_OWNER; + + sharedBeforeEach(async () => { + pool = await deployBasePool({ + pauseWindowDuration: PAUSE_WINDOW_DURATION, + bufferPeriodDuration: BUFFER_PERIOD_DURATION, + owner, + }); + }); + + beforeEach('set sender', () => { + sender = other; + }); + + context('when the sender does not have the recovery mode permission in the authorizer', () => { + itRevertsWithUnallowedSender(); + }); + + context('when the sender has the recovery mode permission in the authorizer', () => { + sharedBeforeEach('grant permission', async () => { + const enableRecoveryAction = await actionId(pool, 'enableRecoveryMode'); + const disableRecoveryAction = await actionId(pool, 'disableRecoveryMode'); + await authorizer + .connect(admin) + .grantPermissions([enableRecoveryAction, disableRecoveryAction], sender.address, [ + ANY_ADDRESS, + ANY_ADDRESS, + ]); + }); + + itCanEnableRecoveryMode(); + }); + }); + + context('with an owner', () => { + let owner: SignerWithAddress; + + sharedBeforeEach(async () => { + owner = poolOwner; + pool = await deployBasePool({ + pauseWindowDuration: PAUSE_WINDOW_DURATION, + bufferPeriodDuration: BUFFER_PERIOD_DURATION, + owner, + }); + }); + + context('when the sender is the owner', () => { + beforeEach('set sender', () => { + sender = owner; + }); + + itRevertsWithUnallowedSender(); + }); + + context('when the sender is not the owner', () => { + beforeEach('set sender', () => { + sender = other; + }); + + context('when the sender does not have the recovery mode permission in the authorizer', () => { + itRevertsWithUnallowedSender(); + }); + + context('when the sender has the recovery mode permission in the authorizer', () => { + sharedBeforeEach('grant permission', async () => { + const enableRecoveryAction = await actionId(pool, 'enableRecoveryMode'); + const disableRecoveryAction = await actionId(pool, 'disableRecoveryMode'); + await authorizer + .connect(admin) + .grantPermissions([enableRecoveryAction, disableRecoveryAction], sender.address, [ + ANY_ADDRESS, + ANY_ADDRESS, + ]); + }); + + itCanEnableRecoveryMode(); + }); + }); + }); + }); + + describe('swap join exit', () => { + const RECOVERY_MODE_EXIT_KIND = 255; + let pool: Contract, minimalPool: Contract; + let poolId: string, minimalPoolId: string; + let initialBalances: BigNumber[]; + + let sender: SignerWithAddress, recipient: SignerWithAddress; + + let normalSwap: (singleSwap: SingleSwap) => Promise; + let normalJoin: () => Promise; + let normalExit: () => Promise; + + const PROTOCOL_SWAP_FEE_PERCENTAGE = fp(0.3); + const OTHER_EXIT_KIND = 1; + const OTHER_JOIN_KIND = 1; + + sharedBeforeEach('deploy and initialize pool', async () => { + sender = poolOwner; + recipient = poolOwner; + const initialBalancePerToken = 1000; + + initialBalances = Array(tokens.length).fill(fp(initialBalancePerToken)); + pool = await deployBasePool({ pauseWindowDuration: MONTH }); + poolId = await pool.getPoolId(); + + minimalPool = await deployBasePool({ + pauseWindowDuration: MONTH, + specialization: PoolSpecialization.MinimalSwapInfoPool, + }); + minimalPoolId = await minimalPool.getPoolId(); + + const request: JoinPoolRequest = { + assets: tokens.addresses, + maxAmountsIn: initialBalances, + userData: WeightedPoolEncoder.joinInit(initialBalances), + fromInternalBalance: false, + }; + + // We mint twice the initial pool balance to fund two pools. + await tokens.mint({ to: sender, amount: fp(2 * initialBalancePerToken + random(1000)) }); + await tokens.approve({ from: sender, to: vault }); + + await vault.connect(sender).joinPool(poolId, sender.address, recipient.address, request); + await vault.connect(sender).joinPool(minimalPoolId, sender.address, recipient.address, request); + }); + + sharedBeforeEach('prepare normal swaps', () => { + const funds: FundManagement = { + sender: poolOwner.address, + recipient: poolOwner.address, + fromInternalBalance: false, + toInternalBalance: false, + }; + + // min amount: 0, deadline: max. + normalSwap = async (singleSwap: SingleSwap) => + (await vault.connect(sender).swap(singleSwap, funds, 0, MAX_UINT256)).wait(); + }); + + sharedBeforeEach('prepare normal join and exit', () => { + const joinRequest: JoinPoolRequest = { + assets: tokens.addresses, + maxAmountsIn: Array(tokens.length).fill(fp(1)), + userData: defaultAbiCoder.encode(['uint256'], [OTHER_JOIN_KIND]), + fromInternalBalance: false, + }; + + normalJoin = async () => + (await vault.connect(sender).joinPool(poolId, sender.address, recipient.address, joinRequest)).wait(); + + const exitRequest: ExitPoolRequest = { + assets: tokens.addresses, + minAmountsOut: Array(tokens.length).fill(0), + userData: defaultAbiCoder.encode(['uint256'], [OTHER_EXIT_KIND]), + toInternalBalance: false, + }; + + normalExit = async () => + (await vault.connect(sender).exitPool(poolId, sender.address, recipient.address, exitRequest)).wait(); + }); + + sharedBeforeEach('set a non-zero protocol swap fee percentage', async () => { + const feesCollector = await deployedAt('v2-vault/ProtocolFeesCollector', await vault.getProtocolFeesCollector()); + + await authorizer + .connect(admin) + .grantPermissions([await actionId(feesCollector, 'setSwapFeePercentage')], admin.address, [ANY_ADDRESS]); + + await feesCollector.connect(admin).setSwapFeePercentage(PROTOCOL_SWAP_FEE_PERCENTAGE); + + expect(await feesCollector.getSwapFeePercentage()).to.equal(PROTOCOL_SWAP_FEE_PERCENTAGE); + }); + + context('when not in recovery mode', () => { + it('the recovery mode exit reverts', async () => { + const preExitBPT = await pool.balanceOf(sender.address); + const exitBPT = preExitBPT.div(3); + + const request: ExitPoolRequest = { + assets: tokens.addresses, + minAmountsOut: Array(tokens.length).fill(0), + userData: defaultAbiCoder.encode(['uint256', 'uint256'], [RECOVERY_MODE_EXIT_KIND, exitBPT]), + toInternalBalance: false, + }; + + await expect( + vault.connect(sender).exitPool(poolId, sender.address, recipient.address, request) + ).to.be.revertedWith('NOT_IN_RECOVERY_MODE'); + }); + + itSwaps(); + + itJoins(); + + itExits(); + }); + + context('when in recovery mode', () => { + sharedBeforeEach('enable recovery mode', async () => { + const enableRecoveryAction = await actionId(pool, 'enableRecoveryMode'); + const disableRecoveryAction = await actionId(pool, 'disableRecoveryMode'); + await authorizer + .connect(admin) + .grantPermissions([enableRecoveryAction, disableRecoveryAction], admin.address, [ANY_ADDRESS, ANY_ADDRESS]); + + await pool.connect(admin).enableRecoveryMode(); + }); + + itSwaps(); + + itJoins(); + + itExits(); + + function itExitsViaRecoveryModeCorrectly() { + let request: ExitPoolRequest; + let preExitBPT: BigNumber, exitBPT: BigNumber; + + sharedBeforeEach(async () => { + preExitBPT = await pool.balanceOf(sender.address); + exitBPT = preExitBPT.div(3); + + request = { + assets: tokens.addresses, + minAmountsOut: Array(tokens.length).fill(0), + userData: defaultAbiCoder.encode(['uint256', 'uint256'], [RECOVERY_MODE_EXIT_KIND, exitBPT]), + toInternalBalance: false, + }; + }); + + it('passes the correct arguments to `_doRecoveryModeExit`', async () => { + const totalSupply = await pool.totalSupply(); + const tx = await vault.connect(sender).exitPool(poolId, sender.address, recipient.address, request); + expectEvent.inIndirectReceipt(await tx.wait(), pool.interface, 'RecoveryModeExit', { + totalSupply, + balances: initialBalances, + bptAmountIn: exitBPT, + }); + }); + + it('burns the expected amount of BPT', async () => { + await vault.connect(sender).exitPool(poolId, sender.address, recipient.address, request); + + const afterExitBalance = await pool.balanceOf(sender.address); + expect(afterExitBalance).to.equal(preExitBPT.sub(exitBPT)); + }); + + it('returns 0 due protocol fees', async () => { + const onExitReturn = await pool + .connect(vaultSigner) + .callStatic.onExitPool(poolId, sender.address, recipient.address, initialBalances, 0, 0, request.userData); + + expect(onExitReturn.length).to.be.eq(2); + expect(onExitReturn[1]).to.deep.eq(Array(tokens.length).fill(bn(0))); + }); + } + + itExitsViaRecoveryModeCorrectly(); + + context('when paused', () => { + sharedBeforeEach('pause pool', async () => { + await authorizer + .connect(admin) + .grantPermissions([await actionId(pool, 'pause')], admin.address, [ANY_ADDRESS]); + + await pool.connect(admin).pause(); + }); + + itExitsViaRecoveryModeCorrectly(); + }); + }); + + function itSwaps() { + let singleSwap: SingleSwap; + let swapRequest: SwapRequest; + + describe('minimal swaps', () => { + sharedBeforeEach('prepare swap request', async () => { + singleSwap = { + poolId: minimalPoolId, + kind: SwapKind.GivenIn, + assetIn: tokens.get(0).instance.address, + assetOut: tokens.get(1).instance.address, + amount: 1, // Needs to be > 0 + userData: '0xdeadbeef', + }; + + const lastChangeBlock = (await vault.getPoolTokens(minimalPoolId)).lastChangeBlock; + swapRequest = { + kind: singleSwap.kind, + tokenIn: singleSwap.assetIn, + tokenOut: singleSwap.assetOut, + amount: singleSwap.amount, + poolId: singleSwap.poolId, + lastChangeBlock: lastChangeBlock, + from: sender.address, + to: recipient.address, + userData: singleSwap.userData, + }; + }); + + it('do not revert', async () => { + await expect(normalSwap(singleSwap)).to.not.be.reverted; + }); + + it('calls inner onSwapMinimal hook with swap parameters', async () => { + const receipt = await normalSwap(singleSwap); + + expectEvent.inIndirectReceipt(receipt, minimalPool.interface, 'InnerOnSwapMinimalCalled', { + request: Object.values(swapRequest), + balanceTokenIn: initialBalances[0], + balanceTokenOut: initialBalances[1], + }); + }); + + it('returns the output of the inner onSwapMinimal hook', async () => { + const onSwap = + 'onSwap((uint8,address,address,uint256,bytes32,uint256,address,address,bytes),uint256,uint256)'; + const onSwapReturn = await minimalPool.connect(vaultSigner).callStatic[onSwap](swapRequest, 0, 0); + expect(onSwapReturn).to.be.eq(await minimalPool.ON_SWAP_MINIMAL_RETURN()); + }); + + it('reverts if swap hook caller is not the vault', async () => { + const onSwap = + 'onSwap((uint8,address,address,uint256,bytes32,uint256,address,address,bytes),uint256,uint256)'; + await expect(minimalPool.connect(other)[onSwap](swapRequest, 0, 0)).to.be.revertedWith('CALLER_NOT_VAULT'); + }); + }); + + describe('general swaps', () => { + sharedBeforeEach('prepare swap request', async () => { + singleSwap = { + poolId, + kind: SwapKind.GivenIn, + assetIn: tokens.get(1).instance.address, + assetOut: tokens.get(2).instance.address, + amount: 1, // Needs to be > 0 + userData: '0xdeadbeef', + }; + + const lastChangeBlock = (await vault.getPoolTokens(poolId)).lastChangeBlock; + swapRequest = { + kind: singleSwap.kind, + tokenIn: singleSwap.assetIn, + tokenOut: singleSwap.assetOut, + amount: singleSwap.amount, + poolId: singleSwap.poolId, + lastChangeBlock: lastChangeBlock, + from: sender.address, + to: recipient.address, + userData: singleSwap.userData, + }; + }); + + it('do not revert', async () => { + await expect(normalSwap(singleSwap)).to.not.be.reverted; + }); + + it('calls inner onSwapGeneral hook with swap parameters', async () => { + const receipt = await normalSwap(singleSwap); + + expectEvent.inIndirectReceipt(receipt, pool.interface, 'InnerOnSwapGeneralCalled', { + request: Object.values(swapRequest), + balances: initialBalances, + indexIn: 1, + indexOut: 2, + }); + }); + + it('returns the output of the inner onSwapGeneral hook', async () => { + const onSwap = + 'onSwap((uint8,address,address,uint256,bytes32,uint256,address,address,bytes),uint256[],uint256,uint256)'; + const onSwapReturn = await pool.connect(vaultSigner).callStatic[onSwap](swapRequest, [], 0, 0); + expect(onSwapReturn).to.be.eq(await pool.ON_SWAP_GENERAL_RETURN()); + }); + + it('reverts if swap hook caller is not the vault', async () => { + const onSwap = + 'onSwap((uint8,address,address,uint256,bytes32,uint256,address,address,bytes),uint256[],uint256,uint256)'; + await expect(minimalPool.connect(other)[onSwap](swapRequest, [], 0, 0)).to.be.revertedWith( + 'CALLER_NOT_VAULT' + ); + }); + }); + } + + function itJoins() { + describe('normal joins', () => { + it('do not revert', async () => { + await expect(normalJoin()).to.not.be.reverted; + }); + + it('calls inner onJoin hook with join parameters', async () => { + const receipt = await normalJoin(); + expectEvent.inIndirectReceipt(receipt, pool.interface, 'InnerOnJoinPoolCalled', { + sender: sender.address, + balances: initialBalances, + userData: defaultAbiCoder.encode(['uint256'], [OTHER_JOIN_KIND]), + }); + }); + + it('returns the output of the inner onJoin hook and 0 due protocol fees', async () => { + const onJoinReturn = await pool + .connect(vaultSigner) + .callStatic.onJoinPool(poolId, sender.address, recipient.address, initialBalances, 0, 0, '0x'); + expect(onJoinReturn).to.be.deep.eq([ + Array(tokens.length).fill(await pool.ON_JOIN_RETURN()), + Array(tokens.length).fill(bn(0)), + ]); + }); + }); + } + + function itExits() { + describe('normal exits', () => { + it('do not revert', async () => { + await expect(normalExit()).to.not.be.reverted; + }); + + it('calls inner onExit hook with exit parameters', async () => { + const receipt = await normalExit(); + expectEvent.inIndirectReceipt(receipt, pool.interface, 'InnerOnExitPoolCalled', { + sender: sender.address, + balances: initialBalances, + userData: defaultAbiCoder.encode(['uint256'], [OTHER_EXIT_KIND]), + }); + }); + + it('returns the output of the inner onExit hook and 0 due protocol fees', async () => { + const onExitReturn = await pool + .connect(vaultSigner) + .callStatic.onExitPool(poolId, sender.address, recipient.address, initialBalances, 0, 0, '0x'); + expect(onExitReturn).to.be.deep.eq([ + Array(tokens.length).fill(await pool.ON_EXIT_RETURN()), + Array(tokens.length).fill(bn(0)), + ]); + }); + }); + } + }); + + describe('pool initialization', () => { + let pool: Contract; + let sender: SignerWithAddress, recipient: SignerWithAddress; + let poolId: string, userData: string; + let request: JoinPoolRequest; + let initialBalances: Array; + + sharedBeforeEach('set up pool and initial join request', async () => { + sender = poolOwner; + recipient = other; + pool = await deployBasePool({ + pauseWindowDuration: PAUSE_WINDOW_DURATION, + }); + poolId = await pool.getPoolId(); + + const initialBalancePerToken = 1000; + + await tokens.mint({ to: sender, amount: fp(initialBalancePerToken) }); + await tokens.approve({ from: sender, to: vault }); + + initialBalances = Array(tokens.length).fill(fp(initialBalancePerToken)); + userData = WeightedPoolEncoder.joinInit(initialBalances); + request = { + assets: tokens.addresses, + maxAmountsIn: initialBalances, + userData, + fromInternalBalance: false, + }; + }); + + context('when paused', () => { + sharedBeforeEach(async () => { + await authorizer + .connect(admin) + .grantPermissions([await actionId(pool, 'pause')], sender.address, [ANY_ADDRESS]); + await pool.connect(sender).pause(); + }); + + it('reverts', async () => { + await expect( + vault.connect(sender).joinPool(poolId, sender.address, recipient.address, request) + ).to.be.revertedWith('PAUSED'); + }); + }); + + context('when not paused', () => { + it('calls inner initialization hook', async () => { + const receipt = await ( + await vault.connect(sender).joinPool(poolId, sender.address, recipient.address, request) + ).wait(); + + expectEvent.inIndirectReceipt(receipt, pool.interface, 'InnerOnInitializePoolCalled', { + userData, + }); + }); + + it('locks the minimum bpt in the zero address', async () => { + const receipt = await ( + await vault.connect(sender).joinPool(poolId, sender.address, recipient.address, request) + ).wait(); + + expectTransferEvent(receipt, { from: ZERO_ADDRESS, to: ZERO_ADDRESS, value: await pool.getMinimumBpt() }, pool); + }); + + it('mints bpt to recipient', async () => { + const receipt = await ( + await vault.connect(sender).joinPool(poolId, sender.address, recipient.address, request) + ).wait(); + + // total BPT is calculated by the mock initial hook; base pool mint it after substracting the minimum BPT amount. + const minimumBpt = await pool.getMinimumBpt(); + const totalBptOut = initialBalances.reduce((previous, current) => previous.add(current)); + expectTransferEvent( + receipt, + { from: ZERO_ADDRESS, to: recipient.address, value: totalBptOut.sub(minimumBpt) }, + pool + ); + }); + + it('returns the output of the inner onInitialize hook and 0 due protocol fees', async () => { + const onInitReturn = await pool + .connect(vaultSigner) + .callStatic.onJoinPool(poolId, sender.address, recipient.address, initialBalances, 0, 0, request.userData); + expect(onInitReturn).to.be.deep.eq([initialBalances, Array(tokens.length).fill(bn(0))]); + }); + }); + }); +}); diff --git a/pkg/pool-weighted/contracts/BaseWeightedPool.sol b/pkg/pool-weighted/contracts/BaseWeightedPool.sol index 8ec10c86da..cdf838d1e0 100644 --- a/pkg/pool-weighted/contracts/BaseWeightedPool.sol +++ b/pkg/pool-weighted/contracts/BaseWeightedPool.sol @@ -46,7 +46,7 @@ abstract contract BaseWeightedPool is BaseMinimalSwapInfoPool { address owner, bool mutableTokens ) - LegacyBasePool( + BasePool( vault, // Given BaseMinimalSwapInfoPool supports both of these specializations, and this Pool never registers // or deregisters any tokens after construction, picking Two Token when the Pool only has two tokens is free diff --git a/pkg/pool-weighted/contracts/WeightedPool.sol b/pkg/pool-weighted/contracts/WeightedPool.sol index 3a42677d9c..efa78788f6 100644 --- a/pkg/pool-weighted/contracts/WeightedPool.sol +++ b/pkg/pool-weighted/contracts/WeightedPool.sol @@ -396,7 +396,7 @@ contract WeightedPool is BaseWeightedPool, WeightedPoolProtocolFees { internal view virtual - override(LegacyBasePool, WeightedPoolProtocolFees) + override(BasePool, WeightedPoolProtocolFees) returns (bool) { return super._isOwnerOnlyAction(actionId); diff --git a/pkg/pool-weighted/contracts/WeightedPoolProtocolFees.sol b/pkg/pool-weighted/contracts/WeightedPoolProtocolFees.sol index 8257357e4d..8a4c537fbb 100644 --- a/pkg/pool-weighted/contracts/WeightedPoolProtocolFees.sol +++ b/pkg/pool-weighted/contracts/WeightedPoolProtocolFees.sol @@ -351,7 +351,7 @@ abstract contract WeightedPoolProtocolFees is BaseWeightedPool, ProtocolFeeCache internal view virtual - override(LegacyBasePool, BasePoolAuthorization) + override(BasePool, BasePoolAuthorization) returns (bool) { return super._isOwnerOnlyAction(actionId); diff --git a/pkg/pool-weighted/contracts/managed/ManagedPool.sol b/pkg/pool-weighted/contracts/managed/ManagedPool.sol index 14c6f2033f..60c44f3168 100644 --- a/pkg/pool-weighted/contracts/managed/ManagedPool.sol +++ b/pkg/pool-weighted/contracts/managed/ManagedPool.sol @@ -67,7 +67,7 @@ contract ManagedPool is ManagedPoolSettings { uint256 pauseWindowDuration, uint256 bufferPeriodDuration ) - BasePool( + NewBasePool( vault, PoolRegistrationLib.registerComposablePool( vault, diff --git a/pkg/pool-weighted/contracts/managed/ManagedPoolSettings.sol b/pkg/pool-weighted/contracts/managed/ManagedPoolSettings.sol index 619362fb95..2dd6184b47 100644 --- a/pkg/pool-weighted/contracts/managed/ManagedPoolSettings.sol +++ b/pkg/pool-weighted/contracts/managed/ManagedPoolSettings.sol @@ -27,7 +27,7 @@ import "@balancer-labs/v2-pool-utils/contracts/lib/PoolRegistrationLib.sol"; import "@balancer-labs/v2-pool-utils/contracts/external-fees/InvariantGrowthProtocolSwapFees.sol"; import "@balancer-labs/v2-pool-utils/contracts/external-fees/ProtocolFeeCache.sol"; import "@balancer-labs/v2-pool-utils/contracts/external-fees/ExternalAUMFees.sol"; -import "@balancer-labs/v2-pool-utils/contracts/BasePool.sol"; +import "@balancer-labs/v2-pool-utils/contracts/NewBasePool.sol"; import "../lib/GradualValueChange.sol"; import "../managed/CircuitBreakerStorageLib.sol"; @@ -41,7 +41,7 @@ import "./ManagedPoolAddRemoveTokenLib.sol"; /** * @title Managed Pool Settings */ -abstract contract ManagedPoolSettings is BasePool, ProtocolFeeCache, IManagedPool { +abstract contract ManagedPoolSettings is NewBasePool, ProtocolFeeCache, IManagedPool { // ManagedPool weights and swap fees can change over time: these periods are expected to be long enough (e.g. days) // that any timestamp manipulation would achieve very little. // solhint-disable not-rely-on-time diff --git a/pkg/pool-weighted/contracts/test/MockManagedPoolSettings.sol b/pkg/pool-weighted/contracts/test/MockManagedPoolSettings.sol index 4d83b0a364..cc5fb0f75f 100644 --- a/pkg/pool-weighted/contracts/test/MockManagedPoolSettings.sol +++ b/pkg/pool-weighted/contracts/test/MockManagedPoolSettings.sol @@ -32,7 +32,7 @@ contract MockManagedPoolSettings is ManagedPoolSettings { uint256 pauseWindowDuration, uint256 bufferPeriodDuration ) - BasePool( + NewBasePool( vault, PoolRegistrationLib.registerPoolWithAssetManagers( vault,