-
Notifications
You must be signed in to change notification settings - Fork 197
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
base: feat/vaults
Are you sure you want to change the base?
Changes from 12 commits
c19a543
b710b58
86f80bf
a0de885
295ba6e
938a519
34d203d
21f475e
331a6e6
2c84783
ccde1b2
d0954f7
c5312c0
9d2b349
e4d3ebc
74917ee
a17c375
8674bba
87c7e01
ad9e476
5212738
0d5f663
ae6d487
2fc25b6
563d852
d2f5f24
4ef7550
6d6242b
ecc06c6
ef47aed
3bb45e8
500e8be
56b7752
364e1e7
04a70a9
3576bb9
16014b0
7b61c6a
6e3b647
8c45174
60b3157
3223ab5
917c685
966ab9c
e79014d
d077613
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,157 @@ | ||||||||||
// 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"; | ||||||||||
|
||||||||||
// 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 { | ||||||||||
uint256 public constant PREDEPOSIT_AMOUNT = 1 ether; | ||||||||||
|
||||||||||
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 commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||||||||||
|
||||||||||
// 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 commentThe 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||
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(); | ||||||||||
|
||||||||||
for (uint256 i = 0; i < deposits.length; i++) { | ||||||||||
StakingVault.Deposit calldata deposit = deposits[i]; | ||||||||||
|
||||||||||
bytes32 validatorId = keccak256(deposit.pubkey); | ||||||||||
|
||||||||||
// cannot predeposit a validator that is already predeposited | ||||||||||
if (validatorPredeposits[validatorId]) revert PredepositValidatorAlreadyPredeposited(); | ||||||||||
|
||||||||||
// cannot predeposit a validator that has withdrawal credentials already proven | ||||||||||
if (validatorWithdrawalCredentials[validatorId] != bytes32(0)) | ||||||||||
revert PredepositValidatorWithdrawalCredentialsAlreadyProven(); | ||||||||||
|
||||||||||
// cannot predeposit a validator with a deposit amount that is not 1 ether | ||||||||||
if (deposit.amount != PREDEPOSIT_AMOUNT) revert PredepositDepositAmountInvalid(); | ||||||||||
|
||||||||||
validatorPredeposits[validatorId] = true; | ||||||||||
} | ||||||||||
|
||||||||||
stakingVault.depositToBeaconChain(deposits); | ||||||||||
} | ||||||||||
|
||||||||||
function proveValidatorWithdrawalCredentials( | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should combine prove + deposit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||||||||||
bytes32[] calldata /* proof */, | ||||||||||
bytes calldata _pubkey, | ||||||||||
bytes32 _withdrawalCredentials | ||||||||||
) external { | ||||||||||
// TODO: proof logic | ||||||||||
// revert if proof is invalid | ||||||||||
|
||||||||||
validatorWithdrawalCredentials[keccak256(_pubkey)] = _withdrawalCredentials; | ||||||||||
} | ||||||||||
|
||||||||||
function depositToProvenValidators( | ||||||||||
StakingVault _stakingVault, | ||||||||||
StakingVault.Deposit[] calldata _deposits | ||||||||||
) external payable { | ||||||||||
if (msg.sender != _stakingVault.nodeOperator()) revert DepositSenderNotNodeOperator(); | ||||||||||
|
||||||||||
for (uint256 i = 0; i < _deposits.length; i++) { | ||||||||||
StakingVault.Deposit calldata deposit = _deposits[i]; | ||||||||||
bytes32 validatorId = keccak256(deposit.pubkey); | ||||||||||
|
||||||||||
if (validatorWithdrawalCredentials[validatorId] != _stakingVault.withdrawalCredentials()) { | ||||||||||
revert DepositToUnprovenValidator(); | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
_stakingVault.depositToBeaconChain(_deposits); | ||||||||||
} | ||||||||||
|
||||||||||
// 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. let's implement optionality in |
||||||||||
StakingVault _stakingVault, | ||||||||||
bytes32[] calldata _validatorIds, | ||||||||||
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]; | ||||||||||
|
||||||||||
// 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 matching the vault's withdrawal credentials | ||||||||||
if (validatorWithdrawalCredentials[validatorId] == _stakingVault.withdrawalCredentials()) { | ||||||||||
revert WithdrawValidatorWithdrawalCredentialsMatchStakingVault(); | ||||||||||
} | ||||||||||
|
||||||||||
// 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 commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||||||||||
if (!success) revert WithdrawValidatorTransferFailed(); | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
// 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()) { | ||||||||||
Jeday marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
revert WithdrawSenderNotNodeOperator(); | ||||||||||
} | ||||||||||
|
||||||||||
// 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(); | ||||||||||
} | ||||||||||
|
||||||||||
// set flag to false to prevent double withdrawal | ||||||||||
validatorPredeposits[validatorId] = false; | ||||||||||
|
||||||||||
(bool success, ) = _recipient.call{value: 1 ether}(""); | ||||||||||
if (!success) revert WithdrawValidatorTransferFailed(); | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
error PredepositNoDeposits(); | ||||||||||
error PredepositValueNotMultipleOfOneEther(); | ||||||||||
error PredepositValueNotMatchingNumberOfDeposits(); | ||||||||||
error PredepositNodeOperatorNotMatching(); | ||||||||||
error PredepositValidatorAlreadyPredeposited(); | ||||||||||
error PredepositValidatorWithdrawalCredentialsAlreadyProven(); | ||||||||||
error PredepositDepositAmountInvalid(); | ||||||||||
error ValidatorNotPreDeposited(); | ||||||||||
error DepositSenderNotNodeOperator(); | ||||||||||
error DepositToUnprovenValidator(); | ||||||||||
error WithdrawSenderNotStakingVaultOwner(); | ||||||||||
error WithdrawRecipientZeroAddress(); | ||||||||||
error WithdrawValidatorNotPreDeposited(); | ||||||||||
error WithdrawValidatorWithdrawalCredentialsMatchStakingVault(); | ||||||||||
error WithdrawValidatorTransferFailed(); | ||||||||||
error WithdrawValidatorWithdrawalCredentialsNotMatchingStakingVault(); | ||||||||||
error WithdrawSenderNotNodeOperator(); | ||||||||||
error WithdrawValidatorDoesNotBelongToNodeOperator(); | ||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"; | ||
|
||
|
@@ -67,6 +68,8 @@ 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 commentThe reason will be displayed to describe this comment to others. Learn more. maybe make There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changable but only for unconnceted vaults |
||
address depositGuardian; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's rename to trustedDepositor, or something There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just depositor is also ok, trusted kind of excessive |
||
bool beaconChainDepositsPaused; | ||
} | ||
|
||
|
@@ -76,6 +79,8 @@ contract StakingVault is IStakingVault, OwnableUpgradeable { | |
*/ | ||
uint64 private constant _VERSION = 1; | ||
|
||
bytes32 public constant DEPOSIT_GUARDIAN_MESSAGE_PREFIX = keccak256("StakingVault.DepositGuardianMessagePrefix"); | ||
Jeday marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* @notice Address of `VaultHub` | ||
* Set immutably in the constructor to avoid storage costs | ||
|
@@ -122,6 +127,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; | ||
} | ||
|
||
/** | ||
|
@@ -315,22 +321,26 @@ contract StakingVault is IStakingVault, OwnableUpgradeable { | |
*/ | ||
function depositToBeaconChain(Deposit[] calldata _deposits) external { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. strawman sanity checks would be needed for the input data There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added BLS check todo, needs clarification on best use for precompile |
||
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(); | ||
if (msg.sender != $.depositGuardian) revert NotAuthorized("depositToBeaconChain", msg.sender); | ||
|
||
uint256 totalAmount = 0; | ||
uint256 numberOfDeposits = _deposits.length; | ||
|
||
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; | ||
} | ||
|
||
|
@@ -404,6 +414,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 | ||
|
@@ -554,6 +584,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 | ||
|
@@ -617,6 +654,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); | ||
Jeday marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* @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 | ||
*/ | ||
|
+1 −1 | README.md | |
+1 −1 | package.json | |
+14 −1 | src/StdChains.sol | |
+3 −3 | src/StdCheats.sol | |
+9 −0 | src/StdInvariant.sol | |
+104 −0 | src/StdJson.sol | |
+1 −1 | src/StdStorage.sol | |
+104 −0 | src/StdToml.sol | |
+290 −70 | src/Vm.sol | |
+635 −608 | src/console.sol | |
+1 −1,555 | src/console2.sol | |
+2 −2 | src/interfaces/IERC4626.sol | |
+1 −5 | src/mocks/MockERC721.sol | |
+693 −4 | src/safeconsole.sol | |
+1 −1 | test/StdAssertions.t.sol | |
+19 −12 | test/StdChains.t.sol | |
+10 −10 | test/StdCheats.t.sol | |
+12 −12 | test/StdError.t.sol | |
+1 −1 | test/StdJson.t.sol | |
+4 −14 | test/StdMath.t.sol | |
+13 −5 | test/StdStorage.t.sol | |
+1 −1 | test/StdStyle.t.sol | |
+1 −1 | test/StdToml.t.sol | |
+12 −12 | test/StdUtils.t.sol | |
+9 −6 | test/Vm.t.sol |
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.
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.
fixed