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

feat: self-impose locks on StakingVault #979

Merged
merged 14 commits into from
Apr 1, 2025
Merged
44 changes: 39 additions & 5 deletions contracts/0.8.25/vaults/Dashboard.sol
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,23 @@ contract Dashboard is Permissions {
SafeERC20.safeTransfer(WETH, _recipient, _amountOfWETH);
}

/**
* @notice Update the locked amount of the staking vault
* @param _amount Amount of ether to lock
*/
function lock(uint256 _amount) external {
_lock(_amount);
}

/**
* @notice Mints stETH shares backed by the vault to the recipient.
* @param _recipient Address of the recipient
* @param _amountOfShares Amount of stETH shares to mint
*/
function mintShares(address _recipient, uint256 _amountOfShares) external payable fundable {
function mintShares(
address _recipient,
uint256 _amountOfShares
) external payable fundable autolock(_amountOfShares) {
_mintShares(_recipient, _amountOfShares);
}

Expand All @@ -275,7 +286,10 @@ contract Dashboard is Permissions {
* @param _recipient Address of the recipient
* @param _amountOfStETH Amount of stETH to mint
*/
function mintStETH(address _recipient, uint256 _amountOfStETH) external payable virtual fundable {
function mintStETH(
address _recipient,
uint256 _amountOfStETH
) external payable fundable autolock(STETH.getSharesByPooledEth(_amountOfStETH)) {
_mintShares(_recipient, STETH.getSharesByPooledEth(_amountOfStETH));
}

Expand All @@ -284,7 +298,10 @@ contract Dashboard is Permissions {
* @param _recipient Address of the recipient
* @param _amountOfWstETH Amount of tokens to mint
*/
function mintWstETH(address _recipient, uint256 _amountOfWstETH) external payable fundable {
function mintWstETH(
address _recipient,
uint256 _amountOfWstETH
) external payable fundable autolock(_amountOfWstETH) {
_mintShares(address(this), _amountOfWstETH);

uint256 mintedStETH = STETH.getPooledEthBySharesRoundUp(_amountOfWstETH);
Expand Down Expand Up @@ -475,6 +492,25 @@ contract Dashboard is Permissions {
_;
}

/**
* @dev Modifier to increase the locked amount if necessary
* @param _newShares The number of new shares to mint
*/
modifier autolock(uint256 _newShares) {
VaultHub.VaultSocket memory socket = vaultSocket();

// Calculate the locked amount required to accommodate the new shares
uint256 requiredLocked = (STETH.getPooledEthBySharesRoundUp(socket.sharesMinted + _newShares) *
TOTAL_BASIS_POINTS) / (TOTAL_BASIS_POINTS - socket.reserveRatioBP);

// If the required locked amount is greater than the current, increase the locked amount
if (requiredLocked > stakingVault().locked()) {
_lock(requiredLocked);
}

_;
}

/**
* @dev Modifier to check if the permit is successful, and if not, check if the allowance is sufficient
*/
Expand Down Expand Up @@ -508,8 +544,6 @@ contract Dashboard is Permissions {
revert InvalidPermit(token);
}

/**

/**
* @dev Burns stETH tokens from the sender backed by the vault
* @param _amountOfStETH Amount of tokens to burn
Expand Down
9 changes: 9 additions & 0 deletions contracts/0.8.25/vaults/Permissions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ abstract contract Permissions is AccessControlConfirmable {
*/
bytes32 public constant WITHDRAW_ROLE = keccak256("vaults.Permissions.Withdraw");

/**
* @notice Permission for locking ether on StakingVault.
*/
bytes32 public constant LOCK_ROLE = keccak256("vaults.Permissions.Lock");

/**
* @notice Permission for minting stETH shares backed by the StakingVault.
*/
Expand Down Expand Up @@ -191,6 +196,10 @@ abstract contract Permissions is AccessControlConfirmable {
stakingVault().withdraw(_recipient, _ether);
}

function _lock(uint256 _locked) internal onlyRole(LOCK_ROLE) {
stakingVault().lock(_locked);
}

/**
* @dev Checks the MINT_ROLE and mints shares backed by the StakingVault.
* @param _recipient The address to mint the shares to.
Expand Down
16 changes: 9 additions & 7 deletions contracts/0.8.25/vaults/StakingVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -315,11 +315,10 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {
* @dev Can only be called by VaultHub; locked amount can only be increased
* @param _locked New amount to lock
*/
function lock(uint256 _locked) external {
if (msg.sender != address(VAULT_HUB)) revert NotAuthorized("lock", msg.sender);

function lock(uint256 _locked) external onlyOwner {
Copy link
Member

Choose a reason for hiding this comment

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

In this configuration, it's possible to enforce _locked <= valuation, but it means that we should prefund 1 ETH in vault factory for deposit, which is fine tbh.

ERC7201Storage storage $ = _getStorage();
if ($.locked > _locked) revert LockedCannotDecreaseOutsideOfReport($.locked, _locked);
if (_locked <= $.locked) revert NewLockedNotGreaterThanCurrent();
if (_locked > valuation()) revert NewLockedExceedsValuation();

$.locked = uint128(_locked);

Expand Down Expand Up @@ -664,10 +663,13 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {

/**
* @notice Thrown when attempting to decrease the locked amount outside of a report
* @param currentlyLocked Current amount of locked ether
* @param attemptedLocked Attempted new locked amount
*/
error LockedCannotDecreaseOutsideOfReport(uint256 currentlyLocked, uint256 attemptedLocked);
error NewLockedNotGreaterThanCurrent();

/**
* @notice Thrown when the locked amount exceeds the valuation
*/
error NewLockedExceedsValuation();

/**
* @notice Thrown when called on the implementation contract
Expand Down
5 changes: 5 additions & 0 deletions contracts/0.8.25/vaults/VaultFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import {BeaconProxy} from "@openzeppelin/contracts-v5.2/proxy/beacon/BeaconProxy.sol";
import {Clones} from "@openzeppelin/contracts-v5.2/proxy/Clones.sol";
import {OwnableUpgradeable} from "contracts/openzeppelin/5.2/upgradeable/access/OwnableUpgradeable.sol";

Check warning on line 9 in contracts/0.8.25/vaults/VaultFactory.sol

View workflow job for this annotation

GitHub Actions / Solhint

imported name OwnableUpgradeable is not used

import {IStakingVault} from "./interfaces/IStakingVault.sol";
import {Delegation} from "./Delegation.sol";
Expand All @@ -18,6 +19,7 @@
uint16 nodeOperatorFeeBP;
address[] funders;
address[] withdrawers;
address[] lockers;
address[] minters;
address[] burners;
address[] rebalancers;
Expand Down Expand Up @@ -79,6 +81,9 @@
for (uint256 i = 0; i < _delegationConfig.withdrawers.length; i++) {
delegation.grantRole(delegation.WITHDRAW_ROLE(), _delegationConfig.withdrawers[i]);
}
for (uint256 i = 0; i < _delegationConfig.lockers.length; i++) {
delegation.grantRole(delegation.LOCK_ROLE(), _delegationConfig.lockers[i]);
}
for (uint256 i = 0; i < _delegationConfig.minters.length; i++) {
delegation.grantRole(delegation.MINT_ROLE(), _delegationConfig.minters[i]);
}
Expand Down
21 changes: 11 additions & 10 deletions contracts/0.8.25/vaults/VaultHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,9 @@ contract VaultHub is PausableUntilWithRoles {
if (IStakingVault(_vault).depositor() != LIDO_LOCATOR.predepositGuarantee())
revert VaultDepositorNotAllowed(IStakingVault(_vault).depositor());

if (IStakingVault(_vault).locked() < CONNECT_DEPOSIT)
revert VaultInsufficientLocked(_vault, IStakingVault(_vault).locked(), CONNECT_DEPOSIT);

VaultSocket memory vsocket = VaultSocket(
_vault,
0, // sharesMinted
Expand All @@ -243,8 +246,6 @@ contract VaultHub is PausableUntilWithRoles {
$.vaultIndex[_vault] = $.sockets.length;
$.sockets.push(vsocket);

IStakingVault(_vault).lock(CONNECT_DEPOSIT);

emit VaultConnected(_vault, _shareLimit, _reserveRatioBP, _rebalanceThresholdBP, _treasuryFeeBP);
}

Expand Down Expand Up @@ -307,19 +308,18 @@ contract VaultHub is PausableUntilWithRoles {
IStakingVault vault_ = IStakingVault(_vault);
uint256 maxMintableRatioBP = TOTAL_BASIS_POINTS - socket.reserveRatioBP;
uint256 maxMintableEther = (vault_.valuation() * maxMintableRatioBP) / TOTAL_BASIS_POINTS;
uint256 etherToLock = LIDO.getPooledEthBySharesRoundUp(vaultSharesAfterMint);
if (etherToLock > maxMintableEther) {
uint256 stETHAfterMint = LIDO.getPooledEthBySharesRoundUp(vaultSharesAfterMint);
if (stETHAfterMint > maxMintableEther) {
revert InsufficientValuationToMint(_vault, vault_.valuation());
}

socket.sharesMinted = uint96(vaultSharesAfterMint);

// Calculate the total ETH that needs to be locked in the vault to maintain the reserve ratio
uint256 totalEtherLocked = (etherToLock * TOTAL_BASIS_POINTS) / maxMintableRatioBP;
if (totalEtherLocked > vault_.locked()) {
vault_.lock(totalEtherLocked);
// Calculate the minimum ETH that needs to be locked in the vault to maintain the reserve ratio
uint256 minLocked = (stETHAfterMint * TOTAL_BASIS_POINTS) / maxMintableRatioBP;
if (minLocked > vault_.locked()) {
revert VaultInsufficientLocked(_vault, vault_.locked(), minLocked);
}

socket.sharesMinted = uint96(vaultSharesAfterMint);
LIDO.mintExternalShares(_recipient, _amountOfShares);

emit MintedSharesOnVault(_vault, _amountOfShares);
Expand Down Expand Up @@ -652,4 +652,5 @@ contract VaultHub is PausableUntilWithRoles {
error ConnectedVaultsLimitTooLow(uint256 connectedVaultsLimit, uint256 currentVaultsCount);
error RelativeShareLimitBPTooHigh(uint256 relativeShareLimitBP, uint256 totalBasisPoints);
error VaultDepositorNotAllowed(address depositor);
error VaultInsufficientLocked(address vault, uint256 currentLocked, uint256 expectedLocked);
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ contract VaultFactory__MockForDashboard is UpgradeableBeacon {
dashboard.grantRole(dashboard.FUND_ROLE(), msg.sender);
dashboard.grantRole(dashboard.WITHDRAW_ROLE(), msg.sender);
dashboard.grantRole(dashboard.MINT_ROLE(), msg.sender);
dashboard.grantRole(dashboard.LOCK_ROLE(), msg.sender);
dashboard.grantRole(dashboard.BURN_ROLE(), msg.sender);
dashboard.grantRole(dashboard.REBALANCE_ROLE(), msg.sender);
dashboard.grantRole(dashboard.PAUSE_BEACON_CHAIN_DEPOSITS_ROLE(), msg.sender);
Expand Down
Loading
Loading