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 46 commits into
base: feat/vaults
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
46 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
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
42 changes: 18 additions & 24 deletions contracts/0.8.25/Accounting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,10 @@ 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;
constructor(ILidoLocator _lidoLocator, ILido _lido) VaultHub(_lidoLocator, _lido) {
LIDO = _lido;
}

Expand Down Expand Up @@ -217,22 +211,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 @@ -305,12 +304,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]>
Jeday marked this conversation as resolved.
Show resolved Hide resolved
// 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))
// Call sha256 precompile.
let result := staticcall(
gas(),
0x02,
0x00,
0x40,
0x00,
0x20
)

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)
Copy link
Contributor

Choose a reason for hiding this comment

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

let's revisit this later: 'The zero slot is used as initial value for dynamic memory arrays and should never be written to (the free memory pointer points to 0x80 initially)'

https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html

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 okay to use 64 bytes of scratch space to store precomile input, it will be overwritten by staticall.

// 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)
}
}
}
Loading
Loading