11// SPDX-License-Identifier: BUSL-1.1
22pragma solidity ^ 0.8.24 ;
33
4+ import { Math } from "@openzeppelin/contracts/utils/math/Math.sol " ;
5+ import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol " ;
46import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol " ;
57import { AlphaVaultHelpers } from "../_utils/AlphaVaultHelpers.sol " ;
8+ import { Range } from "../_interfaces/types/CommonTypes.sol " ;
69
710import { IMetaOracle } from "../_interfaces/IMetaOracle.sol " ;
811import { IAlphaProVault } from "../_interfaces/external/IAlphaProVault.sol " ;
912import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol " ;
1013
1114/// @title UsdcSpotManager
1215/// @notice This contract is a programmatic manager for the USDC-SPOT Charm AlphaProVault.
16+ ///
17+ /// @dev The vault's active zone is defined as lower and upper percentages of FMV.
18+ /// For example, if the active zone is [0.95, 1.05]x and SPOT's FMV price is $1.35.
19+ /// When the market price of SPOT is between [$1.28, $1.41] we consider price to be in the active zone.
20+ ///
21+ /// When in the active zone, the vault provides concentrated liquidity around the market price.
22+ /// When price is outside the active zone, the vault reverts to a full range position.
23+ ///
24+ ///
1325contract UsdcSpotManager is Ownable {
1426 //-------------------------------------------------------------------------
1527 // Libraries
1628 using AlphaVaultHelpers for IAlphaProVault;
29+ using Math for uint256 ;
1730
1831 //-------------------------------------------------------------------------
1932 // Constants & Immutables
@@ -22,6 +35,10 @@ contract UsdcSpotManager is Ownable {
2235 uint256 public constant DECIMALS = 18 ;
2336 uint256 public constant ONE = (10 ** DECIMALS);
2437
38+ /// @dev Vault parameter to set max full range weight (100%).
39+ uint24 public constant VAULT_MAX_FRW = (10 ** 6 );
40+ int24 public constant POOL_MAX_TICK = 48000 ; // (-99.2/+12048.1%)
41+
2542 /// @notice The USDC-SPOT charm alpha vault.
2643 IAlphaProVault public immutable VAULT;
2744
@@ -37,6 +54,20 @@ contract UsdcSpotManager is Ownable {
3754 /// @notice The recorded deviation factor at the time of the last successful rebalance operation.
3855 uint256 public prevDeviation;
3956
57+ /// @notice The lower and upper deviation factor within which
58+ /// SPOT's price is considered to be in the active zone.
59+ Range public activeZoneDeviation;
60+
61+ /// @notice The width of concentrated liquidity band,
62+ /// SPOT's price is in the active zone.
63+ uint256 public concBandDeviationWidth;
64+
65+ /// @notice The maximum USDC balance of the vault's full range position.
66+ uint256 public fullRangeMaxUsdcBal;
67+
68+ /// @notice The maximum percentage of vault's balanced assets in the full range position.
69+ uint256 public fullRangeMaxPerc;
70+
4071 //-----------------------------------------------------------------------------
4172 // Constructor and Initializer
4273
@@ -50,6 +81,13 @@ contract UsdcSpotManager is Ownable {
5081 updateOracle (oracle_);
5182
5283 prevDeviation = 0 ;
84+ activeZoneDeviation = Range ({
85+ lower: ((ONE * 95 ) / 100 ), // 0.95 or 95%
86+ upper: ((ONE * 105 ) / 100 ) // 1.05 or 105%
87+ });
88+ concBandDeviationWidth = (ONE / 20 ); // 0.05 or 5%
89+ fullRangeMaxUsdcBal = 250000 * (10 ** 6 ); // 250k USDC
90+ fullRangeMaxPerc = (ONE / 2 ); // 0.5 or 50%
5391 }
5492
5593 //--------------------------------------------------------------------------
@@ -62,18 +100,6 @@ contract UsdcSpotManager is Ownable {
62100 oracle = oracle_;
63101 }
64102
65- /// @notice Updates the vault's liquidity range parameters.
66- function setLiquidityRanges (
67- int24 baseThreshold ,
68- uint24 fullRangeWeight ,
69- int24 limitThreshold
70- ) external onlyOwner {
71- // Update liquidity parameters on the vault.
72- VAULT.setBaseThreshold (baseThreshold);
73- VAULT.setFullRangeWeight (fullRangeWeight);
74- VAULT.setLimitThreshold (limitThreshold);
75- }
76-
77103 /// @notice Forwards the given calldata to the vault.
78104 /// @param callData The calldata to pass to the vault.
79105 /// @return The data returned by the vault method call.
@@ -87,23 +113,50 @@ contract UsdcSpotManager is Ownable {
87113 return r;
88114 }
89115
116+ /// @notice Updates the active zone definition.
117+ function updateActiveZone (Range memory activeZoneDeviation_ ) external onlyOwner {
118+ activeZoneDeviation = activeZoneDeviation_;
119+ }
120+
121+ /// @notice Updates the width of the concentrated liquidity band.
122+ function updateConcentratedBand (uint256 concBandDeviationWidth_ ) external onlyOwner {
123+ concBandDeviationWidth = concBandDeviationWidth_;
124+ }
125+
126+ /// @notice Updates the absolute and percentage maximum amount of liquidity
127+ /// in the full range liquidity band.
128+ function updateFullRangeLiquidity (
129+ uint256 fullRangeMaxUsdcBal_ ,
130+ uint256 fullRangeMaxPerc_
131+ ) external onlyOwner {
132+ // solhint-disable-next-line custom-errors
133+ require (fullRangeMaxPerc_ <= ONE, "InvalidPerc " );
134+ fullRangeMaxUsdcBal = fullRangeMaxUsdcBal_;
135+ fullRangeMaxPerc = fullRangeMaxPerc_;
136+ }
137+
90138 //--------------------------------------------------------------------------
91139 // External write methods
92140
93141 /// @notice Executes vault rebalance.
94142 function rebalance () public {
95143 (uint256 deviation , bool deviationValid ) = oracle.spotPriceDeviation ();
144+ bool withinActiveZone = (deviationValid && activeZone (deviation));
145+ bool withinActiveZoneBefore = activeZone (prevDeviation);
146+ bool shouldForceRebalance = (withinActiveZone != withinActiveZoneBefore);
147+
148+ // Set liquidity parameters.
149+ withinActiveZone ? _setupActiveZoneLiq (deviation) : _resetLiq ();
96150
97151 // Execute rebalance.
98152 // NOTE: the vault.rebalance() will revert if enough time has not elapsed.
99153 // We thus override with a force rebalance.
100154 // https://learn.charm.fi/charm/technical-references/core/alphaprovault#rebalance
101- (deviationValid && shouldForceRebalance (deviation, prevDeviation))
102- ? VAULT.forceRebalance ()
103- : VAULT.rebalance ();
155+ shouldForceRebalance ? VAULT.forceRebalance () : VAULT.rebalance ();
104156
105157 // Trim positions after rebalance.
106- if (! deviationValid || shouldRemoveLimitRange (deviation)) {
158+ if (! withinActiveZone) {
159+ VAULT.trimLiquidity (POOL, ONE - activeFullRangePerc (), ONE);
107160 VAULT.removeLimitLiquidity (POOL);
108161 }
109162
@@ -114,27 +167,24 @@ contract UsdcSpotManager is Ownable {
114167 }
115168
116169 //-----------------------------------------------------------------------------
117- // External/Public view methods
118-
119- /// @notice Checks if a rebalance has to be forced.
120- function shouldForceRebalance (
121- uint256 deviation ,
122- uint256 prevDeviation_
123- ) public pure returns (bool ) {
124- // We rebalance if the deviation factor has crossed ONE (in either direction).
125- return ((deviation <= ONE && prevDeviation_ > ONE) ||
126- (deviation >= ONE && prevDeviation_ < ONE));
170+ // External/Public read methods
171+
172+ /// @notice Based on the given deviation factor,
173+ /// calculates if the pool needs to be in the active zone.
174+ function activeZone (uint256 deviation ) public view returns (bool ) {
175+ Range memory concBandDeviation = Range ({
176+ lower: (activeZoneDeviation.lower + concBandDeviationWidth / 2 ),
177+ upper: (activeZoneDeviation.upper - concBandDeviationWidth / 2 )
178+ });
179+ return (concBandDeviation.lower <= deviation &&
180+ deviation <= concBandDeviation.upper);
127181 }
128182
129- /// @notice Checks if limit range liquidity needs to be removed.
130- function shouldRemoveLimitRange (uint256 deviation ) public view returns (bool ) {
131- // We only activate the limit range liquidity, when
132- // the vault sells SPOT and deviation is above ONE, or when
133- // the vault buys SPOT and deviation is below ONE
134- bool extraSpot = isOverweightSpot ();
135- bool activeLimitRange = ((deviation >= ONE && extraSpot) ||
136- (deviation <= ONE && ! extraSpot));
137- return (! activeLimitRange);
183+ /// @notice Computes the percentage of liquidity to be deployed into the full range,
184+ /// based on owner defined maximums.
185+ function activeFullRangePerc () public view returns (uint256 ) {
186+ (uint256 usdcBal , ) = VAULT.getTotalAmounts ();
187+ return Math.min (ONE.mulDiv (fullRangeMaxUsdcBal, usdcBal), fullRangeMaxPerc);
138188 }
139189
140190 /// @notice Checks the vault is overweight SPOT and looking to sell the extra SPOT for USDC.
@@ -144,8 +194,44 @@ contract UsdcSpotManager is Ownable {
144194 return VAULT.isUnderweightToken0 ();
145195 }
146196
197+ /// @notice Calculates the Univ3 tick equivalent of the given deviation factor.
198+ function deviationToTicks (uint256 deviation ) public pure returns (int24 ) {
199+ // 2% ~ 200 ticks -> (POOL.tickSpacing())
200+ // NOTE: width can't be zero, we set the minimum possible to 200.
201+ uint256 t = deviation.mulDiv (10000 , ONE);
202+ t -= (t % 200 );
203+ return (t >= 200 ? SafeCast.toInt24 (SafeCast.toInt256 (t)) : int24 (200 ));
204+ }
205+
147206 /// @return Number of decimals representing 1.0.
148207 function decimals () external pure returns (uint8 ) {
149208 return uint8 (DECIMALS);
150209 }
210+
211+ //-----------------------------------------------------------------------------
212+ // Private methods
213+
214+ /// @dev Configures the vault to provide concentrated liquidity in the active zone.
215+ function _setupActiveZoneLiq (uint256 deviation ) private {
216+ VAULT.setFullRangeWeight (
217+ SafeCast.toUint24 (uint256 (VAULT_MAX_FRW).mulDiv (activeFullRangePerc (), ONE))
218+ );
219+ // IMPORTANT: The deviation percentage, to ticks conversion isn't precise.
220+ // In the worst case, there might be liquidity up to 2% outside the active zone definition.
221+ VAULT.setBaseThreshold (deviationToTicks (concBandDeviationWidth));
222+ VAULT.setLimitThreshold (
223+ deviationToTicks (
224+ isOverweightSpot ()
225+ ? activeZoneDeviation.upper - deviation
226+ : deviation - activeZoneDeviation.lower
227+ )
228+ );
229+ }
230+
231+ /// @dev Resets the vault to provide full range liquidity.
232+ function _resetLiq () private {
233+ VAULT.setFullRangeWeight (VAULT_MAX_FRW);
234+ VAULT.setBaseThreshold (POOL_MAX_TICK);
235+ VAULT.setLimitThreshold (POOL_MAX_TICK);
236+ }
151237}
0 commit comments