From 9b0a18309ff818281e4a095e70d2b093c996388c Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Sat, 29 Oct 2022 01:32:38 -0400 Subject: [PATCH 1/4] refactor: rebase LBP --- pkg/balancer-js/src/utils/errors.ts | 1 + .../solidity-utils/helpers/BalancerErrors.sol | 1 + .../lbp/LiquidityBootstrappingPool.sol | 521 +++++++++++++++--- .../test/BaseWeightedPool.behavior.ts | 12 +- .../test/LiquidityBootstrappingPool.test.ts | 15 + 5 files changed, 473 insertions(+), 77 deletions(-) diff --git a/pkg/balancer-js/src/utils/errors.ts b/pkg/balancer-js/src/utils/errors.ts index f5669968c9..002ce7f1a2 100644 --- a/pkg/balancer-js/src/utils/errors.ts +++ b/pkg/balancer-js/src/utils/errors.ts @@ -83,6 +83,7 @@ const balancerErrorCodes: Record = { '354': 'ADD_OR_REMOVE_BPT', '355': 'INVALID_CIRCUIT_BREAKER_BOUNDS', '356': 'CIRCUIT_BREAKER_TRIPPED', + '357': 'UNHANDLED_BY_LBP', '400': 'REENTRANCY', '401': 'SENDER_NOT_ALLOWED', '402': 'PAUSED', diff --git a/pkg/interfaces/contracts/solidity-utils/helpers/BalancerErrors.sol b/pkg/interfaces/contracts/solidity-utils/helpers/BalancerErrors.sol index 25a925e149..db66dacf7a 100644 --- a/pkg/interfaces/contracts/solidity-utils/helpers/BalancerErrors.sol +++ b/pkg/interfaces/contracts/solidity-utils/helpers/BalancerErrors.sol @@ -196,6 +196,7 @@ library Errors { uint256 internal constant ADD_OR_REMOVE_BPT = 354; uint256 internal constant INVALID_CIRCUIT_BREAKER_BOUNDS = 355; uint256 internal constant CIRCUIT_BREAKER_TRIPPED = 356; + uint256 internal constant UNHANDLED_BY_LBP = 357; // Lib uint256 internal constant REENTRANCY = 400; diff --git a/pkg/pool-weighted/contracts/lbp/LiquidityBootstrappingPool.sol b/pkg/pool-weighted/contracts/lbp/LiquidityBootstrappingPool.sol index 71b69ac062..71624c58bc 100644 --- a/pkg/pool-weighted/contracts/lbp/LiquidityBootstrappingPool.sol +++ b/pkg/pool-weighted/contracts/lbp/LiquidityBootstrappingPool.sol @@ -15,19 +15,24 @@ pragma solidity ^0.7.0; pragma experimental ABIEncoderV2; -import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/ReentrancyGuard.sol"; +import "@balancer-labs/v2-interfaces/contracts/pool-weighted/WeightedPoolUserData.sol"; +import "@balancer-labs/v2-interfaces/contracts/vault/IMinimalSwapInfoPool.sol"; + import "@balancer-labs/v2-solidity-utils/contracts/helpers/WordCodec.sol"; import "@balancer-labs/v2-solidity-utils/contracts/math/Math.sol"; +import "@balancer-labs/v2-solidity-utils/contracts/helpers/ScalingHelpers.sol"; + +import "@balancer-labs/v2-pool-utils/contracts/lib/PoolRegistrationLib.sol"; +import "@balancer-labs/v2-pool-utils/contracts/NewBasePool.sol"; import "../lib/GradualValueChange.sol"; import "../lib/ValueCompression.sol"; - -import "../BaseWeightedPool.sol"; +import "../WeightedMath.sol"; /** * @dev Weighted Pool with mutable weights, designed to support V2 Liquidity Bootstrapping. */ -contract LiquidityBootstrappingPool is BaseWeightedPool, ReentrancyGuard { +contract LiquidityBootstrappingPool is IMinimalSwapInfoPool, NewBasePool { // LiquidityBootstrappingPool change their weights 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 @@ -35,15 +40,22 @@ contract LiquidityBootstrappingPool is BaseWeightedPool, ReentrancyGuard { using FixedPoint for uint256; using WordCodec for bytes32; using ValueCompression for uint256; + using BasePoolUserData for bytes; + using WeightedPoolUserData for bytes; // LBPs often involve only two tokens - we support up to four since we're able to pack the entire config in a single // storage slot. + uint256 private constant _MIN_TOKENS = 2; uint256 private constant _MAX_LBP_TOKENS = 4; - // State variables + // 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% uint256 private immutable _totalTokens; + uint256 private _swapFeePercentage; + IERC20 internal immutable _token0; IERC20 internal immutable _token1; IERC20 internal immutable _token2; @@ -61,21 +73,27 @@ contract LiquidityBootstrappingPool is BaseWeightedPool, ReentrancyGuard { // For gas optimization, store start/end weights and timestamps in one bytes32 // Start weights need to be high precision, since restarting the update resets them to "spot" // values. Target end weights do not need as much precision. - // [ 32 bits | 32 bits | 64 bits | 124 bits | 3 bits | 1 bit ] - // [ end timestamp | start timestamp | 4x16 end weights | 4x31 start weights | not used | swap enabled ] - // |MSB LSB| + // [ 32 bits | 32 bits | 64 bits | 124 bits | 2 bits | 1 bit | 1 bit ] + // [ end timestamp | start timestamp | 4x16 end weights | 4x31 start weights | not used | recovery | swap enabled ] + // |MSB LSB| bytes32 private _poolState; // Offsets for data elements in _poolState uint256 private constant _SWAP_ENABLED_OFFSET = 0; - uint256 private constant _START_WEIGHT_OFFSET = 4; - uint256 private constant _END_WEIGHT_OFFSET = 128; - uint256 private constant _START_TIME_OFFSET = 192; - uint256 private constant _END_TIME_OFFSET = 224; + uint256 private constant _RECOVERY_MODE_BIT_OFFSET = 1; + uint256 private constant _START_WEIGHT_OFFSET = _RECOVERY_MODE_BIT_OFFSET + 2; + uint256 private constant _END_WEIGHT_OFFSET = _START_WEIGHT_OFFSET + _MAX_LBP_TOKENS * _START_WEIGHT_BIT_LENGTH; + uint256 private constant _START_TIME_OFFSET = _END_WEIGHT_OFFSET + _MAX_LBP_TOKENS * _END_WEIGHT_BIT_LENGTH; + uint256 private constant _END_TIME_OFFSET = _START_TIME_OFFSET + _TIMESTAMP_BIT_LENGTH; + + uint256 private constant _START_WEIGHT_BIT_LENGTH = 31; + uint256 private constant _END_WEIGHT_BIT_LENGTH = 16; + uint256 private constant _TIMESTAMP_BIT_LENGTH = 32; // Event declarations + event SwapFeePercentageChanged(uint256 swapFeePercentage); event SwapEnabledSet(bool swapEnabled); event GradualWeightUpdateScheduled( uint256 startTime, @@ -96,21 +114,24 @@ contract LiquidityBootstrappingPool is BaseWeightedPool, ReentrancyGuard { address owner, bool swapEnabledOnStart ) - BaseWeightedPool( + NewBasePool( vault, + PoolRegistrationLib.registerPool( + vault, + tokens.length == 2 ? IVault.PoolSpecialization.TWO_TOKEN : IVault.PoolSpecialization.MINIMAL_SWAP_INFO, + tokens + ), name, symbol, - tokens, - new address[](tokens.length), // Pass the zero address: LBPs can't have asset managers - swapFeePercentage, pauseWindowDuration, bufferPeriodDuration, - owner, - false + owner ) { uint256 totalTokens = tokens.length; InputHelpers.ensureInputLengthMatch(totalTokens, normalizedWeights.length); + _require(tokens.length >= _MIN_TOKENS, Errors.MIN_TOKENS); + _require(tokens.length <= _MAX_LBP_TOKENS, Errors.MAX_TOKENS); _totalTokens = totalTokens; @@ -129,6 +150,8 @@ contract LiquidityBootstrappingPool is BaseWeightedPool, ReentrancyGuard { _startGradualWeightChange(currentTime, currentTime, normalizedWeights, normalizedWeights); + _setSwapFeePercentage(swapFeePercentage); + // If false, the pool will start in the disabled state (prevents front-running the enable swaps transaction) _setSwapEnabled(swapEnabledOnStart); } @@ -136,12 +159,27 @@ contract LiquidityBootstrappingPool is BaseWeightedPool, ReentrancyGuard { // External functions /** - * @dev Tells whether swaps are enabled or not for the given pool. + * @notice Return whether swaps are enabled or not for the given pool. */ function getSwapEnabled() public view returns (bool) { return _poolState.decodeBool(_SWAP_ENABLED_OFFSET); } + /** + * @notice Return the current value of the swap fee percentage. + * @dev This is stored separately, as there is no more room in `_poolState`. + */ + function getSwapFeePercentage() public view virtual override returns (uint256) { + return _swapFeePercentage; + } + + /** + * @notice Return the current token weights. + */ + function getNormalizedWeights() external view returns (uint256[] memory) { + return _getNormalizedWeights(); + } + /** * @dev Return start time, end time, and endWeights as an array. * Current weights should be retrieved via `getNormalizedWeights()`. @@ -169,21 +207,21 @@ contract LiquidityBootstrappingPool is BaseWeightedPool, ReentrancyGuard { } /** - * @dev Can pause/unpause trading + * @notice Pause/unpause trading. */ - function setSwapEnabled(bool swapEnabled) external authenticate whenNotPaused nonReentrant { + function setSwapEnabled(bool swapEnabled) external authenticate whenNotPaused { _setSwapEnabled(swapEnabled); } /** - * @dev Schedule a gradual weight change, from the current weights to the given endWeights, - * over startTime to endTime + * @notice Schedule a gradual weight change. + * @dev Weights will change from the current weights to the given endWeights, over startTime to endTime. */ function updateWeightsGradually( uint256 startTime, uint256 endTime, uint256[] memory endWeights - ) external authenticate whenNotPaused nonReentrant { + ) external authenticate whenNotPaused { InputHelpers.ensureInputLengthMatch(_getTotalTokens(), endWeights.length); startTime = GradualValueChange.resolveStartTime(startTime, endTime); @@ -192,7 +230,7 @@ contract LiquidityBootstrappingPool is BaseWeightedPool, ReentrancyGuard { // Internal functions - function _getNormalizedWeight(IERC20 token) internal view override returns (uint256) { + function _getNormalizedWeight(IERC20 token) internal view returns (uint256) { uint256 i; // First, convert token address to a token index @@ -218,7 +256,7 @@ contract LiquidityBootstrappingPool is BaseWeightedPool, ReentrancyGuard { return GradualValueChange.getInterpolatedValue(startWeight, endWeight, startTime, endTime); } - function _getNormalizedWeights() internal view override returns (uint256[] memory) { + function _getNormalizedWeights() internal view returns (uint256[] memory) { uint256 totalTokens = _getTotalTokens(); uint256[] memory normalizedWeights = new uint256[](totalTokens); @@ -240,80 +278,386 @@ contract LiquidityBootstrappingPool is BaseWeightedPool, ReentrancyGuard { // Pool callback functions // Prevent any account other than the owner from joining the pool - function _onInitializePool( - bytes32 poolId, address sender, - address recipient, - uint256[] memory scalingFactors, + address, bytes memory userData - ) internal override returns (uint256, uint256[] memory) { + ) internal view override returns (uint256, uint256[] memory) { // Only the owner can initialize the pool _require(sender == getOwner(), Errors.CALLER_IS_NOT_LBP_OWNER); - return super._onInitializePool(poolId, sender, recipient, scalingFactors, userData); + WeightedPoolUserData.JoinKind kind = userData.joinKind(); + _require(kind == WeightedPoolUserData.JoinKind.INIT, Errors.UNINITIALIZED); + + uint256[] memory amountsIn = userData.initialAmountsIn(); + uint256[] memory scalingFactors = getScalingFactors(); + + InputHelpers.ensureInputLengthMatch(amountsIn.length, scalingFactors.length); + _upscaleArray(amountsIn, scalingFactors); + + uint256[] memory normalizedWeights = _getNormalizedWeights(); + uint256 invariantAfterJoin = WeightedMath._calculateInvariant(normalizedWeights, amountsIn); + + // Set the initial BPT to the value of the invariant times the number of tokens. This makes BPT supply more + // consistent in Pools with similar compositions but different number of tokens. + uint256 bptAmountOut = Math.mul(invariantAfterJoin, amountsIn.length); + + return (bptAmountOut, amountsIn); } + /** + * @dev Called whenever the Pool is joined after the first initialization join (see `_onInitializePool`). + * + * Returns the amount of BPT to mint, the token amounts that the Pool will receive in return, and the number of + * tokens to pay in protocol swap fees. + * + * Implementations of this function might choose to mutate the `balances` array to save gas (e.g. when + * performing intermediate calculations, such as subtraction of due protocol fees). This can be done safely. + * + * Minted BPT will be sent to `recipient`. + * + * 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 _onJoinPool( - bytes32 poolId, address sender, - address recipient, uint256[] memory balances, - uint256 lastChangeBlock, - uint256 protocolSwapFeePercentage, - uint256[] memory scalingFactors, bytes memory userData - ) internal override returns (uint256, uint256[] memory) { + ) internal virtual override returns (uint256, uint256[] memory) { // Only the owner can add liquidity; block public LPs _require(sender == getOwner(), Errors.CALLER_IS_NOT_LBP_OWNER); - return - super._onJoinPool( - poolId, - sender, - recipient, - balances, - lastChangeBlock, - protocolSwapFeePercentage, - scalingFactors, - userData - ); + (uint256 bptAmountOut, uint256[] memory amountsIn) = _doJoin( + sender, + balances, + _getNormalizedWeights(), + getScalingFactors(), + totalSupply(), + userData + ); + + return (bptAmountOut, amountsIn); + } + + /** + * @dev Dispatch code which decodes the provided userdata to perform the specified join type. + * Inheriting contracts may override this function to add additional join types or extra conditions to allow + * or disallow joins under certain circumstances. + */ + function _doJoin( + address, + uint256[] memory balances, + uint256[] memory normalizedWeights, + uint256[] memory scalingFactors, + uint256 totalSupply, + bytes memory userData + ) internal view virtual returns (uint256, uint256[] memory) { + WeightedPoolUserData.JoinKind kind = userData.joinKind(); + + if (kind == WeightedPoolUserData.JoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT) { + return _joinExactTokensInForBPTOut(balances, normalizedWeights, scalingFactors, totalSupply, userData); + } else if (kind == WeightedPoolUserData.JoinKind.TOKEN_IN_FOR_EXACT_BPT_OUT) { + return _joinTokenInForExactBPTOut(balances, normalizedWeights, totalSupply, userData); + } else if (kind == WeightedPoolUserData.JoinKind.ALL_TOKENS_IN_FOR_EXACT_BPT_OUT) { + return _joinAllTokensInForExactBPTOut(balances, totalSupply, userData); + } else { + _revert(Errors.UNHANDLED_JOIN_KIND); + } + } + + function _joinExactTokensInForBPTOut( + uint256[] memory balances, + uint256[] memory normalizedWeights, + uint256[] memory scalingFactors, + uint256 totalSupply, + bytes memory userData + ) private view returns (uint256, uint256[] memory) { + (uint256[] memory amountsIn, uint256 minBPTAmountOut) = userData.exactTokensInForBptOut(); + InputHelpers.ensureInputLengthMatch(balances.length, amountsIn.length); + + _upscaleArray(amountsIn, scalingFactors); + + uint256 bptAmountOut = WeightedMath._calcBptOutGivenExactTokensIn( + balances, + normalizedWeights, + amountsIn, + totalSupply, + getSwapFeePercentage() + ); + + _require(bptAmountOut >= minBPTAmountOut, Errors.BPT_OUT_MIN_AMOUNT); + + return (bptAmountOut, amountsIn); + } + + function _joinTokenInForExactBPTOut( + uint256[] memory balances, + uint256[] memory normalizedWeights, + uint256 totalSupply, + bytes memory userData + ) private view returns (uint256, uint256[] memory) { + (uint256 bptAmountOut, uint256 tokenIndex) = userData.tokenInForExactBptOut(); + // Note that there is no maximum amountIn parameter: this is handled by `IVault.joinPool`. + + _require(tokenIndex < balances.length, Errors.OUT_OF_BOUNDS); + + uint256 amountIn = WeightedMath._calcTokenInGivenExactBptOut( + balances[tokenIndex], + normalizedWeights[tokenIndex], + bptAmountOut, + totalSupply, + getSwapFeePercentage() + ); + + // We join in a single token, so we initialize amountsIn with zeros + uint256[] memory amountsIn = new uint256[](balances.length); + // And then assign the result to the selected token + amountsIn[tokenIndex] = amountIn; + + return (bptAmountOut, amountsIn); + } + + function _joinAllTokensInForExactBPTOut( + uint256[] memory balances, + uint256 totalSupply, + bytes memory userData + ) private pure returns (uint256, uint256[] memory) { + uint256 bptAmountOut = userData.allTokensInForExactBptOut(); + // Note that there is no maximum amountsIn parameter: this is handled by `IVault.joinPool`. + + uint256[] memory amountsIn = WeightedMath._calcAllTokensInGivenExactBptOut(balances, bptAmountOut, totalSupply); + + return (bptAmountOut, amountsIn); + } + + /** + * @dev Called whenever the Pool is exited. + * + * Returns the amount of BPT to burn, the token amounts for each Pool token that the Pool will grant in return, and + * the number of tokens to pay in protocol swap fees. + * + * Implementations of this function might choose to mutate the `balances` array to save gas (e.g. when + * performing intermediate calculations, such as subtraction of due protocol fees). This can be done safely. + * + * BPT will be burnt from `sender`. + * + * The Pool will grant tokens to `recipient`. These amounts are considered upscaled and will be downscaled + * (rounding down) before being returned to the Vault. + */ + function _onExitPool( + address sender, + uint256[] memory balances, + bytes memory userData + ) internal virtual override returns (uint256, uint256[] memory) { + return _doExit(sender, balances, _getNormalizedWeights(), getScalingFactors(), totalSupply(), userData); + } + + /** + * @dev Dispatch code which decodes the provided userdata to perform the specified exit type. + * Inheriting contracts may override this function to add additional exit types or extra conditions to allow + * or disallow exit under certain circumstances. + */ + function _doExit( + address, + uint256[] memory balances, + uint256[] memory normalizedWeights, + uint256[] memory scalingFactors, + uint256 totalSupply, + bytes memory userData + ) internal view virtual returns (uint256, uint256[] memory) { + WeightedPoolUserData.ExitKind kind = userData.exitKind(); + + if (kind == WeightedPoolUserData.ExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT) { + return _exitExactBPTInForTokenOut(balances, normalizedWeights, totalSupply, userData); + } else if (kind == WeightedPoolUserData.ExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT) { + return _exitExactBPTInForTokensOut(balances, totalSupply, userData); + } else if (kind == WeightedPoolUserData.ExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT) { + return _exitBPTInForExactTokensOut(balances, normalizedWeights, scalingFactors, totalSupply, userData); + } else { + _revert(Errors.UNHANDLED_EXIT_KIND); + } + } + + function _exitExactBPTInForTokenOut( + uint256[] memory balances, + uint256[] memory normalizedWeights, + uint256 totalSupply, + bytes memory userData + ) private view returns (uint256, uint256[] memory) { + (uint256 bptAmountIn, uint256 tokenIndex) = userData.exactBptInForTokenOut(); + // Note that there is no minimum amountOut parameter: this is handled by `IVault.exitPool`. + + _require(tokenIndex < balances.length, Errors.OUT_OF_BOUNDS); + + uint256 amountOut = WeightedMath._calcTokenOutGivenExactBptIn( + balances[tokenIndex], + normalizedWeights[tokenIndex], + bptAmountIn, + totalSupply, + getSwapFeePercentage() + ); + + // This is an exceptional situation in which the fee is charged on a token out instead of a token in. + // We exit in a single token, so we initialize amountsOut with zeros + uint256[] memory amountsOut = new uint256[](balances.length); + // And then assign the result to the selected token + amountsOut[tokenIndex] = amountOut; + + return (bptAmountIn, amountsOut); + } + + function _exitExactBPTInForTokensOut( + uint256[] memory balances, + uint256 totalSupply, + bytes memory userData + ) private pure returns (uint256, uint256[] memory) { + uint256 bptAmountIn = userData.exactBptInForTokensOut(); + // Note that there is no minimum amountOut parameter: this is handled by `IVault.exitPool`. + + uint256[] memory amountsOut = WeightedMath._calcTokensOutGivenExactBptIn(balances, bptAmountIn, totalSupply); + return (bptAmountIn, amountsOut); + } + + function _exitBPTInForExactTokensOut( + uint256[] memory balances, + uint256[] memory normalizedWeights, + uint256[] memory scalingFactors, + uint256 totalSupply, + bytes memory userData + ) private view returns (uint256, uint256[] memory) { + (uint256[] memory amountsOut, uint256 maxBPTAmountIn) = userData.bptInForExactTokensOut(); + InputHelpers.ensureInputLengthMatch(amountsOut.length, balances.length); + _upscaleArray(amountsOut, scalingFactors); + + // This is an exceptional situation in which the fee is charged on a token out instead of a token in. + uint256 bptAmountIn = WeightedMath._calcBptInGivenExactTokensOut( + balances, + normalizedWeights, + amountsOut, + totalSupply, + getSwapFeePercentage() + ); + _require(bptAmountIn <= maxBPTAmountIn, Errors.BPT_IN_MAX_AMOUNT); + + return (bptAmountIn, amountsOut); } // Swap overrides - revert unless swaps are enabled + function _onSwapGeneral( + SwapRequest memory, + uint256[] memory, + uint256, + uint256 + ) internal virtual override returns (uint256) { + _revert(Errors.UNHANDLED_BY_LBP); + } + + function _onSwapMinimal( + SwapRequest memory request, + uint256 balanceTokenIn, + uint256 balanceTokenOut + ) internal virtual override returns (uint256) { + uint256 scalingFactorTokenIn = _scalingFactor(request.tokenIn); + uint256 scalingFactorTokenOut = _scalingFactor(request.tokenOut); + + balanceTokenIn = _upscale(balanceTokenIn, scalingFactorTokenIn); + balanceTokenOut = _upscale(balanceTokenOut, scalingFactorTokenOut); + + if (request.kind == IVault.SwapKind.GIVEN_IN) { + // Fees are subtracted before scaling, to reduce the complexity of the rounding direction analysis. + request.amount = _subtractSwapFeeAmount(request.amount); + + // All token amounts are upscaled. + request.amount = _upscale(request.amount, scalingFactorTokenIn); + + uint256 amountOut = _onSwapGivenIn(request, balanceTokenIn, balanceTokenOut); + + // amountOut tokens are exiting the Pool, so we round down. + return _downscaleDown(amountOut, scalingFactorTokenOut); + } else { + // All token amounts are upscaled. + request.amount = _upscale(request.amount, scalingFactorTokenOut); + + uint256 amountIn = _onSwapGivenOut(request, balanceTokenIn, balanceTokenOut); + + // amountIn tokens are entering the Pool, so we round up. + amountIn = _downscaleUp(amountIn, scalingFactorTokenIn); + + // Fees are added after scaling happens, to reduce the complexity of the rounding direction analysis. + return _addSwapFeeAmount(amountIn); + } + } + + /** + * @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()); + } + + /** + * @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 authenticate whenNotPaused { + _setSwapFeePercentage(swapFeePercentage); + } + + function _setSwapFeePercentage(uint256 swapFeePercentage) internal virtual { + _require(swapFeePercentage >= _MIN_SWAP_FEE_PERCENTAGE, Errors.MIN_SWAP_FEE_PERCENTAGE); + _require(swapFeePercentage <= _MAX_SWAP_FEE_PERCENTAGE, Errors.MAX_SWAP_FEE_PERCENTAGE); + + _swapFeePercentage = swapFeePercentage; + + emit SwapFeePercentageChanged(swapFeePercentage); + } + + /** + * @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); + } + function _onSwapGivenIn( SwapRequest memory swapRequest, uint256 currentBalanceTokenIn, uint256 currentBalanceTokenOut - ) internal override returns (uint256) { + ) internal view returns (uint256) { _require(getSwapEnabled(), Errors.SWAPS_DISABLED); - return super._onSwapGivenIn(swapRequest, currentBalanceTokenIn, currentBalanceTokenOut); + return + WeightedMath._calcOutGivenIn( + currentBalanceTokenIn, + _getNormalizedWeight(swapRequest.tokenIn), + currentBalanceTokenOut, + _getNormalizedWeight(swapRequest.tokenOut), + swapRequest.amount + ); } function _onSwapGivenOut( SwapRequest memory swapRequest, uint256 currentBalanceTokenIn, uint256 currentBalanceTokenOut - ) internal override returns (uint256) { + ) internal view returns (uint256) { _require(getSwapEnabled(), Errors.SWAPS_DISABLED); - return super._onSwapGivenOut(swapRequest, currentBalanceTokenIn, currentBalanceTokenOut); - } - - /** - * @dev Extend ownerOnly functions to include the LBP control functions - */ - function _isOwnerOnlyAction(bytes32 actionId) internal view override returns (bool) { return - (actionId == getActionId(LiquidityBootstrappingPool.setSwapEnabled.selector)) || - (actionId == getActionId(LiquidityBootstrappingPool.updateWeightsGradually.selector)) || - super._isOwnerOnlyAction(actionId); + WeightedMath._calcInGivenOut( + currentBalanceTokenIn, + _getNormalizedWeight(swapRequest.tokenIn), + currentBalanceTokenOut, + _getNormalizedWeight(swapRequest.tokenOut), + swapRequest.amount + ); } - // Private functions - /** * @dev When calling updateWeightsGradually again during an update, reset the start weights to the current weights, * if necessary. @@ -354,15 +698,11 @@ contract LiquidityBootstrappingPool is BaseWeightedPool, ReentrancyGuard { emit SwapEnabledSet(swapEnabled); } - function _getMaxTokens() internal pure override returns (uint256) { - return _MAX_LBP_TOKENS; - } - - function _getTotalTokens() internal view virtual override returns (uint256) { + function _getTotalTokens() internal view returns (uint256) { return _totalTokens; } - function _scalingFactor(IERC20 token) internal view virtual override returns (uint256) { + function _scalingFactor(IERC20 token) internal view returns (uint256) { // prettier-ignore if (token == _token0) { return _scalingFactor0; } else if (token == _token1) { return _scalingFactor1; } @@ -373,7 +713,7 @@ contract LiquidityBootstrappingPool is BaseWeightedPool, ReentrancyGuard { } } - function _scalingFactors() internal view virtual override returns (uint256[] memory) { + function getScalingFactors() public view virtual override returns (uint256[] memory) { uint256 totalTokens = _getTotalTokens(); uint256[] memory scalingFactors = new uint256[](totalTokens); @@ -387,4 +727,43 @@ contract LiquidityBootstrappingPool is BaseWeightedPool, ReentrancyGuard { return scalingFactors; } + + // Recovery Mode + + /** + * @notice Returns whether the pool is in Recovery Mode. + */ + function inRecoveryMode() public view override returns (bool) { + return _poolState.decodeBool(_RECOVERY_MODE_BIT_OFFSET); + } + + /** + * @dev Sets the recoveryMode state, and emits the corresponding event. + */ + function _setRecoveryMode(bool enabled) internal virtual override { + _poolState = _poolState.insertBool(enabled, _RECOVERY_MODE_BIT_OFFSET); + + emit RecoveryModeStateChanged(enabled); + } + + function _doRecoveryModeExit( + uint256[] memory balances, + uint256 totalSupply, + bytes memory userData + ) internal pure override returns (uint256 bptAmountIn, uint256[] memory amountsOut) { + bptAmountIn = userData.recoveryModeExit(); + amountsOut = WeightedMath._calcTokensOutGivenExactBptIn(balances, bptAmountIn, totalSupply); + } + + // Misc + + /** + * @dev Extend ownerOnly functions to include the LBP control functions + */ + function _isOwnerOnlyAction(bytes32 actionId) internal view override returns (bool) { + return + (actionId == getActionId(this.setSwapFeePercentage.selector)) || + (actionId == getActionId(LiquidityBootstrappingPool.setSwapEnabled.selector)) || + (actionId == getActionId(LiquidityBootstrappingPool.updateWeightsGradually.selector)); + } } diff --git a/pkg/pool-weighted/test/BaseWeightedPool.behavior.ts b/pkg/pool-weighted/test/BaseWeightedPool.behavior.ts index fa53dd47d1..7eda7b39e6 100644 --- a/pkg/pool-weighted/test/BaseWeightedPool.behavior.ts +++ b/pkg/pool-weighted/test/BaseWeightedPool.behavior.ts @@ -48,7 +48,7 @@ export function itBehavesAsWeightedPool( vault = await Vault.create(); const tokenAmounts = fp(100); - allTokens = await TokenList.create(['MKR', 'DAI', 'SNX', 'BAT'], { sorted: true }); + allTokens = await TokenList.create(['MKR', 'DAI', 'SNX', 'BAT', 'GRT'], { sorted: true }); await allTokens.mint({ to: lp, amount: tokenAmounts }); await allTokens.approve({ to: vault.address, from: lp, amount: tokenAmounts }); }); @@ -430,7 +430,7 @@ export function itBehavesAsWeightedPool( // Calculate bpt amount in so that the invariant ratio // ((bptTotalSupply - bptAmountIn / bptTotalSupply)) // is more than 0.7 - const bptIn = (await pool.getMaxInvariantDecrease()).add(5); + const bptIn = (await pool.getMaxInvariantDecrease()).add(10); await expect(pool.singleExitGivenIn({ bptIn, token })).to.be.revertedWith('MIN_BPT_IN_FOR_TOKEN_OUT'); }); @@ -616,13 +616,13 @@ export function itBehavesAsWeightedPool( }); it('reverts if token in is not in the pool', async () => { - await expect(pool.swapGivenIn({ in: allTokens.BAT, out: 0, amount: 1, from: lp })).to.be.revertedWith( + await expect(pool.swapGivenIn({ in: allTokens.GRT, out: 0, amount: 1, from: lp })).to.be.revertedWith( 'TOKEN_NOT_REGISTERED' ); }); it('reverts if token out is not in the pool', async () => { - await expect(pool.swapGivenIn({ in: 1, out: allTokens.BAT, amount: 1, from: lp })).to.be.revertedWith( + await expect(pool.swapGivenIn({ in: 1, out: allTokens.GRT, amount: 1, from: lp })).to.be.revertedWith( 'TOKEN_NOT_REGISTERED' ); }); @@ -680,13 +680,13 @@ export function itBehavesAsWeightedPool( }); it('reverts if token in is not in the pool when given out', async () => { - await expect(pool.swapGivenOut({ in: allTokens.BAT, out: 0, amount: 1, from: lp })).to.be.revertedWith( + await expect(pool.swapGivenOut({ in: allTokens.GRT, out: 0, amount: 1, from: lp })).to.be.revertedWith( 'TOKEN_NOT_REGISTERED' ); }); it('reverts if token out is not in the pool', async () => { - await expect(pool.swapGivenOut({ in: 1, out: allTokens.BAT, amount: 1, from: lp })).to.be.revertedWith( + await expect(pool.swapGivenOut({ in: 1, out: allTokens.GRT, amount: 1, from: lp })).to.be.revertedWith( 'TOKEN_NOT_REGISTERED' ); }); diff --git a/pkg/pool-weighted/test/LiquidityBootstrappingPool.test.ts b/pkg/pool-weighted/test/LiquidityBootstrappingPool.test.ts index 40f0d8e283..845b6d0fd3 100644 --- a/pkg/pool-weighted/test/LiquidityBootstrappingPool.test.ts +++ b/pkg/pool-weighted/test/LiquidityBootstrappingPool.test.ts @@ -10,6 +10,7 @@ import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; import WeightedPool from '@balancer-labs/v2-helpers/src/models/pools/weighted/WeightedPool'; import { range } from 'lodash'; import { WeightedPoolType } from '../../../pvt/helpers/src/models/pools/weighted/types'; +import { itBehavesAsWeightedPool } from './BaseWeightedPool.behavior'; describe('LiquidityBootstrappingPool', function () { let owner: SignerWithAddress, other: SignerWithAddress; @@ -22,6 +23,20 @@ describe('LiquidityBootstrappingPool', function () { let allTokens: TokenList, tokens: TokenList; + // Add the weighted pool tests for the joins/exits, etc. + + context('for a 2 token pool', () => { + itBehavesAsWeightedPool(2); + }); + + context('for a 3 token pool', () => { + itBehavesAsWeightedPool(3); + }); + + context('for a 4 token pool', () => { + itBehavesAsWeightedPool(4); + }); + sharedBeforeEach('deploy tokens', async () => { allTokens = await TokenList.create(MAX_TOKENS + 1, { sorted: true }); tokens = allTokens.subset(4); From b6d8dcd1931dcbfd5e9e91e55d7e95be6e11c6fa Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Mon, 31 Oct 2022 00:04:41 -0400 Subject: [PATCH 2/4] refactor: visibility and function order --- .../lbp/LiquidityBootstrappingPool.sol | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/pkg/pool-weighted/contracts/lbp/LiquidityBootstrappingPool.sol b/pkg/pool-weighted/contracts/lbp/LiquidityBootstrappingPool.sol index 71624c58bc..def3e8e936 100644 --- a/pkg/pool-weighted/contracts/lbp/LiquidityBootstrappingPool.sol +++ b/pkg/pool-weighted/contracts/lbp/LiquidityBootstrappingPool.sol @@ -323,7 +323,7 @@ contract LiquidityBootstrappingPool is IMinimalSwapInfoPool, NewBasePool { address sender, uint256[] memory balances, bytes memory userData - ) internal virtual override returns (uint256, uint256[] memory) { + ) internal view override returns (uint256, uint256[] memory) { // Only the owner can add liquidity; block public LPs _require(sender == getOwner(), Errors.CALLER_IS_NOT_LBP_OWNER); @@ -351,7 +351,7 @@ contract LiquidityBootstrappingPool is IMinimalSwapInfoPool, NewBasePool { uint256[] memory scalingFactors, uint256 totalSupply, bytes memory userData - ) internal view virtual returns (uint256, uint256[] memory) { + ) internal view returns (uint256, uint256[] memory) { WeightedPoolUserData.JoinKind kind = userData.joinKind(); if (kind == WeightedPoolUserData.JoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT) { @@ -448,7 +448,7 @@ contract LiquidityBootstrappingPool is IMinimalSwapInfoPool, NewBasePool { address sender, uint256[] memory balances, bytes memory userData - ) internal virtual override returns (uint256, uint256[] memory) { + ) internal view override returns (uint256, uint256[] memory) { return _doExit(sender, balances, _getNormalizedWeights(), getScalingFactors(), totalSupply(), userData); } @@ -464,7 +464,7 @@ contract LiquidityBootstrappingPool is IMinimalSwapInfoPool, NewBasePool { uint256[] memory scalingFactors, uint256 totalSupply, bytes memory userData - ) internal view virtual returns (uint256, uint256[] memory) { + ) internal view returns (uint256, uint256[] memory) { WeightedPoolUserData.ExitKind kind = userData.exitKind(); if (kind == WeightedPoolUserData.ExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT) { @@ -597,24 +597,6 @@ contract LiquidityBootstrappingPool is IMinimalSwapInfoPool, NewBasePool { return amount.divUp(getSwapFeePercentage().complement()); } - /** - * @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 authenticate whenNotPaused { - _setSwapFeePercentage(swapFeePercentage); - } - - function _setSwapFeePercentage(uint256 swapFeePercentage) internal virtual { - _require(swapFeePercentage >= _MIN_SWAP_FEE_PERCENTAGE, Errors.MIN_SWAP_FEE_PERCENTAGE); - _require(swapFeePercentage <= _MAX_SWAP_FEE_PERCENTAGE, Errors.MAX_SWAP_FEE_PERCENTAGE); - - _swapFeePercentage = swapFeePercentage; - - emit SwapFeePercentageChanged(swapFeePercentage); - } - /** * @dev Subtracts swap fee amount from `amount`, returning a lower value. */ @@ -658,6 +640,28 @@ contract LiquidityBootstrappingPool is IMinimalSwapInfoPool, NewBasePool { ); } + // Swap Fees + + /** + * @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 authenticate whenNotPaused { + _setSwapFeePercentage(swapFeePercentage); + } + + function _setSwapFeePercentage(uint256 swapFeePercentage) internal virtual { + _require(swapFeePercentage >= _MIN_SWAP_FEE_PERCENTAGE, Errors.MIN_SWAP_FEE_PERCENTAGE); + _require(swapFeePercentage <= _MAX_SWAP_FEE_PERCENTAGE, Errors.MAX_SWAP_FEE_PERCENTAGE); + + _swapFeePercentage = swapFeePercentage; + + emit SwapFeePercentageChanged(swapFeePercentage); + } + + // Gradual weight change + /** * @dev When calling updateWeightsGradually again during an update, reset the start weights to the current weights, * if necessary. @@ -693,14 +697,16 @@ contract LiquidityBootstrappingPool is IMinimalSwapInfoPool, NewBasePool { emit GradualWeightUpdateScheduled(startTime, endTime, startWeights, endWeights); } + function _getTotalTokens() internal view returns (uint256) { + return _totalTokens; + } + function _setSwapEnabled(bool swapEnabled) private { _poolState = _poolState.insertBool(swapEnabled, _SWAP_ENABLED_OFFSET); emit SwapEnabledSet(swapEnabled); } - function _getTotalTokens() internal view returns (uint256) { - return _totalTokens; - } + // Scaling factors function _scalingFactor(IERC20 token) internal view returns (uint256) { // prettier-ignore From 713ce543f48d5120249139ce11e83af02b6623da Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Wed, 2 Nov 2022 10:36:53 -0400 Subject: [PATCH 3/4] fix: adjust offset and remove redundant event --- .../contracts/lbp/LiquidityBootstrappingPool.sol | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/pool-weighted/contracts/lbp/LiquidityBootstrappingPool.sol b/pkg/pool-weighted/contracts/lbp/LiquidityBootstrappingPool.sol index def3e8e936..e7f3a347b8 100644 --- a/pkg/pool-weighted/contracts/lbp/LiquidityBootstrappingPool.sol +++ b/pkg/pool-weighted/contracts/lbp/LiquidityBootstrappingPool.sol @@ -82,7 +82,7 @@ contract LiquidityBootstrappingPool is IMinimalSwapInfoPool, NewBasePool { // Offsets for data elements in _poolState uint256 private constant _SWAP_ENABLED_OFFSET = 0; uint256 private constant _RECOVERY_MODE_BIT_OFFSET = 1; - uint256 private constant _START_WEIGHT_OFFSET = _RECOVERY_MODE_BIT_OFFSET + 2; + uint256 private constant _START_WEIGHT_OFFSET = _RECOVERY_MODE_BIT_OFFSET + 3; uint256 private constant _END_WEIGHT_OFFSET = _START_WEIGHT_OFFSET + _MAX_LBP_TOKENS * _START_WEIGHT_BIT_LENGTH; uint256 private constant _START_TIME_OFFSET = _END_WEIGHT_OFFSET + _MAX_LBP_TOKENS * _END_WEIGHT_BIT_LENGTH; uint256 private constant _END_TIME_OFFSET = _START_TIME_OFFSET + _TIMESTAMP_BIT_LENGTH; @@ -744,12 +744,11 @@ contract LiquidityBootstrappingPool is IMinimalSwapInfoPool, NewBasePool { } /** - * @dev Sets the recoveryMode state, and emits the corresponding event. + * @dev Sets the recoveryMode state. The RecoveryModeStateChanged event is emitted in the RecoveryMode + * base contract, in `enableRecoveryMode` or `disabledRecoveryMode`, before calling this hook. */ function _setRecoveryMode(bool enabled) internal virtual override { _poolState = _poolState.insertBool(enabled, _RECOVERY_MODE_BIT_OFFSET); - - emit RecoveryModeStateChanged(enabled); } function _doRecoveryModeExit( From 4a2f7f43487c957984ba8c4a4c8a7ec36c3f1bea Mon Sep 17 00:00:00 2001 From: Jeffrey Bennett Date: Wed, 2 Nov 2022 11:28:35 -0400 Subject: [PATCH 4/4] refactor: use UNIMPLEMENTED error vs UNHANDLED_BY_LBP --- pkg/balancer-js/src/utils/errors.ts | 1 - .../contracts/solidity-utils/helpers/BalancerErrors.sol | 1 - pkg/pool-weighted/contracts/lbp/LiquidityBootstrappingPool.sol | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/balancer-js/src/utils/errors.ts b/pkg/balancer-js/src/utils/errors.ts index 7a4932e6fd..b90b8d3982 100644 --- a/pkg/balancer-js/src/utils/errors.ts +++ b/pkg/balancer-js/src/utils/errors.ts @@ -83,7 +83,6 @@ const balancerErrorCodes: Record = { '354': 'ADD_OR_REMOVE_BPT', '355': 'INVALID_CIRCUIT_BREAKER_BOUNDS', '356': 'CIRCUIT_BREAKER_TRIPPED', - '357': 'UNHANDLED_BY_LBP', '400': 'REENTRANCY', '401': 'SENDER_NOT_ALLOWED', '402': 'PAUSED', diff --git a/pkg/interfaces/contracts/solidity-utils/helpers/BalancerErrors.sol b/pkg/interfaces/contracts/solidity-utils/helpers/BalancerErrors.sol index d699e51be3..3471a90e41 100644 --- a/pkg/interfaces/contracts/solidity-utils/helpers/BalancerErrors.sol +++ b/pkg/interfaces/contracts/solidity-utils/helpers/BalancerErrors.sol @@ -196,7 +196,6 @@ library Errors { uint256 internal constant ADD_OR_REMOVE_BPT = 354; uint256 internal constant INVALID_CIRCUIT_BREAKER_BOUNDS = 355; uint256 internal constant CIRCUIT_BREAKER_TRIPPED = 356; - uint256 internal constant UNHANDLED_BY_LBP = 357; // Lib uint256 internal constant REENTRANCY = 400; diff --git a/pkg/pool-weighted/contracts/lbp/LiquidityBootstrappingPool.sol b/pkg/pool-weighted/contracts/lbp/LiquidityBootstrappingPool.sol index e7f3a347b8..31d6ab3108 100644 --- a/pkg/pool-weighted/contracts/lbp/LiquidityBootstrappingPool.sol +++ b/pkg/pool-weighted/contracts/lbp/LiquidityBootstrappingPool.sol @@ -550,7 +550,7 @@ contract LiquidityBootstrappingPool is IMinimalSwapInfoPool, NewBasePool { uint256, uint256 ) internal virtual override returns (uint256) { - _revert(Errors.UNHANDLED_BY_LBP); + _revert(Errors.UNIMPLEMENTED); } function _onSwapMinimal(