From 2fbbe8a1454a77ddd00a37f373181782d477bb21 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:47:10 +0400 Subject: [PATCH 1/8] feat: add Dual Governance to scratch deploy --- .env.example | 5 + .gitignore | 3 + deployed-mainnet.json | 8 +- hardhat.config.ts | 1 + lib/dg-installation.ts | 51 +++ lib/state-file.ts | 17 +- package.json | 1 + scripts/dao-deploy.sh | 2 + scripts/defaults/local-devnet-defaults.json | 44 ++ scripts/scratch/deploy-params-testnet.toml | 43 ++ scripts/scratch/steps.json | 3 +- .../steps/0160-deploy-dual-governance.ts | 376 ++++++++++++++++++ .../0150-deploy-tw-upgrading-contracts.ts | 2 +- scripts/utils/upgrade.ts | 7 +- 14 files changed, 548 insertions(+), 15 deletions(-) create mode 100644 lib/dg-installation.ts create mode 100644 scripts/scratch/steps/0160-deploy-dual-governance.ts diff --git a/.env.example b/.env.example index 7cd4b67df4..29fb9c1059 100644 --- a/.env.example +++ b/.env.example @@ -66,6 +66,11 @@ SLOTS_PER_EPOCH=32 GAS_PRIORITY_FEE=1 GAS_MAX_FEE=100 +# Dual Governance deployment +DG_DEPLOYMENT_ENABLED=true +DG_DEPLOYER_ACCOUNT_NETWORK_NAME= +DG_ETHERSCAN_VERIFY=false + # Etherscan API key for verifying contracts ETHERSCAN_API_KEY= diff --git a/.gitignore b/.gitignore index 54f7003981..602a95dfa0 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ artifacts/ foundry/cache/ foundry/out/ +# Dual Governance files +dg/ + # Extracted ABI files lib/abi/*.json diff --git a/deployed-mainnet.json b/deployed-mainnet.json index 840eac830f..992b8238d1 100644 --- a/deployed-mainnet.json +++ b/deployed-mainnet.json @@ -276,14 +276,10 @@ } }, "dg:dualGovernance": { - "proxy": { - "address": "0xC1db28B3301331277e307FDCfF8DE28242A4486E" - } + "address": "0xC1db28B3301331277e307FDCfF8DE28242A4486E" }, "dg:emergencyProtectedTimelock": { - "proxy": { - "address": "0xCE0425301C85c5Ea2A0873A2dEe44d78E02D2316" - } + "address": "0xCE0425301C85c5Ea2A0873A2dEe44d78E02D2316" }, "dummyEmptyContract": { "address": "0x6F6541C2203196fEeDd14CD2C09550dA1CbEDa31", diff --git a/hardhat.config.ts b/hardhat.config.ts index 298f75f820..9cda64c2b1 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -61,6 +61,7 @@ const config: HardhatUserConfig = { // local nodes "local": { url: process.env.LOCAL_RPC_URL || RPC_URL, + timeout: 120000, }, "local-devnet": { url: process.env.LOCAL_RPC_URL || RPC_URL, diff --git a/lib/dg-installation.ts b/lib/dg-installation.ts new file mode 100644 index 0000000000..de9965ec14 --- /dev/null +++ b/lib/dg-installation.ts @@ -0,0 +1,51 @@ +import child_process from "node:child_process"; +import fs from "node:fs/promises"; +import util from "node:util"; + +const DG_REPOSITORY_URL = "https://github.com/lidofinance/dual-governance.git"; +const DG_REPOSITORY_BRANCH = "feature/scratch-deploy-support"; // TODO: use release branch +const DG_INSTALL_DIR = `${process.cwd()}/dg`; + +async function main() { + console.log("Delete DG folder", DG_INSTALL_DIR); + await fs.rm(DG_INSTALL_DIR, { force: true, recursive: true }); + + console.log("Clone DG repo to", DG_INSTALL_DIR); + await runCommand( + `git clone --branch ${DG_REPOSITORY_BRANCH} --single-branch ${DG_REPOSITORY_URL} ${DG_INSTALL_DIR}`, + process.cwd(), + ); + + console.log("Building DualGovernance contracts"); + await runForgeBuild(DG_INSTALL_DIR); + + console.log("Running unit tests"); + await runUnitTests(DG_INSTALL_DIR); +} + +async function runForgeBuild(workingDirectory: string) { + await runCommand("forge build", workingDirectory); +} + +async function runUnitTests(workingDirectory: string) { + await runCommand("npm run test:unit", workingDirectory); +} + +async function runCommand(command: string, workingDirectory: string) { + const exec = util.promisify(child_process.exec); + + try { + const { stdout } = await exec(command, { cwd: workingDirectory }); + console.log("stdout:", stdout); + } catch (error) { + console.error(`Error running command ${command}`, `${error}`); + throw error; + } +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/lib/state-file.ts b/lib/state-file.ts index 93789d9dc6..ce15f862f4 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -108,8 +108,15 @@ export enum Sk { lazyOracle = "lazyOracle", v3TemporaryAdmin = "v3TemporaryAdmin", // Dual Governance + dualGovernanceConfig = "dualGovernanceConfig", + dgAdminExecutor = "dg:admin_executor", dgDualGovernance = "dg:dualGovernance", dgEmergencyProtectedTimelock = "dg:emergencyProtectedTimelock", + dgConfigProvider = "dg:dual_governance_config_provider", + dgEmergencyGovernance = "dg:emergency_governance", + dgEscrowMasterCopy = "dg:escrow_master_copy", + dgResealManager = "dg:reseal_manager", + dgTiebreakerCoreCommittee = "dg:tiebreaker_core_committee", } export function getAddress(contractKey: Sk, state: DeploymentState): string { @@ -140,8 +147,6 @@ export function getAddress(contractKey: Sk, state: DeploymentState): string { case Sk.appSimpleDvt: case Sk.predepositGuarantee: case Sk.vaultHub: - case Sk.dgDualGovernance: - case Sk.dgEmergencyProtectedTimelock: return state[contractKey].proxy.address; case Sk.apmRegistryFactory: case Sk.callsScript: @@ -172,6 +177,14 @@ export function getAddress(contractKey: Sk, state: DeploymentState): string { case Sk.validatorConsolidationRequests: case Sk.twVoteScript: case Sk.v3VoteScript: + case Sk.dgAdminExecutor: + case Sk.dgConfigProvider: + case Sk.dgEmergencyGovernance: + case Sk.dgEscrowMasterCopy: + case Sk.dgResealManager: + case Sk.dgTiebreakerCoreCommittee: + case Sk.dgDualGovernance: + case Sk.dgEmergencyProtectedTimelock: return state[contractKey].address; default: throw new Error(`Unsupported contract entry key ${contractKey}`); diff --git a/package.json b/package.json index dfeee948dd..c3da9ed82e 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "upgrade:mock-voting": "STEPS_FILE=upgrade/steps-mock-voting.json UPGRADE_PARAMETERS_FILE=scripts/upgrade/upgrade-params-mainnet.toml yarn hardhat --network custom run scripts/utils/migrate.ts", "validate:configs": "yarn hardhat validate-configs", "typecheck": "tsc --noEmit", + "dg:install": "yarn ts-node lib/dg-installation.ts", "abis:extract": "hardhat abis:extract", "verify:deployed": "hardhat verify:deployed", "postinstall": "husky" diff --git a/scripts/dao-deploy.sh b/scripts/dao-deploy.sh index ad507c6991..4ec9f0eaef 100755 --- a/scripts/dao-deploy.sh +++ b/scripts/dao-deploy.sh @@ -17,6 +17,8 @@ echo "NETWORK is $NETWORK" rm -f "${NETWORK_STATE_FILE}" +yarn dg:install + # Compile contracts yarn compile diff --git a/scripts/defaults/local-devnet-defaults.json b/scripts/defaults/local-devnet-defaults.json index e5c52cbbfb..249f624a25 100644 --- a/scripts/defaults/local-devnet-defaults.json +++ b/scripts/defaults/local-devnet-defaults.json @@ -166,5 +166,49 @@ "gIndexAfterChange": "0x0000000000000000000000000000000000000000000000000096000000000028", "changeSlot": 0 } + }, + "dualGovernanceConfig": { + "dual_governance": { + "tiebreaker_activation_timeout": 31536000, + "sanity_check_params": { + "max_min_assets_lock_duration": 4147200, + "max_sealable_withdrawal_blockers_count": 255, + "max_tiebreaker_activation_timeout": 63072000, + "min_tiebreaker_activation_timeout": 15768000, + "min_withdrawals_batch_size": 4 + } + }, + "dual_governance_config_provider": { + "first_seal_rage_quit_support": "10000000000000000", + "second_seal_rage_quit_support": "100000000000000000", + "min_assets_lock_duration": 18000, + "rage_quit_eth_withdrawals_delay_growth": 1296000, + "rage_quit_eth_withdrawals_min_delay": 5184000, + "rage_quit_eth_withdrawals_max_delay": 15552000, + "rage_quit_extension_period_duration": 604800, + "veto_cooldown_duration": 18000, + "veto_signalling_deactivation_max_duration": 259200, + "veto_signalling_min_active_duration": 18000, + "veto_signalling_min_duration": 432000, + "veto_signalling_max_duration": 3888000 + }, + "timelock": { + "after_submit_delay": 259200, + "after_schedule_delay": 86400, + "sanity_check_params": { + "min_execution_delay": 259200, + "max_after_submit_delay": 2592000, + "max_after_schedule_delay": 864000, + "max_emergency_mode_duration": 31536000, + "max_emergency_protection_duration": 94608000 + }, + "emergency_protection": { + "emergency_mode_duration": 2592000, + "emergency_protection_end_date": 1781913600 + } + }, + "tiebreaker": { + "execution_delay": 2592000 + } } } diff --git a/scripts/scratch/deploy-params-testnet.toml b/scripts/scratch/deploy-params-testnet.toml index d9af0429d5..a329fb6fff 100644 --- a/scripts/scratch/deploy-params-testnet.toml +++ b/scripts/scratch/deploy-params-testnet.toml @@ -185,3 +185,46 @@ forcedRebalanceThresholdBP = 1800 # Threshold for forced rebalancing in basi infraFeeBP = 500 # Infrastructure fee in basis points (5%) liquidityFeeBP = 400 # Liquidity provision fee in basis points (4%) reservationFeeBP = 100 # Reservation fee in basis points (1%) + +[dualGovernanceConfig] +[dualGovernanceConfig.dual_governance] +tiebreaker_activation_timeout = 31536000 + +[dualGovernanceConfig.dual_governance.sanity_check_params] +max_min_assets_lock_duration = 4147200 +max_sealable_withdrawal_blockers_count = 255 +max_tiebreaker_activation_timeout = 63072000 +min_tiebreaker_activation_timeout = 15768000 +min_withdrawals_batch_size = 4 + +[dualGovernanceConfig.dual_governance_config_provider] +first_seal_rage_quit_support = "10000000000000000" +second_seal_rage_quit_support = "100000000000000000" +min_assets_lock_duration = 18000 +rage_quit_eth_withdrawals_delay_growth = 1296000 +rage_quit_eth_withdrawals_min_delay = 5184000 +rage_quit_eth_withdrawals_max_delay = 15552000 +rage_quit_extension_period_duration = 604800 +veto_cooldown_duration = 18000 +veto_signalling_deactivation_max_duration = 259200 +veto_signalling_min_active_duration = 18000 +veto_signalling_min_duration = 432000 +veto_signalling_max_duration = 3888000 + +[dualGovernanceConfig.timelock] +after_submit_delay = 259200 +after_schedule_delay = 86400 + +[dualGovernanceConfig.timelock.sanity_check_params] +min_execution_delay = 259200 +max_after_submit_delay = 2592000 +max_after_schedule_delay = 864000 +max_emergency_mode_duration = 31536000 +max_emergency_protection_duration = 94608000 + +[dualGovernanceConfig.timelock.emergency_protection] +emergency_mode_duration = 2592000 +emergency_protection_end_date = 1781913600 + +[dualGovernanceConfig.tiebreaker] +execution_delay = 2592000 diff --git a/scripts/scratch/steps.json b/scripts/scratch/steps.json index 7296dd6c87..a7f6f23f73 100644 --- a/scripts/scratch/steps.json +++ b/scripts/scratch/steps.json @@ -17,6 +17,7 @@ "scratch/steps/0120-post-locator-initializers", "scratch/steps/0130-grant-roles", "scratch/steps/0140-plug-staking-modules", - "scratch/steps/0150-transfer-roles" + "scratch/steps/0150-transfer-roles", + "scratch/steps/0160-deploy-dual-governance" ] } diff --git a/scripts/scratch/steps/0160-deploy-dual-governance.ts b/scripts/scratch/steps/0160-deploy-dual-governance.ts new file mode 100644 index 0000000000..42127ab433 --- /dev/null +++ b/scripts/scratch/steps/0160-deploy-dual-governance.ts @@ -0,0 +1,376 @@ +import child_process from "node:child_process"; +import fs from "node:fs/promises"; +import util from "node:util"; + +import { ethers } from "hardhat"; + +import { log } from "lib"; +import { DeploymentState, getAddress, readNetworkState, Sk, updateObjectInState } from "lib/state-file"; + +const DG_INSTALL_DIR = `${process.cwd()}/dg`; +const DG_DEPLOY_ARTIFACTS_DIR = `${DG_INSTALL_DIR}/deploy-artifacts`; + +export async function main() { + if (process.env.DG_DEPLOYMENT_ENABLED == "false") { + log.header("DG deployment disabled"); + return; + } + + log.header(`Deploy DG from folder ${DG_INSTALL_DIR}`); + log.emptyLine(); + + const deployerAccountNetworkName = process.env.DG_DEPLOYER_ACCOUNT_NETWORK_NAME || ""; + if (!deployerAccountNetworkName.length) { + log.error(`You need to set the env variable DG_DEPLOYER_ACCOUNT_NETWORK_NAME to run DG deployment. +To do so, please place first a deployer private key to an accounts.json file in the next format: +{ + "eth": { + "": [""] + } +} + +Then set DG_DEPLOYER_ACCOUNT_NETWORK_NAME= in the .env file. +`); + throw new Error("Env variable DG_DEPLOYER_ACCOUNT_NETWORK_NAME is not set."); + } + + log.warning(`To run the deployment with the local Hardhat node you need to increase allowed memory usage to 16Gb. +> yarn hardhat node --fork --port 8555 --max-memory 16384 + +AND + +> export NODE_OPTIONS=--max_old_space_size=16384 +`); + + const deployer = (await ethers.provider.getSigner()).address; + const state = readNetworkState({ deployer }); + + const network = await ethers.getDefaultProvider(process.env.LOCAL_RPC_URL).getNetwork(); + const chainId = `${network.chainId}`; + + const config = getDGConfig(chainId, state); + + const timestamp = `${Date.now()}`; + const dgDeployConfigFilename = `deploy-config-scratch-${timestamp}.json`; + await writeDGConfigFile(JSON.stringify(config, null, 2), dgDeployConfigFilename); + + const deployerPrivateKey = await getDeployerPrivateKey(deployerAccountNetworkName); + + if (!deployerPrivateKey.length) { + throw new Error("Deployer private key not found"); + } + + let etherscanVerifyOption = ""; + let etherscanApiKey = "ETHERSCAN API KEY PLACEHOLDER"; + if (process.env.DG_ETHERSCAN_VERIFY == "true") { + if (!process.env.ETHERSCAN_API_KEY) { + throw new Error("Env variable ETHERSCAN_API_KEY is not set when DG_ETHERSCAN_VERIFY is set to true"); + } + etherscanVerifyOption = "--verify"; + etherscanApiKey = process.env.ETHERSCAN_API_KEY; + } + + await runCommand( + `DEPLOY_CONFIG_FILE_NAME="${dgDeployConfigFilename}" RPC_URL="${process.env.LOCAL_RPC_URL}" ETHERSCAN_API_KEY="${etherscanApiKey}" DEPLOYER_ADDRESS="${deployer}" npm run forge:script scripts/deploy/DeployConfigurable.s.sol -- --broadcast --slow ${etherscanVerifyOption} --private-key ${deployerPrivateKey}`, + DG_INSTALL_DIR, + ); + + await runDGRegressionTests(chainId, state, process.env.LOCAL_RPC_URL); + + const dgDeployArtifacts = await getDGDeployArtifacts(chainId); + + saveDGNetworkState(dgDeployArtifacts); +} + +async function runDGRegressionTests(networkChainId: string, networkState: DeploymentState, rpcUrl: string) { + log.header("Run DG regression tests"); + + const deployArtifactFilename = await getLatestDGDeployArtifactFilename(networkChainId); + + const dotEnvFile = getDGDotEnvFile(deployArtifactFilename, networkState, rpcUrl); + await writeDGDotEnvFile(dotEnvFile); + + try { + await runCommand("npm run test:regressions", DG_INSTALL_DIR); + } catch (error) { + // TODO: some of regression tests don't work at the moment, need to fix it. + log.error("DG regression tests run failed"); + log(`${error}`); + } +} + +async function runCommand(command: string, workingDirectory: string) { + const exec = util.promisify(child_process.exec); + + try { + const { stdout } = await exec(command, { cwd: workingDirectory }); + log("stdout:", stdout); + } catch (error) { + log.error(`Error running command ${command}`, `${error}`); + throw error; + } +} + +async function writeDGConfigFile(dgConfig: string, filename: string) { + const dgConfigFilePath = `${DG_INSTALL_DIR}/deploy-config/${filename}`; + + return writeFile(dgConfig, dgConfigFilePath, "config"); +} + +async function writeDGDotEnvFile(fileContent: string) { + const dgDotEnvFilePath = `${DG_INSTALL_DIR}/.env`; + + return writeFile(fileContent, dgDotEnvFilePath, ".env"); +} + +async function writeFile(fileContent: string, filePath: string, fileKind: string) { + try { + await fs.writeFile(filePath, fileContent, "utf8"); + log.success(`${fileKind} file successfully saved to ${filePath}`); + } catch (error) { + log.error(`An error has occurred while writing DG ${filePath} file`, `${error}`); + throw error; + } +} + +async function getLatestDGDeployArtifactFilename(networkChainId: string) { + const deployArtifactFilenameRe = new RegExp(`deploy-artifact-${networkChainId}-\\d+.toml`, "ig"); + + let files = []; + try { + files = await fs.readdir(DG_DEPLOY_ARTIFACTS_DIR); + } catch (error) { + log.error("An error has occurred while reading directory:", `${error}`); + throw error; + } + + files = files.filter((file) => file.match(deployArtifactFilenameRe)).sort(); + + if (files.length === 0) { + throw new Error("No deploy artifact file found"); + } + + return files[files.length - 1]; +} + +function getDGConfig(chainId: string, networkState: DeploymentState) { + const daoVoting = getAddress(Sk.appVoting, networkState); + const withdrawalQueue = getAddress(Sk.withdrawalQueueERC721, networkState); + const stEth = getAddress(Sk.appLido, networkState); + const wstEth = getAddress(Sk.wstETH, networkState); + + if (!networkState[Sk.dualGovernanceConfig]) { + throw new Error("DG deploy config is not set, please specify it in the deploy-params-testnet.toml file"); + } + + return { + chain_id: chainId, + dual_governance: { + admin_proposer: daoVoting, + proposals_canceller: daoVoting, + sealable_withdrawal_blockers: [], // TODO: add withdrawalQueue + reseal_committee: daoVoting, + tiebreaker_activation_timeout: + networkState[Sk.dualGovernanceConfig].dual_governance.tiebreaker_activation_timeout, + + signalling_tokens: { + st_eth: stEth, + wst_eth: wstEth, + withdrawal_queue: withdrawalQueue, + }, + sanity_check_params: { + max_min_assets_lock_duration: + networkState[Sk.dualGovernanceConfig].dual_governance.sanity_check_params.max_min_assets_lock_duration, + max_sealable_withdrawal_blockers_count: + networkState[Sk.dualGovernanceConfig].dual_governance.sanity_check_params + .max_sealable_withdrawal_blockers_count, + max_tiebreaker_activation_timeout: + networkState[Sk.dualGovernanceConfig].dual_governance.sanity_check_params.max_tiebreaker_activation_timeout, + min_tiebreaker_activation_timeout: + networkState[Sk.dualGovernanceConfig].dual_governance.sanity_check_params.min_tiebreaker_activation_timeout, + min_withdrawals_batch_size: + networkState[Sk.dualGovernanceConfig].dual_governance.sanity_check_params.min_withdrawals_batch_size, + }, + }, + dual_governance_config_provider: { + first_seal_rage_quit_support: + networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.first_seal_rage_quit_support, + second_seal_rage_quit_support: + networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.second_seal_rage_quit_support, + min_assets_lock_duration: + networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.min_assets_lock_duration, + rage_quit_eth_withdrawals_delay_growth: + networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.rage_quit_eth_withdrawals_delay_growth, + rage_quit_eth_withdrawals_min_delay: + networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.rage_quit_eth_withdrawals_min_delay, + rage_quit_eth_withdrawals_max_delay: + networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.rage_quit_eth_withdrawals_max_delay, + rage_quit_extension_period_duration: + networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.rage_quit_extension_period_duration, + veto_cooldown_duration: + networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.veto_cooldown_duration, + veto_signalling_deactivation_max_duration: + networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.veto_signalling_deactivation_max_duration, + veto_signalling_min_active_duration: + networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.veto_signalling_min_active_duration, + veto_signalling_min_duration: + networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.veto_signalling_min_duration, + veto_signalling_max_duration: + networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.veto_signalling_max_duration, + }, + timelock: { + after_submit_delay: networkState[Sk.dualGovernanceConfig].timelock.after_submit_delay, + after_schedule_delay: networkState[Sk.dualGovernanceConfig].timelock.after_schedule_delay, + sanity_check_params: { + min_execution_delay: networkState[Sk.dualGovernanceConfig].timelock.sanity_check_params.min_execution_delay, + max_after_submit_delay: + networkState[Sk.dualGovernanceConfig].timelock.sanity_check_params.max_after_submit_delay, + max_after_schedule_delay: + networkState[Sk.dualGovernanceConfig].timelock.sanity_check_params.max_after_schedule_delay, + max_emergency_mode_duration: + networkState[Sk.dualGovernanceConfig].timelock.sanity_check_params.max_emergency_mode_duration, + max_emergency_protection_duration: + networkState[Sk.dualGovernanceConfig].timelock.sanity_check_params.max_emergency_protection_duration, + }, + emergency_protection: { + emergency_activation_committee: daoVoting, + emergency_execution_committee: daoVoting, + emergency_governance_proposer: daoVoting, + emergency_mode_duration: + networkState[Sk.dualGovernanceConfig].timelock.emergency_protection.emergency_mode_duration, + emergency_protection_end_date: + networkState[Sk.dualGovernanceConfig].timelock.emergency_protection.emergency_protection_end_date, + }, + }, + tiebreaker: { + execution_delay: networkState[Sk.dualGovernanceConfig].tiebreaker.execution_delay, + committees_count: 1, + quorum: 1, + committees: [ + { + members: [daoVoting], + quorum: 1, + }, + ], + }, + }; +} + +function getDGDotEnvFile(deployArtifactFilename: string, networkState: DeploymentState, rpcUrl: string) { + const stEth = getAddress(Sk.appLido, networkState); + const wstEth = getAddress(Sk.wstETH, networkState); + const withdrawalQueue = getAddress(Sk.withdrawalQueueERC721, networkState); + const hashConsensus = getAddress(Sk.hashConsensusForAccountingOracle, networkState); + const burner = getAddress(Sk.burner, networkState); + const accountingOracle = getAddress(Sk.accountingOracle, networkState); + const elRewardsVault = getAddress(Sk.executionLayerRewardsVault, networkState); + const withdrawalVault = getAddress(Sk.withdrawalVault, networkState); + const oracleReportSanityChecker = getAddress(Sk.oracleReportSanityChecker, networkState); + const acl = getAddress(Sk.aragonAcl, networkState); + const ldo = getAddress(Sk.ldo, networkState); + const daoAgent = getAddress(Sk.appAgent, networkState); + const daoVoting = getAddress(Sk.appVoting, networkState); + const daoTokenManager = getAddress(Sk.appTokenManager, networkState); + + return `MAINNET_RPC_URL=${rpcUrl} +DEPLOY_ARTIFACT_FILE_NAME=${deployArtifactFilename} +DG_TESTS_LIDO_ST_ETH=${stEth} +DG_TESTS_LIDO_WST_ETH=${wstEth} +DG_TESTS_LIDO_WITHDRAWAL_QUEUE=${withdrawalQueue} +DG_TESTS_LIDO_HASH_CONSENSUS=${hashConsensus} +DG_TESTS_LIDO_BURNER=${burner} +DG_TESTS_LIDO_ACCOUNTING_ORACLE=${accountingOracle} +DG_TESTS_LIDO_EL_REWARDS_VAULT=${elRewardsVault} +DG_TESTS_LIDO_WITHDRAWAL_VAULT=${withdrawalVault} +DG_TESTS_LIDO_ORACLE_REPORT_SANITY_CHECKER=${oracleReportSanityChecker} +DG_TESTS_LIDO_DAO_ACL=${acl} +DG_TESTS_LIDO_LDO_TOKEN=${ldo} +DG_TESTS_LIDO_DAO_AGENT=${daoAgent} +DG_TESTS_LIDO_DAO_VOTING=${daoVoting} +DG_TESTS_LIDO_DAO_TOKEN_MANAGER=${daoTokenManager} +`; +} + +async function checkFileExists(path: string) { + return fs + .access(path) + .then(() => true) + .catch(() => false); +} + +async function getDeployerPrivateKey(networkName: string): Promise { + const accountsFilePath = `${process.cwd()}/accounts.json`; + + const accountsFileExists = await checkFileExists(accountsFilePath); + if (!accountsFileExists) { + log.error(`accounts.json file not found at ${accountsFilePath}`); + return ""; + } + + log(`accounts.json file found at ${accountsFilePath}`); + + const accountsFile = (await fs.readFile(accountsFilePath)).toString(); + let accountsJson; + try { + accountsJson = JSON.parse(accountsFile); + } catch (error) { + log.error("accounts.json is not a valid JSON file", `${error}`); + return ""; + } + + const privateKeys = accountsJson.eth && accountsJson.eth[networkName]; + return Array.isArray(privateKeys) ? privateKeys[0] : ""; +} + +interface DGDeployArtifacts { + admin_executor: string; + dualGovernance: string; + dual_governance_config_provider: string; + emergency_governance: string; + escrow_master_copy: string; + reseal_manager: string; + tiebreaker_core_committee: string; + emergencyProtectedTimelock: string; +} + +async function getDGDeployArtifacts(networkChainId: string): Promise { + const deployArtifactFilename = await getLatestDGDeployArtifactFilename(networkChainId); + const deployArtifactFilePath = `${DG_DEPLOY_ARTIFACTS_DIR}/${deployArtifactFilename}`; + + log(`Reading DG deploy artifact file: ${deployArtifactFilePath}`); + + const deployArtifactFile = (await fs.readFile(deployArtifactFilePath)).toString(); + + const contractsAddressesRe = { + admin_executor: /admin_executor = "(.+)"/, + dualGovernance: /dual_governance = "(.+)"/, + dual_governance_config_provider: /dual_governance_config_provider = "(.+)"/, + emergency_governance: /emergency_governance = "(.+)"/, + escrow_master_copy: /escrow_master_copy = "(.+)"/, + reseal_manager: /reseal_manager = "(.+)"/, + tiebreaker_core_committee: /tiebreaker_core_committee = "(.+)"/, + // TODO: tiebreaker_sub_committees ? + emergencyProtectedTimelock: /timelock = "(.+)"/, + } as Record; + + const result = {} as DGDeployArtifacts; + + (Object.keys(contractsAddressesRe) as (keyof DGDeployArtifacts)[]).forEach((key) => { + const address = deployArtifactFile.match(contractsAddressesRe[key]); + log("ADDRESS", (address && address[0]) || "", (address && address[1]) || ""); + if (!address || address.length < 2 || !address[1].length) { + throw new Error(`DG deploy artifact file corrupted: ${key} not found`); + } + + result[key] = address[1]; + }); + + return result; +} + +function saveDGNetworkState(dgDeployArtifacts: DGDeployArtifacts) { + (Object.keys(dgDeployArtifacts) as (keyof DGDeployArtifacts)[]).forEach((key) => { + // TODO: sync operation! + updateObjectInState(`dg:${key}` as Sk, { address: dgDeployArtifacts[key] }); + }); +} diff --git a/scripts/upgrade/steps/0150-deploy-tw-upgrading-contracts.ts b/scripts/upgrade/steps/0150-deploy-tw-upgrading-contracts.ts index daf2fefc0d..16c0affdc6 100644 --- a/scripts/upgrade/steps/0150-deploy-tw-upgrading-contracts.ts +++ b/scripts/upgrade/steps/0150-deploy-tw-upgrading-contracts.ts @@ -16,7 +16,7 @@ export async function main() { await deployWithoutProxy(Sk.twVoteScript, "TWVoteScript", deployer, [ state[Sk.appVoting].proxy.address, - state[Sk.dgDualGovernance].proxy.address, + state[Sk.dgDualGovernance].address, { // Contract addresses agent: getAddress(Sk.appAgent, state), diff --git a/scripts/utils/upgrade.ts b/scripts/utils/upgrade.ts index 7707885e28..ca801a5d4d 100644 --- a/scripts/utils/upgrade.ts +++ b/scripts/utils/upgrade.ts @@ -57,7 +57,7 @@ export async function mockDGAragonVoting( const voting = await loadContract("Voting", votingAddress); const timelock = await loadContract( "IEmergencyProtectedTimelock", - state[Sk.dgEmergencyProtectedTimelock].proxy.address, + state[Sk.dgEmergencyProtectedTimelock].proxy.address, // TODO: del! ); const afterSubmitDelay = await timelock.getAfterSubmitDelay(); const afterScheduleDelay = await timelock.getAfterScheduleDelay(); @@ -75,10 +75,7 @@ export async function mockDGAragonVoting( const executeReceipt = (await executeTx.wait())!; log.success("Voting executed: gas used", executeReceipt.gasUsed); - const dualGovernance = await loadContract( - "IDualGovernance", - state[Sk.dgDualGovernance].proxy.address, - ); + const dualGovernance = await loadContract("IDualGovernance", state[Sk.dgDualGovernance].address); const events = findEventsWithInterfaces(executeReceipt, "ProposalSubmitted", [dualGovernance.interface]); const proposalId = events[0].args.id; log.success("Proposal submitted: proposalId", proposalId); From 96a7f3c680970e7449da82baf1c88b821ce6688c Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Fri, 26 Sep 2025 20:33:50 +0400 Subject: [PATCH 2/8] feat: correct roles transfer for the DG deployment --- contracts/0.4.24/template/LidoTemplate.sol | 46 +++++++- lib/dg-installation.ts | 2 +- scripts/dao-local-deploy.sh | 7 ++ scripts/dao-local-upgrade.sh | 7 ++ scripts/defaults/local-devnet-defaults.json | 46 ++++---- scripts/scratch/deploy-params-testnet.toml | 50 ++++----- scripts/scratch/steps/0150-transfer-roles.ts | 4 +- .../steps/0160-deploy-dual-governance.ts | 104 +++++++++++++++--- 8 files changed, 197 insertions(+), 69 deletions(-) diff --git a/contracts/0.4.24/template/LidoTemplate.sol b/contracts/0.4.24/template/LidoTemplate.sol index 92c01a16d4..646a066b12 100644 --- a/contracts/0.4.24/template/LidoTemplate.sol +++ b/contracts/0.4.24/template/LidoTemplate.sol @@ -42,6 +42,7 @@ contract LidoTemplate is IsContract { string private constant ERROR_BAD_AMOUNTS_LEN = "TMPL_BAD_AMOUNTS_LEN"; string private constant ERROR_INVALID_ID = "TMPL_INVALID_ID"; string private constant ERROR_UNEXPECTED_TOTAL_SUPPLY = "TMPL_UNEXPECTED_TOTAL_SUPPLY"; + string private constant ERROR_INVALID_DG_ADMIN_EXECUTOR = "TMPL_INVALID_DG_ADMIN_EXECUTOR"; // Operational errors string private constant ERROR_PERMISSION_DENIED = "TMPL_PERMISSION_DENIED"; @@ -404,13 +405,50 @@ contract LidoTemplate is IsContract { _setupPermissions(state, repos); _transferRootPermissionsFromTemplateAndFinalizeDAO(state.dao, state.voting); - _resetState(); aragonID.register(keccak256(abi.encodePacked(_daoName)), state.dao); emit TmplDaoFinalized(); } + function finalizePermissionsAfterDGDeployment(address dgAdminExecutor) external onlyOwner { + require(dgAdminExecutor != address(0), ERROR_INVALID_DG_ADMIN_EXECUTOR); + + DeployState memory state = deployState; + + state.acl.grantPermission(dgAdminExecutor, address(state.agent), state.agent.RUN_SCRIPT_ROLE()); + state.acl.grantPermission(dgAdminExecutor, address(state.agent), state.agent.EXECUTE_ROLE()); + + state.acl.revokePermission(address(state.voting), address(state.agent), state.agent.RUN_SCRIPT_ROLE()); + state.acl.revokePermission(address(state.voting), address(state.agent), state.agent.EXECUTE_ROLE()); + + state.acl.setPermissionManager(address(state.agent), address(state.agent), state.agent.RUN_SCRIPT_ROLE()); + state.acl.setPermissionManager(address(state.agent), address(state.agent), state.agent.EXECUTE_ROLE()); + + Kernel apmDAO = Kernel(state.lidoRegistry.kernel()); + ACL apmACL = ACL(apmDAO.acl()); + _transferPermissionFromTemplate(apmACL, apmACL, address(state.agent), apmACL.CREATE_PERMISSIONS_ROLE()); + + _transferPermissionFromTemplate(state.acl, address(state.acl), address(state.agent), state.acl.CREATE_PERMISSIONS_ROLE(), address(state.agent)); + + _resetState(); + } + + function finalizePermissionsWithoutDGDeployment() external onlyOwner { + DeployState memory state = deployState; + + state.acl.setPermissionManager(address(state.voting), address(state.agent), state.agent.RUN_SCRIPT_ROLE()); + state.acl.setPermissionManager(address(state.voting), address(state.agent), state.agent.EXECUTE_ROLE()); + + Kernel apmDAO = Kernel(state.lidoRegistry.kernel()); + ACL apmACL = ACL(apmDAO.acl()); + _transferPermissionFromTemplate(apmACL, apmACL, address(state.agent), apmACL.CREATE_PERMISSIONS_ROLE()); + + _transferPermissionFromTemplate(state.acl, address(state.acl), address(state.voting), state.acl.CREATE_PERMISSIONS_ROLE(), address(state.voting)); + + _resetState(); + } + /* DAO AND APPS */ /** @@ -564,7 +602,6 @@ contract LidoTemplate is IsContract { _transferPermissionFromTemplate(apmACL, _state.lidoRegistry, voting, _state.lidoRegistry.CREATE_REPO_ROLE()); apmACL.setPermissionManager(agent, apmDAO, apmDAO.APP_MANAGER_ROLE()); - _transferPermissionFromTemplate(apmACL, apmACL, agent, apmACL.CREATE_PERMISSIONS_ROLE()); apmACL.setPermissionManager(voting, apmRegistrar, apmRegistrar.CREATE_NAME_ROLE()); apmACL.setPermissionManager(voting, apmRegistrar, apmRegistrar.POINT_ROOTNODE_ROLE()); @@ -634,8 +671,8 @@ contract LidoTemplate is IsContract { } function _createAgentPermissions(ACL _acl, Agent _agent, address _voting) internal { - _createPermissionForVoting(_acl, _agent, _agent.EXECUTE_ROLE(), _voting); - _createPermissionForVoting(_acl, _agent, _agent.RUN_SCRIPT_ROLE(), _voting); + _acl.createPermission(_voting, _agent, _agent.EXECUTE_ROLE(), address(this)); + _acl.createPermission(_voting, _agent, _agent.RUN_SCRIPT_ROLE(), address(this)); } function _createVaultPermissions(ACL _acl, Vault _vault, address _finance, address _voting) internal { @@ -678,7 +715,6 @@ contract LidoTemplate is IsContract { function _transferRootPermissionsFromTemplateAndFinalizeDAO(Kernel _dao, address _voting) private { ACL _acl = ACL(_dao.acl()); _transferPermissionFromTemplate(_acl, _dao, _voting, _dao.APP_MANAGER_ROLE(), _voting); - _transferPermissionFromTemplate(_acl, _acl, _voting, _acl.CREATE_PERMISSIONS_ROLE(), _voting); } function _transferPermissionFromTemplate(ACL _acl, address _app, address _to, bytes32 _permission) private { diff --git a/lib/dg-installation.ts b/lib/dg-installation.ts index de9965ec14..9e39c679c8 100644 --- a/lib/dg-installation.ts +++ b/lib/dg-installation.ts @@ -3,7 +3,7 @@ import fs from "node:fs/promises"; import util from "node:util"; const DG_REPOSITORY_URL = "https://github.com/lidofinance/dual-governance.git"; -const DG_REPOSITORY_BRANCH = "feature/scratch-deploy-support"; // TODO: use release branch +const DG_REPOSITORY_BRANCH = "feature/scratch-deploy-support2"; // TODO: use release branch const DG_INSTALL_DIR = `${process.cwd()}/dg`; async function main() { diff --git a/scripts/dao-local-deploy.sh b/scripts/dao-local-deploy.sh index 119ff803ce..87117d26f5 100755 --- a/scripts/dao-local-deploy.sh +++ b/scripts/dao-local-deploy.sh @@ -24,3 +24,10 @@ yarn hardhat --network $NETWORK run --no-compile scripts/utils/mine.ts # Run acceptance tests export INTEGRATION_WITH_CSM="off" yarn test:integration:fork:local + +# If Dual Governance was deployed +if grep "dg:dual_governance" $NETWORK_STATE_FILE -q; then + # Run DG regression tests + echo "Run Dual Governance regression tests" + (cd dg && npm run test:regressions) +fi diff --git a/scripts/dao-local-upgrade.sh b/scripts/dao-local-upgrade.sh index f0e67fd90f..661c1434be 100755 --- a/scripts/dao-local-upgrade.sh +++ b/scripts/dao-local-upgrade.sh @@ -22,3 +22,10 @@ yarn hardhat --network $NETWORK run --no-compile scripts/utils/mine.ts # Run acceptance tests yarn test:integration:fork:local + +# If Dual Governance was deployed +if grep "dg:dual_governance" $NETWORK_STATE_FILE -q; then + # Run DG regression tests + echo "Run Dual Governance regression tests" + (cd dg && npm run test:regressions) +fi diff --git a/scripts/defaults/local-devnet-defaults.json b/scripts/defaults/local-devnet-defaults.json index 249f624a25..4783b1dcf0 100644 --- a/scripts/defaults/local-devnet-defaults.json +++ b/scripts/defaults/local-devnet-defaults.json @@ -169,46 +169,46 @@ }, "dualGovernanceConfig": { "dual_governance": { - "tiebreaker_activation_timeout": 31536000, + "tiebreaker_activation_timeout": 900, "sanity_check_params": { - "max_min_assets_lock_duration": 4147200, + "max_min_assets_lock_duration": 3600, "max_sealable_withdrawal_blockers_count": 255, - "max_tiebreaker_activation_timeout": 63072000, - "min_tiebreaker_activation_timeout": 15768000, - "min_withdrawals_batch_size": 4 + "max_tiebreaker_activation_timeout": 1800, + "min_tiebreaker_activation_timeout": 300, + "min_withdrawals_batch_size": 1 } }, "dual_governance_config_provider": { "first_seal_rage_quit_support": "10000000000000000", "second_seal_rage_quit_support": "100000000000000000", - "min_assets_lock_duration": 18000, + "min_assets_lock_duration": 300, "rage_quit_eth_withdrawals_delay_growth": 1296000, - "rage_quit_eth_withdrawals_min_delay": 5184000, - "rage_quit_eth_withdrawals_max_delay": 15552000, - "rage_quit_extension_period_duration": 604800, - "veto_cooldown_duration": 18000, - "veto_signalling_deactivation_max_duration": 259200, - "veto_signalling_min_active_duration": 18000, - "veto_signalling_min_duration": 432000, - "veto_signalling_max_duration": 3888000 + "rage_quit_eth_withdrawals_min_delay": 300, + "rage_quit_eth_withdrawals_max_delay": 1800, + "rage_quit_extension_period_duration": 300, + "veto_cooldown_duration": 300, + "veto_signalling_deactivation_max_duration": 1800, + "veto_signalling_min_active_duration": 300, + "veto_signalling_min_duration": 300, + "veto_signalling_max_duration": 1500 }, "timelock": { - "after_submit_delay": 259200, - "after_schedule_delay": 86400, + "after_submit_delay": 300, + "after_schedule_delay": 300, "sanity_check_params": { - "min_execution_delay": 259200, - "max_after_submit_delay": 2592000, - "max_after_schedule_delay": 864000, - "max_emergency_mode_duration": 31536000, - "max_emergency_protection_duration": 94608000 + "min_execution_delay": 300, + "max_after_submit_delay": 1800, + "max_after_schedule_delay": 1800, + "max_emergency_mode_duration": 1800, + "max_emergency_protection_duration": 31536000 }, "emergency_protection": { - "emergency_mode_duration": 2592000, + "emergency_mode_duration": 900, "emergency_protection_end_date": 1781913600 } }, "tiebreaker": { - "execution_delay": 2592000 + "execution_delay": 900 } } } diff --git a/scripts/scratch/deploy-params-testnet.toml b/scripts/scratch/deploy-params-testnet.toml index a329fb6fff..cafa6a00e1 100644 --- a/scripts/scratch/deploy-params-testnet.toml +++ b/scripts/scratch/deploy-params-testnet.toml @@ -188,43 +188,43 @@ reservationFeeBP = 100 # Reservation fee in basis points (1%) [dualGovernanceConfig] [dualGovernanceConfig.dual_governance] -tiebreaker_activation_timeout = 31536000 +tiebreaker_activation_timeout = 900 # 15 minutes [dualGovernanceConfig.dual_governance.sanity_check_params] -max_min_assets_lock_duration = 4147200 +max_min_assets_lock_duration = 3600 # 1 hour max_sealable_withdrawal_blockers_count = 255 -max_tiebreaker_activation_timeout = 63072000 -min_tiebreaker_activation_timeout = 15768000 -min_withdrawals_batch_size = 4 +max_tiebreaker_activation_timeout = 1800 # 30 minutes +min_tiebreaker_activation_timeout = 300 # 5 minutes +min_withdrawals_batch_size = 1 [dualGovernanceConfig.dual_governance_config_provider] first_seal_rage_quit_support = "10000000000000000" second_seal_rage_quit_support = "100000000000000000" -min_assets_lock_duration = 18000 -rage_quit_eth_withdrawals_delay_growth = 1296000 -rage_quit_eth_withdrawals_min_delay = 5184000 -rage_quit_eth_withdrawals_max_delay = 15552000 -rage_quit_extension_period_duration = 604800 -veto_cooldown_duration = 18000 -veto_signalling_deactivation_max_duration = 259200 -veto_signalling_min_active_duration = 18000 -veto_signalling_min_duration = 432000 -veto_signalling_max_duration = 3888000 +min_assets_lock_duration = 300 # 5 minutes +rage_quit_eth_withdrawals_delay_growth = 1296000 # +rage_quit_eth_withdrawals_min_delay = 300 # 5 minutes +rage_quit_eth_withdrawals_max_delay = 1800 # 30 minutes +rage_quit_extension_period_duration = 300 # 5 minutes +veto_cooldown_duration = 300 # 5 minutes +veto_signalling_deactivation_max_duration = 1800 # 30 minutes +veto_signalling_min_duration = 300 # 5 minutes +veto_signalling_max_duration = 1500 # 25 minutes +veto_signalling_min_active_duration = 300 # 5 minutes [dualGovernanceConfig.timelock] -after_submit_delay = 259200 -after_schedule_delay = 86400 +after_submit_delay = 300 # 5 minutes +after_schedule_delay = 300 # 5 minutes [dualGovernanceConfig.timelock.sanity_check_params] -min_execution_delay = 259200 -max_after_submit_delay = 2592000 -max_after_schedule_delay = 864000 -max_emergency_mode_duration = 31536000 -max_emergency_protection_duration = 94608000 +min_execution_delay = 300 # 5 minutes +max_after_submit_delay = 1800 # 30 minutes +max_after_schedule_delay = 1800 # 30 minutes +max_emergency_mode_duration = 1800 # 30 minutes +max_emergency_protection_duration = 31536000 # 1 year [dualGovernanceConfig.timelock.emergency_protection] -emergency_mode_duration = 2592000 -emergency_protection_end_date = 1781913600 +emergency_mode_duration = 900 # 15 minutes +emergency_protection_end_date = 1781913600 # Sat, June 20, 2026 12:00:00 AM GMT+00:00 [dualGovernanceConfig.tiebreaker] -execution_delay = 2592000 +execution_delay = 900 # 15 minutes diff --git a/scripts/scratch/steps/0150-transfer-roles.ts b/scripts/scratch/steps/0150-transfer-roles.ts index 067e85a065..04153b1979 100644 --- a/scripts/scratch/steps/0150-transfer-roles.ts +++ b/scripts/scratch/steps/0150-transfer-roles.ts @@ -34,7 +34,9 @@ export async function main() { for (const contract of ozAdminTransfers) { const contractInstance = await loadContract(contract.name, contract.address); await makeTx(contractInstance, "grantRole", [DEFAULT_ADMIN_ROLE, agent], { from: deployer }); - await makeTx(contractInstance, "renounceRole", [DEFAULT_ADMIN_ROLE, deployer], { from: deployer }); + if (process.env.DG_DEPLOYMENT_ENABLED == "false" || contract.name !== "WithdrawalQueueERC721") { + await makeTx(contractInstance, "renounceRole", [DEFAULT_ADMIN_ROLE, deployer], { from: deployer }); + } } // Change admin for OssifiableProxy contracts diff --git a/scripts/scratch/steps/0160-deploy-dual-governance.ts b/scripts/scratch/steps/0160-deploy-dual-governance.ts index 42127ab433..5fff2c0766 100644 --- a/scripts/scratch/steps/0160-deploy-dual-governance.ts +++ b/scripts/scratch/steps/0160-deploy-dual-governance.ts @@ -4,7 +4,11 @@ import util from "node:util"; import { ethers } from "hardhat"; +import { LidoTemplate, WithdrawalQueueERC721 } from "typechain-types"; + import { log } from "lib"; +import { loadContract } from "lib/contract"; +import { makeTx } from "lib/deploy"; import { DeploymentState, getAddress, readNetworkState, Sk, updateObjectInState } from "lib/state-file"; const DG_INSTALL_DIR = `${process.cwd()}/dg`; @@ -13,6 +17,7 @@ const DG_DEPLOY_ARTIFACTS_DIR = `${DG_INSTALL_DIR}/deploy-artifacts`; export async function main() { if (process.env.DG_DEPLOYMENT_ENABLED == "false") { log.header("DG deployment disabled"); + await finalizePermissionsWithoutDGDeployment(); return; } @@ -70,33 +75,102 @@ AND etherscanApiKey = process.env.ETHERSCAN_API_KEY; } + await unpauseWithdrawalQueue(deployer, state); + await runCommand( `DEPLOY_CONFIG_FILE_NAME="${dgDeployConfigFilename}" RPC_URL="${process.env.LOCAL_RPC_URL}" ETHERSCAN_API_KEY="${etherscanApiKey}" DEPLOYER_ADDRESS="${deployer}" npm run forge:script scripts/deploy/DeployConfigurable.s.sol -- --broadcast --slow ${etherscanVerifyOption} --private-key ${deployerPrivateKey}`, DG_INSTALL_DIR, ); - await runDGRegressionTests(chainId, state, process.env.LOCAL_RPC_URL); - const dgDeployArtifacts = await getDGDeployArtifacts(chainId); + await transferRoles(deployer, dgDeployArtifacts, state); + + await prepareDGRegressionTestsRun(chainId, state, process.env.LOCAL_RPC_URL); + saveDGNetworkState(dgDeployArtifacts); } -async function runDGRegressionTests(networkChainId: string, networkState: DeploymentState, rpcUrl: string) { - log.header("Run DG regression tests"); +async function finalizePermissionsWithoutDGDeployment() { + const deployer = (await ethers.provider.getSigner()).address; + const networkState = readNetworkState({ deployer }); + + const lidoTemplateAddress = getAddress(Sk.lidoTemplate, networkState); + const lidoTemplate = await loadContract("LidoTemplate", lidoTemplateAddress); + + await makeTx(lidoTemplate, "finalizePermissionsWithoutDGDeployment", [], { from: deployer }); +} + +async function transferRoles(deployer: string, dgDeployArtifacts: DGDeployArtifacts, networkState: DeploymentState) { + const aragonAgentAddress = getAddress(Sk.appAgent, networkState); + const votingAddress = getAddress(Sk.appVoting, networkState); + const withdrawalQueueAddress = getAddress(Sk.withdrawalQueueERC721, networkState); + const lidoTemplateAddress = getAddress(Sk.lidoTemplate, networkState); + + const lidoTemplate = await loadContract("LidoTemplate", lidoTemplateAddress); + const withdrawalQueue = await loadContract("WithdrawalQueueERC721", withdrawalQueueAddress); + + const DEFAULT_ADMIN_ROLE = ethers.ZeroHash; + + await makeTx(withdrawalQueue, "grantRole", [DEFAULT_ADMIN_ROLE, aragonAgentAddress], { + from: deployer, + }); + + await makeTx( + withdrawalQueue, + "grantRole", + [await withdrawalQueue.PAUSE_ROLE(), votingAddress /* = reseal_committee */], + { + from: deployer, + }, + ); + + await makeTx( + withdrawalQueue, + "grantRole", + [await withdrawalQueue.RESUME_ROLE(), votingAddress /* = reseal_committee */], + { + from: deployer, + }, + ); + + await makeTx(withdrawalQueue, "grantRole", [await withdrawalQueue.PAUSE_ROLE(), dgDeployArtifacts.reseal_manager], { + from: deployer, + }); + + await makeTx(withdrawalQueue, "grantRole", [await withdrawalQueue.RESUME_ROLE(), dgDeployArtifacts.reseal_manager], { + from: deployer, + }); + + await makeTx(withdrawalQueue, "renounceRole", [await withdrawalQueue.DEFAULT_ADMIN_ROLE(), deployer], { + from: deployer, + }); + + await makeTx(lidoTemplate, "finalizePermissionsAfterDGDeployment", [dgDeployArtifacts.admin_executor], { + from: deployer, + }); +} + +async function unpauseWithdrawalQueue(deployer: string, networkState: DeploymentState) { + const withdrawalQueueAddress = getAddress(Sk.withdrawalQueueERC721, networkState); + const withdrawalQueue = await loadContract("WithdrawalQueueERC721", withdrawalQueueAddress); + + await makeTx(withdrawalQueue, "grantRole", [await withdrawalQueue.RESUME_ROLE(), deployer], { + from: deployer, + }); + + await makeTx(withdrawalQueue, "resume", [], { + from: deployer, + }); +} + +async function prepareDGRegressionTestsRun(networkChainId: string, networkState: DeploymentState, rpcUrl: string) { + log.header("Prepare DG regression tests run: update DG .env file"); const deployArtifactFilename = await getLatestDGDeployArtifactFilename(networkChainId); const dotEnvFile = getDGDotEnvFile(deployArtifactFilename, networkState, rpcUrl); await writeDGDotEnvFile(dotEnvFile); - - try { - await runCommand("npm run test:regressions", DG_INSTALL_DIR); - } catch (error) { - // TODO: some of regression tests don't work at the moment, need to fix it. - log.error("DG regression tests run failed"); - log(`${error}`); - } } async function runCommand(command: string, workingDirectory: string) { @@ -168,7 +242,7 @@ function getDGConfig(chainId: string, networkState: DeploymentState) { dual_governance: { admin_proposer: daoVoting, proposals_canceller: daoVoting, - sealable_withdrawal_blockers: [], // TODO: add withdrawalQueue + sealable_withdrawal_blockers: [withdrawalQueue], reseal_committee: daoVoting, tiebreaker_activation_timeout: networkState[Sk.dualGovernanceConfig].dual_governance.tiebreaker_activation_timeout, @@ -266,6 +340,7 @@ function getDGDotEnvFile(deployArtifactFilename: string, networkState: Deploymen const elRewardsVault = getAddress(Sk.executionLayerRewardsVault, networkState); const withdrawalVault = getAddress(Sk.withdrawalVault, networkState); const oracleReportSanityChecker = getAddress(Sk.oracleReportSanityChecker, networkState); + const stakingRouter = getAddress(Sk.stakingRouter, networkState); const acl = getAddress(Sk.aragonAcl, networkState); const ldo = getAddress(Sk.ldo, networkState); const daoAgent = getAddress(Sk.appAgent, networkState); @@ -283,11 +358,13 @@ DG_TESTS_LIDO_ACCOUNTING_ORACLE=${accountingOracle} DG_TESTS_LIDO_EL_REWARDS_VAULT=${elRewardsVault} DG_TESTS_LIDO_WITHDRAWAL_VAULT=${withdrawalVault} DG_TESTS_LIDO_ORACLE_REPORT_SANITY_CHECKER=${oracleReportSanityChecker} +DG_TESTS_LIDO_STAKING_ROUTER=${stakingRouter} DG_TESTS_LIDO_DAO_ACL=${acl} DG_TESTS_LIDO_LDO_TOKEN=${ldo} DG_TESTS_LIDO_DAO_AGENT=${daoAgent} DG_TESTS_LIDO_DAO_VOTING=${daoVoting} DG_TESTS_LIDO_DAO_TOKEN_MANAGER=${daoTokenManager} +DG_DISABLE_REGRESSION_TESTS_FOR_SCRATCH_DEPLOY=true `; } @@ -357,7 +434,6 @@ async function getDGDeployArtifacts(networkChainId: string): Promise { const address = deployArtifactFile.match(contractsAddressesRe[key]); - log("ADDRESS", (address && address[0]) || "", (address && address[1]) || ""); if (!address || address.length < 2 || !address[1].length) { throw new Error(`DG deploy artifact file corrupted: ${key} not found`); } From 4b6dc09235461993a5d0c9f3d379f3bb17d31f9c Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Mon, 29 Sep 2025 10:52:40 +0400 Subject: [PATCH 3/8] fix: dual governance deployment after rebase --- lib/config-schemas.ts | 61 ++++++++++++++ lib/dg-installation.ts | 16 +--- lib/dg-regression-tests.ts | 22 +++++ lib/subprocess.ts | 14 ++++ package.json | 1 + scripts/dao-local-deploy.sh | 6 +- scripts/dao-local-upgrade.sh | 6 +- scripts/scratch/steps/0150-transfer-roles.ts | 4 - .../steps/0160-deploy-dual-governance.ts | 82 ++++--------------- scripts/utils/scratch.ts | 1 + 10 files changed, 122 insertions(+), 91 deletions(-) create mode 100644 lib/dg-regression-tests.ts create mode 100644 lib/subprocess.ts diff --git a/lib/config-schemas.ts b/lib/config-schemas.ts index 40ee1dad98..c025073510 100644 --- a/lib/config-schemas.ts +++ b/lib/config-schemas.ts @@ -238,6 +238,66 @@ const LidoApmSchema = z.object({ ensRegDurationSec: PositiveIntSchema, }); +// DG deployment config schemas +const DGConfigDGSanityCheckParamsSchema = z.object({ + max_min_assets_lock_duration: PositiveIntSchema, + max_sealable_withdrawal_blockers_count: PositiveIntSchema, + max_tiebreaker_activation_timeout: PositiveIntSchema, + min_tiebreaker_activation_timeout: NonNegativeIntSchema, + min_withdrawals_batch_size: PositiveIntSchema, +}); + +const DGConfigDGSchema = z.object({ + tiebreaker_activation_timeout: NonNegativeIntSchema, + sanity_check_params: DGConfigDGSanityCheckParamsSchema, +}); + +const DGConfigDGConfigProviderSchema = z.object({ + first_seal_rage_quit_support: BigIntStringSchema, + second_seal_rage_quit_support: BigIntStringSchema, + min_assets_lock_duration: NonNegativeIntSchema, + rage_quit_eth_withdrawals_delay_growth: PositiveIntSchema, + rage_quit_eth_withdrawals_min_delay: NonNegativeIntSchema, + rage_quit_eth_withdrawals_max_delay: PositiveIntSchema, + rage_quit_extension_period_duration: NonNegativeIntSchema, + veto_cooldown_duration: NonNegativeIntSchema, + veto_signalling_deactivation_max_duration: PositiveIntSchema, + veto_signalling_min_duration: NonNegativeIntSchema, + veto_signalling_max_duration: PositiveIntSchema, + veto_signalling_min_active_duration: NonNegativeIntSchema, +}); + +const DGConfigTimelockSanityCheckParamsSchema = z.object({ + min_execution_delay: NonNegativeIntSchema, + max_after_submit_delay: PositiveIntSchema, + max_after_schedule_delay: PositiveIntSchema, + max_emergency_mode_duration: PositiveIntSchema, + max_emergency_protection_duration: PositiveIntSchema, +}); + +const DGConfigTimelockEmergencyProtectionSchema = z.object({ + emergency_mode_duration: NonNegativeIntSchema, + emergency_protection_end_date: PositiveIntSchema, +}); + +const DGConfigTimelockSchema = z.object({ + after_submit_delay: NonNegativeIntSchema, + after_schedule_delay: NonNegativeIntSchema, + sanity_check_params: DGConfigTimelockSanityCheckParamsSchema, + emergency_protection: DGConfigTimelockEmergencyProtectionSchema, +}); + +const DGConfigTiebreakerSchema = z.object({ + execution_delay: NonNegativeIntSchema, +}); + +const DGConfigSchema = z.object({ + dual_governance: DGConfigDGSchema, + dual_governance_config_provider: DGConfigDGConfigProviderSchema, + timelock: DGConfigTimelockSchema, + tiebreaker: DGConfigTiebreakerSchema, +}); + // Scratch parameters schema export const ScratchParametersSchema = z.object({ chainSpec: ChainSpecSchema.omit({ genesisTime: true, depositContract: true }), @@ -267,6 +327,7 @@ export const ScratchParametersSchema = z.object({ triggerableWithdrawalsGateway: TriggerableWithdrawalsGatewaySchema, predepositGuarantee: PredepositGuaranteeSchema.omit({ genesisForkVersion: true }), operatorGrid: OperatorGridSchema, + dualGovernanceConfig: DGConfigSchema, }); // Inferred types from zod schemas diff --git a/lib/dg-installation.ts b/lib/dg-installation.ts index 9e39c679c8..9ae532eaeb 100644 --- a/lib/dg-installation.ts +++ b/lib/dg-installation.ts @@ -1,6 +1,6 @@ -import child_process from "node:child_process"; import fs from "node:fs/promises"; -import util from "node:util"; + +import { runCommand } from "./subprocess"; const DG_REPOSITORY_URL = "https://github.com/lidofinance/dual-governance.git"; const DG_REPOSITORY_BRANCH = "feature/scratch-deploy-support2"; // TODO: use release branch @@ -31,18 +31,6 @@ async function runUnitTests(workingDirectory: string) { await runCommand("npm run test:unit", workingDirectory); } -async function runCommand(command: string, workingDirectory: string) { - const exec = util.promisify(child_process.exec); - - try { - const { stdout } = await exec(command, { cwd: workingDirectory }); - console.log("stdout:", stdout); - } catch (error) { - console.error(`Error running command ${command}`, `${error}`); - throw error; - } -} - main() .then(() => process.exit(0)) .catch((error) => { diff --git a/lib/dg-regression-tests.ts b/lib/dg-regression-tests.ts new file mode 100644 index 0000000000..0071fe08e8 --- /dev/null +++ b/lib/dg-regression-tests.ts @@ -0,0 +1,22 @@ +import { log } from "./log"; +import { runCommand } from "./subprocess"; + +const DG_INSTALL_DIR = `${process.cwd()}/dg`; + +async function runDGRegressionTests() { + log.header("Run Dual Governance regression tests"); + try { + await runCommand("npm run test:regressions", DG_INSTALL_DIR); + } catch (error) { + // TODO: some of regression tests don't work at the moment, need to fix it. + log.error("DG regression tests run failed"); + log(`${error}`); + } +} + +runDGRegressionTests() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/lib/subprocess.ts b/lib/subprocess.ts new file mode 100644 index 0000000000..c74137b082 --- /dev/null +++ b/lib/subprocess.ts @@ -0,0 +1,14 @@ +import child_process from "node:child_process"; +import util from "node:util"; + +export async function runCommand(command: string, workingDirectory: string) { + const exec = util.promisify(child_process.exec); + + try { + const { stdout } = await exec(command, { cwd: workingDirectory }); + console.log("stdout:", stdout); + } catch (error) { + console.error(`Error running command ${command}`, `${error}`); + throw error; + } +} diff --git a/package.json b/package.json index c3da9ed82e..c950e2b175 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "validate:configs": "yarn hardhat validate-configs", "typecheck": "tsc --noEmit", "dg:install": "yarn ts-node lib/dg-installation.ts", + "dg:regression-tests": "yarn ts-node lib/dg-regression-tests.ts", "abis:extract": "hardhat abis:extract", "verify:deployed": "hardhat verify:deployed", "postinstall": "husky" diff --git a/scripts/dao-local-deploy.sh b/scripts/dao-local-deploy.sh index 87117d26f5..30a535b352 100755 --- a/scripts/dao-local-deploy.sh +++ b/scripts/dao-local-deploy.sh @@ -26,8 +26,6 @@ export INTEGRATION_WITH_CSM="off" yarn test:integration:fork:local # If Dual Governance was deployed -if grep "dg:dual_governance" $NETWORK_STATE_FILE -q; then - # Run DG regression tests - echo "Run Dual Governance regression tests" - (cd dg && npm run test:regressions) +if grep "dg:dualGovernance" $NETWORK_STATE_FILE -q; then + yarn dg:regression-tests fi diff --git a/scripts/dao-local-upgrade.sh b/scripts/dao-local-upgrade.sh index 661c1434be..4f053ec337 100755 --- a/scripts/dao-local-upgrade.sh +++ b/scripts/dao-local-upgrade.sh @@ -24,8 +24,6 @@ yarn hardhat --network $NETWORK run --no-compile scripts/utils/mine.ts yarn test:integration:fork:local # If Dual Governance was deployed -if grep "dg:dual_governance" $NETWORK_STATE_FILE -q; then - # Run DG regression tests - echo "Run Dual Governance regression tests" - (cd dg && npm run test:regressions) +if grep "dg:dualGovernance" $NETWORK_STATE_FILE -q; then + yarn dg:regression-tests fi diff --git a/scripts/scratch/steps/0150-transfer-roles.ts b/scripts/scratch/steps/0150-transfer-roles.ts index 04153b1979..9563a15b22 100644 --- a/scripts/scratch/steps/0150-transfer-roles.ts +++ b/scripts/scratch/steps/0150-transfer-roles.ts @@ -65,10 +65,6 @@ export async function main() { await makeTx(depositSecurityModule, "setOwner", [agent], { from: deployer }); } - // Transfer ownership of LidoTemplate to agent - const lidoTemplate = await loadContract("LidoTemplate", state[Sk.lidoTemplate].address); - await makeTx(lidoTemplate, "setOwner", [agent], { from: deployer }); - // Transfer admin for WithdrawalsManagerProxy from deployer to voting const withdrawalsManagerProxy = await loadContract("WithdrawalsManagerProxy", state.withdrawalVault.proxy.address); await makeTx(withdrawalsManagerProxy, "proxy_changeAdmin", [voting], { from: deployer }); diff --git a/scripts/scratch/steps/0160-deploy-dual-governance.ts b/scripts/scratch/steps/0160-deploy-dual-governance.ts index 5fff2c0766..fbe0a8ae5a 100644 --- a/scripts/scratch/steps/0160-deploy-dual-governance.ts +++ b/scripts/scratch/steps/0160-deploy-dual-governance.ts @@ -1,15 +1,14 @@ -import child_process from "node:child_process"; import fs from "node:fs/promises"; -import util from "node:util"; import { ethers } from "hardhat"; import { LidoTemplate, WithdrawalQueueERC721 } from "typechain-types"; import { log } from "lib"; -import { loadContract } from "lib/contract"; +import { loadContract, LoadedContract } from "lib/contract"; import { makeTx } from "lib/deploy"; import { DeploymentState, getAddress, readNetworkState, Sk, updateObjectInState } from "lib/state-file"; +import { runCommand } from "lib/subprocess"; const DG_INSTALL_DIR = `${process.cwd()}/dg`; const DG_DEPLOY_ARTIFACTS_DIR = `${DG_INSTALL_DIR}/deploy-artifacts`; @@ -99,6 +98,16 @@ async function finalizePermissionsWithoutDGDeployment() { const lidoTemplate = await loadContract("LidoTemplate", lidoTemplateAddress); await makeTx(lidoTemplate, "finalizePermissionsWithoutDGDeployment", [], { from: deployer }); + + await transferLidoTemplateOwnershipToAgent(deployer, lidoTemplate, getAddress(Sk.appAgent, networkState)); +} + +async function transferLidoTemplateOwnershipToAgent( + deployer: string, + lidoTemplate: LoadedContract, + aragonAgentAddress: string, +) { + await makeTx(lidoTemplate, "setOwner", [aragonAgentAddress], { from: deployer }); } async function transferRoles(deployer: string, dgDeployArtifacts: DGDeployArtifacts, networkState: DeploymentState) { @@ -149,6 +158,8 @@ async function transferRoles(deployer: string, dgDeployArtifacts: DGDeployArtifa await makeTx(lidoTemplate, "finalizePermissionsAfterDGDeployment", [dgDeployArtifacts.admin_executor], { from: deployer, }); + + await transferLidoTemplateOwnershipToAgent(deployer, lidoTemplate, aragonAgentAddress); } async function unpauseWithdrawalQueue(deployer: string, networkState: DeploymentState) { @@ -173,18 +184,6 @@ async function prepareDGRegressionTestsRun(networkChainId: string, networkState: await writeDGDotEnvFile(dotEnvFile); } -async function runCommand(command: string, workingDirectory: string) { - const exec = util.promisify(child_process.exec); - - try { - const { stdout } = await exec(command, { cwd: workingDirectory }); - log("stdout:", stdout); - } catch (error) { - log.error(`Error running command ${command}`, `${error}`); - throw error; - } -} - async function writeDGConfigFile(dgConfig: string, filename: string) { const dgConfigFilePath = `${DG_INSTALL_DIR}/deploy-config/${filename}`; @@ -252,60 +251,13 @@ function getDGConfig(chainId: string, networkState: DeploymentState) { wst_eth: wstEth, withdrawal_queue: withdrawalQueue, }, - sanity_check_params: { - max_min_assets_lock_duration: - networkState[Sk.dualGovernanceConfig].dual_governance.sanity_check_params.max_min_assets_lock_duration, - max_sealable_withdrawal_blockers_count: - networkState[Sk.dualGovernanceConfig].dual_governance.sanity_check_params - .max_sealable_withdrawal_blockers_count, - max_tiebreaker_activation_timeout: - networkState[Sk.dualGovernanceConfig].dual_governance.sanity_check_params.max_tiebreaker_activation_timeout, - min_tiebreaker_activation_timeout: - networkState[Sk.dualGovernanceConfig].dual_governance.sanity_check_params.min_tiebreaker_activation_timeout, - min_withdrawals_batch_size: - networkState[Sk.dualGovernanceConfig].dual_governance.sanity_check_params.min_withdrawals_batch_size, - }, - }, - dual_governance_config_provider: { - first_seal_rage_quit_support: - networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.first_seal_rage_quit_support, - second_seal_rage_quit_support: - networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.second_seal_rage_quit_support, - min_assets_lock_duration: - networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.min_assets_lock_duration, - rage_quit_eth_withdrawals_delay_growth: - networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.rage_quit_eth_withdrawals_delay_growth, - rage_quit_eth_withdrawals_min_delay: - networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.rage_quit_eth_withdrawals_min_delay, - rage_quit_eth_withdrawals_max_delay: - networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.rage_quit_eth_withdrawals_max_delay, - rage_quit_extension_period_duration: - networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.rage_quit_extension_period_duration, - veto_cooldown_duration: - networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.veto_cooldown_duration, - veto_signalling_deactivation_max_duration: - networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.veto_signalling_deactivation_max_duration, - veto_signalling_min_active_duration: - networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.veto_signalling_min_active_duration, - veto_signalling_min_duration: - networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.veto_signalling_min_duration, - veto_signalling_max_duration: - networkState[Sk.dualGovernanceConfig].dual_governance_config_provider.veto_signalling_max_duration, + sanity_check_params: networkState[Sk.dualGovernanceConfig].dual_governance.sanity_check_params, }, + dual_governance_config_provider: networkState[Sk.dualGovernanceConfig].dual_governance_config_provider, timelock: { after_submit_delay: networkState[Sk.dualGovernanceConfig].timelock.after_submit_delay, after_schedule_delay: networkState[Sk.dualGovernanceConfig].timelock.after_schedule_delay, - sanity_check_params: { - min_execution_delay: networkState[Sk.dualGovernanceConfig].timelock.sanity_check_params.min_execution_delay, - max_after_submit_delay: - networkState[Sk.dualGovernanceConfig].timelock.sanity_check_params.max_after_submit_delay, - max_after_schedule_delay: - networkState[Sk.dualGovernanceConfig].timelock.sanity_check_params.max_after_schedule_delay, - max_emergency_mode_duration: - networkState[Sk.dualGovernanceConfig].timelock.sanity_check_params.max_emergency_mode_duration, - max_emergency_protection_duration: - networkState[Sk.dualGovernanceConfig].timelock.sanity_check_params.max_emergency_protection_duration, - }, + sanity_check_params: networkState[Sk.dualGovernanceConfig].timelock.sanity_check_params, emergency_protection: { emergency_activation_committee: daoVoting, emergency_execution_committee: daoVoting, diff --git a/scripts/utils/scratch.ts b/scripts/utils/scratch.ts index 5b34cb048b..dff16965c7 100644 --- a/scripts/utils/scratch.ts +++ b/scripts/utils/scratch.ts @@ -121,5 +121,6 @@ export function scratchParametersToDeploymentState(params: ScratchParameters): R operatorGrid: { deployParameters: params.operatorGrid, }, + dualGovernanceConfig: params.dualGovernanceConfig, }; } From 0b72bb7539ebb37afbfb7e691d321237c83eaef9 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Mon, 29 Sep 2025 17:30:57 +0400 Subject: [PATCH 4/8] fix: minor improvements --- lib/dg-installation.ts | 2 +- scripts/dao-local-deploy.sh | 2 +- scripts/dao-local-upgrade.sh | 2 +- scripts/utils/upgrade.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/dg-installation.ts b/lib/dg-installation.ts index 9ae532eaeb..422ede9513 100644 --- a/lib/dg-installation.ts +++ b/lib/dg-installation.ts @@ -3,7 +3,7 @@ import fs from "node:fs/promises"; import { runCommand } from "./subprocess"; const DG_REPOSITORY_URL = "https://github.com/lidofinance/dual-governance.git"; -const DG_REPOSITORY_BRANCH = "feature/scratch-deploy-support2"; // TODO: use release branch +const DG_REPOSITORY_BRANCH = "feature/scratch-deploy-support"; // TODO: use release branch const DG_INSTALL_DIR = `${process.cwd()}/dg`; async function main() { diff --git a/scripts/dao-local-deploy.sh b/scripts/dao-local-deploy.sh index 30a535b352..05586d5249 100755 --- a/scripts/dao-local-deploy.sh +++ b/scripts/dao-local-deploy.sh @@ -26,6 +26,6 @@ export INTEGRATION_WITH_CSM="off" yarn test:integration:fork:local # If Dual Governance was deployed -if grep "dg:dualGovernance" $NETWORK_STATE_FILE -q; then +if grep "dg:escrow_master_copy" $NETWORK_STATE_FILE -q; then yarn dg:regression-tests fi diff --git a/scripts/dao-local-upgrade.sh b/scripts/dao-local-upgrade.sh index 4f053ec337..a2b7b4d123 100755 --- a/scripts/dao-local-upgrade.sh +++ b/scripts/dao-local-upgrade.sh @@ -24,6 +24,6 @@ yarn hardhat --network $NETWORK run --no-compile scripts/utils/mine.ts yarn test:integration:fork:local # If Dual Governance was deployed -if grep "dg:dualGovernance" $NETWORK_STATE_FILE -q; then +if grep "dg:escrow_master_copy" $NETWORK_STATE_FILE -q; then yarn dg:regression-tests fi diff --git a/scripts/utils/upgrade.ts b/scripts/utils/upgrade.ts index ca801a5d4d..c220d2dc33 100644 --- a/scripts/utils/upgrade.ts +++ b/scripts/utils/upgrade.ts @@ -57,7 +57,7 @@ export async function mockDGAragonVoting( const voting = await loadContract("Voting", votingAddress); const timelock = await loadContract( "IEmergencyProtectedTimelock", - state[Sk.dgEmergencyProtectedTimelock].proxy.address, // TODO: del! + state[Sk.dgEmergencyProtectedTimelock].address, ); const afterSubmitDelay = await timelock.getAfterSubmitDelay(); const afterScheduleDelay = await timelock.getAfterScheduleDelay(); From adad353a1ad46a3daf7179607bcdbd7f13cf1bf5 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Mon, 29 Sep 2025 17:56:58 +0400 Subject: [PATCH 5/8] fix: ci workflow for dual governance scratch deployment --- .../workflows/tests-integration-scratch.yml | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/.github/workflows/tests-integration-scratch.yml b/.github/workflows/tests-integration-scratch.yml index f4430c12b0..3f456680c0 100644 --- a/.github/workflows/tests-integration-scratch.yml +++ b/.github/workflows/tests-integration-scratch.yml @@ -24,6 +24,12 @@ jobs: - name: Common setup uses: ./.github/workflows/setup + - name: Install foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Print forge version + run: forge --version + - name: Set env run: cp .env.example .env @@ -38,6 +44,7 @@ jobs: GAS_MAX_FEE: 100 NETWORK_STATE_FILE: "deployed-local.json" NETWORK_STATE_DEFAULTS_FILE: "scripts/defaults/testnet-defaults.json" + DG_DEPLOYMENT_ENABLED: false - name: Finalize scratch deployment run: yarn hardhat --network local run --no-compile scripts/utils/mine.ts @@ -46,3 +53,58 @@ jobs: run: yarn test:integration:fork:local env: INTEGRATION_WITH_CSM: "off" + + test_hardhat_integration_scratch_with_dg: + name: Hardhat / Scratch with DG + runs-on: ubuntu-latest + timeout-minutes: 120 + env: + SKIP_GAS_REPORT: true + SKIP_CONTRACT_SIZE: true + SKIP_INTERFACES_CHECK: true + + services: + hardhat-node: + image: ghcr.io/lidofinance/hardhat-node:2.26.0-scratch + ports: + - 8555:8545 + + steps: + - uses: actions/checkout@v4 + + - name: Common setup + uses: ./.github/workflows/setup + + - name: Install foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Print forge version + run: forge --version + + - name: Set env + run: cp .env.example .env + + - name: Run scratch deployment + run: ./scripts/dao-deploy.sh + env: + NETWORK: "local" + RPC_URL: "http://localhost:8555" + GENESIS_TIME: 1639659600 # just a random time + DEPLOYER: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" # first acc of default mnemonic "test test ..." + GAS_PRIORITY_FEE: 1 + GAS_MAX_FEE: 100 + NETWORK_STATE_FILE: "deployed-local.json" + NETWORK_STATE_DEFAULTS_FILE: "scripts/defaults/testnet-defaults.json" + DG_DEPLOYMENT_ENABLED: true + DG_DEPLOYER_ACCOUNT_NETWORK_NAME: local + + - name: Finalize scratch deployment + run: yarn hardhat --network local run --no-compile scripts/utils/mine.ts + + - name: Run integration tests + run: yarn test:integration:fork:local + env: + INTEGRATION_WITH_CSM: "off" + + - name: Run Dual Governance regression tests + run: yarn dg:regression-tests From 925b5021d91a63583eb19320f347e66ce985be78 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:44:29 +0400 Subject: [PATCH 6/8] fix: reduce LidoTemplate contract size --- contracts/0.4.24/template/LidoTemplate.sol | 46 +++++++--------------- hardhat.config.ts | 11 ++++++ 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/contracts/0.4.24/template/LidoTemplate.sol b/contracts/0.4.24/template/LidoTemplate.sol index 646a066b12..72f2b1dd54 100644 --- a/contracts/0.4.24/template/LidoTemplate.sol +++ b/contracts/0.4.24/template/LidoTemplate.sol @@ -42,7 +42,7 @@ contract LidoTemplate is IsContract { string private constant ERROR_BAD_AMOUNTS_LEN = "TMPL_BAD_AMOUNTS_LEN"; string private constant ERROR_INVALID_ID = "TMPL_INVALID_ID"; string private constant ERROR_UNEXPECTED_TOTAL_SUPPLY = "TMPL_UNEXPECTED_TOTAL_SUPPLY"; - string private constant ERROR_INVALID_DG_ADMIN_EXECUTOR = "TMPL_INVALID_DG_ADMIN_EXECUTOR"; + string private constant ERROR_INVALID_DG_ADMIN_EXECUTOR = "TMPL_0_ADDR"; // Operational errors string private constant ERROR_PERMISSION_DENIED = "TMPL_PERMISSION_DENIED"; @@ -414,39 +414,17 @@ contract LidoTemplate is IsContract { function finalizePermissionsAfterDGDeployment(address dgAdminExecutor) external onlyOwner { require(dgAdminExecutor != address(0), ERROR_INVALID_DG_ADMIN_EXECUTOR); - DeployState memory state = deployState; - - state.acl.grantPermission(dgAdminExecutor, address(state.agent), state.agent.RUN_SCRIPT_ROLE()); - state.acl.grantPermission(dgAdminExecutor, address(state.agent), state.agent.EXECUTE_ROLE()); + deployState.acl.grantPermission(dgAdminExecutor, address(deployState.agent), deployState.agent.RUN_SCRIPT_ROLE()); + deployState.acl.grantPermission(dgAdminExecutor, address(deployState.agent), deployState.agent.EXECUTE_ROLE()); - state.acl.revokePermission(address(state.voting), address(state.agent), state.agent.RUN_SCRIPT_ROLE()); - state.acl.revokePermission(address(state.voting), address(state.agent), state.agent.EXECUTE_ROLE()); + deployState.acl.revokePermission(address(deployState.voting), address(deployState.agent), deployState.agent.RUN_SCRIPT_ROLE()); + deployState.acl.revokePermission(address(deployState.voting), address(deployState.agent), deployState.agent.EXECUTE_ROLE()); - state.acl.setPermissionManager(address(state.agent), address(state.agent), state.agent.RUN_SCRIPT_ROLE()); - state.acl.setPermissionManager(address(state.agent), address(state.agent), state.agent.EXECUTE_ROLE()); - - Kernel apmDAO = Kernel(state.lidoRegistry.kernel()); - ACL apmACL = ACL(apmDAO.acl()); - _transferPermissionFromTemplate(apmACL, apmACL, address(state.agent), apmACL.CREATE_PERMISSIONS_ROLE()); - - _transferPermissionFromTemplate(state.acl, address(state.acl), address(state.agent), state.acl.CREATE_PERMISSIONS_ROLE(), address(state.agent)); - - _resetState(); + _finalizePermissions(address(deployState.agent)); } function finalizePermissionsWithoutDGDeployment() external onlyOwner { - DeployState memory state = deployState; - - state.acl.setPermissionManager(address(state.voting), address(state.agent), state.agent.RUN_SCRIPT_ROLE()); - state.acl.setPermissionManager(address(state.voting), address(state.agent), state.agent.EXECUTE_ROLE()); - - Kernel apmDAO = Kernel(state.lidoRegistry.kernel()); - ACL apmACL = ACL(apmDAO.acl()); - _transferPermissionFromTemplate(apmACL, apmACL, address(state.agent), apmACL.CREATE_PERMISSIONS_ROLE()); - - _transferPermissionFromTemplate(state.acl, address(state.acl), address(state.voting), state.acl.CREATE_PERMISSIONS_ROLE(), address(state.voting)); - - _resetState(); + _finalizePermissions(address(deployState.voting)); } /* DAO AND APPS */ @@ -754,9 +732,15 @@ contract LidoTemplate is IsContract { return keccak256(abi.encodePacked(_apmRootNode, keccak256(abi.encodePacked(_appName)))); } - /* STATE RESET */ + function _finalizePermissions(address newManager) private { + deployState.acl.setPermissionManager(newManager, address(deployState.agent), deployState.agent.RUN_SCRIPT_ROLE()); + deployState.acl.setPermissionManager(newManager, address(deployState.agent), deployState.agent.EXECUTE_ROLE()); + + ACL apmACL = ACL(deployState.lidoRegistry.kernel().acl()); + _transferPermissionFromTemplate(apmACL, apmACL, address(deployState.agent), apmACL.CREATE_PERMISSIONS_ROLE()); + + _transferPermissionFromTemplate(deployState.acl, address(deployState.acl), newManager, deployState.acl.CREATE_PERMISSIONS_ROLE(), newManager); - function _resetState() private { delete deployState.lidoRegistryEnsNode; delete deployState.lidoRegistry; delete deployState.dao; diff --git a/hardhat.config.ts b/hardhat.config.ts index 9cda64c2b1..ff1759edfa 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -219,6 +219,17 @@ const config: HardhatUserConfig = { evmVersion: "cancun", }, }, + "contracts/0.4.24/template/LidoTemplate.sol": { + version: "0.4.24", + settings: { + optimizer: { + enabled: true, + runs: 50, + }, + viaIR: true, + evmVersion: "constantinople", + }, + }, }, }, tracer: { From d8f4ffdfee79fa793a3edfc38ea4698ea3baa809 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:53:32 +0400 Subject: [PATCH 7/8] fix: ci workflow for dual governance scratch deployment --- .github/workflows/tests-integration-scratch.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests-integration-scratch.yml b/.github/workflows/tests-integration-scratch.yml index 3f456680c0..19106d0e63 100644 --- a/.github/workflows/tests-integration-scratch.yml +++ b/.github/workflows/tests-integration-scratch.yml @@ -31,7 +31,7 @@ jobs: run: forge --version - name: Set env - run: cp .env.example .env + run: cp .env.example .env && echo "DG_DEPLOYMENT_ENABLED=false" >> .env - name: Run scratch deployment run: ./scripts/dao-deploy.sh @@ -44,7 +44,7 @@ jobs: GAS_MAX_FEE: 100 NETWORK_STATE_FILE: "deployed-local.json" NETWORK_STATE_DEFAULTS_FILE: "scripts/defaults/testnet-defaults.json" - DG_DEPLOYMENT_ENABLED: false + DG_DEPLOYMENT_ENABLED: "false" - name: Finalize scratch deployment run: yarn hardhat --network local run --no-compile scripts/utils/mine.ts @@ -95,8 +95,8 @@ jobs: GAS_MAX_FEE: 100 NETWORK_STATE_FILE: "deployed-local.json" NETWORK_STATE_DEFAULTS_FILE: "scripts/defaults/testnet-defaults.json" - DG_DEPLOYMENT_ENABLED: true - DG_DEPLOYER_ACCOUNT_NETWORK_NAME: local + DG_DEPLOYMENT_ENABLED: "true" + DG_DEPLOYER_ACCOUNT_NETWORK_NAME: "local" - name: Finalize scratch deployment run: yarn hardhat --network local run --no-compile scripts/utils/mine.ts From e54cd83bc29c27d59230fe66b4baff7119f37fd1 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Tue, 30 Sep 2025 12:52:26 +0400 Subject: [PATCH 8/8] fix: ci workflow for dual governance scratch deployment --- .github/workflows/tests-integration-scratch.yml | 5 ++++- test.accounts.json.example | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 test.accounts.json.example diff --git a/.github/workflows/tests-integration-scratch.yml b/.github/workflows/tests-integration-scratch.yml index 19106d0e63..641adde50f 100644 --- a/.github/workflows/tests-integration-scratch.yml +++ b/.github/workflows/tests-integration-scratch.yml @@ -82,7 +82,10 @@ jobs: run: forge --version - name: Set env - run: cp .env.example .env + run: cp .env.example .env && echo "DG_DEPLOYER_ACCOUNT_NETWORK_NAME=local" >> .env + + - name: Create accounts.json file + run: cp test.accounts.json.example accounts.json - name: Run scratch deployment run: ./scripts/dao-deploy.sh diff --git a/test.accounts.json.example b/test.accounts.json.example new file mode 100644 index 0000000000..75c744462e --- /dev/null +++ b/test.accounts.json.example @@ -0,0 +1,6 @@ +{ + "eth": { + "local": ["ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"] + }, + "description": "The key $.eth.local above contains the private key of the first account of default mnemonic 'test test ...'" +}