|
| 1 | +// SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | + |
| 3 | +pragma solidity >=0.8.24; |
| 4 | + |
| 5 | +import { ISwapFeePercentageBounds } from "@balancer-labs/v3-interfaces/contracts/vault/ISwapFeePercentageBounds.sol"; |
| 6 | +import { |
| 7 | + IUnbalancedLiquidityInvariantRatioBounds |
| 8 | +} from "@balancer-labs/v3-interfaces/contracts/vault/IUnbalancedLiquidityInvariantRatioBounds.sol"; |
| 9 | +import { IVaultErrors } from "@balancer-labs/v3-interfaces/contracts/vault/IVaultErrors.sol"; |
| 10 | +import { IBasePool } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePool.sol"; |
| 11 | +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; |
| 12 | +import { |
| 13 | + IWeightedPool, |
| 14 | + WeightedPoolDynamicData, |
| 15 | + WeightedPoolImmutableData |
| 16 | +} from "@balancer-labs/v3-interfaces/contracts/pool-weighted/IWeightedPool.sol"; |
| 17 | +import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; |
| 18 | + |
| 19 | +import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; |
| 20 | +import { WeightedMath } from "@balancer-labs/v3-solidity-utils/contracts/math/WeightedMath.sol"; |
| 21 | +import { BalancerPoolToken } from "@balancer-labs/v3-vault/contracts/BalancerPoolToken.sol"; |
| 22 | +import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; |
| 23 | +import { Version } from "@balancer-labs/v3-solidity-utils/contracts/helpers/Version.sol"; |
| 24 | +import { PoolInfo } from "@balancer-labs/v3-pool-utils/contracts/PoolInfo.sol"; |
| 25 | + |
| 26 | +import { IAdaptiveWeightedPool } from "./interfaces/IAdaptiveWeightedPool.sol"; |
| 27 | + |
| 28 | +contract AdaptiveWeightedPool is IAdaptiveWeightedPool, BalancerPoolToken, PoolInfo, Version { |
| 29 | + /// @dev Struct with data for deploying a new WeightedPool. `normalizedWeights` length must match `numTokens`. |
| 30 | + struct NewPoolParams { |
| 31 | + string name; |
| 32 | + string symbol; |
| 33 | + address wrappedBpt; |
| 34 | + uint256[] normalizedWeights; |
| 35 | + uint256[] initialVirtualBalances; |
| 36 | + string version; |
| 37 | + } |
| 38 | + |
| 39 | + /** |
| 40 | + * @notice `getRate` from `IRateProvider` was called on a Weighted Pool. |
| 41 | + * @dev It is not safe to nest Weighted Pools as WITH_RATE tokens in other pools, where they function as their own |
| 42 | + * rate provider. The default `getRate` implementation from `BalancerPoolToken` computes the BPT rate using the |
| 43 | + * invariant, which has a non-trivial (and non-linear) error. Without the ability to specify a rounding direction, |
| 44 | + * the rate could be manipulable. |
| 45 | + * |
| 46 | + * It is fine to nest Weighted Pools as STANDARD tokens, or to use them with external rate providers that are |
| 47 | + * stable and have at most 1 wei of rounding error (e.g., oracle-based). |
| 48 | + */ |
| 49 | + error WeightedPoolBptRateUnsupported(); |
| 50 | + |
| 51 | + // Fees are 18-decimal, floating point values, which will be stored in the Vault using 24 bits. |
| 52 | + // This means they have 0.00001% resolution (i.e., any non-zero bits < 1e11 will cause precision loss). |
| 53 | + // Minimum values help make the math well-behaved (i.e., the swap fee should overwhelm any rounding error). |
| 54 | + // Maximum values protect users by preventing permissioned actors from setting excessively high swap fees. |
| 55 | + uint256 private constant _MIN_SWAP_FEE_PERCENTAGE = 0.001e16; // 0.001% |
| 56 | + uint256 private constant _MAX_SWAP_FEE_PERCENTAGE = 10e16; // 10% |
| 57 | + uint256 private constant _MAX_TOKENS = 8; |
| 58 | + |
| 59 | + // A minimum normalized weight imposes a maximum weight ratio. We need this due to limitations in the |
| 60 | + // implementation of the fixed point power function, as these ratios are often exponents. |
| 61 | + uint256 internal constant _MIN_WEIGHT = 1e16; // 1% |
| 62 | + |
| 63 | + address internal immutable _wrappedBpt; |
| 64 | + |
| 65 | + uint256[] internal _weights; |
| 66 | + uint256[] internal _targetWeights; |
| 67 | + uint256 internal _startChangingTime; |
| 68 | + uint256 internal _endChangingTime; |
| 69 | + uint256[] internal _initialVirtualBalances; // TODO: optimize storage |
| 70 | + uint256[] internal _virtualBalances; |
| 71 | + |
| 72 | + constructor( |
| 73 | + NewPoolParams memory params, |
| 74 | + IVault vault |
| 75 | + ) BalancerPoolToken(vault, params.name, params.symbol) PoolInfo(vault) Version(params.version) { |
| 76 | + InputHelpers.ensureInputLengthMatch(params.normalizedWeights.length, params.initialVirtualBalances.length); |
| 77 | + |
| 78 | + if (params.wrappedBpt == address(0)) { |
| 79 | + revert InvalidWrappedBptLink(); |
| 80 | + } else if (params.normalizedWeights.length > _MAX_TOKENS) { |
| 81 | + revert IVaultErrors.MaxTokens(); |
| 82 | + } |
| 83 | + |
| 84 | + _wrappedBpt = params.wrappedBpt; |
| 85 | + |
| 86 | + // Ensure each normalized weight is above the minimum. |
| 87 | + uint256 normalizedSum = 0; |
| 88 | + for (uint8 i = 0; i < params.normalizedWeights.length; ++i) { |
| 89 | + uint256 normalizedWeight = params.normalizedWeights[i]; |
| 90 | + if (normalizedWeight < _MIN_WEIGHT) { |
| 91 | + revert MinWeight(); |
| 92 | + } |
| 93 | + |
| 94 | + normalizedSum += normalizedWeight; |
| 95 | + _weights.push(normalizedWeight); |
| 96 | + |
| 97 | + _targetWeights.push(0); // Initialize target weights to zero |
| 98 | + _virtualBalances.push(params.initialVirtualBalances[i]); |
| 99 | + _initialVirtualBalances.push(params.initialVirtualBalances[i]); |
| 100 | + } |
| 101 | + |
| 102 | + // Ensure that the normalized weights sum to ONE. |
| 103 | + if (normalizedSum != FixedPoint.ONE) { |
| 104 | + revert NormalizedWeightInvariant(); |
| 105 | + } |
| 106 | + } |
| 107 | + |
| 108 | + modifier onlyWrappedBpt() { |
| 109 | + if (msg.sender != _wrappedBpt) { |
| 110 | + revert SenderNotAllowed(); |
| 111 | + } |
| 112 | + _; |
| 113 | + } |
| 114 | + |
| 115 | + /// @inheritdoc IBasePool |
| 116 | + function computeInvariant(uint256[] memory balancesLiveScaled18, Rounding rounding) public view returns (uint256) { |
| 117 | + function(uint256[] memory, uint256[] memory) internal pure returns (uint256) _upOrDown = rounding == |
| 118 | + Rounding.ROUND_UP |
| 119 | + ? WeightedMath.computeInvariantUp |
| 120 | + : WeightedMath.computeInvariantDown; |
| 121 | + |
| 122 | + for (uint256 i = 0; i < balancesLiveScaled18.length; i++) { |
| 123 | + balancesLiveScaled18[i] += _virtualBalances[i]; |
| 124 | + } |
| 125 | + |
| 126 | + return _upOrDown(_getNormalizedWeights(), balancesLiveScaled18); |
| 127 | + } |
| 128 | + |
| 129 | + /// @inheritdoc IBasePool |
| 130 | + function computeBalance( |
| 131 | + uint256[] memory balancesLiveScaled18, |
| 132 | + uint256 tokenInIndex, |
| 133 | + uint256 invariantRatio |
| 134 | + ) external view returns (uint256 newBalance) { |
| 135 | + return |
| 136 | + WeightedMath.computeBalanceOutGivenInvariant( |
| 137 | + balancesLiveScaled18[tokenInIndex] + _virtualBalances[tokenInIndex], |
| 138 | + _getNormalizedWeight(tokenInIndex), |
| 139 | + invariantRatio |
| 140 | + ); |
| 141 | + } |
| 142 | + |
| 143 | + /// @inheritdoc IWeightedPool |
| 144 | + function getNormalizedWeights() external view returns (uint256[] memory) { |
| 145 | + return _getNormalizedWeights(); |
| 146 | + } |
| 147 | + |
| 148 | + /// @inheritdoc IBasePool |
| 149 | + function onSwap(PoolSwapParams memory request) public virtual returns (uint256) { |
| 150 | + uint256 initialVirtualBalanceTokenIn = _initialVirtualBalances[request.indexIn]; |
| 151 | + |
| 152 | + uint256 virtualBalanceTokenIn = _virtualBalances[request.indexIn]; |
| 153 | + uint256 virtualBalanceTokenOut = _virtualBalances[request.indexOut]; |
| 154 | + |
| 155 | + uint256 balanceTokenInScaled18 = request.balancesScaled18[request.indexIn] + virtualBalanceTokenIn; |
| 156 | + uint256 balanceTokenOutScaled18 = request.balancesScaled18[request.indexOut] + virtualBalanceTokenOut; |
| 157 | + |
| 158 | + uint256 amountInScaled18; |
| 159 | + uint256 amountOutScaled18; |
| 160 | + if (request.kind == SwapKind.EXACT_IN) { |
| 161 | + amountInScaled18 = request.amountGivenScaled18; |
| 162 | + amountOutScaled18 = WeightedMath.computeOutGivenExactIn( |
| 163 | + balanceTokenInScaled18, |
| 164 | + _getNormalizedWeight(request.indexIn), |
| 165 | + balanceTokenOutScaled18, |
| 166 | + _getNormalizedWeight(request.indexOut), |
| 167 | + amountInScaled18 |
| 168 | + ); |
| 169 | + } else { |
| 170 | + // Fees are added after scaling happens, to reduce the complexity of the rounding direction analysis. |
| 171 | + amountOutScaled18 = request.amountGivenScaled18; |
| 172 | + amountInScaled18 = WeightedMath.computeInGivenExactOut( |
| 173 | + balanceTokenInScaled18, |
| 174 | + _getNormalizedWeight(request.indexIn), |
| 175 | + balanceTokenOutScaled18, |
| 176 | + _getNormalizedWeight(request.indexOut), |
| 177 | + amountOutScaled18 |
| 178 | + ); |
| 179 | + } |
| 180 | + |
| 181 | + if (initialVirtualBalanceTokenIn > 0 && virtualBalanceTokenIn > 0) { |
| 182 | + if (amountInScaled18 <= virtualBalanceTokenIn) { |
| 183 | + virtualBalanceTokenIn -= amountInScaled18; |
| 184 | + } else { |
| 185 | + virtualBalanceTokenIn = 0; |
| 186 | + } |
| 187 | + |
| 188 | + _virtualBalances[request.indexIn] = virtualBalanceTokenIn; |
| 189 | + } |
| 190 | + |
| 191 | + return request.kind == SwapKind.EXACT_IN ? amountOutScaled18 : amountInScaled18; |
| 192 | + } |
| 193 | + |
| 194 | + function updateWeights( |
| 195 | + uint256[] memory newWeights, |
| 196 | + uint256 startChangingTime, |
| 197 | + uint256 endChangingTime |
| 198 | + ) external onlyWrappedBpt { |
| 199 | + InputHelpers.ensureInputLengthMatch(_weights.length, newWeights.length); |
| 200 | + |
| 201 | + uint256 normalizedSum = 0; |
| 202 | + for (uint8 i = 0; i < newWeights.length; ++i) { |
| 203 | + uint256 normalizedWeight = newWeights[i]; |
| 204 | + if (normalizedWeight < _MIN_WEIGHT) { |
| 205 | + revert MinWeight(); |
| 206 | + } |
| 207 | + |
| 208 | + _weights[i] = _computeWeight(i); |
| 209 | + _targetWeights[i] = normalizedWeight; |
| 210 | + |
| 211 | + normalizedSum += normalizedWeight; |
| 212 | + } |
| 213 | + |
| 214 | + // Ensure that the normalized weights sum to ONE. |
| 215 | + if (normalizedSum != FixedPoint.ONE) { |
| 216 | + revert NormalizedWeightInvariant(); |
| 217 | + } |
| 218 | + |
| 219 | + if (startChangingTime < block.timestamp || endChangingTime < startChangingTime) { |
| 220 | + revert InvalidTimeRange(); |
| 221 | + } |
| 222 | + |
| 223 | + _startChangingTime = startChangingTime; |
| 224 | + _endChangingTime = endChangingTime; |
| 225 | + } |
| 226 | + |
| 227 | + function bootstrapToken(uint256 tokenIndex, uint256 amountScaled18) external onlyWrappedBpt { |
| 228 | + _virtualBalances[tokenIndex] += amountScaled18; |
| 229 | + } |
| 230 | + |
| 231 | + function getVirtualBalances() external view returns (uint256[] memory virtualBalances) { |
| 232 | + return _virtualBalances; |
| 233 | + } |
| 234 | + |
| 235 | + function getChangingWeightsInfo() |
| 236 | + external |
| 237 | + view |
| 238 | + returns ( |
| 239 | + uint256 startChangingTime, |
| 240 | + uint256 endChangingTime, |
| 241 | + uint256[] memory initialWeights, |
| 242 | + uint256[] memory targetWeights |
| 243 | + ) |
| 244 | + { |
| 245 | + return (_startChangingTime, _endChangingTime, _weights, _targetWeights); |
| 246 | + } |
| 247 | + |
| 248 | + function _getNormalizedWeight(uint256 tokenIndex) internal view virtual returns (uint256) { |
| 249 | + if (tokenIndex >= _weights.length) { |
| 250 | + revert IVaultErrors.InvalidToken(); |
| 251 | + } |
| 252 | + |
| 253 | + return _computeWeight(tokenIndex); |
| 254 | + } |
| 255 | + |
| 256 | + function _getNormalizedWeights() internal view virtual returns (uint256[] memory) { |
| 257 | + uint256[] memory computedWeights = new uint256[](_weights.length); |
| 258 | + for (uint256 i = 0; i < _weights.length; i++) { |
| 259 | + computedWeights[i] = _computeWeight(i); |
| 260 | + } |
| 261 | + |
| 262 | + return computedWeights; |
| 263 | + } |
| 264 | + |
| 265 | + function _computeWeight(uint256 tokenIndex) private view returns (uint256) { |
| 266 | + if (block.timestamp < _startChangingTime) { |
| 267 | + return _weights[tokenIndex]; |
| 268 | + } else if (block.timestamp >= _endChangingTime) { |
| 269 | + return _targetWeights[tokenIndex]; |
| 270 | + } |
| 271 | + |
| 272 | + uint256 targetWeight = _targetWeights[tokenIndex]; |
| 273 | + uint256 currentWeight = _weights[tokenIndex]; |
| 274 | + |
| 275 | + if (currentWeight == targetWeight) { |
| 276 | + return currentWeight; |
| 277 | + } |
| 278 | + |
| 279 | + uint256 timeElapsed = block.timestamp - _startChangingTime; |
| 280 | + uint256 totalDuration = _endChangingTime - _startChangingTime; |
| 281 | + uint256 weightDifference = targetWeight - currentWeight; |
| 282 | + |
| 283 | + return currentWeight + (weightDifference * timeElapsed) / totalDuration; |
| 284 | + } |
| 285 | + |
| 286 | + /// @inheritdoc ISwapFeePercentageBounds |
| 287 | + function getMinimumSwapFeePercentage() external pure returns (uint256) { |
| 288 | + return _MIN_SWAP_FEE_PERCENTAGE; |
| 289 | + } |
| 290 | + |
| 291 | + /// @inheritdoc ISwapFeePercentageBounds |
| 292 | + function getMaximumSwapFeePercentage() external pure returns (uint256) { |
| 293 | + return _MAX_SWAP_FEE_PERCENTAGE; |
| 294 | + } |
| 295 | + |
| 296 | + /// @inheritdoc IUnbalancedLiquidityInvariantRatioBounds |
| 297 | + function getMinimumInvariantRatio() external pure returns (uint256) { |
| 298 | + return WeightedMath._MIN_INVARIANT_RATIO; |
| 299 | + } |
| 300 | + |
| 301 | + /// @inheritdoc IUnbalancedLiquidityInvariantRatioBounds |
| 302 | + function getMaximumInvariantRatio() external pure returns (uint256) { |
| 303 | + return WeightedMath._MAX_INVARIANT_RATIO; |
| 304 | + } |
| 305 | + |
| 306 | + /// @inheritdoc IWeightedPool |
| 307 | + function getWeightedPoolDynamicData() external view virtual returns (WeightedPoolDynamicData memory data) { |
| 308 | + data.balancesLiveScaled18 = _vault.getCurrentLiveBalances(address(this)); |
| 309 | + (, data.tokenRates) = _vault.getPoolTokenRates(address(this)); |
| 310 | + data.staticSwapFeePercentage = _vault.getStaticSwapFeePercentage((address(this))); |
| 311 | + data.totalSupply = totalSupply(); |
| 312 | + |
| 313 | + PoolConfig memory poolConfig = _vault.getPoolConfig(address(this)); |
| 314 | + data.isPoolInitialized = poolConfig.isPoolInitialized; |
| 315 | + data.isPoolPaused = poolConfig.isPoolPaused; |
| 316 | + data.isPoolInRecoveryMode = poolConfig.isPoolInRecoveryMode; |
| 317 | + } |
| 318 | + |
| 319 | + /// @inheritdoc IWeightedPool |
| 320 | + function getWeightedPoolImmutableData() external view virtual returns (WeightedPoolImmutableData memory data) { |
| 321 | + data.tokens = _vault.getPoolTokens(address(this)); |
| 322 | + (data.decimalScalingFactors, ) = _vault.getPoolTokenRates(address(this)); |
| 323 | + data.normalizedWeights = _getNormalizedWeights(); |
| 324 | + } |
| 325 | + |
| 326 | + /// @inheritdoc IRateProvider |
| 327 | + function getRate() public pure override returns (uint256) { |
| 328 | + revert WeightedPoolBptRateUnsupported(); |
| 329 | + } |
| 330 | +} |
0 commit comments