11// SPDX-License-Identifier: BUSL-1.1
2- // solhint-disable-next-line compiler-version
3- pragma solidity ^ 0.7.6 ;
4- pragma abicoder v2;
2+ pragma solidity ^ 0.8.24 ;
53
6- import { FullMath } from "@uniswap/v3-core/contracts/libraries/FullMath.sol " ;
7- import { TickMath } from "@uniswap/v3-core/contracts/libraries/TickMath.sol " ;
8- import { PositionKey } from "@uniswap/v3-periphery/contracts/libraries/PositionKey.sol " ;
4+ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol " ;
5+ import { AlphaVaultHelpers } from "../_utils/AlphaVaultHelpers.sol " ;
96
10- import { ISpotPricingStrategy } from "./_interfaces/ISpotPricingStrategy .sol " ;
11- import { IAlphaProVault } from "./_interfaces/external/IAlphaProVault.sol " ;
7+ import { IMetaOracle } from ".. /_interfaces/IMetaOracle .sol " ;
8+ import { IAlphaProVault } from ".. /_interfaces/external/IAlphaProVault.sol " ;
129import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol " ;
1310
1411/// @title UsdcSpotManager
1512/// @notice This contract is a programmatic manager for the USDC-SPOT Charm AlphaProVault.
16- contract UsdcSpotManager {
17- /// @dev Token Constants.
18- uint256 public constant ONE_SPOT = 1e9 ;
19- uint256 public constant ONE_USDC = 1e6 ;
13+ contract UsdcSpotManager is Ownable {
14+ //-------------------------------------------------------------------------
15+ // Libraries
16+ using AlphaVaultHelpers for IAlphaProVault;
17+
18+ //-------------------------------------------------------------------------
19+ // Constants & Immutables
2020
2121 /// @dev Decimals.
2222 uint256 public constant DECIMALS = 18 ;
2323 uint256 public constant ONE = (10 ** DECIMALS);
2424
25- /// @dev We bound the deviation factor to 100.0.
26- uint256 public constant MAX_DEVIATION = 100 * ONE; // 100.0
27-
28- //-------------------------------------------------------------------------
29- // Storage
30-
3125 /// @notice The USDC-SPOT charm alpha vault.
3226 IAlphaProVault public immutable VAULT;
3327
3428 /// @notice The underlying USDC-SPOT univ3 pool.
3529 IUniswapV3Pool public immutable POOL;
3630
37- /// @notice The vault's token0, the USDC token.
38- address public immutable USDC;
39-
40- /// @notice The vault's token1, the SPOT token.
41- address public immutable SPOT;
42-
43- /// @notice Pricing strategy to price the SPOT token.
44- ISpotPricingStrategy public pricingStrategy;
45-
46- /// @notice The contract owner.
47- address public owner;
48-
4931 //-------------------------------------------------------------------------
50- // Manager storage
32+ // Storage
33+
34+ /// @notice The meta oracle which returns prices of AMPL asset family.
35+ IMetaOracle public oracle;
5136
5237 /// @notice The recorded deviation factor at the time of the last successful rebalance operation.
5338 uint256 public prevDeviation;
5439
55- //--------------------------------------------------------------------------
56- // Modifiers
57-
58- modifier onlyOwner () {
59- // solhint-disable-next-line custom-errors
60- require (msg .sender == owner, "Unauthorized caller " );
61- _;
62- }
63-
6440 //-----------------------------------------------------------------------------
6541 // Constructor and Initializer
6642
6743 /// @notice Constructor initializes the contract with provided addresses.
6844 /// @param vault_ Address of the AlphaProVault contract.
69- /// @param pricingStrategy_ Address of the spot appraiser.
70- constructor (IAlphaProVault vault_ , ISpotPricingStrategy pricingStrategy_ ) {
71- owner = msg .sender ;
72-
45+ /// @param oracle_ Address of the MetaOracle contract.
46+ constructor (IAlphaProVault vault_ , IMetaOracle oracle_ ) Ownable () {
7347 VAULT = vault_;
7448 POOL = vault_.pool ();
75- USDC = vault_.token0 ();
76- SPOT = vault_.token1 ();
7749
78- pricingStrategy = pricingStrategy_ ;
79- // solhint-disable-next-line custom-errors
80- require (pricingStrategy. decimals () == DECIMALS, " Invalid decimals " ) ;
50+ updateOracle (oracle_) ;
51+
52+ prevDeviation = 0 ;
8153 }
8254
8355 //--------------------------------------------------------------------------
8456 // Owner only methods
8557
86- /// @notice Updates the owner role.
87- function transferOwnership (address owner_ ) external onlyOwner {
88- owner = owner_;
89- }
90-
91- /// @notice Updates the Spot pricing strategy reference.
92- function updatePricingStrategy (
93- ISpotPricingStrategy pricingStrategy_
94- ) external onlyOwner {
95- pricingStrategy = pricingStrategy_;
58+ /// @notice Updates the MetaOracle.
59+ function updateOracle (IMetaOracle oracle_ ) public onlyOwner {
60+ // solhint-disable-next-line custom-errors
61+ require (DECIMALS == oracle_.decimals (), "UnexpectedDecimals " );
62+ oracle = oracle_;
9663 }
9764
9865 /// @notice Updates the vault's liquidity range parameters.
@@ -116,7 +83,7 @@ contract UsdcSpotManager {
11683 // solhint-disable-next-line avoid-low-level-calls
11784 (bool success , bytes memory r ) = address (VAULT).call (callData);
11885 // solhint-disable-next-line custom-errors
119- require (success, "Vault call failed " );
86+ require (success, "VaultExecutionFailed " );
12087 return r;
12188 }
12289
@@ -125,107 +92,60 @@ contract UsdcSpotManager {
12592
12693 /// @notice Executes vault rebalance.
12794 function rebalance () public {
128- (uint256 deviation , bool deviationValid ) = computeDeviationFactor ();
129-
130- // We rebalance if the deviation factor has crossed ONE (in either direction).
131- bool forceLiquidityUpdate = ((deviation <= ONE && prevDeviation > ONE) ||
132- (deviation >= ONE && prevDeviation < ONE));
95+ (uint256 deviation , bool deviationValid ) = oracle.spotPriceDeviation ();
13396
13497 // Execute rebalance.
13598 // NOTE: the vault.rebalance() will revert if enough time has not elapsed.
13699 // We thus override with a force rebalance.
137100 // https://learn.charm.fi/charm/technical-references/core/alphaprovault#rebalance
138- forceLiquidityUpdate ? _execForceRebalance () : VAULT.rebalance ();
139-
140- // We only activate the limit range liquidity, when
141- // the vault sells SPOT and deviation is above ONE, or when
142- // the vault buys SPOT and deviation is below ONE
143- bool extraSpot = isOverweightSpot ();
144- bool activeLimitRange = deviationValid &&
145- ((deviation >= ONE && extraSpot) || (deviation <= ONE && ! extraSpot));
101+ (deviationValid && shouldForceRebalance (deviation, prevDeviation))
102+ ? VAULT.forceRebalance ()
103+ : VAULT.rebalance ();
146104
147105 // Trim positions after rebalance.
148- if (! activeLimitRange ) {
149- _removeLimitLiquidity ( );
106+ if (! deviationValid || shouldRemoveLimitRange (deviation) ) {
107+ VAULT. removeLimitLiquidity (POOL );
150108 }
151109
152- // Update rebalance state.
153- prevDeviation = deviation;
110+ // Update valid rebalance state.
111+ if (deviationValid) {
112+ prevDeviation = deviation;
113+ }
154114 }
155115
156- /// @notice Computes the deviation between SPOT's market price and it's FMV price.
157- /// @return The computed deviation factor.
158- function computeDeviationFactor () public returns (uint256 , bool ) {
159- uint256 spotMarketPrice = getSpotUSDPrice ();
160- (uint256 spotTargetPrice , bool spotTargetPriceValid ) = pricingStrategy
161- .perpPrice ();
162- (, bool usdcPriceValid ) = pricingStrategy.usdPrice ();
163- bool deviationValid = (spotTargetPriceValid && usdcPriceValid);
164- uint256 deviation = spotTargetPrice > 0
165- ? FullMath.mulDiv (spotMarketPrice, ONE, spotTargetPrice)
166- : type (uint256 ).max;
167- deviation = (deviation > MAX_DEVIATION) ? MAX_DEVIATION : deviation;
168- return (deviation, deviationValid);
116+ //-----------------------------------------------------------------------------
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));
169127 }
170128
171- //-----------------------------------------------------------------------------
172- // External Public view methods
173-
174- /// @return The computed SPOT price in USD from the underlying univ3 pool.
175- function getSpotUSDPrice () public view returns ( uint256 ) {
176- uint160 sqrtPriceX96 = TickMath. getSqrtRatioAtTick (VAULT. getTwap () );
177- uint256 ratioX192 = uint256 (sqrtPriceX96) * sqrtPriceX96;
178- uint256 usdcPerSpot = FullMath. mulDiv (ONE, ( 1 << 192 ), ratioX192 );
179- return FullMath. mulDiv (usdcPerSpot, ONE_SPOT, ONE_USDC );
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 );
180138 }
181139
182- /// @notice Checks the vault is overweight SPOT, and looking to sell the extra SPOT for USDC.
140+ /// @notice Checks the vault is overweight SPOT and looking to sell the extra SPOT for USDC.
183141 function isOverweightSpot () public view returns (bool ) {
184- // NOTE: This assumes that in the underlying univ3 pool and
185- // token0 is USDC and token1 is SPOT.
186- int24 _marketPrice = VAULT.getTwap ();
187- int24 _limitLower = VAULT.limitLower ();
188- int24 _limitUpper = VAULT.limitUpper ();
189- int24 _limitPrice = (_limitLower + _limitUpper) / 2 ;
190- // The limit range has more token1 than token0 if `_marketPrice >= _limitPrice`,
191- // so the vault looks to sell token1.
192- return (_marketPrice >= _limitPrice);
142+ // NOTE: In the underlying univ3 pool and token0 is USDC and token1 is SPOT.
143+ // Underweight Token0 implies that the limit range has less USDC and more SPOT.
144+ return VAULT.isUnderweightToken0 ();
193145 }
194146
195147 /// @return Number of decimals representing 1.0.
196148 function decimals () external pure returns (uint8 ) {
197149 return uint8 (DECIMALS);
198150 }
199-
200- //-----------------------------------------------------------------------------
201- // Private methods
202-
203- /// @dev A low-level method, which interacts directly with the vault and executes
204- /// a rebalance even when enough time hasn't elapsed since the last rebalance.
205- function _execForceRebalance () private {
206- uint32 _period = VAULT.period ();
207- VAULT.setPeriod (0 );
208- VAULT.rebalance ();
209- VAULT.setPeriod (_period);
210- }
211-
212- /// @dev Removes the vault's limit range liquidity.
213- /// To be invoked right after a rebalance operation, as it assumes that
214- /// the vault has a active limit range liquidity.
215- function _removeLimitLiquidity () private {
216- int24 _limitLower = VAULT.limitLower ();
217- int24 _limitUpper = VAULT.limitUpper ();
218- (uint128 limitLiquidity , , , , ) = _position (_limitLower, _limitUpper);
219- // docs: https://learn.charm.fi/charm/technical-references/core/alphaprovault#emergencyburn
220- VAULT.emergencyBurn (_limitLower, _limitUpper, limitLiquidity);
221- }
222-
223- /// @dev Wrapper around `IUniswapV3Pool.positions()`.
224- function _position (
225- int24 tickLower ,
226- int24 tickUpper
227- ) private view returns (uint128 , uint256 , uint256 , uint128 , uint128 ) {
228- bytes32 positionKey = PositionKey.compute (address (VAULT), tickLower, tickUpper);
229- return POOL.positions (positionKey);
230- }
231151}
0 commit comments