Skip to content

Commit 654ffc5

Browse files
Charm Vault manager update (#217)
* updated wampl eth manager * added USDC spot manager * updated vault unit tests * code review fixes * Update spot-vaults/contracts/WethWamplManager.sol Co-authored-by: Brandon Iles <[email protected]> * deployed new manager * updated info task --------- Co-authored-by: Brandon Iles <[email protected]>
1 parent 01f32a6 commit 654ffc5

File tree

10 files changed

+1481
-383
lines changed

10 files changed

+1481
-383
lines changed

spot-vaults/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The official mainnet addresses are:
66

77
- Bill Broker (SPOT-USDC): [0xA088Aef966CAD7fE0B38e28c2E07590127Ab4ccB](https://etherscan.io/address/0xA088Aef966CAD7fE0B38e28c2E07590127Ab4ccB)
88
- SpotAppraiser: [0x965FBFebDA76d9AA11642C1d0074CdF02e546F3c](https://etherscan.io/address/0x965FBFebDA76d9AA11642C1d0074CdF02e546F3c)
9+
- WethWamplManager: [0x6785fa26191eb531c54fd093931f395c4b01b583](https://etherscan.io/address/0x6785fa26191eb531c54fd093931f395c4b01b583)
910

1011
The official testnet addresses are:
1112

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
// solhint-disable-next-line compiler-version
3+
pragma solidity ^0.7.6;
4+
pragma abicoder v2;
5+
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";
9+
10+
import { IBillBrokerPricingStrategy } from "./_interfaces/IBillBrokerPricingStrategy.sol";
11+
import { IAlphaProVault } from "./_interfaces/external/IAlphaProVault.sol";
12+
import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
13+
14+
/// @title UsdcSpotManager
15+
/// @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;
20+
21+
/// @dev Decimals.
22+
uint256 public constant DECIMALS = 18;
23+
uint256 public constant ONE = (10 ** DECIMALS);
24+
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+
31+
/// @notice The USDC-SPOT charm alpha vault.
32+
IAlphaProVault public immutable VAULT;
33+
34+
/// @notice The underlying USDC-SPOT univ3 pool.
35+
IUniswapV3Pool public immutable POOL;
36+
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+
IBillBrokerPricingStrategy public spotAppraiser;
45+
46+
/// @notice The contract owner.
47+
address public owner;
48+
49+
//-------------------------------------------------------------------------
50+
// Manager storage
51+
52+
/// @notice The recorded deviation factor at the time of the last successful rebalance operation.
53+
uint256 public prevDeviation;
54+
55+
//--------------------------------------------------------------------------
56+
// Modifiers
57+
58+
modifier onlyOwner() {
59+
// solhint-disable-next-line custom-errors
60+
require(msg.sender == owner, "Unauthorized caller");
61+
_;
62+
}
63+
64+
//-----------------------------------------------------------------------------
65+
// Constructor and Initializer
66+
67+
/// @notice Constructor initializes the contract with provided addresses.
68+
/// @param vault_ Address of the AlphaProVault contract.
69+
/// @param spotAppraiser_ Address of the spot appraiser.
70+
constructor(IAlphaProVault vault_, IBillBrokerPricingStrategy spotAppraiser_) {
71+
owner = msg.sender;
72+
73+
VAULT = vault_;
74+
POOL = vault_.pool();
75+
USDC = vault_.token0();
76+
SPOT = vault_.token1();
77+
78+
spotAppraiser = spotAppraiser_;
79+
// solhint-disable-next-line custom-errors
80+
require(spotAppraiser.decimals() == DECIMALS, "Invalid decimals");
81+
}
82+
83+
//--------------------------------------------------------------------------
84+
// Owner only methods
85+
86+
/// @notice Updates the owner role.
87+
function transferOwnership(address owner_) external onlyOwner {
88+
owner = owner_;
89+
}
90+
91+
/// @notice Updates the Spot Appraiser reference.
92+
function setSpotAppraiser(
93+
IBillBrokerPricingStrategy spotAppraiser_
94+
) external onlyOwner {
95+
spotAppraiser = spotAppraiser_;
96+
}
97+
98+
/// @notice Updates the vault's liquidity range parameters.
99+
function setLiquidityRanges(
100+
int24 baseThreshold,
101+
uint24 fullRangeWeight,
102+
int24 limitThreshold
103+
) external onlyOwner {
104+
// Update liquidity parameters on the vault.
105+
VAULT.setBaseThreshold(baseThreshold);
106+
VAULT.setFullRangeWeight(fullRangeWeight);
107+
VAULT.setLimitThreshold(limitThreshold);
108+
}
109+
110+
/// @notice Forwards the given calldata to the vault.
111+
/// @param callData The calldata to pass to the vault.
112+
/// @return The data returned by the vault method call.
113+
function execOnVault(
114+
bytes calldata callData
115+
) external onlyOwner returns (bytes memory) {
116+
// solhint-disable-next-line avoid-low-level-calls
117+
(bool success, bytes memory r) = address(VAULT).call(callData);
118+
// solhint-disable-next-line custom-errors
119+
require(success, "Vault call failed");
120+
return r;
121+
}
122+
123+
//--------------------------------------------------------------------------
124+
// External write methods
125+
126+
/// @notice Executes vault rebalance.
127+
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));
133+
134+
// Execute rebalance.
135+
// NOTE: the vault.rebalance() will revert if enough time has not elapsed.
136+
// We thus override with a force rebalance.
137+
// 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));
146+
147+
// Trim positions after rebalance.
148+
if (!activeLimitRange) {
149+
_removeLimitLiquidity();
150+
}
151+
152+
// Update rebalance state.
153+
prevDeviation = deviation;
154+
}
155+
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) = spotAppraiser.perpPrice();
161+
(, bool usdcPriceValid) = spotAppraiser.usdPrice();
162+
bool deviationValid = (spotTargetPriceValid && usdcPriceValid);
163+
uint256 deviation = spotTargetPrice > 0
164+
? FullMath.mulDiv(spotMarketPrice, ONE, spotTargetPrice)
165+
: type(uint256).max;
166+
deviation = (deviation > MAX_DEVIATION) ? MAX_DEVIATION : deviation;
167+
return (deviation, deviationValid);
168+
}
169+
170+
//-----------------------------------------------------------------------------
171+
// External Public view methods
172+
173+
/// @return The computed SPOT price in USD from the underlying univ3 pool.
174+
function getSpotUSDPrice() public view returns (uint256) {
175+
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(VAULT.getTwap());
176+
uint256 ratioX192 = uint256(sqrtPriceX96) * sqrtPriceX96;
177+
uint256 spotPerUsdc = FullMath.mulDiv(ONE, ratioX192, (1 << 192));
178+
return FullMath.mulDiv(spotPerUsdc, ONE_USDC, ONE_SPOT);
179+
}
180+
181+
/// @notice Checks the vault is overweight SPOT, and looking to sell the extra SPOT for USDC.
182+
function isOverweightSpot() public view returns (bool) {
183+
// NOTE: This assumes that in the underlying univ3 pool and
184+
// token0 is USDC and token1 is SPOT.
185+
int24 _marketPrice = VAULT.getTwap();
186+
int24 _limitLower = VAULT.limitLower();
187+
int24 _limitUpper = VAULT.limitUpper();
188+
int24 _limitPrice = (_limitLower + _limitUpper) / 2;
189+
// The limit range has more token1 than token0 if `_marketPrice >= _limitPrice`,
190+
// so the vault looks to sell token1.
191+
return (_marketPrice >= _limitPrice);
192+
}
193+
194+
/// @return Number of decimals representing 1.0.
195+
function decimals() external pure returns (uint8) {
196+
return uint8(DECIMALS);
197+
}
198+
199+
//-----------------------------------------------------------------------------
200+
// Private methods
201+
202+
/// @dev A low-level method, which interacts directly with the vault and executes
203+
/// a rebalance even when enough time hasn't elapsed since the last rebalance.
204+
function _execForceRebalance() private {
205+
uint32 _period = VAULT.period();
206+
VAULT.setPeriod(0);
207+
VAULT.rebalance();
208+
VAULT.setPeriod(_period);
209+
}
210+
211+
/// @dev Removes the vault's limit range liquidity.
212+
/// To be invoked right after a rebalance operation, as it assumes that
213+
/// the vault has a active limit range liquidity.
214+
function _removeLimitLiquidity() private {
215+
int24 _limitLower = VAULT.limitLower();
216+
int24 _limitUpper = VAULT.limitUpper();
217+
(uint128 limitLiquidity, , , , ) = _position(_limitLower, _limitUpper);
218+
// docs: https://learn.charm.fi/charm/technical-references/core/alphaprovault#emergencyburn
219+
VAULT.emergencyBurn(_limitLower, _limitUpper, limitLiquidity);
220+
}
221+
222+
/// @dev Wrapper around `IUniswapV3Pool.positions()`.
223+
function _position(
224+
int24 tickLower,
225+
int24 tickUpper
226+
) private view returns (uint128, uint256, uint256, uint128, uint128) {
227+
bytes32 positionKey = PositionKey.compute(address(VAULT), tickLower, tickUpper);
228+
return POOL.positions(positionKey);
229+
}
230+
}

0 commit comments

Comments
 (0)