From 2250aa60507f2fc5148ce20ed5392990e1296811 Mon Sep 17 00:00:00 2001 From: VP Date: Mon, 30 Dec 2024 16:58:16 +0100 Subject: [PATCH 01/27] test: add accounting and sanity checker deployment --- test/0.4.24/lido/lido.accounting.test.ts | 959 ++++++++++++----------- test/deploy/dao.ts | 8 +- 2 files changed, 499 insertions(+), 468 deletions(-) diff --git a/test/0.4.24/lido/lido.accounting.test.ts b/test/0.4.24/lido/lido.accounting.test.ts index 719b7d97b..b5c76aabc 100644 --- a/test/0.4.24/lido/lido.accounting.test.ts +++ b/test/0.4.24/lido/lido.accounting.test.ts @@ -3,8 +3,10 @@ import { BigNumberish } from "ethers"; import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import { getStorageAt } from "@nomicfoundation/hardhat-network-helpers"; import { + Accounting, ACL, Lido, LidoExecutionLayerRewardsVault__MockForLidoAccounting, @@ -14,35 +16,42 @@ import { WithdrawalVault__MockForLidoAccounting, WithdrawalVault__MockForLidoAccounting__factory, } from "typechain-types"; +import { ReportValuesStruct } from "typechain-types/contracts/0.8.25/Accounting"; +import { OracleReportSanityChecker__MockForLidoHandleOracleReport__factory } from "typechain-types/factories/test/0.4.24/contracts/OracleReportSanityChecker__MockForLidoHandleOracleReport__factory"; +import { OracleReportSanityChecker__MockForLidoHandleOracleReport } from "typechain-types/test/0.4.24/contracts/OracleReportSanityChecker__MockForLidoHandleOracleReport"; -import { deployLidoDao } from "test/deploy"; +import { streccak } from "lib"; + +import { deployLidoDao, updateLidoLocatorImplementation } from "test/deploy"; describe("Lido:accounting", () => { let deployer: HardhatEthersSigner; - let accounting: HardhatEthersSigner; // let stethWhale: HardhatEthersSigner; let stranger: HardhatEthersSigner; let withdrawalQueue: HardhatEthersSigner; let lido: Lido; let acl: ACL; + let accounting: Accounting; // let locator: LidoLocator; let elRewardsVault: LidoExecutionLayerRewardsVault__MockForLidoAccounting; let withdrawalVault: WithdrawalVault__MockForLidoAccounting; let stakingRouter: StakingRouter__MockForLidoAccounting; + let oracleReportSanityChecker: OracleReportSanityChecker__MockForLidoHandleOracleReport; beforeEach(async () => { // [deployer, accounting, stethWhale, stranger, withdrawalQueue] = await ethers.getSigners(); - [deployer, accounting, stranger, withdrawalQueue] = await ethers.getSigners(); + [deployer, stranger, withdrawalQueue] = await ethers.getSigners(); - [elRewardsVault, stakingRouter, withdrawalVault] = await Promise.all([ + [elRewardsVault, stakingRouter, withdrawalVault, oracleReportSanityChecker] = await Promise.all([ new LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory(deployer).deploy(), new StakingRouter__MockForLidoAccounting__factory(deployer).deploy(), new WithdrawalVault__MockForLidoAccounting__factory(deployer).deploy(), + new OracleReportSanityChecker__MockForLidoHandleOracleReport__factory(deployer).deploy(), ]); - ({ lido, acl } = await deployLidoDao({ + ({ lido, acl, accounting } = await deployLidoDao({ rootAccount: deployer, initialized: true, locatorConfig: { @@ -50,7 +59,7 @@ describe("Lido:accounting", () => { elRewardsVault, withdrawalVault, stakingRouter, - accounting, + oracleReportSanityChecker, }, })); @@ -60,8 +69,6 @@ describe("Lido:accounting", () => { await acl.createPermission(deployer, lido, await lido.PAUSE_ROLE(), deployer); await acl.createPermission(deployer, lido, await lido.UNSAFE_CHANGE_DEPOSITED_VALIDATORS_ROLE(), deployer); await lido.resume(); - - lido = lido.connect(accounting); }); context("processClStateUpdate", async () => { @@ -75,6 +82,7 @@ describe("Lido:accounting", () => { }); it("Updates beacon stats", async () => { + await updateLidoLocatorImplementation(await lido.getLidoLocator(), { accounting: deployer }); await expect( lido.processClStateUpdate( ...args({ @@ -157,463 +165,482 @@ describe("Lido:accounting", () => { }); // TODO: [@tamtamchik] restore tests - context.skip("handleOracleReport", () => { - // it("Update CL validators count if reported more", async () => { - // let depositedValidators = 100n; - // await lido.connect(deployer).unsafeChangeDepositedValidators(depositedValidators); - // - // // first report, 100 validators - // await lido.handleOracleReport( - // ...report({ - // clValidators: depositedValidators, - // }), - // ); - // - // const slot = streccak("lido.Lido.beaconValidators"); - // const lidoAddress = await lido.getAddress(); - // - // let clValidatorsPosition = await getStorageAt(lidoAddress, slot); - // expect(clValidatorsPosition).to.equal(depositedValidators); - // - // depositedValidators = 101n; - // await lido.connect(deployer).unsafeChangeDepositedValidators(depositedValidators); - // - // // second report, 101 validators - // await lido.handleOracleReport( - // ...report({ - // clValidators: depositedValidators, - // }), - // ); - // - // clValidatorsPosition = await getStorageAt(lidoAddress, slot); - // expect(clValidatorsPosition).to.equal(depositedValidators); - // }); - // - // it("Reverts if the `checkAccountingOracleReport` sanity check fails", async () => { - // await oracleReportSanityChecker.mock__checkAccountingOracleReportReverts(true); - // - // await expect(lido.handleOracleReport(...report())).to.be.reverted; - // }); - // - // it("Reverts if the `checkWithdrawalQueueOracleReport` sanity check fails", async () => { - // await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ).to.be.reverted; - // }); - // - // it("Does not revert if the `checkWithdrawalQueueOracleReport` sanity check fails but no withdrawal batches were reported", async () => { - // await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); - // await withdrawalQueue.mock__isPaused(true); - // - // await expect(lido.handleOracleReport(...report())).not.to.be.reverted; - // }); - // - // it("Does not revert if the `checkWithdrawalQueueOracleReport` sanity check fails but `withdrawalQueue` is paused", async () => { - // await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); - // await withdrawalQueue.mock__isPaused(true); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ).not.to.be.reverted; - // }); - // - // it("Does not emit `StETHBurnRequested` if there are no shares to burn", async () => { - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ).not.to.emit(burner, "StETHBurnRequested"); - // }); - // - // it("Emits `StETHBurnRequested` if there are shares to burn", async () => { - // const sharesToBurn = 1n; - // const isCover = false; - // const steth = 1n * 2n; // imitating 1:2 rate, see Burner `mock__prefinalizeReturn` - // - // await withdrawalQueue.mock__prefinalizeReturn(0n, sharesToBurn); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ) - // .to.emit(burner, "StETHBurnRequested") - // .withArgs(isCover, await lido.getAddress(), steth, sharesToBurn); - // }); - // - // it("Withdraws ether from `ElRewardsVault` if EL rewards are greater than 0 as returned from `smoothenTokenRebase`", async () => { - // const withdrawals = 0n; - // const elRewards = 1n; - // const simulatedSharesToBurn = 0n; - // const sharesToBurn = 0n; - // - // await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn( - // withdrawals, - // elRewards, - // simulatedSharesToBurn, - // sharesToBurn, - // ); - // - // // `Mock__RewardsWithdrawn` event is only emitted on the mock to verify - // // that `ElRewardsVault.withdrawRewards` was actually called - // await expect(lido.handleOracleReport(...report())).to.emit(elRewardsVault, "Mock__RewardsWithdrawn"); - // }); - // - // it("Withdraws ether from `WithdrawalVault` if withdrawals are greater than 0 as returned from `smoothenTokenRebase`", async () => { - // const withdrawals = 1n; - // const elRewards = 0n; - // const simulatedSharesToBurn = 0n; - // const sharesToBurn = 0n; - // - // await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn( - // withdrawals, - // elRewards, - // simulatedSharesToBurn, - // sharesToBurn, - // ); - // - // // `Mock__WithdrawalsWithdrawn` event is only emitted on the mock to verify - // // that `WithdrawalVault.withdrawWithdrawals` was actually called - // await expect(lido.handleOracleReport(...report())).to.emit(withdrawalVault, "Mock__WithdrawalsWithdrawn"); - // }); - // - // it("Finalizes withdrawals if there is ether to lock on `WithdrawalQueue` as returned from `prefinalize`", async () => { - // const ethToLock = ether("10.0"); - // await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); - // // top up buffer via submit - // await lido.submit(ZeroAddress, { value: ethToLock }); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n, 2n], - // }), - // ), - // ).to.emit(withdrawalQueue, "WithdrawalsFinalized"); - // }); - // - // it("Updates buffered ether", async () => { - // const initialBufferedEther = await lido.getBufferedEther(); - // const ethToLock = 1n; - // - // // assert that the buffer has enough eth to lock for withdrawals - // // should have some eth from the initial 0xdead holder - // expect(initialBufferedEther).greaterThanOrEqual(ethToLock); - // await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ).to.not.be.reverted; - // - // expect(await lido.getBufferedEther()).to.equal(initialBufferedEther - ethToLock); - // }); - // - // it("Emits an `ETHDistributed` event", async () => { - // const reportTimestamp = await getNextBlockTimestamp(); - // const preClBalance = 0n; - // const clBalance = 1n; - // const withdrawals = 0n; - // const elRewards = 0n; - // const bufferedEther = await lido.getBufferedEther(); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // reportTimestamp: reportTimestamp, - // clBalance, - // }), - // ), - // ) - // .to.emit(lido, "ETHDistributed") - // .withArgs(reportTimestamp, preClBalance, clBalance, withdrawals, elRewards, bufferedEther); - // }); - // - // it("Burns shares if there are shares to burn as returned from `smoothenTokenRebaseReturn`", async () => { - // const sharesRequestedToBurn = 1n; - // - // await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn(0n, 0n, 0n, sharesRequestedToBurn); - // - // // set up steth whale, in case we need to send steth to other accounts - // await setBalance(stethWhale.address, ether("101.0")); - // await lido.connect(stethWhale).submit(ZeroAddress, { value: ether("100.0") }); - // // top up Burner with steth to burn - // await lido.connect(stethWhale).transferShares(burner, sharesRequestedToBurn); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // sharesRequestedToBurn, - // }), - // ), - // ) - // .to.emit(burner, "Mock__CommitSharesToBurnWasCalled") - // .and.to.emit(lido, "SharesBurnt") - // .withArgs(await burner.getAddress(), sharesRequestedToBurn, sharesRequestedToBurn, sharesRequestedToBurn); - // }); - // - // it("Reverts if the number of reward recipients does not match the number of module fees as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { - // // one recipient - // const recipients = [certainAddress("lido:handleOracleReport:single-recipient")]; - // const modulesIds = [1n, 2n]; - // // but two module fees - // const moduleFees = [500n, 500n]; - // const totalFee = 1000; - // const precisionPoints = 10n ** 20n; - // - // await stakingRouter.mock__getStakingRewardsDistribution( - // recipients, - // modulesIds, - // moduleFees, - // totalFee, - // precisionPoints, - // ); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // clBalance: 1n, // made 1 wei of profit, trigers reward processing - // }), - // ), - // ).to.be.revertedWith("WRONG_RECIPIENTS_INPUT"); - // }); - // - // it("Reverts if the number of module ids does not match the number of module fees as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { - // const recipients = [ - // certainAddress("lido:handleOracleReport:recipient1"), - // certainAddress("lido:handleOracleReport:recipient2"), - // ]; - // // one module id - // const modulesIds = [1n]; - // // but two module fees - // const moduleFees = [500n, 500n]; - // const totalFee = 1000; - // const precisionPoints = 10n ** 20n; - // - // await stakingRouter.mock__getStakingRewardsDistribution( - // recipients, - // modulesIds, - // moduleFees, - // totalFee, - // precisionPoints, - // ); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // clBalance: 1n, // made 1 wei of profit, trigers reward processing - // }), - // ), - // ).to.be.revertedWith("WRONG_MODULE_IDS_INPUT"); - // }); - // - // it("Does not mint and transfer any shares if the total fee is zero as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { - // // single staking module - // const recipients = [certainAddress("lido:handleOracleReport:recipient")]; - // const modulesIds = [1n]; - // const moduleFees = [500n]; - // // fee is 0 - // const totalFee = 0; - // const precisionPoints = 10n ** 20n; - // - // await stakingRouter.mock__getStakingRewardsDistribution( - // recipients, - // modulesIds, - // moduleFees, - // totalFee, - // precisionPoints, - // ); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // clBalance: 1n, - // }), - // ), - // ) - // .not.to.emit(lido, "Transfer") - // .and.not.to.emit(lido, "TransferShares") - // .and.not.to.emit(stakingRouter, "Mock__MintedRewardsReported"); - // }); - // - // it("Mints shares to itself and then transfers them to recipients if there are fees to distribute as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { - // // initially, before any rebases, one share costs one steth - // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); - // // thus, the total supply of steth should equal the total number of shares - // expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); - // - // // mock a single staking module with 5% fee with the total protocol fee of 10% - // const stakingModule = { - // address: certainAddress("lido:handleOracleReport:staking-module"), - // id: 1n, - // fee: 5n * 10n ** 18n, // 5% - // }; - // - // const totalFee = 10n * 10n ** 18n; // 10% - // const precisionPoints = 100n * 10n ** 18n; // 100% - // - // await stakingRouter.mock__getStakingRewardsDistribution( - // [stakingModule.address], - // [stakingModule.id], - // [stakingModule.fee], - // totalFee, - // precisionPoints, - // ); - // - // const clBalance = ether("1.0"); - // - // const expectedSharesToMint = - // (clBalance * totalFee * (await lido.getTotalShares())) / - // (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); - // - // const expectedModuleRewardInShares = expectedSharesToMint / (totalFee / stakingModule.fee); - // const expectedTreasuryCutInShares = expectedSharesToMint - expectedModuleRewardInShares; - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // clBalance: ether("1.0"), // 1 ether of profit - // }), - // ), - // ) - // .to.emit(lido, "TransferShares") - // .withArgs(ZeroAddress, stakingModule.address, expectedModuleRewardInShares) - // .and.to.emit(lido, "TransferShares") - // .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) - // .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); - // - // expect(await lido.balanceOf(stakingModule.address)).to.equal( - // await lido.getPooledEthByShares(expectedModuleRewardInShares), - // ); - // - // expect(await lido.balanceOf(await lido.getTreasury())).to.equal( - // await lido.getPooledEthByShares(expectedTreasuryCutInShares), - // ); - // - // // now one share should cost 1.9 steth (10% was distributed as rewards) - // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); - // }); - // - // it("Transfers all new shares to treasury if the module fee is zero as returned `StakingRouter.getStakingRewardsDistribution`", async () => { - // // initially, before any rebases, one share costs one steth - // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); - // // thus, the total supply of steth should equal the total number of shares - // expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); - // - // // mock a single staking module with 0% fee with the total protocol fee of 10% - // const stakingModule = { - // address: certainAddress("lido:handleOracleReport:staking-module"), - // id: 1n, - // fee: 0n, - // }; - // - // const totalFee = 10n * 10n ** 18n; // 10% - // const precisionPoints = 100n * 10n ** 18n; // 100% - // - // await stakingRouter.mock__getStakingRewardsDistribution( - // [stakingModule.address], - // [stakingModule.id], - // [stakingModule.fee], - // totalFee, - // precisionPoints, - // ); - // - // const clBalance = ether("1.0"); - // - // const expectedSharesToMint = - // (clBalance * totalFee * (await lido.getTotalShares())) / - // (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); - // - // const expectedModuleRewardInShares = 0n; - // const expectedTreasuryCutInShares = expectedSharesToMint; - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // clBalance: ether("1.0"), // 1 ether of profit - // }), - // ), - // ) - // .and.to.emit(lido, "TransferShares") - // .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) - // .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); - // - // expect(await lido.balanceOf(stakingModule.address)).to.equal( - // await lido.getPooledEthByShares(expectedModuleRewardInShares), - // ); - // - // expect(await lido.balanceOf(await lido.getTreasury())).to.equal( - // await lido.getPooledEthByShares(expectedTreasuryCutInShares), - // ); - // - // // now one share should cost 1.9 steth (10% was distributed as rewards) - // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); - // }); - // - // it("Relays the report data to `PostTokenRebaseReceiver`", async () => { - // await expect(lido.handleOracleReport(...report())).to.emit( - // postTokenRebaseReceiver, - // "Mock__PostTokenRebaseHandled", - // ); - // }); - // - // it("Does not relay the report data to `PostTokenRebaseReceiver` if the locator returns zero address", async () => { - // const lidoLocatorAddress = await lido.getLidoLocator(); - // - // // Change the locator implementation to support zero address - // await updateLidoLocatorImplementation(lidoLocatorAddress, {}, "LidoLocator__MutableMock", deployer); - // const locatorMutable = await ethers.getContractAt("LidoLocator__MutableMock", lidoLocatorAddress, deployer); - // await locatorMutable.mock___updatePostTokenRebaseReceiver(ZeroAddress); - // - // expect(await locator.postTokenRebaseReceiver()).to.equal(ZeroAddress); - // - // const accountingOracleAddress = await locator.accountingOracle(); - // const accountingOracle = await impersonate(accountingOracleAddress, ether("1000.0")); - // - // await expect(lido.connect(accountingOracle).handleOracleReport(...report())).not.to.emit( - // postTokenRebaseReceiver, - // "Mock__PostTokenRebaseHandled", - // ); - // }); - // - // it("Reverts if there are withdrawal batches submitted and `checkSimulatedShareRate` fails", async () => { - // await oracleReportSanityChecker.mock__checkSimulatedShareRateReverts(true); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ).to.be.reverted; - // }); - // - // it("Does not revert if there are no withdrawal batches submitted but `checkSimulatedShareRate` fails", async () => { - // await oracleReportSanityChecker.mock__checkSimulatedShareRateReverts(true); - // - // await expect(lido.handleOracleReport(...report())).not.to.be.reverted; - // }); - // - // it("Returns post-rebase state", async () => { - // const postRebaseState = await lido.handleOracleReport.staticCall(...report()); - // - // expect(postRebaseState).to.deep.equal([await lido.getTotalPooledEther(), await lido.getTotalShares(), 0n, 0n]); - // }); + context("handleOracleReport", () => { + it("Update CL validators count if reported more", async () => { + await updateLidoLocatorImplementation(await lido.getLidoLocator(), { accountingOracle: deployer }); + + let depositedValidators = 100n; + await lido.connect(deployer).unsafeChangeDepositedValidators(depositedValidators); + + // first report, 100 validators + await accounting.handleOracleReport( + report({ + clValidators: depositedValidators, + }), + ); + + const slot = streccak("lido.Lido.beaconValidators"); + const lidoAddress = await lido.getAddress(); + + let clValidatorsPosition = await getStorageAt(lidoAddress, slot); + expect(clValidatorsPosition).to.equal(depositedValidators); + + depositedValidators = 101n; + await lido.connect(deployer).unsafeChangeDepositedValidators(depositedValidators); + + // second report, 101 validators + await accounting.handleOracleReport( + report({ + clValidators: depositedValidators, + }), + ); + + clValidatorsPosition = await getStorageAt(lidoAddress, slot); + expect(clValidatorsPosition).to.equal(depositedValidators); + }); + + function report(overrides?: Partial): ReportValuesStruct { + return { + timestamp: 0n, + timeElapsed: 0n, + clValidators: 0n, + clBalance: 0n, + withdrawalVaultBalance: 0n, + elRewardsVaultBalance: 0n, + sharesRequestedToBurn: 0n, + withdrawalFinalizationBatches: [], + vaultValues: [], + netCashFlows: [], + ...overrides, + }; + } + + + // it("Reverts if the `checkAccountingOracleReport` sanity check fails", async () => { + // await oracleReportSanityChecker.mock__checkAccountingOracleReportReverts(true); + + // await expect(lido.handleOracleReport(...report())).to.be.reverted; + // }); + + // it("Reverts if the `checkWithdrawalQueueOracleReport` sanity check fails", async () => { + // await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); + // await expect( + // lido.handleOracleReport( + // ...report({ + // withdrawalFinalizationBatches: [1n], + // }), + // ), + // ).to.be.reverted; + // }); + + // it("Does not revert if the `checkWithdrawalQueueOracleReport` sanity check fails but no withdrawal batches were reported", async () => { + // await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); + // await withdrawalQueue.mock__isPaused(true); + + // await expect(lido.handleOracleReport(...report())).not.to.be.reverted; + // }); + + // it("Does not revert if the `checkWithdrawalQueueOracleReport` sanity check fails but `withdrawalQueue` is paused", async () => { + // await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); + // await withdrawalQueue.mock__isPaused(true); + + // await expect( + // lido.handleOracleReport( + // ...report({ + // withdrawalFinalizationBatches: [1n], + // }), + // ), + // ).not.to.be.reverted; + // }); + + // it("Does not emit `StETHBurnRequested` if there are no shares to burn", async () => { + // await expect( + // lido.handleOracleReport( + // ...report({ + // withdrawalFinalizationBatches: [1n], + // }), + // ), + // ).not.to.emit(burner, "StETHBurnRequested"); + // }); + + // it("Emits `StETHBurnRequested` if there are shares to burn", async () => { + // const sharesToBurn = 1n; + // const isCover = false; + // const steth = 1n * 2n; // imitating 1:2 rate, see Burner `mock__prefinalizeReturn` + + // await withdrawalQueue.mock__prefinalizeReturn(0n, sharesToBurn); + + // await expect( + // lido.handleOracleReport( + // ...report({ + // withdrawalFinalizationBatches: [1n], + // }), + // ), + // ) + // .to.emit(burner, "StETHBurnRequested") + // .withArgs(isCover, await lido.getAddress(), steth, sharesToBurn); + // }); + + // it("Withdraws ether from `ElRewardsVault` if EL rewards are greater than 0 as returned from `smoothenTokenRebase`", async () => { + // const withdrawals = 0n; + // const elRewards = 1n; + // const simulatedSharesToBurn = 0n; + // const sharesToBurn = 0n; + + // await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn( + // withdrawals, + // elRewards, + // simulatedSharesToBurn, + // sharesToBurn, + // ); + + // // `Mock__RewardsWithdrawn` event is only emitted on the mock to verify + // // that `ElRewardsVault.withdrawRewards` was actually called + // await expect(lido.handleOracleReport(...report())).to.emit(elRewardsVault, "Mock__RewardsWithdrawn"); + // }); + + // it("Withdraws ether from `WithdrawalVault` if withdrawals are greater than 0 as returned from `smoothenTokenRebase`", async () => { + // const withdrawals = 1n; + // const elRewards = 0n; + // const simulatedSharesToBurn = 0n; + // const sharesToBurn = 0n; + + // await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn( + // withdrawals, + // elRewards, + // simulatedSharesToBurn, + // sharesToBurn, + // ); + + // // `Mock__WithdrawalsWithdrawn` event is only emitted on the mock to verify + // // that `WithdrawalVault.withdrawWithdrawals` was actually called + // await expect(lido.handleOracleReport(...report())).to.emit(withdrawalVault, "Mock__WithdrawalsWithdrawn"); + // }); + + // it("Finalizes withdrawals if there is ether to lock on `WithdrawalQueue` as returned from `prefinalize`", async () => { + // const ethToLock = ether("10.0"); + // await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); + // // top up buffer via submit + // await lido.submit(ZeroAddress, { value: ethToLock }); + + // await expect( + // lido.handleOracleReport( + // ...report({ + // withdrawalFinalizationBatches: [1n, 2n], + // }), + // ), + // ).to.emit(withdrawalQueue, "WithdrawalsFinalized"); + // }); + + // it("Updates buffered ether", async () => { + // const initialBufferedEther = await lido.getBufferedEther(); + // const ethToLock = 1n; + + // // assert that the buffer has enough eth to lock for withdrawals + // // should have some eth from the initial 0xdead holder + // expect(initialBufferedEther).greaterThanOrEqual(ethToLock); + // await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); + + // await expect( + // lido.handleOracleReport( + // ...report({ + // withdrawalFinalizationBatches: [1n], + // }), + // ), + // ).to.not.be.reverted; + + // expect(await lido.getBufferedEther()).to.equal(initialBufferedEther - ethToLock); + // }); + + // it("Emits an `ETHDistributed` event", async () => { + // const reportTimestamp = await getNextBlockTimestamp(); + // const preClBalance = 0n; + // const clBalance = 1n; + // const withdrawals = 0n; + // const elRewards = 0n; + // const bufferedEther = await lido.getBufferedEther(); + + // await expect( + // lido.handleOracleReport( + // ...report({ + // reportTimestamp: reportTimestamp, + // clBalance, + // }), + // ), + // ) + // .to.emit(lido, "ETHDistributed") + // .withArgs(reportTimestamp, preClBalance, clBalance, withdrawals, elRewards, bufferedEther); + // }); + + // it("Burns shares if there are shares to burn as returned from `smoothenTokenRebaseReturn`", async () => { + // const sharesRequestedToBurn = 1n; + + // await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn(0n, 0n, 0n, sharesRequestedToBurn); + + // // set up steth whale, in case we need to send steth to other accounts + // await setBalance(stethWhale.address, ether("101.0")); + // await lido.connect(stethWhale).submit(ZeroAddress, { value: ether("100.0") }); + // // top up Burner with steth to burn + // await lido.connect(stethWhale).transferShares(burner, sharesRequestedToBurn); + + // await expect( + // lido.handleOracleReport( + // ...report({ + // sharesRequestedToBurn, + // }), + // ), + // ) + // .to.emit(burner, "Mock__CommitSharesToBurnWasCalled") + // .and.to.emit(lido, "SharesBurnt") + // .withArgs(await burner.getAddress(), sharesRequestedToBurn, sharesRequestedToBurn, sharesRequestedToBurn); + // }); + + // it("Reverts if the number of reward recipients does not match the number of module fees as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { + // // one recipient + // const recipients = [certainAddress("lido:handleOracleReport:single-recipient")]; + // const modulesIds = [1n, 2n]; + // // but two module fees + // const moduleFees = [500n, 500n]; + // const totalFee = 1000; + // const precisionPoints = 10n ** 20n; + + // await stakingRouter.mock__getStakingRewardsDistribution( + // recipients, + // modulesIds, + // moduleFees, + // totalFee, + // precisionPoints, + // ); + + // await expect( + // lido.handleOracleReport( + // ...report({ + // clBalance: 1n, // made 1 wei of profit, trigers reward processing + // }), + // ), + // ).to.be.revertedWith("WRONG_RECIPIENTS_INPUT"); + // }); + + // it("Reverts if the number of module ids does not match the number of module fees as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { + // const recipients = [ + // certainAddress("lido:handleOracleReport:recipient1"), + // certainAddress("lido:handleOracleReport:recipient2"), + // ]; + // // one module id + // const modulesIds = [1n]; + // // but two module fees + // const moduleFees = [500n, 500n]; + // const totalFee = 1000; + // const precisionPoints = 10n ** 20n; + + // await stakingRouter.mock__getStakingRewardsDistribution( + // recipients, + // modulesIds, + // moduleFees, + // totalFee, + // precisionPoints, + // ); + + // await expect( + // lido.handleOracleReport( + // ...report({ + // clBalance: 1n, // made 1 wei of profit, trigers reward processing + // }), + // ), + // ).to.be.revertedWith("WRONG_MODULE_IDS_INPUT"); + // }); + + // it("Does not mint and transfer any shares if the total fee is zero as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { + // // single staking module + // const recipients = [certainAddress("lido:handleOracleReport:recipient")]; + // const modulesIds = [1n]; + // const moduleFees = [500n]; + // // fee is 0 + // const totalFee = 0; + // const precisionPoints = 10n ** 20n; + + // await stakingRouter.mock__getStakingRewardsDistribution( + // recipients, + // modulesIds, + // moduleFees, + // totalFee, + // precisionPoints, + // ); + + // await expect( + // lido.handleOracleReport( + // ...report({ + // clBalance: 1n, + // }), + // ), + // ) + // .not.to.emit(lido, "Transfer") + // .and.not.to.emit(lido, "TransferShares") + // .and.not.to.emit(stakingRouter, "Mock__MintedRewardsReported"); + // }); + + // it("Mints shares to itself and then transfers them to recipients if there are fees to distribute as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { + // // initially, before any rebases, one share costs one steth + // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); + // // thus, the total supply of steth should equal the total number of shares + // expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); + + // // mock a single staking module with 5% fee with the total protocol fee of 10% + // const stakingModule = { + // address: certainAddress("lido:handleOracleReport:staking-module"), + // id: 1n, + // fee: 5n * 10n ** 18n, // 5% + // }; + + // const totalFee = 10n * 10n ** 18n; // 10% + // const precisionPoints = 100n * 10n ** 18n; // 100% + + // await stakingRouter.mock__getStakingRewardsDistribution( + // [stakingModule.address], + // [stakingModule.id], + // [stakingModule.fee], + // totalFee, + // precisionPoints, + // ); + + // const clBalance = ether("1.0"); + + // const expectedSharesToMint = + // (clBalance * totalFee * (await lido.getTotalShares())) / + // (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); + + // const expectedModuleRewardInShares = expectedSharesToMint / (totalFee / stakingModule.fee); + // const expectedTreasuryCutInShares = expectedSharesToMint - expectedModuleRewardInShares; + + // await expect( + // lido.handleOracleReport( + // ...report({ + // clBalance: ether("1.0"), // 1 ether of profit + // }), + // ), + // ) + // .to.emit(lido, "TransferShares") + // .withArgs(ZeroAddress, stakingModule.address, expectedModuleRewardInShares) + // .and.to.emit(lido, "TransferShares") + // .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) + // .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); + + // expect(await lido.balanceOf(stakingModule.address)).to.equal( + // await lido.getPooledEthByShares(expectedModuleRewardInShares), + // ); + + // expect(await lido.balanceOf(await lido.getTreasury())).to.equal( + // await lido.getPooledEthByShares(expectedTreasuryCutInShares), + // ); + + // // now one share should cost 1.9 steth (10% was distributed as rewards) + // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); + // }); + + // it("Transfers all new shares to treasury if the module fee is zero as returned `StakingRouter.getStakingRewardsDistribution`", async () => { + // // initially, before any rebases, one share costs one steth + // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); + // // thus, the total supply of steth should equal the total number of shares + // expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); + + // // mock a single staking module with 0% fee with the total protocol fee of 10% + // const stakingModule = { + // address: certainAddress("lido:handleOracleReport:staking-module"), + // id: 1n, + // fee: 0n, + // }; + + // const totalFee = 10n * 10n ** 18n; // 10% + // const precisionPoints = 100n * 10n ** 18n; // 100% + + // await stakingRouter.mock__getStakingRewardsDistribution( + // [stakingModule.address], + // [stakingModule.id], + // [stakingModule.fee], + // totalFee, + // precisionPoints, + // ); + + // const clBalance = ether("1.0"); + + // const expectedSharesToMint = + // (clBalance * totalFee * (await lido.getTotalShares())) / + // (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); + + // const expectedModuleRewardInShares = 0n; + // const expectedTreasuryCutInShares = expectedSharesToMint; + + // await expect( + // lido.handleOracleReport( + // ...report({ + // clBalance: ether("1.0"), // 1 ether of profit + // }), + // ), + // ) + // .and.to.emit(lido, "TransferShares") + // .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) + // .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); + + // expect(await lido.balanceOf(stakingModule.address)).to.equal( + // await lido.getPooledEthByShares(expectedModuleRewardInShares), + // ); + + // expect(await lido.balanceOf(await lido.getTreasury())).to.equal( + // await lido.getPooledEthByShares(expectedTreasuryCutInShares), + // ); + + // // now one share should cost 1.9 steth (10% was distributed as rewards) + // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); + // }); + + // it("Relays the report data to `PostTokenRebaseReceiver`", async () => { + // await expect(lido.handleOracleReport(...report())).to.emit( + // postTokenRebaseReceiver, + // "Mock__PostTokenRebaseHandled", + // ); + // }); + + // it("Does not relay the report data to `PostTokenRebaseReceiver` if the locator returns zero address", async () => { + // const lidoLocatorAddress = await lido.getLidoLocator(); + + // // Change the locator implementation to support zero address + // await updateLidoLocatorImplementation(lidoLocatorAddress, {}, "LidoLocator__MutableMock", deployer); + // const locatorMutable = await ethers.getContractAt("LidoLocator__MutableMock", lidoLocatorAddress, deployer); + // await locatorMutable.mock___updatePostTokenRebaseReceiver(ZeroAddress); + + // expect(await locator.postTokenRebaseReceiver()).to.equal(ZeroAddress); + + // const accountingOracleAddress = await locator.accountingOracle(); + // const accountingOracle = await impersonate(accountingOracleAddress, ether("1000.0")); + + // await expect(lido.connect(accountingOracle).handleOracleReport(...report())).not.to.emit( + // postTokenRebaseReceiver, + // "Mock__PostTokenRebaseHandled", + // ); + // }); + + // it("Reverts if there are withdrawal batches submitted and `checkSimulatedShareRate` fails", async () => { + // await oracleReportSanityChecker.mock__checkSimulatedShareRateReverts(true); + + // await expect( + // lido.handleOracleReport( + // ...report({ + // withdrawalFinalizationBatches: [1n], + // }), + // ), + // ).to.be.reverted; + // }); + + // it("Does not revert if there are no withdrawal batches submitted but `checkSimulatedShareRate` fails", async () => { + // await oracleReportSanityChecker.mock__checkSimulatedShareRateReverts(true); + + // await expect(lido.handleOracleReport(...report())).not.to.be.reverted; + // }); + + // it("Returns post-rebase state", async () => { + // const postRebaseState = await lido.handleOracleReport.staticCall(...report()); + + // expect(postRebaseState).to.deep.equal([await lido.getTotalPooledEther(), await lido.getTotalShares(), 0n, 0n]); + // }); }); }); diff --git a/test/deploy/dao.ts b/test/deploy/dao.ts index 70e18dc01..094468898 100644 --- a/test/deploy/dao.ts +++ b/test/deploy/dao.ts @@ -7,7 +7,7 @@ import { Kernel, LidoLocator } from "typechain-types"; import { ether, findEvents, streccak } from "lib"; -import { deployLidoLocator } from "./locator"; +import { deployLidoLocator, updateLidoLocatorImplementation } from "./locator"; interface CreateAddAppArgs { dao: Kernel; @@ -79,7 +79,11 @@ export async function deployLidoDao({ rootAccount, initialized, locatorConfig = await lido.initialize(locator, eip712steth, { value: ether("1.0") }); } - return { lido, dao, acl }; + const locator = await lido.getLidoLocator(); + const accounting = await ethers.deployContract("Accounting", [locator, lido], rootAccount); + await updateLidoLocatorImplementation(locator, { accounting }); + + return { lido, dao, acl, accounting }; } export async function deployLidoDaoForNor({ rootAccount, initialized, locatorConfig = {} }: DeployLidoDaoArgs) { From 266a9901396ff3632accd2f6f5f5eebf4fcac221 Mon Sep 17 00:00:00 2001 From: VP Date: Mon, 30 Dec 2024 17:54:00 +0100 Subject: [PATCH 02/27] feat: proper Accounting initialization --- test/deploy/dao.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/deploy/dao.ts b/test/deploy/dao.ts index 094468898..910fb1fd3 100644 --- a/test/deploy/dao.ts +++ b/test/deploy/dao.ts @@ -80,8 +80,11 @@ export async function deployLidoDao({ rootAccount, initialized, locatorConfig = } const locator = await lido.getLidoLocator(); - const accounting = await ethers.deployContract("Accounting", [locator, lido], rootAccount); + const accountingImpl = await ethers.deployContract("Accounting", [locator, lido], rootAccount); + const accountingProxy = await ethers.deployContract("OssifiableProxy", [accountingImpl, rootAccount, new Uint8Array()], rootAccount); + const accounting = await ethers.getContractAt("Accounting", accountingProxy, rootAccount); await updateLidoLocatorImplementation(locator, { accounting }); + await accounting.initialize(rootAccount); return { lido, dao, acl, accounting }; } From 20e62c7e99a309ebf6664eb048f3ec40f6817008 Mon Sep 17 00:00:00 2001 From: VP Date: Mon, 30 Dec 2024 17:54:30 +0100 Subject: [PATCH 03/27] test: add PostTokenRebaseReceiver --- test/0.4.24/lido/lido.accounting.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/0.4.24/lido/lido.accounting.test.ts b/test/0.4.24/lido/lido.accounting.test.ts index b5c76aabc..a44479c4f 100644 --- a/test/0.4.24/lido/lido.accounting.test.ts +++ b/test/0.4.24/lido/lido.accounting.test.ts @@ -1,5 +1,4 @@ import { expect } from "chai"; -import { BigNumberish } from "ethers"; import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; @@ -8,13 +7,15 @@ import { getStorageAt } from "@nomicfoundation/hardhat-network-helpers"; import { Accounting, ACL, + IPostTokenRebaseReceiver, Lido, LidoExecutionLayerRewardsVault__MockForLidoAccounting, LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory, + PostTokenRebaseReceiver__MockForAccounting__factory, StakingRouter__MockForLidoAccounting, StakingRouter__MockForLidoAccounting__factory, WithdrawalVault__MockForLidoAccounting, - WithdrawalVault__MockForLidoAccounting__factory, + WithdrawalVault__MockForLidoAccounting__factory } from "typechain-types"; import { ReportValuesStruct } from "typechain-types/contracts/0.8.25/Accounting"; import { OracleReportSanityChecker__MockForLidoHandleOracleReport__factory } from "typechain-types/factories/test/0.4.24/contracts/OracleReportSanityChecker__MockForLidoHandleOracleReport__factory"; @@ -33,6 +34,7 @@ describe("Lido:accounting", () => { let lido: Lido; let acl: ACL; let accounting: Accounting; + let postTokenRebaseReceiver: IPostTokenRebaseReceiver; // let locator: LidoLocator; let elRewardsVault: LidoExecutionLayerRewardsVault__MockForLidoAccounting; @@ -44,11 +46,12 @@ describe("Lido:accounting", () => { // [deployer, accounting, stethWhale, stranger, withdrawalQueue] = await ethers.getSigners(); [deployer, stranger, withdrawalQueue] = await ethers.getSigners(); - [elRewardsVault, stakingRouter, withdrawalVault, oracleReportSanityChecker] = await Promise.all([ + [elRewardsVault, stakingRouter, withdrawalVault, oracleReportSanityChecker, postTokenRebaseReceiver] = await Promise.all([ new LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory(deployer).deploy(), new StakingRouter__MockForLidoAccounting__factory(deployer).deploy(), new WithdrawalVault__MockForLidoAccounting__factory(deployer).deploy(), new OracleReportSanityChecker__MockForLidoHandleOracleReport__factory(deployer).deploy(), + new PostTokenRebaseReceiver__MockForAccounting__factory(deployer).deploy(), ]); ({ lido, acl, accounting } = await deployLidoDao({ @@ -60,6 +63,7 @@ describe("Lido:accounting", () => { withdrawalVault, stakingRouter, oracleReportSanityChecker, + postTokenRebaseReceiver }, })); From 595eab290b79a0499a4ad56129dfbf84d6654cc2 Mon Sep 17 00:00:00 2001 From: VP Date: Mon, 30 Dec 2024 18:28:17 +0100 Subject: [PATCH 04/27] test: fix imports --- test/0.4.24/lido/lido.accounting.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/0.4.24/lido/lido.accounting.test.ts b/test/0.4.24/lido/lido.accounting.test.ts index a44479c4f..0911d3230 100644 --- a/test/0.4.24/lido/lido.accounting.test.ts +++ b/test/0.4.24/lido/lido.accounting.test.ts @@ -11,15 +11,15 @@ import { Lido, LidoExecutionLayerRewardsVault__MockForLidoAccounting, LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory, + OracleReportSanityChecker__MockForAccounting, + OracleReportSanityChecker__MockForAccounting__factory, PostTokenRebaseReceiver__MockForAccounting__factory, StakingRouter__MockForLidoAccounting, StakingRouter__MockForLidoAccounting__factory, WithdrawalVault__MockForLidoAccounting, WithdrawalVault__MockForLidoAccounting__factory } from "typechain-types"; -import { ReportValuesStruct } from "typechain-types/contracts/0.8.25/Accounting"; -import { OracleReportSanityChecker__MockForLidoHandleOracleReport__factory } from "typechain-types/factories/test/0.4.24/contracts/OracleReportSanityChecker__MockForLidoHandleOracleReport__factory"; -import { OracleReportSanityChecker__MockForLidoHandleOracleReport } from "typechain-types/test/0.4.24/contracts/OracleReportSanityChecker__MockForLidoHandleOracleReport"; +import { ReportValuesStruct } from "typechain-types/contracts/0.8.9/oracle/AccountingOracle.sol/IReportReceiver"; import { streccak } from "lib"; @@ -40,7 +40,7 @@ describe("Lido:accounting", () => { let elRewardsVault: LidoExecutionLayerRewardsVault__MockForLidoAccounting; let withdrawalVault: WithdrawalVault__MockForLidoAccounting; let stakingRouter: StakingRouter__MockForLidoAccounting; - let oracleReportSanityChecker: OracleReportSanityChecker__MockForLidoHandleOracleReport; + let oracleReportSanityChecker: OracleReportSanityChecker__MockForAccounting; beforeEach(async () => { // [deployer, accounting, stethWhale, stranger, withdrawalQueue] = await ethers.getSigners(); @@ -50,7 +50,7 @@ describe("Lido:accounting", () => { new LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory(deployer).deploy(), new StakingRouter__MockForLidoAccounting__factory(deployer).deploy(), new WithdrawalVault__MockForLidoAccounting__factory(deployer).deploy(), - new OracleReportSanityChecker__MockForLidoHandleOracleReport__factory(deployer).deploy(), + new OracleReportSanityChecker__MockForAccounting__factory(deployer).deploy(), new PostTokenRebaseReceiver__MockForAccounting__factory(deployer).deploy(), ]); From 03733b37ea529e80a8ca43479ceeedf74dde4af1 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Fri, 3 Jan 2025 12:19:24 +0000 Subject: [PATCH 05/27] fix: linter --- test/0.4.24/lido/lido.accounting.test.ts | 58 ++++++++++-------------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/test/0.4.24/lido/lido.accounting.test.ts b/test/0.4.24/lido/lido.accounting.test.ts index 0911d3230..02b1573c5 100644 --- a/test/0.4.24/lido/lido.accounting.test.ts +++ b/test/0.4.24/lido/lido.accounting.test.ts @@ -17,7 +17,7 @@ import { StakingRouter__MockForLidoAccounting, StakingRouter__MockForLidoAccounting__factory, WithdrawalVault__MockForLidoAccounting, - WithdrawalVault__MockForLidoAccounting__factory + WithdrawalVault__MockForLidoAccounting__factory, } from "typechain-types"; import { ReportValuesStruct } from "typechain-types/contracts/0.8.9/oracle/AccountingOracle.sol/IReportReceiver"; @@ -46,13 +46,14 @@ describe("Lido:accounting", () => { // [deployer, accounting, stethWhale, stranger, withdrawalQueue] = await ethers.getSigners(); [deployer, stranger, withdrawalQueue] = await ethers.getSigners(); - [elRewardsVault, stakingRouter, withdrawalVault, oracleReportSanityChecker, postTokenRebaseReceiver] = await Promise.all([ - new LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory(deployer).deploy(), - new StakingRouter__MockForLidoAccounting__factory(deployer).deploy(), - new WithdrawalVault__MockForLidoAccounting__factory(deployer).deploy(), - new OracleReportSanityChecker__MockForAccounting__factory(deployer).deploy(), - new PostTokenRebaseReceiver__MockForAccounting__factory(deployer).deploy(), - ]); + [elRewardsVault, stakingRouter, withdrawalVault, oracleReportSanityChecker, postTokenRebaseReceiver] = + await Promise.all([ + new LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory(deployer).deploy(), + new StakingRouter__MockForLidoAccounting__factory(deployer).deploy(), + new WithdrawalVault__MockForLidoAccounting__factory(deployer).deploy(), + new OracleReportSanityChecker__MockForAccounting__factory(deployer).deploy(), + new PostTokenRebaseReceiver__MockForAccounting__factory(deployer).deploy(), + ]); ({ lido, acl, accounting } = await deployLidoDao({ rootAccount: deployer, @@ -63,7 +64,7 @@ describe("Lido:accounting", () => { withdrawalVault, stakingRouter, oracleReportSanityChecker, - postTokenRebaseReceiver + postTokenRebaseReceiver, }, })); @@ -99,13 +100,13 @@ describe("Lido:accounting", () => { .withArgs(0n, 0n, 100n); }); - type ArgsTuple = [BigNumberish, BigNumberish, BigNumberish, BigNumberish]; + type ArgsTuple = [bigint, bigint, bigint, bigint]; interface Args { - reportTimestamp: BigNumberish; - preClValidators: BigNumberish; - postClValidators: BigNumberish; - postClBalance: BigNumberish; + reportTimestamp: bigint; + preClValidators: bigint; + postClValidators: bigint; + postClBalance: bigint; } function args(overrides?: Partial): ArgsTuple { @@ -131,26 +132,17 @@ describe("Lido:accounting", () => { ); }); - type ArgsTuple = [ - BigNumberish, - BigNumberish, - BigNumberish, - BigNumberish, - BigNumberish, - BigNumberish, - BigNumberish, - BigNumberish, - ]; + type ArgsTuple = [bigint, bigint, bigint, bigint, bigint, bigint, bigint, bigint]; interface Args { - reportTimestamp: BigNumberish; - reportClBalance: BigNumberish; - adjustedPreCLBalance: BigNumberish; - withdrawalsToWithdraw: BigNumberish; - elRewardsToWithdraw: BigNumberish; - lastWithdrawalRequestToFinalize: BigNumberish; - simulatedShareRate: BigNumberish; - etherToLockOnWithdrawalQueue: BigNumberish; + reportTimestamp: bigint; + reportClBalance: bigint; + adjustedPreCLBalance: bigint; + withdrawalsToWithdraw: bigint; + elRewardsToWithdraw: bigint; + lastWithdrawalRequestToFinalize: bigint; + simulatedShareRate: bigint; + etherToLockOnWithdrawalQueue: bigint; } function args(overrides?: Partial): ArgsTuple { @@ -168,7 +160,6 @@ describe("Lido:accounting", () => { } }); - // TODO: [@tamtamchik] restore tests context("handleOracleReport", () => { it("Update CL validators count if reported more", async () => { await updateLidoLocatorImplementation(await lido.getLidoLocator(), { accountingOracle: deployer }); @@ -219,7 +210,6 @@ describe("Lido:accounting", () => { }; } - // it("Reverts if the `checkAccountingOracleReport` sanity check fails", async () => { // await oracleReportSanityChecker.mock__checkAccountingOracleReportReverts(true); From 8dd75347b22edd29235082eb8df7269c0c62e432 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Fri, 3 Jan 2025 12:22:47 +0000 Subject: [PATCH 06/27] chore: fix husky setup --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index a8711c17c..6588b91fc 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "typecheck": "tsc --noEmit", "prepare": "husky", "abis:extract": "hardhat abis:extract", - "verify:deployed": "hardhat verify:deployed" + "verify:deployed": "hardhat verify:deployed", + "postinstall": "husky" }, "lint-staged": { "./**/*.ts": [ From 432aab210b49a89ecc14f75dc6dc5d949478deca Mon Sep 17 00:00:00 2001 From: VP Date: Wed, 8 Jan 2025 10:50:58 +0100 Subject: [PATCH 07/27] feat: impersonate caller instead of locator update --- test/0.4.24/lido/lido.accounting.test.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/0.4.24/lido/lido.accounting.test.ts b/test/0.4.24/lido/lido.accounting.test.ts index 02b1573c5..695c7637a 100644 --- a/test/0.4.24/lido/lido.accounting.test.ts +++ b/test/0.4.24/lido/lido.accounting.test.ts @@ -11,6 +11,7 @@ import { Lido, LidoExecutionLayerRewardsVault__MockForLidoAccounting, LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory, + LidoLocator__factory, OracleReportSanityChecker__MockForAccounting, OracleReportSanityChecker__MockForAccounting__factory, PostTokenRebaseReceiver__MockForAccounting__factory, @@ -21,9 +22,9 @@ import { } from "typechain-types"; import { ReportValuesStruct } from "typechain-types/contracts/0.8.9/oracle/AccountingOracle.sol/IReportReceiver"; -import { streccak } from "lib"; +import { ether, impersonate, streccak } from "lib"; -import { deployLidoDao, updateLidoLocatorImplementation } from "test/deploy"; +import { deployLidoDao } from "test/deploy"; describe("Lido:accounting", () => { let deployer: HardhatEthersSigner; @@ -35,7 +36,7 @@ describe("Lido:accounting", () => { let acl: ACL; let accounting: Accounting; let postTokenRebaseReceiver: IPostTokenRebaseReceiver; - // let locator: LidoLocator; + let locator: LidoLocator; let elRewardsVault: LidoExecutionLayerRewardsVault__MockForLidoAccounting; let withdrawalVault: WithdrawalVault__MockForLidoAccounting; @@ -68,7 +69,7 @@ describe("Lido:accounting", () => { }, })); - // locator = LidoLocator__factory.connect(await lido.getLidoLocator(), deployer); + locator = LidoLocator__factory.connect(await lido.getLidoLocator(), deployer); await acl.createPermission(deployer, lido, await lido.RESUME_ROLE(), deployer); await acl.createPermission(deployer, lido, await lido.PAUSE_ROLE(), deployer); @@ -87,7 +88,8 @@ describe("Lido:accounting", () => { }); it("Updates beacon stats", async () => { - await updateLidoLocatorImplementation(await lido.getLidoLocator(), { accounting: deployer }); + const accountingSigner = await impersonate(await accounting.getAddress(), ether("100.0")); + lido = lido.connect(accountingSigner); await expect( lido.processClStateUpdate( ...args({ @@ -162,11 +164,11 @@ describe("Lido:accounting", () => { context("handleOracleReport", () => { it("Update CL validators count if reported more", async () => { - await updateLidoLocatorImplementation(await lido.getLidoLocator(), { accountingOracle: deployer }); - let depositedValidators = 100n; await lido.connect(deployer).unsafeChangeDepositedValidators(depositedValidators); + const accountingOracleSigner = await impersonate(await locator.accountingOracle(), ether("100.0")); + accounting = accounting.connect(accountingOracleSigner); // first report, 100 validators await accounting.handleOracleReport( report({ From df0127256351620131ec460db798bc92b44d115a Mon Sep 17 00:00:00 2001 From: VP Date: Wed, 8 Jan 2025 10:53:57 +0100 Subject: [PATCH 08/27] chore: add import --- test/0.4.24/lido/lido.accounting.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/0.4.24/lido/lido.accounting.test.ts b/test/0.4.24/lido/lido.accounting.test.ts index 695c7637a..23d498491 100644 --- a/test/0.4.24/lido/lido.accounting.test.ts +++ b/test/0.4.24/lido/lido.accounting.test.ts @@ -11,6 +11,7 @@ import { Lido, LidoExecutionLayerRewardsVault__MockForLidoAccounting, LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory, + LidoLocator, LidoLocator__factory, OracleReportSanityChecker__MockForAccounting, OracleReportSanityChecker__MockForAccounting__factory, From 6ddda8a1fe17c6eed2fab6a62b07402fe880b014 Mon Sep 17 00:00:00 2001 From: VP Date: Thu, 9 Jan 2025 15:03:46 +0100 Subject: [PATCH 09/27] test: add withdrawal queue related tests --- test/0.4.24/lido/lido.accounting.test.ts | 118 ++++++++++++----------- 1 file changed, 64 insertions(+), 54 deletions(-) diff --git a/test/0.4.24/lido/lido.accounting.test.ts b/test/0.4.24/lido/lido.accounting.test.ts index 23d498491..8f8c378da 100644 --- a/test/0.4.24/lido/lido.accounting.test.ts +++ b/test/0.4.24/lido/lido.accounting.test.ts @@ -18,6 +18,8 @@ import { PostTokenRebaseReceiver__MockForAccounting__factory, StakingRouter__MockForLidoAccounting, StakingRouter__MockForLidoAccounting__factory, + WithdrawalQueue__MockForAccounting, + WithdrawalQueue__MockForAccounting__factory, WithdrawalVault__MockForLidoAccounting, WithdrawalVault__MockForLidoAccounting__factory, } from "typechain-types"; @@ -31,7 +33,6 @@ describe("Lido:accounting", () => { let deployer: HardhatEthersSigner; // let stethWhale: HardhatEthersSigner; let stranger: HardhatEthersSigner; - let withdrawalQueue: HardhatEthersSigner; let lido: Lido; let acl: ACL; @@ -43,19 +44,27 @@ describe("Lido:accounting", () => { let withdrawalVault: WithdrawalVault__MockForLidoAccounting; let stakingRouter: StakingRouter__MockForLidoAccounting; let oracleReportSanityChecker: OracleReportSanityChecker__MockForAccounting; + let withdrawalQueue: WithdrawalQueue__MockForAccounting; beforeEach(async () => { // [deployer, accounting, stethWhale, stranger, withdrawalQueue] = await ethers.getSigners(); - [deployer, stranger, withdrawalQueue] = await ethers.getSigners(); - - [elRewardsVault, stakingRouter, withdrawalVault, oracleReportSanityChecker, postTokenRebaseReceiver] = - await Promise.all([ - new LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory(deployer).deploy(), - new StakingRouter__MockForLidoAccounting__factory(deployer).deploy(), - new WithdrawalVault__MockForLidoAccounting__factory(deployer).deploy(), - new OracleReportSanityChecker__MockForAccounting__factory(deployer).deploy(), - new PostTokenRebaseReceiver__MockForAccounting__factory(deployer).deploy(), - ]); + [deployer, stranger] = await ethers.getSigners(); + + [ + elRewardsVault, + stakingRouter, + withdrawalVault, + oracleReportSanityChecker, + postTokenRebaseReceiver, + withdrawalQueue, + ] = await Promise.all([ + new LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory(deployer).deploy(), + new StakingRouter__MockForLidoAccounting__factory(deployer).deploy(), + new WithdrawalVault__MockForLidoAccounting__factory(deployer).deploy(), + new OracleReportSanityChecker__MockForAccounting__factory(deployer).deploy(), + new PostTokenRebaseReceiver__MockForAccounting__factory(deployer).deploy(), + new WithdrawalQueue__MockForAccounting__factory(deployer).deploy(), + ]); ({ lido, acl, accounting } = await deployLidoDao({ rootAccount: deployer, @@ -72,6 +81,9 @@ describe("Lido:accounting", () => { locator = LidoLocator__factory.connect(await lido.getLidoLocator(), deployer); + const accountingOracleSigner = await impersonate(await locator.accountingOracle(), ether("100.0")); + accounting = accounting.connect(accountingOracleSigner); + await acl.createPermission(deployer, lido, await lido.RESUME_ROLE(), deployer); await acl.createPermission(deployer, lido, await lido.PAUSE_ROLE(), deployer); await acl.createPermission(deployer, lido, await lido.UNSAFE_CHANGE_DEPOSITED_VALIDATORS_ROLE(), deployer); @@ -168,8 +180,6 @@ describe("Lido:accounting", () => { let depositedValidators = 100n; await lido.connect(deployer).unsafeChangeDepositedValidators(depositedValidators); - const accountingOracleSigner = await impersonate(await locator.accountingOracle(), ether("100.0")); - accounting = accounting.connect(accountingOracleSigner); // first report, 100 validators await accounting.handleOracleReport( report({ @@ -213,52 +223,52 @@ describe("Lido:accounting", () => { }; } - // it("Reverts if the `checkAccountingOracleReport` sanity check fails", async () => { - // await oracleReportSanityChecker.mock__checkAccountingOracleReportReverts(true); - - // await expect(lido.handleOracleReport(...report())).to.be.reverted; - // }); - - // it("Reverts if the `checkWithdrawalQueueOracleReport` sanity check fails", async () => { - // await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ).to.be.reverted; - // }); + it("Reverts if the `checkAccountingOracleReport` sanity check fails", async () => { + await oracleReportSanityChecker.mock__checkAccountingOracleReportReverts(true); - // it("Does not revert if the `checkWithdrawalQueueOracleReport` sanity check fails but no withdrawal batches were reported", async () => { - // await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); - // await withdrawalQueue.mock__isPaused(true); + await expect(accounting.handleOracleReport(report())).to.be.reverted; + }); - // await expect(lido.handleOracleReport(...report())).not.to.be.reverted; - // }); + it("Reverts if the `checkWithdrawalQueueOracleReport` sanity check fails", async () => { + await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); + await expect( + accounting.handleOracleReport( + report({ + withdrawalFinalizationBatches: [1n], + }), + ), + ).to.be.reverted; + }); - // it("Does not revert if the `checkWithdrawalQueueOracleReport` sanity check fails but `withdrawalQueue` is paused", async () => { - // await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); - // await withdrawalQueue.mock__isPaused(true); + it("Does not revert if the `checkWithdrawalQueueOracleReport` sanity check fails but no withdrawal batches were reported", async () => { + await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); + await withdrawalQueue.mock__isPaused(true); - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ).not.to.be.reverted; - // }); + await expect(accounting.handleOracleReport(report())).not.to.be.reverted; + }); - // it("Does not emit `StETHBurnRequested` if there are no shares to burn", async () => { - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ).not.to.emit(burner, "StETHBurnRequested"); - // }); + // it("Does not revert if the `checkWithdrawalQueueOracleReport` sanity check fails but `withdrawalQueue` is paused", async () => { + // await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); + // await withdrawalQueue.mock__isPaused(true); + + // await expect( + // accounting.handleOracleReport( + // report({ + // withdrawalFinalizationBatches: [1n], + // }), + // ), + // ).not.to.be.reverted; + // }); + + // it("Does not emit `StETHBurnRequested` if there are no shares to burn", async () => { + // await expect( + // accounting.handleOracleReport( + // report({ + // withdrawalFinalizationBatches: [1n], + // }), + // ), + // ).not.to.emit(burner, "StETHBurnRequested"); + // }); // it("Emits `StETHBurnRequested` if there are shares to burn", async () => { // const sharesToBurn = 1n; From c17ab6cbe84411b277319823836a2e457e5fdefb Mon Sep 17 00:00:00 2001 From: VP Date: Thu, 9 Jan 2025 16:15:28 +0100 Subject: [PATCH 10/27] test: add more --- test/0.4.24/lido/lido.accounting.test.ts | 275 ++++++++++++----------- 1 file changed, 143 insertions(+), 132 deletions(-) diff --git a/test/0.4.24/lido/lido.accounting.test.ts b/test/0.4.24/lido/lido.accounting.test.ts index 8f8c378da..6579810c0 100644 --- a/test/0.4.24/lido/lido.accounting.test.ts +++ b/test/0.4.24/lido/lido.accounting.test.ts @@ -1,4 +1,5 @@ import { expect } from "chai"; +import { ZeroAddress } from "ethers"; import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; @@ -7,6 +8,8 @@ import { getStorageAt } from "@nomicfoundation/hardhat-network-helpers"; import { Accounting, ACL, + Burner__MockForAccounting, + Burner__MockForAccounting__factory, IPostTokenRebaseReceiver, Lido, LidoExecutionLayerRewardsVault__MockForLidoAccounting, @@ -25,14 +28,14 @@ import { } from "typechain-types"; import { ReportValuesStruct } from "typechain-types/contracts/0.8.9/oracle/AccountingOracle.sol/IReportReceiver"; -import { ether, impersonate, streccak } from "lib"; +import { ether, getNextBlockTimestamp, impersonate, streccak } from "lib"; import { deployLidoDao } from "test/deploy"; describe("Lido:accounting", () => { let deployer: HardhatEthersSigner; - // let stethWhale: HardhatEthersSigner; let stranger: HardhatEthersSigner; + let stethWhale: HardhatEthersSigner; let lido: Lido; let acl: ACL; @@ -45,10 +48,12 @@ describe("Lido:accounting", () => { let stakingRouter: StakingRouter__MockForLidoAccounting; let oracleReportSanityChecker: OracleReportSanityChecker__MockForAccounting; let withdrawalQueue: WithdrawalQueue__MockForAccounting; + let burner: Burner__MockForAccounting; beforeEach(async () => { // [deployer, accounting, stethWhale, stranger, withdrawalQueue] = await ethers.getSigners(); - [deployer, stranger] = await ethers.getSigners(); + [deployer, stranger, stethWhale] = await ethers.getSigners(); + stethWhale; [ elRewardsVault, @@ -57,6 +62,7 @@ describe("Lido:accounting", () => { oracleReportSanityChecker, postTokenRebaseReceiver, withdrawalQueue, + burner, ] = await Promise.all([ new LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory(deployer).deploy(), new StakingRouter__MockForLidoAccounting__factory(deployer).deploy(), @@ -64,6 +70,7 @@ describe("Lido:accounting", () => { new OracleReportSanityChecker__MockForAccounting__factory(deployer).deploy(), new PostTokenRebaseReceiver__MockForAccounting__factory(deployer).deploy(), new WithdrawalQueue__MockForAccounting__factory(deployer).deploy(), + new Burner__MockForAccounting__factory(deployer).deploy(), ]); ({ lido, acl, accounting } = await deployLidoDao({ @@ -76,6 +83,7 @@ describe("Lido:accounting", () => { stakingRouter, oracleReportSanityChecker, postTokenRebaseReceiver, + burner, }, })); @@ -247,160 +255,163 @@ describe("Lido:accounting", () => { await expect(accounting.handleOracleReport(report())).not.to.be.reverted; }); + /// NOTE: This test is not applicable to the current implementation (Accounting's _checkAccountingOracleReport() checks for checkWithdrawalQueueOracleReport() + /// explicitly in case _report.withdrawalFinalizationBatches.length > 0 // it("Does not revert if the `checkWithdrawalQueueOracleReport` sanity check fails but `withdrawalQueue` is paused", async () => { // await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); // await withdrawalQueue.mock__isPaused(true); - // await expect( - // accounting.handleOracleReport( - // report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ).not.to.be.reverted; + // await expect(accounting.handleOracleReport(report({ withdrawalFinalizationBatches: [1n] }))).not.to.be.reverted; // }); - // it("Does not emit `StETHBurnRequested` if there are no shares to burn", async () => { - // await expect( - // accounting.handleOracleReport( - // report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ).not.to.emit(burner, "StETHBurnRequested"); - // }); + it("Does not emit `StETHBurnRequested` if there are no shares to burn", async () => { + await expect( + accounting.handleOracleReport( + report({ + withdrawalFinalizationBatches: [1n], + }), + ), + ).not.to.emit(burner, "StETHBurnRequested"); + }); - // it("Emits `StETHBurnRequested` if there are shares to burn", async () => { - // const sharesToBurn = 1n; - // const isCover = false; - // const steth = 1n * 2n; // imitating 1:2 rate, see Burner `mock__prefinalizeReturn` + it("Emits `StETHBurnRequested` if there are shares to burn", async () => { + const sharesToBurn = 1n; + const isCover = false; + const steth = 1n * 2n; // imitating 1:2 rate, see Burner `mock__prefinalizeReturn` - // await withdrawalQueue.mock__prefinalizeReturn(0n, sharesToBurn); + await withdrawalQueue.mock__prefinalizeReturn(0n, sharesToBurn); - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ) - // .to.emit(burner, "StETHBurnRequested") - // .withArgs(isCover, await lido.getAddress(), steth, sharesToBurn); - // }); + await expect( + accounting.handleOracleReport( + report({ + withdrawalFinalizationBatches: [1n], + }), + ), + ) + .to.emit(burner, "StETHBurnRequested") + .withArgs(isCover, await accounting.getAddress(), steth, sharesToBurn); + }); - // it("Withdraws ether from `ElRewardsVault` if EL rewards are greater than 0 as returned from `smoothenTokenRebase`", async () => { - // const withdrawals = 0n; - // const elRewards = 1n; - // const simulatedSharesToBurn = 0n; - // const sharesToBurn = 0n; - - // await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn( - // withdrawals, - // elRewards, - // simulatedSharesToBurn, - // sharesToBurn, - // ); + it("Withdraws ether from `ElRewardsVault` if EL rewards are greater than 0 as returned from `smoothenTokenRebase`", async () => { + const withdrawals = 0n; + const elRewards = 1n; + const simulatedSharesToBurn = 0n; + const sharesToBurn = 0n; + + await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn( + withdrawals, + elRewards, + simulatedSharesToBurn, + sharesToBurn, + ); - // // `Mock__RewardsWithdrawn` event is only emitted on the mock to verify - // // that `ElRewardsVault.withdrawRewards` was actually called - // await expect(lido.handleOracleReport(...report())).to.emit(elRewardsVault, "Mock__RewardsWithdrawn"); - // }); + // `Mock__RewardsWithdrawn` event is only emitted on the mock to verify + // that `ElRewardsVault.withdrawRewards` was actually called + await expect(accounting.handleOracleReport(report())).to.emit(elRewardsVault, "Mock__RewardsWithdrawn"); + }); - // it("Withdraws ether from `WithdrawalVault` if withdrawals are greater than 0 as returned from `smoothenTokenRebase`", async () => { - // const withdrawals = 1n; - // const elRewards = 0n; - // const simulatedSharesToBurn = 0n; - // const sharesToBurn = 0n; - - // await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn( - // withdrawals, - // elRewards, - // simulatedSharesToBurn, - // sharesToBurn, - // ); + it("Withdraws ether from `WithdrawalVault` if withdrawals are greater than 0 as returned from `smoothenTokenRebase`", async () => { + const withdrawals = 1n; + const elRewards = 0n; + const simulatedSharesToBurn = 0n; + const sharesToBurn = 0n; + + await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn( + withdrawals, + elRewards, + simulatedSharesToBurn, + sharesToBurn, + ); + const totalFee = 1000; + const precisionPoints = 10n ** 20n; + await stakingRouter.mock__getStakingRewardsDistribution([], [], [], totalFee, precisionPoints); - // // `Mock__WithdrawalsWithdrawn` event is only emitted on the mock to verify - // // that `WithdrawalVault.withdrawWithdrawals` was actually called - // await expect(lido.handleOracleReport(...report())).to.emit(withdrawalVault, "Mock__WithdrawalsWithdrawn"); - // }); + // `Mock__WithdrawalsWithdrawn` event is only emitted on the mock to verify + // that `WithdrawalVault.withdrawWithdrawals` was actually called + await expect(accounting.handleOracleReport(report())).to.emit(withdrawalVault, "Mock__WithdrawalsWithdrawn"); + }); - // it("Finalizes withdrawals if there is ether to lock on `WithdrawalQueue` as returned from `prefinalize`", async () => { - // const ethToLock = ether("10.0"); - // await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); - // // top up buffer via submit - // await lido.submit(ZeroAddress, { value: ethToLock }); + it("Finalizes withdrawals if there is ether to lock on `WithdrawalQueue` as returned from `prefinalize`", async () => { + const ethToLock = ether("10.0"); + await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); + // top up buffer via submit + await lido.submit(ZeroAddress, { value: ethToLock }); - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n, 2n], - // }), - // ), - // ).to.emit(withdrawalQueue, "WithdrawalsFinalized"); - // }); + await expect( + accounting.handleOracleReport( + report({ + withdrawalFinalizationBatches: [1n, 2n], + }), + ), + ).to.emit(withdrawalQueue, "WithdrawalsFinalized"); + }); - // it("Updates buffered ether", async () => { - // const initialBufferedEther = await lido.getBufferedEther(); - // const ethToLock = 1n; + it("Updates buffered ether", async () => { + const initialBufferedEther = await lido.getBufferedEther(); + const ethToLock = 1n; - // // assert that the buffer has enough eth to lock for withdrawals - // // should have some eth from the initial 0xdead holder - // expect(initialBufferedEther).greaterThanOrEqual(ethToLock); - // await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); + // assert that the buffer has enough eth to lock for withdrawals + // should have some eth from the initial 0xdead holder + expect(initialBufferedEther).greaterThanOrEqual(ethToLock); + await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ).to.not.be.reverted; + await expect( + accounting.handleOracleReport( + report({ + withdrawalFinalizationBatches: [1n], + }), + ), + ).to.not.be.reverted; - // expect(await lido.getBufferedEther()).to.equal(initialBufferedEther - ethToLock); - // }); + expect(await lido.getBufferedEther()).to.equal(initialBufferedEther - ethToLock); + }); - // it("Emits an `ETHDistributed` event", async () => { - // const reportTimestamp = await getNextBlockTimestamp(); - // const preClBalance = 0n; - // const clBalance = 1n; - // const withdrawals = 0n; - // const elRewards = 0n; - // const bufferedEther = await lido.getBufferedEther(); + it("Emits an `ETHDistributed` event", async () => { + const reportTimestamp = await getNextBlockTimestamp(); + const preClBalance = 0n; + const clBalance = 1n; + const withdrawals = 0n; + const elRewards = 0n; + const bufferedEther = await lido.getBufferedEther(); - // await expect( - // lido.handleOracleReport( - // ...report({ - // reportTimestamp: reportTimestamp, - // clBalance, - // }), - // ), - // ) - // .to.emit(lido, "ETHDistributed") - // .withArgs(reportTimestamp, preClBalance, clBalance, withdrawals, elRewards, bufferedEther); - // }); + const totalFee = 1000; + const precisionPoints = 10n ** 20n; + await stakingRouter.mock__getStakingRewardsDistribution([], [], [], totalFee, precisionPoints); - // it("Burns shares if there are shares to burn as returned from `smoothenTokenRebaseReturn`", async () => { - // const sharesRequestedToBurn = 1n; + await expect( + accounting.handleOracleReport( + report({ + timestamp: reportTimestamp, + clBalance, + }), + ), + ) + .to.emit(lido, "ETHDistributed") + .withArgs(reportTimestamp, preClBalance, clBalance, withdrawals, elRewards, bufferedEther); + }); - // await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn(0n, 0n, 0n, sharesRequestedToBurn); + // it("Burns shares if there are shares to burn as returned from `smoothenTokenRebaseReturn`", async () => { + // const sharesRequestedToBurn = 1n; - // // set up steth whale, in case we need to send steth to other accounts - // await setBalance(stethWhale.address, ether("101.0")); - // await lido.connect(stethWhale).submit(ZeroAddress, { value: ether("100.0") }); - // // top up Burner with steth to burn - // await lido.connect(stethWhale).transferShares(burner, sharesRequestedToBurn); + // await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn(0n, 0n, 0n, sharesRequestedToBurn); - // await expect( - // lido.handleOracleReport( - // ...report({ - // sharesRequestedToBurn, - // }), - // ), - // ) - // .to.emit(burner, "Mock__CommitSharesToBurnWasCalled") - // .and.to.emit(lido, "SharesBurnt") - // .withArgs(await burner.getAddress(), sharesRequestedToBurn, sharesRequestedToBurn, sharesRequestedToBurn); - // }); + // // set up steth whale, in case we need to send steth to other accounts + // await setBalance(stethWhale.address, ether("101.0")); + // await lido.connect(stethWhale).submit(ZeroAddress, { value: ether("100.0") }); + // // top up Burner with steth to burn + // await lido.connect(stethWhale).transferShares(burner, sharesRequestedToBurn); + + // await expect( + // accounting.handleOracleReport( + // report({ + // sharesRequestedToBurn, + // }), + // ), + // ) + // .to.emit(burner, "Mock__CommitSharesToBurnWasCalled") + // .and.to.emit(lido, "SharesBurnt") + // .withArgs(await burner.getAddress(), sharesRequestedToBurn, sharesRequestedToBurn, sharesRequestedToBurn); + // }); // it("Reverts if the number of reward recipients does not match the number of module fees as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { // // one recipient From e9afa5de3f78fbb300a7b8cbce3a4d0d5766e148 Mon Sep 17 00:00:00 2001 From: VP Date: Fri, 10 Jan 2025 10:49:50 +0100 Subject: [PATCH 11/27] test: fix mocks --- ...ReportSanityChecker__MockForAccounting.sol | 15 ------------- .../contracts/LidoLocator__MockMutable.sol | 21 +++++++++++-------- .../OracleReportSanityChecker__Mock.sol | 8 ------- .../oracle/OracleReportSanityCheckerMocks.sol | 8 ------- 4 files changed, 12 insertions(+), 40 deletions(-) diff --git a/test/0.4.24/contracts/OracleReportSanityChecker__MockForAccounting.sol b/test/0.4.24/contracts/OracleReportSanityChecker__MockForAccounting.sol index aeb260b7e..73280340c 100644 --- a/test/0.4.24/contracts/OracleReportSanityChecker__MockForAccounting.sol +++ b/test/0.4.24/contracts/OracleReportSanityChecker__MockForAccounting.sol @@ -6,7 +6,6 @@ pragma solidity 0.4.24; contract OracleReportSanityChecker__MockForAccounting { bool private checkAccountingOracleReportReverts; bool private checkWithdrawalQueueOracleReportReverts; - bool private checkSimulatedShareRateReverts; uint256 private _withdrawals; uint256 private _elRewards; @@ -54,16 +53,6 @@ contract OracleReportSanityChecker__MockForAccounting { sharesToBurn = _sharesToBurn; } - function checkSimulatedShareRate( - uint256 _postTotalPooledEther, - uint256 _postTotalShares, - uint256 _etherLockedOnWithdrawalQueue, - uint256 _sharesBurntDueToWithdrawals, - uint256 _simulatedShareRate - ) external view { - if (checkSimulatedShareRateReverts) revert(); - } - // mocking function mock__checkAccountingOracleReportReverts(bool reverts) external { @@ -74,10 +63,6 @@ contract OracleReportSanityChecker__MockForAccounting { checkWithdrawalQueueOracleReportReverts = reverts; } - function mock__checkSimulatedShareRateReverts(bool reverts) external { - checkSimulatedShareRateReverts = reverts; - } - function mock__smoothenTokenRebaseReturn( uint256 withdrawals, uint256 elRewards, diff --git a/test/0.8.9/contracts/LidoLocator__MockMutable.sol b/test/0.8.9/contracts/LidoLocator__MockMutable.sol index ead0d44e1..e102d2a4d 100644 --- a/test/0.8.9/contracts/LidoLocator__MockMutable.sol +++ b/test/0.8.9/contracts/LidoLocator__MockMutable.sol @@ -3,7 +3,9 @@ pragma solidity 0.8.9; -contract LidoLocator__MockMutable { +import {ILidoLocator} from "../../../contracts/common/interfaces/ILidoLocator.sol"; + +contract LidoLocator__MockMutable is ILidoLocator { struct Config { address accountingOracle; address depositSecurityModule; @@ -19,6 +21,8 @@ contract LidoLocator__MockMutable { address withdrawalQueue; address withdrawalVault; address oracleDaemonConfig; + address accounting; + address wstETH; } error ZeroAddress(); @@ -37,6 +41,8 @@ contract LidoLocator__MockMutable { address public immutable withdrawalQueue; address public immutable withdrawalVault; address public immutable oracleDaemonConfig; + address public immutable accounting; + address public immutable wstETH; /** * @notice declare service locations @@ -58,25 +64,22 @@ contract LidoLocator__MockMutable { withdrawalQueue = _assertNonZero(_config.withdrawalQueue); withdrawalVault = _assertNonZero(_config.withdrawalVault); oracleDaemonConfig = _assertNonZero(_config.oracleDaemonConfig); + accounting = _assertNonZero(_config.accounting); + wstETH = _assertNonZero(_config.wstETH); } function coreComponents() external view returns (address, address, address, address, address, address) { return (elRewardsVault, oracleReportSanityChecker, stakingRouter, treasury, withdrawalQueue, withdrawalVault); } - function oracleReportComponentsForLido() - external - view - returns (address, address, address, address, address, address, address) - { + function oracleReportComponents() external view returns (address, address, address, address, address, address) { return ( accountingOracle, - elRewardsVault, oracleReportSanityChecker, burner, withdrawalQueue, - withdrawalVault, - postTokenRebaseReceiver + postTokenRebaseReceiver, + stakingRouter ); } diff --git a/test/0.8.9/contracts/OracleReportSanityChecker__Mock.sol b/test/0.8.9/contracts/OracleReportSanityChecker__Mock.sol index a3ff27f95..906940c48 100644 --- a/test/0.8.9/contracts/OracleReportSanityChecker__Mock.sol +++ b/test/0.8.9/contracts/OracleReportSanityChecker__Mock.sol @@ -26,14 +26,6 @@ contract OracleReportSanityChecker__Mock { uint256 _reportTimestamp ) external view {} - function checkSimulatedShareRate( - uint256 _postTotalPooledEther, - uint256 _postTotalShares, - uint256 _etherLockedOnWithdrawalQueue, - uint256 _sharesBurntDueToWithdrawals, - uint256 _simulatedShareRate - ) external view {} - function smoothenTokenRebase( uint256, uint256, diff --git a/test/0.8.9/contracts/oracle/OracleReportSanityCheckerMocks.sol b/test/0.8.9/contracts/oracle/OracleReportSanityCheckerMocks.sol index 3fe1a880a..f10f278bd 100644 --- a/test/0.8.9/contracts/oracle/OracleReportSanityCheckerMocks.sol +++ b/test/0.8.9/contracts/oracle/OracleReportSanityCheckerMocks.sol @@ -125,14 +125,6 @@ contract OracleReportSanityCheckerStub { uint256 _reportTimestamp ) external view {} - function checkSimulatedShareRate( - uint256 _postTotalPooledEther, - uint256 _postTotalShares, - uint256 _etherLockedOnWithdrawalQueue, - uint256 _sharesBurntDueToWithdrawals, - uint256 _simulatedShareRate - ) external view {} - function smoothenTokenRebase( uint256, uint256, From 050bd27f3cbe470f00143067a0a41dfc02cfc801 Mon Sep 17 00:00:00 2001 From: VP Date: Fri, 10 Jan 2025 10:50:21 +0100 Subject: [PATCH 12/27] test: enable all relevant tests --- test/0.4.24/lido/lido.accounting.test.ts | 525 +++++++++++------------ 1 file changed, 253 insertions(+), 272 deletions(-) diff --git a/test/0.4.24/lido/lido.accounting.test.ts b/test/0.4.24/lido/lido.accounting.test.ts index 6579810c0..b0ad90032 100644 --- a/test/0.4.24/lido/lido.accounting.test.ts +++ b/test/0.4.24/lido/lido.accounting.test.ts @@ -3,7 +3,7 @@ import { ZeroAddress } from "ethers"; import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { getStorageAt } from "@nomicfoundation/hardhat-network-helpers"; +import { getStorageAt, setBalance } from "@nomicfoundation/hardhat-network-helpers"; import { Accounting, @@ -28,9 +28,9 @@ import { } from "typechain-types"; import { ReportValuesStruct } from "typechain-types/contracts/0.8.9/oracle/AccountingOracle.sol/IReportReceiver"; -import { ether, getNextBlockTimestamp, impersonate, streccak } from "lib"; +import { certainAddress, ether, getNextBlockTimestamp, impersonate, streccak } from "lib"; -import { deployLidoDao } from "test/deploy"; +import { deployLidoDao, updateLidoLocatorImplementation } from "test/deploy"; describe("Lido:accounting", () => { let deployer: HardhatEthersSigner; @@ -390,275 +390,256 @@ describe("Lido:accounting", () => { .withArgs(reportTimestamp, preClBalance, clBalance, withdrawals, elRewards, bufferedEther); }); - // it("Burns shares if there are shares to burn as returned from `smoothenTokenRebaseReturn`", async () => { - // const sharesRequestedToBurn = 1n; - - // await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn(0n, 0n, 0n, sharesRequestedToBurn); - - // // set up steth whale, in case we need to send steth to other accounts - // await setBalance(stethWhale.address, ether("101.0")); - // await lido.connect(stethWhale).submit(ZeroAddress, { value: ether("100.0") }); - // // top up Burner with steth to burn - // await lido.connect(stethWhale).transferShares(burner, sharesRequestedToBurn); - - // await expect( - // accounting.handleOracleReport( - // report({ - // sharesRequestedToBurn, - // }), - // ), - // ) - // .to.emit(burner, "Mock__CommitSharesToBurnWasCalled") - // .and.to.emit(lido, "SharesBurnt") - // .withArgs(await burner.getAddress(), sharesRequestedToBurn, sharesRequestedToBurn, sharesRequestedToBurn); - // }); + it("Burns shares if there are shares to burn as returned from `smoothenTokenRebaseReturn`", async () => { + const sharesRequestedToBurn = 1n; + + await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn(0n, 0n, 0n, sharesRequestedToBurn); + + // set up steth whale, in case we need to send steth to other accounts + await setBalance(stethWhale.address, ether("101.0")); + await lido.connect(stethWhale).submit(ZeroAddress, { value: ether("100.0") }); + // top up Burner with steth to burn + await lido.connect(stethWhale).transferShares(burner, sharesRequestedToBurn); + + await expect( + accounting.handleOracleReport( + report({ + sharesRequestedToBurn, + }), + ), + ).to.emit(burner, "Mock__CommitSharesToBurnWasCalled"); + + // TODO: SharesBurnt event is not emitted anymore because of the mock implementation + // .and.to.emit(lido, "SharesBurnt") + // .withArgs(await burner.getAddress(), sharesRequestedToBurn, sharesRequestedToBurn, sharesRequestedToBurn); + }); + + it("Reverts if the number of reward recipients does not match the number of module fees as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { + // one recipient + const recipients = [certainAddress("lido:handleOracleReport:single-recipient")]; + const modulesIds = [1n, 2n]; + // but two module fees + const moduleFees = [500n, 500n]; + const totalFee = 1000; + const precisionPoints = 10n ** 20n; + + await stakingRouter.mock__getStakingRewardsDistribution( + recipients, + modulesIds, + moduleFees, + totalFee, + precisionPoints, + ); + + await expect( + accounting.handleOracleReport( + report({ + clBalance: 1n, // made 1 wei of profit, trigers reward processing + }), + ), + ) + .to.be.revertedWithCustomError(accounting, "UnequalArrayLengths") + .withArgs(1, 2); + }); + + it("Reverts if the number of module ids does not match the number of module fees as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { + const recipients = [ + certainAddress("lido:handleOracleReport:recipient1"), + certainAddress("lido:handleOracleReport:recipient2"), + ]; + // one module id + const modulesIds = [1n]; + // but two module fees + const moduleFees = [500n, 500n]; + const totalFee = 1000; + const precisionPoints = 10n ** 20n; + + await stakingRouter.mock__getStakingRewardsDistribution( + recipients, + modulesIds, + moduleFees, + totalFee, + precisionPoints, + ); + + await expect( + accounting.handleOracleReport( + report({ + clBalance: 1n, // made 1 wei of profit, trigers reward processing + }), + ), + ) + .to.be.revertedWithCustomError(accounting, "UnequalArrayLengths") + .withArgs(1, 2); + }); + + it("Does not mint and transfer any shares if the total fee is zero as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { + // single staking module + const recipients = [certainAddress("lido:handleOracleReport:recipient")]; + const modulesIds = [1n]; + const moduleFees = [500n]; + // fee is 0 + const totalFee = 0; + const precisionPoints = 10n ** 20n; + + await stakingRouter.mock__getStakingRewardsDistribution( + recipients, + modulesIds, + moduleFees, + totalFee, + precisionPoints, + ); + + await expect( + accounting.handleOracleReport( + report({ + clBalance: 1n, + }), + ), + ) + .not.to.emit(lido, "Transfer") + .and.not.to.emit(lido, "TransferShares") + .and.not.to.emit(stakingRouter, "Mock__MintedRewardsReported"); + }); + + it("Mints shares to itself and then transfers them to recipients if there are fees to distribute as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { + // initially, before any rebases, one share costs one steth + expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); + // thus, the total supply of steth should equal the total number of shares + expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); + + // mock a single staking module with 5% fee with the total protocol fee of 10% + const stakingModule = { + address: certainAddress("lido:handleOracleReport:staking-module"), + id: 1n, + fee: 5n * 10n ** 18n, // 5% + }; - // it("Reverts if the number of reward recipients does not match the number of module fees as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { - // // one recipient - // const recipients = [certainAddress("lido:handleOracleReport:single-recipient")]; - // const modulesIds = [1n, 2n]; - // // but two module fees - // const moduleFees = [500n, 500n]; - // const totalFee = 1000; - // const precisionPoints = 10n ** 20n; - - // await stakingRouter.mock__getStakingRewardsDistribution( - // recipients, - // modulesIds, - // moduleFees, - // totalFee, - // precisionPoints, - // ); - - // await expect( - // lido.handleOracleReport( - // ...report({ - // clBalance: 1n, // made 1 wei of profit, trigers reward processing - // }), - // ), - // ).to.be.revertedWith("WRONG_RECIPIENTS_INPUT"); - // }); - - // it("Reverts if the number of module ids does not match the number of module fees as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { - // const recipients = [ - // certainAddress("lido:handleOracleReport:recipient1"), - // certainAddress("lido:handleOracleReport:recipient2"), - // ]; - // // one module id - // const modulesIds = [1n]; - // // but two module fees - // const moduleFees = [500n, 500n]; - // const totalFee = 1000; - // const precisionPoints = 10n ** 20n; - - // await stakingRouter.mock__getStakingRewardsDistribution( - // recipients, - // modulesIds, - // moduleFees, - // totalFee, - // precisionPoints, - // ); - - // await expect( - // lido.handleOracleReport( - // ...report({ - // clBalance: 1n, // made 1 wei of profit, trigers reward processing - // }), - // ), - // ).to.be.revertedWith("WRONG_MODULE_IDS_INPUT"); - // }); - - // it("Does not mint and transfer any shares if the total fee is zero as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { - // // single staking module - // const recipients = [certainAddress("lido:handleOracleReport:recipient")]; - // const modulesIds = [1n]; - // const moduleFees = [500n]; - // // fee is 0 - // const totalFee = 0; - // const precisionPoints = 10n ** 20n; - - // await stakingRouter.mock__getStakingRewardsDistribution( - // recipients, - // modulesIds, - // moduleFees, - // totalFee, - // precisionPoints, - // ); - - // await expect( - // lido.handleOracleReport( - // ...report({ - // clBalance: 1n, - // }), - // ), - // ) - // .not.to.emit(lido, "Transfer") - // .and.not.to.emit(lido, "TransferShares") - // .and.not.to.emit(stakingRouter, "Mock__MintedRewardsReported"); - // }); - - // it("Mints shares to itself and then transfers them to recipients if there are fees to distribute as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { - // // initially, before any rebases, one share costs one steth - // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); - // // thus, the total supply of steth should equal the total number of shares - // expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); - - // // mock a single staking module with 5% fee with the total protocol fee of 10% - // const stakingModule = { - // address: certainAddress("lido:handleOracleReport:staking-module"), - // id: 1n, - // fee: 5n * 10n ** 18n, // 5% - // }; - - // const totalFee = 10n * 10n ** 18n; // 10% - // const precisionPoints = 100n * 10n ** 18n; // 100% - - // await stakingRouter.mock__getStakingRewardsDistribution( - // [stakingModule.address], - // [stakingModule.id], - // [stakingModule.fee], - // totalFee, - // precisionPoints, - // ); - - // const clBalance = ether("1.0"); - - // const expectedSharesToMint = - // (clBalance * totalFee * (await lido.getTotalShares())) / - // (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); - - // const expectedModuleRewardInShares = expectedSharesToMint / (totalFee / stakingModule.fee); - // const expectedTreasuryCutInShares = expectedSharesToMint - expectedModuleRewardInShares; - - // await expect( - // lido.handleOracleReport( - // ...report({ - // clBalance: ether("1.0"), // 1 ether of profit - // }), - // ), - // ) - // .to.emit(lido, "TransferShares") - // .withArgs(ZeroAddress, stakingModule.address, expectedModuleRewardInShares) - // .and.to.emit(lido, "TransferShares") - // .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) - // .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); - - // expect(await lido.balanceOf(stakingModule.address)).to.equal( - // await lido.getPooledEthByShares(expectedModuleRewardInShares), - // ); - - // expect(await lido.balanceOf(await lido.getTreasury())).to.equal( - // await lido.getPooledEthByShares(expectedTreasuryCutInShares), - // ); - - // // now one share should cost 1.9 steth (10% was distributed as rewards) - // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); - // }); - - // it("Transfers all new shares to treasury if the module fee is zero as returned `StakingRouter.getStakingRewardsDistribution`", async () => { - // // initially, before any rebases, one share costs one steth - // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); - // // thus, the total supply of steth should equal the total number of shares - // expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); - - // // mock a single staking module with 0% fee with the total protocol fee of 10% - // const stakingModule = { - // address: certainAddress("lido:handleOracleReport:staking-module"), - // id: 1n, - // fee: 0n, - // }; - - // const totalFee = 10n * 10n ** 18n; // 10% - // const precisionPoints = 100n * 10n ** 18n; // 100% - - // await stakingRouter.mock__getStakingRewardsDistribution( - // [stakingModule.address], - // [stakingModule.id], - // [stakingModule.fee], - // totalFee, - // precisionPoints, - // ); - - // const clBalance = ether("1.0"); - - // const expectedSharesToMint = - // (clBalance * totalFee * (await lido.getTotalShares())) / - // (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); - - // const expectedModuleRewardInShares = 0n; - // const expectedTreasuryCutInShares = expectedSharesToMint; - - // await expect( - // lido.handleOracleReport( - // ...report({ - // clBalance: ether("1.0"), // 1 ether of profit - // }), - // ), - // ) - // .and.to.emit(lido, "TransferShares") - // .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) - // .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); - - // expect(await lido.balanceOf(stakingModule.address)).to.equal( - // await lido.getPooledEthByShares(expectedModuleRewardInShares), - // ); - - // expect(await lido.balanceOf(await lido.getTreasury())).to.equal( - // await lido.getPooledEthByShares(expectedTreasuryCutInShares), - // ); - - // // now one share should cost 1.9 steth (10% was distributed as rewards) - // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); - // }); - - // it("Relays the report data to `PostTokenRebaseReceiver`", async () => { - // await expect(lido.handleOracleReport(...report())).to.emit( - // postTokenRebaseReceiver, - // "Mock__PostTokenRebaseHandled", - // ); - // }); - - // it("Does not relay the report data to `PostTokenRebaseReceiver` if the locator returns zero address", async () => { - // const lidoLocatorAddress = await lido.getLidoLocator(); - - // // Change the locator implementation to support zero address - // await updateLidoLocatorImplementation(lidoLocatorAddress, {}, "LidoLocator__MutableMock", deployer); - // const locatorMutable = await ethers.getContractAt("LidoLocator__MutableMock", lidoLocatorAddress, deployer); - // await locatorMutable.mock___updatePostTokenRebaseReceiver(ZeroAddress); - - // expect(await locator.postTokenRebaseReceiver()).to.equal(ZeroAddress); - - // const accountingOracleAddress = await locator.accountingOracle(); - // const accountingOracle = await impersonate(accountingOracleAddress, ether("1000.0")); - - // await expect(lido.connect(accountingOracle).handleOracleReport(...report())).not.to.emit( - // postTokenRebaseReceiver, - // "Mock__PostTokenRebaseHandled", - // ); - // }); - - // it("Reverts if there are withdrawal batches submitted and `checkSimulatedShareRate` fails", async () => { - // await oracleReportSanityChecker.mock__checkSimulatedShareRateReverts(true); - - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ).to.be.reverted; - // }); - - // it("Does not revert if there are no withdrawal batches submitted but `checkSimulatedShareRate` fails", async () => { - // await oracleReportSanityChecker.mock__checkSimulatedShareRateReverts(true); - - // await expect(lido.handleOracleReport(...report())).not.to.be.reverted; - // }); - - // it("Returns post-rebase state", async () => { - // const postRebaseState = await lido.handleOracleReport.staticCall(...report()); - - // expect(postRebaseState).to.deep.equal([await lido.getTotalPooledEther(), await lido.getTotalShares(), 0n, 0n]); - // }); + const totalFee = 10n * 10n ** 18n; // 10% + const precisionPoints = 100n * 10n ** 18n; // 100% + + await stakingRouter.mock__getStakingRewardsDistribution( + [stakingModule.address], + [stakingModule.id], + [stakingModule.fee], + totalFee, + precisionPoints, + ); + + const clBalance = ether("1.0"); + + const expectedSharesToMint = + (clBalance * totalFee * (await lido.getTotalShares())) / + (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); + + const expectedModuleRewardInShares = expectedSharesToMint / (totalFee / stakingModule.fee); + const expectedTreasuryCutInShares = expectedSharesToMint - expectedModuleRewardInShares; + + await expect( + accounting.handleOracleReport( + report({ + clBalance: ether("1.0"), // 1 ether of profit + }), + ), + ) + .to.emit(lido, "TransferShares") + .withArgs(ZeroAddress, stakingModule.address, expectedModuleRewardInShares) + .and.to.emit(lido, "TransferShares") + .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) + .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); + + expect(await lido.balanceOf(stakingModule.address)).to.equal( + await lido.getPooledEthByShares(expectedModuleRewardInShares), + ); + + expect(await lido.balanceOf(await lido.getTreasury())).to.equal( + await lido.getPooledEthByShares(expectedTreasuryCutInShares), + ); + + // now one share should cost 1.9 steth (10% was distributed as rewards) + expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); + }); + + it("Transfers all new shares to treasury if the module fee is zero as returned `StakingRouter.getStakingRewardsDistribution`", async () => { + // initially, before any rebases, one share costs one steth + expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); + // thus, the total supply of steth should equal the total number of shares + expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); + + // mock a single staking module with 0% fee with the total protocol fee of 10% + const stakingModule = { + address: certainAddress("lido:handleOracleReport:staking-module"), + id: 1n, + fee: 0n, + }; + + const totalFee = 10n * 10n ** 18n; // 10% + const precisionPoints = 100n * 10n ** 18n; // 100% + + await stakingRouter.mock__getStakingRewardsDistribution( + [stakingModule.address], + [stakingModule.id], + [stakingModule.fee], + totalFee, + precisionPoints, + ); + + const clBalance = ether("1.0"); + + const expectedSharesToMint = + (clBalance * totalFee * (await lido.getTotalShares())) / + (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); + + const expectedModuleRewardInShares = 0n; + const expectedTreasuryCutInShares = expectedSharesToMint; + + await expect( + accounting.handleOracleReport( + report({ + clBalance: ether("1.0"), // 1 ether of profit + }), + ), + ) + .and.to.emit(lido, "TransferShares") + .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) + .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); + + expect(await lido.balanceOf(stakingModule.address)).to.equal( + await lido.getPooledEthByShares(expectedModuleRewardInShares), + ); + + expect(await lido.balanceOf(await lido.getTreasury())).to.equal( + await lido.getPooledEthByShares(expectedTreasuryCutInShares), + ); + + // now one share should cost 1.9 steth (10% was distributed as rewards) + expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); + }); + + it("Relays the report data to `PostTokenRebaseReceiver`", async () => { + await expect(accounting.handleOracleReport(report())).to.emit( + postTokenRebaseReceiver, + "Mock__PostTokenRebaseHandled", + ); + }); + + it("Does not relay the report data to `PostTokenRebaseReceiver` if the locator returns zero address", async () => { + const lidoLocatorAddress = await lido.getLidoLocator(); + + // Change the locator implementation to support zero address + await updateLidoLocatorImplementation(lidoLocatorAddress, {}, "LidoLocator__MockMutable", deployer); + const locatorMutable = await ethers.getContractAt("LidoLocator__MockMutable", lidoLocatorAddress, deployer); + await locatorMutable.mock___updatePostTokenRebaseReceiver(ZeroAddress); + + expect(await locator.postTokenRebaseReceiver()).to.equal(ZeroAddress); + + const accountingOracleAddress = await locator.accountingOracle(); + const accountingOracle = await impersonate(accountingOracleAddress, ether("1000.0")); + + await expect(accounting.connect(accountingOracle).handleOracleReport(report())).not.to.emit( + postTokenRebaseReceiver, + "Mock__PostTokenRebaseHandled", + ); + }); }); }); From d45668000c07621a2cb5829189eeba78a68e2750 Mon Sep 17 00:00:00 2001 From: VP Date: Fri, 10 Jan 2025 10:56:04 +0100 Subject: [PATCH 13/27] chore: split tests according to contracts --- test/0.4.24/lido/lido.accounting.test.ts | 480 +------ .../accounting.handleOracleReport.test.ts | 1207 ++++++++--------- 2 files changed, 559 insertions(+), 1128 deletions(-) diff --git a/test/0.4.24/lido/lido.accounting.test.ts b/test/0.4.24/lido/lido.accounting.test.ts index b0ad90032..9a5f2e430 100644 --- a/test/0.4.24/lido/lido.accounting.test.ts +++ b/test/0.4.24/lido/lido.accounting.test.ts @@ -1,9 +1,7 @@ import { expect } from "chai"; -import { ZeroAddress } from "ethers"; import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { getStorageAt, setBalance } from "@nomicfoundation/hardhat-network-helpers"; import { Accounting, @@ -14,8 +12,6 @@ import { Lido, LidoExecutionLayerRewardsVault__MockForLidoAccounting, LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory, - LidoLocator, - LidoLocator__factory, OracleReportSanityChecker__MockForAccounting, OracleReportSanityChecker__MockForAccounting__factory, PostTokenRebaseReceiver__MockForAccounting__factory, @@ -26,22 +22,19 @@ import { WithdrawalVault__MockForLidoAccounting, WithdrawalVault__MockForLidoAccounting__factory, } from "typechain-types"; -import { ReportValuesStruct } from "typechain-types/contracts/0.8.9/oracle/AccountingOracle.sol/IReportReceiver"; -import { certainAddress, ether, getNextBlockTimestamp, impersonate, streccak } from "lib"; +import { ether, impersonate } from "lib"; -import { deployLidoDao, updateLidoLocatorImplementation } from "test/deploy"; +import { deployLidoDao } from "test/deploy"; describe("Lido:accounting", () => { let deployer: HardhatEthersSigner; let stranger: HardhatEthersSigner; - let stethWhale: HardhatEthersSigner; let lido: Lido; let acl: ACL; let accounting: Accounting; let postTokenRebaseReceiver: IPostTokenRebaseReceiver; - let locator: LidoLocator; let elRewardsVault: LidoExecutionLayerRewardsVault__MockForLidoAccounting; let withdrawalVault: WithdrawalVault__MockForLidoAccounting; @@ -51,9 +44,7 @@ describe("Lido:accounting", () => { let burner: Burner__MockForAccounting; beforeEach(async () => { - // [deployer, accounting, stethWhale, stranger, withdrawalQueue] = await ethers.getSigners(); - [deployer, stranger, stethWhale] = await ethers.getSigners(); - stethWhale; + [deployer, stranger] = await ethers.getSigners(); [ elRewardsVault, @@ -87,11 +78,6 @@ describe("Lido:accounting", () => { }, })); - locator = LidoLocator__factory.connect(await lido.getLidoLocator(), deployer); - - const accountingOracleSigner = await impersonate(await locator.accountingOracle(), ether("100.0")); - accounting = accounting.connect(accountingOracleSigner); - await acl.createPermission(deployer, lido, await lido.RESUME_ROLE(), deployer); await acl.createPermission(deployer, lido, await lido.PAUSE_ROLE(), deployer); await acl.createPermission(deployer, lido, await lido.UNSAFE_CHANGE_DEPOSITED_VALIDATORS_ROLE(), deployer); @@ -182,464 +168,4 @@ describe("Lido:accounting", () => { }) as ArgsTuple; } }); - - context("handleOracleReport", () => { - it("Update CL validators count if reported more", async () => { - let depositedValidators = 100n; - await lido.connect(deployer).unsafeChangeDepositedValidators(depositedValidators); - - // first report, 100 validators - await accounting.handleOracleReport( - report({ - clValidators: depositedValidators, - }), - ); - - const slot = streccak("lido.Lido.beaconValidators"); - const lidoAddress = await lido.getAddress(); - - let clValidatorsPosition = await getStorageAt(lidoAddress, slot); - expect(clValidatorsPosition).to.equal(depositedValidators); - - depositedValidators = 101n; - await lido.connect(deployer).unsafeChangeDepositedValidators(depositedValidators); - - // second report, 101 validators - await accounting.handleOracleReport( - report({ - clValidators: depositedValidators, - }), - ); - - clValidatorsPosition = await getStorageAt(lidoAddress, slot); - expect(clValidatorsPosition).to.equal(depositedValidators); - }); - - function report(overrides?: Partial): ReportValuesStruct { - return { - timestamp: 0n, - timeElapsed: 0n, - clValidators: 0n, - clBalance: 0n, - withdrawalVaultBalance: 0n, - elRewardsVaultBalance: 0n, - sharesRequestedToBurn: 0n, - withdrawalFinalizationBatches: [], - vaultValues: [], - netCashFlows: [], - ...overrides, - }; - } - - it("Reverts if the `checkAccountingOracleReport` sanity check fails", async () => { - await oracleReportSanityChecker.mock__checkAccountingOracleReportReverts(true); - - await expect(accounting.handleOracleReport(report())).to.be.reverted; - }); - - it("Reverts if the `checkWithdrawalQueueOracleReport` sanity check fails", async () => { - await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); - await expect( - accounting.handleOracleReport( - report({ - withdrawalFinalizationBatches: [1n], - }), - ), - ).to.be.reverted; - }); - - it("Does not revert if the `checkWithdrawalQueueOracleReport` sanity check fails but no withdrawal batches were reported", async () => { - await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); - await withdrawalQueue.mock__isPaused(true); - - await expect(accounting.handleOracleReport(report())).not.to.be.reverted; - }); - - /// NOTE: This test is not applicable to the current implementation (Accounting's _checkAccountingOracleReport() checks for checkWithdrawalQueueOracleReport() - /// explicitly in case _report.withdrawalFinalizationBatches.length > 0 - // it("Does not revert if the `checkWithdrawalQueueOracleReport` sanity check fails but `withdrawalQueue` is paused", async () => { - // await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); - // await withdrawalQueue.mock__isPaused(true); - - // await expect(accounting.handleOracleReport(report({ withdrawalFinalizationBatches: [1n] }))).not.to.be.reverted; - // }); - - it("Does not emit `StETHBurnRequested` if there are no shares to burn", async () => { - await expect( - accounting.handleOracleReport( - report({ - withdrawalFinalizationBatches: [1n], - }), - ), - ).not.to.emit(burner, "StETHBurnRequested"); - }); - - it("Emits `StETHBurnRequested` if there are shares to burn", async () => { - const sharesToBurn = 1n; - const isCover = false; - const steth = 1n * 2n; // imitating 1:2 rate, see Burner `mock__prefinalizeReturn` - - await withdrawalQueue.mock__prefinalizeReturn(0n, sharesToBurn); - - await expect( - accounting.handleOracleReport( - report({ - withdrawalFinalizationBatches: [1n], - }), - ), - ) - .to.emit(burner, "StETHBurnRequested") - .withArgs(isCover, await accounting.getAddress(), steth, sharesToBurn); - }); - - it("Withdraws ether from `ElRewardsVault` if EL rewards are greater than 0 as returned from `smoothenTokenRebase`", async () => { - const withdrawals = 0n; - const elRewards = 1n; - const simulatedSharesToBurn = 0n; - const sharesToBurn = 0n; - - await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn( - withdrawals, - elRewards, - simulatedSharesToBurn, - sharesToBurn, - ); - - // `Mock__RewardsWithdrawn` event is only emitted on the mock to verify - // that `ElRewardsVault.withdrawRewards` was actually called - await expect(accounting.handleOracleReport(report())).to.emit(elRewardsVault, "Mock__RewardsWithdrawn"); - }); - - it("Withdraws ether from `WithdrawalVault` if withdrawals are greater than 0 as returned from `smoothenTokenRebase`", async () => { - const withdrawals = 1n; - const elRewards = 0n; - const simulatedSharesToBurn = 0n; - const sharesToBurn = 0n; - - await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn( - withdrawals, - elRewards, - simulatedSharesToBurn, - sharesToBurn, - ); - const totalFee = 1000; - const precisionPoints = 10n ** 20n; - await stakingRouter.mock__getStakingRewardsDistribution([], [], [], totalFee, precisionPoints); - - // `Mock__WithdrawalsWithdrawn` event is only emitted on the mock to verify - // that `WithdrawalVault.withdrawWithdrawals` was actually called - await expect(accounting.handleOracleReport(report())).to.emit(withdrawalVault, "Mock__WithdrawalsWithdrawn"); - }); - - it("Finalizes withdrawals if there is ether to lock on `WithdrawalQueue` as returned from `prefinalize`", async () => { - const ethToLock = ether("10.0"); - await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); - // top up buffer via submit - await lido.submit(ZeroAddress, { value: ethToLock }); - - await expect( - accounting.handleOracleReport( - report({ - withdrawalFinalizationBatches: [1n, 2n], - }), - ), - ).to.emit(withdrawalQueue, "WithdrawalsFinalized"); - }); - - it("Updates buffered ether", async () => { - const initialBufferedEther = await lido.getBufferedEther(); - const ethToLock = 1n; - - // assert that the buffer has enough eth to lock for withdrawals - // should have some eth from the initial 0xdead holder - expect(initialBufferedEther).greaterThanOrEqual(ethToLock); - await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); - - await expect( - accounting.handleOracleReport( - report({ - withdrawalFinalizationBatches: [1n], - }), - ), - ).to.not.be.reverted; - - expect(await lido.getBufferedEther()).to.equal(initialBufferedEther - ethToLock); - }); - - it("Emits an `ETHDistributed` event", async () => { - const reportTimestamp = await getNextBlockTimestamp(); - const preClBalance = 0n; - const clBalance = 1n; - const withdrawals = 0n; - const elRewards = 0n; - const bufferedEther = await lido.getBufferedEther(); - - const totalFee = 1000; - const precisionPoints = 10n ** 20n; - await stakingRouter.mock__getStakingRewardsDistribution([], [], [], totalFee, precisionPoints); - - await expect( - accounting.handleOracleReport( - report({ - timestamp: reportTimestamp, - clBalance, - }), - ), - ) - .to.emit(lido, "ETHDistributed") - .withArgs(reportTimestamp, preClBalance, clBalance, withdrawals, elRewards, bufferedEther); - }); - - it("Burns shares if there are shares to burn as returned from `smoothenTokenRebaseReturn`", async () => { - const sharesRequestedToBurn = 1n; - - await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn(0n, 0n, 0n, sharesRequestedToBurn); - - // set up steth whale, in case we need to send steth to other accounts - await setBalance(stethWhale.address, ether("101.0")); - await lido.connect(stethWhale).submit(ZeroAddress, { value: ether("100.0") }); - // top up Burner with steth to burn - await lido.connect(stethWhale).transferShares(burner, sharesRequestedToBurn); - - await expect( - accounting.handleOracleReport( - report({ - sharesRequestedToBurn, - }), - ), - ).to.emit(burner, "Mock__CommitSharesToBurnWasCalled"); - - // TODO: SharesBurnt event is not emitted anymore because of the mock implementation - // .and.to.emit(lido, "SharesBurnt") - // .withArgs(await burner.getAddress(), sharesRequestedToBurn, sharesRequestedToBurn, sharesRequestedToBurn); - }); - - it("Reverts if the number of reward recipients does not match the number of module fees as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { - // one recipient - const recipients = [certainAddress("lido:handleOracleReport:single-recipient")]; - const modulesIds = [1n, 2n]; - // but two module fees - const moduleFees = [500n, 500n]; - const totalFee = 1000; - const precisionPoints = 10n ** 20n; - - await stakingRouter.mock__getStakingRewardsDistribution( - recipients, - modulesIds, - moduleFees, - totalFee, - precisionPoints, - ); - - await expect( - accounting.handleOracleReport( - report({ - clBalance: 1n, // made 1 wei of profit, trigers reward processing - }), - ), - ) - .to.be.revertedWithCustomError(accounting, "UnequalArrayLengths") - .withArgs(1, 2); - }); - - it("Reverts if the number of module ids does not match the number of module fees as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { - const recipients = [ - certainAddress("lido:handleOracleReport:recipient1"), - certainAddress("lido:handleOracleReport:recipient2"), - ]; - // one module id - const modulesIds = [1n]; - // but two module fees - const moduleFees = [500n, 500n]; - const totalFee = 1000; - const precisionPoints = 10n ** 20n; - - await stakingRouter.mock__getStakingRewardsDistribution( - recipients, - modulesIds, - moduleFees, - totalFee, - precisionPoints, - ); - - await expect( - accounting.handleOracleReport( - report({ - clBalance: 1n, // made 1 wei of profit, trigers reward processing - }), - ), - ) - .to.be.revertedWithCustomError(accounting, "UnequalArrayLengths") - .withArgs(1, 2); - }); - - it("Does not mint and transfer any shares if the total fee is zero as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { - // single staking module - const recipients = [certainAddress("lido:handleOracleReport:recipient")]; - const modulesIds = [1n]; - const moduleFees = [500n]; - // fee is 0 - const totalFee = 0; - const precisionPoints = 10n ** 20n; - - await stakingRouter.mock__getStakingRewardsDistribution( - recipients, - modulesIds, - moduleFees, - totalFee, - precisionPoints, - ); - - await expect( - accounting.handleOracleReport( - report({ - clBalance: 1n, - }), - ), - ) - .not.to.emit(lido, "Transfer") - .and.not.to.emit(lido, "TransferShares") - .and.not.to.emit(stakingRouter, "Mock__MintedRewardsReported"); - }); - - it("Mints shares to itself and then transfers them to recipients if there are fees to distribute as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { - // initially, before any rebases, one share costs one steth - expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); - // thus, the total supply of steth should equal the total number of shares - expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); - - // mock a single staking module with 5% fee with the total protocol fee of 10% - const stakingModule = { - address: certainAddress("lido:handleOracleReport:staking-module"), - id: 1n, - fee: 5n * 10n ** 18n, // 5% - }; - - const totalFee = 10n * 10n ** 18n; // 10% - const precisionPoints = 100n * 10n ** 18n; // 100% - - await stakingRouter.mock__getStakingRewardsDistribution( - [stakingModule.address], - [stakingModule.id], - [stakingModule.fee], - totalFee, - precisionPoints, - ); - - const clBalance = ether("1.0"); - - const expectedSharesToMint = - (clBalance * totalFee * (await lido.getTotalShares())) / - (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); - - const expectedModuleRewardInShares = expectedSharesToMint / (totalFee / stakingModule.fee); - const expectedTreasuryCutInShares = expectedSharesToMint - expectedModuleRewardInShares; - - await expect( - accounting.handleOracleReport( - report({ - clBalance: ether("1.0"), // 1 ether of profit - }), - ), - ) - .to.emit(lido, "TransferShares") - .withArgs(ZeroAddress, stakingModule.address, expectedModuleRewardInShares) - .and.to.emit(lido, "TransferShares") - .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) - .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); - - expect(await lido.balanceOf(stakingModule.address)).to.equal( - await lido.getPooledEthByShares(expectedModuleRewardInShares), - ); - - expect(await lido.balanceOf(await lido.getTreasury())).to.equal( - await lido.getPooledEthByShares(expectedTreasuryCutInShares), - ); - - // now one share should cost 1.9 steth (10% was distributed as rewards) - expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); - }); - - it("Transfers all new shares to treasury if the module fee is zero as returned `StakingRouter.getStakingRewardsDistribution`", async () => { - // initially, before any rebases, one share costs one steth - expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); - // thus, the total supply of steth should equal the total number of shares - expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); - - // mock a single staking module with 0% fee with the total protocol fee of 10% - const stakingModule = { - address: certainAddress("lido:handleOracleReport:staking-module"), - id: 1n, - fee: 0n, - }; - - const totalFee = 10n * 10n ** 18n; // 10% - const precisionPoints = 100n * 10n ** 18n; // 100% - - await stakingRouter.mock__getStakingRewardsDistribution( - [stakingModule.address], - [stakingModule.id], - [stakingModule.fee], - totalFee, - precisionPoints, - ); - - const clBalance = ether("1.0"); - - const expectedSharesToMint = - (clBalance * totalFee * (await lido.getTotalShares())) / - (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); - - const expectedModuleRewardInShares = 0n; - const expectedTreasuryCutInShares = expectedSharesToMint; - - await expect( - accounting.handleOracleReport( - report({ - clBalance: ether("1.0"), // 1 ether of profit - }), - ), - ) - .and.to.emit(lido, "TransferShares") - .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) - .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); - - expect(await lido.balanceOf(stakingModule.address)).to.equal( - await lido.getPooledEthByShares(expectedModuleRewardInShares), - ); - - expect(await lido.balanceOf(await lido.getTreasury())).to.equal( - await lido.getPooledEthByShares(expectedTreasuryCutInShares), - ); - - // now one share should cost 1.9 steth (10% was distributed as rewards) - expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); - }); - - it("Relays the report data to `PostTokenRebaseReceiver`", async () => { - await expect(accounting.handleOracleReport(report())).to.emit( - postTokenRebaseReceiver, - "Mock__PostTokenRebaseHandled", - ); - }); - - it("Does not relay the report data to `PostTokenRebaseReceiver` if the locator returns zero address", async () => { - const lidoLocatorAddress = await lido.getLidoLocator(); - - // Change the locator implementation to support zero address - await updateLidoLocatorImplementation(lidoLocatorAddress, {}, "LidoLocator__MockMutable", deployer); - const locatorMutable = await ethers.getContractAt("LidoLocator__MockMutable", lidoLocatorAddress, deployer); - await locatorMutable.mock___updatePostTokenRebaseReceiver(ZeroAddress); - - expect(await locator.postTokenRebaseReceiver()).to.equal(ZeroAddress); - - const accountingOracleAddress = await locator.accountingOracle(); - const accountingOracle = await impersonate(accountingOracleAddress, ether("1000.0")); - - await expect(accounting.connect(accountingOracle).handleOracleReport(report())).not.to.emit( - postTokenRebaseReceiver, - "Mock__PostTokenRebaseHandled", - ); - }); - }); }); diff --git a/test/0.8.9/accounting.handleOracleReport.test.ts b/test/0.8.9/accounting.handleOracleReport.test.ts index 540bb98b2..70b09f683 100644 --- a/test/0.8.9/accounting.handleOracleReport.test.ts +++ b/test/0.8.9/accounting.handleOracleReport.test.ts @@ -1,652 +1,557 @@ -// import { expect } from "chai"; -// import { BigNumberish, ZeroAddress } from "ethers"; -// import { ethers } from "hardhat"; -// -// import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -// import { getStorageAt, setBalance } from "@nomicfoundation/hardhat-network-helpers"; -// -// import { -// ACL, -// Burner__MockForAccounting, -// Lido, -// LidoExecutionLayerRewardsVault__MockForLidoAccounting, -// LidoLocator, -// OracleReportSanityChecker__MockForAccounting, -// PostTokenRebaseReceiver__MockForAccounting, -// StakingRouter__MockForLidoAccounting, -// WithdrawalQueue__MockForAccounting, -// WithdrawalVault__MockForLidoAccounting, -// } from "typechain-types"; -// -// import { certainAddress, ether, getNextBlockTimestamp, impersonate, streccak } from "lib"; -// -// import { deployLidoDao, updateLidoLocatorImplementation } from "test/deploy"; -// import { Snapshot } from "test/suite"; - -// TODO: improve coverage -// TODO: more math-focused tests -// TODO: [@tamtamchik] restore tests -describe.skip("Accounting.sol:report", () => { - // let deployer: HardhatEthersSigner; - // let accountingOracle: HardhatEthersSigner; - // let stethWhale: HardhatEthersSigner; - // let stranger: HardhatEthersSigner; - // - // let lido: Lido; - // let acl: ACL; - // let locator: LidoLocator; - // let withdrawalQueue: WithdrawalQueue__MockForAccounting; - // let oracleReportSanityChecker: OracleReportSanityChecker__MockForAccounting; - // let burner: Burner__MockForAccounting; - // let elRewardsVault: LidoExecutionLayerRewardsVault__MockForLidoAccounting; - // let withdrawalVault: WithdrawalVault__MockForLidoAccounting; - // let stakingRouter: StakingRouter__MockForLidoAccounting; - // let postTokenRebaseReceiver: PostTokenRebaseReceiver__MockForAccounting; - // - // let originalState: string; - // - // before(async () => { - // [deployer, accountingOracle, stethWhale, stranger] = await ethers.getSigners(); - // - // [ - // burner, - // elRewardsVault, - // oracleReportSanityChecker, - // postTokenRebaseReceiver, - // stakingRouter, - // withdrawalQueue, - // withdrawalVault, - // ] = await Promise.all([ - // ethers.deployContract("Burner__MockForAccounting"), - // ethers.deployContract("LidoExecutionLayerRewardsVault__MockForLidoAccounting"), - // ethers.deployContract("OracleReportSanityChecker__MockForAccounting"), - // ethers.deployContract("PostTokenRebaseReceiver__MockForAccounting"), - // ethers.deployContract("StakingRouter__MockForLidoAccounting"), - // ethers.deployContract("WithdrawalQueue__MockForAccounting"), - // ethers.deployContract("WithdrawalVault__MockForLidoAccounting"), - // ]); - // - // ({ lido, acl } = await deployLidoDao({ - // rootAccount: deployer, - // initialized: true, - // locatorConfig: { - // accountingOracle, - // oracleReportSanityChecker, - // withdrawalQueue, - // burner, - // elRewardsVault, - // withdrawalVault, - // stakingRouter, - // postTokenRebaseReceiver, - // }, - // })); - // - // locator = await ethers.getContractAt("LidoLocator", await lido.getLidoLocator(), deployer); - // - // await acl.createPermission(deployer, lido, await lido.RESUME_ROLE(), deployer); - // await acl.createPermission(deployer, lido, await lido.PAUSE_ROLE(), deployer); - // await acl.createPermission(deployer, lido, await lido.UNSAFE_CHANGE_DEPOSITED_VALIDATORS_ROLE(), deployer); - // await lido.resume(); - // - // lido = lido.connect(accountingOracle); - // }); - // - // beforeEach(async () => (originalState = await Snapshot.take())); - // - // afterEach(async () => await Snapshot.restore(originalState)); - // - // context("handleOracleReport", () => { - // it("Reverts when the contract is stopped", async () => { - // await lido.connect(deployer).stop(); - // await expect(lido.handleOracleReport(...report())).to.be.revertedWith("CONTRACT_IS_STOPPED"); - // }); - // - // it("Reverts if the caller is not `AccountingOracle`", async () => { - // await expect(lido.connect(stranger).handleOracleReport(...report())).to.be.revertedWith("APP_AUTH_FAILED"); - // }); - // - // it("Reverts if the report timestamp is in the future", async () => { - // const nextBlockTimestamp = await getNextBlockTimestamp(); - // const invalidReportTimestamp = nextBlockTimestamp + 1n; - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // reportTimestamp: invalidReportTimestamp, - // }), - // ), - // ).to.be.revertedWith("INVALID_REPORT_TIMESTAMP"); - // }); - // - // it("Reverts if the number of reported validators is greater than what is stored on the contract", async () => { - // const depositedValidators = 100n; - // await lido.connect(deployer).unsafeChangeDepositedValidators(depositedValidators); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // clValidators: depositedValidators + 1n, - // }), - // ), - // ).to.be.revertedWith("REPORTED_MORE_DEPOSITED"); - // }); - // - // it("Reverts if the number of reported CL validators is less than what is stored on the contract", async () => { - // const depositedValidators = 100n; - // await lido.connect(deployer).unsafeChangeDepositedValidators(depositedValidators); - // - // // first report, 100 validators - // await lido.handleOracleReport( - // ...report({ - // clValidators: depositedValidators, - // }), - // ); - // - // // first report, 99 validators - // await expect( - // lido.handleOracleReport( - // ...report({ - // clValidators: depositedValidators - 1n, - // }), - // ), - // ).to.be.revertedWith("REPORTED_LESS_VALIDATORS"); - // }); - // - // it("Update CL validators count if reported more", async () => { - // let depositedValidators = 100n; - // await lido.connect(deployer).unsafeChangeDepositedValidators(depositedValidators); - // - // // first report, 100 validators - // await lido.handleOracleReport( - // ...report({ - // clValidators: depositedValidators, - // }), - // ); - // - // const slot = streccak("lido.Lido.beaconValidators"); - // const lidoAddress = await lido.getAddress(); - // - // let clValidatorsPosition = await getStorageAt(lidoAddress, slot); - // expect(clValidatorsPosition).to.equal(depositedValidators); - // - // depositedValidators = 101n; - // await lido.connect(deployer).unsafeChangeDepositedValidators(depositedValidators); - // - // // second report, 101 validators - // await lido.handleOracleReport( - // ...report({ - // clValidators: depositedValidators, - // }), - // ); - // - // clValidatorsPosition = await getStorageAt(lidoAddress, slot); - // expect(clValidatorsPosition).to.equal(depositedValidators); - // }); - // - // it("Reverts if the `checkAccountingOracleReport` sanity check fails", async () => { - // await oracleReportSanityChecker.mock__checkAccountingOracleReportReverts(true); - // - // await expect(lido.handleOracleReport(...report())).to.be.reverted; - // }); - // - // it("Reverts if the `checkWithdrawalQueueOracleReport` sanity check fails", async () => { - // await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ).to.be.reverted; - // }); - // - // it("Does not revert if the `checkWithdrawalQueueOracleReport` sanity check fails but no withdrawal batches were reported", async () => { - // await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); - // await withdrawalQueue.mock__isPaused(true); - // - // await expect(lido.handleOracleReport(...report())).not.to.be.reverted; - // }); - // - // it("Does not revert if the `checkWithdrawalQueueOracleReport` sanity check fails but `withdrawalQueue` is paused", async () => { - // await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); - // await withdrawalQueue.mock__isPaused(true); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ).not.to.be.reverted; - // }); - // - // it("Does not emit `StETHBurnRequested` if there are no shares to burn", async () => { - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ).not.to.emit(burner, "StETHBurnRequested"); - // }); - // - // it("Emits `StETHBurnRequested` if there are shares to burn", async () => { - // const sharesToBurn = 1n; - // const isCover = false; - // const steth = 1n * 2n; // imitating 1:2 rate, see Burner `mock__prefinalizeReturn` - // - // await withdrawalQueue.mock__prefinalizeReturn(0n, sharesToBurn); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ) - // .to.emit(burner, "StETHBurnRequested") - // .withArgs(isCover, await lido.getAddress(), steth, sharesToBurn); - // }); - // - // it("Withdraws ether from `ElRewardsVault` if EL rewards are greater than 0 as returned from `smoothenTokenRebase`", async () => { - // const withdrawals = 0n; - // const elRewards = 1n; - // const simulatedSharesToBurn = 0n; - // const sharesToBurn = 0n; - // - // await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn( - // withdrawals, - // elRewards, - // simulatedSharesToBurn, - // sharesToBurn, - // ); - // - // // `Mock__RewardsWithdrawn` event is only emitted on the mock to verify - // // that `ElRewardsVault.withdrawRewards` was actually called - // await expect(lido.handleOracleReport(...report())).to.emit(elRewardsVault, "Mock__RewardsWithdrawn"); - // }); - // - // it("Withdraws ether from `WithdrawalVault` if withdrawals are greater than 0 as returned from `smoothenTokenRebase`", async () => { - // const withdrawals = 1n; - // const elRewards = 0n; - // const simulatedSharesToBurn = 0n; - // const sharesToBurn = 0n; - // - // await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn( - // withdrawals, - // elRewards, - // simulatedSharesToBurn, - // sharesToBurn, - // ); - // - // // `Mock__WithdrawalsWithdrawn` event is only emitted on the mock to verify - // // that `WithdrawalVault.withdrawWithdrawals` was actually called - // await expect(lido.handleOracleReport(...report())).to.emit(withdrawalVault, "Mock__WithdrawalsWithdrawn"); - // }); - // - // it("Finalizes withdrawals if there is ether to lock on `WithdrawalQueue` as returned from `prefinalize`", async () => { - // const ethToLock = ether("10.0"); - // await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); - // // top up buffer via submit - // await lido.submit(ZeroAddress, { value: ethToLock }); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n, 2n], - // }), - // ), - // ).to.emit(withdrawalQueue, "WithdrawalsFinalized"); - // }); - // - // it("Updates buffered ether", async () => { - // const initialBufferedEther = await lido.getBufferedEther(); - // const ethToLock = 1n; - // - // // assert that the buffer has enough eth to lock for withdrawals - // // should have some eth from the initial 0xdead holder - // expect(initialBufferedEther).greaterThanOrEqual(ethToLock); - // await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ).to.not.be.reverted; - // - // expect(await lido.getBufferedEther()).to.equal(initialBufferedEther - ethToLock); - // }); - // - // it("Emits an `ETHDistributed` event", async () => { - // const reportTimestamp = await getNextBlockTimestamp(); - // const preClBalance = 0n; - // const clBalance = 1n; - // const withdrawals = 0n; - // const elRewards = 0n; - // const bufferedEther = await lido.getBufferedEther(); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // reportTimestamp: reportTimestamp, - // clBalance, - // }), - // ), - // ) - // .to.emit(lido, "ETHDistributed") - // .withArgs(reportTimestamp, preClBalance, clBalance, withdrawals, elRewards, bufferedEther); - // }); - // - // it("Burns shares if there are shares to burn as returned from `smoothenTokenRebaseReturn`", async () => { - // const sharesRequestedToBurn = 1n; - // - // await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn(0n, 0n, 0n, sharesRequestedToBurn); - // - // // set up steth whale, in case we need to send steth to other accounts - // await setBalance(stethWhale.address, ether("101.0")); - // await lido.connect(stethWhale).submit(ZeroAddress, { value: ether("100.0") }); - // // top up Burner with steth to burn - // await lido.connect(stethWhale).transferShares(burner, sharesRequestedToBurn); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // sharesRequestedToBurn, - // }), - // ), - // ) - // .to.emit(burner, "Mock__CommitSharesToBurnWasCalled") - // .and.to.emit(lido, "SharesBurnt") - // .withArgs(await burner.getAddress(), sharesRequestedToBurn, sharesRequestedToBurn, sharesRequestedToBurn); - // }); - // - // it("Reverts if the number of reward recipients does not match the number of module fees as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { - // // one recipient - // const recipients = [certainAddress("lido:handleOracleReport:single-recipient")]; - // const modulesIds = [1n, 2n]; - // // but two module fees - // const moduleFees = [500n, 500n]; - // const totalFee = 1000; - // const precisionPoints = 10n ** 20n; - // - // await stakingRouter.mock__getStakingRewardsDistribution( - // recipients, - // modulesIds, - // moduleFees, - // totalFee, - // precisionPoints, - // ); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // clBalance: 1n, // made 1 wei of profit, trigers reward processing - // }), - // ), - // ).to.be.revertedWith("WRONG_RECIPIENTS_INPUT"); - // }); - // - // it("Reverts if the number of module ids does not match the number of module fees as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { - // const recipients = [ - // certainAddress("lido:handleOracleReport:recipient1"), - // certainAddress("lido:handleOracleReport:recipient2"), - // ]; - // // one module id - // const modulesIds = [1n]; - // // but two module fees - // const moduleFees = [500n, 500n]; - // const totalFee = 1000; - // const precisionPoints = 10n ** 20n; - // - // await stakingRouter.mock__getStakingRewardsDistribution( - // recipients, - // modulesIds, - // moduleFees, - // totalFee, - // precisionPoints, - // ); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // clBalance: 1n, // made 1 wei of profit, trigers reward processing - // }), - // ), - // ).to.be.revertedWith("WRONG_MODULE_IDS_INPUT"); - // }); - // - // it("Does not mint and transfer any shares if the total fee is zero as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { - // // single staking module - // const recipients = [certainAddress("lido:handleOracleReport:recipient")]; - // const modulesIds = [1n]; - // const moduleFees = [500n]; - // // fee is 0 - // const totalFee = 0; - // const precisionPoints = 10n ** 20n; - // - // await stakingRouter.mock__getStakingRewardsDistribution( - // recipients, - // modulesIds, - // moduleFees, - // totalFee, - // precisionPoints, - // ); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // clBalance: 1n, - // }), - // ), - // ) - // .not.to.emit(lido, "Transfer") - // .and.not.to.emit(lido, "TransferShares") - // .and.not.to.emit(stakingRouter, "Mock__MintedRewardsReported"); - // }); - // - // it("Mints shares to itself and then transfers them to recipients if there are fees to distribute as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { - // // initially, before any rebases, one share costs one steth - // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); - // // thus, the total supply of steth should equal the total number of shares - // expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); - // - // // mock a single staking module with 5% fee with the total protocol fee of 10% - // const stakingModule = { - // address: certainAddress("lido:handleOracleReport:staking-module"), - // id: 1n, - // fee: 5n * 10n ** 18n, // 5% - // }; - // - // const totalFee = 10n * 10n ** 18n; // 10% - // const precisionPoints = 100n * 10n ** 18n; // 100% - // - // await stakingRouter.mock__getStakingRewardsDistribution( - // [stakingModule.address], - // [stakingModule.id], - // [stakingModule.fee], - // totalFee, - // precisionPoints, - // ); - // - // const clBalance = ether("1.0"); - // - // const expectedSharesToMint = - // (clBalance * totalFee * (await lido.getTotalShares())) / - // (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); - // - // const expectedModuleRewardInShares = expectedSharesToMint / (totalFee / stakingModule.fee); - // const expectedTreasuryCutInShares = expectedSharesToMint - expectedModuleRewardInShares; - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // clBalance: ether("1.0"), // 1 ether of profit - // }), - // ), - // ) - // .to.emit(lido, "TransferShares") - // .withArgs(ZeroAddress, stakingModule.address, expectedModuleRewardInShares) - // .and.to.emit(lido, "TransferShares") - // .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) - // .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); - // - // expect(await lido.balanceOf(stakingModule.address)).to.equal( - // await lido.getPooledEthByShares(expectedModuleRewardInShares), - // ); - // - // expect(await lido.balanceOf(await lido.getTreasury())).to.equal( - // await lido.getPooledEthByShares(expectedTreasuryCutInShares), - // ); - // - // // now one share should cost 1.9 steth (10% was distributed as rewards) - // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); - // }); - // - // it("Transfers all new shares to treasury if the module fee is zero as returned `StakingRouter.getStakingRewardsDistribution`", async () => { - // // initially, before any rebases, one share costs one steth - // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); - // // thus, the total supply of steth should equal the total number of shares - // expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); - // - // // mock a single staking module with 0% fee with the total protocol fee of 10% - // const stakingModule = { - // address: certainAddress("lido:handleOracleReport:staking-module"), - // id: 1n, - // fee: 0n, - // }; - // - // const totalFee = 10n * 10n ** 18n; // 10% - // const precisionPoints = 100n * 10n ** 18n; // 100% - // - // await stakingRouter.mock__getStakingRewardsDistribution( - // [stakingModule.address], - // [stakingModule.id], - // [stakingModule.fee], - // totalFee, - // precisionPoints, - // ); - // - // const clBalance = ether("1.0"); - // - // const expectedSharesToMint = - // (clBalance * totalFee * (await lido.getTotalShares())) / - // (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); - // - // const expectedModuleRewardInShares = 0n; - // const expectedTreasuryCutInShares = expectedSharesToMint; - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // clBalance: ether("1.0"), // 1 ether of profit - // }), - // ), - // ) - // .and.to.emit(lido, "TransferShares") - // .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) - // .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); - // - // expect(await lido.balanceOf(stakingModule.address)).to.equal( - // await lido.getPooledEthByShares(expectedModuleRewardInShares), - // ); - // - // expect(await lido.balanceOf(await lido.getTreasury())).to.equal( - // await lido.getPooledEthByShares(expectedTreasuryCutInShares), - // ); - // - // // now one share should cost 1.9 steth (10% was distributed as rewards) - // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); - // }); - // - // it("Relays the report data to `PostTokenRebaseReceiver`", async () => { - // await expect(lido.handleOracleReport(...report())).to.emit( - // postTokenRebaseReceiver, - // "Mock__PostTokenRebaseHandled", - // ); - // }); - // - // it("Does not relay the report data to `PostTokenRebaseReceiver` if the locator returns zero address", async () => { - // const lidoLocatorAddress = await lido.getLidoLocator(); - // - // // Change the locator implementation to support zero address - // await updateLidoLocatorImplementation(lidoLocatorAddress, {}, "LidoLocator__MockMutable", deployer); - // const locatorMutable = await ethers.getContractAt("LidoLocator__MockMutable", lidoLocatorAddress, deployer); - // await locatorMutable.mock___updatePostTokenRebaseReceiver(ZeroAddress); - // - // expect(await locator.postTokenRebaseReceiver()).to.equal(ZeroAddress); - // - // const accountingOracleAddress = await locator.accountingOracle(); - // const accountingOracleSigner = await impersonate(accountingOracleAddress, ether("1000.0")); - // - // await expect(lido.connect(accountingOracleSigner).handleOracleReport(...report())).not.to.emit( - // postTokenRebaseReceiver, - // "Mock__PostTokenRebaseHandled", - // ); - // }); - // - // it("Reverts if there are withdrawal batches submitted and `checkSimulatedShareRate` fails", async () => { - // await oracleReportSanityChecker.mock__checkSimulatedShareRateReverts(true); - // - // await expect( - // lido.handleOracleReport( - // ...report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ).to.be.reverted; - // }); - // - // it("Does not revert if there are no withdrawal batches submitted but `checkSimulatedShareRate` fails", async () => { - // await oracleReportSanityChecker.mock__checkSimulatedShareRateReverts(true); - // - // await expect(lido.handleOracleReport(...report())).not.to.be.reverted; - // }); - // - // it("Returns post-rebase state", async () => { - // const postRebaseState = await lido.handleOracleReport.staticCall(...report()); - // - // expect(postRebaseState).to.deep.equal([await lido.getTotalPooledEther(), await lido.getTotalShares(), 0n, 0n]); - // }); - // }); -}); +import { expect } from "chai"; +import { ZeroAddress } from "ethers"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import { getStorageAt, setBalance } from "@nomicfoundation/hardhat-network-helpers"; + +import { + Accounting, + ACL, + Burner__MockForAccounting, + Burner__MockForAccounting__factory, + IPostTokenRebaseReceiver, + Lido, + LidoExecutionLayerRewardsVault__MockForLidoAccounting, + LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory, + LidoLocator, + LidoLocator__factory, + OracleReportSanityChecker__MockForAccounting, + OracleReportSanityChecker__MockForAccounting__factory, + PostTokenRebaseReceiver__MockForAccounting__factory, + StakingRouter__MockForLidoAccounting, + StakingRouter__MockForLidoAccounting__factory, + WithdrawalQueue__MockForAccounting, + WithdrawalQueue__MockForAccounting__factory, + WithdrawalVault__MockForLidoAccounting, + WithdrawalVault__MockForLidoAccounting__factory, +} from "typechain-types"; +import { ReportValuesStruct } from "typechain-types/contracts/0.8.9/oracle/AccountingOracle.sol/IReportReceiver"; + +import { certainAddress, ether, getNextBlockTimestamp, impersonate, streccak } from "lib"; + +import { deployLidoDao, updateLidoLocatorImplementation } from "test/deploy"; + +describe("Accounting.sol:report", () => { + let deployer: HardhatEthersSigner; + let stethWhale: HardhatEthersSigner; + + let lido: Lido; + let acl: ACL; + let accounting: Accounting; + let postTokenRebaseReceiver: IPostTokenRebaseReceiver; + let locator: LidoLocator; + + let elRewardsVault: LidoExecutionLayerRewardsVault__MockForLidoAccounting; + let withdrawalVault: WithdrawalVault__MockForLidoAccounting; + let stakingRouter: StakingRouter__MockForLidoAccounting; + let oracleReportSanityChecker: OracleReportSanityChecker__MockForAccounting; + let withdrawalQueue: WithdrawalQueue__MockForAccounting; + let burner: Burner__MockForAccounting; + + beforeEach(async () => { + [deployer, stethWhale] = await ethers.getSigners(); + + [ + elRewardsVault, + stakingRouter, + withdrawalVault, + oracleReportSanityChecker, + postTokenRebaseReceiver, + withdrawalQueue, + burner, + ] = await Promise.all([ + new LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory(deployer).deploy(), + new StakingRouter__MockForLidoAccounting__factory(deployer).deploy(), + new WithdrawalVault__MockForLidoAccounting__factory(deployer).deploy(), + new OracleReportSanityChecker__MockForAccounting__factory(deployer).deploy(), + new PostTokenRebaseReceiver__MockForAccounting__factory(deployer).deploy(), + new WithdrawalQueue__MockForAccounting__factory(deployer).deploy(), + new Burner__MockForAccounting__factory(deployer).deploy(), + ]); + + ({ lido, acl, accounting } = await deployLidoDao({ + rootAccount: deployer, + initialized: true, + locatorConfig: { + withdrawalQueue, + elRewardsVault, + withdrawalVault, + stakingRouter, + oracleReportSanityChecker, + postTokenRebaseReceiver, + burner, + }, + })); + + locator = LidoLocator__factory.connect(await lido.getLidoLocator(), deployer); + + const accountingOracleSigner = await impersonate(await locator.accountingOracle(), ether("100.0")); + accounting = accounting.connect(accountingOracleSigner); + + await acl.createPermission(deployer, lido, await lido.RESUME_ROLE(), deployer); + await acl.createPermission(deployer, lido, await lido.PAUSE_ROLE(), deployer); + await acl.createPermission(deployer, lido, await lido.UNSAFE_CHANGE_DEPOSITED_VALIDATORS_ROLE(), deployer); + await lido.resume(); + }); + + context("handleOracleReport", () => { + it("Update CL validators count if reported more", async () => { + let depositedValidators = 100n; + await lido.connect(deployer).unsafeChangeDepositedValidators(depositedValidators); + + // first report, 100 validators + await accounting.handleOracleReport( + report({ + clValidators: depositedValidators, + }), + ); + + const slot = streccak("lido.Lido.beaconValidators"); + const lidoAddress = await lido.getAddress(); + + let clValidatorsPosition = await getStorageAt(lidoAddress, slot); + expect(clValidatorsPosition).to.equal(depositedValidators); + + depositedValidators = 101n; + await lido.connect(deployer).unsafeChangeDepositedValidators(depositedValidators); + + // second report, 101 validators + await accounting.handleOracleReport( + report({ + clValidators: depositedValidators, + }), + ); + + clValidatorsPosition = await getStorageAt(lidoAddress, slot); + expect(clValidatorsPosition).to.equal(depositedValidators); + }); + + it("Reverts if the `checkAccountingOracleReport` sanity check fails", async () => { + await oracleReportSanityChecker.mock__checkAccountingOracleReportReverts(true); + + await expect(accounting.handleOracleReport(report())).to.be.reverted; + }); + + it("Reverts if the `checkWithdrawalQueueOracleReport` sanity check fails", async () => { + await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); + await expect( + accounting.handleOracleReport( + report({ + withdrawalFinalizationBatches: [1n], + }), + ), + ).to.be.reverted; + }); + + it("Does not revert if the `checkWithdrawalQueueOracleReport` sanity check fails but no withdrawal batches were reported", async () => { + await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); + await withdrawalQueue.mock__isPaused(true); + + await expect(accounting.handleOracleReport(report())).not.to.be.reverted; + }); + + /// NOTE: This test is not applicable to the current implementation (Accounting's _checkAccountingOracleReport() checks for checkWithdrawalQueueOracleReport() + /// explicitly in case _report.withdrawalFinalizationBatches.length > 0 + // it("Does not revert if the `checkWithdrawalQueueOracleReport` sanity check fails but `withdrawalQueue` is paused", async () => { + // await oracleReportSanityChecker.mock__checkWithdrawalQueueOracleReportReverts(true); + // await withdrawalQueue.mock__isPaused(true); + + // await expect(accounting.handleOracleReport(report({ withdrawalFinalizationBatches: [1n] }))).not.to.be.reverted; + // }); + + it("Does not emit `StETHBurnRequested` if there are no shares to burn", async () => { + await expect( + accounting.handleOracleReport( + report({ + withdrawalFinalizationBatches: [1n], + }), + ), + ).not.to.emit(burner, "StETHBurnRequested"); + }); + + it("Emits `StETHBurnRequested` if there are shares to burn", async () => { + const sharesToBurn = 1n; + const isCover = false; + const steth = 1n * 2n; // imitating 1:2 rate, see Burner `mock__prefinalizeReturn` + + await withdrawalQueue.mock__prefinalizeReturn(0n, sharesToBurn); + + await expect( + accounting.handleOracleReport( + report({ + withdrawalFinalizationBatches: [1n], + }), + ), + ) + .to.emit(burner, "StETHBurnRequested") + .withArgs(isCover, await accounting.getAddress(), steth, sharesToBurn); + }); + + it("Withdraws ether from `ElRewardsVault` if EL rewards are greater than 0 as returned from `smoothenTokenRebase`", async () => { + const withdrawals = 0n; + const elRewards = 1n; + const simulatedSharesToBurn = 0n; + const sharesToBurn = 0n; + + await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn( + withdrawals, + elRewards, + simulatedSharesToBurn, + sharesToBurn, + ); + + // `Mock__RewardsWithdrawn` event is only emitted on the mock to verify + // that `ElRewardsVault.withdrawRewards` was actually called + await expect(accounting.handleOracleReport(report())).to.emit(elRewardsVault, "Mock__RewardsWithdrawn"); + }); + + it("Withdraws ether from `WithdrawalVault` if withdrawals are greater than 0 as returned from `smoothenTokenRebase`", async () => { + const withdrawals = 1n; + const elRewards = 0n; + const simulatedSharesToBurn = 0n; + const sharesToBurn = 0n; + + await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn( + withdrawals, + elRewards, + simulatedSharesToBurn, + sharesToBurn, + ); + const totalFee = 1000; + const precisionPoints = 10n ** 20n; + await stakingRouter.mock__getStakingRewardsDistribution([], [], [], totalFee, precisionPoints); + + // `Mock__WithdrawalsWithdrawn` event is only emitted on the mock to verify + // that `WithdrawalVault.withdrawWithdrawals` was actually called + await expect(accounting.handleOracleReport(report())).to.emit(withdrawalVault, "Mock__WithdrawalsWithdrawn"); + }); + + it("Finalizes withdrawals if there is ether to lock on `WithdrawalQueue` as returned from `prefinalize`", async () => { + const ethToLock = ether("10.0"); + await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); + // top up buffer via submit + await lido.submit(ZeroAddress, { value: ethToLock }); + + await expect( + accounting.handleOracleReport( + report({ + withdrawalFinalizationBatches: [1n, 2n], + }), + ), + ).to.emit(withdrawalQueue, "WithdrawalsFinalized"); + }); + + it("Updates buffered ether", async () => { + const initialBufferedEther = await lido.getBufferedEther(); + const ethToLock = 1n; + + // assert that the buffer has enough eth to lock for withdrawals + // should have some eth from the initial 0xdead holder + expect(initialBufferedEther).greaterThanOrEqual(ethToLock); + await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); + + await expect( + accounting.handleOracleReport( + report({ + withdrawalFinalizationBatches: [1n], + }), + ), + ).to.not.be.reverted; + + expect(await lido.getBufferedEther()).to.equal(initialBufferedEther - ethToLock); + }); + + it("Emits an `ETHDistributed` event", async () => { + const reportTimestamp = await getNextBlockTimestamp(); + const preClBalance = 0n; + const clBalance = 1n; + const withdrawals = 0n; + const elRewards = 0n; + const bufferedEther = await lido.getBufferedEther(); -// function report(overrides?: Partial): ReportTuple { -// return Object.values({ -// reportTimestamp: 0n, -// timeElapsed: 0n, -// clValidators: 0n, -// clBalance: 0n, -// withdrawalVaultBalance: 0n, -// elRewardsVaultBalance: 0n, -// sharesRequestedToBurn: 0n, -// withdrawalFinalizationBatches: [], -// simulatedShareRate: 0n, -// ...overrides, -// }) as ReportTuple; -// } - -// interface Report { -// reportTimestamp: BigNumberish; -// timeElapsed: BigNumberish; -// clValidators: BigNumberish; -// clBalance: BigNumberish; -// withdrawalVaultBalance: BigNumberish; -// elRewardsVaultBalance: BigNumberish; -// sharesRequestedToBurn: BigNumberish; -// withdrawalFinalizationBatches: BigNumberish[]; -// simulatedShareRate: BigNumberish; -// } -// -// type ReportTuple = [ -// BigNumberish, -// BigNumberish, -// BigNumberish, -// BigNumberish, -// BigNumberish, -// BigNumberish, -// BigNumberish, -// BigNumberish[], -// BigNumberish, -// ]; + const totalFee = 1000; + const precisionPoints = 10n ** 20n; + await stakingRouter.mock__getStakingRewardsDistribution([], [], [], totalFee, precisionPoints); + + await expect( + accounting.handleOracleReport( + report({ + timestamp: reportTimestamp, + clBalance, + }), + ), + ) + .to.emit(lido, "ETHDistributed") + .withArgs(reportTimestamp, preClBalance, clBalance, withdrawals, elRewards, bufferedEther); + }); + + it("Burns shares if there are shares to burn as returned from `smoothenTokenRebaseReturn`", async () => { + const sharesRequestedToBurn = 1n; + + await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn(0n, 0n, 0n, sharesRequestedToBurn); + + // set up steth whale, in case we need to send steth to other accounts + await setBalance(stethWhale.address, ether("101.0")); + await lido.connect(stethWhale).submit(ZeroAddress, { value: ether("100.0") }); + // top up Burner with steth to burn + await lido.connect(stethWhale).transferShares(burner, sharesRequestedToBurn); + + await expect( + accounting.handleOracleReport( + report({ + sharesRequestedToBurn, + }), + ), + ).to.emit(burner, "Mock__CommitSharesToBurnWasCalled"); + + // TODO: SharesBurnt event is not emitted anymore because of the mock implementation + // .and.to.emit(lido, "SharesBurnt") + // .withArgs(await burner.getAddress(), sharesRequestedToBurn, sharesRequestedToBurn, sharesRequestedToBurn); + }); + + it("Reverts if the number of reward recipients does not match the number of module fees as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { + // one recipient + const recipients = [certainAddress("lido:handleOracleReport:single-recipient")]; + const modulesIds = [1n, 2n]; + // but two module fees + const moduleFees = [500n, 500n]; + const totalFee = 1000; + const precisionPoints = 10n ** 20n; + + await stakingRouter.mock__getStakingRewardsDistribution( + recipients, + modulesIds, + moduleFees, + totalFee, + precisionPoints, + ); + + await expect( + accounting.handleOracleReport( + report({ + clBalance: 1n, // made 1 wei of profit, trigers reward processing + }), + ), + ) + .to.be.revertedWithCustomError(accounting, "UnequalArrayLengths") + .withArgs(1, 2); + }); + + it("Reverts if the number of module ids does not match the number of module fees as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { + const recipients = [ + certainAddress("lido:handleOracleReport:recipient1"), + certainAddress("lido:handleOracleReport:recipient2"), + ]; + // one module id + const modulesIds = [1n]; + // but two module fees + const moduleFees = [500n, 500n]; + const totalFee = 1000; + const precisionPoints = 10n ** 20n; + + await stakingRouter.mock__getStakingRewardsDistribution( + recipients, + modulesIds, + moduleFees, + totalFee, + precisionPoints, + ); + + await expect( + accounting.handleOracleReport( + report({ + clBalance: 1n, // made 1 wei of profit, trigers reward processing + }), + ), + ) + .to.be.revertedWithCustomError(accounting, "UnequalArrayLengths") + .withArgs(1, 2); + }); + + it("Does not mint and transfer any shares if the total fee is zero as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { + // single staking module + const recipients = [certainAddress("lido:handleOracleReport:recipient")]; + const modulesIds = [1n]; + const moduleFees = [500n]; + // fee is 0 + const totalFee = 0; + const precisionPoints = 10n ** 20n; + + await stakingRouter.mock__getStakingRewardsDistribution( + recipients, + modulesIds, + moduleFees, + totalFee, + precisionPoints, + ); + + await expect( + accounting.handleOracleReport( + report({ + clBalance: 1n, + }), + ), + ) + .not.to.emit(lido, "Transfer") + .and.not.to.emit(lido, "TransferShares") + .and.not.to.emit(stakingRouter, "Mock__MintedRewardsReported"); + }); + + it("Mints shares to itself and then transfers them to recipients if there are fees to distribute as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { + // initially, before any rebases, one share costs one steth + expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); + // thus, the total supply of steth should equal the total number of shares + expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); + + // mock a single staking module with 5% fee with the total protocol fee of 10% + const stakingModule = { + address: certainAddress("lido:handleOracleReport:staking-module"), + id: 1n, + fee: 5n * 10n ** 18n, // 5% + }; + + const totalFee = 10n * 10n ** 18n; // 10% + const precisionPoints = 100n * 10n ** 18n; // 100% + + await stakingRouter.mock__getStakingRewardsDistribution( + [stakingModule.address], + [stakingModule.id], + [stakingModule.fee], + totalFee, + precisionPoints, + ); + + const clBalance = ether("1.0"); + + const expectedSharesToMint = + (clBalance * totalFee * (await lido.getTotalShares())) / + (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); + + const expectedModuleRewardInShares = expectedSharesToMint / (totalFee / stakingModule.fee); + const expectedTreasuryCutInShares = expectedSharesToMint - expectedModuleRewardInShares; + + await expect( + accounting.handleOracleReport( + report({ + clBalance: ether("1.0"), // 1 ether of profit + }), + ), + ) + .to.emit(lido, "TransferShares") + .withArgs(ZeroAddress, stakingModule.address, expectedModuleRewardInShares) + .and.to.emit(lido, "TransferShares") + .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) + .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); + + expect(await lido.balanceOf(stakingModule.address)).to.equal( + await lido.getPooledEthByShares(expectedModuleRewardInShares), + ); + + expect(await lido.balanceOf(await lido.getTreasury())).to.equal( + await lido.getPooledEthByShares(expectedTreasuryCutInShares), + ); + + // now one share should cost 1.9 steth (10% was distributed as rewards) + expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); + }); + + it("Transfers all new shares to treasury if the module fee is zero as returned `StakingRouter.getStakingRewardsDistribution`", async () => { + // initially, before any rebases, one share costs one steth + expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); + // thus, the total supply of steth should equal the total number of shares + expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); + + // mock a single staking module with 0% fee with the total protocol fee of 10% + const stakingModule = { + address: certainAddress("lido:handleOracleReport:staking-module"), + id: 1n, + fee: 0n, + }; + + const totalFee = 10n * 10n ** 18n; // 10% + const precisionPoints = 100n * 10n ** 18n; // 100% + + await stakingRouter.mock__getStakingRewardsDistribution( + [stakingModule.address], + [stakingModule.id], + [stakingModule.fee], + totalFee, + precisionPoints, + ); + + const clBalance = ether("1.0"); + + const expectedSharesToMint = + (clBalance * totalFee * (await lido.getTotalShares())) / + (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); + + const expectedModuleRewardInShares = 0n; + const expectedTreasuryCutInShares = expectedSharesToMint; + + await expect( + accounting.handleOracleReport( + report({ + clBalance: ether("1.0"), // 1 ether of profit + }), + ), + ) + .and.to.emit(lido, "TransferShares") + .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) + .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); + + expect(await lido.balanceOf(stakingModule.address)).to.equal( + await lido.getPooledEthByShares(expectedModuleRewardInShares), + ); + + expect(await lido.balanceOf(await lido.getTreasury())).to.equal( + await lido.getPooledEthByShares(expectedTreasuryCutInShares), + ); + + // now one share should cost 1.9 steth (10% was distributed as rewards) + expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); + }); + + it("Relays the report data to `PostTokenRebaseReceiver`", async () => { + await expect(accounting.handleOracleReport(report())).to.emit( + postTokenRebaseReceiver, + "Mock__PostTokenRebaseHandled", + ); + }); + + it("Does not relay the report data to `PostTokenRebaseReceiver` if the locator returns zero address", async () => { + const lidoLocatorAddress = await lido.getLidoLocator(); + + // Change the locator implementation to support zero address + await updateLidoLocatorImplementation(lidoLocatorAddress, {}, "LidoLocator__MockMutable", deployer); + const locatorMutable = await ethers.getContractAt("LidoLocator__MockMutable", lidoLocatorAddress, deployer); + await locatorMutable.mock___updatePostTokenRebaseReceiver(ZeroAddress); + + expect(await locator.postTokenRebaseReceiver()).to.equal(ZeroAddress); + + const accountingOracleAddress = await locator.accountingOracle(); + const accountingOracle = await impersonate(accountingOracleAddress, ether("1000.0")); + + await expect(accounting.connect(accountingOracle).handleOracleReport(report())).not.to.emit( + postTokenRebaseReceiver, + "Mock__PostTokenRebaseHandled", + ); + }); + + function report(overrides?: Partial): ReportValuesStruct { + return { + timestamp: 0n, + timeElapsed: 0n, + clValidators: 0n, + clBalance: 0n, + withdrawalVaultBalance: 0n, + elRewardsVaultBalance: 0n, + sharesRequestedToBurn: 0n, + withdrawalFinalizationBatches: [], + vaultValues: [], + netCashFlows: [], + ...overrides, + }; + } + }); +}); From a3ae0b16ab51393591e36a69470757909b89225f Mon Sep 17 00:00:00 2001 From: VP Date: Tue, 14 Jan 2025 19:13:49 +0100 Subject: [PATCH 14/27] chore: remove unused mock --- .../oracle/OracleReportSanityCheckerMocks.sol | 151 ------------------ 1 file changed, 151 deletions(-) delete mode 100644 test/0.8.9/contracts/oracle/OracleReportSanityCheckerMocks.sol diff --git a/test/0.8.9/contracts/oracle/OracleReportSanityCheckerMocks.sol b/test/0.8.9/contracts/oracle/OracleReportSanityCheckerMocks.sol deleted file mode 100644 index f10f278bd..000000000 --- a/test/0.8.9/contracts/oracle/OracleReportSanityCheckerMocks.sol +++ /dev/null @@ -1,151 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Lido -// SPDX-License-Identifier: GPL-3.0 -// for testing purposes only - -pragma solidity 0.8.9; - -import {IWithdrawalQueue} from "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol"; - -contract LidoStub { - uint256 private _shareRate = 1 ether; - - function getSharesByPooledEth(uint256 _sharesAmount) external view returns (uint256) { - return (_shareRate * _sharesAmount) / 1 ether; - } - - function setShareRate(uint256 _value) external { - _shareRate = _value; - } -} - -contract WithdrawalQueueStub is IWithdrawalQueue { - mapping(uint256 => uint256) private _timestamps; - - function setRequestTimestamp(uint256 _requestId, uint256 _timestamp) external { - _timestamps[_requestId] = _timestamp; - } - - function getWithdrawalStatus( - uint256[] calldata _requestIds - ) external view returns (WithdrawalRequestStatus[] memory statuses) { - statuses = new WithdrawalRequestStatus[](_requestIds.length); - for (uint256 i; i < _requestIds.length; ++i) { - statuses[i].timestamp = _timestamps[_requestIds[i]]; - } - } -} - -contract BurnerStub { - uint256 private nonCover; - uint256 private cover; - - function getSharesRequestedToBurn() external view returns (uint256 coverShares, uint256 nonCoverShares) { - coverShares = cover; - nonCoverShares = nonCover; - } - - function setSharesRequestedToBurn(uint256 _cover, uint256 _nonCover) external { - cover = _cover; - nonCover = _nonCover; - } -} - -interface ILidoLocator { - function lido() external view returns (address); - - function burner() external view returns (address); - - function withdrawalVault() external view returns (address); - - function withdrawalQueue() external view returns (address); -} - -contract LidoLocatorStub is ILidoLocator { - address private immutable LIDO; - address private immutable WITHDRAWAL_VAULT; - address private immutable WITHDRAWAL_QUEUE; - address private immutable EL_REWARDS_VAULT; - address private immutable BURNER; - - constructor( - address _lido, - address _withdrawalVault, - address _withdrawalQueue, - address _elRewardsVault, - address _burner - ) { - LIDO = _lido; - WITHDRAWAL_VAULT = _withdrawalVault; - WITHDRAWAL_QUEUE = _withdrawalQueue; - EL_REWARDS_VAULT = _elRewardsVault; - BURNER = _burner; - } - - function lido() external view returns (address) { - return LIDO; - } - - function withdrawalQueue() external view returns (address) { - return WITHDRAWAL_QUEUE; - } - - function withdrawalVault() external view returns (address) { - return WITHDRAWAL_VAULT; - } - - function elRewardsVault() external view returns (address) { - return EL_REWARDS_VAULT; - } - - function burner() external view returns (address) { - return BURNER; - } -} - -contract OracleReportSanityCheckerStub { - error SelectorNotFound(bytes4 sig, uint256 value, bytes data); - - fallback() external payable { - revert SelectorNotFound(msg.sig, msg.value, msg.data); - } - - function checkAccountingOracleReport( - uint256 _timeElapsed, - uint256 _preCLBalance, - uint256 _postCLBalance, - uint256 _withdrawalVaultBalance, - uint256 _elRewardsVaultBalance, - uint256 _sharesRequestedToBurn, - uint256 _preCLValidators, - uint256 _postCLValidators - ) external view {} - - function checkWithdrawalQueueOracleReport( - uint256[] calldata _withdrawalFinalizationBatches, - uint256 _reportTimestamp - ) external view {} - - function smoothenTokenRebase( - uint256, - uint256, - uint256, - uint256, - uint256 _withdrawalVaultBalance, - uint256 _elRewardsVaultBalance, - uint256, - uint256 _etherToLockForWithdrawals, - uint256 - ) - external - view - returns (uint256 withdrawals, uint256 elRewards, uint256 simulatedSharesToBurn, uint256 sharesToBurn) - { - withdrawals = _withdrawalVaultBalance; - elRewards = _elRewardsVaultBalance; - - simulatedSharesToBurn = 0; - sharesToBurn = _etherToLockForWithdrawals; - } - - function checkExtraDataItemsCountPerTransaction(uint256 _extraDataListItemsCount) external view {} -} From 2ad9d7c2e265c51d0263e505ca806fcf3c93e926 Mon Sep 17 00:00:00 2001 From: VP Date: Wed, 15 Jan 2025 11:35:51 +0100 Subject: [PATCH 15/27] chore: remove unused mock --- .../OracleReportSanityChecker__Mock.sol | 52 ------------------- 1 file changed, 52 deletions(-) delete mode 100644 test/0.8.9/contracts/OracleReportSanityChecker__Mock.sol diff --git a/test/0.8.9/contracts/OracleReportSanityChecker__Mock.sol b/test/0.8.9/contracts/OracleReportSanityChecker__Mock.sol deleted file mode 100644 index 906940c48..000000000 --- a/test/0.8.9/contracts/OracleReportSanityChecker__Mock.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// for testing purposes only - -pragma solidity 0.8.9; - -contract OracleReportSanityChecker__Mock { - error SelectorNotFound(bytes4 sig, uint256 value, bytes data); - - fallback() external payable { - revert SelectorNotFound(msg.sig, msg.value, msg.data); - } - - function checkAccountingOracleReport( - uint256 _timeElapsed, - uint256 _preCLBalance, - uint256 _postCLBalance, - uint256 _withdrawalVaultBalance, - uint256 _elRewardsVaultBalance, - uint256 _sharesRequestedToBurn, - uint256 _preCLValidators, - uint256 _postCLValidators - ) external view {} - - function checkWithdrawalQueueOracleReport( - uint256[] calldata _withdrawalFinalizationBatches, - uint256 _reportTimestamp - ) external view {} - - function smoothenTokenRebase( - uint256, - uint256, - uint256, - uint256, - uint256 _withdrawalVaultBalance, - uint256 _elRewardsVaultBalance, - uint256, - uint256 _etherToLockForWithdrawals, - uint256 - ) - external - view - returns (uint256 withdrawals, uint256 elRewards, uint256 simulatedSharesToBurn, uint256 sharesToBurn) - { - withdrawals = _withdrawalVaultBalance; - elRewards = _elRewardsVaultBalance; - - simulatedSharesToBurn = 0; - sharesToBurn = _etherToLockForWithdrawals; - } - - function checkAccountingExtraDataListItemsCount(uint256 _extraDataListItemsCount) external view {} -} From 661af1f688fde3e52a3a4152aa06207662711035 Mon Sep 17 00:00:00 2001 From: VP Date: Wed, 15 Jan 2025 11:36:36 +0100 Subject: [PATCH 16/27] test: remove accounting and locator from lido test --- test/0.4.24/lido/lido.accounting.test.ts | 8 ++++---- test/deploy/dao.ts | 11 ++--------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/test/0.4.24/lido/lido.accounting.test.ts b/test/0.4.24/lido/lido.accounting.test.ts index 9a5f2e430..344df58d2 100644 --- a/test/0.4.24/lido/lido.accounting.test.ts +++ b/test/0.4.24/lido/lido.accounting.test.ts @@ -4,7 +4,6 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { - Accounting, ACL, Burner__MockForAccounting, Burner__MockForAccounting__factory, @@ -12,6 +11,7 @@ import { Lido, LidoExecutionLayerRewardsVault__MockForLidoAccounting, LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory, + LidoLocator__factory, OracleReportSanityChecker__MockForAccounting, OracleReportSanityChecker__MockForAccounting__factory, PostTokenRebaseReceiver__MockForAccounting__factory, @@ -33,7 +33,6 @@ describe("Lido:accounting", () => { let lido: Lido; let acl: ACL; - let accounting: Accounting; let postTokenRebaseReceiver: IPostTokenRebaseReceiver; let elRewardsVault: LidoExecutionLayerRewardsVault__MockForLidoAccounting; @@ -64,7 +63,7 @@ describe("Lido:accounting", () => { new Burner__MockForAccounting__factory(deployer).deploy(), ]); - ({ lido, acl, accounting } = await deployLidoDao({ + ({ lido, acl } = await deployLidoDao({ rootAccount: deployer, initialized: true, locatorConfig: { @@ -95,7 +94,8 @@ describe("Lido:accounting", () => { }); it("Updates beacon stats", async () => { - const accountingSigner = await impersonate(await accounting.getAddress(), ether("100.0")); + const locator = LidoLocator__factory.connect(await lido.getLidoLocator(), deployer); + const accountingSigner = await impersonate(await locator.accounting(), ether("100.0")); lido = lido.connect(accountingSigner); await expect( lido.processClStateUpdate( diff --git a/test/deploy/dao.ts b/test/deploy/dao.ts index 910fb1fd3..70e18dc01 100644 --- a/test/deploy/dao.ts +++ b/test/deploy/dao.ts @@ -7,7 +7,7 @@ import { Kernel, LidoLocator } from "typechain-types"; import { ether, findEvents, streccak } from "lib"; -import { deployLidoLocator, updateLidoLocatorImplementation } from "./locator"; +import { deployLidoLocator } from "./locator"; interface CreateAddAppArgs { dao: Kernel; @@ -79,14 +79,7 @@ export async function deployLidoDao({ rootAccount, initialized, locatorConfig = await lido.initialize(locator, eip712steth, { value: ether("1.0") }); } - const locator = await lido.getLidoLocator(); - const accountingImpl = await ethers.deployContract("Accounting", [locator, lido], rootAccount); - const accountingProxy = await ethers.deployContract("OssifiableProxy", [accountingImpl, rootAccount, new Uint8Array()], rootAccount); - const accounting = await ethers.getContractAt("Accounting", accountingProxy, rootAccount); - await updateLidoLocatorImplementation(locator, { accounting }); - await accounting.initialize(rootAccount); - - return { lido, dao, acl, accounting }; + return { lido, dao, acl }; } export async function deployLidoDaoForNor({ rootAccount, initialized, locatorConfig = {} }: DeployLidoDaoArgs) { From 36fbd1c7ea81f2833c04562bd63391ac5d574ed2 Mon Sep 17 00:00:00 2001 From: VP Date: Wed, 15 Jan 2025 15:45:24 +0100 Subject: [PATCH 17/27] test: add mocks --- .../contracts/Burner__MockForAccounting.sol | 4 +- .../contracts/Lido__MockForAccounting.sol | 107 ++++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 test/0.8.9/contracts/Lido__MockForAccounting.sol diff --git a/test/0.4.24/contracts/Burner__MockForAccounting.sol b/test/0.4.24/contracts/Burner__MockForAccounting.sol index 6e7aa40f5..776c84829 100644 --- a/test/0.4.24/contracts/Burner__MockForAccounting.sol +++ b/test/0.4.24/contracts/Burner__MockForAccounting.sol @@ -11,7 +11,7 @@ contract Burner__MockForAccounting { uint256 amountOfShares ); - event Mock__CommitSharesToBurnWasCalled(); + event Mock__CommitSharesToBurnWasCalled(uint256 sharesToBurn); function requestBurnShares(address, uint256 _sharesAmountToBurn) external { // imitating share to steth rate 1:2 @@ -22,6 +22,6 @@ contract Burner__MockForAccounting { function commitSharesToBurn(uint256 _sharesToBurn) external { _sharesToBurn; - emit Mock__CommitSharesToBurnWasCalled(); + emit Mock__CommitSharesToBurnWasCalled(_sharesToBurn); } } diff --git a/test/0.8.9/contracts/Lido__MockForAccounting.sol b/test/0.8.9/contracts/Lido__MockForAccounting.sol new file mode 100644 index 000000000..dcc2a5944 --- /dev/null +++ b/test/0.8.9/contracts/Lido__MockForAccounting.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity 0.8.9; + +contract Lido__MockForAccounting { + uint256 public depositedValidatorsValue; + uint256 public reportClValidators; + uint256 public reportClBalance; + + // Emitted when validators number delivered by the oracle + event CLValidatorsUpdated(uint256 indexed reportTimestamp, uint256 preCLValidators, uint256 postCLValidators); + event Mock__CollectRewardsAndProcessWithdrawals( + uint256 _reportTimestamp, + uint256 _reportClBalance, + uint256 _principalCLBalance, + uint256 _withdrawalsToWithdraw, + uint256 _elRewardsToWithdraw, + uint256 _lastWithdrawalRequestToFinalize, + uint256 _withdrawalsShareRate, + uint256 _etherToLockOnWithdrawalQueue + ); + + function setMockedDepositedValidators(uint256 _amount) external { + depositedValidatorsValue = _amount; + } + + function getBeaconStat() + external + view + returns (uint256 depositedValidators, uint256 beaconValidators, uint256 beaconBalance) + { + depositedValidators = depositedValidatorsValue; + beaconValidators = 0; + beaconBalance = 0; + } + + function getTotalPooledEther() external view returns (uint256) { + // return 1 ether; + return 3201000000000000000000; + } + + function getTotalShares() external view returns (uint256) { + // return 1 ether; + return 1000000000000000000; + } + + function getExternalShares() external view returns (uint256) { + return 0; + } + + function getExternalEther() external view returns (uint256) { + return 0; + } + + function collectRewardsAndProcessWithdrawals( + uint256 _reportTimestamp, + uint256 _reportClBalance, + uint256 _adjustedPreCLBalance, + uint256 _withdrawalsToWithdraw, + uint256 _elRewardsToWithdraw, + uint256 _lastWithdrawalRequestToFinalize, + uint256 _simulatedShareRate, + uint256 _etherToLockOnWithdrawalQueue + ) external { + emit Mock__CollectRewardsAndProcessWithdrawals( + _reportTimestamp, + _reportClBalance, + _adjustedPreCLBalance, + _withdrawalsToWithdraw, + _elRewardsToWithdraw, + _lastWithdrawalRequestToFinalize, + _simulatedShareRate, + _etherToLockOnWithdrawalQueue + ); + } + + function emitTokenRebase( + uint256 _reportTimestamp, + uint256 _timeElapsed, + uint256 _preTotalShares, + uint256 _preTotalEther, + uint256 _postTotalShares, + uint256 _postTotalEther, + uint256 _sharesMintedAsFees + ) external {} + + /** + * @notice Process CL related state changes as a part of the report processing + * @dev All data validation was done by Accounting and OracleReportSanityChecker + * @param _reportTimestamp timestamp of the report + * @param _preClValidators number of validators in the previous CL state (for event compatibility) + * @param _reportClValidators number of validators in the current CL state + * @param _reportClBalance total balance of the current CL state + */ + function processClStateUpdate( + uint256 _reportTimestamp, + uint256 _preClValidators, + uint256 _reportClValidators, + uint256 _reportClBalance + ) external { + reportClValidators = _reportClValidators; + reportClBalance = _reportClBalance; + + emit CLValidatorsUpdated(_reportTimestamp, _preClValidators, _reportClValidators); + } +} From 212161e33a117af36f62403473c93078ec0fb857 Mon Sep 17 00:00:00 2001 From: VP Date: Wed, 15 Jan 2025 15:46:55 +0100 Subject: [PATCH 18/27] test: remove cohesion of lido and accounting --- .../accounting.handleOracleReport.test.ts | 480 ++++++++---------- 1 file changed, 222 insertions(+), 258 deletions(-) diff --git a/test/0.8.9/accounting.handleOracleReport.test.ts b/test/0.8.9/accounting.handleOracleReport.test.ts index 70b09f683..f1685ad0e 100644 --- a/test/0.8.9/accounting.handleOracleReport.test.ts +++ b/test/0.8.9/accounting.handleOracleReport.test.ts @@ -3,19 +3,17 @@ import { ZeroAddress } from "ethers"; import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { getStorageAt, setBalance } from "@nomicfoundation/hardhat-network-helpers"; import { Accounting, - ACL, Burner__MockForAccounting, Burner__MockForAccounting__factory, IPostTokenRebaseReceiver, - Lido, + Lido__MockForAccounting, + Lido__MockForAccounting__factory, LidoExecutionLayerRewardsVault__MockForLidoAccounting, LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory, LidoLocator, - LidoLocator__factory, OracleReportSanityChecker__MockForAccounting, OracleReportSanityChecker__MockForAccounting__factory, PostTokenRebaseReceiver__MockForAccounting__factory, @@ -28,20 +26,18 @@ import { } from "typechain-types"; import { ReportValuesStruct } from "typechain-types/contracts/0.8.9/oracle/AccountingOracle.sol/IReportReceiver"; -import { certainAddress, ether, getNextBlockTimestamp, impersonate, streccak } from "lib"; +import { certainAddress, ether, impersonate } from "lib"; -import { deployLidoDao, updateLidoLocatorImplementation } from "test/deploy"; +import { deployLidoLocator, updateLidoLocatorImplementation } from "test/deploy"; describe("Accounting.sol:report", () => { let deployer: HardhatEthersSigner; - let stethWhale: HardhatEthersSigner; - let lido: Lido; - let acl: ACL; let accounting: Accounting; let postTokenRebaseReceiver: IPostTokenRebaseReceiver; let locator: LidoLocator; + let lido: Lido__MockForAccounting; let elRewardsVault: LidoExecutionLayerRewardsVault__MockForLidoAccounting; let withdrawalVault: WithdrawalVault__MockForLidoAccounting; let stakingRouter: StakingRouter__MockForLidoAccounting; @@ -50,9 +46,10 @@ describe("Accounting.sol:report", () => { let burner: Burner__MockForAccounting; beforeEach(async () => { - [deployer, stethWhale] = await ethers.getSigners(); + [deployer] = await ethers.getSigners(); [ + lido, elRewardsVault, stakingRouter, withdrawalVault, @@ -61,6 +58,7 @@ describe("Accounting.sol:report", () => { withdrawalQueue, burner, ] = await Promise.all([ + new Lido__MockForAccounting__factory(deployer).deploy(), new LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory(deployer).deploy(), new StakingRouter__MockForLidoAccounting__factory(deployer).deploy(), new WithdrawalVault__MockForLidoAccounting__factory(deployer).deploy(), @@ -70,51 +68,38 @@ describe("Accounting.sol:report", () => { new Burner__MockForAccounting__factory(deployer).deploy(), ]); - ({ lido, acl, accounting } = await deployLidoDao({ - rootAccount: deployer, - initialized: true, - locatorConfig: { - withdrawalQueue, + locator = await deployLidoLocator( + { + lido, elRewardsVault, withdrawalVault, stakingRouter, oracleReportSanityChecker, postTokenRebaseReceiver, + withdrawalQueue, burner, }, - })); - - locator = LidoLocator__factory.connect(await lido.getLidoLocator(), deployer); + deployer, + ); + + const accountingImpl = await ethers.deployContract("Accounting", [locator, lido], deployer); + const accountingProxy = await ethers.deployContract( + "OssifiableProxy", + [accountingImpl, deployer, new Uint8Array()], + deployer, + ); + accounting = await ethers.getContractAt("Accounting", accountingProxy, deployer); + await updateLidoLocatorImplementation(await locator.getAddress(), { accounting }); + await accounting.initialize(deployer); const accountingOracleSigner = await impersonate(await locator.accountingOracle(), ether("100.0")); accounting = accounting.connect(accountingOracleSigner); - - await acl.createPermission(deployer, lido, await lido.RESUME_ROLE(), deployer); - await acl.createPermission(deployer, lido, await lido.PAUSE_ROLE(), deployer); - await acl.createPermission(deployer, lido, await lido.UNSAFE_CHANGE_DEPOSITED_VALIDATORS_ROLE(), deployer); - await lido.resume(); }); context("handleOracleReport", () => { it("Update CL validators count if reported more", async () => { - let depositedValidators = 100n; - await lido.connect(deployer).unsafeChangeDepositedValidators(depositedValidators); - - // first report, 100 validators - await accounting.handleOracleReport( - report({ - clValidators: depositedValidators, - }), - ); - - const slot = streccak("lido.Lido.beaconValidators"); - const lidoAddress = await lido.getAddress(); - - let clValidatorsPosition = await getStorageAt(lidoAddress, slot); - expect(clValidatorsPosition).to.equal(depositedValidators); - - depositedValidators = 101n; - await lido.connect(deployer).unsafeChangeDepositedValidators(depositedValidators); + const depositedValidators = 100n; + await lido.setMockedDepositedValidators(depositedValidators); // second report, 101 validators await accounting.handleOracleReport( @@ -122,9 +107,6 @@ describe("Accounting.sol:report", () => { clValidators: depositedValidators, }), ); - - clValidatorsPosition = await getStorageAt(lidoAddress, slot); - expect(clValidatorsPosition).to.equal(depositedValidators); }); it("Reverts if the `checkAccountingOracleReport` sanity check fails", async () => { @@ -188,123 +170,108 @@ describe("Accounting.sol:report", () => { .withArgs(isCover, await accounting.getAddress(), steth, sharesToBurn); }); - it("Withdraws ether from `ElRewardsVault` if EL rewards are greater than 0 as returned from `smoothenTokenRebase`", async () => { - const withdrawals = 0n; - const elRewards = 1n; - const simulatedSharesToBurn = 0n; - const sharesToBurn = 0n; - - await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn( - withdrawals, - elRewards, - simulatedSharesToBurn, - sharesToBurn, - ); + // TODO: This test could be moved to `Lido.sol` + // it("Withdraws ether from `ElRewardsVault` if EL rewards are greater than 0 as returned from `smoothenTokenRebase`", async () => { - // `Mock__RewardsWithdrawn` event is only emitted on the mock to verify - // that `ElRewardsVault.withdrawRewards` was actually called - await expect(accounting.handleOracleReport(report())).to.emit(elRewardsVault, "Mock__RewardsWithdrawn"); + it("ensures that `Lido.collectRewardsAndProcessWithdrawals` is called from `Accounting`", async () => { + // `Mock__CollectRewardsAndProcessWithdrawals` event is only emitted on the mock to verify + // that `Lido.collectRewardsAndProcessWithdrawals` was actually called + await expect(accounting.handleOracleReport(report())).to.emit(lido, "Mock__CollectRewardsAndProcessWithdrawals"); }); - it("Withdraws ether from `WithdrawalVault` if withdrawals are greater than 0 as returned from `smoothenTokenRebase`", async () => { - const withdrawals = 1n; - const elRewards = 0n; - const simulatedSharesToBurn = 0n; - const sharesToBurn = 0n; - - await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn( - withdrawals, - elRewards, - simulatedSharesToBurn, - sharesToBurn, - ); - const totalFee = 1000; - const precisionPoints = 10n ** 20n; - await stakingRouter.mock__getStakingRewardsDistribution([], [], [], totalFee, precisionPoints); - - // `Mock__WithdrawalsWithdrawn` event is only emitted on the mock to verify - // that `WithdrawalVault.withdrawWithdrawals` was actually called - await expect(accounting.handleOracleReport(report())).to.emit(withdrawalVault, "Mock__WithdrawalsWithdrawn"); - }); - - it("Finalizes withdrawals if there is ether to lock on `WithdrawalQueue` as returned from `prefinalize`", async () => { - const ethToLock = ether("10.0"); - await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); - // top up buffer via submit - await lido.submit(ZeroAddress, { value: ethToLock }); - - await expect( - accounting.handleOracleReport( - report({ - withdrawalFinalizationBatches: [1n, 2n], - }), - ), - ).to.emit(withdrawalQueue, "WithdrawalsFinalized"); - }); + // it("Withdraws ether from `WithdrawalVault` if withdrawals are greater than 0 as returned from `smoothenTokenRebase`", async () => { + // const withdrawals = 1n; + // const elRewards = 0n; + // const simulatedSharesToBurn = 0n; + // const sharesToBurn = 0n; + + // await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn( + // withdrawals, + // elRewards, + // simulatedSharesToBurn, + // sharesToBurn, + // ); + // const totalFee = 1000; + // const precisionPoints = 10n ** 20n; + // await stakingRouter.mock__getStakingRewardsDistribution([], [], [], totalFee, precisionPoints); + + // // `Mock__WithdrawalsWithdrawn` event is only emitted on the mock to verify + // // that `WithdrawalVault.withdrawWithdrawals` was actually called + // await expect(accounting.handleOracleReport(report())).to.emit(withdrawalVault, "Mock__WithdrawalsWithdrawn"); + // }); - it("Updates buffered ether", async () => { - const initialBufferedEther = await lido.getBufferedEther(); - const ethToLock = 1n; + // it("Finalizes withdrawals if there is ether to lock on `WithdrawalQueue` as returned from `prefinalize`", async () => { + // const ethToLock = ether("10.0"); + // await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); + // // top up buffer via submit + // await lido.submit(ZeroAddress, { value: ethToLock }); + + // await expect( + // accounting.handleOracleReport( + // report({ + // withdrawalFinalizationBatches: [1n, 2n], + // }), + // ), + // ).to.emit(withdrawalQueue, "WithdrawalsFinalized"); + // }); - // assert that the buffer has enough eth to lock for withdrawals - // should have some eth from the initial 0xdead holder - expect(initialBufferedEther).greaterThanOrEqual(ethToLock); - await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); + // it("Updates buffered ether", async () => { + // const initialBufferedEther = await lido.getBufferedEther(); + // const ethToLock = 1n; - await expect( - accounting.handleOracleReport( - report({ - withdrawalFinalizationBatches: [1n], - }), - ), - ).to.not.be.reverted; + // // assert that the buffer has enough eth to lock for withdrawals + // // should have some eth from the initial 0xdead holder + // expect(initialBufferedEther).greaterThanOrEqual(ethToLock); + // await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); - expect(await lido.getBufferedEther()).to.equal(initialBufferedEther - ethToLock); - }); - - it("Emits an `ETHDistributed` event", async () => { - const reportTimestamp = await getNextBlockTimestamp(); - const preClBalance = 0n; - const clBalance = 1n; - const withdrawals = 0n; - const elRewards = 0n; - const bufferedEther = await lido.getBufferedEther(); + // await expect( + // accounting.handleOracleReport( + // report({ + // withdrawalFinalizationBatches: [1n], + // }), + // ), + // ).to.not.be.reverted; - const totalFee = 1000; - const precisionPoints = 10n ** 20n; - await stakingRouter.mock__getStakingRewardsDistribution([], [], [], totalFee, precisionPoints); + // expect(await lido.getBufferedEther()).to.equal(initialBufferedEther - ethToLock); + // }); - await expect( - accounting.handleOracleReport( - report({ - timestamp: reportTimestamp, - clBalance, - }), - ), - ) - .to.emit(lido, "ETHDistributed") - .withArgs(reportTimestamp, preClBalance, clBalance, withdrawals, elRewards, bufferedEther); - }); + // it("Emits an `ETHDistributed` event", async () => { + // const reportTimestamp = await getNextBlockTimestamp(); + // const preClBalance = 0n; + // const clBalance = 1n; + // const withdrawals = 0n; + // const elRewards = 0n; + // const bufferedEther = await lido.getBufferedEther(); + + // const totalFee = 1000; + // const precisionPoints = 10n ** 20n; + // await stakingRouter.mock__getStakingRewardsDistribution([], [], [], totalFee, precisionPoints); + + // await expect( + // accounting.handleOracleReport( + // report({ + // timestamp: reportTimestamp, + // clBalance, + // }), + // ), + // ) + // .to.emit(lido, "ETHDistributed") + // .withArgs(reportTimestamp, preClBalance, clBalance, withdrawals, elRewards, bufferedEther); + // }); it("Burns shares if there are shares to burn as returned from `smoothenTokenRebaseReturn`", async () => { const sharesRequestedToBurn = 1n; - await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn(0n, 0n, 0n, sharesRequestedToBurn); - // set up steth whale, in case we need to send steth to other accounts - await setBalance(stethWhale.address, ether("101.0")); - await lido.connect(stethWhale).submit(ZeroAddress, { value: ether("100.0") }); - // top up Burner with steth to burn - await lido.connect(stethWhale).transferShares(burner, sharesRequestedToBurn); - await expect( accounting.handleOracleReport( report({ sharesRequestedToBurn, }), ), - ).to.emit(burner, "Mock__CommitSharesToBurnWasCalled"); - + ) + .to.emit(burner, "Mock__CommitSharesToBurnWasCalled") + .withArgs(sharesRequestedToBurn); // TODO: SharesBurnt event is not emitted anymore because of the mock implementation // .and.to.emit(lido, "SharesBurnt") // .withArgs(await burner.getAddress(), sharesRequestedToBurn, sharesRequestedToBurn, sharesRequestedToBurn); @@ -392,125 +359,122 @@ describe("Accounting.sol:report", () => { clBalance: 1n, }), ), - ) - .not.to.emit(lido, "Transfer") - .and.not.to.emit(lido, "TransferShares") - .and.not.to.emit(stakingRouter, "Mock__MintedRewardsReported"); - }); - - it("Mints shares to itself and then transfers them to recipients if there are fees to distribute as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { - // initially, before any rebases, one share costs one steth - expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); - // thus, the total supply of steth should equal the total number of shares - expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); - - // mock a single staking module with 5% fee with the total protocol fee of 10% - const stakingModule = { - address: certainAddress("lido:handleOracleReport:staking-module"), - id: 1n, - fee: 5n * 10n ** 18n, // 5% - }; - - const totalFee = 10n * 10n ** 18n; // 10% - const precisionPoints = 100n * 10n ** 18n; // 100% - - await stakingRouter.mock__getStakingRewardsDistribution( - [stakingModule.address], - [stakingModule.id], - [stakingModule.fee], - totalFee, - precisionPoints, - ); - - const clBalance = ether("1.0"); - - const expectedSharesToMint = - (clBalance * totalFee * (await lido.getTotalShares())) / - (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); - - const expectedModuleRewardInShares = expectedSharesToMint / (totalFee / stakingModule.fee); - const expectedTreasuryCutInShares = expectedSharesToMint - expectedModuleRewardInShares; - - await expect( - accounting.handleOracleReport( - report({ - clBalance: ether("1.0"), // 1 ether of profit - }), - ), - ) - .to.emit(lido, "TransferShares") - .withArgs(ZeroAddress, stakingModule.address, expectedModuleRewardInShares) - .and.to.emit(lido, "TransferShares") - .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) - .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); - - expect(await lido.balanceOf(stakingModule.address)).to.equal( - await lido.getPooledEthByShares(expectedModuleRewardInShares), - ); - - expect(await lido.balanceOf(await lido.getTreasury())).to.equal( - await lido.getPooledEthByShares(expectedTreasuryCutInShares), - ); - - // now one share should cost 1.9 steth (10% was distributed as rewards) - expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); + ).not.to.emit(stakingRouter, "Mock__MintedRewardsReported"); }); - it("Transfers all new shares to treasury if the module fee is zero as returned `StakingRouter.getStakingRewardsDistribution`", async () => { - // initially, before any rebases, one share costs one steth - expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); - // thus, the total supply of steth should equal the total number of shares - expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); - - // mock a single staking module with 0% fee with the total protocol fee of 10% - const stakingModule = { - address: certainAddress("lido:handleOracleReport:staking-module"), - id: 1n, - fee: 0n, - }; - - const totalFee = 10n * 10n ** 18n; // 10% - const precisionPoints = 100n * 10n ** 18n; // 100% - - await stakingRouter.mock__getStakingRewardsDistribution( - [stakingModule.address], - [stakingModule.id], - [stakingModule.fee], - totalFee, - precisionPoints, - ); - - const clBalance = ether("1.0"); - - const expectedSharesToMint = - (clBalance * totalFee * (await lido.getTotalShares())) / - (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); - - const expectedModuleRewardInShares = 0n; - const expectedTreasuryCutInShares = expectedSharesToMint; - - await expect( - accounting.handleOracleReport( - report({ - clBalance: ether("1.0"), // 1 ether of profit - }), - ), - ) - .and.to.emit(lido, "TransferShares") - .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) - .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); - - expect(await lido.balanceOf(stakingModule.address)).to.equal( - await lido.getPooledEthByShares(expectedModuleRewardInShares), - ); - - expect(await lido.balanceOf(await lido.getTreasury())).to.equal( - await lido.getPooledEthByShares(expectedTreasuryCutInShares), - ); + // it("Mints shares to itself and then transfers them to recipients if there are fees to distribute as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { + // // initially, before any rebases, one share costs one steth + // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); + // // thus, the total supply of steth should equal the total number of shares + // expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); + + // // mock a single staking module with 5% fee with the total protocol fee of 10% + // const stakingModule = { + // address: certainAddress("lido:handleOracleReport:staking-module"), + // id: 1n, + // fee: 5n * 10n ** 18n, // 5% + // }; + + // const totalFee = 10n * 10n ** 18n; // 10% + // const precisionPoints = 100n * 10n ** 18n; // 100% + + // await stakingRouter.mock__getStakingRewardsDistribution( + // [stakingModule.address], + // [stakingModule.id], + // [stakingModule.fee], + // totalFee, + // precisionPoints, + // ); + + // const clBalance = ether("1.0"); + + // const expectedSharesToMint = + // (clBalance * totalFee * (await lido.getTotalShares())) / + // (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); + + // const expectedModuleRewardInShares = expectedSharesToMint / (totalFee / stakingModule.fee); + // const expectedTreasuryCutInShares = expectedSharesToMint - expectedModuleRewardInShares; + + // await expect( + // accounting.handleOracleReport( + // report({ + // clBalance: ether("1.0"), // 1 ether of profit + // }), + // ), + // ) + // .to.emit(lido, "TransferShares") + // .withArgs(ZeroAddress, stakingModule.address, expectedModuleRewardInShares) + // .and.to.emit(lido, "TransferShares") + // .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) + // .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); + + // expect(await lido.balanceOf(stakingModule.address)).to.equal( + // await lido.getPooledEthByShares(expectedModuleRewardInShares), + // ); + + // expect(await lido.balanceOf(await lido.getTreasury())).to.equal( + // await lido.getPooledEthByShares(expectedTreasuryCutInShares), + // ); + + // // now one share should cost 1.9 steth (10% was distributed as rewards) + // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); + // }); - // now one share should cost 1.9 steth (10% was distributed as rewards) - expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); - }); + // it("Transfers all new shares to treasury if the module fee is zero as returned `StakingRouter.getStakingRewardsDistribution`", async () => { + // // initially, before any rebases, one share costs one steth + // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); + // // thus, the total supply of steth should equal the total number of shares + // expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); + + // // mock a single staking module with 0% fee with the total protocol fee of 10% + // const stakingModule = { + // address: certainAddress("lido:handleOracleReport:staking-module"), + // id: 1n, + // fee: 0n, + // }; + + // const totalFee = 10n * 10n ** 18n; // 10% + // const precisionPoints = 100n * 10n ** 18n; // 100% + + // await stakingRouter.mock__getStakingRewardsDistribution( + // [stakingModule.address], + // [stakingModule.id], + // [stakingModule.fee], + // totalFee, + // precisionPoints, + // ); + + // const clBalance = ether("1.0"); + + // const expectedSharesToMint = + // (clBalance * totalFee * (await lido.getTotalShares())) / + // (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); + + // const expectedModuleRewardInShares = 0n; + // const expectedTreasuryCutInShares = expectedSharesToMint; + + // await expect( + // accounting.handleOracleReport( + // report({ + // clBalance: ether("1.0"), // 1 ether of profit + // }), + // ), + // ) + // .and.to.emit(lido, "TransferShares") + // .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) + // .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); + + // expect(await lido.balanceOf(stakingModule.address)).to.equal( + // await lido.getPooledEthByShares(expectedModuleRewardInShares), + // ); + + // expect(await lido.balanceOf(await lido.getTreasury())).to.equal( + // await lido.getPooledEthByShares(expectedTreasuryCutInShares), + // ); + + // // now one share should cost 1.9 steth (10% was distributed as rewards) + // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); + // }); it("Relays the report data to `PostTokenRebaseReceiver`", async () => { await expect(accounting.handleOracleReport(report())).to.emit( @@ -520,7 +484,7 @@ describe("Accounting.sol:report", () => { }); it("Does not relay the report data to `PostTokenRebaseReceiver` if the locator returns zero address", async () => { - const lidoLocatorAddress = await lido.getLidoLocator(); + const lidoLocatorAddress = await locator.getAddress(); // Change the locator implementation to support zero address await updateLidoLocatorImplementation(lidoLocatorAddress, {}, "LidoLocator__MockMutable", deployer); From 43482eb6bec1af9481f15e91f02e86b28bb6fabf Mon Sep 17 00:00:00 2001 From: VP Date: Wed, 15 Jan 2025 16:27:39 +0100 Subject: [PATCH 19/27] test: port some tests back to Lido contact tests --- test/0.4.24/lido/lido.accounting.test.ts | 47 ++++++++++- .../accounting.handleOracleReport.test.ts | 83 ------------------- 2 files changed, 45 insertions(+), 85 deletions(-) diff --git a/test/0.4.24/lido/lido.accounting.test.ts b/test/0.4.24/lido/lido.accounting.test.ts index 344df58d2..dfd104f2d 100644 --- a/test/0.4.24/lido/lido.accounting.test.ts +++ b/test/0.4.24/lido/lido.accounting.test.ts @@ -23,7 +23,7 @@ import { WithdrawalVault__MockForLidoAccounting__factory, } from "typechain-types"; -import { ether, impersonate } from "lib"; +import { ether, getNextBlockTimestamp, impersonate } from "lib"; import { deployLidoDao } from "test/deploy"; @@ -34,6 +34,7 @@ describe("Lido:accounting", () => { let lido: Lido; let acl: ACL; let postTokenRebaseReceiver: IPostTokenRebaseReceiver; + let locator: LidoLocator; let elRewardsVault: LidoExecutionLayerRewardsVault__MockForLidoAccounting; let withdrawalVault: WithdrawalVault__MockForLidoAccounting; @@ -76,6 +77,7 @@ describe("Lido:accounting", () => { burner, }, })); + locator = LidoLocator__factory.connect(await lido.getLidoLocator(), deployer); await acl.createPermission(deployer, lido, await lido.RESUME_ROLE(), deployer); await acl.createPermission(deployer, lido, await lido.PAUSE_ROLE(), deployer); @@ -94,7 +96,6 @@ describe("Lido:accounting", () => { }); it("Updates beacon stats", async () => { - const locator = LidoLocator__factory.connect(await lido.getLidoLocator(), deployer); const accountingSigner = await impersonate(await locator.accounting(), ether("100.0")); lido = lido.connect(accountingSigner); await expect( @@ -141,6 +142,48 @@ describe("Lido:accounting", () => { ); }); + it("Updates buffered ether", async () => { + const initialBufferedEther = await lido.getBufferedEther(); + const ethToLock = 1n; + + // assert that the buffer has enough eth to lock for withdrawals + // should have some eth from the initial 0xdead holder + expect(initialBufferedEther).greaterThanOrEqual(ethToLock); + await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); + + const accountingSigner = await impersonate(await locator.accounting(), ether("100.0")); + lido = lido.connect(accountingSigner); + + await lido.collectRewardsAndProcessWithdrawals(...args({ etherToLockOnWithdrawalQueue: ethToLock })); + expect(await lido.getBufferedEther()).to.equal(initialBufferedEther - ethToLock); + }); + + it("Emits an `ETHDistributed` event", async () => { + const reportTimestamp = await getNextBlockTimestamp(); + const preClBalance = 0n; + const clBalance = 1n; + const withdrawals = 0n; + const elRewards = 0n; + const bufferedEther = await lido.getBufferedEther(); + + const totalFee = 1000; + const precisionPoints = 10n ** 20n; + await stakingRouter.mock__getStakingRewardsDistribution([], [], [], totalFee, precisionPoints); + + const accountingSigner = await impersonate(await locator.accounting(), ether("100.0")); + lido = lido.connect(accountingSigner); + await expect( + lido.collectRewardsAndProcessWithdrawals( + ...args({ + reportTimestamp, + reportClBalance: clBalance, + }), + ), + ) + .to.emit(lido, "ETHDistributed") + .withArgs(reportTimestamp, preClBalance, clBalance, withdrawals, elRewards, bufferedEther); + }); + type ArgsTuple = [bigint, bigint, bigint, bigint, bigint, bigint, bigint, bigint]; interface Args { diff --git a/test/0.8.9/accounting.handleOracleReport.test.ts b/test/0.8.9/accounting.handleOracleReport.test.ts index f1685ad0e..4c002724b 100644 --- a/test/0.8.9/accounting.handleOracleReport.test.ts +++ b/test/0.8.9/accounting.handleOracleReport.test.ts @@ -170,95 +170,12 @@ describe("Accounting.sol:report", () => { .withArgs(isCover, await accounting.getAddress(), steth, sharesToBurn); }); - // TODO: This test could be moved to `Lido.sol` - // it("Withdraws ether from `ElRewardsVault` if EL rewards are greater than 0 as returned from `smoothenTokenRebase`", async () => { - it("ensures that `Lido.collectRewardsAndProcessWithdrawals` is called from `Accounting`", async () => { // `Mock__CollectRewardsAndProcessWithdrawals` event is only emitted on the mock to verify // that `Lido.collectRewardsAndProcessWithdrawals` was actually called await expect(accounting.handleOracleReport(report())).to.emit(lido, "Mock__CollectRewardsAndProcessWithdrawals"); }); - // it("Withdraws ether from `WithdrawalVault` if withdrawals are greater than 0 as returned from `smoothenTokenRebase`", async () => { - // const withdrawals = 1n; - // const elRewards = 0n; - // const simulatedSharesToBurn = 0n; - // const sharesToBurn = 0n; - - // await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn( - // withdrawals, - // elRewards, - // simulatedSharesToBurn, - // sharesToBurn, - // ); - // const totalFee = 1000; - // const precisionPoints = 10n ** 20n; - // await stakingRouter.mock__getStakingRewardsDistribution([], [], [], totalFee, precisionPoints); - - // // `Mock__WithdrawalsWithdrawn` event is only emitted on the mock to verify - // // that `WithdrawalVault.withdrawWithdrawals` was actually called - // await expect(accounting.handleOracleReport(report())).to.emit(withdrawalVault, "Mock__WithdrawalsWithdrawn"); - // }); - - // it("Finalizes withdrawals if there is ether to lock on `WithdrawalQueue` as returned from `prefinalize`", async () => { - // const ethToLock = ether("10.0"); - // await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); - // // top up buffer via submit - // await lido.submit(ZeroAddress, { value: ethToLock }); - - // await expect( - // accounting.handleOracleReport( - // report({ - // withdrawalFinalizationBatches: [1n, 2n], - // }), - // ), - // ).to.emit(withdrawalQueue, "WithdrawalsFinalized"); - // }); - - // it("Updates buffered ether", async () => { - // const initialBufferedEther = await lido.getBufferedEther(); - // const ethToLock = 1n; - - // // assert that the buffer has enough eth to lock for withdrawals - // // should have some eth from the initial 0xdead holder - // expect(initialBufferedEther).greaterThanOrEqual(ethToLock); - // await withdrawalQueue.mock__prefinalizeReturn(ethToLock, 0n); - - // await expect( - // accounting.handleOracleReport( - // report({ - // withdrawalFinalizationBatches: [1n], - // }), - // ), - // ).to.not.be.reverted; - - // expect(await lido.getBufferedEther()).to.equal(initialBufferedEther - ethToLock); - // }); - - // it("Emits an `ETHDistributed` event", async () => { - // const reportTimestamp = await getNextBlockTimestamp(); - // const preClBalance = 0n; - // const clBalance = 1n; - // const withdrawals = 0n; - // const elRewards = 0n; - // const bufferedEther = await lido.getBufferedEther(); - - // const totalFee = 1000; - // const precisionPoints = 10n ** 20n; - // await stakingRouter.mock__getStakingRewardsDistribution([], [], [], totalFee, precisionPoints); - - // await expect( - // accounting.handleOracleReport( - // report({ - // timestamp: reportTimestamp, - // clBalance, - // }), - // ), - // ) - // .to.emit(lido, "ETHDistributed") - // .withArgs(reportTimestamp, preClBalance, clBalance, withdrawals, elRewards, bufferedEther); - // }); - it("Burns shares if there are shares to burn as returned from `smoothenTokenRebaseReturn`", async () => { const sharesRequestedToBurn = 1n; await oracleReportSanityChecker.mock__smoothenTokenRebaseReturn(0n, 0n, 0n, sharesRequestedToBurn); From 5d3b2ff815b7ccb953d21c35d1514e98b7cdba19 Mon Sep 17 00:00:00 2001 From: VP Date: Thu, 16 Jan 2025 11:37:16 +0100 Subject: [PATCH 20/27] test: fix mint accounting tests --- .../accounting.handleOracleReport.test.ts | 195 ++++++++---------- .../contracts/Lido__MockForAccounting.sol | 10 + 2 files changed, 93 insertions(+), 112 deletions(-) diff --git a/test/0.8.9/accounting.handleOracleReport.test.ts b/test/0.8.9/accounting.handleOracleReport.test.ts index 4c002724b..f4a737512 100644 --- a/test/0.8.9/accounting.handleOracleReport.test.ts +++ b/test/0.8.9/accounting.handleOracleReport.test.ts @@ -279,119 +279,90 @@ describe("Accounting.sol:report", () => { ).not.to.emit(stakingRouter, "Mock__MintedRewardsReported"); }); - // it("Mints shares to itself and then transfers them to recipients if there are fees to distribute as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { - // // initially, before any rebases, one share costs one steth - // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); - // // thus, the total supply of steth should equal the total number of shares - // expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); - - // // mock a single staking module with 5% fee with the total protocol fee of 10% - // const stakingModule = { - // address: certainAddress("lido:handleOracleReport:staking-module"), - // id: 1n, - // fee: 5n * 10n ** 18n, // 5% - // }; - - // const totalFee = 10n * 10n ** 18n; // 10% - // const precisionPoints = 100n * 10n ** 18n; // 100% - - // await stakingRouter.mock__getStakingRewardsDistribution( - // [stakingModule.address], - // [stakingModule.id], - // [stakingModule.fee], - // totalFee, - // precisionPoints, - // ); - - // const clBalance = ether("1.0"); - - // const expectedSharesToMint = - // (clBalance * totalFee * (await lido.getTotalShares())) / - // (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); - - // const expectedModuleRewardInShares = expectedSharesToMint / (totalFee / stakingModule.fee); - // const expectedTreasuryCutInShares = expectedSharesToMint - expectedModuleRewardInShares; - - // await expect( - // accounting.handleOracleReport( - // report({ - // clBalance: ether("1.0"), // 1 ether of profit - // }), - // ), - // ) - // .to.emit(lido, "TransferShares") - // .withArgs(ZeroAddress, stakingModule.address, expectedModuleRewardInShares) - // .and.to.emit(lido, "TransferShares") - // .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) - // .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); - - // expect(await lido.balanceOf(stakingModule.address)).to.equal( - // await lido.getPooledEthByShares(expectedModuleRewardInShares), - // ); - - // expect(await lido.balanceOf(await lido.getTreasury())).to.equal( - // await lido.getPooledEthByShares(expectedTreasuryCutInShares), - // ); - - // // now one share should cost 1.9 steth (10% was distributed as rewards) - // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); - // }); + it("Mints shares to itself and then transfers them to recipients if there are fees to distribute as returned from `StakingRouter.getStakingRewardsDistribution`", async () => { + // mock a single staking module with 5% fee with the total protocol fee of 10% + const stakingModule = { + address: certainAddress("lido:handleOracleReport:staking-module"), + id: 1n, + fee: 5n * 10n ** 18n, // 5% + }; - // it("Transfers all new shares to treasury if the module fee is zero as returned `StakingRouter.getStakingRewardsDistribution`", async () => { - // // initially, before any rebases, one share costs one steth - // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.0")); - // // thus, the total supply of steth should equal the total number of shares - // expect(await lido.getTotalPooledEther()).to.equal(await lido.getTotalShares()); - - // // mock a single staking module with 0% fee with the total protocol fee of 10% - // const stakingModule = { - // address: certainAddress("lido:handleOracleReport:staking-module"), - // id: 1n, - // fee: 0n, - // }; - - // const totalFee = 10n * 10n ** 18n; // 10% - // const precisionPoints = 100n * 10n ** 18n; // 100% - - // await stakingRouter.mock__getStakingRewardsDistribution( - // [stakingModule.address], - // [stakingModule.id], - // [stakingModule.fee], - // totalFee, - // precisionPoints, - // ); - - // const clBalance = ether("1.0"); - - // const expectedSharesToMint = - // (clBalance * totalFee * (await lido.getTotalShares())) / - // (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); - - // const expectedModuleRewardInShares = 0n; - // const expectedTreasuryCutInShares = expectedSharesToMint; - - // await expect( - // accounting.handleOracleReport( - // report({ - // clBalance: ether("1.0"), // 1 ether of profit - // }), - // ), - // ) - // .and.to.emit(lido, "TransferShares") - // .withArgs(ZeroAddress, await lido.getTreasury(), expectedTreasuryCutInShares) - // .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); - - // expect(await lido.balanceOf(stakingModule.address)).to.equal( - // await lido.getPooledEthByShares(expectedModuleRewardInShares), - // ); - - // expect(await lido.balanceOf(await lido.getTreasury())).to.equal( - // await lido.getPooledEthByShares(expectedTreasuryCutInShares), - // ); - - // // now one share should cost 1.9 steth (10% was distributed as rewards) - // expect(await lido.getPooledEthByShares(ether("1.0"))).to.equal(ether("1.9")); - // }); + const totalFee = 10n * 10n ** 18n; // 10% + const precisionPoints = 100n * 10n ** 18n; // 100% + + await stakingRouter.mock__getStakingRewardsDistribution( + [stakingModule.address], + [stakingModule.id], + [stakingModule.fee], + totalFee, + precisionPoints, + ); + + const clBalance = ether("1.0"); + const expectedSharesToMint = + (clBalance * totalFee * (await lido.getTotalShares())) / + (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); + + const expectedModuleRewardInShares = expectedSharesToMint / (totalFee / stakingModule.fee); + const expectedTreasuryCutInShares = expectedSharesToMint - expectedModuleRewardInShares; + + console.log("expectedModuleRewardInShares", expectedModuleRewardInShares); + console.log("expectedTreasuryCutInShares", expectedTreasuryCutInShares); + console.log("stakingModule.address", stakingModule.address); + console.log("await locator.treasury()", await locator.treasury()); + + await expect( + accounting.handleOracleReport( + report({ + clBalance: ether("1.0"), // 1 ether of profit + }), + ), + ) + .to.emit(lido, "TransferShares") + .withArgs(ZeroAddress, stakingModule.address, expectedModuleRewardInShares) + .and.to.emit(lido, "TransferShares") + .withArgs(ZeroAddress, await locator.treasury(), expectedTreasuryCutInShares) + .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); + }); + + it("Transfers all new shares to treasury if the module fee is zero as returned `StakingRouter.getStakingRewardsDistribution`", async () => { + // mock a single staking module with 0% fee with the total protocol fee of 10% + const stakingModule = { + address: certainAddress("lido:handleOracleReport:staking-module"), + id: 1n, + fee: 0n, + }; + + const totalFee = 10n * 10n ** 18n; // 10% + const precisionPoints = 100n * 10n ** 18n; // 100% + + await stakingRouter.mock__getStakingRewardsDistribution( + [stakingModule.address], + [stakingModule.id], + [stakingModule.fee], + totalFee, + precisionPoints, + ); + + const clBalance = ether("1.0"); + + const expectedSharesToMint = + (clBalance * totalFee * (await lido.getTotalShares())) / + (((await lido.getTotalPooledEther()) + clBalance) * precisionPoints - clBalance * totalFee); + + const expectedTreasuryCutInShares = expectedSharesToMint; + + await expect( + accounting.handleOracleReport( + report({ + clBalance: ether("1.0"), // 1 ether of profit + }), + ), + ) + .and.to.emit(lido, "TransferShares") + .withArgs(ZeroAddress, await locator.treasury(), expectedTreasuryCutInShares) + .and.to.emit(stakingRouter, "Mock__MintedRewardsReported"); + }); it("Relays the report data to `PostTokenRebaseReceiver`", async () => { await expect(accounting.handleOracleReport(report())).to.emit( diff --git a/test/0.8.9/contracts/Lido__MockForAccounting.sol b/test/0.8.9/contracts/Lido__MockForAccounting.sol index dcc2a5944..19135c140 100644 --- a/test/0.8.9/contracts/Lido__MockForAccounting.sol +++ b/test/0.8.9/contracts/Lido__MockForAccounting.sol @@ -20,6 +20,12 @@ contract Lido__MockForAccounting { uint256 _withdrawalsShareRate, uint256 _etherToLockOnWithdrawalQueue ); + /** + * @notice An executed shares transfer from `sender` to `recipient`. + * + * @dev emitted in pair with an ERC20-defined `Transfer` event. + */ + event TransferShares(address indexed from, address indexed to, uint256 sharesValue); function setMockedDepositedValidators(uint256 _amount) external { depositedValidatorsValue = _amount; @@ -104,4 +110,8 @@ contract Lido__MockForAccounting { emit CLValidatorsUpdated(_reportTimestamp, _preClValidators, _reportClValidators); } + + function mintShares(address _recipient, uint256 _sharesAmount) external { + emit TransferShares(address(0), _recipient, _sharesAmount); + } } From e10f79654c484de6e35f9e2a3e2f7fc2e2458d6b Mon Sep 17 00:00:00 2001 From: VP Date: Thu, 16 Jan 2025 11:40:18 +0100 Subject: [PATCH 21/27] test: fix import --- test/0.4.24/lido/lido.accounting.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/0.4.24/lido/lido.accounting.test.ts b/test/0.4.24/lido/lido.accounting.test.ts index dfd104f2d..10641e061 100644 --- a/test/0.4.24/lido/lido.accounting.test.ts +++ b/test/0.4.24/lido/lido.accounting.test.ts @@ -11,6 +11,7 @@ import { Lido, LidoExecutionLayerRewardsVault__MockForLidoAccounting, LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory, + LidoLocator, LidoLocator__factory, OracleReportSanityChecker__MockForAccounting, OracleReportSanityChecker__MockForAccounting__factory, From b7fdc3230e2682652b4ddf52da9f2e70d282317a Mon Sep 17 00:00:00 2001 From: VP Date: Thu, 16 Jan 2025 16:15:42 +0100 Subject: [PATCH 22/27] test: remove unused mocks --- ...yerRewardsVault__MockForLidoAccounting.sol | 14 ---- ...ReportSanityChecker__MockForAccounting.sol | 77 ------------------- ...WithdrawalVault__MockForLidoAccounting.sol | 15 ---- test/0.4.24/lido/lido.accounting.test.ts | 30 +------- 4 files changed, 1 insertion(+), 135 deletions(-) delete mode 100644 test/0.4.24/contracts/LidoExecutionLayerRewardsVault__MockForLidoAccounting.sol delete mode 100644 test/0.4.24/contracts/OracleReportSanityChecker__MockForAccounting.sol delete mode 100644 test/0.4.24/contracts/WithdrawalVault__MockForLidoAccounting.sol diff --git a/test/0.4.24/contracts/LidoExecutionLayerRewardsVault__MockForLidoAccounting.sol b/test/0.4.24/contracts/LidoExecutionLayerRewardsVault__MockForLidoAccounting.sol deleted file mode 100644 index 0dc35aa7d..000000000 --- a/test/0.4.24/contracts/LidoExecutionLayerRewardsVault__MockForLidoAccounting.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// for testing purposes only - -pragma solidity 0.4.24; - -contract LidoExecutionLayerRewardsVault__MockForLidoAccounting { - event Mock__RewardsWithdrawn(); - - function withdrawRewards(uint256 _maxAmount) external returns (uint256 amount) { - // emitting mock event to test that the function was in fact called - emit Mock__RewardsWithdrawn(); - return _maxAmount; - } -} diff --git a/test/0.4.24/contracts/OracleReportSanityChecker__MockForAccounting.sol b/test/0.4.24/contracts/OracleReportSanityChecker__MockForAccounting.sol deleted file mode 100644 index 73280340c..000000000 --- a/test/0.4.24/contracts/OracleReportSanityChecker__MockForAccounting.sol +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// for testing purposes only - -pragma solidity 0.4.24; - -contract OracleReportSanityChecker__MockForAccounting { - bool private checkAccountingOracleReportReverts; - bool private checkWithdrawalQueueOracleReportReverts; - - uint256 private _withdrawals; - uint256 private _elRewards; - uint256 private _simulatedSharesToBurn; - uint256 private _sharesToBurn; - - function checkAccountingOracleReport( - uint256 _timeElapsed, - uint256 _preCLBalance, - uint256 _postCLBalance, - uint256 _withdrawalVaultBalance, - uint256 _elRewardsVaultBalance, - uint256 _sharesRequestedToBurn, - uint256 _preCLValidators, - uint256 _postCLValidators - ) external view { - if (checkAccountingOracleReportReverts) revert(); - } - - function checkWithdrawalQueueOracleReport( - uint256 _lastFinalizableRequestId, - uint256 _reportTimestamp - ) external view { - if (checkWithdrawalQueueOracleReportReverts) revert(); - } - - function smoothenTokenRebase( - uint256 _preTotalPooledEther, - uint256 _preTotalShares, - uint256 _preCLBalance, - uint256 _postCLBalance, - uint256 _withdrawalVaultBalance, - uint256 _elRewardsVaultBalance, - uint256 _sharesRequestedToBurn, - uint256 _etherToLockForWithdrawals, - uint256 _newSharesToBurnForWithdrawals - ) - external - view - returns (uint256 withdrawals, uint256 elRewards, uint256 simulatedSharesToBurn, uint256 sharesToBurn) - { - withdrawals = _withdrawals; - elRewards = _elRewards; - simulatedSharesToBurn = _simulatedSharesToBurn; - sharesToBurn = _sharesToBurn; - } - - // mocking - - function mock__checkAccountingOracleReportReverts(bool reverts) external { - checkAccountingOracleReportReverts = reverts; - } - - function mock__checkWithdrawalQueueOracleReportReverts(bool reverts) external { - checkWithdrawalQueueOracleReportReverts = reverts; - } - - function mock__smoothenTokenRebaseReturn( - uint256 withdrawals, - uint256 elRewards, - uint256 simulatedSharesToBurn, - uint256 sharesToBurn - ) external { - _withdrawals = withdrawals; - _elRewards = elRewards; - _simulatedSharesToBurn = simulatedSharesToBurn; - _sharesToBurn = sharesToBurn; - } -} diff --git a/test/0.4.24/contracts/WithdrawalVault__MockForLidoAccounting.sol b/test/0.4.24/contracts/WithdrawalVault__MockForLidoAccounting.sol deleted file mode 100644 index fccca7ecd..000000000 --- a/test/0.4.24/contracts/WithdrawalVault__MockForLidoAccounting.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// for testing purposes only - -pragma solidity 0.4.24; - -contract WithdrawalVault__MockForLidoAccounting { - event Mock__WithdrawalsWithdrawn(); - - function withdrawWithdrawals(uint256 _amount) external { - _amount; - - // emitting mock event to test that the function was in fact called - emit Mock__WithdrawalsWithdrawn(); - } -} diff --git a/test/0.4.24/lido/lido.accounting.test.ts b/test/0.4.24/lido/lido.accounting.test.ts index 10641e061..c3fdbab17 100644 --- a/test/0.4.24/lido/lido.accounting.test.ts +++ b/test/0.4.24/lido/lido.accounting.test.ts @@ -7,21 +7,13 @@ import { ACL, Burner__MockForAccounting, Burner__MockForAccounting__factory, - IPostTokenRebaseReceiver, Lido, - LidoExecutionLayerRewardsVault__MockForLidoAccounting, - LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory, LidoLocator, LidoLocator__factory, - OracleReportSanityChecker__MockForAccounting, - OracleReportSanityChecker__MockForAccounting__factory, - PostTokenRebaseReceiver__MockForAccounting__factory, StakingRouter__MockForLidoAccounting, StakingRouter__MockForLidoAccounting__factory, WithdrawalQueue__MockForAccounting, WithdrawalQueue__MockForAccounting__factory, - WithdrawalVault__MockForLidoAccounting, - WithdrawalVault__MockForLidoAccounting__factory, } from "typechain-types"; import { ether, getNextBlockTimestamp, impersonate } from "lib"; @@ -34,33 +26,17 @@ describe("Lido:accounting", () => { let lido: Lido; let acl: ACL; - let postTokenRebaseReceiver: IPostTokenRebaseReceiver; let locator: LidoLocator; - let elRewardsVault: LidoExecutionLayerRewardsVault__MockForLidoAccounting; - let withdrawalVault: WithdrawalVault__MockForLidoAccounting; let stakingRouter: StakingRouter__MockForLidoAccounting; - let oracleReportSanityChecker: OracleReportSanityChecker__MockForAccounting; let withdrawalQueue: WithdrawalQueue__MockForAccounting; let burner: Burner__MockForAccounting; beforeEach(async () => { [deployer, stranger] = await ethers.getSigners(); - [ - elRewardsVault, - stakingRouter, - withdrawalVault, - oracleReportSanityChecker, - postTokenRebaseReceiver, - withdrawalQueue, - burner, - ] = await Promise.all([ - new LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory(deployer).deploy(), + [stakingRouter, withdrawalQueue, burner] = await Promise.all([ new StakingRouter__MockForLidoAccounting__factory(deployer).deploy(), - new WithdrawalVault__MockForLidoAccounting__factory(deployer).deploy(), - new OracleReportSanityChecker__MockForAccounting__factory(deployer).deploy(), - new PostTokenRebaseReceiver__MockForAccounting__factory(deployer).deploy(), new WithdrawalQueue__MockForAccounting__factory(deployer).deploy(), new Burner__MockForAccounting__factory(deployer).deploy(), ]); @@ -70,11 +46,7 @@ describe("Lido:accounting", () => { initialized: true, locatorConfig: { withdrawalQueue, - elRewardsVault, - withdrawalVault, stakingRouter, - oracleReportSanityChecker, - postTokenRebaseReceiver, burner, }, })); From 633d6ba8b7a18a73882de377d50ba641d5053d72 Mon Sep 17 00:00:00 2001 From: VP Date: Thu, 16 Jan 2025 16:16:52 +0100 Subject: [PATCH 23/27] test: rename mock event --- .../contracts/Burner__MockForAccounting.sol | 4 +- .../accounting.handleOracleReport.test.ts | 40 +++++-------------- 2 files changed, 13 insertions(+), 31 deletions(-) diff --git a/test/0.4.24/contracts/Burner__MockForAccounting.sol b/test/0.4.24/contracts/Burner__MockForAccounting.sol index 776c84829..a8a3bd36d 100644 --- a/test/0.4.24/contracts/Burner__MockForAccounting.sol +++ b/test/0.4.24/contracts/Burner__MockForAccounting.sol @@ -4,7 +4,7 @@ pragma solidity 0.4.24; contract Burner__MockForAccounting { - event StETHBurnRequested( + event Mock__StETHBurnRequested( bool indexed isCover, address indexed requestedBy, uint256 amountOfStETH, @@ -16,7 +16,7 @@ contract Burner__MockForAccounting { function requestBurnShares(address, uint256 _sharesAmountToBurn) external { // imitating share to steth rate 1:2 uint256 _stETHAmount = _sharesAmountToBurn * 2; - emit StETHBurnRequested(false, msg.sender, _stETHAmount, _sharesAmountToBurn); + emit Mock__StETHBurnRequested(false, msg.sender, _stETHAmount, _sharesAmountToBurn); } function commitSharesToBurn(uint256 _sharesToBurn) external { diff --git a/test/0.8.9/accounting.handleOracleReport.test.ts b/test/0.8.9/accounting.handleOracleReport.test.ts index f4a737512..ef24aeaca 100644 --- a/test/0.8.9/accounting.handleOracleReport.test.ts +++ b/test/0.8.9/accounting.handleOracleReport.test.ts @@ -11,8 +11,6 @@ import { IPostTokenRebaseReceiver, Lido__MockForAccounting, Lido__MockForAccounting__factory, - LidoExecutionLayerRewardsVault__MockForLidoAccounting, - LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory, LidoLocator, OracleReportSanityChecker__MockForAccounting, OracleReportSanityChecker__MockForAccounting__factory, @@ -21,8 +19,6 @@ import { StakingRouter__MockForLidoAccounting__factory, WithdrawalQueue__MockForAccounting, WithdrawalQueue__MockForAccounting__factory, - WithdrawalVault__MockForLidoAccounting, - WithdrawalVault__MockForLidoAccounting__factory, } from "typechain-types"; import { ReportValuesStruct } from "typechain-types/contracts/0.8.9/oracle/AccountingOracle.sol/IReportReceiver"; @@ -38,8 +34,6 @@ describe("Accounting.sol:report", () => { let locator: LidoLocator; let lido: Lido__MockForAccounting; - let elRewardsVault: LidoExecutionLayerRewardsVault__MockForLidoAccounting; - let withdrawalVault: WithdrawalVault__MockForLidoAccounting; let stakingRouter: StakingRouter__MockForLidoAccounting; let oracleReportSanityChecker: OracleReportSanityChecker__MockForAccounting; let withdrawalQueue: WithdrawalQueue__MockForAccounting; @@ -48,31 +42,19 @@ describe("Accounting.sol:report", () => { beforeEach(async () => { [deployer] = await ethers.getSigners(); - [ - lido, - elRewardsVault, - stakingRouter, - withdrawalVault, - oracleReportSanityChecker, - postTokenRebaseReceiver, - withdrawalQueue, - burner, - ] = await Promise.all([ - new Lido__MockForAccounting__factory(deployer).deploy(), - new LidoExecutionLayerRewardsVault__MockForLidoAccounting__factory(deployer).deploy(), - new StakingRouter__MockForLidoAccounting__factory(deployer).deploy(), - new WithdrawalVault__MockForLidoAccounting__factory(deployer).deploy(), - new OracleReportSanityChecker__MockForAccounting__factory(deployer).deploy(), - new PostTokenRebaseReceiver__MockForAccounting__factory(deployer).deploy(), - new WithdrawalQueue__MockForAccounting__factory(deployer).deploy(), - new Burner__MockForAccounting__factory(deployer).deploy(), - ]); + [lido, stakingRouter, oracleReportSanityChecker, postTokenRebaseReceiver, withdrawalQueue, burner] = + await Promise.all([ + new Lido__MockForAccounting__factory(deployer).deploy(), + new StakingRouter__MockForLidoAccounting__factory(deployer).deploy(), + new OracleReportSanityChecker__MockForAccounting__factory(deployer).deploy(), + new PostTokenRebaseReceiver__MockForAccounting__factory(deployer).deploy(), + new WithdrawalQueue__MockForAccounting__factory(deployer).deploy(), + new Burner__MockForAccounting__factory(deployer).deploy(), + ]); locator = await deployLidoLocator( { lido, - elRewardsVault, - withdrawalVault, stakingRouter, oracleReportSanityChecker, postTokenRebaseReceiver, @@ -149,7 +131,7 @@ describe("Accounting.sol:report", () => { withdrawalFinalizationBatches: [1n], }), ), - ).not.to.emit(burner, "StETHBurnRequested"); + ).not.to.emit(burner, "Mock__StETHBurnRequested"); }); it("Emits `StETHBurnRequested` if there are shares to burn", async () => { @@ -166,7 +148,7 @@ describe("Accounting.sol:report", () => { }), ), ) - .to.emit(burner, "StETHBurnRequested") + .to.emit(burner, "Mock__StETHBurnRequested") .withArgs(isCover, await accounting.getAddress(), steth, sharesToBurn); }); From edb02158b9ce8ae6f2104a28465257444d7dcc43 Mon Sep 17 00:00:00 2001 From: VP Date: Thu, 16 Jan 2025 16:17:19 +0100 Subject: [PATCH 24/27] test: move mock to newer solidity version to allow custom errors --- ...ReportSanityChecker__MockForAccounting.sol | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 test/0.8.9/contracts/OracleReportSanityChecker__MockForAccounting.sol diff --git a/test/0.8.9/contracts/OracleReportSanityChecker__MockForAccounting.sol b/test/0.8.9/contracts/OracleReportSanityChecker__MockForAccounting.sol new file mode 100644 index 000000000..5575d6ca6 --- /dev/null +++ b/test/0.8.9/contracts/OracleReportSanityChecker__MockForAccounting.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity 0.8.9; + +contract OracleReportSanityChecker__MockForAccounting { + bool private checkAccountingOracleReportReverts; + bool private checkWithdrawalQueueOracleReportReverts; + + uint256 private _withdrawals; + uint256 private _elRewards; + uint256 private _simulatedSharesToBurn; + uint256 private _sharesToBurn; + + error CheckAccountingOracleReportReverts(); + error CheckWithdrawalQueueOracleReportReverts(); + + function checkAccountingOracleReport( + uint256 _timeElapsed, + uint256 _preCLBalance, + uint256 _postCLBalance, + uint256 _withdrawalVaultBalance, + uint256 _elRewardsVaultBalance, + uint256 _sharesRequestedToBurn, + uint256 _preCLValidators, + uint256 _postCLValidators + ) external view { + if (checkAccountingOracleReportReverts) revert CheckAccountingOracleReportReverts(); + } + + function checkWithdrawalQueueOracleReport( + uint256 _lastFinalizableRequestId, + uint256 _reportTimestamp + ) external view { + if (checkWithdrawalQueueOracleReportReverts) revert CheckWithdrawalQueueOracleReportReverts(); + } + + function smoothenTokenRebase( + uint256 _preTotalPooledEther, + uint256 _preTotalShares, + uint256 _preCLBalance, + uint256 _postCLBalance, + uint256 _withdrawalVaultBalance, + uint256 _elRewardsVaultBalance, + uint256 _sharesRequestedToBurn, + uint256 _etherToLockForWithdrawals, + uint256 _newSharesToBurnForWithdrawals + ) + external + view + returns (uint256 withdrawals, uint256 elRewards, uint256 simulatedSharesToBurn, uint256 sharesToBurn) + { + withdrawals = _withdrawals; + elRewards = _elRewards; + simulatedSharesToBurn = _simulatedSharesToBurn; + sharesToBurn = _sharesToBurn; + } + + // mocking + + function mock__checkAccountingOracleReportReverts(bool reverts) external { + checkAccountingOracleReportReverts = reverts; + } + + function mock__checkWithdrawalQueueOracleReportReverts(bool reverts) external { + checkWithdrawalQueueOracleReportReverts = reverts; + } + + function mock__smoothenTokenRebaseReturn( + uint256 withdrawals, + uint256 elRewards, + uint256 simulatedSharesToBurn, + uint256 sharesToBurn + ) external { + _withdrawals = withdrawals; + _elRewards = elRewards; + _simulatedSharesToBurn = simulatedSharesToBurn; + _sharesToBurn = sharesToBurn; + } +} From 88168eff4f1ab21b40f500bc7b9a8715e4bb9a4b Mon Sep 17 00:00:00 2001 From: VP Date: Thu, 16 Jan 2025 16:17:41 +0100 Subject: [PATCH 25/27] test: add custom errors check --- test/0.8.9/accounting.handleOracleReport.test.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/0.8.9/accounting.handleOracleReport.test.ts b/test/0.8.9/accounting.handleOracleReport.test.ts index ef24aeaca..7b773ceb5 100644 --- a/test/0.8.9/accounting.handleOracleReport.test.ts +++ b/test/0.8.9/accounting.handleOracleReport.test.ts @@ -94,7 +94,11 @@ describe("Accounting.sol:report", () => { it("Reverts if the `checkAccountingOracleReport` sanity check fails", async () => { await oracleReportSanityChecker.mock__checkAccountingOracleReportReverts(true); - await expect(accounting.handleOracleReport(report())).to.be.reverted; + await expect(accounting.handleOracleReport(report())).to.be.revertedWithCustomError( + oracleReportSanityChecker, + "CheckAccountingOracleReportReverts", + ); + expect(await lido.reportClValidators()).to.equal(depositedValidators); }); it("Reverts if the `checkWithdrawalQueueOracleReport` sanity check fails", async () => { @@ -105,7 +109,7 @@ describe("Accounting.sol:report", () => { withdrawalFinalizationBatches: [1n], }), ), - ).to.be.reverted; + ).to.be.revertedWithCustomError(oracleReportSanityChecker, "CheckWithdrawalQueueOracleReportReverts"); }); it("Does not revert if the `checkWithdrawalQueueOracleReport` sanity check fails but no withdrawal batches were reported", async () => { @@ -288,11 +292,6 @@ describe("Accounting.sol:report", () => { const expectedModuleRewardInShares = expectedSharesToMint / (totalFee / stakingModule.fee); const expectedTreasuryCutInShares = expectedSharesToMint - expectedModuleRewardInShares; - console.log("expectedModuleRewardInShares", expectedModuleRewardInShares); - console.log("expectedTreasuryCutInShares", expectedTreasuryCutInShares); - console.log("stakingModule.address", stakingModule.address); - console.log("await locator.treasury()", await locator.treasury()); - await expect( accounting.handleOracleReport( report({ From 3af7f9b6ded19cbf0cf2cd90f55d68170bac8b58 Mon Sep 17 00:00:00 2001 From: VP Date: Thu, 16 Jan 2025 16:18:37 +0100 Subject: [PATCH 26/27] test: check actual reporting --- test/0.8.9/accounting.handleOracleReport.test.ts | 12 +++++++++++- test/0.8.9/contracts/Lido__MockForAccounting.sol | 4 +--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/test/0.8.9/accounting.handleOracleReport.test.ts b/test/0.8.9/accounting.handleOracleReport.test.ts index 7b773ceb5..a40c15ec5 100644 --- a/test/0.8.9/accounting.handleOracleReport.test.ts +++ b/test/0.8.9/accounting.handleOracleReport.test.ts @@ -80,7 +80,7 @@ describe("Accounting.sol:report", () => { context("handleOracleReport", () => { it("Update CL validators count if reported more", async () => { - const depositedValidators = 100n; + let depositedValidators = 100n; await lido.setMockedDepositedValidators(depositedValidators); // second report, 101 validators @@ -89,6 +89,16 @@ describe("Accounting.sol:report", () => { clValidators: depositedValidators, }), ); + // first report, 100 validators + await accounting.handleOracleReport( + report({ + clValidators: depositedValidators, + }), + ); + expect(await lido.reportClValidators()).to.equal(depositedValidators); + + depositedValidators = 101n; + await lido.setMockedDepositedValidators(depositedValidators); }); it("Reverts if the `checkAccountingOracleReport` sanity check fails", async () => { diff --git a/test/0.8.9/contracts/Lido__MockForAccounting.sol b/test/0.8.9/contracts/Lido__MockForAccounting.sol index 19135c140..fc0c95582 100644 --- a/test/0.8.9/contracts/Lido__MockForAccounting.sol +++ b/test/0.8.9/contracts/Lido__MockForAccounting.sol @@ -37,17 +37,15 @@ contract Lido__MockForAccounting { returns (uint256 depositedValidators, uint256 beaconValidators, uint256 beaconBalance) { depositedValidators = depositedValidatorsValue; - beaconValidators = 0; + beaconValidators = reportClValidators; beaconBalance = 0; } function getTotalPooledEther() external view returns (uint256) { - // return 1 ether; return 3201000000000000000000; } function getTotalShares() external view returns (uint256) { - // return 1 ether; return 1000000000000000000; } From ade6c0308cc94b3023ee735c2578c112e6b8b003 Mon Sep 17 00:00:00 2001 From: VP Date: Thu, 16 Jan 2025 16:30:58 +0100 Subject: [PATCH 27/27] test: fix the reporting test case --- test/0.8.9/accounting.handleOracleReport.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/0.8.9/accounting.handleOracleReport.test.ts b/test/0.8.9/accounting.handleOracleReport.test.ts index a40c15ec5..c62d65af0 100644 --- a/test/0.8.9/accounting.handleOracleReport.test.ts +++ b/test/0.8.9/accounting.handleOracleReport.test.ts @@ -83,22 +83,24 @@ describe("Accounting.sol:report", () => { let depositedValidators = 100n; await lido.setMockedDepositedValidators(depositedValidators); - // second report, 101 validators + // first report, 100 validators await accounting.handleOracleReport( report({ clValidators: depositedValidators, }), ); - // first report, 100 validators + expect(await lido.reportClValidators()).to.equal(depositedValidators); + + depositedValidators = 101n; + await lido.setMockedDepositedValidators(depositedValidators); + + // second report, 101 validators await accounting.handleOracleReport( report({ clValidators: depositedValidators, }), ); expect(await lido.reportClValidators()).to.equal(depositedValidators); - - depositedValidators = 101n; - await lido.setMockedDepositedValidators(depositedValidators); }); it("Reverts if the `checkAccountingOracleReport` sanity check fails", async () => { @@ -108,7 +110,6 @@ describe("Accounting.sol:report", () => { oracleReportSanityChecker, "CheckAccountingOracleReportReverts", ); - expect(await lido.reportClValidators()).to.equal(depositedValidators); }); it("Reverts if the `checkWithdrawalQueueOracleReport` sanity check fails", async () => {