diff --git a/hardhat.config.ts b/hardhat.config.ts index 2db23fcc9..d7b6d8977 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -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: [ diff --git a/lib/protocol/discover.ts b/lib/protocol/discover.ts index ee99d8de6..079f03e62 100644 --- a/lib/protocol/discover.ts +++ b/lib/protocol/discover.ts @@ -64,39 +64,39 @@ const getFoundationContracts = async (locator: LoadedContract, 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; @@ -104,11 +104,11 @@ const getFoundationContracts = async (locator: LoadedContract, conf * Load Aragon contracts required for protocol. */ const getAragonContracts = async (lido: LoadedContract, 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; }; @@ -116,7 +116,9 @@ const getAragonContracts = async (lido: LoadedContract, config: ProtocolNe * Load staking modules contracts registered in the staking router. */ const getStakingModules = async (stakingRouter: LoadedContract, 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), @@ -127,7 +129,7 @@ const getStakingModules = async (stakingRouter: LoadedContract, c * Load HashConsensus contract for accounting oracle. */ const getHashConsensus = async (accountingOracle: LoadedContract, 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; diff --git a/lib/protocol/networks.ts b/lib/protocol/networks.ts index 4ba3a5a3f..7bb775aab 100644 --- a/lib/protocol/networks.ts +++ b/lib/protocol/networks.ts @@ -16,8 +16,7 @@ export class ProtocolNetworkConfig { constructor( public readonly env: Record, public readonly defaults: Record, - ) { - } + ) {} get(key: keyof ProtocolNetworkItems): string { return process.env[this.env[key]] || this.defaults[key] || ""; @@ -54,14 +53,10 @@ const defaultEnv = { } as ProtocolNetworkItems; const getPrefixedEnv = (prefix: string, obj: Record): Record => - 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): Record => - Object.fromEntries( - Object.entries(obj).map(([key]) => [key, ""]), - ); + Object.fromEntries(Object.entries(obj).map(([key]) => [key, ""])); export async function getNetworkConfig(network: string): Promise { const defaults = getDefaults(defaultEnv) as Record; @@ -69,17 +64,15 @@ export async function getNetworkConfig(network: string): Promise +// 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 {} diff --git a/test/integration/negative-rebase.ts b/test/integration/negative-rebase.ts new file mode 100644 index 000000000..9f4b49d52 --- /dev/null +++ b/test/integration/negative-rebase.ts @@ -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); + // } + }); +});