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

[SI][Vaults] feat: Dashboard UX updates #915

Merged
merged 43 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
b6156ad
feat: dashboard token recovery
Jeday Dec 26, 2024
a11d6b6
feat: add locator, update burn/mint methods, fixes, tests
DiRaiks Jan 9, 2025
29ef4ad
tests: update Delegation constructor
DiRaiks Jan 9, 2025
b5c839f
tests: add tests for burnWstETHWithPermit
DiRaiks Jan 9, 2025
4b16505
fix: add nft recovery
Jeday Jan 10, 2025
41f6125
fix(docs): dashboard comment
Jeday Jan 10, 2025
ae59476
Merge branch 'feat/vaults' of github.com:lidofinance/core into feat/d…
Jeday Jan 10, 2025
de3d3b9
fix: update naming for burn/mint
Jeday Jan 10, 2025
3261a8c
fix(test): check allowance in dashboard
Jeday Jan 10, 2025
c228afd
fix(test): remove extra await
Jeday Jan 10, 2025
c6cc70f
fix(test): dashboard address reuse
Jeday Jan 10, 2025
739a60d
test: dashboard valuation and recieve
Jeday Jan 10, 2025
839b726
fix: happy path integration test and linters
tamtamchik Jan 10, 2025
99f7d9a
fix: tests
tamtamchik Jan 10, 2025
186e266
test: update tests for dashboard
tamtamchik Jan 10, 2025
0aea721
fix: reduce dashboard._burn gas
Jeday Jan 13, 2025
c4e7ceb
chore: simplify burnWstETH
tamtamchik Jan 13, 2025
62710b0
Merge branch 'feat/dashboard-ux-updates' of github.com:lidofinance/co…
Jeday Jan 14, 2025
95b11fb
fix: use eth address convention
Jeday Jan 14, 2025
b5ce3a4
fix: to lowercase address
Jeday Jan 14, 2025
fe87ca3
fix: revert to checksum
Jeday Jan 14, 2025
50259ec
Merge pull request #908 from lidofinance/feat/vaults-dashboard-recovery
tamtamchik Jan 14, 2025
fac1f9d
feat(vaults): mint/burn steth
Jeday Jan 15, 2025
710ccac
docs: burn shares permit comment
Jeday Jan 15, 2025
77d8739
fix: use round up
Jeday Jan 16, 2025
0903095
feat: fix burnWsteth
Jeday Jan 16, 2025
5300444
feat(test): mint wsteth wei tests
Jeday Jan 16, 2025
f24c48e
test: variable wei/shareRate burnWsteth test
Jeday Jan 17, 2025
90f64d4
fix: burner event order
Jeday Jan 17, 2025
5a907fc
docs: comment
Jeday Jan 17, 2025
b529f7b
Merge branch 'feat/vaults' of github.com:lidofinance/core into feat/d…
Jeday Jan 17, 2025
5215e35
docs: add notice
Jeday Jan 17, 2025
f78235c
Merge branch 'feat/vaults' of github.com:lidofinance/core into feat/d…
Jeday Jan 22, 2025
bea9a49
test: fix oz version
Jeday Jan 22, 2025
616a0f8
fix: use safeERC20
Jeday Jan 23, 2025
2b871fb
Merge branch 'feat/vaults' of github.com:lidofinance/core into feat/d…
Jeday Jan 23, 2025
c9c7f74
test: whitespace
Jeday Jan 23, 2025
e470a89
fix: fund/withdraw naming
Jeday Jan 23, 2025
4ff312b
Merge branch 'feat/vaults' of github.com:lidofinance/core into feat/d…
Jeday Jan 28, 2025
34d4519
fix: dashboard tests
Jeday Jan 28, 2025
b7e5536
fix: remove onlyRole
Jeday Jan 29, 2025
0c2c170
fix: unused imports & naming
Jeday Jan 29, 2025
067f38b
test: fix delegation test
Jeday Jan 29, 2025
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
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
Copy link
Contributor

Choose a reason for hiding this comment

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

thresholdReserveRatio vs reserveRatioThreshold is a bit weird

Copy link
Contributor

Choose a reason for hiding this comment

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

folkyatina marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

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

Please update following comment:

/**
 * @title Dashboard
 * @notice This contract is meant to be used as the owner of `StakingVault`.
 * This contract improves the vault UX by bundling all functions from the vault and vault hub
 * in this single contract. It provides administrative functions for managing the staking vault,
 * including funding, withdrawing, depositing to the beacon chain, minting, burning, and rebalancing operations.
 * All these functions are only callable by the account with the DEFAULT_ADMIN_ROLE.
 * TODO: need to add recover methods for ERC20, probably in a separate contract
 */

Copy link
Contributor

Choose a reason for hiding this comment

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

fixed

Copy link
Contributor

Choose a reason for hiding this comment

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

votingCommittee -> multiConfirmationRoles

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

reserveRatio() -> reserveRatioBP() or at least comment that this is the BP convention

Copy link
Contributor

Choose a reason for hiding this comment

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

fixed

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 document whether already minted are included or not

function totalMintableShares() public view returns (uint256)

Copy link
Contributor

Choose a reason for hiding this comment

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

fixed

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";
Copy link
Contributor

Choose a reason for hiding this comment

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

not used ⚠️

Copy link
Contributor

Choose a reason for hiding this comment

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

fixed


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");
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
if (_weth == address(0)) revert ZeroArgument("_WETH");
if (_weth == address(0)) revert ZeroArgument("_weth");

Copy link
Contributor

Choose a reason for hiding this comment

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

fixed

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);
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 an admin argument to avoid using msg.sender inside the contract

Copy link
Contributor

Choose a reason for hiding this comment

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

maybe a bit later will be done by @failingtwice


// 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();
Copy link
Contributor

Choose a reason for hiding this comment

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

should we emit args of the init?

}

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

/**
* @notice Returns the maximum number of shares that can be minted with deposited ether.
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 Returns the maximum number of shares that can be minted with deposited ether.
* @notice Returns the maximum number of shares that can be minted with funded ether.

Copy link
Contributor

Choose a reason for hiding this comment

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

fixed

* @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) {
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 projectedMintableShares(uint256 _etherToFund) external view returns (uint256) {
function projectedNewMintableShares(uint256 _etherToFund) external view returns (uint256) {

not a strong opinion though, just to disambiguate with totalMintableShares having diff vs acc semantics

Copy link
Contributor

Choose a reason for hiding this comment

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

fixed

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");
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
if (msg.value == 0) revert ZeroArgument("msg.value");

why do we need this though?

Copy link
Contributor

Choose a reason for hiding this comment

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

fixed

}
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.
Jeday marked this conversation as resolved.
Show resolved Hide resolved
* @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
Copy link
Contributor

Choose a reason for hiding this comment

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

amountX vs Xamount is a bit incosistent

Copy link
Contributor

Choose a reason for hiding this comment

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

fixed

) external payable virtual onlyRole(DEFAULT_ADMIN_ROLE) fundAndProceed {
_mint(address(this), _tokens);
_mint(address(this), _amountOfWstETH);

uint256 stETHAmount = STETH.getPooledEthByShares(_amountOfWstETH);
Jeday marked this conversation as resolved.
Show resolved Hide resolved

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);
Jeday marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @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) {
folkyatina marked this conversation as resolved.
Show resolved Hide resolved
WSTETH.transferFrom(msg.sender, address(this), _amountOfWstETH);

uint256 stETHAmount = WSTETH.unwrap(_amountOfWstETH);
uint256 sharesAmount = STETH.getSharesByPooledEth(stETHAmount);
tamtamchik marked this conversation as resolved.
Show resolved Hide resolved

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)
Jeday marked this conversation as resolved.
Show resolved Hide resolved
{
_burn(_tokens);
_burn(msg.sender, _amountOfShares);
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
_burn(msg.sender, _amountOfShares);
_burnFrom(msg.sender, _amountOfShares);

Copy link
Contributor

Choose a reason for hiding this comment

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

fixed

}

/**
* @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)) {
tamtamchik marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Contributor

Choose a reason for hiding this comment

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

keccak256("Vault.Delegation.CuratorRole") ⇒ keccak256("vaults.Delegation.CuratorRole") and so on

Copy link
Contributor

Choose a reason for hiding this comment

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

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 remove msg.sender from initialization

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

vote -> multiConfirmationRole (everywhere)

Copy link
Contributor

Choose a reason for hiding this comment

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

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