Skip to content

Commit

Permalink
feat: make VaultHub pausable
Browse files Browse the repository at this point in the history
  • Loading branch information
folkyatina committed Jan 3, 2025
1 parent df8400f commit 4e18178
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 5 deletions.
41 changes: 41 additions & 0 deletions contracts/0.8.25/utils/OZPausableUntil.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2024 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
pragma solidity 0.8.25;

import {PausableUntil} from "contracts/common/utils/PausableUntil.sol";
import {AccessControlEnumerableUpgradeable} from "contracts/openzeppelin/5.0.2/upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol";

/// @title PausableAccessControlEnumerableUpgradeable aka PausableACEU
/// @author folkyatina
abstract contract OZPausableUntil is PausableUntil, AccessControlEnumerableUpgradeable {
/// @notice role that allows to pause the hub
bytes32 public constant PAUSE_ROLE = keccak256("OZPausableUntil.PauseRole");
/// @notice role that allows to resume the hub
bytes32 public constant RESUME_ROLE = keccak256("OZPausableUntil.ResumeRole");

/// @notice Resume withdrawal requests placement and finalization
/// @dev Contract is deployed in paused state and should be resumed explicitly
function resume() external onlyRole(RESUME_ROLE) {
_resume();
}

/// @notice Pause withdrawal requests placement and finalization. Claiming finalized requests will still be available
/// @param _duration pause duration in seconds (use `PAUSE_INFINITELY` for unlimited)
/// @dev Reverts if contract is already paused
/// @dev Reverts reason if sender has no `PAUSE_ROLE`
/// @dev Reverts if zero duration is passed
function pauseFor(uint256 _duration) external onlyRole(PAUSE_ROLE) {
_pauseFor(_duration);
}

/// @notice Pause withdrawal requests placement and finalization. Claiming finalized requests will still be available
/// @param _pauseUntilInclusive the last second to pause until inclusive
/// @dev Reverts if the timestamp is in the past
/// @dev Reverts if sender has no `PAUSE_ROLE`
/// @dev Reverts if contract is already paused
function pauseUntil(uint256 _pauseUntilInclusive) external onlyRole(PAUSE_ROLE) {
_pauseUntil(_pauseUntilInclusive);
}
}
12 changes: 7 additions & 5 deletions contracts/0.8.25/vaults/VaultHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ import {IStakingVault} from "./interfaces/IStakingVault.sol";
import {ILido as IStETH} from "../interfaces/ILido.sol";
import {IBeaconProxy} from "./interfaces/IBeaconProxy.sol";

import {OZPausableUntil} from "../utils/OZPausableUntil.sol";

import {Math256} from "contracts/common/lib/Math256.sol";

/// @notice VaultHub is a contract that manages vaults connected to the Lido protocol
/// It allows to connect vaults, disconnect them, mint and burn stETH
/// It also allows to force rebalance of the vaults
/// Also, it passes the report from the accounting oracle to the vaults and charges fees
/// @author folkyatina
abstract contract VaultHub is AccessControlEnumerableUpgradeable {
abstract contract VaultHub is AccessControlEnumerableUpgradeable, OZPausableUntil {
/// @custom:storage-location erc7201:VaultHub
struct VaultHubStorage {
/// @notice vault sockets with vaults connected to the hub
Expand Down Expand Up @@ -217,7 +219,7 @@ abstract contract VaultHub is AccessControlEnumerableUpgradeable {
/// @param _vault vault address
/// @dev msg.sender should be vault's owner
/// @dev vault's `mintedShares` should be zero
function voluntaryDisconnect(address _vault) external {
function voluntaryDisconnect(address _vault) external whenResumed {
if (_vault == address(0)) revert ZeroArgument("_vault");
_vaultAuth(_vault, "disconnect");

Expand All @@ -229,7 +231,7 @@ abstract contract VaultHub is AccessControlEnumerableUpgradeable {
/// @param _recipient address of the receiver
/// @param _amountOfShares amount of stETH shares to mint
/// @dev msg.sender should be vault's owner
function mintSharesBackedByVault(address _vault, address _recipient, uint256 _amountOfShares) external {
function mintSharesBackedByVault(address _vault, address _recipient, uint256 _amountOfShares) external whenResumed {
if (_vault == address(0)) revert ZeroArgument("_vault");
if (_recipient == address(0)) revert ZeroArgument("_recipient");
if (_amountOfShares == 0) revert ZeroArgument("_amountOfShares");
Expand Down Expand Up @@ -268,7 +270,7 @@ abstract contract VaultHub is AccessControlEnumerableUpgradeable {
/// @param _amountOfShares amount of shares to burn
/// @dev msg.sender should be vault's owner
/// @dev VaultHub must have all the stETH on its balance
function burnSharesBackedByVault(address _vault, uint256 _amountOfShares) public {
function burnSharesBackedByVault(address _vault, uint256 _amountOfShares) public whenResumed {
if (_vault == address(0)) revert ZeroArgument("_vault");
if (_amountOfShares == 0) revert ZeroArgument("_amountOfShares");
_vaultAuth(_vault, "burn");
Expand Down Expand Up @@ -334,7 +336,7 @@ abstract contract VaultHub is AccessControlEnumerableUpgradeable {
/// @notice rebalances the vault by writing off the amount of ether equal
/// to `msg.value` from the vault's minted stETH
/// @dev msg.sender should be vault's contract
function rebalance() external payable {
function rebalance() external payable whenResumed {
if (msg.value == 0) revert ZeroArgument("msg.value");

VaultSocket storage socket = _connectedSocket(msg.sender);
Expand Down
38 changes: 38 additions & 0 deletions contracts/common/lib/UnstructuredStorage.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2023 Lido <[email protected]>, Aragon
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.9;

Check failure on line 4 in contracts/common/lib/UnstructuredStorage.sol

View workflow job for this annotation

GitHub Actions / Solhint

Compiler version ^0.8.9 must be fixed

library UnstructuredStorage {
function getStorageBool(bytes32 position) internal view returns (bool data) {
assembly { data := sload(position) }
}

function getStorageAddress(bytes32 position) internal view returns (address data) {
assembly { data := sload(position) }
}

function getStorageBytes32(bytes32 position) internal view returns (bytes32 data) {
assembly { data := sload(position) }
}

function getStorageUint256(bytes32 position) internal view returns (uint256 data) {
assembly { data := sload(position) }
}

function setStorageBool(bytes32 position, bool data) internal {
assembly { sstore(position, data) }
}

function setStorageAddress(bytes32 position, address data) internal {
assembly { sstore(position, data) }
}

function setStorageBytes32(bytes32 position, bytes32 data) internal {
assembly { sstore(position, data) }
}

function setStorageUint256(bytes32 position, uint256 data) internal {
assembly { sstore(position, data) }
}
}
97 changes: 97 additions & 0 deletions contracts/common/utils/PausableUntil.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// SPDX-FileCopyrightText: 2023 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.9;

Check failure on line 3 in contracts/common/utils/PausableUntil.sol

View workflow job for this annotation

GitHub Actions / Solhint

Compiler version ^0.8.9 must be fixed

import {UnstructuredStorage} from "contracts/common/lib/UnstructuredStorage.sol";


abstract contract PausableUntil {
using UnstructuredStorage for bytes32;

/// Contract resume/pause control storage slot
bytes32 internal constant RESUME_SINCE_TIMESTAMP_POSITION = keccak256("lido.PausableUntil.resumeSinceTimestamp");
/// Special value for the infinite pause
uint256 public constant PAUSE_INFINITELY = type(uint256).max;

/// @notice Emitted when paused by the `pauseFor` or `pauseUntil` call
event Paused(uint256 duration);
/// @notice Emitted when resumed by the `resume` call
event Resumed();

error ZeroPauseDuration();
error PausedExpected();
error ResumedExpected();
error PauseUntilMustBeInFuture();

/// @notice Reverts when paused
modifier whenResumed() {
_checkResumed();
_;
}

function _checkPaused() internal view {
if (!isPaused()) {
revert PausedExpected();
}
}

function _checkResumed() internal view {
if (isPaused()) {
revert ResumedExpected();
}
}

/// @notice Returns whether the contract is paused
function isPaused() public view returns (bool) {
return block.timestamp < RESUME_SINCE_TIMESTAMP_POSITION.getStorageUint256();
}

/// @notice Returns one of:
/// - PAUSE_INFINITELY if paused infinitely returns
/// - first second when get contract get resumed if paused for specific duration
/// - some timestamp in past if not paused
function getResumeSinceTimestamp() external view returns (uint256) {
return RESUME_SINCE_TIMESTAMP_POSITION.getStorageUint256();
}

function _resume() internal {
_checkPaused();
RESUME_SINCE_TIMESTAMP_POSITION.setStorageUint256(block.timestamp);
emit Resumed();
}

function _pauseFor(uint256 _duration) internal {
_checkResumed();
if (_duration == 0) revert ZeroPauseDuration();

uint256 resumeSince;
if (_duration == PAUSE_INFINITELY) {
resumeSince = PAUSE_INFINITELY;
} else {
resumeSince = block.timestamp + _duration;
}
_setPausedState(resumeSince);
}

function _pauseUntil(uint256 _pauseUntilInclusive) internal {
_checkResumed();
if (_pauseUntilInclusive < block.timestamp) revert PauseUntilMustBeInFuture();

uint256 resumeSince;
if (_pauseUntilInclusive != PAUSE_INFINITELY) {
resumeSince = _pauseUntilInclusive + 1;
} else {
resumeSince = PAUSE_INFINITELY;
}
_setPausedState(resumeSince);
}

function _setPausedState(uint256 _resumeSince) internal {
RESUME_SINCE_TIMESTAMP_POSITION.setStorageUint256(_resumeSince);
if (_resumeSince == PAUSE_INFINITELY) {
emit Paused(PAUSE_INFINITELY);
} else {
emit Paused(_resumeSince - block.timestamp);
}
}

Check warning

Code scanning / Slither

Dangerous strict equalities Medium

}

0 comments on commit 4e18178

Please sign in to comment.