Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/feat/vaults' into feat/vaults-fu…
Browse files Browse the repository at this point in the history
…zzing-share-rate

# Conflicts:
#	package.json
#	test/0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol
  • Loading branch information
tamtamchik committed Feb 10, 2025
2 parents 8b88a72 + 31bf140 commit 7f6f404
Show file tree
Hide file tree
Showing 97 changed files with 5,134 additions and 3,528 deletions.
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ LOCAL_STAKING_ROUTER_ADDRESS=
LOCAL_VALIDATORS_EXIT_BUS_ORACLE_ADDRESS=
LOCAL_WITHDRAWAL_QUEUE_ADDRESS=
LOCAL_WITHDRAWAL_VAULT_ADDRESS=
LOCAL_STAKING_VAULT_FACTORY_ADDRESS=
LOCAL_STAKING_VAULT_BEACON_ADDRESS=

# RPC URL for a separate, non Hardhat Network node (Anvil, Infura, Alchemy, etc.)
MAINNET_RPC_URL=http://localhost:8545
Expand All @@ -46,13 +48,15 @@ MAINNET_STAKING_ROUTER_ADDRESS=
MAINNET_VALIDATORS_EXIT_BUS_ORACLE_ADDRESS=
MAINNET_WITHDRAWAL_QUEUE_ADDRESS=
MAINNET_WITHDRAWAL_VAULT_ADDRESS=
MAINNET_STAKING_VAULT_FACTORY_ADDRESS=
MAINNET_STAKING_VAULT_BEACON_ADDRESS=

HOLESKY_RPC_URL=
SEPOLIA_RPC_URL=

# RPC URL for Hardhat Network forking, required for running tests on mainnet fork with tracing (Infura, Alchemy, etc.)
# https://hardhat.org/hardhat-network/docs/guides/forking-other-networks#forking-other-networks
HARDHAT_FORKING_URL=
HARDHAT_FORKING_URL=https://eth.drpc.org

# Scratch deployment via hardhat variables
DEPLOYER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
path: ./coverage/cobertura-coverage.xml
publish: true
# TODO: restore to 95% before release
threshold: 80
threshold: 90
diff: true
diff-branch: master
diff-storage: _core_coverage_reports
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/tests-integration-mainnet.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
name: Integration Tests

#on: [push]
#
#jobs:
Expand All @@ -10,7 +9,7 @@ name: Integration Tests
#
# services:
# hardhat-node:
# image: ghcr.io/lidofinance/hardhat-node:2.22.17
# image: ghcr.io/lidofinance/hardhat-node:2.22.18
# ports:
# - 8545:8545
# env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests-integration-scratch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:

services:
hardhat-node:
image: ghcr.io/lidofinance/hardhat-node:2.22.17-scratch
image: ghcr.io/lidofinance/hardhat-node:2.22.18-scratch
ports:
- 8555:8545

Expand Down
7 changes: 6 additions & 1 deletion .solhintignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
contracts/openzeppelin/
contracts/0.8.9/utils/access/AccessControl.sol
contracts/0.8.9/utils/access/AccessControlEnumerable.sol

contracts/0.4.24/template/
contracts/0.6.11/deposit_contract.sol
contracts/0.6.12/WstETH.sol
contracts/0.6.12/
contracts/0.8.4/WithdrawalsManagerProxy.sol
contracts/0.8.9/LidoExecutionLayerRewardsVault.sol
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ mainnet environment, allowing you to run integration tests with trace logging.
> [!NOTE]
> Ensure that `HARDHAT_FORKING_URL` is set to Ethereum Mainnet RPC and `MAINNET_*` environment variables are set in the
> `.env` file (refer to `.env.example` for guidance).
> `.env` file (refer to `.env.example` for guidance). Otherwise, the tests will run against the Scratch deployment.
```bash
# Run all integration tests
Expand Down
21 changes: 18 additions & 3 deletions contracts/0.4.24/Lido.sol
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,11 @@ contract Lido is Versioned, StETHPermit, AragonApp {
event DepositedValidatorsChanged(uint256 depositedValidators);

// Emitted when oracle accounting report processed
// @dev `principalCLBalance` is the balance of the validators on previous report
// @dev `preClBalance` is the balance of the validators on previous report
// plus the amount of ether that was deposited to the deposit contract since then
event ETHDistributed(
uint256 indexed reportTimestamp,
uint256 principalCLBalance, // preClBalance + deposits
uint256 preClBalance, // actually its preClBalance + deposits due to compatibility reasons
uint256 postCLBalance,
uint256 withdrawalsWithdrawn,
uint256 executionLayerRewardsWithdrawn,
Expand Down Expand Up @@ -196,7 +196,7 @@ contract Lido is Versioned, StETHPermit, AragonApp {
* @param _eip712StETH eip712 helper contract for StETH
*/
function initialize(address _lidoLocator, address _eip712StETH) public payable onlyInit {
_bootstrapInitialHolder();
_bootstrapInitialHolder(); // stone in the elevator

LIDO_LOCATOR_POSITION.setStorageAddress(_lidoLocator);
emit LidoLocatorSet(_lidoLocator);
Expand Down Expand Up @@ -947,6 +947,21 @@ contract Lido is Versioned, StETHPermit, AragonApp {
return internalEther.add(_getExternalEther(internalEther));
}

/// @dev the numerator (in ether) of the share rate for StETH conversion between shares and ether and vice versa.
/// using the numerator and denominator different from totalShares and totalPooledEther allows to:
/// - avoid double precision loss on additional division on external ether calculations
/// - optimize gas cost of conversions between shares and ether
function _getShareRateNumerator() internal view returns (uint256) {
return _getInternalEther();
}

/// @dev the denominator (in shares) of the share rate for StETH conversion between shares and ether and vice versa.
function _getShareRateDenominator() internal view returns (uint256) {
uint256 externalShares = EXTERNAL_SHARES_POSITION.getStorageUint256();
uint256 internalShares = _getTotalShares() - externalShares; // never 0 because of the stone in the elevator
return internalShares;
}

/// @notice Calculate the maximum amount of external shares that can be minted while maintaining
/// maximum allowed external ratio limits
/// @return Maximum amount of external shares that can be minted
Expand Down
34 changes: 25 additions & 9 deletions contracts/0.4.24/StETH.sol
Original file line number Diff line number Diff line change
Expand Up @@ -303,17 +303,17 @@ contract StETH is IERC20, Pausable {
*/
function getSharesByPooledEth(uint256 _ethAmount) public view returns (uint256) {
return _ethAmount
.mul(_getTotalShares())
.div(_getTotalPooledEther());
.mul(_getShareRateDenominator()) // denominator in shares
.div(_getShareRateNumerator()); // numerator in ether
}

/**
* @return the amount of ether that corresponds to `_sharesAmount` token shares.
*/
function getPooledEthByShares(uint256 _sharesAmount) public view returns (uint256) {
return _sharesAmount
.mul(_getTotalPooledEther())
.div(_getTotalShares());
.mul(_getShareRateNumerator()) // numerator in ether
.div(_getShareRateDenominator()); // denominator in shares
}

/**
Expand All @@ -322,14 +322,14 @@ contract StETH is IERC20, Pausable {
* for `shareRate >= 0.5`, `getSharesByPooledEth(getPooledEthBySharesRoundUp(1))` will be 1.
*/
function getPooledEthBySharesRoundUp(uint256 _sharesAmount) public view returns (uint256 etherAmount) {
uint256 totalEther = _getTotalPooledEther();
uint256 totalShares = _getTotalShares();
uint256 numeratorInEther = _getShareRateNumerator();
uint256 denominatorInShares = _getShareRateDenominator();

etherAmount = _sharesAmount
.mul(totalEther)
.div(totalShares);
.mul(numeratorInEther)
.div(denominatorInShares);

if (etherAmount.mul(totalShares) != _sharesAmount.mul(totalEther)) {
if (_sharesAmount.mul(numeratorInEther) != etherAmount.mul(denominatorInShares)) {
++etherAmount;
}
}
Expand Down Expand Up @@ -389,6 +389,22 @@ contract StETH is IERC20, Pausable {
*/
function _getTotalPooledEther() internal view returns (uint256);

/**
* @return the numerator of the protocol's share rate (in ether).
* @dev used to convert shares to tokens and vice versa.
*/
function _getShareRateNumerator() internal view returns (uint256) {
return _getTotalPooledEther();
}

/**
* @return the denominator of the protocol's share rate (in shares).
* @dev used to convert shares to tokens and vice versa.
*/
function _getShareRateDenominator() internal view returns (uint256) {
return _getTotalShares();
}

/**
* @notice Moves `_amount` tokens from `_sender` to `_recipient`.
* Emits a `Transfer` event.
Expand Down
61 changes: 32 additions & 29 deletions contracts/0.8.25/Accounting.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Lido <[email protected]>
// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
Expand All @@ -17,9 +17,11 @@ import {ReportValues} from "contracts/common/interfaces/ReportValues.sol";

/// @title Lido Accounting contract
/// @author folkyatina
/// @notice contract is responsible for handling oracle reports
/// @notice contract is responsible for handling accounting oracle reports
/// calculating all the state changes that is required to apply the report
/// and distributing calculated values to relevant parts of the protocol
/// @dev accounting is inherited from VaultHub contract to reduce gas costs and
/// simplify the auth flows, but they are mostly independent
contract Accounting is VaultHub {
struct Contracts {
address accountingOracleAddress;
Expand Down Expand Up @@ -54,11 +56,12 @@ contract Accounting is VaultHub {
uint256 sharesToBurnForWithdrawals;
/// @notice number of stETH shares that will be burned from Burner this report
uint256 totalSharesToBurn;
/// @notice number of stETH shares to mint as a fee to Lido treasury
/// @notice number of stETH shares to mint as a protocol fee
uint256 sharesToMintAsFees;
/// @notice amount of NO fees to transfer to each module
StakingRewardsDistribution rewardDistribution;
/// @notice amount of CL ether that is not rewards earned during this report period
/// the sum of CL balance on the previous report and the amount of fresh deposits since then
uint256 principalClBalance;
/// @notice total number of stETH shares after the report is applied
uint256 postTotalShares;
Expand Down Expand Up @@ -104,11 +107,11 @@ contract Accounting is VaultHub {

/// @notice calculates all the state changes that is required to apply the report
/// @param _report report values
/// @param _withdrawalShareRate maximum share rate used for withdrawal resolution
/// @param _withdrawalShareRate maximum share rate used for withdrawal finalization
/// if _withdrawalShareRate == 0, no withdrawals are
/// simulated
function simulateOracleReport(
ReportValues memory _report,
ReportValues calldata _report,
uint256 _withdrawalShareRate
) public view returns (CalculatedValues memory update) {
Contracts memory contracts = _loadOracleReportContracts();
Expand All @@ -120,7 +123,7 @@ contract Accounting is VaultHub {
/// @notice Updates accounting stats, collects EL rewards and distributes collected rewards
/// if beacon balance increased, performs withdrawal requests finalization
/// @dev periodically called by the AccountingOracle contract
function handleOracleReport(ReportValues memory _report) external {
function handleOracleReport(ReportValues calldata _report) external {
Contracts memory contracts = _loadOracleReportContracts();
if (msg.sender != contracts.accountingOracleAddress) revert NotAuthorized("handleOracleReport", msg.sender);

Expand All @@ -136,7 +139,7 @@ contract Accounting is VaultHub {
/// @dev prepare all the required data to process the report
function _calculateOracleReportContext(
Contracts memory _contracts,
ReportValues memory _report
ReportValues calldata _report
) internal view returns (PreReportState memory pre, CalculatedValues memory update, uint256 withdrawalsShareRate) {
pre = _snapshotPreReportState();

Expand All @@ -161,7 +164,7 @@ contract Accounting is VaultHub {
function _simulateOracleReport(
Contracts memory _contracts,
PreReportState memory _pre,
ReportValues memory _report,
ReportValues calldata _report,
uint256 _withdrawalsShareRate
) internal view returns (CalculatedValues memory update) {
update.rewardDistribution = _getStakingRewardsDistribution(_contracts.stakingRouter);
Expand Down Expand Up @@ -239,7 +242,7 @@ contract Accounting is VaultHub {
/// @dev return amount to lock on withdrawal queue and shares to burn depending on the finalization batch parameters
function _calculateWithdrawals(
Contracts memory _contracts,
ReportValues memory _report,
ReportValues calldata _report,
uint256 _simulatedShareRate
) internal view returns (uint256 etherToLock, uint256 sharesToBurn) {
if (_report.withdrawalFinalizationBatches.length != 0 && !_contracts.withdrawalQueue.isPaused()) {
Expand All @@ -250,36 +253,36 @@ contract Accounting is VaultHub {
}
}

/// @dev calculates shares that are minted to treasury as the protocol fees
/// @dev calculates shares that are minted as the protocol fees
function _calculateFeesAndExternalEther(
ReportValues memory _report,
ReportValues calldata _report,
PreReportState memory _pre,
CalculatedValues memory _calculated
CalculatedValues memory _update
) internal pure returns (uint256 sharesToMintAsFees, uint256 postExternalEther) {
// we are calculating the share rate equal to the post-rebase share rate
// but with fees taken as eth deduction
// and without externalBalance taken into account
uint256 shares = _pre.totalShares - _calculated.totalSharesToBurn - _pre.externalShares;
uint256 eth = _pre.totalPooledEther - _calculated.etherToFinalizeWQ - _pre.externalEther;
uint256 shares = _pre.totalShares - _update.totalSharesToBurn - _pre.externalShares;
uint256 eth = _pre.totalPooledEther - _update.etherToFinalizeWQ - _pre.externalEther;

uint256 unifiedClBalance = _report.clBalance + _calculated.withdrawals;
uint256 unifiedClBalance = _report.clBalance + _update.withdrawals;

// Don't mint/distribute any protocol fee on the non-profitable Lido oracle report
// (when consensus layer balance delta is zero or negative).
// See LIP-12 for details:
// https://research.lido.fi/t/lip-12-on-chain-part-of-the-rewards-distribution-after-the-merge/1625
if (unifiedClBalance > _calculated.principalClBalance) {
uint256 totalRewards = unifiedClBalance - _calculated.principalClBalance + _calculated.elRewards;
uint256 totalFee = _calculated.rewardDistribution.totalFee;
uint256 precision = _calculated.rewardDistribution.precisionPoints;
if (unifiedClBalance > _update.principalClBalance) {
uint256 totalRewards = unifiedClBalance - _update.principalClBalance + _update.elRewards;
uint256 totalFee = _update.rewardDistribution.totalFee;
uint256 precision = _update.rewardDistribution.precisionPoints;
uint256 feeEther = (totalRewards * totalFee) / precision;
eth += totalRewards - feeEther;

// but we won't pay fees in ether, so we need to calculate how many shares we need to mint as fees
sharesToMintAsFees = (feeEther * shares) / eth;
} else {
uint256 clPenalty = _calculated.principalClBalance - unifiedClBalance;
eth = eth - clPenalty + _calculated.elRewards;
uint256 clPenalty = _update.principalClBalance - unifiedClBalance;
eth = eth - clPenalty + _update.elRewards;
}

// externalBalance is rebasing at the same rate as the primary balance does
Expand All @@ -289,10 +292,10 @@ contract Accounting is VaultHub {
/// @dev applies the precalculated changes to the protocol state
function _applyOracleReportContext(
Contracts memory _contracts,
ReportValues memory _report,
ReportValues calldata _report,
PreReportState memory _pre,
CalculatedValues memory _update,
uint256 _simulatedShareRate
uint256 _withdrawalShareRate
) internal {
_checkAccountingOracleReport(_contracts, _report, _pre, _update);

Expand Down Expand Up @@ -328,13 +331,13 @@ contract Accounting is VaultHub {
_update.withdrawals,
_update.elRewards,
lastWithdrawalRequestToFinalize,
_simulatedShareRate,
_withdrawalShareRate,
_update.etherToFinalizeWQ
);

_updateVaults(
_report.vaultValues,
_report.netCashFlows,
_report.inOutDeltas,
_update.vaultsLockedEther,
_update.vaultsTreasuryFeeShares
);
Expand All @@ -343,7 +346,7 @@ contract Accounting is VaultHub {
STETH.mintExternalShares(LIDO_LOCATOR.treasury(), _update.totalVaultsTreasuryFeeShares);
}

_notifyObserver(_contracts.postTokenRebaseReceiver, _report, _pre, _update);
_notifyRebaseObserver(_contracts.postTokenRebaseReceiver, _report, _pre, _update);

LIDO.emitTokenRebase(
_report.timestamp,
Expand All @@ -360,7 +363,7 @@ contract Accounting is VaultHub {
/// reverts if a check fails
function _checkAccountingOracleReport(
Contracts memory _contracts,
ReportValues memory _report,
ReportValues calldata _report,
PreReportState memory _pre,
CalculatedValues memory _update
) internal {
Expand Down Expand Up @@ -389,9 +392,9 @@ contract Accounting is VaultHub {
}

/// @dev Notify observer about the completed token rebase.
function _notifyObserver(
function _notifyRebaseObserver(
IPostTokenRebaseReceiver _postTokenRebaseReceiver,
ReportValues memory _report,
ReportValues calldata _report,
PreReportState memory _pre,
CalculatedValues memory _update
) internal {
Expand Down
16 changes: 16 additions & 0 deletions contracts/0.8.25/interfaces/IDepositContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
pragma solidity 0.8.25;

interface IDepositContract {
function get_deposit_root() external view returns (bytes32 rootHash);

function deposit(
bytes calldata pubkey, // 48 bytes
bytes calldata withdrawal_credentials, // 32 bytes
bytes calldata signature, // 96 bytes
bytes32 deposit_data_root
) external payable;
}
Loading

0 comments on commit 7f6f404

Please sign in to comment.