From 80d3a72a20e0166739e7eff0aec8341ce523752f Mon Sep 17 00:00:00 2001 From: VP Date: Wed, 18 Jun 2025 19:06:48 +0200 Subject: [PATCH 01/11] feat: lazy oracle unit tests (batch and count) --- .../contracts/Lido__MockForLazyOracle.sol | 12 ++ .../OperatorGrid__MockForLazyOracle.sol | 12 ++ .../contracts/VaultHub__MockForLazyOracle.sol | 84 ++++++++++ .../contracts/Vault__MockForLazyOracle.sol | 12 ++ .../vaults/lazyOracle/lazyOracle.test.ts | 144 ++++++++++++++++++ .../vaults/vaulthub/vaulthub.hub.test.ts.todo | 70 --------- test/suite/constants.ts | 1 + 7 files changed, 265 insertions(+), 70 deletions(-) create mode 100644 test/0.8.25/vaults/lazyOracle/contracts/Lido__MockForLazyOracle.sol create mode 100644 test/0.8.25/vaults/lazyOracle/contracts/OperatorGrid__MockForLazyOracle.sol create mode 100644 test/0.8.25/vaults/lazyOracle/contracts/VaultHub__MockForLazyOracle.sol create mode 100644 test/0.8.25/vaults/lazyOracle/contracts/Vault__MockForLazyOracle.sol create mode 100644 test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts diff --git a/test/0.8.25/vaults/lazyOracle/contracts/Lido__MockForLazyOracle.sol b/test/0.8.25/vaults/lazyOracle/contracts/Lido__MockForLazyOracle.sol new file mode 100644 index 0000000000..eeec485542 --- /dev/null +++ b/test/0.8.25/vaults/lazyOracle/contracts/Lido__MockForLazyOracle.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity >=0.8.0; + +contract Lido__MockForLazyOracle { + constructor() {} + + function getPooledEthBySharesRoundUp(uint256 value) external view returns (uint256) { + return value; + } +} diff --git a/test/0.8.25/vaults/lazyOracle/contracts/OperatorGrid__MockForLazyOracle.sol b/test/0.8.25/vaults/lazyOracle/contracts/OperatorGrid__MockForLazyOracle.sol new file mode 100644 index 0000000000..57c817d3fd --- /dev/null +++ b/test/0.8.25/vaults/lazyOracle/contracts/OperatorGrid__MockForLazyOracle.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity >=0.8.0; + +contract OperatorGrid__MockForLazyOracle { + constructor() {} + + function effectiveShareLimit(address) external view returns (uint256) { + return 1000000000000000000; + } +} diff --git a/test/0.8.25/vaults/lazyOracle/contracts/VaultHub__MockForLazyOracle.sol b/test/0.8.25/vaults/lazyOracle/contracts/VaultHub__MockForLazyOracle.sol new file mode 100644 index 0000000000..9d85b8a22c --- /dev/null +++ b/test/0.8.25/vaults/lazyOracle/contracts/VaultHub__MockForLazyOracle.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity >=0.8.0; + +import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; +import {ILido} from "contracts/common/interfaces/ILido.sol"; +import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; +import {VaultHub} from "contracts/0.8.25/vaults/VaultHub.sol"; +import {RefSlotCache} from "contracts/0.8.25/vaults/lib/RefSlotCache.sol"; +import "hardhat/console.sol"; + +contract VaultHub__MockForLazyOracle { + using RefSlotCache for RefSlotCache.Int112WithRefSlotCache; + + address[] public mock__vaults; + mapping(address vault => VaultHub.VaultConnection connection) public mock__vaultConnections; + mapping(address vault => VaultHub.VaultRecord record) public mock__vaultRecords; + + constructor() { + mock__vaults.push(address(0)); + } + + function mock__addVault(address vault) external { + mock__vaults.push(vault); + } + + function mock__setVaultConnection(address vault, VaultHub.VaultConnection memory connection) external { + mock__vaultConnections[vault] = connection; + } + + function mock__setVaultRecord(address vault, VaultHub.VaultRecord memory record) external { + mock__vaultRecords[vault] = record; + } + + function vaultsCount() external view returns (uint256) { + return mock__vaults.length - 1; + } + + function vaultByIndex(uint256 index) external view returns (address) { + return mock__vaults[index]; + } + + function inOutDeltaAsOfLastRefSlot(address vault) external view returns (int256) { + return mock__vaultRecords[vault].inOutDelta.value; + } + + function vaultConnection(address vault) external view returns (VaultHub.VaultConnection memory) { + return mock__vaultConnections[vault]; + // console.log("vaultConnection", vault); + // VaultHub.VaultConnection memory connection = VaultHub.VaultConnection({ + // owner: address(1), + // shareLimit: 1000000000000000000, + // vaultIndex: 1, + // pendingDisconnect: false, + // reserveRatioBP: 10000, + // forcedRebalanceThresholdBP: 10000, + // infraFeeBP: 10000, + // liquidityFeeBP: 10000, + // reservationFeeBP: 10000, + // isBeaconDepositsManuallyPaused: false + // }); + // return connection; + } + + function maxLockableValue(address vault) external view returns (uint256) { + return 1000000000000000000; + } + + function vaultRecord(address vault) external view returns (VaultHub.VaultRecord memory) { + return mock__vaultRecords[vault]; + } + + // function vaultRecord(address vault) external view returns (VaultHub.VaultRecord memory) { + // VaultHub.VaultRecord memory record = VaultHub.VaultRecord({ + // report: VaultHub.Report({totalValue: uint112(1), inOutDelta: int112(int256(2)), timestamp: uint32(3)}), + // locked: uint128(4), + // liabilityShares: 5, + // inOutDelta: RefSlotCache.Int112WithRefSlotCache({value: int112(int256(6)), valueOnRefSlot: 7, refSlot: 8}) + // }); + + // return record; + // } +} diff --git a/test/0.8.25/vaults/lazyOracle/contracts/Vault__MockForLazyOracle.sol b/test/0.8.25/vaults/lazyOracle/contracts/Vault__MockForLazyOracle.sol new file mode 100644 index 0000000000..38377968dd --- /dev/null +++ b/test/0.8.25/vaults/lazyOracle/contracts/Vault__MockForLazyOracle.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity >=0.8.0; + +contract Vault__MockForLazyOracle { + constructor() {} + + function withdrawalCredentials() external view returns (bytes32) { + return bytes32(0); + } +} diff --git a/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts b/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts new file mode 100644 index 0000000000..69f63df3f2 --- /dev/null +++ b/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts @@ -0,0 +1,144 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { + LazyOracle, + Lido__MockForLazyOracle, + LidoLocator, + OperatorGrid__MockForLazyOracle, + VaultHub__MockForLazyOracle, +} from "typechain-types"; + +import { randomAddress } from "lib"; + +import { deployLidoLocator } from "test/deploy"; +import { Snapshot, ZERO_BYTES32 } from "test/suite"; + +describe("LazyOracle.sol", () => { + let locator: LidoLocator; + let vaultHub: VaultHub__MockForLazyOracle; + let operatorGrid: OperatorGrid__MockForLazyOracle; + let lido: Lido__MockForLazyOracle; + let lazyOracle: LazyOracle; + + let originalState: string; + + before(async () => { + vaultHub = await ethers.deployContract("VaultHub__MockForLazyOracle", []); + operatorGrid = await ethers.deployContract("OperatorGrid__MockForLazyOracle", []); + lido = await ethers.deployContract("Lido__MockForLazyOracle", []); + + locator = await deployLidoLocator({ + vaultHub: vaultHub, + operatorGrid: operatorGrid, + lido: lido, + }); + + lazyOracle = await ethers.deployContract("LazyOracle", [locator]); + }); + + beforeEach(async () => (originalState = await Snapshot.take())); + + afterEach(async () => await Snapshot.restore(originalState)); + + context("batchVaultsInfo", () => { + it("returns the vault count", async () => { + await vaultHub.mock__addVault(randomAddress()); + expect(await lazyOracle.vaultsCount()).to.equal(1n); + + await vaultHub.mock__addVault(randomAddress()); + expect(await lazyOracle.vaultsCount()).to.equal(2n); + }); + + async function createVault(): Promise { + const vault = await ethers.deployContract("Vault__MockForLazyOracle", []); + return await vault.getAddress(); + } + + it("returns the vault info", async () => { + const vault1 = await createVault(); + await vaultHub.mock__addVault(vault1); + + await vaultHub.mock__setVaultConnection(vault1, { + owner: randomAddress(), + shareLimit: 1000n, + vaultIndex: 1, + pendingDisconnect: false, + reserveRatioBP: 10000, + forcedRebalanceThresholdBP: 10000, + infraFeeBP: 10000, + liquidityFeeBP: 10000, + reservationFeeBP: 10000, + isBeaconDepositsManuallyPaused: false, + }); + + await vaultHub.mock__setVaultRecord(vault1, { + report: { + totalValue: 1000000000000000000n, + inOutDelta: 2000000000000000000n, + timestamp: 1000000000n, + }, + locked: 3n, + liabilityShares: 4n, + inOutDelta: { + value: 5n, + valueOnRefSlot: 6n, + refSlot: 7n, + }, + }); + const vaults = await lazyOracle.batchVaultsInfo(0n, 2n); + + expect(vaults.length).to.equal(1); + + const vaultInfo = vaults[0]; + expect(vaultInfo.vault).to.equal(vault1); + expect(vaultInfo.vaultIndex).to.equal(1n); + expect(vaultInfo.balance).to.equal(0n); + expect(vaultInfo.inOutDelta).to.equal(5n); + expect(vaultInfo.withdrawalCredentials).to.equal(ZERO_BYTES32); + expect(vaultInfo.liabilityShares).to.equal(4n); + expect(vaultInfo.mintableStETH).to.equal(0n); + expect(vaultInfo.shareLimit).to.equal(1000n); + expect(vaultInfo.reserveRatioBP).to.equal(10000); + expect(vaultInfo.forcedRebalanceThresholdBP).to.equal(10000); + expect(vaultInfo.infraFeeBP).to.equal(10000); + expect(vaultInfo.liquidityFeeBP).to.equal(10000); + expect(vaultInfo.reservationFeeBP).to.equal(10000); + expect(vaultInfo.pendingDisconnect).to.equal(false); + }); + + it("returns the vault info with pagination", async () => { + const vault1 = await createVault(); + await vaultHub.mock__addVault(vault1); + const vault2 = await createVault(); + await vaultHub.mock__addVault(vault2); + const vault3 = await createVault(); + await vaultHub.mock__addVault(vault3); + + const vaults1 = await lazyOracle.batchVaultsInfo(0n, 1n); + expect(vaults1.length).to.equal(1); + expect(vaults1[0].vault).to.equal(vault1); + + const vaults2 = await lazyOracle.batchVaultsInfo(1n, 1n); + expect(vaults2.length).to.equal(1); + expect(vaults2[0].vault).to.equal(vault2); + + const vaults3 = await lazyOracle.batchVaultsInfo(0n, 4n); + expect(vaults3.length).to.equal(3); + expect(vaults3[0].vault).to.equal(vault1); + expect(vaults3[1].vault).to.equal(vault2); + expect(vaults3[2].vault).to.equal(vault3); + + const vaults4 = await lazyOracle.batchVaultsInfo(1n, 3n); + expect(vaults4.length).to.equal(2); + expect(vaults4[0].vault).to.equal(vault2); + expect(vaults4[1].vault).to.equal(vault3); + + const vaults5 = await lazyOracle.batchVaultsInfo(0n, 0n); + expect(vaults5.length).to.equal(0); + + const vaults6 = await lazyOracle.batchVaultsInfo(3n, 1n); + expect(vaults6.length).to.equal(0); + }); + }); +}); diff --git a/test/0.8.25/vaults/vaulthub/vaulthub.hub.test.ts.todo b/test/0.8.25/vaults/vaulthub/vaulthub.hub.test.ts.todo index d5bbf493b6..dfa3bcc736 100644 --- a/test/0.8.25/vaults/vaulthub/vaulthub.hub.test.ts.todo +++ b/test/0.8.25/vaults/vaulthub/vaulthub.hub.test.ts.todo @@ -289,76 +289,6 @@ describe("VaultHub.sol:hub", () => { }); }); - context("batchVaultsInfo", () => { - it("returns the vault info", async () => { - const { vault: vault1 } = await createAndConnectVault(vaultFactory); - const { vault: vault2 } = await createAndConnectVault(vaultFactory); - const vaultAddress1 = await vault1.getAddress(); - const vaultAddress2 = await vault2.getAddress(); - const vaults = await vaultHub.batchVaultsInfo(0n, 2n); - - expect(vaults.length).to.equal(2); - - const vaultInfo = vaults[0]; - expect(vaultInfo.vault).to.equal(vaultAddress1); - expect(vaultInfo.balance).to.equal(CONNECT_DEPOSIT); - expect(vaultInfo.withdrawalCredentials).to.equal(ZERO_BYTES32); - expect(vaultInfo.shareLimit).to.equal(SHARE_LIMIT); - expect(vaultInfo.reserveRatioBP).to.equal(RESERVE_RATIO_BP); - expect(vaultInfo.forcedRebalanceThresholdBP).to.equal(FORCED_REBALANCE_THRESHOLD_BP); - expect(vaultInfo.infraFeeBP).to.equal(INFRA_FEE_BP); - expect(vaultInfo.liquidityFeeBP).to.equal(LIQUIDITY_FEE_BP); - expect(vaultInfo.reservationFeeBP).to.equal(RESERVATION_FEE_BP); - expect(vaultInfo.pendingDisconnect).to.equal(false); - - const vaultInfo2 = vaults[1]; - expect(vaultInfo2.vault).to.equal(vaultAddress2); - expect(vaultInfo2.balance).to.equal(CONNECT_DEPOSIT); - expect(vaultInfo2.withdrawalCredentials).to.equal(ZERO_BYTES32); - expect(vaultInfo2.shareLimit).to.equal(SHARE_LIMIT); - expect(vaultInfo2.reserveRatioBP).to.equal(RESERVE_RATIO_BP); - expect(vaultInfo2.forcedRebalanceThresholdBP).to.equal(FORCED_REBALANCE_THRESHOLD_BP); - expect(vaultInfo2.infraFeeBP).to.equal(INFRA_FEE_BP); - expect(vaultInfo2.liquidityFeeBP).to.equal(LIQUIDITY_FEE_BP); - expect(vaultInfo2.reservationFeeBP).to.equal(RESERVATION_FEE_BP); - expect(vaultInfo2.pendingDisconnect).to.equal(false); - }); - - it("returns the vault info with pagination", async () => { - const { vault: vault1 } = await createAndConnectVault(vaultFactory); - const { vault: vault2 } = await createAndConnectVault(vaultFactory); - const { vault: vault3 } = await createAndConnectVault(vaultFactory); - const vaultAddress1 = await vault1.getAddress(); - const vaultAddress2 = await vault2.getAddress(); - const vaultAddress3 = await vault3.getAddress(); - - const vaults1 = await vaultHub.batchVaultsInfo(0n, 1n); - expect(vaults1.length).to.equal(1); - expect(vaults1[0].vault).to.equal(vaultAddress1); - - const vaults2 = await vaultHub.batchVaultsInfo(1n, 1n); - expect(vaults2.length).to.equal(1); - expect(vaults2[0].vault).to.equal(vaultAddress2); - - const vaults3 = await vaultHub.batchVaultsInfo(0n, 4n); - expect(vaults3.length).to.equal(3); - expect(vaults3[0].vault).to.equal(vaultAddress1); - expect(vaults3[1].vault).to.equal(vaultAddress2); - expect(vaults3[2].vault).to.equal(vaultAddress3); - - const vaults4 = await vaultHub.batchVaultsInfo(1n, 3n); - expect(vaults4.length).to.equal(2); - expect(vaults4[0].vault).to.equal(vaultAddress2); - expect(vaults4[1].vault).to.equal(vaultAddress3); - - const vaults5 = await vaultHub.batchVaultsInfo(0n, 0n); - expect(vaults5.length).to.equal(0); - - const vaults6 = await vaultHub.batchVaultsInfo(3n, 1n); - expect(vaults6.length).to.equal(0); - }); - }); - context("isVaultHealthy", () => { it("reverts if vault is not connected", async () => { await expect(vaultHub.isVaultHealthy(randomAddress())).to.be.revertedWithCustomError( diff --git a/test/suite/constants.ts b/test/suite/constants.ts index 84450de583..a510063b3d 100644 --- a/test/suite/constants.ts +++ b/test/suite/constants.ts @@ -9,5 +9,6 @@ export const LIMITER_PRECISION_BASE = BigInt(10 ** 9); export const SHARE_RATE_PRECISION = BigInt(10 ** 27); export const ZERO_HASH = new Uint8Array(32).fill(0); +export const ZERO_BYTES32 = "0x" + Buffer.from(ZERO_HASH).toString("hex"); export const VAULTS_MAX_RELATIVE_SHARE_LIMIT_BP = 10_00n; From cad4340b0fdf36984d2f32b92aaedcae83e7b917 Mon Sep 17 00:00:00 2001 From: VP Date: Thu, 26 Jun 2025 17:39:13 +0200 Subject: [PATCH 02/11] feat: add some tests --- .../vaults/lazyOracle/lazyOracle.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts b/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts index 69f63df3f2..2f86f26b86 100644 --- a/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts +++ b/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts @@ -141,4 +141,28 @@ describe("LazyOracle.sol", () => { expect(vaults6.length).to.equal(0); }); }); + + context("getter functions", () => { + it("return latest report data", async () => { + const reportData = await lazyOracle.latestReportData(); + expect(reportData.timestamp).to.equal(0n); + expect(reportData.treeRoot).to.equal(ZERO_BYTES32); + expect(reportData.reportCid).to.equal(""); + }); + + it("return latest report timestamp", async () => { + const timestamp = await lazyOracle.latestReportTimestamp(); + expect(timestamp).to.equal(0n); + }); + + it("return quarantine period", async () => { + const quarantinePeriod = await lazyOracle.quarantinePeriod(); + expect(quarantinePeriod).to.equal(0n); + }); + + it("return max reward ratio", async () => { + const maxRewardRatio = await lazyOracle.maxRewardRatioBP(); + expect(maxRewardRatio).to.equal(0); + }); + }); }); From 11f2fcc84c3eda6c7dbfb16116dc8371a011ed36 Mon Sep 17 00:00:00 2001 From: VP Date: Thu, 26 Jun 2025 17:41:30 +0200 Subject: [PATCH 03/11] test: quarantine info getter --- test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts b/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts index 2f86f26b86..25f8854fe3 100644 --- a/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts +++ b/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts @@ -164,5 +164,12 @@ describe("LazyOracle.sol", () => { const maxRewardRatio = await lazyOracle.maxRewardRatioBP(); expect(maxRewardRatio).to.equal(0); }); + + it("return quarantine info", async () => { + const quarantineInfo = await lazyOracle.vaultQuarantine(randomAddress()); + expect(quarantineInfo.isActive).to.equal(false); + expect(quarantineInfo.pendingTotalValueIncrease).to.equal(0n); + expect(quarantineInfo.startTimestamp).to.equal(0n); + }); }); }); From bd2f214941950f65ae4fe0352ca8dfb69c94cdb2 Mon Sep 17 00:00:00 2001 From: VP Date: Thu, 26 Jun 2025 18:32:48 +0200 Subject: [PATCH 04/11] test: more cases --- .../vaults/lazyOracle/lazyOracle.test.ts | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts b/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts index 25f8854fe3..bfe228b328 100644 --- a/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts +++ b/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts @@ -1,6 +1,8 @@ import { expect } from "chai"; import { ethers } from "hardhat"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; + import { LazyOracle, Lido__MockForLazyOracle, @@ -15,6 +17,7 @@ import { deployLidoLocator } from "test/deploy"; import { Snapshot, ZERO_BYTES32 } from "test/suite"; describe("LazyOracle.sol", () => { + let deployer: SignerWithAddress; let locator: LidoLocator; let vaultHub: VaultHub__MockForLazyOracle; let operatorGrid: OperatorGrid__MockForLazyOracle; @@ -24,6 +27,7 @@ describe("LazyOracle.sol", () => { let originalState: string; before(async () => { + [deployer] = await ethers.getSigners(); vaultHub = await ethers.deployContract("VaultHub__MockForLazyOracle", []); operatorGrid = await ethers.deployContract("OperatorGrid__MockForLazyOracle", []); lido = await ethers.deployContract("Lido__MockForLazyOracle", []); @@ -33,8 +37,16 @@ describe("LazyOracle.sol", () => { operatorGrid: operatorGrid, lido: lido, }); + const lazyOracleImpl = await ethers.deployContract("LazyOracle", [locator]); + + const proxy = await ethers.deployContract( + "OssifiableProxy", + [lazyOracleImpl, deployer, new Uint8Array()], + deployer, + ); + lazyOracle = await ethers.getContractAt("LazyOracle", proxy); - lazyOracle = await ethers.deployContract("LazyOracle", [locator]); + await lazyOracle.initialize(deployer.address, 259200n, 350n); }); beforeEach(async () => (originalState = await Snapshot.take())); @@ -140,6 +152,13 @@ describe("LazyOracle.sol", () => { const vaults6 = await lazyOracle.batchVaultsInfo(3n, 1n); expect(vaults6.length).to.equal(0); }); + + it("returns the empty vault info for exceeding offset", async () => { + const vault = await createVault(); + await vaultHub.mock__addVault(vault); + const vaults = await lazyOracle.batchVaultsInfo(1n, 1n); + expect(vaults.length).to.equal(0); + }); }); context("getter functions", () => { @@ -157,12 +176,12 @@ describe("LazyOracle.sol", () => { it("return quarantine period", async () => { const quarantinePeriod = await lazyOracle.quarantinePeriod(); - expect(quarantinePeriod).to.equal(0n); + expect(quarantinePeriod).to.equal(259200n); }); it("return max reward ratio", async () => { const maxRewardRatio = await lazyOracle.maxRewardRatioBP(); - expect(maxRewardRatio).to.equal(0); + expect(maxRewardRatio).to.equal(350n); }); it("return quarantine info", async () => { @@ -172,4 +191,13 @@ describe("LazyOracle.sol", () => { expect(quarantineInfo.startTimestamp).to.equal(0n); }); }); + + context("sanity params", () => { + it("update quarantine period", async () => { + await lazyOracle.grantRole(await lazyOracle.UPDATE_SANITY_PARAMS_ROLE(), deployer.address); + await lazyOracle.updateSanityParams(250000n, 1000n); + expect(await lazyOracle.quarantinePeriod()).to.equal(250000n); + expect(await lazyOracle.maxRewardRatioBP()).to.equal(1000n); + }); + }); }); From 1c34944f2c44e1ec30c6745648e4d1493d5a9692 Mon Sep 17 00:00:00 2001 From: VP Date: Fri, 27 Jun 2025 12:34:31 +0200 Subject: [PATCH 05/11] test: proof related cases --- .../contracts/VaultHub__MockForLazyOracle.sol | 26 +++ .../vaults/lazyOracle/lazyOracle.test.ts | 144 +++++++++++- .../vaulthub/vaulthub.reporting.test.ts.todo | 208 ------------------ 3 files changed, 164 insertions(+), 214 deletions(-) diff --git a/test/0.8.25/vaults/lazyOracle/contracts/VaultHub__MockForLazyOracle.sol b/test/0.8.25/vaults/lazyOracle/contracts/VaultHub__MockForLazyOracle.sol index 9d85b8a22c..3f239895ee 100644 --- a/test/0.8.25/vaults/lazyOracle/contracts/VaultHub__MockForLazyOracle.sol +++ b/test/0.8.25/vaults/lazyOracle/contracts/VaultHub__MockForLazyOracle.sol @@ -17,6 +17,14 @@ contract VaultHub__MockForLazyOracle { mapping(address vault => VaultHub.VaultConnection connection) public mock__vaultConnections; mapping(address vault => VaultHub.VaultRecord record) public mock__vaultRecords; + address public mock__lastReportedVault; + uint256 public mock__lastReported_timestamp; + uint256 public mock__lastReported_totalValue; + int256 public mock__lastReported_inOutDelta; + uint256 public mock__lastReported_cumulativeLidoFees; + uint256 public mock__lastReported_liabilityShares; + uint256 public mock__lastReported_slashingReserve; + constructor() { mock__vaults.push(address(0)); } @@ -71,6 +79,24 @@ contract VaultHub__MockForLazyOracle { return mock__vaultRecords[vault]; } + function applyVaultReport( + address _vault, + uint256 _reportTimestamp, + uint256 _reportTotalValue, + int256 _reportInOutDelta, + uint256 _reportCumulativeLidoFees, + uint256 _reportLiabilityShares, + uint256 _reportSlashingReserve + ) external { + mock__lastReportedVault = _vault; + mock__lastReported_timestamp = _reportTimestamp; + mock__lastReported_totalValue = _reportTotalValue; + mock__lastReported_inOutDelta = _reportInOutDelta; + mock__lastReported_cumulativeLidoFees = _reportCumulativeLidoFees; + mock__lastReported_liabilityShares = _reportLiabilityShares; + mock__lastReported_slashingReserve = _reportSlashingReserve; + } + // function vaultRecord(address vault) external view returns (VaultHub.VaultRecord memory) { // VaultHub.VaultRecord memory record = VaultHub.VaultRecord({ // report: VaultHub.Report({totalValue: uint112(1), inOutDelta: int112(int256(2)), timestamp: uint32(3)}), diff --git a/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts b/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts index bfe228b328..22c8fae6c7 100644 --- a/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts +++ b/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts @@ -11,7 +11,8 @@ import { VaultHub__MockForLazyOracle, } from "typechain-types"; -import { randomAddress } from "lib"; +import { ether, getCurrentBlockTimestamp, impersonate, randomAddress } from "lib"; +import { createVaultsReportTree, VaultReportItem } from "lib/protocol/helpers/vaults"; import { deployLidoLocator } from "test/deploy"; import { Snapshot, ZERO_BYTES32 } from "test/suite"; @@ -53,6 +54,11 @@ describe("LazyOracle.sol", () => { afterEach(async () => await Snapshot.restore(originalState)); + async function createVault(): Promise { + const vault = await ethers.deployContract("Vault__MockForLazyOracle", []); + return await vault.getAddress(); + } + context("batchVaultsInfo", () => { it("returns the vault count", async () => { await vaultHub.mock__addVault(randomAddress()); @@ -62,11 +68,6 @@ describe("LazyOracle.sol", () => { expect(await lazyOracle.vaultsCount()).to.equal(2n); }); - async function createVault(): Promise { - const vault = await ethers.deployContract("Vault__MockForLazyOracle", []); - return await vault.getAddress(); - } - it("returns the vault info", async () => { const vault1 = await createVault(); await vaultHub.mock__addVault(vault1); @@ -200,4 +201,135 @@ describe("LazyOracle.sol", () => { expect(await lazyOracle.maxRewardRatioBP()).to.equal(1000n); }); }); + + context("updateReportData", () => { + it("reverts report update data call from non-Accounting contract", async () => { + await expect(lazyOracle.updateReportData(0, ethers.ZeroHash, "")).to.be.revertedWithCustomError( + lazyOracle, + "NotAuthorized", + ); + }); + + it("accepts report data from Accounting contract", async () => { + const accountingAddress = await impersonate(await locator.accountingOracle(), ether("1")); + await expect(lazyOracle.connect(accountingAddress).updateReportData(0, ethers.ZeroHash, "")).to.not.reverted; + }); + + it("returns lastest report data correctly", async () => { + const accountingAddress = await impersonate(await locator.accountingOracle(), ether("1")); + const reportTimestamp = await getCurrentBlockTimestamp(); + await expect(lazyOracle.connect(accountingAddress).updateReportData(reportTimestamp, ethers.ZeroHash, "test_cid")) + .to.not.reverted; + + const lastReportData = await lazyOracle.latestReportData(); + expect(lastReportData.timestamp).to.equal(reportTimestamp); + expect(lastReportData.treeRoot).to.equal(ethers.ZeroHash); + expect(lastReportData.reportCid).to.equal("test_cid"); + }); + }); + + context("updateVaultData", () => { + const TEST_PROOF = ["0xd129d34738564e7a38fa20b209e965b5fa6036268546a0d58bbe5806b2469c2e"]; + const TEST_ROOT = "0x4d7731e031705b521abbc5848458dc64ab85c2c3262be16f57bf5ea82a82178a"; + + it("reverts on invalid proof", async () => { + const accountingAddress = await impersonate(await locator.accountingOracle(), ether("1")); + await expect(lazyOracle.connect(accountingAddress).updateReportData(0, ethers.ZeroHash, "")).to.not.reverted; + await vaultHub.mock__addVault("0xEcB7C8D2BaF7270F90066B4cd8286e2CA1154F60"); + + await expect( + lazyOracle.updateVaultData( + "0xEcB7C8D2BaF7270F90066B4cd8286e2CA1154F60", + 99170000769726969624n, + 10000000n, + 0n, + 0n, + TEST_PROOF, + ), + ).to.be.revertedWithCustomError(lazyOracle, "InvalidProof"); + }); + + it("accepts generated proof", async () => { + const vaultsReport: VaultReportItem[] = [ + ["0xE312f1ed35c4dBd010A332118baAD69d45A0E302", 33000000000000000000n, 0n, 0n, 0n], + ["0x652b70E0Ae932896035d553fEaA02f37Ab34f7DC", 3100000000000000000n, 0n, 0n, 510300000000000000n], + [ + "0x20d34FD0482E3BdC944952D0277A306860be0014", + 2580000000000012501n, + 580000000000012501n, + 0n, + 1449900000000010001n, + ], + ["0x60B614c42d92d6c2E68AF7f4b741867648aBf9A4", 1000000000000000000n, 1000000000000000000n, 0n, 0n], + [ + "0xE6BdAFAac1d91605903D203539faEd173793b7D7", + 1030000000000000000n, + 1030000000000000000n, + 0n, + 400000000000000000n, + ], + ["0x34ebc5780F36d3fD6F1e7b43CF8DB4a80dCE42De", 1000000000000000000n, 1000000000000000000n, 0n, 0n], + [ + "0x3018F0cC632Aa3805a8a676613c62F55Ae4018C7", + 2000000000000000000n, + 2000000000000000000n, + 0n, + 100000000000000000n, + ], + [ + "0x40998324129B774fFc7cDA103A2d2cFd23EcB56e", + 1000000000000000000n, + 1000000000000000000n, + 0n, + 300000000000000000n, + ], + ["0x4ae099982712e2164fBb973554991111A418ab2B", 1000000000000000000n, 1000000000000000000n, 0n, 0n], + ["0x59536AC6211C1deEf1EE37CDC11242A0bDc7db83", 1000000000000000000n, 1000000000000000000n, 0n, 0n], + ]; + + const tree = createVaultsReportTree(vaultsReport); + const accountingAddress = await impersonate(await locator.accountingOracle(), ether("100")); + const timestamp = await getCurrentBlockTimestamp(); + await lazyOracle.connect(accountingAddress).updateReportData(timestamp, tree.root, ""); + + for (let index = 0; index < vaultsReport.length; index++) { + const vaultReport = vaultsReport[index]; + + await lazyOracle.updateVaultData( + vaultReport[0], + vaultReport[1], + vaultReport[2], + vaultReport[3], + vaultReport[4], + tree.getProof(index), + ); + expect(await vaultHub.mock__lastReportedVault()).to.equal(vaultReport[0]); + expect(await vaultHub.mock__lastReported_timestamp()).to.equal(timestamp); + expect(await vaultHub.mock__lastReported_cumulativeLidoFees()).to.equal(vaultReport[2]); + expect(await vaultHub.mock__lastReported_liabilityShares()).to.equal(vaultReport[3]); + expect(await vaultHub.mock__lastReported_slashingReserve()).to.equal(vaultReport[4]); + } + + expect(tree.root).to.equal("0x14a968ec37647b2086e05d9c19762eb528736cc3618fb99101ec4adb27f63c26"); + const proof = tree.getProof(1); + expect(proof).to.deep.equal([ + "0x05d2e4cb42d7a2fc8347e6f6157e039b62f6380d2fcf545520db8029e6b541cc", + "0x3027050bbe118641c9dab8adb053cc2071b29f78f9edfbc678c4f525f2fbe1de", + "0x1b0d29f502033ef4f86abb47a8efa9f0d26dd92de90cd4e721282d60d85d0e9b", + "0x033aa9c0ad17d6c5e220abc83c91fb35f89ad0bc3fff9ca80b0160d813a7394b", + ]); + }); + + it("calculates merkle tree the same way as off-chain implementation", async () => { + const values: VaultReportItem[] = [ + ["0xc1F9c4a809cbc6Cb2cA60bCa09cE9A55bD5337Db", 2500000000000000000n, 2500000000000000000n, 0n, 1n], + ["0xEcB7C8D2BaF7270F90066B4cd8286e2CA1154F60", 99170000769726969624n, 33000000000000000000n, 0n, 0n], + ]; + + const tree = createVaultsReportTree(values); + expect(tree.root).to.equal(TEST_ROOT); + const proof = tree.getProof(1); + expect(proof).to.deep.equal(TEST_PROOF); + }); + }); }); diff --git a/test/0.8.25/vaults/vaulthub/vaulthub.reporting.test.ts.todo b/test/0.8.25/vaults/vaulthub/vaulthub.reporting.test.ts.todo index b5024415c2..f78b40f7dd 100644 --- a/test/0.8.25/vaults/vaulthub/vaulthub.reporting.test.ts.todo +++ b/test/0.8.25/vaults/vaulthub/vaulthub.reporting.test.ts.todo @@ -35,9 +35,6 @@ const RESERVATION_FEE_BP = 1_00n; const CONNECT_DEPOSIT = ether("1"); -const TEST_ROOT = "0x4d7731e031705b521abbc5848458dc64ab85c2c3262be16f57bf5ea82a82178a"; -const TEST_PROOF = ["0xd129d34738564e7a38fa20b209e965b5fa6036268546a0d58bbe5806b2469c2e"]; - describe("VaultHub.sol:reporting", () => { let deployer: HardhatEthersSigner; let user: HardhatEthersSigner; @@ -184,210 +181,5 @@ describe("VaultHub.sol:reporting", () => { afterEach(async () => await Snapshot.restore(originalState)); - context("updateReportData", () => { - it("reverts report update data call from non-Accounting contract", async () => { - await expect(vaultHub.updateReportData(0, ethers.ZeroHash, "")).to.be.revertedWithCustomError( - vaultHub, - "NotAuthorized", - ); - }); - - it("accepts report data from Accounting contract", async () => { - const accountingAddress = await impersonate(await locator.accounting(), ether("1")); - await expect(vaultHub.connect(accountingAddress).updateReportData(0, ethers.ZeroHash, "")).to.not.reverted; - }); - - it("returns lastest report data correctly", async () => { - const accountingAddress = await impersonate(await locator.accounting(), ether("1")); - const reportTimestamp = await getCurrentBlockTimestamp(); - await expect(vaultHub.connect(accountingAddress).updateReportData(reportTimestamp, ethers.ZeroHash, "test_cid")) - .to.not.reverted; - - const lastReportData = await vaultHub.latestReportData(); - expect(lastReportData.timestamp).to.equal(reportTimestamp); - expect(lastReportData.treeRoot).to.equal(ethers.ZeroHash); - expect(lastReportData.reportCid).to.equal("test_cid"); - }); - }); - context("updateVaultData", () => { - it("reverts on invalid proof", async () => { - const accountingAddress = await impersonate(await locator.accounting(), ether("1")); - await expect(vaultHub.connect(accountingAddress).updateReportData(0, TEST_ROOT, "")).to.not.reverted; - await vaultHub.harness__connectVault( - "0xEcB7C8D2BaF7270F90066B4cd8286e2CA1154F60", - SHARE_LIMIT, - RESERVE_RATIO_BP, - FORCED_REBALANCE_THRESHOLD_BP, - INFRA_FEE_BP, - LIQUIDITY_FEE_BP, - RESERVATION_FEE_BP, - ); - - await expect( - vaultHub.updateVaultData( - "0xEcB7C8D2BaF7270F90066B4cd8286e2CA1154F60", - 99170000769726969624n, - 33000000000000000001n, - 0n, - 0n, - TEST_PROOF, - ), - ).to.be.revertedWithCustomError(vaultHub, "InvalidProof"); - }); - - it("accepts generated proof", async () => { - const vaultsReport: VaultReportItem[] = [ - ["0xE312f1ed35c4dBd010A332118baAD69d45A0E302", 33000000000000000000n, 33000000000000000000n, 0n, 0n], - [ - "0x652b70E0Ae932896035d553fEaA02f37Ab34f7DC", - 3100000000000000000n, - 3100000000000000000n, - 0n, - 510300000000000000n, - ], - [ - "0x20d34FD0482E3BdC944952D0277A306860be0014", - 2580000000000012501n, - 580000000000012501n, - 0n, - 1449900000000010001n, - ], - ["0x60B614c42d92d6c2E68AF7f4b741867648aBf9A4", 1000000000000000000n, 1000000000000000000n, 0n, 0n], - [ - "0xE6BdAFAac1d91605903D203539faEd173793b7D7", - 1030000000000000000n, - 1030000000000000000n, - 0n, - 400000000000000000n, - ], - ["0x34ebc5780F36d3fD6F1e7b43CF8DB4a80dCE42De", 1000000000000000000n, 1000000000000000000n, 0n, 0n], - [ - "0x3018F0cC632Aa3805a8a676613c62F55Ae4018C7", - 2000000000000000000n, - 2000000000000000000n, - 0n, - 100000000000000000n, - ], - [ - "0x40998324129B774fFc7cDA103A2d2cFd23EcB56e", - 1000000000000000000n, - 1000000000000000000n, - 0n, - 300000000000000000n, - ], - ["0x4ae099982712e2164fBb973554991111A418ab2B", 1000000000000000000n, 1000000000000000000n, 0n, 0n], - ["0x59536AC6211C1deEf1EE37CDC11242A0bDc7db83", 1000000000000000000n, 1000000000000000000n, 0n, 0n], - ]; - - const tree = createVaultsReportTree(vaultsReport); - const accountingAddress = await impersonate(await locator.accounting(), ether("100")); - await vaultHub.connect(accountingAddress).updateReportData(await getCurrentBlockTimestamp(), tree.root, ""); - - for (let index = 0; index < vaultsReport.length; index++) { - const vaultReport = vaultsReport[index]; - await vaultHub.harness__connectVault( - vaultReport[0], - SHARE_LIMIT, - RESERVE_RATIO_BP, - FORCED_REBALANCE_THRESHOLD_BP, - INFRA_FEE_BP, - LIQUIDITY_FEE_BP, - RESERVATION_FEE_BP, - ); - await setCode(vaultReport[0], vaultCode); - await vaultHub.updateVaultData( - vaultReport[0], - vaultReport[1], - vaultReport[2], - vaultReport[3], - vaultReport[4], - tree.getProof(index), - ); - } - - expect(tree.root).to.equal("0x305228cb82b2385b40ebeb7f0b805e58c2e9942bd84183eb1d603b765af94ca1"); - const proof = tree.getProof(1); - expect(proof).to.deep.equal([ - "0x3dfaa9117d824d40ae979f184ce0a9e60d7474912e7b53603e40b0b34cbba72f", - "0x33c5d49ae39b473dc097b8987ab2f876542ad500209b96af5600da11289fe643", - "0x5060c4e8e98281c0181273abcabb2e9e8f06fe6353e99f96606bb87635c9b090", - ]); - }); - - it("calculates merkle tree the same way as off-chain implementation", async () => { - const values: VaultReportItem[] = [ - ["0xc1F9c4a809cbc6Cb2cA60bCa09cE9A55bD5337Db", 2500000000000000000n, 2500000000000000000n, 0n, 1n], - ["0xEcB7C8D2BaF7270F90066B4cd8286e2CA1154F60", 99170000769726969624n, 33000000000000000000n, 0n, 0n], - ]; - - const tree = createVaultsReportTree(values); - expect(tree.root).to.equal(TEST_ROOT); - const proof = tree.getProof(1); - expect(proof).to.deep.equal(TEST_PROOF); - }); - - async function updateVaultReportHelper( - vault: StakingVault__MockForVaultHub, - totalValue: bigint, - inOutDelta: bigint, - treasuryFees: bigint, - liabilityShares: bigint, - ) { - const vaultReport: VaultReportItem = [ - await vault.getAddress(), - totalValue, - inOutDelta, - treasuryFees, - liabilityShares, - ]; - const tree = createVaultsReportTree([vaultReport]); - const accountingAddress = await impersonate(await locator.accounting(), ether("100")); - await vaultHub.connect(accountingAddress).updateReportData(await getCurrentBlockTimestamp(), tree.root, ""); - - await vaultHub.updateVaultData( - vault.getAddress(), - totalValue, - inOutDelta, - treasuryFees, - liabilityShares, - tree.getProof(0), - ); - } - - it("calculates cumulative vaults treasury fees", async () => { - const vault = await createAndConnectVault(vaultFactory, { - shareLimit: ether("100"), // just to bypass the share limit check - reserveRatioBP: 50_00n, // 50% - forcedRebalanceThresholdBP: 50_00n, // 50% - }); - - await updateVaultReportHelper(vault, 99170000769726969624n, 33000000000000000000n, 100n, 0n); - - const vaultSocket = await vaultHub["vaultSocket(uint256)"](0n); - expect(vaultSocket.feeSharesCharged).to.equal(100n); - - await updateVaultReportHelper(vault, 99170000769726969624n, 33000000000000000000n, 101n, 0n); - - const vaultSocket2 = await vaultHub["vaultSocket(uint256)"](0n); - expect(vaultSocket2.feeSharesCharged).to.equal(101n); - }); - - it("rejects incorrectly reported cumulative vaults treasury fees", async () => { - const vault = await createAndConnectVault(vaultFactory, { - shareLimit: ether("100"), // just to bypass the share limit check - reserveRatioBP: 50_00n, // 50% - forcedRebalanceThresholdBP: 50_00n, // 50% - }); - - await updateVaultReportHelper(vault, 99170000769726969624n, 33000000000000000000n, 100n, 0n); - - const vaultSocket = await vaultHub["vaultSocket(uint256)"](0n); - expect(vaultSocket.feeSharesCharged).to.equal(100n); - - await expect(updateVaultReportHelper(vault, 99170000769726969624n, 33000000000000000000n, 99n, 0n)) - .to.be.revertedWithCustomError(vaultHub, "InvalidFees") - .withArgs(vault.getAddress(), 99n, 100n); - }); - }); }); From 878fa3c5e7cccaa856f699f688bad214bb073a9c Mon Sep 17 00:00:00 2001 From: VP Date: Fri, 27 Jun 2025 18:28:57 +0200 Subject: [PATCH 06/11] feat: sanity checks tests --- contracts/0.8.25/vaults/LazyOracle.sol | 9 ++-- .../vaults/lazyOracle/lazyOracle.test.ts | 44 +++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/contracts/0.8.25/vaults/LazyOracle.sol b/contracts/0.8.25/vaults/LazyOracle.sol index 735577b16e..98759b4598 100644 --- a/contracts/0.8.25/vaults/LazyOracle.sol +++ b/contracts/0.8.25/vaults/LazyOracle.sol @@ -300,10 +300,10 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable { /// @param _totalValue the total value of the vault in refSlot /// @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 - ) public returns (uint256 totalValueWithoutQuarantine, int256 inOutDeltaOnRefSlot) { + function _handleSanityChecks(address _vault, uint256 _totalValue) + internal + returns (uint256 totalValueWithoutQuarantine, int256 inOutDeltaOnRefSlot) + { VaultHub vaultHub = _vaultHub(); VaultHub.VaultRecord memory record = vaultHub.vaultRecord(_vault); @@ -404,6 +404,7 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable { event QuarantinedDeposit(address indexed vault, uint128 delta); event SanityParamsUpdated(uint64 quarantinePeriod, uint16 maxRewardRatioBP); event QuarantineExpired(address indexed vault, uint128 delta); + error AdminCannotBeZero(); error NotAuthorized(); error InvalidProof(); diff --git a/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts b/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts index 22c8fae6c7..e201eaec15 100644 --- a/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts +++ b/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts @@ -332,4 +332,48 @@ describe("LazyOracle.sol", () => { expect(proof).to.deep.equal(TEST_PROOF); }); }); + + context("handleSanityChecks", () => { + it("limit", async () => { + const vault = await createVault(); + const vaultReport: VaultReportItem = [vault, ether("250"), 0n, 0n, 0n]; + + const tree = createVaultsReportTree([vaultReport]); + const accountingAddress = await impersonate(await locator.accountingOracle(), ether("100")); + const timestamp = await getCurrentBlockTimestamp(); + await lazyOracle.connect(accountingAddress).updateReportData(timestamp, tree.root, ""); + + await vaultHub.mock__addVault(vault); + await vaultHub.mock__setVaultRecord(vault, { + report: { + totalValue: ether("100"), + inOutDelta: ether("100"), + timestamp, + }, + locked: 0n, + liabilityShares: 0n, + inOutDelta: { + value: ether("100"), + valueOnRefSlot: ether("100"), + refSlot: 0n, + }, + }); + + await lazyOracle.updateVaultData( + vaultReport[0], + vaultReport[1], + vaultReport[2], + vaultReport[3], + vaultReport[4], + tree.getProof(0), + ); + await expect(await vaultHub.mock__lastReported_totalValue()).to.equal(ether("100")); + + const quarantineInfo = await lazyOracle.vaultQuarantine(vault); + expect(quarantineInfo.isActive).to.equal(true); + expect(quarantineInfo.pendingTotalValueIncrease).to.equal(ether("150")); + expect(quarantineInfo.startTimestamp).to.equal(timestamp); + expect(quarantineInfo.endTimestamp).to.equal(timestamp + 259200n); + }); + }); }); From cecbfeb48b08c3361d44789c50da88cb6fb59d3a Mon Sep 17 00:00:00 2001 From: VP Date: Mon, 30 Jun 2025 14:58:17 +0200 Subject: [PATCH 07/11] feat: sanity checker tests --- .../contracts/VaultHub__MockForLazyOracle.sol | 7 ++ .../vaults/lazyOracle/lazyOracle.test.ts | 100 +++++++++++++++--- 2 files changed, 95 insertions(+), 12 deletions(-) diff --git a/test/0.8.25/vaults/lazyOracle/contracts/VaultHub__MockForLazyOracle.sol b/test/0.8.25/vaults/lazyOracle/contracts/VaultHub__MockForLazyOracle.sol index 3f239895ee..6d2a5d774b 100644 --- a/test/0.8.25/vaults/lazyOracle/contracts/VaultHub__MockForLazyOracle.sol +++ b/test/0.8.25/vaults/lazyOracle/contracts/VaultHub__MockForLazyOracle.sol @@ -95,6 +95,13 @@ contract VaultHub__MockForLazyOracle { mock__lastReported_cumulativeLidoFees = _reportCumulativeLidoFees; mock__lastReported_liabilityShares = _reportLiabilityShares; mock__lastReported_slashingReserve = _reportSlashingReserve; + + mock__vaultRecords[_vault].report.inOutDelta = int112(_reportInOutDelta); + mock__vaultRecords[_vault].inOutDelta.value = int112(_reportInOutDelta); + mock__vaultRecords[_vault].inOutDelta.valueOnRefSlot = int112(_reportInOutDelta); + mock__vaultRecords[_vault].inOutDelta.refSlot = uint32(_reportTimestamp); + mock__vaultRecords[_vault].report.timestamp = uint32(_reportTimestamp); + mock__vaultRecords[_vault].report.totalValue = uint112(_reportTotalValue); } // function vaultRecord(address vault) external view returns (VaultHub.VaultRecord memory) { diff --git a/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts b/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts index e201eaec15..78b1a7194b 100644 --- a/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts +++ b/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts @@ -11,7 +11,7 @@ import { VaultHub__MockForLazyOracle, } from "typechain-types"; -import { ether, getCurrentBlockTimestamp, impersonate, randomAddress } from "lib"; +import { advanceChainTime, ether, getCurrentBlockTimestamp, impersonate, randomAddress } from "lib"; import { createVaultsReportTree, VaultReportItem } from "lib/protocol/helpers/vaults"; import { deployLidoLocator } from "test/deploy"; @@ -27,6 +27,8 @@ describe("LazyOracle.sol", () => { let originalState: string; + const QUARANTINE_PERIOD = 259200n; + before(async () => { [deployer] = await ethers.getSigners(); vaultHub = await ethers.deployContract("VaultHub__MockForLazyOracle", []); @@ -47,7 +49,7 @@ describe("LazyOracle.sol", () => { ); lazyOracle = await ethers.getContractAt("LazyOracle", proxy); - await lazyOracle.initialize(deployer.address, 259200n, 350n); + await lazyOracle.initialize(deployer.address, QUARANTINE_PERIOD, 350n); }); beforeEach(async () => (originalState = await Snapshot.take())); @@ -348,7 +350,7 @@ describe("LazyOracle.sol", () => { report: { totalValue: ether("100"), inOutDelta: ether("100"), - timestamp, + timestamp: timestamp - 100n, }, locked: 0n, liabilityShares: 0n, @@ -359,21 +361,95 @@ describe("LazyOracle.sol", () => { }, }); - await lazyOracle.updateVaultData( - vaultReport[0], - vaultReport[1], - vaultReport[2], - vaultReport[3], - vaultReport[4], - tree.getProof(0), - ); + await expect( + lazyOracle.updateVaultData( + vaultReport[0], + vaultReport[1], + vaultReport[2], + vaultReport[3], + vaultReport[4], + tree.getProof(0), + ), + ) + .to.emit(lazyOracle, "QuarantinedDeposit") + .withArgs(vault, ether("150")); await expect(await vaultHub.mock__lastReported_totalValue()).to.equal(ether("100")); const quarantineInfo = await lazyOracle.vaultQuarantine(vault); expect(quarantineInfo.isActive).to.equal(true); expect(quarantineInfo.pendingTotalValueIncrease).to.equal(ether("150")); expect(quarantineInfo.startTimestamp).to.equal(timestamp); - expect(quarantineInfo.endTimestamp).to.equal(timestamp + 259200n); + expect(quarantineInfo.endTimestamp).to.equal(timestamp + QUARANTINE_PERIOD); + + // Second report - in 24 hours we add more funds to the vault + const vaultReport2: VaultReportItem = [vault, ether("340"), 0n, 0n, 0n]; + + const tree2 = createVaultsReportTree([vaultReport2]); + await advanceChainTime(60n * 60n * 23n); + const timestamp2 = await getCurrentBlockTimestamp(); + await lazyOracle.connect(accountingAddress).updateReportData(timestamp2, tree2.root, ""); + + await lazyOracle.updateVaultData( + vaultReport2[0], + vaultReport2[1], + vaultReport2[2], + vaultReport2[3], + vaultReport2[4], + tree2.getProof(0), + ); + await expect(await vaultHub.mock__lastReported_totalValue()).to.equal(ether("100")); + + const quarantineInfo2 = await lazyOracle.vaultQuarantine(vault); + expect(quarantineInfo2.isActive).to.equal(true); + expect(quarantineInfo2.pendingTotalValueIncrease).to.equal(ether("150")); + expect(quarantineInfo2.startTimestamp).to.equal(timestamp); + expect(quarantineInfo2.endTimestamp).to.equal(timestamp + QUARANTINE_PERIOD); + + // Third report - in 3 days - we keep the vault at the same level + const vaultReport3: VaultReportItem = [vault, ether("340"), 0n, 0n, 0n]; + + const tree3 = createVaultsReportTree([vaultReport3]); + await advanceChainTime(60n * 60n * 23n * 5n); + const timestamp3 = await getCurrentBlockTimestamp(); + await lazyOracle.connect(accountingAddress).updateReportData(timestamp3, tree3.root, ""); + + await lazyOracle.updateVaultData( + vaultReport3[0], + vaultReport3[1], + vaultReport3[2], + vaultReport3[3], + vaultReport3[4], + tree3.getProof(0), + ); + + const quarantineInfo3 = await lazyOracle.vaultQuarantine(vault); + expect(quarantineInfo3.isActive).to.equal(true); + expect(quarantineInfo3.pendingTotalValueIncrease).to.equal(ether("90")); + expect(quarantineInfo3.startTimestamp).to.equal(timestamp3); + expect(quarantineInfo3.endTimestamp).to.equal(timestamp3 + QUARANTINE_PERIOD); + + // Fourth report - in 4 days - we keep the vault at the same level + const vaultReport4: VaultReportItem = [vault, ether("340"), 0n, 0n, 0n]; + + const tree4 = createVaultsReportTree([vaultReport4]); + await advanceChainTime(60n * 60n * 23n * 4n); + const timestamp4 = await getCurrentBlockTimestamp(); + await lazyOracle.connect(accountingAddress).updateReportData(timestamp4, tree4.root, ""); + + await lazyOracle.updateVaultData( + vaultReport4[0], + vaultReport4[1], + vaultReport4[2], + vaultReport4[3], + vaultReport4[4], + tree4.getProof(0), + ); + + const quarantineInfo4 = await lazyOracle.vaultQuarantine(vault); + expect(quarantineInfo4.isActive).to.equal(false); + expect(quarantineInfo4.pendingTotalValueIncrease).to.equal(0n); + expect(quarantineInfo4.startTimestamp).to.equal(0n); + expect(quarantineInfo4.endTimestamp).to.equal(0n); }); }); }); From a841dc5e614464bd441e04d7fb5d2d8f4e23e517 Mon Sep 17 00:00:00 2001 From: VP Date: Tue, 1 Jul 2025 15:54:22 +0200 Subject: [PATCH 08/11] chore: remove leftovers --- .../contracts/VaultHub__MockForLazyOracle.sol | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/test/0.8.25/vaults/lazyOracle/contracts/VaultHub__MockForLazyOracle.sol b/test/0.8.25/vaults/lazyOracle/contracts/VaultHub__MockForLazyOracle.sol index 6d2a5d774b..6fd2def903 100644 --- a/test/0.8.25/vaults/lazyOracle/contracts/VaultHub__MockForLazyOracle.sol +++ b/test/0.8.25/vaults/lazyOracle/contracts/VaultHub__MockForLazyOracle.sol @@ -8,7 +8,6 @@ import {ILido} from "contracts/common/interfaces/ILido.sol"; import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; import {VaultHub} from "contracts/0.8.25/vaults/VaultHub.sol"; import {RefSlotCache} from "contracts/0.8.25/vaults/lib/RefSlotCache.sol"; -import "hardhat/console.sol"; contract VaultHub__MockForLazyOracle { using RefSlotCache for RefSlotCache.Int112WithRefSlotCache; @@ -55,20 +54,6 @@ contract VaultHub__MockForLazyOracle { function vaultConnection(address vault) external view returns (VaultHub.VaultConnection memory) { return mock__vaultConnections[vault]; - // console.log("vaultConnection", vault); - // VaultHub.VaultConnection memory connection = VaultHub.VaultConnection({ - // owner: address(1), - // shareLimit: 1000000000000000000, - // vaultIndex: 1, - // pendingDisconnect: false, - // reserveRatioBP: 10000, - // forcedRebalanceThresholdBP: 10000, - // infraFeeBP: 10000, - // liquidityFeeBP: 10000, - // reservationFeeBP: 10000, - // isBeaconDepositsManuallyPaused: false - // }); - // return connection; } function maxLockableValue(address vault) external view returns (uint256) { @@ -103,15 +88,4 @@ contract VaultHub__MockForLazyOracle { mock__vaultRecords[_vault].report.timestamp = uint32(_reportTimestamp); mock__vaultRecords[_vault].report.totalValue = uint112(_reportTotalValue); } - - // function vaultRecord(address vault) external view returns (VaultHub.VaultRecord memory) { - // VaultHub.VaultRecord memory record = VaultHub.VaultRecord({ - // report: VaultHub.Report({totalValue: uint112(1), inOutDelta: int112(int256(2)), timestamp: uint32(3)}), - // locked: uint128(4), - // liabilityShares: 5, - // inOutDelta: RefSlotCache.Int112WithRefSlotCache({value: int112(int256(6)), valueOnRefSlot: 7, refSlot: 8}) - // }); - - // return record; - // } } From 678524f221aa4cf63aeba06856aae089f026b853 Mon Sep 17 00:00:00 2001 From: VP Date: Wed, 2 Jul 2025 20:47:01 +0200 Subject: [PATCH 09/11] feat: remove indexes from batch vaults --- contracts/0.8.25/vaults/LazyOracle.sol | 2 -- test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/contracts/0.8.25/vaults/LazyOracle.sol b/contracts/0.8.25/vaults/LazyOracle.sol index 98759b4598..d77b1503f0 100644 --- a/contracts/0.8.25/vaults/LazyOracle.sol +++ b/contracts/0.8.25/vaults/LazyOracle.sol @@ -92,7 +92,6 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable { struct VaultInfo { address vault; - uint96 vaultIndex; uint256 balance; int256 inOutDelta; bytes32 withdrawalCredentials; @@ -205,7 +204,6 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable { VaultHub.VaultRecord memory record = vaultHub.vaultRecord(vaultAddress); batch[i] = VaultInfo( vaultAddress, - connection.vaultIndex, address(vault).balance, vaultHub.inOutDeltaAsOfLastRefSlot(vaultAddress), vault.withdrawalCredentials(), diff --git a/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts b/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts index 78b1a7194b..c348a5128d 100644 --- a/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts +++ b/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts @@ -107,7 +107,6 @@ describe("LazyOracle.sol", () => { const vaultInfo = vaults[0]; expect(vaultInfo.vault).to.equal(vault1); - expect(vaultInfo.vaultIndex).to.equal(1n); expect(vaultInfo.balance).to.equal(0n); expect(vaultInfo.inOutDelta).to.equal(5n); expect(vaultInfo.withdrawalCredentials).to.equal(ZERO_BYTES32); From 48bb54422a0732e3b0aae5d821660acfa80fda48 Mon Sep 17 00:00:00 2001 From: VP Date: Fri, 4 Jul 2025 10:10:38 +0200 Subject: [PATCH 10/11] feat: proper inout delta for batch getter --- contracts/0.8.25/vaults/LazyOracle.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/0.8.25/vaults/LazyOracle.sol b/contracts/0.8.25/vaults/LazyOracle.sol index d77b1503f0..0644ef6688 100644 --- a/contracts/0.8.25/vaults/LazyOracle.sol +++ b/contracts/0.8.25/vaults/LazyOracle.sol @@ -205,7 +205,7 @@ contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable { batch[i] = VaultInfo( vaultAddress, address(vault).balance, - vaultHub.inOutDeltaAsOfLastRefSlot(vaultAddress), + record.inOutDelta.value, vault.withdrawalCredentials(), record.liabilityShares, _mintableStETH(vaultAddress), From ad33d442c25b7f8ac035e00ea52ff7669e5bda1b Mon Sep 17 00:00:00 2001 From: VP Date: Fri, 4 Jul 2025 13:40:03 +0200 Subject: [PATCH 11/11] test: update cases --- .../vaults/lazyOracle/lazyOracle.test.ts | 125 +++++++++++++++--- 1 file changed, 107 insertions(+), 18 deletions(-) diff --git a/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts b/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts index c348a5128d..193b385dbe 100644 --- a/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts +++ b/test/0.8.25/vaults/lazyOracle/lazyOracle.test.ts @@ -196,8 +196,12 @@ describe("LazyOracle.sol", () => { context("sanity params", () => { it("update quarantine period", async () => { + await expect(lazyOracle.updateSanityParams(250000n, 1000n)) + .to.be.revertedWithCustomError(lazyOracle, "AccessControlUnauthorizedAccount") + .withArgs(deployer.address, await lazyOracle.UPDATE_SANITY_PARAMS_ROLE()); + await lazyOracle.grantRole(await lazyOracle.UPDATE_SANITY_PARAMS_ROLE(), deployer.address); - await lazyOracle.updateSanityParams(250000n, 1000n); + await expect(lazyOracle.updateSanityParams(250000n, 1000n)).to.not.reverted; expect(await lazyOracle.quarantinePeriod()).to.equal(250000n); expect(await lazyOracle.maxRewardRatioBP()).to.equal(1000n); }); @@ -335,7 +339,84 @@ describe("LazyOracle.sol", () => { }); context("handleSanityChecks", () => { - it("limit", async () => { + it("allows some percentage of the EL and CL rewards handling", async () => { + const vault = await createVault(); + const VAULT_TOTAL_VALUE = ether("100"); + const maxRewardRatio = await lazyOracle.maxRewardRatioBP(); + const maxRewardValue = (maxRewardRatio * VAULT_TOTAL_VALUE) / 10000n; + const vaultReport: VaultReportItem = [vault, VAULT_TOTAL_VALUE + maxRewardValue, 0n, 0n, 0n]; + + const tree = createVaultsReportTree([vaultReport]); + const accountingAddress = await impersonate(await locator.accountingOracle(), ether("100")); + const timestamp = await getCurrentBlockTimestamp(); + await lazyOracle.connect(accountingAddress).updateReportData(timestamp, tree.root, ""); + + await vaultHub.mock__addVault(vault); + await vaultHub.mock__setVaultRecord(vault, { + report: { + totalValue: VAULT_TOTAL_VALUE, + inOutDelta: VAULT_TOTAL_VALUE, + timestamp: timestamp - 100n, + }, + locked: 0n, + liabilityShares: 0n, + inOutDelta: { + value: VAULT_TOTAL_VALUE, + valueOnRefSlot: VAULT_TOTAL_VALUE, + refSlot: 0n, + }, + }); + + await lazyOracle.updateVaultData( + vaultReport[0], + vaultReport[1], + vaultReport[2], + vaultReport[3], + vaultReport[4], + tree.getProof(0), + ); + await expect(await vaultHub.mock__lastReported_totalValue()).to.equal(VAULT_TOTAL_VALUE + maxRewardValue); + + const quarantineInfo = await lazyOracle.vaultQuarantine(vault); + expect(quarantineInfo.isActive).to.equal(false); + + // Second report exceeds the max reward value by 1 wei - should be quarantined + const vaultReport2: VaultReportItem = [vault, VAULT_TOTAL_VALUE + maxRewardValue + 1n, 0n, 0n, 0n]; + + const tree2 = createVaultsReportTree([vaultReport2]); + await lazyOracle.connect(accountingAddress).updateReportData(timestamp, tree2.root, ""); + + await vaultHub.mock__setVaultRecord(vault, { + report: { + totalValue: VAULT_TOTAL_VALUE, + inOutDelta: VAULT_TOTAL_VALUE, + timestamp: timestamp - 100n, + }, + locked: 0n, + liabilityShares: 0n, + inOutDelta: { + value: VAULT_TOTAL_VALUE, + valueOnRefSlot: VAULT_TOTAL_VALUE, + refSlot: 0n, + }, + }); + + await lazyOracle.updateVaultData( + vaultReport2[0], + vaultReport2[1], + vaultReport2[2], + vaultReport2[3], + vaultReport2[4], + tree2.getProof(0), + ); + await expect(await vaultHub.mock__lastReported_totalValue()).to.equal(VAULT_TOTAL_VALUE); + + const quarantineInfo2 = await lazyOracle.vaultQuarantine(vault); + expect(quarantineInfo2.isActive).to.equal(true); + expect(quarantineInfo2.pendingTotalValueIncrease).to.equal(maxRewardValue + 1n); + }); + + it("limit the vault total value", async () => { const vault = await createVault(); const vaultReport: VaultReportItem = [vault, ether("250"), 0n, 0n, 0n]; @@ -412,14 +493,18 @@ describe("LazyOracle.sol", () => { const timestamp3 = await getCurrentBlockTimestamp(); await lazyOracle.connect(accountingAddress).updateReportData(timestamp3, tree3.root, ""); - await lazyOracle.updateVaultData( - vaultReport3[0], - vaultReport3[1], - vaultReport3[2], - vaultReport3[3], - vaultReport3[4], - tree3.getProof(0), - ); + await expect( + lazyOracle.updateVaultData( + vaultReport3[0], + vaultReport3[1], + vaultReport3[2], + vaultReport3[3], + vaultReport3[4], + tree3.getProof(0), + ), + ) + .to.emit(lazyOracle, "QuarantinedDeposit") + .withArgs(vault, ether("90")); const quarantineInfo3 = await lazyOracle.vaultQuarantine(vault); expect(quarantineInfo3.isActive).to.equal(true); @@ -435,14 +520,18 @@ describe("LazyOracle.sol", () => { const timestamp4 = await getCurrentBlockTimestamp(); await lazyOracle.connect(accountingAddress).updateReportData(timestamp4, tree4.root, ""); - await lazyOracle.updateVaultData( - vaultReport4[0], - vaultReport4[1], - vaultReport4[2], - vaultReport4[3], - vaultReport4[4], - tree4.getProof(0), - ); + await expect( + lazyOracle.updateVaultData( + vaultReport4[0], + vaultReport4[1], + vaultReport4[2], + vaultReport4[3], + vaultReport4[4], + tree4.getProof(0), + ), + ) + .to.emit(lazyOracle, "QuarantineExpired") + .withArgs(vault, ether("90")); const quarantineInfo4 = await lazyOracle.vaultQuarantine(vault); expect(quarantineInfo4.isActive).to.equal(false);