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

Draft
wants to merge 8 commits into
base: feat/vaults
Choose a base branch
from
129 changes: 126 additions & 3 deletions contracts/0.8.25/vaults/Dashboard.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ import {IERC20} from "@openzeppelin/contracts-v5.0.2/token/ERC20/IERC20.sol";
import {OwnableUpgradeable} from "contracts/openzeppelin/5.0.2/upgradeable/access/OwnableUpgradeable.sol";
import {VaultHub} from "./VaultHub.sol";

interface IWeth {
function withdraw(uint) external;
function deposit() external payable;
}

interface IWstETH {
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`.
Expand All @@ -35,15 +45,27 @@ contract Dashboard is AccessControlEnumerable {
/// @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);
}

/**
Expand Down Expand Up @@ -126,6 +148,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 address(stakingVault).balance - stakingVault.locked();
DiRaiks marked this conversation as resolved.
Show resolved Hide resolved
}

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

/**
Expand All @@ -150,6 +215,15 @@ 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) {
Copy link
Member

Choose a reason for hiding this comment

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

Missing transferFrom?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added

IWeth(weth).withdraw(_wethAmount);
_fund();
}

/**
* @notice Withdraws ether from the staking vault to a recipient
* @param _recipient Address of the recipient
Expand All @@ -159,6 +233,15 @@ 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.
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@folkyatina The idea is to send to the fundByWeth method the amount of WETH that needs to be "unwrap" to the ETH and then fund Vault
WETH withdraw method https://github.com/gnosis/canonical-weth/blob/master/contracts/WETH9.sol#L38

Now I understand that it seems that this code will not work correctly

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated

* @param _tokens Amount of tokens to withdraw
*/
function withdrawToWeth(uint256 _tokens) external virtual onlyRole(DEFAULT_ADMIN_ROLE) {
_withdraw(address(weth), _tokens);
DiRaiks marked this conversation as resolved.
Show resolved Hide resolved
IWeth(weth).deposit{value: _tokens}();
}

/**
* @notice Requests the exit of a validator from the staking vault
* @param _validatorPublicKey Public key of the validator to exit
Expand Down Expand Up @@ -194,13 +277,53 @@ 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(_recipient, _tokens);
IWstETH(wstETH).wrap(_tokens);
}

/**
* @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) {
IWstETH(wstETH).unwrap(_tokens);
_burn(_tokens);
}

/**
* @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 _permit data required for the stETH.permit() method to set the allowance
*/
function burnWstETHWithPermit(uint256 _tokens, PermitInput calldata _permit) external virtual onlyRole(DEFAULT_ADMIN_ROLE) {
stETH.permit(msg.sender, address(this), _permit.value, _permit.deadline, _permit.v, _permit.r, _permit.s);
IWstETH(wstETH).unwrap(_tokens);
_burn(_tokens);
}

/**
* @notice Rebalances the vault by transferring ether
* @param _ether Amount of ether to rebalance
Expand Down
29 changes: 29 additions & 0 deletions contracts/0.8.25/vaults/VaultHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,35 @@ abstract contract VaultHub is AccessControlEnumerableUpgradeable {
return $.sockets[$.vaultIndex[IHubVault(_vault)]];
}

/// @notice Returns all vaults owned by a given address
/// @param _owner Address of the owner
/// @return An array of vaults owned by the given address
function vaultsByOwner(address _owner) external view returns (IHubVault[] memory) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure about the correctness of this method, but such a method will be very useful

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe needs vaultsIndexesByOwner and then this method around it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The thing is that Vaults are created without an owner, and then the owner and other roles are added. And it seems to me that storing another array in memory that will be updated frequently may be a bad decision.
If this is not the case, then it makes sense to have an additional owner field in the VaultSocket structure.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe needs vaultsIndexesByOwner and then this method around it.

We can't maintain the correctness of such index, because the owner can be changed in the vault, independently.

Jeday 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.

If the owner is the dashboard contract, how are you planning to use it?

VaultHubStorage storage $ = _getVaultHubStorage();
uint256 count = 0;

// First, count how many vaults belong to the owner
for (uint256 i = 1; i < $.sockets.length; i++) {
if ($.sockets[i].vault.owner() == _owner) {
count++;
}
}

// Create an array to hold the owner's vaults
IHubVault[] memory ownerVaults = new IHubVault[](count);
uint256 index = 0;

// Populate the array with the owner's vaults
for (uint256 i = 1; i < $.sockets.length; i++) {
if ($.sockets[i].vault.owner() == _owner) {
ownerVaults[index] = $.sockets[i].vault;
index++;
}
}

return ownerVaults;
}

/// @notice connects a vault to the hub
/// @param _vault vault address
/// @param _shareLimit maximum number of stETH shares that can be minted by the vault
Expand Down
Loading