Skip to content

Commit

Permalink
Merge pull request #136 from lidofinance/feat/oracle-integration
Browse files Browse the repository at this point in the history
Accounting Oracle integration tests
  • Loading branch information
tamtamchik authored Sep 5, 2024
2 parents bdc1379 + ba80c84 commit 7febc70
Show file tree
Hide file tree
Showing 10 changed files with 1,249 additions and 130 deletions.
34 changes: 28 additions & 6 deletions lib/protocol/discover.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import hre from "hardhat";

import { AccountingOracle, Lido, LidoLocator, StakingRouter } from "typechain-types";
import { AccountingOracle, Lido, LidoLocator, StakingRouter, WithdrawalQueueERC721 } from "typechain-types";

import { batch, log } from "lib";

Expand All @@ -15,6 +15,7 @@ import {
ProtocolContracts,
ProtocolSigners,
StakingModuleContracts,
WstETHContracts,
} from "./types";

const guard = (address: string, env: string) => {
Expand All @@ -39,6 +40,7 @@ const getDiscoveryConfig = async () => {

log.debug("Discovery config", {
"Network": hre.network.name,
"Source": config.source,
"Locator address": locatorAddress,
"Agent address": agentAddress,
"Voting address": votingAddress,
Expand All @@ -60,8 +62,8 @@ const loadContract = async <Name extends ContractName>(name: Name, address: stri
/**
* Load all Lido protocol foundation contracts.
*/
const getFoundationContracts = async (locator: LoadedContract<LidoLocator>, config: ProtocolNetworkConfig) =>
(await batch({
const getCoreContracts = async (locator: LoadedContract<LidoLocator>, config: ProtocolNetworkConfig) => {
return (await batch({
accountingOracle: loadContract(
"AccountingOracle",
config.get("accountingOracle") || (await locator.accountingOracle()),
Expand Down Expand Up @@ -99,6 +101,7 @@ const getFoundationContracts = async (locator: LoadedContract<LidoLocator>, conf
config.get("oracleDaemonConfig") || (await locator.oracleDaemonConfig()),
),
})) as CoreContracts;
};

/**
* Load Aragon contracts required for protocol.
Expand Down Expand Up @@ -126,24 +129,42 @@ const getStakingModules = async (stakingRouter: LoadedContract<StakingRouter>, c
/**
* Load HashConsensus contract for accounting oracle.
*/
const getHashConsensus = async (accountingOracle: LoadedContract<AccountingOracle>, config: ProtocolNetworkConfig) => {
const getHashConsensusContract = async (
accountingOracle: LoadedContract<AccountingOracle>,
config: ProtocolNetworkConfig,
) => {
const hashConsensusAddress = config.get("hashConsensus") || (await accountingOracle.getConsensusContract());
return (await batch({
hashConsensus: loadContract("HashConsensus", hashConsensusAddress),
})) as HashConsensusContracts;
};

/**
* Load wstETH contracts.
* @notice https://github.com/lidofinance/core/issues/163 – wstETH contract should be a part of the CoreContracts
*/
const getWstEthContract = async (
withdrawalQueue: LoadedContract<WithdrawalQueueERC721>,
config: ProtocolNetworkConfig,
) => {
const wstETHAddress = config.get("wstETH") || (await withdrawalQueue.WSTETH());
return (await batch({
wstETH: loadContract("WstETH", wstETHAddress),
})) as WstETHContracts;
};

export async function discover() {
const networkConfig = await getDiscoveryConfig();
const locator = await loadContract("LidoLocator", networkConfig.get("locator"));
const foundationContracts = await getFoundationContracts(locator, networkConfig);
const foundationContracts = await getCoreContracts(locator, networkConfig);

const contracts = {
locator,
...foundationContracts,
...(await getAragonContracts(foundationContracts.lido, networkConfig)),
...(await getStakingModules(foundationContracts.stakingRouter, networkConfig)),
...(await getHashConsensus(foundationContracts.accountingOracle, networkConfig)),
...(await getHashConsensusContract(foundationContracts.accountingOracle, networkConfig)),
...(await getWstEthContract(foundationContracts.withdrawalQueue, networkConfig)),
} as ProtocolContracts;

log.debug("Contracts discovered", {
Expand All @@ -165,6 +186,7 @@ export async function discover() {
"ACL": contracts.acl.address,
"Burner": foundationContracts.burner.address,
"Legacy Oracle": foundationContracts.legacyOracle.address,
"wstETH": contracts.wstETH.address,
});

const signers = {
Expand Down
132 changes: 78 additions & 54 deletions lib/protocol/helpers/accounting.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from "chai";
import { Result } from "ethers";
import { ContractTransactionResponse, formatEther, Result } from "ethers";
import { ethers } from "hardhat";

import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
Expand Down Expand Up @@ -96,7 +96,11 @@ export const report = async (
reportElVault = true,
reportWithdrawalsVault = true,
} = {} as Partial<OracleReportOptions>,
) => {
): Promise<{
data: AccountingOracle.ReportDataStruct;
reportTx: ContractTransactionResponse | undefined;
extraDataTx: ContractTransactionResponse | undefined;
}> => {
const { hashConsensus, lido, elRewardsVault, withdrawalVault, burner, accountingOracle } = ctx.contracts;

// Fast-forward to next report time
Expand All @@ -115,15 +119,15 @@ export const report = async (

log.debug("Beacon", {
"Beacon validators": postBeaconValidators,
"Beacon balance": ethers.formatEther(postCLBalance),
"Beacon balance": formatEther(postCLBalance),
});

elRewardsVaultBalance = elRewardsVaultBalance ?? (await ethers.provider.getBalance(elRewardsVault.address));
withdrawalVaultBalance = withdrawalVaultBalance ?? (await ethers.provider.getBalance(withdrawalVault.address));

log.debug("Balances", {
"Withdrawal vault": ethers.formatEther(withdrawalVaultBalance),
"ElRewards vault": ethers.formatEther(elRewardsVaultBalance),
"Withdrawal vault": formatEther(withdrawalVaultBalance),
"ElRewards vault": formatEther(elRewardsVaultBalance),
});

// excludeVaultsBalance safely forces LIDO to see vault balances as empty allowing zero/negative rebase
Expand All @@ -147,8 +151,8 @@ export const report = async (

log.debug("Burner", {
"Shares Requested To Burn": sharesRequestedToBurn,
"Withdrawal vault": ethers.formatEther(withdrawalVaultBalance),
"ElRewards vault": ethers.formatEther(elRewardsVaultBalance),
"Withdrawal vault": formatEther(withdrawalVaultBalance),
"ElRewards vault": formatEther(elRewardsVaultBalance),
});

let isBunkerMode = false;
Expand All @@ -169,10 +173,10 @@ export const report = async (
const { postTotalPooledEther, postTotalShares, withdrawals, elRewards } = simulatedReport!;

log.debug("Simulated report", {
"Post Total Pooled Ether": ethers.formatEther(postTotalPooledEther),
"Post Total Pooled Ether": formatEther(postTotalPooledEther),
"Post Total Shares": postTotalShares,
"Withdrawals": ethers.formatEther(withdrawals),
"El Rewards": ethers.formatEther(elRewards),
"Withdrawals": formatEther(withdrawals),
"El Rewards": formatEther(elRewards),
});

if (simulatedShareRate === null) {
Expand All @@ -195,7 +199,7 @@ export const report = async (
}

if (dryRun) {
const reportData = {
const data = {
consensusVersion: await accountingOracle.getConsensusVersion(),
refSlot,
numValidators: postBeaconValidators,
Expand All @@ -214,22 +218,22 @@ export const report = async (
} as AccountingOracle.ReportDataStruct;

log.debug("Final Report (Dry Run)", {
"Consensus version": reportData.consensusVersion,
"Ref slot": reportData.refSlot,
"CL balance": reportData.clBalanceGwei,
"Num validators": reportData.numValidators,
"Withdrawal vault balance": reportData.withdrawalVaultBalance,
"EL rewards vault balance": reportData.elRewardsVaultBalance,
"Shares requested to burn": reportData.sharesRequestedToBurn,
"Withdrawal finalization batches": reportData.withdrawalFinalizationBatches,
"Simulated share rate": reportData.simulatedShareRate,
"Is bunker mode": reportData.isBunkerMode,
"Extra data format": reportData.extraDataFormat,
"Extra data hash": reportData.extraDataHash,
"Extra data items count": reportData.extraDataItemsCount,
"Consensus version": data.consensusVersion,
"Ref slot": data.refSlot,
"CL balance": data.clBalanceGwei,
"Num validators": data.numValidators,
"Withdrawal vault balance": data.withdrawalVaultBalance,
"EL rewards vault balance": data.elRewardsVaultBalance,
"Shares requested to burn": data.sharesRequestedToBurn,
"Withdrawal finalization batches": data.withdrawalFinalizationBatches,
"Simulated share rate": data.simulatedShareRate,
"Is bunker mode": data.isBunkerMode,
"Extra data format": data.extraDataFormat,
"Extra data hash": data.extraDataHash,
"Extra data items count": data.extraDataItemsCount,
});

return { report: reportData, reportTx: undefined, extraDataTx: undefined };
return { data, reportTx: undefined, extraDataTx: undefined };
}

const reportParams = {
Expand All @@ -253,19 +257,15 @@ export const report = async (
return submitReport(ctx, reportParams);
};

/**
* Wait for the next available report time.
*/
export const waitNextAvailableReportTime = async (ctx: ProtocolContext): Promise<void> => {
export const getReportTimeElapsed = async (ctx: ProtocolContext) => {
const { hashConsensus } = ctx.contracts;
const { slotsPerEpoch, secondsPerSlot, genesisTime } = await hashConsensus.getChainConfig();
const { refSlot } = await hashConsensus.getCurrentFrame();

const time = await getCurrentBlockTimestamp();

const { epochsPerFrame } = await hashConsensus.getFrameConfig();

log.debug("Current frame", {
log.debug("Report elapse time", {
"Ref slot": refSlot,
"Ref slot date": new Date(Number(genesisTime + refSlot * secondsPerSlot) * 1000).toUTCString(),
"Epochs per frame": epochsPerFrame,
Expand All @@ -282,18 +282,37 @@ export const waitNextAvailableReportTime = async (ctx: ProtocolContext): Promise
// add 10 slots to be sure that the next frame starts
const nextFrameStartWithOffset = nextFrameStart + secondsPerSlot * 10n;

const timeToAdvance = Number(nextFrameStartWithOffset - time);
return {
time,
nextFrameStart,
nextFrameStartWithOffset,
timeElapsed: nextFrameStartWithOffset - time,
};
};

/**
* Wait for the next available report time.
*/
export const waitNextAvailableReportTime = async (ctx: ProtocolContext): Promise<void> => {
const { hashConsensus } = ctx.contracts;
const { slotsPerEpoch } = await hashConsensus.getChainConfig();
const { epochsPerFrame } = await hashConsensus.getFrameConfig();
const { refSlot } = await hashConsensus.getCurrentFrame();

const slotsPerFrame = slotsPerEpoch * epochsPerFrame;

const { nextFrameStartWithOffset, timeElapsed } = await getReportTimeElapsed(ctx);

await advanceChainTime(timeToAdvance);
await advanceChainTime(timeElapsed);

const timeAfterAdvance = await getCurrentBlockTimestamp();

const nextFrame = await hashConsensus.getCurrentFrame();

log.debug("Next frame", {
"Next ref slot": nextRefSlot,
"Next frame date": new Date(Number(nextFrameStart) * 1000).toUTCString(),
"Time to advance": timeToAdvance,
"Next ref slot": refSlot + slotsPerFrame,
"Next frame date": new Date(Number(nextFrameStartWithOffset) * 1000).toUTCString(),
"Time to advance": timeElapsed,
"Time after advance": timeAfterAdvance,
"Time after advance date": new Date(Number(timeAfterAdvance) * 1000).toUTCString(),
"Ref slot": nextFrame.refSlot,
Expand Down Expand Up @@ -328,9 +347,9 @@ const simulateReport = async (
log.debug("Simulating oracle report", {
"Ref Slot": refSlot,
"Beacon Validators": beaconValidators,
"CL Balance": ethers.formatEther(clBalance),
"Withdrawal Vault Balance": ethers.formatEther(withdrawalVaultBalance),
"El Rewards Vault Balance": ethers.formatEther(elRewardsVaultBalance),
"CL Balance": formatEther(clBalance),
"Withdrawal Vault Balance": formatEther(withdrawalVaultBalance),
"El Rewards Vault Balance": formatEther(elRewardsVaultBalance),
});

const [postTotalPooledEther, postTotalShares, withdrawals, elRewards] = await lido
Expand All @@ -348,10 +367,10 @@ const simulateReport = async (
);

log.debug("Simulation result", {
"Post Total Pooled Ether": ethers.formatEther(postTotalPooledEther),
"Post Total Pooled Ether": formatEther(postTotalPooledEther),
"Post Total Shares": postTotalShares,
"Withdrawals": ethers.formatEther(withdrawals),
"El Rewards": ethers.formatEther(elRewards),
"Withdrawals": formatEther(withdrawals),
"El Rewards": formatEther(elRewards),
});

return { postTotalPooledEther, postTotalShares, withdrawals, elRewards };
Expand Down Expand Up @@ -380,9 +399,9 @@ export const handleOracleReport = async (
log.debug("Handle oracle report", {
"Ref Slot": refSlot,
"Beacon Validators": beaconValidators,
"CL Balance": ethers.formatEther(clBalance),
"Withdrawal Vault Balance": ethers.formatEther(withdrawalVaultBalance),
"El Rewards Vault Balance": ethers.formatEther(elRewardsVaultBalance),
"CL Balance": formatEther(clBalance),
"Withdrawal Vault Balance": formatEther(withdrawalVaultBalance),
"El Rewards Vault Balance": formatEther(elRewardsVaultBalance),
});

const handleReportTx = await lido.connect(accountingOracleAccount).handleOracleReport(
Expand Down Expand Up @@ -437,7 +456,7 @@ const getFinalizationBatches = async (

log.debug("Calculating finalization batches", {
"Share Rate": shareRate,
"Available Eth": ethers.formatEther(availableEth),
"Available Eth": formatEther(availableEth),
"Max Timestamp": maxTimestamp,
});

Expand Down Expand Up @@ -508,15 +527,19 @@ export const submitReport = async (
extraDataItemsCount = 0n,
extraDataList = new Uint8Array(),
} = {} as OracleReportPushOptions,
) => {
): Promise<{
data: AccountingOracle.ReportDataStruct;
reportTx: ContractTransactionResponse;
extraDataTx: ContractTransactionResponse;
}> => {
const { accountingOracle } = ctx.contracts;

log.debug("Pushing oracle report", {
"Ref slot": refSlot,
"CL balance": ethers.formatEther(clBalance),
"CL balance": formatEther(clBalance),
"Validators": numValidators,
"Withdrawal vault": ethers.formatEther(withdrawalVaultBalance),
"El rewards vault": ethers.formatEther(elRewardsVaultBalance),
"Withdrawal vault": formatEther(withdrawalVaultBalance),
"El rewards vault": formatEther(elRewardsVaultBalance),
"Shares requested to burn": sharesRequestedToBurn,
"Simulated share rate": simulatedShareRate,
"Staking module ids with newly exited validators": stakingModuleIdsWithNewlyExitedValidators,
Expand Down Expand Up @@ -559,18 +582,19 @@ export const submitReport = async (
consensusVersion,
});

log.debug("Pushing oracle report", data);
log.debug("Pushed oracle report for reached consensus", data);

const reportTx = await accountingOracle.connect(submitter).submitReportData(data, oracleVersion);

await trace("accountingOracle.submitReportData", reportTx);

log.debug("Pushing oracle report", {
log.debug("Pushed oracle report main data", {
"Ref slot": refSlot,
"Consensus version": consensusVersion,
"Report hash": hash,
});

let extraDataTx;
let extraDataTx: ContractTransactionResponse;
if (extraDataFormat) {
extraDataTx = await accountingOracle.connect(submitter).submitReportExtraDataList(extraDataList);
await trace("accountingOracle.submitReportExtraDataList", extraDataTx);
Expand Down Expand Up @@ -613,7 +637,7 @@ export const submitReport = async (
"Report hash": hash,
});

return { report, reportTx, extraDataTx };
return { data, reportTx, extraDataTx };
};

/**
Expand Down
1 change: 1 addition & 0 deletions lib/protocol/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export {
OracleReportPushOptions,
ensureHashConsensusInitialEpoch,
ensureOracleCommitteeMembers,
getReportTimeElapsed,
waitNextAvailableReportTime,
handleOracleReport,
submitReport,
Expand Down
Loading

0 comments on commit 7febc70

Please sign in to comment.