Skip to content

Commit

Permalink
Scalable usdc spot strategy (#231)
Browse files Browse the repository at this point in the history
Apply suggestions from code review

Co-authored-by: Brandon Iles <[email protected]>

review fix

caching bool instead of prev deviation
  • Loading branch information
aalavandhan authored Nov 11, 2024
1 parent 62c5fb4 commit 70a2bc2
Show file tree
Hide file tree
Showing 3 changed files with 389 additions and 155 deletions.
9 changes: 6 additions & 3 deletions spot-vaults/contracts/_utils/AlphaVaultHelpers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ library AlphaVaultHelpers {
if (percToRemove <= 0) {
return;
}

int24 _fullLower = vault.fullLower();
int24 _fullUpper = vault.fullUpper();
int24 _baseLower = vault.baseLower();
Expand All @@ -61,8 +60,12 @@ library AlphaVaultHelpers {
);
// 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);
if (fullLiquidityToBurn > 0) {
vault.emergencyBurn(_fullLower, _fullUpper, fullLiquidityToBurn);
}
if (baseLiquidityToBurn > 0) {
vault.emergencyBurn(_baseLower, _baseUpper, baseLiquidityToBurn);
}
}

/// @dev A low-level method, which interacts directly with the vault and executes
Expand Down
170 changes: 131 additions & 39 deletions spot-vaults/contracts/charm/UsdcSpotManager.sol
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.24;

import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { AlphaVaultHelpers } from "../_utils/AlphaVaultHelpers.sol";
import { Range } 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 UsdcSpotManager
/// @notice This contract is a programmatic manager for the USDC-SPOT Charm AlphaProVault.
///
/// @dev The vault's active zone is defined as lower and upper percentages of FMV.
/// For example, if the active zone is [0.95, 1.05]x and SPOT's FMV price is $1.35.
/// When the market price of SPOT is between [$1.28, $1.41] we consider price to be in the active zone.
///
/// When in the active zone, the vault provides concentrated liquidity around the market price.
/// When price is outside the active zone, the vault reverts to a full range position.
///
///
contract UsdcSpotManager is Ownable {
//-------------------------------------------------------------------------
// Libraries
using AlphaVaultHelpers for IAlphaProVault;
using Math for uint256;

//-------------------------------------------------------------------------
// Constants & Immutables
Expand All @@ -22,6 +35,10 @@ contract UsdcSpotManager is Ownable {
uint256 public constant DECIMALS = 18;
uint256 public constant ONE = (10 ** DECIMALS);

/// @dev Vault parameter to set max full range weight (100%).
uint24 public constant VAULT_MAX_FRW = (10 ** 6);
int24 public constant POOL_MAX_TICK = 48000; // (-99.2/+12048.1%)

/// @notice The USDC-SPOT charm alpha vault.
IAlphaProVault public immutable VAULT;

Expand All @@ -34,8 +51,22 @@ contract UsdcSpotManager is Ownable {
/// @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;
/// @notice The lower and upper deviation factor within which
/// SPOT's price is considered to be in the active zone.
Range public activeZoneDeviation;

/// @notice The width of concentrated liquidity band,
/// SPOT's price is in the active zone.
uint256 public concBandDeviationWidth;

/// @notice The maximum USDC balance of the vault's full range position.
uint256 public fullRangeMaxUsdcBal;

/// @notice The maximum percentage of vault's balanced assets in the full range position.
uint256 public fullRangeMaxPerc;

/// @notice If price was within the active zone at the time of the last successful rebalance operation.
bool public prevWithinActiveZone;

//-----------------------------------------------------------------------------
// Constructor and Initializer
Expand All @@ -49,7 +80,14 @@ contract UsdcSpotManager is Ownable {

updateOracle(oracle_);

prevDeviation = 0;
prevWithinActiveZone = false;
activeZoneDeviation = Range({
lower: ((ONE * 95) / 100), // 0.95 or 95%
upper: ((ONE * 105) / 100) // 1.05 or 105%
});
concBandDeviationWidth = (ONE / 20); // 0.05 or 5%
fullRangeMaxUsdcBal = 250000 * (10 ** 6); // 250k USDC
fullRangeMaxPerc = (ONE / 2); // 0.5 or 50%
}

//--------------------------------------------------------------------------
Expand All @@ -62,18 +100,6 @@ contract UsdcSpotManager is Ownable {
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.
Expand All @@ -87,54 +113,73 @@ contract UsdcSpotManager is Ownable {
return r;
}

/// @notice Updates the active zone definition.
function updateActiveZone(Range memory activeZoneDeviation_) external onlyOwner {
activeZoneDeviation = activeZoneDeviation_;
}

/// @notice Updates the width of the concentrated liquidity band.
function updateConcentratedBand(uint256 concBandDeviationWidth_) external onlyOwner {
concBandDeviationWidth = concBandDeviationWidth_;
}

/// @notice Updates the absolute and percentage maximum amount of liquidity
/// in the full range liquidity band.
function updateFullRangeLiquidity(
uint256 fullRangeMaxUsdcBal_,
uint256 fullRangeMaxPerc_
) external onlyOwner {
// solhint-disable-next-line custom-errors
require(fullRangeMaxPerc_ <= ONE, "InvalidPerc");
fullRangeMaxUsdcBal = fullRangeMaxUsdcBal_;
fullRangeMaxPerc = fullRangeMaxPerc_;
}

//--------------------------------------------------------------------------
// External write methods

/// @notice Executes vault rebalance.
function rebalance() public {
(uint256 deviation, bool deviationValid) = oracle.spotPriceDeviation();
bool withinActiveZone = (deviationValid && activeZone(deviation));
bool shouldForceRebalance = (withinActiveZone != prevWithinActiveZone);

// Set liquidity parameters.
withinActiveZone ? _setupActiveZoneLiq(deviation) : _resetLiq();

// 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();
shouldForceRebalance ? VAULT.forceRebalance() : VAULT.rebalance();

// Trim positions after rebalance.
if (!deviationValid || shouldRemoveLimitRange(deviation)) {
if (!withinActiveZone) {
VAULT.trimLiquidity(POOL, ONE - activeFullRangePerc(), ONE);
VAULT.removeLimitLiquidity(POOL);
}

// Update valid rebalance state.
if (deviationValid) {
prevDeviation = deviation;
prevWithinActiveZone = withinActiveZone;
}
}

//-----------------------------------------------------------------------------
// 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));
// External/Public read methods

/// @notice Based on the given deviation factor,
/// calculates if the pool needs to be in the active zone.
function activeZone(uint256 deviation) public view returns (bool) {
return (activeZoneDeviation.lower <= deviation &&
deviation <= activeZoneDeviation.upper);
}

/// @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 Computes the percentage of liquidity to be deployed into the full range,
/// based on owner defined maximums.
function activeFullRangePerc() public view returns (uint256) {
(uint256 usdcBal, ) = VAULT.getTotalAmounts();
return Math.min(ONE.mulDiv(fullRangeMaxUsdcBal, usdcBal), fullRangeMaxPerc);
}

/// @notice Checks the vault is overweight SPOT and looking to sell the extra SPOT for USDC.
Expand All @@ -144,8 +189,55 @@ contract UsdcSpotManager is Ownable {
return VAULT.isUnderweightToken0();
}

/// @notice Calculates the Univ3 tick equivalent of the given deviation factor.
function deviationToTicks(uint256 deviation) public pure returns (int24) {
// 2% ~ 200 ticks -> (POOL.tickSpacing())
// NOTE: width can't be zero, we set the minimum possible to 200.
uint256 t = deviation.mulDiv(10000, ONE);
t -= (t % 200);
return (t >= 200 ? SafeCast.toInt24(SafeCast.toInt256(t)) : int24(200));
}

/// @return Number of decimals representing 1.0.
function decimals() external pure returns (uint8) {
return uint8(DECIMALS);
}

//-----------------------------------------------------------------------------
// Private methods

/// @dev Configures the vault to provide concentrated liquidity in the active zone.
function _setupActiveZoneLiq(uint256 deviation) private {
VAULT.setFullRangeWeight(
SafeCast.toUint24(uint256(VAULT_MAX_FRW).mulDiv(activeFullRangePerc(), ONE))
);

// IMPORTANT:
//
// If price is exactly at the bounds of `activeZoneDeviation`,
// the concentrated liquidity will be *at most*
// `deviationToTicks(concBandDeviationWidth/2)` outside the bounds.
//
VAULT.setBaseThreshold(deviationToTicks(concBandDeviationWidth));
VAULT.setLimitThreshold(
deviationToTicks(
isOverweightSpot()
? Math.max(
activeZoneDeviation.upper - deviation,
concBandDeviationWidth / 2
)
: Math.max(
deviation - activeZoneDeviation.lower,
concBandDeviationWidth / 2
)
)
);
}

/// @dev Resets the vault to provide full range liquidity.
function _resetLiq() private {
VAULT.setFullRangeWeight(VAULT_MAX_FRW);
VAULT.setBaseThreshold(POOL_MAX_TICK);
VAULT.setLimitThreshold(POOL_MAX_TICK);
}
}
Loading

0 comments on commit 70a2bc2

Please sign in to comment.