Skip to content

Commit

Permalink
Merge pull request #914 from lidofinance/pausable-vault-hub
Browse files Browse the repository at this point in the history
feat: make VaultHub pausable
  • Loading branch information
folkyatina authored Jan 16, 2025
2 parents 50a3eeb + 520f9ba commit 50d8b6e
Show file tree
Hide file tree
Showing 8 changed files with 399 additions and 17 deletions.
51 changes: 51 additions & 0 deletions contracts/0.8.25/utils/PausableUntilWithRoles.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi>
// 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 PausableUntilWithRoles
* @notice a `PausableUntil` implementation using OpenZeppelin's `AccessControlEnumerableUpgradeable`
* @dev the inheriting contract must use `whenNotPaused` modifier from `PausableUntil` to block some functions on pause
*/
abstract contract PausableUntilWithRoles is PausableUntil, AccessControlEnumerableUpgradeable {
/// @notice role that allows to pause the contract
bytes32 public constant PAUSE_ROLE = keccak256("PausableUntilWithRoles.PauseRole");
/// @notice role that allows to resume the contract
bytes32 public constant RESUME_ROLE = keccak256("PausableUntilWithRoles.ResumeRole");

/**
* @notice Resume the contract
* @dev Reverts if contracts is not paused
* @dev Reverts if sender has no `RESUME_ROLE`
*/
function resume() external onlyRole(RESUME_ROLE) {
_resume();
}

/**
* @notice Pause the contract for a specified period
* @param _duration pause duration in seconds (use `PAUSE_INFINITELY` for unlimited)
* @dev Reverts if contract is already paused
* @dev Reverts 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 the contract until a specified timestamp
* @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);
}
}
13 changes: 7 additions & 6 deletions contracts/0.8.25/vaults/VaultHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@
pragma solidity 0.8.25;

import {IBeacon} from "@openzeppelin/contracts-v5.0.2/proxy/beacon/IBeacon.sol";
import {AccessControlEnumerableUpgradeable} from "contracts/openzeppelin/5.0.2/upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol";
import {OwnableUpgradeable} from "contracts/openzeppelin/5.0.2/upgradeable/access/OwnableUpgradeable.sol";

import {IStakingVault} from "./interfaces/IStakingVault.sol";
import {ILido as IStETH} from "../interfaces/ILido.sol";
import {IBeaconProxy} from "./interfaces/IBeaconProxy.sol";

import {PausableUntilWithRoles} from "../utils/PausableUntilWithRoles.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 PausableUntilWithRoles {
/// @custom:storage-location erc7201:VaultHub
struct VaultHubStorage {
/// @notice vault sockets with vaults connected to the hub
Expand Down Expand Up @@ -217,7 +218,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 +230,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 +269,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 +335,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
39 changes: 39 additions & 0 deletions contracts/common/lib/UnstructuredStorage.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2023 Lido <info@lido.fi>, Aragon
// SPDX-License-Identifier: MIT

// solhint-disable-next-line lido/fixed-compiler-version
pragma solidity ^0.8.9;

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) }
}
}
101 changes: 101 additions & 0 deletions contracts/common/utils/PausableUntil.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0
// solhint-disable-next-line lido/fixed-compiler-version
pragma solidity ^0.8.9;

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

/**
* @title PausableUntil
* @notice allows to pause the contract for a specific duration or indefinitely
*/
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 if paused
modifier whenResumed() {
_checkResumed();
_;
}

/// @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
/// - the timestamp when the 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 _checkPaused() internal view {
if (!isPaused()) {
revert PausedExpected();
}
}

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

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);
}
}
}
9 changes: 6 additions & 3 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const RPC_URL: string = process.env.RPC_URL || "";
const config: HardhatUserConfig = {
defaultNetwork: "hardhat",
gasReporter: {
enabled: true,
enabled: process.env.SKIP_GAS_REPORT ? false : true,
},
networks: {
"hardhat": {
Expand Down Expand Up @@ -144,7 +144,10 @@ const config: HardhatUserConfig = {
},
watcher: {
test: {
tasks: [{ command: "test", params: { testFiles: ["{path}"] } }],
tasks: [
{ command: "compile", params: { quiet: true } },
{ command: "test", params: { noCompile: true, testFiles: ["{path}"] } },
],
files: ["./test/**/*"],
clearOnStart: true,
start: "echo Running tests...",
Expand All @@ -171,7 +174,7 @@ const config: HardhatUserConfig = {
contractSizer: {
alphaSort: false,
disambiguatePaths: false,
runOnCompile: true,
runOnCompile: process.env.SKIP_CONTRACT_SIZE ? false : true,
strict: true,
except: ["template", "mocks", "@aragon", "openzeppelin", "test"],
},
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"test:sequential": "hardhat test test/**/*.test.ts",
"test:trace": "hardhat test test/**/*.test.ts --trace --disabletracer",
"test:fulltrace": "hardhat test test/**/*.test.ts --fulltrace --disabletracer",
"test:watch": "hardhat watch test",
"test:watch": "SKIP_GAS_REPORT=true SKIP_CONTRACT_SIZE=true hardhat watch test",
"test:integration": "hardhat test test/integration/**/*.ts",
"test:integration:trace": "hardhat test test/integration/**/*.ts --trace --disabletracer",
"test:integration:fulltrace": "hardhat test test/integration/**/*.ts --fulltrace --disabletracer",
Expand Down Expand Up @@ -91,7 +91,7 @@
"lint-staged": "15.2.10",
"prettier": "3.4.1",
"prettier-plugin-solidity": "1.4.1",
"solhint": "5.0.3",
"solhint": "5.0.4",
"solhint-plugin-lido": "0.0.4",
"solidity-coverage": "0.8.14",
"ts-node": "10.9.2",
Expand Down
Loading

0 comments on commit 50d8b6e

Please sign in to comment.