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
8 changes: 8 additions & 0 deletions lib/pdg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,11 @@ export enum PDGPolicy {
ALLOW_PROVE,
ALLOW_DEPOSIT_AND_PROVE,
}

export enum ValidatorStage {
NONE,
PREDEPOSITED,
PROVEN,
ACTIVATED,
COMPENSATED,
}
28 changes: 9 additions & 19 deletions lib/protocol/helpers/vaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
de0x,
findEventsWithInterfaces,
generatePredeposit,
generateTopUp,
getCurrentBlockTimestamp,
impersonate,
log,
Expand Down Expand Up @@ -520,12 +519,7 @@ export const generatePredepositData = async (
});
};

export const getProofAndDepositData = async (
ctx: ProtocolContext,
validator: Validator,
withdrawalCredentials: string,
amount: bigint = ether("31"),
) => {
export const mockProof = async (ctx: ProtocolContext, validator: Validator) => {
const { predepositGuarantee } = ctx.contracts;

// Step 3: Prove and deposit the validator
Expand All @@ -538,20 +532,16 @@ export const getProofAndDepositData = async (
);
const proof = await mockCLtree.buildProof(validatorIndex, beaconBlockHeader);

const postdeposit = generateTopUp(validator.container, amount);
const pubkey = hexlify(validator.container.pubkey);

const witnesses = [
{
proof,
pubkey,
validatorIndex,
childBlockTimestamp,
slot: beaconBlockHeader.slot,
proposerIndex: beaconBlockHeader.proposerIndex,
},
];
return { witnesses, postdeposit };
return {
proof,
pubkey,
validatorIndex,
childBlockTimestamp,
slot: beaconBlockHeader.slot,
proposerIndex: beaconBlockHeader.proposerIndex,
};
};

export async function calculateLockedValue(
Expand Down
9 changes: 8 additions & 1 deletion lib/time.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { expect } from "chai";
import { ethers } from "hardhat";

import { time } from "@nomicfoundation/hardhat-network-helpers";
Expand Down Expand Up @@ -48,8 +49,14 @@ export async function getNextBlock() {
}

export async function advanceChainTime(seconds: bigint) {
await ethers.provider.send("evm_increaseTime", [Number(seconds)]);
const currentTimestamp = await getCurrentBlockTimestamp();
await ethers.provider.send("evm_setNextBlockTimestamp", [Number(currentTimestamp + seconds)]);
await ethers.provider.send("evm_mine");

expect(await getCurrentBlockTimestamp()).to.be.equal(
currentTimestamp + seconds,
"Chain time was not advanced correctly",
);
}

export function formatTimeInterval(sec: number | bigint) {
Expand Down
122 changes: 105 additions & 17 deletions test/integration/vaults/disconnected.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
import { Dashboard, DepositContract, StakingVault } from "typechain-types";

import {
addressToWC,
certainAddress,
ether,
generateDepositStruct,
Expand All @@ -17,12 +18,13 @@ import {
MAX_SANE_SETTLED_GROWTH,
toGwei,
toLittleEndian64,
ValidatorStage,
} from "lib";
import {
createVaultWithDashboard,
getProofAndDepositData,
getProtocolContext,
getPubkeys,
mockProof,
ProtocolContext,
reportVaultDataWithProof,
setupLidoForVaults,
Expand Down Expand Up @@ -311,7 +313,7 @@ describe("Integration: Actions with vault disconnected from hub", () => {
);
});

it("Can deposit to beacon chain using predeposit guarantee", async () => {
it("Can deposit to beacon chain using PDG", async () => {
const { predepositGuarantee } = ctx.contracts;
const withdrawalCredentials = await stakingVault.withdrawalCredentials();
const validator = generateValidator(withdrawalCredentials, true);
Expand All @@ -338,26 +340,112 @@ describe("Integration: Actions with vault disconnected from hub", () => {
anyValue,
);

const { witnesses, postdeposit } = await getProofAndDepositData(
ctx,
validator,
withdrawalCredentials,
ether("2016"),
);
const witness = await mockProof(ctx, validator);

await expect(
predepositGuarantee.connect(nodeOperator).proveWCActivateAndTopUpValidators(witnesses, [postdeposit.amount]),
predepositGuarantee.connect(nodeOperator).proveWCActivateAndTopUpValidators([witness], [ether("2016")]),
)
.to.emit(predepositGuarantee, "ValidatorProven")
.withArgs(witnesses[0].pubkey, nodeOperator, await stakingVault.getAddress(), withdrawalCredentials)
.withArgs(witness.pubkey, nodeOperator, await stakingVault.getAddress(), withdrawalCredentials)
.to.emit(depositContract, "DepositEvent")
.withArgs(
postdeposit.pubkey,
withdrawalCredentials,
toLittleEndian64(toGwei(ether("2047"))),
anyValue,
anyValue,
);
.withArgs(witness.pubkey, withdrawalCredentials, toLittleEndian64(toGwei(ether("2047"))), anyValue, anyValue);
});

it("Can deposit to beacon chain using PDG even if messing with staged balance", async () => {
const { predepositGuarantee, vaultHub } = ctx.contracts;
const withdrawalCredentials = await stakingVault.withdrawalCredentials();
const validator = generateValidator(withdrawalCredentials, true);

await predepositGuarantee.connect(nodeOperator).topUpNodeOperatorBalance(nodeOperator, {
value: ether("1"),
});

const predepositData = await generatePredeposit(validator, {
depositDomain: await predepositGuarantee.DEPOSIT_DOMAIN(),
});

await expect(
predepositGuarantee
.connect(nodeOperator)
.predeposit(stakingVault, [predepositData.deposit], [predepositData.depositY]),
).to.emit(depositContract, "DepositEvent");

await stakingVault.connect(await impersonate(await stakingVault.depositor())).unstage(ether("1"));

const witness = await mockProof(ctx, validator);
await expect(predepositGuarantee.connect(nodeOperator).proveWCAndActivate(witness))
.to.emit(predepositGuarantee, "ValidatorProven")
.withArgs(witness.pubkey, nodeOperator, stakingVault, withdrawalCredentials)
.not.to.emit(depositContract, "DepositEvent")
.not.to.emit(stakingVault, "EtherUnstaged");

const validatorStatus = await predepositGuarantee.validatorStatus(validator.container.pubkey);
expect(validatorStatus.stage).to.equal(ValidatorStage.PROVEN);
expect(validatorStatus.stakingVault).to.equal(stakingVault);
expect(validatorStatus.nodeOperator).to.equal(nodeOperator);

expect(await predepositGuarantee.pendingActivations(stakingVault)).to.equal(1);

await expect(stakingVault.connect(owner).transferOwnership(vaultHub))
.to.emit(stakingVault, "OwnershipTransferStarted")
.withArgs(owner, vaultHub);

await expect(vaultHub.connectVault(stakingVault)).to.be.revertedWithCustomError(
vaultHub,
"InsufficientStagedBalance",
);

await stakingVault.connect(await impersonate(await stakingVault.depositor())).stage(ether("1"));

await expect(vaultHub.connectVault(stakingVault))
.to.emit(stakingVault, "OwnershipTransferred")
.withArgs(owner, vaultHub);

expect(await vaultHub.isVaultConnected(stakingVault)).to.equal(true);

await expect(predepositGuarantee.connect(stranger).activateValidator(validator.container.pubkey))
.to.emit(predepositGuarantee, "ValidatorActivated")
.withArgs(validator.container.pubkey, nodeOperator, stakingVault, withdrawalCredentials);
});

it("Can receive compensation for disproven predeposit even if messing with staged balance", async () => {
const { predepositGuarantee } = ctx.contracts;

await predepositGuarantee.connect(nodeOperator).topUpNodeOperatorBalance(nodeOperator, {
value: ether("1"),
});

const invalidWithdrawalCredentials = addressToWC(nodeOperator.address);
const invalidValidator = generateValidator(invalidWithdrawalCredentials);

const invalidValidatorHackedWC = {
...invalidValidator,
container: {
...invalidValidator.container,
withdrawalCredentials: await stakingVault.withdrawalCredentials(),
},
};

const predepositData = await generatePredeposit(invalidValidatorHackedWC, {
depositDomain: await predepositGuarantee.DEPOSIT_DOMAIN(),
});

await expect(
predepositGuarantee
.connect(nodeOperator)
.predeposit(stakingVault, [predepositData.deposit], [predepositData.depositY]),
).to.emit(depositContract, "DepositEvent");

await stakingVault.connect(await impersonate(predepositGuarantee, ether("10"))).unstage(ether("1"));

const witness = await mockProof(ctx, invalidValidator);
expect(await predepositGuarantee.pendingActivations(stakingVault)).to.equal(1);
await expect(
predepositGuarantee.connect(stranger).proveInvalidValidatorWC(witness, invalidWithdrawalCredentials),
)
.to.emit(predepositGuarantee, "ValidatorCompensated")
.withArgs(stakingVault, nodeOperator, invalidValidator.container.pubkey, ether("0"), ether("0"))
.not.to.emit(stakingVault, "EtherUnstaged");
});
});

Expand Down
Loading
Loading