Skip to content

Commit

Permalink
feat: add PDG integration to dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeday committed Feb 12, 2025
1 parent 966ab9c commit e79014d
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 120 deletions.
26 changes: 24 additions & 2 deletions contracts/0.8.25/vaults/Dashboard.sol
Original file line number Diff line number Diff line change
Expand Up @@ -411,12 +411,30 @@ contract Dashboard is Permissions {
_rebalanceVault(_ether);
}

/**
* @notice withdraws ether of disputed validator from PDG
* @param _pubkey of validator that was proven invalid in PDG
* @param _recipient address to receive the `PREDEPOSIT_AMOUNT`
*/
function withdrawDisputedValidator(bytes calldata _pubkey, address _recipient) external {
_withdrawDisputedValidatorFromPDG(_pubkey, _recipient);
}

/**
* @notice funds vault with ether of disproven validator from PDG
* @param _pubkey of validator that was proven invalid in PDG
*/
function refundDisputedValidatorToVault(bytes calldata _pubkey) external {
uint128 _amount = _withdrawDisputedValidatorFromPDG(_pubkey, address(this));
_fund(_amount);
}

/**
* @notice recovers ERC20 tokens or ether from the dashboard contract to sender
* @param _token Address of the token to recover or 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee for ether
* @param _recipient Address of the recovery recipient
*/
function recoverERC20(address _token, address _recipient, uint256 _amount) external onlyRole(DEFAULT_ADMIN_ROLE) {
function recoverERC20(address _token, address _recipient, uint256 _amount) external onlyRole(ASSET_RECOVERY_ROLE) {
if (_token == address(0)) revert ZeroArgument("_token");
if (_recipient == address(0)) revert ZeroArgument("_recipient");
if (_amount == 0) revert ZeroArgument("_amount");
Expand All @@ -439,7 +457,11 @@ contract Dashboard is Permissions {
* @param _tokenId token id to recover
* @param _recipient Address of the recovery recipient
*/
function recoverERC721(address _token, uint256 _tokenId, address _recipient) external onlyRole(DEFAULT_ADMIN_ROLE) {
function recoverERC721(
address _token,
uint256 _tokenId,
address _recipient
) external onlyRole(ASSET_RECOVERY_ROLE) {
if (_token == address(0)) revert ZeroArgument("_token");
if (_recipient == address(0)) revert ZeroArgument("_recipient");

Expand Down
18 changes: 18 additions & 0 deletions contracts/0.8.25/vaults/Permissions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {AccessControlVoteable} from "contracts/0.8.25/utils/AccessControlVoteabl
import {OwnableUpgradeable} from "contracts/openzeppelin/5.2/upgradeable/access/OwnableUpgradeable.sol";

import {IStakingVault} from "./interfaces/IStakingVault.sol";
import {IPredepositGuarantee} from "./interfaces/IPredepositGuarantee.sol";
import {VaultHub} from "./VaultHub.sol";

/**
Expand Down Expand Up @@ -64,6 +65,16 @@ abstract contract Permissions is AccessControlVoteable {
*/
bytes32 public constant VOLUNTARY_DISCONNECT_ROLE = keccak256("StakingVault.Permissions.VoluntaryDisconnect");

/**
* @notice Permission for recover assets from Delegate contracts
*/
bytes32 public constant PDG_WITHDRAWAL_ROLE = keccak256("StakingVault.Permissions.PDGWithdrawal");

/**
* @notice Permission for recover assets from Delegate contracts
*/
bytes32 public constant ASSET_RECOVERY_ROLE = keccak256("StakingVault.Permissions.AssetRecovery");

/**
* @notice Address of the implementation contract
* @dev Used to prevent initialization in the implementation
Expand Down Expand Up @@ -149,6 +160,13 @@ abstract contract Permissions is AccessControlVoteable {
vaultHub.voluntaryDisconnect(address(stakingVault()));
}

function _withdrawDisputedValidatorFromPDG(
bytes calldata _pubkey,
address _recipient
) internal onlyRole(PDG_WITHDRAWAL_ROLE) returns (uint128) {
return IPredepositGuarantee(stakingVault().depositGuardian()).withdrawDisprovenPredeposit(_pubkey, _recipient);
}

function _transferStakingVaultOwnership(address _newOwner) internal onlyIfVotedBy(_votingCommittee()) {
OwnableUpgradeable(address(stakingVault())).transferOwnership(_newOwner);
}
Expand Down
2 changes: 2 additions & 0 deletions contracts/0.8.25/vaults/VaultFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ struct DelegationConfig {
address curator;
address nodeOperatorManager;
address nodeOperatorFeeClaimer;
address assetRecoverer;
uint16 curatorFeeBP;
uint16 nodeOperatorFeeBP;
}
Expand Down Expand Up @@ -87,6 +88,7 @@ contract VaultFactory {
delegation.grantRole(delegation.CURATOR_ROLE(), _delegationConfig.curator);
delegation.grantRole(delegation.NODE_OPERATOR_MANAGER_ROLE(), _delegationConfig.nodeOperatorManager);
delegation.grantRole(delegation.NODE_OPERATOR_FEE_CLAIMER_ROLE(), _delegationConfig.nodeOperatorFeeClaimer);
delegation.grantRole(delegation.ASSET_RECOVERY_ROLE(), _delegationConfig.assetRecoverer);

// grant temporary roles to factory
delegation.grantRole(delegation.CURATOR_ROLE(), address(this));
Expand Down
94 changes: 94 additions & 0 deletions contracts/0.8.25/vaults/interfaces/IPredepositGuarantee.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0

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

import {IStakingVaultOwnable} from "./IStakingVault.sol";

/**
* @title ICLProofVerifier
* @author Lido
* @notice Interface for the internal `CLProofVerifier` contract
*/
interface ICLProofVerifier {
struct ValidatorWitness {
bytes32[] proof;
bytes pubkey;
uint256 validatorIndex;
uint64 childBlockTimestamp;
}
}

/**
* @title IPredepositGuarantee
* @author Lido
* @notice Interface for the `PredepositGuarantee` contract
*/
interface IPredepositGuarantee is ICLProofVerifier {
enum BondStatus {
NONE,
AWAITING_PROOF,
PROVED,
PROVED_INVALID
}

struct NodeOperatorBond {
uint128 total;
uint128 locked;
}

struct ValidatorStatus {
BondStatus bondStatus;
IStakingVaultOwnable stakingVault;
address nodeOperator;
}

// constructor and initializer interfaces not needed in interface definition

function nodeOperatorBond(address _nodeOperator) external view returns (NodeOperatorBond memory);

function nodeOperatorVoucher(address _nodeOperator) external view returns (address);

function validatorStatus(bytes calldata _validatorPubkey) external view returns (ValidatorStatus memory);

function topUpNodeOperatorBond(address _nodeOperator) external payable;

function withdrawNodeOperatorBond(address _nodeOperator, uint128 _amount, address _recipient) external;

function setNodeOperatorVoucher(address _voucher) external payable;

function predeposit(
IStakingVaultOwnable _stakingVault,
IStakingVaultOwnable.Deposit[] calldata _deposits
) external payable;

function proveValidatorWC(ValidatorWitness calldata _witness) external;

function depositToBeaconChain(
IStakingVaultOwnable _stakingVault,
IStakingVaultOwnable.Deposit[] calldata _deposits
) external payable;

function proveAndDeposit(
ValidatorWitness[] calldata _witnesses,
IStakingVaultOwnable.Deposit[] calldata _deposits,
IStakingVaultOwnable _stakingVault
) external payable;

function proveInvalidValidatorWC(
ValidatorWitness calldata _witness,
bytes32 _invalidWithdrawalCredentials
) external;

function withdrawDisprovenPredeposit(
bytes calldata _validatorPubkey,
address _recipient
) external returns (uint128 amount);

function disproveAndWithdraw(
ValidatorWitness calldata _witness,
bytes32 _invalidWithdrawalCredentials,
address _recipient
) external returns (uint128 amount);
}
11 changes: 3 additions & 8 deletions contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ pragma solidity 0.8.25;
import {GIndex, pack, concat} from "contracts/0.8.25/lib/GIndex.sol";
import {SSZ} from "contracts/0.8.25/lib/SSZ.sol";

import {ICLProofVerifier} from "../interfaces/IPredepositGuarantee.sol";

/**
* @title CLProofVerifier
* @author Lido
Expand All @@ -22,14 +24,7 @@ import {SSZ} from "contracts/0.8.25/lib/SSZ.sol";
* (e.g. Pectra, Altair, etc.)
*
*/
abstract contract CLProofVerifier {
struct ValidatorWitness {
bytes32[] proof;
bytes pubkey;
uint256 validatorIndex;
uint64 childBlockTimestamp;
}

abstract contract CLProofVerifier is ICLProofVerifier {
// See `BEACON_ROOTS_ADDRESS` constant in the EIP-4788.
address public constant BEACON_ROOTS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,14 @@ import {PausableUntilWithRoles} from "contracts/0.8.25/utils/PausableUntilWithRo
import {CLProofVerifier} from "./CLProofVerifier.sol";

import {IStakingVaultOwnable} from "../interfaces/IStakingVault.sol";
import {IPredepositGuarantee} from "../interfaces/IPredepositGuarantee.sol";

/**
* @title PredepositGuarantee
* @author Lido
* @notice
*/
contract PredepositGuarantee is CLProofVerifier, PausableUntilWithRoles {
enum BondStatus {
NO_RECORD,
AWAITING_PROOF,
PROVED,
PROVED_INVALID,
WITHDRAWN
}

struct NodeOperatorBond {
uint128 total;
uint128 locked;
}

struct ValidatorStatus {
BondStatus bondStatus;
IStakingVaultOwnable stakingVault;
address nodeOperator;
}

contract PredepositGuarantee is IPredepositGuarantee, CLProofVerifier, PausableUntilWithRoles {
/**
* @notice ERC-7201 storage namespace for the vault
* @dev ERC-7201 namespace is used to prevent upgrade collisions
Expand Down Expand Up @@ -186,7 +168,7 @@ contract PredepositGuarantee is CLProofVerifier, PausableUntilWithRoles {
for (uint256 i = 0; i < _deposits.length; i++) {
IStakingVaultOwnable.Deposit calldata _deposit = _deposits[i];

if ($.validatorStatuses[_deposit.pubkey].bondStatus != BondStatus.NO_RECORD) {
if ($.validatorStatuses[_deposit.pubkey].bondStatus != BondStatus.NONE) {
revert MustBeNewValidatorPubkey(_deposit.pubkey, $.validatorStatuses[_deposit.pubkey].bondStatus);
}

Expand Down Expand Up @@ -318,38 +300,41 @@ contract PredepositGuarantee is CLProofVerifier, PausableUntilWithRoles {

// called by the staking vault owner if the predeposited validator was proven invalid
// i.e. node operator was malicious and has stolen vault ether
function withdrawDisprovenPredeposit(bytes calldata validatorPubkey, address _recipient) public whenResumed {
ValidatorStatus storage validator = _getStorage().validatorStatuses[validatorPubkey];
function withdrawDisprovenPredeposit(
bytes calldata _validatorPubkey,
address _recipient
) public whenResumed returns (uint128) {
ValidatorStatus storage validator = _getStorage().validatorStatuses[_validatorPubkey];

IStakingVaultOwnable _stakingVault = validator.stakingVault;
address _nodeOperator = validator.nodeOperator;

if (_recipient == address(0)) revert ZeroArgument("_recipient");

if (_recipient == address(validator.stakingVault)) revert WithdrawToVaultNotAllowed();
if (_recipient == address(_stakingVault)) revert WithdrawToVaultNotAllowed();

if (msg.sender != validator.stakingVault.owner()) revert WithdrawSenderNotStakingVaultOwner();
if (msg.sender != _stakingVault.owner()) revert WithdrawSenderNotStakingVaultOwner();

if (validator.bondStatus != BondStatus.PROVED_INVALID) revert ValidatorNotProvenInvalid(validator.bondStatus);

validator.bondStatus = BondStatus.WITHDRAWN;
delete _getStorage().validatorStatuses[_validatorPubkey];

(bool success, ) = _recipient.call{value: PREDEPOSIT_AMOUNT}("");

if (!success) revert WithdrawalFailed();

emit ValidatorDisprovenWithdrawn(
validator.nodeOperator,
validatorPubkey,
address(validator.stakingVault),
_recipient
);
emit ValidatorDisprovenWithdrawn(_nodeOperator, _validatorPubkey, address(_stakingVault), _recipient);

return PREDEPOSIT_AMOUNT;
}

function disproveAndWithdraw(
ValidatorWitness calldata _witness,
bytes32 _invalidWithdrawalCredentials,
address _recipient
) external {
) external returns (uint128) {
proveInvalidValidatorWC(_witness, _invalidWithdrawalCredentials);
withdrawDisprovenPredeposit(_witness.pubkey, _recipient);
return withdrawDisprovenPredeposit(_witness.pubkey, _recipient);
}

/// Internal functions
Expand Down
1 change: 1 addition & 0 deletions test/0.8.25/vaults/delegation/delegation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ describe("Delegation.sol", () => {
exitRequester,
disconnecter,
curator,
assetRecoverer: curator,
nodeOperatorManager,
nodeOperatorFeeClaimer,
curatorFeeBP: 0n,
Expand Down
Loading

0 comments on commit e79014d

Please sign in to comment.