Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merkle Oracle integration experiment #977

Draft
wants to merge 13 commits into
base: feat/vaults
Choose a base branch
from
39 changes: 20 additions & 19 deletions contracts/0.8.25/Accounting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,6 @@ contract Accounting {
uint256 postTotalShares;
/// @notice amount of ether under the protocol after the report is applied
uint256 postTotalPooledEther;
/// @notice amount of ether to be locked in the vaults
uint256[] vaultsLockedEther;
/// @notice amount of shares to be minted as vault fees to the treasury
uint256[] vaultsTreasuryFeeShares;
/// @notice total amount of shares to be minted as vault fees to the treasury
uint256 totalVaultsTreasuryFeeShares;
}
Expand Down Expand Up @@ -221,15 +217,15 @@ contract Accounting {

// Calculate the amount of ether locked in the vaults to back external balance of stETH
// and the amount of shares to mint as fees to the treasury for each vault
(update.vaultsLockedEther, update.vaultsTreasuryFeeShares, update.totalVaultsTreasuryFeeShares) =
_contracts.vaultHub.calculateVaultsRebase(
_report.vaultValues,
_pre.totalShares,
_pre.totalPooledEther,
update.postInternalShares,
update.postInternalEther,
update.sharesToMintAsFees
);
// (update.vaultsLockedEther, update.vaultsTreasuryFeeShares, update.totalVaultsTreasuryFeeShares) =
// _contracts.vaultHub.calculateVaultsRebase(
// _report.vaultValues,
// _pre.totalShares,
// _pre.totalPooledEther,
// update.postInternalShares,
// update.postInternalEther,
// update.sharesToMintAsFees
// );

uint256 externalShares = _pre.externalShares + update.totalVaultsTreasuryFeeShares;

Expand Down Expand Up @@ -328,13 +324,18 @@ contract Accounting {
_update.etherToFinalizeWQ
);

// TODO: Remove this once decide on vaults reporting
_contracts.vaultHub.updateVaults(
_report.vaultValues,
_report.inOutDeltas,
_update.vaultsLockedEther,
_update.vaultsTreasuryFeeShares
_contracts.vaultHub.updateVaultsData(
_report.timestamp,
_report.vaultsDataTreeRoot,
_report.vaultsDataTreeCid
);
// TODO: Remove this once decide on vaults reporting
// _contracts.vaultHub.updateVaults(
// _report.vaultValues,
// _report.inOutDeltas,
// _update.vaultsLockedEther,
// _update.vaultsTreasuryFeeShares
// );

if (_update.totalVaultsTreasuryFeeShares > 0) {
_contracts.vaultHub.mintVaultsTreasuryFeeShares(_update.totalVaultsTreasuryFeeShares);
Expand Down
23 changes: 20 additions & 3 deletions contracts/0.8.25/vaults/StakingVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {

__Ownable_init(_owner);
_getStorage().nodeOperator = _nodeOperator;
ERC7201Storage storage $ = _getStorage();
$.report.timestamp = uint64(block.timestamp);
}

/**
Expand Down Expand Up @@ -193,6 +195,7 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {
* @dev Valuation = latestReport.valuation + (current inOutDelta - latestReport.inOutDelta)
*/
function valuation() public view returns (uint256) {
checkReportFreshness();
ERC7201Storage storage $ = _getStorage();
return uint256(int256(int128($.report.valuation) + $.inOutDelta - $.report.inOutDelta));
}
Expand Down Expand Up @@ -356,16 +359,17 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {
* @param _inOutDelta New net difference between funded and withdrawn ether
* @param _locked New amount of locked ether
*/
function report(uint256 _valuation, int256 _inOutDelta, uint256 _locked) external {
function report(uint256 _timestamp, uint256 _valuation, int256 _inOutDelta, uint256 _locked) external {
if (msg.sender != address(VAULT_HUB)) revert NotAuthorized("report", msg.sender);

ERC7201Storage storage $ = _getStorage();

$.report.timestamp = uint64(_timestamp);
$.report.valuation = uint128(_valuation);
$.report.inOutDelta = int128(_inOutDelta);
$.locked = uint128(_locked);

emit Reported(_valuation, _inOutDelta, _locked);
emit Reported(_timestamp, _valuation, _inOutDelta, _locked);
}

/**
Expand Down Expand Up @@ -531,6 +535,14 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {
emit ValidatorWithdrawalTriggered(msg.sender, _pubkeys, _amounts, _refundRecipient, excess);
}

function isReportFresh() internal view returns (bool) {
return block.timestamp - _getStorage().report.timestamp < 1 days;
}

function checkReportFreshness() internal view {
if (!isReportFresh()) revert FreshReportRequired();
}

function _getStorage() private pure returns (ERC7201Storage storage $) {
assembly {
$.slot := ERC7201_STORAGE_LOCATION
Expand Down Expand Up @@ -566,7 +578,7 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {
* @param inOutDelta Net difference between ether funded and withdrawn from `StakingVault`
* @param locked Amount of ether locked in `StakingVault`
*/
event Reported(uint256 valuation, int256 inOutDelta, uint256 locked);
event Reported(uint256 timestamp, uint256 valuation, int256 inOutDelta, uint256 locked);

/**
* @notice Emitted if `owner` of `StakingVault` is a contract and its `onReport` hook reverts
Expand Down Expand Up @@ -724,4 +736,9 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {
* @notice Thrown when partial withdrawals are not allowed when valuation is below locked
*/
error PartialWithdrawalNotAllowed();

/**
* @notice Thrown when the report is not fresh
*/
error FreshReportRequired();
}
120 changes: 84 additions & 36 deletions contracts/0.8.25/vaults/VaultHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
pragma solidity 0.8.25;

import {OwnableUpgradeable} from "contracts/openzeppelin/5.2/upgradeable/access/OwnableUpgradeable.sol";
import {MerkleProof} from "@openzeppelin/contracts-v5.2/utils/cryptography/MerkleProof.sol";

import {IStakingVault} from "./interfaces/IStakingVault.sol";
import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol";
Expand All @@ -31,6 +32,12 @@ contract VaultHub is PausableUntilWithRoles {
mapping(address => uint256) vaultIndex;
/// @notice allowed beacon addresses
mapping(bytes32 => bool) vaultProxyCodehash;
/// @notice timestamp of the vaults data
uint256 vaultsDataTimestamp;
/// @notice root of the vaults data tree
bytes32 vaultsDataTreeRoot;
/// @notice CID of the vaults data tree
string vaultsDataTreeCid;
}

struct VaultSocket {
Expand Down Expand Up @@ -234,6 +241,19 @@ contract VaultHub is PausableUntilWithRoles {
emit ShareLimitUpdated(_vault, _shareLimit);
}

function updateVaultsData(
uint256 _vaultsDataTimestamp,
bytes32 _vaultsDataTreeRoot,
string memory _vaultsDataTreeCid
) external {
if (msg.sender != LIDO_LOCATOR.accounting()) revert NotAuthorized("updateVaultsData", msg.sender);

VaultHubStorage storage $ = _getVaultHubStorage();
$.vaultsDataTimestamp = _vaultsDataTimestamp;
$.vaultsDataTreeRoot = _vaultsDataTreeRoot;
$.vaultsDataTreeCid = _vaultsDataTreeCid;
}

/// @notice force disconnects a vault from the hub
/// @param _vault vault address
/// @dev msg.sender must have VAULT_MASTER_ROLE
Expand Down Expand Up @@ -410,7 +430,7 @@ contract VaultHub is PausableUntilWithRoles {

socket.pendingDisconnect = true;

vault_.report(vault_.valuation(), vault_.inOutDelta(), 0);
vault_.report(block.timestamp, vault_.valuation(), vault_.inOutDelta(), 0);

emit VaultDisconnected(_vault);
}
Expand Down Expand Up @@ -531,44 +551,71 @@ contract VaultHub is PausableUntilWithRoles {
treasuryFeeShares = potentialRewards * socket.treasuryFeeBP * _postInternalShares / (_postInternalEther * TOTAL_BASIS_POINTS);
}

function updateVaults(
uint256[] memory _valuations,
int256[] memory _inOutDeltas,
uint256[] memory _locked,
uint256[] memory _treasureFeeShares
) external {
if (msg.sender != LIDO_LOCATOR.accounting()) revert NotAuthorized("updateVaults", msg.sender);
function invalidateVaultsData(address _vault, uint256 _valuation, int256 _inOutDelta, uint256 _fees, uint256 _sharesMinted, bytes32[] memory _proof) external {
VaultHubStorage storage $ = _getVaultHubStorage();

for (uint256 i = 0; i < _valuations.length; i++) {
VaultSocket storage socket = $.sockets[i + 1];

if (socket.pendingDisconnect) continue; // we skip disconnected vaults

uint256 treasuryFeeShares = _treasureFeeShares[i];
if (treasuryFeeShares > 0) {
socket.sharesMinted += uint96(treasuryFeeShares);
}

IStakingVault(socket.vault).report(_valuations[i], _inOutDeltas[i], _locked[i]);
}

uint256 length = $.sockets.length;

for (uint256 i = 1; i < length; i++) {
VaultSocket storage socket = $.sockets[i];
if (socket.pendingDisconnect) {
// remove disconnected vault from the list
VaultSocket memory lastSocket = $.sockets[length - 1];
$.sockets[i] = lastSocket;
$.vaultIndex[lastSocket.vault] = i;
$.sockets.pop(); // TODO: replace with length--
delete $.vaultIndex[socket.vault];
--length;
}
}
if ($.vaultIndex[_vault] == 0) revert NotConnectedToHub(_vault);

bytes32 root = $.vaultsDataTreeRoot;
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encodePacked(_vault, _valuation, _inOutDelta, _fees, _sharesMinted))));
if (!MerkleProof.verify(_proof, root, leaf)) revert InvalidProof();

VaultSocket storage socket = $.sockets[$.vaultIndex[_vault]];
uint256 newMintedShares = socket.sharesMinted;
newMintedShares += _fees;

uint256 internalEther = LIDO.getTotalPooledEther() - LIDO.getExternalEther();
uint256 internalShares = LIDO.getTotalShares() - LIDO.getExternalShares();

uint256 lockedEther = Math256.max(
// combining two division into one here:
// uint256 newMintedStETH = (newMintedShares * _postInternalEther) / _postInternalShares;
// uint256 lockedEther = newMintedStETH * TOTAL_BASIS_POINTS / (TOTAL_BASIS_POINTS - socket.reserveRatioBP);
(newMintedShares * internalEther * TOTAL_BASIS_POINTS)
/ (internalShares * (TOTAL_BASIS_POINTS - socket.reserveRatioBP)),
CONNECT_DEPOSIT
);
socket.sharesMinted = uint96(newMintedShares);
IStakingVault(socket.vault).report($.vaultsDataTimestamp, _valuation, _inOutDelta, lockedEther);
}

// function updateVaults(
// uint256[] memory _valuations,
// int256[] memory _inOutDeltas,
// uint256[] memory _locked,
// uint256[] memory _treasureFeeShares
// ) external {
// if (msg.sender != LIDO_LOCATOR.accounting()) revert NotAuthorized("updateVaults", msg.sender);
// VaultHubStorage storage $ = _getVaultHubStorage();

// for (uint256 i = 0; i < _valuations.length; i++) {
// VaultSocket storage socket = $.sockets[i + 1];

// if (socket.pendingDisconnect) continue; // we skip disconnected vaults

// uint256 treasuryFeeShares = _treasureFeeShares[i];
// if (treasuryFeeShares > 0) {
// socket.sharesMinted += uint96(treasuryFeeShares);
// }

// IStakingVault(socket.vault).report(_valuations[i], _inOutDeltas[i], _locked[i]);
// }

// uint256 length = $.sockets.length;

// for (uint256 i = 1; i < length; i++) {
// VaultSocket storage socket = $.sockets[i];
// if (socket.pendingDisconnect) {
// // remove disconnected vault from the list
// VaultSocket memory lastSocket = $.sockets[length - 1];
// $.sockets[i] = lastSocket;
// $.vaultIndex[lastSocket.vault] = i;
// $.sockets.pop(); // TODO: replace with length--
// delete $.vaultIndex[socket.vault];
// --length;
// }
// }
// }

function mintVaultsTreasuryFeeShares(uint256 _amountOfShares) external {
if (msg.sender != LIDO_LOCATOR.accounting()) revert NotAuthorized("mintVaultsTreasuryFeeShares", msg.sender);
LIDO.mintExternalShares(LIDO_LOCATOR.treasury(), _amountOfShares);
Expand Down Expand Up @@ -642,4 +689,5 @@ contract VaultHub is PausableUntilWithRoles {
error ConnectedVaultsLimitTooLow(uint256 connectedVaultsLimit, uint256 currentVaultsCount);
error RelativeShareLimitBPTooHigh(uint256 relativeShareLimitBP, uint256 totalBasisPoints);
error VaultDepositorNotAllowed(address depositor);
error InvalidProof();
}
3 changes: 2 additions & 1 deletion contracts/0.8.25/vaults/interfaces/IStakingVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface IStakingVault {
* @custom:inOutDelta Net difference between ether funded and withdrawn from `StakingVault`
*/
struct Report {
uint64 timestamp;
uint128 valuation;
int128 inOutDelta;
}
Expand Down Expand Up @@ -43,7 +44,7 @@ interface IStakingVault {
function lock(uint256 _locked) external;
function rebalance(uint256 _ether) external;
function latestReport() external view returns (Report memory);
function report(uint256 _valuation, int256 _inOutDelta, uint256 _locked) external;
function report(uint256 _timestamp, uint256 _valuation, int256 _inOutDelta, uint256 _locked) external;
function withdrawalCredentials() external view returns (bytes32);
function beaconChainDepositsPaused() external view returns (bool);
function pauseBeaconChainDeposits() external;
Expand Down
15 changes: 11 additions & 4 deletions contracts/0.8.9/oracle/AccountingOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,15 @@ 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;
// uint256[] vaultsValues;
/// @dev The in-out deltas (deposits - withdrawals) of the vaults as observed at the reference slot.
int256[] vaultsInOutDeltas;
// int256[] vaultsInOutDeltas;
/// @dev The total vaults fees as observed at the reference slot.
uint256 vaultsTotalFees;
/// @dev Merkle Tree root of the vaults data.
bytes32 vaultsDataTreeRoot;
/// @notice CID of the published Merkle tree of the vault data.
string vaultsDataTreeCid;
///
/// 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 @@ -581,8 +587,9 @@ contract AccountingOracle is BaseOracle {
data.elRewardsVaultBalance,
data.sharesRequestedToBurn,
data.withdrawalFinalizationBatches,
data.vaultsValues,
data.vaultsInOutDeltas
data.vaultsTotalFees,
data.vaultsDataTreeRoot,
data.vaultsDataTreeCid
)
);

Expand Down
10 changes: 8 additions & 2 deletions contracts/common/interfaces/ReportValues.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ struct ReportValues {
/// @notice array of combined values for each Lido vault
/// (sum of all the balances of Lido validators of the vault
/// plus the balance of the vault itself)
uint256[] vaultValues;
// uint256[] vaultValues;
/// @notice in-out deltas (deposits - withdrawals) of each Lido vault
int256[] inOutDeltas;
// int256[] inOutDeltas;
/// @notice overall vaults fees
uint256 vaultsTotalFees;
/// @notice Merkle Tree root of the vaults data.
bytes32 vaultsDataTreeRoot;
/// @notice CID of the published Merkle tree of the vault data.
string vaultsDataTreeCid;
}
12 changes: 7 additions & 5 deletions lib/oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ const DEFAULT_REPORT_FIELDS: OracleReport = {
sharesRequestedToBurn: 0n,
withdrawalFinalizationBatches: [],
isBunkerMode: false,
vaultsValues: [],
vaultsInOutDeltas: [],
vaultsTotalFees: 0n,
vaultsDataTreeRoot: ethers.ZeroHash,
vaultsDataTreeCid: "",
extraDataFormat: 0n,
extraDataHash: ethers.ZeroHash,
extraDataItemsCount: 0n,
Expand All @@ -65,8 +66,9 @@ export function getReportDataItems(r: OracleReport) {
r.sharesRequestedToBurn,
r.withdrawalFinalizationBatches,
r.isBunkerMode,
r.vaultsValues,
r.vaultsInOutDeltas,
r.vaultsTotalFees,
r.vaultsDataTreeRoot,
r.vaultsDataTreeCid,
r.extraDataFormat,
r.extraDataHash,
r.extraDataItemsCount,
Expand All @@ -76,7 +78,7 @@ export function getReportDataItems(r: OracleReport) {
export function calcReportDataHash(reportItems: ReportAsArray) {
const data = ethers.AbiCoder.defaultAbiCoder().encode(
[
"(uint256, uint256, uint256, uint256, uint256[], uint256[], uint256, uint256, uint256, uint256[], bool, uint256[], int256[], uint256, bytes32, uint256)",
"(uint256, uint256, uint256, uint256, uint256[], uint256[], uint256, uint256, uint256, uint256[], bool, uint256, bytes32, string, uint256, bytes32, uint256)",
],
[reportItems],
);
Expand Down
Loading
Loading