Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
4c5a074
fix(VaultHub): insufficient shortfall due to rounding
failingtwice Oct 6, 2025
80753cc
feat: update shortfall formula
failingtwice Oct 7, 2025
e60be77
feat: use precise shortfall calculation
failingtwice Oct 7, 2025
41dcc20
fix: remove old comments
failingtwice Oct 7, 2025
be85f78
feat: update bad debt case
failingtwice Oct 7, 2025
9fe1dd0
test: shortfall fuzzing wip
failingtwice Oct 7, 2025
bdcbeaf
feat(Dashboard): reset settled growth on disconnect
failingtwice Oct 13, 2025
66bec36
feat: reset settled growth on abandon
failingtwice Oct 14, 2025
bdeff50
feat: disburse fee circuit breaker
failingtwice Oct 14, 2025
0782a40
feat: revert disburse when paused
failingtwice Oct 14, 2025
d4ae689
feat: special function for high fee disburse
failingtwice Oct 14, 2025
3155612
feat: 1% threshold rationale
failingtwice Oct 14, 2025
8177d45
feat: rename event field
failingtwice Oct 14, 2025
038f7dd
Merge pull request #1509 from lidofinance/confirmation-event
failingtwice Oct 14, 2025
0f5cfdf
feat: disburse abnormally high fee as admin
failingtwice Oct 15, 2025
ec3b2fa
feat: rename high fee threshold
failingtwice Oct 15, 2025
1a2f158
feat: pass settled growth to connect to avoid foot violence
failingtwice Oct 15, 2025
e35aa1f
feat: use a simpler equation with slight overcompensation with ceilDiv
failingtwice Oct 15, 2025
f55e2ba
Merge branch 'audit-6' of https://github.com/lidofinance/core into sh…
failingtwice Oct 15, 2025
21814a1
test: delete wip fuzz test
failingtwice Oct 15, 2025
72b72ec
feat: hammer down rounding with 10 wei overcompensation
failingtwice Oct 15, 2025
54af2bc
fix(PDG): prevent frontrun of the batch of activations
folkyatina Oct 15, 2025
b41b419
fix(PDG): check that array lengths match
folkyatina Oct 15, 2025
2e59e77
chore: fix a natspec
folkyatina Oct 15, 2025
0376681
fix(OperatorGrid): more parameter validation for admin methods
folkyatina Oct 15, 2025
093e651
chore: fixed some comments and on event
folkyatina Oct 15, 2025
515f34e
chore: fix comment
folkyatina Oct 15, 2025
4492527
test: fix tests
folkyatina Oct 15, 2025
39c4703
chore: fix a comment
folkyatina Oct 15, 2025
b9d66cb
docs: better natspec for Dashboard
folkyatina Oct 16, 2025
ca97347
chore: fix a warning
folkyatina Oct 16, 2025
4204ee6
chore: fix some comments
folkyatina Oct 16, 2025
ffee488
fix(Dashboard): add return bool for transferVaultOwnership
folkyatina Oct 16, 2025
acba5fe
chore: more natspec improvements
folkyatina Oct 16, 2025
9e795b0
fix(OperatorGrid): add confirm expiry setter
folkyatina Oct 16, 2025
90b43f0
Merge pull request #1511 from lidofinance/fix/pdg-small-fixes
folkyatina Oct 16, 2025
f643fe8
Merge branch 'audit-6' of https://github.com/lidofinance/core into fi…
failingtwice Oct 16, 2025
96fb51a
test: fix hardcoded values
failingtwice Oct 16, 2025
7521df1
Merge pull request #1506 from lidofinance/fix-no-fee
folkyatina Oct 17, 2025
1f44239
Merge pull request #1497 from lidofinance/shortfall-rounding-fix
failingtwice Oct 17, 2025
4b2ca37
feat: add vault factory chaining for upgrade
folkyatina Oct 17, 2025
f6a4d14
fix: fix upgrade deploy for the new factory
folkyatina Oct 17, 2025
ad8e59d
test: fix a flaky test
folkyatina Oct 17, 2025
1afcebc
Merge remote-tracking branch 'origin/audit-6' into feat/upgreadable-v…
folkyatina Oct 17, 2025
0436884
test: add some tests for the factory
folkyatina Oct 19, 2025
5e71d92
fix: fixes after the review
folkyatina Oct 19, 2025
4c9ce94
chore(LazyOracle): add natspec and rearrange methods a bit
folkyatina Oct 19, 2025
176d889
Merge pull request #1513 from lidofinance/feat/upgreadable-vault-factory
folkyatina Oct 19, 2025
8f9cc04
fix(TW): hardened assembly
folkyatina Oct 19, 2025
25c2be3
Merge pull request #1515 from lidofinance/fix/prosecco-review-comments
folkyatina Oct 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions contracts/0.8.25/utils/Confirmations.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ abstract contract Confirmations {
* @notice Tracks confirmations
* @custom:storage-location erc7201:Lido.Utils.Confirmations
* @dev We cannot set confirmExpiry to 0 because this means that all confirmations have to be in the same block,
* which can never be guaranteed. And, more importantly, if the `_setConfirmExpiry` is restricted by
* the `onlyConfirmed` modifier, the confirmation expiry will be tricky to change.
* This is why confirmExpiry is private, set to a default value of 1 hour and cannot be set to 0.
* which can never be guaranteed. And, more importantly, if the `_setConfirmExpiry` requires confirmation,
* the confirmation expiry will be tricky to change.
* This is why confirmExpiry is private, set to a default value of 1 days and cannot be set to 0.
*
* Storage layout:
* - callData: msg.data of the call (selector + arguments)
Expand Down Expand Up @@ -197,6 +197,7 @@ abstract contract Confirmations {

/**
* @dev Emitted when the confirmation expiry is set.
* @param sender msg.sender of the call
* @param oldConfirmExpiry The old confirmation expiry.
* @param newConfirmExpiry The new confirmation expiry.
*/
Expand All @@ -205,12 +206,12 @@ abstract contract Confirmations {
/**
* @dev Emitted when a role member confirms.
* @param member The address of the confirming member.
* @param role The role of the confirming member.
* @param roleOrAddress The role or address of the confirming member.
* @param confirmTimestamp The timestamp of the confirmation.
* @param expiryTimestamp The timestamp when this confirmation expires.
* @param data The msg.data of the confirmation (selector + arguments).
*/
event RoleMemberConfirmed(address indexed member, bytes32 indexed role, uint256 confirmTimestamp, uint256 expiryTimestamp, bytes data);
event RoleMemberConfirmed(address indexed member, bytes32 indexed roleOrAddress, uint256 confirmTimestamp, uint256 expiryTimestamp, bytes data);

/**
* @dev Thrown when attempting to set confirmation expiry out of bounds.
Expand Down
109 changes: 67 additions & 42 deletions contracts/0.8.25/vaults/LazyOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,12 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
/// @param _quarantinePeriod the quarantine period, seconds
/// @param _maxRewardRatioBP the max reward ratio, basis points
/// @param _maxLidoFeeRatePerSecond the max Lido fee rate per second
function initialize(address _admin, uint256 _quarantinePeriod, uint256 _maxRewardRatioBP, uint256 _maxLidoFeeRatePerSecond) external initializer {
function initialize(
address _admin,
uint256 _quarantinePeriod,
uint256 _maxRewardRatioBP,
uint256 _maxLidoFeeRatePerSecond
) external initializer {
if (_admin == address(0)) revert AdminCannotBeZero();
_grantRole(DEFAULT_ADMIN_ROLE, _admin);

Expand All @@ -161,7 +166,12 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
/// @return refSlot of the report
/// @return treeRoot merkle root of the report
/// @return reportCid IPFS CID for the report JSON file
function latestReportData() external view returns (uint256 timestamp, uint256 refSlot, bytes32 treeRoot, string memory reportCid) {
function latestReportData() external view returns (
uint256 timestamp,
uint256 refSlot,
bytes32 treeRoot,
string memory reportCid
) {
Storage storage $ = _storage();
return ($.vaultsDataTimestamp, $.vaultsDataRefSlot, $.vaultsDataTreeRoot, $.vaultsDataReportCid);
}
Expand Down Expand Up @@ -238,28 +248,6 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
return _vaultInfo(_vault, _vaultHub());
}

function _vaultInfo(address _vault, VaultHub _vh) internal view returns (VaultInfo memory) {
IStakingVault vault = IStakingVault(_vault);
VaultHub.VaultConnection memory connection = _vh.vaultConnection(_vault);
VaultHub.VaultRecord memory record = _vh.vaultRecord(_vault);
return VaultInfo(
_vault,
vault.availableBalance() + vault.stagedBalance(),
record.inOutDelta.currentValue(),
vault.withdrawalCredentials(),
record.liabilityShares,
record.maxLiabilityShares,
_mintableStETH(_vault),
connection.shareLimit,
connection.reserveRatioBP,
connection.forcedRebalanceThresholdBP,
connection.infraFeeBP,
connection.liquidityFeeBP,
connection.reservationFeeBP,
_vh.isPendingDisconnect(_vault)
);
}

/**
* @notice batch method to mass check the validator stages in PredepositGuarantee contract
* @param _pubkeys the array of validator's pubkeys to check
Expand Down Expand Up @@ -305,14 +293,20 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
$.vaultsDataTreeRoot = _vaultsDataTreeRoot;
$.vaultsDataReportCid = _vaultsDataReportCid;

emit VaultsReportDataUpdated(_vaultsDataTimestamp, _vaultsDataRefSlot, _vaultsDataTreeRoot, _vaultsDataReportCid);
emit VaultsReportDataUpdated(
_vaultsDataTimestamp,
_vaultsDataRefSlot,
_vaultsDataTreeRoot,
_vaultsDataReportCid
);
}

/// @notice Permissionless update of the vault data
/// @param _vault the address of the vault
/// @param _totalValue the total value of the vault
/// @param _cumulativeLidoFees the cumulative Lido fees accrued on the vault (nominated in ether)
/// @param _liabilityShares the liabilityShares of the vault
/// @param _liabilityShares the liabilityShares value of the vault (on the vaultsDataRefSlot)
/// @param _maxLiabilityShares the maxLiabilityShares value of the vault (on the vaultsDataRefSlot)
/// @param _proof the proof of the reported data
function updateVaultData(
address _vault,
Expand Down Expand Up @@ -374,37 +368,63 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
delete quarantines[_vault];
}

function _vaultInfo(address _vault, VaultHub _vh) internal view returns (VaultInfo memory) {
IStakingVault vault = IStakingVault(_vault);
VaultHub.VaultConnection memory connection = _vh.vaultConnection(_vault);
VaultHub.VaultRecord memory record = _vh.vaultRecord(_vault);
return VaultInfo(
_vault,
vault.availableBalance() + vault.stagedBalance(),
record.inOutDelta.currentValue(),
vault.withdrawalCredentials(),
record.liabilityShares,
record.maxLiabilityShares,
_mintableStETH(_vault, _vh),
connection.shareLimit,
connection.reserveRatioBP,
connection.forcedRebalanceThresholdBP,
connection.infraFeeBP,
connection.liquidityFeeBP,
connection.reservationFeeBP,
_vh.isPendingDisconnect(_vault)
);
}

/// @notice handle sanity checks for the vault lazy report data
/// @param _vault the address of the vault
/// @param _totalValue the total value of the vault in refSlot
/// @param _reportRefSlot the refSlot of the report
/// @param _reportTimestamp the timestamp of the report
/// @param _cumulativeLidoFees the cumulative Lido fees accrued on the vault (nominated in ether)
/// @param _liabilityShares the liabilityShares value of the vault (on the _reportRefSlot)
/// @param _maxLiabilityShares the maxLiabilityShares value of the vault (on the _reportRefSlot)
/// @return totalValueWithoutQuarantine the smoothed total value of the vault after sanity checks
/// @return inOutDeltaOnRefSlot the inOutDelta in the refSlot
function _handleSanityChecks(
address _vault,
uint256 _totalValue,
uint48 _reportRefSlot,
uint256 _reportRefSlot,
uint256 _reportTimestamp,
uint256 _cumulativeLidoFees,
uint256 _liabilityShares,
uint256 _maxLiabilityShares
) internal returns (uint256 totalValueWithoutQuarantine, int256 inOutDeltaOnRefSlot) {
VaultHub vaultHub = _vaultHub();
VaultHub.VaultRecord memory record = vaultHub.vaultRecord(_vault);
uint48 previousReportTs = record.report.timestamp;

// 0. Check if the report is already fresh enough
if (uint48(_reportTimestamp) <= record.report.timestamp) {
if (uint48(_reportTimestamp) <= previousReportTs) {
revert VaultReportIsFreshEnough();
}

// 1. Calculate inOutDelta in the refSlot
int256 currentInOutDelta = record.inOutDelta.currentValue();
inOutDeltaOnRefSlot = record.inOutDelta.getValueForRefSlot(_reportRefSlot);
inOutDeltaOnRefSlot = record.inOutDelta.getValueForRefSlot(uint48(_reportRefSlot));

// 2. Sanity check for total value increase
totalValueWithoutQuarantine = _processTotalValue(_vault, _totalValue, inOutDeltaOnRefSlot, record);
totalValueWithoutQuarantine = _processTotalValue(
_vault, _totalValue, inOutDeltaOnRefSlot, record, _reportTimestamp);

// 3. Sanity check for dynamic total value underflow
if (int256(totalValueWithoutQuarantine) + currentInOutDelta - inOutDeltaOnRefSlot < 0) {
Expand All @@ -417,7 +437,7 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
revert CumulativeLidoFeesTooLow(_cumulativeLidoFees, previousCumulativeLidoFees);
}

uint256 maxLidoFees = (_reportTimestamp - record.report.timestamp) * uint256(_storage().maxLidoFeeRatePerSecond);
uint256 maxLidoFees = (_reportTimestamp - previousReportTs) * uint256(_storage().maxLidoFeeRatePerSecond);
if (_cumulativeLidoFees - previousCumulativeLidoFees > maxLidoFees) {
revert CumulativeLidoFeesTooLarge(_cumulativeLidoFees - previousCumulativeLidoFees, maxLidoFees);
}
Expand Down Expand Up @@ -468,7 +488,8 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
address _vault,
uint256 _reportedTotalValue,
int256 _inOutDeltaOnRefSlot,
VaultHub.VaultRecord memory record
VaultHub.VaultRecord memory record,
uint256 _reportTimestamp
) internal returns (uint256 totalValueWithoutQuarantine) {
if (_reportedTotalValue > MAX_SANE_TOTAL_VALUE) {
revert TotalValueTooLarge();
Expand All @@ -493,7 +514,7 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
uint256 quarantineThreshold =
onchainTotalValueOnRefSlot * (TOTAL_BASIS_POINTS + $.maxRewardRatioBP) / TOTAL_BASIS_POINTS;
// 3. Determine current quarantine state
QuarantineState currentState = _determineQuarantineState(quarantine, quarantinedValue, $);
QuarantineState currentState = _determineQuarantineState(quarantine, quarantinedValue, _reportTimestamp);


// Execute logic based on current state and conditions ----------------
Expand All @@ -505,7 +526,12 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
return _reportedTotalValue;
} else {
// Transition: NO_QUARANTINE → QUARANTINE_ACTIVE (start new quarantine)
_startNewQuarantine(_vault, quarantine, _reportedTotalValue - onchainTotalValueOnRefSlot, $.vaultsDataTimestamp);
_startNewQuarantine(
_vault,
quarantine,
_reportedTotalValue - onchainTotalValueOnRefSlot,
_reportTimestamp
);
return onchainTotalValueOnRefSlot;
}
} else if (currentState == QuarantineState.QUARANTINE_ACTIVE) {
Expand Down Expand Up @@ -533,7 +559,7 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
} else {
// Transition: QUARANTINE_EXPIRED → QUARANTINE_ACTIVE (release old, start new)
emit QuarantineReleased(_vault, quarantinedValue);
_startNewQuarantine(_vault, quarantine, totalValueIncrease - quarantinedValue, $.vaultsDataTimestamp);
_startNewQuarantine(_vault, quarantine, totalValueIncrease - quarantinedValue, _reportTimestamp);
return onchainTotalValueOnRefSlot + quarantinedValue;
}
}
Expand All @@ -542,24 +568,24 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
function _determineQuarantineState(
Quarantine storage _quarantine,
uint256 _quarantinedValue,
Storage storage $
uint256 _vaultsDataTimestamp
) internal view returns (QuarantineState) {
if (_quarantinedValue == 0) {
return QuarantineState.NO_QUARANTINE;
}

bool isQuarantineExpired = ($.vaultsDataTimestamp - _quarantine.startTimestamp) >= $.quarantinePeriod;
bool isQuarantineExpired = (_vaultsDataTimestamp - _quarantine.startTimestamp) >= _storage().quarantinePeriod;
return isQuarantineExpired ? QuarantineState.QUARANTINE_EXPIRED : QuarantineState.QUARANTINE_ACTIVE;
}

function _startNewQuarantine(
address _vault,
Quarantine storage _quarantine,
uint256 _amountToQuarantine,
uint64 _currentTimestamp
uint256 _currentTimestamp
) internal {
_quarantine.pendingTotalValueIncrease = uint128(_amountToQuarantine);
_quarantine.startTimestamp = _currentTimestamp;
_quarantine.startTimestamp = uint64(_currentTimestamp);
emit QuarantineActivated(_vault, _amountToQuarantine);
}

Expand All @@ -575,9 +601,8 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
emit SanityParamsUpdated(_quarantinePeriod, _maxRewardRatioBP, _maxLidoFeeRatePerSecond);
}

function _mintableStETH(address _vault) internal view returns (uint256) {
VaultHub vaultHub = _vaultHub();
uint256 mintableShares = vaultHub.totalMintingCapacityShares(_vault, 0 /* zero eth delta */);
function _mintableStETH(address _vault, VaultHub _vh) internal view returns (uint256) {
uint256 mintableShares = _vh.totalMintingCapacityShares(_vault, 0 /* zero eth delta */);
return _getPooledEthBySharesRoundUp(mintableShares);
}

Expand Down
Loading