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
@@ -34,8 +51,22 @@ contract UsdcSpotManager is Ownable {
3451 /// @notice The meta oracle which returns prices of AMPL asset family.
3552 IMetaOracle public oracle;
3653
37- /// @notice The recorded deviation factor at the time of the last successful rebalance operation.
38- uint256 public prevDeviation;
54+ /// @notice The lower and upper deviation factor within which
55+ /// SPOT's price is considered to be in the active zone.
56+ Range public activeZoneDeviation;
57+
58+ /// @notice The width of concentrated liquidity band,
59+ /// SPOT's price is in the active zone.
60+ uint256 public concBandDeviationWidth;
61+
62+ /// @notice The maximum USDC balance of the vault's full range position.
63+ uint256 public fullRangeMaxUsdcBal;
64+
65+ /// @notice The maximum percentage of vault's balanced assets in the full range position.
66+ uint256 public fullRangeMaxPerc;
67+
68+ /// @notice If price was within the active zone at the time of the last successful rebalance operation.
69+ bool public prevWithinActiveZone;
3970
4071 //-----------------------------------------------------------------------------
4172 // Constructor and Initializer
@@ -49,7 +80,14 @@ contract UsdcSpotManager is Ownable {
4980
5081 updateOracle (oracle_);
5182
52- prevDeviation = 0 ;
83+ prevWithinActiveZone = false ;
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,54 +113,73 @@ 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 shouldForceRebalance = (withinActiveZone != prevWithinActiveZone);
146+
147+ // Set liquidity parameters.
148+ withinActiveZone ? _setupActiveZoneLiq (deviation) : _resetLiq ();
96149
97150 // Execute rebalance.
98151 // NOTE: the vault.rebalance() will revert if enough time has not elapsed.
99152 // We thus override with a force rebalance.
100153 // https://learn.charm.fi/charm/technical-references/core/alphaprovault#rebalance
101- (deviationValid && shouldForceRebalance (deviation, prevDeviation))
102- ? VAULT.forceRebalance ()
103- : VAULT.rebalance ();
154+ shouldForceRebalance ? VAULT.forceRebalance () : VAULT.rebalance ();
104155
105156 // Trim positions after rebalance.
106- if (! deviationValid || shouldRemoveLimitRange (deviation)) {
157+ if (! withinActiveZone) {
158+ VAULT.trimLiquidity (POOL, ONE - activeFullRangePerc (), ONE);
107159 VAULT.removeLimitLiquidity (POOL);
108160 }
109161
110162 // Update valid rebalance state.
111163 if (deviationValid) {
112- prevDeviation = deviation ;
164+ prevWithinActiveZone = withinActiveZone ;
113165 }
114166 }
115167
116168 //-----------------------------------------------------------------------------
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));
169+ // External/Public read methods
170+
171+ /// @notice Based on the given deviation factor,
172+ /// calculates if the pool needs to be in the active zone.
173+ function activeZone (uint256 deviation ) public view returns (bool ) {
174+ return (activeZoneDeviation.lower <= deviation &&
175+ deviation <= activeZoneDeviation.upper);
127176 }
128177
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);
178+ /// @notice Computes the percentage of liquidity to be deployed into the full range,
179+ /// based on owner defined maximums.
180+ function activeFullRangePerc () public view returns (uint256 ) {
181+ (uint256 usdcBal , ) = VAULT.getTotalAmounts ();
182+ return Math.min (ONE.mulDiv (fullRangeMaxUsdcBal, usdcBal), fullRangeMaxPerc);
138183 }
139184
140185 /// @notice Checks the vault is overweight SPOT and looking to sell the extra SPOT for USDC.
@@ -144,8 +189,55 @@ contract UsdcSpotManager is Ownable {
144189 return VAULT.isUnderweightToken0 ();
145190 }
146191
192+ /// @notice Calculates the Univ3 tick equivalent of the given deviation factor.
193+ function deviationToTicks (uint256 deviation ) public pure returns (int24 ) {
194+ // 2% ~ 200 ticks -> (POOL.tickSpacing())
195+ // NOTE: width can't be zero, we set the minimum possible to 200.
196+ uint256 t = deviation.mulDiv (10000 , ONE);
197+ t -= (t % 200 );
198+ return (t >= 200 ? SafeCast.toInt24 (SafeCast.toInt256 (t)) : int24 (200 ));
199+ }
200+
147201 /// @return Number of decimals representing 1.0.
148202 function decimals () external pure returns (uint8 ) {
149203 return uint8 (DECIMALS);
150204 }
205+
206+ //-----------------------------------------------------------------------------
207+ // Private methods
208+
209+ /// @dev Configures the vault to provide concentrated liquidity in the active zone.
210+ function _setupActiveZoneLiq (uint256 deviation ) private {
211+ VAULT.setFullRangeWeight (
212+ SafeCast.toUint24 (uint256 (VAULT_MAX_FRW).mulDiv (activeFullRangePerc (), ONE))
213+ );
214+
215+ // IMPORTANT:
216+ //
217+ // If price is exactly at the bounds of `activeZoneDeviation`,
218+ // the concentrated liquidity will be *at most*
219+ // `deviationToTicks(concBandDeviationWidth/2)` outside the bounds.
220+ //
221+ VAULT.setBaseThreshold (deviationToTicks (concBandDeviationWidth));
222+ VAULT.setLimitThreshold (
223+ deviationToTicks (
224+ isOverweightSpot ()
225+ ? Math.max (
226+ activeZoneDeviation.upper - deviation,
227+ concBandDeviationWidth / 2
228+ )
229+ : Math.max (
230+ deviation - activeZoneDeviation.lower,
231+ concBandDeviationWidth / 2
232+ )
233+ )
234+ );
235+ }
236+
237+ /// @dev Resets the vault to provide full range liquidity.
238+ function _resetLiq () private {
239+ VAULT.setFullRangeWeight (VAULT_MAX_FRW);
240+ VAULT.setBaseThreshold (POOL_MAX_TICK);
241+ VAULT.setLimitThreshold (POOL_MAX_TICK);
242+ }
151243}
0 commit comments