-
Notifications
You must be signed in to change notification settings - Fork 360
/
Copy pathStrategyBase.sol
330 lines (291 loc) · 16.7 KB
/
StrategyBase.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
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../interfaces/IStrategyManager.sol";
import "../permissions/Pausable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol";
/**
* @title Base implementation of `IStrategy` interface, designed to be inherited from by more complex strategies.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice Simple, basic, "do-nothing" Strategy that holds a single underlying token and returns it on withdrawals.
* Implements minimal versions of the IStrategy functions, this contract is designed to be inherited by
* more complex strategies, which can then override its functions as necessary.
* @dev Note that some functions have their mutability restricted; developers inheriting from this contract cannot broaden
* the mutability without modifying this contract itself.
* @dev This contract is expressly *not* intended for use with 'fee-on-transfer'-type tokens.
* Setting the `underlyingToken` to be a fee-on-transfer token may result in improper accounting.
* @notice This contract functions similarly to an ERC4626 vault, only without issuing a token.
* To mitigate against the common "inflation attack" vector, we have chosen to use the 'virtual shares' mitigation route,
* similar to [OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC4626.sol).
* We acknowledge that this mitigation has the known downside of the virtual shares causing some losses to users, which are pronounced
* particularly in the case of the share exchange rate changing signficantly, either positively or negatively.
* For a fairly thorough discussion of this issue and our chosen mitigation strategy, we recommend reading through
* [this thread](https://github.com/OpenZeppelin/openzeppelin-contracts/issues/3706) on the OpenZeppelin repo.
* We specifically use a share offset of `SHARES_OFFSET` and a balance offset of `BALANCE_OFFSET`.
*/
contract StrategyBase is Initializable, Pausable, IStrategy {
using SafeERC20 for IERC20;
uint8 internal constant PAUSED_DEPOSITS = 0;
uint8 internal constant PAUSED_WITHDRAWALS = 1;
/**
* @notice virtual shares used as part of the mitigation of the common 'share inflation' attack vector.
* Constant value chosen to reasonably reduce attempted share inflation by the first depositor, while still
* incurring reasonably small losses to depositors
*/
uint256 internal constant SHARES_OFFSET = 1e3;
/**
* @notice virtual balance used as part of the mitigation of the common 'share inflation' attack vector
* Constant value chosen to reasonably reduce attempted share inflation by the first depositor, while still
* incurring reasonably small losses to depositors
*/
uint256 internal constant BALANCE_OFFSET = 1e3;
/**
* @notice The maximum total shares for a given strategy
* @dev This constant prevents overflow in offchain services for rewards
*/
uint256 internal constant MAX_TOTAL_SHARES = 1e38 - 1;
/// @notice EigenLayer's StrategyManager contract
IStrategyManager public immutable strategyManager;
/// @notice The underlying token for shares in this Strategy
IERC20 public underlyingToken;
/// @notice The total number of extant shares in this Strategy
uint256 public totalShares;
/// @notice Simply checks that the `msg.sender` is the `strategyManager`, which is an address stored immutably at construction.
modifier onlyStrategyManager() {
require(msg.sender == address(strategyManager), OnlyStrategyManager());
_;
}
/// @notice Since this contract is designed to be initializable, the constructor simply sets `strategyManager`, the only immutable variable.
constructor(IStrategyManager _strategyManager, IPauserRegistry _pauserRegistry) Pausable(_pauserRegistry) {
strategyManager = _strategyManager;
_disableInitializers();
}
function initialize(
IERC20 _underlyingToken
) public virtual initializer {
_initializeStrategyBase(_underlyingToken);
}
/// @notice Sets the `underlyingToken` and `pauserRegistry` for the strategy.
function _initializeStrategyBase(
IERC20 _underlyingToken
) internal onlyInitializing {
underlyingToken = _underlyingToken;
_setPausedStatus(_UNPAUSE_ALL);
emit StrategyTokenSet(underlyingToken, IERC20Metadata(address(_underlyingToken)).decimals());
}
/**
* @notice Used to deposit tokens into this Strategy
* @param token is the ERC20 token being deposited
* @param amount is the amount of token being deposited
* @dev This function is only callable by the strategyManager contract. It is invoked inside of the strategyManager's
* `depositIntoStrategy` function, and individual share balances are recorded in the strategyManager as well.
* @dev Note that the assumption is made that `amount` of `token` has already been transferred directly to this contract
* (as performed in the StrategyManager's deposit functions). In particular, setting the `underlyingToken` of this contract
* to be a fee-on-transfer token will break the assumption that the amount this contract *received* of the token is equal to
* the amount that was input when the transfer was performed (i.e. the amount transferred 'out' of the depositor's balance).
* @dev Note that any validation of `token` is done inside `_beforeDeposit`. This can be overridden if needed.
* @return newShares is the number of new shares issued at the current exchange ratio.
*/
function deposit(
IERC20 token,
uint256 amount
) external virtual override onlyWhenNotPaused(PAUSED_DEPOSITS) onlyStrategyManager returns (uint256 newShares) {
// call hook to allow for any pre-deposit logic
_beforeDeposit(token, amount);
// copy `totalShares` value to memory, prior to any change
uint256 priorTotalShares = totalShares;
/**
* @notice calculation of newShares *mirrors* `underlyingToShares(amount)`, but is different since the balance of `underlyingToken`
* has already been increased due to the `strategyManager` transferring tokens to this strategy prior to calling this function
*/
// account for virtual shares and balance
uint256 virtualShareAmount = priorTotalShares + SHARES_OFFSET;
uint256 virtualTokenBalance = _tokenBalance() + BALANCE_OFFSET;
// calculate the prior virtual balance to account for the tokens that were already transferred to this contract
uint256 virtualPriorTokenBalance = virtualTokenBalance - amount;
newShares = (amount * virtualShareAmount) / virtualPriorTokenBalance;
// extra check for correctness / against edge case where share rate can be massively inflated as a 'griefing' sort of attack
require(newShares != 0, NewSharesZero());
// update total share amount to account for deposit
totalShares = (priorTotalShares + newShares);
require(totalShares <= MAX_TOTAL_SHARES, TotalSharesExceedsMax());
// emit exchange rate
_emitExchangeRate(virtualTokenBalance, totalShares + SHARES_OFFSET);
return newShares;
}
/**
* @notice Used to withdraw tokens from this Strategy, to the `recipient`'s address
* @param recipient is the address to receive the withdrawn funds
* @param token is the ERC20 token being transferred out
* @param amountShares is the amount of shares being withdrawn
* @dev This function is only callable by the strategyManager contract. It is invoked inside of the strategyManager's
* other functions, and individual share balances are recorded in the strategyManager as well.
* @dev Note that any validation of `token` is done inside `_beforeWithdrawal`. This can be overridden if needed.
*/
function withdraw(
address recipient,
IERC20 token,
uint256 amountShares
) external virtual override onlyWhenNotPaused(PAUSED_WITHDRAWALS) onlyStrategyManager {
// call hook to allow for any pre-withdrawal logic
_beforeWithdrawal(recipient, token, amountShares);
// copy `totalShares` value to memory, prior to any change
uint256 priorTotalShares = totalShares;
require(amountShares <= priorTotalShares, WithdrawalAmountExceedsTotalDeposits());
/**
* @notice calculation of amountToSend *mirrors* `sharesToUnderlying(amountShares)`, but is different since the `totalShares` has already
* been decremented. Specifically, notice how we use `priorTotalShares` here instead of `totalShares`.
*/
// account for virtual shares and balance
uint256 virtualPriorTotalShares = priorTotalShares + SHARES_OFFSET;
uint256 virtualTokenBalance = _tokenBalance() + BALANCE_OFFSET;
// calculate ratio based on virtual shares and balance, being careful to multiply before dividing
uint256 amountToSend = (virtualTokenBalance * amountShares) / virtualPriorTotalShares;
// Decrease the `totalShares` value to reflect the withdrawal
totalShares = priorTotalShares - amountShares;
// emit exchange rate
_emitExchangeRate(virtualTokenBalance - amountToSend, totalShares + SHARES_OFFSET);
_afterWithdrawal(recipient, token, amountToSend);
}
/**
* @notice Called in the external `deposit` function, before any logic is executed. Expected to be overridden if strategies want such logic.
* @param token The token being deposited
*/
function _beforeDeposit(
IERC20 token,
uint256 // amount
) internal virtual {
require(token == underlyingToken, OnlyUnderlyingToken());
}
/**
* @notice Called in the external `withdraw` function, before any logic is executed. Expected to be overridden if strategies want such logic.
* @param token The token being withdrawn
*/
function _beforeWithdrawal(
address, // recipient
IERC20 token,
uint256 // amountShares
) internal virtual {
require(token == underlyingToken, OnlyUnderlyingToken());
}
/**
* @notice Transfers tokens to the recipient after a withdrawal is processed
* @dev Called in the external `withdraw` function after all logic is executed
* @param recipient The destination of the tokens
* @param token The ERC20 being transferred
* @param amountToSend The amount of `token` to transfer
*/
function _afterWithdrawal(address recipient, IERC20 token, uint256 amountToSend) internal virtual {
token.safeTransfer(recipient, amountToSend);
}
/**
* @notice Currently returns a brief string explaining the strategy's goal & purpose, but for more complex
* strategies, may be a link to metadata that explains in more detail.
*/
function explanation() external pure virtual override returns (string memory) {
return "Base Strategy implementation to inherit from for more complex implementations";
}
/**
* @notice Used to convert a number of shares to the equivalent amount of underlying tokens for this strategy.
* @notice In contrast to `sharesToUnderlying`, this function guarantees no state modifications
* @param amountShares is the amount of shares to calculate its conversion into the underlying token
* @return The amount of underlying tokens corresponding to the input `amountShares`
* @dev Implementation for these functions in particular may vary significantly for different strategies
*/
function sharesToUnderlyingView(
uint256 amountShares
) public view virtual override returns (uint256) {
// account for virtual shares and balance
uint256 virtualTotalShares = totalShares + SHARES_OFFSET;
uint256 virtualTokenBalance = _tokenBalance() + BALANCE_OFFSET;
// calculate ratio based on virtual shares and balance, being careful to multiply before dividing
return (virtualTokenBalance * amountShares) / virtualTotalShares;
}
/**
* @notice Used to convert a number of shares to the equivalent amount of underlying tokens for this strategy.
* @notice In contrast to `sharesToUnderlyingView`, this function **may** make state modifications
* @param amountShares is the amount of shares to calculate its conversion into the underlying token
* @return The amount of underlying tokens corresponding to the input `amountShares`
* @dev Implementation for these functions in particular may vary significantly for different strategies
*/
function sharesToUnderlying(
uint256 amountShares
) public view virtual override returns (uint256) {
return sharesToUnderlyingView(amountShares);
}
/**
* @notice Used to convert an amount of underlying tokens to the equivalent amount of shares in this strategy.
* @notice In contrast to `underlyingToShares`, this function guarantees no state modifications
* @param amountUnderlying is the amount of `underlyingToken` to calculate its conversion into strategy shares
* @return The amount of shares corresponding to the input `amountUnderlying`
* @dev Implementation for these functions in particular may vary significantly for different strategies
*/
function underlyingToSharesView(
uint256 amountUnderlying
) public view virtual returns (uint256) {
// account for virtual shares and balance
uint256 virtualTotalShares = totalShares + SHARES_OFFSET;
uint256 virtualTokenBalance = _tokenBalance() + BALANCE_OFFSET;
// calculate ratio based on virtual shares and balance, being careful to multiply before dividing
return (amountUnderlying * virtualTotalShares) / virtualTokenBalance;
}
/**
* @notice Used to convert an amount of underlying tokens to the equivalent amount of shares in this strategy.
* @notice In contrast to `underlyingToSharesView`, this function **may** make state modifications
* @param amountUnderlying is the amount of `underlyingToken` to calculate its conversion into strategy shares
* @return The amount of shares corresponding to the input `amountUnderlying`
* @dev Implementation for these functions in particular may vary significantly for different strategies
*/
function underlyingToShares(
uint256 amountUnderlying
) external view virtual returns (uint256) {
return underlyingToSharesView(amountUnderlying);
}
/**
* @notice convenience function for fetching the current underlying value of all of the `user`'s shares in
* this strategy. In contrast to `userUnderlying`, this function guarantees no state modifications
*/
function userUnderlyingView(
address user
) external view virtual returns (uint256) {
return sharesToUnderlyingView(shares(user));
}
/**
* @notice convenience function for fetching the current underlying value of all of the `user`'s shares in
* this strategy. In contrast to `userUnderlyingView`, this function **may** make state modifications
*/
function userUnderlying(
address user
) external virtual returns (uint256) {
return sharesToUnderlying(shares(user));
}
/**
* @notice convenience function for fetching the current total shares of `user` in this strategy, by
* querying the `strategyManager` contract
*/
function shares(
address user
) public view virtual returns (uint256) {
return strategyManager.stakerDepositShares(user, IStrategy(address(this)));
}
/// @notice Internal function used to fetch this contract's current balance of `underlyingToken`.
// slither-disable-next-line dead-code
function _tokenBalance() internal view virtual returns (uint256) {
return underlyingToken.balanceOf(address(this));
}
/// @notice Internal function used to emit the exchange rate of the strategy in wad (18 decimals)
/// @dev Tokens that do not have 18 decimals must have offchain services scale the exchange rate down to proper magnitude
function _emitExchangeRate(uint256 virtualTokenBalance, uint256 virtualTotalShares) internal {
// Emit asset over shares ratio.
emit ExchangeRateEmitted((1e18 * virtualTokenBalance) / virtualTotalShares);
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[48] private __gap;
}