-
Notifications
You must be signed in to change notification settings - Fork 195
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: predeposit guardian concept WIP, full of bugs and errors
- Loading branch information
1 parent
295ba6e
commit 938a519
Showing
2 changed files
with
158 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
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 failure Code scanning / Slither Reentrancy vulnerabilities High
Reentrancy in PredepositDepositGuardian.withdrawAsVaultOwner(address,bytes[]):
External calls: - msg.sender.call{value: 1000000000000000000}() State variables written after the call(s): - validatorStatuses[validatorPubkeyHash] = ValidatorStatus.WITHDRAWN PredepositDepositGuardian.validatorStatuses can be used in cross function reentrancies: - PredepositDepositGuardian.deposit(address,IStakingVault.Deposit[]) - PredepositDepositGuardian.predeposit(address,IStakingVault.Deposit[]) - PredepositDepositGuardian.proveWithdrawalCredentials(bytes32[],bytes,bytes32) - PredepositDepositGuardian.validatorStatuses - PredepositDepositGuardian.withdrawAsNodeOperator(bytes[]) - PredepositDepositGuardian.withdrawAsVaultOwner(address,bytes[]) 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 failure Code scanning / Slither Reentrancy vulnerabilities High
Reentrancy in PredepositDepositGuardian.withdrawAsNodeOperator(bytes[]):
External calls: - msg.sender.call{value: 1000000000000000000}() State variables written after the call(s): - validatorStatuses[validatorPubkeyHash] = ValidatorStatus.WITHDRAWN PredepositDepositGuardian.validatorStatuses can be used in cross function reentrancies: - PredepositDepositGuardian.deposit(address,IStakingVault.Deposit[]) - PredepositDepositGuardian.predeposit(address,IStakingVault.Deposit[]) - PredepositDepositGuardian.proveWithdrawalCredentials(bytes32[],bytes,bytes32) - PredepositDepositGuardian.validatorStatuses - PredepositDepositGuardian.withdrawAsNodeOperator(bytes[]) - PredepositDepositGuardian.withdrawAsVaultOwner(address,bytes[]) 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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters