@shift-defi/strategies 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,32 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ interface ICompoundV3Supply {
5
+ struct InternalHarvestLocalVars {
6
+ address depositToken;
7
+ address cToken;
8
+ uint256 cTokenDecimalsFactor;
9
+ uint256 dTokenDecimalsFactor;
10
+ uint256 cTokenIncome;
11
+ uint256 cTokenIncomeInAsset;
12
+ uint256 beforeBalance;
13
+ uint256 compRewardsInAsset;
14
+ uint256 treasuryFee;
15
+ uint256 missingFeeInAsset;
16
+ uint256 cTokensToWithdraw;
17
+ uint256 currentCTokenBalance;
18
+ uint256 incomeShare;
19
+ }
20
+
21
+ struct InternalEnterLocalVars {
22
+ uint256 balance;
23
+ address depositToken;
24
+ address cToken;
25
+ }
26
+
27
+ struct InternalExitLocalVars {
28
+ uint256 balance;
29
+ uint256 liquidity;
30
+ address cToken;
31
+ }
32
+ }
@@ -0,0 +1,35 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ interface ICurveGauge {
5
+ struct AutomaticHarvestLocalVars {
6
+ address gaugeCached;
7
+ address lpTokenCached;
8
+ address asset0Cached;
9
+ address asset1Cached;
10
+ uint256 lastStoredGaugeBalance;
11
+ uint256 lastStoredVirtualPrice;
12
+ uint256 currentGaugeBalance;
13
+ uint256 currentVirtualPrice;
14
+ uint256 accruedLpValue;
15
+ uint256 gaugeTokensToTreasury;
16
+ uint256 asset0BalanceBefore;
17
+ uint256 asset1BalanceBefore;
18
+ uint256 asset0Rewards;
19
+ uint256 asset1Rewards;
20
+ uint256 reinvestGaugeDelta;
21
+ uint256 feeFromReinvest;
22
+ }
23
+
24
+ function gauge() external view returns (address);
25
+
26
+ function lpToken() external view returns (address);
27
+
28
+ function underlyingAsset0() external view returns (address);
29
+
30
+ function underlyingAsset1() external view returns (address);
31
+
32
+ function lastStoredVirtualPrice() external view returns (uint256);
33
+
34
+ function lastStoredGaugeBalance() external view returns (uint256);
35
+ }
@@ -0,0 +1,26 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ interface IFluidSupply {
5
+ struct AutomaticHarvestLocalVars {
6
+ address fTokenCached;
7
+ address merkleRewardToken;
8
+ uint256 accruedAssetsValue;
9
+ uint256 accruedFTokenAmount;
10
+ uint256 fTokensToTreasury;
11
+ uint256 fTokensAmountBefore;
12
+ uint256 reinvestFTokenDelta;
13
+ uint256 feeFromReinvest;
14
+ }
15
+
16
+ struct ClaimParams {
17
+ uint256 cumulativeAmount;
18
+ uint256 cycle;
19
+ bytes32[] merkleProof;
20
+ bytes metadata;
21
+ }
22
+
23
+ /// @notice Claims rewards from Fluid Merkle distributor
24
+ /// @param claimParams The parameters for the claim operation
25
+ function manualClaim(ClaimParams calldata claimParams) external;
26
+ }
@@ -0,0 +1,63 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ interface IMorphoVault {
5
+ // ---- Structs ----
6
+
7
+ struct AutomaticHarvestLocalVars {
8
+ address morphoVaultCached;
9
+ address underlyingAssetCached;
10
+ uint256 rewardTokensLength;
11
+ uint256 accruedAssetsValue;
12
+ uint256 accruedVaultTokens;
13
+ uint256 vaultTokensToTreasury;
14
+ uint256 lpAmountBefore;
15
+ uint256 reinvestLpDelta;
16
+ uint256 feeFromReinvest;
17
+ uint256 currentAssetsValue;
18
+ }
19
+
20
+ struct ManualClaimLocalVars {
21
+ address morphoVaultCached;
22
+ address underlyingAssetCached;
23
+ address strategyContainerCached;
24
+ uint256 rewardTokensLength;
25
+ address treasury;
26
+ uint256 feePct;
27
+ uint256 accruedAssetsValue;
28
+ uint256 accruedVaultTokens;
29
+ uint256 vaultTokensToTreasury;
30
+ uint256 lpAmountBefore;
31
+ address[] users;
32
+ uint256 reinvestLpDelta;
33
+ uint256 feeFromReinvest;
34
+ uint256 currentAssetsValue;
35
+ }
36
+
37
+ // ---- Events ----
38
+
39
+ event RewardTokensUpdated(address[] rewardTokens);
40
+
41
+ // ---- Errors ----
42
+
43
+ error RewardTokenMatchesUnderlyingAsset();
44
+ error RewardTokenMatchesMorphoVault();
45
+
46
+ // ---- Functions ----
47
+
48
+ /// @notice Returns the reward tokens
49
+ /// @return The addresses of the reward tokens
50
+ function getRewardTokens() external view returns (address[] memory);
51
+
52
+ /// @notice Sets the reward tokens. Only callabe by role HARVEST_MANAGER_ROLE
53
+ /// @param _rewardTokens The addresses of the reward tokens
54
+ function setRewardTokens(address[] memory _rewardTokens) external;
55
+
56
+ /// @notice Manually claims the rewards. Only callable by role MERKLE_CLAIMER_ROLE
57
+ /// @dev Underlying asset claimed via manual claim is instantly reinvested,
58
+ /// other reward tokens are reinvested during standard harvest
59
+ /// @param tokens The addresses of the tokens
60
+ /// @param amounts The amounts of the tokens
61
+ /// @param proofs The proofs of the merkle proofs
62
+ function manualClaim(address[] calldata tokens, uint256[] calldata amounts, bytes32[][] calldata proofs) external;
63
+ }
@@ -0,0 +1,340 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
5
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6
+ import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
7
+ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
8
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
9
+
10
+ import {IStrategyContainer} from "@shift-defi/core/interfaces/IStrategyContainer.sol";
11
+ import {StrategyTemplate} from "@shift-defi/core/StrategyTemplate.sol";
12
+ import {Errors} from "@shift-defi/core/libraries/Errors.sol";
13
+
14
+ import {IMorphoVault} from "../interfaces/IMorphoVault.sol";
15
+
16
+ import {IAngleMerkleDistributor} from "../dependencies/angle/IAngleMerkleDistributor.sol";
17
+
18
+ contract MorphoVault is AccessControlUpgradeable, StrategyTemplate, IMorphoVault {
19
+ using Math for uint256;
20
+ using SafeERC20 for IERC20;
21
+
22
+ /// @notice State ID for the Morpho vault state (morpho vault)
23
+ bytes32 internal constant MORPHO_VAULT_STATE_ID = keccak256("MORPHO_VAULT_STATE_ID");
24
+
25
+ /// @notice State ID for the underlying asset state (holding the asset directly)
26
+ bytes32 internal constant UNDERLYING_ASSET_STATE_ID = keccak256("UNDERLYING_ASSET_STATE_ID");
27
+
28
+ /// @notice Role for the Merkle claimer
29
+ bytes32 private constant MERKLE_CLAIMER_ROLE = keccak256("MERKLE_CLAIMER_ROLE");
30
+
31
+ /// @notice The Morpho vault address
32
+ address public morphoVault;
33
+
34
+ /// @notice The underlying asset address
35
+ address public underlyingAsset;
36
+
37
+ /// @notice The address of the Morpho Merkle distributor contract
38
+ address public merkleDistributor;
39
+
40
+ /// @notice The addresses of the reward tokens
41
+ address[] internal rewardTokens;
42
+
43
+ /// @notice The last recorded value of vault token amount * share price
44
+ uint256 public lastAssetsValue;
45
+
46
+ struct SlippageParams {
47
+ uint256 enterMaxSlippage;
48
+ uint256 exitMaxSlippage;
49
+ uint256 emergencyExitMaxSlippage;
50
+ }
51
+
52
+ constructor() {
53
+ _disableInitializers();
54
+ }
55
+
56
+ /// @notice Initializes the MorphoVaultStrategy contract
57
+ /// @dev Sets up the Morpho vault, underlying asset, reward tokens, and configures the strategy states
58
+ /// @param strategyContainer The address of the strategy container contract
59
+ /// @param defaultAdmin The address of the default admin
60
+ /// @param merkleClaimer The address of the Merkle claimer
61
+ /// @param _morphoVault The address of the Morpho vault
62
+ /// @param _merkleDistributor The address of the Merkle distributor
63
+ /// @param _rewardTokens The addresses of the reward tokens
64
+ /// @param slippageParams The slippage parameters
65
+
66
+ function initialize(
67
+ address strategyContainer,
68
+ address defaultAdmin,
69
+ address merkleClaimer,
70
+ address _morphoVault,
71
+ address _merkleDistributor,
72
+ address[] calldata _rewardTokens,
73
+ SlippageParams calldata slippageParams
74
+ ) external initializer {
75
+ __AccessControl_init();
76
+
77
+ __StrategyTemplate_init(
78
+ strategyContainer,
79
+ slippageParams.enterMaxSlippage,
80
+ slippageParams.exitMaxSlippage,
81
+ slippageParams.emergencyExitMaxSlippage
82
+ );
83
+
84
+ require(defaultAdmin != address(0), Errors.ZeroAddress());
85
+ _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
86
+ require(merkleClaimer != address(0), Errors.ZeroAddress());
87
+ _grantRole(MERKLE_CLAIMER_ROLE, merkleClaimer);
88
+
89
+ require(_morphoVault != address(0), Errors.ZeroAddress());
90
+ morphoVault = _morphoVault;
91
+ underlyingAsset = IERC4626(morphoVault).asset();
92
+
93
+ require(_merkleDistributor != address(0), Errors.ZeroAddress());
94
+ merkleDistributor = _merkleDistributor;
95
+
96
+ _setRewardTokens(_rewardTokens);
97
+
98
+ _setState(UNDERLYING_ASSET_STATE_ID, false, false, true, 0);
99
+ _setState(MORPHO_VAULT_STATE_ID, true, true, false, 1);
100
+ }
101
+
102
+ /// @inheritdoc IMorphoVault
103
+ function getRewardTokens() external view override returns (address[] memory) {
104
+ return rewardTokens;
105
+ }
106
+
107
+ /// @inheritdoc IMorphoVault
108
+ function setRewardTokens(address[] memory _rewardTokens) external onlyStrategyContainerOrHarvestManager {
109
+ _setRewardTokens(_rewardTokens);
110
+ }
111
+
112
+ function _setRewardTokens(address[] memory _rewardTokens) private {
113
+ uint256 rewardTokensLength = _rewardTokens.length;
114
+ address underlyingAssetCached = underlyingAsset;
115
+ address morphoVaultCached = morphoVault;
116
+
117
+ for (uint256 i = 0; i < rewardTokensLength; ++i) {
118
+ require(_rewardTokens[i] != address(0), Errors.ZeroAddress());
119
+ require(_rewardTokens[i] != underlyingAssetCached, RewardTokenMatchesUnderlyingAsset());
120
+ require(_rewardTokens[i] != morphoVaultCached, RewardTokenMatchesMorphoVault());
121
+ }
122
+
123
+ rewardTokens = _rewardTokens;
124
+ emit RewardTokensUpdated(_rewardTokens);
125
+ }
126
+
127
+ function stateNav(bytes32 stateId) public view override returns (uint256) {
128
+ if (stateId == UNDERLYING_ASSET_STATE_ID) {
129
+ return _underlyingAssetNav();
130
+ } else if (stateId == MORPHO_VAULT_STATE_ID) {
131
+ return _morphoVaultNav();
132
+ } else if (stateId == NO_ALLOCATION_STATE_ID) {
133
+ return 0;
134
+ } else {
135
+ revert StateNotFound(stateId);
136
+ }
137
+ }
138
+
139
+ function _underlyingAssetNav() private view returns (uint256) {
140
+ address underlyingAssetCached = underlyingAsset;
141
+ return getTokenAmountInNotion(underlyingAssetCached, IERC20(underlyingAssetCached).balanceOf(address(this)));
142
+ }
143
+
144
+ function _morphoVaultNav() private view returns (uint256) {
145
+ address morphoVaultCached = morphoVault;
146
+ uint256 lpAmount = IERC4626(morphoVaultCached).balanceOf(address(this));
147
+ uint256 underlyingAssetAmount = IERC4626(morphoVaultCached).convertToAssets(lpAmount);
148
+ return getTokenAmountInNotion(underlyingAsset, underlyingAssetAmount);
149
+ }
150
+
151
+ function _enterTarget() internal override {
152
+ _enterMorphoVault();
153
+ }
154
+
155
+ function _enterState(bytes32 stateId) internal override {
156
+ if (stateId == MORPHO_VAULT_STATE_ID) {
157
+ _enterMorphoVault();
158
+ } else {
159
+ revert StateNotFound(stateId);
160
+ }
161
+ }
162
+
163
+ function _enterMorphoVault() private {
164
+ address underlyingAssetCached = underlyingAsset;
165
+ address morphoVaultCached = morphoVault;
166
+
167
+ uint256 enterAmount = IERC20(underlyingAssetCached).balanceOf(address(this));
168
+
169
+ if (enterAmount == 0) {
170
+ return;
171
+ }
172
+
173
+ IERC20(underlyingAssetCached).safeIncreaseAllowance(morphoVaultCached, enterAmount);
174
+ IERC4626(morphoVaultCached).deposit(enterAmount, address(this));
175
+
176
+ lastAssetsValue = IERC4626(morphoVaultCached).convertToAssets(
177
+ IERC4626(morphoVaultCached).balanceOf(address(this))
178
+ );
179
+ }
180
+
181
+ function _exitTarget(uint256 share) internal override {
182
+ _exitMorphoVault(share);
183
+ }
184
+
185
+ function _exitFromState(bytes32 stateId, uint256 share) internal override {
186
+ if (stateId == MORPHO_VAULT_STATE_ID) {
187
+ _exitMorphoVault(share);
188
+ } else {
189
+ revert StateNotFound(stateId);
190
+ }
191
+ }
192
+
193
+ function _exitMorphoVault(uint256 share) private {
194
+ address morphoVaultCached = morphoVault;
195
+
196
+ uint256 lpAmount = IERC4626(morphoVaultCached).balanceOf(address(this));
197
+ uint256 lpToWithdraw = lpAmount.mulDiv(share, MAX_BPS);
198
+
199
+ if (lpToWithdraw == 0) {
200
+ return;
201
+ }
202
+
203
+ IERC4626(morphoVaultCached).redeem(lpToWithdraw, address(this), address(this));
204
+
205
+ lastAssetsValue = IERC4626(morphoVaultCached).convertToAssets(
206
+ IERC4626(morphoVaultCached).balanceOf(address(this))
207
+ );
208
+ }
209
+
210
+ function _emergencyExit(bytes32 toStateId, uint256 share) internal override {
211
+ if (toStateId == UNDERLYING_ASSET_STATE_ID) {
212
+ _exitMorphoVault(share);
213
+ } else {
214
+ revert StateNotFound(toStateId);
215
+ }
216
+ }
217
+
218
+ function _calculateAccruedAssetsValue() private view returns (uint256) {
219
+ address morphoVaultCached = morphoVault;
220
+ uint256 currentAssetsValue = IERC4626(morphoVaultCached).convertToAssets(
221
+ IERC4626(morphoVaultCached).balanceOf(address(this))
222
+ );
223
+ uint256 lastAssetsValueCached = lastAssetsValue;
224
+
225
+ if (currentAssetsValue > lastAssetsValueCached) {
226
+ return currentAssetsValue - lastAssetsValueCached;
227
+ }
228
+
229
+ return 0;
230
+ }
231
+
232
+ function _harvest(bytes32 _stateId, address _treasury, uint256 _feePct) internal override {
233
+ AutomaticHarvestLocalVars memory vars;
234
+
235
+ vars.morphoVaultCached = morphoVault;
236
+ vars.underlyingAssetCached = underlyingAsset;
237
+ vars.rewardTokensLength = rewardTokens.length;
238
+
239
+ vars.accruedAssetsValue = _calculateAccruedAssetsValue();
240
+
241
+ if (vars.accruedAssetsValue > 0) {
242
+ vars.accruedVaultTokens = IERC4626(vars.morphoVaultCached).convertToShares(vars.accruedAssetsValue);
243
+ vars.vaultTokensToTreasury = vars.accruedVaultTokens.mulDiv(_feePct, MAX_BPS);
244
+ }
245
+
246
+ if (_stateId == MORPHO_VAULT_STATE_ID) {
247
+ for (uint256 i = 0; i < vars.rewardTokensLength; i++) {
248
+ _swapToInputTokens(rewardTokens[i], vars.underlyingAssetCached, 0, false);
249
+ }
250
+
251
+ vars.lpAmountBefore = IERC4626(vars.morphoVaultCached).balanceOf(address(this));
252
+ _enterMorphoVault();
253
+ vars.reinvestLpDelta = IERC4626(vars.morphoVaultCached).balanceOf(address(this)) - vars.lpAmountBefore;
254
+
255
+ if (vars.reinvestLpDelta > 0) {
256
+ vars.feeFromReinvest = vars.reinvestLpDelta.mulDiv(_feePct, MAX_BPS);
257
+ if (vars.feeFromReinvest > 0) {
258
+ vars.vaultTokensToTreasury += vars.feeFromReinvest;
259
+ }
260
+ }
261
+ }
262
+
263
+ if (vars.vaultTokensToTreasury > 0) {
264
+ IERC20(vars.morphoVaultCached).safeTransfer(_treasury, vars.vaultTokensToTreasury);
265
+ }
266
+
267
+ vars.currentAssetsValue = IERC4626(vars.morphoVaultCached).convertToAssets(
268
+ IERC4626(vars.morphoVaultCached).balanceOf(address(this))
269
+ );
270
+
271
+ if (vars.currentAssetsValue > lastAssetsValue) {
272
+ lastAssetsValue = vars.currentAssetsValue;
273
+ }
274
+ }
275
+
276
+ /// @inheritdoc IMorphoVault
277
+ function manualClaim(
278
+ address[] calldata tokens,
279
+ uint256[] calldata amounts,
280
+ bytes32[][] calldata proofs
281
+ ) external nonReentrant onlyRole(MERKLE_CLAIMER_ROLE) {
282
+ require(!isNavResolutionMode(), NavResolutionModeActivated());
283
+
284
+ ManualClaimLocalVars memory vars;
285
+
286
+ vars.morphoVaultCached = morphoVault;
287
+ vars.underlyingAssetCached = underlyingAsset;
288
+ vars.strategyContainerCached = _strategyContainer;
289
+
290
+ vars.rewardTokensLength = tokens.length;
291
+
292
+ vars.treasury = IStrategyContainer(vars.strategyContainerCached).treasury();
293
+ require(vars.treasury != address(0), Errors.ZeroAddress());
294
+
295
+ vars.feePct = IStrategyContainer(vars.strategyContainerCached).feePct();
296
+
297
+ vars.accruedAssetsValue = _calculateAccruedAssetsValue();
298
+ if (vars.accruedAssetsValue > 0) {
299
+ vars.accruedVaultTokens = IERC4626(vars.morphoVaultCached).convertToShares(vars.accruedAssetsValue);
300
+ vars.vaultTokensToTreasury = vars.accruedVaultTokens.mulDiv(vars.feePct, MAX_BPS);
301
+ }
302
+
303
+ vars.lpAmountBefore = IERC4626(vars.morphoVaultCached).balanceOf(address(this));
304
+
305
+ vars.users = new address[](1);
306
+ vars.users[0] = address(this);
307
+
308
+ IAngleMerkleDistributor(merkleDistributor).claim(vars.users, tokens, amounts, proofs);
309
+
310
+ if (currentStateId() == MORPHO_VAULT_STATE_ID) {
311
+ for (uint256 i = 0; i < vars.rewardTokensLength; i++) {
312
+ if (tokens[i] == vars.underlyingAssetCached) {
313
+ _enterMorphoVault();
314
+ break;
315
+ }
316
+ }
317
+
318
+ vars.reinvestLpDelta = IERC4626(vars.morphoVaultCached).balanceOf(address(this)) - vars.lpAmountBefore;
319
+
320
+ if (vars.reinvestLpDelta > 0) {
321
+ vars.feeFromReinvest = vars.reinvestLpDelta.mulDiv(vars.feePct, MAX_BPS);
322
+ if (vars.feeFromReinvest > 0) {
323
+ vars.vaultTokensToTreasury += vars.feeFromReinvest;
324
+ }
325
+ }
326
+ }
327
+
328
+ if (vars.vaultTokensToTreasury > 0) {
329
+ IERC20(vars.morphoVaultCached).safeTransfer(vars.treasury, vars.vaultTokensToTreasury);
330
+ }
331
+
332
+ vars.currentAssetsValue = IERC4626(vars.morphoVaultCached).convertToAssets(
333
+ IERC4626(vars.morphoVaultCached).balanceOf(address(this))
334
+ );
335
+
336
+ if (vars.currentAssetsValue > lastAssetsValue) {
337
+ lastAssetsValue = vars.currentAssetsValue;
338
+ }
339
+ }
340
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@shift-defi/strategies",
3
+ "version": "0.1.0",
4
+ "description": "Strategy smart contracts for Shift DeFi Platform",
5
+ "author": "ShiftDeFi",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/ShiftDeFi/shift-defi-strategies"
10
+ },
11
+ "homepage": "https://shiftdefi.com/",
12
+ "files": [
13
+ "contracts/**/*.sol"
14
+ ],
15
+ "scripts": {
16
+ "prepare": "husky"
17
+ },
18
+ "dependencies": {
19
+ "@shift-defi/core": "^0.7.0"
20
+ },
21
+ "devDependencies": {
22
+ "@commitlint/cli": "^20.2.0",
23
+ "@commitlint/config-conventional": "^20.2.0",
24
+ "husky": "^9.1.7",
25
+ "lint-staged": "^16.2.7",
26
+ "prettier": "^3.7.4",
27
+ "prettier-plugin-solidity": "^2.2.1"
28
+ },
29
+ "lint-staged": {
30
+ "*.sol": [
31
+ "prettier --write --plugin=prettier-plugin-solidity"
32
+ ],
33
+ "*.{ts,json,md,yml,yaml}": [
34
+ "prettier --write"
35
+ ]
36
+ }
37
+ }