Skip to content

Commit

Permalink
test: fix sanity checker issue
Browse files Browse the repository at this point in the history
  • Loading branch information
tamtamchik committed Nov 26, 2024
1 parent 599806a commit 6f8c01c
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 49 deletions.
23 changes: 23 additions & 0 deletions test/0.8.9/contracts/Accounting__MockForSanityChecker.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: UNLICENSED
// for testing purposes only

pragma solidity 0.8.9;

import { ReportValues } from "contracts/0.8.9/oracle/AccountingOracle.sol";
import { IReportReceiver } from "contracts/0.8.9/oracle/AccountingOracle.sol";

contract Accounting__MockForSanityChecker is IReportReceiver {
struct HandleOracleReportCallData {
ReportValues arg;
uint256 callCount;
}

HandleOracleReportCallData public lastCall__handleOracleReport;

function handleOracleReport(ReportValues memory values) external override {
lastCall__handleOracleReport = HandleOracleReportCallData(
values,
++lastCall__handleOracleReport.callCount
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
StakingRouter__MockForSanityChecker,
} from "typechain-types";

import { ether, getCurrentBlockTimestamp } from "lib";
import { ether, getCurrentBlockTimestamp, impersonate } from "lib";

import { Snapshot } from "test/suite";

Expand All @@ -24,12 +24,12 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => {
let accountingOracle: AccountingOracle__MockForSanityChecker;
let stakingRouter: StakingRouter__MockForSanityChecker;
let deployer: HardhatEthersSigner;
let accountingSigner: HardhatEthersSigner;

const defaultLimitsList = {
exitedValidatorsPerDayLimit: 50n,
appearedValidatorsPerDayLimit: 75n,
annualBalanceIncreaseBPLimit: 10_00n, // 10%
simulatedShareRateDeviationBPLimit: 2_50n, // 2.5%
maxValidatorExitRequestsPerReport: 2000n,
maxItemsPerExtraDataTransaction: 15n,
maxNodeOperatorsPerExtraDataItem: 16n,
Expand Down Expand Up @@ -60,6 +60,8 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => {
const sanityCheckerAddress = deployer.address;

const burner = await ethers.deployContract("Burner__MockForSanityChecker", []);
const accounting = await ethers.deployContract("Accounting__MockForSanityChecker", []);

accountingOracle = await ethers.deployContract("AccountingOracle__MockForSanityChecker", [
deployer.address,
12,
Expand All @@ -83,22 +85,38 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => {
withdrawalVault: deployer.address,
postTokenRebaseReceiver: deployer.address,
oracleDaemonConfig: deployer.address,
accounting: await accounting.getAddress(),
},
]);

checker = await ethers.deployContract("OracleReportSanityChecker", [
await locator.getAddress(),
deployer.address,
Object.values(defaultLimitsList),
]);
const locatorAddress = await locator.getAddress();

checker = await ethers
.getContractFactory("OracleReportSanityChecker")
.then((f) => f.deploy(locatorAddress, deployer.address, defaultLimitsList));

accountingSigner = await impersonate(await accounting.getAddress(), ether("1"));
});

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

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

context("OracleReportSanityChecker checkAccountingOracleReport authorization", () => {
it("should allow calling from Accounting address", async () => {
await checker.connect(accountingSigner).checkAccountingOracleReport(0, 110 * 1e9, 109.99 * 1e9, 0, 0, 0, 10, 10);
});

it("should not allow calling from non-Accounting address", async () => {
const [, otherClient] = await ethers.getSigners();
await expect(
checker.connect(otherClient).checkAccountingOracleReport(0, 110 * 1e9, 110.01 * 1e9, 0, 0, 0, 10, 10),
).to.be.revertedWithCustomError(checker, "CalledNotFromAccounting");
});
});

context("OracleReportSanityChecker is functional", () => {
it(`base parameters are correct`, async () => {
it("base parameters are correct", async () => {
const locateChecker = await locator.oracleReportSanityChecker();
expect(locateChecker).to.equal(deployer.address);

Expand Down Expand Up @@ -137,7 +155,7 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => {
expect(structSizeInBits).to.lessThanOrEqual(256);
});

it(`second opinion can be changed or removed`, async () => {
it("second opinion can be changed or removed", async () => {
expect(await checker.secondOpinionOracle()).to.equal(ZeroAddress);

const clOraclesRole = await checker.SECOND_OPINION_MANAGER_ROLE();
Expand All @@ -163,7 +181,7 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => {
]);
}

it(`sums negative rebases for a few days`, async () => {
it("sums negative rebases for a few days", async () => {
const reportChecker = await newChecker();
const timestamp = await getCurrentBlockTimestamp();
expect(await reportChecker.sumNegativeRebasesNotOlderThan(timestamp - 18n * SLOTS_PER_DAY)).to.equal(0);
Expand All @@ -172,7 +190,7 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => {
expect(await reportChecker.sumNegativeRebasesNotOlderThan(timestamp - 18n * SLOTS_PER_DAY)).to.equal(250);
});

it(`sums negative rebases for 18 days`, async () => {
it("sums negative rebases for 18 days", async () => {
const reportChecker = await newChecker();
const timestamp = await getCurrentBlockTimestamp();

Expand All @@ -187,7 +205,7 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => {
expect(expectedSum).to.equal(100 + 150 + 5 + 10);
});

it(`returns exited validators count`, async () => {
it("returns exited validators count", async () => {
const reportChecker = await newChecker();
const timestamp = await getCurrentBlockTimestamp();

Expand All @@ -203,7 +221,7 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => {
expect(await reportChecker.exitedValidatorsAtTimestamp(timestamp - 1n * SLOTS_PER_DAY)).to.equal(15);
});

it(`returns exited validators count for missed or non-existent report`, async () => {
it("returns exited validators count for missed or non-existent report", async () => {
const reportChecker = await newChecker();
const timestamp = await getCurrentBlockTimestamp();
await reportChecker.addReportData(timestamp - 19n * SLOTS_PER_DAY, 10, 100);
Expand All @@ -227,41 +245,47 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => {
});

context("OracleReportSanityChecker additional balance decrease check", () => {
it(`works for IncorrectCLBalanceDecrease`, async () => {
await expect(checker.checkAccountingOracleReport(0, ether("320"), ether("300"), 0, 0, 0, 10, 10))
it("works for IncorrectCLBalanceDecrease", async () => {
await expect(
checker.connect(accountingSigner).checkAccountingOracleReport(0, ether("320"), ether("300"), 0, 0, 0, 10, 10),
)
.to.be.revertedWithCustomError(checker, "IncorrectCLBalanceDecrease")
.withArgs(20n * ether("1"), 10n * ether("1") + 10n * ether("0.101"));
});

it(`works as accamulation for IncorrectCLBalanceDecrease`, async () => {
it("works as accamulation for IncorrectCLBalanceDecrease", async () => {
const genesisTime = await accountingOracle.GENESIS_TIME();
const timestamp = await getCurrentBlockTimestamp();
const refSlot = (timestamp - genesisTime) / 12n;
const prevRefSlot = refSlot - SLOTS_PER_DAY;

await accountingOracle.setLastProcessingRefSlot(prevRefSlot);
await checker.checkAccountingOracleReport(0, ether("320"), ether("310"), 0, 0, 0, 10, 10);
await checker
.connect(accountingSigner)
.checkAccountingOracleReport(0, ether("320"), ether("310"), 0, 0, 0, 10, 10);

await accountingOracle.setLastProcessingRefSlot(refSlot);
await expect(checker.checkAccountingOracleReport(0, ether("310"), ether("300"), 0, 0, 0, 10, 10))
await expect(
checker.connect(accountingSigner).checkAccountingOracleReport(0, ether("310"), ether("300"), 0, 0, 0, 10, 10),
)
.to.be.revertedWithCustomError(checker, "IncorrectCLBalanceDecrease")
.withArgs(20n * ether("1"), 10n * ether("1") + 10n * ether("0.101"));
});

it(`works for happy path and report is not ready`, async () => {
it("works for happy path and report is not ready", async () => {
const genesisTime = await accountingOracle.GENESIS_TIME();
const timestamp = await getCurrentBlockTimestamp();
const refSlot = (timestamp - genesisTime) / 12n;

await accountingOracle.setLastProcessingRefSlot(refSlot);

// Expect to pass through
await checker.checkAccountingOracleReport(0, 96 * 1e9, 96 * 1e9, 0, 0, 0, 10, 10);
await checker.connect(accountingSigner).checkAccountingOracleReport(0, 96 * 1e9, 96 * 1e9, 0, 0, 0, 10, 10);

const secondOpinionOracle = await deploySecondOpinionOracle();

await expect(
checker.checkAccountingOracleReport(0, ether("330"), ether("300"), 0, 0, 0, 10, 10),
checker.connect(accountingSigner).checkAccountingOracleReport(0, ether("330"), ether("300"), 0, 0, 0, 10, 10),
).to.be.revertedWithCustomError(checker, "NegativeRebaseFailedSecondOpinionReportIsNotReady");

await secondOpinionOracle.addReport(refSlot, {
Expand All @@ -271,7 +295,9 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => {
numValidators: 0,
exitedValidators: 0,
});
await expect(checker.checkAccountingOracleReport(0, ether("330"), ether("300"), 0, 0, 0, 10, 10))
await expect(
checker.connect(accountingSigner).checkAccountingOracleReport(0, ether("330"), ether("300"), 0, 0, 0, 10, 10),
)
.to.emit(checker, "NegativeCLRebaseConfirmed")
.withArgs(refSlot, ether("300"), ether("0"));
});
Expand All @@ -288,28 +314,38 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => {

await stakingRouter.mock__addStakingModuleExitedValidators(1, 1);
await accountingOracle.setLastProcessingRefSlot(refSlot55);
await checker.checkAccountingOracleReport(0, ether("320"), ether("320"), 0, 0, 0, 10, 10);
await checker
.connect(accountingSigner)
.checkAccountingOracleReport(0, ether("320"), ether("320"), 0, 0, 0, 10, 10);

await stakingRouter.mock__removeStakingModule(1);
await stakingRouter.mock__addStakingModuleExitedValidators(1, 2);
await accountingOracle.setLastProcessingRefSlot(refSlot54);
await checker.checkAccountingOracleReport(0, ether("320"), ether("320"), 0, 0, 0, 10, 10);
await checker
.connect(accountingSigner)
.checkAccountingOracleReport(0, ether("320"), ether("320"), 0, 0, 0, 10, 10);

await stakingRouter.mock__removeStakingModule(1);
await stakingRouter.mock__addStakingModuleExitedValidators(1, 3);
await accountingOracle.setLastProcessingRefSlot(refSlot18);
await checker.checkAccountingOracleReport(0, ether("320"), ether("320"), 0, 0, 0, 10, 10);
await checker
.connect(accountingSigner)
.checkAccountingOracleReport(0, ether("320"), ether("320"), 0, 0, 0, 10, 10);

await accountingOracle.setLastProcessingRefSlot(refSlot17);
await checker.checkAccountingOracleReport(0, ether("320"), ether("315"), 0, 0, 0, 10, 10);
await checker
.connect(accountingSigner)
.checkAccountingOracleReport(0, ether("320"), ether("315"), 0, 0, 0, 10, 10);

await accountingOracle.setLastProcessingRefSlot(refSlot);
await expect(checker.checkAccountingOracleReport(0, ether("315"), ether("300"), 0, 0, 0, 10, 10))
await expect(
checker.connect(accountingSigner).checkAccountingOracleReport(0, ether("315"), ether("300"), 0, 0, 0, 10, 10),
)
.to.be.revertedWithCustomError(checker, "IncorrectCLBalanceDecrease")
.withArgs(20n * ether("1"), 7n * ether("1") + 8n * ether("0.101"));
});

it(`works for reports close together`, async () => {
it("works for reports close together", async () => {
const genesisTime = await accountingOracle.GENESIS_TIME();
const timestamp = await getCurrentBlockTimestamp();
const refSlot = (timestamp - genesisTime) / 12n;
Expand All @@ -327,7 +363,9 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => {
exitedValidators: 0,
});

await expect(checker.checkAccountingOracleReport(0, ether("330"), ether("299"), 0, 0, 0, 10, 10))
await expect(
checker.connect(accountingSigner).checkAccountingOracleReport(0, ether("330"), ether("299"), 0, 0, 0, 10, 10),
)
.to.be.revertedWithCustomError(checker, "NegativeRebaseFailedCLBalanceMismatch")
.withArgs(ether("299"), ether("302"), anyValue);

Expand All @@ -339,7 +377,10 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => {
numValidators: 0,
exitedValidators: 0,
});
await expect(checker.checkAccountingOracleReport(0, ether("330"), ether("299"), 0, 0, 0, 10, 10))

await expect(
checker.connect(accountingSigner).checkAccountingOracleReport(0, ether("330"), ether("299"), 0, 0, 0, 10, 10),
)
.to.emit(checker, "NegativeCLRebaseConfirmed")
.withArgs(refSlot, ether("299"), ether("0"));

Expand All @@ -351,12 +392,15 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => {
numValidators: 0,
exitedValidators: 0,
});
await expect(checker.checkAccountingOracleReport(0, 110 * 1e9, 100.01 * 1e9, 0, 0, 0, 10, 10))

await expect(
checker.connect(accountingSigner).checkAccountingOracleReport(0, 110 * 1e9, 100.01 * 1e9, 0, 0, 0, 10, 10),
)
.to.be.revertedWithCustomError(checker, "NegativeRebaseFailedCLBalanceMismatch")
.withArgs(100.01 * 1e9, 100 * 1e9, anyValue);
});

it(`works for reports with incorrect withdrawal vault balance`, async () => {
it("works for reports with incorrect withdrawal vault balance", async () => {
const genesisTime = await accountingOracle.GENESIS_TIME();
const timestamp = await getCurrentBlockTimestamp();
const refSlot = (timestamp - genesisTime) / 12n;
Expand All @@ -373,7 +417,12 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => {
numValidators: 0,
exitedValidators: 0,
});
await expect(checker.checkAccountingOracleReport(0, ether("330"), ether("299"), ether("1"), 0, 0, 10, 10))

await expect(
checker
.connect(accountingSigner)
.checkAccountingOracleReport(0, ether("330"), ether("299"), ether("1"), 0, 0, 10, 10),
)
.to.emit(checker, "NegativeCLRebaseConfirmed")
.withArgs(refSlot, ether("299"), ether("1"));

Expand All @@ -385,14 +434,19 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => {
numValidators: 0,
exitedValidators: 0,
});
await expect(checker.checkAccountingOracleReport(0, ether("330"), ether("299"), ether("1"), 0, 0, 10, 10))

await expect(
checker
.connect(accountingSigner)
.checkAccountingOracleReport(0, ether("330"), ether("299"), ether("1"), 0, 0, 10, 10),
)
.to.be.revertedWithCustomError(checker, "NegativeRebaseFailedWithdrawalVaultBalanceMismatch")
.withArgs(ether("1"), 0);
});
});

context("OracleReportSanityChecker roles", () => {
it(`CL Oracle related functions require INITIAL_SLASHING_AND_PENALTIES_MANAGER_ROLE`, async () => {
it("CL Oracle related functions require INITIAL_SLASHING_AND_PENALTIES_MANAGER_ROLE", async () => {
const role = await checker.INITIAL_SLASHING_AND_PENALTIES_MANAGER_ROLE();

await expect(checker.setInitialSlashingAndPenaltiesAmount(0, 0)).to.be.revertedWithOZAccessControlError(
Expand All @@ -404,7 +458,7 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => {
await expect(checker.setInitialSlashingAndPenaltiesAmount(1000, 101)).to.not.be.reverted;
});

it(`CL Oracle related functions require SECOND_OPINION_MANAGER_ROLE`, async () => {
it("CL Oracle related functions require SECOND_OPINION_MANAGER_ROLE", async () => {
const clOraclesRole = await checker.SECOND_OPINION_MANAGER_ROLE();

await expect(
Expand All @@ -415,17 +469,4 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => {
await expect(checker.setSecondOpinionOracleAndCLBalanceUpperMargin(ZeroAddress, 74)).to.not.be.reverted;
});
});

context("OracleReportSanityChecker checkAccountingOracleReport authorization", () => {
it("should allow calling from Lido address", async () => {
await checker.checkAccountingOracleReport(0, 110 * 1e9, 109.99 * 1e9, 0, 0, 0, 10, 10);
});

it("should not allow calling from non-Lido address", async () => {
const [, otherClient] = await ethers.getSigners();
await expect(
checker.connect(otherClient).checkAccountingOracleReport(0, 110 * 1e9, 110.01 * 1e9, 0, 0, 0, 10, 10),
).to.be.revertedWithCustomError(checker, "CalledNotFromLido");
});
});
});

0 comments on commit 6f8c01c

Please sign in to comment.