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] feat: Dashboard UX updates #915

Open
wants to merge 13 commits into
base: feat/vaults
Choose a base branch
from
2 changes: 2 additions & 0 deletions contracts/0.8.25/interfaces/ILido.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ interface ILido is IERC20, IERC20Permit {

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

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

function rebalanceExternalEtherToInternal() external payable;

function getTotalPooledEther() external view returns (uint256);
Expand Down
112 changes: 55 additions & 57 deletions contracts/0.8.25/vaults/Dashboard.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@

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 {IStakingVault} from "./interfaces/IStakingVault.sol";
import {ILido as IStETH} from "../interfaces/ILido.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 {ILido as IStETH} from "contracts/0.8.25/interfaces/ILido.sol";
import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol";
import {IStakingVault} from "contracts/0.8.25/vaults/interfaces/IStakingVault.sol";

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

function deposit() external payable;
}
Expand Down Expand Up @@ -54,7 +54,7 @@
IWstETH public immutable WSTETH;

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

/// @notice The underlying `StakingVault` contract
IStakingVault public stakingVault;
Expand All @@ -71,20 +71,18 @@
}

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

_SELF = address(this);
STETH = IStETH(_stETH);
WETH = IWeth(_weth);
WSTETH = IWstETH(_wstETH);
WETH = IWETH9(_weth);
STETH = IStETH(ILidoLocator(_lidoLocator).lido());
WSTETH = IWstETH(ILidoLocator(_lidoLocator).wstETH());
}

/**
Expand All @@ -109,6 +107,9 @@
vaultHub = VaultHub(stakingVault.vaultHub());
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);

// Allow WSTETH to transfer STETH on behalf of the dashboard
Jeday marked this conversation as resolved.
Show resolved Hide resolved
STETH.approve(address(WSTETH), type(uint256).max);

emit Initialized();
}

Expand Down Expand Up @@ -180,11 +181,11 @@

/**
* @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
* @param _etherToFund 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);
function projectedMintableShares(uint256 _etherToFund) external view returns (uint256) {
uint256 _totalShares = _totalMintableShares(stakingVault.valuation() + _etherToFund);
uint256 _sharesMinted = vaultSocket().sharesMinted;

if (_totalShares < _sharesMinted) return 0;
Expand All @@ -199,14 +200,11 @@
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");
}
Expand All @@ -230,7 +228,7 @@
* @notice Funds the staking vault with ether
*/
function fund() external payable virtual onlyRole(DEFAULT_ADMIN_ROLE) {
_fund();
_fund(msg.value);
}

/**
Expand All @@ -243,8 +241,7 @@
WETH.transferFrom(msg.sender, address(this), _wethAmount);
WETH.withdraw(_wethAmount);

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

/**
Expand Down Expand Up @@ -290,16 +287,17 @@
/**
* @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
* @param _amountOfWstETH Amount of tokens to mint
*/
function mintWstETH(
address _recipient,
uint256 _tokens
uint256 _amountOfWstETH
) external payable virtual onlyRole(DEFAULT_ADMIN_ROLE) fundAndProceed {
_mint(address(this), _tokens);
_mint(address(this), _amountOfWstETH);

uint256 stETHAmount = STETH.getPooledEthByShares(_amountOfWstETH);

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

Expand All @@ -308,24 +306,21 @@
* @param _amountOfShares Amount of shares to burn
*/
function burn(uint256 _amountOfShares) external virtual onlyRole(DEFAULT_ADMIN_ROLE) {
_burn(_amountOfShares);
_burn(msg.sender, _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
* @param _amountOfWstETH 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);
function burnWstETH(uint256 _amountOfWstETH) external virtual onlyRole(DEFAULT_ADMIN_ROLE) {
WSTETH.transferFrom(msg.sender, address(this), _amountOfWstETH);

uint256 stETHAmount = WSTETH.unwrap(_amountOfWstETH);
uint256 sharesAmount = STETH.getSharesByPooledEth(stETHAmount);
Copy link
Member

Choose a reason for hiding this comment

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

Do we need this? Isn't _amountOfWstETH already the same as sharesAmount?

Copy link
Member

@tamtamchik tamtamchik Jan 10, 2025

Choose a reason for hiding this comment

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

It seems that using _amountOfWstETH is the correct approach. However, we need to conduct a test with small numbers to check for rounding issues.


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

Check failure

Code scanning / Slither

Unchecked transfer High


/**
* @dev Modifier to check if the permit is successful, and if not, check if the allowance is sufficient
Expand Down Expand Up @@ -362,43 +357,40 @@

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

/**
* @notice Burns wstETH tokens from the sender backed by the vault using EIP-2612 Permit.
* @param _tokens Amount of wstETH tokens to burn
* @param _amountOfWstETH Amount of wstETH tokens to burn
* @param _permit data required for the wstETH.permit() method to set the allowance
*/
function burnWstETHWithPermit(
uint256 _tokens,
uint256 _amountOfWstETH,
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);

WSTETH.transferFrom(msg.sender, address(this), _amountOfWstETH);
uint256 stETHAmount = WSTETH.unwrap(_amountOfWstETH);
uint256 sharesAmount = STETH.getSharesByPooledEth(stETHAmount);

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

/**
Expand All @@ -416,7 +408,7 @@
*/
modifier fundAndProceed() {
if (msg.value > 0) {
_fund();
_fund(msg.value);
}
_;
}
Expand Down Expand Up @@ -444,8 +436,8 @@
/**
* @dev Funds the staking vault with the ether sent in the transaction
*/
function _fund() internal {
stakingVault.fund{value: msg.value}();
function _fund(uint256 _value) internal {
stakingVault.fund{value: _value}();
}

/**
Expand Down Expand Up @@ -492,17 +484,23 @@
* @dev Burns stETH tokens from the sender backed by the vault
* @param _amountOfShares Amount of tokens to burn
*/
function _burn(uint256 _amountOfShares) internal {
STETH.transferSharesFrom(msg.sender, address(vaultHub), _amountOfShares);
function _burn(address _sender, uint256 _amountOfShares) internal {
if (_sender == address(this)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We might be able to skip this check, hear me out. I check steth code.
You need to set infinity steth allowance to address(this) at contract init.
This will allow you to call transferSharesFrom with _sender==address(this).

Copy link
Member

Choose a reason for hiding this comment

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

Infinity steth allowance looks acceptable to me here. However, the condition also does not bother me much.
I need feedback from more experienced individuals (like AP or EM) because having an unlimited allowance doesn’t seem safu.

Copy link
Member

Choose a reason for hiding this comment

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

Looks like we have woo wide permissions for the contact when adding infinity steth allowance, so better stick with original code.

STETH.transferShares(address(vaultHub), _amountOfShares);
} else {
STETH.transferSharesFrom(_sender, address(vaultHub), _amountOfShares);
}

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

Check warning

Code scanning / Slither

Unused return Medium

Check warning

Code scanning / Slither

Unused return Medium


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

Expand Down
11 changes: 5 additions & 6 deletions contracts/0.8.25/vaults/Delegation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,11 @@ contract Delegation is Dashboard {
uint256 public voteLifetime;

/**
* @notice Initializes the contract with the stETH address.
* @param _stETH The address of the stETH token.
* @notice Initializes the contract with the weth address.
* @param _weth Address of the weth token contract.
* @param _wstETH Address of the wstETH token contract.
* @param _lidoLocator Address of the Lido locator contract.
*/
constructor(address _stETH, address _weth, address _wstETH) Dashboard(_stETH, _weth, _wstETH) {}
constructor(address _weth, address _lidoLocator) Dashboard(_weth, _lidoLocator) {}

/**
* @notice Initializes the contract:
Expand Down Expand Up @@ -207,7 +206,7 @@ contract Delegation is Dashboard {
* @notice Funds the StakingVault with ether.
*/
function fund() external payable override onlyRole(STAKER_ROLE) {
_fund();
_fund(msg.value);
}

/**
Expand Down Expand Up @@ -250,7 +249,7 @@ contract Delegation is Dashboard {
* @param _amountOfShares The amount of shares to burn.
*/
function burn(uint256 _amountOfShares) external override onlyRole(TOKEN_MASTER_ROLE) {
_burn(_amountOfShares);
_burn(msg.sender, _amountOfShares);
}

/**
Expand Down
2 changes: 0 additions & 2 deletions test/0.8.25/vaults/contracts/WETH9__MockForVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

pragma solidity 0.4.24;
tamtamchik marked this conversation as resolved.
Show resolved Hide resolved

import {StETH} from "contracts/0.4.24/StETH.sol";

contract WETH9__MockForVault {
string public name = "Wrapped Ether";
string public symbol = "WETH";
Expand Down
Jeday marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
interface ILidoLocator {
Jeday marked this conversation as resolved.
Show resolved Hide resolved
function lido() external view returns (address);

function wstETH() external view returns (address);
}

contract LidoLocator__HarnessForDashboard is ILidoLocator {
address private immutable LIDO;
address private immutable WSTETH;

constructor(
address _lido,
address _wstETH
) {
LIDO = _lido;
WSTETH = _wstETH;
}

function lido() external view returns (address) {
return LIDO;
}

function wstETH() external view returns (address) {
return WSTETH;
}
}
Loading
Loading