Skip to content

Commit

Permalink
feat: predeposit guardian concept WIP, full of bugs and errors
Browse files Browse the repository at this point in the history
  • Loading branch information
failingtwice committed Jan 28, 2025
1 parent 295ba6e commit 938a519
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 24 deletions.
127 changes: 127 additions & 0 deletions contracts/0.8.25/vaults/PredepositDepositGuardian.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
pragma solidity 0.8.25;

import {StakingVault} from "./StakingVault.sol";

contract PredepositDepositGuardian {
enum ValidatorStatus {
NO_RECORD,
AWAITING_PROOF,
RESOLVED,
WITHDRAWN
}

mapping(address nodeOperator => bytes32 validatorPubkeyHash) public nodeOperatorToValidators;
mapping(bytes32 validatorPubkeyHash => ValidatorStatus validatorStatus) public validatorStatuses;
mapping(bytes32 validatorPubkeyHash => bytes32 withdrawalCredentials) public wcRecords;

function predeposit(address stakingVault, StakingVault.Deposit[] calldata deposits) external payable {
if (msg.value % 1 ether != 0) revert PredepositMustBeMultipleOfOneEther();
if (msg.value / 1 ether != deposits.length) revert PredepositMustBeOneEtherPerDeposit();
if (msg.sender != StakingVault(payable(stakingVault)).nodeOperator()) revert MustBeNodeOperatorOfStakingVault();

for (uint256 i = 0; i < deposits.length; i++) {
StakingVault.Deposit calldata deposit = deposits[i];

if (validatorStatuses[keccak256(deposit.pubkey)] != ValidatorStatus.AWAITING_PROOF) {
revert MustBeNewValidatorPubkey();
}

nodeOperatorToValidators[msg.sender] = keccak256(deposit.pubkey);
validatorStatuses[keccak256(deposit.pubkey)] = ValidatorStatus.AWAITING_PROOF;

if (deposit.amount != 1 ether) revert PredepositMustBeOneEtherPerDeposit();
}

// we don't need to pass deposit root or signature because the msg.sender is deposit guardian itself
StakingVault(payable(stakingVault)).depositToBeaconChain(deposits, bytes32(0), bytes(""));
}

function proveWithdrawalCredentials(
bytes32[] calldata proof,

Check failure on line 44 in contracts/0.8.25/vaults/PredepositDepositGuardian.sol

View workflow job for this annotation

GitHub Actions / Solhint

Variable "proof" is unused
bytes calldata validatorPubkey,
bytes32 withdrawalCredentials
) external {
// TODO: proof logic

bytes32 validatorPubkeyHash = keccak256(validatorPubkey);
wcRecords[validatorPubkeyHash] = withdrawalCredentials;
validatorStatuses[validatorPubkeyHash] = ValidatorStatus.RESOLVED;
}

function deposit(address _stakingVault, StakingVault.Deposit[] calldata deposits) external payable {
if (msg.sender != StakingVault(payable(_stakingVault)).nodeOperator())
revert MustBeNodeOperatorOfStakingVault();

for (uint256 i = 0; i < deposits.length; i++) {
StakingVault.Deposit calldata deposit = deposits[i];

if (validatorStatuses[keccak256(deposit.pubkey)] != ValidatorStatus.RESOLVED) {
revert MustBeResolvedValidatorPubkey();
}
}

// we don't need to pass deposit root or signature because the msg.sender is deposit guardian itself
StakingVault(payable(_stakingVault)).depositToBeaconChain(deposits, bytes32(0), bytes(""));
}

function withdrawAsVaultOwner(address stakingVault, bytes[] calldata validatorPubkeys) external {
if (msg.sender != StakingVault(payable(stakingVault)).owner()) revert MustBeVaultOwner();

for (uint256 i = 0; i < validatorPubkeys.length; i++) {
bytes32 validatorPubkeyHash = keccak256(validatorPubkeys[i]);

if (validatorStatuses[validatorPubkeyHash] != ValidatorStatus.RESOLVED) {
revert MustBeResolvedValidatorPubkey();
}

if (validatorStatuses[validatorPubkeyHash] == ValidatorStatus.WITHDRAWN) {
revert ValidatorAlreadyWithdrawn();
}

if (wcRecords[validatorPubkeyHash] == StakingVault(payable(stakingVault)).withdrawalCredentials()) {
revert ValidatorWithdrawalCredentialsMatchVaultWithdrawalCredentials();
}

msg.sender.call{value: 1 ether}("");

validatorStatuses[validatorPubkeyHash] = ValidatorStatus.WITHDRAWN;
}
}

Check warning

Code scanning / Slither

Unchecked low-level calls Medium


function withdrawAsNodeOperator(bytes[] calldata validatorPubkeys) external {
for (uint256 i = 0; i < validatorPubkeys.length; i++) {
bytes32 validatorPubkeyHash = keccak256(validatorPubkeys[i]);

if (validatorStatuses[validatorPubkeyHash] != ValidatorStatus.RESOLVED) {
revert MustBeResolvedValidatorPubkey();
}

if (validatorStatuses[validatorPubkeyHash] == ValidatorStatus.WITHDRAWN) {
revert ValidatorAlreadyWithdrawn();
}

if (nodeOperatorToValidators[msg.sender] != validatorPubkeyHash) {
revert ValidatorMustBelongToSender();
}

msg.sender.call{value: 1 ether}("");

validatorStatuses[validatorPubkeyHash] = ValidatorStatus.WITHDRAWN;
}
}

Check warning

Code scanning / Slither

Unchecked low-level calls Medium


error PredepositMustBeMultipleOfOneEther();
error PredepositMustBeOneEtherPerDeposit();
error MustBeNodeOperatorOfStakingVault();
error MustBeNewValidatorPubkey();
error WithdrawalFailed();
error MustBeResolvedValidatorPubkey();
error ValidatorMustBelongToSender();
error MustBeVaultOwner();
error ValidatorWithdrawalCredentialsMatchVaultWithdrawalCredentials();
error ValidatorAlreadyWithdrawn();
}
55 changes: 31 additions & 24 deletions contracts/0.8.25/vaults/StakingVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -324,20 +324,42 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {
bytes calldata _signature
) external {
if (_deposits.length == 0) revert ZeroArgument("_deposits");

bytes32 currentGlobalDepositRoot = BEACON_CHAIN_DEPOSIT_CONTRACT.get_deposit_root();
if (_expectedGlobalDepositRoot != currentGlobalDepositRoot)
revert GlobalDepositRootMismatch(_expectedGlobalDepositRoot, currentGlobalDepositRoot);
if (!isBalanced()) revert Unbalanced();

ERC7201Storage storage $ = _getStorage();
if (msg.sender != $.nodeOperator) revert NotAuthorized("depositToBeaconChain", msg.sender);
if ($.beaconChainDepositsPaused) revert BeaconChainDepositsArePaused();
if (!isBalanced()) revert Unbalanced();

uint256 totalAmount = 0;
uint256 numberOfDeposits = _deposits.length;
// XOR is a commutative operation, so the aggregate root will be the same regardless of the order of deposits
bytes32 depositDataBatchXorRoot;

if (msg.sender != $.depositGuardian) {
bytes32 currentGlobalDepositRoot = BEACON_CHAIN_DEPOSIT_CONTRACT.get_deposit_root();
if (_expectedGlobalDepositRoot != currentGlobalDepositRoot)
revert GlobalDepositRootMismatch(_expectedGlobalDepositRoot, currentGlobalDepositRoot);

bytes32 depositDataBatchXorRoot;

for (uint256 i = 0; i < numberOfDeposits; i++) {
Deposit calldata deposit = _deposits[i];

depositDataBatchXorRoot ^= keccak256(abi.encodePacked(deposit.depositDataRoot));
}

if (
!SignatureChecker.isValidSignatureNow(
$.depositGuardian,
keccak256(
abi.encodePacked(
DEPOSIT_GUARDIAN_MESSAGE_PREFIX,
_expectedGlobalDepositRoot,
depositDataBatchXorRoot
)
),
_signature
)
) revert DepositGuardianSignatureInvalid();
}

uint256 totalAmount = 0;

for (uint256 i = 0; i < numberOfDeposits; i++) {
Deposit calldata deposit = _deposits[i];
Expand All @@ -350,23 +372,8 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {
);

totalAmount += deposit.amount;
depositDataBatchXorRoot ^= keccak256(abi.encodePacked(deposit.depositDataRoot));
}

if (
!SignatureChecker.isValidSignatureNow(
$.depositGuardian,
keccak256(
abi.encodePacked(
DEPOSIT_GUARDIAN_MESSAGE_PREFIX,
_expectedGlobalDepositRoot,
depositDataBatchXorRoot
)
),
_signature
)
) revert DepositGuardianSignatureInvalid();

emit DepositedToBeaconChain(msg.sender, numberOfDeposits, totalAmount);
}

Expand Down

0 comments on commit 938a519

Please sign in to comment.