-
Notifications
You must be signed in to change notification settings - Fork 270
Expand file tree
/
Copy pathLido.sol
More file actions
1683 lines (1420 loc) · 72.7 KB
/
Lido.sol
File metadata and controls
1683 lines (1420 loc) · 72.7 KB
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
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0
/* See contracts/COMPILERS.md */
pragma solidity 0.4.24;
import {AragonApp, UnstructuredStorage} from "@aragon/os/contracts/apps/AragonApp.sol";
import {SafeMath} from "@aragon/os/contracts/lib/math/SafeMath.sol";
import {ILidoLocator} from "../common/interfaces/ILidoLocator.sol";
import {StETHPermit} from "./StETHPermit.sol";
import {Versioned} from "./utils/Versioned.sol";
import {StakeLimitUtils, StakeLimitUnstructuredStorage, StakeLimitState} from "./lib/StakeLimitUtils.sol";
import {UnstructuredStorageExt} from "./utils/UnstructuredStorageExt.sol";
import {Math256} from "../common/lib/Math256.sol";
interface IStakingRouter {
function getTotalFeeE4Precision() external view returns (uint16 totalFee);
function TOTAL_BASIS_POINTS() external view returns (uint256);
function getWithdrawalCredentials() external view returns (bytes32);
function getStakingFeeAggregateDistributionE4Precision()
external
view
returns (uint16 modulesFee, uint16 treasuryFee);
function receiveDepositableEther() external payable;
}
interface IWithdrawalQueue {
function unfinalizedStETH() external view returns (uint256);
function isBunkerModeActive() external view returns (bool);
function isPaused() external view returns (bool);
function finalize(uint256 _lastIdToFinalize, uint256 _maxShareRate) external payable;
}
interface IRedeemsBuffer {
function fundReserve() external payable;
function withdrawUnredeemed() external;
function getRedeemedEther() external view returns (uint256);
}
interface ILidoExecutionLayerRewardsVault {
function withdrawRewards(uint256 _maxAmount) external returns (uint256 amount);
}
interface IWithdrawalVault {
function withdrawWithdrawals(uint256 _amount) external;
}
interface IAccountingOracle {
/// @dev returns a tuple instead of a structure to avoid allocating memory
function getProcessingState()
external
view
returns (
uint256 currentFrameRefSlot,
uint256 processingDeadlineTime,
bytes32 mainDataHash,
bool mainDataSubmitted,
bytes32 extraDataHash,
uint256 extraDataFormat,
bool extraDataSubmitted,
uint256 extraDataItemsCount,
uint256 extraDataItemsSubmitted
);
function getLastProcessingRefSlot() external view returns (uint256);
}
/**
* @title Liquid staking pool implementation
*
* Lido is an Ethereum liquid staking protocol solving the problem of frozen staked ether on the Consensus Layer
* being unavailable for transfers and DeFi on the Execution Layer.
*
* 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 the Lido oracle reports
* rewards, no `Transfer` events are emitted: doing so would require an event for each token holder
* and thus running an unbounded loop.
*
* ######### STRUCTURED STORAGE #########
* NB: The order of inheritance must preserve the structured storage layout of the previous versions.
*
* @dev Lido is derived from `StETHPermit` that has a structured storage:
* SLOT 0: mapping (address => uint256) private shares (`StETH`)
* SLOT 1: mapping (address => mapping (address => uint256)) private allowances (`StETH`)
* SLOT 2: mapping (address => uint256) internal noncesByAddress (`StETHPermit`)
*
* `Versioned` and `AragonApp` both don't have the pre-allocated structured storage.
*/
contract Lido is Versioned, StETHPermit, AragonApp {
using SafeMath for uint256;
using UnstructuredStorage for bytes32;
using UnstructuredStorageExt for bytes32;
using StakeLimitUnstructuredStorage for bytes32;
using StakeLimitUtils for StakeLimitState.Data;
/// ACL Roles
bytes32 public constant PAUSE_ROLE = 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d; // keccak256("PAUSE_ROLE");
bytes32 public constant RESUME_ROLE = 0x2fc10cc8ae19568712f7a176fb4978616a610650813c9d05326c34abb62749c7; // keccak256("RESUME_ROLE");
bytes32 public constant STAKING_PAUSE_ROLE = 0x84ea57490227bc2be925c684e2a367071d69890b629590198f4125a018eb1de8; // keccak256("STAKING_PAUSE_ROLE")
bytes32 public constant STAKING_CONTROL_ROLE = 0xa42eee1333c0758ba72be38e728b6dadb32ea767de5b4ddbaea1dae85b1b051f; // keccak256("STAKING_CONTROL_ROLE")
bytes32 public constant BUFFER_RESERVE_MANAGER_ROLE =
0x33969636f1fbf3d7d062d4de4a08e7bd3c46606ec28b3a4398d2665be559b921; // keccak256("BUFFER_RESERVE_MANAGER_ROLE")
uint256 private constant DEPOSIT_SIZE = 32 ether;
uint256 internal constant TOTAL_BASIS_POINTS = 10000;
/// @dev storage slot position for the total and external shares (from StETH contract)
/// Since version 3, high 128 bits are used for the external shares
/// |----- 128 bit -----|------ 128 bit -------|
/// | external shares | total shares |
/// keccak256("lido.StETH.totalAndExternalShares")
bytes32 internal constant TOTAL_AND_EXTERNAL_SHARES_POSITION = TOTAL_SHARES_POSITION_LOW128;
/// @dev storage slot position for the Lido protocol contracts locator
/// Since version 3, high 96 bits are used for the max external ratio BP
/// |----- 96 bit -----|------ 160 bit -------|
/// |max external ratio| lido locator address |
/// keccak256("lido.Lido.lidoLocatorAndMaxExternalRatio")
bytes32 internal constant LOCATOR_AND_MAX_EXTERNAL_RATIO_POSITION =
0xd92bc31601d11a10411d08f59b7146d8a5915af253cde25f8e66b67beb4be223;
/// @dev amount of ether (on the current Ethereum side) buffered on this smart contract balance
/// and amount of ether deposited since last report
/// depositedPostReport lifecycle:
/// 1) increased by `withdrawDepositableEther()` as CL deposits are performed;
/// 2) resets on report processing via `processClStateUpdate()`
/// |------ 128 bit --------|----- 128 bit ------|
/// | deposited post report | buffered ether |
/// keccak256("lido.Lido.bufferedEtherAndDepositedPostReport");
bytes32 internal constant BUFFERED_ETHER_AND_DEPOSITED_POST_REPORT_POSITION =
0x81a11fa1111afa59b50051f60ccf604a39d96acb484dc467ad8eadb4a63f0a5f;
/// @dev CL validators balance and deposited balance since last report
/// |----- 128 bit ------------|------ 128 bit -------|
/// | CL validators balance | CL pending balance |
/// keccak256("lido.Lido.clValidatorsBalanceAndClPendingBalance");
bytes32 internal constant CL_VALIDATORS_BALANCE_AND_CL_PENDING_BALANCE_POSITION =
0x096e465397f38e659238ccd5d5a2c434ced54a63fd8d694045bfb058ab9d8112;
/// @dev number of initial seed deposits (incrementing counter), ex. deposited validators
/// keccak256("lido.Lido.seedDepositsCount");
bytes32 internal constant SEED_DEPOSITS_COUNT_POSITION =
0x3f0eaa2c0f16ff9775c078f3df30470d8c042317b24ad1defa240b1c3e10b238;
/// @dev storage slot position of the staking rate limit structure
/// keccak256("lido.Lido.stakeLimit");
bytes32 internal constant STAKING_STATE_POSITION =
0xa3678de4a579be090bed1177e0a24f77cc29d181ac22fd7688aca344d8938015;
/// @dev storage slot position for the total amount of execution layer rewards received by Lido contract.
/// keccak256("lido.Lido.totalELRewardsCollected");
bytes32 internal constant TOTAL_EL_REWARDS_COLLECTED_POSITION =
0xafe016039542d12eec0183bb0b1ffc2ca45b027126a494672fba4154ee77facb;
/// @dev Storage slot for deposits reserve.
/// Holds buffered ether that remains depositable even when withdrawals demand exists.
/// Lifecycle:
/// 1) can be decreased by `setDepositsReserveTarget()` when target is lowered;
/// 2) consumed by `withdrawDepositableEther()` as CL deposits are performed;
/// 3) synced to target on report processing via `_updateBufferedEtherAllocation()`
/// keccak256("lido.Lido.depositsReserve")
bytes32 internal constant DEPOSITS_RESERVE_POSITION =
0xda4fbe3b9cbd98dfae5dff538bbff4ba61f38979d4d7419bcd006f3e6250ec13;
/// @dev Storage slot for deposits reserve target.
/// Stores governance-configured value that deposits reserve is restored to on each oracle report.
/// Set via `setDepositsReserveTarget()`, gated by `BUFFER_RESERVE_MANAGER_ROLE`
/// keccak256("lido.Lido.depositsReserveTarget")
bytes32 internal constant DEPOSITS_RESERVE_TARGET_POSITION =
0x3d3e9bd6e90e5d1f1c6839835bcbe5746a47c9a013d1eae6e80c248264c06a81;
/// @dev Storage slot for redeems reserve target ratio.
/// Stores governance-configured ratio (in basis points) of internal ether.
/// Set via `setRedeemsReserveTargetRatio()`, gated by `BUFFER_RESERVE_MANAGER_ROLE`
/// keccak256("lido.Lido.redeemsReserveTargetRatio")
bytes32 internal constant REDEEMS_RESERVE_TARGET_RATIO_POSITION =
0xa3ab8c45cc56567e890b52bdd4f310aacdc4a7b9a4384808e34fb3b77524a729;
/// @dev Storage slot for the RedeemsBuffer contract address.
/// The buffer physically holds reserve ETH. Set via `setRedeemsBuffer()`.
/// keccak256("lido.Lido.redeemsBuffer")
bytes32 internal constant REDEEMS_BUFFER_POSITION =
0x8f3a06db2c1a07e3a5e3b5e3bfc1e2b4c7a8f9d0e1c2b3a4f5e6d7c8b9a0f1e2;
/// @dev Storage slot for the redeems reserve snapshot (absolute ETH value).
/// Snapshotted during oracle reports via `_growRedeemsReserve()`.
/// Between reports, `bufferedEther` includes this amount even though ETH is
/// physically on the RedeemsBuffer contract. The allocation priority protects
/// the reserve from CL deposits and WQ finalization.
/// keccak256("lido.Lido.redeemsReserve")
bytes32 internal constant REDEEMS_RESERVE_POSITION =
0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2;
/// @dev Storage slot for redeems reserve growth share.
/// Basis points determining how shared allocation (withdrawalsReserve + unreserved) is split
/// between reserve growth and WQ finalization when surplus is insufficient.
/// 0 (default) = reserve grows only from surplus. 8000 = 80% to reserve, 20% to WQ.
/// keccak256("lido.Lido.redeemsReserveGrowthShare")
bytes32 internal constant REDEEMS_RESERVE_GROWTH_SHARE_POSITION =
0x165efeb2acd150f40e68b22ff2e9492cf5007021f951e4109c407f04e4e36129;
// Staking was paused (don't accept user's ether submits)
event StakingPaused();
// Staking was resumed (accept user's ether submits)
event StakingResumed();
// Staking limit was set (rate limits user's submits)
event StakingLimitSet(uint256 maxStakeLimit, uint256 stakeLimitIncreasePerBlock);
// Staking limit was removed
event StakingLimitRemoved();
// Emitted when CL balances are updated by the oracle
event CLBalancesUpdated(uint256 indexed reportTimestamp, uint256 clValidatorsBalance, uint256 clPendingBalance);
// Emitted when CL pending balance is updated during deposits to CL
event DepositedPostReportUpdated(uint256 depositedPostReport);
// Emitted when depositedValidators value is changed
event DepositedValidatorsChanged(uint256 depositedValidators);
// Emitted when oracle accounting report processed
// @dev `preCLBalance` is actually the principal CL balance: the sum of the previous report's
// CL validators balance, CL pending balance, and deposited balance since the last report.
// The parameter name is kept for ABI backward compatibility.
event ETHDistributed(
uint256 indexed reportTimestamp,
uint256 preCLBalance, // actually its preCLBalance + deposits due to compatibility reasons
uint256 postCLBalance,
uint256 withdrawalsWithdrawn,
uint256 executionLayerRewardsWithdrawn,
uint256 postBufferedEther
);
// Emitted when the token is rebased (an accounting oracle report is delivered)
event TokenRebased(
uint256 indexed reportTimestamp,
uint256 timeElapsed,
uint256 preTotalShares,
uint256 preTotalEther,
uint256 postTotalShares,
uint256 postTotalEther,
uint256 sharesMintedAsFees
);
// Lido locator set
event LidoLocatorSet(address lidoLocator);
// The amount of ETH withdrawn from LidoExecutionLayerRewardsVault to Lido
event ELRewardsReceived(uint256 amount);
// The amount of ETH withdrawn from WithdrawalVault to Lido
event WithdrawalsReceived(uint256 amount);
// Records a deposit made by a user
event Submitted(address indexed sender, uint256 amount, address referral);
// The `amount` of ether was sent to the deposit_contract.deposit function
event Unbuffered(uint256 amount);
// Internal share rate updated
event InternalShareRateUpdated(
uint256 indexed reportTimestamp,
uint256 postInternalShares,
uint256 postInternalEther,
uint256 sharesMintedAsFees
);
// External shares minted for receiver
event ExternalSharesMinted(address indexed receiver, uint256 amountOfShares);
// External shares burned for account
event ExternalSharesBurnt(uint256 amountOfShares);
// Maximum ratio of external shares to total shares in basis points set
event MaxExternalRatioBPSet(uint256 maxExternalRatioBP);
// External ether transferred to buffer
event ExternalEtherTransferredToBuffer(uint256 amount);
// Bad debt internalized
event ExternalBadDebtInternalized(uint256 amountOfShares);
// Emitted when current deposits reserve is updated.
// Can be emitted from `withdrawDepositableEther()`, `collectRewardsAndProcessWithdrawals()`,
// and `setDepositsReserveTarget()` when target is lowered below current reserve.
event DepositsReserveSet(uint256 depositsReserve);
// Emitted when deposits reserve target is set via `setDepositsReserveTarget()`.
// Emitted even if the new value equals the previous one
event DepositsReserveTargetSet(uint256 depositsReserveTarget);
// Emitted when redeems reserve target ratio is set via `setRedeemsReserveTargetRatio()`
event RedeemsReserveTargetRatioSet(uint256 ratioBP);
// Emitted when redeems reserve replenishment share is set
event RedeemsReserveGrowthShareSet(uint256 shareBP);
// Emitted when the RedeemsBuffer address is set
event RedeemsBufferSet(address buffer);
// Emitted when the redeems reserve snapshot is updated
event RedeemsReserveSet(uint256 reserve);
// Emitted when ETH is returned from RedeemsBuffer
event RedeemsBufferDrained(uint256 amount);
/**
* @notice Initializer function for scratch deploy of Lido contract
*
* @param _lidoLocator lido locator contract
* @param _eip712StETH eip712 helper contract for StETH
*
* @dev NB: by default, staking and the whole Lido pool are in paused state
* @dev The contract's balance must be non-zero to mint initial shares of stETH
*/
function initialize(address _lidoLocator, address _eip712StETH) public payable onlyInit {
_bootstrapInitialHolder(); // stone in the elevator
_setLidoLocator(_lidoLocator);
emit LidoLocatorSet(_lidoLocator);
_initializeEIP712StETH(_eip712StETH);
_setContractVersion(4);
ILidoLocator locator = ILidoLocator(_lidoLocator);
_approve(_withdrawalQueue(locator), _burner(locator), INFINITE_ALLOWANCE);
initialized();
}
/**
* @notice A function to finalize upgrade to v4 (from v3). Can be called only once
*/
function finalizeUpgrade_v4() external {
require(hasInitialized(), "NOT_INITIALIZED");
_checkContractVersion(3);
_setContractVersion(4);
_migrateStorage_v3_to_v4();
}
function _migrateStorage_v3_to_v4() internal {
/// @dev prevent migration if the last oracle report wasn't submitted, otherwise deposits
/// made after refSlot and before migration (and report's tx) will be lost
require(_isOracleMainDataSubmitted(), "NO_REPORT");
/// @dev storage slots used in v3
// keccak256("lido.Lido.clBalanceAndClValidators")
bytes32 CL_BALANCE_AND_CL_VALIDATORS_POSITION =
0xc36804a03ec742b57b141e4e5d8d3bd1ddb08451fd0f9983af8aaab357a78e2f;
// keccak256("lido.Lido.bufferedEtherAndDepositedValidators");
bytes32 BUFFERED_ETHER_AND_DEPOSITED_VALIDATORS_POSITION =
0xa84c096ee27e195f25d7b6c7c2a03229e49f1a2a5087e57ce7d7127707942fe3;
(uint256 clValidatorsBalance, uint256 clValidators) =
CL_BALANCE_AND_CL_VALIDATORS_POSITION.getLowAndHighUint128();
(uint256 bufferedEther, uint256 depositedValidators) =
BUFFERED_ETHER_AND_DEPOSITED_VALIDATORS_POSITION.getLowAndHighUint128();
/// @dev convert ex-transientBalance to amount submitted to the Deposit contract
/// after the last accounting oracle report
uint256 depositedPostReport = (depositedValidators - clValidators) * DEPOSIT_SIZE;
_setBufferedEtherAndDepositedPostReport(bufferedEther, depositedPostReport);
/// @dev no pending balance at the moment of upgrade
_setClValidatorsBalanceAndClPendingBalance(clValidatorsBalance, 0);
_setSeedDepositsCount(depositedValidators);
// wipe out the slots
CL_BALANCE_AND_CL_VALIDATORS_POSITION.setStorageUint256(0);
BUFFERED_ETHER_AND_DEPOSITED_VALIDATORS_POSITION.setStorageUint256(0);
}
/**
* @notice Stop accepting new ether to the protocol
*
* @dev While accepting new ether is stopped, calls to the `submit` function,
* as well as to the default payable function, will revert.
*/
function pauseStaking() external {
_auth(STAKING_PAUSE_ROLE);
require(!isStakingPaused(), "ALREADY_PAUSED");
_pauseStaking();
}
/**
* @notice Resume accepting new ether to the protocol (if `pauseStaking` was called previously)
* NB: Staking could be rate-limited by imposing a limit on the stake amount
* at each moment in time, see `setStakingLimit()` and `removeStakingLimit()`
*
* @dev Preserves staking limit if it was set previously
*/
function resumeStaking() external {
_auth(STAKING_CONTROL_ROLE);
require(hasInitialized(), "NOT_INITIALIZED");
_whenNotStopped();
require(isStakingPaused(), "ALREADY_RESUMED");
_resumeStaking();
}
/**
* @notice Set the staking rate limit
*
* ▲ Stake limit
* │..... ..... ........ ... .... ... Stake limit = max
* │ . . . . . . . . .
* │ . . . . . . . . .
* │ . . . . .
* │──────────────────────────────────────────────────> Time
* │ ^ ^ ^ ^^^ ^ ^ ^ ^^^ ^ Stake events
*
* @dev Reverts if:
* - `_maxStakeLimit` == 0
* - `_maxStakeLimit` >= 2^95 (1/2 of uint96)
* - `_maxStakeLimit` < `_stakeLimitIncreasePerBlock`
* - `_maxStakeLimit` / `_stakeLimitIncreasePerBlock` >= 2^32 (only if `_stakeLimitIncreasePerBlock` != 0)
*
* @param _maxStakeLimit max stake limit value
* @param _stakeLimitIncreasePerBlock stake limit increase per single block
*/
function setStakingLimit(uint256 _maxStakeLimit, uint256 _stakeLimitIncreasePerBlock) external {
_auth(STAKING_CONTROL_ROLE);
require(_maxStakeLimit <= uint96(-1) / 2, "TOO_LARGE_MAX_STAKE_LIMIT");
STAKING_STATE_POSITION.setStorageStakeLimitStruct(
STAKING_STATE_POSITION.getStorageStakeLimitStruct()
.setStakingLimit(_maxStakeLimit, _stakeLimitIncreasePerBlock)
);
emit StakingLimitSet(_maxStakeLimit, _stakeLimitIncreasePerBlock);
}
/**
* @notice Remove the staking rate limit
*/
function removeStakingLimit() external {
_auth(STAKING_CONTROL_ROLE);
STAKING_STATE_POSITION.setStorageStakeLimitStruct(
STAKING_STATE_POSITION.getStorageStakeLimitStruct().removeStakingLimit()
);
emit StakingLimitRemoved();
}
/**
* @notice Check staking state: whether it's paused or not
*/
function isStakingPaused() public view returns (bool) {
return STAKING_STATE_POSITION.getStorageStakeLimitStruct().isStakingPaused();
}
/**
* @return the maximum amount of ether that can be staked in the current block
* @dev Special return values:
* - 2^256 - 1 if staking is unlimited;
* - 0 if staking is paused or if limit is exhausted.
*/
function getCurrentStakeLimit() external view returns (uint256) {
return _getCurrentStakeLimit(STAKING_STATE_POSITION.getStorageStakeLimitStruct());
}
/**
* @notice Get the full info about current stake limit params and state
* @dev Might be used for the advanced integration requests.
* @return isStakingPaused_ staking pause state (equivalent to return of isStakingPaused())
* @return isStakingLimitSet whether the stake limit is set
* @return currentStakeLimit current stake limit (equivalent to return of getCurrentStakeLimit())
* @return maxStakeLimit max stake limit
* @return maxStakeLimitGrowthBlocks blocks needed to restore max stake limit from the fully exhausted state
* @return prevStakeLimit previously reached stake limit
* @return prevStakeBlockNumber previously seen block number
*/
function getStakeLimitFullInfo()
external
view
returns (
bool isStakingPaused_,
bool isStakingLimitSet,
uint256 currentStakeLimit,
uint256 maxStakeLimit,
uint256 maxStakeLimitGrowthBlocks,
uint256 prevStakeLimit,
uint256 prevStakeBlockNumber
)
{
StakeLimitState.Data memory stakeLimitData = STAKING_STATE_POSITION.getStorageStakeLimitStruct();
isStakingPaused_ = stakeLimitData.isStakingPaused();
isStakingLimitSet = stakeLimitData.isStakingLimitSet();
currentStakeLimit = _getCurrentStakeLimit(stakeLimitData);
maxStakeLimit = stakeLimitData.maxStakeLimit;
maxStakeLimitGrowthBlocks = stakeLimitData.maxStakeLimitGrowthBlocks;
prevStakeLimit = stakeLimitData.prevStakeLimit;
prevStakeBlockNumber = stakeLimitData.prevStakeBlockNumber;
}
/**
* @return the maximum allowed external shares ratio as basis points of total shares [0-10000]
*/
function getMaxExternalRatioBP() external view returns (uint256) {
return _getMaxExternalRatioBP();
}
/**
* @notice Set the maximum allowed external shares ratio as basis points of total shares
* @param _maxExternalRatioBP The maximum ratio in basis points [0-10000]
*/
function setMaxExternalRatioBP(uint256 _maxExternalRatioBP) external {
_auth(STAKING_CONTROL_ROLE);
_setMaxExternalRatioBP(_maxExternalRatioBP);
}
/**
* @notice Send funds to the pool and mint StETH to the `msg.sender` address
* @dev Users are able to submit their funds by sending ether to the contract address
* Unlike vanilla Ethereum Deposit contract, accepting only 32-Ether transactions, Lido
* accepts payments of any size. Submitted ether is stored in the buffer until someone calls
* deposit() and pushes it to the Ethereum Deposit contract.
*/
// solhint-disable-next-line no-complex-fallback
function() external payable {
// protection against accidental submissions by calling non-existent function
require(msg.data.length == 0, "NON_EMPTY_DATA");
_submit(0);
}
/**
* @notice Send funds to the pool with the optional `_referral` parameter and mint StETH to the `msg.sender` address
* @param _referral optional referral address
* @return Amount of StETH shares minted
*/
function submit(address _referral) external payable returns (uint256) {
return _submit(_referral);
}
/**
* @notice A payable function for execution layer rewards. Can be called only by `ExecutionLayerRewardsVault`
* @dev We need a dedicated function because funds received by the default payable function
* are treated as a user deposit
*/
function receiveELRewards() external payable {
_auth(_elRewardsVault());
TOTAL_EL_REWARDS_COLLECTED_POSITION.setStorageUint256(getTotalELRewardsCollected().add(msg.value));
emit ELRewardsReceived(msg.value);
}
/**
* @notice A payable function for withdrawals acquisition. Can be called only by `WithdrawalVault`
* @dev We need a dedicated function because funds received by the default payable function
* are treated as a user deposit
*/
function receiveWithdrawals() external payable {
_auth(_withdrawalVault());
emit WithdrawalsReceived(msg.value);
}
/**
* @notice Stop pool routine operations
*/
function stop() external {
_auth(PAUSE_ROLE);
_stop();
_pauseStaking();
}
/**
* @notice Resume pool routine operations
* @dev Staking is resumed after this call using the previously set limits (if any)
*/
function resume() external {
_auth(RESUME_ROLE);
_resume();
_resumeStaking();
}
/**
* @return the amount of ether temporarily buffered on this contract balance
* @dev Buffered balance is kept on the contract from the moment the funds are received from user
* until the moment they are actually sent to the official Deposit contract or used to fulfill withdrawal requests
*/
function getBufferedEther() external view returns (uint256) {
return _getBufferedEther();
}
/**
* @notice Buffered ether split into reserve buckets.
* @param total Total buffered ether, equal to `getBufferedEther()`.
* @param unreserved Buffer remainder after both reserves are filled. Available for additional CL deposits
* beyond the deposits reserve
* @param depositsReserve Buffer portion available for CL deposits, protected from withdrawals demand.
* Resets on each oracle report, decreases via `withdrawDepositableEther()`
* @param withdrawalsReserve Buffer portion allocated to unfinalized withdrawals. Not depositable to CL.
* Zero when all withdrawal requests are finalized
*/
struct BufferedEtherAllocation {
uint256 total;
uint256 redeemsReserve;
uint256 unreserved;
uint256 depositsReserve;
uint256 withdrawalsReserve;
}
/**
* @notice Calculates buffered ether allocation across reserves
* @dev Buffer is split by priority:
*
* 1. redeemsReserve - snapshot, highest priority, protected from deposits and WQ
* 2. depositsReserve - per-frame CL deposit allowance
* 3. withdrawalsReserve - covers unfinalized withdrawal requests
* 4. unreserved - excess, available for additional CL deposits
*
* bufferedEther includes the redeems reserve (soft-reserved, physically on RedeemsBuffer).
* Between reports, bufferedEther > Lido.balance by the reserve amount.
*
* ┌──────────────── bufferedEther ────────────────┐
* ├───────────┬────────────────────┬──────────────┼─────┬──────────────┐
* │▓▓▓▓▓▓▓▓▓▓▓│●●●●●●●●●●●●●●●●●●●●│●●●●●●●●●●●●●●●○○○○○│○○○○○○○○○○○○○○│
* ├───────────┼────────────────────┼──────────────┼─────┼──────────────┤
* └─ Redeems ─┼─ Deposits Reserve ─┼─ WQ Reserve ─┘ ├─ Unreserved ─┘
* Reserve │ └── Unfinalized stETH ┘
* │
* ▓ - physically on RedeemsBuffer (soft-reserved in bufferedEther)
* ● - physically on Lido
* ○ - not covered by Buffered Ether
*/
function _getBufferedEtherAllocation() internal view returns (BufferedEtherAllocation allocation) {
uint256 remaining = _getBufferedEther();
allocation.total = remaining;
allocation.redeemsReserve = Math256.min(remaining, REDEEMS_RESERVE_POSITION.getStorageUint256());
remaining -= allocation.redeemsReserve;
allocation.depositsReserve = Math256.min(remaining, DEPOSITS_RESERVE_POSITION.getStorageUint256());
remaining -= allocation.depositsReserve;
allocation.withdrawalsReserve = Math256.min(remaining, _withdrawalQueue().unfinalizedStETH());
remaining -= allocation.withdrawalsReserve;
allocation.unreserved = remaining;
}
/**
* @notice Returns the current redeems reserve snapshot.
* @dev Snapshotted on each oracle report. Between reports, only redemptions reduce it
* (indirectly — bufferedEther stays the same, but buffer's physical balance decreases).
*/
function getRedeemsReserve() external view returns (uint256) {
return _getBufferedEtherAllocation().redeemsReserve;
}
/**
* @notice Returns the currently effective deposits reserve — buffer portion available for CL deposits, protected
* from withdrawals demand
* @dev Capped by current buffered ether. See `_getBufferedEtherAllocation()`
*/
function getDepositsReserve() external view returns (uint256 depositsReserve) {
return _getBufferedEtherAllocation().depositsReserve;
}
/**
* @dev Stores new deposits reserve value and emits DepositsReserveSet event
*/
function _setDepositsReserve(uint256 _newDepositsReserve) internal {
DEPOSITS_RESERVE_POSITION.setStorageUint256(_newDepositsReserve);
emit DepositsReserveSet(_newDepositsReserve);
}
/**
* @notice Returns the currently effective withdrawals reserve
* @dev This reserve is computed after deposits reserve is applied
* @return Amount reserved to satisfy unfinalized withdrawals
*/
function getWithdrawalsReserve() external view returns (uint256) {
return _getBufferedEtherAllocation().withdrawalsReserve;
}
/**
* @notice Returns configured target for deposits reserve
* @return depositsReserveTarget Configured reserve target in wei
*/
function getDepositsReserveTarget() public view returns (uint256) {
return DEPOSITS_RESERVE_TARGET_POSITION.getStorageUint256();
}
/**
* @notice Sets deposits reserve target
* @dev Always updates target and emits DepositsReserveTargetSet
* If target is lowered below current reserve, reserve is reduced immediately
* If target is increased, reserve is not increased here and is synced on report processing via
* `_updateBufferedEtherAllocation()`
* @param _newDepositsReserveTarget New target value in wei
*/
function setDepositsReserveTarget(uint256 _newDepositsReserveTarget) external {
_auth(BUFFER_RESERVE_MANAGER_ROLE);
DEPOSITS_RESERVE_TARGET_POSITION.setStorageUint256(_newDepositsReserveTarget);
emit DepositsReserveTargetSet(_newDepositsReserveTarget);
uint256 currentDepositsReserve = DEPOSITS_RESERVE_POSITION.getStorageUint256();
// Do not increase reserve mid-frame: this could reduce available ETH for withdrawals finalization
// relative to the report reference slot assumptions. Increases are applied on oracle report processing.
if (_newDepositsReserveTarget < currentDepositsReserve) {
_setDepositsReserve(_newDepositsReserveTarget);
}
}
/**
* @notice Sets redeems reserve target ratio as basis points of internal ether.
* The buffer is replenished to this target on each oracle report.
* @param _ratioBP Target ratio in basis points [0-10000]
*/
function setRedeemsReserveTargetRatio(uint256 _ratioBP) external {
_auth(BUFFER_RESERVE_MANAGER_ROLE);
require(_ratioBP <= TOTAL_BASIS_POINTS, "INVALID_RATIO");
REDEEMS_RESERVE_TARGET_RATIO_POSITION.setStorageUint256(_ratioBP);
emit RedeemsReserveTargetRatioSet(_ratioBP);
}
/**
* @return the redeems reserve target ratio in basis points
*/
function getRedeemsReserveTargetRatio() external view returns (uint256) {
return REDEEMS_RESERVE_TARGET_RATIO_POSITION.getStorageUint256();
}
/**
* @notice Sets the growth share for redeems reserve.
* When unreserved surplus is insufficient to fill the reserve, this share (in BP)
* determines what fraction of the shared allocation (withdrawalsReserve + unreserved)
* goes to reserve growth vs WQ finalization.
* 0 (default) = reserve grows only from genuine surplus. 10000 = 100% to reserve.
* @param _shareBP Growth share in basis points [0-10000]
*/
function setRedeemsReserveGrowthShare(uint256 _shareBP) external {
_auth(BUFFER_RESERVE_MANAGER_ROLE);
require(_shareBP <= TOTAL_BASIS_POINTS, "INVALID_SHARE");
REDEEMS_RESERVE_GROWTH_SHARE_POSITION.setStorageUint256(_shareBP);
emit RedeemsReserveGrowthShareSet(_shareBP);
}
/**
* @return the redeems reserve growth share in basis points
*/
function getRedeemsReserveGrowthShare() external view returns (uint256) {
return REDEEMS_RESERVE_GROWTH_SHARE_POSITION.getStorageUint256();
}
/**
* @notice Returns the current redeems reserve target in absolute ETH,
* computed from the ratio and current internal ether.
*/
function getRedeemsReserveTarget() external view returns (uint256) {
return _getRedeemsReserveTarget();
}
function _getRedeemsReserveTarget() internal view returns (uint256) {
return _getInternalEther()
* REDEEMS_RESERVE_TARGET_RATIO_POSITION.getStorageUint256()
/ TOTAL_BASIS_POINTS;
}
/**
* @notice Sets the RedeemsBuffer contract address
* @dev Setting to address(0) disables reserve; buffer won't receive ETH on reports
* @param _buffer Address of the RedeemsBuffer contract
*/
function setRedeemsBuffer(address _buffer) external {
_auth(BUFFER_RESERVE_MANAGER_ROLE);
REDEEMS_BUFFER_POSITION.setStorageAddress(_buffer);
emit RedeemsBufferSet(_buffer);
}
/**
* @return the RedeemsBuffer address
*/
function getRedeemsBuffer() external view returns (address) {
return REDEEMS_BUFFER_POSITION.getStorageAddress();
}
/**
* @notice Receives ETH back from RedeemsBuffer (unredeemed return on report).
* @dev bufferedEther is NOT modified — ETH was already counted in bufferedEther.
*/
function receiveFromRedeemsBuffer() external payable {
_auth(REDEEMS_BUFFER_POSITION.getStorageAddress());
emit RedeemsBufferDrained(msg.value);
}
function _setRedeemsReserve(uint256 _reserve) internal {
REDEEMS_RESERVE_POSITION.setStorageUint256(_reserve);
emit RedeemsReserveSet(_reserve);
}
/**
* @return the amount of ether held by external sources to back external shares
*/
function getExternalEther() external view returns (uint256) {
return _getExternalEther(_getInternalEther());
}
/**
* @return the total amount of shares backed by external ether sources
*/
function getExternalShares() external view returns (uint256) {
return _getExternalShares();
}
/**
* @return the maximum amount of external shares that can be minted under the current external ratio limit
*/
function getMaxMintableExternalShares() external view returns (uint256) {
return _getMaxMintableExternalShares();
}
/**
* @return the total amount of Execution Layer rewards collected to the Lido contract
* @dev ether received through LidoExecutionLayerRewardsVault is kept on this contract's balance the same way
* as other buffered ether is kept (until it gets deposited or withdrawn)
*/
function getTotalELRewardsCollected() public view returns (uint256) {
return TOTAL_EL_REWARDS_COLLECTED_POSITION.getStorageUint256();
}
/**
* @return the Lido Locator address
*/
function getLidoLocator() external view returns (ILidoLocator) {
return _getLidoLocator();
}
/**
* @dev DEPRECATED: Use getBalanceStats() for new integrations
* @notice Get the key values related to the Consensus Layer side of the contract.
* @return depositedValidators - number of deposited validators from Lido contract side
* @return beaconValidators - number of Lido validators visible on Consensus Layer, reported by oracle
* @return beaconBalance - total amount of ether on the Consensus Layer side (sum of all the balances of Lido validators)
*/
function getBeaconStat()
external
view
returns (uint256 depositedValidators, uint256 beaconValidators, uint256 beaconBalance)
{
depositedValidators = _getSeedDepositsCount();
(uint256 clValidatorsBalance, uint256 clPendingBalance) = _getClValidatorsBalanceAndClPendingBalance();
/// @dev Since there is now no gap between the deposit on EL and its observation on the CL layer,
/// for compatibility, beaconValidators = depositedValidators.
/// @dev beaconBalance returned as sum of active and pending balances because this amounts
/// are visible on the CL side at moment of report
return (depositedValidators, depositedValidators, clValidatorsBalance.add(clPendingBalance));
}
/// @notice Returns current balance statistics
/// @return clValidatorsBalanceAtLastReport Sum of validator's active balances in wei
/// @return clPendingBalanceAtLastReport Sum of validator's pending deposits in wei
/// @return depositedSinceLastReport Deposits made since last oracle report
function getBalanceStats()
external
view
returns (
uint256 clValidatorsBalanceAtLastReport,
uint256 clPendingBalanceAtLastReport,
uint256 depositedSinceLastReport
)
{
(clValidatorsBalanceAtLastReport, clPendingBalanceAtLastReport) = _getClValidatorsBalanceAndClPendingBalance();
depositedSinceLastReport = _getDepositedPostReport();
}
/**
* @notice Check that Lido allows depositing buffered ether to the Consensus Layer
* @dev Depends on the bunker mode and protocol pause state
*/
function canDeposit() public view returns (bool) {
return !_withdrawalQueue().isBunkerModeActive() && !isStopped() && _isOracleMainDataSubmitted();
}
/// @notice Check if the last oracle report was submitted
/// @dev Since deposits at the module level are not tracked separately between Oracle reports but are only
/// accounted for as part of `pendingDeposits`, which are then used in `sanityChecks` when processing
/// a new report, it is necessary to ensure that they remain unchanged from the beginning of a new
/// frame until a new report is received
function _isOracleMainDataSubmitted() internal view returns (bool mainDataSubmitted) {
IAccountingOracle oracle = IAccountingOracle(_getLidoLocator().accountingOracle());
/// @dev get mainDataSubmitted flag from oracle processing state
(,,, mainDataSubmitted,,,,,) = oracle.getProcessingState();
if (!mainDataSubmitted) {
/// @dev allow deposits in case of initial deploy
/// this flow will not be triggered onchain in most cases, so
/// no worry about gas consumption on 2nd call
return oracle.getLastProcessingRefSlot() == 0;
}
}
/**
* @return the amount of ether in the buffer that can be deposited to the Consensus Layer
* @dev Equals buffered ether minus withdrawals reserve from `_getBufferedEtherAllocation()`
*/
function getDepositableEther() external view returns (uint256) {
return _getDepositableEther(_getBufferedEtherAllocation());
}
/**
* @notice Calculates depositable amount from precomputed buffer allocation
* @return Depositable amount, equal to `allocation.depositsReserve + allocation.unreserved`
*/
function _getDepositableEther(BufferedEtherAllocation allocation) internal pure returns (uint256) {
return allocation.depositsReserve + allocation.unreserved;
}
/**
* @dev Spends depositable buffer and updates stored deposits reserve accordingly.
* Decreases stored deposits reserve by spent amount, bounded below by zero
*/
function _spendDepositableEther(uint256 _depositAmount) internal {
BufferedEtherAllocation memory allocation = _getBufferedEtherAllocation();
uint256 depositableEther = _getDepositableEther(allocation);
require(_depositAmount <= depositableEther, "NOT_ENOUGH_ETHER");
/// @dev the requested amount will be sent to DepositContract, so we increment
/// depositedPostReport counter to keep _getInternalEther value correct
uint256 depositedPostReport = _getDepositedPostReport().add(_depositAmount);
_setBufferedEtherAndDepositedPostReport(allocation.total.sub(_depositAmount), depositedPostReport);
emit Unbuffered(_depositAmount);
emit DepositedPostReportUpdated(depositedPostReport);
uint256 storedDepositsReserve = DEPOSITS_RESERVE_POSITION.getStorageUint256();
if (storedDepositsReserve > 0) {
_setDepositsReserve(storedDepositsReserve > _depositAmount ? storedDepositsReserve - _depositAmount : 0);
}
}
/**
* @notice Withdraw `_amount` of buffer to Staking Router
* @dev Can be called only by the Staking Router contract
* @notice _seedDepositsCount - DEPRECATED, it is used only for backward compatibility
*
* @param _amount amount of ETH to withdraw
* @param _seedDepositsCount amount of seed deposits. In case of top up this value will be equal to 0
*/
function withdrawDepositableEther(uint256 _amount, uint256 _seedDepositsCount) external {
require(canDeposit(), "CAN_NOT_DEPOSIT");
IStakingRouter stakingRouter = _stakingRouter();
_auth(address(stakingRouter));
require(_amount != 0, "ZERO_AMOUNT");
_spendDepositableEther(_amount);
if (_seedDepositsCount > 0) {
uint256 newSeedDepositsCount = _getSeedDepositsCount().add(_seedDepositsCount);
_setSeedDepositsCount(newSeedDepositsCount);
/// @dev event name is kept for backward compatibility
emit DepositedValidatorsChanged(newSeedDepositsCount);
}
/// @dev forward the requested amount of ether to the StakingRouter
stakingRouter.receiveDepositableEther.value(_amount)();
}
/**
* @notice Mint stETH shares
* @param _recipient recipient of the shares
* @param _amountOfShares amount of shares to mint
* @dev can be called only by accounting
*/
function mintShares(address _recipient, uint256 _amountOfShares) external {
_auth(_accounting());
_whenNotStopped();
_mintShares(_recipient, _amountOfShares);
_emitTransferAfterMintingShares(_recipient, _amountOfShares);
}
/**
* @notice Burn stETH shares from the `msg.sender` address
* @param _amountOfShares amount of shares to burn
* @dev can be called only by burner
*/
function burnShares(uint256 _amountOfShares) external {
_auth(_burner());
_whenNotStopped();
uint256 preRebaseTokenAmount = getPooledEthByShares(_amountOfShares);
_burnShares(msg.sender, _amountOfShares);