Skip to content

Commit 46ab8dd

Browse files
committed
Scalable usdc spot strategy
Apply suggestions from code review Co-authored-by: Brandon Iles <brandon@fragments.org> review fix
1 parent 5f9abfc commit 46ab8dd

File tree

3 files changed

+377
-151
lines changed

3 files changed

+377
-151
lines changed

spot-vaults/contracts/_utils/AlphaVaultHelpers.sol

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ library AlphaVaultHelpers {
4444
if (percToRemove <= 0) {
4545
return;
4646
}
47-
4847
int24 _fullLower = vault.fullLower();
4948
int24 _fullUpper = vault.fullUpper();
5049
int24 _baseLower = vault.baseLower();
@@ -61,8 +60,12 @@ library AlphaVaultHelpers {
6160
);
6261
// docs: https://learn.charm.fi/charm/technical-references/core/alphaprovault#emergencyburn
6362
// We remove the calculated percentage of base and full range liquidity.
64-
vault.emergencyBurn(_fullLower, _fullUpper, fullLiquidityToBurn);
65-
vault.emergencyBurn(_baseLower, _baseUpper, baseLiquidityToBurn);
63+
if (fullLiquidityToBurn > 0) {
64+
vault.emergencyBurn(_fullLower, _fullUpper, fullLiquidityToBurn);
65+
}
66+
if (baseLiquidityToBurn > 0) {
67+
vault.emergencyBurn(_baseLower, _baseUpper, baseLiquidityToBurn);
68+
}
6669
}
6770

6871
/// @dev A low-level method, which interacts directly with the vault and executes
Lines changed: 121 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,32 @@
11
// SPDX-License-Identifier: BUSL-1.1
22
pragma 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";
46
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
57
import { AlphaVaultHelpers } from "../_utils/AlphaVaultHelpers.sol";
8+
import { Range } from "../_interfaces/types/CommonTypes.sol";
69

710
import { IMetaOracle } from "../_interfaces/IMetaOracle.sol";
811
import { IAlphaProVault } from "../_interfaces/external/IAlphaProVault.sol";
912
import { 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+
///
1325
contract 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

Comments
 (0)