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 45 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
44 changes: 19 additions & 25 deletions contracts/0.8.25/Accounting.sol
Copy link
Contributor

Choose a reason for hiding this comment

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

can we roll it back?

Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,11 @@ contract Accounting is VaultHub {
/// @notice deposit size in wei (for pre-maxEB accounting)
uint256 private constant DEPOSIT_SIZE = 32 ether;

/// @notice Lido Locator contract
ILidoLocator public immutable LIDO_LOCATOR;
/// @notice Lido contract
ILido public immutable LIDO;

constructor(
ILidoLocator _lidoLocator,
ILido _lido
) VaultHub(_lido) {
LIDO_LOCATOR = _lidoLocator;
LIDO = _lido;
constructor(ILidoLocator _lidoLocator) VaultHub(_lidoLocator) {
LIDO = ILido(_lidoLocator.lido());
}

function initialize(address _admin) external initializer {
Expand Down Expand Up @@ -220,22 +214,27 @@ contract Accounting is VaultHub {
update.withdrawals -
update.principalClBalance + // total cl rewards (or penalty)
update.elRewards + // ELRewards
postExternalEther - _pre.externalEther // vaults rebase
- update.etherToFinalizeWQ; // withdrawals
postExternalEther -
_pre.externalEther - // vaults rebase
update.etherToFinalizeWQ; // withdrawals

// Calculate the amount of ether locked in the vaults to back external balance of stETH
// and the amount of shares to mint as fees to the treasury for each vaults
(update.vaultsLockedEther, update.vaultsTreasuryFeeShares, update.totalVaultsTreasuryFeeShares) =
_calculateVaultsRebase(
update.postTotalShares,
update.postTotalPooledEther,
_pre.totalShares,
_pre.totalPooledEther,
update.sharesToMintAsFees
);
(
update.vaultsLockedEther,
update.vaultsTreasuryFeeShares,
update.totalVaultsTreasuryFeeShares
) = _calculateVaultsRebase(
update.postTotalShares,
update.postTotalPooledEther,
_pre.totalShares,
_pre.totalPooledEther,
update.sharesToMintAsFees
);

update.postTotalPooledEther +=
update.totalVaultsTreasuryFeeShares * update.postTotalPooledEther / update.postTotalShares;
(update.totalVaultsTreasuryFeeShares * update.postTotalPooledEther) /
update.postTotalShares;
update.postTotalShares += update.totalVaultsTreasuryFeeShares;
}

Expand Down Expand Up @@ -308,12 +307,7 @@ contract Accounting is VaultHub {
];
}

LIDO.processClStateUpdate(
_report.timestamp,
_pre.clValidators,
_report.clValidators,
_report.clBalance
);
LIDO.processClStateUpdate(_report.timestamp, _pre.clValidators, _report.clValidators, _report.clBalance);

if (_update.totalSharesToBurn > 0) {
_contracts.burner.commitSharesToBurn(_update.totalSharesToBurn);
Expand Down
122 changes: 122 additions & 0 deletions contracts/0.8.25/lib/GIndex.sol
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this looks like it should be collected under a library

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, but

  • using allows it act as one
  • this 100% unchanged CSM code, which is easier to trust and audit

Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// SPDX-FileCopyrightText: 2024 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

/*
GIndex library from CSM
original: https://github.com/lidofinance/community-staking-module/blob/7071c2096983a7780a5f147963aaa5405c0badb1/src/lib/GIndex.sol
*/

pragma solidity 0.8.25;

type GIndex is bytes32;

using {isRoot, isParentOf, index, width, shr, shl, concat, unwrap, pow} for GIndex global;

error IndexOutOfRange();

/// @param gI Is a generalized index of a node in a tree.
/// @param p Is a power of a tree level the node belongs to.
/// @return GIndex
function pack(uint256 gI, uint8 p) pure returns (GIndex) {
if (gI > type(uint248).max) {
revert IndexOutOfRange();
}

// NOTE: We can consider adding additional metadata like a fork version.
return GIndex.wrap(bytes32((gI << 8) | p));
}

function unwrap(GIndex self) pure returns (bytes32) {
return GIndex.unwrap(self);
}

function isRoot(GIndex self) pure returns (bool) {
return index(self) == 1;
}

function index(GIndex self) pure returns (uint256) {
return uint256(unwrap(self)) >> 8;
}

function width(GIndex self) pure returns (uint256) {
return 1 << pow(self);
}

function pow(GIndex self) pure returns (uint8) {
return uint8(uint256(unwrap(self)));
}

/// @return Generalized index of the nth neighbor of the node to the right.
function shr(GIndex self, uint256 n) pure returns (GIndex) {
uint256 i = index(self);
uint256 w = width(self);

if ((i % w) + n >= w) {
revert IndexOutOfRange();
}

return pack(i + n, pow(self));
}

/// @return Generalized index of the nth neighbor of the node to the left.
function shl(GIndex self, uint256 n) pure returns (GIndex) {
uint256 i = index(self);
uint256 w = width(self);

if (i % w < n) {
revert IndexOutOfRange();
}

return pack(i - n, pow(self));
}

// See https://github.com/protolambda/remerkleable/blob/91ed092d08ef0ba5ab076f0a34b0b371623db728/remerkleable/tree.py#L46
function concat(GIndex lhs, GIndex rhs) pure returns (GIndex) {
uint256 lhsMSbIndex = fls(index(lhs));
uint256 rhsMSbIndex = fls(index(rhs));

if (lhsMSbIndex + 1 + rhsMSbIndex > 248) {
revert IndexOutOfRange();
}

return pack((index(lhs) << rhsMSbIndex) | (index(rhs) ^ (1 << rhsMSbIndex)), pow(rhs));
}

function isParentOf(GIndex self, GIndex child) pure returns (bool) {
uint256 parentIndex = index(self);
uint256 childIndex = index(child);

if (parentIndex >= childIndex) {
return false;
}

while (childIndex > 0) {
if (childIndex == parentIndex) {
return true;
}

childIndex = childIndex >> 1;
}

return false;
}

/// @dev From Solady LibBit, see https://github.com/Vectorized/solady/blob/main/src/utils/LibBit.sol.
/// @dev Find last set.
/// Returns the index of the most significant bit of `x`,
/// counting from the least significant bit position.
/// If `x` is zero, returns 256.
function fls(uint256 x) pure returns (uint256 r) {
/// @solidity memory-safe-assembly
assembly {
// prettier-ignore
r := or(shl(8, iszero(x)), shl(7, lt(0xffffffffffffffffffffffffffffffff, x)))
r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x))))
r := or(r, shl(5, lt(0xffffffff, shr(r, x))))
r := or(r, shl(4, lt(0xffff, shr(r, x))))
r := or(r, shl(3, lt(0xff, shr(r, x))))
// prettier-ignore
r := or(r, byte(and(0x1f, shr(shr(r, x), 0x8421084210842108cc6318c6db6d54be)),
0x0706060506020504060203020504030106050205030304010505030400000000))
}
}
132 changes: 132 additions & 0 deletions contracts/0.8.25/lib/SSZ.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// SPDX-FileCopyrightText: 2024 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

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

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

/*
Cut and modified version of SSZ library from CSM only has methods for merkilized SSZ proof validation
original: https://github.com/lidofinance/community-staking-module/blob/7071c2096983a7780a5f147963aaa5405c0badb1/src/lib/SSZ.sol
*/
library SSZ {
error BranchHasMissingItem();
error BranchHasExtraItem();
error InvalidProof();
error InvalidPubkeyLength();

/// @notice Modified version of `verify` from Solady `MerkleProofLib` to support generalized indices and sha256 precompile.
/// @dev Reverts if `leaf` doesn't exist in the Merkle tree with `root`, given `proof`.
function verifyProof(bytes32[] calldata proof, bytes32 root, bytes32 leaf, GIndex gIndex) internal view {
uint256 index = gIndex.index();
/// @solidity memory-safe-assembly
assembly {
// Check if `proof` is empty.
if iszero(proof.length) {
// revert InvalidProof()
mstore(0x00, 0x09bde339)
revert(0x1c, 0x04)
}
// Left shift by 5 is equivalent to multiplying by 0x20.
let end := add(proof.offset, shl(5, proof.length))
// Initialize `offset` to the offset of `proof` in the calldata.
let offset := proof.offset
// Iterate over proof elements to compute root hash.
// prettier-ignore
for { } 1 { } {
// Slot of `leaf` in scratch space.
// If the condition is true: 0x20, otherwise: 0x00.
let scratch := shl(5, and(index, 1))
index := shr(1, index)
if iszero(index) {
// revert BranchHasExtraItem()
mstore(0x00, 0x5849603f)
// 0x1c = 28 => offset in 32-byte word of a slot 0x00
revert(0x1c, 0x04)
}
// Store elements to hash contiguously in scratch space.
// Scratch space is 64 bytes (0x00 - 0x3f) and both elements are 32 bytes.
mstore(scratch, leaf)
mstore(xor(scratch, 0x20), calldataload(offset))
Copy link
Contributor

Choose a reason for hiding this comment

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

it's basically to store to 0x00 and 0x20 not using the if condition

// Call sha256 precompile.
let result := staticcall(
gas(),
0x02,
0x00,
0x40,
0x00,
0x20
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let result := staticcall(
gas(),
0x02,
0x00,
0x40,
0x00,
0x20
)
let result := staticcall(
gas(),
0x02, /* precompile */
0x00, /* input memory offset */
0x40, /* input length */
0x00, /* output memory offset */
0x20 /* output length */
)


if iszero(result) {
// Precompile returns no data on OutOfGas error.
revert(0, 0)
}

// Reuse `leaf` to store the hash to reduce stack operations.
leaf := mload(0x00)
offset := add(offset, 0x20)
if iszero(lt(offset, end)) {
break
}
}

if iszero(eq(index, 1)) {
// revert BranchHasMissingItem()
mstore(0x00, 0x1b6661c3)
revert(0x1c, 0x04)
}

if iszero(eq(leaf, root)) {
// revert InvalidProof()
mstore(0x00, 0x09bde339)
revert(0x1c, 0x04)
}
}
}

/// @notice Extracted part from `verifyProof` for hashing two leaves
/// @dev Combines 2 bytes32 in 64 bytes input for sha256 precompile
function sha256Pair(bytes32 left, bytes32 right) internal view returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
// Store `left` at memory position 0x00
mstore(0x00, left)
// Store `right` at memory position 0x20
mstore(0x20, right)

// Call SHA-256 precompile (0x02) with 64-byte input at memory 0x00
let success := staticcall(gas(), 0x02, 0x00, 0x40, 0x00, 0x20)
if iszero(success) {
revert(0, 0)
}

// Load the resulting hash from memory
result := mload(0x00)
}
}

/// @notice Extracted and modified part from `hashTreeRoot` for hashing validator pubkey from calldata
/// @dev Reverts if `pubkey` length is not 48
function pubkeyRoot(bytes calldata pubkey) internal view returns (bytes32 _pubkeyRoot) {
if (pubkey.length != 48) revert InvalidPubkeyLength();

/// @solidity memory-safe-assembly
assembly {
// Copy 48 bytes of `pubkey` to memory at 0x00
calldatacopy(0x00, pubkey.offset, 48)

// Zero the remaining 16 bytes to form a 64-byte input block
mstore(0x30, 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ it corrupts the free memory slot

Copy link
Contributor

Choose a reason for hiding this comment

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

image

Copy link
Contributor

Choose a reason for hiding this comment

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

fixed


// Call the SHA-256 precompile (0x02) with the 64-byte input
if iszero(staticcall(gas(), 0x02, 0x00, 0x40, 0x00, 0x20)) {
revert(0, 0)
}

// Load the resulting SHA-256 hash
_pubkeyRoot := mload(0x00)
}
}
}
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,25 +411,43 @@
_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`
Copy link
Contributor

Choose a reason for hiding this comment

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

recipient can't be a vault

*/
function withdrawDisputedValidator(bytes calldata _pubkey, address _recipient) external {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why is it called "disputed"? It can either be proven of disproven?

Copy link
Contributor

Choose a reason for hiding this comment

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

Disproven ok

_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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
function refundDisputedValidatorToVault(bytes calldata _pubkey) external {
function refundDisprovenValidatorToVault(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");

if (_token == ETH) {
(bool success, ) = payable(_recipient).call{value: _amount}("");
if (!success) revert EthTransferFailed(_recipient, _amount);
} else {
SafeERC20.safeTransfer(IERC20(_token), _recipient, _amount);
}

emit ERC20Recovered(_recipient, _token, _amount);
}

Check failure

Code scanning / Slither

Functions that send Ether to arbitrary destinations High


/**
* @notice Transfers a given token_id of an ERC721-compatible NFT (defined by the token contract address)
Expand All @@ -439,7 +457,11 @@
* @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
Loading
Loading