Skip to content
Merged
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
2 changes: 1 addition & 1 deletion lib/protocol/helpers/vaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ export async function reportVaultDataWithProof(
.updateReportData(reportTimestampArg, reportRefSlotArg, reportTree.root, "");
}

return await lazyOracle.updateVaultData(
return lazyOracle.updateVaultData(
await stakingVault.getAddress(),
vaultReport.totalValue,
vaultReport.cumulativeLidoFees,
Expand Down
58 changes: 58 additions & 0 deletions test/hooks/assertion/equalStETH.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Custom Chai assertion for stETH value comparisons with fixed rounding margin.
* The file will be auto-included in the test suite by the chai setup, no need to import it.
*/
import { Assertion, util } from "chai";

const STETH_ROUNDING_MARGIN = 5n;

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Chai {
interface Assertion {
/**
* Asserts that the actual value is equal to the expected value within the stETH rounding margin.
* This uses a fixed margin of 5 wei to account for stETH share rounding.
*
* @param {bigint} expected - The expected value in wei.
*
* @example
* expect(mintingCapacity).to.equalStETH(ether("32.8"));
*/
equalStETH(expected: bigint): Assertion;
}
}
}

Assertion.addMethod("equalStETH", function (expected: bigint) {
const actual = util.flag(this, "object") as bigint;

// Check if both values are bigints
this.assert(
typeof actual === "bigint",
"expected #{this} to be a bigint",
"expected #{this} not to be a bigint",
expected,
actual,
);

this.assert(
typeof expected === "bigint",
"expected value must be a bigint",
"expected value must be a bigint",
expected,
actual,
);

// Calculate the absolute difference
const diff = actual > expected ? actual - expected : expected - actual;

// Assert the difference is within the margin
this.assert(
diff <= STETH_ROUNDING_MARGIN,
`expected #{act} to equal #{exp} ± ${STETH_ROUNDING_MARGIN} wei (stETH rounding margin), but difference was ${diff} wei`,
`expected #{act} not to equal #{exp} ± ${STETH_ROUNDING_MARGIN} wei (stETH rounding margin)`,
expected,
actual,
);
});
1 change: 1 addition & 0 deletions test/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as Mocha from "mocha";

import { mine } from "@nomicfoundation/hardhat-network-helpers";

import "./assertion/equalStETH";
import "./assertion/revertedWithOZAccessControlError";

// Increase number of stack frames shown in error messages
Expand Down
78 changes: 12 additions & 66 deletions test/integration/vaults/obligations.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -771,8 +771,7 @@ describe("Integration: Vault redemptions and fees obligations", () => {
});
});

// TODO: Need to fix the disconnect flow first
context.skip("Disconnect flow", () => {
context("Disconnect flow", () => {
it("Reverts when trying to disconnect with unsettled obligations", async () => {
await reportVaultDataWithProof(ctx, stakingVault, { cumulativeLidoFees: ether("1.1") });

Expand All @@ -784,20 +783,22 @@ describe("Integration: Vault redemptions and fees obligations", () => {

// will revert because of the unsettled obligations event trying to settle using the connection deposit
await expect(dashboard.voluntaryDisconnect())
.to.be.revertedWithCustomError(vaultHub, "UnsettledObligationsExceedsAllowance")
.withArgs(stakingVault, ether("1"), 0);
.to.be.revertedWithCustomError(vaultHub, "NoUnsettledLidoFeesShouldBeLeft")
.withArgs(stakingVault, ether("1.1"));

expect(obligations.cumulativeLidoFees).to.equal(ether("1.1"));
expect(await ethers.provider.getBalance(stakingVault)).to.equal(ether("1"));
});

it("Allows to disconnect when all obligations are settled", async () => {
await reportVaultDataWithProof(ctx, stakingVault, { cumulativeLidoFees: ether("1.1") });
await dashboard.fund({ value: ether("0.1") });
await dashboard.fund({ value: ether("1.1") });

await expect(vaultHub.settleLidoFees(stakingVault))
.to.emit(vaultHub, "LidoFeesSettled")
.withArgs(stakingVault, ether("1.1"), ether("1.1"), ether("1.1"));

await expect(dashboard.voluntaryDisconnect())
.to.emit(vaultHub, "VaultObligationsSettled")
.withArgs(stakingVault, 0n, ether("1.1"), 0n, 0n, ether("1.1"))
.to.emit(vaultHub, "VaultDisconnectInitiated")
.withArgs(stakingVault);
});
Expand Down Expand Up @@ -826,66 +827,11 @@ describe("Integration: Vault redemptions and fees obligations", () => {
const totalValue = await vaultHub.totalValue(stakingVault);
await dashboard.voluntaryDisconnect();

// take the last fees from the post disconnect report (1.1 ether because fees are cumulative)
await expect(reportVaultDataWithProof(ctx, stakingVault, { totalValue, cumulativeLidoFees: ether("1.1") }))
.to.be.revertedWithCustomError(vaultHub, "UnsettledObligationsExceedsAllowance")
.withArgs(stakingVault, ether("0.1"), 0);
});

it("Should take last fees from the post disconnect report with direct transfer", async () => {
// 1 ether of the connection deposit will be settled to the treasury
await reportVaultDataWithProof(ctx, stakingVault, { cumulativeLidoFees: ether("1") });

const totalValueOnRefSlot = await vaultHub.totalValue(stakingVault);

// successfully disconnect
await dashboard.voluntaryDisconnect();

// adding 1 ether to cover the exit fees
await owner.sendTransaction({ to: stakingVault, value: ether("1") });

// take the last fees from the post disconnect report (1.1 ether because fees are cumulative)
await expect(
await reportVaultDataWithProof(ctx, stakingVault, {
totalValue: totalValueOnRefSlot,
cumulativeLidoFees: ether("1.1"),
}),
)
.to.emit(vaultHub, "VaultObligationsSettled")
.withArgs(stakingVault, 0n, ether("0.1"), 0n, 0n, ether("1.1"))
.to.emit(vaultHub, "VaultDisconnectCompleted")
.withArgs(stakingVault);

// 0.9 ether should be left in the vault
expect(await ethers.provider.getBalance(stakingVault)).to.equal(ether("0.9"));
});

it("Should take last fees from the post disconnect report with fund", async () => {
// 1 ether of the connection deposit will be settled to the treasury
await reportVaultDataWithProof(ctx, stakingVault, { cumulativeLidoFees: ether("1") });

const totalValueOnRefSlot = await vaultHub.totalValue(stakingVault);

// successfully disconnect
await dashboard.voluntaryDisconnect();

// adding 1 ether to cover the exit fees
await dashboard.fund({ value: ether("1") });

// take the last fees from the post disconnect report (1.1 ether because fees are cumulative)
await expect(
await reportVaultDataWithProof(ctx, stakingVault, {
totalValue: totalValueOnRefSlot,
cumulativeLidoFees: ether("1.1"),
}),
)
.to.emit(vaultHub, "VaultObligationsSettled")
.withArgs(stakingVault, 0n, ether("0.1"), 0n, 0n, ether("1.1"))
.to.emit(vaultHub, "VaultDisconnectCompleted")
.withArgs(stakingVault);
// we forgive the last fees
await expect(reportVaultDataWithProof(ctx, stakingVault, { totalValue, cumulativeLidoFees: ether("1.1") })).not.to
.be.reverted;

// 0.9 ether should be left in the vault
expect(await ethers.provider.getBalance(stakingVault)).to.equal(ether("0.9"));
expect(await vaultHub.isPendingDisconnect(stakingVault)).to.be.false;
});
});
});
4 changes: 2 additions & 2 deletions test/integration/vaults/roles.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ describe("Integration: Staking Vaults Dashboard Roles Initial Setup", () => {
}
});

describe.skip("Verify ACL for methods that require only role", () => {
describe("Verify ACL for methods that require only role", () => {
describe("Dashboard methods", () => {
it("setNodeOperatorFeeRecipient", async () => {
await testGrantingRole(
Expand All @@ -98,7 +98,7 @@ describe("Integration: Staking Vaults Dashboard Roles Initial Setup", () => {
}
});

describe.skip("Verify ACL for methods that require only role", () => {
describe("Verify ACL for methods that require only role", () => {
describe("Dashboard methods", () => {
it("setNodeOperatorFeeRecipient", async () => {
await testGrantingRole(
Expand Down
Loading
Loading