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

feat: Vaults #874

Draft
wants to merge 429 commits into
base: develop
Choose a base branch
from
Draft

feat: Vaults #874

wants to merge 429 commits into from

Conversation

tamtamchik
Copy link
Member

@tamtamchik tamtamchik commented Nov 17, 2024

❗ 🚧 Under construction 🚧 ❗

Staking Vaults

New way of isolated staking, through separate vaults, with optional stETH liquidity

  • Support minting external backed stETH in Lido contract
  • Extract protocol accounting to separate contract (two-step, better precision)
  • StakingVault + VaultHub
  • Dashboard and Delegation for Vaults
  • VaultFactory

tamtamchik and others added 30 commits October 23, 2024 16:25
tests: restore and improve unit tests
test: integration tests on devnet 0
chore: add soft limits for external balance
Remove simulatedShareRate from report
chore: cleanup postTokenRebaseReceiver
Copy link
Contributor

@TheDZhon TheDZhon left a comment

Choose a reason for hiding this comment

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

🚛

return address(VAULT_HUB);
}

receive() external payable {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Here we have an attack scenario

This func accepts ether without updating valuation (and inOutDelta) and it's not possible to withdraw it back.
The more important thing is that while calling rebalance() it gets used to reduce inOutDelta allowing to underflow valuation()

$.report.inOutDelta = SafeCast.toInt128(_inOutDelta);
$.locked = SafeCast.toUint128(_locked);

try IReportReceiver(owner()).onReport(_valuation, _inOutDelta, _locked) {} catch (bytes memory reason) {
Copy link
Contributor

Choose a reason for hiding this comment

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

it has another purpose to allow non-revert 'soft' failure
⚠️ it has a different bug though: OOG should be handled differently

EOA check is needed also

$.locked = SafeCast.toUint128(_locked);

try IReportReceiver(owner()).onReport(_valuation, _inOutDelta, _locked) {} catch (bytes memory reason) {
emit OnReportFailed(address(this), reason);
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe rename this event


event Funded(address indexed sender, uint256 amount);
event Withdrawn(address indexed sender, address indexed recipient, uint256 amount);
event DepositedToBeaconChain(address indexed sender, uint256 deposits, uint256 amount);
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe should target pectra (array of amounts)

Comment on lines +357 to +359
event Locked(uint256 locked);
event Reported(address indexed vault, uint256 valuation, int256 inOutDelta, uint256 locked);
event OnReportFailed(address vault, bytes reason);
Copy link
Contributor

Choose a reason for hiding this comment

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

here have three different approaches:

  • do not include the emitter contract (and it's fine indeed)
  • include the address but indexed
  • include non-indexed addr

let's use the first approach 🧠

/**
* @dev Modifier to fund the staking vault if msg.value > 0
*/
modifier fundAndProceed() {
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
modifier fundAndProceed() {
modifier prefundable() {

* @dev Transfers ownership of the staking vault to a new owner
* @param _newOwner Address of the new owner
*/
function _transferStVaultOwnership(address _newOwner) internal {
Copy link
Contributor

Choose a reason for hiding this comment

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

see the rename proposal above

* @dev Requests the exit of a validator from the staking vault
* @param _validatorPublicKey Public key of the validator to exit
*/
function _requestValidatorExit(bytes calldata _validatorPublicKey) internal {
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe plural for keys


// ==================== Errors ====================

/// @notice Error for zero address arguments
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
/// @notice Error for zero address arguments
/// @notice Error for zero value arguments

/// @notice Error when the withdrawable amount is insufficient.
/// @param withdrawable The amount that is withdrawable
/// @param requested The amount requested to withdraw
error InsufficientWithdrawableAmount(uint256 withdrawable, uint256 requested);
Copy link
Contributor

Choose a reason for hiding this comment

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

should have a view func to show withdrawable amount
for rebalance we also need a view to show what would happen

Copy link
Contributor

Choose a reason for hiding this comment

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

this error should be moved to Delegation not being used here at all (it seems)

Copy link
Contributor

@TheDZhon TheDZhon left a comment

Choose a reason for hiding this comment

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

🚛 🚛 🚛

// ==================== Constants ====================

uint256 private constant BP_BASE = 10000; // Basis points base (100%)
uint256 private constant MAX_FEE = BP_BASE; // Maximum fee in basis points (100%)
Copy link
Contributor

Choose a reason for hiding this comment

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

TOTAL_BASIS_POINTS

Comment on lines +43 to +44
* - vote on ownership transfer
* - vote on performance fee changes
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe veto instead of approval (ET dynamics)
later

* @notice Role for the Lido DAO.
* This can be the Lido DAO agent, EasyTrack or any other DAO decision-making system.
* Lido DAO can:
* - set the operator role
Copy link
Contributor

Choose a reason for hiding this comment

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

not sure

* Lido DAO can:
* - set the operator role
* - vote on ownership transfer
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

what about VaultHub.connect?

Copy link
Contributor

Choose a reason for hiding this comment

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

another issue: vault should express a WILL to connect (two-step)

Copy link
Contributor

Choose a reason for hiding this comment

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

🧠 🧠 🧠 LDO staking

Copy link
Contributor

Choose a reason for hiding this comment

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

these comments might be not relevant to Delegation

* Key master can:
* - deposit validators to the beacon chain
*/
bytes32 public constant KEY_MASTER_ROLE = keccak256("Vault.Delegation.KeyMasterRole");
Copy link
Contributor

Choose a reason for hiding this comment

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

BEACON_CHAIN_DEPOSITOR_ROLE?

function onReport(uint256 _valuation, int256 _inOutDelta, uint256 _locked) external {
if (msg.sender != address(stakingVault)) revert OnlyStVaultCanCallOnReportHook();

managementDue += (_valuation * managementFee) / 365 / BP_BASE;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ there is no timeElapsed (in case of missing reports e.g.)

Copy link
Contributor

Choose a reason for hiding this comment

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

and 365.25

Comment on lines +387 to +388
int256 unlocked = int256(stakingVault.valuation()) - int256(stakingVault.locked());
uint256 unreserved = unlocked >= 0 ? uint256(unlocked) : 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

can query stakingVault.unlocked() instead

modifier onlyIfVotedBy(bytes32[] memory _committee, uint256 _votingPeriod) {
bytes32 callId = keccak256(msg.data);
uint256 committeeSize = _committee.length;
uint256 votingStart = block.timestamp - _votingPeriod;
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
uint256 votingStart = block.timestamp - _votingPeriod;
uint256 votesVaildSince = block.timestamp - _votingPeriod;

// ==================== Events ====================

/// @notice Emitted when a role member votes on a function requiring committee approval.
event RoleMemberVoted(address member, bytes32 role, uint256 timestamp, bytes data);
Copy link
Contributor

Choose a reason for hiding this comment

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

indexed fields?

/// @notice Emitted when a role member votes on a function requiring committee approval.
event RoleMemberVoted(address member, bytes32 role, uint256 timestamp, bytes data);

// ==================== Errors ====================
Copy link
Contributor

Choose a reason for hiding this comment

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

fees not covered with event changes

@tamtamchik tamtamchik added solidity issues/tasks related to smart contract code next-upgrade Things to pickup for the next protocol upgrade vaults labels Dec 12, 2024
Copy link
Contributor

@TheDZhon TheDZhon left a comment

Choose a reason for hiding this comment

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

First lap finished 👍


import {IStakingVault} from "./interfaces/IStakingVault.sol";

pragma solidity 0.8.25;
Copy link
Contributor

Choose a reason for hiding this comment

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

🚓 pragma should go before the imports

Comment on lines +14 to +15
uint256 managementFee;
uint256 performanceFee;
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
uint256 managementFee;
uint256 performanceFee;
uint256 managementFeeBP;
uint256 performanceFeeBP;


pragma solidity 0.8.25;

interface IDelegation {
Copy link
Contributor

Choose a reason for hiding this comment

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

why we don't import this interface though?

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 add the comment that it's intended (to hook up the particular interface strictly)

}

contract VaultFactory is UpgradeableBeacon {
address public immutable delegationImpl;
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
address public immutable delegationImpl;
address public immutable DELEGATION_IMPL;

Copy link
Contributor

Choose a reason for hiding this comment

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

Why it's immutable?

Copy link
Contributor

Choose a reason for hiding this comment

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

should think about disentangling the beacon and factory (from Azat)

Copy link
Contributor

Choose a reason for hiding this comment

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

@loga4 (the problem is that it's impossible to upgrade the delegation for the factory)

delegation.grantRole(delegation.LIDO_DAO_ROLE(), _lidoAgent);
delegation.grantRole(delegation.MANAGER_ROLE(), _initializationParams.manager);
delegation.grantRole(delegation.OPERATOR_ROLE(), _initializationParams.operator);
delegation.grantRole(delegation.DEFAULT_ADMIN_ROLE(), msg.sender);
Copy link
Contributor

Choose a reason for hiding this comment

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

scary though, let's add a comment that those roles above have their admins

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 not rely on msg.sender and use input parameters instead to make the deployments automated without pain (suggested defaultAdmin above to replace msg.sender)


delegation.initialize(address(this), address(vault));

delegation.grantRole(delegation.LIDO_DAO_ROLE(), _lidoAgent);
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe LIDO_AGENT should be immutable for the factory

/// @notice Creates a new StakingVault and Delegation contracts
/// @param _stakingVaultParams The params of vault initialization
/// @param _initializationParams The params of vault initialization
function createVault(
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 createVault(
function createVaultWithDelegation(

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 extract voting modifier to a library or extension for AC

* @notice The term "fee" is used to express the fee percentage as basis points, e.g. 5%,
* while "due" is the actual amount of the fee, e.g. 1 ether
*/
contract Delegation is Dashboard, IReportReceiver {
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
contract Delegation is Dashboard, IReportReceiver {
contract Delegation is Dashboard, IReportReceiver, AssetRecoverer {


emit VaultCreated(address(delegation), address(vault));
emit DelegationCreated(msg.sender, address(delegation));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧠 role setup has dependencies on the Delegation internals which is not perfect

Comment on lines +254 to +287
function _calculateFeesAndExternalEther(
ReportValues memory _report,
PreReportState memory _pre,
CalculatedValues memory _calculated
) internal pure returns (uint256 sharesToMintAsFees, uint256 postExternalEther) {
// we are calculating the share rate equal to the post-rebase share rate
// but with fees taken as eth deduction
// and without externalBalance taken into account
uint256 shares = _pre.totalShares - _calculated.totalSharesToBurn - _pre.externalShares;
uint256 eth = _pre.totalPooledEther - _calculated.etherToFinalizeWQ - _pre.externalEther;

uint256 unifiedClBalance = _report.clBalance + _calculated.withdrawals;

// Don't mint/distribute any protocol fee on the non-profitable Lido oracle report
// (when consensus layer balance delta is zero or negative).
// See LIP-12 for details:
// https://research.lido.fi/t/lip-12-on-chain-part-of-the-rewards-distribution-after-the-merge/1625
if (unifiedClBalance > _calculated.principalClBalance) {
uint256 totalRewards = unifiedClBalance - _calculated.principalClBalance + _calculated.elRewards;
uint256 totalFee = _calculated.rewardDistribution.totalFee;
uint256 precision = _calculated.rewardDistribution.precisionPoints;
uint256 feeEther = (totalRewards * totalFee) / precision;
eth += totalRewards - feeEther;

// but we won't pay fees in ether, so we need to calculate how many shares we need to mint as fees
sharesToMintAsFees = (feeEther * shares) / eth;
} else {
uint256 clPenalty = _calculated.principalClBalance - unifiedClBalance;
eth = eth - clPenalty + _calculated.elRewards;
}

// externalBalance is rebasing at the same rate as the primary balance does
postExternalEther = (_pre.externalShares * eth) / shares;
}
Comment on lines +7 to +70
interface ILido {
function getSharesByPooledEth(uint256) external view returns (uint256);

function getPooledEthByShares(uint256) external view returns (uint256);

function getPooledEthBySharesRoundUp(uint256) external view returns (uint256);

function transferFrom(address, address, uint256) external;

function transferSharesFrom(address, address, uint256) external returns (uint256);

function rebalanceExternalEtherToInternal() external payable;

function getTotalPooledEther() external view returns (uint256);

function getExternalEther() external view returns (uint256);

function getExternalShares() external view returns (uint256);

function mintExternalShares(address, uint256) external;

function burnExternalShares(uint256) external;

function getMaxMintableExternalShares() external view returns (uint256);

function getTotalShares() external view returns (uint256);

function getBeaconStat()
external
view
returns (uint256 depositedValidators, uint256 beaconValidators, uint256 beaconBalance);

function processClStateUpdate(
uint256 _reportTimestamp,
uint256 _preClValidators,
uint256 _reportClValidators,
uint256 _reportClBalance
) external;

function collectRewardsAndProcessWithdrawals(
uint256 _reportTimestamp,
uint256 _reportClBalance,
uint256 _adjustedPreCLBalance,
uint256 _withdrawalsToWithdraw,
uint256 _elRewardsToWithdraw,
uint256 _lastWithdrawalRequestToFinalize,
uint256 _simulatedShareRate,
uint256 _etherToLockOnWithdrawalQueue
) external;

function emitTokenRebase(
uint256 _reportTimestamp,
uint256 _timeElapsed,
uint256 _preTotalShares,
uint256 _preTotalEther,
uint256 _postTotalShares,
uint256 _postTotalEther,
uint256 _sharesMintedAsFees
) external;

function mintShares(address _recipient, uint256 _sharesAmount) external;

function burnShares(address _account, uint256 _sharesAmount) external;
}

Check warning

Code scanning / Slither

Incorrect erc20 interface Medium

ILido has incorrect ERC20 function interface:ILido.transferFrom(address,address,uint256)
Comment on lines +295 to +298
function _burn(uint256 _amountOfShares) internal {
STETH.transferSharesFrom(msg.sender, address(vaultHub), _amountOfShares);
vaultHub.burnSharesBackedByVault(address(stakingVault), _amountOfShares);
}

Check warning

Code scanning / Slither

Unused return Medium

Comment on lines +292 to +296
function transferAndBurnSharesBackedByVault(address _vault, uint256 _amountOfShares) external {
STETH.transferSharesFrom(msg.sender, address(this), _amountOfShares);

burnSharesBackedByVault(_vault, _amountOfShares);
}

Check warning

Code scanning / Slither

Unused return Medium

Copy link
Contributor

@TheDZhon TheDZhon left a comment

Choose a reason for hiding this comment

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

🥹 👍

contracts/0.4.24/StETH.sol Show resolved Hide resolved
contracts/0.4.24/StETH.sol Show resolved Hide resolved
contracts/0.4.24/StETH.sol Show resolved Hide resolved
contracts/0.4.24/Lido.sol Show resolved Hide resolved
contracts/0.4.24/Lido.sol Show resolved Hide resolved
contracts/0.4.24/Lido.sol Show resolved Hide resolved
contracts/0.4.24/Lido.sol Show resolved Hide resolved
contracts/0.4.24/Lido.sol Show resolved Hide resolved
contracts/0.4.24/Lido.sol Show resolved Hide resolved
contracts/0.4.24/Lido.sol Show resolved Hide resolved
tamtamchik

This comment was marked as off-topic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
next-upgrade Things to pickup for the next protocol upgrade solidity issues/tasks related to smart contract code vaults
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants