@@ -13,9 +13,17 @@ import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3
1313
1414/// @title UsdcSpotManager
1515/// @notice This contract is a programmatic manager for the USDC-SPOT Charm AlphaProVault.
16- /// @dev The vault provides highly concentrated liquidity when the market-price of SPOT is within
17- /// the "active" zone as defined by the owner. When price moves outside the
18- /// zone, the vault moves all assets into a full range position (infinite band).
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+ /// The vault further minimizes IL from charm's passive rebalancing,
25+ /// by limiting buying SPOT above it's FMV or selling SPOT below it's FMV.
26+ ///
1927contract UsdcSpotManager is Ownable {
2028 //-------------------------------------------------------------------------
2129 // Libraries
@@ -59,6 +67,9 @@ contract UsdcSpotManager is Ownable {
5967 /// @notice The maximum USDC balance of the vault's full range position.
6068 uint256 public fullRangeMaxUsdcBal;
6169
70+ /// @notice The maximum percentage of vault's balanced assets in the full range position.
71+ uint256 public fullRangeMaxPerc;
72+
6273 //-----------------------------------------------------------------------------
6374 // Constructor and Initializer
6475
@@ -76,8 +87,9 @@ contract UsdcSpotManager is Ownable {
7687 lower: ((ONE * 95 ) / 100 ), // 0.95 or 95%
7788 upper: ((ONE * 105 ) / 100 ) // 1.05 or 105%
7889 });
79- concBandDeviationWidth = (( ONE * 5 ) / 100 ); // 0.05 or 5%
90+ concBandDeviationWidth = (ONE / 20 ); // 0.05 or 5%
8091 fullRangeMaxUsdcBal = 250000 * (10 ** 6 ); // 250k USDC
92+ fullRangeMaxPerc = (ONE / 2 ); // 0.5 or 50%
8193 }
8294
8395 //--------------------------------------------------------------------------
@@ -113,9 +125,16 @@ contract UsdcSpotManager is Ownable {
113125 concBandDeviationWidth = concBandDeviationWidth_;
114126 }
115127
116- /// @notice Updates the amount of liquidity in the full range liquidity band.
117- function updateFullRangeLiquidity (uint256 fullRangeMaxUsdcBal_ ) external onlyOwner {
128+ /// @notice Updates the absolute and percentage maximum amount of liquidity
129+ /// in the full range liquidity band.
130+ function updateFullRangeLiquidity (
131+ uint256 fullRangeMaxUsdcBal_ ,
132+ uint256 fullRangeMaxPerc_
133+ ) external onlyOwner {
134+ // solhint-disable-next-line custom-errors
135+ require (fullRangeMaxPerc_ <= ONE, "InvalidPerc " );
118136 fullRangeMaxUsdcBal = fullRangeMaxUsdcBal_;
137+ fullRangeMaxPerc = fullRangeMaxPerc_;
119138 }
120139
121140 //--------------------------------------------------------------------------
@@ -127,7 +146,6 @@ contract UsdcSpotManager is Ownable {
127146 bool withinActiveZone = (deviationValid && activeZone (deviation));
128147 bool withinActiveZoneBefore = activeZone (prevDeviation);
129148 bool shouldForceRebalance = (withinActiveZone != withinActiveZoneBefore);
130- (withinActiveZone && ! withinActiveZoneBefore));
131149
132150 // Set liquidity parameters.
133151 withinActiveZone ? _setupActiveZoneLiq (deviation) : _resetLiq ();
@@ -139,7 +157,7 @@ contract UsdcSpotManager is Ownable {
139157 shouldForceRebalance ? VAULT.forceRebalance () : VAULT.rebalance ();
140158
141159 // Trim positions after rebalance.
142- _trimLiquidity (withinActiveZone);
160+ _trimLiquidity (deviation, withinActiveZone);
143161
144162 // Update valid rebalance state.
145163 if (deviationValid) {
@@ -148,10 +166,10 @@ contract UsdcSpotManager is Ownable {
148166 }
149167
150168 //-----------------------------------------------------------------------------
151- // External/Public view methods
169+ // External/Public read methods
152170
153171 /// @notice Based on the given deviation factor,
154- /// calculates if the pool is in the active zone.
172+ /// calculates if the pool needs to be in the active zone.
155173 function activeZone (uint256 deviation ) public view returns (bool ) {
156174 Range memory concBandDeviation = Range ({
157175 lower: (activeZoneDeviation.lower + concBandDeviationWidth / 2 ),
@@ -161,6 +179,23 @@ contract UsdcSpotManager is Ownable {
161179 deviation <= concBandDeviation.upper);
162180 }
163181
182+ /// @notice Computes the percentage of liquidity to be deployed into the full range,
183+ /// based on owner defined maximums.
184+ function activeFullRangePerc () public view returns (uint256 ) {
185+ (uint256 usdcBal , ) = VAULT.getTotalAmounts ();
186+ return Math.min (ONE.mulDiv (fullRangeMaxUsdcBal, usdcBal), fullRangeMaxPerc);
187+ }
188+
189+ /// @notice Checks if limit range liquidity needs to be removed.
190+ function activeLimitRange (uint256 deviation ) public view returns (bool ) {
191+ // We only activate the limit range liquidity, when
192+ // the vault sells SPOT and deviation is above (ONE-concBandDeviationWidth/2), or when
193+ // the vault buys SPOT and deviation is below (ONE+concBandDeviationWidth/2)
194+ bool extraSpot = isOverweightSpot ();
195+ return ((extraSpot && deviation >= (ONE - concBandDeviationWidth / 2 )) ||
196+ (! extraSpot && deviation <= (ONE + concBandDeviationWidth / 2 )));
197+ }
198+
164199 /// @notice Checks the vault is overweight SPOT and looking to sell the extra SPOT for USDC.
165200 function isOverweightSpot () public view returns (bool ) {
166201 // NOTE: In the underlying univ3 pool and token0 is USDC and token1 is SPOT.
@@ -182,22 +217,24 @@ contract UsdcSpotManager is Ownable {
182217 return uint8 (DECIMALS);
183218 }
184219
220+ //-----------------------------------------------------------------------------
221+ // Private methods
222+
185223 /// @dev Configures the vault to provide concentrated liquidity in the active zone.
186224 function _setupActiveZoneLiq (uint256 deviation ) private {
187- (uint256 usdcBal , ) = VAULT.getTotalAmounts ();
188- uint256 fullRangeWeight = uint256 (VAULT_MAX_FRW).mulDiv (
189- fullRangeMaxUsdcBal,
190- usdcBal
225+ VAULT.setFullRangeWeight (
226+ SafeCast.toUint24 (uint256 (VAULT_MAX_FRW).mulDiv (activeFullRangePerc (), ONE))
191227 );
192- uint256 limitBandDeviationWidth = isOverweightSpot ()
193- ? activeZoneDeviation.upper - deviation
194- : deviation - activeZoneDeviation.lower;
195- VAULT.setFullRangeWeight (SafeCast.toUint24 (fullRangeWeight));
196-
197228 // IMPORTANT: The deviation percentage, to ticks conversion isn't precise.
198229 // In the worst case, there might be liquidity up to 2% outside the active zone definition.
199230 VAULT.setBaseThreshold (deviationToTicks (concBandDeviationWidth));
200- VAULT.setLimitThreshold (deviationToTicks (limitBandDeviationWidth));
231+ VAULT.setLimitThreshold (
232+ deviationToTicks (
233+ isOverweightSpot ()
234+ ? activeZoneDeviation.upper - deviation
235+ : deviation - activeZoneDeviation.lower
236+ )
237+ );
201238 }
202239
203240 /// @dev Resets the vault to provide full range liquidity.
@@ -208,15 +245,12 @@ contract UsdcSpotManager is Ownable {
208245 }
209246
210247 /// @dev Removes excess liquidity from the vault after rebalance.
211- function _trimLiquidity (bool withinActiveZone ) private {
212- // If we are in the active zone, we keep all the liquidity.
213- if (withinActiveZone) {
214- return ;
248+ function _trimLiquidity (uint256 deviation , bool withinActiveZone ) private {
249+ if (! withinActiveZone) {
250+ VAULT.trimLiquidity (POOL, ONE - activeFullRangePerc (), ONE);
251+ }
252+ if (! activeLimitRange (deviation)) {
253+ VAULT.removeLimitLiquidity (POOL);
215254 }
216- // Otherwise, we trim positions.
217- (uint256 usdcBal , ) = VAULT.getTotalAmounts ();
218- uint256 percToRemove = (ONE - ONE.mulDiv (fullRangeMaxUsdcBal, usdcBal));
219- VAULT.trimLiquidity (POOL, percToRemove, ONE);
220- VAULT.removeLimitLiquidity (POOL);
221255 }
222256}
0 commit comments