Skip to content

Commit e575177

Browse files
authored
Merge pull request #624 from lidofinance/feature/shapella-upgrade-followups
Feat: shapella upgrade followups
2 parents 89ad4cc + c161475 commit e575177

File tree

130 files changed

+8747
-3739
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

130 files changed

+8747
-3739
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,7 @@ cli/vendor
4343

4444
# OS relative
4545
.DS_Store
46+
47+
# foundry artifacts
48+
foundry/cache
49+
foundry/out

.gitmodules

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[submodule "foundry/lib/forge-std"]
2+
path = foundry/lib/forge-std
3+
url = https://github.com/foundry-rs/forge-std
4+
branch = v1.3.0

README.md

+9
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ The contract also works as a wrapper that accepts stETH tokens and mints wstETH
105105
* docker
106106
* node.js v12
107107
* (optional) Lerna
108+
* (optional) Foundry
108109

109110
### Installing Aragon & other deps
110111

@@ -239,6 +240,14 @@ so full branch coverage will never be reported until
239240

240241
[solidity-coverage#219]: https://github.com/sc-forks/solidity-coverage/issues/269
241242

243+
Run fuzzing tests with foundry:
244+
245+
```bash
246+
curl -L https://foundry.paradigm.xyz | bash
247+
foundryup
248+
forge test
249+
```
250+
242251
## Deploying
243252

244253
We have several ways to deploy lido smart-contracts and run DAO locally, you can find documents here:

contracts/0.4.24/Lido.sol

+67-93
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ interface IStakingRouter {
8181
uint256 _maxDepositsCount,
8282
uint256 _stakingModuleId,
8383
bytes _depositCalldata
84-
) external payable returns (uint256);
84+
) external payable;
8585

8686
function getStakingRewardsDistribution()
8787
external
@@ -101,6 +101,11 @@ interface IStakingRouter {
101101
function getTotalFeeE4Precision() external view returns (uint16 totalFee);
102102

103103
function getStakingFeeAggregateDistributionE4Precision() external view returns (uint16 modulesFee, uint16 treasuryFee);
104+
105+
function getStakingModuleMaxDepositsCount(uint256 _stakingModuleId, uint256 _depositableEther)
106+
external
107+
view
108+
returns (uint256);
104109
}
105110

106111
interface IWithdrawalQueue {
@@ -160,7 +165,8 @@ contract Lido is Versioned, StETHPermit, AragonApp {
160165

161166
uint256 private constant DEPOSIT_SIZE = 32 ether;
162167
uint256 public constant TOTAL_BASIS_POINTS = 10000;
163-
/// @dev special value for the last finalizable withdrawal request id
168+
/// @dev special value to not finalize withdrawal requests
169+
/// see the `_lastFinalizableRequestId` arg for `handleOracleReport()`
164170
uint256 private constant DONT_FINALIZE_WITHDRAWALS = 0;
165171

166172
/// @dev storage slot position for the Lido protocol contracts locator
@@ -244,18 +250,25 @@ contract Lido is Versioned, StETHPermit, AragonApp {
244250
// The `amount` of ether was sent to the deposit_contract.deposit function
245251
event Unbuffered(uint256 amount);
246252

247-
// The amount of ETH sent from StakingRouter contract to Lido contract when deposit called
248-
event StakingRouterDepositRemainderReceived(uint256 amount);
249-
250253
/**
251254
* @dev As AragonApp, Lido contract must be initialized with following variables:
252255
* NB: by default, staking and the whole Lido pool are in paused state
256+
*
257+
* The contract's balance must be non-zero to allow initial holder bootstrap.
258+
*
253259
* @param _lidoLocator lido locator contract
254260
* @param _eip712StETH eip712 helper contract for StETH
255261
*/
256262
function initialize(address _lidoLocator, address _eip712StETH)
257-
public onlyInit
263+
public
264+
payable
265+
onlyInit
258266
{
267+
uint256 amount = _bootstrapInitialHolder();
268+
BUFFERED_ETHER_POSITION.setStorageUint256(amount);
269+
270+
emit Submitted(INITIAL_TOKEN_HOLDER, amount, 0);
271+
259272
_initialize_v2(_lidoLocator, _eip712StETH);
260273
initialized();
261274
}
@@ -284,15 +297,19 @@ contract Lido is Versioned, StETHPermit, AragonApp {
284297
* @notice A function to finalize upgrade to v2 (from v1). Can be called only once
285298
* @dev Value "1" in CONTRACT_VERSION_POSITION is skipped due to change in numbering
286299
*
300+
* The initial protocol token holder must exist.
301+
*
287302
* For more details see https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-10.md
288303
*/
289304
function finalizeUpgrade_v2(address _lidoLocator, address _eip712StETH) external {
290-
require(hasInitialized(), "NOT_INITIALIZED");
291305
_checkContractVersion(0);
306+
require(hasInitialized(), "NOT_INITIALIZED");
292307

293308
require(_lidoLocator != address(0), "LIDO_LOCATOR_ZERO_ADDRESS");
294309
require(_eip712StETH != address(0), "EIP712_STETH_ZERO_ADDRESS");
295310

311+
require(_sharesOf(INITIAL_TOKEN_HOLDER) != 0, "INITIAL_HOLDER_EXISTS");
312+
296313
_initialize_v2(_lidoLocator, _eip712StETH);
297314
}
298315

@@ -432,7 +449,7 @@ contract Lido is Versioned, StETHPermit, AragonApp {
432449
* accepts payments of any size. Submitted Ethers are stored in Buffer until someone calls
433450
* deposit() and pushes them to the Ethereum Deposit contract.
434451
*/
435-
// solhint-disable-next-line
452+
// solhint-disable-next-line no-complex-fallback
436453
function() external payable {
437454
// protection against accidental submissions by calling non-existent function
438455
require(msg.data.length == 0, "NON_EMPTY_DATA");
@@ -472,17 +489,6 @@ contract Lido is Versioned, StETHPermit, AragonApp {
472489
emit WithdrawalsReceived(msg.value);
473490
}
474491

475-
/**
476-
* @notice A payable function for staking router deposits remainder. Can be called only by `StakingRouter`
477-
* @dev We need a dedicated function because funds received by the default payable function
478-
* are treated as a user deposit
479-
*/
480-
function receiveStakingRouterDepositRemainder() external payable {
481-
require(msg.sender == getLidoLocator().stakingRouter());
482-
483-
emit StakingRouterDepositRemainderReceived(msg.value);
484-
}
485-
486492
/**
487493
* @notice Stop pool routine operations
488494
*/
@@ -550,10 +556,9 @@ contract Lido is Versioned, StETHPermit, AragonApp {
550556
* @param _lastFinalizableRequestId right boundary of requestId range if equals 0, no requests should be finalized
551557
* @param _simulatedShareRate share rate that was simulated by oracle when the report data created (1e27 precision)
552558
*
553-
* NB: `_simulatedShareRate` should be calculated by the Oracle daemon
554-
* invoking the method with static call and passing `_lastFinalizableRequestId` == `_simulatedShareRate` == 0
555-
* plugging the returned values to the following formula:
556-
* `_simulatedShareRate = (postTotalPooledEther * 1e27) / postTotalShares`
559+
* NB: `_simulatedShareRate` should be calculated off-chain by calling the method with `eth_call` JSON-RPC API
560+
* while passing `_lastFinalizableRequestId` == `_simulatedShareRate` == 0, and plugging the returned values
561+
* to the following formula: `_simulatedShareRate = (postTotalPooledEther * 1e27) / postTotalShares`
557562
*
558563
* @return postTotalPooledEther amount of ether in the protocol after report
559564
* @return postTotalShares amount of shares in the protocol after report
@@ -677,14 +682,10 @@ contract Lido is Versioned, StETHPermit, AragonApp {
677682
* @dev Returns depositable ether amount.
678683
* Takes into account unfinalized stETH required by WithdrawalQueue
679684
*/
680-
function getDepositableEther() public view returns (uint256 depositableEth) {
681-
uint256 bufferedEth = _getBufferedEther();
685+
function getDepositableEther() public view returns (uint256) {
686+
uint256 bufferedEther = _getBufferedEther();
682687
uint256 withdrawalReserve = IWithdrawalQueue(getLidoLocator().withdrawalQueue()).unfinalizedStETH();
683-
684-
if (bufferedEth > withdrawalReserve) {
685-
bufferedEth -= withdrawalReserve;
686-
depositableEth = bufferedEth.div(DEPOSIT_SIZE).mul(DEPOSIT_SIZE);
687-
}
688+
return bufferedEther > withdrawalReserve ? bufferedEther - withdrawalReserve : 0;
688689
}
689690

690691
/**
@@ -697,36 +698,29 @@ contract Lido is Versioned, StETHPermit, AragonApp {
697698
ILidoLocator locator = getLidoLocator();
698699

699700
require(msg.sender == locator.depositSecurityModule(), "APP_AUTH_DSM_FAILED");
700-
require(_stakingModuleId <= uint24(-1), "STAKING_MODULE_ID_TOO_LARGE");
701701
require(canDeposit(), "CAN_NOT_DEPOSIT");
702702

703-
uint256 depositableEth = getDepositableEther();
704-
705-
if (depositableEth > 0) {
706-
/// available ether amount for deposits (multiple of 32eth)
707-
depositableEth = Math256.min(depositableEth, _maxDepositsCount.mul(DEPOSIT_SIZE));
708-
709-
uint256 unaccountedEth = _getUnaccountedEther();
710-
/// @dev transfer ether to SR and make deposit at the same time
711-
/// @notice allow zero value of depositableEth, in this case SR will simply transfer the unaccounted ether to Lido contract
712-
uint256 depositsCount = IStakingRouter(locator.stakingRouter()).deposit.value(depositableEth)(
713-
_maxDepositsCount,
714-
_stakingModuleId,
715-
_depositCalldata
716-
);
717-
718-
uint256 depositedAmount = depositsCount.mul(DEPOSIT_SIZE);
719-
assert(depositedAmount <= depositableEth);
720-
721-
if (depositsCount > 0) {
722-
uint256 newDepositedValidators = DEPOSITED_VALIDATORS_POSITION.getStorageUint256().add(depositsCount);
723-
DEPOSITED_VALIDATORS_POSITION.setStorageUint256(newDepositedValidators);
724-
emit DepositedValidatorsChanged(newDepositedValidators);
725-
726-
_markAsUnbuffered(depositedAmount);
727-
assert(_getUnaccountedEther() == unaccountedEth);
728-
}
729-
}
703+
IStakingRouter stakingRouter = IStakingRouter(locator.stakingRouter());
704+
uint256 depositsCount = Math256.min(
705+
_maxDepositsCount,
706+
stakingRouter.getStakingModuleMaxDepositsCount(_stakingModuleId, getDepositableEther())
707+
);
708+
if (depositsCount == 0) return;
709+
710+
uint256 depositsValue = depositsCount.mul(DEPOSIT_SIZE);
711+
/// @dev firstly update the local state of the contract to prevent a reentrancy attack,
712+
/// even if the StakingRouter is a trusted contract.
713+
BUFFERED_ETHER_POSITION.setStorageUint256(_getBufferedEther().sub(depositsValue));
714+
emit Unbuffered(depositsValue);
715+
716+
uint256 newDepositedValidators = DEPOSITED_VALIDATORS_POSITION.getStorageUint256().add(depositsCount);
717+
DEPOSITED_VALIDATORS_POSITION.setStorageUint256(newDepositedValidators);
718+
emit DepositedValidatorsChanged(newDepositedValidators);
719+
720+
/// @dev transfer ether to StakingRouter and make a deposit at the same time. All the ether
721+
/// sent to StakingRouter is counted as deposited. If StakingRouter can't deposit all
722+
/// passed ether it MUST revert the whole transaction (never happens in normal circumstances)
723+
stakingRouter.deposit.value(depositsValue)(depositsCount, _stakingModuleId, _depositCalldata);
730724
}
731725

732726
/// DEPRECATED PUBLIC METHODS
@@ -937,14 +931,7 @@ contract Lido is Versioned, StETHPermit, AragonApp {
937931
STAKING_STATE_POSITION.setStorageStakeLimitStruct(stakeLimitData.updatePrevStakeLimit(currentStakeLimit - msg.value));
938932
}
939933

940-
uint256 sharesAmount;
941-
if (_getTotalPooledEther() != 0 && _getTotalShares() != 0) {
942-
sharesAmount = getSharesByPooledEth(msg.value);
943-
} else {
944-
// totalPooledEther is 0: for first-ever deposit
945-
// assume that shares correspond to Ether 1-to-1
946-
sharesAmount = msg.value;
947-
}
934+
uint256 sharesAmount = getSharesByPooledEth(msg.value);
948935

949936
_mintShares(msg.sender, sharesAmount);
950937

@@ -1095,30 +1082,13 @@ contract Lido is Versioned, StETHPermit, AragonApp {
10951082
_emitTransferAfterMintingShares(treasury, treasuryReward);
10961083
}
10971084

1098-
/**
1099-
* @dev Records a deposit to the deposit_contract.deposit function
1100-
* @param _amount Total amount deposited to the Consensus Layer side
1101-
*/
1102-
function _markAsUnbuffered(uint256 _amount) internal {
1103-
BUFFERED_ETHER_POSITION.setStorageUint256(_getBufferedEther().sub(_amount));
1104-
1105-
emit Unbuffered(_amount);
1106-
}
1107-
11081085
/**
11091086
* @dev Gets the amount of Ether temporary buffered on this contract balance
11101087
*/
11111088
function _getBufferedEther() internal view returns (uint256) {
11121089
return BUFFERED_ETHER_POSITION.getStorageUint256();
11131090
}
11141091

1115-
/**
1116-
* @dev Gets unaccounted (excess) Ether on this contract balance
1117-
*/
1118-
function _getUnaccountedEther() internal view returns (uint256) {
1119-
return address(this).balance.sub(_getBufferedEther());
1120-
}
1121-
11221092
/// @dev Calculates and returns the total base balance (multiple of 32) of validators in transient state,
11231093
/// i.e. submitted to the official Deposit contract but not yet visible in the CL state.
11241094
/// @return transient balance in wei (1e-18 Ether)
@@ -1127,7 +1097,7 @@ contract Lido is Versioned, StETHPermit, AragonApp {
11271097
uint256 clValidators = CL_VALIDATORS_POSITION.getStorageUint256();
11281098
// clValidators can never be less than deposited ones.
11291099
assert(depositedValidators >= clValidators);
1130-
return depositedValidators.sub(clValidators).mul(DEPOSIT_SIZE);
1100+
return (depositedValidators - clValidators).mul(DEPOSIT_SIZE);
11311101
}
11321102

11331103
/**
@@ -1203,7 +1173,7 @@ contract Lido is Versioned, StETHPermit, AragonApp {
12031173
* (i.e., postpone the extra rewards to be applied during the next rounds)
12041174
* 5. Invoke finalization of the withdrawal requests
12051175
* 6. Distribute protocol fee (treasury & node operators)
1206-
* 7. Burn excess shares (withdrawn stETH at least)
1176+
* 7. Burn excess shares within the allowed limit (can postpone some shares to be burnt later)
12071177
* 8. Complete token rebase by informing observers (emit an event and call the external receivers if any)
12081178
* 9. Sanity check for the provided simulated share rate
12091179
*/
@@ -1288,8 +1258,9 @@ contract Lido is Versioned, StETHPermit, AragonApp {
12881258
);
12891259

12901260
// Step 7.
1291-
// Burn excess shares (withdrawn stETH at least)
1292-
uint256 burntWithdrawalQueueShares = _burnSharesLimited(
1261+
// Burn excess shares within the allowed limit (can postpone some shares to be burnt later)
1262+
// Return actually burnt shares of the current report's finalized withdrawal requests to use in sanity checks
1263+
uint256 burntCurrentWithdrawalShares = _burnSharesLimited(
12931264
IBurner(_contracts.burner),
12941265
_contracts.withdrawalQueue,
12951266
reportContext.sharesToBurnFromWithdrawalQueue,
@@ -1313,7 +1284,7 @@ contract Lido is Versioned, StETHPermit, AragonApp {
13131284
postTotalPooledEther,
13141285
postTotalShares,
13151286
reportContext.etherToLockOnWithdrawalQueue,
1316-
burntWithdrawalQueueShares,
1287+
burntCurrentWithdrawalShares,
13171288
_reportedData.simulatedShareRate
13181289
);
13191290
}
@@ -1377,17 +1348,20 @@ contract Lido is Versioned, StETHPermit, AragonApp {
13771348
/*
13781349
* @dev Perform burning of `stETH` shares via the dedicated `Burner` contract.
13791350
*
1380-
* NB: some of the burning amount can be postponed for the next reports
1381-
* if positive token rebase smoothened.
1351+
* NB: some of the burning amount can be postponed for the next reports if positive token rebase smoothened.
1352+
* It's possible that underlying shares of the current oracle report's finalized withdrawals won't be burnt
1353+
* completely in a single oracle report round due to the provided `_sharesToBurnLimit` limit
13821354
*
1383-
* @return burnt shares from withdrawals queue (when some requests finalized)
1355+
* @return shares actually burnt for the current oracle report's finalized withdrawals
1356+
* these shares are assigned to be burnt most recently, so the amount can be calculated deducting
1357+
* `postponedSharesToBurn` shares (if any) after the burn commitment & execution
13841358
*/
13851359
function _burnSharesLimited(
13861360
IBurner _burner,
13871361
address _withdrawalQueue,
13881362
uint256 _sharesToBurnFromWithdrawalQueue,
13891363
uint256 _sharesToBurnLimit
1390-
) internal returns (uint256 burntWithdrawalsShares) {
1364+
) internal returns (uint256 burntCurrentWithdrawalShares) {
13911365
if (_sharesToBurnFromWithdrawalQueue > 0) {
13921366
_burner.requestBurnShares(_withdrawalQueue, _sharesToBurnFromWithdrawalQueue);
13931367
}
@@ -1403,7 +1377,7 @@ contract Lido is Versioned, StETHPermit, AragonApp {
14031377
(uint256 coverShares, uint256 nonCoverShares) = _burner.getSharesRequestedToBurn();
14041378
uint256 postponedSharesToBurn = coverShares.add(nonCoverShares);
14051379

1406-
burntWithdrawalsShares =
1380+
burntCurrentWithdrawalShares =
14071381
postponedSharesToBurn < _sharesToBurnFromWithdrawalQueue ?
14081382
_sharesToBurnFromWithdrawalQueue - postponedSharesToBurn : 0;
14091383
}

0 commit comments

Comments
 (0)