From c0bd5c2744a15c6a1ede5d63dd9f74b8f38e8b40 Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Tue, 26 Nov 2024 17:00:00 +0500 Subject: [PATCH 01/21] chore: enable gas reporter --- hardhat.config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hardhat.config.ts b/hardhat.config.ts index a193b18c0..482205831 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -12,6 +12,7 @@ import "hardhat-tracer"; import "hardhat-watcher"; import "hardhat-ignore-warnings"; import "hardhat-contract-sizer"; +import "hardhat-gas-reporter"; import { globSync } from "glob"; import { TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS } from "hardhat/builtin-tasks/task-names"; import { HardhatUserConfig, subtask } from "hardhat/config"; @@ -50,6 +51,9 @@ function loadAccounts(networkName: string) { const config: HardhatUserConfig = { defaultNetwork: "hardhat", + gasReporter: { + enabled: true, + }, networks: { "hardhat": { // setting base fee to 0 to avoid extra calculations doesn't work :( From e85791b96c31cf8599385f4b3d60402cd67ae4b7 Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Tue, 26 Nov 2024 18:49:03 +0500 Subject: [PATCH 02/21] feat: delegation layer with committee actions --- contracts/0.8.25/vaults/VaultDashboard.sol | 4 +- .../0.8.25/vaults/VaultDelegationLayer.sol | 265 ++++++++++++++++++ ...kingVault__MockForVaultDelegationLayer.sol | 24 ++ .../vault-delegation-layer-voting.test.ts | 181 ++++++++++++ 4 files changed, 472 insertions(+), 2 deletions(-) create mode 100644 contracts/0.8.25/vaults/VaultDelegationLayer.sol create mode 100644 test/0.8.25/vaults/contracts/StakingVault__MockForVaultDelegationLayer.sol create mode 100644 test/0.8.25/vaults/vault-delegation-layer-voting.test.ts diff --git a/contracts/0.8.25/vaults/VaultDashboard.sol b/contracts/0.8.25/vaults/VaultDashboard.sol index 34f4b3cfd..0385c5fe3 100644 --- a/contracts/0.8.25/vaults/VaultDashboard.sol +++ b/contracts/0.8.25/vaults/VaultDashboard.sol @@ -82,7 +82,7 @@ contract VaultDashboard is AccessControlEnumerable { /// VAULT MANAGEMENT /// - function transferStakingVaultOwnership(address _newOwner) external onlyRole(OWNER) { + function transferStakingVaultOwnership(address _newOwner) public virtual onlyRole(OWNER) { OwnableUpgradeable(address(stakingVault)).transferOwnership(_newOwner); } @@ -138,7 +138,7 @@ contract VaultDashboard is AccessControlEnumerable { _; } - /// EVENTS // + /// EVENTS /// event Initialized(); /// ERRORS /// diff --git a/contracts/0.8.25/vaults/VaultDelegationLayer.sol b/contracts/0.8.25/vaults/VaultDelegationLayer.sol new file mode 100644 index 000000000..8095406e9 --- /dev/null +++ b/contracts/0.8.25/vaults/VaultDelegationLayer.sol @@ -0,0 +1,265 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +// See contracts/COMPILERS.md +pragma solidity 0.8.25; + +import {AccessControlEnumerable} from "@openzeppelin/contracts-v5.0.2/access/extensions/AccessControlEnumerable.sol"; +import {OwnableUpgradeable} from "contracts/openzeppelin/5.0.2/upgradeable/access/OwnableUpgradeable.sol"; +import {IStakingVault} from "./interfaces/IStakingVault.sol"; +import {IReportReceiver} from "./interfaces/IReportReceiver.sol"; +import {VaultDashboard} from "./VaultDashboard.sol"; +import {Math256} from "contracts/common/lib/Math256.sol"; + +// TODO: natspec +// TODO: events + +// VaultDelegationLayer: Delegates vault operations to different parties: +// - Manager: manages fees +// - Staker: can fund the vault and withdraw funds +// - Operator: can claim performance due and assigns Keymaster sub-role +// - Keymaster: Operator's sub-role for depositing to beacon chain +// - Plumber: manages liquidity, i.e. mints and burns stETH +// - Lido DAO: acts on behalf of Lido DAO (Lido Agent, EasyTrack, etc.) +contract VaultDelegationLayer is VaultDashboard, IReportReceiver { + uint256 private constant BP_BASE = 100_00; + uint256 private constant MAX_FEE = BP_BASE; + + bytes32 public constant STAKER_ROLE = keccak256("Vault.VaultDelegationLayer.StakerRole"); + bytes32 public constant OPERATOR_ROLE = keccak256("Vault.VaultDelegationLayer.OperatorRole"); + bytes32 public constant KEY_MASTER_ROLE = keccak256("Vault.VaultDelegationLayer.KeyMasterRole"); + bytes32 public constant TOKEN_MASTER_ROLE = keccak256("Vault.VaultDelegationLayer.TokenMasterRole"); + bytes32 public constant LIDO_DAO_ROLE = keccak256("Vault.VaultDelegationLayer.LidoDAORole"); + + IStakingVault.Report public lastClaimedReport; + + uint256 public managementFee; + uint256 public performanceFee; + uint256 public managementDue; + + mapping(bytes32 callId => mapping(bytes32 role => uint256 timestamp)) public votings; + + constructor(address _stETH) VaultDashboard(_stETH) {} + + // TODO: adding fix LIDO DAO role + function initialize(address _defaultAdmin, address _stakingVault) external override { + _initialize(_defaultAdmin, _stakingVault); + _setRoleAdmin(KEY_MASTER_ROLE, OPERATOR_ROLE); + _setRoleAdmin(OPERATOR_ROLE, LIDO_DAO_ROLE); + } + + /// * * * * * VIEW FUNCTIONS * * * * * /// + + function withdrawable() public view returns (uint256) { + uint256 reserved = Math256.max(stakingVault.locked(), managementDue + performanceDue()); + uint256 value = stakingVault.valuation(); + + if (reserved > value) { + return 0; + } + + return value - reserved; + } + + function performanceDue() public view returns (uint256) { + IStakingVault.Report memory latestReport = stakingVault.latestReport(); + + int128 rewardsAccrued = int128(latestReport.valuation - lastClaimedReport.valuation) - + (latestReport.inOutDelta - lastClaimedReport.inOutDelta); + + if (rewardsAccrued > 0) { + return (uint128(rewardsAccrued) * performanceFee) / BP_BASE; + } else { + return 0; + } + } + + function ownershipTransferCommittee() public pure returns (bytes32[] memory) { + bytes32[] memory roles = new bytes32[](3); + + roles[0] = MANAGER_ROLE; + roles[1] = OPERATOR_ROLE; + roles[2] = LIDO_DAO_ROLE; + + return roles; + } + + function performanceFeeCommittee() public pure returns (bytes32[] memory) { + bytes32[] memory roles = new bytes32[](2); + + roles[0] = MANAGER_ROLE; + roles[1] = OPERATOR_ROLE; + + return roles; + } + + function setManagementFee(uint256 _newManagementFee) external onlyRole(MANAGER_ROLE) { + if (_newManagementFee > MAX_FEE) revert NewFeeCannotExceedMaxFee(); + + managementFee = _newManagementFee; + } + + function setPerformanceFee(uint256 _newPerformanceFee) external onlyIfVotedBy(performanceFeeCommittee(), 7 days) { + if (_newPerformanceFee > MAX_FEE) revert NewFeeCannotExceedMaxFee(); + if (performanceDue() > 0) revert PerformanceDueUnclaimed(); + + performanceFee = _newPerformanceFee; + } + + function claimManagementDue(address _recipient, bool _liquid) external onlyRole(MANAGER_ROLE) { + if (_recipient == address(0)) revert ZeroArgument("_recipient"); + + if (!stakingVault.isHealthy()) { + revert VaultNotHealthy(); + } + + uint256 due = managementDue; + + if (due > 0) { + managementDue = 0; + + if (_liquid) { + vaultHub.mintStethBackedByVault(address(stakingVault), _recipient, due); + } else { + _withdrawDue(_recipient, due); + } + } + } + + function fund() external payable override onlyRole(STAKER_ROLE) { + stakingVault.fund{value: msg.value}(); + } + + function withdraw(address _recipient, uint256 _ether) external override onlyRole(STAKER_ROLE) { + if (_recipient == address(0)) revert ZeroArgument("_recipient"); + if (_ether == 0) revert ZeroArgument("_ether"); + if (withdrawable() < _ether) revert InsufficientWithdrawableAmount(withdrawable(), _ether); + + stakingVault.withdraw(_recipient, _ether); + } + + function depositToBeaconChain( + uint256 _numberOfDeposits, + bytes calldata _pubkeys, + bytes calldata _signatures + ) external override onlyRole(KEY_MASTER_ROLE) { + stakingVault.depositToBeaconChain(_numberOfDeposits, _pubkeys, _signatures); + } + + function claimPerformanceDue(address _recipient, bool _liquid) external onlyRole(OPERATOR_ROLE) { + if (_recipient == address(0)) revert ZeroArgument("_recipient"); + + uint256 due = performanceDue(); + + if (due > 0) { + lastClaimedReport = stakingVault.latestReport(); + + if (_liquid) { + vaultHub.mintStethBackedByVault(address(stakingVault), _recipient, due); + } else { + _withdrawDue(_recipient, due); + } + } + } + + /// * * * * * PLUMBER FUNCTIONS * * * * * /// + + function mint( + address _recipient, + uint256 _tokens + ) external payable override onlyRole(TOKEN_MASTER_ROLE) fundAndProceed { + vaultHub.mintStethBackedByVault(address(stakingVault), _recipient, _tokens); + } + + function burn(uint256 _tokens) external override onlyRole(TOKEN_MASTER_ROLE) { + stETH.transferFrom(msg.sender, address(vaultHub), _tokens); + vaultHub.burnStethBackedByVault(address(stakingVault), _tokens); + } + + /// * * * * * VAULT CALLBACK * * * * * /// + + // solhint-disable-next-line no-unused-vars + function onReport(uint256 _valuation, int256 _inOutDelta, uint256 _locked) external { + if (msg.sender != address(stakingVault)) revert OnlyVaultCanCallOnReportHook(); + + managementDue += (_valuation * managementFee) / 365 / BP_BASE; + } + + /// * * * * * QUORUM FUNCTIONS * * * * * /// + + function transferStakingVaultOwnership( + address _newOwner + ) public override onlyIfVotedBy(ownershipTransferCommittee(), 7 days) { + OwnableUpgradeable(address(stakingVault)).transferOwnership(_newOwner); + } + + /// * * * * * INTERNAL FUNCTIONS * * * * * /// + + function _withdrawDue(address _recipient, uint256 _ether) internal { + int256 unlocked = int256(stakingVault.valuation()) - int256(stakingVault.locked()); + uint256 unreserved = unlocked >= 0 ? uint256(unlocked) : 0; + if (unreserved < _ether) revert InsufficientUnlockedAmount(unreserved, _ether); + + stakingVault.withdraw(_recipient, _ether); + } + + /// @notice Requires approval from all committee members within a voting period + /// @dev Uses a bitmap to track new votes within the call instead of updating storage immediately, + /// this way we avoid unnecessary storage writes if the vote is deciding + /// because the votes will reset anyway + /// @param _committee Array of role identifiers that form the voting committee + /// @param _votingPeriod Time window in seconds during which votes remain valid + /// @custom:throws UnauthorizedCaller if caller has none of the committee roles + /// @custom:security Votes expire after _votingPeriod seconds to prevent stale approvals + modifier onlyIfVotedBy(bytes32[] memory _committee, uint256 _votingPeriod) { + bytes32 callId = keccak256(msg.data); + uint256 committeeSize = _committee.length; + uint256 votingStart = block.timestamp - _votingPeriod; + uint256 voteTally = 0; + uint256 votesToUpdateBitmap = 0; + + for (uint256 i = 0; i < committeeSize; ++i) { + bytes32 role = _committee[i]; + + if (super.hasRole(role, msg.sender)) { + voteTally++; + votesToUpdateBitmap |= (1 << i); + + emit RoleMemberVoted(msg.sender, role, block.timestamp, msg.data); + } else if (votings[callId][role] >= votingStart) { + voteTally++; + } + } + + if (votesToUpdateBitmap == 0) revert UnauthorizedCaller(); + + if (voteTally == committeeSize) { + for (uint256 i = 0; i < committeeSize; ++i) { + bytes32 role = _committee[i]; + delete votings[callId][role]; + } + _; + } else { + for (uint256 i = 0; i < committeeSize; ++i) { + if ((votesToUpdateBitmap & (1 << i)) != 0) { + bytes32 role = _committee[i]; + votings[callId][role] = block.timestamp; + } + } + } + } + + /// * * * * * EVENTS * * * * * /// + + event RoleMemberVoted(address member, bytes32 role, uint256 timestamp, bytes data); + + /// * * * * * ERRORS * * * * * /// + + error UnauthorizedCaller(); + error NewFeeCannotExceedMaxFee(); + error PerformanceDueUnclaimed(); + error InsufficientUnlockedAmount(uint256 unlocked, uint256 requested); + error VaultNotHealthy(); + error OnlyVaultCanCallOnReportHook(); + error FeeCannotExceed100(); +} diff --git a/test/0.8.25/vaults/contracts/StakingVault__MockForVaultDelegationLayer.sol b/test/0.8.25/vaults/contracts/StakingVault__MockForVaultDelegationLayer.sol new file mode 100644 index 000000000..75c22c5fb --- /dev/null +++ b/test/0.8.25/vaults/contracts/StakingVault__MockForVaultDelegationLayer.sol @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +// See contracts/COMPILERS.md +pragma solidity 0.8.25; + +import {IStakingVault} from "contracts/0.8.25/vaults/interfaces/IStakingVault.sol"; +import {OwnableUpgradeable} from "contracts/openzeppelin/5.0.2/upgradeable/access/OwnableUpgradeable.sol"; + +contract StakingVault__MockForVaultDelegationLayer is OwnableUpgradeable { + address public constant vaultHub = address(0xABCD); + + function latestReport() public pure returns (IStakingVault.Report memory) { + return IStakingVault.Report({valuation: 1 ether, inOutDelta: 0}); + } + + constructor() { + _transferOwnership(msg.sender); + } + + function initialize(address _owner) external { + _transferOwnership(_owner); + } +} diff --git a/test/0.8.25/vaults/vault-delegation-layer-voting.test.ts b/test/0.8.25/vaults/vault-delegation-layer-voting.test.ts new file mode 100644 index 000000000..abd1ebf96 --- /dev/null +++ b/test/0.8.25/vaults/vault-delegation-layer-voting.test.ts @@ -0,0 +1,181 @@ +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { advanceChainTime, certainAddress, days, proxify } from "lib"; +import { Snapshot } from "test/suite"; +import { StakingVault__MockForVaultDelegationLayer, VaultDelegationLayer } from "typechain-types"; + +describe.only("VaultDelegationLayer:Voting", () => { + let deployer: HardhatEthersSigner; + let owner: HardhatEthersSigner; + let manager: HardhatEthersSigner; + let operator: HardhatEthersSigner; + let lidoDao: HardhatEthersSigner; + let stranger: HardhatEthersSigner; + + let stakingVault: StakingVault__MockForVaultDelegationLayer; + let vaultDelegationLayer: VaultDelegationLayer; + + let originalState: string; + + before(async () => { + [deployer, owner, manager, operator, lidoDao, stranger] = await ethers.getSigners(); + + const steth = certainAddress("vault-delegation-layer-voting-steth"); + stakingVault = await ethers.deployContract("StakingVault__MockForVaultDelegationLayer"); + const impl = await ethers.deployContract("VaultDelegationLayer", [steth]); + // use a regular proxy for now + [vaultDelegationLayer] = await proxify({ impl, admin: owner, caller: deployer }); + + await vaultDelegationLayer.initialize(owner, stakingVault); + expect(await vaultDelegationLayer.isInitialized()).to.be.true; + expect(await vaultDelegationLayer.hasRole(await vaultDelegationLayer.OWNER(), owner)).to.be.true; + expect(await vaultDelegationLayer.vaultHub()).to.equal(await stakingVault.vaultHub()); + + await stakingVault.initialize(await vaultDelegationLayer.getAddress()); + + vaultDelegationLayer = vaultDelegationLayer.connect(owner); + }); + + beforeEach(async () => { + originalState = await Snapshot.take(); + }); + + afterEach(async () => { + await Snapshot.restore(originalState); + }); + + describe("setPerformanceFee", () => { + it("reverts if the caller does not have the required role", async () => { + expect(vaultDelegationLayer.connect(stranger).setPerformanceFee(100)).to.be.revertedWithCustomError( + vaultDelegationLayer, + "UnauthorizedCaller", + ); + }); + + it("executes if called by all distinct committee members", async () => { + await vaultDelegationLayer.grantRole(await vaultDelegationLayer.MANAGER_ROLE(), manager); + await vaultDelegationLayer.grantRole(await vaultDelegationLayer.LIDO_DAO_ROLE(), lidoDao); + await vaultDelegationLayer.connect(lidoDao).grantRole(await vaultDelegationLayer.OPERATOR_ROLE(), operator); + + expect(await vaultDelegationLayer.hasRole(await vaultDelegationLayer.MANAGER_ROLE(), manager)).to.be.true; + expect(await vaultDelegationLayer.hasRole(await vaultDelegationLayer.OPERATOR_ROLE(), operator)).to.be.true; + + const previousFee = await vaultDelegationLayer.performanceFee(); + const newFee = previousFee + 1n; + + // remains unchanged + await vaultDelegationLayer.connect(manager).setPerformanceFee(newFee); + expect(await vaultDelegationLayer.performanceFee()).to.equal(previousFee); + + // updated + await vaultDelegationLayer.connect(operator).setPerformanceFee(newFee); + expect(await vaultDelegationLayer.performanceFee()).to.equal(newFee); + }); + + it("executes if called by a single member with all roles", async () => { + await vaultDelegationLayer.grantRole(await vaultDelegationLayer.MANAGER_ROLE(), manager); + await vaultDelegationLayer.grantRole(await vaultDelegationLayer.LIDO_DAO_ROLE(), lidoDao); + await vaultDelegationLayer.connect(lidoDao).grantRole(await vaultDelegationLayer.OPERATOR_ROLE(), manager); + + const previousFee = await vaultDelegationLayer.performanceFee(); + const newFee = previousFee + 1n; + + // updated with a single transaction + await vaultDelegationLayer.connect(manager).setPerformanceFee(newFee); + expect(await vaultDelegationLayer.performanceFee()).to.equal(newFee); + }) + + it("does not execute if the vote is expired", async () => { + await vaultDelegationLayer.grantRole(await vaultDelegationLayer.MANAGER_ROLE(), manager); + await vaultDelegationLayer.grantRole(await vaultDelegationLayer.LIDO_DAO_ROLE(), lidoDao); + await vaultDelegationLayer.connect(lidoDao).grantRole(await vaultDelegationLayer.OPERATOR_ROLE(), operator); + + expect(await vaultDelegationLayer.hasRole(await vaultDelegationLayer.MANAGER_ROLE(), manager)).to.be.true; + expect(await vaultDelegationLayer.hasRole(await vaultDelegationLayer.OPERATOR_ROLE(), operator)).to.be.true; + + const previousFee = await vaultDelegationLayer.performanceFee(); + const newFee = previousFee + 1n; + + // remains unchanged + await vaultDelegationLayer.connect(manager).setPerformanceFee(newFee); + expect(await vaultDelegationLayer.performanceFee()).to.equal(previousFee); + + await advanceChainTime(days(7n) + 1n); + + // remains unchanged + await vaultDelegationLayer.connect(operator).setPerformanceFee(newFee); + expect(await vaultDelegationLayer.performanceFee()).to.equal(previousFee); + }); + }); + + + describe("transferStakingVaultOwnership", () => { + it("reverts if the caller does not have the required role", async () => { + expect(vaultDelegationLayer.connect(stranger).transferStakingVaultOwnership(certainAddress("vault-delegation-layer-voting-new-owner"))).to.be.revertedWithCustomError( + vaultDelegationLayer, + "UnauthorizedCaller", + ); + }); + + it("executes if called by all distinct committee members", async () => { + await vaultDelegationLayer.grantRole(await vaultDelegationLayer.MANAGER_ROLE(), manager); + await vaultDelegationLayer.grantRole(await vaultDelegationLayer.LIDO_DAO_ROLE(), lidoDao); + await vaultDelegationLayer.connect(lidoDao).grantRole(await vaultDelegationLayer.OPERATOR_ROLE(), operator); + + expect(await vaultDelegationLayer.hasRole(await vaultDelegationLayer.MANAGER_ROLE(), manager)).to.be.true; + expect(await vaultDelegationLayer.hasRole(await vaultDelegationLayer.OPERATOR_ROLE(), operator)).to.be.true; + + const newOwner = certainAddress("vault-delegation-layer-voting-new-owner"); + + // remains unchanged + await vaultDelegationLayer.connect(manager).transferStakingVaultOwnership(newOwner); + expect(await stakingVault.owner()).to.equal(vaultDelegationLayer); + + // remains unchanged + await vaultDelegationLayer.connect(operator).transferStakingVaultOwnership(newOwner); + expect(await stakingVault.owner()).to.equal(vaultDelegationLayer); + + // updated + await vaultDelegationLayer.connect(lidoDao).transferStakingVaultOwnership(newOwner); + expect(await stakingVault.owner()).to.equal(newOwner); + }); + + it("executes if called by a single member with all roles", async () => { + await vaultDelegationLayer.grantRole(await vaultDelegationLayer.MANAGER_ROLE(), lidoDao); + await vaultDelegationLayer.grantRole(await vaultDelegationLayer.LIDO_DAO_ROLE(), lidoDao); + await vaultDelegationLayer.connect(lidoDao).grantRole(await vaultDelegationLayer.OPERATOR_ROLE(), lidoDao); + + const newOwner = certainAddress("vault-delegation-layer-voting-new-owner"); + + // updated with a single transaction + await vaultDelegationLayer.connect(lidoDao).transferStakingVaultOwnership(newOwner); + expect(await stakingVault.owner()).to.equal(newOwner); + }) + + it("does not execute if the vote is expired", async () => { + await vaultDelegationLayer.grantRole(await vaultDelegationLayer.MANAGER_ROLE(), manager); + await vaultDelegationLayer.grantRole(await vaultDelegationLayer.LIDO_DAO_ROLE(), lidoDao); + await vaultDelegationLayer.connect(lidoDao).grantRole(await vaultDelegationLayer.OPERATOR_ROLE(), operator); + + expect(await vaultDelegationLayer.hasRole(await vaultDelegationLayer.MANAGER_ROLE(), manager)).to.be.true; + expect(await vaultDelegationLayer.hasRole(await vaultDelegationLayer.OPERATOR_ROLE(), operator)).to.be.true; + + const newOwner = certainAddress("vault-delegation-layer-voting-new-owner"); + + // remains unchanged + await vaultDelegationLayer.connect(manager).transferStakingVaultOwnership(newOwner); + expect(await stakingVault.owner()).to.equal(vaultDelegationLayer); + + // remains unchanged + await vaultDelegationLayer.connect(operator).transferStakingVaultOwnership(newOwner); + expect(await stakingVault.owner()).to.equal(vaultDelegationLayer); + + await advanceChainTime(days(7n) + 1n); + + // remains unchanged + await vaultDelegationLayer.connect(lidoDao).transferStakingVaultOwnership(newOwner); + expect(await stakingVault.owner()).to.equal(vaultDelegationLayer); + }); + }); +}); From ab5264790f06aceacf3e1cf60119e1b9adebfb7e Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Tue, 26 Nov 2024 18:51:58 +0500 Subject: [PATCH 03/21] fix: remove misleading comments --- contracts/0.8.25/vaults/VaultDelegationLayer.sol | 6 ------ 1 file changed, 6 deletions(-) diff --git a/contracts/0.8.25/vaults/VaultDelegationLayer.sol b/contracts/0.8.25/vaults/VaultDelegationLayer.sol index 8095406e9..368539cb0 100644 --- a/contracts/0.8.25/vaults/VaultDelegationLayer.sol +++ b/contracts/0.8.25/vaults/VaultDelegationLayer.sol @@ -162,8 +162,6 @@ contract VaultDelegationLayer is VaultDashboard, IReportReceiver { } } - /// * * * * * PLUMBER FUNCTIONS * * * * * /// - function mint( address _recipient, uint256 _tokens @@ -176,8 +174,6 @@ contract VaultDelegationLayer is VaultDashboard, IReportReceiver { vaultHub.burnStethBackedByVault(address(stakingVault), _tokens); } - /// * * * * * VAULT CALLBACK * * * * * /// - // solhint-disable-next-line no-unused-vars function onReport(uint256 _valuation, int256 _inOutDelta, uint256 _locked) external { if (msg.sender != address(stakingVault)) revert OnlyVaultCanCallOnReportHook(); @@ -185,8 +181,6 @@ contract VaultDelegationLayer is VaultDashboard, IReportReceiver { managementDue += (_valuation * managementFee) / 365 / BP_BASE; } - /// * * * * * QUORUM FUNCTIONS * * * * * /// - function transferStakingVaultOwnership( address _newOwner ) public override onlyIfVotedBy(ownershipTransferCommittee(), 7 days) { From 043b26e69c5d94901061d23a06da8e08fbbfdcd4 Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Thu, 28 Nov 2024 13:24:07 +0500 Subject: [PATCH 04/21] feat: update owner contracts --- .../vaults/StVaultOwnerWithDashboard.sol | 180 ++++++++++++++++++ .../0.8.25/vaults/VaultDelegationLayer.sol | 95 +++++---- 2 files changed, 237 insertions(+), 38 deletions(-) create mode 100644 contracts/0.8.25/vaults/StVaultOwnerWithDashboard.sol diff --git a/contracts/0.8.25/vaults/StVaultOwnerWithDashboard.sol b/contracts/0.8.25/vaults/StVaultOwnerWithDashboard.sol new file mode 100644 index 000000000..32a8948c0 --- /dev/null +++ b/contracts/0.8.25/vaults/StVaultOwnerWithDashboard.sol @@ -0,0 +1,180 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +// See contracts/COMPILERS.md +pragma solidity 0.8.25; + +import {IStakingVault} from "./interfaces/IStakingVault.sol"; +import {AccessControlEnumerable} from "@openzeppelin/contracts-v5.0.2/access/extensions/AccessControlEnumerable.sol"; +import {IERC20} from "@openzeppelin/contracts-v5.0.2/token/ERC20/IERC20.sol"; +import {OwnableUpgradeable} from "contracts/openzeppelin/5.0.2/upgradeable/access/OwnableUpgradeable.sol"; +import {VaultHub} from "./VaultHub.sol"; + +contract StVaultOwnerWithDashboard is AccessControlEnumerable { + address private immutable _SELF; + bool public isInitialized; + + IERC20 public immutable stETH; + IStakingVault public stakingVault; + VaultHub public vaultHub; + + constructor(address _stETH) { + if (_stETH == address(0)) revert ZeroArgument("_stETH"); + + _SELF = address(this); + stETH = IERC20(_stETH); + } + + /// INITIALIZATION /// + + function initialize(address _defaultAdmin, address _stakingVault) external virtual { + _initialize(_defaultAdmin, _stakingVault); + } + + function _initialize(address _defaultAdmin, address _stakingVault) internal { + if (_defaultAdmin == address(0)) revert ZeroArgument("_defaultAdmin"); + if (_stakingVault == address(0)) revert ZeroArgument("_stakingVault"); + if (isInitialized) revert AlreadyInitialized(); + if (address(this) == _SELF) revert NonProxyCallsForbidden(); + + isInitialized = true; + + _grantRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + + stakingVault = IStakingVault(_stakingVault); + vaultHub = VaultHub(stakingVault.vaultHub()); + + emit Initialized(); + } + + /// VIEW FUNCTIONS /// + + function vaultSocket() public view returns (VaultHub.VaultSocket memory) { + return vaultHub.vaultSocket(address(stakingVault)); + } + + function shareLimit() external view returns (uint96) { + return vaultSocket().shareLimit; + } + + function sharesMinted() external view returns (uint96) { + return vaultSocket().sharesMinted; + } + + function reserveRatio() external view returns (uint16) { + return vaultSocket().reserveRatio; + } + + function thresholdReserveRatio() external view returns (uint16) { + return vaultSocket().reserveRatioThreshold; + } + + function treasuryFee() external view returns (uint16) { + return vaultSocket().treasuryFeeBP; + } + + /// VAULT MANAGEMENT /// + + function transferStVaultOwnership(address _newOwner) external virtual onlyRole(DEFAULT_ADMIN_ROLE) { + _transferStVaultOwnership(_newOwner); + } + + function disconnectFromVaultHub() external payable virtual onlyRole(DEFAULT_ADMIN_ROLE) { + _disconnectFromVaultHub(); + } + + function fund() external payable virtual onlyRole(DEFAULT_ADMIN_ROLE) { + _fund(); + } + + function withdraw(address _recipient, uint256 _ether) external virtual onlyRole(DEFAULT_ADMIN_ROLE) { + _withdraw(_recipient, _ether); + } + + function requestValidatorExit(bytes calldata _validatorPublicKey) external onlyRole(DEFAULT_ADMIN_ROLE) { + _requestValidatorExit(_validatorPublicKey); + } + + function depositToBeaconChain( + uint256 _numberOfDeposits, + bytes calldata _pubkeys, + bytes calldata _signatures + ) external virtual onlyRole(DEFAULT_ADMIN_ROLE) { + _depositToBeaconChain(_numberOfDeposits, _pubkeys, _signatures); + } + + function mint( + address _recipient, + uint256 _tokens + ) external payable virtual onlyRole(DEFAULT_ADMIN_ROLE) fundAndProceed { + _mint(_recipient, _tokens); + } + + function burn(uint256 _tokens) external virtual onlyRole(DEFAULT_ADMIN_ROLE) { + _burn(_tokens); + } + + function rebalanceVault(uint256 _ether) external payable virtual onlyRole(DEFAULT_ADMIN_ROLE) fundAndProceed { + _rebalanceVault(_ether); + } + + /// INTERNAL /// + + modifier fundAndProceed() { + if (msg.value > 0) { + _fund(); + } + _; + } + + function _transferStVaultOwnership(address _newOwner) internal { + OwnableUpgradeable(address(stakingVault)).transferOwnership(_newOwner); + } + + function _disconnectFromVaultHub() internal { + vaultHub.disconnectVault(address(stakingVault)); + } + + function _fund() internal { + stakingVault.fund{value: msg.value}(); + } + + function _withdraw(address _recipient, uint256 _ether) internal { + stakingVault.withdraw(_recipient, _ether); + } + + function _requestValidatorExit(bytes calldata _validatorPublicKey) internal { + stakingVault.requestValidatorExit(_validatorPublicKey); + } + + function _depositToBeaconChain( + uint256 _numberOfDeposits, + bytes calldata _pubkeys, + bytes calldata _signatures + ) internal { + stakingVault.depositToBeaconChain(_numberOfDeposits, _pubkeys, _signatures); + } + + function _mint(address _recipient, uint256 _tokens) internal { + vaultHub.mintStethBackedByVault(address(stakingVault), _recipient, _tokens); + } + + function _burn(uint256 _tokens) internal { + stETH.transferFrom(msg.sender, address(vaultHub), _tokens); + vaultHub.burnStethBackedByVault(address(stakingVault), _tokens); + } + + function _rebalanceVault(uint256 _ether) internal { + stakingVault.rebalance(_ether); + } + + /// EVENTS /// + event Initialized(); + + /// ERRORS /// + + error ZeroArgument(string); + error InsufficientWithdrawableAmount(uint256 withdrawable, uint256 requested); + error NonProxyCallsForbidden(); + error AlreadyInitialized(); +} diff --git a/contracts/0.8.25/vaults/VaultDelegationLayer.sol b/contracts/0.8.25/vaults/VaultDelegationLayer.sol index 368539cb0..1c61460c9 100644 --- a/contracts/0.8.25/vaults/VaultDelegationLayer.sol +++ b/contracts/0.8.25/vaults/VaultDelegationLayer.sol @@ -8,28 +8,26 @@ import {AccessControlEnumerable} from "@openzeppelin/contracts-v5.0.2/access/ext import {OwnableUpgradeable} from "contracts/openzeppelin/5.0.2/upgradeable/access/OwnableUpgradeable.sol"; import {IStakingVault} from "./interfaces/IStakingVault.sol"; import {IReportReceiver} from "./interfaces/IReportReceiver.sol"; -import {VaultDashboard} from "./VaultDashboard.sol"; +import {StVaultOwnerWithDashboard} from "./StVaultOwnerWithDashboard.sol"; import {Math256} from "contracts/common/lib/Math256.sol"; -// TODO: natspec -// TODO: events - -// VaultDelegationLayer: Delegates vault operations to different parties: -// - Manager: manages fees -// - Staker: can fund the vault and withdraw funds -// - Operator: can claim performance due and assigns Keymaster sub-role -// - Keymaster: Operator's sub-role for depositing to beacon chain -// - Plumber: manages liquidity, i.e. mints and burns stETH -// - Lido DAO: acts on behalf of Lido DAO (Lido Agent, EasyTrack, etc.) -contract VaultDelegationLayer is VaultDashboard, IReportReceiver { +// kinda out of ideas what to name this contract +contract StVaultOwnerWithDelegation is StVaultOwnerWithDashboard, IReportReceiver { + /// CONSTANTS /// + uint256 private constant BP_BASE = 100_00; uint256 private constant MAX_FEE = BP_BASE; - bytes32 public constant STAKER_ROLE = keccak256("Vault.VaultDelegationLayer.StakerRole"); - bytes32 public constant OPERATOR_ROLE = keccak256("Vault.VaultDelegationLayer.OperatorRole"); - bytes32 public constant KEY_MASTER_ROLE = keccak256("Vault.VaultDelegationLayer.KeyMasterRole"); - bytes32 public constant TOKEN_MASTER_ROLE = keccak256("Vault.VaultDelegationLayer.TokenMasterRole"); - bytes32 public constant LIDO_DAO_ROLE = keccak256("Vault.VaultDelegationLayer.LidoDAORole"); + /// ROLES /// + + bytes32 public constant MANAGER_ROLE = keccak256("Vault.StVaultOwnerWithDelegation.ManagerRole"); + bytes32 public constant STAKER_ROLE = keccak256("Vault.StVaultOwnerWithDelegation.StakerRole"); + bytes32 public constant OPERATOR_ROLE = keccak256("Vault.StVaultOwnerWithDelegation.OperatorRole"); + bytes32 public constant KEY_MASTER_ROLE = keccak256("Vault.StVaultOwnerWithDelegation.KeyMasterRole"); + bytes32 public constant TOKEN_MASTER_ROLE = keccak256("Vault.StVaultOwnerWithDelegation.TokenMasterRole"); + bytes32 public constant LIDO_DAO_ROLE = keccak256("Vault.StVaultOwnerWithDelegation.LidoDAORole"); + + /// STATE /// IStakingVault.Report public lastClaimedReport; @@ -37,18 +35,24 @@ contract VaultDelegationLayer is VaultDashboard, IReportReceiver { uint256 public performanceFee; uint256 public managementDue; + /// VOTING /// + mapping(bytes32 callId => mapping(bytes32 role => uint256 timestamp)) public votings; - constructor(address _stETH) VaultDashboard(_stETH) {} + constructor(address _stETH) StVaultOwnerWithDashboard(_stETH) {} + + /// INITIALIZATION /// - // TODO: adding fix LIDO DAO role function initialize(address _defaultAdmin, address _stakingVault) external override { _initialize(_defaultAdmin, _stakingVault); - _setRoleAdmin(KEY_MASTER_ROLE, OPERATOR_ROLE); + + _grantRole(LIDO_DAO_ROLE, _defaultAdmin); _setRoleAdmin(OPERATOR_ROLE, LIDO_DAO_ROLE); + _setRoleAdmin(LIDO_DAO_ROLE, LIDO_DAO_ROLE); + _setRoleAdmin(KEY_MASTER_ROLE, OPERATOR_ROLE); } - /// * * * * * VIEW FUNCTIONS * * * * * /// + /// VIEW FUNCTIONS /// function withdrawable() public view returns (uint256) { uint256 reserved = Math256.max(stakingVault.locked(), managementDue + performanceDue()); @@ -93,6 +97,8 @@ contract VaultDelegationLayer is VaultDashboard, IReportReceiver { return roles; } + /// FEE MANAGEMENT /// + function setManagementFee(uint256 _newManagementFee) external onlyRole(MANAGER_ROLE) { if (_newManagementFee > MAX_FEE) revert NewFeeCannotExceedMaxFee(); @@ -126,8 +132,22 @@ contract VaultDelegationLayer is VaultDashboard, IReportReceiver { } } + /// VAULT MANAGEMENT /// + + function transferStVaultOwnership( + address _newOwner + ) public override onlyIfVotedBy(ownershipTransferCommittee(), 7 days) { + _transferStVaultOwnership(_newOwner); + } + + function disconnectFromVaultHub() external payable override onlyRole(MANAGER_ROLE) { + _disconnectFromVaultHub(); + } + + /// VAULT OPERATIONS /// + function fund() external payable override onlyRole(STAKER_ROLE) { - stakingVault.fund{value: msg.value}(); + _fund(); } function withdraw(address _recipient, uint256 _ether) external override onlyRole(STAKER_ROLE) { @@ -135,7 +155,7 @@ contract VaultDelegationLayer is VaultDashboard, IReportReceiver { if (_ether == 0) revert ZeroArgument("_ether"); if (withdrawable() < _ether) revert InsufficientWithdrawableAmount(withdrawable(), _ether); - stakingVault.withdraw(_recipient, _ether); + _withdraw(_recipient, _ether); } function depositToBeaconChain( @@ -143,7 +163,7 @@ contract VaultDelegationLayer is VaultDashboard, IReportReceiver { bytes calldata _pubkeys, bytes calldata _signatures ) external override onlyRole(KEY_MASTER_ROLE) { - stakingVault.depositToBeaconChain(_numberOfDeposits, _pubkeys, _signatures); + _depositToBeaconChain(_numberOfDeposits, _pubkeys, _signatures); } function claimPerformanceDue(address _recipient, bool _liquid) external onlyRole(OPERATOR_ROLE) { @@ -155,7 +175,7 @@ contract VaultDelegationLayer is VaultDashboard, IReportReceiver { lastClaimedReport = stakingVault.latestReport(); if (_liquid) { - vaultHub.mintStethBackedByVault(address(stakingVault), _recipient, due); + _mint(_recipient, due); } else { _withdrawDue(_recipient, due); } @@ -166,35 +186,34 @@ contract VaultDelegationLayer is VaultDashboard, IReportReceiver { address _recipient, uint256 _tokens ) external payable override onlyRole(TOKEN_MASTER_ROLE) fundAndProceed { - vaultHub.mintStethBackedByVault(address(stakingVault), _recipient, _tokens); + _mint(_recipient, _tokens); } function burn(uint256 _tokens) external override onlyRole(TOKEN_MASTER_ROLE) { - stETH.transferFrom(msg.sender, address(vaultHub), _tokens); - vaultHub.burnStethBackedByVault(address(stakingVault), _tokens); + _burn(_tokens); + } + + function rebalanceVault(uint256 _ether) external payable override onlyRole(MANAGER_ROLE) fundAndProceed { + _rebalanceVault(_ether); } + /// REPORT HANDLING /// + // solhint-disable-next-line no-unused-vars function onReport(uint256 _valuation, int256 _inOutDelta, uint256 _locked) external { - if (msg.sender != address(stakingVault)) revert OnlyVaultCanCallOnReportHook(); + if (msg.sender != address(stakingVault)) revert OnlyStVaultCanCallOnReportHook(); managementDue += (_valuation * managementFee) / 365 / BP_BASE; } - function transferStakingVaultOwnership( - address _newOwner - ) public override onlyIfVotedBy(ownershipTransferCommittee(), 7 days) { - OwnableUpgradeable(address(stakingVault)).transferOwnership(_newOwner); - } - - /// * * * * * INTERNAL FUNCTIONS * * * * * /// + /// INTERNAL /// function _withdrawDue(address _recipient, uint256 _ether) internal { int256 unlocked = int256(stakingVault.valuation()) - int256(stakingVault.locked()); uint256 unreserved = unlocked >= 0 ? uint256(unlocked) : 0; if (unreserved < _ether) revert InsufficientUnlockedAmount(unreserved, _ether); - stakingVault.withdraw(_recipient, _ether); + _withdraw(_recipient, _ether); } /// @notice Requires approval from all committee members within a voting period @@ -254,6 +273,6 @@ contract VaultDelegationLayer is VaultDashboard, IReportReceiver { error PerformanceDueUnclaimed(); error InsufficientUnlockedAmount(uint256 unlocked, uint256 requested); error VaultNotHealthy(); - error OnlyVaultCanCallOnReportHook(); + error OnlyStVaultCanCallOnReportHook(); error FeeCannotExceed100(); } From 110212398bd5f6436861242110a7440de063d5ba Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Thu, 28 Nov 2024 13:24:42 +0500 Subject: [PATCH 05/21] feat: reanme del owner --- .../{VaultDelegationLayer.sol => StVaultOwnerWithDelegation.sol} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename contracts/0.8.25/vaults/{VaultDelegationLayer.sol => StVaultOwnerWithDelegation.sol} (100%) diff --git a/contracts/0.8.25/vaults/VaultDelegationLayer.sol b/contracts/0.8.25/vaults/StVaultOwnerWithDelegation.sol similarity index 100% rename from contracts/0.8.25/vaults/VaultDelegationLayer.sol rename to contracts/0.8.25/vaults/StVaultOwnerWithDelegation.sol From 29cde40ca170f4b12768a6257d0d9d24309c955f Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Thu, 28 Nov 2024 15:14:07 +0500 Subject: [PATCH 06/21] fix: clean up --- .../vaults/StVaultOwnerWithDashboard.sol | 164 ++++++++++- .../vaults/StVaultOwnerWithDelegation.sol | 278 +++++++++++++++--- contracts/0.8.25/vaults/StakingVault.sol | 14 +- 3 files changed, 391 insertions(+), 65 deletions(-) diff --git a/contracts/0.8.25/vaults/StVaultOwnerWithDashboard.sol b/contracts/0.8.25/vaults/StVaultOwnerWithDashboard.sol index 32a8948c0..85d98f244 100644 --- a/contracts/0.8.25/vaults/StVaultOwnerWithDashboard.sol +++ b/contracts/0.8.25/vaults/StVaultOwnerWithDashboard.sol @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2024 Lido // SPDX-License-Identifier: GPL-3.0 +// SPDX-FileCopyrightText: 2024 Lido // See contracts/COMPILERS.md pragma solidity 0.8.25; @@ -10,14 +10,35 @@ import {IERC20} from "@openzeppelin/contracts-v5.0.2/token/ERC20/IERC20.sol"; import {OwnableUpgradeable} from "contracts/openzeppelin/5.0.2/upgradeable/access/OwnableUpgradeable.sol"; import {VaultHub} from "./VaultHub.sol"; +/** + * @title StVaultOwnerWithDashboard + * @notice This contract is meant to be used as the owner of `StakingVault`. + * This contract improves the vault UX by bundling all functions from the vault and vault hub + * in this single contract. It provides administrative functions for managing the staking vault, + * including funding, withdrawing, depositing to the beacon chain, minting, burning, and rebalancing operations. + * All these functions are only callable by the account with the DEFAULT_ADMIN_ROLE. + */ contract StVaultOwnerWithDashboard is AccessControlEnumerable { + /// @notice Address of the implementation contract + /// @dev Used to prevent initialization in the implementation address private immutable _SELF; + + /// @notice Indicates whether the contract has been initialized bool public isInitialized; + /// @notice The stETH token contract IERC20 public immutable stETH; + + /// @notice The underlying `StakingVault` contract IStakingVault public stakingVault; + + /// @notice The `VaultHub` contract VaultHub public vaultHub; + /** + * @notice Constructor sets the stETH token address and the implementation contract address. + * @param _stETH Address of the stETH token contract. + */ constructor(address _stETH) { if (_stETH == address(0)) revert ZeroArgument("_stETH"); @@ -25,12 +46,20 @@ contract StVaultOwnerWithDashboard is AccessControlEnumerable { stETH = IERC20(_stETH); } - /// INITIALIZATION /// - + /** + * @notice Initializes the contract with the default admin and `StakingVault` address. + * @param _defaultAdmin Address to be granted the `DEFAULT_ADMIN_ROLE`, i.e. the actual owner of the stVault + * @param _stakingVault Address of the `StakingVault` contract. + */ function initialize(address _defaultAdmin, address _stakingVault) external virtual { _initialize(_defaultAdmin, _stakingVault); } + /** + * @dev Internal initialize function. + * @param _defaultAdmin Address to be granted the `DEFAULT_ADMIN_ROLE` + * @param _stakingVault Address of the `StakingVault` contract. + */ function _initialize(address _defaultAdmin, address _stakingVault) internal { if (_defaultAdmin == address(0)) revert ZeroArgument("_defaultAdmin"); if (_stakingVault == address(0)) revert ZeroArgument("_stakingVault"); @@ -47,54 +76,103 @@ contract StVaultOwnerWithDashboard is AccessControlEnumerable { emit Initialized(); } - /// VIEW FUNCTIONS /// + // ==================== View Functions ==================== + /** + * @notice Returns the vault socket data for the staking vault. + * @return VaultSocket struct containing vault data + */ function vaultSocket() public view returns (VaultHub.VaultSocket memory) { return vaultHub.vaultSocket(address(stakingVault)); } + /** + * @notice Returns the stETH share limit of the vault + * @return The share limit as a uint96 + */ function shareLimit() external view returns (uint96) { return vaultSocket().shareLimit; } + /** + * @notice Returns the number of stETHshares minted + * @return The shares minted as a uint96 + */ function sharesMinted() external view returns (uint96) { return vaultSocket().sharesMinted; } + /** + * @notice Returns the reserve ratio of the vault + * @return The reserve ratio as a uint16 + */ function reserveRatio() external view returns (uint16) { return vaultSocket().reserveRatio; } + /** + * @notice Returns the threshold reserve ratio of the vault. + * @return The threshold reserve ratio as a uint16. + */ function thresholdReserveRatio() external view returns (uint16) { return vaultSocket().reserveRatioThreshold; } + /** + * @notice Returns the treasury fee basis points. + * @return The treasury fee in basis points as a uint16. + */ function treasuryFee() external view returns (uint16) { return vaultSocket().treasuryFeeBP; } - /// VAULT MANAGEMENT /// + // ==================== Vault Management Functions ==================== + /** + * @notice Transfers ownership of the staking vault to a new owner. + * @param _newOwner Address of the new owner. + */ function transferStVaultOwnership(address _newOwner) external virtual onlyRole(DEFAULT_ADMIN_ROLE) { _transferStVaultOwnership(_newOwner); } + /** + * @notice Disconnects the staking vault from the vault hub. + */ function disconnectFromVaultHub() external payable virtual onlyRole(DEFAULT_ADMIN_ROLE) { _disconnectFromVaultHub(); } + /** + * @notice Funds the staking vault with ether + */ function fund() external payable virtual onlyRole(DEFAULT_ADMIN_ROLE) { _fund(); } + /** + * @notice Withdraws ether from the staking vault to a recipient + * @param _recipient Address of the recipient + * @param _ether Amount of ether to withdraw + */ function withdraw(address _recipient, uint256 _ether) external virtual onlyRole(DEFAULT_ADMIN_ROLE) { _withdraw(_recipient, _ether); } + /** + * @notice Requests the exit of a validator from the staking vault + * @param _validatorPublicKey Public key of the validator to exit + */ function requestValidatorExit(bytes calldata _validatorPublicKey) external onlyRole(DEFAULT_ADMIN_ROLE) { _requestValidatorExit(_validatorPublicKey); } + /** + * @notice Deposits validators to the beacon chain + * @param _numberOfDeposits Number of validator deposits + * @param _pubkeys Concatenated public keys of the validators + * @param _signatures Concatenated signatures of the validators + */ function depositToBeaconChain( uint256 _numberOfDeposits, bytes calldata _pubkeys, @@ -103,6 +181,11 @@ contract StVaultOwnerWithDashboard is AccessControlEnumerable { _depositToBeaconChain(_numberOfDeposits, _pubkeys, _signatures); } + /** + * @notice Mints stETH tokens backed by the vault to a recipient. + * @param _recipient Address of the recipient + * @param _tokens Amount of tokens to mint + */ function mint( address _recipient, uint256 _tokens @@ -110,16 +193,27 @@ contract StVaultOwnerWithDashboard is AccessControlEnumerable { _mint(_recipient, _tokens); } + /** + * @notice Burns stETH tokens from the sender backed by the vault + * @param _tokens Amount of tokens to burn + */ function burn(uint256 _tokens) external virtual onlyRole(DEFAULT_ADMIN_ROLE) { _burn(_tokens); } - function rebalanceVault(uint256 _ether) external payable virtual onlyRole(DEFAULT_ADMIN_ROLE) fundAndProceed { + /** + * @notice Rebalances the vault by transferring ether + * @param _ether Amount of ether to rebalance + */ + function rebalanceVault(uint256 _ether) external virtual onlyRole(DEFAULT_ADMIN_ROLE) { _rebalanceVault(_ether); } - /// INTERNAL /// + // ==================== Internal Functions ==================== + /** + * @dev Modifier to fund the staking vault if msg.value > 0 + */ modifier fundAndProceed() { if (msg.value > 0) { _fund(); @@ -127,26 +221,51 @@ contract StVaultOwnerWithDashboard is AccessControlEnumerable { _; } + /** + * @dev Transfers ownership of the staking vault to a new owner + * @param _newOwner Address of the new owner + */ function _transferStVaultOwnership(address _newOwner) internal { OwnableUpgradeable(address(stakingVault)).transferOwnership(_newOwner); } + /** + * @dev Disconnects the staking vault from the vault hub + */ function _disconnectFromVaultHub() internal { vaultHub.disconnectVault(address(stakingVault)); } + /** + * @dev Funds the staking vault with the ether sent in the transaction + */ function _fund() internal { stakingVault.fund{value: msg.value}(); } + /** + * @dev Withdraws ether from the staking vault to a recipient + * @param _recipient Address of the recipient + * @param _ether Amount of ether to withdraw + */ function _withdraw(address _recipient, uint256 _ether) internal { stakingVault.withdraw(_recipient, _ether); } + /** + * @dev Requests the exit of a validator from the staking vault + * @param _validatorPublicKey Public key of the validator to exit + */ function _requestValidatorExit(bytes calldata _validatorPublicKey) internal { stakingVault.requestValidatorExit(_validatorPublicKey); } + /** + * @dev Deposits validators to the beacon chain + * @param _numberOfDeposits Number of validator deposits + * @param _pubkeys Concatenated public keys of the validators + * @param _signatures Concatenated signatures of the validators + */ function _depositToBeaconChain( uint256 _numberOfDeposits, bytes calldata _pubkeys, @@ -155,26 +274,51 @@ contract StVaultOwnerWithDashboard is AccessControlEnumerable { stakingVault.depositToBeaconChain(_numberOfDeposits, _pubkeys, _signatures); } + /** + * @dev Mints stETH tokens backed by the vault to a recipient + * @param _recipient Address of the recipient + * @param _tokens Amount of tokens to mint + */ function _mint(address _recipient, uint256 _tokens) internal { vaultHub.mintStethBackedByVault(address(stakingVault), _recipient, _tokens); } + /** + * @dev Burns stETH tokens from the sender backed by the vault + * @param _tokens Amount of tokens to burn + */ function _burn(uint256 _tokens) internal { stETH.transferFrom(msg.sender, address(vaultHub), _tokens); vaultHub.burnStethBackedByVault(address(stakingVault), _tokens); } + /** + * @dev Rebalances the vault by transferring ether + * @param _ether Amount of ether to rebalance + */ function _rebalanceVault(uint256 _ether) internal { stakingVault.rebalance(_ether); } - /// EVENTS /// + // ==================== Events ==================== + + /// @notice Emitted when the contract is initialized event Initialized(); - /// ERRORS /// + // ==================== Errors ==================== + + /// @notice Error for zero address arguments + /// @param argName Name of the argument that is zero + error ZeroArgument(string argName); - error ZeroArgument(string); + /// @notice Error when the withdrawable amount is insufficient. + /// @param withdrawable The amount that is withdrawable + /// @param requested The amount requested to withdraw error InsufficientWithdrawableAmount(uint256 withdrawable, uint256 requested); + + /// @notice Error when direct calls to the implementation are forbidden error NonProxyCallsForbidden(); + + /// @notice Error when the contract is already initialized. error AlreadyInitialized(); } diff --git a/contracts/0.8.25/vaults/StVaultOwnerWithDelegation.sol b/contracts/0.8.25/vaults/StVaultOwnerWithDelegation.sol index 1c61460c9..46f48cd27 100644 --- a/contracts/0.8.25/vaults/StVaultOwnerWithDelegation.sol +++ b/contracts/0.8.25/vaults/StVaultOwnerWithDelegation.sol @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2024 Lido // SPDX-License-Identifier: GPL-3.0 +// SPDX-FileCopyrightText: 2024 Lido // See contracts/COMPILERS.md pragma solidity 0.8.25; @@ -11,50 +11,158 @@ import {IReportReceiver} from "./interfaces/IReportReceiver.sol"; import {StVaultOwnerWithDashboard} from "./StVaultOwnerWithDashboard.sol"; import {Math256} from "contracts/common/lib/Math256.sol"; -// kinda out of ideas what to name this contract +/** + * @title StVaultOwnerWithDelegation + * @notice This contract serves as an owner for `StakingVault` with additional delegation capabilities. + * It extends `StVaultOwnerWithDashboard` and implements `IReportReceiver`. + * The contract provides administrative functions for managing the staking vault, + * including funding, withdrawing, depositing to the beacon chain, minting, burning, + * rebalancing operations, and fee management. All these functions are only callable + * by accounts with the appropriate roles. + * + * @notice `IReportReceiver` is implemented to receive reports from the staking vault, which in turn + * receives the report from the vault hub. We need the report to calculate the accumulated management due. + * + * @notice The term "fee" is used to express the fee percentage as basis points, e.g. 5%, + * while "due" is the actual amount of the fee, e.g. 1 ether + */ contract StVaultOwnerWithDelegation is StVaultOwnerWithDashboard, IReportReceiver { - /// CONSTANTS /// - - uint256 private constant BP_BASE = 100_00; - uint256 private constant MAX_FEE = BP_BASE; - - /// ROLES /// - + // ==================== Constants ==================== + + uint256 private constant BP_BASE = 10000; // Basis points base (100%) + uint256 private constant MAX_FEE = BP_BASE; // Maximum fee in basis points (100%) + + // ==================== Roles ==================== + + /** + * @notice Role for the manager. + * Manager manages the vault on behalf of the owner. + * Manager can: + * - set the management fee + * - claim the management due + * - disconnect the vault from the vault hub + * - rebalance the vault + * - vote on ownership transfer + * - vote on performance fee changes + */ bytes32 public constant MANAGER_ROLE = keccak256("Vault.StVaultOwnerWithDelegation.ManagerRole"); + + /** + * @notice Role for the staker. + * Staker can: + * - fund the vault + * - withdraw from the vault + */ bytes32 public constant STAKER_ROLE = keccak256("Vault.StVaultOwnerWithDelegation.StakerRole"); + + /** @notice Role for the operator + * Operator can: + * - claim the performance due + * - vote on performance fee changes + * - vote on ownership transfer + * - set the Key Master role + */ bytes32 public constant OPERATOR_ROLE = keccak256("Vault.StVaultOwnerWithDelegation.OperatorRole"); + + /** + * @notice Role for the key master. + * Key master can: + * - deposit validators to the beacon chain + */ bytes32 public constant KEY_MASTER_ROLE = keccak256("Vault.StVaultOwnerWithDelegation.KeyMasterRole"); + + /** + * @notice Role for the token master. + * Token master can: + * - mint stETH tokens + * - burn stETH tokens + */ bytes32 public constant TOKEN_MASTER_ROLE = keccak256("Vault.StVaultOwnerWithDelegation.TokenMasterRole"); + + /** + * @notice Role for the Lido DAO. + * This can be the Lido DAO agent, EasyTrack or any other DAO decision-making system. + * Lido DAO can: + * - set the operator role + * - vote on ownership transfer + */ bytes32 public constant LIDO_DAO_ROLE = keccak256("Vault.StVaultOwnerWithDelegation.LidoDAORole"); - /// STATE /// + // ==================== State Variables ==================== + /// @notice The last report for which the performance due was claimed IStakingVault.Report public lastClaimedReport; + /// @notice Management fee in basis points uint256 public managementFee; + + /// @notice Performance fee in basis points uint256 public performanceFee; + + /** + * @notice Accumulated management fee due amount + * Management due is calculated as a percentage (`managementFee`) of the vault valuation increase + * since the last report. + */ uint256 public managementDue; - /// VOTING /// + // ==================== Voting ==================== - mapping(bytes32 callId => mapping(bytes32 role => uint256 timestamp)) public votings; + /// @notice Tracks votes for function calls requiring multi-role approval. + mapping(bytes32 => mapping(bytes32 => uint256)) public votings; - constructor(address _stETH) StVaultOwnerWithDashboard(_stETH) {} + // ==================== Initialization ==================== - /// INITIALIZATION /// + /** + * @notice Constructor sets the stETH token address. + * @param _stETH Address of the stETH token contract. + */ + constructor(address _stETH) StVaultOwnerWithDashboard(_stETH) {} + /** + * @notice Initializes the contract with the default admin and `StakingVault` address. + * Sets up roles and role administrators. + * @param _defaultAdmin Address to be granted the `DEFAULT_ADMIN_ROLE`. + * @param _stakingVault Address of the `StakingVault` contract. + */ function initialize(address _defaultAdmin, address _stakingVault) external override { _initialize(_defaultAdmin, _stakingVault); + /** + * Granting `LIDO_DAO_ROLE` to the default admin is needed to set the initial Lido DAO address + * in the `createVault` function in the vault factory, so that we don't have to pass it + * to this initialize function and break the inherited function signature. + * This role will be revoked in the `createVault` function in the vault factory and + * will only remain on the Lido DAO address + */ _grantRole(LIDO_DAO_ROLE, _defaultAdmin); + + /** + * The node operator in the vault must be approved by Lido DAO. + * The vault owner (`DEFAULT_ADMIN_ROLE`) cannot change the node operator. + */ _setRoleAdmin(OPERATOR_ROLE, LIDO_DAO_ROLE); + + /** + * Only Lido DAO can assign the Lido DAO role. + */ _setRoleAdmin(LIDO_DAO_ROLE, LIDO_DAO_ROLE); + + /** + * The operator role can change the key master role. + */ _setRoleAdmin(KEY_MASTER_ROLE, OPERATOR_ROLE); } - /// VIEW FUNCTIONS /// + // ==================== View Functions ==================== + /** + * @notice Returns the amount of ether that can be withdrawn from the vault + * accounting for the locked amount, the management due and the performance due. + * @return The withdrawable amount in ether. + */ function withdrawable() public view returns (uint256) { + // Question: shouldn't we reserve both locked + dues, not max(locked, dues)? uint256 reserved = Math256.max(stakingVault.locked(), managementDue + performanceDue()); uint256 value = stakingVault.valuation(); @@ -65,6 +173,10 @@ contract StVaultOwnerWithDelegation is StVaultOwnerWithDashboard, IReportReceive return value - reserved; } + /** + * @notice Calculates the performance fee due based on the latest report. + * @return The performance fee due in ether. + */ function performanceDue() public view returns (uint256) { IStakingVault.Report memory latestReport = stakingVault.latestReport(); @@ -78,46 +190,58 @@ contract StVaultOwnerWithDelegation is StVaultOwnerWithDashboard, IReportReceive } } + /** + * @notice Returns the committee roles required for transferring the ownership of the staking vault. + * @return An array of role identifiers. + */ function ownershipTransferCommittee() public pure returns (bytes32[] memory) { bytes32[] memory roles = new bytes32[](3); - roles[0] = MANAGER_ROLE; roles[1] = OPERATOR_ROLE; roles[2] = LIDO_DAO_ROLE; - return roles; } + /** + * @notice Returns the committee roles required for performance fee changes. + * @return An array of role identifiers. + */ function performanceFeeCommittee() public pure returns (bytes32[] memory) { bytes32[] memory roles = new bytes32[](2); - roles[0] = MANAGER_ROLE; roles[1] = OPERATOR_ROLE; - return roles; } - /// FEE MANAGEMENT /// + // ==================== Fee Management ==================== + /** + * @notice Sets the management fee. + * @param _newManagementFee The new management fee in basis points. + */ function setManagementFee(uint256 _newManagementFee) external onlyRole(MANAGER_ROLE) { if (_newManagementFee > MAX_FEE) revert NewFeeCannotExceedMaxFee(); - managementFee = _newManagementFee; } + /** + * @notice Sets the performance fee. + * @param _newPerformanceFee The new performance fee in basis points. + */ function setPerformanceFee(uint256 _newPerformanceFee) external onlyIfVotedBy(performanceFeeCommittee(), 7 days) { if (_newPerformanceFee > MAX_FEE) revert NewFeeCannotExceedMaxFee(); if (performanceDue() > 0) revert PerformanceDueUnclaimed(); - performanceFee = _newPerformanceFee; } + /** + * @notice Claims the accumulated management fee. + * @param _recipient Address of the recipient. + * @param _liquid If true, mints stETH tokens; otherwise, withdraws ether. + */ function claimManagementDue(address _recipient, bool _liquid) external onlyRole(MANAGER_ROLE) { if (_recipient == address(0)) revert ZeroArgument("_recipient"); - - if (!stakingVault.isHealthy()) { - revert VaultNotHealthy(); - } + if (!stakingVault.isHealthy()) revert VaultNotHealthy(); uint256 due = managementDue; @@ -132,32 +256,55 @@ contract StVaultOwnerWithDelegation is StVaultOwnerWithDashboard, IReportReceive } } - /// VAULT MANAGEMENT /// + // ==================== Vault Management Functions ==================== + /** + * @notice Transfers ownership of the staking vault to a new owner. + * Requires approval from the ownership transfer committee. + * @param _newOwner Address of the new owner. + */ function transferStVaultOwnership( address _newOwner ) public override onlyIfVotedBy(ownershipTransferCommittee(), 7 days) { _transferStVaultOwnership(_newOwner); } + /** + * @notice Disconnects the staking vault from the vault hub. + */ function disconnectFromVaultHub() external payable override onlyRole(MANAGER_ROLE) { _disconnectFromVaultHub(); } - /// VAULT OPERATIONS /// + // ==================== Vault Operations ==================== + /** + * @notice Funds the staking vault with ether. + */ function fund() external payable override onlyRole(STAKER_ROLE) { _fund(); } + /** + * @notice Withdraws ether from the staking vault to a recipient. + * @param _recipient Address of the recipient. + * @param _ether Amount of ether to withdraw. + */ function withdraw(address _recipient, uint256 _ether) external override onlyRole(STAKER_ROLE) { if (_recipient == address(0)) revert ZeroArgument("_recipient"); if (_ether == 0) revert ZeroArgument("_ether"); - if (withdrawable() < _ether) revert InsufficientWithdrawableAmount(withdrawable(), _ether); + uint256 available = withdrawable(); + if (available < _ether) revert InsufficientWithdrawableAmount(available, _ether); _withdraw(_recipient, _ether); } + /** + * @notice Deposits validators to the beacon chain. + * @param _numberOfDeposits Number of validator deposits. + * @param _pubkeys Concatenated public keys of the validators. + * @param _signatures Concatenated signatures of the validators. + */ function depositToBeaconChain( uint256 _numberOfDeposits, bytes calldata _pubkeys, @@ -166,6 +313,11 @@ contract StVaultOwnerWithDelegation is StVaultOwnerWithDashboard, IReportReceive _depositToBeaconChain(_numberOfDeposits, _pubkeys, _signatures); } + /** + * @notice Claims the performance fee due. + * @param _recipient Address of the recipient. + * @param _liquid If true, mints stETH tokens; otherwise, withdraws ether. + */ function claimPerformanceDue(address _recipient, bool _liquid) external onlyRole(OPERATOR_ROLE) { if (_recipient == address(0)) revert ZeroArgument("_recipient"); @@ -182,6 +334,11 @@ contract StVaultOwnerWithDelegation is StVaultOwnerWithDashboard, IReportReceive } } + /** + * @notice Mints stETH tokens backed by the vault to a recipient. + * @param _recipient Address of the recipient. + * @param _tokens Amount of tokens to mint. + */ function mint( address _recipient, uint256 _tokens @@ -189,25 +346,43 @@ contract StVaultOwnerWithDelegation is StVaultOwnerWithDashboard, IReportReceive _mint(_recipient, _tokens); } + /** + * @notice Burns stETH tokens from the sender backed by the vault. + * @param _tokens Amount of tokens to burn. + */ function burn(uint256 _tokens) external override onlyRole(TOKEN_MASTER_ROLE) { _burn(_tokens); } - function rebalanceVault(uint256 _ether) external payable override onlyRole(MANAGER_ROLE) fundAndProceed { + /** + * @notice Rebalances the vault by transferring ether. + * @param _ether Amount of ether to rebalance. + */ + function rebalanceVault(uint256 _ether) external override onlyRole(MANAGER_ROLE) { _rebalanceVault(_ether); } - /// REPORT HANDLING /// + // ==================== Report Handling ==================== - // solhint-disable-next-line no-unused-vars + /** + * @notice Hook called by the staking vault during the report in the staking vault. + * @param _valuation The new valuation of the vault. + * @param _inOutDelta The net inflow or outflow since the last report. + * @param _locked The amount of funds locked in the vault. + */ function onReport(uint256 _valuation, int256 _inOutDelta, uint256 _locked) external { if (msg.sender != address(stakingVault)) revert OnlyStVaultCanCallOnReportHook(); managementDue += (_valuation * managementFee) / 365 / BP_BASE; } - /// INTERNAL /// + // ==================== Internal Functions ==================== + /** + * @dev Withdraws the due amount to a recipient, ensuring sufficient unlocked funds. + * @param _recipient Address of the recipient. + * @param _ether Amount of ether to withdraw. + */ function _withdrawDue(address _recipient, uint256 _ether) internal { int256 unlocked = int256(stakingVault.valuation()) - int256(stakingVault.locked()); uint256 unreserved = unlocked >= 0 ? uint256(unlocked) : 0; @@ -216,14 +391,12 @@ contract StVaultOwnerWithDelegation is StVaultOwnerWithDashboard, IReportReceive _withdraw(_recipient, _ether); } - /// @notice Requires approval from all committee members within a voting period - /// @dev Uses a bitmap to track new votes within the call instead of updating storage immediately, - /// this way we avoid unnecessary storage writes if the vote is deciding - /// because the votes will reset anyway - /// @param _committee Array of role identifiers that form the voting committee - /// @param _votingPeriod Time window in seconds during which votes remain valid - /// @custom:throws UnauthorizedCaller if caller has none of the committee roles - /// @custom:security Votes expire after _votingPeriod seconds to prevent stale approvals + /** + * @dev Modifier that requires approval from all committee members within a voting period. + * Uses a bitmap to track new votes within the call instead of updating storage immediately. + * @param _committee Array of role identifiers that form the voting committee. + * @param _votingPeriod Time window in seconds during which votes remain valid. + */ modifier onlyIfVotedBy(bytes32[] memory _committee, uint256 _votingPeriod) { bytes32 callId = keccak256(msg.data); uint256 committeeSize = _committee.length; @@ -244,7 +417,7 @@ contract StVaultOwnerWithDelegation is StVaultOwnerWithDashboard, IReportReceive } } - if (votesToUpdateBitmap == 0) revert UnauthorizedCaller(); + if (votesToUpdateBitmap == 0) revert NotACommitteeMember(); if (voteTally == committeeSize) { for (uint256 i = 0; i < committeeSize; ++i) { @@ -262,17 +435,30 @@ contract StVaultOwnerWithDelegation is StVaultOwnerWithDashboard, IReportReceive } } - /// * * * * * EVENTS * * * * * /// + // ==================== Events ==================== + /// @notice Emitted when a role member votes on a function requiring committee approval. event RoleMemberVoted(address member, bytes32 role, uint256 timestamp, bytes data); - /// * * * * * ERRORS * * * * * /// + // ==================== Errors ==================== - error UnauthorizedCaller(); + /// @notice Thrown if the caller is not a member of the committee. + error NotACommitteeMember(); + + /// @notice Thrown if the new fee exceeds the maximum allowed fee. error NewFeeCannotExceedMaxFee(); + + /// @notice Thrown if the performance due is unclaimed. error PerformanceDueUnclaimed(); + + /// @notice Thrown if the unlocked amount is insufficient. + /// @param unlocked The amount that is unlocked. + /// @param requested The amount requested to withdraw. error InsufficientUnlockedAmount(uint256 unlocked, uint256 requested); + + /// @notice Error when the vault is not healthy. error VaultNotHealthy(); + + /// @notice Hook can only be called by the staking vault. error OnlyStVaultCanCallOnReportHook(); - error FeeCannotExceed100(); } diff --git a/contracts/0.8.25/vaults/StakingVault.sol b/contracts/0.8.25/vaults/StakingVault.sol index 5d3324c17..38e9084a7 100644 --- a/contracts/0.8.25/vaults/StakingVault.sol +++ b/contracts/0.8.25/vaults/StakingVault.sol @@ -20,7 +20,6 @@ contract StakingVault is IStakingVault, IBeaconProxy, VaultBeaconChainDepositor, /// @custom:storage-location erc7201:StakingVault.Vault struct VaultStorage { IStakingVault.Report report; - uint128 locked; int128 inOutDelta; } @@ -61,7 +60,7 @@ contract StakingVault is IStakingVault, IBeaconProxy, VaultBeaconChainDepositor, _transferOwnership(_owner); } - function version() public pure virtual returns(uint256) { + function version() public pure virtual returns (uint256) { return _version; } @@ -81,18 +80,14 @@ contract StakingVault is IStakingVault, IBeaconProxy, VaultBeaconChainDepositor, function valuation() public view returns (uint256) { VaultStorage storage $ = _getVaultStorage(); - return uint256(int256( - int128($.report.valuation) - + $.inOutDelta - - $.report.inOutDelta - )); + return uint256(int256(int128($.report.valuation) + $.inOutDelta - $.report.inOutDelta)); } function isHealthy() public view returns (bool) { return valuation() >= _getVaultStorage().locked; } - function locked() external view returns(uint256) { + function locked() external view returns (uint256) { return _getVaultStorage().locked; } @@ -105,7 +100,7 @@ contract StakingVault is IStakingVault, IBeaconProxy, VaultBeaconChainDepositor, return _valuation - _locked; } - function inOutDelta() external view returns(int256) { + function inOutDelta() external view returns (int256) { return _getVaultStorage().inOutDelta; } @@ -166,6 +161,7 @@ contract StakingVault is IStakingVault, IBeaconProxy, VaultBeaconChainDepositor, emit Locked(_locked); } + // TODO: SHOULD THIS BE PAYABLE? function rebalance(uint256 _ether) external { if (_ether == 0) revert ZeroArgument("_ether"); if (_ether > address(this).balance) revert InsufficientBalance(address(this).balance); From 137ced50e9fa2e246fa583be8aed62ebf840e047 Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Thu, 28 Nov 2024 18:01:04 +0500 Subject: [PATCH 07/21] test: update tests --- .../vaults/StVaultOwnerWithDashboard.sol | 4 +- .../vaults/StVaultOwnerWithDelegation.sol | 12 +- contracts/0.8.25/vaults/StakingVault.sol | 2 +- contracts/0.8.25/vaults/VaultDashboard.sol | 150 -------------- contracts/0.8.25/vaults/VaultFactory.sol | 97 +++++---- contracts/0.8.25/vaults/VaultStaffRoom.sol | 189 ------------------ .../vaults/interfaces/IStakingVault.sol | 2 +- lib/proxy.ts | 34 ++-- lib/state-file.ts | 2 +- scripts/scratch/steps/0145-deploy-vaults.ts | 4 +- ... => stvault-owner-with-delegation.test.ts} | 32 +-- .../vault-delegation-layer-voting.test.ts | 136 ++++++------- test/0.8.25/vaults/vault.test.ts | 15 +- test/0.8.25/vaults/vaultFactory.test.ts | 33 +-- .../vaults-happy-path.integration.ts | 37 ++-- 15 files changed, 218 insertions(+), 531 deletions(-) delete mode 100644 contracts/0.8.25/vaults/VaultDashboard.sol delete mode 100644 contracts/0.8.25/vaults/VaultStaffRoom.sol rename test/0.8.25/vaults/{vaultStaffRoom.test.ts => stvault-owner-with-delegation.test.ts} (65%) diff --git a/contracts/0.8.25/vaults/StVaultOwnerWithDashboard.sol b/contracts/0.8.25/vaults/StVaultOwnerWithDashboard.sol index 85d98f244..b4f206397 100644 --- a/contracts/0.8.25/vaults/StVaultOwnerWithDashboard.sol +++ b/contracts/0.8.25/vaults/StVaultOwnerWithDashboard.sol @@ -205,7 +205,7 @@ contract StVaultOwnerWithDashboard is AccessControlEnumerable { * @notice Rebalances the vault by transferring ether * @param _ether Amount of ether to rebalance */ - function rebalanceVault(uint256 _ether) external virtual onlyRole(DEFAULT_ADMIN_ROLE) { + function rebalanceVault(uint256 _ether) external payable virtual onlyRole(DEFAULT_ADMIN_ROLE) fundAndProceed { _rebalanceVault(_ether); } @@ -297,7 +297,7 @@ contract StVaultOwnerWithDashboard is AccessControlEnumerable { * @param _ether Amount of ether to rebalance */ function _rebalanceVault(uint256 _ether) internal { - stakingVault.rebalance(_ether); + stakingVault.rebalance{value: msg.value}(_ether); } // ==================== Events ==================== diff --git a/contracts/0.8.25/vaults/StVaultOwnerWithDelegation.sol b/contracts/0.8.25/vaults/StVaultOwnerWithDelegation.sol index 46f48cd27..40776e36f 100644 --- a/contracts/0.8.25/vaults/StVaultOwnerWithDelegation.sol +++ b/contracts/0.8.25/vaults/StVaultOwnerWithDelegation.sol @@ -138,15 +138,15 @@ contract StVaultOwnerWithDelegation is StVaultOwnerWithDashboard, IReportReceive _grantRole(LIDO_DAO_ROLE, _defaultAdmin); /** - * The node operator in the vault must be approved by Lido DAO. - * The vault owner (`DEFAULT_ADMIN_ROLE`) cannot change the node operator. + * Only Lido DAO can assign the Lido DAO role. */ - _setRoleAdmin(OPERATOR_ROLE, LIDO_DAO_ROLE); + _setRoleAdmin(LIDO_DAO_ROLE, LIDO_DAO_ROLE); /** - * Only Lido DAO can assign the Lido DAO role. + * The node operator in the vault must be approved by Lido DAO. + * The vault owner (`DEFAULT_ADMIN_ROLE`) cannot change the node operator. */ - _setRoleAdmin(LIDO_DAO_ROLE, LIDO_DAO_ROLE); + _setRoleAdmin(OPERATOR_ROLE, LIDO_DAO_ROLE); /** * The operator role can change the key master role. @@ -358,7 +358,7 @@ contract StVaultOwnerWithDelegation is StVaultOwnerWithDashboard, IReportReceive * @notice Rebalances the vault by transferring ether. * @param _ether Amount of ether to rebalance. */ - function rebalanceVault(uint256 _ether) external override onlyRole(MANAGER_ROLE) { + function rebalanceVault(uint256 _ether) external payable override onlyRole(MANAGER_ROLE) fundAndProceed { _rebalanceVault(_ether); } diff --git a/contracts/0.8.25/vaults/StakingVault.sol b/contracts/0.8.25/vaults/StakingVault.sol index 38e9084a7..5970b3853 100644 --- a/contracts/0.8.25/vaults/StakingVault.sol +++ b/contracts/0.8.25/vaults/StakingVault.sol @@ -162,7 +162,7 @@ contract StakingVault is IStakingVault, IBeaconProxy, VaultBeaconChainDepositor, } // TODO: SHOULD THIS BE PAYABLE? - function rebalance(uint256 _ether) external { + function rebalance(uint256 _ether) external payable { if (_ether == 0) revert ZeroArgument("_ether"); if (_ether > address(this).balance) revert InsufficientBalance(address(this).balance); // TODO: should we revert on msg.value > _ether diff --git a/contracts/0.8.25/vaults/VaultDashboard.sol b/contracts/0.8.25/vaults/VaultDashboard.sol deleted file mode 100644 index 0385c5fe3..000000000 --- a/contracts/0.8.25/vaults/VaultDashboard.sol +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Lido -// SPDX-License-Identifier: GPL-3.0 - -// See contracts/COMPILERS.md -pragma solidity 0.8.25; - -import {IStakingVault} from "./interfaces/IStakingVault.sol"; -import {AccessControlEnumerable} from "@openzeppelin/contracts-v5.0.2/access/extensions/AccessControlEnumerable.sol"; -import {IERC20} from "@openzeppelin/contracts-v5.0.2/token/ERC20/IERC20.sol"; -import {OwnableUpgradeable} from "contracts/openzeppelin/5.0.2/upgradeable/access/OwnableUpgradeable.sol"; -import {VaultHub} from "./VaultHub.sol"; - -// TODO: natspec -// TODO: think about the name - -contract VaultDashboard is AccessControlEnumerable { - bytes32 public constant OWNER = DEFAULT_ADMIN_ROLE; - bytes32 public constant MANAGER_ROLE = keccak256("Vault.VaultDashboard.ManagerRole"); - - IERC20 public immutable stETH; - address private immutable _SELF; - - bool public isInitialized; - IStakingVault public stakingVault; - VaultHub public vaultHub; - - constructor(address _stETH) { - if (_stETH == address(0)) revert ZeroArgument("_stETH"); - - _SELF = address(this); - stETH = IERC20(_stETH); - } - - function initialize(address _defaultAdmin, address _stakingVault) external virtual { - _initialize(_defaultAdmin, _stakingVault); - } - - function _initialize(address _defaultAdmin, address _stakingVault) internal { - if (_defaultAdmin == address(0)) revert ZeroArgument("_defaultAdmin"); - if (_stakingVault == address(0)) revert ZeroArgument("_stakingVault"); - if (isInitialized) revert AlreadyInitialized(); - - if (address(this) == _SELF) { - revert NonProxyCallsForbidden(); - } - - isInitialized = true; - - _grantRole(OWNER, _defaultAdmin); - - stakingVault = IStakingVault(_stakingVault); - vaultHub = VaultHub(stakingVault.vaultHub()); - - emit Initialized(); - } - - /// GETTERS /// - - function vaultSocket() external view returns (VaultHub.VaultSocket memory) { - return vaultHub.vaultSocket(address(stakingVault)); - } - - function shareLimit() external view returns (uint96) { - return vaultHub.vaultSocket(address(stakingVault)).shareLimit; - } - - function sharesMinted() external view returns (uint96) { - return vaultHub.vaultSocket(address(stakingVault)).sharesMinted; - } - - function reserveRatio() external view returns (uint16) { - return vaultHub.vaultSocket(address(stakingVault)).reserveRatio; - } - - function thresholdReserveRatioBP() external view returns (uint16) { - return vaultHub.vaultSocket(address(stakingVault)).reserveRatioThreshold; - } - - function treasuryFeeBP() external view returns (uint16) { - return vaultHub.vaultSocket(address(stakingVault)).treasuryFeeBP; - } - - /// VAULT MANAGEMENT /// - - function transferStakingVaultOwnership(address _newOwner) public virtual onlyRole(OWNER) { - OwnableUpgradeable(address(stakingVault)).transferOwnership(_newOwner); - } - - function disconnectFromHub() external payable onlyRole(MANAGER_ROLE) { - vaultHub.disconnectVault(address(stakingVault)); - } - - /// OPERATION /// - - function fund() external payable virtual onlyRole(MANAGER_ROLE) { - stakingVault.fund{value: msg.value}(); - } - - function withdraw(address _recipient, uint256 _ether) external virtual onlyRole(MANAGER_ROLE) { - stakingVault.withdraw(_recipient, _ether); - } - - function requestValidatorExit(bytes calldata _validatorPublicKey) external onlyRole(MANAGER_ROLE) { - stakingVault.requestValidatorExit(_validatorPublicKey); - } - - function depositToBeaconChain( - uint256 _numberOfDeposits, - bytes calldata _pubkeys, - bytes calldata _signatures - ) external virtual onlyRole(MANAGER_ROLE) { - stakingVault.depositToBeaconChain(_numberOfDeposits, _pubkeys, _signatures); - } - - /// LIQUIDITY /// - - function mint(address _recipient, uint256 _tokens) external payable virtual onlyRole(MANAGER_ROLE) fundAndProceed { - vaultHub.mintStethBackedByVault(address(stakingVault), _recipient, _tokens); - } - - function burn(uint256 _tokens) external virtual onlyRole(MANAGER_ROLE) { - stETH.transferFrom(msg.sender, address(vaultHub), _tokens); - vaultHub.burnStethBackedByVault(address(stakingVault), _tokens); - } - - /// REBALANCE /// - - function rebalanceVault(uint256 _ether) external payable virtual onlyRole(MANAGER_ROLE) fundAndProceed { - stakingVault.rebalance(_ether); - } - - /// MODIFIERS /// - - modifier fundAndProceed() { - if (msg.value > 0) { - stakingVault.fund{value: msg.value}(); - } - _; - } - - /// EVENTS /// - event Initialized(); - - /// ERRORS /// - - error ZeroArgument(string); - error InsufficientWithdrawableAmount(uint256 withdrawable, uint256 requested); - error NonProxyCallsForbidden(); - error AlreadyInitialized(); -} diff --git a/contracts/0.8.25/vaults/VaultFactory.sol b/contracts/0.8.25/vaults/VaultFactory.sol index f66190911..143b727c1 100644 --- a/contracts/0.8.25/vaults/VaultFactory.sol +++ b/contracts/0.8.25/vaults/VaultFactory.sol @@ -9,89 +9,102 @@ import {IStakingVault} from "./interfaces/IStakingVault.sol"; pragma solidity 0.8.25; -interface IVaultStaffRoom { - struct VaultStaffRoomParams { +interface IStVaultOwnerWithDelegation { + struct InitializationParams { uint256 managementFee; uint256 performanceFee; address manager; address operator; } - function OWNER() external view returns (bytes32); + function DEFAULT_ADMIN_ROLE() external view returns (bytes32); + function MANAGER_ROLE() external view returns (bytes32); + function OPERATOR_ROLE() external view returns (bytes32); + function LIDO_DAO_ROLE() external view returns (bytes32); + function initialize(address admin, address stakingVault) external; + function setManagementFee(uint256 _newManagementFee) external; + function setPerformanceFee(uint256 _newPerformanceFee) external; + function grantRole(bytes32 role, address account) external; + function revokeRole(bytes32 role, address account) external; } contract VaultFactory is UpgradeableBeacon { - - address public immutable vaultStaffRoomImpl; + address public immutable stVaultOwnerWithDelegationImpl; /// @param _owner The address of the VaultFactory owner /// @param _stakingVaultImpl The address of the StakingVault implementation - /// @param _vaultStaffRoomImpl The address of the VaultStaffRoom implementation - constructor(address _owner, address _stakingVaultImpl, address _vaultStaffRoomImpl) UpgradeableBeacon(_stakingVaultImpl, _owner) { - if (_vaultStaffRoomImpl == address(0)) revert ZeroArgument("_vaultStaffRoom"); - - vaultStaffRoomImpl = _vaultStaffRoomImpl; + /// @param _stVaultOwnerWithDelegationImpl The address of the StVaultOwnerWithDelegation implementation + constructor( + address _owner, + address _stakingVaultImpl, + address _stVaultOwnerWithDelegationImpl + ) UpgradeableBeacon(_stakingVaultImpl, _owner) { + if (_stVaultOwnerWithDelegationImpl == address(0)) revert ZeroArgument("_stVaultOwnerWithDelegation"); + + stVaultOwnerWithDelegationImpl = _stVaultOwnerWithDelegationImpl; } - /// @notice Creates a new StakingVault and VaultStaffRoom contracts + /// @notice Creates a new StakingVault and StVaultOwnerWithDelegation contracts /// @param _stakingVaultParams The params of vault initialization - /// @param _vaultStaffRoomParams The params of vault initialization + /// @param _initializationParams The params of vault initialization function createVault( bytes calldata _stakingVaultParams, - IVaultStaffRoom.VaultStaffRoomParams calldata _vaultStaffRoomParams - ) - external - returns(IStakingVault vault, IVaultStaffRoom vaultStaffRoom) - { - if (_vaultStaffRoomParams.manager == address(0)) revert ZeroArgument("manager"); - if (_vaultStaffRoomParams.operator == address(0)) revert ZeroArgument("operator"); + IStVaultOwnerWithDelegation.InitializationParams calldata _initializationParams, + address _lidoAgent + ) external returns (IStakingVault vault, IStVaultOwnerWithDelegation stVaultOwnerWithDelegation) { + if (_initializationParams.manager == address(0)) revert ZeroArgument("manager"); + if (_initializationParams.operator == address(0)) revert ZeroArgument("operator"); vault = IStakingVault(address(new BeaconProxy(address(this), ""))); - vaultStaffRoom = IVaultStaffRoom(Clones.clone(vaultStaffRoomImpl)); + stVaultOwnerWithDelegation = IStVaultOwnerWithDelegation(Clones.clone(stVaultOwnerWithDelegationImpl)); - //grant roles for factory to set fees and roles - vaultStaffRoom.initialize(address(this), address(vault)); + stVaultOwnerWithDelegation.initialize(address(this), address(vault)); - vaultStaffRoom.grantRole(vaultStaffRoom.MANAGER_ROLE(), _vaultStaffRoomParams.manager); - vaultStaffRoom.grantRole(vaultStaffRoom.OPERATOR_ROLE(), _vaultStaffRoomParams.operator); - vaultStaffRoom.grantRole(vaultStaffRoom.OWNER(), msg.sender); + stVaultOwnerWithDelegation.grantRole(stVaultOwnerWithDelegation.LIDO_DAO_ROLE(), _lidoAgent); + stVaultOwnerWithDelegation.grantRole(stVaultOwnerWithDelegation.MANAGER_ROLE(), _initializationParams.manager); + stVaultOwnerWithDelegation.grantRole( + stVaultOwnerWithDelegation.OPERATOR_ROLE(), + _initializationParams.operator + ); + stVaultOwnerWithDelegation.grantRole(stVaultOwnerWithDelegation.DEFAULT_ADMIN_ROLE(), msg.sender); - vaultStaffRoom.grantRole(vaultStaffRoom.MANAGER_ROLE(), address(this)); - vaultStaffRoom.setManagementFee(_vaultStaffRoomParams.managementFee); - vaultStaffRoom.setPerformanceFee(_vaultStaffRoomParams.performanceFee); + stVaultOwnerWithDelegation.grantRole(stVaultOwnerWithDelegation.MANAGER_ROLE(), address(this)); + stVaultOwnerWithDelegation.setManagementFee(_initializationParams.managementFee); + stVaultOwnerWithDelegation.setPerformanceFee(_initializationParams.performanceFee); //revoke roles from factory - vaultStaffRoom.revokeRole(vaultStaffRoom.MANAGER_ROLE(), address(this)); - vaultStaffRoom.revokeRole(vaultStaffRoom.OWNER(), address(this)); + stVaultOwnerWithDelegation.revokeRole(stVaultOwnerWithDelegation.MANAGER_ROLE(), address(this)); + stVaultOwnerWithDelegation.revokeRole(stVaultOwnerWithDelegation.DEFAULT_ADMIN_ROLE(), address(this)); + stVaultOwnerWithDelegation.revokeRole(stVaultOwnerWithDelegation.LIDO_DAO_ROLE(), address(this)); - vault.initialize(address(vaultStaffRoom), _stakingVaultParams); + vault.initialize(address(stVaultOwnerWithDelegation), _stakingVaultParams); - emit VaultCreated(address(vaultStaffRoom), address(vault)); - emit VaultStaffRoomCreated(msg.sender, address(vaultStaffRoom)); + emit VaultCreated(address(stVaultOwnerWithDelegation), address(vault)); + emit StVaultOwnerWithDelegationCreated(msg.sender, address(stVaultOwnerWithDelegation)); } /** - * @notice Event emitted on a Vault creation - * @param owner The address of the Vault owner - * @param vault The address of the created Vault - */ + * @notice Event emitted on a Vault creation + * @param owner The address of the Vault owner + * @param vault The address of the created Vault + */ event VaultCreated(address indexed owner, address indexed vault); /** - * @notice Event emitted on a VaultStaffRoom creation - * @param admin The address of the VaultStaffRoom admin - * @param vaultStaffRoom The address of the created VaultStaffRoom - */ - event VaultStaffRoomCreated(address indexed admin, address indexed vaultStaffRoom); + * @notice Event emitted on a StVaultOwnerWithDelegation creation + * @param admin The address of the StVaultOwnerWithDelegation admin + * @param stVaultOwnerWithDelegation The address of the created StVaultOwnerWithDelegation + */ + event StVaultOwnerWithDelegationCreated(address indexed admin, address indexed stVaultOwnerWithDelegation); error ZeroArgument(string); } diff --git a/contracts/0.8.25/vaults/VaultStaffRoom.sol b/contracts/0.8.25/vaults/VaultStaffRoom.sol deleted file mode 100644 index 217597839..000000000 --- a/contracts/0.8.25/vaults/VaultStaffRoom.sol +++ /dev/null @@ -1,189 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Lido -// SPDX-License-Identifier: GPL-3.0 - -// See contracts/COMPILERS.md -pragma solidity 0.8.25; - -import {AccessControlEnumerable} from "@openzeppelin/contracts-v5.0.2/access/extensions/AccessControlEnumerable.sol"; -import {IStakingVault} from "./interfaces/IStakingVault.sol"; -import {IReportReceiver} from "./interfaces/IReportReceiver.sol"; -import {VaultDashboard} from "./VaultDashboard.sol"; -import {Math256} from "contracts/common/lib/Math256.sol"; - -// TODO: natspec -// TODO: events - -// VaultStaffRoom: Delegates vault operations to different parties: -// - Manager: manages fees -// - Staker: can fund the vault and withdraw funds -// - Operator: can claim performance due and assigns Keymaster sub-role -// - Keymaster: Operator's sub-role for depositing to beacon chain -// - Plumber: manages liquidity, i.e. mints and burns stETH -contract VaultStaffRoom is VaultDashboard, IReportReceiver { - uint256 private constant BP_BASE = 100_00; - uint256 private constant MAX_FEE = BP_BASE; - - bytes32 public constant STAKER_ROLE = keccak256("Vault.VaultStaffRoom.StakerRole"); - bytes32 public constant OPERATOR_ROLE = keccak256("Vault.VaultStaffRoom.OperatorRole"); - bytes32 public constant KEYMASTER_ROLE = keccak256("Vault.VaultStaffRoom.KeymasterRole"); - bytes32 public constant PLUMBER_ROLE = keccak256("Vault.VaultStaffRoom.PlumberRole"); - - IStakingVault.Report public lastClaimedReport; - - uint256 public managementFee; - uint256 public performanceFee; - uint256 public managementDue; - - constructor( - address _stETH - ) VaultDashboard(_stETH) { - } - - function initialize(address _defaultAdmin, address _stakingVault) external override { - _initialize(_defaultAdmin, _stakingVault); - _setRoleAdmin(KEYMASTER_ROLE, OPERATOR_ROLE); - } - - /// * * * * * VIEW FUNCTIONS * * * * * /// - - function withdrawable() public view returns (uint256) { - uint256 reserved = Math256.max(stakingVault.locked(), managementDue + performanceDue()); - uint256 value = stakingVault.valuation(); - - if (reserved > value) { - return 0; - } - - return value - reserved; - } - - function performanceDue() public view returns (uint256) { - IStakingVault.Report memory latestReport = stakingVault.latestReport(); - - int128 rewardsAccrued = int128(latestReport.valuation - lastClaimedReport.valuation) - - (latestReport.inOutDelta - lastClaimedReport.inOutDelta); - - if (rewardsAccrued > 0) { - return (uint128(rewardsAccrued) * performanceFee) / BP_BASE; - } else { - return 0; - } - } - - /// * * * * * MANAGER FUNCTIONS * * * * * /// - - function setManagementFee(uint256 _newManagementFee) external onlyRole(MANAGER_ROLE) { - if (_newManagementFee > MAX_FEE) revert NewFeeCannotExceedMaxFee(); - - managementFee = _newManagementFee; - } - - function setPerformanceFee(uint256 _newPerformanceFee) external onlyRole(MANAGER_ROLE) { - if (_newPerformanceFee > MAX_FEE) revert NewFeeCannotExceedMaxFee(); - if (performanceDue() > 0) revert PerformanceDueUnclaimed(); - - performanceFee = _newPerformanceFee; - } - - function claimManagementDue(address _recipient, bool _liquid) external onlyRole(MANAGER_ROLE) { - if (_recipient == address(0)) revert ZeroArgument("_recipient"); - - if (!stakingVault.isHealthy()) { - revert VaultNotHealthy(); - } - - uint256 due = managementDue; - - if (due > 0) { - managementDue = 0; - - if (_liquid) { - vaultHub.mintStethBackedByVault(address(stakingVault), _recipient, due); - } else { - _withdrawDue(_recipient, due); - } - } - } - - /// * * * * * FUNDER FUNCTIONS * * * * * /// - - function fund() external payable override onlyRole(STAKER_ROLE) { - stakingVault.fund{value: msg.value}(); - } - - function withdraw(address _recipient, uint256 _ether) external override onlyRole(STAKER_ROLE) { - if (_recipient == address(0)) revert ZeroArgument("_recipient"); - if (_ether == 0) revert ZeroArgument("_ether"); - if (withdrawable() < _ether) revert InsufficientWithdrawableAmount(withdrawable(), _ether); - - stakingVault.withdraw(_recipient, _ether); - } - - /// * * * * * KEYMASTER FUNCTIONS * * * * * /// - - function depositToBeaconChain( - uint256 _numberOfDeposits, - bytes calldata _pubkeys, - bytes calldata _signatures - ) external override onlyRole(KEYMASTER_ROLE) { - stakingVault.depositToBeaconChain(_numberOfDeposits, _pubkeys, _signatures); - } - - /// * * * * * OPERATOR FUNCTIONS * * * * * /// - - function claimPerformanceDue(address _recipient, bool _liquid) external onlyRole(OPERATOR_ROLE) { - if (_recipient == address(0)) revert ZeroArgument("_recipient"); - - uint256 due = performanceDue(); - - if (due > 0) { - lastClaimedReport = stakingVault.latestReport(); - - if (_liquid) { - vaultHub.mintStethBackedByVault(address(stakingVault), _recipient, due); - } else { - _withdrawDue(_recipient, due); - } - } - } - - /// * * * * * PLUMBER FUNCTIONS * * * * * /// - - function mint(address _recipient, uint256 _tokens) external payable override onlyRole(PLUMBER_ROLE) fundAndProceed { - vaultHub.mintStethBackedByVault(address(stakingVault), _recipient, _tokens); - } - - function burn(uint256 _tokens) external override onlyRole(PLUMBER_ROLE) { - stETH.transferFrom(msg.sender, address(vaultHub), _tokens); - vaultHub.burnStethBackedByVault(address(stakingVault), _tokens); - } - - /// * * * * * VAULT CALLBACK * * * * * /// - - // solhint-disable-next-line no-unused-vars - function onReport(uint256 _valuation, int256 _inOutDelta, uint256 _locked) external { - if (msg.sender != address(stakingVault)) revert OnlyVaultCanCallOnReportHook(); - - managementDue += (_valuation * managementFee) / 365 / BP_BASE; - } - - /// * * * * * INTERNAL FUNCTIONS * * * * * /// - - function _withdrawDue(address _recipient, uint256 _ether) internal { - int256 unlocked = int256(stakingVault.valuation()) - int256(stakingVault.locked()); - uint256 unreserved = unlocked >= 0 ? uint256(unlocked) : 0; - if (unreserved < _ether) revert InsufficientUnlockedAmount(unreserved, _ether); - - stakingVault.withdraw(_recipient, _ether); - } - - /// * * * * * ERRORS * * * * * /// - - error SenderHasNeitherRole(address account, bytes32 role1, bytes32 role2); - error NewFeeCannotExceedMaxFee(); - error PerformanceDueUnclaimed(); - error InsufficientUnlockedAmount(uint256 unlocked, uint256 requested); - error VaultNotHealthy(); - error OnlyVaultCanCallOnReportHook(); - error FeeCannotExceed100(); -} diff --git a/contracts/0.8.25/vaults/interfaces/IStakingVault.sol b/contracts/0.8.25/vaults/interfaces/IStakingVault.sol index c98bb40e3..989629a09 100644 --- a/contracts/0.8.25/vaults/interfaces/IStakingVault.sol +++ b/contracts/0.8.25/vaults/interfaces/IStakingVault.sol @@ -40,7 +40,7 @@ interface IStakingVault { function requestValidatorExit(bytes calldata _validatorPublicKey) external; - function rebalance(uint256 _ether) external; + function rebalance(uint256 _ether) external payable; function report(uint256 _valuation, int256 _inOutDelta, uint256 _locked) external; } diff --git a/lib/proxy.ts b/lib/proxy.ts index 1a6564f05..60dd65110 100644 --- a/lib/proxy.ts +++ b/lib/proxy.ts @@ -8,14 +8,14 @@ import { OssifiableProxy, OssifiableProxy__factory, StakingVault, + StVaultOwnerWithDelegation, VaultFactory, - VaultStaffRoom, } from "typechain-types"; import { findEventsWithInterfaces } from "lib"; -import { IVaultStaffRoom } from "../typechain-types/contracts/0.8.25/vaults/VaultFactory.sol/VaultFactory"; -import VaultStaffRoomParamsStruct = IVaultStaffRoom.VaultStaffRoomParamsStruct; +import { IStVaultOwnerWithDelegation } from "../typechain-types/contracts/0.8.25/vaults/VaultFactory.sol/VaultFactory"; +import StVaultOwnerWithDelegationInitializationParamsStruct = IStVaultOwnerWithDelegation.InitializationParamsStruct; interface ProxifyArgs { impl: T; @@ -44,22 +44,23 @@ interface CreateVaultResponse { tx: ContractTransactionResponse; proxy: BeaconProxy; vault: StakingVault; - vaultStaffRoom: VaultStaffRoom; + stVaultOwnerWithDelegation: StVaultOwnerWithDelegation; } export async function createVaultProxy( vaultFactory: VaultFactory, _owner: HardhatEthersSigner, + _lidoAgent: HardhatEthersSigner, ): Promise { // Define the parameters for the struct - const vaultStaffRoomParams: VaultStaffRoomParamsStruct = { + const initializationParams: StVaultOwnerWithDelegationInitializationParamsStruct = { managementFee: 100n, performanceFee: 200n, manager: await _owner.getAddress(), operator: await _owner.getAddress(), }; - const tx = await vaultFactory.connect(_owner).createVault("0x", vaultStaffRoomParams); + const tx = await vaultFactory.connect(_owner).createVault("0x", initializationParams, _lidoAgent); // Get the receipt manually const receipt = (await tx.wait())!; @@ -70,23 +71,28 @@ export async function createVaultProxy( const event = events[0]; const { vault } = event.args; - const vaultStaffRoomEvents = findEventsWithInterfaces(receipt, "VaultStaffRoomCreated", [vaultFactory.interface]); - if (vaultStaffRoomEvents.length === 0) throw new Error("VaultStaffRoom creation event not found"); + const stVaultOwnerWithDelegationEvents = findEventsWithInterfaces( + receipt, + "StVaultOwnerWithDelegationCreated", + [vaultFactory.interface], + ); - const { vaultStaffRoom: vaultStaffRoomAddress } = vaultStaffRoomEvents[0].args; + if (stVaultOwnerWithDelegationEvents.length === 0) throw new Error("StVaultOwnerWithDelegation creation event not found"); + + const { stVaultOwnerWithDelegation: stVaultOwnerWithDelegationAddress } = stVaultOwnerWithDelegationEvents[0].args; const proxy = (await ethers.getContractAt("BeaconProxy", vault, _owner)) as BeaconProxy; const stakingVault = (await ethers.getContractAt("StakingVault", vault, _owner)) as StakingVault; - const vaultStaffRoom = (await ethers.getContractAt( - "VaultStaffRoom", - vaultStaffRoomAddress, + const stVaultOwnerWithDelegation = (await ethers.getContractAt( + "StVaultOwnerWithDelegation", + stVaultOwnerWithDelegationAddress, _owner, - )) as VaultStaffRoom; + )) as StVaultOwnerWithDelegation; return { tx, proxy, vault: stakingVault, - vaultStaffRoom: vaultStaffRoom, + stVaultOwnerWithDelegation, }; } diff --git a/lib/state-file.ts b/lib/state-file.ts index 5530fabf4..e791a09a8 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -90,7 +90,7 @@ export enum Sk { // Vaults stakingVaultImpl = "stakingVaultImpl", stakingVaultFactory = "stakingVaultFactory", - vaultStaffRoomImpl = "vaultStaffRoomImpl", + stVaultOwnerWithDelegationImpl = "stVaultOwnerWithDelegationImpl", } export function getAddress(contractKey: Sk, state: DeploymentState): string { diff --git a/scripts/scratch/steps/0145-deploy-vaults.ts b/scripts/scratch/steps/0145-deploy-vaults.ts index 10fc0834b..645c03f60 100644 --- a/scripts/scratch/steps/0145-deploy-vaults.ts +++ b/scripts/scratch/steps/0145-deploy-vaults.ts @@ -23,8 +23,8 @@ export async function main() { ]); const impAddress = await imp.getAddress(); - // Deploy VaultStaffRoom implementation contract - const room = await deployWithoutProxy(Sk.vaultStaffRoomImpl, "VaultStaffRoom", deployer, [lidoAddress]); + // Deploy StVaultOwnerWithDelegation implementation contract + const room = await deployWithoutProxy(Sk.stVaultOwnerWithDelegationImpl, "StVaultOwnerWithDelegation", deployer, [lidoAddress]); const roomAddress = await room.getAddress(); // Deploy VaultFactory contract diff --git a/test/0.8.25/vaults/vaultStaffRoom.test.ts b/test/0.8.25/vaults/stvault-owner-with-delegation.test.ts similarity index 65% rename from test/0.8.25/vaults/vaultStaffRoom.test.ts rename to test/0.8.25/vaults/stvault-owner-with-delegation.test.ts index 96ac1b33f..fda887f3d 100644 --- a/test/0.8.25/vaults/vaultStaffRoom.test.ts +++ b/test/0.8.25/vaults/stvault-owner-with-delegation.test.ts @@ -8,9 +8,9 @@ import { LidoLocator, StakingVault, StETH__HarnessForVaultHub, + StVaultOwnerWithDelegation, VaultFactory, VaultHub, - VaultStaffRoom, } from "typechain-types"; import { certainAddress, createVaultProxy, ether } from "lib"; @@ -18,17 +18,18 @@ import { certainAddress, createVaultProxy, ether } from "lib"; import { deployLidoLocator } from "test/deploy"; import { Snapshot } from "test/suite"; -describe("VaultStaffRoom.sol", () => { +describe("StVaultOwnerWithDelegation.sol", () => { let deployer: HardhatEthersSigner; let admin: HardhatEthersSigner; let holder: HardhatEthersSigner; let stranger: HardhatEthersSigner; + let lidoAgent: HardhatEthersSigner; let vaultOwner1: HardhatEthersSigner; let depositContract: DepositContract__MockForBeaconChainDepositor; let vaultHub: VaultHub; let implOld: StakingVault; - let vaultStaffRoom: VaultStaffRoom; + let stVaultOwnerWithDelegation: StVaultOwnerWithDelegation; let vaultFactory: VaultFactory; let steth: StETH__HarnessForVaultHub; @@ -40,7 +41,7 @@ describe("VaultStaffRoom.sol", () => { const treasury = certainAddress("treasury"); before(async () => { - [deployer, admin, holder, stranger, vaultOwner1] = await ethers.getSigners(); + [deployer, admin, holder, stranger, vaultOwner1, lidoAgent] = await ethers.getSigners(); locator = await deployLidoLocator(); steth = await ethers.deployContract("StETH__HarnessForVaultHub", [holder], { @@ -52,8 +53,8 @@ describe("VaultStaffRoom.sol", () => { // VaultHub vaultHub = await ethers.deployContract("Accounting", [admin, locator, steth, treasury], { from: deployer }); implOld = await ethers.deployContract("StakingVault", [vaultHub, depositContract], { from: deployer }); - vaultStaffRoom = await ethers.deployContract("VaultStaffRoom", [steth], { from: deployer }); - vaultFactory = await ethers.deployContract("VaultFactory", [admin, implOld, vaultStaffRoom], { from: deployer }); + stVaultOwnerWithDelegation = await ethers.deployContract("StVaultOwnerWithDelegation", [steth], { from: deployer }); + vaultFactory = await ethers.deployContract("VaultFactory", [admin, implOld, stVaultOwnerWithDelegation], { from: deployer }); //add role to factory await vaultHub.connect(admin).grantRole(await vaultHub.VAULT_MASTER_ROLE(), admin); @@ -68,30 +69,33 @@ describe("VaultStaffRoom.sol", () => { context("performanceDue", () => { it("performanceDue ", async () => { - const { vaultStaffRoom: vsr } = await createVaultProxy(vaultFactory, vaultOwner1); + const { stVaultOwnerWithDelegation } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); - await vsr.performanceDue(); + await stVaultOwnerWithDelegation.performanceDue(); }); }); context("initialize", async () => { it("reverts if initialize from implementation", async () => { - await expect(vaultStaffRoom.initialize(admin, implOld)).to.revertedWithCustomError( - vaultStaffRoom, + await expect(stVaultOwnerWithDelegation.initialize(admin, implOld)).to.revertedWithCustomError( + stVaultOwnerWithDelegation, "NonProxyCallsForbidden", ); }); it("reverts if already initialized", async () => { - const { vault: vault1, vaultStaffRoom: vsr } = await createVaultProxy(vaultFactory, vaultOwner1); + const { vault: vault1, stVaultOwnerWithDelegation } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); - await expect(vsr.initialize(admin, vault1)).to.revertedWithCustomError(vsr, "AlreadyInitialized"); + await expect(stVaultOwnerWithDelegation.initialize(admin, vault1)).to.revertedWithCustomError( + stVaultOwnerWithDelegation, + "AlreadyInitialized", + ); }); it("initialize", async () => { - const { tx, vaultStaffRoom: vsr } = await createVaultProxy(vaultFactory, vaultOwner1); + const { tx, stVaultOwnerWithDelegation } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); - await expect(tx).to.emit(vsr, "Initialized"); + await expect(tx).to.emit(stVaultOwnerWithDelegation, "Initialized"); }); }); }); diff --git a/test/0.8.25/vaults/vault-delegation-layer-voting.test.ts b/test/0.8.25/vaults/vault-delegation-layer-voting.test.ts index abd1ebf96..497cf5972 100644 --- a/test/0.8.25/vaults/vault-delegation-layer-voting.test.ts +++ b/test/0.8.25/vaults/vault-delegation-layer-voting.test.ts @@ -3,9 +3,9 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { advanceChainTime, certainAddress, days, proxify } from "lib"; import { Snapshot } from "test/suite"; -import { StakingVault__MockForVaultDelegationLayer, VaultDelegationLayer } from "typechain-types"; +import { StakingVault__MockForVaultDelegationLayer, StVaultOwnerWithDelegation } from "typechain-types"; -describe.only("VaultDelegationLayer:Voting", () => { +describe("VaultDelegationLayer:Voting", () => { let deployer: HardhatEthersSigner; let owner: HardhatEthersSigner; let manager: HardhatEthersSigner; @@ -14,7 +14,7 @@ describe.only("VaultDelegationLayer:Voting", () => { let stranger: HardhatEthersSigner; let stakingVault: StakingVault__MockForVaultDelegationLayer; - let vaultDelegationLayer: VaultDelegationLayer; + let stVaultOwnerWithDelegation: StVaultOwnerWithDelegation; let originalState: string; @@ -23,18 +23,18 @@ describe.only("VaultDelegationLayer:Voting", () => { const steth = certainAddress("vault-delegation-layer-voting-steth"); stakingVault = await ethers.deployContract("StakingVault__MockForVaultDelegationLayer"); - const impl = await ethers.deployContract("VaultDelegationLayer", [steth]); + const impl = await ethers.deployContract("StVaultOwnerWithDelegation", [steth]); // use a regular proxy for now - [vaultDelegationLayer] = await proxify({ impl, admin: owner, caller: deployer }); + [stVaultOwnerWithDelegation] = await proxify({ impl, admin: owner, caller: deployer }); - await vaultDelegationLayer.initialize(owner, stakingVault); - expect(await vaultDelegationLayer.isInitialized()).to.be.true; - expect(await vaultDelegationLayer.hasRole(await vaultDelegationLayer.OWNER(), owner)).to.be.true; - expect(await vaultDelegationLayer.vaultHub()).to.equal(await stakingVault.vaultHub()); + await stVaultOwnerWithDelegation.initialize(owner, stakingVault); + expect(await stVaultOwnerWithDelegation.isInitialized()).to.be.true; + expect(await stVaultOwnerWithDelegation.hasRole(await stVaultOwnerWithDelegation.DEFAULT_ADMIN_ROLE(), owner)).to.be.true; + expect(await stVaultOwnerWithDelegation.vaultHub()).to.equal(await stakingVault.vaultHub()); - await stakingVault.initialize(await vaultDelegationLayer.getAddress()); + await stakingVault.initialize(await stVaultOwnerWithDelegation.getAddress()); - vaultDelegationLayer = vaultDelegationLayer.connect(owner); + stVaultOwnerWithDelegation = stVaultOwnerWithDelegation.connect(owner); }); beforeEach(async () => { @@ -47,135 +47,135 @@ describe.only("VaultDelegationLayer:Voting", () => { describe("setPerformanceFee", () => { it("reverts if the caller does not have the required role", async () => { - expect(vaultDelegationLayer.connect(stranger).setPerformanceFee(100)).to.be.revertedWithCustomError( - vaultDelegationLayer, - "UnauthorizedCaller", + expect(stVaultOwnerWithDelegation.connect(stranger).setPerformanceFee(100)).to.be.revertedWithCustomError( + stVaultOwnerWithDelegation, + "NotACommitteeMember", ); }); it("executes if called by all distinct committee members", async () => { - await vaultDelegationLayer.grantRole(await vaultDelegationLayer.MANAGER_ROLE(), manager); - await vaultDelegationLayer.grantRole(await vaultDelegationLayer.LIDO_DAO_ROLE(), lidoDao); - await vaultDelegationLayer.connect(lidoDao).grantRole(await vaultDelegationLayer.OPERATOR_ROLE(), operator); + await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), manager); + await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.LIDO_DAO_ROLE(), lidoDao); + await stVaultOwnerWithDelegation.connect(lidoDao).grantRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), operator); - expect(await vaultDelegationLayer.hasRole(await vaultDelegationLayer.MANAGER_ROLE(), manager)).to.be.true; - expect(await vaultDelegationLayer.hasRole(await vaultDelegationLayer.OPERATOR_ROLE(), operator)).to.be.true; + expect(await stVaultOwnerWithDelegation.hasRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), manager)).to.be.true; + expect(await stVaultOwnerWithDelegation.hasRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), operator)).to.be.true; - const previousFee = await vaultDelegationLayer.performanceFee(); + const previousFee = await stVaultOwnerWithDelegation.performanceFee(); const newFee = previousFee + 1n; // remains unchanged - await vaultDelegationLayer.connect(manager).setPerformanceFee(newFee); - expect(await vaultDelegationLayer.performanceFee()).to.equal(previousFee); + await stVaultOwnerWithDelegation.connect(manager).setPerformanceFee(newFee); + expect(await stVaultOwnerWithDelegation.performanceFee()).to.equal(previousFee); // updated - await vaultDelegationLayer.connect(operator).setPerformanceFee(newFee); - expect(await vaultDelegationLayer.performanceFee()).to.equal(newFee); + await stVaultOwnerWithDelegation.connect(operator).setPerformanceFee(newFee); + expect(await stVaultOwnerWithDelegation.performanceFee()).to.equal(newFee); }); it("executes if called by a single member with all roles", async () => { - await vaultDelegationLayer.grantRole(await vaultDelegationLayer.MANAGER_ROLE(), manager); - await vaultDelegationLayer.grantRole(await vaultDelegationLayer.LIDO_DAO_ROLE(), lidoDao); - await vaultDelegationLayer.connect(lidoDao).grantRole(await vaultDelegationLayer.OPERATOR_ROLE(), manager); + await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), manager); + await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.LIDO_DAO_ROLE(), lidoDao); + await stVaultOwnerWithDelegation.connect(lidoDao).grantRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), manager); - const previousFee = await vaultDelegationLayer.performanceFee(); + const previousFee = await stVaultOwnerWithDelegation.performanceFee(); const newFee = previousFee + 1n; // updated with a single transaction - await vaultDelegationLayer.connect(manager).setPerformanceFee(newFee); - expect(await vaultDelegationLayer.performanceFee()).to.equal(newFee); + await stVaultOwnerWithDelegation.connect(manager).setPerformanceFee(newFee); + expect(await stVaultOwnerWithDelegation.performanceFee()).to.equal(newFee); }) it("does not execute if the vote is expired", async () => { - await vaultDelegationLayer.grantRole(await vaultDelegationLayer.MANAGER_ROLE(), manager); - await vaultDelegationLayer.grantRole(await vaultDelegationLayer.LIDO_DAO_ROLE(), lidoDao); - await vaultDelegationLayer.connect(lidoDao).grantRole(await vaultDelegationLayer.OPERATOR_ROLE(), operator); + await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), manager); + await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.LIDO_DAO_ROLE(), lidoDao); + await stVaultOwnerWithDelegation.connect(lidoDao).grantRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), operator); - expect(await vaultDelegationLayer.hasRole(await vaultDelegationLayer.MANAGER_ROLE(), manager)).to.be.true; - expect(await vaultDelegationLayer.hasRole(await vaultDelegationLayer.OPERATOR_ROLE(), operator)).to.be.true; + expect(await stVaultOwnerWithDelegation.hasRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), manager)).to.be.true; + expect(await stVaultOwnerWithDelegation.hasRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), operator)).to.be.true; - const previousFee = await vaultDelegationLayer.performanceFee(); + const previousFee = await stVaultOwnerWithDelegation.performanceFee(); const newFee = previousFee + 1n; // remains unchanged - await vaultDelegationLayer.connect(manager).setPerformanceFee(newFee); - expect(await vaultDelegationLayer.performanceFee()).to.equal(previousFee); + await stVaultOwnerWithDelegation.connect(manager).setPerformanceFee(newFee); + expect(await stVaultOwnerWithDelegation.performanceFee()).to.equal(previousFee); await advanceChainTime(days(7n) + 1n); // remains unchanged - await vaultDelegationLayer.connect(operator).setPerformanceFee(newFee); - expect(await vaultDelegationLayer.performanceFee()).to.equal(previousFee); + await stVaultOwnerWithDelegation.connect(operator).setPerformanceFee(newFee); + expect(await stVaultOwnerWithDelegation.performanceFee()).to.equal(previousFee); }); }); describe("transferStakingVaultOwnership", () => { it("reverts if the caller does not have the required role", async () => { - expect(vaultDelegationLayer.connect(stranger).transferStakingVaultOwnership(certainAddress("vault-delegation-layer-voting-new-owner"))).to.be.revertedWithCustomError( - vaultDelegationLayer, - "UnauthorizedCaller", + expect(stVaultOwnerWithDelegation.connect(stranger).transferStVaultOwnership(certainAddress("vault-delegation-layer-voting-new-owner"))).to.be.revertedWithCustomError( + stVaultOwnerWithDelegation, + "NotACommitteeMember", ); }); it("executes if called by all distinct committee members", async () => { - await vaultDelegationLayer.grantRole(await vaultDelegationLayer.MANAGER_ROLE(), manager); - await vaultDelegationLayer.grantRole(await vaultDelegationLayer.LIDO_DAO_ROLE(), lidoDao); - await vaultDelegationLayer.connect(lidoDao).grantRole(await vaultDelegationLayer.OPERATOR_ROLE(), operator); + await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), manager); + await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.LIDO_DAO_ROLE(), lidoDao); + await stVaultOwnerWithDelegation.connect(lidoDao).grantRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), operator); - expect(await vaultDelegationLayer.hasRole(await vaultDelegationLayer.MANAGER_ROLE(), manager)).to.be.true; - expect(await vaultDelegationLayer.hasRole(await vaultDelegationLayer.OPERATOR_ROLE(), operator)).to.be.true; + expect(await stVaultOwnerWithDelegation.hasRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), manager)).to.be.true; + expect(await stVaultOwnerWithDelegation.hasRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), operator)).to.be.true; const newOwner = certainAddress("vault-delegation-layer-voting-new-owner"); // remains unchanged - await vaultDelegationLayer.connect(manager).transferStakingVaultOwnership(newOwner); - expect(await stakingVault.owner()).to.equal(vaultDelegationLayer); + await stVaultOwnerWithDelegation.connect(manager).transferStVaultOwnership(newOwner); + expect(await stakingVault.owner()).to.equal(stVaultOwnerWithDelegation); // remains unchanged - await vaultDelegationLayer.connect(operator).transferStakingVaultOwnership(newOwner); - expect(await stakingVault.owner()).to.equal(vaultDelegationLayer); + await stVaultOwnerWithDelegation.connect(operator).transferStVaultOwnership(newOwner); + expect(await stakingVault.owner()).to.equal(stVaultOwnerWithDelegation); // updated - await vaultDelegationLayer.connect(lidoDao).transferStakingVaultOwnership(newOwner); + await stVaultOwnerWithDelegation.connect(lidoDao).transferStVaultOwnership(newOwner); expect(await stakingVault.owner()).to.equal(newOwner); }); it("executes if called by a single member with all roles", async () => { - await vaultDelegationLayer.grantRole(await vaultDelegationLayer.MANAGER_ROLE(), lidoDao); - await vaultDelegationLayer.grantRole(await vaultDelegationLayer.LIDO_DAO_ROLE(), lidoDao); - await vaultDelegationLayer.connect(lidoDao).grantRole(await vaultDelegationLayer.OPERATOR_ROLE(), lidoDao); + await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), lidoDao); + await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.LIDO_DAO_ROLE(), lidoDao); + await stVaultOwnerWithDelegation.connect(lidoDao).grantRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), lidoDao); const newOwner = certainAddress("vault-delegation-layer-voting-new-owner"); // updated with a single transaction - await vaultDelegationLayer.connect(lidoDao).transferStakingVaultOwnership(newOwner); + await stVaultOwnerWithDelegation.connect(lidoDao).transferStVaultOwnership(newOwner); expect(await stakingVault.owner()).to.equal(newOwner); }) it("does not execute if the vote is expired", async () => { - await vaultDelegationLayer.grantRole(await vaultDelegationLayer.MANAGER_ROLE(), manager); - await vaultDelegationLayer.grantRole(await vaultDelegationLayer.LIDO_DAO_ROLE(), lidoDao); - await vaultDelegationLayer.connect(lidoDao).grantRole(await vaultDelegationLayer.OPERATOR_ROLE(), operator); + await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), manager); + await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.LIDO_DAO_ROLE(), lidoDao); + await stVaultOwnerWithDelegation.connect(lidoDao).grantRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), operator); - expect(await vaultDelegationLayer.hasRole(await vaultDelegationLayer.MANAGER_ROLE(), manager)).to.be.true; - expect(await vaultDelegationLayer.hasRole(await vaultDelegationLayer.OPERATOR_ROLE(), operator)).to.be.true; + expect(await stVaultOwnerWithDelegation.hasRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), manager)).to.be.true; + expect(await stVaultOwnerWithDelegation.hasRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), operator)).to.be.true; const newOwner = certainAddress("vault-delegation-layer-voting-new-owner"); // remains unchanged - await vaultDelegationLayer.connect(manager).transferStakingVaultOwnership(newOwner); - expect(await stakingVault.owner()).to.equal(vaultDelegationLayer); + await stVaultOwnerWithDelegation.connect(manager).transferStVaultOwnership(newOwner); + expect(await stakingVault.owner()).to.equal(stVaultOwnerWithDelegation); // remains unchanged - await vaultDelegationLayer.connect(operator).transferStakingVaultOwnership(newOwner); - expect(await stakingVault.owner()).to.equal(vaultDelegationLayer); + await stVaultOwnerWithDelegation.connect(operator).transferStVaultOwnership(newOwner); + expect(await stakingVault.owner()).to.equal(stVaultOwnerWithDelegation); await advanceChainTime(days(7n) + 1n); // remains unchanged - await vaultDelegationLayer.connect(lidoDao).transferStakingVaultOwnership(newOwner); - expect(await stakingVault.owner()).to.equal(vaultDelegationLayer); + await stVaultOwnerWithDelegation.connect(lidoDao).transferStVaultOwnership(newOwner); + expect(await stakingVault.owner()).to.equal(stVaultOwnerWithDelegation); }); }); }); diff --git a/test/0.8.25/vaults/vault.test.ts b/test/0.8.25/vaults/vault.test.ts index 3dc531fb4..510d9087a 100644 --- a/test/0.8.25/vaults/vault.test.ts +++ b/test/0.8.25/vaults/vault.test.ts @@ -9,9 +9,9 @@ import { StakingVault, StakingVault__factory, StETH__HarnessForVaultHub, + StVaultOwnerWithDelegation, VaultFactory, VaultHub__MockForVault, - VaultStaffRoom, } from "typechain-types"; import { createVaultProxy, ether, impersonate } from "lib"; @@ -24,6 +24,7 @@ describe("StakingVault.sol", async () => { let executionLayerRewardsSender: HardhatEthersSigner; let stranger: HardhatEthersSigner; let holder: HardhatEthersSigner; + let lidoAgent: HardhatEthersSigner; let delegatorSigner: HardhatEthersSigner; let vaultHub: VaultHub__MockForVault; @@ -32,13 +33,13 @@ describe("StakingVault.sol", async () => { let stakingVault: StakingVault; let steth: StETH__HarnessForVaultHub; let vaultFactory: VaultFactory; - let vaultStaffRoomImpl: VaultStaffRoom; + let stVaulOwnerWithDelegation: StVaultOwnerWithDelegation; let vaultProxy: StakingVault; let originalState: string; before(async () => { - [deployer, owner, executionLayerRewardsSender, stranger, holder] = await ethers.getSigners(); + [deployer, owner, executionLayerRewardsSender, stranger, holder, lidoAgent] = await ethers.getSigners(); vaultHub = await ethers.deployContract("VaultHub__MockForVault", { from: deployer }); steth = await ethers.deployContract("StETH__HarnessForVaultHub", [holder], { @@ -51,16 +52,16 @@ describe("StakingVault.sol", async () => { vaultCreateFactory = new StakingVault__factory(owner); stakingVault = await ethers.getContractFactory("StakingVault").then((f) => f.deploy(vaultHub, depositContract)); - vaultStaffRoomImpl = await ethers.deployContract("VaultStaffRoom", [steth], { from: deployer }); + stVaulOwnerWithDelegation = await ethers.deployContract("StVaultOwnerWithDelegation", [steth], { from: deployer }); - vaultFactory = await ethers.deployContract("VaultFactory", [deployer, stakingVault, vaultStaffRoomImpl], { + vaultFactory = await ethers.deployContract("VaultFactory", [deployer, stakingVault, stVaulOwnerWithDelegation], { from: deployer, }); - const { vault, vaultStaffRoom } = await createVaultProxy(vaultFactory, owner); + const { vault, stVaultOwnerWithDelegation } = await createVaultProxy(vaultFactory, owner, lidoAgent); vaultProxy = vault; - delegatorSigner = await impersonate(await vaultStaffRoom.getAddress(), ether("100.0")); + delegatorSigner = await impersonate(await stVaultOwnerWithDelegation.getAddress(), ether("100.0")); }); beforeEach(async () => (originalState = await Snapshot.take())); diff --git a/test/0.8.25/vaults/vaultFactory.test.ts b/test/0.8.25/vaults/vaultFactory.test.ts index 4c6111012..64161862d 100644 --- a/test/0.8.25/vaults/vaultFactory.test.ts +++ b/test/0.8.25/vaults/vaultFactory.test.ts @@ -12,7 +12,7 @@ import { StETH__HarnessForVaultHub, VaultFactory, VaultHub, - VaultStaffRoom, + StVaultOwnerWithDelegation, } from "typechain-types"; import { certainAddress, createVaultProxy, ether } from "lib"; @@ -25,6 +25,7 @@ describe("VaultFactory.sol", () => { let admin: HardhatEthersSigner; let holder: HardhatEthersSigner; let stranger: HardhatEthersSigner; + let lidoAgent: HardhatEthersSigner; let vaultOwner1: HardhatEthersSigner; let vaultOwner2: HardhatEthersSigner; @@ -32,7 +33,7 @@ describe("VaultFactory.sol", () => { let vaultHub: VaultHub; let implOld: StakingVault; let implNew: StakingVault__HarnessForTestUpgrade; - let vaultStaffRoom: VaultStaffRoom; + let stVaultOwnerWithDelegation: StVaultOwnerWithDelegation; let vaultFactory: VaultFactory; let steth: StETH__HarnessForVaultHub; @@ -44,7 +45,7 @@ describe("VaultFactory.sol", () => { const treasury = certainAddress("treasury"); before(async () => { - [deployer, admin, holder, stranger, vaultOwner1, vaultOwner2] = await ethers.getSigners(); + [deployer, admin, holder, stranger, vaultOwner1, vaultOwner2, lidoAgent] = await ethers.getSigners(); locator = await deployLidoLocator(); steth = await ethers.deployContract("StETH__HarnessForVaultHub", [holder], { @@ -59,8 +60,8 @@ describe("VaultFactory.sol", () => { implNew = await ethers.deployContract("StakingVault__HarnessForTestUpgrade", [vaultHub, depositContract], { from: deployer, }); - vaultStaffRoom = await ethers.deployContract("VaultStaffRoom", [steth], { from: deployer }); - vaultFactory = await ethers.deployContract("VaultFactory", [admin, implOld, vaultStaffRoom], { from: deployer }); + stVaultOwnerWithDelegation = await ethers.deployContract("StVaultOwnerWithDelegation", [steth], { from: deployer }); + vaultFactory = await ethers.deployContract("VaultFactory", [admin, implOld, stVaultOwnerWithDelegation], { from: deployer }); //add role to factory await vaultHub.connect(admin).grantRole(await vaultHub.VAULT_MASTER_ROLE(), admin); @@ -86,10 +87,10 @@ describe("VaultFactory.sol", () => { .withArgs(ZeroAddress); }); - it("reverts if `_vaultStaffRoom` is zero address", async () => { + it("reverts if `_stVaultOwnerWithDelegation` is zero address", async () => { await expect(ethers.deployContract("VaultFactory", [admin, implOld, ZeroAddress], { from: deployer })) .to.be.revertedWithCustomError(vaultFactory, "ZeroArgument") - .withArgs("_vaultStaffRoom"); + .withArgs("_stVaultOwnerWithDelegation"); }); it("works and emit `OwnershipTransferred`, `Upgraded` events", async () => { @@ -112,21 +113,21 @@ describe("VaultFactory.sol", () => { context("createVault", () => { it("works with empty `params`", async () => { - const { tx, vault, vaultStaffRoom: vsr } = await createVaultProxy(vaultFactory, vaultOwner1); + const { tx, vault, stVaultOwnerWithDelegation } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); await expect(tx) .to.emit(vaultFactory, "VaultCreated") - .withArgs(await vsr.getAddress(), await vault.getAddress()); + .withArgs(await stVaultOwnerWithDelegation.getAddress(), await vault.getAddress()); await expect(tx) - .to.emit(vaultFactory, "VaultStaffRoomCreated") - .withArgs(await vaultOwner1.getAddress(), await vsr.getAddress()); + .to.emit(vaultFactory, "StVaultOwnerWithDelegationCreated") + .withArgs(await vaultOwner1.getAddress(), await stVaultOwnerWithDelegation.getAddress()); - expect(await vsr.getAddress()).to.eq(await vault.owner()); + expect(await stVaultOwnerWithDelegation.getAddress()).to.eq(await vault.owner()); expect(await vault.getBeacon()).to.eq(await vaultFactory.getAddress()); }); - it("works with non-empty `params`", async () => {}); + it("works with non-empty `params`", async () => { }); }); context("connect", () => { @@ -148,8 +149,8 @@ describe("VaultFactory.sol", () => { }; //create vault - const { vault: vault1, vaultStaffRoom: delegator1 } = await createVaultProxy(vaultFactory, vaultOwner1); - const { vault: vault2, vaultStaffRoom: delegator2 } = await createVaultProxy(vaultFactory, vaultOwner2); + const { vault: vault1, stVaultOwnerWithDelegation: delegator1 } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); + const { vault: vault2, stVaultOwnerWithDelegation: delegator2 } = await createVaultProxy(vaultFactory, vaultOwner2, lidoAgent); //owner of vault is delegator expect(await delegator1.getAddress()).to.eq(await vault1.owner()); @@ -223,7 +224,7 @@ describe("VaultFactory.sol", () => { expect(implAfter).to.eq(await implNew.getAddress()); //create new vault with new implementation - const { vault: vault3 } = await createVaultProxy(vaultFactory, vaultOwner1); + const { vault: vault3 } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); //we upgrade implementation and do not add it to whitelist await expect( diff --git a/test/integration/vaults-happy-path.integration.ts b/test/integration/vaults-happy-path.integration.ts index 6d9bd801f..391e2bf0f 100644 --- a/test/integration/vaults-happy-path.integration.ts +++ b/test/integration/vaults-happy-path.integration.ts @@ -4,7 +4,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { StakingVault, VaultStaffRoom } from "typechain-types"; +import { StakingVault, StVaultOwnerWithDelegation } from "typechain-types"; import { impersonate, log, trace, updateBalance } from "lib"; import { getProtocolContext, ProtocolContext } from "lib/protocol"; @@ -45,6 +45,7 @@ describe("Scenario: Staking Vaults Happy Path", () => { let alice: HardhatEthersSigner; let bob: HardhatEthersSigner; let mario: HardhatEthersSigner; + let lidoAgent: HardhatEthersSigner; let depositContract: string; @@ -54,7 +55,7 @@ describe("Scenario: Staking Vaults Happy Path", () => { let vault101: StakingVault; let vault101Address: string; - let vault101AdminContract: VaultStaffRoom; + let vault101AdminContract: StVaultOwnerWithDelegation; let vault101BeaconBalance = 0n; let vault101MintingMaximum = 0n; @@ -68,7 +69,7 @@ describe("Scenario: Staking Vaults Happy Path", () => { before(async () => { ctx = await getProtocolContext(); - [ethHolder, alice, bob, mario] = await ethers.getSigners(); + [ethHolder, alice, bob, mario, lidoAgent] = await ethers.getSigners(); const { depositSecurityModule } = ctx.contracts; depositContract = await depositSecurityModule.DEPOSIT_CONTRACT(); @@ -138,10 +139,10 @@ describe("Scenario: Staking Vaults Happy Path", () => { const { stakingVaultFactory } = ctx.contracts; const implAddress = await stakingVaultFactory.implementation(); - const adminContractImplAddress = await stakingVaultFactory.vaultStaffRoomImpl(); + const adminContractImplAddress = await stakingVaultFactory.stVaultOwnerWithDelegationImpl(); const vaultImpl = await ethers.getContractAt("StakingVault", implAddress); - const vaultFactoryAdminContract = await ethers.getContractAt("VaultStaffRoom", adminContractImplAddress); + const vaultFactoryAdminContract = await ethers.getContractAt("StVaultOwnerWithDelegation", adminContractImplAddress); expect(await vaultImpl.VAULT_HUB()).to.equal(ctx.contracts.accounting.address); expect(await vaultImpl.DEPOSIT_CONTRACT()).to.equal(depositContract); @@ -159,7 +160,7 @@ describe("Scenario: Staking Vaults Happy Path", () => { performanceFee: VAULT_NODE_OPERATOR_FEE, manager: alice, operator: bob, - }); + }, lidoAgent); const createVaultTxReceipt = await trace("vaultsFactory.createVault", deployTx); const createVaultEvents = ctx.getEvents(createVaultTxReceipt, "VaultCreated"); @@ -167,31 +168,31 @@ describe("Scenario: Staking Vaults Happy Path", () => { expect(createVaultEvents.length).to.equal(1n); vault101 = await ethers.getContractAt("StakingVault", createVaultEvents[0].args?.vault); - vault101AdminContract = await ethers.getContractAt("VaultStaffRoom", createVaultEvents[0].args?.owner); + vault101AdminContract = await ethers.getContractAt("StVaultOwnerWithDelegation", createVaultEvents[0].args?.owner); - expect(await vault101AdminContract.hasRole(await vault101AdminContract.OWNER(), alice)).to.be.true; + expect(await vault101AdminContract.hasRole(await vault101AdminContract.DEFAULT_ADMIN_ROLE(), alice)).to.be.true; expect(await vault101AdminContract.hasRole(await vault101AdminContract.MANAGER_ROLE(), alice)).to.be.true; expect(await vault101AdminContract.hasRole(await vault101AdminContract.OPERATOR_ROLE(), bob)).to.be.true; - expect(await vault101AdminContract.hasRole(await vault101AdminContract.KEYMASTER_ROLE(), alice)).to.be.false; - expect(await vault101AdminContract.hasRole(await vault101AdminContract.KEYMASTER_ROLE(), bob)).to.be.false; + expect(await vault101AdminContract.hasRole(await vault101AdminContract.KEY_MASTER_ROLE(), alice)).to.be.false; + expect(await vault101AdminContract.hasRole(await vault101AdminContract.KEY_MASTER_ROLE(), bob)).to.be.false; - expect(await vault101AdminContract.hasRole(await vault101AdminContract.PLUMBER_ROLE(), alice)).to.be.false; - expect(await vault101AdminContract.hasRole(await vault101AdminContract.PLUMBER_ROLE(), bob)).to.be.false; + expect(await vault101AdminContract.hasRole(await vault101AdminContract.TOKEN_MASTER_ROLE(), alice)).to.be.false; + expect(await vault101AdminContract.hasRole(await vault101AdminContract.TOKEN_MASTER_ROLE(), bob)).to.be.false; }); it("Should allow Alice to assign staker and plumber roles", async () => { await vault101AdminContract.connect(alice).grantRole(await vault101AdminContract.STAKER_ROLE(), alice); - await vault101AdminContract.connect(alice).grantRole(await vault101AdminContract.PLUMBER_ROLE(), mario); + await vault101AdminContract.connect(alice).grantRole(await vault101AdminContract.TOKEN_MASTER_ROLE(), mario); - expect(await vault101AdminContract.hasRole(await vault101AdminContract.PLUMBER_ROLE(), mario)).to.be.true; - expect(await vault101AdminContract.hasRole(await vault101AdminContract.PLUMBER_ROLE(), mario)).to.be.true; + expect(await vault101AdminContract.hasRole(await vault101AdminContract.TOKEN_MASTER_ROLE(), mario)).to.be.true; + expect(await vault101AdminContract.hasRole(await vault101AdminContract.TOKEN_MASTER_ROLE(), mario)).to.be.true; }); it("Should allow Bob to assign the keymaster role", async () => { - await vault101AdminContract.connect(bob).grantRole(await vault101AdminContract.KEYMASTER_ROLE(), bob); + await vault101AdminContract.connect(bob).grantRole(await vault101AdminContract.KEY_MASTER_ROLE(), bob); - expect(await vault101AdminContract.hasRole(await vault101AdminContract.KEYMASTER_ROLE(), bob)).to.be.true; + expect(await vault101AdminContract.hasRole(await vault101AdminContract.KEY_MASTER_ROLE(), bob)).to.be.true; }); it("Should allow Lido to recognize vaults and connect them to accounting", async () => { @@ -444,7 +445,7 @@ describe("Scenario: Staking Vaults Happy Path", () => { }); it("Should allow Alice to disconnect vaults from the hub providing the debt in ETH", async () => { - const disconnectTx = await vault101AdminContract.connect(alice).disconnectFromHub(); + const disconnectTx = await vault101AdminContract.connect(alice).disconnectFromVaultHub(); const disconnectTxReceipt = await trace("vault.disconnectFromHub", disconnectTx); const disconnectEvents = ctx.getEvents(disconnectTxReceipt, "VaultDisconnected"); From ae0f7f15c164040ce2e7aea55a4300d7a6e20ef4 Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Thu, 28 Nov 2024 18:03:05 +0500 Subject: [PATCH 08/21] fix: renames --- ...ng.test.ts => st-vault-owner-with-delegation-voting.test.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename test/0.8.25/vaults/{vault-delegation-layer-voting.test.ts => st-vault-owner-with-delegation-voting.test.ts} (99%) diff --git a/test/0.8.25/vaults/vault-delegation-layer-voting.test.ts b/test/0.8.25/vaults/st-vault-owner-with-delegation-voting.test.ts similarity index 99% rename from test/0.8.25/vaults/vault-delegation-layer-voting.test.ts rename to test/0.8.25/vaults/st-vault-owner-with-delegation-voting.test.ts index 497cf5972..85130c896 100644 --- a/test/0.8.25/vaults/vault-delegation-layer-voting.test.ts +++ b/test/0.8.25/vaults/st-vault-owner-with-delegation-voting.test.ts @@ -5,7 +5,7 @@ import { advanceChainTime, certainAddress, days, proxify } from "lib"; import { Snapshot } from "test/suite"; import { StakingVault__MockForVaultDelegationLayer, StVaultOwnerWithDelegation } from "typechain-types"; -describe("VaultDelegationLayer:Voting", () => { +describe("StVaultOwnerWithDelegation:Voting", () => { let deployer: HardhatEthersSigner; let owner: HardhatEthersSigner; let manager: HardhatEthersSigner; From 481979dd954743952f2482eb4ee2cb34b0507e7d Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Fri, 29 Nov 2024 16:02:53 +0500 Subject: [PATCH 09/21] fix: rename long name to Dashboard --- .../{StVaultOwnerWithDashboard.sol => Dashboard.sol} | 4 ++-- contracts/0.8.25/vaults/StVaultOwnerWithDelegation.sol | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) rename contracts/0.8.25/vaults/{StVaultOwnerWithDashboard.sol => Dashboard.sol} (99%) diff --git a/contracts/0.8.25/vaults/StVaultOwnerWithDashboard.sol b/contracts/0.8.25/vaults/Dashboard.sol similarity index 99% rename from contracts/0.8.25/vaults/StVaultOwnerWithDashboard.sol rename to contracts/0.8.25/vaults/Dashboard.sol index b4f206397..cdedf3ad7 100644 --- a/contracts/0.8.25/vaults/StVaultOwnerWithDashboard.sol +++ b/contracts/0.8.25/vaults/Dashboard.sol @@ -11,14 +11,14 @@ import {OwnableUpgradeable} from "contracts/openzeppelin/5.0.2/upgradeable/acces import {VaultHub} from "./VaultHub.sol"; /** - * @title StVaultOwnerWithDashboard + * @title Dashboard * @notice This contract is meant to be used as the owner of `StakingVault`. * This contract improves the vault UX by bundling all functions from the vault and vault hub * in this single contract. It provides administrative functions for managing the staking vault, * including funding, withdrawing, depositing to the beacon chain, minting, burning, and rebalancing operations. * All these functions are only callable by the account with the DEFAULT_ADMIN_ROLE. */ -contract StVaultOwnerWithDashboard is AccessControlEnumerable { +contract Dashboard is AccessControlEnumerable { /// @notice Address of the implementation contract /// @dev Used to prevent initialization in the implementation address private immutable _SELF; diff --git a/contracts/0.8.25/vaults/StVaultOwnerWithDelegation.sol b/contracts/0.8.25/vaults/StVaultOwnerWithDelegation.sol index 40776e36f..3e0c1052a 100644 --- a/contracts/0.8.25/vaults/StVaultOwnerWithDelegation.sol +++ b/contracts/0.8.25/vaults/StVaultOwnerWithDelegation.sol @@ -8,13 +8,13 @@ import {AccessControlEnumerable} from "@openzeppelin/contracts-v5.0.2/access/ext import {OwnableUpgradeable} from "contracts/openzeppelin/5.0.2/upgradeable/access/OwnableUpgradeable.sol"; import {IStakingVault} from "./interfaces/IStakingVault.sol"; import {IReportReceiver} from "./interfaces/IReportReceiver.sol"; -import {StVaultOwnerWithDashboard} from "./StVaultOwnerWithDashboard.sol"; +import {Dashboard} from "./Dashboard.sol"; import {Math256} from "contracts/common/lib/Math256.sol"; /** * @title StVaultOwnerWithDelegation * @notice This contract serves as an owner for `StakingVault` with additional delegation capabilities. - * It extends `StVaultOwnerWithDashboard` and implements `IReportReceiver`. + * It extends `Dashboard` and implements `IReportReceiver`. * The contract provides administrative functions for managing the staking vault, * including funding, withdrawing, depositing to the beacon chain, minting, burning, * rebalancing operations, and fee management. All these functions are only callable @@ -26,7 +26,7 @@ import {Math256} from "contracts/common/lib/Math256.sol"; * @notice The term "fee" is used to express the fee percentage as basis points, e.g. 5%, * while "due" is the actual amount of the fee, e.g. 1 ether */ -contract StVaultOwnerWithDelegation is StVaultOwnerWithDashboard, IReportReceiver { +contract StVaultOwnerWithDelegation is Dashboard, IReportReceiver { // ==================== Constants ==================== uint256 private constant BP_BASE = 10000; // Basis points base (100%) @@ -117,7 +117,7 @@ contract StVaultOwnerWithDelegation is StVaultOwnerWithDashboard, IReportReceive * @notice Constructor sets the stETH token address. * @param _stETH Address of the stETH token contract. */ - constructor(address _stETH) StVaultOwnerWithDashboard(_stETH) {} + constructor(address _stETH) Dashboard(_stETH) {} /** * @notice Initializes the contract with the default admin and `StakingVault` address. From ce82205dc9306c24b01bfcaddbb9d131d1b1c88f Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Fri, 29 Nov 2024 16:06:32 +0500 Subject: [PATCH 10/21] fix: rename long name to Delegation --- ...OwnerWithDelegation.sol => Delegation.sol} | 16 +-- contracts/0.8.25/vaults/VaultFactory.sol | 59 ++++---- lib/proxy.ts | 28 ++-- lib/state-file.ts | 2 +- scripts/scratch/steps/0145-deploy-vaults.ts | 4 +- ...vault-owner-with-delegation-voting.test.ts | 132 +++++++++--------- .../stvault-owner-with-delegation.test.ts | 28 ++-- test/0.8.25/vaults/vault.test.ts | 10 +- test/0.8.25/vaults/vaultFactory.test.ts | 26 ++-- .../vaults-happy-path.integration.ts | 10 +- 10 files changed, 156 insertions(+), 159 deletions(-) rename contracts/0.8.25/vaults/{StVaultOwnerWithDelegation.sol => Delegation.sol} (96%) diff --git a/contracts/0.8.25/vaults/StVaultOwnerWithDelegation.sol b/contracts/0.8.25/vaults/Delegation.sol similarity index 96% rename from contracts/0.8.25/vaults/StVaultOwnerWithDelegation.sol rename to contracts/0.8.25/vaults/Delegation.sol index 3e0c1052a..466a74a5a 100644 --- a/contracts/0.8.25/vaults/StVaultOwnerWithDelegation.sol +++ b/contracts/0.8.25/vaults/Delegation.sol @@ -12,7 +12,7 @@ import {Dashboard} from "./Dashboard.sol"; import {Math256} from "contracts/common/lib/Math256.sol"; /** - * @title StVaultOwnerWithDelegation + * @title Delegation * @notice This contract serves as an owner for `StakingVault` with additional delegation capabilities. * It extends `Dashboard` and implements `IReportReceiver`. * The contract provides administrative functions for managing the staking vault, @@ -26,7 +26,7 @@ import {Math256} from "contracts/common/lib/Math256.sol"; * @notice The term "fee" is used to express the fee percentage as basis points, e.g. 5%, * while "due" is the actual amount of the fee, e.g. 1 ether */ -contract StVaultOwnerWithDelegation is Dashboard, IReportReceiver { +contract Delegation is Dashboard, IReportReceiver { // ==================== Constants ==================== uint256 private constant BP_BASE = 10000; // Basis points base (100%) @@ -45,7 +45,7 @@ contract StVaultOwnerWithDelegation is Dashboard, IReportReceiver { * - vote on ownership transfer * - vote on performance fee changes */ - bytes32 public constant MANAGER_ROLE = keccak256("Vault.StVaultOwnerWithDelegation.ManagerRole"); + bytes32 public constant MANAGER_ROLE = keccak256("Vault.Delegation.ManagerRole"); /** * @notice Role for the staker. @@ -53,7 +53,7 @@ contract StVaultOwnerWithDelegation is Dashboard, IReportReceiver { * - fund the vault * - withdraw from the vault */ - bytes32 public constant STAKER_ROLE = keccak256("Vault.StVaultOwnerWithDelegation.StakerRole"); + bytes32 public constant STAKER_ROLE = keccak256("Vault.Delegation.StakerRole"); /** @notice Role for the operator * Operator can: @@ -62,14 +62,14 @@ contract StVaultOwnerWithDelegation is Dashboard, IReportReceiver { * - vote on ownership transfer * - set the Key Master role */ - bytes32 public constant OPERATOR_ROLE = keccak256("Vault.StVaultOwnerWithDelegation.OperatorRole"); + bytes32 public constant OPERATOR_ROLE = keccak256("Vault.Delegation.OperatorRole"); /** * @notice Role for the key master. * Key master can: * - deposit validators to the beacon chain */ - bytes32 public constant KEY_MASTER_ROLE = keccak256("Vault.StVaultOwnerWithDelegation.KeyMasterRole"); + bytes32 public constant KEY_MASTER_ROLE = keccak256("Vault.Delegation.KeyMasterRole"); /** * @notice Role for the token master. @@ -77,7 +77,7 @@ contract StVaultOwnerWithDelegation is Dashboard, IReportReceiver { * - mint stETH tokens * - burn stETH tokens */ - bytes32 public constant TOKEN_MASTER_ROLE = keccak256("Vault.StVaultOwnerWithDelegation.TokenMasterRole"); + bytes32 public constant TOKEN_MASTER_ROLE = keccak256("Vault.Delegation.TokenMasterRole"); /** * @notice Role for the Lido DAO. @@ -86,7 +86,7 @@ contract StVaultOwnerWithDelegation is Dashboard, IReportReceiver { * - set the operator role * - vote on ownership transfer */ - bytes32 public constant LIDO_DAO_ROLE = keccak256("Vault.StVaultOwnerWithDelegation.LidoDAORole"); + bytes32 public constant LIDO_DAO_ROLE = keccak256("Vault.Delegation.LidoDAORole"); // ==================== State Variables ==================== diff --git a/contracts/0.8.25/vaults/VaultFactory.sol b/contracts/0.8.25/vaults/VaultFactory.sol index 143b727c1..834bac741 100644 --- a/contracts/0.8.25/vaults/VaultFactory.sol +++ b/contracts/0.8.25/vaults/VaultFactory.sol @@ -9,7 +9,7 @@ import {IStakingVault} from "./interfaces/IStakingVault.sol"; pragma solidity 0.8.25; -interface IStVaultOwnerWithDelegation { +interface IDelegation { struct InitializationParams { uint256 managementFee; uint256 performanceFee; @@ -37,59 +37,56 @@ interface IStVaultOwnerWithDelegation { } contract VaultFactory is UpgradeableBeacon { - address public immutable stVaultOwnerWithDelegationImpl; + address public immutable delegationImpl; /// @param _owner The address of the VaultFactory owner /// @param _stakingVaultImpl The address of the StakingVault implementation - /// @param _stVaultOwnerWithDelegationImpl The address of the StVaultOwnerWithDelegation implementation + /// @param _delegationImpl The address of the Delegation implementation constructor( address _owner, address _stakingVaultImpl, - address _stVaultOwnerWithDelegationImpl + address _delegationImpl ) UpgradeableBeacon(_stakingVaultImpl, _owner) { - if (_stVaultOwnerWithDelegationImpl == address(0)) revert ZeroArgument("_stVaultOwnerWithDelegation"); + if (_delegationImpl == address(0)) revert ZeroArgument("_delegation"); - stVaultOwnerWithDelegationImpl = _stVaultOwnerWithDelegationImpl; + delegationImpl = _delegationImpl; } - /// @notice Creates a new StakingVault and StVaultOwnerWithDelegation contracts + /// @notice Creates a new StakingVault and Delegation contracts /// @param _stakingVaultParams The params of vault initialization /// @param _initializationParams The params of vault initialization function createVault( bytes calldata _stakingVaultParams, - IStVaultOwnerWithDelegation.InitializationParams calldata _initializationParams, + IDelegation.InitializationParams calldata _initializationParams, address _lidoAgent - ) external returns (IStakingVault vault, IStVaultOwnerWithDelegation stVaultOwnerWithDelegation) { + ) external returns (IStakingVault vault, IDelegation delegation) { if (_initializationParams.manager == address(0)) revert ZeroArgument("manager"); if (_initializationParams.operator == address(0)) revert ZeroArgument("operator"); vault = IStakingVault(address(new BeaconProxy(address(this), ""))); - stVaultOwnerWithDelegation = IStVaultOwnerWithDelegation(Clones.clone(stVaultOwnerWithDelegationImpl)); + delegation = IDelegation(Clones.clone(delegationImpl)); - stVaultOwnerWithDelegation.initialize(address(this), address(vault)); + delegation.initialize(address(this), address(vault)); - stVaultOwnerWithDelegation.grantRole(stVaultOwnerWithDelegation.LIDO_DAO_ROLE(), _lidoAgent); - stVaultOwnerWithDelegation.grantRole(stVaultOwnerWithDelegation.MANAGER_ROLE(), _initializationParams.manager); - stVaultOwnerWithDelegation.grantRole( - stVaultOwnerWithDelegation.OPERATOR_ROLE(), - _initializationParams.operator - ); - stVaultOwnerWithDelegation.grantRole(stVaultOwnerWithDelegation.DEFAULT_ADMIN_ROLE(), msg.sender); + delegation.grantRole(delegation.LIDO_DAO_ROLE(), _lidoAgent); + delegation.grantRole(delegation.MANAGER_ROLE(), _initializationParams.manager); + delegation.grantRole(delegation.OPERATOR_ROLE(), _initializationParams.operator); + delegation.grantRole(delegation.DEFAULT_ADMIN_ROLE(), msg.sender); - stVaultOwnerWithDelegation.grantRole(stVaultOwnerWithDelegation.MANAGER_ROLE(), address(this)); - stVaultOwnerWithDelegation.setManagementFee(_initializationParams.managementFee); - stVaultOwnerWithDelegation.setPerformanceFee(_initializationParams.performanceFee); + delegation.grantRole(delegation.MANAGER_ROLE(), address(this)); + delegation.setManagementFee(_initializationParams.managementFee); + delegation.setPerformanceFee(_initializationParams.performanceFee); //revoke roles from factory - stVaultOwnerWithDelegation.revokeRole(stVaultOwnerWithDelegation.MANAGER_ROLE(), address(this)); - stVaultOwnerWithDelegation.revokeRole(stVaultOwnerWithDelegation.DEFAULT_ADMIN_ROLE(), address(this)); - stVaultOwnerWithDelegation.revokeRole(stVaultOwnerWithDelegation.LIDO_DAO_ROLE(), address(this)); + delegation.revokeRole(delegation.MANAGER_ROLE(), address(this)); + delegation.revokeRole(delegation.DEFAULT_ADMIN_ROLE(), address(this)); + delegation.revokeRole(delegation.LIDO_DAO_ROLE(), address(this)); - vault.initialize(address(stVaultOwnerWithDelegation), _stakingVaultParams); + vault.initialize(address(delegation), _stakingVaultParams); - emit VaultCreated(address(stVaultOwnerWithDelegation), address(vault)); - emit StVaultOwnerWithDelegationCreated(msg.sender, address(stVaultOwnerWithDelegation)); + emit VaultCreated(address(delegation), address(vault)); + emit DelegationCreated(msg.sender, address(delegation)); } /** @@ -100,11 +97,11 @@ contract VaultFactory is UpgradeableBeacon { event VaultCreated(address indexed owner, address indexed vault); /** - * @notice Event emitted on a StVaultOwnerWithDelegation creation - * @param admin The address of the StVaultOwnerWithDelegation admin - * @param stVaultOwnerWithDelegation The address of the created StVaultOwnerWithDelegation + * @notice Event emitted on a Delegation creation + * @param admin The address of the Delegation admin + * @param delegation The address of the created Delegation */ - event StVaultOwnerWithDelegationCreated(address indexed admin, address indexed stVaultOwnerWithDelegation); + event DelegationCreated(address indexed admin, address indexed delegation); error ZeroArgument(string); } diff --git a/lib/proxy.ts b/lib/proxy.ts index 60dd65110..ec9d9b31b 100644 --- a/lib/proxy.ts +++ b/lib/proxy.ts @@ -8,14 +8,14 @@ import { OssifiableProxy, OssifiableProxy__factory, StakingVault, - StVaultOwnerWithDelegation, + Delegation, VaultFactory, } from "typechain-types"; import { findEventsWithInterfaces } from "lib"; -import { IStVaultOwnerWithDelegation } from "../typechain-types/contracts/0.8.25/vaults/VaultFactory.sol/VaultFactory"; -import StVaultOwnerWithDelegationInitializationParamsStruct = IStVaultOwnerWithDelegation.InitializationParamsStruct; +import { IDelegation } from "../typechain-types/contracts/0.8.25/vaults/VaultFactory.sol/VaultFactory"; +import DelegationInitializationParamsStruct = IDelegation.InitializationParamsStruct; interface ProxifyArgs { impl: T; @@ -44,7 +44,7 @@ interface CreateVaultResponse { tx: ContractTransactionResponse; proxy: BeaconProxy; vault: StakingVault; - stVaultOwnerWithDelegation: StVaultOwnerWithDelegation; + delegation: Delegation; } export async function createVaultProxy( @@ -53,7 +53,7 @@ export async function createVaultProxy( _lidoAgent: HardhatEthersSigner, ): Promise { // Define the parameters for the struct - const initializationParams: StVaultOwnerWithDelegationInitializationParamsStruct = { + const initializationParams: DelegationInitializationParamsStruct = { managementFee: 100n, performanceFee: 200n, manager: await _owner.getAddress(), @@ -71,28 +71,28 @@ export async function createVaultProxy( const event = events[0]; const { vault } = event.args; - const stVaultOwnerWithDelegationEvents = findEventsWithInterfaces( + const delegationEvents = findEventsWithInterfaces( receipt, - "StVaultOwnerWithDelegationCreated", + "DelegationCreated", [vaultFactory.interface], ); - if (stVaultOwnerWithDelegationEvents.length === 0) throw new Error("StVaultOwnerWithDelegation creation event not found"); + if (delegationEvents.length === 0) throw new Error("Delegation creation event not found"); - const { stVaultOwnerWithDelegation: stVaultOwnerWithDelegationAddress } = stVaultOwnerWithDelegationEvents[0].args; + const { delegation: delegationAddress } = delegationEvents[0].args; const proxy = (await ethers.getContractAt("BeaconProxy", vault, _owner)) as BeaconProxy; const stakingVault = (await ethers.getContractAt("StakingVault", vault, _owner)) as StakingVault; - const stVaultOwnerWithDelegation = (await ethers.getContractAt( - "StVaultOwnerWithDelegation", - stVaultOwnerWithDelegationAddress, + const delegation = (await ethers.getContractAt( + "Delegation", + delegationAddress, _owner, - )) as StVaultOwnerWithDelegation; + )) as Delegation; return { tx, proxy, vault: stakingVault, - stVaultOwnerWithDelegation, + delegation, }; } diff --git a/lib/state-file.ts b/lib/state-file.ts index e791a09a8..2618ce3d7 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -90,7 +90,7 @@ export enum Sk { // Vaults stakingVaultImpl = "stakingVaultImpl", stakingVaultFactory = "stakingVaultFactory", - stVaultOwnerWithDelegationImpl = "stVaultOwnerWithDelegationImpl", + delegationImpl = "delegationImpl", } export function getAddress(contractKey: Sk, state: DeploymentState): string { diff --git a/scripts/scratch/steps/0145-deploy-vaults.ts b/scripts/scratch/steps/0145-deploy-vaults.ts index 645c03f60..0c377065f 100644 --- a/scripts/scratch/steps/0145-deploy-vaults.ts +++ b/scripts/scratch/steps/0145-deploy-vaults.ts @@ -23,8 +23,8 @@ export async function main() { ]); const impAddress = await imp.getAddress(); - // Deploy StVaultOwnerWithDelegation implementation contract - const room = await deployWithoutProxy(Sk.stVaultOwnerWithDelegationImpl, "StVaultOwnerWithDelegation", deployer, [lidoAddress]); + // Deploy Delegation implementation contract + const room = await deployWithoutProxy(Sk.delegationImpl, "Delegation", deployer, [lidoAddress]); const roomAddress = await room.getAddress(); // Deploy VaultFactory contract diff --git a/test/0.8.25/vaults/st-vault-owner-with-delegation-voting.test.ts b/test/0.8.25/vaults/st-vault-owner-with-delegation-voting.test.ts index 85130c896..8e3495b64 100644 --- a/test/0.8.25/vaults/st-vault-owner-with-delegation-voting.test.ts +++ b/test/0.8.25/vaults/st-vault-owner-with-delegation-voting.test.ts @@ -3,9 +3,9 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import { advanceChainTime, certainAddress, days, proxify } from "lib"; import { Snapshot } from "test/suite"; -import { StakingVault__MockForVaultDelegationLayer, StVaultOwnerWithDelegation } from "typechain-types"; +import { StakingVault__MockForVaultDelegationLayer, Delegation } from "typechain-types"; -describe("StVaultOwnerWithDelegation:Voting", () => { +describe("Delegation:Voting", () => { let deployer: HardhatEthersSigner; let owner: HardhatEthersSigner; let manager: HardhatEthersSigner; @@ -14,7 +14,7 @@ describe("StVaultOwnerWithDelegation:Voting", () => { let stranger: HardhatEthersSigner; let stakingVault: StakingVault__MockForVaultDelegationLayer; - let stVaultOwnerWithDelegation: StVaultOwnerWithDelegation; + let delegation: Delegation; let originalState: string; @@ -23,18 +23,18 @@ describe("StVaultOwnerWithDelegation:Voting", () => { const steth = certainAddress("vault-delegation-layer-voting-steth"); stakingVault = await ethers.deployContract("StakingVault__MockForVaultDelegationLayer"); - const impl = await ethers.deployContract("StVaultOwnerWithDelegation", [steth]); + const impl = await ethers.deployContract("Delegation", [steth]); // use a regular proxy for now - [stVaultOwnerWithDelegation] = await proxify({ impl, admin: owner, caller: deployer }); + [delegation] = await proxify({ impl, admin: owner, caller: deployer }); - await stVaultOwnerWithDelegation.initialize(owner, stakingVault); - expect(await stVaultOwnerWithDelegation.isInitialized()).to.be.true; - expect(await stVaultOwnerWithDelegation.hasRole(await stVaultOwnerWithDelegation.DEFAULT_ADMIN_ROLE(), owner)).to.be.true; - expect(await stVaultOwnerWithDelegation.vaultHub()).to.equal(await stakingVault.vaultHub()); + await delegation.initialize(owner, stakingVault); + expect(await delegation.isInitialized()).to.be.true; + expect(await delegation.hasRole(await delegation.DEFAULT_ADMIN_ROLE(), owner)).to.be.true; + expect(await delegation.vaultHub()).to.equal(await stakingVault.vaultHub()); - await stakingVault.initialize(await stVaultOwnerWithDelegation.getAddress()); + await stakingVault.initialize(await delegation.getAddress()); - stVaultOwnerWithDelegation = stVaultOwnerWithDelegation.connect(owner); + delegation = delegation.connect(owner); }); beforeEach(async () => { @@ -47,135 +47,135 @@ describe("StVaultOwnerWithDelegation:Voting", () => { describe("setPerformanceFee", () => { it("reverts if the caller does not have the required role", async () => { - expect(stVaultOwnerWithDelegation.connect(stranger).setPerformanceFee(100)).to.be.revertedWithCustomError( - stVaultOwnerWithDelegation, + expect(delegation.connect(stranger).setPerformanceFee(100)).to.be.revertedWithCustomError( + delegation, "NotACommitteeMember", ); }); it("executes if called by all distinct committee members", async () => { - await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), manager); - await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.LIDO_DAO_ROLE(), lidoDao); - await stVaultOwnerWithDelegation.connect(lidoDao).grantRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), operator); + await delegation.grantRole(await delegation.MANAGER_ROLE(), manager); + await delegation.grantRole(await delegation.LIDO_DAO_ROLE(), lidoDao); + await delegation.connect(lidoDao).grantRole(await delegation.OPERATOR_ROLE(), operator); - expect(await stVaultOwnerWithDelegation.hasRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), manager)).to.be.true; - expect(await stVaultOwnerWithDelegation.hasRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), operator)).to.be.true; + expect(await delegation.hasRole(await delegation.MANAGER_ROLE(), manager)).to.be.true; + expect(await delegation.hasRole(await delegation.OPERATOR_ROLE(), operator)).to.be.true; - const previousFee = await stVaultOwnerWithDelegation.performanceFee(); + const previousFee = await delegation.performanceFee(); const newFee = previousFee + 1n; // remains unchanged - await stVaultOwnerWithDelegation.connect(manager).setPerformanceFee(newFee); - expect(await stVaultOwnerWithDelegation.performanceFee()).to.equal(previousFee); + await delegation.connect(manager).setPerformanceFee(newFee); + expect(await delegation.performanceFee()).to.equal(previousFee); // updated - await stVaultOwnerWithDelegation.connect(operator).setPerformanceFee(newFee); - expect(await stVaultOwnerWithDelegation.performanceFee()).to.equal(newFee); + await delegation.connect(operator).setPerformanceFee(newFee); + expect(await delegation.performanceFee()).to.equal(newFee); }); it("executes if called by a single member with all roles", async () => { - await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), manager); - await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.LIDO_DAO_ROLE(), lidoDao); - await stVaultOwnerWithDelegation.connect(lidoDao).grantRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), manager); + await delegation.grantRole(await delegation.MANAGER_ROLE(), manager); + await delegation.grantRole(await delegation.LIDO_DAO_ROLE(), lidoDao); + await delegation.connect(lidoDao).grantRole(await delegation.OPERATOR_ROLE(), manager); - const previousFee = await stVaultOwnerWithDelegation.performanceFee(); + const previousFee = await delegation.performanceFee(); const newFee = previousFee + 1n; // updated with a single transaction - await stVaultOwnerWithDelegation.connect(manager).setPerformanceFee(newFee); - expect(await stVaultOwnerWithDelegation.performanceFee()).to.equal(newFee); + await delegation.connect(manager).setPerformanceFee(newFee); + expect(await delegation.performanceFee()).to.equal(newFee); }) it("does not execute if the vote is expired", async () => { - await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), manager); - await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.LIDO_DAO_ROLE(), lidoDao); - await stVaultOwnerWithDelegation.connect(lidoDao).grantRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), operator); + await delegation.grantRole(await delegation.MANAGER_ROLE(), manager); + await delegation.grantRole(await delegation.LIDO_DAO_ROLE(), lidoDao); + await delegation.connect(lidoDao).grantRole(await delegation.OPERATOR_ROLE(), operator); - expect(await stVaultOwnerWithDelegation.hasRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), manager)).to.be.true; - expect(await stVaultOwnerWithDelegation.hasRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), operator)).to.be.true; + expect(await delegation.hasRole(await delegation.MANAGER_ROLE(), manager)).to.be.true; + expect(await delegation.hasRole(await delegation.OPERATOR_ROLE(), operator)).to.be.true; - const previousFee = await stVaultOwnerWithDelegation.performanceFee(); + const previousFee = await delegation.performanceFee(); const newFee = previousFee + 1n; // remains unchanged - await stVaultOwnerWithDelegation.connect(manager).setPerformanceFee(newFee); - expect(await stVaultOwnerWithDelegation.performanceFee()).to.equal(previousFee); + await delegation.connect(manager).setPerformanceFee(newFee); + expect(await delegation.performanceFee()).to.equal(previousFee); await advanceChainTime(days(7n) + 1n); // remains unchanged - await stVaultOwnerWithDelegation.connect(operator).setPerformanceFee(newFee); - expect(await stVaultOwnerWithDelegation.performanceFee()).to.equal(previousFee); + await delegation.connect(operator).setPerformanceFee(newFee); + expect(await delegation.performanceFee()).to.equal(previousFee); }); }); describe("transferStakingVaultOwnership", () => { it("reverts if the caller does not have the required role", async () => { - expect(stVaultOwnerWithDelegation.connect(stranger).transferStVaultOwnership(certainAddress("vault-delegation-layer-voting-new-owner"))).to.be.revertedWithCustomError( - stVaultOwnerWithDelegation, + expect(delegation.connect(stranger).transferStVaultOwnership(certainAddress("vault-delegation-layer-voting-new-owner"))).to.be.revertedWithCustomError( + delegation, "NotACommitteeMember", ); }); it("executes if called by all distinct committee members", async () => { - await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), manager); - await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.LIDO_DAO_ROLE(), lidoDao); - await stVaultOwnerWithDelegation.connect(lidoDao).grantRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), operator); + await delegation.grantRole(await delegation.MANAGER_ROLE(), manager); + await delegation.grantRole(await delegation.LIDO_DAO_ROLE(), lidoDao); + await delegation.connect(lidoDao).grantRole(await delegation.OPERATOR_ROLE(), operator); - expect(await stVaultOwnerWithDelegation.hasRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), manager)).to.be.true; - expect(await stVaultOwnerWithDelegation.hasRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), operator)).to.be.true; + expect(await delegation.hasRole(await delegation.MANAGER_ROLE(), manager)).to.be.true; + expect(await delegation.hasRole(await delegation.OPERATOR_ROLE(), operator)).to.be.true; const newOwner = certainAddress("vault-delegation-layer-voting-new-owner"); // remains unchanged - await stVaultOwnerWithDelegation.connect(manager).transferStVaultOwnership(newOwner); - expect(await stakingVault.owner()).to.equal(stVaultOwnerWithDelegation); + await delegation.connect(manager).transferStVaultOwnership(newOwner); + expect(await stakingVault.owner()).to.equal(delegation); // remains unchanged - await stVaultOwnerWithDelegation.connect(operator).transferStVaultOwnership(newOwner); - expect(await stakingVault.owner()).to.equal(stVaultOwnerWithDelegation); + await delegation.connect(operator).transferStVaultOwnership(newOwner); + expect(await stakingVault.owner()).to.equal(delegation); // updated - await stVaultOwnerWithDelegation.connect(lidoDao).transferStVaultOwnership(newOwner); + await delegation.connect(lidoDao).transferStVaultOwnership(newOwner); expect(await stakingVault.owner()).to.equal(newOwner); }); it("executes if called by a single member with all roles", async () => { - await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), lidoDao); - await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.LIDO_DAO_ROLE(), lidoDao); - await stVaultOwnerWithDelegation.connect(lidoDao).grantRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), lidoDao); + await delegation.grantRole(await delegation.MANAGER_ROLE(), lidoDao); + await delegation.grantRole(await delegation.LIDO_DAO_ROLE(), lidoDao); + await delegation.connect(lidoDao).grantRole(await delegation.OPERATOR_ROLE(), lidoDao); const newOwner = certainAddress("vault-delegation-layer-voting-new-owner"); // updated with a single transaction - await stVaultOwnerWithDelegation.connect(lidoDao).transferStVaultOwnership(newOwner); + await delegation.connect(lidoDao).transferStVaultOwnership(newOwner); expect(await stakingVault.owner()).to.equal(newOwner); }) it("does not execute if the vote is expired", async () => { - await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), manager); - await stVaultOwnerWithDelegation.grantRole(await stVaultOwnerWithDelegation.LIDO_DAO_ROLE(), lidoDao); - await stVaultOwnerWithDelegation.connect(lidoDao).grantRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), operator); + await delegation.grantRole(await delegation.MANAGER_ROLE(), manager); + await delegation.grantRole(await delegation.LIDO_DAO_ROLE(), lidoDao); + await delegation.connect(lidoDao).grantRole(await delegation.OPERATOR_ROLE(), operator); - expect(await stVaultOwnerWithDelegation.hasRole(await stVaultOwnerWithDelegation.MANAGER_ROLE(), manager)).to.be.true; - expect(await stVaultOwnerWithDelegation.hasRole(await stVaultOwnerWithDelegation.OPERATOR_ROLE(), operator)).to.be.true; + expect(await delegation.hasRole(await delegation.MANAGER_ROLE(), manager)).to.be.true; + expect(await delegation.hasRole(await delegation.OPERATOR_ROLE(), operator)).to.be.true; const newOwner = certainAddress("vault-delegation-layer-voting-new-owner"); // remains unchanged - await stVaultOwnerWithDelegation.connect(manager).transferStVaultOwnership(newOwner); - expect(await stakingVault.owner()).to.equal(stVaultOwnerWithDelegation); + await delegation.connect(manager).transferStVaultOwnership(newOwner); + expect(await stakingVault.owner()).to.equal(delegation); // remains unchanged - await stVaultOwnerWithDelegation.connect(operator).transferStVaultOwnership(newOwner); - expect(await stakingVault.owner()).to.equal(stVaultOwnerWithDelegation); + await delegation.connect(operator).transferStVaultOwnership(newOwner); + expect(await stakingVault.owner()).to.equal(delegation); await advanceChainTime(days(7n) + 1n); // remains unchanged - await stVaultOwnerWithDelegation.connect(lidoDao).transferStVaultOwnership(newOwner); - expect(await stakingVault.owner()).to.equal(stVaultOwnerWithDelegation); + await delegation.connect(lidoDao).transferStVaultOwnership(newOwner); + expect(await stakingVault.owner()).to.equal(delegation); }); }); }); diff --git a/test/0.8.25/vaults/stvault-owner-with-delegation.test.ts b/test/0.8.25/vaults/stvault-owner-with-delegation.test.ts index fda887f3d..ce3953e43 100644 --- a/test/0.8.25/vaults/stvault-owner-with-delegation.test.ts +++ b/test/0.8.25/vaults/stvault-owner-with-delegation.test.ts @@ -8,7 +8,7 @@ import { LidoLocator, StakingVault, StETH__HarnessForVaultHub, - StVaultOwnerWithDelegation, + Delegation, VaultFactory, VaultHub, } from "typechain-types"; @@ -18,7 +18,7 @@ import { certainAddress, createVaultProxy, ether } from "lib"; import { deployLidoLocator } from "test/deploy"; import { Snapshot } from "test/suite"; -describe("StVaultOwnerWithDelegation.sol", () => { +describe("Delegation.sol", () => { let deployer: HardhatEthersSigner; let admin: HardhatEthersSigner; let holder: HardhatEthersSigner; @@ -29,7 +29,7 @@ describe("StVaultOwnerWithDelegation.sol", () => { let depositContract: DepositContract__MockForBeaconChainDepositor; let vaultHub: VaultHub; let implOld: StakingVault; - let stVaultOwnerWithDelegation: StVaultOwnerWithDelegation; + let delegation: Delegation; let vaultFactory: VaultFactory; let steth: StETH__HarnessForVaultHub; @@ -53,8 +53,8 @@ describe("StVaultOwnerWithDelegation.sol", () => { // VaultHub vaultHub = await ethers.deployContract("Accounting", [admin, locator, steth, treasury], { from: deployer }); implOld = await ethers.deployContract("StakingVault", [vaultHub, depositContract], { from: deployer }); - stVaultOwnerWithDelegation = await ethers.deployContract("StVaultOwnerWithDelegation", [steth], { from: deployer }); - vaultFactory = await ethers.deployContract("VaultFactory", [admin, implOld, stVaultOwnerWithDelegation], { from: deployer }); + delegation = await ethers.deployContract("Delegation", [steth], { from: deployer }); + vaultFactory = await ethers.deployContract("VaultFactory", [admin, implOld, delegation], { from: deployer }); //add role to factory await vaultHub.connect(admin).grantRole(await vaultHub.VAULT_MASTER_ROLE(), admin); @@ -69,33 +69,33 @@ describe("StVaultOwnerWithDelegation.sol", () => { context("performanceDue", () => { it("performanceDue ", async () => { - const { stVaultOwnerWithDelegation } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); + const { delegation } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); - await stVaultOwnerWithDelegation.performanceDue(); + await delegation.performanceDue(); }); }); context("initialize", async () => { it("reverts if initialize from implementation", async () => { - await expect(stVaultOwnerWithDelegation.initialize(admin, implOld)).to.revertedWithCustomError( - stVaultOwnerWithDelegation, + await expect(delegation.initialize(admin, implOld)).to.revertedWithCustomError( + delegation, "NonProxyCallsForbidden", ); }); it("reverts if already initialized", async () => { - const { vault: vault1, stVaultOwnerWithDelegation } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); + const { vault: vault1, delegation } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); - await expect(stVaultOwnerWithDelegation.initialize(admin, vault1)).to.revertedWithCustomError( - stVaultOwnerWithDelegation, + await expect(delegation.initialize(admin, vault1)).to.revertedWithCustomError( + delegation, "AlreadyInitialized", ); }); it("initialize", async () => { - const { tx, stVaultOwnerWithDelegation } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); + const { tx, delegation } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); - await expect(tx).to.emit(stVaultOwnerWithDelegation, "Initialized"); + await expect(tx).to.emit(delegation, "Initialized"); }); }); }); diff --git a/test/0.8.25/vaults/vault.test.ts b/test/0.8.25/vaults/vault.test.ts index 510d9087a..608f9209a 100644 --- a/test/0.8.25/vaults/vault.test.ts +++ b/test/0.8.25/vaults/vault.test.ts @@ -9,7 +9,7 @@ import { StakingVault, StakingVault__factory, StETH__HarnessForVaultHub, - StVaultOwnerWithDelegation, + Delegation, VaultFactory, VaultHub__MockForVault, } from "typechain-types"; @@ -33,7 +33,7 @@ describe("StakingVault.sol", async () => { let stakingVault: StakingVault; let steth: StETH__HarnessForVaultHub; let vaultFactory: VaultFactory; - let stVaulOwnerWithDelegation: StVaultOwnerWithDelegation; + let stVaulOwnerWithDelegation: Delegation; let vaultProxy: StakingVault; let originalState: string; @@ -52,16 +52,16 @@ describe("StakingVault.sol", async () => { vaultCreateFactory = new StakingVault__factory(owner); stakingVault = await ethers.getContractFactory("StakingVault").then((f) => f.deploy(vaultHub, depositContract)); - stVaulOwnerWithDelegation = await ethers.deployContract("StVaultOwnerWithDelegation", [steth], { from: deployer }); + stVaulOwnerWithDelegation = await ethers.deployContract("Delegation", [steth], { from: deployer }); vaultFactory = await ethers.deployContract("VaultFactory", [deployer, stakingVault, stVaulOwnerWithDelegation], { from: deployer, }); - const { vault, stVaultOwnerWithDelegation } = await createVaultProxy(vaultFactory, owner, lidoAgent); + const { vault, delegation } = await createVaultProxy(vaultFactory, owner, lidoAgent); vaultProxy = vault; - delegatorSigner = await impersonate(await stVaultOwnerWithDelegation.getAddress(), ether("100.0")); + delegatorSigner = await impersonate(await delegation.getAddress(), ether("100.0")); }); beforeEach(async () => (originalState = await Snapshot.take())); diff --git a/test/0.8.25/vaults/vaultFactory.test.ts b/test/0.8.25/vaults/vaultFactory.test.ts index 64161862d..9bff2d3c2 100644 --- a/test/0.8.25/vaults/vaultFactory.test.ts +++ b/test/0.8.25/vaults/vaultFactory.test.ts @@ -12,7 +12,7 @@ import { StETH__HarnessForVaultHub, VaultFactory, VaultHub, - StVaultOwnerWithDelegation, + Delegation, } from "typechain-types"; import { certainAddress, createVaultProxy, ether } from "lib"; @@ -33,7 +33,7 @@ describe("VaultFactory.sol", () => { let vaultHub: VaultHub; let implOld: StakingVault; let implNew: StakingVault__HarnessForTestUpgrade; - let stVaultOwnerWithDelegation: StVaultOwnerWithDelegation; + let delegation: Delegation; let vaultFactory: VaultFactory; let steth: StETH__HarnessForVaultHub; @@ -60,8 +60,8 @@ describe("VaultFactory.sol", () => { implNew = await ethers.deployContract("StakingVault__HarnessForTestUpgrade", [vaultHub, depositContract], { from: deployer, }); - stVaultOwnerWithDelegation = await ethers.deployContract("StVaultOwnerWithDelegation", [steth], { from: deployer }); - vaultFactory = await ethers.deployContract("VaultFactory", [admin, implOld, stVaultOwnerWithDelegation], { from: deployer }); + delegation = await ethers.deployContract("Delegation", [steth], { from: deployer }); + vaultFactory = await ethers.deployContract("VaultFactory", [admin, implOld, delegation], { from: deployer }); //add role to factory await vaultHub.connect(admin).grantRole(await vaultHub.VAULT_MASTER_ROLE(), admin); @@ -87,10 +87,10 @@ describe("VaultFactory.sol", () => { .withArgs(ZeroAddress); }); - it("reverts if `_stVaultOwnerWithDelegation` is zero address", async () => { + it("reverts if `_delegation` is zero address", async () => { await expect(ethers.deployContract("VaultFactory", [admin, implOld, ZeroAddress], { from: deployer })) .to.be.revertedWithCustomError(vaultFactory, "ZeroArgument") - .withArgs("_stVaultOwnerWithDelegation"); + .withArgs("_delegation"); }); it("works and emit `OwnershipTransferred`, `Upgraded` events", async () => { @@ -113,17 +113,17 @@ describe("VaultFactory.sol", () => { context("createVault", () => { it("works with empty `params`", async () => { - const { tx, vault, stVaultOwnerWithDelegation } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); + const { tx, vault, delegation } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); await expect(tx) .to.emit(vaultFactory, "VaultCreated") - .withArgs(await stVaultOwnerWithDelegation.getAddress(), await vault.getAddress()); + .withArgs(await delegation.getAddress(), await vault.getAddress()); await expect(tx) - .to.emit(vaultFactory, "StVaultOwnerWithDelegationCreated") - .withArgs(await vaultOwner1.getAddress(), await stVaultOwnerWithDelegation.getAddress()); + .to.emit(vaultFactory, "DelegationCreated") + .withArgs(await vaultOwner1.getAddress(), await delegation.getAddress()); - expect(await stVaultOwnerWithDelegation.getAddress()).to.eq(await vault.owner()); + expect(await delegation.getAddress()).to.eq(await vault.owner()); expect(await vault.getBeacon()).to.eq(await vaultFactory.getAddress()); }); @@ -149,8 +149,8 @@ describe("VaultFactory.sol", () => { }; //create vault - const { vault: vault1, stVaultOwnerWithDelegation: delegator1 } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); - const { vault: vault2, stVaultOwnerWithDelegation: delegator2 } = await createVaultProxy(vaultFactory, vaultOwner2, lidoAgent); + const { vault: vault1, delegation: delegator1 } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); + const { vault: vault2, delegation: delegator2 } = await createVaultProxy(vaultFactory, vaultOwner2, lidoAgent); //owner of vault is delegator expect(await delegator1.getAddress()).to.eq(await vault1.owner()); diff --git a/test/integration/vaults-happy-path.integration.ts b/test/integration/vaults-happy-path.integration.ts index 391e2bf0f..93994e34c 100644 --- a/test/integration/vaults-happy-path.integration.ts +++ b/test/integration/vaults-happy-path.integration.ts @@ -4,7 +4,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { StakingVault, StVaultOwnerWithDelegation } from "typechain-types"; +import { StakingVault, Delegation } from "typechain-types"; import { impersonate, log, trace, updateBalance } from "lib"; import { getProtocolContext, ProtocolContext } from "lib/protocol"; @@ -55,7 +55,7 @@ describe("Scenario: Staking Vaults Happy Path", () => { let vault101: StakingVault; let vault101Address: string; - let vault101AdminContract: StVaultOwnerWithDelegation; + let vault101AdminContract: Delegation; let vault101BeaconBalance = 0n; let vault101MintingMaximum = 0n; @@ -139,10 +139,10 @@ describe("Scenario: Staking Vaults Happy Path", () => { const { stakingVaultFactory } = ctx.contracts; const implAddress = await stakingVaultFactory.implementation(); - const adminContractImplAddress = await stakingVaultFactory.stVaultOwnerWithDelegationImpl(); + const adminContractImplAddress = await stakingVaultFactory.delegationImpl(); const vaultImpl = await ethers.getContractAt("StakingVault", implAddress); - const vaultFactoryAdminContract = await ethers.getContractAt("StVaultOwnerWithDelegation", adminContractImplAddress); + const vaultFactoryAdminContract = await ethers.getContractAt("Delegation", adminContractImplAddress); expect(await vaultImpl.VAULT_HUB()).to.equal(ctx.contracts.accounting.address); expect(await vaultImpl.DEPOSIT_CONTRACT()).to.equal(depositContract); @@ -168,7 +168,7 @@ describe("Scenario: Staking Vaults Happy Path", () => { expect(createVaultEvents.length).to.equal(1n); vault101 = await ethers.getContractAt("StakingVault", createVaultEvents[0].args?.vault); - vault101AdminContract = await ethers.getContractAt("StVaultOwnerWithDelegation", createVaultEvents[0].args?.owner); + vault101AdminContract = await ethers.getContractAt("Delegation", createVaultEvents[0].args?.owner); expect(await vault101AdminContract.hasRole(await vault101AdminContract.DEFAULT_ADMIN_ROLE(), alice)).to.be.true; expect(await vault101AdminContract.hasRole(await vault101AdminContract.MANAGER_ROLE(), alice)).to.be.true; From d2c800801f5898b738af32d2e272cc437b6414d1 Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Fri, 29 Nov 2024 16:07:30 +0500 Subject: [PATCH 11/21] fix: file renaming --- ...r-with-delegation-voting.test.ts => delegation-voting.test.ts} | 0 .../{stvault-owner-with-delegation.test.ts => delegation.test.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename test/0.8.25/vaults/{st-vault-owner-with-delegation-voting.test.ts => delegation-voting.test.ts} (100%) rename test/0.8.25/vaults/{stvault-owner-with-delegation.test.ts => delegation.test.ts} (100%) diff --git a/test/0.8.25/vaults/st-vault-owner-with-delegation-voting.test.ts b/test/0.8.25/vaults/delegation-voting.test.ts similarity index 100% rename from test/0.8.25/vaults/st-vault-owner-with-delegation-voting.test.ts rename to test/0.8.25/vaults/delegation-voting.test.ts diff --git a/test/0.8.25/vaults/stvault-owner-with-delegation.test.ts b/test/0.8.25/vaults/delegation.test.ts similarity index 100% rename from test/0.8.25/vaults/stvault-owner-with-delegation.test.ts rename to test/0.8.25/vaults/delegation.test.ts From ebbad1a3af2e8aa4a7e0a0d851132a7048ee1bc4 Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Fri, 29 Nov 2024 17:51:14 +0500 Subject: [PATCH 12/21] fix: disable warning for unused report values --- contracts/0.8.25/vaults/Delegation.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/0.8.25/vaults/Delegation.sol b/contracts/0.8.25/vaults/Delegation.sol index 466a74a5a..8a18f8f32 100644 --- a/contracts/0.8.25/vaults/Delegation.sol +++ b/contracts/0.8.25/vaults/Delegation.sol @@ -370,6 +370,7 @@ contract Delegation is Dashboard, IReportReceiver { * @param _inOutDelta The net inflow or outflow since the last report. * @param _locked The amount of funds locked in the vault. */ + // solhint-disable-next-line no-unused-vars function onReport(uint256 _valuation, int256 _inOutDelta, uint256 _locked) external { if (msg.sender != address(stakingVault)) revert OnlyStVaultCanCallOnReportHook(); From b2ef4fe3ab20d5ef3b1a7b151883dccf03e82c7e Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Fri, 29 Nov 2024 19:13:42 +0500 Subject: [PATCH 13/21] fix: grant NO role to set fee --- contracts/0.8.25/vaults/VaultFactory.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/0.8.25/vaults/VaultFactory.sol b/contracts/0.8.25/vaults/VaultFactory.sol index 834bac741..2a30c9d29 100644 --- a/contracts/0.8.25/vaults/VaultFactory.sol +++ b/contracts/0.8.25/vaults/VaultFactory.sol @@ -74,12 +74,14 @@ contract VaultFactory is UpgradeableBeacon { delegation.grantRole(delegation.OPERATOR_ROLE(), _initializationParams.operator); delegation.grantRole(delegation.DEFAULT_ADMIN_ROLE(), msg.sender); + delegation.grantRole(delegation.OPERATOR_ROLE(), address(this)); delegation.grantRole(delegation.MANAGER_ROLE(), address(this)); delegation.setManagementFee(_initializationParams.managementFee); delegation.setPerformanceFee(_initializationParams.performanceFee); //revoke roles from factory delegation.revokeRole(delegation.MANAGER_ROLE(), address(this)); + delegation.revokeRole(delegation.OPERATOR_ROLE(), address(this)); delegation.revokeRole(delegation.DEFAULT_ADMIN_ROLE(), address(this)); delegation.revokeRole(delegation.LIDO_DAO_ROLE(), address(this)); From d6078950d1352b7271c96b95bd9fd2684428e813 Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Mon, 2 Dec 2024 12:56:16 +0500 Subject: [PATCH 14/21] fix: clean up imports --- contracts/0.8.25/vaults/Delegation.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/0.8.25/vaults/Delegation.sol b/contracts/0.8.25/vaults/Delegation.sol index 8a18f8f32..dd697600a 100644 --- a/contracts/0.8.25/vaults/Delegation.sol +++ b/contracts/0.8.25/vaults/Delegation.sol @@ -4,12 +4,10 @@ // See contracts/COMPILERS.md pragma solidity 0.8.25; -import {AccessControlEnumerable} from "@openzeppelin/contracts-v5.0.2/access/extensions/AccessControlEnumerable.sol"; -import {OwnableUpgradeable} from "contracts/openzeppelin/5.0.2/upgradeable/access/OwnableUpgradeable.sol"; import {IStakingVault} from "./interfaces/IStakingVault.sol"; import {IReportReceiver} from "./interfaces/IReportReceiver.sol"; -import {Dashboard} from "./Dashboard.sol"; import {Math256} from "contracts/common/lib/Math256.sol"; +import {Dashboard} from "./Dashboard.sol"; /** * @title Delegation From 41bbc8efe5d5029b7a838fc79cddd038f4dbedd2 Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Mon, 2 Dec 2024 12:56:47 +0500 Subject: [PATCH 15/21] fix: rebalanace should not be payable --- contracts/0.8.25/vaults/Dashboard.sol | 2 +- contracts/0.8.25/vaults/StakingVault.sol | 3 +-- contracts/0.8.25/vaults/interfaces/IStakingVault.sol | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/0.8.25/vaults/Dashboard.sol b/contracts/0.8.25/vaults/Dashboard.sol index cdedf3ad7..b581ec101 100644 --- a/contracts/0.8.25/vaults/Dashboard.sol +++ b/contracts/0.8.25/vaults/Dashboard.sol @@ -297,7 +297,7 @@ contract Dashboard is AccessControlEnumerable { * @param _ether Amount of ether to rebalance */ function _rebalanceVault(uint256 _ether) internal { - stakingVault.rebalance{value: msg.value}(_ether); + stakingVault.rebalance(_ether); } // ==================== Events ==================== diff --git a/contracts/0.8.25/vaults/StakingVault.sol b/contracts/0.8.25/vaults/StakingVault.sol index 92b5466eb..a7e330619 100644 --- a/contracts/0.8.25/vaults/StakingVault.sol +++ b/contracts/0.8.25/vaults/StakingVault.sol @@ -162,8 +162,7 @@ contract StakingVault is IStakingVault, IBeaconProxy, VaultBeaconChainDepositor, emit Locked(_locked); } - // TODO: SHOULD THIS BE PAYABLE? - function rebalance(uint256 _ether) external payable { + function rebalance(uint256 _ether) external { if (_ether == 0) revert ZeroArgument("_ether"); if (_ether > address(this).balance) revert InsufficientBalance(address(this).balance); // TODO: should we revert on msg.value > _ether diff --git a/contracts/0.8.25/vaults/interfaces/IStakingVault.sol b/contracts/0.8.25/vaults/interfaces/IStakingVault.sol index 989629a09..c98bb40e3 100644 --- a/contracts/0.8.25/vaults/interfaces/IStakingVault.sol +++ b/contracts/0.8.25/vaults/interfaces/IStakingVault.sol @@ -40,7 +40,7 @@ interface IStakingVault { function requestValidatorExit(bytes calldata _validatorPublicKey) external; - function rebalance(uint256 _ether) external payable; + function rebalance(uint256 _ether) external; function report(uint256 _valuation, int256 _inOutDelta, uint256 _locked) external; } From b75c74218abd38ee18dc80f97f2e939a05ad1424 Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Mon, 2 Dec 2024 13:05:05 +0500 Subject: [PATCH 16/21] feat: add a comment for clarity on contract duplication --- contracts/0.8.25/vaults/VaultBeaconChainDepositor.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/contracts/0.8.25/vaults/VaultBeaconChainDepositor.sol b/contracts/0.8.25/vaults/VaultBeaconChainDepositor.sol index dfc27930d..e3768043f 100644 --- a/contracts/0.8.25/vaults/VaultBeaconChainDepositor.sol +++ b/contracts/0.8.25/vaults/VaultBeaconChainDepositor.sol @@ -17,6 +17,15 @@ interface IDepositContract { ) external payable; } +/** + * @dev This contract is used to deposit keys to the Beacon Chain. + * This is the same as BeaconChainDepositor except the Solidity version is 0.8.25. + * We cannot use the BeaconChainDepositor contract from the common library because + * it is using an older Solidity version. We also cannot have a common contract with a version + * range because that would break the verification of the old contracts using the 0.8.9 version of this contract. + * + * This contract will be refactored to support custom deposit amounts for MAX_EB. + */ contract VaultBeaconChainDepositor { uint256 internal constant PUBLIC_KEY_LENGTH = 48; uint256 internal constant SIGNATURE_LENGTH = 96; From 1cc1dedfc791946b8c6af209e2f4e14046e0624f Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Mon, 2 Dec 2024 13:05:37 +0500 Subject: [PATCH 17/21] fix: remove unused import --- contracts/0.8.25/vaults/StakingVault.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/0.8.25/vaults/StakingVault.sol b/contracts/0.8.25/vaults/StakingVault.sol index a7e330619..791273c02 100644 --- a/contracts/0.8.25/vaults/StakingVault.sol +++ b/contracts/0.8.25/vaults/StakingVault.sol @@ -12,7 +12,6 @@ import {IReportReceiver} from "./interfaces/IReportReceiver.sol"; import {IStakingVault} from "./interfaces/IStakingVault.sol"; import {IBeaconProxy} from "./interfaces/IBeaconProxy.sol"; import {VaultBeaconChainDepositor} from "./VaultBeaconChainDepositor.sol"; -import {Versioned} from "../utils/Versioned.sol"; // TODO: extract interface and implement it From a8f95a9d6f0509f76a8f8d091f43300b2efd32dc Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Mon, 2 Dec 2024 13:24:18 +0500 Subject: [PATCH 18/21] feat: add detailed explainers --- contracts/0.8.25/vaults/StakingVault.sol | 154 ++++++++++++++++++++++- 1 file changed, 148 insertions(+), 6 deletions(-) diff --git a/contracts/0.8.25/vaults/StakingVault.sol b/contracts/0.8.25/vaults/StakingVault.sol index 791273c02..2828c99e8 100644 --- a/contracts/0.8.25/vaults/StakingVault.sol +++ b/contracts/0.8.25/vaults/StakingVault.sol @@ -13,10 +13,71 @@ import {IStakingVault} from "./interfaces/IStakingVault.sol"; import {IBeaconProxy} from "./interfaces/IBeaconProxy.sol"; import {VaultBeaconChainDepositor} from "./VaultBeaconChainDepositor.sol"; -// TODO: extract interface and implement it - +/** + * @title StakingVault + * @author Lido + * @notice A staking contract that manages staking operations and ETH deposits to the Beacon Chain + * @dev + * + * ARCHITECTURE & STATE MANAGEMENT + * ------------------------------ + * The vault uses ERC7201 namespaced storage pattern with a main VaultStorage struct containing: + * - report: Latest metrics snapshot (valuation and inOutDelta at time of report) + * - locked: Amount of ETH that cannot be withdrawn (managed by VaultHub) + * - inOutDelta: Running tally of deposits minus withdrawals since last report + * + * CORE MECHANICS + * ------------- + * 1. Deposits & Withdrawals + * - Owner can deposit ETH via fund() + * - Owner can withdraw unlocked ETH via withdraw() + * - All deposits/withdrawals update inOutDelta + * - Withdrawals are only allowed if vault remains healthy + * + * 2. Valuation & Health + * - Total value = report.valuation + (current inOutDelta - report.inOutDelta) + * - Vault is "healthy" if total value >= locked amount + * - Unlocked ETH = max(0, total value - locked amount) + * + * 3. Beacon Chain Integration + * - Can deposit validators (32 ETH each) to Beacon Chain + * - Withdrawal credentials are derived from vault address + * - Can request validator exits when needed by emitting the event, + * which acts as a signal to the operator to exit the validator, + * Triggerable Exits are not supported for now + * + * 4. Reporting & Updates + * - VaultHub periodically updates report data + * - Reports capture valuation and inOutDelta at the time of report + * - VaultHub can increase locked amount outside of reports + * + * 5. Rebalancing + * - Owner or VaultHub can trigger rebalancing when unhealthy + * - Moves ETH between vault and VaultHub to maintain health + * + * ACCESS CONTROL + * ------------- + * - Owner: Can fund, withdraw, deposit to beacon chain, request exits + * - VaultHub: Can update reports, lock amounts, force rebalance when unhealthy + * - Beacon: Controls implementation upgrades + * + * SECURITY CONSIDERATIONS + * ---------------------- + * - Locked amounts can only increase outside of reports + * - Withdrawals blocked if they would make vault unhealthy + * - Only VaultHub can update core state via reports + * - Uses ERC7201 storage pattern to prevent upgrade collisions + * - Withdrawal credentials are immutably tied to vault address + * + */ contract StakingVault is IStakingVault, IBeaconProxy, VaultBeaconChainDepositor, OwnableUpgradeable { /// @custom:storage-location erc7201:StakingVault.Vault + /** + * @dev Main storage structure for the vault + * @param report Latest report data containing valuation and inOutDelta + * @param locked Amount of ETH locked in the vault and cannot be withdrawn + * @param inOutDelta Net difference between deposits and withdrawals + */ struct VaultStorage { IStakingVault.Report report; uint128 locked; @@ -56,18 +117,34 @@ contract StakingVault is IStakingVault, IBeaconProxy, VaultBeaconChainDepositor, __Ownable_init(_owner); } + /** + * @notice Returns the current version of the contract + * @return uint64 contract version number + */ function version() public pure virtual returns (uint64) { return _version; } + /** + * @notice Returns the version of the contract when it was initialized + * @return uint64 The initialized version number + */ function getInitializedVersion() public view returns (uint64) { return _getInitializedVersion(); } + /** + * @notice Returns the beacon proxy address that controls this contract's implementation + * @return address The beacon proxy address + */ function getBeacon() public view returns (address) { return ERC1967Utils.getBeacon(); } + /** + * @notice Returns the address of the VaultHub contract + * @return address The VaultHub contract address + */ function vaultHub() public view override returns (address) { return address(VAULT_HUB); } @@ -78,19 +155,38 @@ contract StakingVault is IStakingVault, IBeaconProxy, VaultBeaconChainDepositor, emit ExecutionLayerRewardsReceived(msg.sender, msg.value); } + /** + * @notice Returns the TVL of the vault + * @return uint256 total valuation in ETH + * @dev Calculated as: + * latestReport.valuation + (current inOutDelta - latestReport.inOutDelta) + */ function valuation() public view returns (uint256) { VaultStorage storage $ = _getVaultStorage(); return uint256(int256(int128($.report.valuation) + $.inOutDelta - $.report.inOutDelta)); } + /** + * @notice Checks if the vault is in a healthy state + * @return true if valuation >= locked amount + */ function isHealthy() public view returns (bool) { return valuation() >= _getVaultStorage().locked; } + /** + * @notice Returns the current amount of ETH locked in the vault + * @return uint256 The amount of locked ETH + */ function locked() external view returns (uint256) { return _getVaultStorage().locked; } + /** + * @notice Returns amount of ETH available for withdrawal + * @return uint256 unlocked ETH that can be withdrawn + * @dev Calculated as: valuation - locked amount (returns 0 if locked > valuation) + */ function unlocked() public view returns (uint256) { uint256 _valuation = valuation(); uint256 _locked = _getVaultStorage().locked; @@ -100,14 +196,26 @@ contract StakingVault is IStakingVault, IBeaconProxy, VaultBeaconChainDepositor, return _valuation - _locked; } + /** + * @notice Returns the net difference between deposits and withdrawals + * @return int256 The current inOutDelta value + */ function inOutDelta() external view returns (int256) { return _getVaultStorage().inOutDelta; } + /** + * @notice Returns the withdrawal credentials for Beacon Chain deposits + * @return bytes32 withdrawal credentials derived from vault address + */ function withdrawalCredentials() public view returns (bytes32) { return bytes32((0x01 << 248) + uint160(address(this))); } + /** + * @notice Allows owner to fund the vault with ETH + * @dev Updates inOutDelta to track the net deposits + */ function fund() external payable onlyOwner { if (msg.value == 0) revert ZeroArgument("msg.value"); @@ -117,6 +225,12 @@ contract StakingVault is IStakingVault, IBeaconProxy, VaultBeaconChainDepositor, emit Funded(msg.sender, msg.value); } + /** + * @notice Allows owner to withdraw unlocked ETH + * @param _recipient Address to receive the ETH + * @param _ether Amount of ETH to withdraw + * @dev Checks for sufficient unlocked balance and vault health + */ function withdraw(address _recipient, uint256 _ether) external onlyOwner { if (_recipient == address(0)) revert ZeroArgument("_recipient"); if (_ether == 0) revert ZeroArgument("_ether"); @@ -134,6 +248,13 @@ contract StakingVault is IStakingVault, IBeaconProxy, VaultBeaconChainDepositor, emit Withdrawn(msg.sender, _recipient, _ether); } + /** + * @notice Deposits ETH to the Beacon Chain for validators + * @param _numberOfDeposits Number of 32 ETH deposits to make + * @param _pubkeys Validator public keys + * @param _signatures Validator signatures + * @dev Ensures vault is healthy and handles deposit logistics + */ function depositToBeaconChain( uint256 _numberOfDeposits, bytes calldata _pubkeys, @@ -146,10 +267,19 @@ contract StakingVault is IStakingVault, IBeaconProxy, VaultBeaconChainDepositor, emit DepositedToBeaconChain(msg.sender, _numberOfDeposits, _numberOfDeposits * 32 ether); } + /** + * @notice Requests validator exit from the Beacon Chain + * @param _validatorPublicKey Public key of validator to exit + */ function requestValidatorExit(bytes calldata _validatorPublicKey) external onlyOwner { emit ValidatorsExitRequest(msg.sender, _validatorPublicKey); } + /** + * @notice Updates the locked ETH amount + * @param _locked New amount to lock + * @dev Can only be called by VaultHub and cannot decrease locked amount + */ function lock(uint256 _locked) external { if (msg.sender != address(VAULT_HUB)) revert NotAuthorized("lock", msg.sender); @@ -161,15 +291,16 @@ contract StakingVault is IStakingVault, IBeaconProxy, VaultBeaconChainDepositor, emit Locked(_locked); } + /** + * @notice Rebalances ETH between vault and VaultHub + * @param _ether Amount of ETH to rebalance + * @dev Can be called by owner or VaultHub when unhealthy + */ function rebalance(uint256 _ether) external { if (_ether == 0) revert ZeroArgument("_ether"); if (_ether > address(this).balance) revert InsufficientBalance(address(this).balance); - // TODO: should we revert on msg.value > _ether if (owner() == msg.sender || (!isHealthy() && msg.sender == address(VAULT_HUB))) { - // force rebalance - // TODO: check rounding here - // mint some stETH in Lido v2 and burn it on the vault VaultStorage storage $ = _getVaultStorage(); $.inOutDelta -= SafeCast.toInt128(int256(_ether)); @@ -181,11 +312,22 @@ contract StakingVault is IStakingVault, IBeaconProxy, VaultBeaconChainDepositor, } } + /** + * @notice Returns the latest report data for the vault + * @return Report struct containing valuation and inOutDelta from last report + */ function latestReport() external view returns (IStakingVault.Report memory) { VaultStorage storage $ = _getVaultStorage(); return $.report; } + /** + * @notice Updates vault report with new metrics + * @param _valuation New total valuation + * @param _inOutDelta New in/out delta + * @param _locked New locked amount + * @dev Can only be called by VaultHub + */ function report(uint256 _valuation, int256 _inOutDelta, uint256 _locked) external { if (msg.sender != address(VAULT_HUB)) revert NotAuthorized("update", msg.sender); From dd485cd408c1346e6b792d8c9851f4696b38049a Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Mon, 2 Dec 2024 13:34:52 +0500 Subject: [PATCH 19/21] fix: make eslint happy --- lib/proxy.ts | 2 +- test/0.8.25/vaults/delegation-voting.test.ts | 8 ++++++-- test/0.8.25/vaults/delegation.test.ts | 14 +++++++------- test/0.8.25/vaults/vault.test.ts | 2 +- test/0.8.25/vaults/vaultFactory.test.ts | 10 +++++----- test/integration/vaults-happy-path.integration.ts | 2 +- 6 files changed, 21 insertions(+), 17 deletions(-) diff --git a/lib/proxy.ts b/lib/proxy.ts index ec9d9b31b..5d439f45e 100644 --- a/lib/proxy.ts +++ b/lib/proxy.ts @@ -5,10 +5,10 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { BeaconProxy, + Delegation, OssifiableProxy, OssifiableProxy__factory, StakingVault, - Delegation, VaultFactory, } from "typechain-types"; diff --git a/test/0.8.25/vaults/delegation-voting.test.ts b/test/0.8.25/vaults/delegation-voting.test.ts index 8e3495b64..31ce5d307 100644 --- a/test/0.8.25/vaults/delegation-voting.test.ts +++ b/test/0.8.25/vaults/delegation-voting.test.ts @@ -1,9 +1,13 @@ -import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { expect } from "chai"; import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { Delegation,StakingVault__MockForVaultDelegationLayer } from "typechain-types"; + import { advanceChainTime, certainAddress, days, proxify } from "lib"; + import { Snapshot } from "test/suite"; -import { StakingVault__MockForVaultDelegationLayer, Delegation } from "typechain-types"; describe("Delegation:Voting", () => { let deployer: HardhatEthersSigner; diff --git a/test/0.8.25/vaults/delegation.test.ts b/test/0.8.25/vaults/delegation.test.ts index 56b6d064a..e5109bb49 100644 --- a/test/0.8.25/vaults/delegation.test.ts +++ b/test/0.8.25/vaults/delegation.test.ts @@ -5,12 +5,12 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { Accounting, + Delegation, DepositContract__MockForBeaconChainDepositor, LidoLocator, OssifiableProxy, StakingVault, StETH__HarnessForVaultHub, - Delegation, VaultFactory, } from "typechain-types"; @@ -76,9 +76,9 @@ describe("Delegation.sol", () => { context("performanceDue", () => { it("performanceDue ", async () => { - const { delegation } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); + const { delegation: delegation_ } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); - await delegation.performanceDue(); + await delegation_.performanceDue(); }); }); @@ -91,18 +91,18 @@ describe("Delegation.sol", () => { }); it("reverts if already initialized", async () => { - const { vault: vault1, delegation } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); + const { vault: vault1, delegation: delegation_ } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); - await expect(delegation.initialize(admin, vault1)).to.revertedWithCustomError( + await expect(delegation_.initialize(admin, vault1)).to.revertedWithCustomError( delegation, "AlreadyInitialized", ); }); it("initialize", async () => { - const { tx, delegation } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); + const { tx, delegation: delegation_ } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); - await expect(tx).to.emit(delegation, "Initialized"); + await expect(tx).to.emit(delegation_, "Initialized"); }); }); }); diff --git a/test/0.8.25/vaults/vault.test.ts b/test/0.8.25/vaults/vault.test.ts index 476cc8629..6ec6677de 100644 --- a/test/0.8.25/vaults/vault.test.ts +++ b/test/0.8.25/vaults/vault.test.ts @@ -5,11 +5,11 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { + Delegation, DepositContract__MockForBeaconChainDepositor, StakingVault, StakingVault__factory, StETH__HarnessForVaultHub, - Delegation, VaultFactory, VaultHub__MockForVault, } from "typechain-types"; diff --git a/test/0.8.25/vaults/vaultFactory.test.ts b/test/0.8.25/vaults/vaultFactory.test.ts index 823d0203e..3bf21e073 100644 --- a/test/0.8.25/vaults/vaultFactory.test.ts +++ b/test/0.8.25/vaults/vaultFactory.test.ts @@ -6,6 +6,7 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { Accounting, + Delegation, DepositContract__MockForBeaconChainDepositor, LidoLocator, OssifiableProxy, @@ -13,7 +14,6 @@ import { StakingVault__HarnessForTestUpgrade, StETH__HarnessForVaultHub, VaultFactory, - Delegation, } from "typechain-types"; import { certainAddress, createVaultProxy, ether } from "lib"; @@ -122,17 +122,17 @@ describe("VaultFactory.sol", () => { context("createVault", () => { it("works with empty `params`", async () => { - const { tx, vault, delegation } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); + const { tx, vault, delegation: delegation_ } = await createVaultProxy(vaultFactory, vaultOwner1, lidoAgent); await expect(tx) .to.emit(vaultFactory, "VaultCreated") - .withArgs(await delegation.getAddress(), await vault.getAddress()); + .withArgs(await delegation_.getAddress(), await vault.getAddress()); await expect(tx) .to.emit(vaultFactory, "DelegationCreated") - .withArgs(await vaultOwner1.getAddress(), await delegation.getAddress()); + .withArgs(await vaultOwner1.getAddress(), await delegation_.getAddress()); - expect(await delegation.getAddress()).to.eq(await vault.owner()); + expect(await delegation_.getAddress()).to.eq(await vault.owner()); expect(await vault.getBeacon()).to.eq(await vaultFactory.getAddress()); }); diff --git a/test/integration/vaults-happy-path.integration.ts b/test/integration/vaults-happy-path.integration.ts index 93994e34c..6c524b66f 100644 --- a/test/integration/vaults-happy-path.integration.ts +++ b/test/integration/vaults-happy-path.integration.ts @@ -4,7 +4,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { StakingVault, Delegation } from "typechain-types"; +import { Delegation,StakingVault } from "typechain-types"; import { impersonate, log, trace, updateBalance } from "lib"; import { getProtocolContext, ProtocolContext } from "lib/protocol"; From fc5b704398e6c6e51e2191d2b7018f7734beea4f Mon Sep 17 00:00:00 2001 From: Azat Serikov Date: Mon, 2 Dec 2024 13:40:48 +0500 Subject: [PATCH 20/21] fix: make eslint even happier --- test/0.8.25/vaults/delegation-voting.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/0.8.25/vaults/delegation-voting.test.ts b/test/0.8.25/vaults/delegation-voting.test.ts index 31ce5d307..c5650b6ed 100644 --- a/test/0.8.25/vaults/delegation-voting.test.ts +++ b/test/0.8.25/vaults/delegation-voting.test.ts @@ -3,7 +3,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { Delegation,StakingVault__MockForVaultDelegationLayer } from "typechain-types"; +import { Delegation, StakingVault__MockForVaultDelegationLayer } from "typechain-types"; import { advanceChainTime, certainAddress, days, proxify } from "lib"; @@ -51,7 +51,7 @@ describe("Delegation:Voting", () => { describe("setPerformanceFee", () => { it("reverts if the caller does not have the required role", async () => { - expect(delegation.connect(stranger).setPerformanceFee(100)).to.be.revertedWithCustomError( + await expect(delegation.connect(stranger).setPerformanceFee(100)).to.be.revertedWithCustomError( delegation, "NotACommitteeMember", ); @@ -116,7 +116,7 @@ describe("Delegation:Voting", () => { describe("transferStakingVaultOwnership", () => { it("reverts if the caller does not have the required role", async () => { - expect(delegation.connect(stranger).transferStVaultOwnership(certainAddress("vault-delegation-layer-voting-new-owner"))).to.be.revertedWithCustomError( + await expect(delegation.connect(stranger).transferStVaultOwnership(certainAddress("vault-delegation-layer-voting-new-owner"))).to.be.revertedWithCustomError( delegation, "NotACommitteeMember", ); From 847c9ab0f038ff65c60c7cfede5bbed9db33f528 Mon Sep 17 00:00:00 2001 From: Aleksei Potapkin Date: Mon, 2 Dec 2024 11:50:09 +0200 Subject: [PATCH 21/21] chore: missed new line --- contracts/0.8.25/vaults/Delegation.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/0.8.25/vaults/Delegation.sol b/contracts/0.8.25/vaults/Delegation.sol index dd697600a..8c03899a8 100644 --- a/contracts/0.8.25/vaults/Delegation.sol +++ b/contracts/0.8.25/vaults/Delegation.sol @@ -53,7 +53,8 @@ contract Delegation is Dashboard, IReportReceiver { */ bytes32 public constant STAKER_ROLE = keccak256("Vault.Delegation.StakerRole"); - /** @notice Role for the operator + /** + * @notice Role for the operator * Operator can: * - claim the performance due * - vote on performance fee changes