diff --git a/.env.example b/.env.example index 916e48b31..b654199fd 100644 --- a/.env.example +++ b/.env.example @@ -25,9 +25,10 @@ LOCAL_WITHDRAWAL_VAULT_ADDRESS= # RPC URL for a separate, non Hardhat Network node (Anvil, Infura, Alchemy, etc.) MAINNET_RPC_URL=http://localhost:8545 + # RPC URL for Hardhat Network forking, required for running tests on mainnet fork with tracing (Infura, Alchemy, etc.) # https://hardhat.org/hardhat-network/docs/guides/forking-other-networks#forking-other-networks -MAINNET_FORKING_URL= +HARDHAT_FORKING_URL= # https://docs.lido.fi/deployed-contracts MAINNET_LOCATOR_ADDRESS=0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fcd9c9941..b95f36970 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,9 @@ the [Lido Research Forum](https://research.lido.fi/). - [Foundry](https://book.getfoundry.sh/) latest available version > [!TIP] -> On macOS with Homebrew, it is recommended to install Node.js using [`n`](https://github.com/tj/n) or [ > `nvm`](https://github.com/nvm-sh/nvm) version managers. +> On macOS with Homebrew, it is recommended to install Node.js using [`n`](https://github.com/tj/n) or +> [ >`nvm`](https://github.com/nvm-sh/nvm) version managers. +> > Example setup process using `n` package manager for zsh users: > > ```bash @@ -325,8 +327,8 @@ This is the most common method for running integration tests. It uses an instanc mainnet environment, allowing you to run integration tests with trace logging. > [!NOTE] -> Ensure that `MAINNET_FORKING_URL` and other `MAINNET_*` environment variables are set in the `.env` file (refer to -> `.env.example` for guidance). +> Ensure that `HARDHAT_FORKING_URL` is set to Ethereum Mainnet RPC and `MAINNET_*` environment variables are set in the +> `.env` file (refer to `.env.example` for guidance). ```bash # Run all integration tests diff --git a/contracts/0.4.24/template/LidoTemplate.sol b/contracts/0.4.24/template/LidoTemplate.sol index b589f04d8..49af31697 100644 --- a/contracts/0.4.24/template/LidoTemplate.sol +++ b/contracts/0.4.24/template/LidoTemplate.sol @@ -74,6 +74,7 @@ contract LidoTemplate is IsContract { string private constant LIDO_APP_NAME = "lido"; string private constant ORACLE_APP_NAME = "oracle"; string private constant NODE_OPERATORS_REGISTRY_APP_NAME = "node-operators-registry"; + string private constant SIMPLE_DVT_APP_NAME = "simple-dvt"; // DAO config constants bool private constant TOKEN_TRANSFERABLE = true; @@ -85,6 +86,7 @@ contract LidoTemplate is IsContract { Repo lido; Repo oracle; Repo nodeOperatorsRegistry; + Repo simpleDVT; Repo aragonAgent; Repo aragonFinance; Repo aragonTokenManager; @@ -104,6 +106,7 @@ contract LidoTemplate is IsContract { Lido lido; LegacyOracle oracle; NodeOperatorsRegistry operators; + NodeOperatorsRegistry sdvt; address stakingRouter; } @@ -261,7 +264,6 @@ contract LidoTemplate is IsContract { bytes _nodeOperatorsRegistryContentURI, address _oracleImplAddress, bytes _oracleContentURI - ) external onlyOwner { require(deployState.lidoRegistry != address(0), ERROR_REGISTRY_NOT_DEPLOYED); @@ -293,14 +295,18 @@ contract LidoTemplate is IsContract { _oracleContentURI ); + apmRepos.simpleDVT = lidoRegistry.newRepoWithVersion( + SIMPLE_DVT_APP_NAME, + this, + _initialSemanticVersion, + _nodeOperatorsRegistryImplAddress, + _nodeOperatorsRegistryContentURI + ); + emit TmplReposCreated(); } - function newDAO( - string _tokenName, - string _tokenSymbol, - uint64[4] _votingSettings - ) external onlyOwner { + function newDAO(string _tokenName, string _tokenSymbol, uint64[4] _votingSettings) external onlyOwner { DeployState memory state = deployState; require(state.lidoRegistry != address(0), ERROR_REGISTRY_NOT_DEPLOYED); @@ -328,7 +334,7 @@ contract LidoTemplate is IsContract { _votingSettings[0], // support _votingSettings[1], // acceptance _votingSettings[2], // duration - _votingSettings[3] // objectionPhaseDuration + _votingSettings[3] // objectionPhaseDuration ); bytes memory noInit = new bytes(0); @@ -345,6 +351,14 @@ contract LidoTemplate is IsContract { ) ); + state.sdvt = NodeOperatorsRegistry( + _installNonDefaultApp( + state.dao, + _getAppId(SIMPLE_DVT_APP_NAME, state.lidoRegistryEnsNode), + noInit + ) + ); + state.oracle = LegacyOracle( _installNonDefaultApp(state.dao, _getAppId(ORACLE_APP_NAME, state.lidoRegistryEnsNode), noInit) ); @@ -387,13 +401,7 @@ contract LidoTemplate is IsContract { emit TmplTokensIssued(totalAmount); } - function finalizeDAO( - string _daoName, - uint256 _unvestedTokensAmount, - address _stakingRouter - ) - external onlyOwner - { + function finalizeDAO(string _daoName, uint256 _unvestedTokensAmount, address _stakingRouter) external onlyOwner { require(_stakingRouter != address(0)); DeployState memory state = deployState; APMRepos memory repos = apmRepos; @@ -474,9 +482,7 @@ contract LidoTemplate is IsContract { uint64 _acceptance, uint64 _duration, uint64 _objectionPhaseDuration - ) - private returns (Voting) - { + ) private returns (Voting) { bytes32 appId = _getAppId(ARAGON_VOTING_APP_NAME, _lidoRegistryEnsNode); bytes memory initializeData = abi.encodeWithSelector( Voting(0).initialize.selector, @@ -511,11 +517,7 @@ contract LidoTemplate is IsContract { /* TOKEN */ - function _createToken( - string memory _name, - string memory _symbol, - uint8 _decimals - ) internal returns (MiniMeToken) { + function _createToken(string memory _name, string memory _symbol, uint8 _decimals) internal returns (MiniMeToken) { MiniMeToken token = miniMeFactory.createCloneToken(MiniMeToken(address(0)), 0, _name, _decimals, _symbol, true); return token; } @@ -530,10 +532,7 @@ contract LidoTemplate is IsContract { uint64 _vestingEnd, bool _vestingRevokable, uint256 _expectedFinalTotalSupply - ) - private - returns (uint256 totalAmount) - { + ) private returns (uint256 totalAmount) { totalAmount = 0; uint256 i; @@ -614,6 +613,18 @@ contract LidoTemplate is IsContract { acl.createPermission(_state.stakingRouter, _state.operators, _state.operators.STAKING_ROUTER_ROLE(), voting); acl.createPermission(_state.agent, _state.operators, _state.operators.MANAGE_NODE_OPERATOR_ROLE(), voting); + // SimpleDVT + perms[0] = _state.operators.MANAGE_SIGNING_KEYS(); + perms[1] = _state.operators.SET_NODE_OPERATOR_LIMIT_ROLE(); + perms[2] = _state.operators.MANAGE_NODE_OPERATOR_ROLE(); + for (i = 0; i < 3; ++i) { + _createPermissionForVoting(acl, _state.sdvt, perms[i], voting); + } + acl.createPermission(_state.stakingRouter, _state.sdvt, _state.sdvt.STAKING_ROUTER_ROLE(), this); + acl.grantPermission(_state.agent, _state.sdvt, _state.sdvt.STAKING_ROUTER_ROLE()); + + _transferPermissionFromTemplate(acl, _state.sdvt, voting, _state.sdvt.STAKING_ROUTER_ROLE()); + // Lido perms[0] = _state.lido.PAUSE_ROLE(); perms[1] = _state.lido.RESUME_ROLE(); @@ -630,38 +641,20 @@ contract LidoTemplate is IsContract { _createPermissionForTemplate(_acl, _tokenManager, _tokenManager.ASSIGN_ROLE()); } - function _createPermissionForVoting( - ACL _acl, - address _app, - bytes32 perm, - address _voting - ) internal { + function _createPermissionForVoting(ACL _acl, address _app, bytes32 perm, address _voting) internal { _acl.createPermission(_voting, _app, perm, _voting); } - function _createAgentPermissions( - ACL _acl, - Agent _agent, - address _voting - ) internal { + function _createAgentPermissions(ACL _acl, Agent _agent, address _voting) internal { _createPermissionForVoting(_acl, _agent, _agent.EXECUTE_ROLE(), _voting); _createPermissionForVoting(_acl, _agent, _agent.RUN_SCRIPT_ROLE(), _voting); } - function _createVaultPermissions( - ACL _acl, - Vault _vault, - address _finance, - address _voting - ) internal { + function _createVaultPermissions(ACL _acl, Vault _vault, address _finance, address _voting) internal { _acl.createPermission(_finance, _vault, _vault.TRANSFER_ROLE(), _voting); } - function _createFinancePermissions( - ACL _acl, - Finance _finance, - address _voting - ) internal { + function _createFinancePermissions(ACL _acl, Finance _finance, address _voting) internal { _createPermissionForVoting(_acl, _finance, _finance.EXECUTE_PAYMENTS_ROLE(), _voting); _createPermissionForVoting(_acl, _finance, _finance.MANAGE_PAYMENTS_ROLE(), _voting); _createPermissionForVoting(_acl, _finance, _finance.CREATE_PAYMENTS_ROLE(), _voting); @@ -673,39 +666,23 @@ contract LidoTemplate is IsContract { _createPermissionForVoting(_acl, registry, registry.REGISTRY_ADD_EXECUTOR_ROLE(), _voting); } - function _createVotingPermissions( - ACL _acl, - Voting _voting, - address _tokenManager - ) internal { + function _createVotingPermissions(ACL _acl, Voting _voting, address _tokenManager) internal { _createPermissionForVoting(_acl, _voting, _voting.MODIFY_QUORUM_ROLE(), _voting); _createPermissionForVoting(_acl, _voting, _voting.MODIFY_SUPPORT_ROLE(), _voting); _acl.createPermission(_tokenManager, _voting, _voting.CREATE_VOTES_ROLE(), _voting); } - function _configureTokenManagerPermissions( - ACL _acl, - TokenManager _tokenManager, - address _voting - ) internal { + function _configureTokenManagerPermissions(ACL _acl, TokenManager _tokenManager, address _voting) internal { _removePermissionFromTemplate(_acl, _tokenManager, _tokenManager.ISSUE_ROLE()); _removePermissionFromTemplate(_acl, _tokenManager, _tokenManager.ASSIGN_ROLE()); _createPermissionForVoting(_acl, _tokenManager, _tokenManager.ASSIGN_ROLE(), _voting); } - function _createPermissionForTemplate( - ACL _acl, - address _app, - bytes32 _permission - ) private { + function _createPermissionForTemplate(ACL _acl, address _app, bytes32 _permission) private { _acl.createPermission(address(this), _app, _permission, address(this)); } - function _removePermissionFromTemplate( - ACL _acl, - address _app, - bytes32 _permission - ) private { + function _removePermissionFromTemplate(ACL _acl, address _app, bytes32 _permission) private { _acl.revokePermission(address(this), _app, _permission); _acl.removePermissionManager(_app, _permission); } @@ -716,12 +693,7 @@ contract LidoTemplate is IsContract { _transferPermissionFromTemplate(_acl, _acl, _voting, _acl.CREATE_PERMISSIONS_ROLE(), _voting); } - function _transferPermissionFromTemplate( - ACL _acl, - address _app, - address _to, - bytes32 _permission - ) private { + function _transferPermissionFromTemplate(ACL _acl, address _app, address _to, bytes32 _permission) private { _transferPermissionFromTemplate(_acl, _app, _to, _permission, _to); } diff --git a/globals.d.ts b/globals.d.ts index 7637be82d..b08580600 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -1,19 +1,21 @@ declare namespace NodeJS { export interface ProcessEnv { - /* RPC URL for Hardhat Network forking, required for running tests on mainnet fork with tracing */ - MAINNET_FORKING_URL?: string; + /* iternal logging verbosity (used in scratch deploy / integration tests) */ + LOG_LEVEL?: "all" | "debug" | "info" | "warn" | "error" | "none"; // default: "info" - /* logging verbosity */ - LOG_LEVEL?: "all" | "debug" | "info" | "warn" | "error" | "none"; + /** + * Flags for changing the behavior of the Hardhat Network + */ + + /* RPC URL for Hardhat Network forking, required for running tests on mainnet fork with tracing */ + HARDHAT_FORKING_URL?: string; /** * Flags for changing the behavior of the integration tests */ /* if "on" the integration tests will deploy the contracts to the empty Hardhat Network node using scratch deploy */ - INTEGRATION_SCRATCH_DEPLOY?: "on" | "off"; - /* if "on" the integration tests will enable assertions and checks for the simple DVT module */ - INTEGRATION_SIMPLE_DVT_MODULE?: "on" | "off"; + INTEGRATION_ON_SCRATCH?: "on" | "off"; // default: "off" /** * Network configuration for the protocol discovery. diff --git a/hardhat.config.ts b/hardhat.config.ts index c4b0c2ef0..03f7a0b81 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -21,16 +21,18 @@ import { mochaRootHooks } from "test/hooks"; import "./tasks"; const RPC_URL: string = process.env.RPC_URL || ""; -const MAINNET_FORKING_URL = process.env.MAINNET_FORKING_URL || ""; -const INTEGRATION_SCRATCH_DEPLOY = process.env.INTEGRATION_SCRATCH_DEPLOY || "off"; const ACCOUNTS_PATH = "./accounts.json"; -/** - * Determines the forking configuration for Hardhat. - * @returns The forking configuration object or undefined. - */ +const HARDHAT_FORKING_URL = process.env.HARDHAT_FORKING_URL || ""; + +const INTEGRATION_WITH_SCRATCH_DEPLOY = process.env.INTEGRATION_WITH_SCRATCH_DEPLOY || "off"; + +/* Determines the forking configuration for Hardhat */ function getHardhatForkingConfig() { - return INTEGRATION_SCRATCH_DEPLOY === "on" || !MAINNET_FORKING_URL ? undefined : { url: MAINNET_FORKING_URL }; + if (INTEGRATION_WITH_SCRATCH_DEPLOY === "on" || !HARDHAT_FORKING_URL) { + return undefined; + } + return { url: HARDHAT_FORKING_URL }; } function loadAccounts(networkName: string) { diff --git a/lib/protocol/context.ts b/lib/protocol/context.ts index ed05cc8c4..2ec5353aa 100644 --- a/lib/protocol/context.ts +++ b/lib/protocol/context.ts @@ -23,12 +23,11 @@ export const getProtocolContext = async (): Promise<ProtocolContext> => { // By default, all flags are "on" const flags = { - isScratchDeploy: process.env.INTEGRATION_SCRATCH_DEPLOY === "on", - withSimpleDvtModule: process.env.INTEGRATION_SIMPLE_DVT_MODULE !== "off", + onScratch: process.env.INTEGRATION_ON_SCRATCH === "on", } as ProtocolContextFlags; log.debug("Protocol context flags", { - "With simple DVT module": flags.withSimpleDvtModule, + "On scratch": flags.onScratch, }); const context = { diff --git a/lib/protocol/helpers/index.ts b/lib/protocol/helpers/index.ts index be5b6a4ac..66c854bbb 100644 --- a/lib/protocol/helpers/index.ts +++ b/lib/protocol/helpers/index.ts @@ -14,6 +14,5 @@ export { report, } from "./accounting"; -export { sdvtEnsureOperators } from "./sdvt.helper"; - -export { norEnsureOperators } from "./nor.helper"; +export { sdvtEnsureOperators } from "./sdvt"; +export { norEnsureOperators } from "./nor"; diff --git a/lib/protocol/helpers/nor.helper.ts b/lib/protocol/helpers/nor.ts similarity index 100% rename from lib/protocol/helpers/nor.helper.ts rename to lib/protocol/helpers/nor.ts diff --git a/lib/protocol/helpers/sdvt.helper.ts b/lib/protocol/helpers/sdvt.ts similarity index 99% rename from lib/protocol/helpers/sdvt.helper.ts rename to lib/protocol/helpers/sdvt.ts index 9d58304fa..85b1981ac 100644 --- a/lib/protocol/helpers/sdvt.helper.ts +++ b/lib/protocol/helpers/sdvt.ts @@ -6,7 +6,7 @@ import { impersonate, log, streccak, trace } from "lib"; import { ether } from "../../units"; import { ProtocolContext } from "../types"; -import { getOperatorManagerAddress, getOperatorName, getOperatorRewardAddress } from "./nor.helper"; +import { getOperatorManagerAddress, getOperatorName, getOperatorRewardAddress } from "./nor"; const MIN_OPS_COUNT = 3n; const MIN_OP_KEYS_COUNT = 10n; diff --git a/lib/protocol/networks.ts b/lib/protocol/networks.ts index 4b2b1f4c5..c82cc91c5 100644 --- a/lib/protocol/networks.ts +++ b/lib/protocol/networks.ts @@ -80,8 +80,7 @@ async function getLocalNetworkConfig(network: string, source: string): Promise<P locator: config["lidoLocator"].proxy.address, agentAddress: config["app:aragon-agent"].proxy.address, votingAddress: config["app:aragon-voting"].proxy.address, - easyTrackAddress: config["app:aragon-agent"].proxy.address, - sdvt: config["app:node-operators-registry"].proxy.address, + easyTrackAddress: config["app:aragon-voting"].proxy.address, }; return new ProtocolNetworkConfig(getPrefixedEnv(network.toUpperCase(), defaultEnv), defaults, `${network}-${source}`); } diff --git a/lib/protocol/provision.ts b/lib/protocol/provision.ts index 476e33e0c..29a0d7681 100644 --- a/lib/protocol/provision.ts +++ b/lib/protocol/provision.ts @@ -3,6 +3,7 @@ import { ensureOracleCommitteeMembers, ensureStakeLimit, norEnsureOperators, + sdvtEnsureOperators, unpauseStaking, unpauseWithdrawalQueue, } from "./helpers"; @@ -21,6 +22,7 @@ export const provision = async (ctx: ProtocolContext) => { await unpauseWithdrawalQueue(ctx); await norEnsureOperators(ctx, 3n, 5n); + await sdvtEnsureOperators(ctx, 3n, 5n); await ensureStakeLimit(ctx); }; diff --git a/lib/protocol/types.ts b/lib/protocol/types.ts index f94f43070..ff5963429 100644 --- a/lib/protocol/types.ts +++ b/lib/protocol/types.ts @@ -134,8 +134,7 @@ export type ProtocolSigners = { export type Signer = keyof ProtocolSigners; export type ProtocolContextFlags = { - isScratchDeploy: boolean; - withSimpleDvtModule: boolean; + onScratch: boolean; }; export type ProtocolContext = { diff --git a/lib/state-file.ts b/lib/state-file.ts index ba7d271cf..a2aa3348a 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -12,11 +12,12 @@ export type DeploymentState = { [key: string]: any; }; -export const AppNames = { +export const TemplateAppNames = { // Lido apps LIDO: "lido", ORACLE: "oracle", NODE_OPERATORS_REGISTRY: "node-operators-registry", + SIMPLE_DVT: "simple-dvt", // Aragon apps ARAGON_AGENT: "aragon-agent", ARAGON_FINANCE: "aragon-finance", @@ -32,6 +33,7 @@ export enum Sk { appLido = "app:lido", appOracle = `app:oracle`, appNodeOperatorsRegistry = "app:node-operators-registry", + appSimpleDvt = "app:simple-dvt", aragonAcl = "aragon-acl", aragonEvmScriptRegistry = "aragon-evm-script-registry", aragonApmRegistry = "aragon-apm-registry", @@ -57,6 +59,8 @@ export enum Sk { lidoTemplate = "lidoTemplate", miniMeTokenFactory = "miniMeTokenFactory", lidoTemplateCreateStdAppReposTx = "lidoTemplateCreateStdAppReposTx", + nodeOperatorsRegistry = "nodeOperatorsRegistry", + simpleDvt = "simpleDvt", createAppReposTx = "createAppReposTx", lidoTemplateNewDaoTx = "lidoTemplateNewDaoTx", callsScript = "callsScript", @@ -138,13 +142,25 @@ export function readNetworkState({ deployer?: string; networkStateFile?: string; } = {}) { + const networkName = hardhatNetwork.name; + const networkChainId = hardhatNetwork.config.chainId; + const fileName = networkStateFile ? resolve(NETWORK_STATE_FILE_DIR, networkStateFile) - : _getFileName(hardhatNetwork.name, NETWORK_STATE_FILE_BASENAME, NETWORK_STATE_FILE_DIR); + : _getFileName(networkName, NETWORK_STATE_FILE_BASENAME, NETWORK_STATE_FILE_DIR); + const state = _readStateFile(fileName); + // Validate the deployer if (deployer !== undefined && deployer != state.deployer) { - throw new Error(`The specified deployer ${deployer} does not match the one ${state.deployer} in the state file`); + throw new Error(`The specified deployer ${deployer} does not match the one ${state.deployer} in the state file!`); + } + + // Validate the chainId + if (state[Sk.chainSpec].chainId && networkChainId !== parseInt(state[Sk.chainSpec].chainId)) { + throw new Error( + `The chainId: ${networkChainId} does not match the one (${state[Sk.chainSpec].chainId}) in the state file!`, + ); } return state; diff --git a/package.json b/package.json index 5ba138163..c8461a5f5 100644 --- a/package.json +++ b/package.json @@ -25,10 +25,10 @@ "test:integration": "hardhat test test/integration/**/*.ts --bail", "test:integration:trace": "hardhat test test/integration/**/*.ts --trace --disabletracer --bail", "test:integration:fulltrace": "hardhat test test/integration/**/*.ts --fulltrace --disabletracer --bail", - "test:integration:scratch": "INTEGRATION_SCRATCH_DEPLOY=on INTEGRATION_SIMPLE_DVT_MODULE=off hardhat test test/integration/**/*.ts --bail", - "test:integration:scratch:trace": "INTEGRATION_SCRATCH_DEPLOY=on INTEGRATION_SIMPLE_DVT_MODULE=off hardhat test test/integration/**/*.ts --trace --disabletracer --bail", - "test:integration:scratch:fulltrace": "INTEGRATION_SCRATCH_DEPLOY=on INTEGRATION_SIMPLE_DVT_MODULE=off hardhat test test/integration/**/*.ts --fulltrace --disabletracer --bail", - "test:integration:fork:local": "INTEGRATION_SIMPLE_DVT_MODULE=off hardhat test test/integration/**/*.ts --network local --bail", + "test:integration:scratch": "INTEGRATION_WITH_SCRATCH_DEPLOY=on hardhat test test/integration/**/*.ts --bail", + "test:integration:scratch:trace": "INTEGRATION_WITH_SCRATCH_DEPLOY=on hardhat test test/integration/**/*.ts --trace --disabletracer --bail", + "test:integration:scratch:fulltrace": "INTEGRATION_WITH_SCRATCH_DEPLOY=on hardhat test test/integration/**/*.ts --fulltrace --disabletracer --bail", + "test:integration:fork:local": "hardhat test test/integration/**/*.ts --network local --bail", "test:integration:fork:mainnet": "hardhat test test/integration/**/*.ts --network mainnet-fork --bail", "typecheck": "tsc --noEmit", "prepare": "husky", diff --git a/scripts/scratch/deployed-testnet-defaults.json b/scripts/scratch/deployed-testnet-defaults.json index 5717913dd..e3524eff1 100644 --- a/scripts/scratch/deployed-testnet-defaults.json +++ b/scripts/scratch/deployed-testnet-defaults.json @@ -132,6 +132,12 @@ "stuckPenaltyDelay": 172800 } }, + "simpleDvt": { + "deployParameters": { + "stakingModuleTypeId": "curated-onchain-v1", + "stuckPenaltyDelay": 432000 + } + }, "withdrawalQueueERC721": { "deployParameters": { "name": "Lido: stETH Withdrawal NFT", diff --git a/scripts/scratch/steps.json b/scripts/scratch/steps.json index 85f28f4cd..cd389cbcb 100644 --- a/scripts/scratch/steps.json +++ b/scripts/scratch/steps.json @@ -14,7 +14,7 @@ "scratch/steps/0110-finalize-dao", "scratch/steps/0120-initialize-non-aragon-contracts", "scratch/steps/0130-grant-roles", - "scratch/steps/0140-plug-curated-staking-module", + "scratch/steps/0140-plug-staking-modules", "scratch/steps/0150-transfer-roles" ] } diff --git a/scripts/scratch/steps/0070-deploy-dao.ts b/scripts/scratch/steps/0070-deploy-dao.ts index baab075ee..77690daab 100644 --- a/scripts/scratch/steps/0070-deploy-dao.ts +++ b/scripts/scratch/steps/0070-deploy-dao.ts @@ -9,12 +9,12 @@ import { makeTx } from "lib/deploy"; import { findEvents, findEventsWithInterfaces } from "lib/event"; import { cy, log, yl } from "lib/log"; import { - AppNames, DeploymentState, persistNetworkState, readNetworkState, setValueInState, Sk, + TemplateAppNames, updateObjectInState, } from "lib/state-file"; @@ -111,7 +111,7 @@ async function saveStateFromNewDAOTx(newDAOReceipt: ContractTransactionReceipt) const appInstalledEvents = findEvents(newDAOReceipt, "TmplAppInstalled"); const lidoApmEnsName = state[Sk.lidoApmEnsName]; - const VALID_APP_NAMES = Object.entries(AppNames).map((e) => e[1]); + const VALID_APP_NAMES = Object.entries(TemplateAppNames).map((e) => e[1]); const appIdNameEntries = VALID_APP_NAMES.map((name) => [ethers.namehash(`${name}.${lidoApmEnsName}`), name]); const appNameByAppId = Object.fromEntries(appIdNameEntries); const expectedAppIds = appIdNameEntries.map((e) => e[0]); diff --git a/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts b/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts index e88628697..dab37394b 100644 --- a/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts @@ -13,7 +13,9 @@ export async function main() { const lidoAddress = state[Sk.appLido].proxy.address; const legacyOracleAddress = state[Sk.appOracle].proxy.address; const nodeOperatorsRegistryAddress = state[Sk.appNodeOperatorsRegistry].proxy.address; - const nodeOperatorsRegistryParams = state["nodeOperatorsRegistry"].deployParameters; + const nodeOperatorsRegistryParams = state[Sk.nodeOperatorsRegistry].deployParameters; + const simpleDvtRegistryAddress = state[Sk.appSimpleDvt].proxy.address; + const simpleDvtRegistryParams = state[Sk.simpleDvt].deployParameters; const validatorsExitBusOracleParams = state[Sk.validatorsExitBusOracle].deployParameters; const accountingOracleParams = state[Sk.accountingOracle].deployParameters; const stakingRouterAddress = state[Sk.stakingRouter].proxy.address; @@ -37,15 +39,30 @@ export async function main() { // Initialize NodeOperatorsRegistry // https://github.com/ethereum/solidity-examples/blob/master/docs/bytes/Bytes.md#description - const stakingModuleTypeId = - "0x" + - ethers.AbiCoder.defaultAbiCoder().encode(["string"], [nodeOperatorsRegistryParams.stakingModuleTypeId]).slice(-64); + const encodeStakingModuleTypeId = (stakingModuleTypeId: string): string => + "0x" + ethers.AbiCoder.defaultAbiCoder().encode(["string"], [stakingModuleTypeId]).slice(-64); const nodeOperatorsRegistry = await loadContract("NodeOperatorsRegistry", nodeOperatorsRegistryAddress); await makeTx( nodeOperatorsRegistry, "initialize", - [lidoLocatorAddress, stakingModuleTypeId, nodeOperatorsRegistryParams.stuckPenaltyDelay], + [ + lidoLocatorAddress, + encodeStakingModuleTypeId(nodeOperatorsRegistryParams.stakingModuleTypeId), + nodeOperatorsRegistryParams.stuckPenaltyDelay, + ], + { from: deployer }, + ); + + const simpleDvtRegistry = await loadContract("NodeOperatorsRegistry", simpleDvtRegistryAddress); + await makeTx( + simpleDvtRegistry, + "initialize", + [ + lidoLocatorAddress, + encodeStakingModuleTypeId(simpleDvtRegistryParams.stakingModuleTypeId), + simpleDvtRegistryParams.stuckPenaltyDelay, + ], { from: deployer }, ); diff --git a/scripts/scratch/steps/0130-grant-roles.ts b/scripts/scratch/steps/0130-grant-roles.ts index 39c073b7a..b6deb8af6 100644 --- a/scripts/scratch/steps/0130-grant-roles.ts +++ b/scripts/scratch/steps/0130-grant-roles.ts @@ -1,5 +1,7 @@ import { ethers } from "hardhat"; +import { Burner, StakingRouter, ValidatorsExitBusOracle, WithdrawalQueueERC721 } from "typechain-types"; + import { loadContract } from "lib/contract"; import { makeTx } from "lib/deploy"; import { log } from "lib/log"; @@ -10,7 +12,9 @@ export async function main() { const state = readNetworkState({ deployer }); const lidoAddress = state[Sk.appLido].proxy.address; + const agentAddress = state[Sk.appAgent].proxy.address; const nodeOperatorsRegistryAddress = state[Sk.appNodeOperatorsRegistry].proxy.address; + const simpleDvtApp = state[Sk.appSimpleDvt].proxy.address; const gateSealAddress = state.gateSeal.address; const burnerAddress = state[Sk.burner].address; const stakingRouterAddress = state[Sk.stakingRouter].proxy.address; @@ -20,50 +24,50 @@ export async function main() { const depositSecurityModuleAddress = state[Sk.depositSecurityModule].address; // StakingRouter - const stakingRouter = await loadContract("StakingRouter", stakingRouterAddress); + const stakingRouter = await loadContract<StakingRouter>("StakingRouter", stakingRouterAddress); await makeTx( stakingRouter, "grantRole", - [await stakingRouter.getFunction("STAKING_MODULE_PAUSE_ROLE")(), depositSecurityModuleAddress], + [await stakingRouter.STAKING_MODULE_PAUSE_ROLE(), depositSecurityModuleAddress], { from: deployer }, ); await makeTx( stakingRouter, "grantRole", - [await stakingRouter.getFunction("STAKING_MODULE_RESUME_ROLE")(), depositSecurityModuleAddress], + [await stakingRouter.STAKING_MODULE_RESUME_ROLE(), depositSecurityModuleAddress], { from: deployer }, ); await makeTx( stakingRouter, "grantRole", - [await stakingRouter.getFunction("REPORT_EXITED_VALIDATORS_ROLE")(), accountingOracleAddress], - { from: deployer }, - ); - await makeTx( - stakingRouter, - "grantRole", - [await stakingRouter.getFunction("REPORT_REWARDS_MINTED_ROLE")(), lidoAddress], + [await stakingRouter.REPORT_EXITED_VALIDATORS_ROLE(), accountingOracleAddress], { from: deployer }, ); + await makeTx(stakingRouter, "grantRole", [await stakingRouter.REPORT_REWARDS_MINTED_ROLE(), lidoAddress], { + from: deployer, + }); + await makeTx(stakingRouter, "grantRole", [await stakingRouter.STAKING_MODULE_MANAGE_ROLE(), agentAddress], { + from: deployer, + }); // ValidatorsExitBusOracle if (gateSealAddress) { - const validatorsExitBusOracle = await loadContract("ValidatorsExitBusOracle", validatorsExitBusOracleAddress); - await makeTx( - validatorsExitBusOracle, - "grantRole", - [await validatorsExitBusOracle.getFunction("PAUSE_ROLE")(), gateSealAddress], - { from: deployer }, + const validatorsExitBusOracle = await loadContract<ValidatorsExitBusOracle>( + "ValidatorsExitBusOracle", + validatorsExitBusOracleAddress, ); + await makeTx(validatorsExitBusOracle, "grantRole", [await validatorsExitBusOracle.PAUSE_ROLE(), gateSealAddress], { + from: deployer, + }); } else { log(`GateSeal is not specified or deployed: skipping assigning PAUSE_ROLE of validatorsExitBusOracle`); log.emptyLine(); } // WithdrawalQueue - const withdrawalQueue = await loadContract("WithdrawalQueueERC721", withdrawalQueueAddress); + const withdrawalQueue = await loadContract<WithdrawalQueueERC721>("WithdrawalQueueERC721", withdrawalQueueAddress); if (gateSealAddress) { - await makeTx(withdrawalQueue, "grantRole", [await withdrawalQueue.getFunction("PAUSE_ROLE")(), gateSealAddress], { + await makeTx(withdrawalQueue, "grantRole", [await withdrawalQueue.PAUSE_ROLE(), gateSealAddress], { from: deployer, }); } else { @@ -71,24 +75,21 @@ export async function main() { log.emptyLine(); } - await makeTx(withdrawalQueue, "grantRole", [await withdrawalQueue.getFunction("FINALIZE_ROLE")(), lidoAddress], { + await makeTx(withdrawalQueue, "grantRole", [await withdrawalQueue.FINALIZE_ROLE(), lidoAddress], { from: deployer, }); - await makeTx( - withdrawalQueue, - "grantRole", - [await withdrawalQueue.getFunction("ORACLE_ROLE")(), accountingOracleAddress], - { from: deployer }, - ); + await makeTx(withdrawalQueue, "grantRole", [await withdrawalQueue.ORACLE_ROLE(), accountingOracleAddress], { + from: deployer, + }); // Burner - const burner = await loadContract("Burner", burnerAddress); + const burner = await loadContract<Burner>("Burner", burnerAddress); // NB: REQUEST_BURN_SHARES_ROLE is already granted to Lido in Burner constructor - await makeTx( - burner, - "grantRole", - [await burner.getFunction("REQUEST_BURN_SHARES_ROLE")(), nodeOperatorsRegistryAddress], - { from: deployer }, - ); + await makeTx(burner, "grantRole", [await burner.REQUEST_BURN_SHARES_ROLE(), nodeOperatorsRegistryAddress], { + from: deployer, + }); + await makeTx(burner, "grantRole", [await burner.REQUEST_BURN_SHARES_ROLE(), simpleDvtApp], { + from: deployer, + }); } diff --git a/scripts/scratch/steps/0140-plug-curated-staking-module.ts b/scripts/scratch/steps/0140-plug-staking-modules.ts similarity index 71% rename from scripts/scratch/steps/0140-plug-curated-staking-module.ts rename to scripts/scratch/steps/0140-plug-staking-modules.ts index f9180f1f0..40cb4068c 100644 --- a/scripts/scratch/steps/0140-plug-curated-staking-module.ts +++ b/scripts/scratch/steps/0140-plug-staking-modules.ts @@ -5,10 +5,15 @@ import { makeTx } from "lib/deploy"; import { streccak } from "lib/keccak"; import { readNetworkState, Sk } from "lib/state-file"; +const STAKING_MODULE_MANAGE_ROLE = streccak("STAKING_MODULE_MANAGE_ROLE"); + const NOR_STAKING_MODULE_TARGET_SHARE_BP = 10000; // 100% const NOR_STAKING_MODULE_MODULE_FEE_BP = 500; // 5% const NOR_STAKING_MODULE_TREASURY_FEE_BP = 500; // 5% -const STAKING_MODULE_MANAGE_ROLE = streccak("STAKING_MODULE_MANAGE_ROLE"); + +const SDVT_STAKING_MODULE_TARGET_SHARE_BP = 400; // 4% +const SDVT_STAKING_MODULE_MODULE_FEE_BP = 800; // 8% +const SDVT_STAKING_MODULE_TREASURY_FEE_BP = 200; // 2% export async function main() { const deployer = (await ethers.provider.getSigner()).address; @@ -16,10 +21,6 @@ export async function main() { // Get contract instances const stakingRouter = await loadContract("StakingRouter", state.stakingRouter.proxy.address); - const nodeOperatorsRegistry = await loadContract( - "NodeOperatorsRegistry", - state[Sk.appNodeOperatorsRegistry].proxy.address, - ); // Grant STAKING_MODULE_MANAGE_ROLE to deployer await makeTx(stakingRouter, "grantRole", [STAKING_MODULE_MANAGE_ROLE, deployer], { from: deployer }); @@ -30,7 +31,7 @@ export async function main() { "addStakingModule", [ state.nodeOperatorsRegistry.deployParameters.stakingModuleTypeId, - nodeOperatorsRegistry.address, + state[Sk.appNodeOperatorsRegistry].proxy.address, NOR_STAKING_MODULE_TARGET_SHARE_BP, NOR_STAKING_MODULE_MODULE_FEE_BP, NOR_STAKING_MODULE_TREASURY_FEE_BP, @@ -38,6 +39,19 @@ export async function main() { { from: deployer }, ); + await makeTx( + stakingRouter, + "addStakingModule", + [ + state.simpleDvt.deployParameters.stakingModuleTypeId, + state[Sk.appSimpleDvt].proxy.address, + SDVT_STAKING_MODULE_TARGET_SHARE_BP, + SDVT_STAKING_MODULE_MODULE_FEE_BP, + SDVT_STAKING_MODULE_TREASURY_FEE_BP, + ], + { from: deployer }, + ); + // Renounce STAKING_MODULE_MANAGE_ROLE from deployer await makeTx(stakingRouter, "renounceRole", [STAKING_MODULE_MANAGE_ROLE, deployer], { from: deployer }); } diff --git a/test/integration/accounting.integration.ts b/test/integration/accounting.integration.ts index ef47c7c68..bb3d61c9e 100644 --- a/test/integration/accounting.integration.ts +++ b/test/integration/accounting.integration.ts @@ -50,16 +50,16 @@ describe("Accounting", () => { await finalizeWithdrawalQueue(ctx, stEthHolder, ethHolder); await norEnsureOperators(ctx, 3n, 5n); - if (ctx.flags.withSimpleDvtModule) { - await sdvtEnsureOperators(ctx, 3n, 5n); - } + await sdvtEnsureOperators(ctx, 3n, 5n); + // Deposit node operators const dsmSigner = await impersonate(depositSecurityModule.address, AMOUNT); await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, CURATED_MODULE_ID, ZERO_HASH); + await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, SIMPLE_DVT_MODULE_ID, ZERO_HASH); await report(ctx, { - clDiff: ether("32") * 3n, // 32 ETH * 3 validators - clAppearedValidators: 3n, + clDiff: ether("32") * 6n, // 32 ETH * (3 + 3) validators + clAppearedValidators: 6n, excludeVaultsBalances: true, }); }); @@ -325,7 +325,7 @@ describe("Accounting", () => { const norSharesAsFees = transferSharesEvents[hasWithdrawals ? 1 : 0]; // if withdrawals processed goes after burner and NOR, if no withdrawals processed goes after NOR - const sdvtSharesAsFees = ctx.flags.withSimpleDvtModule ? transferSharesEvents[hasWithdrawals ? 2 : 1] : null; + const sdvtSharesAsFees = transferSharesEvents[hasWithdrawals ? 2 : 1]; expect(transferSharesEvents.length).to.equal( hasWithdrawals ? 2n : 1n + stakingModulesCount, @@ -668,7 +668,7 @@ describe("Accounting", () => { const norSharesAsFees = transferSharesEvents[hasWithdrawals ? 1 : 0]; // if withdrawals processed goes after burner and NOR, if no withdrawals processed goes after NOR - const sdvtSharesAsFees = ctx.flags.withSimpleDvtModule ? transferSharesEvents[hasWithdrawals ? 2 : 1] : null; + const sdvtSharesAsFees = transferSharesEvents[hasWithdrawals ? 2 : 1]; expect(transferSharesEvents.length).to.equal( hasWithdrawals ? 2n : 1n + stakingModulesCount, @@ -768,7 +768,7 @@ describe("Accounting", () => { const norSharesAsFees = transferSharesEvents[hasWithdrawals ? 1 : 0]; // if withdrawals processed goes after burner and NOR, if no withdrawals processed goes after NOR - const sdvtSharesAsFees = ctx.flags.withSimpleDvtModule ? transferSharesEvents[hasWithdrawals ? 2 : 1] : null; + const sdvtSharesAsFees = transferSharesEvents[hasWithdrawals ? 2 : 1]; expect(transferSharesEvents.length).to.equal( hasWithdrawals ? 2n : 1n + stakingModulesCount, diff --git a/test/integration/protocol-happy-path.integration.ts b/test/integration/protocol-happy-path.integration.ts index 7ed7353d2..0ec08bf56 100644 --- a/test/integration/protocol-happy-path.integration.ts +++ b/test/integration/protocol-happy-path.integration.ts @@ -77,10 +77,8 @@ describe("Protocol Happy Path", () => { await norEnsureOperators(ctx, 3n, 5n); expect(await ctx.contracts.nor.getNodeOperatorsCount()).to.be.at.least(3n); - if (ctx.flags.withSimpleDvtModule) { - await sdvtEnsureOperators(ctx, 3n, 5n); - expect(await ctx.contracts.sdvt.getNodeOperatorsCount()).to.be.at.least(3n); - } + await sdvtEnsureOperators(ctx, 3n, 5n); + expect(await ctx.contracts.sdvt.getNodeOperatorsCount()).to.be.at.least(3n); }); it("Should allow ETH holders to submit 100 ETH stake", async () => { @@ -222,25 +220,23 @@ describe("Protocol Happy Path", () => { const depositCountsNor = unbufferedAmountNor / ether("32"); let expectedBufferedEtherAfterDeposit = bufferedEtherBeforeDeposit - unbufferedAmountNor; - if (ctx.flags.withSimpleDvtModule) { - const depositSdvtTx = await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, SIMPLE_DVT_MODULE_ID, ZERO_HASH); - const depositSdvtReceipt = await trace<ContractTransactionReceipt>("lido.deposit (Simple DVT)", depositSdvtTx); + const depositSdvtTx = await lido.connect(dsmSigner).deposit(MAX_DEPOSIT, SIMPLE_DVT_MODULE_ID, ZERO_HASH); + const depositSdvtReceipt = await trace<ContractTransactionReceipt>("lido.deposit (Simple DVT)", depositSdvtTx); - const unbufferedEventSdvt = ctx.getEvents(depositSdvtReceipt, "Unbuffered")[0]; - const depositedValidatorsChangedEventSdvt = ctx.getEvents(depositSdvtReceipt, "DepositedValidatorsChanged")[0]; + const unbufferedEventSdvt = ctx.getEvents(depositSdvtReceipt, "Unbuffered")[0]; + const depositedValidatorsChangedEventSdvt = ctx.getEvents(depositSdvtReceipt, "DepositedValidatorsChanged")[0]; - const unbufferedAmountSdvt = unbufferedEventSdvt.args[0]; - const newValidatorsCountSdvt = depositedValidatorsChangedEventSdvt.args[0]; + const unbufferedAmountSdvt = unbufferedEventSdvt.args[0]; + const newValidatorsCountSdvt = depositedValidatorsChangedEventSdvt.args[0]; - const depositCountsTotal = depositCountsNor + unbufferedAmountSdvt / ether("32"); - expectedBufferedEtherAfterDeposit -= unbufferedAmountSdvt; + const depositCountsTotal = depositCountsNor + unbufferedAmountSdvt / ether("32"); + expectedBufferedEtherAfterDeposit -= unbufferedAmountSdvt; - expect(depositCountsTotal).to.be.gt(0n, "Deposit counts"); - expect(newValidatorsCountSdvt).to.equal( - depositedValidatorsBefore + depositCountsTotal, - "New validators count after deposit", - ); - } + expect(depositCountsTotal).to.be.gt(0n, "Deposit counts"); + expect(newValidatorsCountSdvt).to.equal( + depositedValidatorsBefore + depositCountsTotal, + "New validators count after deposit", + ); const bufferedEtherAfterDeposit = await lido.getBufferedEther(); @@ -287,23 +283,16 @@ describe("Protocol Happy Path", () => { let expectedBurnerTransfers = norStatus.hasPenalizedOperators ? 1n : 0n; let expectedTransfers = norStatus.activeOperators; - let sdvtStatusLog = {}; - if (ctx.flags.withSimpleDvtModule) { - const sdvtStatus = await getNodeOperatorsStatus(sdvt); + const sdvtStatus = await getNodeOperatorsStatus(sdvt); - expectedBurnerTransfers += sdvtStatus.hasPenalizedOperators ? 1n : 0n; - expectedTransfers += sdvtStatus.activeOperators; - - sdvtStatusLog = { - "SDVT active operators": sdvtStatus.activeOperators, - "SDVT (transfer to burner)": sdvtStatus.hasPenalizedOperators, - }; - } + expectedBurnerTransfers += sdvtStatus.hasPenalizedOperators ? 1n : 0n; + expectedTransfers += sdvtStatus.activeOperators; log.debug("Expected distributions", { "NOR active operators": norStatus.activeOperators, "NOR (transfer to burner)": norStatus.hasPenalizedOperators, - ...sdvtStatusLog, + "SDVT active operators": sdvtStatus.activeOperators, + "SDVT (transfer to burner)": sdvtStatus.hasPenalizedOperators, }); const treasuryBalanceBeforeRebase = await lido.sharesOf(treasuryAddress); @@ -339,10 +328,9 @@ describe("Protocol Happy Path", () => { const toBurnerTransfer = transferEvents[0]; const toNorTransfer = transferEvents[1]; - const toSdvtTransfer = ctx.flags.withSimpleDvtModule ? transferEvents[2] : undefined; - const toTreasuryTransfer = ctx.flags.withSimpleDvtModule ? transferEvents[3] : transferEvents[2]; - - const expectedTransferEvents = ctx.flags.withSimpleDvtModule ? 4 : 3; + const toSdvtTransfer = transferEvents[2]; + const toTreasuryTransfer = transferEvents[3]; + const expectedTransferEvents = 4; expect(transferEvents.length).to.equal(expectedTransferEvents, "Transfer events count"); @@ -362,15 +350,13 @@ describe("Protocol Happy Path", () => { "Transfer to NOR", ); - if (ctx.flags.withSimpleDvtModule) { - expect(toSdvtTransfer?.args.toObject()).to.include( - { - from: ZeroAddress, - to: sdvt.address, - }, - "Transfer to SDVT", - ); - } + expect(toSdvtTransfer?.args.toObject()).to.include( + { + from: ZeroAddress, + to: sdvt.address, + }, + "Transfer to SDVT", + ); expect(toTreasuryTransfer?.args.toObject()).to.include( {