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

[Draft] Vaults UX suggestions #893

Open
wants to merge 57 commits into
base: feat-immutable-operator-in-vault
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
8d6b255
feat: suggestions for additional methods to improve the UX of interac…
DiRaiks Dec 6, 2024
d4250c1
fix: weth call withdraw
DiRaiks Dec 6, 2024
3725cea
feat: add burn for wstETH (with permit)
DiRaiks Dec 6, 2024
8af0dc0
Update contracts/0.8.25/vaults/Dashboard.sol
DiRaiks Dec 6, 2024
e539ece
Update contracts/0.8.25/vaults/Dashboard.sol
DiRaiks Dec 6, 2024
1205e6e
feat: update interfaces, update methods for work with weth/wsteth
DiRaiks Dec 9, 2024
4d3b8ea
Merge branch 'feat/vaults-suggestions' of github.com:lidofinance/core…
DiRaiks Dec 9, 2024
b3a50f5
feat: delete vaultsByOwner from VaultHub
DiRaiks Dec 9, 2024
76ec271
featL add permit modifier, fix errors, update Delegation constructor
DiRaiks Dec 16, 2024
a4febe8
Merge branch 'feat-immutable-operator-in-vault' of github.com:lidofin…
DiRaiks Dec 16, 2024
31d419b
feat: update dashboard consts and methods, add tests
DiRaiks Dec 17, 2024
d4b170c
Merge branch 'feat-immutable-operator-in-vault' into feat/vaults-sugg…
tamtamchik Dec 17, 2024
0099af6
fix: dashboard naming & tests
Jeday Dec 18, 2024
7045ec3
test: add tests for mintWstETH, burnWstETH
DiRaiks Dec 18, 2024
705bb31
fix: burnWstETHWithPermit method
DiRaiks Dec 18, 2024
5ba2431
Merge branch 'feat/vaults-suggestions' of github.com:lidofinance/core…
DiRaiks Dec 18, 2024
f3b4ed9
fix: canMint lower bound
Jeday Dec 18, 2024
aaf6ee6
Merge branch 'feat/vaults-suggestions' of github.com:lidofinance/core…
Jeday Dec 18, 2024
7c8eb29
test: canMint bound by shareLimit
Jeday Dec 18, 2024
8e0e547
tests: fix burnWstETH
DiRaiks Dec 18, 2024
1eb5e62
fix: dashboard naming
Jeday Dec 18, 2024
776f3c5
Merge branch 'feat/vaults-suggestions' of github.com:lidofinance/core…
Jeday Dec 18, 2024
8669b43
fix: merge canMintShares
Jeday Dec 18, 2024
77fccb4
chore: fix pragma for test contracts
tamtamchik Dec 18, 2024
d1a3e7e
chore: fix scratch deploy
tamtamchik Dec 18, 2024
b8028c7
test(integration): stabilize vaults happy path
tamtamchik Dec 18, 2024
b4e1e35
test: fix tests
tamtamchik Dec 18, 2024
17669d6
test(integration): skip negative rebase tests for now
tamtamchik Dec 18, 2024
c89ac39
test: can withdraw test
Jeday Dec 19, 2024
6616579
chore: extract some vaults helpers to library
tamtamchik Dec 19, 2024
04100c0
chore: update StETH harness contract for dashboard tests
tamtamchik Dec 19, 2024
65ec051
tests: start burn permit tests
DiRaiks Dec 19, 2024
6065f91
chore: simplify constructor
tamtamchik Dec 19, 2024
00223f4
fix: constructor
tamtamchik Dec 19, 2024
7d6da64
tests: fix steth events
DiRaiks Dec 19, 2024
7b58f62
Merge branch 'feat/steth-permit' of github.com:lidofinance/core into …
DiRaiks Dec 19, 2024
85baf12
Merge pull request #903 from lidofinance/feat/steth-permit
tamtamchik Dec 19, 2024
dd9157e
Merge branch 'feat-immutable-operator-in-vault' into feat/vaults-sugg…
tamtamchik Dec 19, 2024
856e6ef
fix: comments and tests
tamtamchik Dec 19, 2024
14e6273
fix: integration tests
tamtamchik Dec 19, 2024
c56493e
Merge branch 'feat/vaults-suggestions' of github.com:lidofinance/core…
DiRaiks Dec 19, 2024
3d5fbb8
tests: add tests for burnWstETHWithPermit and burnWithPermit, fix bur…
DiRaiks Dec 19, 2024
dcc5203
Merge pull request #902 from lidofinance/feat/vaults-actions
Jeday Dec 20, 2024
e9e105d
fix: dashboard naming
Jeday Dec 20, 2024
02c78b9
Merge branch 'feat-immutable-operator-in-vault' of github.com:lidofin…
Jeday Dec 20, 2024
592f06f
fix: add ERC20 token to lido interface
Jeday Dec 20, 2024
fe033da
test: fix vault hub mock
Jeday Dec 20, 2024
777d6ce
fix: interfaces&imports
Jeday Dec 20, 2024
13a0464
fix: ILido
Jeday Dec 20, 2024
14fe2d8
test(integration): fix and update vaults happy path
tamtamchik Dec 20, 2024
0739501
Merge branch 'feat-immutable-operator-in-vault' into feat/vaults-sugg…
tamtamchik Dec 20, 2024
49ba3fd
Merge branch 'feat-immutable-operator-in-vault' into feat/vaults-sugg…
tamtamchik Dec 20, 2024
972b84c
fix: delegation tests
tamtamchik Dec 20, 2024
6d4bd11
Merge branch 'feat-immutable-operator-in-vault' into feat/vaults-sugg…
tamtamchik Dec 20, 2024
c808b26
chore: updates after review
tamtamchik Dec 20, 2024
10897a0
fix: contract compilation
tamtamchik Dec 20, 2024
e2c380f
chore: restore some formating
tamtamchik Dec 20, 2024
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
7 changes: 4 additions & 3 deletions contracts/0.8.25/interfaces/ILido.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
// See contracts/COMPILERS.md
pragma solidity 0.8.25;

interface ILido {
import {IERC20} from "@openzeppelin/contracts-v5.0.2/token/ERC20/IERC20.sol";
import {IERC20Permit} from "@openzeppelin/contracts-v5.0.2/token/ERC20/extensions/IERC20Permit.sol";

interface ILido is IERC20, IERC20Permit {
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;
Expand Down
239 changes: 233 additions & 6 deletions contracts/0.8.25/vaults/Dashboard.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,29 @@
// See contracts/COMPILERS.md
pragma solidity 0.8.25;

import {IStakingVault} from "./interfaces/IStakingVault.sol";
import {AccessControlEnumerable} from "@openzeppelin/contracts-v5.0.2/access/extensions/AccessControlEnumerable.sol";
import {OwnableUpgradeable} from "contracts/openzeppelin/5.0.2/upgradeable/access/OwnableUpgradeable.sol";
import {IERC20} from "@openzeppelin/contracts-v5.0.2/token/ERC20/IERC20.sol";
import {IERC20Permit} from "@openzeppelin/contracts-v5.0.2/token/ERC20/extensions/IERC20Permit.sol";

import {Math256} from "contracts/common/lib/Math256.sol";

import {VaultHub} from "./VaultHub.sol";
import {ILido as StETH} from "../interfaces/ILido.sol";

import {IStakingVault} from "./interfaces/IStakingVault.sol";
import {ILido as IStETH} from "../interfaces/ILido.sol";

interface IWeth is IERC20 {
function withdraw(uint) external;

function deposit() external payable;
}

interface IWstETH is IERC20, IERC20Permit {
function wrap(uint256) external returns (uint256);

function unwrap(uint256) external returns (uint256);
}

/**
* @title Dashboard
Expand All @@ -23,28 +41,50 @@
/// @notice Address of the implementation contract
/// @dev Used to prevent initialization in the implementation
address private immutable _SELF;
/// @dev basis points base
uint256 private constant TOTAL_BASIS_POINTS = 100_00;

/// @notice Indicates whether the contract has been initialized
bool public isInitialized;

/// @notice The stETH token contract
StETH public immutable STETH;
IStETH public immutable STETH;

/// @notice The wrapped staked ether token contract
IWstETH public immutable WSTETH;

/// @notice The wrapped ether token contract
IWeth public immutable WETH;

/// @notice The underlying `StakingVault` contract
IStakingVault public stakingVault;

/// @notice The `VaultHub` contract
VaultHub public vaultHub;

struct PermitInput {
uint256 value;
uint256 deadline;
uint8 v;
bytes32 r;
bytes32 s;
}

/**
* @notice Constructor sets the stETH token address and the implementation contract address.
* @param _stETH Address of the stETH token contract.
* @param _weth Address of the weth token contract.
* @param _wstETH Address of the wstETH token contract.
*/
constructor(address _stETH) {
constructor(address _stETH, address _weth, address _wstETH) {
if (_stETH == address(0)) revert ZeroArgument("_stETH");
if (_weth == address(0)) revert ZeroArgument("_WETH");
if (_wstETH == address(0)) revert ZeroArgument("_wstETH");

_SELF = address(this);
STETH = StETH(_stETH);
STETH = IStETH(_stETH);
WETH = IWeth(_weth);
WSTETH = IWstETH(_wstETH);
}

/**
Expand Down Expand Up @@ -102,7 +142,7 @@
* @notice Returns the reserve ratio of the vault
* @return The reserve ratio as a uint16
*/
function reserveRatio() external view returns (uint16) {
function reserveRatio() public view returns (uint16) {
return vaultSocket().reserveRatioBP;
}

Expand All @@ -122,8 +162,55 @@
return vaultSocket().treasuryFeeBP;
}

/**
* @notice Returns the valuation of the vault in ether.
* @return The valuation as a uint256.
*/
function valuation() external view returns (uint256) {
return stakingVault.valuation();
}

/**
* @notice Returns the total of shares that can be minted on the vault bound by valuation and vault share limit.
* @return The maximum number of stETH shares as a uint256.
*/
function totalMintableShares() public view returns (uint256) {
return _totalMintableShares(stakingVault.valuation());
}

/**
* @notice Returns the maximum number of shares that can be minted with deposited ether.
* @param _ether the amount of ether to be funded, can be zero
* @return the maximum number of shares that can be minted by ether
*/
function getMintableShares(uint256 _ether) external view returns (uint256) {
uint256 _totalShares = _totalMintableShares(stakingVault.valuation() + _ether);
uint256 _sharesMinted = vaultSocket().sharesMinted;

if (_totalShares < _sharesMinted) return 0;
return _totalShares - _sharesMinted;
}

/**
* @notice Returns the amount of ether that can be withdrawn from the staking vault.
* @return The amount of ether that can be withdrawn.
*/
function getWithdrawableEther() external view returns (uint256) {
return Math256.min(address(stakingVault).balance, stakingVault.unlocked());
}

// TODO: add preview view methods for minting and burning

// ==================== Vault Management Functions ====================

/**
* @dev Receive function to accept ether
*/
// TODO: Consider the amount of ether on balance of the contract
receive() external payable {
if (msg.value == 0) revert ZeroArgument("msg.value");
}

/**
* @notice Transfers ownership of the staking vault to a new owner.
* @param _newOwner Address of the new owner.
Expand All @@ -146,6 +233,20 @@
_fund();
}

/**
* @notice Funds the staking vault with wrapped ether. Approvals for the passed amounts should be done before.
* @param _wethAmount Amount of wrapped ether to fund the staking vault with
*/
function fundByWeth(uint256 _wethAmount) external virtual onlyRole(DEFAULT_ADMIN_ROLE) {
DiRaiks marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
function fundByWeth(uint256 _wethAmount) external virtual onlyRole(DEFAULT_ADMIN_ROLE) {
function fundWeth(uint256 _wethAmount) external virtual onlyRole(DEFAULT_ADMIN_ROLE) {

if (WETH.allowance(msg.sender, address(this)) < _wethAmount) revert("ERC20: transfer amount exceeds allowance");

WETH.transferFrom(msg.sender, address(this), _wethAmount);
WETH.withdraw(_wethAmount);

// TODO: find way to use _fund() instead of stakingVault directly
stakingVault.fund{value: _wethAmount}();
}
Fixed Show fixed Hide fixed

/**
* @notice Withdraws ether from the staking vault to a recipient
* @param _recipient Address of the recipient
Expand All @@ -155,6 +256,17 @@
_withdraw(_recipient, _ether);
}

/**
* @notice Withdraws stETH tokens from the staking vault to wrapped ether.
* @param _recipient Address of the recipient
* @param _ether Amount of ether to withdraw
*/
function withdrawToWeth(address _recipient, uint256 _ether) external virtual onlyRole(DEFAULT_ADMIN_ROLE) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
function withdrawToWeth(address _recipient, uint256 _ether) external virtual onlyRole(DEFAULT_ADMIN_ROLE) {
function withdrawWeth(address _recipient, uint256 _ether) external virtual onlyRole(DEFAULT_ADMIN_ROLE) {

_withdraw(address(this), _ether);
WETH.deposit{value: _ether}();
WETH.transfer(_recipient, _ether);
}

/**
* @notice Requests the exit of a validator from the staking vault
* @param _validatorPublicKey Public key of the validator to exit
Expand All @@ -175,6 +287,22 @@
_mint(_recipient, _amountOfShares);
}

/**
* @notice Mints wstETH tokens backed by the vault to a recipient. Approvals for the passed amounts should be done before.
* @param _recipient Address of the recipient
* @param _tokens Amount of tokens to mint
*/
function mintWstETH(
address _recipient,
uint256 _tokens
) external payable virtual onlyRole(DEFAULT_ADMIN_ROLE) fundAndProceed {
_mint(address(this), _tokens);

STETH.approve(address(WSTETH), _tokens);
uint256 wstETHAmount = WSTETH.wrap(_tokens);
WSTETH.transfer(_recipient, wstETHAmount);
}

/**
* @notice Burns stETH shares from the sender backed by the vault
* @param _amountOfShares Amount of shares to burn
Expand All @@ -183,6 +311,96 @@
_burn(_amountOfShares);
}

/**
* @notice Burns wstETH tokens from the sender backed by the vault. Approvals for the passed amounts should be done before.
* @param _tokens Amount of wstETH tokens to burn
*/
function burnWstETH(uint256 _tokens) external virtual onlyRole(DEFAULT_ADMIN_ROLE) {
WSTETH.transferFrom(msg.sender, address(this), _tokens);

uint256 stETHAmount = WSTETH.unwrap(_tokens);

STETH.transfer(address(vaultHub), stETHAmount);

uint256 sharesAmount = STETH.getSharesByPooledEth(stETHAmount);

vaultHub.burnSharesBackedByVault(address(stakingVault), sharesAmount);
}
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed

/**
* @dev Modifier to check if the permit is successful, and if not, check if the allowance is sufficient
*/
modifier trustlessPermit(
address token,
address owner,
address spender,
PermitInput calldata permitInput
) {
// Try permit() before allowance check to advance nonce if possible
try
IERC20Permit(token).permit(
owner,
spender,
permitInput.value,
permitInput.deadline,
permitInput.v,
permitInput.r,
permitInput.s
)
{
_;
return;
} catch {
// Permit potentially got frontran. Continue anyways if allowance is sufficient.
if (IERC20(token).allowance(owner, spender) >= permitInput.value) {
_;
return;
}
}
revert("Permit failure");
}

/**
* @notice Burns stETH tokens from the sender backed by the vault using EIP-2612 Permit.
* @param _tokens Amount of stETH tokens to burn
* @param _permit data required for the stETH.permit() method to set the allowance
*/
function burnWithPermit(
uint256 _tokens,
PermitInput calldata _permit
)
external
virtual
onlyRole(DEFAULT_ADMIN_ROLE)
trustlessPermit(address(STETH), msg.sender, address(this), _permit)
{
_burn(_tokens);
}

/**
* @notice Burns wstETH tokens from the sender backed by the vault using EIP-2612 Permit.
* @param _tokens Amount of wstETH tokens to burn
* @param _permit data required for the wstETH.permit() method to set the allowance
*/
function burnWstETHWithPermit(
uint256 _tokens,
PermitInput calldata _permit
)
external
virtual
onlyRole(DEFAULT_ADMIN_ROLE)
trustlessPermit(address(WSTETH), msg.sender, address(this), _permit)
{
WSTETH.transferFrom(msg.sender, address(this), _tokens);
uint256 stETHAmount = WSTETH.unwrap(_tokens);

STETH.transfer(address(vaultHub), stETHAmount);

uint256 sharesAmount = STETH.getSharesByPooledEth(stETHAmount);

vaultHub.burnSharesBackedByVault(address(stakingVault), sharesAmount);
}

/**
* @notice Rebalances the vault by transferring ether
* @param _ether Amount of ether to rebalance
Expand Down Expand Up @@ -279,6 +497,15 @@
vaultHub.burnSharesBackedByVault(address(stakingVault), _amountOfShares);
}

/**
* @dev calculates total shares vault can mint
* @param _valuation custom vault valuation
*/
function _totalMintableShares(uint256 _valuation) internal view returns (uint256) {
Jeday marked this conversation as resolved.
Show resolved Hide resolved
uint256 maxMintableStETH = (_valuation * (TOTAL_BASIS_POINTS - vaultSocket().reserveRatioBP)) / TOTAL_BASIS_POINTS;
return Math256.min(STETH.getSharesByPooledEth(maxMintableStETH), vaultSocket().shareLimit);
}

/**
* @dev Rebalances the vault by transferring ether
* @param _ether Amount of ether to rebalance
Expand Down
4 changes: 3 additions & 1 deletion contracts/0.8.25/vaults/Delegation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,10 @@ contract Delegation is Dashboard {
/**
* @notice Constructor sets the stETH token address.
* @param _stETH Address of the stETH token contract.
* @param _weth Address of the weth token contract.
* @param _wstETH Address of the wstETH token contract.
*/
constructor(address _stETH) Dashboard(_stETH) {}
constructor(address _stETH, address _weth, address _wstETH) Dashboard(_stETH, _weth, _wstETH) {}

/**
* @notice Initializes the contract with the default admin and `StakingVault` address.
Expand Down
1 change: 1 addition & 0 deletions contracts/0.8.25/vaults/StakingVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ contract StakingVault is IStakingVault, IBeaconProxy, BeaconChainDepositLogistic
* @notice Initializes `StakingVault` with an owner, operator, and optional parameters
* @param _owner Address that will own the vault
* @param _operator Address of the node operator
* @param - Additional initialization parameters
*/
function initialize(address _owner, address _operator, bytes calldata /* _params */ ) external onlyBeacon initializer {
__Ownable_init(_owner);
Expand Down
Loading
Loading