From a20a2e5a4e4ab6ba7bd94355593f35c389dd761c Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Tue, 12 Dec 2023 17:47:28 +0100 Subject: [PATCH] Scripts for governance (#92) Co-authored-by: Vlad Bochok <41153528+vladbochok@users.noreply.github.com> --- ethereum/package.json | 1 + ethereum/scripts/migrate-governance.ts | 245 +++++++++++++++++++++++++ zksync/src/upgradeL2BridgeImpl.ts | 29 ++- zksync/src/utils.ts | 39 +++- 4 files changed, 295 insertions(+), 19 deletions(-) create mode 100644 ethereum/scripts/migrate-governance.ts diff --git a/ethereum/package.json b/ethereum/package.json index 8228716da..56bfcca3c 100644 --- a/ethereum/package.json +++ b/ethereum/package.json @@ -84,6 +84,7 @@ "initialize-allow-list": "ts-node scripts/initialize-l1-allow-list.ts", "initialize-validator": "ts-node scripts/initialize-validator.ts", "initialize-governance": "ts-node scripts/initialize-governance.ts", + "migrate-governance": "ts-node scripts/migrate-governance.ts", "upgrade-1": "ts-node scripts/upgrades/upgrade-1.ts", "upgrade-2": "ts-node scripts/upgrades/upgrade-2.ts", "upgrade-3": "ts-node scripts/upgrades/upgrade-3.ts", diff --git a/ethereum/scripts/migrate-governance.ts b/ethereum/scripts/migrate-governance.ts new file mode 100644 index 000000000..eac786a4c --- /dev/null +++ b/ethereum/scripts/migrate-governance.ts @@ -0,0 +1,245 @@ +/// Temporary script that generated the needed calldata for the migration of the governance. + +import { Command } from "commander"; +import { BigNumber, ethers, Wallet } from "ethers"; +import { Deployer } from "../src.ts/deploy"; +import { formatUnits, parseUnits } from "ethers/lib/utils"; +import { applyL1ToL2Alias, getAddressFromEnv, getNumberFromEnv, web3Provider } from "./utils"; +import * as hre from "hardhat"; +import * as fs from "fs"; + +import { getL1TxInfo } from "../../zksync/src/utils"; + +import { UpgradeableBeaconFactory } from "../../zksync/typechain/UpgradeableBeaconFactory"; +import { Provider } from "zksync-web3"; + +const provider = web3Provider(); +const priorityTxMaxGasLimit = BigNumber.from(getNumberFromEnv("CONTRACTS_PRIORITY_TX_MAX_GAS_LIMIT")); + +const L2ERC20BridgeABI = JSON.parse( + fs + .readFileSync( + "../zksync/artifacts-zk/cache-zk/solpp-generated-contracts/bridge/L2ERC20Bridge.sol/L2ERC20Bridge.json" + ) + .toString() +).abi; + +interface TxInfo { + data: string; + to: string; + value?: string; +} + +async function getERC20BeaconAddress(l2Erc20BridgeAddress: string) { + const provider = new Provider(process.env.API_WEB3_JSON_RPC_HTTP_URL); + const contract = new ethers.Contract(l2Erc20BridgeAddress, L2ERC20BridgeABI, provider); + return await contract.l2TokenBeacon(); +} + +function displayTx(msg: string, info: TxInfo) { + console.log(msg); + console.log(JSON.stringify(info, null, 2), "\n"); +} + +async function main() { + const program = new Command(); + + program.version("0.1.0").name("migrate-governance"); + + program + .option("--new-governance-address ") + .option("--gas-price ") + .option("--refund-recipient ") + .action(async (cmd) => { + const gasPrice = cmd.gasPrice ? parseUnits(cmd.gasPrice, "gwei") : await provider.getGasPrice(); + console.log(`Using gas price: ${formatUnits(gasPrice, "gwei")} gwei`); + + const refundRecipient = cmd.refundRecipient; + console.log(`Using refund recipient: ${refundRecipient}`); + + // This action is very dangerous, and so we double check that the governance in env is the same + // one as the user provided manually. + const governanceAddressFromEnv = getAddressFromEnv("CONTRACTS_GOVERNANCE_ADDR").toLowerCase(); + const userProvidedAddress = cmd.newGovernanceAddress.toLowerCase(); + if (governanceAddressFromEnv !== userProvidedAddress) { + throw new Error("Governance mismatch"); + } + + // We won't be making any transactions with this wallet, we just need + // it to initialize the Deployer object. + const deployWallet = Wallet.createRandom(); + const deployer = new Deployer({ + deployWallet, + verbose: true, + }); + + const expectedDeployedBytecode = hre.artifacts.readArtifactSync("Governance").deployedBytecode; + + const isBytecodeCorrect = + (await provider.getCode(userProvidedAddress)).toLowerCase() === expectedDeployedBytecode.toLowerCase(); + if (!isBytecodeCorrect) { + throw new Error("The address does not contain governance bytecode"); + } + + console.log("Firstly, the current governor should transfer its ownership to the new governance contract."); + console.log("All the transactions below can be executed in one batch"); + + // Step 1. Transfer ownership of all the contracts to the new governor. + + // Below we are preparing the calldata for the L1 transactions + const zkSync = deployer.zkSyncContract(deployWallet); + const allowlist = deployer.l1AllowList(deployWallet); + const validatorTimelock = deployer.validatorTimelock(deployWallet); + + const l1Erc20Bridge = deployer.transparentUpgradableProxyContract( + deployer.addresses.Bridges.ERC20BridgeProxy, + deployWallet + ); + + const erc20MigrationTx = l1Erc20Bridge.interface.encodeFunctionData("changeAdmin", [governanceAddressFromEnv]); + displayTx("L1 ERC20 bridge migration calldata:", { + data: erc20MigrationTx, + to: l1Erc20Bridge.address, + }); + + const zkSyncSetPendingGovernor = zkSync.interface.encodeFunctionData("setPendingGovernor", [ + governanceAddressFromEnv, + ]); + displayTx("zkSync Diamond Proxy migration calldata:", { + data: zkSyncSetPendingGovernor, + to: zkSync.address, + }); + + const allowListGovernorMigration = allowlist.interface.encodeFunctionData("transferOwnership", [ + governanceAddressFromEnv, + ]); + displayTx("AllowList migration calldata:", { + data: allowListGovernorMigration, + to: allowlist.address, + }); + + const validatorTimelockMigration = validatorTimelock.interface.encodeFunctionData("transferOwnership", [ + governanceAddressFromEnv, + ]); + displayTx("Validator timelock migration calldata:", { + data: validatorTimelockMigration, + to: validatorTimelock.address, + }); + + // Below, we prepare the transactions to migrate the L2 contracts. + + // Note that since these are L2 contracts, the governance must be aliased. + const aliasedNewGovernor = applyL1ToL2Alias(governanceAddressFromEnv); + + // L2 ERC20 bridge as well as Weth token are a transparent upgradable proxy. + const l2ERC20Bridge = deployer.transparentUpgradableProxyContract( + process.env.CONTRACTS_L2_ERC20_BRIDGE_ADDR!, + deployWallet + ); + const l2Erc20BridgeCalldata = l2ERC20Bridge.interface.encodeFunctionData("changeAdmin", [aliasedNewGovernor]); + const l2TxForErc20Bridge = await getL1TxInfo( + deployer, + l2ERC20Bridge.address, + l2Erc20BridgeCalldata, + refundRecipient, + gasPrice, + priorityTxMaxGasLimit, + provider + ); + displayTx("L2 ERC20 bridge changeAdmin: ", l2TxForErc20Bridge); + + const l2wethToken = deployer.transparentUpgradableProxyContract( + process.env.CONTRACTS_L2_WETH_TOKEN_PROXY_ADDR!, + deployWallet + ); + const l2WethUpgradeCalldata = l2wethToken.interface.encodeFunctionData("changeAdmin", [aliasedNewGovernor]); + const l2TxForWethUpgrade = await getL1TxInfo( + deployer, + l2wethToken.address, + l2WethUpgradeCalldata, + refundRecipient, + gasPrice, + priorityTxMaxGasLimit, + provider + ); + displayTx("L2 Weth upgrade: ", l2TxForWethUpgrade); + + // L2 Tokens are BeaconProxies + const l2Erc20BeaconAddress: string = await getERC20BeaconAddress(l2ERC20Bridge.address); + const l2Erc20TokenBeacon = UpgradeableBeaconFactory.connect(l2Erc20BeaconAddress, deployWallet); + const l2Erc20BeaconCalldata = l2Erc20TokenBeacon.interface.encodeFunctionData("transferOwnership", [ + aliasedNewGovernor, + ]); + const l2TxForErc20BeaconUpgrade = await getL1TxInfo( + deployer, + l2Erc20BeaconAddress, + l2Erc20BeaconCalldata, + refundRecipient, + gasPrice, + priorityTxMaxGasLimit, + provider + ); + displayTx("L2 ERC20 beacon upgrade: ", l2TxForErc20BeaconUpgrade); + + // Small delimeter for better readability. + console.log("\n\n\n", "-".repeat(20), "\n\n\n"); + + console.log("Secondly, the new governor needs to accept all the roles where they need to be accepted."); + + // Step 2. Accept the roles on L1. Transparent proxy and Beacon proxy contracts do NOT require accepting new ownership. + // However, the following do require: + // - zkSync Diamond Proxy + // - ValidatorTimelock. + // - Allowlist. + + const calls = [ + { + target: zkSync.address, + value: 0, + data: zkSync.interface.encodeFunctionData("acceptGovernor"), + }, + { + target: allowlist.address, + value: 0, + data: allowlist.interface.encodeFunctionData("acceptOwnership"), + }, + { + target: validatorTimelock.address, + value: 0, + data: validatorTimelock.interface.encodeFunctionData("acceptOwnership"), + }, + ]; + + const operation = { + calls: calls, + predecessor: ethers.constants.HashZero, + salt: ethers.constants.HashZero, + }; + + const governance = deployer.governanceContract(deployWallet); + + const scheduleTransparentCalldata = governance.interface.encodeFunctionData("scheduleTransparent", [ + operation, + 0, + ]); + displayTx("Schedule transparent calldata:\n", { + data: scheduleTransparentCalldata, + to: governance.address, + }); + + const executeCalldata = governance.interface.encodeFunctionData("execute", [operation]); + displayTx("Execute calldata:\n", { + data: executeCalldata, + to: governance.address, + }); + }); + + await program.parseAsync(process.argv); +} + +main() + .then(() => process.exit(0)) + .catch((err) => { + console.error("Error:", err); + process.exit(1); + }); diff --git a/zksync/src/upgradeL2BridgeImpl.ts b/zksync/src/upgradeL2BridgeImpl.ts index 0fb38aa64..a7077dfe6 100644 --- a/zksync/src/upgradeL2BridgeImpl.ts +++ b/zksync/src/upgradeL2BridgeImpl.ts @@ -9,7 +9,7 @@ import { Provider } from "zksync-web3"; import { REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT } from "zksync-web3/build/src/utils"; import { getAddressFromEnv, getNumberFromEnv, web3Provider } from "../../ethereum/scripts/utils"; import { Deployer } from "../../ethereum/src.ts/deploy"; -import { awaitPriorityOps, computeL2Create2Address, create2DeployFromL1 } from "./utils"; +import { awaitPriorityOps, computeL2Create2Address, create2DeployFromL1, getL1TxInfo } from "./utils"; export function getContractBytecode(contractName: string) { return hre.artifacts.readArtifactSync(contractName).bytecode; @@ -25,7 +25,7 @@ function checkSupportedContract(contract: any): contract is SupportedContracts { return true; } -const priorityTxMaxGasLimit = getNumberFromEnv("CONTRACTS_PRIORITY_TX_MAX_GAS_LIMIT"); +const priorityTxMaxGasLimit = BigNumber.from(getNumberFromEnv("CONTRACTS_PRIORITY_TX_MAX_GAS_LIMIT")); const l2Erc20BridgeProxyAddress = getAddressFromEnv("CONTRACTS_L2_ERC20_BRIDGE_ADDR"); const EIP1967_IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; @@ -62,30 +62,25 @@ async function getBeaconProxyUpgradeCalldata(target: string) { return proxyInterface.encodeFunctionData("upgradeTo", [target]); } -async function getL1TxInfo( +async function getBridgeUpgradeTxInfo( deployer: Deployer, - to: string, - l2Calldata: string, + target: string, refundRecipient: string, gasPrice: BigNumber ) { - const zksync = deployer.zkSyncContract(ethers.Wallet.createRandom().connect(provider)); - const l1Calldata = zksync.interface.encodeFunctionData("requestL2Transaction", [ - to, - 0, + const l2Calldata = await getTransparentProxyUpgradeCalldata(target); + + return await getL1TxInfo( + deployer, + l2Erc20BridgeProxyAddress, l2Calldata, - priorityTxMaxGasLimit, - REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT, - [], // It is assumed that the target has already been deployed refundRecipient, - ]); - - const neededValue = await zksync.l2TransactionBaseCost( gasPrice, priorityTxMaxGasLimit, - REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT + provider ); + return { to: zksync.address, data: l1Calldata, @@ -114,7 +109,7 @@ async function getTokenBeaconUpgradeTxInfo( ) { const l2Calldata = await getBeaconProxyUpgradeCalldata(target); - return await getL1TxInfo(deployer, proxy, l2Calldata, refundRecipient, gasPrice); + return await getL1TxInfo(deployer, proxy, l2Calldata, refundRecipient, gasPrice, priorityTxMaxGasLimit, provider); } async function getTxInfo( diff --git a/zksync/src/utils.ts b/zksync/src/utils.ts index bee4d7cd2..60ddf864c 100644 --- a/zksync/src/utils.ts +++ b/zksync/src/utils.ts @@ -1,13 +1,14 @@ import { artifacts } from "hardhat"; import { Interface } from "ethers/lib/utils"; +import type { Deployer } from "../../ethereum/src.ts/deploy"; import { deployedAddressesFromEnv } from "../../ethereum/src.ts/deploy"; import { IZkSyncFactory } from "../../ethereum/typechain/IZkSyncFactory"; -import type { BytesLike, Wallet } from "ethers"; +import type { BigNumber, BytesLike, Wallet } from "ethers"; import { ethers } from "ethers"; import type { Provider } from "zksync-web3"; -import { sleep } from "zksync-web3/build/src/utils"; +import { REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT, sleep } from "zksync-web3/build/src/utils"; // eslint-disable-next-line @typescript-eslint/no-var-requires export const REQUIRED_L2_GAS_PRICE_PER_PUBDATA = require("../../SystemConfig.json").REQUIRED_L2_GAS_PRICE_PER_PUBDATA; @@ -129,3 +130,37 @@ export async function awaitPriorityOps( } } } + +export async function getL1TxInfo( + deployer: Deployer, + to: string, + l2Calldata: string, + refundRecipient: string, + gasPrice: BigNumber, + priorityTxMaxGasLimit: BigNumber, + provider: ethers.providers.JsonRpcProvider +) { + const zksync = deployer.zkSyncContract(ethers.Wallet.createRandom().connect(provider)); + const l1Calldata = zksync.interface.encodeFunctionData("requestL2Transaction", [ + to, + 0, + l2Calldata, + priorityTxMaxGasLimit, + REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT, + [], // It is assumed that the target has already been deployed + refundRecipient, + ]); + + const neededValue = await zksync.l2TransactionBaseCost( + gasPrice, + priorityTxMaxGasLimit, + REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT + ); + + return { + to: zksync.address, + data: l1Calldata, + value: neededValue.toString(), + gasPrice: gasPrice.toString(), + }; +}