@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.
- package/contracts/aave-v3/AaveV3Supply.sol +57 -0
- package/contracts/aave-v3/AaveV3SupplyBase.sol +177 -0
- package/contracts/aave-v3/AaveV3SupplyWithMerkle.sol +174 -0
- package/contracts/interfaces/IAaveV3Supply.sol +26 -0
- package/contracts/interfaces/IAaveV3SupplyWithMerkle.sol +58 -0
- package/contracts/interfaces/IMorphoVault.sol +2 -0
- package/contracts/morpho/MorphoVault.sol +9 -10
- package/package.json +8 -5
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|