Skip to content

Commit

Permalink
Merge pull request #542 from lidofinance/feature/oracle-improvements
Browse files Browse the repository at this point in the history
Oracle improvements & still-missing features
  • Loading branch information
Psirex authored Feb 5, 2023
2 parents 516181d + cb56185 commit 00f1a45
Show file tree
Hide file tree
Showing 25 changed files with 749 additions and 204 deletions.
33 changes: 29 additions & 4 deletions contracts/0.4.24/nos/NodeOperatorsRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,20 @@ contract NodeOperatorsRegistry is AragonApp, IStakingModule, Versioned {
// updated (as opposed to pulling by node ops), we don't need any handling here
}

/// @notice Updates the number of the validators in the EXITED state for node operator with given id
/// @notice Called by StakingRouter to update the number of the validators of the given node
/// operator that were requested to exit but failed to do so in the max allowed time
///
/// @param _nodeOperatorId Id of the node operator
/// @param _stuckValidatorKeysCount New number of stuck validators of the node operator
function updateStuckValidatorsKeysCount(uint256 _nodeOperatorId, uint256 _stuckValidatorKeysCount)
external
{
_updateStuckValidatorsKeysCount(_nodeOperatorId, _stuckValidatorKeysCount, false);
}

/// @notice Called by StakingRouter to update the number of the validators in the EXITED state
/// for node operator with given id
///
/// @param _nodeOperatorId Id of the node operator
/// @param _exitedValidatorsKeysCount New number of EXITED validators of the node operator
/// @return Total number of exited validators across all node operators.
Expand Down Expand Up @@ -395,11 +408,15 @@ contract NodeOperatorsRegistry is AragonApp, IStakingModule, Versioned {
/// @param _nodeOperatorId Id of the node operator
/// @param _exitedValidatorsKeysCount New number of EXITED validators of the node operator
/// @return Total number of exited validators across all node operators.
function unsafeUpdateExitedValidatorsKeysCount(uint256 _nodeOperatorId, uint256 _exitedValidatorsKeysCount)
function unsafeUpdateValidatorsKeysCount(
uint256 _nodeOperatorId,
uint256 _exitedValidatorsKeysCount,
uint256 _stuckValidatorsKeysCount
)
external
returns (uint256)
{
return _updateExitedValidatorsKeysCount(_nodeOperatorId, _exitedValidatorsKeysCount, true);
_updateStuckValidatorsKeysCount(_nodeOperatorId, _stuckValidatorsKeysCount, true);
_updateExitedValidatorsKeysCount(_nodeOperatorId, _exitedValidatorsKeysCount, true);
}

function _updateExitedValidatorsKeysCount(
Expand Down Expand Up @@ -435,6 +452,14 @@ contract NodeOperatorsRegistry is AragonApp, IStakingModule, Versioned {
return totalSigningKeysStats.exitedSigningKeysCount;
}

function _updateStuckValidatorsKeysCount(
uint256 _nodeOperatorId,
uint256 _stuckValidatorKeysCount,
bool _allowDecrease
) internal {
// FIXME: implement
}

/// @notice Invalidates all unused validators keys for all node operators
function invalidateReadyToDepositKeys() external {
_auth(INVALIDATE_READY_TO_DEPOSIT_KEYS_ROLE);
Expand Down
8 changes: 3 additions & 5 deletions contracts/0.4.24/oracle/LidoOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -257,11 +257,6 @@ contract LidoOracle is AragonApp {
POST_COMPLETED_TOTAL_POOLED_ETHER_POSITION.setStorageUint256(postTotalEther);
TIME_ELAPSED_POSITION.setStorageUint256(timeElapsed);

ChainSpec memory spec = _getChainSpec();
uint256 lastEpoch = LAST_COMPLETED_EPOCH_ID_POSITION.getStorageUint256();
uint256 newEpoch = lastEpoch + timeElapsed / (spec.secondsPerSlot * spec.slotsPerEpoch);
LAST_COMPLETED_EPOCH_ID_POSITION.setStorageUint256(newEpoch);

emit PostTotalShares(postTotalEther, preTotalEther, timeElapsed, postTotalShares);
}

Expand All @@ -272,8 +267,11 @@ contract LidoOracle is AragonApp {
external
{
require(msg.sender == getNewOracle(), "SENDER_NOT_ALLOWED");

// new oracle's ref. slot is the last slot of the epoch preceding the one the frame starts at
uint256 epochId = (_refSlot + 1) / _getChainSpec().slotsPerEpoch;
LAST_COMPLETED_EPOCH_ID_POSITION.setStorageUint256(epochId);

emit Completed(epochId, uint128(_clBalance), uint128(_clValidators));
}

Expand Down
19 changes: 19 additions & 0 deletions contracts/0.4.24/test_helpers/MockLegacyOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ interface ILegacyOracle {
import "../oracle/LidoOracle.sol";

contract MockLegacyOracle is ILegacyOracle, LidoOracle {

struct HandleConsensusLayerReportCallData {
uint256 totalCalls;
uint256 refSlot;
uint256 clBalance;
uint256 clValidators;
}

HandleConsensusLayerReportCallData public lastCall__handleConsensusLayerReport;

uint64 internal _epochsPerFrame;
uint64 internal _slotsPerEpoch;
uint64 internal _secondsPerSlot;
Expand All @@ -38,6 +48,15 @@ contract MockLegacyOracle is ILegacyOracle, LidoOracle {
);
}

function handleConsensusLayerReport(uint256 refSlot, uint256 clBalance, uint256 clValidators)
external
{
++lastCall__handleConsensusLayerReport.totalCalls;
lastCall__handleConsensusLayerReport.refSlot = refSlot;
lastCall__handleConsensusLayerReport.clBalance = clBalance;
lastCall__handleConsensusLayerReport.clValidators = clValidators;
}


function setParams(
uint64 epochsPerFrame,
Expand Down
143 changes: 132 additions & 11 deletions contracts/0.8.9/StakingRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version
error ErrorAppAuthLidoFailed();
error ErrorStakingModuleStatusTheSame();
error ErrorStakingModuleWrongName();
error UnexpectedCurrentKeysCount(
uint256 currentModuleExitedKeysCount,
uint256 currentNodeOpExitedKeysCount,
uint256 currentNodeOpStuckKeysCount
);

enum StakingModuleStatus {
Active, // deposits and rewards allowed
Expand Down Expand Up @@ -96,6 +101,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version
bytes32 public constant STAKING_MODULE_RESUME_ROLE = keccak256("STAKING_MODULE_RESUME_ROLE");
bytes32 public constant STAKING_MODULE_MANAGE_ROLE = keccak256("STAKING_MODULE_MANAGE_ROLE");
bytes32 public constant REPORT_EXITED_KEYS_ROLE = keccak256("REPORT_EXITED_KEYS_ROLE");
bytes32 public constant UNSAFE_SET_EXITED_KEYS_ROLE = keccak256("UNSAFE_SET_EXITED_KEYS_ROLE");
bytes32 public constant REPORT_REWARDS_MINTED_ROLE = keccak256("REPORT_REWARDS_MINTED_ROLE");

bytes32 internal constant LIDO_POSITION = keccak256("lido.StakingRouter.lido");
Expand Down Expand Up @@ -246,20 +252,24 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version
function updateExitedKeysCountByStakingModule(
uint256[] calldata _stakingModuleIds,
uint256[] calldata _exitedKeysCounts
) external onlyRole(REPORT_EXITED_KEYS_ROLE) {
)
external
onlyRole(REPORT_EXITED_KEYS_ROLE)
{
for (uint256 i = 0; i < _stakingModuleIds.length; ) {
StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleIds[i]);
uint256 prevExitedKeysCount = stakingModule.exitedKeysCount;
if (_exitedKeysCounts[i] < prevExitedKeysCount) {
uint256 prevReportedExitedKeysCount = stakingModule.exitedKeysCount;
if (_exitedKeysCounts[i] < prevReportedExitedKeysCount) {
revert ErrorExitedKeysCountCannotDecrease();
}
(uint256 moduleExitedKeysCount,,) = IStakingModule(stakingModule.stakingModuleAddress)
.getValidatorsKeysStats();
if (moduleExitedKeysCount < prevExitedKeysCount) {
if (moduleExitedKeysCount < prevReportedExitedKeysCount) {
// not all of the exited keys were async reported to the module
emit StakingModuleExitedKeysIncompleteReporting(
stakingModule.id,
prevExitedKeysCount - moduleExitedKeysCount);
prevReportedExitedKeysCount - moduleExitedKeysCount
);
}
stakingModule.exitedKeysCount = _exitedKeysCounts[i];
unchecked { ++i; }
Expand All @@ -270,16 +280,127 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version
uint256 _stakingModuleId,
uint256[] calldata _nodeOperatorIds,
uint256[] calldata _exitedKeysCounts
) external onlyRole(REPORT_EXITED_KEYS_ROLE) {
)
external
onlyRole(REPORT_EXITED_KEYS_ROLE)
{
StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleId);
address moduleAddr = stakingModule.stakingModuleAddress;
(uint256 prevExitedKeysCount,,) = IStakingModule(moduleAddr).getValidatorsKeysStats();
uint256 newExitedKeysCount;
for (uint256 i = 0; i < _nodeOperatorIds.length; ) {
uint256 exitedKeysCount = IStakingModule(moduleAddr)
newExitedKeysCount = IStakingModule(moduleAddr)
.updateExitedValidatorsKeysCount(_nodeOperatorIds[i], _exitedKeysCounts[i]);
if (exitedKeysCount == stakingModule.exitedKeysCount) {
// oracle finished updating exited keys for all node ops
IStakingModule(moduleAddr).finishUpdatingExitedValidatorsKeysCount();
}
unchecked { ++i; }
}
uint256 prevReportedExitedKeysCount = stakingModule.exitedKeysCount;
if (prevExitedKeysCount < prevReportedExitedKeysCount &&
newExitedKeysCount >= prevReportedExitedKeysCount
) {
// oracle finished updating exited keys for all node ops
IStakingModule(moduleAddr).finishUpdatingExitedValidatorsKeysCount();
}
}

struct KeysCountCorrection {
uint256 currentModuleExitedKeysCount;
uint256 currentNodeOperatorExitedKeysCount;
uint256 currentNodeOperatorStuckKeysCount;
uint256 newModuleExitedKeysCount;
uint256 newNodeOperatorExitedKeysCount;
uint256 newNodeOperatorStuckKeysCount;
}

/**
* @notice Sets exited keys count for the given module and given node operator in that module
* without performing critical safety checks, e.g. that exited keys count cannot decrease.
*
* Should only be used by the DAO in extreme cases and with sufficient precautions to correct
* invalid data reported by the oracle committee due to a bug in the oracle daemon.
*
* @param _stakingModuleId ID of the staking module.
*
* @param _nodeOperatorId ID of the node operator.
*
* @param _triggerUpdateFinish Whether to call `finishUpdatingExitedValidatorsKeysCount` on
* the module after applying the corrections.
*
* @param _correction.currentModuleExitedKeysCount The expected current number of exited keys
* of the module that is being corrected.
*
* @param _correction.currentNodeOperatorExitedKeysCount The expected current number of exited
* keys of the node operator that is being corrected.
*
* @param _correction.currentNodeOperatorStuckKeysCount The expected current number of stuck
* keys of the node operator that is being corrected.
*
* @param _correction.newModuleExitedKeysCount The corrected number of exited keys of the module.
*
* @param _correction.newNodeOperatorExitedKeysCount The corrected number of exited keys of the
* node operator.
*
* @param _correction.newNodeOperatorStuckKeysCount The corrected number of stuck keys of the
* node operator.
*
* Reverts if the current numbers of exited and stuck keys of the module and node operator don't
* match the supplied expected current values.
*/
function unsafeSetExitedKeysCount(
uint256 _stakingModuleId,
uint256 _nodeOperatorId,
bool _triggerUpdateFinish,
KeysCountCorrection memory _correction
)
external
onlyRole(UNSAFE_SET_EXITED_KEYS_ROLE)
{
StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleId);
address moduleAddr = stakingModule.stakingModuleAddress;

(uint256 nodeOpExitedKeysCount,,) = IStakingModule(moduleAddr)
.getValidatorsKeysStats(_nodeOperatorId);

// FIXME: get current value from the staking module
uint256 nodeOpStuckKeysCount;

if (_correction.currentModuleExitedKeysCount != stakingModule.exitedKeysCount ||
_correction.currentNodeOperatorExitedKeysCount != nodeOpExitedKeysCount ||
_correction.currentNodeOperatorStuckKeysCount != nodeOpStuckKeysCount
) {
revert UnexpectedCurrentKeysCount(
stakingModule.exitedKeysCount,
nodeOpExitedKeysCount,
nodeOpStuckKeysCount
);
}

stakingModule.exitedKeysCount = _correction.newModuleExitedKeysCount;

IStakingModule(moduleAddr).unsafeUpdateValidatorsKeysCount(
_nodeOperatorId,
_correction.newNodeOperatorExitedKeysCount,
_correction.newNodeOperatorStuckKeysCount
);

if (_triggerUpdateFinish) {
IStakingModule(moduleAddr).finishUpdatingExitedValidatorsKeysCount();
}
}

function reportStakingModuleStuckKeysCountByNodeOperator(
uint256 _stakingModuleId,
uint256[] calldata _nodeOperatorIds,
uint256[] calldata _stuckKeysCounts
)
external
onlyRole(REPORT_EXITED_KEYS_ROLE)
{
address moduleAddr = _getStakingModuleById(_stakingModuleId).stakingModuleAddress;
for (uint256 i = 0; i < _nodeOperatorIds.length; ) {
IStakingModule(moduleAddr).updateStuckValidatorsKeysCount(
_nodeOperatorIds[i],
_stuckKeysCounts[i]
);
unchecked { ++i; }
}
}
Expand Down
23 changes: 18 additions & 5 deletions contracts/0.8.9/interfaces/IStakingModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,33 @@ interface IStakingModule {
/// @param _totalShares Amount of stETH shares that were minted to reward all node operators.
function handleRewardsMinted(uint256 _totalShares) external;

/// @notice Updates the number of the validators of the given node operator that were requested
/// to exit but failed to do so in the max allowed time
/// @param _nodeOperatorId Id of the node operator
/// @param _stuckValidatorKeysCount New number of stuck validators of the node operator
function updateStuckValidatorsKeysCount(
uint256 _nodeOperatorId,
uint256 _stuckValidatorKeysCount
) external;

/// @notice Updates the number of the validators in the EXITED state for node operator with given id
/// @param _nodeOperatorId Id of the node operator
/// @param _exitedValidatorsKeysCount New number of EXITED validators of the node operator
/// @param _exitedValidatorKeysCount New number of EXITED validators of the node operator
/// @return number of exited validators across all node operators
function updateExitedValidatorsKeysCount(
uint256 _nodeOperatorId,
uint256 _exitedValidatorsKeysCount
uint256 _exitedValidatorKeysCount
) external returns (uint256);

/// @notice Unsafely updates the number of the validators in the EXITED state for node operator with given id
/// @notice Unsafely updates the validators count stats for node operator with given id
/// @param _nodeOperatorId Id of the node operator
/// @param _exitedValidatorsKeysCount New number of EXITED validators of the node operator
/// @return number of exited validators across all node operators
function unsafeUpdateExitedValidatorsKeysCount(uint256 _nodeOperatorId, uint256 _exitedValidatorsKeysCount) external returns (uint256);
/// @param _stuckValidatorsKeysCount New number of stuck validators of the node operator
function unsafeUpdateValidatorsKeysCount(
uint256 _nodeOperatorId,
uint256 _exitedValidatorsKeysCount,
uint256 _stuckValidatorsKeysCount
) external;

/// @notice Called by StakingRouter after oracle finishes updating exited keys counts for all operators.
function finishUpdatingExitedValidatorsKeysCount() external;
Expand Down
Loading

0 comments on commit 00f1a45

Please sign in to comment.