Skip to content

Commit

Permalink
Merge pull request #924 from lidofinance/accounting-review-fixes
Browse files Browse the repository at this point in the history
chore: accounting fixes after review
  • Loading branch information
tamtamchik authored Jan 28, 2025
2 parents 334155a + 3e9489d commit 50419bc
Show file tree
Hide file tree
Showing 26 changed files with 147 additions and 125 deletions.
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
4 changes: 2 additions & 2 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
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 <info@lido.fi>
// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi>
// 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
7 changes: 3 additions & 4 deletions contracts/0.8.9/oracle/AccountingOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,8 @@ contract AccountingOracle is BaseOracle {
/// @dev The values of the vaults as observed at the reference slot.
/// Sum of all the balances of Lido validators of the vault plus the balance of the vault itself.
uint256[] vaultsValues;
/// @dev The net cash flows of the vaults as observed at the reference slot.
/// Flow of the funds in and out of the vaults (deposit/withdrawal) without the rewards.
int256[] vaultsNetCashFlows;
/// @dev The in-out deltas (deposits - withdrawals) of the vaults as observed at the reference slot.
int256[] vaultsInOutDeltas;
///
/// Extra data — the oracle information that allows asynchronous processing in
/// chunks, after the main data is processed. The oracle doesn't enforce that extra data
Expand Down Expand Up @@ -583,7 +582,7 @@ contract AccountingOracle is BaseOracle {
data.sharesRequestedToBurn,
data.withdrawalFinalizationBatches,
data.vaultsValues,
data.vaultsNetCashFlows
data.vaultsInOutDeltas
)
);

Expand Down
2 changes: 1 addition & 1 deletion contracts/common/interfaces/ILidoLocator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
// solhint-disable-next-line
// solhint-disable-next-line lido/fixed-compiler-version
pragma solidity >=0.4.24 <0.9.0;

interface ILidoLocator {
Expand Down
9 changes: 4 additions & 5 deletions contracts/common/interfaces/ReportValues.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
// solhint-disable-next-line
pragma solidity >=0.4.24 <0.9.0;
// solhint-disable-next-line lido/fixed-compiler-version
pragma solidity ^0.8.9;

struct ReportValues {
/// @notice timestamp of the block the report is based on. All provided report values is actual on this timestamp
Expand All @@ -27,7 +27,6 @@ struct ReportValues {
/// (sum of all the balances of Lido validators of the vault
/// plus the balance of the vault itself)
uint256[] vaultValues;
/// @notice netCashFlow of each Lido vault
/// (difference between deposits to and withdrawals from the vault)
int256[] netCashFlows;
/// @notice in-out deltas (deposits - withdrawals) of each Lido vault
int256[] inOutDeltas;
}
20 changes: 10 additions & 10 deletions contracts/testnets/sepolia/SepoliaDepositAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
/* See contracts/COMPILERS.md */
pragma solidity 0.8.9;

import "@openzeppelin/contracts-v4.4/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts-v4.4/access/Ownable.sol";
import "../../0.8.9/utils/Versioned.sol";
import {IERC20} from "@openzeppelin/contracts-v4.4/token/ERC20/IERC20.sol";
import {Ownable} from "@openzeppelin/contracts-v4.4/access/Ownable.sol";
import {Versioned} from "../../0.8.9/utils/Versioned.sol";

interface IDepositContract {
event DepositEvent(bytes pubkey, bytes withdrawal_credentials, bytes amount, bytes signature, bytes index);
Expand Down Expand Up @@ -43,10 +43,10 @@ contract SepoliaDepositAdapter is IDepositContract, Ownable, Versioned {
error ZeroAddress(string field);

// Sepolia original deposit contract address
ISepoliaDepositContract public immutable originalContract;
ISepoliaDepositContract public immutable ORIGINAL_CONTRACT;

constructor(address _deposit_contract) {
originalContract = ISepoliaDepositContract(_deposit_contract);
ORIGINAL_CONTRACT = ISepoliaDepositContract(_deposit_contract);
}

function initialize(address _owner) external {
Expand All @@ -57,11 +57,11 @@ contract SepoliaDepositAdapter is IDepositContract, Ownable, Versioned {
}

function get_deposit_root() external view override returns (bytes32) {
return originalContract.get_deposit_root();
return ORIGINAL_CONTRACT.get_deposit_root();
}

function get_deposit_count() external view override returns (bytes memory) {
return originalContract.get_deposit_count();
return ORIGINAL_CONTRACT.get_deposit_count();
}

receive() external payable {
Expand All @@ -79,8 +79,8 @@ contract SepoliaDepositAdapter is IDepositContract, Ownable, Versioned {
}

function recoverBepolia() external onlyOwner {
uint256 bepoliaOwnTokens = originalContract.balanceOf(address(this));
bool success = originalContract.transfer(owner(), bepoliaOwnTokens);
uint256 bepoliaOwnTokens = ORIGINAL_CONTRACT.balanceOf(address(this));
bool success = ORIGINAL_CONTRACT.transfer(owner(), bepoliaOwnTokens);
if (!success) {
revert BepoliaRecoverFailed();
}
Expand All @@ -93,7 +93,7 @@ contract SepoliaDepositAdapter is IDepositContract, Ownable, Versioned {
bytes calldata signature,
bytes32 deposit_data_root
) external payable override {
originalContract.deposit{value: msg.value}(pubkey, withdrawal_credentials, signature, deposit_data_root);
ORIGINAL_CONTRACT.deposit{value: msg.value}(pubkey, withdrawal_credentials, signature, deposit_data_root);
// solhint-disable-next-line avoid-low-level-calls
(bool success,) = owner().call{value: msg.value}("");
if (!success) {
Expand Down
4 changes: 2 additions & 2 deletions lib/oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const DEFAULT_REPORT_FIELDS: OracleReport = {
withdrawalFinalizationBatches: [],
isBunkerMode: false,
vaultsValues: [],
vaultsNetCashFlows: [],
vaultsInOutDeltas: [],
extraDataFormat: 0n,
extraDataHash: ethers.ZeroHash,
extraDataItemsCount: 0n,
Expand All @@ -66,7 +66,7 @@ export function getReportDataItems(r: OracleReport) {
r.withdrawalFinalizationBatches,
r.isBunkerMode,
r.vaultsValues,
r.vaultsNetCashFlows,
r.vaultsInOutDeltas,
r.extraDataFormat,
r.extraDataHash,
r.extraDataItemsCount,
Expand Down
Loading

0 comments on commit 50419bc

Please sign in to comment.