-
Notifications
You must be signed in to change notification settings - Fork 345
/
StrategyManager.sol
444 lines (398 loc) · 21 KB
/
StrategyManager.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.12;
import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol";
import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../interfaces/IEigenPodManager.sol";
import "../permissions/Pausable.sol";
import "./StrategyManagerStorage.sol";
import "../libraries/EIP1271SignatureUtils.sol";
/**
* @title The primary entry- and exit-point for funds into and out of EigenLayer.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice This contract is for managing deposits in different strategies. The main
* functionalities are:
* - adding and removing strategies that any delegator can deposit into
* - enabling deposit of assets into specified strategy(s)
*/
contract StrategyManager is
Initializable,
OwnableUpgradeable,
ReentrancyGuardUpgradeable,
Pausable,
StrategyManagerStorage
{
using SafeERC20 for IERC20;
// index for flag that pauses deposits when set
uint8 internal constant PAUSED_DEPOSITS = 0;
// chain id at the time of contract deployment
uint256 internal immutable ORIGINAL_CHAIN_ID;
modifier onlyStrategyWhitelister() {
require(
msg.sender == strategyWhitelister, "StrategyManager.onlyStrategyWhitelister: not the strategyWhitelister"
);
_;
}
modifier onlyStrategiesWhitelistedForDeposit(IStrategy strategy) {
require(
strategyIsWhitelistedForDeposit[strategy],
"StrategyManager.onlyStrategiesWhitelistedForDeposit: strategy not whitelisted"
);
_;
}
modifier onlyDelegationManager() {
require(msg.sender == address(delegation), "StrategyManager.onlyDelegationManager: not the DelegationManager");
_;
}
/**
* @param _delegation The delegation contract of EigenLayer.
* @param _slasher The primary slashing contract of EigenLayer.
* @param _eigenPodManager The contract that keeps track of EigenPod stakes for restaking beacon chain ether.
*/
constructor(
IDelegationManager _delegation,
IEigenPodManager _eigenPodManager,
ISlasher _slasher
) StrategyManagerStorage(_delegation, _eigenPodManager, _slasher) {
_disableInitializers();
ORIGINAL_CHAIN_ID = block.chainid;
}
// EXTERNAL FUNCTIONS
/**
* @notice Initializes the strategy manager contract. Sets the `pauserRegistry` (currently **not** modifiable after being set),
* and transfers contract ownership to the specified `initialOwner`.
* @param _pauserRegistry Used for access control of pausing.
* @param initialOwner Ownership of this contract is transferred to this address.
* @param initialStrategyWhitelister The initial value of `strategyWhitelister` to set.
* @param initialPausedStatus The initial value of `_paused` to set.
*/
function initialize(
address initialOwner,
address initialStrategyWhitelister,
IPauserRegistry _pauserRegistry,
uint256 initialPausedStatus
) external initializer {
_DOMAIN_SEPARATOR = _calculateDomainSeparator();
_initializePauser(_pauserRegistry, initialPausedStatus);
_transferOwnership(initialOwner);
_setStrategyWhitelister(initialStrategyWhitelister);
}
/**
* @notice Deposits `amount` of `token` into the specified `strategy`, with the resultant shares credited to `msg.sender`
* @param strategy is the specified strategy where deposit is to be made,
* @param token is the denomination in which the deposit is to be made,
* @param amount is the amount of token to be deposited in the strategy by the staker
* @return shares The amount of new shares in the `strategy` created as part of the action.
* @dev The `msg.sender` must have previously approved this contract to transfer at least `amount` of `token` on their behalf.
*
* WARNING: Depositing tokens that allow reentrancy (eg. ERC-777) into a strategy is not recommended. This can lead to attack vectors
* where the token balance and corresponding strategy shares are not in sync upon reentrancy.
*/
function depositIntoStrategy(
IStrategy strategy,
IERC20 token,
uint256 amount
) external onlyWhenNotPaused(PAUSED_DEPOSITS) nonReentrant returns (uint256 shares) {
shares = _depositIntoStrategy(msg.sender, strategy, token, amount);
}
/**
* @notice Used for depositing an asset into the specified strategy with the resultant shares credited to `staker`,
* who must sign off on the action.
* Note that the assets are transferred out/from the `msg.sender`, not from the `staker`; this function is explicitly designed
* purely to help one address deposit 'for' another.
* @param strategy is the specified strategy where deposit is to be made,
* @param token is the denomination in which the deposit is to be made,
* @param amount is the amount of token to be deposited in the strategy by the staker
* @param staker the staker that the deposited assets will be credited to
* @param expiry the timestamp at which the signature expires
* @param signature is a valid signature from the `staker`. either an ECDSA signature if the `staker` is an EOA, or data to forward
* following EIP-1271 if the `staker` is a contract
* @return shares The amount of new shares in the `strategy` created as part of the action.
* @dev The `msg.sender` must have previously approved this contract to transfer at least `amount` of `token` on their behalf.
* @dev A signature is required for this function to eliminate the possibility of griefing attacks, specifically those
* targeting stakers who may be attempting to undelegate.
* @dev Cannot be called if thirdPartyTransfersForbidden is set to true for this strategy
*
* WARNING: Depositing tokens that allow reentrancy (eg. ERC-777) into a strategy is not recommended. This can lead to attack vectors
* where the token balance and corresponding strategy shares are not in sync upon reentrancy
*/
function depositIntoStrategyWithSignature(
IStrategy strategy,
IERC20 token,
uint256 amount,
address staker,
uint256 expiry,
bytes memory signature
) external onlyWhenNotPaused(PAUSED_DEPOSITS) nonReentrant returns (uint256 shares) {
require(
!thirdPartyTransfersForbidden[strategy],
"StrategyManager.depositIntoStrategyWithSignature: third transfers disabled"
);
require(expiry >= block.timestamp, "StrategyManager.depositIntoStrategyWithSignature: signature expired");
// calculate struct hash, then increment `staker`'s nonce
uint256 nonce = nonces[staker];
bytes32 structHash = keccak256(abi.encode(DEPOSIT_TYPEHASH, staker, strategy, token, amount, nonce, expiry));
unchecked {
nonces[staker] = nonce + 1;
}
// calculate the digest hash
bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator(), structHash));
/**
* check validity of signature:
* 1) if `staker` is an EOA, then `signature` must be a valid ECDSA signature from `staker`,
* indicating their intention for this action
* 2) if `staker` is a contract, then `signature` will be checked according to EIP-1271
*/
EIP1271SignatureUtils.checkSignature_EIP1271(staker, digestHash, signature);
// deposit the tokens (from the `msg.sender`) and credit the new shares to the `staker`
shares = _depositIntoStrategy(staker, strategy, token, amount);
}
/// @notice Used by the DelegationManager to remove a Staker's shares from a particular strategy when entering the withdrawal queue
function removeShares(address staker, IStrategy strategy, uint256 shares) external onlyDelegationManager {
_removeShares(staker, strategy, shares);
}
/// @notice Used by the DelegationManager to award a Staker some shares that have passed through the withdrawal queue
function addShares(
address staker,
IERC20 token,
IStrategy strategy,
uint256 shares
) external onlyDelegationManager {
_addShares(staker, token, strategy, shares);
}
/// @notice Used by the DelegationManager to convert withdrawn shares to tokens and send them to a recipient
function withdrawSharesAsTokens(
address recipient,
IStrategy strategy,
uint256 shares,
IERC20 token
) external onlyDelegationManager {
strategy.withdraw(recipient, token, shares);
}
/**
* If true for a strategy, a user cannot depositIntoStrategyWithSignature into that strategy for another staker
* and also when performing DelegationManager.queueWithdrawals, a staker can only withdraw to themselves.
* Defaulted to false for all existing strategies.
* @param strategy The strategy to set `thirdPartyTransfersForbidden` value to
* @param value bool value to set `thirdPartyTransfersForbidden` to
*/
function setThirdPartyTransfersForbidden(IStrategy strategy, bool value) external onlyStrategyWhitelister {
_setThirdPartyTransfersForbidden(strategy, value);
}
/**
* @notice Owner-only function to change the `strategyWhitelister` address.
* @param newStrategyWhitelister new address for the `strategyWhitelister`.
*/
function setStrategyWhitelister(address newStrategyWhitelister) external onlyOwner {
_setStrategyWhitelister(newStrategyWhitelister);
}
/**
* @notice Owner-only function that adds the provided Strategies to the 'whitelist' of strategies that stakers can deposit into
* @param strategiesToWhitelist Strategies that will be added to the `strategyIsWhitelistedForDeposit` mapping (if they aren't in it already)
* @param thirdPartyTransfersForbiddenValues bool values to set `thirdPartyTransfersForbidden` to for each strategy
*/
function addStrategiesToDepositWhitelist(
IStrategy[] calldata strategiesToWhitelist,
bool[] calldata thirdPartyTransfersForbiddenValues
) external onlyStrategyWhitelister {
require(
strategiesToWhitelist.length == thirdPartyTransfersForbiddenValues.length,
"StrategyManager.addStrategiesToDepositWhitelist: array lengths do not match"
);
uint256 strategiesToWhitelistLength = strategiesToWhitelist.length;
for (uint256 i = 0; i < strategiesToWhitelistLength;) {
// change storage and emit event only if strategy is not already in whitelist
if (!strategyIsWhitelistedForDeposit[strategiesToWhitelist[i]]) {
strategyIsWhitelistedForDeposit[strategiesToWhitelist[i]] = true;
emit StrategyAddedToDepositWhitelist(strategiesToWhitelist[i]);
_setThirdPartyTransfersForbidden(strategiesToWhitelist[i], thirdPartyTransfersForbiddenValues[i]);
}
unchecked {
++i;
}
}
}
/**
* @notice Owner-only function that removes the provided Strategies from the 'whitelist' of strategies that stakers can deposit into
* @param strategiesToRemoveFromWhitelist Strategies that will be removed to the `strategyIsWhitelistedForDeposit` mapping (if they are in it)
*/
function removeStrategiesFromDepositWhitelist(IStrategy[] calldata strategiesToRemoveFromWhitelist)
external
onlyStrategyWhitelister
{
uint256 strategiesToRemoveFromWhitelistLength = strategiesToRemoveFromWhitelist.length;
for (uint256 i = 0; i < strategiesToRemoveFromWhitelistLength;) {
// change storage and emit event only if strategy is already in whitelist
if (strategyIsWhitelistedForDeposit[strategiesToRemoveFromWhitelist[i]]) {
strategyIsWhitelistedForDeposit[strategiesToRemoveFromWhitelist[i]] = false;
emit StrategyRemovedFromDepositWhitelist(strategiesToRemoveFromWhitelist[i]);
// Set mapping value to default false value
_setThirdPartyTransfersForbidden(strategiesToRemoveFromWhitelist[i], false);
}
unchecked {
++i;
}
}
}
// INTERNAL FUNCTIONS
/**
* @notice This function adds `shares` for a given `strategy` to the `staker` and runs through the necessary update logic.
* @param staker The address to add shares to
* @param token The token that is being deposited (used for indexing)
* @param strategy The Strategy in which the `staker` is receiving shares
* @param shares The amount of shares to grant to the `staker`
* @dev In particular, this function calls `delegation.increaseDelegatedShares(staker, strategy, shares)` to ensure that all
* delegated shares are tracked, increases the stored share amount in `stakerStrategyShares[staker][strategy]`, and adds `strategy`
* to the `staker`'s list of strategies, if it is not in the list already.
*/
function _addShares(address staker, IERC20 token, IStrategy strategy, uint256 shares) internal {
// sanity checks on inputs
require(staker != address(0), "StrategyManager._addShares: staker cannot be zero address");
require(shares != 0, "StrategyManager._addShares: shares should not be zero!");
// if they dont have existing shares of this strategy, add it to their strats
if (stakerStrategyShares[staker][strategy] == 0) {
require(
stakerStrategyList[staker].length < MAX_STAKER_STRATEGY_LIST_LENGTH,
"StrategyManager._addShares: deposit would exceed MAX_STAKER_STRATEGY_LIST_LENGTH"
);
stakerStrategyList[staker].push(strategy);
}
// add the returned shares to their existing shares for this strategy
stakerStrategyShares[staker][strategy] += shares;
emit Deposit(staker, token, strategy, shares);
}
/**
* @notice Internal function in which `amount` of ERC20 `token` is transferred from `msg.sender` to the Strategy-type contract
* `strategy`, with the resulting shares credited to `staker`.
* @param staker The address that will be credited with the new shares.
* @param strategy The Strategy contract to deposit into.
* @param token The ERC20 token to deposit.
* @param amount The amount of `token` to deposit.
* @return shares The amount of *new* shares in `strategy` that have been credited to the `staker`.
*/
function _depositIntoStrategy(
address staker,
IStrategy strategy,
IERC20 token,
uint256 amount
) internal onlyStrategiesWhitelistedForDeposit(strategy) returns (uint256 shares) {
// transfer tokens from the sender to the strategy
token.safeTransferFrom(msg.sender, address(strategy), amount);
// deposit the assets into the specified strategy and get the equivalent amount of shares in that strategy
shares = strategy.deposit(token, amount);
// add the returned shares to the staker's existing shares for this strategy
_addShares(staker, token, strategy, shares);
// Increase shares delegated to operator, if needed
delegation.increaseDelegatedShares(staker, strategy, shares);
return shares;
}
/**
* @notice Decreases the shares that `staker` holds in `strategy` by `shareAmount`.
* @param staker The address to decrement shares from
* @param strategy The strategy for which the `staker`'s shares are being decremented
* @param shareAmount The amount of shares to decrement
* @dev If the amount of shares represents all of the staker`s shares in said strategy,
* then the strategy is removed from stakerStrategyList[staker] and 'true' is returned. Otherwise 'false' is returned.
*/
function _removeShares(address staker, IStrategy strategy, uint256 shareAmount) internal returns (bool) {
// sanity checks on inputs
require(shareAmount != 0, "StrategyManager._removeShares: shareAmount should not be zero!");
//check that the user has sufficient shares
uint256 userShares = stakerStrategyShares[staker][strategy];
require(shareAmount <= userShares, "StrategyManager._removeShares: shareAmount too high");
//unchecked arithmetic since we just checked this above
unchecked {
userShares = userShares - shareAmount;
}
// subtract the shares from the staker's existing shares for this strategy
stakerStrategyShares[staker][strategy] = userShares;
// if no existing shares, remove the strategy from the staker's dynamic array of strategies
if (userShares == 0) {
_removeStrategyFromStakerStrategyList(staker, strategy);
// return true in the event that the strategy was removed from stakerStrategyList[staker]
return true;
}
// return false in the event that the strategy was *not* removed from stakerStrategyList[staker]
return false;
}
/**
* @notice Removes `strategy` from `staker`'s dynamic array of strategies, i.e. from `stakerStrategyList[staker]`
* @param staker The user whose array will have an entry removed
* @param strategy The Strategy to remove from `stakerStrategyList[staker]`
*/
function _removeStrategyFromStakerStrategyList(address staker, IStrategy strategy) internal {
//loop through all of the strategies, find the right one, then replace
uint256 stratsLength = stakerStrategyList[staker].length;
uint256 j = 0;
for (; j < stratsLength;) {
if (stakerStrategyList[staker][j] == strategy) {
//replace the strategy with the last strategy in the list
stakerStrategyList[staker][j] = stakerStrategyList[staker][stakerStrategyList[staker].length - 1];
break;
}
unchecked {
++j;
}
}
// if we didn't find the strategy, revert
require(j != stratsLength, "StrategyManager._removeStrategyFromStakerStrategyList: strategy not found");
// pop off the last entry in the list of strategies
stakerStrategyList[staker].pop();
}
/**
* @notice Internal function for modifying `thirdPartyTransfersForbidden`.
* Used inside of the `setThirdPartyTransfersForbidden` and `addStrategiesToDepositWhitelist` functions.
* @param strategy The strategy to set `thirdPartyTransfersForbidden` value to
* @param value bool value to set `thirdPartyTransfersForbidden` to
*/
function _setThirdPartyTransfersForbidden(IStrategy strategy, bool value) internal {
emit UpdatedThirdPartyTransfersForbidden(strategy, value);
thirdPartyTransfersForbidden[strategy] = value;
}
/**
* @notice Internal function for modifying the `strategyWhitelister`. Used inside of the `setStrategyWhitelister` and `initialize` functions.
* @param newStrategyWhitelister The new address for the `strategyWhitelister` to take.
*/
function _setStrategyWhitelister(address newStrategyWhitelister) internal {
emit StrategyWhitelisterChanged(strategyWhitelister, newStrategyWhitelister);
strategyWhitelister = newStrategyWhitelister;
}
// VIEW FUNCTIONS
/**
* @notice Get all details on the staker's deposits and corresponding shares
* @param staker The staker of interest, whose deposits this function will fetch
* @return (staker's strategies, shares in these strategies)
*/
function getDeposits(address staker) external view returns (IStrategy[] memory, uint256[] memory) {
uint256 strategiesLength = stakerStrategyList[staker].length;
uint256[] memory shares = new uint256[](strategiesLength);
for (uint256 i = 0; i < strategiesLength;) {
shares[i] = stakerStrategyShares[staker][stakerStrategyList[staker][i]];
unchecked {
++i;
}
}
return (stakerStrategyList[staker], shares);
}
/// @notice Simple getter function that returns `stakerStrategyList[staker].length`.
function stakerStrategyListLength(address staker) external view returns (uint256) {
return stakerStrategyList[staker].length;
}
/**
* @notice Getter function for the current EIP-712 domain separator for this contract.
* @dev The domain separator will change in the event of a fork that changes the ChainID.
*/
function domainSeparator() public view returns (bytes32) {
if (block.chainid == ORIGINAL_CHAIN_ID) {
return _DOMAIN_SEPARATOR;
} else {
return _calculateDomainSeparator();
}
}
// @notice Internal function for calculating the current domain separator of this contract
function _calculateDomainSeparator() internal view returns (bytes32) {
return keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes("EigenLayer")), block.chainid, address(this)));
}
}