Skip to content

Commit

Permalink
feat: add accounting&delegation to predeposit guardian
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeday committed Jan 30, 2025
1 parent d0954f7 commit c5312c0
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 73 deletions.
243 changes: 170 additions & 73 deletions contracts/0.8.25/vaults/PredepositGuardian.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,46 +11,148 @@ import {StakingVault} from "./StakingVault.sol";
contract PredepositGuardian {
uint256 public constant PREDEPOSIT_AMOUNT = 1 ether;

mapping(bytes32 validatorId => bool isPreDeposited) public validatorPredeposits;
mapping(bytes32 validatorId => bytes32 withdrawalCredentials) public validatorWithdrawalCredentials;
enum ValidatorStatus {
NO_RECORD,
AWAITING_PROOF,
PROVED,
PROVED_INVALID,
WITHDRAWN
}

mapping(address nodeOperator => uint256) public nodeOperatorCollateral;
mapping(address nodeOperator => uint256) public nodeOperatorCollateralLocked;
mapping(address nodeOperator => address delegate) public nodeOperatorDelegate;

mapping(bytes32 validatorPubkeyHash => ValidatorStatus validatorStatus) public validatorStatuses;
mapping(bytes32 validatorPubkeyHash => bytes32 withdrawalCredentials) public validatorWithdrawalCredentials;

/// views

function nodeOperatorBalance(address nodeOperator) external view returns (uint256, uint256) {
return (nodeOperatorCollateral[nodeOperator], nodeOperatorCollateralLocked[nodeOperator]);
}

/// Balance operations

function topUpNodeOperatorCollateral(address _nodeOperator) external payable {
if (msg.value == 0) revert ZeroArgument("msg.value");
_topUpNodeOperatorCollateral(_nodeOperator);
}

function withdrawNodeOperatorCollateral(address _nodeOperator, uint256 _amount, address _recipient) external {
if (_amount == 0) revert ZeroArgument("amount");
if (_nodeOperator == address(0)) revert ZeroArgument("_nodeOperator");
// TODO: delegate
if (msg.sender != _nodeOperator) revert MustBeNodeOperator();

if (nodeOperatorCollateral[_nodeOperator] - nodeOperatorCollateralLocked[_nodeOperator] >= _amount)
revert NotEnoughUnlockedCollateralToWithdraw();

nodeOperatorCollateral[_nodeOperator] -= _amount;
(bool success, ) = _recipient.call{value: _amount}("");

if (!success) revert WithdrawalFailed();

// TODO: event
}

// delegation

function delegateNodeOperator(address _delegate) external {
nodeOperatorDelegate[msg.sender] = _delegate;
// TODO: event
}

// Question: predeposit is permissionless, i.e. the msg.sender doesn't have to be the node operator,
// however, the deposit will still revert if it wasn't signed with the validator private key
function predeposit(StakingVault stakingVault, StakingVault.Deposit[] calldata deposits) external payable {
if (deposits.length == 0) revert PredepositNoDeposits();
if (msg.value % PREDEPOSIT_AMOUNT != 0) revert PredepositValueNotMultipleOfOneEther();
if (msg.value / PREDEPOSIT_AMOUNT != deposits.length) revert PredepositValueNotMatchingNumberOfDeposits();
function predeposit(StakingVault _stakingVault, StakingVault.Deposit[] calldata _deposits) external payable {
if (_deposits.length == 0) revert PredepositNoDeposits();

for (uint256 i = 0; i < deposits.length; i++) {
StakingVault.Deposit calldata deposit = deposits[i];
address _nodeOperator = StakingVault(payable(_stakingVault)).nodeOperator();
_isValidNodeOperatorCaller(_nodeOperator);

bytes32 validatorId = keccak256(deposit.pubkey);
// optional top up
if (msg.value != 0) {
_topUpNodeOperatorCollateral(_nodeOperator);
}

uint256 unlockedCollateral = nodeOperatorCollateral[_nodeOperator] -
nodeOperatorCollateralLocked[_nodeOperator];

uint256 totalDepositAmount = PREDEPOSIT_AMOUNT * _deposits.length;

if (unlockedCollateral < totalDepositAmount) revert NotEnoughUnlockedCollateralToPredeposit();

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

// cannot predeposit a validator that is already predeposited
if (validatorPredeposits[validatorId]) revert PredepositValidatorAlreadyPredeposited();
bytes32 validatorId = keccak256(_deposit.pubkey);

// cannot predeposit a validator that has withdrawal credentials already proven
if (validatorWithdrawalCredentials[validatorId] != bytes32(0))
revert PredepositValidatorWithdrawalCredentialsAlreadyProven();
if (validatorStatuses[validatorId] != ValidatorStatus.NO_RECORD) {
revert MustBeNewValidatorPubkey();
}

// cannot predeposit a validator with a deposit amount that is not 1 ether
if (deposit.amount != PREDEPOSIT_AMOUNT) revert PredepositDepositAmountInvalid();
if (_deposit.amount != PREDEPOSIT_AMOUNT) revert PredepositDepositAmountInvalid();

validatorPredeposits[validatorId] = true;
validatorStatuses[validatorId] = ValidatorStatus.AWAITING_PROOF;
// this prevents cross deposit to other vault
validatorWithdrawalCredentials[validatorId] = _stakingVault.withdrawalCredentials();
}

stakingVault.depositToBeaconChain(deposits);
nodeOperatorCollateralLocked[_nodeOperator] += totalDepositAmount;
_stakingVault.depositToBeaconChain(_deposits);
// TODO: event
}

function proveValidatorWithdrawalCredentials(
bytes32[] calldata /* proof */,
bytes calldata _pubkey,
bytes32 _withdrawalCredentials
function proveValidatorDeposit(
StakingVault _stakingVault,
bytes32[] calldata proof,
StakingVault.Deposit calldata _deposit
) external {
// TODO: proof logic
// revert if proof is invalid
bytes32 validatorId = keccak256(_deposit.pubkey);

// check that the validator is predeposited
if (validatorStatuses[validatorId] != ValidatorStatus.AWAITING_PROOF) {
revert ValidatorNotPreDeposited();
}

// check that predeposit was made to the staking vault in proof
if (validatorWithdrawalCredentials[validatorId] != _stakingVault.withdrawalCredentials()) {
revert InvalidStakingVault();
}

validatorWithdrawalCredentials[keccak256(_pubkey)] = _withdrawalCredentials;
if (!_isValidProof(proof, _stakingVault.withdrawalCredentials(), _deposit)) revert InvalidProof();

address _nodeOperator = _stakingVault.nodeOperator();
nodeOperatorCollateralLocked[_nodeOperator] -= PREDEPOSIT_AMOUNT;

validatorStatuses[validatorId] = ValidatorStatus.PROVED;

// TODO: event
}

function proveInvalidValidatorDeposit(
bytes32[] calldata proof,
StakingVault.Deposit calldata _deposit,
bytes32 _invalidWC
) external {
bytes32 validatorId = keccak256(_deposit.pubkey);

// check that the validator is predeposited
if (validatorStatuses[validatorId] != ValidatorStatus.AWAITING_PROOF) {
revert ValidatorNotPreDeposited();
}

if (validatorWithdrawalCredentials[validatorId] == _invalidWC) {
revert WithdrawalCredentialsAreValid();
}

if (!_isValidProof(proof, _invalidWC, _deposit)) revert InvalidProof();

validatorStatuses[validatorId] = ValidatorStatus.PROVED_INVALID;

// TODO: event
}

function depositToProvenValidators(
Expand All @@ -73,67 +175,46 @@ contract PredepositGuardian {

// called by the staking vault owner if the predeposited validator has a different withdrawal credentials than the vault's withdrawal credentials,
// i.e. node operator was malicious
function withdrawDisprovenPredeposits(
StakingVault _stakingVault,
bytes32[] calldata _validatorIds,
address _recipient
) external {
function slashCollateral(StakingVault _stakingVault, bytes32 _validatorId, address _recipient) external {
if (msg.sender != _stakingVault.owner()) revert WithdrawSenderNotStakingVaultOwner();
if (_recipient == address(0)) revert WithdrawRecipientZeroAddress();

uint256 validatorsLength = _validatorIds.length;
for (uint256 i = 0; i < validatorsLength; i++) {
bytes32 validatorId = _validatorIds[i];
if (validatorStatuses[_validatorId] != ValidatorStatus.PROVED_INVALID) {
revert SlashingNotPermitted();
}

// cannot withdraw predeposit for a validator that is not pre-deposited
if (!validatorPredeposits[validatorId]) {
revert WithdrawValidatorNotPreDeposited();
}
if (validatorWithdrawalCredentials[_validatorId] != _stakingVault.withdrawalCredentials()) {
revert WithdrawValidatorWithdrawalCredentialsNotMatchingStakingVault();
}

// cannot withdraw predeposit for a validator that has withdrawal credentials matching the vault's withdrawal credentials
if (validatorWithdrawalCredentials[validatorId] == _stakingVault.withdrawalCredentials()) {
revert WithdrawValidatorWithdrawalCredentialsMatchStakingVault();
}
validatorStatuses[_validatorId] = ValidatorStatus.WITHDRAWN;

// set flag to false to prevent double withdrawal
validatorPredeposits[validatorId] = false;
(bool success, ) = _recipient.call{value: PREDEPOSIT_AMOUNT}("");
if (!success) revert WithdrawValidatorTransferFailed();

(bool success, ) = _recipient.call{value: 1 ether}("");
if (!success) revert WithdrawValidatorTransferFailed();
}
//TODO: events
}

// called by the node operator if the predeposited validator has the same withdrawal credentials as the vault's withdrawal credentials,
// i.e. node operator was honest
function withdrawProvenPredeposits(
StakingVault _stakingVault,
bytes32[] calldata _validatorIds,
address _recipient
) external {
uint256 validatorsLength = _validatorIds.length;
for (uint256 i = 0; i < validatorsLength; i++) {
bytes32 validatorId = _validatorIds[i];

if (msg.sender != _stakingVault.nodeOperator()) {
revert WithdrawSenderNotNodeOperator();
}
/// Internal functions

// cannot withdraw predeposit for a validator that is not pre-deposited
if (!validatorPredeposits[validatorId]) {
revert WithdrawValidatorNotPreDeposited();
}

// cannot withdraw predeposit for a validator that has withdrawal credentials not matching the vault's withdrawal credentials
if (validatorWithdrawalCredentials[validatorId] != _stakingVault.withdrawalCredentials()) {
revert WithdrawValidatorWithdrawalCredentialsNotMatchingStakingVault();
}
function _isValidProof(
bytes32[] calldata _proof,

Check failure on line 201 in contracts/0.8.25/vaults/PredepositGuardian.sol

View workflow job for this annotation

GitHub Actions / Solhint

Variable "_proof" is unused
bytes32 _withdrawalCredentials,

Check failure on line 202 in contracts/0.8.25/vaults/PredepositGuardian.sol

View workflow job for this annotation

GitHub Actions / Solhint

Variable "_withdrawalCredentials" is unused
StakingVault.Deposit calldata _deposit

Check failure on line 203 in contracts/0.8.25/vaults/PredepositGuardian.sol

View workflow job for this annotation

GitHub Actions / Solhint

Variable "_deposit" is unused
) internal pure returns (bool) {
// proof logic
return true;
}

// set flag to false to prevent double withdrawal
validatorPredeposits[validatorId] = false;
function _topUpNodeOperatorCollateral(address _nodeOperator) internal {
if (_nodeOperator == address(0)) revert ZeroArgument("_nodeOperator");
nodeOperatorCollateral[_nodeOperator] += msg.value;
// TODO: event
}

(bool success, ) = _recipient.call{value: 1 ether}("");
if (!success) revert WithdrawValidatorTransferFailed();
}
function _isValidNodeOperatorCaller(address _nodeOperator) internal view {
if (msg.sender != _nodeOperator && nodeOperatorDelegate[_nodeOperator] != msg.sender)
revert MustBeNodeOperator();
}

error PredepositNoDeposits();
Expand All @@ -154,4 +235,20 @@ contract PredepositGuardian {
error WithdrawValidatorWithdrawalCredentialsNotMatchingStakingVault();
error WithdrawSenderNotNodeOperator();
error WithdrawValidatorDoesNotBelongToNodeOperator();
///

error NotEnoughUnlockedCollateralToWithdraw();
// TODO: rename to mention delegate
error MustBeNodeOperatorOfStakingVault();
error MustBeNodeOperator();
error WithdrawalFailed();
error ZeroArgument(string argument);
// TODO: args NO, amount - unlocked
error NotEnoughUnlockedCollateralToPredeposit();
error MustBeNewValidatorPubkey();
error InvalidProof();
error InvalidStakingVault();
error ProofOfWrongDeposit();
error WithdrawalCredentialsAreValid();
error SlashingNotPermitted();
}
2 changes: 2 additions & 0 deletions contracts/0.8.25/vaults/StakingVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,8 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {
for (uint256 i = 0; i < numberOfDeposits; i++) {
Deposit calldata deposit = _deposits[i];

//TODO: check BLS signature

BEACON_CHAIN_DEPOSIT_CONTRACT.deposit{value: deposit.amount}(
deposit.pubkey,
bytes.concat(withdrawalCredentials()),
Expand Down

0 comments on commit c5312c0

Please sign in to comment.