diff --git a/contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol b/contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol index 63630795a..bcd905a9f 100644 --- a/contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol +++ b/contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol @@ -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) { diff --git a/contracts/0.8.25/vaults/predeposit_guarantee/PredepositGuarantee.sol b/contracts/0.8.25/vaults/predeposit_guarantee/PredepositGuarantee.sol index 8a477ea0b..697aea317 100644 --- a/contracts/0.8.25/vaults/predeposit_guarantee/PredepositGuarantee.sol +++ b/contracts/0.8.25/vaults/predeposit_guarantee/PredepositGuarantee.sol @@ -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 * * * * * // @@ -344,7 +344,7 @@ contract PredepositGuarantee is CLProofVerifier, PausableUntilWithRoles { address _recipient ) external { proveInvalidValidatorWC(_witness, _invalidWithdrawalCredentials); - withdrawDisprovenPredeposit(_witness. , _recipient); + withdrawDisprovenPredeposit(_witness.pubkey, _recipient); } /// Internal functions @@ -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, diff --git a/test/0.8.25/vaults/predeposit-guarantee/cl-proof-verifyer.test.ts b/test/0.8.25/vaults/predeposit-guarantee/cl-proof-verifyer.test.ts index f000a46a9..7f63f7c59 100644 --- a/test/0.8.25/vaults/predeposit-guarantee/cl-proof-verifyer.test.ts +++ b/test/0.8.25/vaults/predeposit-guarantee/cl-proof-verifyer.test.ts @@ -2,9 +2,9 @@ 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"; @@ -12,7 +12,7 @@ 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(), @@ -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), diff --git a/test/0.8.25/vaults/predeposit-guarantee/predeposit-guarantee.test.ts b/test/0.8.25/vaults/predeposit-guarantee/predeposit-guarantee.test.ts index 9371bdbc0..e1540b91f 100644 --- a/test/0.8.25/vaults/predeposit-guarantee/predeposit-guarantee.test.ts +++ b/test/0.8.25/vaults/predeposit-guarantee/predeposit-guarantee.test.ts @@ -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 { + 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")); + }); + }); +}); diff --git a/test/0.8.25/vaults/staking-vault/contracts/VaultFactory__MockForStakingVault.sol b/test/0.8.25/vaults/staking-vault/contracts/VaultFactory__MockForStakingVault.sol index f32a75808..4628d148d 100644 --- a/test/0.8.25/vaults/staking-vault/contracts/VaultFactory__MockForStakingVault.sol +++ b/test/0.8.25/vaults/staking-vault/contracts/VaultFactory__MockForStakingVault.sol @@ -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)); } diff --git a/test/0.8.25/vaults/staking-vault/staking-vault.test.ts b/test/0.8.25/vaults/staking-vault/staking-vault.test.ts index 075fd82a3..fc28ee484 100644 --- a/test/0.8.25/vaults/staking-vault/staking-vault.test.ts +++ b/test/0.8.25/vaults/staking-vault/staking-vault.test.ts @@ -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"); }); }); @@ -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< @@ -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"); diff --git a/test/deploy/locator.ts b/test/deploy/locator.ts index e41e54111..5e3fc78fe 100644 --- a/test/deploy/locator.ts +++ b/test/deploy/locator.ts @@ -30,6 +30,7 @@ async function deployDummyLocator(config?: Partial, de withdrawalVault: certainAddress("dummy-locator:withdrawalVault"), accounting: certainAddress("dummy-locator:withdrawalVault"), wstETH: certainAddress("dummy-locator:wstETH"), + predepositGuarantee: certainAddress("dummy-locator:predepositGuarantee"), ...config, });