--- tags: withdrawals title: Withdrawals. Protocol safety nets for oracle reports --- ## Withdrawals. Protocol safety nets for oracle reports ⚠️ **THE DOCUMENT IS UNDER CONSTRUCTION** ⚠️ --- >**DISCLAIMER** > >All of the magic numbers in the doc are DAO-controlled. >They are fixed here as a reasonable defaults for initial proposal. ### Required reading - [Lido on Ethereum. Protocol accounting with enabled withdrawals](https://hackmd.io/@lido/HknYRrCws?type=view) - [LIP-2. Oracle contract upgrade to v2](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-2.md#sanity-checks-the-oracles-reports-by-configurable-values) - [Postmortem: Disrupted rewards distribution due to missed oracle reports](https://hackmd.io/@lido/HJckQzmtj?type=view) ### Considerations - Need to check oracle-reported data for sanity - 3rd-parties can top-up the protocol vaults to block oracle reports (if caps are too strict) - Too large rebases are subject of oracle report sandwiching (above 27% APR) - Want to re-use/upgrade the currently existing sanity checks - Prevent exiting all Lido validators by the misbehaving oracle committee - Check that old validators exit before new ones ### Glossary - Hard caps: lead to reverts if not met - Soft limits: saturation values, don't revert if exceeded, though emit events to warn ### Accounting oracle Solidity structure definition: ```solidity struct Report { // Consensus info uint256 _slotId; // CL values uint256 _clValidators; uint64 _clBalanceGwei; // Staking router address[] _stakingModules; uint256[] _nodeOperatorsWithExitedValidators; uint256[] _exitedValidatorsNumbers; // EL values uint256 _withdrawalVaultBalance; // decision uint256 _requestIdToFinalizeUpTo; bool _bunkerModeFlag; // todo: to be utilized later } ``` Lido: ```solidity function handleOracleReport( // Oracle report timing uint256 _timeElapsed, // CL values uint256 _clValidators, uint256 _clBalance, // EL values uint256 _withdrawalVaultBalance, uint256 _elRewardsVaultBalance, // Decision about withdrawals processing uint256 _requestIdToFinalizeUpTo, uint256 _finalizationShareRate ) external returns ``` + appearedValidator (Lido) StakingRouter: ```solidity // Staking router address[] _stakingModules; uint256[] _nodeOperatorsWithExitedValidators; uint256[] _exitedValidatorsNumbers; ``` WithdrawalQueue: ```solidity uint256 _previousReportTimestamp, bool isBunkerMode ``` #### Hard caps ##### Cap 1. Withdrawals vault one-off reported balance Can't exceed the balance at the current block. ```solidity if (_withdrawalVaultBalance > WithdrawalsVault.balance) { revert IncorrectWithdrawalsVaultBalance(); } ``` - in Lido ##### Cap 2. Consensus Layer one-off balances decrease Prohibit >5% one-off decrease. Have to account for withdrawals vault (why: the first week of the Shapella-enabled reports with massive skimmed rewards, leading to the lowered consensus layer balance) ```solidity _unifiedPostCLBalance = _postCLBalance + _withdrawalVaultBalance; if ((_preCLBalance - _unifiedPostCLBalance) > 5%) { revert IncorrectCLBalanceDecrease(); } ``` - in Lido >NOTE: was for the whole TVL before. ##### Cap 3. Consensus Layer annual balances increase Prohibit >10% *annual* increase. Must not account for withdrawals vault (why: suppose all validators had less than 30 ETH, i.e. unable to been skimmed) ```solidity reportBalanceDiff = (_postCLBalance - _preCLBalance); annualBalanceDiff = reportBalanceDiff * 365 days / _timeElapsed; if (annualBalanceDiff > 10%) { revert IncorrectBeaconBalanceIncrease(); } ``` - Lido >NOTE: was for the whole TVL before ##### Cap 4. Activation & exit churn limit Networks allows changing a validator set only within a certain [churn limit](https://kb.beaconcha.in/glossary#2.-pending). 55 validators per epoch correspond to the state if roughly 95% of total Ether supply is staked. ```solidity churnLimit = 55 * _timeElapsed / 1 epoch; if (_appearedValidators > churnLimit) { revert IncorrectAppearedValidators(); } if (_exitedValidators > churnLimit) { revert IncorrectExitedValidators(); } ``` - AccountingOracle itself? - Only Lido currently knows `appeared` - Though, Lido doesn't know `exited` ##### Cap 5. No finalized id up to newer than the allowed report margin 1h before the reported slot or below DAO defined delay (set temporary) ```solidity lastRequestTimestamp = withdrawalRequest(_requestIdToFinalizeUpTo).requestBlock; timeDiff = max(refReportTimestamp - 1h) - lastRequestTimestamp; if (timeDiff < 0) { revert IncorrectRequestFinalization(); } ``` * Bunker mode can change the margin (?) * 1h * WithdrawalQueue? ##### Cap 6. shareRate calculated off-chain is consistent with the on-chain one ```solidity ``` - maybe not in the contract - share rate should be sane (like TVL/total shares currently: getTotalPooledEther() / getTotalShares()) - tolerance? #### Soft limits ##### Positive rebase limiter No more than 27% apr increase per single report. ```solidity // initial value limiterVal = 27% of TVL // account for `beaconBalanceDiff` // NB: can either increase or decrease `limiterVal` limiterVal -= beaconBalanceDiff // saturate `_reportedWCBalance` _reportedWCBalance = min( limiterVal, _reportedWCBalance ); limiterVal -= _reportedWCBalance; // saturate `_ELRewardsVaultBalance` _ELRewardsVaultBalance = min( limiterVal, _ELRewardsVaultBalance ) limiterVal -= _ELRewardsVaultBalance; // saturate coverage applyCoverage(limiterVal) ``` ### ValidatorExitBus oracle Solidity structure definition: ```solidity struct Report { uint256 _slotId, address[] _stakingModules, uint256[] _nodeOperatorIds, uint256[] _validatorIds, bytes[] _validatorPubkeys } ``` #### Hard caps ##### Rate limit TODO (tooling?) ##### Ordering requirements For each particular node operator `validatorId` can't be less than previously reported (i.e., The old validators are have to be exited before the new ones).