Burner
The contract provides a way for Lido protocol to burn stETH token shares as a means to finalize withdrawals, penalize untimely exiting node operators, and, possibly, cover losses in staking.
It relies on the rebasing nature of stETH. The Lido contract calculates
user balance using the following equation:
balanceOf(account) = shares[account] * totalPooledEther / totalShares.
Therefore, burning shares (e.g. decreasing the totalShares amount) increases stETH holders' balances.
It's presumed that actual shares burning happens inside the Lido contract as a part of the AccountingOracle report.
Burner provides a safe and deterministic way to incur a positive stETH token rebase by gradually
decreasing totalShares that can be correctly handled by 3rd party protocols integrated with stETH.
Burner accepts burning requests in the following two ways:
- Locking someone's pre-approved stETH by the caller with the assigned
REQUEST_BURN_SHARES_ROLE; - Locking caller-provided stETH with the
REQUEST_BURN_MY_STETH_ROLEassigned role.
Those burn requests are initially set by the contract to a pending state.
Actual burning happens as part of an oracle (AccountingOracle) report handling by Accounting to prevent
additional fluctuations of the existing stETH token rebase period (~24h).
We also distinguish two types of shares burn requests:
- request to cover a slashing event (e.g. decreasing of the total pooled ETH amount between the two consecutive oracle reports);
- request to burn shares for any other cases (non-cover).
The contract has two separate counters for the burnt shares: cover and non-cover ones. The contract is
exclusively responsible for the stETH shares burning by Lido and burning allowed only from the contract's
own balance only.
Shares burnt counters​
The contract keeps count of all shares ever burned by way of maintaining two internal counters:
totalCoverSharesBurnt and totalNonCoverSharesBurnt for cover and non-cover burns, respectively.
These counters are increased when actual stETH burn is performed as part of the Lido Oracle report.
This makes it possible to split any stETH rebase into two sub-components: the rewards-induced rebase and cover application-induced rebase, which can be done as follows:
-
Before the rebase, store the previous values of both counters, as well as the value of stETH share price:
prevCoverSharesBurnt = Burner.totalCoverSharesBurnt()
prevSharePrice = stETH.totalSupply() / stETH.getTotalShares() -
After the rebase, perform the following calculations:
sharesBurntFromOldToNew = Burner.totalCoverSharesBurnt() - prevCoverSharesBurnt;
newSharePriceAfterCov = stETH.totalSupply() / (stETH.getTotalShares() + sharesBurntFromOldToNew);
newSharePrice = stETH.totalSupply() / stETH.getTotalShares();
// rewards-induced share price increase
rewardPerShare = newSharePriceAfterCov - prevSharePrice;
// cover-induced share price increase
nonRewardSharePriceIncrease = newSharePrice - prevSharePrice - rewardPerShare;
Constants​
LOCATOR()​
Returns the address of the LidoLocator contract.
ILidoLocator public immutable LOCATOR;
LIDO()​
Returns the address of the Lido (stETH) contract.
ILido public immutable LIDO;
Roles​
REQUEST_BURN_MY_STETH_ROLE()​
An ACL role granting the permission to burn caller's own stETH.
bytes32 public constant REQUEST_BURN_MY_STETH_ROLE = keccak256("REQUEST_BURN_MY_STETH_ROLE");
REQUEST_BURN_SHARES_ROLE()​
An ACL role granting the permission to burn stETH shares on behalf of others.
bytes32 public constant REQUEST_BURN_SHARES_ROLE = keccak256("REQUEST_BURN_SHARES_ROLE");
View methods​
getCoverSharesBurnt()​
Returns the total cover shares ever burnt.
function getCoverSharesBurnt() external view returns (uint256)
getNonCoverSharesBurnt()​
Returns the total non-cover shares ever burnt.
function getNonCoverSharesBurnt() external view returns (uint256)
getExcessStETH()​
Returns the stETH amount belonging to the burner contract address but not marked for burning.
function getExcessStETH() external view returns (uint256)
getSharesRequestedToBurn()​
Returns numbers of cover and non-cover shares requested to burn.
function getSharesRequestedToBurn() external view returns (uint256 coverShares, uint256 nonCoverShares)
isMigrationAllowed()​
Returns whether migration from the old Burner is allowed. Used during V3 upgrade only.
function isMigrationAllowed() external view returns (bool)
Methods​
requestBurnMyStETHForCover()​
Transfers stETH tokens from the message sender and irreversibly locks these on the burner contract address.
Internally converts tokens amount into underlying shares amount and marks the converted shares amount
for cover-backed burning by increasing the internal coverSharesBurnRequested counter.
function requestBurnMyStETHForCover(uint256 _stETHAmountToBurn) external
Reverts if any of the following is true:
msg.senderis not a holder of theREQUEST_BURN_MY_STETH_ROLErole;- no stETH provided (
_stETHAmountToBurn == 0); - no stETH transferred (allowance exceeded).
Parameters​
| Name | Type | Description |
|---|---|---|
_stETHAmountToBurn | uint256 | stETH tokens amount (not shares amount) to burn |
requestBurnSharesForCover()​
Transfers stETH shares from _from and irreversibly locks these on the burner contract address.
Internally marks the shares amount for cover-backed burning by increasing the internal coverSharesBurnRequested counter.
Can be called only by a holder of REQUEST_BURN_SHARES_ROLE. After Lido V2 upgrade not actually called by any contract and supposed to be called by Lido DAO Agent in case of a need for cover.
function requestBurnSharesForCover(address _from, uint256 _sharesAmountToBurn)
Reverts if any of the following is true:
msg.senderis not a holder of theREQUEST_BURN_SHARES_ROLErole;- no stETH shares provided (
_sharesAmountToBurn == 0); - no stETH shares transferred (allowance exceeded).
Parameters​
| Name | Type | Description |
|---|---|---|
_from | address | address to transfer shares from |
_sharesAmountToBurn | uint256 | shares amount (not stETH tokens amount) to burn |
requestBurnMyShares()​
Transfers stETH shares from the message sender and irreversibly locks these on the burner contract address.
Marks the shares amount for non-cover backed burning by increasing the internal nonCoverSharesBurnRequested counter.
This is the preferred method for burning non-cover shares as it prevents dust accumulation.
function requestBurnMyShares(uint256 _sharesAmountToBurn) external
Reverts if any of the following is true:
msg.senderis not a holder of theREQUEST_BURN_MY_STETH_ROLErole;- no stETH shares provided (
_sharesAmountToBurn == 0); - no stETH shares transferred (allowance exceeded).
Parameters​
| Name | Type | Description |
|---|---|---|
_sharesAmountToBurn | uint256 | shares amount (not stETH tokens amount) to burn |
requestBurnMyStETH()​
DEPRECATED: Use requestBurnMyShares instead to prevent dust accumulation.
Transfers stETH tokens from the message sender and irreversibly locks these on the burner contract address.
Internally converts tokens amount into underlying shares amount and marks the converted amount for
non-cover backed burning by increasing the internal nonCoverSharesBurnRequested counter.
function requestBurnMyStETH(uint256 _stETHAmountToBurn) external
Reverts if any of the following is true:
msg.senderis not a holder of theREQUEST_BURN_MY_STETH_ROLErole;- no stETH provided (
_stETHAmountToBurn == 0); - no stETH transferred (allowance exceeded).
Parameters​
| Name | Type | Description |
|---|---|---|
_stETHAmountToBurn | uint256 | stETH tokens amount (not shares amount) to burn |
requestBurnShares()​
Transfers stETH shares from _from and irreversibly locks these on the burner contract address.
Internally marks the shares amount for non-cover backed burning by increasing the internal nonCoverSharesBurnRequested counter.
Can be called only by a holder of the REQUEST_BURN_SHARES_ROLE role which after
Lido V2 upgrade is either Lido or NodeOperatorsRegistry.
Lido needs this to request shares locked on the WithdrawalQueueERC721 and
NodeOperatorsRegistry needs it to request burning shares to penalize the rewards of misbehaving node operators.
function requestBurnShares(address _from, uint256 _sharesAmountToBurn)
Reverts if any of the following is true:
msg.senderis not a holder ofREQUEST_BURN_SHARES_ROLErole;- no stETH shares provided (
_sharesAmountToBurn == 0); - no stETH shares transferred (allowance exceeded).
Parameters​
| Name | Type | Description |
|---|---|---|
_from | address | address to transfer shares from |
_sharesAmountToBurn | uint256 | shares amount (not stETH tokens amount) to burn |
recoverExcessStETH()​
Transfers the excess stETH amount (e.g. belonging to the burner contract address but not marked for burning)
to the Lido treasury address (the DAO Agent contract) set upon the contract construction.
Does nothing if the getExcessStETH view func returns 0 (zero), i.e. there is no excess stETH
on the contract's balance.
function recoverExcessStETH() external
recoverERC20()​
Transfers a given amount of an ERC20-token (defined by the provided contract address) belonging
to the burner contract address to the Lido treasury (the DAO Agent contract) address.
function recoverERC20(address _token, uint256 _amount) external
Reverts if any of the following is true:
_amountvalue is 0 (zero);_tokenaddress is 0 (zero);_tokenaddress equals to thestETHaddress (userecoverExcessStETHinstead).
Parameters​
| Name | Type | Description |
|---|---|---|
_token | address | ERC20-compatible token address to recover |
_amount | uint256 | Amount to recover |
recoverERC721()​
Transfers a given ERC721-compatible NFT (defined by the contract address) belonging
to the burner contract address to the Lido treasury (the DAO Agent) address.
function recoverERC721(address _token, uint256 _tokenId) external
Reverts if any of the following is true:
_tokenaddress is 0 (zero);_tokenaddress equals to thestETHaddress (userecoverExcessStETHinstead).
Parameters​
| Name | Type | Description |
|---|---|---|
_token | address | ERC721-compatible token address to recover |
_tokenId | uint256 | Token id to recover |
commitSharesToBurn()​
Marks previously requested to burn cover and non-cover share as burnt.
Emits StETHBurnt event for the cover and non-cover shares marked as burnt.
Performs the actual shares burning by calling LIDO.burnShares().
This function is called by the Accounting contract as part of oracle report handling.
If _sharesToBurn is 0 does nothing.
function commitSharesToBurn(uint256 _sharesToBurn) external
Reverts if any of the following is true:
msg.senderaddress is NOT equal to theAccountingcontract address (viaLOCATOR.accounting());_sharesToBurnis greater than the cover plus non-cover shares requested to burn.
Parameters​
| Name | Type | Description |
|---|---|---|
_sharesToBurn | uint256 | Amount of cover plus non-cover shares to mark as burnt |
initialize()​
Initializes the contract by setting up roles and migration allowance. Should be called only once during deployment.
function initialize(address _admin, bool _isMigrationAllowed) external
Parameters​
| Name | Type | Description |
|---|---|---|
_admin | address | Address to be granted the DEFAULT_ADMIN_ROLE |
_isMigrationAllowed | bool | Whether migration from old Burner is allowed |
migrate()​
Migrates state from the old Burner contract. Can be called only by the Lido contract and only once.
function migrate(address _oldBurner) external
Parameters​
| Name | Type | Description |
|---|---|---|
_oldBurner | address | Address of the old Burner contract |
Events​
StETHBurnRequested​
Emitted when a new stETH burning request is added.
event StETHBurnRequested(
bool indexed isCover,
address indexed requestedBy,
uint256 amountOfStETH,
uint256 amountOfShares
)
StETHBurnt​
Emitted when stETH is burnt.
event StETHBurnt(bool indexed isCover, uint256 amountOfStETH, uint256 amountOfShares)
ExcessStETHRecovered​
Emitted when excess stETH is recovered to the treasury.
event ExcessStETHRecovered(address indexed requestedBy, uint256 amountOfStETH, uint256 amountOfShares)
ERC20Recovered​
Emitted when ERC20 tokens are recovered to the treasury.
event ERC20Recovered(address indexed requestedBy, address indexed token, uint256 amount)
ERC721Recovered​
Emitted when ERC721 NFTs are recovered to the treasury.
event ERC721Recovered(address indexed requestedBy, address indexed token, uint256 tokenId)