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/.github/workflows/tests-integration-scratch.yml b/.github/workflows/tests-integration-scratch.yml index f4430c12b0..641adde50f 100644 --- a/.github/workflows/tests-integration-scratch.yml +++ b/.github/workflows/tests-integration-scratch.yml @@ -24,8 +24,14 @@ 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 + run: cp .env.example .env && echo "DG_DEPLOYMENT_ENABLED=false" >> .env - name: Run scratch deployment run: ./scripts/dao-deploy.sh @@ -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,61 @@ 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 && 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 + 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 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/contracts/0.4.24/template/LidoTemplate.sol b/contracts/0.4.24/template/LidoTemplate.sol index 92c01a16d4..72f2b1dd54 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_0_ADDR"; // Operational errors string private constant ERROR_PERMISSION_DENIED = "TMPL_PERMISSION_DENIED"; @@ -404,13 +405,28 @@ 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.acl.grantPermission(dgAdminExecutor, address(deployState.agent), deployState.agent.RUN_SCRIPT_ROLE()); + deployState.acl.grantPermission(dgAdminExecutor, address(deployState.agent), deployState.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()); + + _finalizePermissions(address(deployState.agent)); + } + + function finalizePermissionsWithoutDGDeployment() external onlyOwner { + _finalizePermissions(address(deployState.voting)); + } + /* DAO AND APPS */ /** @@ -564,7 +580,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 +649,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 +693,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 { @@ -718,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/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..ff1759edfa 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, @@ -218,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: { 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 new file mode 100644 index 0000000000..422ede9513 --- /dev/null +++ b/lib/dg-installation.ts @@ -0,0 +1,39 @@ +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-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); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); 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/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/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 dfeee948dd..c950e2b175 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,8 @@ "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", + "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-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/dao-local-deploy.sh b/scripts/dao-local-deploy.sh index 119ff803ce..05586d5249 100755 --- a/scripts/dao-local-deploy.sh +++ b/scripts/dao-local-deploy.sh @@ -24,3 +24,8 @@ 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: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 f0e67fd90f..a2b7b4d123 100755 --- a/scripts/dao-local-upgrade.sh +++ b/scripts/dao-local-upgrade.sh @@ -22,3 +22,8 @@ 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:escrow_master_copy" $NETWORK_STATE_FILE -q; then + yarn dg:regression-tests +fi diff --git a/scripts/defaults/local-devnet-defaults.json b/scripts/defaults/local-devnet-defaults.json index e5c52cbbfb..4783b1dcf0 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": 900, + "sanity_check_params": { + "max_min_assets_lock_duration": 3600, + "max_sealable_withdrawal_blockers_count": 255, + "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": 300, + "rage_quit_eth_withdrawals_delay_growth": 1296000, + "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": 300, + "after_schedule_delay": 300, + "sanity_check_params": { + "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": 900, + "emergency_protection_end_date": 1781913600 + } + }, + "tiebreaker": { + "execution_delay": 900 + } } } diff --git a/scripts/scratch/deploy-params-testnet.toml b/scripts/scratch/deploy-params-testnet.toml index d9af0429d5..cafa6a00e1 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 = 900 # 15 minutes + +[dualGovernanceConfig.dual_governance.sanity_check_params] +max_min_assets_lock_duration = 3600 # 1 hour +max_sealable_withdrawal_blockers_count = 255 +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 = 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 = 300 # 5 minutes +after_schedule_delay = 300 # 5 minutes + +[dualGovernanceConfig.timelock.sanity_check_params] +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 = 900 # 15 minutes +emergency_protection_end_date = 1781913600 # Sat, June 20, 2026 12:00:00 AM GMT+00:00 + +[dualGovernanceConfig.tiebreaker] +execution_delay = 900 # 15 minutes 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/0150-transfer-roles.ts b/scripts/scratch/steps/0150-transfer-roles.ts index 067e85a065..9563a15b22 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 @@ -63,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 new file mode 100644 index 0000000000..fbe0a8ae5a --- /dev/null +++ b/scripts/scratch/steps/0160-deploy-dual-governance.ts @@ -0,0 +1,404 @@ +import fs from "node:fs/promises"; + +import { ethers } from "hardhat"; + +import { LidoTemplate, WithdrawalQueueERC721 } from "typechain-types"; + +import { log } from "lib"; +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`; + +export async function main() { + if (process.env.DG_DEPLOYMENT_ENABLED == "false") { + log.header("DG deployment disabled"); + await finalizePermissionsWithoutDGDeployment(); + 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 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, + ); + + const dgDeployArtifacts = await getDGDeployArtifacts(chainId); + + await transferRoles(deployer, dgDeployArtifacts, state); + + await prepareDGRegressionTestsRun(chainId, state, process.env.LOCAL_RPC_URL); + + saveDGNetworkState(dgDeployArtifacts); +} + +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 }); + + 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) { + 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, + }); + + await transferLidoTemplateOwnershipToAgent(deployer, lidoTemplate, aragonAgentAddress); +} + +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); +} + +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: [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: 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: networkState[Sk.dualGovernanceConfig].timelock.sanity_check_params, + 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 stakingRouter = getAddress(Sk.stakingRouter, 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_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 +`; +} + +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]); + 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/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, }; } diff --git a/scripts/utils/upgrade.ts b/scripts/utils/upgrade.ts index 7707885e28..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, + state[Sk.dgEmergencyProtectedTimelock].address, ); 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); 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 ...'" +}