-
Notifications
You must be signed in to change notification settings - Fork 193
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
base: feat/vaults
Are you sure you want to change the base?
Changes from all commits
8d6b255
d4250c1
3725cea
8af0dc0
e539ece
1205e6e
4d3b8ea
b3a50f5
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 |
---|---|---|
|
@@ -7,9 +7,26 @@ pragma solidity 0.8.25; | |
import {IStakingVault} from "./interfaces/IStakingVault.sol"; | ||
import {AccessControlEnumerable} from "@openzeppelin/contracts-v5.0.2/access/extensions/AccessControlEnumerable.sol"; | ||
import {IERC20} from "@openzeppelin/contracts-v5.0.2/token/ERC20/IERC20.sol"; | ||
import {IERC20Permit} from "@openzeppelin/contracts-v5.0.2/token/ERC20/extensions/draft-IERC20Permit.sol"; | ||
import {OwnableUpgradeable} from "contracts/openzeppelin/5.0.2/upgradeable/access/OwnableUpgradeable.sol"; | ||
import {VaultHub} from "./VaultHub.sol"; | ||
|
||
/// @notice Interface defining a Lido liquid staking pool | ||
/// @dev see also [Lido liquid staking pool core contract](https://docs.lido.fi/contracts/lido) | ||
interface IStETH is IERC20, IERC20Permit { | ||
function getSharesByPooledEth(uint256 _ethAmount) public view returns (uint256) | ||
} | ||
|
||
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 | ||
* @notice This contract is meant to be used as the owner of `StakingVault`. | ||
|
@@ -27,23 +44,35 @@ contract Dashboard is AccessControlEnumerable { | |
bool public isInitialized; | ||
|
||
/// @notice The stETH token contract | ||
IERC20 public immutable stETH; | ||
IStETH public immutable stETH; | ||
|
||
/// @notice The underlying `StakingVault` contract | ||
IStakingVault public stakingVault; | ||
|
||
/// @notice The `VaultHub` contract | ||
VaultHub public vaultHub; | ||
|
||
/// @notice The wrapped ether token contract | ||
IWeth public weth; | ||
|
||
/// @notice The wrapped staked ether token contract | ||
IWstETH public wstETH; | ||
|
||
/** | ||
* @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 = IERC20(_stETH); | ||
weth = IWeth(_weth); | ||
wstETH = IWstETH(_wstETH); | ||
} | ||
|
||
/** | ||
|
@@ -126,6 +155,49 @@ contract Dashboard is AccessControlEnumerable { | |
return vaultSocket().treasuryFeeBP; | ||
} | ||
|
||
/** | ||
* @notice Returns the maximum number of stETH shares that can be minted on the vault. | ||
* @return The maximum number of stETH shares as a uint256. | ||
*/ | ||
function maxMintableShares() external view returns (uint256) { | ||
return vaultHub._maxMintableShares(address(stakingVault), vaultSocket().reserveRatio); | ||
} | ||
|
||
/** | ||
* @notice Returns the maximum number of stETH shares that can be minted. | ||
* @return The maximum number of stETH shares that can be minted. | ||
*/ | ||
function canMint() external view returns (uint256) { | ||
|
||
uint256 maxMintableShares = maxMintableShares(); | ||
uint256 sharesMinted = vaultSocket().sharesMinted; | ||
|
||
return maxMintableShares - sharesMinted; | ||
} | ||
|
||
/** | ||
* @notice Returns the maximum number of stETH that can be minted for deposited ether. | ||
* @param _ether The amount of ether to check. | ||
* @return the maximum number of stETH that can be minted by ether | ||
*/ | ||
function canMintByEther(uint256 _ether) external view returns (uint256) { | ||
if (_ether == 0) return 0; | ||
|
||
uint256 maxMintableShares = maxMintableShares(); | ||
uint256 sharesMinted = vaultSocket().sharesMinted; | ||
uint256 sharesToMint = stETH.getSharesByPooledEth(_ether); | ||
|
||
return sharesMinted + sharesToMint > maxMintableShares ? maxMintableShares - sharesMinted : sharesToMint; | ||
} | ||
|
||
/** | ||
* @notice Returns the amount of ether that can be withdrawn from the staking vault. | ||
* @return The amount of ether that can be withdrawn. | ||
*/ | ||
function canWithdraw() external view returns (uint256) { | ||
return Math256.min(address(stakingVault).balance, stakingVault.unlocked()); | ||
} | ||
|
||
// ==================== Vault Management Functions ==================== | ||
|
||
/** | ||
|
@@ -150,6 +222,16 @@ contract Dashboard is AccessControlEnumerable { | |
_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) { | ||
weth.transferFrom(msg.sender, address(this), _wethAmount); | ||
weth.withdraw(_wethAmount); | ||
_fund{value: _wethAmount}(); | ||
} | ||
|
||
/** | ||
* @notice Withdraws ether from the staking vault to a recipient | ||
* @param _recipient Address of the recipient | ||
|
@@ -159,6 +241,17 @@ contract Dashboard is AccessControlEnumerable { | |
_withdraw(_recipient, _ether); | ||
} | ||
|
||
/** | ||
* @notice Withdraws stETH tokens from the staking vault to wrapped ether. Approvals for the passed amounts should be done before. | ||
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. Sorry, didn't get what it should do. RN, ether is still on the dashboard contract. Should it be transferred to msg.sender or some other recepient? 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. @folkyatina The idea is to send to the Now I understand that it seems that this code will not work correctly 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. Updated |
||
* @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) { | ||
_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 | ||
|
@@ -194,13 +287,62 @@ contract Dashboard is AccessControlEnumerable { | |
} | ||
|
||
/** | ||
* @notice Burns stETH tokens from the sender backed by the vault | ||
* @param _tokens Amount of tokens to burn | ||
* @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 tokens from the sender backed by the vault. Approvals for the passed amounts should be done before. | ||
* @param _tokens Amount of stETH tokens to burn | ||
*/ | ||
function burn(uint256 _tokens) external virtual onlyRole(DEFAULT_ADMIN_ROLE) { | ||
_burn(_tokens); | ||
} | ||
|
||
/** | ||
* @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); | ||
stETH.approve(address(wstETH), _tokens); | ||
|
||
uint256 stETHAmount = wstETH.unwrap(_tokens); | ||
_burn(stETHAmount); | ||
} | ||
|
||
/** | ||
* @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) { | ||
DiRaiks marked this conversation as resolved.
Show resolved
Hide resolved
|
||
stETH.permit(msg.sender, address(this), _permit.value, _permit.deadline, _permit.v, _permit.r, _permit.s); | ||
_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 _wstETHPermit data required for the wstETH.permit() method to set the allowance | ||
*/ | ||
function burnWstETHWithPermit(uint256 _tokens, PermitInput calldata _wstETHPermit) external virtual onlyRole(DEFAULT_ADMIN_ROLE) { | ||
wstETH.permit(msg.sender, address(this), _wstETHPermit.value, _wstETHPermit.deadline, _wstETHPermit.v, _wstETHPermit.r, _wstETHPermit.s); | ||
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. Needs check for allowance to prevent ddos. See updated WQ contract. |
||
|
||
wstETH.transferFrom(msg.sender, address(this), _tokens); | ||
stETH.approve(address(wstETH), _tokens); | ||
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. You don't need approve for unwrap. |
||
uint256 stETHAmount = wstETH.unwrap(_tokens); | ||
_burn(stETHAmount); | ||
} | ||
|
||
/** | ||
* @notice Rebalances the vault by transferring ether | ||
* @param _ether Amount of ether to rebalance | ||
|
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.
Missing
transferFrom
?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.
added