@shift-defi/strategies 0.1.0 → 0.2.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,57 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
+ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
6
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
7
+
8
+ import {AaveV3SupplyBase} from "./AaveV3SupplyBase.sol";
9
+
10
+ contract AaveV3Supply is AaveV3SupplyBase {
11
+ using SafeERC20 for IERC20;
12
+ using Math for uint256;
13
+
14
+ constructor() {
15
+ _disableInitializers();
16
+ }
17
+
18
+ /// @notice Initializes the AaveV3Supply strategy contract
19
+ /// @dev Sets up the Aave pool, reserve asset, and aToken addresses, and configures the strategy states
20
+ /// @param strategyContainer The address of the strategy container contract
21
+ /// @param _pool The address of the Aave V3 Pool contract
22
+ /// @param _reserveAsset The address of the underlying asset to be supplied to Aave
23
+ /// @param slippageParams The enter/exit/emergency-exit maximum slippage parameters
24
+ function initialize(
25
+ address strategyContainer,
26
+ address _pool,
27
+ address _reserveAsset,
28
+ SlippageParams calldata slippageParams
29
+ ) external initializer {
30
+ __AaveV3Supply_init(strategyContainer, _pool, _reserveAsset, slippageParams);
31
+ }
32
+
33
+ function _harvest(bytes32 stateId, address treasury, uint256 feePct) internal override {
34
+ // Supplied state only: a paused reserve reverts aToken fee transfers, which must not block token-state exits
35
+ if (stateId != AAVE_RESERVE_SUPPLIED_STATE_ID) {
36
+ return;
37
+ }
38
+
39
+ AaveHarvestLocalVars memory vars;
40
+ vars.reserveATokenCached = reserveAToken;
41
+ vars.currentReserveATokenBalance = IERC20(vars.reserveATokenCached).balanceOf(address(this));
42
+ vars.lastReserveATokenBalanceCached = lastReserveATokenBalance;
43
+
44
+ if (vars.currentReserveATokenBalance <= vars.lastReserveATokenBalanceCached) {
45
+ return;
46
+ }
47
+
48
+ vars.income = vars.currentReserveATokenBalance - vars.lastReserveATokenBalanceCached;
49
+ vars.fee = vars.income.mulDiv(feePct, MAX_BPS);
50
+
51
+ if (vars.fee > 0) {
52
+ IERC20(vars.reserveATokenCached).safeTransfer(treasury, vars.fee);
53
+ }
54
+
55
+ lastReserveATokenBalance = IERC20(vars.reserveATokenCached).balanceOf(address(this));
56
+ }
57
+ }
@@ -0,0 +1,177 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
+ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
6
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
7
+
8
+ import {StrategyTemplate} from "@shift-defi/core/StrategyTemplate.sol";
9
+ import {IStrategyTemplate} from "@shift-defi/core/interfaces/IStrategyTemplate.sol";
10
+ import {Errors} from "@shift-defi/core/libraries/Errors.sol";
11
+
12
+ import {IPool} from "../dependencies/aave-v3/IPool.sol";
13
+ import {IAaveV3Supply} from "../interfaces/IAaveV3Supply.sol";
14
+
15
+ /// @notice Shared Aave V3 supply logic. Concrete strategies only need to implement `_harvest` -
16
+ /// everything else (state machine, enter/exit, NAV) is identical regardless of how rewards
17
+ /// are harvested.
18
+ abstract contract AaveV3SupplyBase is StrategyTemplate, IAaveV3Supply {
19
+ using SafeERC20 for IERC20;
20
+ using Math for uint256;
21
+
22
+ /// @notice The Aave V3 Pool contract address
23
+ address public pool;
24
+
25
+ /// @notice The underlying asset address that is being supplied to Aave
26
+ address public reserveAsset;
27
+
28
+ /// @notice The aToken address corresponding to the reserve asset
29
+ address public reserveAToken;
30
+
31
+ /// @notice The last recorded balance of aTokens, used for harvest calculations
32
+ uint256 public lastReserveATokenBalance;
33
+
34
+ /// @notice State ID for the underlying asset state (holding the asset directly)
35
+ bytes32 internal constant UNDERLYING_ASSET_STATE_ID = keccak256("UNDERLYING_ASSET_STATE_ID");
36
+
37
+ /// @notice State ID for the Aave reserve supplied state (asset supplied to Aave)
38
+ bytes32 internal constant AAVE_RESERVE_SUPPLIED_STATE_ID = keccak256("AAVE_RESERVE_SUPPLIED_STATE_ID");
39
+
40
+ /// @notice Aave's ray precision, the unit of the reserve liquidity index
41
+ uint256 internal constant RAY = 1e27;
42
+
43
+ /// @dev Shared initializer logic, exposed so subclasses can compose it with their own additional setup
44
+ /// inside a single `initializer`-guarded call.
45
+ function __AaveV3Supply_init(
46
+ address strategyContainer,
47
+ address _pool,
48
+ address _reserveAsset,
49
+ SlippageParams calldata slippageParams
50
+ ) internal onlyInitializing {
51
+ __StrategyTemplate_init(
52
+ strategyContainer,
53
+ slippageParams.enterMaxSlippage,
54
+ slippageParams.exitMaxSlippage,
55
+ slippageParams.emergencyExitMaxSlippage
56
+ );
57
+
58
+ require(_pool != address(0), Errors.ZeroAddress());
59
+ pool = _pool;
60
+
61
+ require(_reserveAsset != address(0), Errors.ZeroAddress());
62
+ reserveAsset = _reserveAsset;
63
+
64
+ reserveAToken = IPool(_pool).getReserveAToken(_reserveAsset);
65
+
66
+ _setState(UNDERLYING_ASSET_STATE_ID, false, false, true, 0);
67
+ _setState(AAVE_RESERVE_SUPPLIED_STATE_ID, true, true, false, 1);
68
+ }
69
+
70
+ // -------- Strategy Template Implementations --------
71
+
72
+ /// @inheritdoc IStrategyTemplate
73
+ function stateNav(bytes32 stateId) public view override returns (uint256) {
74
+ if (stateId == UNDERLYING_ASSET_STATE_ID) {
75
+ return underlyingAssetNav();
76
+ } else if (stateId == AAVE_RESERVE_SUPPLIED_STATE_ID) {
77
+ return aaveReserveSuppliedNav();
78
+ } else if (stateId == NO_ALLOCATION_STATE_ID) {
79
+ return 0;
80
+ }
81
+ revert StateNotFound(stateId);
82
+ }
83
+
84
+ /// @notice Calculates the NAV for the underlying asset state
85
+ /// @dev Returns the value of underlying assets held by the contract in notion
86
+ /// @return The NAV of underlying assets in notion
87
+ function underlyingAssetNav() public view returns (uint256) {
88
+ address reserveAssetCached = reserveAsset;
89
+ return getTokenAmountInNotion(reserveAssetCached, IERC20(reserveAssetCached).balanceOf(address(this)));
90
+ }
91
+
92
+ /// @notice Calculates the NAV for the Aave reserve supplied state
93
+ /// @dev Returns the value of aTokens held by the contract in notion
94
+ /// @return The NAV of aTokens in notion
95
+ function aaveReserveSuppliedNav() public view returns (uint256) {
96
+ return getTokenAmountInNotion(reserveAsset, IERC20(reserveAToken).balanceOf(address(this)));
97
+ }
98
+
99
+ function _enterTarget() internal override {
100
+ _enterAaveReserveSupplied();
101
+ }
102
+
103
+ function _enterState(bytes32 stateId) internal override {
104
+ if (stateId == AAVE_RESERVE_SUPPLIED_STATE_ID) {
105
+ _enterAaveReserveSupplied();
106
+ } else {
107
+ revert StateNotFound(stateId);
108
+ }
109
+ }
110
+
111
+ function _enterAaveReserveSupplied() internal {
112
+ address reserveAssetCached = reserveAsset;
113
+ uint256 underlyingAssetBalance = IERC20(reserveAssetCached).balanceOf(address(this));
114
+
115
+ if (underlyingAssetBalance == 0) {
116
+ return;
117
+ }
118
+
119
+ address poolCached = pool;
120
+
121
+ // Aave reverts a supply that mints zero scaled aTokens (amount * RAY / liquidityIndex rounds down to 0).
122
+ // Leave such dust idle for a later supply so it can't be used to make harvest revert.
123
+ if (underlyingAssetBalance.mulDiv(RAY, IPool(poolCached).getReserveNormalizedIncome(reserveAssetCached)) == 0) {
124
+ return;
125
+ }
126
+
127
+ IERC20(reserveAssetCached).safeIncreaseAllowance(poolCached, underlyingAssetBalance);
128
+ IPool(poolCached).supply(reserveAssetCached, underlyingAssetBalance, address(this), 0);
129
+ lastReserveATokenBalance = IERC20(reserveAToken).balanceOf(address(this));
130
+ }
131
+
132
+ function _exitTarget(uint256 share) internal override {
133
+ _exitAaveReserveSupplied(share);
134
+ }
135
+
136
+ function _exitFromState(bytes32 stateId, uint256 share) internal override {
137
+ if (stateId == AAVE_RESERVE_SUPPLIED_STATE_ID) {
138
+ _exitAaveReserveSupplied(share);
139
+ } else {
140
+ revert StateNotFound(stateId);
141
+ }
142
+ }
143
+
144
+ function _exitAaveReserveSupplied(uint256 share) internal {
145
+ uint256 allocatedAmount = IERC20(reserveAToken).balanceOf(address(this));
146
+ require(allocatedAmount > 0, NoReserveAllocation());
147
+
148
+ uint256 amountToWithdraw = allocatedAmount.mulDiv(share, MAX_BPS);
149
+ _exitAaveReserveSuppliedFlat(amountToWithdraw);
150
+ }
151
+
152
+ function _emergencyExit(bytes32 toStateId, uint256 share) internal override {
153
+ if (toStateId == UNDERLYING_ASSET_STATE_ID) {
154
+ _exitAaveReserveSupplied(share);
155
+ } else {
156
+ revert StateNotFound(toStateId);
157
+ }
158
+ }
159
+
160
+ function _exitAaveReserveSuppliedFlat(uint256 amount) internal returns (uint256) {
161
+ require(amount > 0, WithdrawAmountTooSmall());
162
+ address reserveAssetCached = reserveAsset;
163
+
164
+ uint256 reserveBalanceBefore = IERC20(reserveAssetCached).balanceOf(address(this));
165
+ uint256 withdrawnAmount = IPool(pool).withdraw(reserveAssetCached, amount, address(this));
166
+
167
+ require(
168
+ IERC20(reserveAssetCached).balanceOf(address(this)) >= reserveBalanceBefore + withdrawnAmount,
169
+ WithdrawAmountMismatch()
170
+ );
171
+
172
+ lastReserveATokenBalance = IERC20(reserveAToken).balanceOf(address(this));
173
+ return withdrawnAmount;
174
+ }
175
+
176
+ uint256[50] private __gap;
177
+ }
@@ -0,0 +1,174 @@
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 {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
7
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
8
+
9
+ import {IStrategyContainer} from "@shift-defi/core/interfaces/IStrategyContainer.sol";
10
+ import {Errors} from "@shift-defi/core/libraries/Errors.sol";
11
+
12
+ import {AaveV3SupplyBase} from "./AaveV3SupplyBase.sol";
13
+ import {IAngleMerkleDistributor} from "../dependencies/angle/IAngleMerkleDistributor.sol";
14
+ import {IAaveV3SupplyWithMerkle} from "../interfaces/IAaveV3SupplyWithMerkle.sol";
15
+
16
+ contract AaveV3SupplyWithMerkle is AccessControlUpgradeable, AaveV3SupplyBase, IAaveV3SupplyWithMerkle {
17
+ using SafeERC20 for IERC20;
18
+ using Math for uint256;
19
+
20
+ /// @notice Role for the Merkle claimer
21
+ bytes32 private constant MERKLE_CLAIMER_ROLE = keccak256("MERKLE_CLAIMER_ROLE");
22
+
23
+ /// @notice The address of the Merkle distributor contract
24
+ address public merkleDistributor;
25
+
26
+ /// @notice The addresses of the reward tokens reinvested during automatic harvest
27
+ address[] internal rewardTokens;
28
+
29
+ constructor() {
30
+ _disableInitializers();
31
+ }
32
+
33
+ /// @notice Initializes the AaveV3SupplyWithMerkle strategy contract
34
+ /// @dev Composes `AaveV3SupplyBase`'s initializer with the Merkle claim setup inside one
35
+ /// `initializer`-guarded call
36
+ /// @param strategyContainer The address of the strategy container contract
37
+ /// @param defaultAdmin The address of the default admin
38
+ /// @param merkleClaimer The address of the Merkle claimer
39
+ /// @param _pool The address of the Aave V3 Pool contract
40
+ /// @param _reserveAsset The address of the underlying asset to be supplied to Aave
41
+ /// @param _merkleDistributor The address of the Merkle distributor
42
+ /// @param _rewardTokens The addresses of the reward tokens
43
+ /// @param slippageParams The enter/exit/emergency-exit maximum slippage parameters
44
+ function initialize(
45
+ address strategyContainer,
46
+ address defaultAdmin,
47
+ address merkleClaimer,
48
+ address _pool,
49
+ address _reserveAsset,
50
+ address _merkleDistributor,
51
+ address[] calldata _rewardTokens,
52
+ SlippageParams calldata slippageParams
53
+ ) external initializer {
54
+ __AccessControl_init();
55
+ __AaveV3Supply_init(strategyContainer, _pool, _reserveAsset, slippageParams);
56
+
57
+ require(defaultAdmin != address(0), Errors.ZeroAddress());
58
+ _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
59
+ require(merkleClaimer != address(0), Errors.ZeroAddress());
60
+ _grantRole(MERKLE_CLAIMER_ROLE, merkleClaimer);
61
+
62
+ require(_merkleDistributor != address(0), Errors.ZeroAddress());
63
+ merkleDistributor = _merkleDistributor;
64
+
65
+ _setRewardTokens(_rewardTokens);
66
+ }
67
+
68
+ /// @inheritdoc IAaveV3SupplyWithMerkle
69
+ function getRewardTokens() external view override returns (address[] memory) {
70
+ return rewardTokens;
71
+ }
72
+
73
+ /// @inheritdoc IAaveV3SupplyWithMerkle
74
+ function setRewardTokens(address[] memory _rewardTokens) external onlyStrategyContainerOrHarvestManager {
75
+ _setRewardTokens(_rewardTokens);
76
+ }
77
+
78
+ function _setRewardTokens(address[] memory _rewardTokens) private {
79
+ uint256 rewardTokensLength = _rewardTokens.length;
80
+ address reserveAssetCached = reserveAsset;
81
+ address reserveATokenCached = reserveAToken;
82
+
83
+ for (uint256 i = 0; i < rewardTokensLength; ++i) {
84
+ require(_rewardTokens[i] != address(0), Errors.ZeroAddress());
85
+ require(_rewardTokens[i] != reserveAssetCached, RewardTokenMatchesReserveAsset());
86
+ require(_rewardTokens[i] != reserveATokenCached, RewardTokenMatchesReserveAToken());
87
+ }
88
+
89
+ rewardTokens = _rewardTokens;
90
+ emit RewardTokensUpdated(_rewardTokens);
91
+ }
92
+
93
+ function _harvest(bytes32 stateId, address treasury, uint256 feePct) internal override {
94
+ // Supplied state only: a paused reserve reverts aToken fee transfers, which must not block token-state exits
95
+ if (stateId != AAVE_RESERVE_SUPPLIED_STATE_ID) {
96
+ return;
97
+ }
98
+
99
+ AutomaticHarvestLocalVars memory vars;
100
+ vars.reserveATokenCached = reserveAToken;
101
+ vars.reserveAssetCached = reserveAsset;
102
+ vars.balanceBeforeReinvest = lastReserveATokenBalance;
103
+ vars.currentBalance = IERC20(vars.reserveATokenCached).balanceOf(address(this));
104
+
105
+ if (vars.currentBalance > vars.balanceBeforeReinvest) {
106
+ vars.feeToTreasury = (vars.currentBalance - vars.balanceBeforeReinvest).mulDiv(feePct, MAX_BPS);
107
+ }
108
+
109
+ vars.rewardTokensLength = rewardTokens.length;
110
+ for (uint256 i = 0; i < vars.rewardTokensLength; ++i) {
111
+ _swapToInputTokens(rewardTokens[i], vars.reserveAssetCached, 0, false);
112
+ }
113
+ vars.aTokenBalanceBefore = IERC20(vars.reserveATokenCached).balanceOf(address(this));
114
+ _enterAaveReserveSupplied();
115
+ vars.aTokenDelta = IERC20(vars.reserveATokenCached).balanceOf(address(this)) - vars.aTokenBalanceBefore;
116
+ if (vars.aTokenDelta > 0) {
117
+ vars.feeFromReinvest = vars.aTokenDelta.mulDiv(feePct, MAX_BPS);
118
+ if (vars.feeFromReinvest > 0) {
119
+ vars.feeToTreasury += vars.feeFromReinvest;
120
+ }
121
+ }
122
+
123
+ if (vars.feeToTreasury > 0) {
124
+ IERC20(vars.reserveATokenCached).safeTransfer(treasury, vars.feeToTreasury);
125
+ }
126
+
127
+ vars.currentBalance = IERC20(vars.reserveATokenCached).balanceOf(address(this));
128
+
129
+ lastReserveATokenBalance = Math.max(vars.currentBalance, vars.balanceBeforeReinvest);
130
+ }
131
+
132
+ /// @inheritdoc IAaveV3SupplyWithMerkle
133
+ function manualClaim(
134
+ address[] calldata tokens,
135
+ uint256[] calldata amounts,
136
+ bytes32[][] calldata proofs
137
+ ) external nonReentrant onlyRole(MERKLE_CLAIMER_ROLE) {
138
+ require(!isNavResolutionMode(), NavResolutionModeActivated());
139
+
140
+ ManualClaimLocalVars memory vars;
141
+ vars.reserveATokenCached = reserveAToken;
142
+ vars.reserveAssetCached = reserveAsset;
143
+ vars.strategyContainerCached = _strategyContainer;
144
+
145
+ vars.treasury = IStrategyContainer(vars.strategyContainerCached).treasury();
146
+ require(vars.treasury != address(0), Errors.ZeroAddress());
147
+ vars.feePct = IStrategyContainer(vars.strategyContainerCached).feePct();
148
+ vars.lastBalanceCached = lastReserveATokenBalance;
149
+
150
+ vars.users = new address[](1);
151
+ vars.users[0] = address(this);
152
+ IAngleMerkleDistributor(merkleDistributor).claim(vars.users, tokens, amounts, proofs);
153
+
154
+ if (currentStateId() == AAVE_RESERVE_SUPPLIED_STATE_ID) {
155
+ uint256 tokensLength = tokens.length;
156
+ for (uint256 i = 0; i < tokensLength; ++i) {
157
+ if (tokens[i] == vars.reserveAssetCached) {
158
+ _enterAaveReserveSupplied();
159
+ break;
160
+ }
161
+ }
162
+
163
+ vars.currentBalance = IERC20(vars.reserveATokenCached).balanceOf(address(this));
164
+ if (vars.currentBalance > vars.lastBalanceCached) {
165
+ vars.fee = (vars.currentBalance - vars.lastBalanceCached).mulDiv(vars.feePct, MAX_BPS);
166
+ if (vars.fee > 0) {
167
+ IERC20(vars.reserveATokenCached).safeTransfer(vars.treasury, vars.fee);
168
+ }
169
+ }
170
+ }
171
+
172
+ lastReserveATokenBalance = IERC20(vars.reserveATokenCached).balanceOf(address(this));
173
+ }
174
+ }
@@ -0,0 +1,26 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ interface IAaveV3Supply {
5
+ // ---- Structs ----
6
+
7
+ struct SlippageParams {
8
+ uint256 enterMaxSlippage;
9
+ uint256 exitMaxSlippage;
10
+ uint256 emergencyExitMaxSlippage;
11
+ }
12
+
13
+ struct AaveHarvestLocalVars {
14
+ address reserveATokenCached;
15
+ uint256 lastReserveATokenBalanceCached;
16
+ uint256 currentReserveATokenBalance;
17
+ uint256 income;
18
+ uint256 fee;
19
+ }
20
+
21
+ // ---- Errors ----
22
+
23
+ error NoReserveAllocation();
24
+ error WithdrawAmountTooSmall();
25
+ error WithdrawAmountMismatch();
26
+ }
@@ -0,0 +1,58 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ interface IAaveV3SupplyWithMerkle {
5
+ // ---- Structs ----
6
+
7
+ struct AutomaticHarvestLocalVars {
8
+ address reserveAssetCached;
9
+ address reserveATokenCached;
10
+ uint256 rewardTokensLength;
11
+ uint256 aTokenBalanceBefore;
12
+ uint256 aTokenDelta;
13
+ uint256 balanceBeforeReinvest;
14
+ uint256 currentBalance;
15
+ uint256 feeToTreasury;
16
+ uint256 feeFromReinvest;
17
+ }
18
+
19
+ struct ManualClaimLocalVars {
20
+ address reserveATokenCached;
21
+ address strategyContainerCached;
22
+ address reserveAssetCached;
23
+ address treasury;
24
+ uint256 feePct;
25
+ uint256 lastBalanceCached;
26
+ uint256 currentBalance;
27
+ uint256 fee;
28
+ address[] users;
29
+ }
30
+
31
+ // ---- Events ----
32
+
33
+ event RewardTokensUpdated(address[] rewardTokens);
34
+
35
+ // ---- Errors ----
36
+
37
+ error RewardTokenMatchesReserveAsset();
38
+ error RewardTokenMatchesReserveAToken();
39
+
40
+ // ---- Functions ----
41
+
42
+ /// @notice Returns the reward tokens
43
+ /// @return The addresses of the reward tokens
44
+ function getRewardTokens() external view returns (address[] memory);
45
+
46
+ /// @notice Sets the reward tokens. Only callable by role HARVEST_MANAGER_ROLE
47
+ /// @param _rewardTokens The addresses of the reward tokens
48
+ function setRewardTokens(address[] memory _rewardTokens) external;
49
+
50
+ /// @notice Manually claims the rewards. Only callable by role MERKLE_CLAIMER_ROLE
51
+ /// @dev Only the reserve asset is reinvested instantly, and only while currently supplied to Aave;
52
+ /// any other claimed reward (e.g. WMON), and any claim made while not supplied, is left on the
53
+ /// contract and reinvested by the next automatic harvest instead
54
+ /// @param tokens The addresses of the tokens
55
+ /// @param amounts The amounts of the tokens
56
+ /// @param proofs The proofs of the merkle proofs
57
+ function manualClaim(address[] calldata tokens, uint256[] calldata amounts, bytes32[][] calldata proofs) external;
58
+ }
@@ -15,6 +15,7 @@ interface IMorphoVault {
15
15
  uint256 reinvestLpDelta;
16
16
  uint256 feeFromReinvest;
17
17
  uint256 currentAssetsValue;
18
+ uint256 lastAssetsValueCached;
18
19
  }
19
20
 
20
21
  struct ManualClaimLocalVars {
@@ -32,6 +33,7 @@ interface IMorphoVault {
32
33
  uint256 reinvestLpDelta;
33
34
  uint256 feeFromReinvest;
34
35
  uint256 currentAssetsValue;
36
+ uint256 lastAssetsValueCached;
35
37
  }
36
38
 
37
39
  // ---- Events ----
@@ -215,12 +215,11 @@ contract MorphoVault is AccessControlUpgradeable, StrategyTemplate, IMorphoVault
215
215
  }
216
216
  }
217
217
 
218
- function _calculateAccruedAssetsValue() private view returns (uint256) {
218
+ function _calculateAccruedAssetsValue(uint256 lastAssetsValueCached) private view returns (uint256) {
219
219
  address morphoVaultCached = morphoVault;
220
220
  uint256 currentAssetsValue = IERC4626(morphoVaultCached).convertToAssets(
221
221
  IERC4626(morphoVaultCached).balanceOf(address(this))
222
222
  );
223
- uint256 lastAssetsValueCached = lastAssetsValue;
224
223
 
225
224
  if (currentAssetsValue > lastAssetsValueCached) {
226
225
  return currentAssetsValue - lastAssetsValueCached;
@@ -236,7 +235,9 @@ contract MorphoVault is AccessControlUpgradeable, StrategyTemplate, IMorphoVault
236
235
  vars.underlyingAssetCached = underlyingAsset;
237
236
  vars.rewardTokensLength = rewardTokens.length;
238
237
 
239
- vars.accruedAssetsValue = _calculateAccruedAssetsValue();
238
+ // Cached before reinvesting: `_enterMorphoVault` overwrites `lastAssetsValue` with the pre-fee value
239
+ vars.lastAssetsValueCached = lastAssetsValue;
240
+ vars.accruedAssetsValue = _calculateAccruedAssetsValue(vars.lastAssetsValueCached);
240
241
 
241
242
  if (vars.accruedAssetsValue > 0) {
242
243
  vars.accruedVaultTokens = IERC4626(vars.morphoVaultCached).convertToShares(vars.accruedAssetsValue);
@@ -268,9 +269,7 @@ contract MorphoVault is AccessControlUpgradeable, StrategyTemplate, IMorphoVault
268
269
  IERC4626(vars.morphoVaultCached).balanceOf(address(this))
269
270
  );
270
271
 
271
- if (vars.currentAssetsValue > lastAssetsValue) {
272
- lastAssetsValue = vars.currentAssetsValue;
273
- }
272
+ lastAssetsValue = Math.max(vars.currentAssetsValue, vars.lastAssetsValueCached);
274
273
  }
275
274
 
276
275
  /// @inheritdoc IMorphoVault
@@ -294,7 +293,9 @@ contract MorphoVault is AccessControlUpgradeable, StrategyTemplate, IMorphoVault
294
293
 
295
294
  vars.feePct = IStrategyContainer(vars.strategyContainerCached).feePct();
296
295
 
297
- vars.accruedAssetsValue = _calculateAccruedAssetsValue();
296
+ // Cached before reinvesting: `_enterMorphoVault` overwrites `lastAssetsValue` with the pre-fee value
297
+ vars.lastAssetsValueCached = lastAssetsValue;
298
+ vars.accruedAssetsValue = _calculateAccruedAssetsValue(vars.lastAssetsValueCached);
298
299
  if (vars.accruedAssetsValue > 0) {
299
300
  vars.accruedVaultTokens = IERC4626(vars.morphoVaultCached).convertToShares(vars.accruedAssetsValue);
300
301
  vars.vaultTokensToTreasury = vars.accruedVaultTokens.mulDiv(vars.feePct, MAX_BPS);
@@ -333,8 +334,6 @@ contract MorphoVault is AccessControlUpgradeable, StrategyTemplate, IMorphoVault
333
334
  IERC4626(vars.morphoVaultCached).balanceOf(address(this))
334
335
  );
335
336
 
336
- if (vars.currentAssetsValue > lastAssetsValue) {
337
- lastAssetsValue = vars.currentAssetsValue;
338
- }
337
+ lastAssetsValue = Math.max(vars.currentAssetsValue, vars.lastAssetsValueCached);
339
338
  }
340
339
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shift-defi/strategies",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Strategy smart contracts for Shift DeFi Platform",
5
5
  "author": "ShiftDeFi",
6
6
  "license": "MIT",
@@ -10,13 +10,16 @@
10
10
  },
11
11
  "homepage": "https://shiftdefi.com/",
12
12
  "files": [
13
- "contracts/**/*.sol"
13
+ "contracts/**/*.sol",
14
+ "!contracts/dependencies/safe"
14
15
  ],
15
16
  "scripts": {
16
- "prepare": "husky"
17
+ "prepare": "husky",
18
+ "claim": "set -a; [ -f tools/merkl-claim.env ] && . ./tools/merkl-claim.env; set +a; node tools/merkl-claim.mjs",
19
+ "claim:propose": "npm run claim -- --propose"
17
20
  },
18
21
  "dependencies": {
19
- "@shift-defi/core": "^0.7.0"
22
+ "@shift-defi/core": "^0.7.1"
20
23
  },
21
24
  "devDependencies": {
22
25
  "@commitlint/cli": "^20.2.0",
@@ -30,7 +33,7 @@
30
33
  "*.sol": [
31
34
  "prettier --write --plugin=prettier-plugin-solidity"
32
35
  ],
33
- "*.{ts,json,md,yml,yaml}": [
36
+ "*.{ts,mjs,json,md,yml,yaml}": [
34
37
  "prettier --write"
35
38
  ]
36
39
  }