From 39baa2e5f687bf689231a0a7d7db611436c5cdfc Mon Sep 17 00:00:00 2001 From: aalavandhann <6264334+aalavandhan@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:33:33 -0400 Subject: [PATCH] refactored charm manager code, moved logic to helpers and using the new pricer for all vaults --- spot-vaults/contracts/UsdcSpotManager.sol | 231 ---------- spot-vaults/contracts/WethWamplManager.sol | 431 ------------------ .../contracts/_interfaces/IMetaOracle.sol | 46 ++ .../contracts/_interfaces/IPerpPricer.sol | 27 ++ .../_interfaces/ISpotPricingStrategy.sol | 22 - .../_interfaces/errors/BillBrokerErrors.sol | 8 + .../CommonErrors.sol} | 11 +- .../_interfaces/errors/SwingTraderErrors.sol | 8 + .../_interfaces/external/IAlphaProVault.sol | 3 +- .../_interfaces/external/IBondController.sol | 5 + .../contracts/_interfaces/external/IERC20.sol | 14 + .../_interfaces/external/IPerpFeePolicy.sol | 7 + .../external/IPerpetualTranche.sol | 13 + .../_interfaces/external/ITranche.sol | 6 + .../contracts/_interfaces/external/IWAMPL.sol | 2 - .../{ => types}/BillBrokerTypes.sol | 20 +- .../_interfaces/types/CommonTypes.sol | 22 + .../_interfaces/types/CommonTypes_7x.sol | 16 + .../contracts/_utils/AlphaVaultHelpers.sol | 90 ++++ spot-vaults/contracts/_utils/LineHelpers.sol | 84 ++++ .../contracts/_utils/LineHelpers_7x.sol | 85 ++++ .../contracts/_utils/UniswapV3PoolHelpers.sol | 48 ++ .../contracts/charm/UsdcSpotManager.sol | 151 ++++++ .../contracts/charm/WethWamplManager.sol | 238 ++++++++++ 24 files changed, 871 insertions(+), 717 deletions(-) delete mode 100644 spot-vaults/contracts/UsdcSpotManager.sol delete mode 100644 spot-vaults/contracts/WethWamplManager.sol create mode 100644 spot-vaults/contracts/_interfaces/IMetaOracle.sol create mode 100644 spot-vaults/contracts/_interfaces/IPerpPricer.sol delete mode 100644 spot-vaults/contracts/_interfaces/ISpotPricingStrategy.sol create mode 100644 spot-vaults/contracts/_interfaces/errors/BillBrokerErrors.sol rename spot-vaults/contracts/_interfaces/{BillBrokerErrors.sol => errors/CommonErrors.sol} (63%) create mode 100644 spot-vaults/contracts/_interfaces/errors/SwingTraderErrors.sol create mode 100644 spot-vaults/contracts/_interfaces/external/IBondController.sol create mode 100644 spot-vaults/contracts/_interfaces/external/IERC20.sol create mode 100644 spot-vaults/contracts/_interfaces/external/IPerpFeePolicy.sol create mode 100644 spot-vaults/contracts/_interfaces/external/IPerpetualTranche.sol create mode 100644 spot-vaults/contracts/_interfaces/external/ITranche.sol rename spot-vaults/contracts/_interfaces/{ => types}/BillBrokerTypes.sol (68%) create mode 100644 spot-vaults/contracts/_interfaces/types/CommonTypes.sol create mode 100644 spot-vaults/contracts/_interfaces/types/CommonTypes_7x.sol create mode 100644 spot-vaults/contracts/_utils/AlphaVaultHelpers.sol create mode 100644 spot-vaults/contracts/_utils/LineHelpers.sol create mode 100644 spot-vaults/contracts/_utils/LineHelpers_7x.sol create mode 100644 spot-vaults/contracts/_utils/UniswapV3PoolHelpers.sol create mode 100644 spot-vaults/contracts/charm/UsdcSpotManager.sol create mode 100644 spot-vaults/contracts/charm/WethWamplManager.sol diff --git a/spot-vaults/contracts/UsdcSpotManager.sol b/spot-vaults/contracts/UsdcSpotManager.sol deleted file mode 100644 index 05d985b3..00000000 --- a/spot-vaults/contracts/UsdcSpotManager.sol +++ /dev/null @@ -1,231 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -// solhint-disable-next-line compiler-version -pragma solidity ^0.7.6; -pragma abicoder v2; - -import { FullMath } from "@uniswap/v3-core/contracts/libraries/FullMath.sol"; -import { TickMath } from "@uniswap/v3-core/contracts/libraries/TickMath.sol"; -import { PositionKey } from "@uniswap/v3-periphery/contracts/libraries/PositionKey.sol"; - -import { ISpotPricingStrategy } from "./_interfaces/ISpotPricingStrategy.sol"; -import { IAlphaProVault } from "./_interfaces/external/IAlphaProVault.sol"; -import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; - -/// @title UsdcSpotManager -/// @notice This contract is a programmatic manager for the USDC-SPOT Charm AlphaProVault. -contract UsdcSpotManager { - /// @dev Token Constants. - uint256 public constant ONE_SPOT = 1e9; - uint256 public constant ONE_USDC = 1e6; - - /// @dev Decimals. - uint256 public constant DECIMALS = 18; - uint256 public constant ONE = (10 ** DECIMALS); - - /// @dev We bound the deviation factor to 100.0. - uint256 public constant MAX_DEVIATION = 100 * ONE; // 100.0 - - //------------------------------------------------------------------------- - // Storage - - /// @notice The USDC-SPOT charm alpha vault. - IAlphaProVault public immutable VAULT; - - /// @notice The underlying USDC-SPOT univ3 pool. - IUniswapV3Pool public immutable POOL; - - /// @notice The vault's token0, the USDC token. - address public immutable USDC; - - /// @notice The vault's token1, the SPOT token. - address public immutable SPOT; - - /// @notice Pricing strategy to price the SPOT token. - ISpotPricingStrategy public pricingStrategy; - - /// @notice The contract owner. - address public owner; - - //------------------------------------------------------------------------- - // Manager storage - - /// @notice The recorded deviation factor at the time of the last successful rebalance operation. - uint256 public prevDeviation; - - //-------------------------------------------------------------------------- - // Modifiers - - modifier onlyOwner() { - // solhint-disable-next-line custom-errors - require(msg.sender == owner, "Unauthorized caller"); - _; - } - - //----------------------------------------------------------------------------- - // Constructor and Initializer - - /// @notice Constructor initializes the contract with provided addresses. - /// @param vault_ Address of the AlphaProVault contract. - /// @param pricingStrategy_ Address of the spot appraiser. - constructor(IAlphaProVault vault_, ISpotPricingStrategy pricingStrategy_) { - owner = msg.sender; - - VAULT = vault_; - POOL = vault_.pool(); - USDC = vault_.token0(); - SPOT = vault_.token1(); - - pricingStrategy = pricingStrategy_; - // solhint-disable-next-line custom-errors - require(pricingStrategy.decimals() == DECIMALS, "Invalid decimals"); - } - - //-------------------------------------------------------------------------- - // Owner only methods - - /// @notice Updates the owner role. - function transferOwnership(address owner_) external onlyOwner { - owner = owner_; - } - - /// @notice Updates the Spot pricing strategy reference. - function updatePricingStrategy( - ISpotPricingStrategy pricingStrategy_ - ) external onlyOwner { - pricingStrategy = pricingStrategy_; - } - - /// @notice Updates the vault's liquidity range parameters. - function setLiquidityRanges( - int24 baseThreshold, - uint24 fullRangeWeight, - int24 limitThreshold - ) external onlyOwner { - // Update liquidity parameters on the vault. - VAULT.setBaseThreshold(baseThreshold); - VAULT.setFullRangeWeight(fullRangeWeight); - VAULT.setLimitThreshold(limitThreshold); - } - - /// @notice Forwards the given calldata to the vault. - /// @param callData The calldata to pass to the vault. - /// @return The data returned by the vault method call. - function execOnVault( - bytes calldata callData - ) external onlyOwner returns (bytes memory) { - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory r) = address(VAULT).call(callData); - // solhint-disable-next-line custom-errors - require(success, "Vault call failed"); - return r; - } - - //-------------------------------------------------------------------------- - // External write methods - - /// @notice Executes vault rebalance. - function rebalance() public { - (uint256 deviation, bool deviationValid) = computeDeviationFactor(); - - // We rebalance if the deviation factor has crossed ONE (in either direction). - bool forceLiquidityUpdate = ((deviation <= ONE && prevDeviation > ONE) || - (deviation >= ONE && prevDeviation < ONE)); - - // Execute rebalance. - // NOTE: the vault.rebalance() will revert if enough time has not elapsed. - // We thus override with a force rebalance. - // https://learn.charm.fi/charm/technical-references/core/alphaprovault#rebalance - forceLiquidityUpdate ? _execForceRebalance() : VAULT.rebalance(); - - // We only activate the limit range liquidity, when - // the vault sells SPOT and deviation is above ONE, or when - // the vault buys SPOT and deviation is below ONE - bool extraSpot = isOverweightSpot(); - bool activeLimitRange = deviationValid && - ((deviation >= ONE && extraSpot) || (deviation <= ONE && !extraSpot)); - - // Trim positions after rebalance. - if (!activeLimitRange) { - _removeLimitLiquidity(); - } - - // Update rebalance state. - prevDeviation = deviation; - } - - /// @notice Computes the deviation between SPOT's market price and it's FMV price. - /// @return The computed deviation factor. - function computeDeviationFactor() public returns (uint256, bool) { - uint256 spotMarketPrice = getSpotUSDPrice(); - (uint256 spotTargetPrice, bool spotTargetPriceValid) = pricingStrategy - .perpPrice(); - (, bool usdcPriceValid) = pricingStrategy.usdPrice(); - bool deviationValid = (spotTargetPriceValid && usdcPriceValid); - uint256 deviation = spotTargetPrice > 0 - ? FullMath.mulDiv(spotMarketPrice, ONE, spotTargetPrice) - : type(uint256).max; - deviation = (deviation > MAX_DEVIATION) ? MAX_DEVIATION : deviation; - return (deviation, deviationValid); - } - - //----------------------------------------------------------------------------- - // External Public view methods - - /// @return The computed SPOT price in USD from the underlying univ3 pool. - function getSpotUSDPrice() public view returns (uint256) { - uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(VAULT.getTwap()); - uint256 ratioX192 = uint256(sqrtPriceX96) * sqrtPriceX96; - uint256 usdcPerSpot = FullMath.mulDiv(ONE, (1 << 192), ratioX192); - return FullMath.mulDiv(usdcPerSpot, ONE_SPOT, ONE_USDC); - } - - /// @notice Checks the vault is overweight SPOT, and looking to sell the extra SPOT for USDC. - function isOverweightSpot() public view returns (bool) { - // NOTE: This assumes that in the underlying univ3 pool and - // token0 is USDC and token1 is SPOT. - int24 _marketPrice = VAULT.getTwap(); - int24 _limitLower = VAULT.limitLower(); - int24 _limitUpper = VAULT.limitUpper(); - int24 _limitPrice = (_limitLower + _limitUpper) / 2; - // The limit range has more token1 than token0 if `_marketPrice >= _limitPrice`, - // so the vault looks to sell token1. - return (_marketPrice >= _limitPrice); - } - - /// @return Number of decimals representing 1.0. - function decimals() external pure returns (uint8) { - return uint8(DECIMALS); - } - - //----------------------------------------------------------------------------- - // Private methods - - /// @dev A low-level method, which interacts directly with the vault and executes - /// a rebalance even when enough time hasn't elapsed since the last rebalance. - function _execForceRebalance() private { - uint32 _period = VAULT.period(); - VAULT.setPeriod(0); - VAULT.rebalance(); - VAULT.setPeriod(_period); - } - - /// @dev Removes the vault's limit range liquidity. - /// To be invoked right after a rebalance operation, as it assumes that - /// the vault has a active limit range liquidity. - function _removeLimitLiquidity() private { - int24 _limitLower = VAULT.limitLower(); - int24 _limitUpper = VAULT.limitUpper(); - (uint128 limitLiquidity, , , , ) = _position(_limitLower, _limitUpper); - // docs: https://learn.charm.fi/charm/technical-references/core/alphaprovault#emergencyburn - VAULT.emergencyBurn(_limitLower, _limitUpper, limitLiquidity); - } - - /// @dev Wrapper around `IUniswapV3Pool.positions()`. - function _position( - int24 tickLower, - int24 tickUpper - ) private view returns (uint128, uint256, uint256, uint128, uint128) { - bytes32 positionKey = PositionKey.compute(address(VAULT), tickLower, tickUpper); - return POOL.positions(positionKey); - } -} diff --git a/spot-vaults/contracts/WethWamplManager.sol b/spot-vaults/contracts/WethWamplManager.sol deleted file mode 100644 index b51cc16c..00000000 --- a/spot-vaults/contracts/WethWamplManager.sol +++ /dev/null @@ -1,431 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -// solhint-disable-next-line compiler-version -pragma solidity ^0.7.6; -pragma abicoder v2; - -import { FullMath } from "@uniswap/v3-core/contracts/libraries/FullMath.sol"; -import { TickMath } from "@uniswap/v3-core/contracts/libraries/TickMath.sol"; -import { PositionKey } from "@uniswap/v3-periphery/contracts/libraries/PositionKey.sol"; -import { SafeCast } from "@uniswap/v3-core/contracts/libraries/SafeCast.sol"; - -import { IAlphaProVault } from "./_interfaces/external/IAlphaProVault.sol"; -import { IChainlinkOracle } from "./_interfaces/external/IChainlinkOracle.sol"; -import { IAmpleforthOracle } from "./_interfaces/external/IAmpleforthOracle.sol"; -import { IWAMPL } from "./_interfaces/external/IWAMPL.sol"; -import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; - -/// @title WethWamplManager -/// @notice This contract is a programmatic manager for the WETH-WAMPL Charm AlphaProVault. -contract WethWamplManager { - /// @dev Constants for AMPL and WAMPL units and supply limits. - uint256 public constant ONE_AMPL = 1e9; - uint256 public constant ONE_WAMPL = 1e18; - - /// @dev Decimals. - uint256 public constant DECIMALS = 18; - uint256 public constant ONE = (10 ** DECIMALS); - - /// @dev At all times active liquidity percentage is no lower than 20%. - uint256 public constant MIN_ACTIVE_LIQ_PERC = ONE / 5; // 20% - - /// @dev We bound the deviation factor to 100.0. - uint256 public constant MAX_DEVIATION = 100 * ONE; // 100.0 - - /// @dev Oracle constants. - uint256 public constant CL_ORACLE_STALENESS_THRESHOLD_SEC = 3600 * 24; // 1 day - - //------------------------------------------------------------------------- - // Storage - - /// @notice The WETH-WAMPL charm alpha vault. - IAlphaProVault public immutable VAULT; - - /// @notice The underlying WETH-WAMPL univ3 pool. - IUniswapV3Pool public immutable POOL; - - /// @notice The vault's token0, the WETH token. - address public immutable WETH; - - /// @notice The vault's token1, the WAMPL token. - IWAMPL public immutable WAMPL; - - /// @notice The cpi oracle which returns AMPL's price target in USD. - IAmpleforthOracle public cpiOracle; - - /// @notice The chainlink oracle which returns ETH's current USD price. - IChainlinkOracle public ethOracle; - - /// @notice The contract owner. - address public owner; - - //------------------------------------------------------------------------- - // Active percentage calculation parameters - // - // The deviation factor (or deviation) is defined as the ratio between - // AMPL's current market price and its target price. - // The deviation is 1.0, when AMPL is at the target. - // - // The active liquidity percentage (a value between 20% to 100%) - // is computed based on pair-wise linear function, defined by the contract owner. - // - // If the current deviation is below ONE, function f1 is used - // else function f2 is used. Both f1 and f2 are defined by the owner. - // They are lines, with 2 {x,y} coordinates. The x coordinates are deviation factors, - // and y coordinates are active liquidity percentages. - // - // Both deviation and active liquidity percentage and represented internally - // as a fixed-point number with {DECIMALS} places. - // - - /// @notice A data structure to define a geometric Line with two points. - struct Line { - // x-coordinate of the first point. - uint256 x1; - // y-coordinate of the first point. - uint256 y1; - // x-coordinate of the second point. - uint256 x2; - // y-coordinate of the second point. - uint256 y2; - } - - /// @notice Active percentage calculation function for when deviation is below ONE. - Line public activeLiqPercFn1; - - /// @notice Active percentage calculation function for when deviation is above ONE. - Line public activeLiqPercFn2; - - //------------------------------------------------------------------------- - // Manager parameters - - /// @notice The delta between the current and last recorded active liquidity percentage values - /// outside which a rebalance is executed forcefully. - uint256 public tolerableActiveLiqPercDelta; - - //------------------------------------------------------------------------- - // Manager storage - - /// @notice The recorded deviation factor at the time of the last successful rebalance operation. - uint256 public prevDeviation; - - //-------------------------------------------------------------------------- - // Modifiers - - modifier onlyOwner() { - // solhint-disable-next-line custom-errors - require(msg.sender == owner, "Unauthorized caller"); - _; - } - - //----------------------------------------------------------------------------- - // Constructor and Initializer - - /// @notice Constructor initializes the contract with provided addresses. - /// @param vault_ Address of the AlphaProVault contract. - /// @param cpiOracle_ Address of the Ampleforth CPI oracle contract. - /// @param ethOracle_ Address of the Chainlink ETH price oracle contract. - constructor( - IAlphaProVault vault_, - IAmpleforthOracle cpiOracle_, - IChainlinkOracle ethOracle_ - ) { - owner = msg.sender; - - VAULT = vault_; - POOL = vault_.pool(); - WETH = vault_.token0(); - WAMPL = IWAMPL(vault_.token1()); - - cpiOracle = cpiOracle_; - ethOracle = ethOracle_; - - activeLiqPercFn1 = Line({ - x1: ONE / 2, // 0.5 - y1: ONE / 5, // 20% - x2: ONE, // 1.0 - y2: ONE // 100% - }); - activeLiqPercFn2 = Line({ - x1: ONE, // 1.0 - y1: ONE, // 100% - x2: ONE * 2, // 2.0 - y2: ONE / 5 // 20% - }); - - tolerableActiveLiqPercDelta = ONE / 10; // 10% - prevDeviation = 0; - } - - //-------------------------------------------------------------------------- - // Owner only methods - - /// @notice Updates the owner role. - function transferOwnership(address owner_) external onlyOwner { - owner = owner_; - } - - /// @notice Updates the ampleforth cpi oracle. - function setCpiOracle(IAmpleforthOracle cpiOracle_) external onlyOwner { - cpiOracle = cpiOracle_; - } - - /// @notice Updates the chainlink eth usd price oracle. - function setEthOracle(IChainlinkOracle ethOracle_) external onlyOwner { - ethOracle = ethOracle_; - } - - /// @notice Updates the active liquidity percentage calculation parameters. - function setActivePercParams( - uint256 tolerableActiveLiqPercDelta_, - Line memory activeLiqPercFn1_, - Line memory activeLiqPercFn2_ - ) external onlyOwner { - tolerableActiveLiqPercDelta = tolerableActiveLiqPercDelta_; - activeLiqPercFn1 = activeLiqPercFn1_; - activeLiqPercFn2 = activeLiqPercFn2_; - } - - /// @notice Updates the vault's liquidity range parameters. - function setLiquidityRanges( - int24 baseThreshold, - uint24 fullRangeWeight, - int24 limitThreshold - ) external onlyOwner { - // Update liquidity parameters on the vault. - VAULT.setBaseThreshold(baseThreshold); - VAULT.setFullRangeWeight(fullRangeWeight); - VAULT.setLimitThreshold(limitThreshold); - } - - /// @notice Forwards the given calldata to the vault. - /// @param callData The calldata to pass to the vault. - /// @return The data returned by the vault method call. - function execOnVault( - bytes calldata callData - ) external onlyOwner returns (bytes memory) { - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory r) = address(VAULT).call(callData); - // solhint-disable-next-line custom-errors - require(success, "Vault call failed"); - return r; - } - - //-------------------------------------------------------------------------- - // External write methods - - /// @notice Executes vault rebalance. - function rebalance() public { - // Get the current deviation factor. - (uint256 deviation, bool deviationValid) = computeDeviationFactor(); - - // Calculate the current active liquidity percentage. - uint256 activeLiqPerc = deviationValid - ? computeActiveLiqPerc(deviation) - : MIN_ACTIVE_LIQ_PERC; - - // We have to rebalance out of turn - // - if the active liquidity perc has deviated significantly, or - // - if the deviation factor has crossed ONE (in either direction). - uint256 prevActiveLiqPerc = computeActiveLiqPerc(prevDeviation); - uint256 activeLiqPercDelta = (activeLiqPerc > prevActiveLiqPerc) - ? activeLiqPerc - prevActiveLiqPerc - : prevActiveLiqPerc - activeLiqPerc; - bool forceLiquidityUpdate = (activeLiqPercDelta > tolerableActiveLiqPercDelta) || - ((deviation <= ONE && prevDeviation > ONE) || - (deviation >= ONE && prevDeviation < ONE)); - - // Execute rebalance. - // NOTE: the vault.rebalance() will revert if enough time has not elapsed. - // We thus override with a force rebalance. - // https://learn.charm.fi/charm/technical-references/core/alphaprovault#rebalance - forceLiquidityUpdate ? _execForceRebalance() : VAULT.rebalance(); - - // We only activate the limit range liquidity, when - // the vault sells WAMPL and deviation is above ONE, or when - // the vault buys WAMPL and deviation is below ONE - bool extraWampl = isOverweightWampl(); - bool activeLimitRange = deviationValid && - ((deviation >= ONE && extraWampl) || (deviation <= ONE && !extraWampl)); - - // Trim positions after rebalance. - _trimLiquidity(activeLiqPerc, activeLimitRange); - - // Update rebalance state. - prevDeviation = deviation; - } - - /// @notice Computes the deviation between AMPL's market price and target. - /// @return The computed deviation factor. - function computeDeviationFactor() public returns (uint256, bool) { - (uint256 ethUSDPrice, bool ethPriceValid) = getEthUSDPrice(); - uint256 marketPrice = getAmplUSDPrice(ethUSDPrice); - (uint256 targetPrice, bool targetPriceValid) = _getAmpleforthOracleData( - cpiOracle - ); - bool deviationValid = (ethPriceValid && targetPriceValid); - uint256 deviation = (targetPrice > 0) - ? FullMath.mulDiv(marketPrice, ONE, targetPrice) - : type(uint256).max; - deviation = (deviation > MAX_DEVIATION) ? MAX_DEVIATION : deviation; - return (deviation, deviationValid); - } - - //----------------------------------------------------------------------------- - // External Public view methods - - /// @notice Computes active liquidity percentage based on the provided deviation factor. - /// @return The computed active liquidity percentage. - function computeActiveLiqPerc(uint256 deviation) public view returns (uint256) { - return - (deviation <= ONE) - ? _computeActiveLiqPerc(activeLiqPercFn1, deviation) - : _computeActiveLiqPerc(activeLiqPercFn2, deviation); - } - - /// @notice Computes the AMPL price in USD. - /// @param ethUSDPrice The ETH price in USD. - /// @return The computed AMPL price in USD. - function getAmplUSDPrice(uint256 ethUSDPrice) public view returns (uint256) { - return - FullMath.mulDiv( - getWamplUSDPrice(ethUSDPrice), - ONE_AMPL, - WAMPL.wrapperToUnderlying(ONE_WAMPL) // #AMPL per WAMPL - ); - } - - /// @notice Computes the WAMPL price in USD based on ETH price. - /// @param ethUSDPrice The ETH price in USD. - /// @return The computed WAMPL price in USD. - function getWamplUSDPrice(uint256 ethUSDPrice) public view returns (uint256) { - // We first get the WETH-WAMPL price from the pool and then convert that - // to a USD price using the given ETH-USD price. - uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(VAULT.getTwap()); - uint256 ratioX192 = uint256(sqrtPriceX96) * sqrtPriceX96; - // NOTE: Since both weth and wampl have 18 decimals, - // we don't adjust the `wamplPerWeth`. - uint256 wamplPerWeth = FullMath.mulDiv(ONE, ratioX192, (1 << 192)); - return FullMath.mulDiv(ethUSDPrice, ONE, wamplPerWeth); - } - - /// @notice Fetches the current ETH price in USD from the Chainlink oracle. - /// @return The ETH price in USD and its validity. - function getEthUSDPrice() public view returns (uint256, bool) { - return _getCLOracleData(ethOracle); - } - - /// @notice Checks the vault is overweight WAMPL, and looking to sell the extra WAMPL for WETH. - function isOverweightWampl() public view returns (bool) { - // NOTE: This assumes that in the underlying univ3 pool and - // token0 is WETH and token1 is WAMPL. - int24 _marketPrice = VAULT.getTwap(); - int24 _limitLower = VAULT.limitLower(); - int24 _limitUpper = VAULT.limitUpper(); - int24 _limitPrice = (_limitLower + _limitUpper) / 2; - // The limit range has more token1 than token0 if `_marketPrice >= _limitPrice`, - // so the vault looks to sell token1. - return (_marketPrice >= _limitPrice); - } - - /// @return Number of decimals representing 1.0. - function decimals() external pure returns (uint8) { - return uint8(DECIMALS); - } - - //----------------------------------------------------------------------------- - // Private methods - - /// @dev Trims the vault's current liquidity. - /// To be invoked right after a rebalance operation, as it assumes that all of the vault's - /// liquidity has been deployed before trimming. - function _trimLiquidity(uint256 activePerc, bool activeLimitRange) private { - // Calculated baseLiquidityToBurn, baseLiquidityToBurn will be lesser than fullLiquidity, baseLiquidity - // Thus, there's no risk of overflow. - if (activePerc < ONE) { - int24 _fullLower = VAULT.fullLower(); - int24 _fullUpper = VAULT.fullUpper(); - int24 _baseLower = VAULT.baseLower(); - int24 _baseUpper = VAULT.baseUpper(); - (uint128 fullLiquidity, , , , ) = _position(_fullLower, _fullUpper); - (uint128 baseLiquidity, , , , ) = _position(_baseLower, _baseUpper); - uint128 fullLiquidityToBurn = uint128( - FullMath.mulDiv(uint256(fullLiquidity), ONE - activePerc, ONE) - ); - uint128 baseLiquidityToBurn = uint128( - FullMath.mulDiv(uint256(baseLiquidity), ONE - activePerc, ONE) - ); - // docs: https://learn.charm.fi/charm/technical-references/core/alphaprovault#emergencyburn - // We remove the calculated percentage of base and full range liquidity. - VAULT.emergencyBurn(_fullLower, _fullUpper, fullLiquidityToBurn); - VAULT.emergencyBurn(_baseLower, _baseUpper, baseLiquidityToBurn); - } - - // When the limit range is not active, we remove entirely. - if (!activeLimitRange) { - int24 _limitLower = VAULT.limitLower(); - int24 _limitUpper = VAULT.limitUpper(); - (uint128 limitLiquidity, , , , ) = _position(_limitLower, _limitUpper); - // docs: https://learn.charm.fi/charm/technical-references/core/alphaprovault#emergencyburn - VAULT.emergencyBurn(_limitLower, _limitUpper, limitLiquidity); - } - } - - /// @dev Fetches most recent report from the given ampleforth oracle contract. - /// The returned report is a fixed point number with {DECIMALS} places. - function _getAmpleforthOracleData( - IAmpleforthOracle oracle - ) private returns (uint256, bool) { - (uint256 p, bool valid) = oracle.getData(); - return (FullMath.mulDiv(p, ONE, 10 ** oracle.DECIMALS()), valid); - } - - /// @dev A low-level method, which interacts directly with the vault and executes - /// a rebalance even when enough time hasn't elapsed since the last rebalance. - function _execForceRebalance() private { - uint32 _period = VAULT.period(); - VAULT.setPeriod(0); - VAULT.rebalance(); - VAULT.setPeriod(_period); - } - - /// @dev Wrapper around `IUniswapV3Pool.positions()`. - function _position( - int24 tickLower, - int24 tickUpper - ) private view returns (uint128, uint256, uint256, uint128, uint128) { - bytes32 positionKey = PositionKey.compute(address(VAULT), tickLower, tickUpper); - return POOL.positions(positionKey); - } - - /// @dev Fetches most recent report from the given chain link oracle contract. - /// The data is considered invalid if the latest report is stale. - /// The returned report is a fixed point number with {DECIMALS} places. - function _getCLOracleData( - IChainlinkOracle oracle - ) private view returns (uint256, bool) { - (, int256 p, , uint256 updatedAt, ) = oracle.latestRoundData(); - uint256 price = FullMath.mulDiv(uint256(p), ONE, 10 ** oracle.decimals()); - return ( - price, - (block.timestamp - updatedAt) <= CL_ORACLE_STALENESS_THRESHOLD_SEC - ); - } - - /// @dev We compute activeLiqPerc value given a linear fn and deviation. - function _computeActiveLiqPerc( - Line memory fn, - uint256 deviation - ) private pure returns (uint256) { - deviation = (deviation > MAX_DEVIATION) ? MAX_DEVIATION : deviation; - int256 dlY = SafeCast.toInt256(fn.y2) - SafeCast.toInt256(fn.y1); - int256 dlX = SafeCast.toInt256(fn.x2) - SafeCast.toInt256(fn.x1); - int256 activeLiqPerc = SafeCast.toInt256(fn.y2) + - (((SafeCast.toInt256(deviation) - SafeCast.toInt256(fn.x2)) * dlY) / dlX); - activeLiqPerc = (activeLiqPerc < int256(MIN_ACTIVE_LIQ_PERC)) - ? int256(MIN_ACTIVE_LIQ_PERC) - : activeLiqPerc; - activeLiqPerc = (activeLiqPerc > int256(ONE)) ? int256(ONE) : activeLiqPerc; - // Casting from int256 to uint256 here is safe as activeLiqPerc >= 0. - return uint256(activeLiqPerc); - } -} diff --git a/spot-vaults/contracts/_interfaces/IMetaOracle.sol b/spot-vaults/contracts/_interfaces/IMetaOracle.sol new file mode 100644 index 00000000..6bc22f88 --- /dev/null +++ b/spot-vaults/contracts/_interfaces/IMetaOracle.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: BUSL-1.1 + +/// @notice Oracle adapter for AMPL and its family of assets. +// solhint-disable-next-line compiler-version +interface IMetaOracle { + /// @return Number of decimals representing the prices returned. + function decimals() external pure returns (uint8); + + /// @return price The price of USDC tokens in dollars. + /// @return isValid True if the returned price is valid. + function usdcPrice() external returns (uint256 price, bool isValid); + + /// @notice Computes the deviation between SPOT's market price and FMV price. + /// @return deviation The computed deviation factor. + /// @return isValid True if the returned deviation is valid. + function spotPriceDeviation() external returns (uint256 deviation, bool isValid); + + /// @notice Computes the deviation between AMPL's market price and price target. + /// @return deviation The computed deviation factor. + /// @return isValid True if the returned deviation is valid. + function amplPriceDeviation() external returns (uint256 deviation, bool isValid); + + /// @return price The price of SPOT in dollars. + /// @return isValid True if the returned price is valid. + function spotUsdPrice() external returns (uint256 price, bool isValid); + + /// @return price The price of AMPL in dollars. + /// @return isValid True if the returned price is valid. + function amplUsdPrice() external returns (uint256 price, bool isValid); + + /// @return price The SPOT FMV price in dollars. + /// @return isValid True if the returned price is valid. + function spotFmvUsdPrice() external returns (uint256 price, bool isValid); + + /// @return price The AMPL target price in dollars. + /// @return isValid True if the returned price is valid. + function amplTargetUsdPrice() external returns (uint256 price, bool isValid); + + /// @return price The WAMPL price in dollars. + /// @return isValid True if the returned price is valid. + function wamplUsdPrice() external returns (uint256 price, bool isValid); + + /// @return price The ETH price in dollars. + /// @return isValid True if the returned price is valid. + function ethUsdPrice() external returns (uint256 price, bool isValid); +} diff --git a/spot-vaults/contracts/_interfaces/IPerpPricer.sol b/spot-vaults/contracts/_interfaces/IPerpPricer.sol new file mode 100644 index 00000000..f05af15c --- /dev/null +++ b/spot-vaults/contracts/_interfaces/IPerpPricer.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BUSL-1.1 + +/// @notice Pricing strategy contract to price perps and its underlying token. +// solhint-disable-next-line compiler-version +interface IPerpPricer { + /// @return Number of decimals representing the prices returned. + function decimals() external pure returns (uint8); + + /// @return price The price of reference USD tokens. + /// @return isValid True if the returned price is valid. + function usdPrice() external returns (uint256 price, bool isValid); + + /// @return price The price of perp tokens in dollars. + /// @return isValid True if the returned price is valid. + function perpUsdPrice() external returns (uint256 price, bool isValid); + + /// @return price The price of underlying tokens (which back perp) in dollars. + /// @return isValid True if the returned price is valid. + function underlyingUsdPrice() external returns (uint256 price, bool isValid); + + /// @return price Perp's fmv price in dollars. + /// @return isValid True if the returned price is valid. + function perpFmvUsdPrice() external returns (uint256 price, bool isValid); + + /// @return Perp's computed discount factor. + function perpDiscountFactor() external returns (uint256); +} diff --git a/spot-vaults/contracts/_interfaces/ISpotPricingStrategy.sol b/spot-vaults/contracts/_interfaces/ISpotPricingStrategy.sol deleted file mode 100644 index 2f794c76..00000000 --- a/spot-vaults/contracts/_interfaces/ISpotPricingStrategy.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -/** - * @title ISpotPricingStrategy - * - * @notice Pricing strategy adapter for a BillBroker vault - * which accepts Perp and USDC tokens. - * - */ -// solhint-disable-next-line compiler-version -interface ISpotPricingStrategy { - /// @return Number of decimals representing the prices returned. - function decimals() external pure returns (uint8); - - /// @return price The price of USD tokens. - /// @return isValid True if the returned price is valid. - function usdPrice() external returns (uint256 price, bool isValid); - - /// @return price The price of perp tokens. - /// @return isValid True if the returned price is valid. - function perpPrice() external returns (uint256 price, bool isValid); -} diff --git a/spot-vaults/contracts/_interfaces/errors/BillBrokerErrors.sol b/spot-vaults/contracts/_interfaces/errors/BillBrokerErrors.sol new file mode 100644 index 00000000..e1dc341f --- /dev/null +++ b/spot-vaults/contracts/_interfaces/errors/BillBrokerErrors.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +/// @notice Expect AR lower bound to be under the upper bound. +error InvalidARBound(); + +/// @notice Expected pre and post swap AR delta to be non-increasing or non-decreasing. +error UnexpectedARDelta(); diff --git a/spot-vaults/contracts/_interfaces/BillBrokerErrors.sol b/spot-vaults/contracts/_interfaces/errors/CommonErrors.sol similarity index 63% rename from spot-vaults/contracts/_interfaces/BillBrokerErrors.sol rename to spot-vaults/contracts/_interfaces/errors/CommonErrors.sol index babbae79..c8d6b263 100644 --- a/spot-vaults/contracts/_interfaces/BillBrokerErrors.sol +++ b/spot-vaults/contracts/_interfaces/errors/CommonErrors.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: BUSL-1.1 +/// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.24; /// @notice Expected contract call to be triggered by authorized caller. @@ -10,15 +10,6 @@ error UnexpectedDecimals(); /// @notice Expected perc value to be at most (1 * 10**DECIMALS), i.e) 1.0 or 100%. error InvalidPerc(); -/// @notice Expected Senior CDR bound to be more than 1.0 or 100%. -error InvalidSeniorCDRBound(); - -/// @notice Expect AR lower bound to be under the upper bound. -error InvalidARBound(); - -/// @notice Expected pre and post swap AR delta to be non-increasing or non-decreasing. -error UnexpectedARDelta(); - /// @notice Slippage higher than tolerance requested by user. error SlippageTooHigh(); diff --git a/spot-vaults/contracts/_interfaces/errors/SwingTraderErrors.sol b/spot-vaults/contracts/_interfaces/errors/SwingTraderErrors.sol new file mode 100644 index 00000000..f5435af9 --- /dev/null +++ b/spot-vaults/contracts/_interfaces/errors/SwingTraderErrors.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +/// @notice Exceeded max active redemption requests per account. +error TooManyRedemptionRequests(); + +/// @notice Exceeded enforced swap limit. +error SwapLimitExceeded(); diff --git a/spot-vaults/contracts/_interfaces/external/IAlphaProVault.sol b/spot-vaults/contracts/_interfaces/external/IAlphaProVault.sol index d599cdfd..39b4d5fe 100644 --- a/spot-vaults/contracts/_interfaces/external/IAlphaProVault.sol +++ b/spot-vaults/contracts/_interfaces/external/IAlphaProVault.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable-next-line compiler-version -pragma solidity ^0.7.6; +pragma solidity ^0.8.24; import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; diff --git a/spot-vaults/contracts/_interfaces/external/IBondController.sol b/spot-vaults/contracts/_interfaces/external/IBondController.sol new file mode 100644 index 00000000..5488c920 --- /dev/null +++ b/spot-vaults/contracts/_interfaces/external/IBondController.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable-next-line compiler-version +interface IBondController { + function collateralBalance() external view returns (uint256); +} diff --git a/spot-vaults/contracts/_interfaces/external/IERC20.sol b/spot-vaults/contracts/_interfaces/external/IERC20.sol new file mode 100644 index 00000000..caab7e44 --- /dev/null +++ b/spot-vaults/contracts/_interfaces/external/IERC20.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable-next-line compiler-version +interface IERC20 { + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 value) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 value) external returns (bool); + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); +} diff --git a/spot-vaults/contracts/_interfaces/external/IPerpFeePolicy.sol b/spot-vaults/contracts/_interfaces/external/IPerpFeePolicy.sol new file mode 100644 index 00000000..b4aba5c9 --- /dev/null +++ b/spot-vaults/contracts/_interfaces/external/IPerpFeePolicy.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable-next-line compiler-version +interface IPerpFeePolicy { + function decimals() external returns (uint8); + function deviationRatio() external returns (uint256); + function computePerpRolloverFeePerc(uint256 dr) external returns (int256); +} diff --git a/spot-vaults/contracts/_interfaces/external/IPerpetualTranche.sol b/spot-vaults/contracts/_interfaces/external/IPerpetualTranche.sol new file mode 100644 index 00000000..a730f331 --- /dev/null +++ b/spot-vaults/contracts/_interfaces/external/IPerpetualTranche.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable-next-line compiler-version +interface IPerpetualTranche { + function underlying() external view returns (address); + function getTVL() external returns (uint256); + function totalSupply() external returns (uint256); + function getReserveCount() external returns (uint256); + function getReserveAt(uint256 index) external returns (address); + function deviationRatio() external returns (uint256); + function getReserveTokenValue(address t) external returns (uint256); + function getReserveTokenBalance(address t) external returns (uint256); + function feePolicy() external returns (address); +} diff --git a/spot-vaults/contracts/_interfaces/external/ITranche.sol b/spot-vaults/contracts/_interfaces/external/ITranche.sol new file mode 100644 index 00000000..51a582d3 --- /dev/null +++ b/spot-vaults/contracts/_interfaces/external/ITranche.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable-next-line compiler-version +interface ITranche { + function bond() external view returns (address); + function totalSupply() external view returns (uint256); +} diff --git a/spot-vaults/contracts/_interfaces/external/IWAMPL.sol b/spot-vaults/contracts/_interfaces/external/IWAMPL.sol index 0176dd22..b5f24a1a 100644 --- a/spot-vaults/contracts/_interfaces/external/IWAMPL.sol +++ b/spot-vaults/contracts/_interfaces/external/IWAMPL.sol @@ -1,7 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable-next-line compiler-version -pragma solidity ^0.7.6; - interface IWAMPL { function wrapperToUnderlying(uint256 wamples) external view returns (uint256); } diff --git a/spot-vaults/contracts/_interfaces/BillBrokerTypes.sol b/spot-vaults/contracts/_interfaces/types/BillBrokerTypes.sol similarity index 68% rename from spot-vaults/contracts/_interfaces/BillBrokerTypes.sol rename to spot-vaults/contracts/_interfaces/types/BillBrokerTypes.sol index d24601ad..a99b3c27 100644 --- a/spot-vaults/contracts/_interfaces/BillBrokerTypes.sol +++ b/spot-vaults/contracts/_interfaces/types/BillBrokerTypes.sol @@ -1,25 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.24; -/// @notice A data structure to define a geometric Line with two points. -struct Line { - /// @notice x-coordinate of the first point. - uint256 x1; - /// @notice y-coordinate of the first point. - uint256 y1; - /// @notice x-coordinate of the second point. - uint256 x2; - /// @notice y-coordinate of the second point. - uint256 y2; -} - -/// @notice A data structure to define a numeric Range. -struct Range { - /// @notice Lower bound of the range. - uint256 lower; - /// @notice Upper bound of the range. - uint256 upper; -} +import { Range } from "./CommonTypes.sol"; /// @notice A data structure to store various fees associated with BillBroker operations. struct BillBrokerFees { diff --git a/spot-vaults/contracts/_interfaces/types/CommonTypes.sol b/spot-vaults/contracts/_interfaces/types/CommonTypes.sol new file mode 100644 index 00000000..00ea2009 --- /dev/null +++ b/spot-vaults/contracts/_interfaces/types/CommonTypes.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +/// @notice A data structure to define a geometric Line with two points. +struct Line { + // @dev x-coordinate of the first point. + uint256 x1; + // @dev y-coordinate of the first point. + uint256 y1; + // @dev x-coordinate of the second point. + uint256 x2; + // @dev y-coordinate of the second point. + uint256 y2; +} + +/// @notice A data structure to define a numeric Range. +struct Range { + // @dev Lower bound of the range. + uint256 lower; + // @dev Upper bound of the range. + uint256 upper; +} diff --git a/spot-vaults/contracts/_interfaces/types/CommonTypes_7x.sol b/spot-vaults/contracts/_interfaces/types/CommonTypes_7x.sol new file mode 100644 index 00000000..dff860f5 --- /dev/null +++ b/spot-vaults/contracts/_interfaces/types/CommonTypes_7x.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable-next-line compiler-version +pragma solidity ^0.7.6; +pragma abicoder v2; + +/// @notice A data structure to define a geometric Line with two points. +struct Line { + // @dev x-coordinate of the first point. + uint256 x1; + // @dev y-coordinate of the first point. + uint256 y1; + // @dev x-coordinate of the second point. + uint256 x2; + // @dev y-coordinate of the second point. + uint256 y2; +} diff --git a/spot-vaults/contracts/_utils/AlphaVaultHelpers.sol b/spot-vaults/contracts/_utils/AlphaVaultHelpers.sol new file mode 100644 index 00000000..d738bcf6 --- /dev/null +++ b/spot-vaults/contracts/_utils/AlphaVaultHelpers.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { IAlphaProVault } from "../_interfaces/external/IAlphaProVault.sol"; +import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; + +/** + * @title AlphaVaultHelpers + * + * @notice Library with helper functions for Charm's Alpha Vaults. + * + */ +library AlphaVaultHelpers { + /// @dev Checks if the vault is underweight token0 (ie overweight token1). + function isUnderweightToken0(IAlphaProVault vault) internal view returns (bool) { + // `vault.getTwap()` returns the twap tick from the underlying univ3 pool. + // https://learn.charm.fi/charm/technical-references/core/alphaprovault#gettwap + int24 _priceTick = vault.getTwap(); + int24 _limitLower = vault.limitLower(); + int24 _limitUpper = vault.limitUpper(); + int24 _limitPriceTick = (_limitLower + _limitUpper) / 2; + // The limit range has more token1 than token0 if `_priceTick >= _limitPriceTick`, + // so the vault looks to sell token1. + return (_priceTick >= _limitPriceTick); + } + + /// @dev Removes the vault's limit range liquidity completely. + function removeLimitLiquidity(IAlphaProVault vault, IUniswapV3Pool pool) internal { + int24 _limitLower = vault.limitLower(); + int24 _limitUpper = vault.limitUpper(); + uint128 limitLiquidity = getLiquidity(vault, pool, _limitLower, _limitUpper); + // docs: https://learn.charm.fi/charm/technical-references/core/alphaprovault#emergencyburn + vault.emergencyBurn(_limitLower, _limitUpper, limitLiquidity); + } + + /// @dev Removes a percentage of the base and full range liquidity. + function trimLiquidity( + IAlphaProVault vault, + IUniswapV3Pool pool, + uint256 percToRemove, + uint256 one + ) internal { + if (percToRemove <= 0) { + return; + } + + int24 _fullLower = vault.fullLower(); + int24 _fullUpper = vault.fullUpper(); + int24 _baseLower = vault.baseLower(); + int24 _baseUpper = vault.baseUpper(); + uint128 fullLiquidity = getLiquidity(vault, pool, _fullLower, _fullUpper); + uint128 baseLiquidity = getLiquidity(vault, pool, _baseLower, _baseUpper); + // Calculated baseLiquidityToBurn, baseLiquidityToBurn will be lesser than fullLiquidity, baseLiquidity + // Thus, there's no risk of overflow. + uint128 fullLiquidityToBurn = uint128( + Math.mulDiv(uint256(fullLiquidity), percToRemove, one) + ); + uint128 baseLiquidityToBurn = uint128( + Math.mulDiv(uint256(baseLiquidity), percToRemove, one) + ); + // docs: https://learn.charm.fi/charm/technical-references/core/alphaprovault#emergencyburn + // We remove the calculated percentage of base and full range liquidity. + vault.emergencyBurn(_fullLower, _fullUpper, fullLiquidityToBurn); + vault.emergencyBurn(_baseLower, _baseUpper, baseLiquidityToBurn); + } + + /// @dev A low-level method, which interacts directly with the vault and executes + /// a rebalance even when enough time hasn't elapsed since the last rebalance. + function forceRebalance(IAlphaProVault vault) internal { + uint32 _period = vault.period(); + vault.setPeriod(0); + vault.rebalance(); + vault.setPeriod(_period); + } + + /// @dev Wrapper around `IUniswapV3Pool.positions()`. + function getLiquidity( + IAlphaProVault vault, + IUniswapV3Pool pool, + int24 tickLower, + int24 tickUpper + ) internal view returns (uint128) { + bytes32 positionKey = keccak256( + abi.encodePacked(address(vault), tickLower, tickUpper) + ); + (uint128 liquidity, , , , ) = pool.positions(positionKey); + return liquidity; + } +} diff --git a/spot-vaults/contracts/_utils/LineHelpers.sol b/spot-vaults/contracts/_utils/LineHelpers.sol new file mode 100644 index 00000000..33de080b --- /dev/null +++ b/spot-vaults/contracts/_utils/LineHelpers.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { Line } from "../_interfaces/types/CommonTypes.sol"; + +/** + * @title LineHelpers + * + * @notice Library with helper functions for the Line data structure. + * + */ +library LineHelpers { + /// @dev We compute the average height of the line between {xL,xU}. + /// Clips the final y value between [yMin, yMax]. + function avgY( + Line memory fn, + uint256 xL, + uint256 xU, + uint256 yMin, + uint256 yMax + ) internal pure returns (uint256) { + // if the line has a zero slope, return any y + if (fn.y1 == fn.y2) { + return _clip(fn.y1, yMin, yMax); + } + + uint256 yL = computeY(fn, xL, 0, type(uint256).max); + uint256 yU = computeY(fn, xU, 0, type(uint256).max); + uint256 avgY_ = (yL + yU) / 2; + return _clip(avgY_, yMin, yMax); + } + + /// @dev This function computes y for a given x on the line (fn), bounded by yMin and yMax. + function computeY( + Line memory fn, + uint256 x, + uint256 yMin, + uint256 yMax + ) internal pure returns (uint256) { + // m = (y2-y1)/(x2-x1) + // y = y1 + m * (x-x1) + + // If the line has a zero slope, return a y value clipped between yMin and yMax + if (fn.y1 == fn.y2) { + return _clip(fn.y1, yMin, yMax); + } + + // Determine if m is positive + bool posM = (fn.y2 > fn.y1 && fn.x2 > fn.x1) || (fn.y2 < fn.y1 && fn.x2 < fn.x1); + + // Determine if (x - x1) is positive + bool posDelX1 = (x > fn.x1); + + // Calculate absolute differences to ensure no underflow + uint256 dlY = fn.y2 > fn.y1 ? (fn.y2 - fn.y1) : (fn.y1 - fn.y2); + uint256 dlX = fn.x2 > fn.x1 ? (fn.x2 - fn.x1) : (fn.x1 - fn.x2); + uint256 delX1 = posDelX1 ? (x - fn.x1) : (fn.x1 - x); + + // Calculate m * (x-x1) + uint256 mDelX1 = Math.mulDiv(delX1, dlY, dlX); + + uint256 y = 0; + + // When m * (x-x1) is positive + if ((posM && posDelX1) || (!posM && !posDelX1)) { + y = fn.y1 + mDelX1; + } + // When m * (x-x1) is negative + else { + y = (fn.y1 > mDelX1) ? (fn.y1 - mDelX1) : yMin; // Ensures no underflow + } + + // Return the y value clipped between yMin and yMax + return _clip(y, yMin, yMax); + } + + // @dev Helper function to clip y between min and max values + function _clip(uint256 y, uint256 min, uint256 max) private pure returns (uint256) { + y = (y <= min) ? min : y; + y = (y >= max) ? max : y; + return y; + } +} diff --git a/spot-vaults/contracts/_utils/LineHelpers_7x.sol b/spot-vaults/contracts/_utils/LineHelpers_7x.sol new file mode 100644 index 00000000..e6f4062a --- /dev/null +++ b/spot-vaults/contracts/_utils/LineHelpers_7x.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable-next-line compiler-version +pragma solidity ^0.7.6; + +import { FullMath } from "@uniswap/v3-core/contracts/libraries/FullMath.sol"; +import { Line } from "../_interfaces/types/CommonTypes_7x.sol"; + +/** + * @title LineHelpers + * + * @notice Library with helper functions for the Line data structure. + * + */ +library LineHelpers { + /// @dev We compute the average height of the line between {xL,xU}. + /// Clips the final y value between [yMin, yMax]. + function avgY( + Line memory fn, + uint256 xL, + uint256 xU, + uint256 yMin, + uint256 yMax + ) internal pure returns (uint256) { + // if the line has a zero slope, return any y + if (fn.y1 == fn.y2) { + return _clip(fn.y1, yMin, yMax); + } + + uint256 yL = computeY(fn, xL, 0, type(uint256).max); + uint256 yU = computeY(fn, xU, 0, type(uint256).max); + uint256 avgY_ = (yL + yU) / 2; + return _clip(avgY_, yMin, yMax); + } + + /// @dev This function computes y for a given x on the line (fn), bounded by yMin and yMax. + function computeY( + Line memory fn, + uint256 x, + uint256 yMin, + uint256 yMax + ) internal pure returns (uint256) { + // m = (y2-y1)/(x2-x1) + // y = y1 + m * (x-x1) + + // If the line has a zero slope, return a y value clipped between yMin and yMax + if (fn.y1 == fn.y2) { + return _clip(fn.y1, yMin, yMax); + } + + // Determine if m is positive + bool posM = (fn.y2 > fn.y1 && fn.x2 > fn.x1) || (fn.y2 < fn.y1 && fn.x2 < fn.x1); + + // Determine if (x - x1) is positive + bool posDelX1 = (x > fn.x1); + + // Calculate absolute differences to ensure no underflow + uint256 dlY = fn.y2 > fn.y1 ? (fn.y2 - fn.y1) : (fn.y1 - fn.y2); + uint256 dlX = fn.x2 > fn.x1 ? (fn.x2 - fn.x1) : (fn.x1 - fn.x2); + uint256 delX1 = posDelX1 ? (x - fn.x1) : (fn.x1 - x); + + // Calculate m * (x-x1) + uint256 mDelX1 = FullMath.mulDiv(delX1, dlY, dlX); + + uint256 y = 0; + + // When m * (x-x1) is positive + if ((posM && posDelX1) || (!posM && !posDelX1)) { + y = fn.y1 + mDelX1; + } + // When m * (x-x1) is negative + else { + y = (fn.y1 > mDelX1) ? (fn.y1 - mDelX1) : yMin; // Ensures no underflow + } + + // Return the y value clipped between yMin and yMax + return _clip(y, yMin, yMax); + } + + // @dev Helper function to clip y between min and max values + function _clip(uint256 y, uint256 min, uint256 max) private pure returns (uint256) { + y = (y <= min) ? min : y; + y = (y >= max) ? max : y; + return y; + } +} diff --git a/spot-vaults/contracts/_utils/UniswapV3PoolHelpers.sol b/spot-vaults/contracts/_utils/UniswapV3PoolHelpers.sol new file mode 100644 index 00000000..8c44d3cd --- /dev/null +++ b/spot-vaults/contracts/_utils/UniswapV3PoolHelpers.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable-next-line compiler-version +pragma solidity ^0.7.6; + +import { FullMath } from "@uniswap/v3-core/contracts/libraries/FullMath.sol"; +import { TickMath } from "@uniswap/v3-core/contracts/libraries/TickMath.sol"; +import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; + +/** + * @title UniswapV3PoolHelpers + * + * @notice Library with helper functions for a UniswapV3Pool. + * + */ +library UniswapV3PoolHelpers { + /// @notice Calculates the Time-Weighted Average Price (TWAP) given the TWAP tick and unit token amounts. + /// @param twapTick The Time-Weighted Average Price tick. + /// @param token0UnitAmt The fixed-point amount of token0 equivalent to 1.0. + /// @param token1UnitAmt The fixed-point amount of token1 equivalent to 1.0. + /// @param one 1.0 represented in the same fixed point denomination as calculated TWAP. + /// @return The computed TWAP price. + function calculateTwap( + int24 twapTick, + uint256 token0UnitAmt, + uint256 token1UnitAmt, + uint256 one + ) internal pure returns (uint256) { + uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(twapTick); + uint256 ratioX192 = uint256(sqrtPriceX96) * sqrtPriceX96; + uint256 twapPrice = FullMath.mulDiv(one, (1 << 192), ratioX192); + return FullMath.mulDiv(twapPrice, token1UnitAmt, token0UnitAmt); + } + + /// @notice Retrieves the Time-Weighted Average Price (TWAP) tick from a Uniswap V3 pool over a given duration. + /// @param pool The Uniswap V3 pool. + /// @param twapDuration The TWAP duration. + /// @return The TWAP tick. + function getTwapTick( + IUniswapV3Pool pool, + uint32 twapDuration + ) internal view returns (int24) { + uint32[] memory secondsAgo = new uint32[](2); + secondsAgo[0] = twapDuration; + secondsAgo[1] = 0; + (int56[] memory tickCumulatives, ) = pool.observe(secondsAgo); + return int24((tickCumulatives[1] - tickCumulatives[0]) / twapDuration); + } +} diff --git a/spot-vaults/contracts/charm/UsdcSpotManager.sol b/spot-vaults/contracts/charm/UsdcSpotManager.sol new file mode 100644 index 00000000..51f64968 --- /dev/null +++ b/spot-vaults/contracts/charm/UsdcSpotManager.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { AlphaVaultHelpers } from "../_utils/AlphaVaultHelpers.sol"; + +import { IMetaOracle } from "../_interfaces/IMetaOracle.sol"; +import { IAlphaProVault } from "../_interfaces/external/IAlphaProVault.sol"; +import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; + +/// @title UsdcSpotManager +/// @notice This contract is a programmatic manager for the USDC-SPOT Charm AlphaProVault. +contract UsdcSpotManager is Ownable { + //------------------------------------------------------------------------- + // Libraries + using AlphaVaultHelpers for IAlphaProVault; + + //------------------------------------------------------------------------- + // Constants & Immutables + + /// @dev Decimals. + uint256 public constant DECIMALS = 18; + uint256 public constant ONE = (10 ** DECIMALS); + + /// @notice The USDC-SPOT charm alpha vault. + IAlphaProVault public immutable VAULT; + + /// @notice The underlying USDC-SPOT univ3 pool. + IUniswapV3Pool public immutable POOL; + + //------------------------------------------------------------------------- + // Storage + + /// @notice The meta oracle which returns prices of AMPL asset family. + IMetaOracle public oracle; + + /// @notice The recorded deviation factor at the time of the last successful rebalance operation. + uint256 public prevDeviation; + + //----------------------------------------------------------------------------- + // Constructor and Initializer + + /// @notice Constructor initializes the contract with provided addresses. + /// @param vault_ Address of the AlphaProVault contract. + /// @param oracle_ Address of the MetaOracle contract. + constructor(IAlphaProVault vault_, IMetaOracle oracle_) Ownable() { + VAULT = vault_; + POOL = vault_.pool(); + + updateOracle(oracle_); + + prevDeviation = 0; + } + + //-------------------------------------------------------------------------- + // Owner only methods + + /// @notice Updates the MetaOracle. + function updateOracle(IMetaOracle oracle_) public onlyOwner { + // solhint-disable-next-line custom-errors + require(DECIMALS == oracle_.decimals(), "UnexpectedDecimals"); + oracle = oracle_; + } + + /// @notice Updates the vault's liquidity range parameters. + function setLiquidityRanges( + int24 baseThreshold, + uint24 fullRangeWeight, + int24 limitThreshold + ) external onlyOwner { + // Update liquidity parameters on the vault. + VAULT.setBaseThreshold(baseThreshold); + VAULT.setFullRangeWeight(fullRangeWeight); + VAULT.setLimitThreshold(limitThreshold); + } + + /// @notice Forwards the given calldata to the vault. + /// @param callData The calldata to pass to the vault. + /// @return The data returned by the vault method call. + function execOnVault( + bytes calldata callData + ) external onlyOwner returns (bytes memory) { + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory r) = address(VAULT).call(callData); + // solhint-disable-next-line custom-errors + require(success, "VaultExecutionFailed"); + return r; + } + + //-------------------------------------------------------------------------- + // External write methods + + /// @notice Executes vault rebalance. + function rebalance() public { + (uint256 deviation, bool deviationValid) = oracle.spotPriceDeviation(); + + // Execute rebalance. + // NOTE: the vault.rebalance() will revert if enough time has not elapsed. + // We thus override with a force rebalance. + // https://learn.charm.fi/charm/technical-references/core/alphaprovault#rebalance + (deviationValid && shouldForceRebalance(deviation, prevDeviation)) + ? VAULT.forceRebalance() + : VAULT.rebalance(); + + // Trim positions after rebalance. + if (!deviationValid || shouldRemoveLimitRange(deviation)) { + VAULT.removeLimitLiquidity(POOL); + } + + // Update valid rebalance state. + if (deviationValid) { + prevDeviation = deviation; + } + } + + //----------------------------------------------------------------------------- + // External/Public view methods + + /// @notice Checks if a rebalance has to be forced. + function shouldForceRebalance( + uint256 deviation, + uint256 prevDeviation_ + ) public pure returns (bool) { + // We rebalance if the deviation factor has crossed ONE (in either direction). + return ((deviation <= ONE && prevDeviation_ > ONE) || + (deviation >= ONE && prevDeviation_ < ONE)); + } + + /// @notice Checks if limit range liquidity needs to be removed. + function shouldRemoveLimitRange(uint256 deviation) public view returns (bool) { + // We only activate the limit range liquidity, when + // the vault sells SPOT and deviation is above ONE, or when + // the vault buys SPOT and deviation is below ONE + bool extraSpot = isOverweightSpot(); + bool activeLimitRange = ((deviation >= ONE && extraSpot) || + (deviation <= ONE && !extraSpot)); + return (!activeLimitRange); + } + + /// @notice Checks the vault is overweight SPOT and looking to sell the extra SPOT for USDC. + function isOverweightSpot() public view returns (bool) { + // NOTE: In the underlying univ3 pool and token0 is USDC and token1 is SPOT. + // Underweight Token0 implies that the limit range has less USDC and more SPOT. + return VAULT.isUnderweightToken0(); + } + + /// @return Number of decimals representing 1.0. + function decimals() external pure returns (uint8) { + return uint8(DECIMALS); + } +} diff --git a/spot-vaults/contracts/charm/WethWamplManager.sol b/spot-vaults/contracts/charm/WethWamplManager.sol new file mode 100644 index 00000000..6af065e0 --- /dev/null +++ b/spot-vaults/contracts/charm/WethWamplManager.sol @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.24; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { AlphaVaultHelpers } from "../_utils/AlphaVaultHelpers.sol"; +import { LineHelpers } from "../_utils/LineHelpers.sol"; +import { Line } from "../_interfaces/types/CommonTypes.sol"; + +import { IMetaOracle } from "../_interfaces/IMetaOracle.sol"; +import { IAlphaProVault } from "../_interfaces/external/IAlphaProVault.sol"; +import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; + +/// @title WethWamplManager +/// @notice This contract is a programmatic manager for the WETH-WAMPL Charm AlphaProVault. +contract WethWamplManager is Ownable { + //------------------------------------------------------------------------- + // Libraries + using AlphaVaultHelpers for IAlphaProVault; + using SafeCast for uint256; + using LineHelpers for Line; + + //------------------------------------------------------------------------- + // Constants & Immutables + + /// @dev Decimals. + uint256 public constant DECIMALS = 18; + uint256 public constant ONE = (10 ** DECIMALS); + + /// @dev At all times active liquidity percentage is no lower than 20%. + uint256 public constant MIN_ACTIVE_LIQ_PERC = ONE / 5; // 20% + + /// @notice The WETH-WAMPL charm alpha vault. + IAlphaProVault public immutable VAULT; + + /// @notice The underlying WETH-WAMPL univ3 pool. + IUniswapV3Pool public immutable POOL; + + //------------------------------------------------------------------------- + // Storage + + /// @notice The meta oracle which returns prices of AMPL asset family. + IMetaOracle public oracle; + + //------------------------------------------------------------------------- + // Active percentage calculation parameters + // + // The deviation factor (or deviation) is defined as the ratio between + // AMPL's current market price and its target price. + // The deviation is 1.0, when AMPL is at the target. + // + // The active liquidity percentage (a value between 20% to 100%) + // is computed based on pair-wise linear function, defined by the contract owner. + // + // If the current deviation is below ONE, function f1 is used + // else function f2 is used. Both f1 and f2 are defined by the owner. + // They are lines, with 2 {x,y} coordinates. The x coordinates are deviation factors, + // and y coordinates are active liquidity percentages. + // + // Both deviation and active liquidity percentage and represented internally + // as a fixed-point number with {DECIMALS} places. + // + + /// @notice Active percentage calculation function for when deviation is below ONE. + Line public activeLiqPercFn1; + + /// @notice Active percentage calculation function for when deviation is above ONE. + Line public activeLiqPercFn2; + + /// @notice The delta between the current and last recorded active liquidity percentage values + /// outside which a rebalance is executed forcefully. + uint256 public tolerableActiveLiqPercDelta; + + /// @notice The recorded deviation factor at the time of the last successful rebalance operation. + uint256 public prevDeviation; + + //----------------------------------------------------------------------------- + // Constructor and Initializer + + /// @notice Constructor initializes the contract with provided addresses. + /// @param vault_ Address of the AlphaProVault contract. + /// @param oracle_ Address of the MetaOracle contract. + constructor(IAlphaProVault vault_, IMetaOracle oracle_) Ownable() { + VAULT = vault_; + POOL = vault_.pool(); + + updateOracle(oracle_); + + activeLiqPercFn1 = Line({ + x1: ONE / 2, // 0.5 + y1: ONE / 5, // 20% + x2: ONE, // 1.0 + y2: ONE // 100% + }); + activeLiqPercFn2 = Line({ + x1: ONE, // 1.0 + y1: ONE, // 100% + x2: ONE * 2, // 2.0 + y2: ONE / 5 // 20% + }); + + tolerableActiveLiqPercDelta = ONE / 10; // 10% + prevDeviation = 0; + } + + //-------------------------------------------------------------------------- + // Owner only methods + + /// @notice Updates the MetaOracle. + function updateOracle(IMetaOracle oracle_) public onlyOwner { + // solhint-disable-next-line custom-errors + require(DECIMALS == oracle_.decimals(), "UnexpectedDecimals"); + oracle = oracle_; + } + + /// @notice Updates the vault's liquidity range parameters. + function setLiquidityRanges( + int24 baseThreshold, + uint24 fullRangeWeight, + int24 limitThreshold + ) external onlyOwner { + // Update liquidity parameters on the vault. + VAULT.setBaseThreshold(baseThreshold); + VAULT.setFullRangeWeight(fullRangeWeight); + VAULT.setLimitThreshold(limitThreshold); + } + + /// @notice Forwards the given calldata to the vault. + /// @param callData The calldata to pass to the vault. + /// @return The data returned by the vault method call. + function execOnVault( + bytes calldata callData + ) external onlyOwner returns (bytes memory) { + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory r) = address(VAULT).call(callData); + // solhint-disable-next-line custom-errors + require(success, "VaultExecutionFailed"); + return r; + } + + /// @notice Updates the active liquidity percentage calculation parameters. + function setActivePercParams( + uint256 tolerableActiveLiqPercDelta_, + Line memory activeLiqPercFn1_, + Line memory activeLiqPercFn2_ + ) external onlyOwner { + tolerableActiveLiqPercDelta = tolerableActiveLiqPercDelta_; + activeLiqPercFn1 = activeLiqPercFn1_; + activeLiqPercFn2 = activeLiqPercFn2_; + } + + //-------------------------------------------------------------------------- + // External write methods + + /// @notice Executes vault rebalance. + function rebalance() public { + // Get the current deviation factor. + (uint256 deviation, bool deviationValid) = oracle.amplPriceDeviation(); + + // Calculate the active liquidity percentages. + uint256 activeLiqPerc = deviationValid + ? computeActiveLiqPerc(deviation) + : MIN_ACTIVE_LIQ_PERC; + uint256 prevActiveLiqPerc = computeActiveLiqPerc(prevDeviation); + uint256 activeLiqPercDelta = (activeLiqPerc > prevActiveLiqPerc) + ? activeLiqPerc - prevActiveLiqPerc + : prevActiveLiqPerc - activeLiqPerc; + + // Execute rebalance. + // NOTE: the vault.rebalance() will revert if enough time has not elapsed. + // We thus override with a force rebalance. + // https://learn.charm.fi/charm/technical-references/core/alphaprovault#rebalance + (deviationValid && + shouldForceRebalance(deviation, prevDeviation, activeLiqPercDelta)) + ? VAULT.forceRebalance() + : VAULT.rebalance(); + + // Trim positions after rebalance. + VAULT.trimLiquidity(POOL, ONE - activeLiqPerc, ONE); + if (!deviationValid || shouldRemoveLimitRange(deviation)) { + VAULT.removeLimitLiquidity(POOL); + } + + // Update valid rebalance state. + if (deviationValid) { + prevDeviation = deviation; + } + } + + //----------------------------------------------------------------------------- + // External Public view methods + + /// @notice Computes active liquidity percentage based on the provided deviation factor. + /// @return The computed active liquidity percentage. + function computeActiveLiqPerc(uint256 deviation) public view returns (uint256) { + Line memory fn = (deviation <= ONE) ? activeLiqPercFn1 : activeLiqPercFn2; + return fn.computeY(deviation, MIN_ACTIVE_LIQ_PERC, ONE); + } + + /// @notice Checks if a rebalance has to be forced. + function shouldForceRebalance( + uint256 deviation, + uint256 prevDeviation_, + uint256 activeLiqPercDelta + ) public view returns (bool) { + // We have to rebalance out of turn + // - if the active liquidity perc has deviated significantly, or + // - if the deviation factor has crossed ONE (in either direction). + return + (activeLiqPercDelta > tolerableActiveLiqPercDelta) || + ((deviation <= ONE && prevDeviation_ > ONE) || + (deviation >= ONE && prevDeviation_ < ONE)); + } + + /// @notice Checks if limit range liquidity needs to be removed. + function shouldRemoveLimitRange(uint256 deviation) public view returns (bool) { + // We only activate the limit range liquidity, when + // the vault sells WAMPL and deviation is above ONE, or when + // the vault buys WAMPL and deviation is below ONE + bool extraWampl = isOverweightWampl(); + bool activeLimitRange = ((deviation >= ONE && extraWampl) || + (deviation <= ONE && !extraWampl)); + return (!activeLimitRange); + } + + /// @notice Checks the vault is overweight WAMPL, + /// and looking to sell the extra WAMPL for WETH. + function isOverweightWampl() public view returns (bool) { + // NOTE: In the underlying univ3 pool and token0 is WETH and token1 is WAMPL. + // Underweight Token0 implies that the limit range has less WETH and more WAMPL. + return VAULT.isUnderweightToken0(); + } + + /// @return Number of decimals representing 1.0. + function decimals() external pure returns (uint8) { + return uint8(DECIMALS); + } +}