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

feat: Negative rebase Sepolia integration tests #862

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ const config: HardhatUserConfig = {
chainId: 11155111,
accounts: loadAccounts("sepolia"),
},
"sepolia-fork": {
url: process.env.SEPOLIA_RPC_URL || RPC_URL,
chainId: 11155111,
},
},
solidity: {
compilers: [
Expand Down
34 changes: 18 additions & 16 deletions lib/protocol/discover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,59 +64,61 @@ const getFoundationContracts = async (locator: LoadedContract<LidoLocator>, conf
(await batch({
accountingOracle: loadContract(
"AccountingOracle",
config.get("accountingOracle") || await locator.accountingOracle(),
config.get("accountingOracle") || (await locator.accountingOracle()),
),
depositSecurityModule: loadContract(
"DepositSecurityModule",
config.get("depositSecurityModule") || await locator.depositSecurityModule(),
config.get("depositSecurityModule") || (await locator.depositSecurityModule()),
),
elRewardsVault: loadContract(
"LidoExecutionLayerRewardsVault",
config.get("elRewardsVault") || await locator.elRewardsVault(),
config.get("elRewardsVault") || (await locator.elRewardsVault()),
),
legacyOracle: loadContract("LegacyOracle", config.get("legacyOracle") || await locator.legacyOracle()),
lido: loadContract("Lido", config.get("lido") || await locator.lido()),
legacyOracle: loadContract("LegacyOracle", config.get("legacyOracle") || (await locator.legacyOracle())),
lido: loadContract("Lido", config.get("lido") || (await locator.lido())),
oracleReportSanityChecker: loadContract(
"OracleReportSanityChecker",
config.get("oracleReportSanityChecker") || await locator.oracleReportSanityChecker(),
config.get("oracleReportSanityChecker") || (await locator.oracleReportSanityChecker()),
),
burner: loadContract("Burner", config.get("burner") || await locator.burner()),
stakingRouter: loadContract("StakingRouter", config.get("stakingRouter") || await locator.stakingRouter()),
burner: loadContract("Burner", config.get("burner") || (await locator.burner())),
stakingRouter: loadContract("StakingRouter", config.get("stakingRouter") || (await locator.stakingRouter())),
validatorsExitBusOracle: loadContract(
"ValidatorsExitBusOracle",
config.get("validatorsExitBusOracle") || await locator.validatorsExitBusOracle(),
config.get("validatorsExitBusOracle") || (await locator.validatorsExitBusOracle()),
),
withdrawalQueue: loadContract(
"WithdrawalQueueERC721",
config.get("withdrawalQueue") || await locator.withdrawalQueue(),
config.get("withdrawalQueue") || (await locator.withdrawalQueue()),
),
withdrawalVault: loadContract(
"WithdrawalVault",
config.get("withdrawalVault") || await locator.withdrawalVault(),
config.get("withdrawalVault") || (await locator.withdrawalVault()),
),
oracleDaemonConfig: loadContract(
"OracleDaemonConfig",
config.get("oracleDaemonConfig") || await locator.oracleDaemonConfig(),
config.get("oracleDaemonConfig") || (await locator.oracleDaemonConfig()),
),
})) as CoreContracts;

/**
* Load Aragon contracts required for protocol.
*/
const getAragonContracts = async (lido: LoadedContract<Lido>, config: ProtocolNetworkConfig) => {
const kernelAddress = config.get("kernel") || await lido.kernel();
const kernelAddress = config.get("kernel") || (await lido.kernel());
const kernel = await loadContract("Kernel", kernelAddress);
return (await batch({
kernel: new Promise((resolve) => resolve(kernel)), // Avoiding double loading
acl: loadContract("ACL", config.get("acl") || await kernel.acl()),
acl: loadContract("ACL", config.get("acl") || (await kernel.acl())),
})) as AragonContracts;
};

/**
* Load staking modules contracts registered in the staking router.
*/
const getStakingModules = async (stakingRouter: LoadedContract<StakingRouter>, config: ProtocolNetworkConfig) => {
const [nor, sdvt] = await stakingRouter.getStakingModules();
const [nor, initialSdvt] = await stakingRouter.getStakingModules();
// NOTE: Temporary workaround for missing staking modules in the staking router for Sepolia testnet.
const sdvt = initialSdvt ? initialSdvt : nor;
return (await batch({
nor: loadContract("NodeOperatorsRegistry", config.get("nor") || nor.stakingModuleAddress),
sdvt: loadContract("NodeOperatorsRegistry", config.get("sdvt") || sdvt.stakingModuleAddress),
Expand All @@ -127,7 +129,7 @@ const getStakingModules = async (stakingRouter: LoadedContract<StakingRouter>, c
* Load HashConsensus contract for accounting oracle.
*/
const getHashConsensus = async (accountingOracle: LoadedContract<AccountingOracle>, config: ProtocolNetworkConfig) => {
const hashConsensusAddress = config.get("hashConsensus") || await accountingOracle.getConsensusContract();
const hashConsensusAddress = config.get("hashConsensus") || (await accountingOracle.getConsensusContract());
return (await batch({
hashConsensus: loadContract("HashConsensus", hashConsensusAddress),
})) as HashConsensusContracts;
Expand Down
42 changes: 23 additions & 19 deletions lib/protocol/networks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ export class ProtocolNetworkConfig {
constructor(
public readonly env: Record<keyof ProtocolNetworkItems, string>,
public readonly defaults: Record<keyof ProtocolNetworkItems, string>,
) {
}
) {}

get(key: keyof ProtocolNetworkItems): string {
return process.env[this.env[key]] || this.defaults[key] || "";
Expand Down Expand Up @@ -54,32 +53,26 @@ const defaultEnv = {
} as ProtocolNetworkItems;

const getPrefixedEnv = (prefix: string, obj: Record<string, string>): Record<string, string> =>
Object.fromEntries(
Object.entries(obj).map(([key, value]) => [key, `${prefix}_${value}`]),
);
Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, `${prefix}_${value}`]));

const getDefaults = (obj: Record<string, string>): Record<string, string> =>
Object.fromEntries(
Object.entries(obj).map(([key]) => [key, ""]),
);
Object.fromEntries(Object.entries(obj).map(([key]) => [key, ""]));

export async function getNetworkConfig(network: string): Promise<ProtocolNetworkConfig> {
const defaults = getDefaults(defaultEnv) as Record<keyof ProtocolNetworkItems, string>;

switch (network) {
case "local":
const config = await parseLocalDeploymentJson();
return new ProtocolNetworkConfig(
getPrefixedEnv("LOCAL", defaultEnv),
{
...defaults,
locator: config["lidoLocator"].proxy.address,
agentAddress: config["app:aragon-agent"].proxy.address,
votingAddress: config["app:aragon-voting"].proxy.address,
// Overrides for local development
easyTrackAddress: config["app:aragon-agent"].proxy.address,
sdvt: config["app:node-operators-registry"].proxy.address,
});
return new ProtocolNetworkConfig(getPrefixedEnv("LOCAL", defaultEnv), {
...defaults,
locator: config["lidoLocator"].proxy.address,
agentAddress: config["app:aragon-agent"].proxy.address,
votingAddress: config["app:aragon-voting"].proxy.address,
// Overrides for local development
easyTrackAddress: config["app:aragon-agent"].proxy.address,
sdvt: config["app:node-operators-registry"].proxy.address,
});

case "mainnet-fork":
case "hardhat":
Expand All @@ -94,6 +87,17 @@ export async function getNetworkConfig(network: string): Promise<ProtocolNetwork
easyTrackAddress: "0xFE5986E06210aC1eCC1aDCafc0cc7f8D63B3F977",
});

case "sepolia-fork":
return new ProtocolNetworkConfig(getPrefixedEnv("SEPOLIA", defaultEnv), {
...defaults,
locator: "0x8f6254332f69557A72b0DA2D5F0Bc07d4CA991E7",
// https://docs.lido.fi/deployed-contracts/#dao-contracts
agentAddress: "0x32A0E5828B62AAb932362a4816ae03b860b65e83",
votingAddress: "0x39A0EbdEE54cB319f4F42141daaBDb6ba25D341A",
// https://docs.lido.fi/deployed-contracts/#easy-track
easyTrackAddress: "0xF0211b7660680B49De1A7E9f25C65660F0a13Fea",
});

default:
throw new Error(`Network ${network} is not supported`);
}
Expand Down
24 changes: 24 additions & 0 deletions test/0.8.9/ISepoliaDepositContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2024 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

/* See contracts/COMPILERS.md */
pragma solidity 0.8.9;

import "@openzeppelin/contracts-v4.4/token/ERC20/IERC20.sol";

interface IDepositContract {
event DepositEvent(bytes pubkey, bytes withdrawal_credentials, bytes amount, bytes signature, bytes index);

function deposit(
bytes calldata pubkey,
bytes calldata withdrawal_credentials,
bytes calldata signature,
bytes32 deposit_data_root
) external payable;

function get_deposit_root() external view returns (bytes32);

function get_deposit_count() external view returns (bytes memory);
}

interface ISepoliaDepositContract is IDepositContract, IERC20 {}
129 changes: 129 additions & 0 deletions test/integration/negative-rebase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { expect } from "chai";
import { ethers } from "hardhat";

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

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

import { Snapshot } from "test/suite";

describe("Negative rebase", () => {
let ctx: ProtocolContext;
let snapshot: string;
let ethHolder: HardhatEthersSigner;

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

[ethHolder] = await ethers.getSigners();
await setBalance(ethHolder.address, ether("1000000"));
const network = await ethers.provider.getNetwork();
console.log("network", network.name);
if (network.name == "sepolia" || network.name == "sepolia-fork") {
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 beaconStat = await ctx.contracts.lido.getBeaconStat();
if (beaconStat.beaconValidators == 0n) {
const MAX_DEPOSIT = 96n;
const CURATED_MODULE_ID = 1n;
const ZERO_HASH = new Uint8Array(32).fill(0);

const dsmSigner = await impersonate(ctx.contracts.depositSecurityModule.address, ether("100"));
await ctx.contracts.lido.connect(dsmSigner).deposit(MAX_DEPOSIT, CURATED_MODULE_ID, ZERO_HASH);
}
}

snapshot = await Snapshot.take();
});

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

const exitedValidatorsCount = async () => {
const ids = await ctx.contracts.stakingRouter.getStakingModuleIds();
let exited = 0n;
for (const id of ids) {
const module = await ctx.contracts.stakingRouter.getStakingModule(id);
exited += module["exitedValidatorsCount"];
}
return exited;
};

it("Should store correctly exited validators count", async () => {
const { locator, oracleReportSanityChecker } = ctx.contracts;

expect((await locator.oracleReportSanityChecker()) == oracleReportSanityChecker.address);

await report(ctx, {
clDiff: ether("96"),
skipWithdrawals: true,
clAppearedValidators: 3n,
});

const currentExited = await exitedValidatorsCount();
const reportExitedValidators = currentExited + 2n;
await report(ctx, {
clDiff: ether("0"),
skipWithdrawals: true,
clAppearedValidators: 0n,
stakingModuleIdsWithNewlyExitedValidators: [1n],
numExitedValidatorsByStakingModule: [reportExitedValidators],
});

const count = await oracleReportSanityChecker.getReportDataCount();
expect(count).to.be.greaterThanOrEqual(2);

const lastReportData = await oracleReportSanityChecker.reportData(count - 1n);
const beforeLastReportData = await oracleReportSanityChecker.reportData(count - 2n);

expect(lastReportData.totalExitedValidators).to.be.equal(reportExitedValidators);
expect(beforeLastReportData.totalExitedValidators).to.be.equal(currentExited);

// for (let i = count - 1n; i >= 0; --i) {
// const reportData = await oracleReportSanityChecker.reportData(i);
// console.log("reportData", i, reportData);
// }
});

it("Should store correctly many negative rebases", async () => {
const { locator, oracleReportSanityChecker } = ctx.contracts;

expect((await locator.oracleReportSanityChecker()) == oracleReportSanityChecker.address);

await report(ctx, {
clDiff: ether("96"),
skipWithdrawals: true,
clAppearedValidators: 3n,
});

const REPORTS_REPEATED = 56;
const SINGLE_REPORT_DECREASE = -1000000000n;
for (let i = 0; i < REPORTS_REPEATED; i++) {
await report(ctx, {
clDiff: SINGLE_REPORT_DECREASE * BigInt(i + 1),
skipWithdrawals: true,
});
}
const count = await oracleReportSanityChecker.getReportDataCount();
expect(count).to.be.greaterThanOrEqual(REPORTS_REPEATED + 1);

for (let i = count - 1n, j = REPORTS_REPEATED - 1; i >= 0 && j >= 0; --i, --j) {
const reportData = await oracleReportSanityChecker.reportData(i);
expect(reportData.negativeCLRebaseWei).to.be.equal(-1n * SINGLE_REPORT_DECREASE * BigInt(j + 1));
}
// for (let i = count - 1n; i >= 0; --i) {
// const reportData = await oracleReportSanityChecker.reportData(i);
// console.log("reportData", i, reportData);
// }
});
});
Loading