-
Notifications
You must be signed in to change notification settings - Fork 195
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][POC] a very raw bug-ridden proof-of-concept for PredepositGuardian #932
base: vault-guardian
Are you sure you want to change the base?
Conversation
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
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[])
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 failure
Code scanning / Slither
Reentrancy vulnerabilities High
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[])
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👀
bytes32 _expectedGlobalDepositRoot, | ||
bytes calldata _signature | ||
) external { | ||
function depositToBeaconChain(Deposit[] calldata _deposits) external { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
strawman sanity checks would be needed for the input data
@@ -68,6 +68,7 @@ contract StakingVault is IStakingVault, OwnableUpgradeable { | |||
uint128 locked; | |||
int128 inOutDelta; | |||
address nodeOperator; | |||
// depositGuardian becomes the depositor, instead of just guardian, perhaps a renaming is needed 🌚 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe make depositGuardian
immutable
|
||
// TODO: think about naming. It's not a deposit guardian, it's the depositor itself | ||
// TODO: minor UX improvement: perhaps there's way to reuse predeposits for a different validator without withdrawing | ||
contract PredepositGuardian { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
contract PredepositGuardian { | |
contract PredepositGuarantee { |
mapping(bytes32 validatorId => bool isPreDeposited) public validatorPredeposits; | ||
mapping(bytes32 validatorId => bytes32 withdrawalCredentials) public validatorWithdrawalCredentials; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mapping(bytes32 validatorId => bool isPreDeposited) public validatorPredeposits; | |
mapping(bytes32 validatorId => bytes32 withdrawalCredentials) public validatorWithdrawalCredentials; | |
mapping(address nodeOperator => uint256) public nodeOperatorGuarantee; | |
mapping(bytes validatorPubkey => bytes32 withdrawalCredentials) public validatorWithdrawalCredentials; |
mapping(bytes32 validatorId => bytes32 withdrawalCredentials) public validatorWithdrawalCredentials; | ||
|
||
// 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
didn't get this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this function can't be permissionless
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- let's include accounting and 'outsourcing' for funding
|
||
// 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( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure what should be done here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's implement optionality in Dashboard
(either fund stVault or withdraw to the outer address)
// set flag to false to prevent double withdrawal | ||
validatorPredeposits[validatorId] = false; | ||
|
||
(bool success, ) = _recipient.call{value: 1 ether}(""); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for (uint256 i = 0; i < validatorsLength; i++) { | ||
bytes32 validatorId = _validatorIds[i]; | ||
|
||
if (msg.sender != _stakingVault.nodeOperator()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why inside for loop?
stakingVault.depositToBeaconChain(deposits); | ||
} | ||
|
||
function proveValidatorWithdrawalCredentials( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should combine prove + deposit
THIS IS A POC, NOT READY FOR PRODUCTION, PRACTICALLY PSEUDOCODE