Skip to content

Commit

Permalink
feat: pausable beacon deposits
Browse files Browse the repository at this point in the history
  • Loading branch information
tamtamchik committed Jan 15, 2025
1 parent eac8eb9 commit 41c1c7e
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 3 deletions.
18 changes: 17 additions & 1 deletion contracts/0.8.25/vaults/Delegation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ contract Delegation is Dashboard {
* - votes operator fee;
* - votes on vote lifetime;
* - votes on ownership transfer;
* - claims curator due.
* - claims curator due;
* - pauses deposits to beacon chain;
* - resumes deposits to beacon chain.
*/
bytes32 public constant CURATOR_ROLE = keccak256("Vault.Delegation.CuratorRole");

Expand Down Expand Up @@ -346,6 +348,20 @@ contract Delegation is Dashboard {
_voluntaryDisconnect();
}

/**
* @notice Pauses deposits to beacon chain from the StakingVault.
*/
function pauseBeaconDeposits() external onlyRole(CURATOR_ROLE) {
IStakingVault(stakingVault).pauseBeaconDeposits();
}

/**
* @notice Resumes deposits to beacon chain from the StakingVault.
*/
function resumeBeaconDeposits() external onlyRole(CURATOR_ROLE) {
IStakingVault(stakingVault).resumeBeaconDeposits();
}

/**
* @dev Modifier that implements a mechanism for multi-role committee approval.
* Each unique function call (identified by msg.data: selector + arguments) requires
Expand Down
51 changes: 49 additions & 2 deletions contracts/0.8.25/vaults/StakingVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import {ERC1967Utils} from "@openzeppelin/contracts-v5.0.2/proxy/ERC1967/ERC1967
* - `withdraw()`
* - `requestValidatorExit()`
* - `rebalance()`
* - `pauseDeposits()`
* - `resumeDeposits()`
* - Operator:
* - `depositToBeaconChain()`
* - VaultHub:
Expand All @@ -60,12 +62,14 @@ contract StakingVault is IStakingVault, IBeaconProxy, BeaconChainDepositLogistic
* @custom:locked Amount of ether locked on StakingVault by VaultHub and cannot be withdrawn by owner
* @custom:inOutDelta Net difference between ether funded and withdrawn from StakingVault
* @custom:operator Address of the node operator
* @custom:depositsPaused Whether beacon deposits are paused by the vault owner
*/
struct ERC7201Storage {
Report report;
uint128 locked;
int128 inOutDelta;
address operator;
bool depositsPaused;
}

/**
Expand Down Expand Up @@ -217,8 +221,15 @@ contract StakingVault is IStakingVault, IBeaconProxy, BeaconChainDepositLogistic
* @return Report struct containing valuation and inOutDelta from last report
*/
function latestReport() external view returns (IStakingVault.Report memory) {
ERC7201Storage storage $ = _getStorage();
return $.report;
return _getStorage().report;
}

/**
* @notice Returns whether deposits are paused by the vault owner
* @return True if deposits are paused
*/
function areBeaconDepositsPaused() external view returns (bool) {
return _getStorage().depositsPaused;
}

/**
Expand Down Expand Up @@ -317,6 +328,7 @@ contract StakingVault is IStakingVault, IBeaconProxy, BeaconChainDepositLogistic
if (_numberOfDeposits == 0) revert ZeroArgument("_numberOfDeposits");
if (!isBalanced()) revert Unbalanced();
if (msg.sender != _getStorage().operator) revert NotAuthorized("depositToBeaconChain", msg.sender);
if (_getStorage().depositsPaused) revert BeaconChainDepositsNotAllowed();

_makeBeaconChainDeposits32ETH(_numberOfDeposits, bytes.concat(withdrawalCredentials()), _pubkeys, _signatures);
emit DepositedToBeaconChain(msg.sender, _numberOfDeposits, _numberOfDeposits * 32 ether);
Expand Down Expand Up @@ -389,6 +401,26 @@ contract StakingVault is IStakingVault, IBeaconProxy, BeaconChainDepositLogistic
emit Reported(_valuation, _inOutDelta, _locked);
}

/**
* @notice Pauses deposits to beacon chain
* @dev Can only be called by the vault owner
*/
function pauseBeaconDeposits() external onlyOwner {
_getStorage().depositsPaused = true;

emit BeaconDepositsPaused();
}

/**
* @notice Resumes deposits to beacon chain
* @dev Can only be called by the vault owner
*/
function resumeBeaconDeposits() external onlyOwner {
_getStorage().depositsPaused = false;

emit BeaconDepositsResumed();
}

function _getStorage() private pure returns (ERC7201Storage storage $) {
assembly {
$.slot := ERC721_STORAGE_LOCATION
Expand Down Expand Up @@ -449,6 +481,16 @@ contract StakingVault is IStakingVault, IBeaconProxy, BeaconChainDepositLogistic
*/
event OnReportFailed(bytes reason);

/**
* @notice Emitted when deposits to beacon chain are paused
*/
event BeaconDepositsPaused();

/**
* @notice Emitted when deposits to beacon chain are resumed
*/
event BeaconDepositsResumed();

/**
* @notice Thrown when an invalid zero value is passed
* @param name Name of the argument that was zero
Expand Down Expand Up @@ -511,4 +553,9 @@ contract StakingVault is IStakingVault, IBeaconProxy, BeaconChainDepositLogistic
* @notice Thrown when the onReport() hook reverts with an Out of Gas error
*/
error UnrecoverableError();

/**
* @notice Thrown when trying to deposit to beacon chain while deposits are paused
*/
error BeaconChainDepositsNotAllowed();
}
3 changes: 3 additions & 0 deletions contracts/0.8.25/vaults/interfaces/IStakingVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface IStakingVault {
function isBalanced() external view returns (bool);
function unlocked() external view returns (uint256);
function inOutDelta() external view returns (int256);
function areBeaconDepositsPaused() external view returns (bool);
function withdrawalCredentials() external view returns (bytes32);
function fund() external payable;
function withdraw(address _recipient, uint256 _ether) external;
Expand All @@ -40,6 +41,8 @@ interface IStakingVault {
function requestValidatorExit(bytes calldata _pubkeys) external;
function lock(uint256 _locked) external;
function rebalance(uint256 _ether) external;
function pauseBeaconDeposits() external;
function resumeBeaconDeposits() external;
function latestReport() external view returns (Report memory);
function report(uint256 _valuation, int256 _inOutDelta, uint256 _locked) external;
}
28 changes: 28 additions & 0 deletions test/0.8.25/vaults/delegation/delegation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,4 +623,32 @@ describe("Delegation.sol", () => {
expect(await vault.owner()).to.equal(newOwner);
});
});

context("pauseBeaconDeposits", () => {
it("reverts if the caller is not a curator", async () => {
await expect(delegation.connect(stranger).pauseBeaconDeposits()).to.be.revertedWithCustomError(
delegation,
"AccessControlUnauthorizedAccount",
);
});

it("pauses the beacon deposits", async () => {
await expect(delegation.connect(curator).pauseBeaconDeposits()).to.emit(vault, "BeaconDepositsPaused");
expect(await vault.areBeaconDepositsPaused()).to.be.true;
});
});

context("resumeBeaconDeposits", () => {
it("reverts if the caller is not a curator", async () => {
await expect(delegation.connect(stranger).resumeBeaconDeposits()).to.be.revertedWithCustomError(
delegation,
"AccessControlUnauthorizedAccount",
);
});

it("resumes the beacon deposits", async () => {
await expect(delegation.connect(curator).resumeBeaconDeposits()).to.emit(vault, "BeaconDepositsResumed");
expect(await vault.areBeaconDepositsPaused()).to.be.false;
});
});
});
43 changes: 43 additions & 0 deletions test/0.8.25/vaults/staking-vault/staking-vault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ describe("StakingVault", () => {
);
expect(await stakingVault.valuation()).to.equal(0n);
expect(await stakingVault.isBalanced()).to.be.true;
expect(await stakingVault.areBeaconDepositsPaused()).to.be.false;
});
});

Expand Down Expand Up @@ -294,6 +295,40 @@ describe("StakingVault", () => {
});
});

context("pauseBeaconDeposits", () => {
it("reverts if called by a non-owner", async () => {
await expect(stakingVault.connect(stranger).pauseBeaconDeposits())
.to.be.revertedWithCustomError(stakingVault, "OwnableUnauthorizedAccount")
.withArgs(await stranger.getAddress());
});

it("allows to pause deposits", async () => {
await expect(stakingVault.connect(vaultOwner).pauseBeaconDeposits()).to.emit(
stakingVault,
"BeaconDepositsPaused",
);
expect(await stakingVault.areBeaconDepositsPaused()).to.be.true;
});
});

context("resumeBeaconDeposits", () => {
it("reverts if called by a non-owner", async () => {
await expect(stakingVault.connect(stranger).resumeBeaconDeposits())
.to.be.revertedWithCustomError(stakingVault, "OwnableUnauthorizedAccount")
.withArgs(await stranger.getAddress());
});

it("allows to resume deposits", async () => {
await stakingVault.connect(vaultOwner).pauseBeaconDeposits();

await expect(stakingVault.connect(vaultOwner).resumeBeaconDeposits()).to.emit(
stakingVault,
"BeaconDepositsResumed",
);
expect(await stakingVault.areBeaconDepositsPaused()).to.be.false;
});
});

context("depositToBeaconChain", () => {
it("reverts if called by a non-operator", async () => {
await expect(stakingVault.connect(stranger).depositToBeaconChain(1, "0x", "0x"))
Expand All @@ -315,6 +350,14 @@ describe("StakingVault", () => {
);
});

it("reverts if the deposits are paused", async () => {
await stakingVault.connect(vaultOwner).pauseBeaconDeposits();
await expect(stakingVault.connect(operator).depositToBeaconChain(1, "0x", "0x")).to.be.revertedWithCustomError(
stakingVault,
"BeaconChainDepositsNotAllowed",
);
});

it("makes deposits to the beacon chain and emits the DepositedToBeaconChain event", async () => {
await stakingVault.fund({ value: ether("32") });

Expand Down

0 comments on commit 41c1c7e

Please sign in to comment.