Skip to content

Commit

Permalink
test: pdg happy path
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeday committed Feb 11, 2025
1 parent 60b3157 commit 3223ab5
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@ abstract contract CLProofVerifier {
// See `BEACON_ROOTS_ADDRESS` constant in the EIP-4788.
address public constant BEACON_ROOTS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02;

// Index of parent node for (Pubkey,WC) in validator container
// GIndex of parent node for (Pubkey,WC) in validator container
// unlikely to change, same between mainnet/testnets
GIndex public immutable GI_PUBKEY_WC_PARENT = pack(1 << 2, 2);
// Index of stateRoot in Beacon Block state
// GIndex of stateRoot in Beacon Block state
// unlikely to change, same between mainnet/testnets
GIndex public immutable GI_STATE_VIEW = pack((1 << 3) + 3, 3);
// Index of first validator in CL state
// can change between hardforks and must be updated
GIndex public immutable GI_FIRST_VALIDATOR;

constructor(GIndex _gIFirstValidator) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ contract PredepositGuarantee is CLProofVerifier, PausableUntilWithRoles {
$.nodeOperatorBonds[_nodeOperator].locked += totalDepositAmount;
_stakingVault.depositToBeaconChain(_deposits);

emit ValidatorPreDeposited(_nodeOperator, address(_stakingVault), _deposits.length);
emit ValidatorsPreDeposited(_nodeOperator, address(_stakingVault), _deposits.length);
}

// * * * * * Positive Proof Flow * * * * * //
Expand Down Expand Up @@ -344,7 +344,7 @@ contract PredepositGuarantee is CLProofVerifier, PausableUntilWithRoles {
address _recipient
) external {
proveInvalidValidatorWC(_witness, _invalidWithdrawalCredentials);
withdrawDisprovenPredeposit(_witness. , _recipient);
withdrawDisprovenPredeposit(_witness.pubkey, _recipient);
}

/// Internal functions
Expand Down Expand Up @@ -388,7 +388,11 @@ contract PredepositGuarantee is CLProofVerifier, PausableUntilWithRoles {
event NodeOperatorBondToppedUp(address indexed nodeOperator, uint256 amount);
event NodeOperatorBondWithdrawn(address indexed nodeOperator, uint256 amount, address indexed recipient);
event NodeOperatorVoucherSet(address indexed nodeOperator, address indexed voucher);
event ValidatorPreDeposited(address indexed nodeOperator, address indexed stakingVault, uint256 numberOfDeposits);
event ValidatorsPreDeposited(
address indexed nodeOperator,
address indexed stakingVault,
uint256 numberOfValidators
);
event ValidatorProven(
address indexed nodeOperator,
bytes indexed validatorPubkey,
Expand Down
27 changes: 24 additions & 3 deletions test/0.8.25/vaults/predeposit-guarantee/cl-proof-verifyer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import { expect } from "chai";
import { hexlify, parseUnits, randomBytes } from "ethers";
import { ethers } from "hardhat";

import { CLProofVerifier__Harness, SSZMerkleTree } from "typechain-types";
import { CLProofVerifier__Harness, IStakingVault, SSZHelpers, SSZMerkleTree } from "typechain-types";

import { impersonate } from "lib";
import { ether, impersonate } from "lib";

import { Snapshot } from "test/suite";

const randomBytes32 = (): string => hexlify(randomBytes(32));
const randomInt = (max: number): number => Math.floor(Math.random() * max);
const randomValidatorPubkey = (): string => hexlify(randomBytes(48));

export const generateValidator = (customWC?: string, customPukey?: string) => {
export const generateValidator = (customWC?: string, customPukey?: string): SSZHelpers.ValidatorStruct => {
return {
pubkey: customPukey ?? randomValidatorPubkey(),
withdrawalCredentials: customWC ?? randomBytes32(),
Expand All @@ -25,6 +25,27 @@ export const generateValidator = (customWC?: string, customPukey?: string) => {
};
};

export const generatePredeposit = (validator: SSZHelpers.ValidatorStruct): IStakingVault.DepositStruct => {
return {
pubkey: validator.pubkey,
amount: ether("1"),
signature: randomBytes(96),
depositDataRoot: randomBytes32(),
};
};

export const generatePostDeposit = (
validator: SSZHelpers.ValidatorStruct,
amount = ether("31"),
): IStakingVault.DepositStruct => {
return {
pubkey: validator.pubkey,
amount,
signature: randomBytes(96),
depositDataRoot: randomBytes32(),
};
};

export const generateBeaconHeader = (stateRoot: string) => {
return {
slot: randomInt(1743359),
Expand Down
199 changes: 198 additions & 1 deletion test/0.8.25/vaults/predeposit-guarantee/predeposit-guarantee.test.ts
Original file line number Diff line number Diff line change
@@ -1 +1,198 @@
describe("PredepositGuarantee.sol", () => {});
import { expect } from "chai";
import { ZeroAddress } from "ethers";
import { ethers } from "hardhat";

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

import {
DepositContract__MockForStakingVault,
LidoLocator,
OssifiableProxy,
PredepositGuarantee,
SSZMerkleTree,
StakingVault,
StakingVault__factory,
VaultHub__MockForStakingVault,
} from "typechain-types";

import { ether, findEvents } from "lib";

import { deployLidoLocator } from "test/deploy";
import { Snapshot } from "test/suite";

import {
generateBeaconHeader,
generatePostDeposit,
generatePredeposit,
generateValidator,
prepareLocalMerkleTree,
setBeaconBlockRoot,
} from "./cl-proof-verifyer.test";

describe("PredepositGuarantee.sol", () => {
let deployer: HardhatEthersSigner;
let admin: HardhatEthersSigner;
let vaultOwner: HardhatEthersSigner;
let vaultOperator: HardhatEthersSigner;
let vaultOperatorVoucher: HardhatEthersSigner;
let stranger: HardhatEthersSigner;

let proxy: OssifiableProxy;
let pdgImpl: PredepositGuarantee;
let pdg: PredepositGuarantee;
let locator: LidoLocator;
let vaultHub: VaultHub__MockForStakingVault;
let sszMerkleTree: SSZMerkleTree;
let stakingVault: StakingVault;
let depositContract: DepositContract__MockForStakingVault;

let originalState: string;

async function deployStakingVault(owner: HardhatEthersSigner, operator: HardhatEthersSigner): Promise<StakingVault> {
const stakingVaultImplementation_ = await ethers.deployContract("StakingVault", [
vaultHub,
await depositContract.getAddress(),
]);

// deploying factory/beacon
const vaultFactory_ = await ethers.deployContract("VaultFactory__MockForStakingVault", [
await stakingVaultImplementation_.getAddress(),
]);

// deploying beacon proxy
const vaultCreation = await vaultFactory_.createVault(owner, operator, pdg).then((tx) => tx.wait());
if (!vaultCreation) throw new Error("Vault creation failed");
const events = findEvents(vaultCreation, "VaultCreated");
if (events.length != 1) throw new Error("There should be exactly one VaultCreated event");
const vaultCreatedEvent = events[0];

const stakingVault_ = StakingVault__factory.connect(vaultCreatedEvent.args.vault, owner);
expect(await stakingVault_.owner()).to.equal(owner);

return stakingVault_;
}

before(async () => {
[deployer, admin, vaultOwner, vaultOperator, vaultOperatorVoucher, stranger] = await ethers.getSigners();

// local merkle tree with 1st validator
const localMerkle = await prepareLocalMerkleTree();
sszMerkleTree = localMerkle.sszMerkleTree;

// ether deposit contract
depositContract = await ethers.deployContract("DepositContract__MockForStakingVault");

// PDG
pdgImpl = await ethers.deployContract("PredepositGuarantee", [localMerkle.gIFirstValidator], { from: deployer });
proxy = await ethers.deployContract("OssifiableProxy", [pdgImpl, admin, new Uint8Array()], admin);
pdg = await ethers.getContractAt("PredepositGuarantee", proxy, vaultOperator);

// PDG init
const initTX = await pdg.initialize(admin);
await expect(initTX).to.be.emit(pdg, "Initialized").withArgs(1);

// PDG dependants
locator = await deployLidoLocator({ predepositGuarantee: pdg });
expect(await locator.predepositGuarantee()).to.equal(await pdg.getAddress());
vaultHub = await ethers.deployContract("VaultHub__MockForStakingVault");
stakingVault = await deployStakingVault(vaultOwner, vaultOperator);
});

beforeEach(async () => (originalState = await Snapshot.take()));

afterEach(async () => await Snapshot.restore(originalState));

context("constructor", () => {
it("reverts on impl initialization", async () => {
await expect(pdgImpl.initialize(stranger)).to.be.revertedWithCustomError(pdgImpl, "InvalidInitialization");
});
it("reverts on `_admin` address is zero", async () => {
const pdgProxy = await ethers.deployContract("OssifiableProxy", [pdgImpl, admin, new Uint8Array()], admin);
const pdgLocal = await ethers.getContractAt("PredepositGuarantee", pdgProxy, vaultOperator);
await expect(pdgLocal.initialize(ZeroAddress))
.to.be.revertedWithCustomError(pdgImpl, "ZeroArgument")
.withArgs("_admin");
});
});

context("happy path", () => {
it("can use PDG happy path", async () => {
// NO sets voucher
await pdg.setNodeOperatorVoucher(vaultOperatorVoucher);
expect(await pdg.nodeOperatorVoucher(vaultOperator)).to.equal(vaultOperatorVoucher);

// Voucher funds PDG for operator
await pdg.connect(vaultOperatorVoucher).topUpNodeOperatorBond(vaultOperator, { value: ether("1") });
let [operatorBondTotal, operatorBondLocked] = await pdg.nodeOperatorBond(vaultOperator);
expect(operatorBondTotal).to.equal(ether("1"));
expect(operatorBondLocked).to.equal(0n);

// Staking Vault is funded with enough ether to run validator
await stakingVault.fund({ value: ether("32") });
expect(await stakingVault.valuation()).to.equal(ether("32"));

// NO generates validator for vault
const vaultWC = await stakingVault.withdrawalCredentials();
const validator = generateValidator(vaultWC);

// NO runs predeposit for the vault
const predepositData = generatePredeposit(validator);
const predepositTX = pdg.predeposit(stakingVault, [predepositData]);

await expect(predepositTX)
.to.emit(pdg, "ValidatorsPreDeposited")
.withArgs(vaultOperator, stakingVault, 1)
.to.emit(stakingVault, "DepositedToBeaconChain")
.withArgs(pdg, 1, predepositData.amount)
.to.emit(depositContract, "DepositEvent")
.withArgs(predepositData.pubkey, vaultWC, predepositData.signature, predepositData.depositDataRoot);

[operatorBondTotal, operatorBondLocked] = await pdg.nodeOperatorBond(vaultOperator);
expect(operatorBondTotal).to.equal(ether("1"));
expect(operatorBondLocked).to.equal(ether("1"));

// Validator is added to CL merkle tree
await sszMerkleTree.addValidatorLeaf(validator);
const validatorIndex = (await sszMerkleTree.leafCount()) - 1n;

// Beacon Block is generated with new CL state
const stateRoot = await sszMerkleTree.getMerkleRoot();
const beaconBlockHeader = generateBeaconHeader(stateRoot);
const beaconBlockMerkle = await sszMerkleTree.getBeaconBlockHeaderProof(beaconBlockHeader);

/// Beacon Block root is posted to EL
const childBlockTimestamp = await setBeaconBlockRoot(beaconBlockMerkle.root);

// NO collects validator proof
const validatorMerkle = await sszMerkleTree.getValidatorPubkeyWCParentProof(validator);
const stateProof = await sszMerkleTree.getMerkleProof(validatorIndex);
const concatenatedProof = [...validatorMerkle.proof, ...stateProof, ...beaconBlockMerkle.proof];

// NO posts proof and triggers deposit to total of 32 ether
const postDepositData = generatePostDeposit(validator, ether("31"));
const proveAndDepositTx = pdg.proveAndDeposit(
[{ pubkey: validator.pubkey, validatorIndex, childBlockTimestamp, proof: concatenatedProof }],
[postDepositData],
stakingVault,
);

await expect(proveAndDepositTx)
.to.emit(pdg, "ValidatorProven")
.withArgs(vaultOperator, validator.pubkey, stakingVault, vaultWC)
.to.emit(stakingVault, "DepositedToBeaconChain")
.withArgs(pdg, 1, postDepositData.amount)
.to.emit(depositContract, "DepositEvent")
.withArgs(postDepositData.pubkey, vaultWC, postDepositData.signature, postDepositData.depositDataRoot);

[operatorBondTotal, operatorBondLocked] = await pdg.nodeOperatorBond(vaultOperator);
expect(operatorBondTotal).to.equal(ether("1"));
expect(operatorBondLocked).to.equal(ether("0"));

// NOs voucher withdraws bond from PDG
await pdg.connect(vaultOperatorVoucher).withdrawNodeOperatorBond(vaultOperator, ether("1"), vaultOperator);
[operatorBondTotal, operatorBondLocked] = await pdg.nodeOperatorBond(vaultOperator);
expect(operatorBondTotal).to.equal(ether("0"));
expect(operatorBondLocked).to.equal(ether("0"));
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ contract VaultFactory__MockForStakingVault is UpgradeableBeacon {

constructor(address _stakingVaultImplementation) UpgradeableBeacon(_stakingVaultImplementation, msg.sender) {}

function createVault(address _owner, address _operator) external {
function createVault(address _owner, address _operator, address _depositGuardian) external {
IStakingVault vault = IStakingVault(address(new BeaconProxy(address(this), "")));
vault.initialize(_owner, _operator, _operator, "");
vault.initialize(_owner, _operator, _depositGuardian, "");

emit VaultCreated(address(vault));
}
Expand Down
23 changes: 4 additions & 19 deletions test/0.8.25/vaults/staking-vault/staking-vault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ describe("StakingVault.sol", () => {

it("reverts on initialization", async () => {
await expect(
stakingVaultImplementation.connect(stranger).initialize(vaultOwner, operator, "0x"),
stakingVaultImplementation.connect(stranger).initialize(vaultOwner, operator, operator, "0x"),
).to.be.revertedWithCustomError(stakingVaultImplementation, "InvalidInitialization");
});
});
Expand Down Expand Up @@ -510,23 +510,8 @@ describe("StakingVault.sol", () => {
});
});

context("computeDepositDataRoot", () => {
it("computes the deposit data root", async () => {
// sample tx data: https://etherscan.io/tx/0x02980d44c119b0a8e3ca0d31c288e9f177c76fb4d7ab616563e399dd9c7c6507
const pubkey =
"0x8d6aa059b52f6b11d07d73805d409feba07dffb6442c4ef6645f7caa4038b1047e072cba21eb766579f8286ccac630b0";
const withdrawalCredentials = "0x010000000000000000000000b8b5da17a1b7a8ad1cf45a12e1e61d3577052d35";
const signature =
"0xab95e358d002fd79bc08564a2db057dd5164af173915eba9e3e9da233d404c0eb0058760bc30cb89abbc55cf57f0c5a6018cdb17df73ca39ddc80a323a13c2e7ba942faa86757b26120b3a58dcce5d89e95ea1ee8fa3276ffac0f0ad9313211d";
const amount = ether("32");
const expectedDepositDataRoot = "0xb28f86815813d7da8132a2979836b326094a350e7aa301ba611163d4b7ca77be";

computeDepositDataRoot(withdrawalCredentials, pubkey, signature, amount);

expect(await stakingVault.computeDepositDataRoot(pubkey, withdrawalCredentials, signature, amount)).to.equal(
expectedDepositDataRoot,
);
});
context("setDepositGuardian", () => {
// TODO:
});

async function deployStakingVaultBehindBeaconProxy(): Promise<
Expand All @@ -553,7 +538,7 @@ describe("StakingVault.sol", () => {

// deploying beacon proxy
const vaultCreation = await vaultFactory_
.createVault(await vaultOwner.getAddress(), await operator.getAddress())
.createVault(await vaultOwner.getAddress(), await operator.getAddress(), await operator.getAddress())
.then((tx) => tx.wait());
if (!vaultCreation) throw new Error("Vault creation failed");
const events = findEvents(vaultCreation, "VaultCreated");
Expand Down
1 change: 1 addition & 0 deletions test/deploy/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ async function deployDummyLocator(config?: Partial<LidoLocator.ConfigStruct>, de
withdrawalVault: certainAddress("dummy-locator:withdrawalVault"),
accounting: certainAddress("dummy-locator:withdrawalVault"),
wstETH: certainAddress("dummy-locator:wstETH"),
predepositGuarantee: certainAddress("dummy-locator:predepositGuarantee"),
...config,
});

Expand Down

0 comments on commit 3223ab5

Please sign in to comment.