Skip to content

Commit

Permalink
Scripts for governance (matter-labs#92)
Browse files Browse the repository at this point in the history
Co-authored-by: Vlad Bochok <41153528+vladbochok@users.noreply.github.com>
  • Loading branch information
StanislavBreadless and vladbochok authored Dec 12, 2023
1 parent a8429e8 commit a20a2e5
Show file tree
Hide file tree
Showing 4 changed files with 295 additions and 19 deletions.
1 change: 1 addition & 0 deletions ethereum/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
245 changes: 245 additions & 0 deletions ethereum/scripts/migrate-governance.ts
Original file line number Diff line number Diff line change
@@ -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 <new-governance-address>")
.option("--gas-price <gas-price>")
.option("--refund-recipient <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);
});
29 changes: 12 additions & 17 deletions zksync/src/upgradeL2BridgeImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
39 changes: 37 additions & 2 deletions zksync/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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(),
};
}

0 comments on commit a20a2e5

Please sign in to comment.