Skip to content

Fix vault freshness check and repeating reports #1215

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

Merged
merged 13 commits into from
Jul 23, 2025
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
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: 11 additions & 0 deletions contracts/0.8.25/vaults/LazyOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,16 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
VaultHub vaultHub = _vaultHub();
VaultHub.VaultRecord memory record = vaultHub.vaultRecord(_vault);

uint32 vaultReportTimestamp = record.report.timestamp;
uint32 latestReportTimestamp32 = uint32(_storage().vaultsDataTimestamp);

// 0. Check if the report is already fresh enough (already applied or vault is freshly connected)
// by checking if the vault's report ts is greater or equal to the latest report ts in LazyOracle
// but taking in account that vault's report is truncated to uint32 and can be overflown
if (Math256.ccwDistance32(vaultReportTimestamp, latestReportTimestamp32) < vaultHub.REPORT_FRESHNESS_DELTA()) {
revert VaultReportIsFreshEnough();
}

// 1. Calculate inOutDelta in the refSlot
int256 currentInOutDelta = record.inOutDelta.value;
inOutDeltaOnRefSlot = vaultHub.inOutDeltaAsOfLastRefSlot(_vault);
Expand Down Expand Up @@ -408,4 +418,5 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
error InvalidProof();
error UnderflowInTotalValueCalculation();
error TotalValueTooLarge();
error VaultReportIsFreshEnough();
}
28 changes: 18 additions & 10 deletions contracts/0.8.25/vaults/VaultHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -983,7 +983,7 @@ contract VaultHub is PausableUntilWithRoles {
report: Report({
totalValue: uint112(vaultBalance),
inOutDelta: int112(int256(vaultBalance)),
timestamp: uint32(_lazyOracle().latestReportTimestamp())
timestamp: uint32(block.timestamp)
}),
locked: uint128(CONNECT_DEPOSIT),
liabilityShares: 0,
Expand Down Expand Up @@ -1188,11 +1188,23 @@ contract VaultHub is PausableUntilWithRoles {

function _isReportFresh(VaultRecord storage _record) internal view returns (bool) {
uint256 latestReportTimestamp = _lazyOracle().latestReportTimestamp();
return
// check if AccountingOracle brought fresh report
uint32(latestReportTimestamp) == _record.report.timestamp &&
// if Accounting Oracle stop bringing the report, last report is fresh for 2 days
block.timestamp - latestReportTimestamp < REPORT_FRESHNESS_DELTA;
uint32 latestReportTimestamp32 = uint32(latestReportTimestamp);
uint32 vaultReportTimestamp = _record.report.timestamp;

// happy path. vault's report ts is the same as the latest AO report ts
if (latestReportTimestamp32 == vaultReportTimestamp) {
// check if AO has not stopped bringing the report
return block.timestamp - latestReportTimestamp < REPORT_FRESHNESS_DELTA;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// happy path. vault's report ts is the same as the latest AO report ts
if (latestReportTimestamp32 == vaultReportTimestamp) {
// check if AO has not stopped bringing the report
return block.timestamp - latestReportTimestamp < REPORT_FRESHNESS_DELTA;
}

🔪


// if false, we need to check if vault's report is slightly newer than the latest AO report
// but as we are using uint32, we need to find the counterclockwise distance
// between vaultReportTimestamp and latestReportTimestamp32 in modulo 2**32
// and check if it's less than REPORT_FRESHNESS_DELTA

uint256 ccwDistance = Math256.ccwDistance32(vaultReportTimestamp, latestReportTimestamp32);

return ccwDistance < REPORT_FRESHNESS_DELTA && block.timestamp - latestReportTimestamp < REPORT_FRESHNESS_DELTA;
}

function _isVaultHealthy(
Expand Down Expand Up @@ -1517,10 +1529,6 @@ contract VaultHub is PausableUntilWithRoles {
return LIDO.getSharesByPooledEth(_ether);
}

function _getPooledEthByShares(uint256 _ether) internal view returns (uint256) {
return LIDO.getPooledEthByShares(_ether);
}

function _getPooledEthBySharesRoundUp(uint256 _shares) internal view returns (uint256) {
return LIDO.getPooledEthBySharesRoundUp(_shares);
}
Expand Down
10 changes: 10 additions & 0 deletions contracts/common/lib/Math256.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,14 @@ library Math256 {
function absDiff(uint256 a, uint256 b) internal pure returns (uint256) {
return a > b ? a - b : b - a;
}

/// @dev Returns the distance between two 32-bit numbers in counterclockwise direction
/// @param a the first number that's suppose to be larger than b
/// @param b the second number that's suppose to be smaller than a
/// @return the distance between a and b in counterclockwise direction modulo 2**32 + 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// @return the distance between a and b in counterclockwise direction modulo 2**32 + 1
/// @return the distance between a and b in counterclockwise direction modulo 2**32

/// @dev this function is used to check if a is larger than b taking into account the overflow of uint32
function ccwDistance32(uint32 a, uint32 b) internal pure returns (uint256) {
uint256 modulo = uint256(~uint32(0)) + 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
uint256 modulo = uint256(~uint32(0)) + 1;
uint256 modulo = 1 << 32;

return (modulo + a - b) % modulo;
}
}
65 changes: 41 additions & 24 deletions lib/protocol/helpers/vaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,45 +198,62 @@ export async function setupLidoForVaults(ctx: ProtocolContext) {
await acl.connect(agentSigner).revokePermission(agentAddress, lido.address, role);
}

// address, totalValue, treasuryFees, liabilityShares, slashingReserve
export type VaultReportItem = [string, bigint, bigint, bigint, bigint];
export type VaultReportItem = {
vault: string;
totalValue: bigint;
accruedLidoFees: bigint;
liabilityShares: bigint;
slashingReserve: bigint;
};

export function createVaultsReportTree(vaults: VaultReportItem[]) {
return StandardMerkleTree.of(vaults, ["address", "uint256", "uint256", "uint256", "uint256"]);
// Utility type to extract all value types from an object type
export type ValuesOf<T> = T[keyof T];

// Auto-extract value types from VaultReportItem
export type VaultReportValues = ValuesOf<VaultReportItem>[];

export function createVaultsReportTree(vaultReports: VaultReportItem[]): StandardMerkleTree<VaultReportValues> {
return StandardMerkleTree.of(
vaultReports.map((vaultReport) => [
vaultReport.vault,
vaultReport.totalValue,
vaultReport.accruedLidoFees,
vaultReport.liabilityShares,
vaultReport.slashingReserve,
]),
["address", "uint256", "uint256", "uint256", "uint256"],
);
}

export async function reportVaultDataWithProof(
ctx: ProtocolContext,
stakingVault: StakingVault,
params: {
totalValue?: bigint;
accruedLidoFees?: bigint;
liabilityShares?: bigint;
} = {},
params: Partial<Omit<VaultReportItem, "vault">> = {},
updateReportData = true,
) {
const { vaultHub, locator, lazyOracle } = ctx.contracts;

const totalValueArg = params.totalValue ?? (await vaultHub.totalValue(stakingVault));
const liabilitySharesArg = params.liabilityShares ?? (await vaultHub.liabilityShares(stakingVault));
const vaultReport: VaultReportItem = {
vault: await stakingVault.getAddress(),
totalValue: params.totalValue ?? (await vaultHub.totalValue(stakingVault)),
accruedLidoFees: params.accruedLidoFees ?? 0n,
liabilityShares: params.liabilityShares ?? (await vaultHub.liabilityShares(stakingVault)),
slashingReserve: params.slashingReserve ?? 0n,
};

const vaultReport: VaultReportItem = [
await stakingVault.getAddress(),
totalValueArg,
params.accruedLidoFees ?? 0n,
liabilitySharesArg,
0n,
];
const reportTree = createVaultsReportTree([vaultReport]);

const accountingSigner = await impersonate(await locator.accountingOracle(), ether("100"));
await lazyOracle.connect(accountingSigner).updateReportData(await getCurrentBlockTimestamp(), reportTree.root, "");
if (updateReportData) {
const accountingSigner = await impersonate(await locator.accountingOracle(), ether("100"));
await lazyOracle.connect(accountingSigner).updateReportData(await getCurrentBlockTimestamp(), reportTree.root, "");
}

return await lazyOracle.updateVaultData(
await stakingVault.getAddress(),
totalValueArg,
params.accruedLidoFees ?? 0n,
liabilitySharesArg,
0n,
vaultReport.totalValue,
vaultReport.accruedLidoFees,
vaultReport.liabilityShares,
vaultReport.slashingReserve,
reportTree.getProof(0),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {RefSlotCache} from "contracts/0.8.25/vaults/lib/RefSlotCache.sol";
contract VaultHub__MockForLazyOracle {
using RefSlotCache for RefSlotCache.Int112WithRefSlotCache;

uint256 public constant REPORT_FRESHNESS_DELTA = 2 days;

address[] public mock__vaults;
mapping(address vault => VaultHub.VaultConnection connection) public mock__vaultConnections;
mapping(address vault => VaultHub.VaultRecord record) public mock__vaultRecords;
Expand Down Expand Up @@ -56,7 +58,7 @@ contract VaultHub__MockForLazyOracle {
return mock__vaultConnections[vault];
}

function maxLockableValue(address vault) external view returns (uint256) {
function maxLockableValue(address) external pure returns (uint256) {
return 1000000000000000000;
}

Expand Down
Loading
Loading