Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
733 changes: 733 additions & 0 deletions contracts/upgrade/StakingRouterV3VoteScript.sol

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions lib/state-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export enum Sk {
v3Template = "v3Template",
v3Addresses = "v3Addresses",
v3VoteScript = "v3VoteScript",
stakingRouterV3VoteScript = "stakingRouterV3VoteScript",
operatorGrid = "operatorGrid",
validatorConsolidationRequests = "validatorConsolidationRequests",
lazyOracle = "lazyOracle",
Expand Down Expand Up @@ -196,6 +197,7 @@ export function getAddress(contractKey: Sk, state: DeploymentState): string {
case Sk.minFirstAllocationStrategy:
case Sk.validatorConsolidationRequests:
case Sk.v3VoteScript:
case Sk.stakingRouterV3VoteScript:
case Sk.depositsTempStorage:
case Sk.beaconChainDepositor:
case Sk.vaultsAdapter:
Expand Down
16 changes: 0 additions & 16 deletions scripts/dao-hoodi-v3-phase-2.sh

This file was deleted.

17 changes: 17 additions & 0 deletions scripts/dao-staking-router-v3-vote.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash
set -e +u
set -o pipefail

export NETWORK=${NETWORK:="hoodi"}
export RPC_URL=${RPC_URL:="http://127.0.0.1:8545"}

export DEPLOYER=${DEPLOYER:="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"}
export GAS_PRIORITY_FEE=${GAS_PRIORITY_FEE:=1}
export GAS_MAX_FEE=${GAS_MAX_FEE:=100}

export NETWORK_STATE_FILE=${NETWORK_STATE_FILE:="deployed-hoodi.json"}
export STEPS_FILE=${STEPS_FILE:="upgrade/steps-staking-router-v3-vote.json"}
export UPGRADE_PARAMETERS_FILE=${UPGRADE_PARAMETERS_FILE:="scripts/upgrade/upgrade-params-hoodi.toml"}
export STAKING_ROUTER_V3_VOTE_PROPOSAL_METADATA=${STAKING_ROUTER_V3_VOTE_PROPOSAL_METADATA:="staking-router-v3-vote"}

yarn hardhat --network $NETWORK run --no-compile scripts/utils/migrate.ts
7 changes: 7 additions & 0 deletions scripts/upgrade/steps-staking-router-v3-vote.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"steps": [
"upgrade/steps/0000-check-env",
"upgrade/steps/0100-deploy-staking-router-v3-vote-script",
"upgrade/steps/0200-run-staking-router-v3-vote-script"
]
}
3 changes: 0 additions & 3 deletions scripts/upgrade/steps-upgrade-hoodi-v3-phase-2.json

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import assert from "assert";
import { ethers } from "hardhat";
import { readStakingRouterV3VoteScriptParameters } from "scripts/utils/staking-router-v3-vote";

import { deployWithoutProxy, Sk } from "lib";

export async function main() {
const deployer = (await ethers.provider.getSigner()).address;
assert.equal(process.env.DEPLOYER, deployer);

const params = readStakingRouterV3VoteScriptParameters();

await deployWithoutProxy(Sk.stakingRouterV3VoteScript, "StakingRouterV3VoteScript", deployer, [params]);
}
25 changes: 0 additions & 25 deletions scripts/upgrade/steps/0100-upgrade-hoodi-to-v3-phase-2.ts

This file was deleted.

162 changes: 162 additions & 0 deletions scripts/upgrade/steps/0200-run-staking-router-v3-vote-script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import assert from "assert";
import { Contract, JsonRpcProvider } from "ethers";
import { ethers } from "hardhat";

import { advanceChainTime, ether, log } from "lib";
import { readNetworkState, Sk } from "lib/state-file";

const ONE_HOUR = 3600n;
const DG_ABI = [
"function getProposers() view returns ((address account,address executor)[])",
"function submitProposal((address target,uint96 value,bytes payload)[] calls,string metadata) returns (uint256 proposalId)",
"function scheduleProposal(uint256 proposalId)",
];
const TIMELOCK_ABI = [
"function getAfterSubmitDelay() view returns (uint32)",
"function getAfterScheduleDelay() view returns (uint32)",
"function execute(uint256 proposalId)",
];
const VOTE_SCRIPT_ABI = [
"function AGENT() view returns (address)",
"function ITEMS_COUNT() view returns (uint256)",
"function getAgentForwardCalldata() view returns (bytes)",
];

async function getRpcProvider() {
const rpcUrl = process.env.RPC_URL;
if (!rpcUrl) {
throw new Error("RPC_URL is required");
}

const provider = new JsonRpcProvider(rpcUrl);
const clientVersion = String(await provider.send("web3_clientVersion", []));
const clientLower = clientVersion.toLowerCase();
const rpcPrefix = clientLower.includes("anvil") ? "anvil" : "hardhat";

return { provider, rpcPrefix };
}

async function impersonateRpc(provider: JsonRpcProvider, rpcPrefix: string, address: string) {
await provider.send(`${rpcPrefix}_impersonateAccount`, [address]);
await provider.send(`${rpcPrefix}_setBalance`, [address, ethers.toQuantity(ether("100"))]);
return provider.getSigner(address);
}

export async function main(): Promise<void> {
const deployer = (await ethers.provider.getSigner()).address;
assert.equal(process.env.DEPLOYER, deployer);

// State file can be imported from another environment with a different historical deployer.
// For vote execution on forks we only need addresses from that state, not deployer equality.
const state = readNetworkState();
const voteScriptAddress = state[Sk.stakingRouterV3VoteScript]?.address;
const dualGovernanceAddress = state[Sk.dgDualGovernance]?.proxy?.address;
const timelockAddress = state[Sk.dgEmergencyProtectedTimelock]?.proxy?.address;

if (!voteScriptAddress) {
throw new Error(`State key ${Sk.stakingRouterV3VoteScript} is missing. Run deploy step first.`);
}
if (!dualGovernanceAddress) {
throw new Error(`State key ${Sk.dgDualGovernance} is missing in state file.`);
}
if (!timelockAddress) {
throw new Error(`State key ${Sk.dgEmergencyProtectedTimelock} is missing in state file.`);
}

const { provider: rpcProvider, rpcPrefix } = await getRpcProvider();
const voteScript = new Contract(voteScriptAddress, VOTE_SCRIPT_ABI, rpcProvider);
const dualGovernance = new Contract(dualGovernanceAddress, DG_ABI, rpcProvider);
const timelock = new Contract(timelockAddress, TIMELOCK_ABI, rpcProvider);

const agentAddress = (await voteScript.getFunction("AGENT")()) as string;
const itemsCount = (await voteScript.getFunction("ITEMS_COUNT")()) as bigint;
const agentForwardCalldata = (await voteScript.getFunction("getAgentForwardCalldata")()) as string;

const proposers = (await dualGovernance.getFunction("getProposers")()) as { account: string; executor: string }[];
const preferredProposer = process.env.STAKING_ROUTER_V3_VOTE_PROPOSER || state[Sk.appVoting]?.proxy?.address;
const proposer = proposers.find((p) => p.account.toLowerCase() === preferredProposer?.toLowerCase()) ?? proposers[0];

if (!proposer) {
throw new Error("No proposer found in DualGovernance.");
}

const proposerAddress = proposer.account;
const executorAddress =
process.env.STAKING_ROUTER_V3_VOTE_EXECUTOR ||
(typeof proposer.executor === "string" && proposer.executor !== ethers.ZeroAddress ? proposer.executor : undefined);

if (!executorAddress) {
throw new Error("DualGovernance proposer has no executor and STAKING_ROUTER_V3_VOTE_EXECUTOR is not set.");
}

const proposalMetadata = process.env.STAKING_ROUTER_V3_VOTE_PROPOSAL_METADATA || "staking-router-v3-vote";
const proposalCalls = [{ target: agentAddress, value: 0n, payload: agentForwardCalldata }];

log.info("Prepared StakingRouterV3 vote script for DG execution", {
voteScript: voteScriptAddress,
itemsCount: itemsCount.toString(),
dualGovernance: dualGovernanceAddress,
timelock: timelockAddress,
proposer: proposerAddress,
executor: executorAddress,
});

const proposerSigner = await impersonateRpc(rpcProvider, rpcPrefix, proposerAddress);
const executorSigner = await impersonateRpc(rpcProvider, rpcPrefix, executorAddress);

const proposalId = (await dualGovernance
.connect(proposerSigner)
.getFunction("submitProposal")
.staticCall(proposalCalls, proposalMetadata)) as bigint;

const submitTx = await dualGovernance.connect(proposerSigner).getFunction("submitProposal")(
proposalCalls,
proposalMetadata,
);
await log.txLink(submitTx.hash);
const submitReceipt = await submitTx.wait();
if (!submitReceipt) {
throw new Error("submitProposal transaction was not mined");
}
log.success("DG proposal submitted", proposalId.toString());

const afterSubmitDelay = (await timelock.getFunction("getAfterSubmitDelay")()) as bigint;
await advanceChainTime(afterSubmitDelay);
const scheduleTx = await dualGovernance.connect(proposerSigner).getFunction("scheduleProposal")(proposalId);
await log.txLink(scheduleTx.hash);
const scheduleReceipt = await scheduleTx.wait();
if (!scheduleReceipt) {
throw new Error("scheduleProposal transaction was not mined");
}
log.success("DG proposal scheduled", proposalId.toString());

const afterScheduleDelay = (await timelock.getFunction("getAfterScheduleDelay")()) as bigint;
await advanceChainTime(afterScheduleDelay);

const maxExecuteRetries = Number(process.env.STAKING_ROUTER_V3_VOTE_EXECUTE_RETRIES || "0");
let lastError: unknown;

for (let attempt = 0; attempt <= maxExecuteRetries; attempt++) {
try {
const executeTx = await timelock.connect(executorSigner).getFunction("execute")(proposalId);
await log.txLink(executeTx.hash);
const executeReceipt = await executeTx.wait();
if (!executeReceipt) {
throw new Error("timelock.execute transaction was not mined");
}
log.success("Atomic StakingRouterV3 vote script executed via Dual Governance", proposalId.toString());
log.info("Execution receipt", {
txHash: executeTx.hash,
gasUsed: executeReceipt.gasUsed.toString(),
});
return;
} catch (error) {
lastError = error;
if (attempt < maxExecuteRetries) {
await advanceChainTime(ONE_HOUR);
}
}
}

throw new Error(`Failed to execute proposal ${proposalId.toString()}. ` + `Original error: ${String(lastError)}`);
}
62 changes: 56 additions & 6 deletions scripts/upgrade/upgrade-params-hoodi.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,59 @@
# Lido Protocol Upgrade Parameters - Hoodi Configuration
# This file contains deployment parameters for upgrading Lido protocol contracts on Ethereum Hoodi testnet

# Predeposit guarantee configuration for validator deposit guarantees
[predepositGuarantee]
genesisForkVersion = "0x10000910" # Ethereum Hoodi testnet genesis fork version
gIndex = "0x0000000000000000000000000000000000000000000000000096000000000028" # Generalized index for state verification
gIndexAfterChange = "0x0000000000000000000000000000000000000000000000000096000000000028"
changeSlot = 0 # Slot number when the change takes effect
[stakingRouterV3VoteScript]
# Common addresses
agent = "0x0534aA41907c9631fae990960bCC72d75fA7cfeD"
stakingRouter = "0xCc820558B39ee15C7C45B59390B503b83fb499A8"
burner = "0xb2c99cd38a2636a6281a849C8de938B3eF4A7C3D"
triggerableWithdrawalsGateway = "0x6679090D92b08a2a686eF8614feECD8cDFE209db"
easyTrackEVMScriptExecutor = "0x79a20FD0FA36453B2F45eAbab19bfef43575Ba9E"
resealManager = "0x05172CbCDb7307228F781436b327679e4DAE166B"
identifiedCommunityStakersGateManager = "0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53"
gateSeal = "0x725166f143DdcD9EC1b96dfb70f16E3f44968A65"
# TODO: upgrade-specific GateSealV3 is not present in artifacts/hoodi/deploy-hoodi.json.
# Prefill required.
gateSealV3 = ""
generalDelayedPenaltyReporter = "0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53"
hashConsensusInitialEpoch = 47480

[stakingRouterV3VoteScript.upgrade]
csmProxy = "0x79CEf36D84743222f37765204Bec41E92a93E59d"
csmImpl = "0x6140195075a2BFaE0C332377af1f1461Fced0791"
parametersRegistryProxy = "0xA4aD5236963f9Fe4229864712269D8d79B65C5Ad"
parametersRegistryImpl = "0xae34514B7A5B402264dB13ac8ffe25766173B3cc"
feeOracleProxy = "0xe7314f561B2e72f9543F1004e741bab6Fc51028B"
feeOracleImpl = "0x2fcc1bE07eAf30FC7daC541327B96B038C73eE7a"
feeOracleConsensusVersion = 4
vettedGateProxy = "0x10a254E724fe2b7f305F76f3F116a3969c53845f"
vettedGateImpl = "0x86915F7D0C68A627385eF36DdB40A48391118882"
accountingProxy = "0xA54b90BA34C5f326BC1485054080994e38FB4C60"
accountingImpl = "0xD32b59d6054b94516f8EEE2A84C3c33DaF1d06B8"
feeDistributorProxy = "0xaCd9820b0A2229a82dc1A0770307ce5522FF3582"
feeDistributorImpl = "0x1B952bd0C5d65B86d4F1F29589A31584e2bE038f"
exitPenaltiesProxy = "0xD259b31083Be841E5C85b2D481Cfc17C14276800"
exitPenaltiesImpl = "0xF8CC5BFFC0580395a5D8bBE3C4CD07ce3F623d89"
strikesProxy = "0x8fBA385C3c334D251eE413e79d4D3890db98693c"
strikesImpl = "0xF9CcA99e34197E5fc70aB05F5dEb112EcD2367e9"
oldPermissionlessGate = "0x5553077102322689876A6AdFd48D75014c28acfb"
verifier = "0x1773b2Ff99A030F6000554Cb8A5Ec93145650cbA"
# TODO: upgrade-specific VerifierV3 is not present in artifacts/hoodi/deploy-hoodi.json.
# Prefill required.
verifierV3 = ""
permissionlessGate = "0x5553077102322689876A6AdFd48D75014c28acfb"
ejector = "0x777bd76326E4aDcD353b03AD45b33BAF41048476"

[stakingRouterV3VoteScript.curatedModule]
# TODO: curated module deployment addresses are not present in artifacts/hoodi.
# Prefill required.
module = ""
accounting = ""
ejector = ""
hashConsensus = ""
moduleName = "curated-onchain-v1"
stakeShareLimit = 2000
priorityExitShareThreshold = 2500
stakingModuleFee = 800
treasuryFee = 200
maxDepositsPerBlock = 30
minDepositBlockDistance = 25
52 changes: 45 additions & 7 deletions scripts/upgrade/upgrade-params-mainnet.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,52 @@
# Lido Protocol Upgrade Parameters - Mainnet Configuration
# This file contains deployment parameters for upgrading Lido protocol contracts on Ethereum mainnet

# Predeposit guarantee configuration for validator deposit guarantees
[predepositGuarantee]
genesisForkVersion = "0x00000000" # Ethereum mainnet genesis fork version: https://github.com/ethereum/consensus-specs/blob/01b53691dcc36d37a5ad8994b3a32d8de69fb1aa/configs/mainnet.yaml#L30
gIndex = "0x0000000000000000000000000000000000000000000000000096000000000028" # Generalized index for state verification: https://research.lido.fi/t/lip-27-ensuring-compatibility-with-ethereum-s-pectra-upgrade/9444#p-20086-update-gindexes-5
gIndexAfterChange = "0x0000000000000000000000000000000000000000000000000096000000000028" # Required for hardfork upgrades, the same for now
changeSlot = 0 # Slot number when the change takes effect

[consolidationGateway]
maxConsolidationRequestsLimit = 8000 # Maximum number of consolidations requests that can be processed
consolidationsPerFrame = 1 # Number of consolidations processed per frame
frameDurationInSec = 48 # Duration of each processing frame in seconds

[stakingRouterV3VoteScript]
agent = "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c"
easyTrackEVMScriptExecutor = "0xFE5986E06210aC1eCC1aDCafc0cc7f8D63B3F977"
resealManager = "0x7914b5a1539b97Bd0bbd155757F25FD79A522d24"
identifiedCommunityStakersGateManager = "0xC52fC3081123073078698F1EAc2f1Dc7Bd71880f"
gateSeal = "0xE1686C2E90eb41a48356c1cC7FaA17629af3ADB3"
# TODO: upgrade-specific GateSealV3 is not present in artifacts/mainnet/deploy-mainnet.json.
# Prefill required.
gateSealV3 = ""
generalDelayedPenaltyReporter = "0xC52fC3081123073078698F1EAc2f1Dc7Bd71880f"
hashConsensusInitialEpoch = 47480

[stakingRouterV3VoteScript.upgrade]
csmProxy = "0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F"
csmImpl = "0x1eB6d4da13ca9566c17F526aE0715325d7a07665"
vettedGateProxy = "0xB314D4A76C457c93150d308787939063F4Cc67E0"
parametersRegistryImpl = "0x25fdC3BE9977CD4da679dF72A64C8B6Bd5216A78"
feeOracleImpl = "0xe0B234f99E413E27D9Bc31aBba9A49A3e570Da97"
feeOracleConsensusVersion = 4
vettedGateImpl = "0x65D4D92Cd0EabAa05cD5A46269C24b71C21cfdc4"
accountingImpl = "0x6f09d2426c7405C5546413e6059F884D2D03f449"
feeDistributorImpl = "0x5DCF7cF7c6645E9E822a379dF046a8b0390251A1"
exitPenaltiesImpl = "0xDa22fA1CEa40d05Fe4CD536967afdD839586D546"
strikesImpl = "0x3E5021424c9e13FC853e523Cd68ebBec848956a0"
oldPermissionlessGate = "0xcF33a38111d0B1246A3F38a838fb41D626B454f0"
verifier = "0xdC5FE1782B6943f318E05230d688713a560063DC"
# TODO: upgrade-specific VerifierV3 is not present in artifacts/mainnet/deploy-mainnet.json.
# Prefill required.
verifierV3 = ""
permissionlessGate = "0xcF33a38111d0B1246A3F38a838fb41D626B454f0"
ejector = "0xc72b58aa02E0e98cF8A4a0E9Dce75e763800802C"

[stakingRouterV3VoteScript.curatedModule]
# TODO: curated module deployment addresses are not present in artifacts/mainnet.
# Prefill required.
module = ""
hashConsensus = ""
moduleName = "curated-onchain-v1"
stakeShareLimit = 2000
priorityExitShareThreshold = 2500
stakingModuleFee = 800
treasuryFee = 200
maxDepositsPerBlock = 30
minDepositBlockDistance = 25
Loading
Loading