Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[VAULTS] PredepositGuarantee #932

Open
wants to merge 66 commits into
base: feat/vaults
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
c19a543
feat: verify deposit guardian upon deposit
failingtwice Jan 27, 2025
b710b58
feat: add comment
failingtwice Jan 27, 2025
86f80bf
fix: use bytes for sig and XOR for aggregate root
failingtwice Jan 28, 2025
a0de885
fix: remove unused imports
failingtwice Jan 28, 2025
295ba6e
fix: elaborate comment
failingtwice Jan 28, 2025
938a519
feat: predeposit guardian concept WIP, full of bugs and errors
failingtwice Jan 28, 2025
34d203d
refactor: cleanup logic
failingtwice Jan 29, 2025
21f475e
feat: remove unnecessary mapping
failingtwice Jan 29, 2025
331a6e6
feat: remove node operator check
failingtwice Jan 30, 2025
2c84783
fix: rename
failingtwice Jan 30, 2025
ccde1b2
fix: comment on naming
failingtwice Jan 30, 2025
d0954f7
fix: comment on naming
failingtwice Jan 30, 2025
c5312c0
feat: add accounting&delegation to predeposit guardian
Jeday Jan 30, 2025
9d2b349
feat: proof validation
Jeday Jan 31, 2025
e4d3ebc
fix: clean up errors
Jeday Jan 31, 2025
74917ee
fix: mitigate mal staking vault
Jeday Jan 31, 2025
a17c375
fix: prove using validator container
Jeday Feb 3, 2025
8674bba
fix: withdraw
Jeday Feb 3, 2025
87c7e01
fix: use uint256 for collateral
Jeday Feb 3, 2025
ad9e476
fix: merge prove flows
Jeday Feb 3, 2025
5212738
fix: move predeposit to vaults
Jeday Feb 3, 2025
0d5f663
test: pdg cl verifier test
Jeday Feb 4, 2025
ae6d487
fix: use correct merkle implementation
Jeday Feb 4, 2025
2fc25b6
feat: add predeposit guarantee to vault factory
Jeday Feb 5, 2025
563d852
docs: update libs with refs
Jeday Feb 5, 2025
d2f5f24
fix: remove GIndex usage
Jeday Feb 5, 2025
4ef7550
fix: rework no voucher
Jeday Feb 6, 2025
6d6242b
fix: rewrite hashTreeRoot for calldata
Jeday Feb 6, 2025
ecc06c6
test: local testing merkle tree
Jeday Feb 6, 2025
ef47aed
feat: enhance PredepositGuarantee with events and improved error hand…
DiRaiks Feb 6, 2025
3bb45e8
feat: rework for wc proof
Jeday Feb 7, 2025
500e8be
feat: clean up SSZ lib
Jeday Feb 7, 2025
56b7752
docs: clProofVerifier
Jeday Feb 7, 2025
364e1e7
test: update to childBlockTimestamp
Jeday Feb 8, 2025
04a70a9
Merge branch 'predeposit-guardian' of github.com:lidofinance/core int…
DiRaiks Feb 10, 2025
3576bb9
refactor: simplify node operator bond top-up logic
DiRaiks Feb 10, 2025
16014b0
test: export PG test helpers
Jeday Feb 10, 2025
7b61c6a
Merge pull request #936 from lidofinance/predeposit-guardian-fixes
Jeday Feb 10, 2025
6e3b647
feat: integrate predeposit guarantee into locator
Jeday Feb 10, 2025
8c45174
fix: add whenResumed
Jeday Feb 10, 2025
60b3157
fix: move up from internal function
Jeday Feb 11, 2025
3223ab5
test: pdg happy path
Jeday Feb 11, 2025
917c685
fix: enforce balance multiple of ether
Jeday Feb 11, 2025
966ab9c
Merge branch 'feat/vaults' of github.com:lidofinance/core into predep…
Jeday Feb 11, 2025
e79014d
feat: add PDG integration to dashboard
Jeday Feb 12, 2025
d077613
feat: allow GIndex change via slot proof
Jeday Feb 13, 2025
0bbf207
fix: rename deposit guardian to depositor in StakingVault
DiRaiks Feb 18, 2025
d42777e
fix: update references from depositGuardian to depositor across contr…
DiRaiks Feb 18, 2025
118163c
Merge branch 'feat/vaults' of github.com:lidofinance/core into predep…
DiRaiks Mar 2, 2025
c1f271d
test: update createVault calls to include deposit contract address
DiRaiks Mar 2, 2025
6f37fc5
test: update asset recovery role test to use specific role
DiRaiks Mar 2, 2025
65bbd88
test: integrate predeposit guarantee into vault tests
DiRaiks Mar 2, 2025
ba74cb4
test: add predepositGuarantee to LidoLocator and test configurations
DiRaiks Mar 2, 2025
da9d8aa
fix: update VaultHub deployment to use LidoLocator address
DiRaiks Mar 2, 2025
01450ad
refactor: simplify LidoLocator deployment in non-Aragon contracts script
DiRaiks Mar 2, 2025
7a8ef28
feat: update VaultHub constructor to accept StETH address directly
DiRaiks Mar 2, 2025
e423920
feat: add PredepositGuarantee deployment to non-Aragon contracts script
DiRaiks Mar 2, 2025
8d15647
refactor: update VaultHub constructor to use LidoLocator for StETH ad…
DiRaiks Mar 2, 2025
0479f8a
refactor: deploy PredepositGuarantee behind ossifiable proxy
DiRaiks Mar 2, 2025
ae75131
feat: add unregistered validator flow
Jeday Mar 3, 2025
715a0fd
feat: remove locator call from vaulthub constructor
Jeday Mar 3, 2025
1418175
chore: revert accounting formating
Jeday Mar 4, 2025
322e3a9
chore: add line
Jeday Mar 4, 2025
8749e6a
fix: naming&comments
Jeday Mar 4, 2025
93bffbc
fix: remove depositor setter
Jeday Mar 4, 2025
b098888
chore: remove guardian naming
Jeday Mar 4, 2025
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
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;
}
}

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;
}
}

error PredepositMustBeMultipleOfOneEther();
error PredepositMustBeOneEtherPerDeposit();
error MustBeNodeOperatorOfStakingVault();
error MustBeNewValidatorPubkey();
error WithdrawalFailed();
error MustBeResolvedValidatorPubkey();
error ValidatorMustBelongToSender();
error MustBeVaultOwner();
error ValidatorWithdrawalCredentialsMatchVaultWithdrawalCredentials();
error ValidatorAlreadyWithdrawn();
}
92 changes: 87 additions & 5 deletions contracts/0.8.25/vaults/StakingVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
pragma solidity 0.8.25;

import {OwnableUpgradeable} from "contracts/openzeppelin/5.2/upgradeable/access/OwnableUpgradeable.sol";
import {SignatureChecker} from "@openzeppelin/contracts-v5.2/utils/cryptography/SignatureChecker.sol";

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

Expand Down Expand Up @@ -67,6 +68,7 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {
uint128 locked;
int128 inOutDelta;
address nodeOperator;
address depositGuardian;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's rename to trustedDepositor, or something

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just depositor is also ok, trusted kind of excessive

bool beaconChainDepositsPaused;
}

Expand All @@ -76,6 +78,8 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {
*/
uint64 private constant _VERSION = 1;

bytes32 public constant DEPOSIT_GUARDIAN_MESSAGE_PREFIX = keccak256("StakingVault.DepositGuardianMessagePrefix");

/**
* @notice Address of `VaultHub`
* Set immutably in the constructor to avoid storage costs
Expand Down Expand Up @@ -122,6 +126,7 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {
function initialize(address _owner, address _nodeOperator, bytes calldata /* _params */) external initializer {
__Ownable_init(_owner);
_getStorage().nodeOperator = _nodeOperator;
_getStorage().depositGuardian = _owner;
}

/**
Expand Down Expand Up @@ -313,24 +318,59 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {
* @param _deposits Array of deposit structs
* @dev Includes a check to ensure StakingVault is balanced before making deposits
*/
function depositToBeaconChain(Deposit[] calldata _deposits) external {
function depositToBeaconChain(
Deposit[] calldata _deposits,
bytes32 _expectedGlobalDepositRoot,
bytes calldata _signature
) external {
if (_deposits.length == 0) revert ZeroArgument("_deposits");
ERC7201Storage storage $ = _getStorage();
if (!isBalanced()) revert Unbalanced();

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

uint256 totalAmount = 0;
uint256 numberOfDeposits = _deposits.length;

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];

BEACON_CHAIN_DEPOSIT_CONTRACT.deposit{value: deposit.amount}(
deposit.pubkey,
bytes.concat(withdrawalCredentials()),
deposit.signature,
deposit.depositDataRoot
);

totalAmount += deposit.amount;
}

Expand Down Expand Up @@ -404,6 +444,26 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {
emit Reported(_valuation, _inOutDelta, _locked);
}

/**
* @notice Sets the deposit guardian
* @param _depositGuardian The address of the deposit guardian
* @dev In case where the deposit guardian is a contract, it must implement EIP-1271.
* The interface check for EIP-1271 is omitted because the only way to check for whether an account is a contract
* is to call `extcodesize` on it. This method is not reliable, as it can be broken, for instance, by CREATE2-deployed contracts.
* So to avoid false positives, the contract shifts this responsibility to the owner.
* If a contract mistakenly assigned as a deposit guardian is not a valid EIP-1271 signer,
* the owner can simply set it to a different address.
*/
function setDepositGuardian(address _depositGuardian) external onlyOwner {
if (_depositGuardian == address(0)) revert ZeroArgument("_depositGuardian");

ERC7201Storage storage $ = _getStorage();
address oldDepositGuardian = $.depositGuardian;
$.depositGuardian = _depositGuardian;

emit DepositGuardianSet(oldDepositGuardian, _depositGuardian);
}

/**
* @notice Computes the deposit data root for a validator deposit
* @param _pubkey Validator public key, 48 bytes
Expand Down Expand Up @@ -554,6 +614,13 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {
*/
event BeaconChainDepositsResumed();

/**
* @notice Emitted when the deposit guardian is set
* @param oldDepositGuardian The address of the old deposit guardian
* @param newDepositGuardian The address of the new deposit guardian
*/
event DepositGuardianSet(address oldDepositGuardian, address newDepositGuardian);

/**
* @notice Thrown when an invalid zero value is passed
* @param name Name of the argument that was zero
Expand Down Expand Up @@ -617,6 +684,21 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {
*/
error UnrecoverableError();

/**
* @notice Thrown when the global deposit root does not match the expected global deposit root
*/
error GlobalDepositRootMismatch(bytes32 expected, bytes32 actual);

/**
* @notice Thrown when the guardian signature is invalid
*/
error DepositGuardianSignatureInvalid();

/**
* @notice Thrown when the deposit guardian is not an EIP-1271 contract
*/
error DepositGuardianContractDoesNotSupportEIP1271();

/**
* @notice Thrown when trying to pause deposits to beacon chain while deposits are already paused
*/
Expand Down
30 changes: 28 additions & 2 deletions contracts/0.8.25/vaults/interfaces/IStakingVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,52 @@ interface IStakingVault {
}

function initialize(address _owner, address _operator, bytes calldata _params) external;
function version() external pure returns(uint64);

function version() external pure returns (uint64);

function getInitializedVersion() external view returns (uint64);

function vaultHub() external view returns (address);

function depositContract() external view returns (address);

function nodeOperator() external view returns (address);

function locked() external view returns (uint256);

function valuation() external view returns (uint256);

function isBalanced() external view returns (bool);

function unlocked() external view returns (uint256);

function inOutDelta() external view returns (int256);

function beaconChainDepositsPaused() external view returns (bool);

function withdrawalCredentials() external view returns (bytes32);

function fund() external payable;

function withdraw(address _recipient, uint256 _ether) external;
function depositToBeaconChain(Deposit[] calldata _deposits) external;

function depositToBeaconChain(
Deposit[] calldata _deposits,
bytes32 _expectedGlobalDepositRoot,
bytes calldata _guardianSignature
) external;

function requestValidatorExit(bytes calldata _pubkeys) external;

function lock(uint256 _locked) external;

function rebalance(uint256 _ether) external;

function pauseBeaconChainDeposits() external;

function resumeBeaconChainDeposits() external;

function latestReport() external view returns (Report memory);

function report(uint256 _valuation, int256 _inOutDelta, uint256 _locked) external;
}
Loading
Loading