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+ /// @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).
1319contract UsdcSpotManager is Ownable {
1420 //-------------------------------------------------------------------------
1521 // Libraries
1622 using AlphaVaultHelpers for IAlphaProVault;
23+ using Math for uint256 ;
1724
1825 //-------------------------------------------------------------------------
1926 // Constants & Immutables
@@ -22,6 +29,10 @@ contract UsdcSpotManager is Ownable {
2229 uint256 public constant DECIMALS = 18 ;
2330 uint256 public constant ONE = (10 ** DECIMALS);
2431
32+ /// @dev Vault parameter to set max full range weight (100%).
33+ uint24 public constant VAULT_MAX_FRW = (10 ** 6 );
34+ int24 public constant POOL_MAX_TICK = 880000 ;
35+
2536 /// @notice The USDC-SPOT charm alpha vault.
2637 IAlphaProVault public immutable VAULT;
2738
@@ -37,6 +48,17 @@ contract UsdcSpotManager is Ownable {
3748 /// @notice The recorded deviation factor at the time of the last successful rebalance operation.
3849 uint256 public prevDeviation;
3950
51+ /// @notice The lower and upper deviation factor within which
52+ /// SPOT's price is considered to be in the active zone.
53+ Range public activeZoneDeviation;
54+
55+ /// @notice The width of concentrated liquidity band,
56+ /// SPOT's price is in the active zone.
57+ uint256 public concBandDeviationWidth;
58+
59+ /// @notice The maximum USDC balance of the vault's full range position.
60+ uint256 public fullRangeMaxUsdcBal;
61+
4062 //-----------------------------------------------------------------------------
4163 // Constructor and Initializer
4264
@@ -50,6 +72,12 @@ contract UsdcSpotManager is Ownable {
5072 updateOracle (oracle_);
5173
5274 prevDeviation = 0 ;
75+ activeZoneDeviation = Range ({
76+ lower: ((ONE * 95 ) / 100 ), // 0.95 or 95%
77+ upper: ((ONE * 105 ) / 100 ) // 1.05 or 105%
78+ });
79+ concBandDeviationWidth = ((ONE * 5 ) / 100 ); // 0.05 or 5%
80+ fullRangeMaxUsdcBal = 250000 * (10 ** 6 ); // 250k USDC
5381 }
5482
5583 //--------------------------------------------------------------------------
@@ -62,18 +90,6 @@ contract UsdcSpotManager is Ownable {
6290 oracle = oracle_;
6391 }
6492
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-
7793 /// @notice Forwards the given calldata to the vault.
7894 /// @param callData The calldata to pass to the vault.
7995 /// @return The data returned by the vault method call.
@@ -87,25 +103,43 @@ contract UsdcSpotManager is Ownable {
87103 return r;
88104 }
89105
106+ /// @notice Updates the active zone definition.
107+ function updateActiveZone (Range memory activeZoneDeviation_ ) external onlyOwner {
108+ activeZoneDeviation = activeZoneDeviation_;
109+ }
110+
111+ /// @notice Updates the width of the concentrated liquidity band.
112+ function updateConcentratedBand (uint256 concBandDeviationWidth_ ) external onlyOwner {
113+ concBandDeviationWidth = concBandDeviationWidth_;
114+ }
115+
116+ /// @notice Updates the amount of liquidity in the full range liquidity band.
117+ function updateFullRangeLiquidity (uint256 fullRangeMaxUsdcBal_ ) external onlyOwner {
118+ fullRangeMaxUsdcBal = fullRangeMaxUsdcBal_;
119+ }
120+
90121 //--------------------------------------------------------------------------
91122 // External write methods
92123
93124 /// @notice Executes vault rebalance.
94125 function rebalance () public {
95126 (uint256 deviation , bool deviationValid ) = oracle.spotPriceDeviation ();
127+ bool withinActiveZone = (deviationValid && activeZone (deviation));
128+ bool withinActiveZoneBefore = activeZone (prevDeviation);
129+ bool shouldForceRebalance = ((! withinActiveZone && withinActiveZoneBefore) ||
130+ (withinActiveZone && ! withinActiveZoneBefore));
131+
132+ // Set liquidity parameters.
133+ withinActiveZone ? _setupActiveZoneLiq (deviation) : _resetLiq ();
96134
97135 // Execute rebalance.
98136 // NOTE: the vault.rebalance() will revert if enough time has not elapsed.
99137 // We thus override with a force rebalance.
100138 // https://learn.charm.fi/charm/technical-references/core/alphaprovault#rebalance
101- (deviationValid && shouldForceRebalance (deviation, prevDeviation))
102- ? VAULT.forceRebalance ()
103- : VAULT.rebalance ();
139+ shouldForceRebalance ? VAULT.forceRebalance () : VAULT.rebalance ();
104140
105141 // Trim positions after rebalance.
106- if (! deviationValid || shouldRemoveLimitRange (deviation)) {
107- VAULT.removeLimitLiquidity (POOL);
108- }
142+ _trimLiquidity (withinActiveZone);
109143
110144 // Update valid rebalance state.
111145 if (deviationValid) {
@@ -116,25 +150,15 @@ contract UsdcSpotManager is Ownable {
116150 //-----------------------------------------------------------------------------
117151 // External/Public view methods
118152
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));
127- }
128-
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);
153+ /// @notice Based on the given deviation factor,
154+ /// calculates if the pool needs to be in the active zone.
155+ function activeZone (uint256 deviation ) public view returns (bool ) {
156+ Range memory concBandDeviation = Range ({
157+ lower: (activeZoneDeviation.lower + concBandDeviationWidth / 2 ),
158+ upper: (activeZoneDeviation.upper - concBandDeviationWidth / 2 )
159+ });
160+ return (concBandDeviation.lower <= deviation &&
161+ deviation <= concBandDeviation.upper);
138162 }
139163
140164 /// @notice Checks the vault is overweight SPOT and looking to sell the extra SPOT for USDC.
@@ -144,8 +168,55 @@ contract UsdcSpotManager is Ownable {
144168 return VAULT.isUnderweightToken0 ();
145169 }
146170
171+ /// @notice Calculates the Univ3 tick equivalent of the given deviation factor.
172+ function deviationToTicks (uint256 deviation ) public pure returns (int24 ) {
173+ // 2% ~ 200 ticks -> (POOL.tickSpacing())
174+ // NOTE: width can't be zero, we set the minimum possible to 200.
175+ uint256 t = deviation.mulDiv (10000 , ONE);
176+ t -= (t % 200 );
177+ return (t >= 200 ? SafeCast.toInt24 (SafeCast.toInt256 (t)) : int24 (200 ));
178+ }
179+
147180 /// @return Number of decimals representing 1.0.
148181 function decimals () external pure returns (uint8 ) {
149182 return uint8 (DECIMALS);
150183 }
184+
185+ /// @dev Configures the vault to provide concentrated liquidity in the active zone.
186+ function _setupActiveZoneLiq (uint256 deviation ) private {
187+ (uint256 usdcBal , ) = VAULT.getTotalAmounts ();
188+ uint256 fullRangeWeight = uint256 (VAULT_MAX_FRW).mulDiv (
189+ fullRangeMaxUsdcBal,
190+ usdcBal
191+ );
192+ uint256 limitBandDeviationWidth = isOverweightSpot ()
193+ ? activeZoneDeviation.upper - deviation
194+ : deviation - activeZoneDeviation.lower;
195+ VAULT.setFullRangeWeight (SafeCast.toUint24 (fullRangeWeight));
196+
197+ // IMPORTANT: The deviation percentage, to ticks conversion isn't precise.
198+ // In the worst case, there might be liquidity up to 2% outside the active zone definition.
199+ VAULT.setBaseThreshold (deviationToTicks (concBandDeviationWidth));
200+ VAULT.setLimitThreshold (deviationToTicks (limitBandDeviationWidth));
201+ }
202+
203+ /// @dev Resets the vault to provide full range liquidity.
204+ function _resetLiq () private {
205+ VAULT.setFullRangeWeight (VAULT_MAX_FRW);
206+ VAULT.setBaseThreshold (POOL_MAX_TICK);
207+ VAULT.setLimitThreshold (POOL_MAX_TICK);
208+ }
209+
210+ /// @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 ;
215+ }
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);
221+ }
151222}
0 commit comments