forked from lidofinance/core
-
Notifications
You must be signed in to change notification settings - Fork 0
/
StETH.sol
482 lines (433 loc) · 18.5 KB
/
StETH.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
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
// SPDX-FileCopyrightText: 2020 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0
/* See contracts/COMPILERS.md */
pragma solidity 0.4.24;
import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol";
import "@aragon/os/contracts/common/UnstructuredStorage.sol";
import "@aragon/os/contracts/lib/math/SafeMath.sol";
import "./lib/Pausable.sol";
/**
* @title Interest-bearing ERC20-like token for Lido Liquid Stacking protocol.
*
* This contract is abstract. To make the contract deployable override the
* `_getTotalPooledEther` function. `Lido.sol` contract inherits StETH and defines
* the `_getTotalPooledEther` function.
*
* StETH balances are dynamic and represent the holder's share in the total amount
* of Ether controlled by the protocol. Account shares aren't normalized, so the
* contract also stores the sum of all shares to calculate each account's token balance
* which equals to:
*
* shares[account] * _getTotalPooledEther() / _getTotalShares()
*
* For example, assume that we have:
*
* _getTotalPooledEther() -> 10 ETH
* sharesOf(user1) -> 100
* sharesOf(user2) -> 400
*
* Therefore:
*
* balanceOf(user1) -> 2 tokens which corresponds 2 ETH
* balanceOf(user2) -> 8 tokens which corresponds 8 ETH
*
* Since balances of all token holders change when the amount of total pooled Ether
* changes, this token cannot fully implement ERC20 standard: it only emits `Transfer`
* events upon explicit transfer between holders. In contrast, when total amount of
* pooled Ether increases, no `Transfer` events are generated: doing so would require
* emitting an event for each token holder and thus running an unbounded loop.
*
* The token inherits from `Pausable` and uses `whenNotStopped` modifier for methods
* which change `shares` or `allowances`. `_stop` and `_resume` functions are overridden
* in `Lido.sol` and might be called by an account with the `PAUSE_ROLE` assigned by the
* DAO. This is useful for emergency scenarios, e.g. a protocol bug, where one might want
* to freeze all token transfers and approvals until the emergency is resolved.
*/
contract StETH is IERC20, Pausable {
using SafeMath for uint256;
using UnstructuredStorage for bytes32;
/**
* @dev StETH balances are dynamic and are calculated based on the accounts' shares
* and the total amount of Ether controlled by the protocol. Account shares aren't
* normalized, so the contract also stores the sum of all shares to calculate
* each account's token balance which equals to:
*
* shares[account] * _getTotalPooledEther() / _getTotalShares()
*/
mapping (address => uint256) private shares;
/**
* @dev Allowances are nominated in tokens, not token shares.
*/
mapping (address => mapping (address => uint256)) private allowances;
/**
* @dev Storage position used for holding the total amount of shares in existence.
*
* The Lido protocol is built on top of Aragon and uses the Unstructured Storage pattern
* for value types:
*
* https://blog.openzeppelin.com/upgradeability-using-unstructured-storage
* https://blog.8bitzen.com/posts/20-02-2020-understanding-how-solidity-upgradeable-unstructured-proxies-work
*
* For reference types, conventional storage variables are used since it's non-trivial
* and error-prone to implement reference-type unstructured storage using Solidity v0.4;
* see https://github.com/lidofinance/lido-dao/issues/181#issuecomment-736098834
*/
bytes32 internal constant TOTAL_SHARES_POSITION = keccak256("lido.StETH.totalShares");
/**
* @notice An executed shares transfer from `sender` to `recipient`.
*
* @dev emitted in pair with an ERC20-defined `Transfer` event.
*/
event TransferShares(
address indexed from,
address indexed to,
uint256 sharesValue
);
/**
* @notice An executed `burnShares` request
*
* @dev Reports simultaneously burnt shares amount
* and corresponding stETH amount.
* The stETH amount is calculated twice: before and after the burning incurred rebase.
*
* @param account holder of the burnt shares
* @param preRebaseTokenAmount amount of stETH the burnt shares corresponded to before the burn
* @param postRebaseTokenAmount amount of stETH the burnt shares corresponded to after the burn
* @param sharesAmount amount of burnt shares
*/
event SharesBurnt(
address indexed account,
uint256 preRebaseTokenAmount,
uint256 postRebaseTokenAmount,
uint256 sharesAmount
);
/**
* @return the name of the token.
*/
function name() public pure returns (string) {
return "Liquid staked Ether 2.0";
}
/**
* @return the symbol of the token, usually a shorter version of the
* name.
*/
function symbol() public pure returns (string) {
return "stETH";
}
/**
* @return the number of decimals for getting user representation of a token amount.
*/
function decimals() public pure returns (uint8) {
return 18;
}
/**
* @return the amount of tokens in existence.
*
* @dev Always equals to `_getTotalPooledEther()` since token amount
* is pegged to the total amount of Ether controlled by the protocol.
*/
function totalSupply() public view returns (uint256) {
return _getTotalPooledEther();
}
/**
* @return the entire amount of Ether controlled by the protocol.
*
* @dev The sum of all ETH balances in the protocol, equals to the total supply of stETH.
*/
function getTotalPooledEther() public view returns (uint256) {
return _getTotalPooledEther();
}
/**
* @return the amount of tokens owned by the `_account`.
*
* @dev Balances are dynamic and equal the `_account`'s share in the amount of the
* total Ether controlled by the protocol. See `sharesOf`.
*/
function balanceOf(address _account) public view returns (uint256) {
return getPooledEthByShares(_sharesOf(_account));
}
/**
* @notice Moves `_amount` tokens from the caller's account to the `_recipient` account.
*
* @return a boolean value indicating whether the operation succeeded.
* Emits a `Transfer` event.
* Emits a `TransferShares` event.
*
* Requirements:
*
* - `_recipient` cannot be the zero address.
* - the caller must have a balance of at least `_amount`.
* - the contract must not be paused.
*
* @dev The `_amount` argument is the amount of tokens, not shares.
*/
function transfer(address _recipient, uint256 _amount) public returns (bool) {
_transfer(msg.sender, _recipient, _amount);
return true;
}
/**
* @return the remaining number of tokens that `_spender` is allowed to spend
* on behalf of `_owner` through `transferFrom`. This is zero by default.
*
* @dev This value changes when `approve` or `transferFrom` is called.
*/
function allowance(address _owner, address _spender) public view returns (uint256) {
return allowances[_owner][_spender];
}
/**
* @notice Sets `_amount` as the allowance of `_spender` over the caller's tokens.
*
* @return a boolean value indicating whether the operation succeeded.
* Emits an `Approval` event.
*
* Requirements:
*
* - `_spender` cannot be the zero address.
* - the contract must not be paused.
*
* @dev The `_amount` argument is the amount of tokens, not shares.
*/
function approve(address _spender, uint256 _amount) public returns (bool) {
_approve(msg.sender, _spender, _amount);
return true;
}
/**
* @notice Moves `_amount` tokens from `_sender` to `_recipient` using the
* allowance mechanism. `_amount` is then deducted from the caller's
* allowance.
*
* @return a boolean value indicating whether the operation succeeded.
*
* Emits a `Transfer` event.
* Emits a `TransferShares` event.
* Emits an `Approval` event indicating the updated allowance.
*
* Requirements:
*
* - `_sender` and `_recipient` cannot be the zero addresses.
* - `_sender` must have a balance of at least `_amount`.
* - the caller must have allowance for `_sender`'s tokens of at least `_amount`.
* - the contract must not be paused.
*
* @dev The `_amount` argument is the amount of tokens, not shares.
*/
function transferFrom(address _sender, address _recipient, uint256 _amount) public returns (bool) {
uint256 currentAllowance = allowances[_sender][msg.sender];
require(currentAllowance >= _amount, "TRANSFER_AMOUNT_EXCEEDS_ALLOWANCE");
_transfer(_sender, _recipient, _amount);
_approve(_sender, msg.sender, currentAllowance.sub(_amount));
return true;
}
/**
* @notice Atomically increases the allowance granted to `_spender` by the caller by `_addedValue`.
*
* This is an alternative to `approve` that can be used as a mitigation for
* problems described in:
* https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol#L42
* Emits an `Approval` event indicating the updated allowance.
*
* Requirements:
*
* - `_spender` cannot be the the zero address.
* - the contract must not be paused.
*/
function increaseAllowance(address _spender, uint256 _addedValue) public returns (bool) {
_approve(msg.sender, _spender, allowances[msg.sender][_spender].add(_addedValue));
return true;
}
/**
* @notice Atomically decreases the allowance granted to `_spender` by the caller by `_subtractedValue`.
*
* This is an alternative to `approve` that can be used as a mitigation for
* problems described in:
* https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol#L42
* Emits an `Approval` event indicating the updated allowance.
*
* Requirements:
*
* - `_spender` cannot be the zero address.
* - `_spender` must have allowance for the caller of at least `_subtractedValue`.
* - the contract must not be paused.
*/
function decreaseAllowance(address _spender, uint256 _subtractedValue) public returns (bool) {
uint256 currentAllowance = allowances[msg.sender][_spender];
require(currentAllowance >= _subtractedValue, "DECREASED_ALLOWANCE_BELOW_ZERO");
_approve(msg.sender, _spender, currentAllowance.sub(_subtractedValue));
return true;
}
/**
* @return the total amount of shares in existence.
*
* @dev The sum of all accounts' shares can be an arbitrary number, therefore
* it is necessary to store it in order to calculate each account's relative share.
*/
function getTotalShares() public view returns (uint256) {
return _getTotalShares();
}
/**
* @return the amount of shares owned by `_account`.
*/
function sharesOf(address _account) public view returns (uint256) {
return _sharesOf(_account);
}
/**
* @return the amount of shares that corresponds to `_ethAmount` protocol-controlled Ether.
*/
function getSharesByPooledEth(uint256 _ethAmount) public view returns (uint256) {
uint256 totalPooledEther = _getTotalPooledEther();
if (totalPooledEther == 0) {
return 0;
} else {
return _ethAmount
.mul(_getTotalShares())
.div(totalPooledEther);
}
}
/**
* @return the amount of Ether that corresponds to `_sharesAmount` token shares.
*/
function getPooledEthByShares(uint256 _sharesAmount) public view returns (uint256) {
uint256 totalShares = _getTotalShares();
if (totalShares == 0) {
return 0;
} else {
return _sharesAmount
.mul(_getTotalPooledEther())
.div(totalShares);
}
}
/**
* @notice Moves `_sharesAmount` token shares from the caller's account to the `_recipient` account.
*
* @return amount of transferred tokens.
* Emits a `TransferShares` event.
* Emits a `Transfer` event.
*
* Requirements:
*
* - `_recipient` cannot be the zero address.
* - the caller must have at least `_sharesAmount` shares.
* - the contract must not be paused.
*
* @dev The `_sharesAmount` argument is the amount of shares, not tokens.
*/
function transferShares(address _recipient, uint256 _sharesAmount) public returns (uint256) {
_transferShares(msg.sender, _recipient, _sharesAmount);
emit TransferShares(msg.sender, _recipient, _sharesAmount);
uint256 tokensAmount = getPooledEthByShares(_sharesAmount);
emit Transfer(msg.sender, _recipient, tokensAmount);
return tokensAmount;
}
/**
* @return the total amount (in wei) of Ether controlled by the protocol.
* @dev This is used for calculating tokens from shares and vice versa.
* @dev This function is required to be implemented in a derived contract.
*/
function _getTotalPooledEther() internal view returns (uint256);
/**
* @notice Moves `_amount` tokens from `_sender` to `_recipient`.
* Emits a `Transfer` event.
* Emits a `TransferShares` event.
*/
function _transfer(address _sender, address _recipient, uint256 _amount) internal {
uint256 _sharesToTransfer = getSharesByPooledEth(_amount);
_transferShares(_sender, _recipient, _sharesToTransfer);
emit Transfer(_sender, _recipient, _amount);
emit TransferShares(_sender, _recipient, _sharesToTransfer);
}
/**
* @notice Sets `_amount` as the allowance of `_spender` over the `_owner` s tokens.
*
* Emits an `Approval` event.
*
* Requirements:
*
* - `_owner` cannot be the zero address.
* - `_spender` cannot be the zero address.
* - the contract must not be paused.
*/
function _approve(address _owner, address _spender, uint256 _amount) internal whenNotStopped {
require(_owner != address(0), "APPROVE_FROM_ZERO_ADDRESS");
require(_spender != address(0), "APPROVE_TO_ZERO_ADDRESS");
allowances[_owner][_spender] = _amount;
emit Approval(_owner, _spender, _amount);
}
/**
* @return the total amount of shares in existence.
*/
function _getTotalShares() internal view returns (uint256) {
return TOTAL_SHARES_POSITION.getStorageUint256();
}
/**
* @return the amount of shares owned by `_account`.
*/
function _sharesOf(address _account) internal view returns (uint256) {
return shares[_account];
}
/**
* @notice Moves `_sharesAmount` shares from `_sender` to `_recipient`.
*
* Requirements:
*
* - `_sender` cannot be the zero address.
* - `_recipient` cannot be the zero address.
* - `_sender` must hold at least `_sharesAmount` shares.
* - the contract must not be paused.
*/
function _transferShares(address _sender, address _recipient, uint256 _sharesAmount) internal whenNotStopped {
require(_sender != address(0), "TRANSFER_FROM_THE_ZERO_ADDRESS");
require(_recipient != address(0), "TRANSFER_TO_THE_ZERO_ADDRESS");
uint256 currentSenderShares = shares[_sender];
require(_sharesAmount <= currentSenderShares, "TRANSFER_AMOUNT_EXCEEDS_BALANCE");
shares[_sender] = currentSenderShares.sub(_sharesAmount);
shares[_recipient] = shares[_recipient].add(_sharesAmount);
}
/**
* @notice Creates `_sharesAmount` shares and assigns them to `_recipient`, increasing the total amount of shares.
* @dev This doesn't increase the token total supply.
*
* Requirements:
*
* - `_recipient` cannot be the zero address.
* - the contract must not be paused.
*/
function _mintShares(address _recipient, uint256 _sharesAmount) internal whenNotStopped returns (uint256 newTotalShares) {
require(_recipient != address(0), "MINT_TO_THE_ZERO_ADDRESS");
newTotalShares = _getTotalShares().add(_sharesAmount);
TOTAL_SHARES_POSITION.setStorageUint256(newTotalShares);
shares[_recipient] = shares[_recipient].add(_sharesAmount);
// Notice: we're not emitting a Transfer event from the zero address here since shares mint
// works by taking the amount of tokens corresponding to the minted shares from all other
// token holders, proportionally to their share. The total supply of the token doesn't change
// as the result. This is equivalent to performing a send from each other token holder's
// address to `address`, but we cannot reflect this as it would require sending an unbounded
// number of events.
}
/**
* @notice Destroys `_sharesAmount` shares from `_account`'s holdings, decreasing the total amount of shares.
* @dev This doesn't decrease the token total supply.
*
* Requirements:
*
* - `_account` cannot be the zero address.
* - `_account` must hold at least `_sharesAmount` shares.
* - the contract must not be paused.
*/
function _burnShares(address _account, uint256 _sharesAmount) internal whenNotStopped returns (uint256 newTotalShares) {
require(_account != address(0), "BURN_FROM_THE_ZERO_ADDRESS");
uint256 accountShares = shares[_account];
require(_sharesAmount <= accountShares, "BURN_AMOUNT_EXCEEDS_BALANCE");
uint256 preRebaseTokenAmount = getPooledEthByShares(_sharesAmount);
newTotalShares = _getTotalShares().sub(_sharesAmount);
TOTAL_SHARES_POSITION.setStorageUint256(newTotalShares);
shares[_account] = accountShares.sub(_sharesAmount);
uint256 postRebaseTokenAmount = getPooledEthByShares(_sharesAmount);
emit SharesBurnt(_account, preRebaseTokenAmount, postRebaseTokenAmount, _sharesAmount);
// Notice: we're not emitting a Transfer event to the zero address here since shares burn
// works by redistributing the amount of tokens corresponding to the burned shares between
// all other token holders. The total supply of the token doesn't change as the result.
// This is equivalent to performing a send from `address` to each other token holder address,
// but we cannot reflect this as it would require sending an unbounded number of events.
// We're emitting `SharesBurnt` event to provide an explicit rebase log record nonetheless.
}
}