---
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).