Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Staking Router 1.5 #78

Draft
wants to merge 204 commits into
base: develop
Choose a base branch
from
Draft

Staking Router 1.5 #78

wants to merge 204 commits into from

Conversation

avsetsin
Copy link
Contributor

No description provided.

Copy link

github-actions bot commented Apr 22, 2024

badge

Code Coverage Summary

Filename                                                                                 Stmts    Miss  Cover    Missing
-------------------------------------------------------------------------------------  -------  ------  -------  ----------------------------------------------------------------------------------------------------------------------------------------
contracts/0.4.24/Lido.sol                                                                  212       0  100.00%
contracts/0.4.24/StETH.sol                                                                  72       0  100.00%
contracts/0.4.24/StETHPermit.sol                                                            15       0  100.00%
contracts/0.4.24/lib/Packed64x4.sol                                                          5       0  100.00%
contracts/0.4.24/lib/SigningKeys.sol                                                        36       0  100.00%
contracts/0.4.24/lib/StakeLimitUtils.sol                                                    37       0  100.00%
contracts/0.4.24/nos/NodeOperatorsRegistry.sol                                             509       0  100.00%
contracts/0.4.24/oracle/LegacyOracle.sol                                                    72       0  100.00%
contracts/0.4.24/test_helpers/MinFirstAllocationStrategyConsumerMockLegacyVersion.sol        3       3  0.00%    15-24
contracts/0.4.24/utils/Pausable.sol                                                          9       0  100.00%
contracts/0.4.24/utils/Versioned.sol                                                         5       0  100.00%
contracts/0.6.12/WstETH.sol                                                                 17       0  100.00%
contracts/0.8.4/WithdrawalsManagerProxy.sol                                                 61       0  100.00%
contracts/0.8.9/BeaconChainDepositor.sol                                                    21       2  90.48%   48, 51
contracts/0.8.9/Burner.sol                                                                  71       0  100.00%
contracts/0.8.9/DepositSecurityModule.sol                                                  128       0  100.00%
contracts/0.8.9/EIP712StETH.sol                                                             16       0  100.00%
contracts/0.8.9/LidoExecutionLayerRewardsVault.sol                                          16       0  100.00%
contracts/0.8.9/LidoLocator.sol                                                             18       0  100.00%
contracts/0.8.9/OracleDaemonConfig.sol                                                      28       0  100.00%
contracts/0.8.9/StakingRouter.sol                                                          312       0  100.00%
contracts/0.8.9/WithdrawalQueue.sol                                                         88       0  100.00%
contracts/0.8.9/WithdrawalQueueBase.sol                                                    146       0  100.00%
contracts/0.8.9/WithdrawalQueueERC721.sol                                                   89       0  100.00%
contracts/0.8.9/WithdrawalVault.sol                                                         21       0  100.00%
contracts/0.8.9/lib/Math.sol                                                                 4       0  100.00%
contracts/0.8.9/lib/PositiveTokenRebaseLimiter.sol                                          22       0  100.00%
contracts/0.8.9/lib/UnstructuredRefStorage.sol                                               2       0  100.00%
contracts/0.8.9/oracle/AccountingOracle.sol                                                192       3  98.44%   190-191, 742
contracts/0.8.9/oracle/BaseOracle.sol                                                       89       1  98.88%   397
contracts/0.8.9/oracle/HashConsensus.sol                                                   263     117  55.51%   330, 341-357, 399-438, 452-483, 516-592, 626, 640, 689, 718, 732, 755-852, 870, 884, 889-894, 919, 948, 978, 1005, 1010, 1017, 1054-1073
contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol                                          91      91  0.00%    96-461
contracts/0.8.9/proxy/OssifiableProxy.sol                                                   17       0  100.00%
contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol                                232       0  100.00%
contracts/0.8.9/test_helpers/MinFirstAllocationStrategyConsumerMockModernVersion.sol         3       3  0.00%    15-24
contracts/0.8.9/test_helpers/ModuleSolo.sol                                                 17      17  0.00%    26-155
contracts/0.8.9/test_helpers/StakingModuleMock.sol                                          60      60  0.00%    18-252
contracts/0.8.9/test_helpers/StakingRouterMock.sol                                           5       2  60.00%   16-20
contracts/0.8.9/utils/PausableUntil.sol                                                     31       0  100.00%
contracts/0.8.9/utils/Versioned.sol                                                         11       0  100.00%
contracts/0.8.9/utils/access/AccessControl.sol                                              23       0  100.00%
contracts/0.8.9/utils/access/AccessControlEnumerable.sol                                     9       0  100.00%
TOTAL                                                                                     3078     299  90.29%

Diff against master

Filename                                                                                 Stmts    Miss  Cover
-------------------------------------------------------------------------------------  -------  ------  --------
contracts/0.4.24/nos/NodeOperatorsRegistry.sol                                             +24       0  +100.00%
contracts/0.4.24/test_helpers/MinFirstAllocationStrategyConsumerMockLegacyVersion.sol       +3      +3  +100.00%
contracts/0.8.9/DepositSecurityModule.sol                                                  +24       0  +100.00%
contracts/0.8.9/StakingRouter.sol                                                           +9       0  +100.00%
contracts/0.8.9/oracle/AccountingOracle.sol                                                +21      +2  -0.98%
contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol                                +61       0  +100.00%
contracts/0.8.9/test_helpers/MinFirstAllocationStrategyConsumerMockModernVersion.sol        +3      +3  +100.00%
contracts/0.8.9/test_helpers/ModuleSolo.sol                                                +17     +17  +100.00%
contracts/0.8.9/test_helpers/StakingModuleMock.sol                                         +60     +60  +100.00%
contracts/0.8.9/test_helpers/StakingRouterMock.sol                                          +5      +2  +60.00%
TOTAL                                                                                     +227     +87  -2.27%

Results for commit: fafa232

Minimum allowed coverage is 0%

♻️ This comment has been updated with latest results

@TheDZhon TheDZhon changed the base branch from master to develop May 14, 2024 07:53
@tamtamchik tamtamchik added the contracts Pull requests that add add or update contracts. label May 22, 2024
*/
function setMaxDeposits(uint256 newValue) external onlyOwner {
_setMaxDeposits(newValue);
function setMaxOperatorsPerUnvetting(uint256 newValue) external onlyOwner {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to add note that this is really technical constraint to avoid unbound loop issue and meaningful value for this param would be ~200 or so...


uint256 nodeOperatorsCount = nodeOperatorIds.length / 8;

if (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's unclear from first glance that the question is in the way how the line might be cut in pieces

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, would suggest extracting a helper func to validate the input calldata

uint256 stakingModuleId,
uint256 depositsValue
) internal {
stakingModule.lastDepositAt = uint64(block.timestamp);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this counter cause we have this _setLastDepositBlock(block.number); within deposit security module.

/// as the next transaction hash.
///
/// | 32 bytes | array of items
/// | nextHash | ...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please update the comment within line 361, cause bytes32 extraDataHash; is now hash of first item in the chain but not hash of the whole extra data

Copy link
Member

@skozin skozin Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

based on the code, it's in a different order: nextHash and then data chunk and not the other way

assembly {
dataHash := calldataload(data.offset)
}

Copy link
Member

@ujenjt ujenjt Oct 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why so hacky?

bytes32(msg.data[data.offset:data.offset+32])

We usually try to avoid inline assembly when possbile.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible let's simplify it

@@ -772,7 +803,7 @@ contract AccountingOracle is BaseOracle {
dataOffset := add(dataOffset, 5)
}

if (iter.itemType == 0) {
if (iter.lastSortingKey == 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what? looks like it's overcomplicated logic, or even worse might be wrong for some edge cases

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double checked - it should work but it's very ugly

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is just checking the increase and sequence of item indices between transactions in the third phase, then please make it more semantic

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (index != iter.nextIndex) (see my comment for line 750)

ExtraDataIterState memory iter = ExtraDataIterState({
index: 0,
index: procState.itemsProcessed > 0 ? procState.itemsProcessed - 1 : 0,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd simplify by renaming iter.index to iter.nextIndex, passing procState.itemsProcessed unconditionally here, and changing the check on lines 806-811 to if (index != iter.nextIndex)

/// Depending on the size of the extra data, the processing might need to be split into
/// multiple transactions. Each transaction contains a chunk of report data (an array of items)
/// and the hash of the next transaction. The last transaction will contain ZERO_HASH
/// as the next transaction hash.
Copy link
Member

@skozin skozin Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd avoid using the term "transaction hash" since it means a specific and different thing within the blockchain context

Copy link
Member

@skozin skozin Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, the extra data format hasn't changed per see (it's still a list of items of the same format); what has changed is its transport format, LIST in this case: now it is not just an array of items but something more involved, having multiple chunks linked using hashes

so i'd ask to revert the changes in lines 278-286 and move the description of the transport format to line 388 where it actually belongs (see my comment there)

@@ -357,13 +375,15 @@ contract AccountingOracle is BaseOracle {
uint256 public constant EXTRA_DATA_FORMAT_EMPTY = 0;

/// @notice The list format for the extra data array. Used when all extra data processing
/// fits into a single transaction.
/// fits into a single or multiple transactions.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to mention single/multiple here imo, i'd just say "Used when the oracle report contains extra data"

///
/// Extra data is passed within a single transaction as a bytearray containing all data items
/// Depend on the extra data size it passed within a single or multiple transactions.
/// Each transaction contains next transaction hash and a bytearray containing data items
Copy link
Member

@skozin skozin Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on the extra data size, it's passed within a single or multiple transactions.
Each transaction contains data consisting of 1) the keccak256 hash of the next
transaction's data or `ZERO_BYTES32` if there are no more data chunks, and 2) a chunk
of report data (an array of items).

The `extraDataHash` field of the `ReportData` struct is calculated as a keccak256 hash
over the first transaction's data, i.e. over the first data chunk with the second
transaction's data hash (or `ZERO_BYTES32`) prepended.

ReportData.extraDataHash := hash0
hash0 := keccak256(| hash1 | extraData[0], ... extraData[n] |)
hash1 := keccak256(| hash2 | extraData[n + 1], ... extraData[m] |)
...
hashK := keccak256(| ZERO_BYTES32 | extraData[x + 1], ... extraData[extraDataItemsCount] |)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(see the line 281's comment)

@@ -400,11 +420,11 @@ contract AccountingOracle is BaseOracle {

/// @notice Submits report extra data in the EXTRA_DATA_FORMAT_LIST format for processing.
///
/// @param items The extra data items list. See docs for the `EXTRA_DATA_FORMAT_LIST`
/// @param data The extra data chunk with items list. See docs for the `EXTRA_DATA_FORMAT_LIST`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

id remove "with items list", it misleads more than helps

@@ -133,6 +132,8 @@ contract AccountingOracle is BaseOracle {
bytes32 internal constant EXTRA_DATA_PROCESSING_STATE_POSITION =
keccak256("lido.AccountingOracle.extraDataProcessingState");

bytes32 internal constant ZERO_HASH = bytes32(0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd call it ZERO_BYTES32 instead since "zero hash" usually means "hash of a zero data" which is not zero bytes

/// as the next transaction hash.
///
/// | 32 bytes | array of items
/// | nextHash | ...
Copy link
Member

@skozin skozin Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

based on the code, it's in a different order: nextHash and then data chunk and not the other way

ExtraDataIterState memory iter = ExtraDataIterState({
index: 0,
index: procState.itemsProcessed > 0 ? procState.itemsProcessed - 1 : 0,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd simplify by renaming iter.index to iter.nextIndex, passing procState.itemsProcessed unconditionally here, and changing the check on lines 806-811 to if (index != iter.nextIndex)

@@ -772,7 +803,7 @@ contract AccountingOracle is BaseOracle {
dataOffset := add(dataOffset, 5)
}

if (iter.itemType == 0) {
if (iter.lastSortingKey == 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (index != iter.nextIndex) (see my comment for line 750)


while (dataOffset < data.length) {
uint256 index;
uint256 itemType;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the reason for moving these two variables to a broader scope?

Copy link
Contributor

@TheDZhon TheDZhon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM except only a few things for the future

@@ -93,8 +102,8 @@ contract NodeOperatorsRegistry is AragonApp, Versioned {
uint8 internal constant TOTAL_DEPOSITED_KEYS_COUNT_OFFSET = 3;

// TargetValidatorsStats
/// @dev Flag enable/disable limiting target active validators count for operator
uint8 internal constant IS_TARGET_LIMIT_ACTIVE_OFFSET = 0;
/// @dev Target limit mode, allows limiting target active validators count for operator (0 = disabled, 1 = soft mode, 2 = forced mode)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs should clearly outline that 'forced' = 'boosted'

@@ -280,6 +295,22 @@ contract NodeOperatorsRegistry is AragonApp, Versioned {
emit StakingModuleTypeSet(_type);
}

function finalizeUpgrade_v3() external {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we haven't removed finalizeUpgrade_v2?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, will put it in the backlog

@@ -1254,6 +1355,9 @@ contract NodeOperatorsRegistry is AragonApp, Versioned {
_saveOperatorStuckPenaltyStats(_nodeOperatorId, stuckPenaltyStats);
_updateSummaryMaxValidatorsCount(_nodeOperatorId);
_increaseValidatorsKeysNonce();

emit NodeOperatorPenaltyCleared(_nodeOperatorId);
return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need return bool though?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a fix for a technical debt. The method interface assumed return bool, but always returned false because there was no return statement. This was found during a security audit. We preferred to add return: true and keep the method interface


/**
* @title DepositSecurityModule
* @dev The contract represents a security module for handling deposits.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now we mention 'unvetting' in the methods below, would be great to give an insight on this term too

_addGuardian(addresses[i]);

unchecked {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure that this optimization is worth doing tbh
This methods gets called barely once a year, but readability is a bit worse than it was

bytes32 public constant REPORT_EXITED_VALIDATORS_ROLE = keccak256("REPORT_EXITED_VALIDATORS_ROLE");
bytes32 public constant UNSAFE_SET_EXITED_VALIDATORS_ROLE = keccak256("UNSAFE_SET_EXITED_VALIDATORS_ROLE");
bytes32 public constant REPORT_REWARDS_MINTED_ROLE = keccak256("REPORT_REWARDS_MINTED_ROLE");

bytes32 internal constant LIDO_POSITION = keccak256("lido.StakingRouter.lido");

/// @dev Credentials which allows the DAO to withdraw Ether on the 2.0 side
/// @dev Credentials to withdraw ETH on Consensus Layer side.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

newStakingModule.lastDepositAt = uint64(block.timestamp);
newStakingModule.lastDepositBlock = block.number;
emit StakingRouterETHDeposited(newStakingModuleId, 0);
/// @dev Simulate zero value deposit to prevent real deposits into the new StakingModule via
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why it's called 'simulate'? maybe 'initialize' or 'pretend'?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should it be prevented, though?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make sure that the minimum distance is kept between 2 sequential deposits in rare edge cases. For example, if we add a module and update DSM contract, as for example in SR+CSM update.

revert ExtraDataHashCannotBeZeroForNonEmptyData();
}
}

IOracleReportSanityChecker(LOCATOR.oracleReportSanityChecker())
.checkAccountingExtraDataListItemsCount(data.extraDataItemsCount);

ILegacyOracle(LEGACY_ORACLE).handleConsensusLayerReport(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would have been great having LEGACY_ORACLE removed completely 😢
Let's schedule it for the next upgrade

@@ -105,13 +102,14 @@ struct LimitsList {

/// @dev The packed version of the LimitsList struct to be effectively persisted in storage
struct LimitsListPacked {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool, still fits 256bits 🍷

@@ -31,7 +31,7 @@ library MinFirstAllocationStrategy {
uint256[] memory buckets,
uint256[] memory capacities,
uint256 allocationSize
) internal pure returns (uint256 allocated) {
) public pure returns (uint256 allocated, uint256[] memory) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't forget to add docs and artifacts bc now it's a separately deploying thing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs updated

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
contracts Pull requests that add add or update contracts.
Projects
None yet
Development

Successfully merging this pull request may close these issues.