Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions test/0.8.9/accounting.handleOracleReport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,161 @@ describe("Accounting.sol:report", () => {
expect(simulated.postTotalShares).to.equal(preTotalShares);
expect(simulated.postTotalPooledEther).to.equal(preTotalPooledEther);
});

context("should match handleOracleReport results", () => {
it("happy path", async () => {
const simulated = await accounting.simulateOracleReport(report());

await accounting.handleOracleReport(report());

const postExternalShares = await lido.getExternalShares();

expect(simulated.withdrawalsVaultTransfer).to.equal(0n);
expect(simulated.elRewardsVaultTransfer).to.equal(0n);
expect(simulated.etherToFinalizeWQ).to.equal(0n);
expect(simulated.sharesToFinalizeWQ).to.equal(0n);
expect(simulated.sharesToBurnForWithdrawals).to.equal(0n);
expect(simulated.totalSharesToBurn).to.equal(0n);
expect(simulated.sharesToMintAsFees).to.equal(0n);

expect(simulated.principalClBalance).to.equal(0n);

expect(simulated.preTotalShares).to.equal(await lido.getTotalShares());
expect(simulated.preTotalPooledEther).to.equal(await lido.getTotalPooledEther());

expect(simulated.postTotalShares).to.equal(simulated.preTotalShares + simulated.sharesToMintAsFees);
expect(simulated.postInternalShares).to.equal(
simulated.preTotalShares - postExternalShares + simulated.sharesToMintAsFees,
);
});

it("with CL validators increase", async () => {
const depositedValidators = 100n;
const clValidators = 50n;

await lido.mock__setDepositedValidators(depositedValidators);

const reportData = report({ clValidators, clBalance: 0n });
const simulated = await accounting.simulateOracleReport(reportData);
const tx = await accounting.handleOracleReport(reportData);
const receipt = await tx.wait();

const collectEvent = receipt?.logs
.map((log) => {
try {
return lido.interface.parseLog({ topics: [...log.topics], data: log.data });
} catch {
return null;
}
})
.find((e) => e?.name === "Mock__CollectRewardsAndProcessWithdrawals");

if (collectEvent) {
expect(simulated.principalClBalance).to.equal(collectEvent.args._principalCLBalance);
expect(simulated.withdrawalsVaultTransfer).to.equal(collectEvent.args._withdrawalsToWithdraw);
expect(simulated.elRewardsVaultTransfer).to.equal(collectEvent.args._elRewardsToWithdraw);
}

const postExternalShares = await lido.getExternalShares();

expect(simulated.withdrawalsVaultTransfer).to.equal(0n);
expect(simulated.elRewardsVaultTransfer).to.equal(0n);
expect(simulated.etherToFinalizeWQ).to.equal(0n);
expect(simulated.sharesToFinalizeWQ).to.equal(0n);
expect(simulated.sharesToBurnForWithdrawals).to.equal(0n);
expect(simulated.totalSharesToBurn).to.equal(0n);
expect(simulated.sharesToMintAsFees).to.equal(0n);

const expectedPrincipalCl = 0n + clValidators * 32n * 10n ** 18n;
expect(simulated.principalClBalance).to.equal(expectedPrincipalCl);

expect(simulated.preTotalShares).to.equal(await lido.getTotalShares());
expect(simulated.preTotalPooledEther).to.equal(await lido.getTotalPooledEther());

expect(simulated.postTotalShares).to.equal(simulated.preTotalShares + simulated.sharesToMintAsFees);
expect(simulated.postInternalShares).to.equal(
simulated.preTotalShares - postExternalShares + simulated.sharesToMintAsFees,
);
});

it("with withdrawal finalization", async () => {
const depositedValidators = 100n;
const clValidators = 100n;
const clBalance = ether("3200");
const withdrawalFinalizationBatches = [1n, 2n, 3n];
const simulatedShareRate = 10n ** 27n;

await lido.mock__setDepositedValidators(depositedValidators);
await withdrawalQueue.mock__prefinalizeReturn(ether("10"), ether("0.01"));

const reportData = report({
clValidators,
clBalance,
withdrawalFinalizationBatches,
simulatedShareRate,
});

const simulated = await accounting.simulateOracleReport(reportData);

await accounting.handleOracleReport(reportData);

const postExternalShares = await lido.getExternalShares();

expect(simulated.withdrawalsVaultTransfer).to.equal(0n);
expect(simulated.elRewardsVaultTransfer).to.equal(0n);
expect(simulated.etherToFinalizeWQ).to.equal(ether("10"));
expect(simulated.sharesToFinalizeWQ).to.equal(ether("0.01"));
expect(simulated.sharesToBurnForWithdrawals).to.equal(0n);
expect(simulated.totalSharesToBurn).to.equal(0n);
expect(simulated.sharesToMintAsFees).to.equal(0n);

const expectedPrincipalCl = 0n + clValidators * 32n * 10n ** 18n;
expect(simulated.principalClBalance).to.equal(expectedPrincipalCl);

expect(simulated.preTotalShares).to.equal(await lido.getTotalShares());
expect(simulated.preTotalPooledEther).to.equal(await lido.getTotalPooledEther());

expect(simulated.postTotalShares).to.equal(simulated.preTotalShares + simulated.sharesToMintAsFees);
expect(simulated.postInternalShares).to.equal(
simulated.preTotalShares - postExternalShares + simulated.sharesToMintAsFees,
);
});

it("with bad debt and external shares", async () => {
const totalShares = ether("1000");
const externalShares = ether("100");
const badDebtToInternalize = ether("50");

await lido.mock__setTotalShares(totalShares);
await lido.mock__setExternalShares(externalShares);
await vaultHub.setBadDebtToInternalize(badDebtToInternalize);

const simulated = await accounting.simulateOracleReport(report());

await accounting.handleOracleReport(report());

const preExternalShares = externalShares;
const preInternalShares = totalShares - preExternalShares;

expect(simulated.withdrawalsVaultTransfer).to.equal(0n);
expect(simulated.elRewardsVaultTransfer).to.equal(0n);
expect(simulated.etherToFinalizeWQ).to.equal(0n);
expect(simulated.sharesToFinalizeWQ).to.equal(0n);
expect(simulated.sharesToBurnForWithdrawals).to.equal(0n);
expect(simulated.totalSharesToBurn).to.equal(0n);
expect(simulated.sharesToMintAsFees).to.equal(0n);

expect(simulated.principalClBalance).to.equal(0n);

expect(simulated.preTotalShares).to.equal(await lido.getTotalShares());
expect(simulated.preTotalPooledEther).to.equal(await lido.getTotalPooledEther());

expect(simulated.postTotalShares).to.equal(simulated.preTotalShares + simulated.sharesToMintAsFees);
expect(simulated.postInternalShares).to.equal(
preInternalShares + simulated.sharesToMintAsFees + badDebtToInternalize,
);
});
});
});

context("handleOracleReport", () => {
Expand Down
29 changes: 19 additions & 10 deletions test/0.8.9/contracts/Lido__MockForAccounting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ contract Lido__MockForAccounting {
uint256 public depositedValidatorsValue;
uint256 public reportClValidators;
uint256 public reportClBalance;
uint256 private externalSharesValue = 0;
uint256 private totalSharesValue = 1000000000000000000;

// Emitted when validators number delivered by the oracle
event CLValidatorsUpdated(uint256 indexed reportTimestamp, uint256 preCLValidators, uint256 postCLValidators);
event Mock__CollectRewardsAndProcessWithdrawals(
uint256 _reportTimestamp,
Expand All @@ -20,17 +21,20 @@ 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 mock__setDepositedValidators(uint256 _amount) external {
depositedValidatorsValue = _amount;
}

function mock__setExternalShares(uint256 _amount) external {
externalSharesValue = _amount;
}

function mock__setTotalShares(uint256 _amount) external {
totalSharesValue = _amount;
}

function getBeaconStat()
external
view
Expand All @@ -45,12 +49,12 @@ contract Lido__MockForAccounting {
return 3201000000000000000000;
}

function getTotalShares() external pure returns (uint256) {
return 1000000000000000000;
function getTotalShares() external view returns (uint256) {
return totalSharesValue;
}

function getExternalShares() external pure returns (uint256) {
return 0;
function getExternalShares() external view returns (uint256) {
return externalSharesValue;
}

function getExternalEther() external pure returns (uint256) {
Expand Down Expand Up @@ -114,4 +118,9 @@ contract Lido__MockForAccounting {
function mintShares(address _recipient, uint256 _sharesAmount) external {
emit TransferShares(address(0), _recipient, _sharesAmount);
}

function internalizeExternalBadDebt(uint256 _badDebt) external {
externalSharesValue -= _badDebt;
totalSharesValue = totalSharesValue;
}
}
138 changes: 138 additions & 0 deletions test/integration/vaults/accounting.integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { expect } from "chai";
import { ethers } from "hardhat";

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

import { Dashboard, StakingVault, VaultHub } from "typechain-types";

import { findEventsWithInterfaces } from "lib";
import {
createVaultWithDashboard,
getProtocolContext,
ProtocolContext,
report,
reportVaultDataWithProof,
setupLidoForVaults,
} from "lib/protocol";
import { finalizeWQViaElVault } from "lib/protocol";
import { ether } from "lib/units";

import { Snapshot } from "test/suite";

describe("Integration: accounting", () => {
let ctx: ProtocolContext;
let snapshot: string;
let originalSnapshot: string;

let owner: HardhatEthersSigner;
let stranger: HardhatEthersSigner;
let agentSigner: HardhatEthersSigner;
let nodeOperator: HardhatEthersSigner;
let stakingVault: StakingVault;
let dashboard: Dashboard;
let vaultHub: VaultHub;

before(async () => {
ctx = await getProtocolContext();
const { stakingVaultFactory } = ctx.contracts;
vaultHub = ctx.contracts.vaultHub;
originalSnapshot = await Snapshot.take();

[, owner, nodeOperator, stranger] = await ethers.getSigners();
await setupLidoForVaults(ctx);

agentSigner = await ctx.getSigner("agent");

({ stakingVault, dashboard } = await createVaultWithDashboard(
ctx,
stakingVaultFactory,
owner,
nodeOperator,
nodeOperator,
));

dashboard = dashboard.connect(owner);

await dashboard.fund({ value: ether("100") });
await dashboard.mintShares(owner, await dashboard.remainingMintingCapacityShares(0n));

await ctx.contracts.lido.connect(stranger).submit(owner.address, { value: ether("100") });

await finalizeWQViaElVault(ctx);
await reportVaultDataWithProof(ctx, stakingVault);

await setBalance(ctx.contracts.elRewardsVault.address, 0);
await setBalance(ctx.contracts.withdrawalVault.address, 0);
});

beforeEach(async () => (snapshot = await Snapshot.take()));
afterEach(async () => await Snapshot.restore(snapshot));
after(async () => await Snapshot.restore(originalSnapshot));

context("Withdrawals: finalization with external shares", () => {
it("Should finalize requests from withdrawal vault using force rebalance", async () => {
const withdrawalRequestAmount = ether("10");
const { withdrawalQueue, lido } = ctx.contracts;
const stakingVaultAddress = await stakingVault.getAddress();

await lido.connect(owner).approve(withdrawalQueue.address, withdrawalRequestAmount);
await lido.connect(stranger).approve(withdrawalQueue.address, withdrawalRequestAmount);

const firstRequestTx = await withdrawalQueue
.connect(owner)
.requestWithdrawals([withdrawalRequestAmount], owner.address);
const secondRequestTx = await withdrawalQueue
.connect(stranger)
.requestWithdrawals([withdrawalRequestAmount], stranger.address);

const firstRequestReceipt = await firstRequestTx.wait();
const secondRequestReceipt = await secondRequestTx.wait();

const [firstRequestEvent] = findEventsWithInterfaces(firstRequestReceipt!, "WithdrawalRequested", [
withdrawalQueue.interface,
]);
const [secondRequestEvent] = findEventsWithInterfaces(secondRequestReceipt!, "WithdrawalRequested", [
withdrawalQueue.interface,
]);

const firstRequest = firstRequestEvent!.args.requestId;
const secondRequest = secondRequestEvent!.args.requestId;

let [firstStatus, secondStatus] = await withdrawalQueue.getWithdrawalStatus([firstRequest, secondRequest]);

expect(firstStatus.isFinalized).to.be.false;
expect(secondStatus.isFinalized).to.be.false;

// Set balance to cover only first request
await setBalance(ctx.contracts.lido.address, withdrawalRequestAmount);

await expect(report(ctx, { clDiff: 0n })).to.be.reverted;

const balanceBefore = await ethers.provider.getBalance(stakingVaultAddress);

await vaultHub.connect(agentSigner).setLiabilitySharesTarget(stakingVaultAddress, 0n);
const forceRebalanceTx = await vaultHub.connect(agentSigner).forceRebalance(stakingVaultAddress);

const forceRebalanceReceipt = await forceRebalanceTx.wait();
const [rebalanceEvent] = findEventsWithInterfaces(forceRebalanceReceipt!, "VaultRebalanced", [
vaultHub.interface,
]);
const rebalancedValue = rebalanceEvent!.args.etherWithdrawn;

await report(ctx, { clDiff: 0n });

const balanceAfter = await ethers.provider.getBalance(stakingVault);

[firstStatus, secondStatus] = await withdrawalQueue.getWithdrawalStatus([firstRequest, secondRequest]);

expect(firstStatus.isFinalized).to.be.true;
expect(secondStatus.isFinalized).to.be.true;

const balanceWithdrawn = balanceBefore - balanceAfter;

expect(balanceWithdrawn).to.equal(rebalancedValue);
expect(rebalancedValue).to.be.gte(withdrawalRequestAmount);
});
});
});
Loading
Loading