Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Second opinion oracle test #211

Closed
wants to merge 13 commits into from
69 changes: 56 additions & 13 deletions lib/protocol/helpers/accounting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
impersonate,
log,
ONE_GWEI,
streccak,
trace,
} from "lib";

Expand Down Expand Up @@ -352,19 +353,61 @@ const simulateReport = async (
"El Rewards Vault Balance": formatEther(elRewardsVaultBalance),
});

const [postTotalPooledEther, postTotalShares, withdrawals, elRewards] = await lido
.connect(accountingOracleAccount)
.handleOracleReport.staticCall(
reportTimestamp,
1n * 24n * 60n * 60n, // 1 day
beaconValidators,
clBalance,
withdrawalVaultBalance,
elRewardsVaultBalance,
0n,
[],
0n,
);
// NOTE: To enable negative rebase sanity checker, the static call below
// replaced with advanced eth_call with stateDiff.
// const [postTotalPooledEther1, postTotalShares1, withdrawals1, elRewards1] = await lido
// .connect(accountingOracleAccount)
// .handleOracleReport.staticCall(
// reportTimestamp,
// 1n * 24n * 60n * 60n, // 1 day
// beaconValidators,
// clBalance,
// withdrawalVaultBalance,
// elRewardsVaultBalance,
// 0n,
// [],
// 0n,
// );

// Step 1: Encode the function call data
const data = lido.interface.encodeFunctionData("handleOracleReport", [
reportTimestamp,
BigInt(24 * 60 * 60), // 1 day in seconds
beaconValidators,
clBalance,
withdrawalVaultBalance,
elRewardsVaultBalance,
BigInt(0),
[],
BigInt(0),
]);

// Step 2: Prepare the transaction object
const transactionObject = {
to: lido.address,
from: accountingOracleAccount.address,
data: data,
};

// Step 3: Prepare call parameters, state diff and perform eth_call
const accountingOracleAddr = await accountingOracle.getAddress();
const callParams = [transactionObject, "latest"];
const LAST_PROCESSING_REF_SLOT_POSITION = streccak("lido.BaseOracle.lastProcessingRefSlot");
const stateDiff = {
[accountingOracleAddr]: {
stateDiff: {
[LAST_PROCESSING_REF_SLOT_POSITION]: refSlot, // setting the processing refslot for the sanity checker
},
},
};

const returnData = await ethers.provider.send("eth_call", [...callParams, stateDiff]);

// Step 4: Decode the returned data
const [[postTotalPooledEther, postTotalShares, withdrawals, elRewards]] = lido.interface.decodeFunctionResult(
"handleOracleReport",
returnData,
);

log.debug("Simulation result", {
"Post Total Pooled Ether": formatEther(postTotalPooledEther),
Expand Down
11 changes: 11 additions & 0 deletions test/0.8.9/contracts/SecondOpinionOracleMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ contract SecondOpinionOracleMock is ISecondOpinionOracle {
reports[refSlot] = report;
}

function addPlainReport(uint256 refSlot, uint256 clBalanceGwei, uint256 withdrawalVaultBalanceWei) external {

reports[refSlot] = Report({
success: true,
clBalanceGwei: clBalanceGwei,
withdrawalVaultBalanceWei: withdrawalVaultBalanceWei,
numValidators: 0,
exitedValidators: 0
});
}

function removeReport(uint256 refSlot) external {
delete reports[refSlot];
}
Expand Down
113 changes: 113 additions & 0 deletions test/integration/second-opinion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { expect } from "chai";
import { ethers } from "hardhat";

import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";

import { SecondOpinionOracleMock } from "typechain-types";

import { ether, impersonate, ONE_GWEI } from "lib";
import { getProtocolContext, ProtocolContext } from "lib/protocol";
import { finalizeWithdrawalQueue, norEnsureOperators, report, sdvtEnsureOperators } from "lib/protocol/helpers";

import { bailOnFailure, Snapshot } from "test/suite";

const AMOUNT = ether("100");
const MAX_DEPOSIT = 150n;
const CURATED_MODULE_ID = 1n;
const INITIAL_REPORTED_BALANCE = ether("32") * 3n; // 32 ETH * 3 validators
const DIFF_AMOUNT = ether("10");

const ZERO_HASH = new Uint8Array(32).fill(0);

describe("Second opinion", () => {
let ctx: ProtocolContext;

let ethHolder: HardhatEthersSigner;
let stEthHolder: HardhatEthersSigner;

let snapshot: string;
let originalState: string;

let secondOpinion: SecondOpinionOracleMock;

before(async () => {
ctx = await getProtocolContext();

[stEthHolder, ethHolder] = await ethers.getSigners();

snapshot = await Snapshot.take();

const { lido, depositSecurityModule, oracleReportSanityChecker } = ctx.contracts;

await finalizeWithdrawalQueue(ctx, stEthHolder, ethHolder);

await norEnsureOperators(ctx, 3n, 5n);
if (ctx.flags.withSimpleDvtModule) {
await sdvtEnsureOperators(ctx, 3n, 5n);
}

const { chainId } = await ethers.provider.getNetwork();
// Sepolia-specific initialization
if (chainId === 11155111n) {
// Sepolia deposit contract address https://sepolia.etherscan.io/token/0x7f02c3e3c98b133055b8b348b2ac625669ed295d
const sepoliaDepositContractAddress = "0x7f02C3E3c98b133055B8B348B2Ac625669Ed295D";
const bepoliaWhaleHolder = "0xf97e180c050e5Ab072211Ad2C213Eb5AEE4DF134";
const BEPOLIA_TO_TRANSFER = 20;

const bepoliaToken = await ethers.getContractAt("ISepoliaDepositContract", sepoliaDepositContractAddress);
const bepiloaSigner = await ethers.getImpersonatedSigner(bepoliaWhaleHolder);

const adapterAddr = await ctx.contracts.stakingRouter.DEPOSIT_CONTRACT();
await bepoliaToken.connect(bepiloaSigner).transfer(adapterAddr, BEPOLIA_TO_TRANSFER);
}
const dsmSigner = await impersonate(depositSecurityModule.address, AMOUNT);
await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, CURATED_MODULE_ID, ZERO_HASH);

secondOpinion = await ethers.deployContract("SecondOpinionOracleMock", []);
const soAddress = await secondOpinion.getAddress();

const agentSigner = await ctx.getSigner("agent", AMOUNT);
await oracleReportSanityChecker
.connect(agentSigner)
.grantRole(await oracleReportSanityChecker.SECOND_OPINION_MANAGER_ROLE(), agentSigner.address);

await report(ctx, {
clDiff: INITIAL_REPORTED_BALANCE,
clAppearedValidators: 3n,
excludeVaultsBalances: true,
});

await oracleReportSanityChecker.connect(agentSigner).setSecondOpinionOracleAndCLBalanceUpperMargin(soAddress, 74n);
});

beforeEach(bailOnFailure);

beforeEach(async () => (originalState = await Snapshot.take()));

afterEach(async () => await Snapshot.restore(originalState));

after(async () => await Snapshot.restore(snapshot)); // Rollback to the initial state pre deployment

it("Should account correctly with no CL rebase", async () => {
const { hashConsensus, accountingOracle, oracleReportSanityChecker } = ctx.contracts;

// Report without second opinion is failing
await expect(report(ctx, { clDiff: -DIFF_AMOUNT, excludeVaultsBalances: true })).to.be.revertedWithCustomError(
oracleReportSanityChecker,
"NegativeRebaseFailedSecondOpinionReportIsNotReady",
);

// Provide a second opinion
const curFrame = await hashConsensus.getCurrentFrame();
const expectedBalance = (INITIAL_REPORTED_BALANCE - DIFF_AMOUNT) / ONE_GWEI;
await secondOpinion.addPlainReport(curFrame.reportProcessingDeadlineSlot, expectedBalance, 0n);

const lastProcessingRefSlotBefore = await accountingOracle.getLastProcessingRefSlot();
await report(ctx, { clDiff: -DIFF_AMOUNT, excludeVaultsBalances: true });
const lastProcessingRefSlotAfter = await accountingOracle.getLastProcessingRefSlot();
expect(lastProcessingRefSlotBefore).to.be.lessThan(
lastProcessingRefSlotAfter,
"LastProcessingRefSlot should be updated",
);
});
});
Loading