From c59fe930b84f347e35541a1496cec10e15e7783c Mon Sep 17 00:00:00 2001 From: Artyom Veremeenko Date: Sat, 18 Oct 2025 11:33:51 +0300 Subject: [PATCH 01/13] fix: integrations upgrade run without GENESIS_TIME it was needed only for TW upgrade --- scripts/upgrade/steps/0000-check-env.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/upgrade/steps/0000-check-env.ts b/scripts/upgrade/steps/0000-check-env.ts index 9f9b78275e..c5159f9a3a 100644 --- a/scripts/upgrade/steps/0000-check-env.ts +++ b/scripts/upgrade/steps/0000-check-env.ts @@ -20,7 +20,7 @@ export async function main() { throw new Error("Env variable GAS_MAX_FEE is not set"); } - if (!process.env.GENESIS_TIME) { + if (process.env.MODE === "scratch" && !process.env.GENESIS_TIME) { throw new Error("Env variable GENESIS_TIME is not set"); } From 18577c75be046b7e904445250fcfba7264ce1896 Mon Sep 17 00:00:00 2001 From: Artyom Veremeenko Date: Sat, 18 Oct 2025 11:37:20 +0300 Subject: [PATCH 02/13] feat: fail upgrade on integrations if execute tx gas > fusaka limit --- scripts/utils/upgrade.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/utils/upgrade.ts b/scripts/utils/upgrade.ts index 7707885e28..b361d90acc 100644 --- a/scripts/utils/upgrade.ts +++ b/scripts/utils/upgrade.ts @@ -12,7 +12,9 @@ import { loadContract } from "lib/contract"; import { findEventsWithInterfaces } from "lib/event"; import { DeploymentState, getAddress, Sk } from "lib/state-file"; -const UPGRADE_PARAMETERS_FILE = process.env.UPGRADE_PARAMETERS_FILE || "scripts/upgrade/upgrade-params-mainnet.toml"; +const FUSAKA_TX_LIMIT = 2n ** 24n; // 16M = 16_777_216 + +const UPGRADE_PARAMETERS_FILE = process.env.UPGRADE_PARAMETERS_FILE; export { UpgradeParameters }; @@ -93,5 +95,10 @@ export async function mockDGAragonVoting( const proposalExecutedReceipt = (await proposalExecutedTx.wait())!; log.success("Proposal executed: gas used", proposalExecutedReceipt.gasUsed); + if (proposalExecutedReceipt.gasUsed > FUSAKA_TX_LIMIT) { + log.error("Proposal executed: gas used exceeds FUSAKA_TX_LIMIT"); + process.exit(1); + } + return { voteId, proposalId, executeReceipt, scheduleReceipt, proposalExecutedReceipt }; } From 5fa608b72e1fbcc924fd7ac9090b3dffa8b3b989 Mon Sep 17 00:00:00 2001 From: Artyom Veremeenko Date: Sat, 18 Oct 2025 12:05:27 +0300 Subject: [PATCH 03/13] chore: remove TW obsolete stuff --- lib/config-schemas.ts | 7 +- lib/state-file.ts | 1 - package.json | 2 +- .../.env.sample.obsolete | 9 - .../test-scratch-upgrade.sh | 26 -- .../tw-deploy.ts.obsolete | 305 ------------------ .../tw-verify.ts.obsolete | 92 ------ .../upgrade/steps/0500-mock-aragon-voting.ts | 11 - scripts/upgrade/upgrade-params-mainnet.toml | 16 +- tasks/validate-configs.ts | 8 - upgrade-parameters-mainnet.json | 8 - 11 files changed, 4 insertions(+), 481 deletions(-) delete mode 100644 scripts/triggerable-withdrawals/.env.sample.obsolete delete mode 100644 scripts/triggerable-withdrawals/test-scratch-upgrade.sh delete mode 100644 scripts/triggerable-withdrawals/tw-deploy.ts.obsolete delete mode 100644 scripts/triggerable-withdrawals/tw-verify.ts.obsolete delete mode 100644 scripts/upgrade/steps/0500-mock-aragon-voting.ts delete mode 100644 upgrade-parameters-mainnet.json diff --git a/lib/config-schemas.ts b/lib/config-schemas.ts index a2f4e4799f..ddef3640a4 100644 --- a/lib/config-schemas.ts +++ b/lib/config-schemas.ts @@ -72,7 +72,7 @@ const BurnerSchema = z.object({ totalNonCoverSharesBurnt: BigIntStringSchema.optional(), }); -// Triggerable withdrawals gateway schema +// Triggerable withdrawals gateway schema (used in scratch configs) const TriggerableWithdrawalsGatewaySchema = z.object({ maxExitRequestsLimit: PositiveIntSchema, exitsPerFrame: PositiveIntSchema, @@ -117,11 +117,6 @@ export const UpgradeParametersSchema = z.object({ burner: BurnerSchema, oracleVersions: OracleVersionsSchema.optional(), aragonAppVersions: AragonAppVersionsSchema.optional(), - triggerableWithdrawalsGateway: TriggerableWithdrawalsGatewaySchema, - triggerableWithdrawals: z.object({ - exit_events_lookback_window_in_slots: PositiveIntSchema, - nor_exit_deadline_in_sec: PositiveIntSchema, - }), }); // Gate seal schema (for scratch deployment) diff --git a/lib/state-file.ts b/lib/state-file.ts index 93789d9dc6..a00fda803b 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -170,7 +170,6 @@ export function getAddress(contractKey: Sk, state: DeploymentState): string { case Sk.stakingVaultFactory: case Sk.minFirstAllocationStrategy: case Sk.validatorConsolidationRequests: - case Sk.twVoteScript: case Sk.v3VoteScript: return state[contractKey].address; default: diff --git a/package.json b/package.json index dfeee948dd..90ee7accce 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "test:integration:upgrade": "yarn test:integration:upgrade:helper test/integration/**/*.ts", "test:integration:upgrade:helper": "cp deployed-mainnet.json deployed-mainnet-upgrade.json && NETWORK_STATE_FILE=deployed-mainnet-upgrade.json UPGRADE_PARAMETERS_FILE=scripts/upgrade/upgrade-params-mainnet.toml MODE=forking UPGRADE=true GAS_PRIORITY_FEE=1 GAS_MAX_FEE=100 DEPLOYER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 hardhat test --trace --disabletracer", "test:integration:upgrade-template": "cp deployed-mainnet.json deployed-mainnet-upgrade.json && NETWORK_STATE_FILE=deployed-mainnet-upgrade.json UPGRADE_PARAMETERS_FILE=scripts/upgrade/upgrade-params-mainnet.toml MODE=forking TEMPLATE_TEST=true GAS_PRIORITY_FEE=1 GAS_MAX_FEE=100 DEPLOYER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 hardhat test test/integration/upgrade/*.ts --fulltrace --disabletracer", - "test:integration:scratch": "yarn test:integration:scratch:helper test/integration/**/*.ts", + "test:integration:scratch": "DEPLOYER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 SKIP_INTERFACES_CHECK=true SKIP_CONTRACT_SIZE=true SKIP_GAS_REPORT=true GENESIS_TIME=1639659600 GAS_PRIORITY_FEE=1 GAS_MAX_FEE=100 yarn test:integration:scratch:helper test/integration/**/*.ts", "test:integration:scratch:helper": "MODE=scratch hardhat test", "test:integration:scratch:trace": "MODE=scratch hardhat test test/integration/**/*.ts --trace --disabletracer", "test:integration:scratch:fulltrace": "MODE=scratch hardhat test test/integration/**/*.ts --fulltrace --disabletracer", diff --git a/scripts/triggerable-withdrawals/.env.sample.obsolete b/scripts/triggerable-withdrawals/.env.sample.obsolete deleted file mode 100644 index 8157a2159d..0000000000 --- a/scripts/triggerable-withdrawals/.env.sample.obsolete +++ /dev/null @@ -1,9 +0,0 @@ -# Deployer -DEPLOYER= -DEPLOYER_PRIVATE_KEY= -# Chain config -RPC_URL= -NETWORK= -# Deploy transactions gas -GAS_PRIORITY_FEE= -GAS_MAX_FEE= diff --git a/scripts/triggerable-withdrawals/test-scratch-upgrade.sh b/scripts/triggerable-withdrawals/test-scratch-upgrade.sh deleted file mode 100644 index 0f0c4f67c3..0000000000 --- a/scripts/triggerable-withdrawals/test-scratch-upgrade.sh +++ /dev/null @@ -1,26 +0,0 @@ -# RPC_URL: http://localhost:8555 -# DEPLOYER: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" # first acc of default mnemonic "test test ..." -# GAS_PRIORITY_FEE: 1 -# GAS_MAX_FEE: 100 -# NETWORK_STATE_FILE: deployed-mainnet-upgrade.json -# UPGRADE_PARAMETERS_FILE: upgrade-parameters-mainnet.json - -export RPC_URL=${RPC_URL:="http://127.0.0.1:8545"} # if defined use the value set to default otherwise -export SLOTS_PER_EPOCH=32 -export GENESIS_TIME=1606824023 # just some time -# export WITHDRAWAL_QUEUE_BASE_URI="<< SET IF REQUIED >>" -# export DSM_PREDEFINED_ADDRESS="<< SET IF REQUIED >>" - -export DEPLOYER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 # first acc of default mnemonic "test test ..." -export GAS_PRIORITY_FEE=1 -export GAS_MAX_FEE=100 - -export NETWORK_STATE_FILE=deployed-mainnet-upgrade.json - -cp deployed-mainnet.json $NETWORK_STATE_FILE - -yarn upgrade:deploy -yarn upgrade:mock-voting -# cp $NETWORK_STATE_FILE deployed-mainnet.json -# yarn hardhat --network custom run --no-compile scripts/utils/mine.ts -yarn test:integration diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts.obsolete b/scripts/triggerable-withdrawals/tw-deploy.ts.obsolete deleted file mode 100644 index 40c99a7aad..0000000000 --- a/scripts/triggerable-withdrawals/tw-deploy.ts.obsolete +++ /dev/null @@ -1,305 +0,0 @@ -import * as dotenv from "dotenv"; -import { ethers } from "hardhat"; -import { join } from "path"; - -import { LidoLocator } from "typechain-types"; - -import { - cy, - deployImplementation, - DeploymentState, - findEvents, - loadContract, - log, - makeTx, - persistNetworkState, - readNetworkState, - Sk, - updateObjectInState, -} from "lib"; - -dotenv.config({ path: join(__dirname, "../../.env") }); - -//-------------------------------------------------------------------------- -// Helpers -//-------------------------------------------------------------------------- - -function requireEnv(variable: string): string { - const value = process.env[variable]; - if (!value) throw new Error(`Environment variable ${variable} is not set`); - log(`Using env variable ${variable}=${value}`); - return value; -} - -async function deployGateSeal( - state: DeploymentState, - deployer: string, - sealableContracts: string[], - sealDuration: number, - expiryTimestamp: number, - kind: Sk.gateSeal | Sk.gateSealTW, -): Promise { - const gateSealFactory = await loadContract("IGateSealFactory", state[Sk.gateSeal].factoryAddress); - - const receipt = await makeTx( - gateSealFactory, - "create_gate_seal", - [state[Sk.gateSeal].sealingCommittee, sealDuration, sealableContracts, expiryTimestamp], - { from: deployer }, - ); - - // Extract and log the new GateSeal address - const gateSealAddress = await findEvents(receipt, "GateSealCreated")[0].args.gate_seal; - log(`GateSeal created: ${cy(gateSealAddress)}`); - log.emptyLine(); - - // Update the state with the new GateSeal address - updateObjectInState(kind, { - factoryAddress: state[Sk.gateSeal].factoryAddress, - sealDuration, - expiryTimestamp, - sealingCommittee: state[Sk.gateSeal].sealingCommittee, - address: gateSealAddress, - }); - - return gateSealAddress; -} - -//-------------------------------------------------------------------------- -// Main -//-------------------------------------------------------------------------- - -async function main(): Promise { - // ----------------------------------------------------------------------- - // Environment & chain context - // ----------------------------------------------------------------------- - const deployer = ethers.getAddress(requireEnv("DEPLOYER")); - - const { chainId } = await ethers.provider.getNetwork(); - const currentBlock = await ethers.provider.getBlock("latest"); - if (!currentBlock) throw new Error("Failed to fetch the latest block"); - - log(cy(`Deploying contracts on chain ${chainId}`)); - - // ----------------------------------------------------------------------- - // State & configuration - // ----------------------------------------------------------------------- - const state = readNetworkState(); - persistNetworkState(state); - - const chainSpec = state[Sk.chainSpec] as { - slotsPerEpoch: number; - secondsPerSlot: number; - genesisTime: number; - depositContractAddress: string; // legacy support - depositContract?: string; - }; - - log(`Chain spec: ${JSON.stringify(chainSpec, null, 2)}`); - - // Consensus‑spec constants - const SECONDS_PER_SLOT = chainSpec.secondsPerSlot; - const SLOTS_PER_EPOCH = chainSpec.slotsPerEpoch; - const GENESIS_TIME = chainSpec.genesisTime; - const DEPOSIT_CONTRACT_ADDRESS = chainSpec.depositContractAddress ?? chainSpec.depositContract; - const SHARD_COMMITTEE_PERIOD_SLOTS = 2 ** 8 * SLOTS_PER_EPOCH; // 8192 - - // G‑indices (phase0 spec) - const VALIDATOR_PREV_GINDEX = "0x0000000000000000000000000000000000000000000000000096000000000028"; - const VALIDATOR_CURR_GINDEX = VALIDATOR_PREV_GINDEX; - const FIRST_HISTORICAL_SUMMARY_PREV_GINDEX = "0x000000000000000000000000000000000000000000000000000000b600000018"; - const FIRST_HISTORICAL_SUMMARY_CURR_GINDEX = FIRST_HISTORICAL_SUMMARY_PREV_GINDEX; - const BLOCK_ROOT_IN_SUMMARY_PREV_GINDEX = "0x000000000000000000000000000000000000000000000000000000000040000d"; - const BLOCK_ROOT_IN_SUMMARY_CURR_GINDEX = BLOCK_ROOT_IN_SUMMARY_PREV_GINDEX; - - const FIRST_SUPPORTED_SLOT = 364032 * SLOTS_PER_EPOCH; // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7600.md#activation - const PIVOT_SLOT = FIRST_SUPPORTED_SLOT; - const CAPELLA_SLOT = 194048 * 32; // capellaSlot @see https://github.com/ethereum/consensus-specs/blob/365320e778965631cbef11fd93328e82a746b1f6/specs/capella/fork.md?plain=1#L22 - const SLOTS_PER_HISTORICAL_ROOT = 8192; - - // TriggerableWithdrawalsGateway params - const TRIGGERABLE_WITHDRAWALS_MAX_LIMIT = 11_200; - const TRIGGERABLE_WITHDRAWALS_LIMIT_PER_FRAME = 1; - const TRIGGERABLE_WITHDRAWALS_FRAME_DURATION = 48; - - // GateSeal params - const GATE_SEAL_EXPIRY_TIMESTAMP = currentBlock.timestamp + 365 * 24 * 60 * 60; // 1 year - const GATE_SEAL_DURATION_SECONDS = 14 * 24 * 60 * 60; // 14 days - - const agent = state["app:aragon-agent"].proxy.address; - log(`Using agent: ${agent}`); - - const locator = await loadContract("LidoLocator", state[Sk.lidoLocator].proxy.address); - - // ----------------------------------------------------------------------- - // Deployments - // ----------------------------------------------------------------------- - - // 1. ValidatorsExitBusOracle - const validatorsExitBusOracle = await deployImplementation( - Sk.validatorsExitBusOracle, - "ValidatorsExitBusOracle", - deployer, - [SECONDS_PER_SLOT, GENESIS_TIME, locator.address], - ); - log.success(`ValidatorsExitBusOracle: ${validatorsExitBusOracle.address}`); - - // 2. TriggerableWithdrawalsGateway - const triggerableWithdrawalsGateway = await deployImplementation( - Sk.triggerableWithdrawalsGateway, - "TriggerableWithdrawalsGateway", - deployer, - [ - agent, - locator.address, - TRIGGERABLE_WITHDRAWALS_MAX_LIMIT, - TRIGGERABLE_WITHDRAWALS_LIMIT_PER_FRAME, - TRIGGERABLE_WITHDRAWALS_FRAME_DURATION, - ], - ); - log.success(`TriggerableWithdrawalsGateway: ${triggerableWithdrawalsGateway.address}`); - - // 3. WithdrawalVault - const withdrawalVault = await deployImplementation(Sk.withdrawalVault, "WithdrawalVault", deployer, [ - await locator.lido(), - await locator.treasury(), - triggerableWithdrawalsGateway.address, - ]); - log.success(`WithdrawalVault: ${withdrawalVault.address}`); - - // ----------------------------------------------------------------------- - // Shared libraries - // ----------------------------------------------------------------------- - const minFirstAllocationStrategyAddress = state[Sk.minFirstAllocationStrategy].address; - const libraries = { - MinFirstAllocationStrategy: minFirstAllocationStrategyAddress, - } as const; - - // 4. StakingRouter - const stakingRouter = await deployImplementation( - Sk.stakingRouter, - "StakingRouter", - deployer, - [DEPOSIT_CONTRACT_ADDRESS], - { libraries }, - ); - log.success(`StakingRouter: ${stakingRouter.address}`); - - // 5. NodeOperatorsRegistry - const nor = await deployImplementation(Sk.appNodeOperatorsRegistry, "NodeOperatorsRegistry", deployer, [], { - libraries, - }); - log.success(`NodeOperatorsRegistry: ${nor.address}`); - - // 6. ValidatorExitDelayVerifier - const gIndexes = { - gIFirstValidatorPrev: VALIDATOR_PREV_GINDEX, - gIFirstValidatorCurr: VALIDATOR_CURR_GINDEX, - gIFirstHistoricalSummaryPrev: FIRST_HISTORICAL_SUMMARY_PREV_GINDEX, - gIFirstHistoricalSummaryCurr: FIRST_HISTORICAL_SUMMARY_CURR_GINDEX, - gIFirstBlockRootInSummaryPrev: BLOCK_ROOT_IN_SUMMARY_PREV_GINDEX, - gIFirstBlockRootInSummaryCurr: BLOCK_ROOT_IN_SUMMARY_CURR_GINDEX, - }; - - const validatorExitDelayVerifier = await deployImplementation( - Sk.validatorExitDelayVerifier, - "ValidatorExitDelayVerifier", - deployer, - [ - locator.address, - gIndexes, - FIRST_SUPPORTED_SLOT, - PIVOT_SLOT, - CAPELLA_SLOT, - SLOTS_PER_HISTORICAL_ROOT, // slotsPerHistoricalRoot - SLOTS_PER_EPOCH, - SECONDS_PER_SLOT, - GENESIS_TIME, - SHARD_COMMITTEE_PERIOD_SLOTS * SECONDS_PER_SLOT, // shardCommitteePeriodInSeconds - ], - ); - log.success(`ValidatorExitDelayVerifier: ${validatorExitDelayVerifier.address}`); - - // 7. AccountingOracle - const accountingOracle = await deployImplementation(Sk.accountingOracle, "AccountingOracle", deployer, [ - locator.address, - await locator.lido(), - await locator.legacyOracle(), - SECONDS_PER_SLOT, - GENESIS_TIME, - ]); - log.success(`AccountingOracle: ${accountingOracle.address}`); - - // ----------------------------------------------------------------------- - // New LidoLocator (all addresses consolidated) - // ----------------------------------------------------------------------- - const locatorConfig = [ - await locator.accountingOracle(), - await locator.depositSecurityModule(), - await locator.elRewardsVault(), - await locator.legacyOracle(), - await locator.lido(), - await locator.oracleReportSanityChecker(), - await locator.postTokenRebaseReceiver(), - await locator.burner(), - await locator.stakingRouter(), - await locator.treasury(), - await locator.validatorsExitBusOracle(), - await locator.withdrawalQueue(), - await locator.withdrawalVault(), - await locator.oracleDaemonConfig(), - validatorExitDelayVerifier.address, - triggerableWithdrawalsGateway.address, - ]; - - // 8. Deploy new LidoLocator - const newLocator = await deployImplementation(Sk.lidoLocator, "LidoLocator", deployer, [locatorConfig]); - log.success(`LidoLocator: ${newLocator.address}`); - - const updatedState = readNetworkState(); - persistNetworkState(updatedState); - - // 9. GateSeal for withdrawalQueueERC721 - const WQ_GATE_SEAL = await deployGateSeal( - updatedState, - deployer, - [updatedState[Sk.withdrawalQueueERC721].proxy.address], - GATE_SEAL_DURATION_SECONDS, - GATE_SEAL_EXPIRY_TIMESTAMP, - Sk.gateSeal, - ); - - // 10. GateSeal for Triggerable Withdrawals - const TW_GATE_SEAL = await deployGateSeal( - updatedState, - deployer, - [updatedState[Sk.triggerableWithdrawalsGateway].implementation.address, await locator.validatorsExitBusOracle()], - GATE_SEAL_DURATION_SECONDS, - GATE_SEAL_EXPIRY_TIMESTAMP, - Sk.gateSealTW, - ); - - // ----------------------------------------------------------------------- - // Governance summary - // ----------------------------------------------------------------------- - log.emptyLine(); - log(`Configuration for governance script:`); - log.emptyLine(); - log(`LIDO_LOCATOR_IMPL = "${newLocator.address}"`); - log(`ACCOUNTING_ORACLE_IMPL = "${accountingOracle.address}"`); - log(`VALIDATORS_EXIT_BUS_ORACLE_IMPL = "${validatorsExitBusOracle.address}"`); - log(`WITHDRAWAL_VAULT_IMPL = "${withdrawalVault.address}"`); - log(`STAKING_ROUTER_IMPL = "${stakingRouter.address}"`); - log(`NODE_OPERATORS_REGISTRY_IMPL = "${nor.address}"`); - log(`VALIDATOR_EXIT_DELAY_VERIFIER_IMPL = "${validatorExitDelayVerifier.address}"`); - log(`TRIGGERABLE_WITHDRAWALS_GATEWAY_IMPL = "${triggerableWithdrawalsGateway.address}"\n`); - log.emptyLine(); - log(`WQ_GATE_SEAL = "${WQ_GATE_SEAL}"`); - log(`TW_GATE_SEAL = "${TW_GATE_SEAL}"`); - log.emptyLine(); -} - -main().catch((error) => { - log.error(error); - process.exitCode = 1; -}); diff --git a/scripts/triggerable-withdrawals/tw-verify.ts.obsolete b/scripts/triggerable-withdrawals/tw-verify.ts.obsolete deleted file mode 100644 index fae50f443b..0000000000 --- a/scripts/triggerable-withdrawals/tw-verify.ts.obsolete +++ /dev/null @@ -1,92 +0,0 @@ -import * as dotenv from "dotenv"; -import { ethers, run } from "hardhat"; -import { join } from "path"; - -import { LidoLocator } from "typechain-types"; - -import { cy, loadContract, log, persistNetworkState, readNetworkState, Sk } from "lib"; - -dotenv.config({ path: join(__dirname, "../../.env") }); - -function getEnvVariable(name: string, defaultValue?: string) { - const value = process.env[name]; - if (value === undefined) { - if (defaultValue === undefined) { - throw new Error(`Env variable ${name} must be set`); - } - return defaultValue; - } else { - log(`Using env variable ${name}=${value}`); - return value; - } -} - -// Must comply with the specification -// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters-1 -const SECONDS_PER_SLOT = 12; - -// Must match the beacon chain genesis_time: https://beaconstate-mainnet.chainsafe.io/eth/v1/beacon/genesis -// and the current value: https://etherscan.io/address/0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb -const genesisTime = parseInt(getEnvVariable("GENESIS_TIME")); - -async function main() { - const chainId = (await ethers.provider.getNetwork()).chainId; - - log(cy(`Deploy of contracts on chain ${chainId}`)); - - const state = readNetworkState(); - persistNetworkState(state); - - // Read contracts addresses from config - const locator = await loadContract("LidoLocator", state[Sk.lidoLocator].proxy.address); - - const LIDO_PROXY = await locator.lido(); - const TREASURY_PROXY = await locator.treasury(); - - const validatorsExitBusOracleArgs = [SECONDS_PER_SLOT, genesisTime, locator.address]; - const withdrawalVaultArgs = [LIDO_PROXY, TREASURY_PROXY]; - const validatorExitDelayVerifierArgs = [ - locator.address, - "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorPrev, - "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorCurr, - "0x0000000000000000000000000000000000000000000000000000000000005b00", // GIndex gIHistoricalSummariesPrev, - "0x0000000000000000000000000000000000000000000000000000000000005b00", // GIndex gIHistoricalSummariesCurr, - 1, // uint64 firstSupportedSlot, - 1, // uint64 pivotSlot, - 32, // uint32 slotsPerEpoch, - 12, // uint32 secondsPerSlot, - genesisTime, // uint64 genesisTime, - 2 ** 8 * 32 * 12, // uint32 shardCommitteePeriodInSeconds - ]; - - await run("verify:verify", { - address: state[Sk.withdrawalVault].implementation.address, - constructorArguments: withdrawalVaultArgs, - contract: "contracts/0.8.9/WithdrawalVault.sol:WithdrawalVault", - }); - - await run("verify:verify", { - address: state[Sk.validatorsExitBusOracle].implementation.address, - constructorArguments: validatorsExitBusOracleArgs, - contract: "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol:ValidatorsExitBusOracle", - }); - - await run("verify:verify", { - address: state[Sk.validatorExitDelayVerifier].implementation.address, - constructorArguments: validatorExitDelayVerifierArgs, - contract: "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol:ValidatorsExitBusOracle", - }); - - await run("verify:verify", { - address: state[Sk.lidoLocator].implementation.address, - constructorArguments: [], // TBD - contract: "contracts/0.8.9/LidoLocator.sol:LidoLocator", - }); -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - log.error(error); - process.exit(1); - }); diff --git a/scripts/upgrade/steps/0500-mock-aragon-voting.ts b/scripts/upgrade/steps/0500-mock-aragon-voting.ts deleted file mode 100644 index df403cdb38..0000000000 --- a/scripts/upgrade/steps/0500-mock-aragon-voting.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { mockDGAragonVoting } from "scripts/utils/upgrade"; - -import { readNetworkState, Sk } from "lib/state-file"; - -export async function main(): Promise> { - const state = readNetworkState(); - const voteScriptAddress = state[Sk.twVoteScript].address; - const votingDescription = "TW Lido Upgrade description placeholder"; - const proposalMetadata = "TW Lido Upgrade proposal metadata placeholder"; - return mockDGAragonVoting(voteScriptAddress, votingDescription, proposalMetadata, state); -} diff --git a/scripts/upgrade/upgrade-params-mainnet.toml b/scripts/upgrade/upgrade-params-mainnet.toml index d384f889fa..518bbc680c 100644 --- a/scripts/upgrade/upgrade-params-mainnet.toml +++ b/scripts/upgrade/upgrade-params-mainnet.toml @@ -77,18 +77,6 @@ isMigrationAllowed = true # Must be on for the upgrade to work (for vebo_consensus_version = 4 # Validators Exit Bus Oracle consensus version ao_consensus_version = 4 # Accounting Oracle consensus version -# ================================================================================================ -# TRIGGERABLE WITHDRAWALS (TW) UPGRADE PARAMETERS -# ================================================================================================ -# All parameters related to the Triggerable Withdrawals upgrade are grouped here for easy access -# TW Gateway configuration for managing validator exit requests -[triggerableWithdrawalsGateway] -maxExitRequestsLimit = 13000 # Maximum number of exit requests that can be processed -exitsPerFrame = 1 # Number of exits processed per frame -frameDurationInSec = 48 # Duration of each processing frame in seconds - -# TW Exit management configuration -[triggerableWithdrawals] -exit_events_lookback_window_in_slots = 7200 # Lookback window for exit events in slots (1 day) -nor_exit_deadline_in_sec = 1800 # Node operator registry exit deadline in seconds (30 minutes) +# Sources (todo) +# - https://research.lido.fi/t/default-risk-assessment-framework-and-fees-parameters-for-lido-v3-stvaults/10504 diff --git a/tasks/validate-configs.ts b/tasks/validate-configs.ts index 4675139ca7..cf37a3ae7b 100644 --- a/tasks/validate-configs.ts +++ b/tasks/validate-configs.ts @@ -113,14 +113,6 @@ const EXPECTED_MISSING_IN_SCRATCH = [ path: "oracleVersions.ao_consensus_version", reason: "Oracle versions are upgrade-specific configuration", }, - { - path: "triggerableWithdrawals.exit_events_lookback_window_in_slots", - reason: "TODO", - }, - { - path: "triggerableWithdrawals.nor_exit_deadline_in_sec", - reason: "TODO", - }, ]; // Special mappings where the same concept has different names diff --git a/upgrade-parameters-mainnet.json b/upgrade-parameters-mainnet.json deleted file mode 100644 index 0af322a408..0000000000 --- a/upgrade-parameters-mainnet.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "chainSpec": { - "slotsPerEpoch": 32, - "secondsPerSlot": 12, - "genesisTime": null, - "depositContract": "0x00000000219ab540356cBB839Cbe05303d7705Fa" - } -} From a758468faf2c96f485a4a035ac855c2fa6cb05fb Mon Sep 17 00:00:00 2001 From: Artyom Veremeenko Date: Sat, 18 Oct 2025 12:44:04 +0300 Subject: [PATCH 04/13] feat(V3Template): make upgrade expiry timestamp configurable in ctor --- .github/workflows/tests-integration-mainnet.yml | 1 - .../workflows/tests-integration-upgrade-template.yml | 1 - contracts/upgrade/V3Template.sol | 12 +++++++----- lib/config-schemas.ts | 6 ++++++ .../steps/0200-deploy-v3-upgrading-contracts.ts | 5 ++++- scripts/upgrade/upgrade-params-mainnet.toml | 5 +++++ 6 files changed, 22 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests-integration-mainnet.yml b/.github/workflows/tests-integration-mainnet.yml index 19973389cc..97ceceb59f 100644 --- a/.github/workflows/tests-integration-mainnet.yml +++ b/.github/workflows/tests-integration-mainnet.yml @@ -41,7 +41,6 @@ jobs: GAS_PRIORITY_FEE: 1 GAS_MAX_FEE: 100 NETWORK_STATE_FILE: deployed-mainnet-upgrade.json - GENESIS_TIME: 1606824023 # needed only for TW upgrade - name: Mock Aragon voting run: yarn upgrade:mock-voting diff --git a/.github/workflows/tests-integration-upgrade-template.yml b/.github/workflows/tests-integration-upgrade-template.yml index e5bffe6f64..dabcb59202 100644 --- a/.github/workflows/tests-integration-upgrade-template.yml +++ b/.github/workflows/tests-integration-upgrade-template.yml @@ -26,4 +26,3 @@ jobs: env: RPC_URL: "${{ secrets.ETH_RPC_URL }}" UPGRADE_PARAMETERS_FILE: upgrade-parameters-mainnet.json - GENESIS_TIME: 1606824023 # needed only for TW upgrade diff --git a/contracts/upgrade/V3Template.sol b/contracts/upgrade/V3Template.sol index 56aa997561..5214caa12c 100644 --- a/contracts/upgrade/V3Template.sol +++ b/contracts/upgrade/V3Template.sol @@ -91,10 +91,10 @@ contract V3Template is V3Addresses { bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; - // Timestamp since startUpgrade() and finishUpgrade() revert with Expired() - // This behavior is introduced to disarm the template if the upgrade voting creation or enactment didn't - // happen in proper time period - uint256 public constant EXPIRE_SINCE_INCLUSIVE = 1761868800; // 2025-10-31 00:00:00 UTC + // Timestamp since which startUpgrade() + // This behavior is introduced to disarm the template if the upgrade voting creation or enactment + // didn't happen in proper time period + uint256 public immutable EXPIRE_SINCE_INCLUSIVE; // Initial value of upgradeBlockNumber storage variable uint256 public constant UPGRADE_NOT_STARTED = 0; @@ -123,7 +123,9 @@ contract V3Template is V3Addresses { /// @param _params Params required to initialize the addresses contract - constructor(V3AddressesParams memory _params) V3Addresses(_params) { + /// @param _expireSinceInclusive Unix timestamp after which upgrade actions revert + constructor(V3AddressesParams memory _params, uint256 _expireSinceInclusive) V3Addresses(_params) { + EXPIRE_SINCE_INCLUSIVE = _expireSinceInclusive; contractsWithBurnerAllowances.push(WITHDRAWAL_QUEUE); // NB: NOR and SIMPLE_DVT allowances are set to 0 in TW upgrade, so they are not migrated contractsWithBurnerAllowances.push(CSM_ACCOUNTING); diff --git a/lib/config-schemas.ts b/lib/config-schemas.ts index ddef3640a4..5acc41c3bd 100644 --- a/lib/config-schemas.ts +++ b/lib/config-schemas.ts @@ -85,6 +85,11 @@ const OracleVersionsSchema = z.object({ ao_consensus_version: PositiveIntSchema, }); +// V3 vote script params +const V3VoteScriptSchema = z.object({ + expiryTimestamp: NonNegativeIntSchema, +}); + // Aragon app versions schema const AragonAppVersionsSchema = z.object({ nor_version: z.array(z.number()).length(3), @@ -117,6 +122,7 @@ export const UpgradeParametersSchema = z.object({ burner: BurnerSchema, oracleVersions: OracleVersionsSchema.optional(), aragonAppVersions: AragonAppVersionsSchema.optional(), + v3VoteScript: V3VoteScriptSchema, }); // Gate seal schema (for scratch deployment) diff --git a/scripts/upgrade/steps/0200-deploy-v3-upgrading-contracts.ts b/scripts/upgrade/steps/0200-deploy-v3-upgrading-contracts.ts index 0b296467ae..f63b930f23 100644 --- a/scripts/upgrade/steps/0200-deploy-v3-upgrading-contracts.ts +++ b/scripts/upgrade/steps/0200-deploy-v3-upgrading-contracts.ts @@ -53,7 +53,10 @@ export async function main() { getAddress(Sk.aragonAcl, state), ]; - const template = await deployWithoutProxy(Sk.v3Template, "V3Template", deployer, [addressesParams]); + const template = await deployWithoutProxy(Sk.v3Template, "V3Template", deployer, [ + addressesParams, + parameters.v3VoteScript.expiryTimestamp, + ]); await deployWithoutProxy(Sk.v3VoteScript, "V3VoteScript", deployer, [ [template.address, state[Sk.appLido].aragonApp.id], diff --git a/scripts/upgrade/upgrade-params-mainnet.toml b/scripts/upgrade/upgrade-params-mainnet.toml index 518bbc680c..4777e6d19d 100644 --- a/scripts/upgrade/upgrade-params-mainnet.toml +++ b/scripts/upgrade/upgrade-params-mainnet.toml @@ -77,6 +77,11 @@ isMigrationAllowed = true # Must be on for the upgrade to work (for vebo_consensus_version = 4 # Validators Exit Bus Oracle consensus version ao_consensus_version = 4 # Accounting Oracle consensus version +[v3VoteScript] +# Expiry timestamp after which the upgrade transaction will revert +# Format: Unix timestamp (seconds since epoch) +# The upgrade transaction must be executed before this deadline +expiryTimestamp = 1765324800 # December 10, 2025 00:00:00 UTC # Sources (todo) # - https://research.lido.fi/t/default-risk-assessment-framework-and-fees-parameters-for-lido-v3-stvaults/10504 From 4199bbdfa2d891e0eebaaf8d67bdee25feea7103 Mon Sep 17 00:00:00 2001 From: Artyom Veremeenko Date: Sat, 18 Oct 2025 13:10:42 +0300 Subject: [PATCH 05/13] feat(upgrade): add missing OracleDaemonConfig parameters to V3VoteScript --- contracts/upgrade/V3Addresses.sol | 3 +- contracts/upgrade/V3VoteScript.sol | 64 ++++++++++++++++++++++-------- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/contracts/upgrade/V3Addresses.sol b/contracts/upgrade/V3Addresses.sol index 8ed4f7c623..97c7823d8b 100644 --- a/contracts/upgrade/V3Addresses.sol +++ b/contracts/upgrade/V3Addresses.sol @@ -127,6 +127,7 @@ contract V3Addresses { address public immutable NODE_OPERATORS_REGISTRY; address public immutable SIMPLE_DVT; address public immutable CSM_ACCOUNTING; + address public immutable ORACLE_DAEMON_CONFIG; constructor( V3AddressesParams memory params @@ -159,7 +160,6 @@ contract V3Addresses { GATE_SEAL = params.gateSealForVaults; EVM_SCRIPT_EXECUTOR = params.evmScriptExecutor; VAULTS_ADAPTER = params.vaultsAdapter; - // // Discovered via other contracts // @@ -183,6 +183,7 @@ contract V3Addresses { VALIDATORS_EXIT_BUS_ORACLE = newLocatorImpl.validatorsExitBusOracle(); WITHDRAWAL_QUEUE = newLocatorImpl.withdrawalQueue(); WSTETH = newLocatorImpl.wstETH(); + ORACLE_DAEMON_CONFIG = newLocatorImpl.oracleDaemonConfig(); { // Retrieve contracts with burner allowances to migrate: NOR, SDVT and CSM ACCOUNTING diff --git a/contracts/upgrade/V3VoteScript.sol b/contracts/upgrade/V3VoteScript.sol index a886bde5c1..7d84e99328 100644 --- a/contracts/upgrade/V3VoteScript.sol +++ b/contracts/upgrade/V3VoteScript.sol @@ -14,6 +14,11 @@ interface IKernel { function APP_BASES_NAMESPACE() external view returns (bytes32); } +interface IOracleDaemonConfig { + function CONFIG_MANAGER_ROLE() external view returns (bytes32); + function set(string calldata _key, bytes calldata _value) external; +} + interface IStakingRouter { function REPORT_REWARDS_MINTED_ROLE() external view returns (bytes32); } @@ -30,7 +35,7 @@ contract V3VoteScript is OmnibusBase { // // Constants // - uint256 public constant VOTE_ITEMS_COUNT = 13; + uint256 public constant VOTE_ITEMS_COUNT = 17; // // Immutables @@ -58,21 +63,18 @@ contract V3VoteScript is OmnibusBase { voteItems = new VoteItem[](VOTE_ITEMS_COUNT); uint256 index = 0; - // Start the upgrade process voteItems[index++] = VoteItem({ description: "1. Call UpgradeTemplateV3.startUpgrade", call: _forwardCall(TEMPLATE.AGENT(), params.upgradeTemplate, abi.encodeCall(V3Template.startUpgrade, ())) }); - // Upgrade LidoLocator implementation voteItems[index++] = VoteItem({ description: "2. Upgrade LidoLocator implementation", call: _forwardCall(TEMPLATE.AGENT(), TEMPLATE.LOCATOR(), abi.encodeCall(IOssifiableProxy.proxy__upgradeTo, (TEMPLATE.NEW_LOCATOR_IMPL()))) }); - // Grant APP_MANAGER_ROLE to the AGENT voteItems[index++] = VoteItem({ - description: "3. Grant APP_MANAGER_ROLE to the AGENT", + description: "3. Grant Aragon APP_MANAGER_ROLE to the AGENT", call: _forwardCall( TEMPLATE.AGENT(), TEMPLATE.ACL(), @@ -85,7 +87,6 @@ contract V3VoteScript is OmnibusBase { ) }); - // Set Lido implementation in Kernel voteItems[index++] = VoteItem({ description: "4. Set Lido implementation in Kernel", call: _forwardCall( @@ -95,9 +96,8 @@ contract V3VoteScript is OmnibusBase { ) }); - // Revoke APP_MANAGER_ROLE from the AGENT on Kernel ACL voteItems[index++] = VoteItem({ - description: "5. Revoke APP_MANAGER_ROLE from the AGENT", + description: "5. Revoke Aragon APP_MANAGER_ROLE from the AGENT", call: _forwardCall( TEMPLATE.AGENT(), TEMPLATE.ACL(), @@ -110,7 +110,6 @@ contract V3VoteScript is OmnibusBase { ) }); - // Revoke REQUEST_BURN_SHARES_ROLE from Lido bytes32 requestBurnSharesRole = IBurner(TEMPLATE.OLD_BURNER()).REQUEST_BURN_SHARES_ROLE(); voteItems[index++] = VoteItem({ description: "6. Revoke REQUEST_BURN_SHARES_ROLE from Lido", @@ -121,7 +120,6 @@ contract V3VoteScript is OmnibusBase { ) }); - // Revoke REQUEST_BURN_SHARES_ROLE from Curated staking modules (NodeOperatorsRegistry) voteItems[index++] = VoteItem({ description: "7. Revoke REQUEST_BURN_SHARES_ROLE from Curated staking module", call: _forwardCall( @@ -131,7 +129,6 @@ contract V3VoteScript is OmnibusBase { ) }); - // Revoke REQUEST_BURN_SHARES_ROLE from SimpleDVT voteItems[index++] = VoteItem({ description: "8. Revoke REQUEST_BURN_SHARES_ROLE from SimpleDVT", call: _forwardCall( @@ -141,7 +138,6 @@ contract V3VoteScript is OmnibusBase { ) }); - // Revoke REQUEST_BURN_SHARES_ROLE from CS Accounting voteItems[index++] = VoteItem({ description: "9. Revoke REQUEST_BURN_SHARES_ROLE from Community Staking Accounting", call: _forwardCall( @@ -151,7 +147,6 @@ contract V3VoteScript is OmnibusBase { ) }); - // Upgrade AccountingOracle implementation voteItems[index++] = VoteItem({ description: "10. Upgrade AccountingOracle implementation", call: _forwardCall( @@ -161,7 +156,6 @@ contract V3VoteScript is OmnibusBase { ) }); - // Revoke REPORT_REWARDS_MINTED_ROLE from Lido bytes32 reportRewardsMintedRole = IStakingRouter(TEMPLATE.STAKING_ROUTER()).REPORT_REWARDS_MINTED_ROLE(); voteItems[index++] = VoteItem({ description: "11. Revoke REPORT_REWARDS_MINTED_ROLE from Lido", @@ -172,7 +166,6 @@ contract V3VoteScript is OmnibusBase { ) }); - // Grant REPORT_REWARDS_MINTED_ROLE to Accounting voteItems[index++] = VoteItem({ description: "12. Grant REPORT_REWARDS_MINTED_ROLE to Accounting", call: _forwardCall( @@ -182,9 +175,46 @@ contract V3VoteScript is OmnibusBase { ) }); - // Finish the upgrade process + bytes32 configManagerRole = IOracleDaemonConfig(TEMPLATE.ORACLE_DAEMON_CONFIG()).CONFIG_MANAGER_ROLE(); + + voteItems[index++] = VoteItem({ + description: "13. Grant OracleDaemonConfig's CONFIG_MANAGER_ROLE to Agent", + call: _forwardCall( + TEMPLATE.AGENT(), + TEMPLATE.ORACLE_DAEMON_CONFIG(), + abi.encodeCall(IAccessControl.grantRole, (configManagerRole, TEMPLATE.AGENT())) + ) + }); + + voteItems[index++] = VoteItem({ + description: "14. Set SLASHING_RESERVE_WE_RIGHT_SHIFT to 0x2000 at OracleDaemonConfig", + call: _forwardCall( + TEMPLATE.AGENT(), + TEMPLATE.ORACLE_DAEMON_CONFIG(), + abi.encodeCall(IOracleDaemonConfig.set, ("SLASHING_RESERVE_WE_RIGHT_SHIFT", abi.encode(0x2000))) + ) + }); + + voteItems[index++] = VoteItem({ + description: "15. Set SLASHING_RESERVE_WE_LEFT_SHIFT to 0x2000 at OracleDaemonConfig", + call: _forwardCall( + TEMPLATE.AGENT(), + TEMPLATE.ORACLE_DAEMON_CONFIG(), + abi.encodeCall(IOracleDaemonConfig.set, ("SLASHING_RESERVE_WE_LEFT_SHIFT", abi.encode(0x2000))) + ) + }); + + voteItems[index++] = VoteItem({ + description: "16. Revoke OracleDaemonConfig's CONFIG_MANAGER_ROLE from Agent", + call: _forwardCall( + TEMPLATE.AGENT(), + TEMPLATE.ORACLE_DAEMON_CONFIG(), + abi.encodeCall(IAccessControl.revokeRole, (configManagerRole, TEMPLATE.AGENT())) + ) + }); + voteItems[index++] = VoteItem({ - description: "13. Call UpgradeTemplateV3.finishUpgrade", + description: "17. Call UpgradeTemplateV3.finishUpgrade", call: _forwardCall(TEMPLATE.AGENT(), params.upgradeTemplate, abi.encodeCall(V3Template.finishUpgrade, ())) }); From 47ebd49233f078478830e78040519d17e526086c Mon Sep 17 00:00:00 2001 From: Artyom Veremeenko Date: Sat, 18 Oct 2025 14:26:23 +0300 Subject: [PATCH 06/13] refactor: remove interfaces in return values of external function --- contracts/0.4.24/Lido.sol | 4 ++-- contracts/0.8.25/vaults/StakingVault.sol | 6 +++--- contracts/0.8.25/vaults/VaultFactory.sol | 12 ++++++------ contracts/0.8.25/vaults/dashboard/Dashboard.sol | 2 +- contracts/0.8.25/vaults/dashboard/Permissions.sol | 4 ++-- contracts/0.8.25/vaults/interfaces/IStakingVault.sol | 3 +-- tasks/validate-configs.ts | 4 ++++ .../StakingVault__HarnessForTestUpgrade.sol | 4 ++-- .../contracts/StakingVault__MockForPDG.sol | 2 +- 9 files changed, 22 insertions(+), 19 deletions(-) diff --git a/contracts/0.4.24/Lido.sol b/contracts/0.4.24/Lido.sol index c670a1597a..84d9d3a71e 100644 --- a/contracts/0.4.24/Lido.sol +++ b/contracts/0.4.24/Lido.sol @@ -586,8 +586,8 @@ contract Lido is Versioned, StETHPermit, AragonApp { /** * @return the Lido Locator address */ - function getLidoLocator() external view returns (ILidoLocator) { - return _getLidoLocator(); + function getLidoLocator() external view returns (address) { + return address(_getLidoLocator()); } /** diff --git a/contracts/0.8.25/vaults/StakingVault.sol b/contracts/0.8.25/vaults/StakingVault.sol index 7f80bfb8bd..1e097d3748 100644 --- a/contracts/0.8.25/vaults/StakingVault.sol +++ b/contracts/0.8.25/vaults/StakingVault.sol @@ -67,7 +67,7 @@ contract StakingVault is IStakingVault, Ownable2StepUpgradeable { * @notice Address of `BeaconChainDepositContract` * Set immutably in the constructor to avoid storage costs */ - IDepositContract public immutable DEPOSIT_CONTRACT; + address public immutable DEPOSIT_CONTRACT; /* * ╔══════════════════════════════════════════════════╗ @@ -104,7 +104,7 @@ contract StakingVault is IStakingVault, Ownable2StepUpgradeable { */ constructor(address _beaconChainDepositContract) { if (_beaconChainDepositContract == address(0)) revert ZeroArgument("_beaconChainDepositContract"); - DEPOSIT_CONTRACT = IDepositContract(_beaconChainDepositContract); + DEPOSIT_CONTRACT = _beaconChainDepositContract; _disableInitializers(); } @@ -536,7 +536,7 @@ contract StakingVault is IStakingVault, Ownable2StepUpgradeable { uint256 balance = availableBalance(); if (_deposit.amount > balance) revert InsufficientBalance(balance, _deposit.amount); - DEPOSIT_CONTRACT.deposit{value: _deposit.amount}( + IDepositContract(DEPOSIT_CONTRACT).deposit{value: _deposit.amount}( _deposit.pubkey, _withdrawalCredentials, _deposit.signature, diff --git a/contracts/0.8.25/vaults/VaultFactory.sol b/contracts/0.8.25/vaults/VaultFactory.sol index d786cd7837..b321cda157 100644 --- a/contracts/0.8.25/vaults/VaultFactory.sol +++ b/contracts/0.8.25/vaults/VaultFactory.sol @@ -63,20 +63,20 @@ contract VaultFactory { uint256 _nodeOperatorFeeBP, uint256 _confirmExpiry, Permissions.RoleAssignment[] calldata _roleAssignments - ) external payable returns (IStakingVault vault, Dashboard dashboard) { + ) external payable returns (address vault, Dashboard dashboard) { // check if the msg.value is enough to cover the connect deposit ILidoLocator locator = ILidoLocator(LIDO_LOCATOR); if (msg.value < VaultHub(payable(locator.vaultHub())).CONNECT_DEPOSIT()) revert InsufficientFunds(); // create the vault proxy - vault = IStakingVault(_deployVault()); + vault = _deployVault(); // create the dashboard proxy bytes memory immutableArgs = abi.encode(address(vault)); dashboard = Dashboard(payable(Clones.cloneWithImmutableArgs(DASHBOARD_IMPL, immutableArgs))); // initialize StakingVault with the dashboard address as the owner - vault.initialize(address(dashboard), _nodeOperator, locator.predepositGuarantee()); + IStakingVault(vault).initialize(address(dashboard), _nodeOperator, locator.predepositGuarantee()); // initialize Dashboard with the factory address as the default admin, grant optional roles and connect to VaultHub dashboard.initialize(address(this), _nodeOperatorManager, _nodeOperatorManager, _nodeOperatorFeeBP, _confirmExpiry); @@ -111,18 +111,18 @@ contract VaultFactory { uint256 _nodeOperatorFeeBP, uint256 _confirmExpiry, Permissions.RoleAssignment[] calldata _roleAssignments - ) external returns (IStakingVault vault, Dashboard dashboard) { + ) external returns (address vault, Dashboard dashboard) { ILidoLocator locator = ILidoLocator(LIDO_LOCATOR); // create the vault proxy - vault = IStakingVault(_deployVault()); + vault = _deployVault(); // create the dashboard proxy bytes memory immutableArgs = abi.encode(address(vault)); dashboard = Dashboard(payable(Clones.cloneWithImmutableArgs(DASHBOARD_IMPL, immutableArgs))); // initialize StakingVault with the dashboard address as the owner - vault.initialize(address(dashboard), _nodeOperator, locator.predepositGuarantee()); + IStakingVault(vault).initialize(address(dashboard), _nodeOperator, locator.predepositGuarantee()); // initialize Dashboard with the _defaultAdmin as the default admin, grant optional node operator managed roles dashboard.initialize(_defaultAdmin, address(this), _nodeOperatorManager, _nodeOperatorFeeBP, _confirmExpiry); diff --git a/contracts/0.8.25/vaults/dashboard/Dashboard.sol b/contracts/0.8.25/vaults/dashboard/Dashboard.sol index 298a3ad094..980d7a0de2 100644 --- a/contracts/0.8.25/vaults/dashboard/Dashboard.sol +++ b/contracts/0.8.25/vaults/dashboard/Dashboard.sol @@ -439,7 +439,7 @@ contract Dashboard is NodeOperatorFee { if (pdgPolicy != PDGPolicy.ALLOW_DEPOSIT_AND_PROVE) revert ForbiddenByPDGPolicy(); IStakingVault stakingVault_ = _stakingVault(); - IDepositContract depositContract = stakingVault_.DEPOSIT_CONTRACT(); + IDepositContract depositContract = IDepositContract(stakingVault_.DEPOSIT_CONTRACT()); for (uint256 i = 0; i < _deposits.length; i++) { totalAmount += _deposits[i].amount; diff --git a/contracts/0.8.25/vaults/dashboard/Permissions.sol b/contracts/0.8.25/vaults/dashboard/Permissions.sol index 1cf316ea0b..82d97c7d55 100644 --- a/contracts/0.8.25/vaults/dashboard/Permissions.sol +++ b/contracts/0.8.25/vaults/dashboard/Permissions.sol @@ -148,8 +148,8 @@ abstract contract Permissions is AccessControlConfirmable { * @notice Returns the address of the underlying StakingVault. * @return The address of the StakingVault. */ - function stakingVault() external view returns (IStakingVault) { - return _stakingVault(); + function stakingVault() external view returns (address) { + return address(_stakingVault()); } // ==================== Role Management Functions ==================== diff --git a/contracts/0.8.25/vaults/interfaces/IStakingVault.sol b/contracts/0.8.25/vaults/interfaces/IStakingVault.sol index 4b5e086b27..81b24ea856 100644 --- a/contracts/0.8.25/vaults/interfaces/IStakingVault.sol +++ b/contracts/0.8.25/vaults/interfaces/IStakingVault.sol @@ -5,7 +5,6 @@ // solhint-disable-next-line lido/fixed-compiler-version pragma solidity >=0.8.0; -import {IDepositContract} from "contracts/common/interfaces/IDepositContract.sol"; /** * @title IStakingVault @@ -28,7 +27,7 @@ interface IStakingVault { bytes32 depositDataRoot; } - function DEPOSIT_CONTRACT() external view returns (IDepositContract); + function DEPOSIT_CONTRACT() external view returns (address); function initialize(address _owner, address _nodeOperator, address _depositor) external; function version() external pure returns (uint64); function getInitializedVersion() external view returns (uint64); diff --git a/tasks/validate-configs.ts b/tasks/validate-configs.ts index cf37a3ae7b..199bf8f007 100644 --- a/tasks/validate-configs.ts +++ b/tasks/validate-configs.ts @@ -113,6 +113,10 @@ const EXPECTED_MISSING_IN_SCRATCH = [ path: "oracleVersions.ao_consensus_version", reason: "Oracle versions are upgrade-specific configuration", }, + { + path: "v3VoteScript.expiryTimestamp", + reason: "V3 vote script expiry timestamp is upgrade-specific configuration", + }, ]; // Special mappings where the same concept has different names diff --git a/test/0.8.25/vaults/contracts/StakingVault__HarnessForTestUpgrade.sol b/test/0.8.25/vaults/contracts/StakingVault__HarnessForTestUpgrade.sol index 9e54732e8a..db5fed7b67 100644 --- a/test/0.8.25/vaults/contracts/StakingVault__HarnessForTestUpgrade.sol +++ b/test/0.8.25/vaults/contracts/StakingVault__HarnessForTestUpgrade.sol @@ -25,7 +25,7 @@ contract StakingVault__HarnessForTestUpgrade is IStakingVault, Ownable2StepUpgra */ uint64 private constant _VERSION = 2; - IDepositContract public immutable DEPOSIT_CONTRACT; + address public immutable DEPOSIT_CONTRACT; bytes32 private constant ERC7201_STORAGE_LOCATION = 0x2ec50241a851d8d3fea472e7057288d4603f7a7f78e6d18a9c12cad84552b100; @@ -33,7 +33,7 @@ contract StakingVault__HarnessForTestUpgrade is IStakingVault, Ownable2StepUpgra constructor(address _beaconChainDepositContract) { if (_beaconChainDepositContract == address(0)) revert ZeroArgument("_beaconChainDepositContract"); - DEPOSIT_CONTRACT = IDepositContract(_beaconChainDepositContract); + DEPOSIT_CONTRACT = _beaconChainDepositContract; // Prevents reinitialization of the implementation _disableInitializers(); diff --git a/test/0.8.25/vaults/predepositGuarantee/contracts/StakingVault__MockForPDG.sol b/test/0.8.25/vaults/predepositGuarantee/contracts/StakingVault__MockForPDG.sol index 42f0990c86..840777dbc5 100644 --- a/test/0.8.25/vaults/predepositGuarantee/contracts/StakingVault__MockForPDG.sol +++ b/test/0.8.25/vaults/predepositGuarantee/contracts/StakingVault__MockForPDG.sol @@ -52,7 +52,7 @@ contract StakingVault__MockForPDG is IStakingVault { withdrawalCredentials_ = _withdrawalCredentials; } - function DEPOSIT_CONTRACT() external view override returns (IDepositContract) {} + function DEPOSIT_CONTRACT() external view override returns (address) {} function initialize(address _owner, address _nodeOperator, address _depositor) external override {} From 8869a3af8139ffa81f21156f88a45e9620b622b8 Mon Sep 17 00:00:00 2001 From: Artyom Veremeenko Date: Sat, 18 Oct 2025 14:48:31 +0300 Subject: [PATCH 07/13] fix: incorrect outdated interface in V3TemporaryAdmin --- contracts/0.8.25/utils/V3TemporaryAdmin.sol | 13 ++++++++----- contracts/0.8.25/vaults/VaultFactory.sol | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/contracts/0.8.25/utils/V3TemporaryAdmin.sol b/contracts/0.8.25/utils/V3TemporaryAdmin.sol index 6d42360e8a..a797053d60 100644 --- a/contracts/0.8.25/utils/V3TemporaryAdmin.sol +++ b/contracts/0.8.25/utils/V3TemporaryAdmin.sol @@ -13,7 +13,7 @@ interface IVaultHub { function BAD_DEBT_MASTER_ROLE() external view returns (bytes32); } -interface IPausableUntil { +interface IPausableUntilWithRoles { function PAUSE_ROLE() external view returns (bytes32); } @@ -39,15 +39,18 @@ interface IStakingRouter { address stakingModuleAddress; uint16 stakingModuleFee; uint16 treasuryFee; - uint16 targetShare; + uint16 stakeShareLimit; uint8 status; string name; uint64 lastDepositAt; uint256 lastDepositBlock; uint256 exitedValidatorsCount; + uint16 priorityExitShareThreshold; + uint64 maxDepositsPerBlock; + uint64 minDepositBlockDistance; } - function getStakingModules() external view returns (StakingModule[] memory); + function getStakingModules() external view returns (StakingModule[] memory res); } interface ICSModule { @@ -141,7 +144,7 @@ contract V3TemporaryAdmin { */ function _setupVaultHub(address _vaultHub, address _vaultsAdapter) private { // Get roles from the contract - bytes32 pauseRole = IPausableUntil(_vaultHub).PAUSE_ROLE(); + bytes32 pauseRole = IPausableUntilWithRoles(_vaultHub).PAUSE_ROLE(); bytes32 vaultMasterRole = IVaultHub(_vaultHub).VAULT_MASTER_ROLE(); bytes32 redemptionMasterRole = IVaultHub(_vaultHub).REDEMPTION_MASTER_ROLE(); bytes32 validatorExitRole = IVaultHub(_vaultHub).VALIDATOR_EXIT_ROLE(); @@ -164,7 +167,7 @@ contract V3TemporaryAdmin { * @param _predepositGuarantee The PredepositGuarantee contract address */ function _setupPredepositGuarantee(address _predepositGuarantee) private { - bytes32 pauseRole = IPausableUntil(_predepositGuarantee).PAUSE_ROLE(); + bytes32 pauseRole = IPausableUntilWithRoles(_predepositGuarantee).PAUSE_ROLE(); IAccessControl(_predepositGuarantee).grantRole(pauseRole, GATE_SEAL); _transferAdminToAgent(_predepositGuarantee); } diff --git a/contracts/0.8.25/vaults/VaultFactory.sol b/contracts/0.8.25/vaults/VaultFactory.sol index b321cda157..3432ef55ca 100644 --- a/contracts/0.8.25/vaults/VaultFactory.sol +++ b/contracts/0.8.25/vaults/VaultFactory.sol @@ -82,7 +82,7 @@ contract VaultFactory { dashboard.initialize(address(this), _nodeOperatorManager, _nodeOperatorManager, _nodeOperatorFeeBP, _confirmExpiry); dashboard.connectToVaultHub{value: msg.value}(0); - + // _roleAssignments can only include DEFAULT_ADMIN_ROLE's subroles, // which is why it's important to revoke the NODE_OPERATOR_MANAGER_ROLE BEFORE granting roles if (_roleAssignments.length > 0) dashboard.grantRoles(_roleAssignments); From 55a4429637524c0d98867292ade36c52151c95ef Mon Sep 17 00:00:00 2001 From: Artyom Veremeenko Date: Sat, 18 Oct 2025 15:47:33 +0300 Subject: [PATCH 08/13] fix: improve check-interfaces.ts - now all interfaces checks pass - add capability to skip specific signatures for given interface - consider contract's predecessors functions as well --- tasks/check-interfaces.ts | 178 ++++++++++++++++++++++++++++++++++---- 1 file changed, 160 insertions(+), 18 deletions(-) diff --git a/tasks/check-interfaces.ts b/tasks/check-interfaces.ts index f52ba64a80..726de62595 100644 --- a/tasks/check-interfaces.ts +++ b/tasks/check-interfaces.ts @@ -1,7 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import { Interface } from "ethers"; import { task } from "hardhat/config"; const SKIP_NAMES_REGEX = /(^@|Mock|Harness|deposit_contract|build-info|^test)/; @@ -10,6 +9,7 @@ const PAIRS_TO_SKIP: { interfaceFqn: string; contractFqn: string; reason: string; + skipInterfaceSignatures?: string[]; }[] = [ { interfaceFqn: "contracts/0.4.24/Lido.sol:IOracleReportSanityChecker", @@ -36,6 +36,23 @@ const PAIRS_TO_SKIP: { contractFqn: "contracts/0.4.24/StETH.sol:StETH", reason: "Fixing requires WithdrawalQueue redeploy", }, + { + interfaceFqn: "contracts/0.8.25/vaults/dashboard/Dashboard.sol:IWstETH", + contractFqn: "contracts/0.6.12/WstETH.sol:WstETH", + reason: "Cannot redeploy WstETH", + }, + { + interfaceFqn: "contracts/0.8.9/Burner.sol:ILido", + contractFqn: "contracts/0.4.24/Lido.sol:Lido", + reason: "Parameter name mismatches - fixing requires Burner redeploy", + skipInterfaceSignatures: [ + "function allowance(address owner, address spender) returns (uint256)", + "function approve(address spender, uint256 amount) returns (bool)", + "function balanceOf(address account) returns (uint256)", + "function transfer(address recipient, uint256 amount) returns (bool)", + "function transferFrom(address sender, address recipient, uint256 amount) returns (bool)", + ], + }, ]; task("check-interfaces").setAction(async (_, hre) => { @@ -45,6 +62,7 @@ task("check-interfaces").setAction(async (_, hre) => { missingInContract: string[]; missingInInterface: string[]; isFullMatchExpected: boolean; + parameterNameMismatches: string[]; }[] = []; console.log("Checking interfaces defined within contracts..."); @@ -202,7 +220,7 @@ task("check-interfaces").setAction(async (_, hre) => { (pair.interfaceFqn === interfaceFqn && pair.contractFqn === correspondingContractFqn) || (pair.interfaceFqn === correspondingContractFqn && pair.contractFqn === interfaceFqn), ); - if (skipPair) { + if (skipPair && !skipPair.skipInterfaceSignatures) { console.log(`ℹ️ skipping '${interfaceFqn}' and '${correspondingContractFqn}' (${skipPair.reason})`); continue; } @@ -212,32 +230,140 @@ task("check-interfaces").setAction(async (_, hre) => { const interfaceAbi = (await hre.artifacts.readArtifact(interfaceFqn)).abi; const contractAbi = (await hre.artifacts.readArtifact(correspondingContractFqn)).abi; - const interfaceSignatures = new Interface(interfaceAbi) - .format() - .filter((entry) => !entry.startsWith("constructor(")) - .sort(); + // Helper function to get function signatures with parameter names for strict comparison + function getFunctionSignaturesWithNames( + abi: Array<{ + type: string; + name: string; + inputs: Array<{ type: string; name: string }>; + outputs?: Array<{ type: string }>; + }>, + ): string[] { + return abi + .filter((item) => item.type === "function") + .map((func) => { + const inputs = func.inputs.map((input) => `${input.type} ${input.name}`).join(", "); + const outputs = func.outputs ? ` returns (${func.outputs.map((output) => output.type).join(", ")})` : ""; + return `function ${func.name}(${inputs})${outputs}`; + }) + .sort(); + } - const contractSignatures = new Interface(contractAbi) - .format() - .filter((entry) => !entry.startsWith("constructor(")) - .sort(); + // Helper function to get function signatures without parameter names for basic compatibility check + function getFunctionSignaturesWithoutNames( + abi: Array<{ + type: string; + name: string; + inputs: Array<{ type: string }>; + outputs?: Array<{ type: string }>; + }>, + ): string[] { + return abi + .filter((item) => item.type === "function") + .map((func) => { + const inputs = func.inputs.map((input) => input.type).join(","); + const outputs = func.outputs ? ` returns (${func.outputs.map((output) => output.type).join(",")})` : ""; + return `function ${func.name}(${inputs})${outputs}`; + }) + .sort(); + } - // Find entries in interface ABI that are missing from contract ABI - const missingInContract = interfaceSignatures.filter((ifaceEntry) => !contractSignatures.includes(ifaceEntry)); + const interfaceSignaturesWithNames = getFunctionSignaturesWithNames(interfaceAbi); + const contractSignaturesWithNames = getFunctionSignaturesWithNames(contractAbi); + const interfaceSignaturesWithoutNames = getFunctionSignaturesWithoutNames(interfaceAbi); + const contractSignaturesWithoutNames = getFunctionSignaturesWithoutNames(contractAbi); + + // Validate that skipped signatures actually exist in the interface + if (skipPair?.skipInterfaceSignatures && skipPair.skipInterfaceSignatures.length > 0) { + const invalidSignatures = skipPair.skipInterfaceSignatures.filter( + (sig) => !interfaceSignaturesWithNames.includes(sig), + ); + if (invalidSignatures.length > 0) { + console.error( + `❌ Invalid signatures in skipInterfaceSignatures for '${interfaceFqn}' and '${correspondingContractFqn}':`, + ); + invalidSignatures.forEach((sig) => { + console.error(` ${sig}`); + }); + console.error(`Available signatures in interface:`); + interfaceSignaturesWithNames.forEach((sig) => { + console.error(` ${sig}`); + }); + console.error(); + process.exit(1); + } + } + + // Find entries in interface ABI that are missing from contract ABI (by signature only) + const missingInContractBySignature = interfaceSignaturesWithoutNames.filter( + (ifaceEntry) => !contractSignaturesWithoutNames.includes(ifaceEntry), + ); - // Find entries in contract ABI that are missing from interface ABI - const missingInInterface = contractSignatures.filter( - (contractEntry) => !interfaceSignatures.includes(contractEntry), + // Find entries in contract ABI that are missing from interface ABI (by signature only) + const missingInInterfaceBySignature = contractSignaturesWithoutNames.filter( + (contractEntry) => !interfaceSignaturesWithoutNames.includes(contractEntry), ); + // Find parameter name mismatches (functions that exist in both but have different parameter names) + const parameterNameMismatches: string[] = []; + for (const ifaceSig of interfaceSignaturesWithNames) { + // Check if this signature should be skipped + if (skipPair?.skipInterfaceSignatures?.includes(ifaceSig)) { + continue; + } + + // Extract function signature without parameter names for matching + const ifaceSigWithoutNames = ifaceSig.replace(/\(([^)]+)\)/, (match, params) => { + const paramList = params + .split(", ") + .map((param: string) => { + const parts = param.trim().split(" "); + return parts[0]; // Keep only the type part + }) + .join(", "); + return `(${paramList})`; + }); + + const matchingContractSig = contractSignaturesWithNames.find((contractSig) => { + const contractSigWithoutNames = contractSig.replace(/\(([^)]+)\)/, (match, params) => { + const paramList = params + .split(", ") + .map((param: string) => { + const parts = param.trim().split(" "); + return parts[0]; // Keep only the type part + }) + .join(", "); + return `(${paramList})`; + }); + return contractSigWithoutNames === ifaceSigWithoutNames; + }); + + if (matchingContractSig && ifaceSig !== matchingContractSig) { + parameterNameMismatches.push(`Interface: ${ifaceSig}`); + parameterNameMismatches.push(`Contract: ${matchingContractSig}`); + parameterNameMismatches.push(""); // Empty line for readability + } + } + + // Use the signature-based comparison for basic compatibility + const missingInContract = missingInContractBySignature; + const missingInInterface = missingInInterfaceBySignature; + // // Determine if full match is expected (interface name matches contract name) // const [, contractFileName, contractName] = correspondingContractFqn.match(/([^/]+)\.sol:(.+)$/) || []; // const isFullMatchExpected = contractFileName === contractName; const isFullMatchExpected = false; // TODO: full match mode is yet disabled - // const hasMismatch = (isFullMatchExpected && missingInContract.length > 0) || missingInInterface.length > 0; - const hasMismatch = missingInContract.length > 0; + // Check for any type of mismatch: missing functions or parameter name mismatches + const hasMismatch = missingInContract.length > 0 || parameterNameMismatches.length > 0; + + // Log info about skipped signatures if any + if (skipPair?.skipInterfaceSignatures && skipPair.skipInterfaceSignatures.length > 0) { + console.log( + `ℹ️ skipping ${skipPair.skipInterfaceSignatures.length} signature(s) for '${interfaceFqn}' and '${correspondingContractFqn}' (${skipPair.reason})`, + ); + } if (hasMismatch) { mismatchedInterfaces.push({ @@ -246,6 +372,7 @@ task("check-interfaces").setAction(async (_, hre) => { missingInContract, missingInInterface, isFullMatchExpected, + parameterNameMismatches, }); } else { const matchType = isFullMatchExpected ? "fully matches" : "is sub-interface of"; @@ -263,7 +390,14 @@ task("check-interfaces").setAction(async (_, hre) => { } for (const mismatch of mismatchedInterfaces) { - const { interfaceFqn, contractFqn, missingInContract, missingInInterface, isFullMatchExpected } = mismatch; + const { + interfaceFqn, + contractFqn, + missingInContract, + missingInInterface, + isFullMatchExpected, + parameterNameMismatches, + } = mismatch; console.error(`~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~`); console.error(); @@ -285,6 +419,14 @@ task("check-interfaces").setAction(async (_, hre) => { console.error(); } + if (parameterNameMismatches.length > 0) { + console.error(`📋 Parameter name mismatches (${parameterNameMismatches.length / 3} functions):`); + parameterNameMismatches.forEach((entry) => { + console.error(` ${entry}`); + }); + console.error(); + } + if (isFullMatchExpected && missingInInterface.length > 0) { console.error(`📋 Entries missing in interface (${missingInInterface.length}):`); missingInInterface.forEach((entry) => { From f2ffc8711951faaf194556ed70767c0f4d488344 Mon Sep 17 00:00:00 2001 From: Artyom Veremeenko Date: Thu, 23 Oct 2025 16:31:21 +0300 Subject: [PATCH 09/13] Revert "refactor: remove interfaces in return values of external function" This reverts commit 47ebd49233f078478830e78040519d17e526086c. Without tasks/validate-configs.ts change --- contracts/0.4.24/Lido.sol | 4 ++-- contracts/0.8.25/vaults/StakingVault.sol | 6 +++--- contracts/0.8.25/vaults/VaultFactory.sol | 12 ++++++------ contracts/0.8.25/vaults/dashboard/Dashboard.sol | 2 +- contracts/0.8.25/vaults/dashboard/Permissions.sol | 4 ++-- contracts/0.8.25/vaults/interfaces/IStakingVault.sol | 3 ++- .../StakingVault__HarnessForTestUpgrade.sol | 4 ++-- .../contracts/StakingVault__MockForPDG.sol | 2 +- 8 files changed, 19 insertions(+), 18 deletions(-) diff --git a/contracts/0.4.24/Lido.sol b/contracts/0.4.24/Lido.sol index 84d9d3a71e..c670a1597a 100644 --- a/contracts/0.4.24/Lido.sol +++ b/contracts/0.4.24/Lido.sol @@ -586,8 +586,8 @@ contract Lido is Versioned, StETHPermit, AragonApp { /** * @return the Lido Locator address */ - function getLidoLocator() external view returns (address) { - return address(_getLidoLocator()); + function getLidoLocator() external view returns (ILidoLocator) { + return _getLidoLocator(); } /** diff --git a/contracts/0.8.25/vaults/StakingVault.sol b/contracts/0.8.25/vaults/StakingVault.sol index 1e097d3748..7f80bfb8bd 100644 --- a/contracts/0.8.25/vaults/StakingVault.sol +++ b/contracts/0.8.25/vaults/StakingVault.sol @@ -67,7 +67,7 @@ contract StakingVault is IStakingVault, Ownable2StepUpgradeable { * @notice Address of `BeaconChainDepositContract` * Set immutably in the constructor to avoid storage costs */ - address public immutable DEPOSIT_CONTRACT; + IDepositContract public immutable DEPOSIT_CONTRACT; /* * ╔══════════════════════════════════════════════════╗ @@ -104,7 +104,7 @@ contract StakingVault is IStakingVault, Ownable2StepUpgradeable { */ constructor(address _beaconChainDepositContract) { if (_beaconChainDepositContract == address(0)) revert ZeroArgument("_beaconChainDepositContract"); - DEPOSIT_CONTRACT = _beaconChainDepositContract; + DEPOSIT_CONTRACT = IDepositContract(_beaconChainDepositContract); _disableInitializers(); } @@ -536,7 +536,7 @@ contract StakingVault is IStakingVault, Ownable2StepUpgradeable { uint256 balance = availableBalance(); if (_deposit.amount > balance) revert InsufficientBalance(balance, _deposit.amount); - IDepositContract(DEPOSIT_CONTRACT).deposit{value: _deposit.amount}( + DEPOSIT_CONTRACT.deposit{value: _deposit.amount}( _deposit.pubkey, _withdrawalCredentials, _deposit.signature, diff --git a/contracts/0.8.25/vaults/VaultFactory.sol b/contracts/0.8.25/vaults/VaultFactory.sol index 3432ef55ca..2c48b22c41 100644 --- a/contracts/0.8.25/vaults/VaultFactory.sol +++ b/contracts/0.8.25/vaults/VaultFactory.sol @@ -63,20 +63,20 @@ contract VaultFactory { uint256 _nodeOperatorFeeBP, uint256 _confirmExpiry, Permissions.RoleAssignment[] calldata _roleAssignments - ) external payable returns (address vault, Dashboard dashboard) { + ) external payable returns (IStakingVault vault, Dashboard dashboard) { // check if the msg.value is enough to cover the connect deposit ILidoLocator locator = ILidoLocator(LIDO_LOCATOR); if (msg.value < VaultHub(payable(locator.vaultHub())).CONNECT_DEPOSIT()) revert InsufficientFunds(); // create the vault proxy - vault = _deployVault(); + vault = IStakingVault(_deployVault()); // create the dashboard proxy bytes memory immutableArgs = abi.encode(address(vault)); dashboard = Dashboard(payable(Clones.cloneWithImmutableArgs(DASHBOARD_IMPL, immutableArgs))); // initialize StakingVault with the dashboard address as the owner - IStakingVault(vault).initialize(address(dashboard), _nodeOperator, locator.predepositGuarantee()); + vault.initialize(address(dashboard), _nodeOperator, locator.predepositGuarantee()); // initialize Dashboard with the factory address as the default admin, grant optional roles and connect to VaultHub dashboard.initialize(address(this), _nodeOperatorManager, _nodeOperatorManager, _nodeOperatorFeeBP, _confirmExpiry); @@ -111,18 +111,18 @@ contract VaultFactory { uint256 _nodeOperatorFeeBP, uint256 _confirmExpiry, Permissions.RoleAssignment[] calldata _roleAssignments - ) external returns (address vault, Dashboard dashboard) { + ) external returns (IStakingVault vault, Dashboard dashboard) { ILidoLocator locator = ILidoLocator(LIDO_LOCATOR); // create the vault proxy - vault = _deployVault(); + vault = IStakingVault(_deployVault()); // create the dashboard proxy bytes memory immutableArgs = abi.encode(address(vault)); dashboard = Dashboard(payable(Clones.cloneWithImmutableArgs(DASHBOARD_IMPL, immutableArgs))); // initialize StakingVault with the dashboard address as the owner - IStakingVault(vault).initialize(address(dashboard), _nodeOperator, locator.predepositGuarantee()); + vault.initialize(address(dashboard), _nodeOperator, locator.predepositGuarantee()); // initialize Dashboard with the _defaultAdmin as the default admin, grant optional node operator managed roles dashboard.initialize(_defaultAdmin, address(this), _nodeOperatorManager, _nodeOperatorFeeBP, _confirmExpiry); diff --git a/contracts/0.8.25/vaults/dashboard/Dashboard.sol b/contracts/0.8.25/vaults/dashboard/Dashboard.sol index 980d7a0de2..298a3ad094 100644 --- a/contracts/0.8.25/vaults/dashboard/Dashboard.sol +++ b/contracts/0.8.25/vaults/dashboard/Dashboard.sol @@ -439,7 +439,7 @@ contract Dashboard is NodeOperatorFee { if (pdgPolicy != PDGPolicy.ALLOW_DEPOSIT_AND_PROVE) revert ForbiddenByPDGPolicy(); IStakingVault stakingVault_ = _stakingVault(); - IDepositContract depositContract = IDepositContract(stakingVault_.DEPOSIT_CONTRACT()); + IDepositContract depositContract = stakingVault_.DEPOSIT_CONTRACT(); for (uint256 i = 0; i < _deposits.length; i++) { totalAmount += _deposits[i].amount; diff --git a/contracts/0.8.25/vaults/dashboard/Permissions.sol b/contracts/0.8.25/vaults/dashboard/Permissions.sol index 82d97c7d55..1cf316ea0b 100644 --- a/contracts/0.8.25/vaults/dashboard/Permissions.sol +++ b/contracts/0.8.25/vaults/dashboard/Permissions.sol @@ -148,8 +148,8 @@ abstract contract Permissions is AccessControlConfirmable { * @notice Returns the address of the underlying StakingVault. * @return The address of the StakingVault. */ - function stakingVault() external view returns (address) { - return address(_stakingVault()); + function stakingVault() external view returns (IStakingVault) { + return _stakingVault(); } // ==================== Role Management Functions ==================== diff --git a/contracts/0.8.25/vaults/interfaces/IStakingVault.sol b/contracts/0.8.25/vaults/interfaces/IStakingVault.sol index 81b24ea856..4b5e086b27 100644 --- a/contracts/0.8.25/vaults/interfaces/IStakingVault.sol +++ b/contracts/0.8.25/vaults/interfaces/IStakingVault.sol @@ -5,6 +5,7 @@ // solhint-disable-next-line lido/fixed-compiler-version pragma solidity >=0.8.0; +import {IDepositContract} from "contracts/common/interfaces/IDepositContract.sol"; /** * @title IStakingVault @@ -27,7 +28,7 @@ interface IStakingVault { bytes32 depositDataRoot; } - function DEPOSIT_CONTRACT() external view returns (address); + function DEPOSIT_CONTRACT() external view returns (IDepositContract); function initialize(address _owner, address _nodeOperator, address _depositor) external; function version() external pure returns (uint64); function getInitializedVersion() external view returns (uint64); diff --git a/test/0.8.25/vaults/contracts/StakingVault__HarnessForTestUpgrade.sol b/test/0.8.25/vaults/contracts/StakingVault__HarnessForTestUpgrade.sol index db5fed7b67..9e54732e8a 100644 --- a/test/0.8.25/vaults/contracts/StakingVault__HarnessForTestUpgrade.sol +++ b/test/0.8.25/vaults/contracts/StakingVault__HarnessForTestUpgrade.sol @@ -25,7 +25,7 @@ contract StakingVault__HarnessForTestUpgrade is IStakingVault, Ownable2StepUpgra */ uint64 private constant _VERSION = 2; - address public immutable DEPOSIT_CONTRACT; + IDepositContract public immutable DEPOSIT_CONTRACT; bytes32 private constant ERC7201_STORAGE_LOCATION = 0x2ec50241a851d8d3fea472e7057288d4603f7a7f78e6d18a9c12cad84552b100; @@ -33,7 +33,7 @@ contract StakingVault__HarnessForTestUpgrade is IStakingVault, Ownable2StepUpgra constructor(address _beaconChainDepositContract) { if (_beaconChainDepositContract == address(0)) revert ZeroArgument("_beaconChainDepositContract"); - DEPOSIT_CONTRACT = _beaconChainDepositContract; + DEPOSIT_CONTRACT = IDepositContract(_beaconChainDepositContract); // Prevents reinitialization of the implementation _disableInitializers(); diff --git a/test/0.8.25/vaults/predepositGuarantee/contracts/StakingVault__MockForPDG.sol b/test/0.8.25/vaults/predepositGuarantee/contracts/StakingVault__MockForPDG.sol index 840777dbc5..42f0990c86 100644 --- a/test/0.8.25/vaults/predepositGuarantee/contracts/StakingVault__MockForPDG.sol +++ b/test/0.8.25/vaults/predepositGuarantee/contracts/StakingVault__MockForPDG.sol @@ -52,7 +52,7 @@ contract StakingVault__MockForPDG is IStakingVault { withdrawalCredentials_ = _withdrawalCredentials; } - function DEPOSIT_CONTRACT() external view override returns (address) {} + function DEPOSIT_CONTRACT() external view override returns (IDepositContract) {} function initialize(address _owner, address _nodeOperator, address _depositor) external override {} From e6b6c30930089918faec1b9ef926de5c035538fa Mon Sep 17 00:00:00 2001 From: Artyom Veremeenko Date: Thu, 23 Oct 2025 16:58:12 +0300 Subject: [PATCH 10/13] feat: set maxExternalRatioBP to 3% upon upgrade (value configurable) --- contracts/0.4.24/Lido.sol | 6 ++- contracts/upgrade/V3Template.sol | 9 ++-- lib/config-schemas.ts | 1 + .../0200-deploy-v3-upgrading-contracts.ts | 1 + scripts/upgrade/upgrade-params-mainnet.toml | 2 + .../lido/lido.finalizeUpgrade_v3.test.ts | 50 +++++++++---------- 6 files changed, 38 insertions(+), 31 deletions(-) diff --git a/contracts/0.4.24/Lido.sol b/contracts/0.4.24/Lido.sol index c670a1597a..9a37830112 100644 --- a/contracts/0.4.24/Lido.sol +++ b/contracts/0.4.24/Lido.sol @@ -238,8 +238,9 @@ contract Lido is Versioned, StETHPermit, AragonApp { * For more details see https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-10.md * @param _oldBurner The address of the old Burner contract to migrate from * @param _contractsWithBurnerAllowances Contracts that have allowances for the old burner to be migrated + * @param _initialMaxExternalRatioBP Initial maximum external ratio in basis points */ - function finalizeUpgrade_v3(address _oldBurner, address[] _contractsWithBurnerAllowances) external { + function finalizeUpgrade_v3(address _oldBurner, address[] _contractsWithBurnerAllowances, uint256 _initialMaxExternalRatioBP) external { require(hasInitialized(), "NOT_INITIALIZED"); _checkContractVersion(2); _setContractVersion(3); @@ -247,6 +248,9 @@ contract Lido is Versioned, StETHPermit, AragonApp { _migrateStorage_v2_to_v3(); _migrateBurner_v2_to_v3(_oldBurner, _contractsWithBurnerAllowances); + + _setMaxExternalRatioBP(_initialMaxExternalRatioBP); + emit MaxExternalRatioBPSet(_initialMaxExternalRatioBP); } function _migrateStorage_v2_to_v3() internal { diff --git a/contracts/upgrade/V3Template.sol b/contracts/upgrade/V3Template.sol index 5214caa12c..bdc54b46f0 100644 --- a/contracts/upgrade/V3Template.sol +++ b/contracts/upgrade/V3Template.sol @@ -34,7 +34,7 @@ interface IBurner is IBurnerWithoutAccessControl, IAccessControlEnumerable { } interface ILidoWithFinalizeUpgrade is ILido { - function finalizeUpgrade_v3(address _oldBurner, address[] calldata _contractsWithBurnerAllowances) external; + function finalizeUpgrade_v3(address _oldBurner, address[] calldata _contractsWithBurnerAllowances, uint256 _initialMaxExternalRatioBP) external; } interface IAccountingOracle is IBaseOracle { @@ -111,6 +111,7 @@ contract V3Template is V3Addresses { uint256 public initialTotalShares; uint256 public initialTotalPooledEther; address[] public contractsWithBurnerAllowances; + uint256 public immutable INITIAL_MAX_EXTERNAL_RATIO_BP; // // Slots for transient storage @@ -124,8 +125,10 @@ contract V3Template is V3Addresses { /// @param _params Params required to initialize the addresses contract /// @param _expireSinceInclusive Unix timestamp after which upgrade actions revert - constructor(V3AddressesParams memory _params, uint256 _expireSinceInclusive) V3Addresses(_params) { + /// @param _initialMaxExternalRatioBP Initial maximum external ratio in basis points + constructor(V3AddressesParams memory _params, uint256 _expireSinceInclusive, uint256 _initialMaxExternalRatioBP) V3Addresses(_params) { EXPIRE_SINCE_INCLUSIVE = _expireSinceInclusive; + INITIAL_MAX_EXTERNAL_RATIO_BP = _initialMaxExternalRatioBP; contractsWithBurnerAllowances.push(WITHDRAWAL_QUEUE); // NB: NOR and SIMPLE_DVT allowances are set to 0 in TW upgrade, so they are not migrated contractsWithBurnerAllowances.push(CSM_ACCOUNTING); @@ -160,7 +163,7 @@ contract V3Template is V3Addresses { isUpgradeFinished = true; - ILidoWithFinalizeUpgrade(LIDO).finalizeUpgrade_v3(OLD_BURNER, contractsWithBurnerAllowances); + ILidoWithFinalizeUpgrade(LIDO).finalizeUpgrade_v3(OLD_BURNER, contractsWithBurnerAllowances, INITIAL_MAX_EXTERNAL_RATIO_BP); IAccountingOracle(ACCOUNTING_ORACLE).finalizeUpgrade_v4(EXPECTED_FINAL_ACCOUNTING_ORACLE_CONSENSUS_VERSION); diff --git a/lib/config-schemas.ts b/lib/config-schemas.ts index 5acc41c3bd..17e55e7736 100644 --- a/lib/config-schemas.ts +++ b/lib/config-schemas.ts @@ -88,6 +88,7 @@ const OracleVersionsSchema = z.object({ // V3 vote script params const V3VoteScriptSchema = z.object({ expiryTimestamp: NonNegativeIntSchema, + initialMaxExternalRatioBP: BasisPointsSchema, }); // Aragon app versions schema diff --git a/scripts/upgrade/steps/0200-deploy-v3-upgrading-contracts.ts b/scripts/upgrade/steps/0200-deploy-v3-upgrading-contracts.ts index f63b930f23..b115937934 100644 --- a/scripts/upgrade/steps/0200-deploy-v3-upgrading-contracts.ts +++ b/scripts/upgrade/steps/0200-deploy-v3-upgrading-contracts.ts @@ -56,6 +56,7 @@ export async function main() { const template = await deployWithoutProxy(Sk.v3Template, "V3Template", deployer, [ addressesParams, parameters.v3VoteScript.expiryTimestamp, + parameters.v3VoteScript.initialMaxExternalRatioBP, ]); await deployWithoutProxy(Sk.v3VoteScript, "V3VoteScript", deployer, [ diff --git a/scripts/upgrade/upgrade-params-mainnet.toml b/scripts/upgrade/upgrade-params-mainnet.toml index 4777e6d19d..2de6c06474 100644 --- a/scripts/upgrade/upgrade-params-mainnet.toml +++ b/scripts/upgrade/upgrade-params-mainnet.toml @@ -82,6 +82,8 @@ ao_consensus_version = 4 # Accounting Oracle consensus version # Format: Unix timestamp (seconds since epoch) # The upgrade transaction must be executed before this deadline expiryTimestamp = 1765324800 # December 10, 2025 00:00:00 UTC +# Initial maximum external ratio in basis points for Lido v3 +initialMaxExternalRatioBP = 300 # 3% value set upon upgrade for the initial phase # Sources (todo) # - https://research.lido.fi/t/default-risk-assessment-framework-and-fees-parameters-for-lido-v3-stvaults/10504 diff --git a/test/0.4.24/lido/lido.finalizeUpgrade_v3.test.ts b/test/0.4.24/lido/lido.finalizeUpgrade_v3.test.ts index 09e47684ec..4556df87b2 100644 --- a/test/0.4.24/lido/lido.finalizeUpgrade_v3.test.ts +++ b/test/0.4.24/lido/lido.finalizeUpgrade_v3.test.ts @@ -88,7 +88,7 @@ describe("Lido.sol:finalizeUpgrade_v3", () => { afterEach(async () => await Snapshot.restore(originalState)); it("Reverts if not initialized", async () => { - await expect(lido.finalizeUpgrade_v3(ZeroAddress, [])).to.be.revertedWith("NOT_INITIALIZED"); + await expect(lido.finalizeUpgrade_v3(ZeroAddress, [], 0)).to.be.revertedWith("NOT_INITIALIZED"); }); context("initialized", () => { @@ -105,31 +105,29 @@ describe("Lido.sol:finalizeUpgrade_v3", () => { const unexpectedVersion = 1n; await lido.harness_setContractVersion(unexpectedVersion); await expect( - lido.finalizeUpgrade_v3(oldBurner, [ - nodeOperatorsRegistryAddress, - simpleDvtAddress, - csmAccountingAddress, - withdrawalQueueAddress, - ]), + lido.finalizeUpgrade_v3( + oldBurner, + [nodeOperatorsRegistryAddress, simpleDvtAddress, csmAccountingAddress, withdrawalQueueAddress], + 0, + ), ).to.be.revertedWith("UNEXPECTED_CONTRACT_VERSION"); }); it("Reverts if old burner is the same as new burner", async () => { - await expect(lido.finalizeUpgrade_v3(burner, [])).to.be.revertedWith("OLD_BURNER_SAME_AS_NEW"); + await expect(lido.finalizeUpgrade_v3(burner, [], 0)).to.be.revertedWith("OLD_BURNER_SAME_AS_NEW"); }); it("Reverts if old burner is zero address", async () => { - await expect(lido.finalizeUpgrade_v3(ZeroAddress, [])).to.be.revertedWith("OLD_BURNER_ADDRESS_ZERO"); + await expect(lido.finalizeUpgrade_v3(ZeroAddress, [], 0)).to.be.revertedWith("OLD_BURNER_ADDRESS_ZERO"); }); it("Sets contract version to 3", async () => { await expect( - lido.finalizeUpgrade_v3(oldBurner, [ - nodeOperatorsRegistryAddress, - simpleDvtAddress, - csmAccountingAddress, - withdrawalQueueAddress, - ]), + lido.finalizeUpgrade_v3( + oldBurner, + [nodeOperatorsRegistryAddress, simpleDvtAddress, csmAccountingAddress, withdrawalQueueAddress], + 0, + ), ) .to.emit(lido, "ContractVersionSet") .withArgs(finalizeVersion); @@ -146,12 +144,11 @@ describe("Lido.sol:finalizeUpgrade_v3", () => { const depositedValidators = await getStorageAtPosition(lido, "lido.Lido.depositedValidators"); await expect( - lido.finalizeUpgrade_v3(oldBurner, [ - nodeOperatorsRegistryAddress, - simpleDvtAddress, - csmAccountingAddress, - withdrawalQueueAddress, - ]), + lido.finalizeUpgrade_v3( + oldBurner, + [nodeOperatorsRegistryAddress, simpleDvtAddress, csmAccountingAddress, withdrawalQueueAddress], + 0, + ), ).to.not.be.reverted; expect(await lido.getLidoLocator()).to.equal(locator); @@ -168,12 +165,11 @@ describe("Lido.sol:finalizeUpgrade_v3", () => { expect(await lido.sharesOf(oldBurner)).to.equal(sharesOnOldBurner); await expect( - lido.finalizeUpgrade_v3(oldBurner, [ - nodeOperatorsRegistryAddress, - simpleDvtAddress, - csmAccountingAddress, - withdrawalQueueAddress, - ]), + lido.finalizeUpgrade_v3( + oldBurner, + [nodeOperatorsRegistryAddress, simpleDvtAddress, csmAccountingAddress, withdrawalQueueAddress], + 0, + ), ) .to.emit(lido, "TransferShares") .withArgs(oldBurner, burner, sharesOnOldBurner); From 7b17c47fd2e4392d5ff9bd2b299fa7f82b450668 Mon Sep 17 00:00:00 2001 From: Artyom Veremeenko Date: Mon, 27 Oct 2025 14:37:25 +0300 Subject: [PATCH 11/13] fix: scratch config validation --- tasks/validate-configs.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tasks/validate-configs.ts b/tasks/validate-configs.ts index bf4bda4b96..04d5df0295 100644 --- a/tasks/validate-configs.ts +++ b/tasks/validate-configs.ts @@ -141,6 +141,10 @@ const EXPECTED_MISSING_IN_SCRATCH = [ path: "v3VoteScript.expiryTimestamp", reason: "V3 vote script expiry timestamp is upgrade-specific configuration", }, + { + path: "v3VoteScript.initialMaxExternalRatioBP", + reason: "V3 vote script initial max external ratio BP is upgrade-specific configuration", + }, ]; // Special mappings where the same concept has different names From 034c05ea5ccd7b042e13ebc02956a8bd180506d5 Mon Sep 17 00:00:00 2001 From: Aleksei Potapkin Date: Mon, 27 Oct 2025 15:12:48 +0200 Subject: [PATCH 12/13] test: fix a test --- .../vaults/disconnected.integration.ts | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/test/integration/vaults/disconnected.integration.ts b/test/integration/vaults/disconnected.integration.ts index 65dd90f49b..59f447d507 100644 --- a/test/integration/vaults/disconnected.integration.ts +++ b/test/integration/vaults/disconnected.integration.ts @@ -149,7 +149,7 @@ describe("Integration: Actions with vault disconnected from hub", () => { }); it("Can not change the tier as owner of the vault", async () => { - const { operatorGrid, vaultHub } = ctx.contracts; + const { operatorGrid } = ctx.contracts; const agentSigner = await ctx.getSigner("agent"); await operatorGrid.connect(agentSigner).registerGroup(nodeOperator, 1000); @@ -164,24 +164,19 @@ describe("Integration: Actions with vault disconnected from hub", () => { }, ]); - const ownerRoleAsAddress = ethers.zeroPadValue(await owner.getAddress(), 32); - let confirmTimestamp = await getNextBlockTimestamp(); - let expiryTimestamp = confirmTimestamp + (await operatorGrid.getConfirmExpiry()); - const msgData = operatorGrid.interface.encodeFunctionData("changeTier", [ - await stakingVault.getAddress(), - 1, - 1000, - ]); + await expect(operatorGrid.connect(owner).changeTier(stakingVault, 1n, 1000n)).to.be.revertedWithCustomError( + operatorGrid, + "VaultNotConnected", + ); - await expect(operatorGrid.connect(owner).changeTier(stakingVault, 1n, 1000n)) - .to.emit(operatorGrid, "RoleMemberConfirmed") - .withArgs(owner, ownerRoleAsAddress, confirmTimestamp, expiryTimestamp, msgData); + const nodeOperatorRoleAsAddress = ethers.zeroPadValue(nodeOperator.address, 32); + const msgData = operatorGrid.interface.encodeFunctionData("changeTier", [stakingVault, 1n, 1000n]); + const confirmTimestamp = await getNextBlockTimestamp(); + const expiryTimestamp = confirmTimestamp + (await operatorGrid.getConfirmExpiry()); - confirmTimestamp = await getNextBlockTimestamp(); - expiryTimestamp = confirmTimestamp + (await operatorGrid.getConfirmExpiry()); - await expect( - operatorGrid.connect(nodeOperator).changeTier(stakingVault, 1n, 1000n), - ).to.be.revertedWithCustomError(vaultHub, "NotConnectedToHub"); + await expect(operatorGrid.connect(nodeOperator).changeTier(stakingVault, 1n, 1000n)) + .to.emit(operatorGrid, "RoleMemberConfirmed") + .withArgs(nodeOperator, nodeOperatorRoleAsAddress, confirmTimestamp, expiryTimestamp, msgData); }); describe("Funding", () => { From 192add893854699c2157dda097fb5d875b37920f Mon Sep 17 00:00:00 2001 From: Aleksei Potapkin Date: Mon, 27 Oct 2025 15:34:37 +0200 Subject: [PATCH 13/13] test: fix a test --- test/integration/vaults/disconnected.integration.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/integration/vaults/disconnected.integration.ts b/test/integration/vaults/disconnected.integration.ts index 59f447d507..11d0a33c6c 100644 --- a/test/integration/vaults/disconnected.integration.ts +++ b/test/integration/vaults/disconnected.integration.ts @@ -170,7 +170,11 @@ describe("Integration: Actions with vault disconnected from hub", () => { ); const nodeOperatorRoleAsAddress = ethers.zeroPadValue(nodeOperator.address, 32); - const msgData = operatorGrid.interface.encodeFunctionData("changeTier", [stakingVault, 1n, 1000n]); + const msgData = operatorGrid.interface.encodeFunctionData("changeTier", [ + await stakingVault.getAddress(), + 1n, + 1000n, + ]); const confirmTimestamp = await getNextBlockTimestamp(); const expiryTimestamp = confirmTimestamp + (await operatorGrid.getConfirmExpiry());