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: make VaultHub pausable #914

Merged
merged 9 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
49 changes: 49 additions & 0 deletions contracts/0.8.25/utils/PausableUntilWithRoles.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// 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
*/
folkyatina marked this conversation as resolved.
Show resolved Hide resolved
function resume() external onlyRole(RESUME_ROLE) {
_resume();
}

/**
* @notice Pause the contract
folkyatina marked this conversation as resolved.
Show resolved Hide resolved
* @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 the contract until a specific timestamp
folkyatina marked this conversation as resolved.
Show resolved Hide resolved
* @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;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need to support ^0.8.9? Why not 0.8.25? We have a 0.8.9 copy already.


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

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);
}
}
Dismissed Show dismissed Hide dismissed
}
9 changes: 6 additions & 3 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function loadAccounts(networkName: string) {
const config: HardhatUserConfig = {
defaultNetwork: "hardhat",
gasReporter: {
enabled: true,
enabled: process.env.SKIP_GAS_REPORT ? false : true,
},
networks: {
"hardhat": {
Expand Down Expand Up @@ -198,7 +198,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 @@ -225,7 +228,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 @@ -90,7 +90,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
Loading