arb-upgrades
Version:
681 lines (610 loc) • 22.1 kB
text/typescript
import { HardhatRuntimeEnvironment } from 'hardhat/types/runtime'
import { writeFileSync, readFileSync, unlinkSync, existsSync } from 'fs'
import childProcess from 'child_process'
import prompts from 'prompts'
import {
QueuedUpdates,
CurrentDeployments,
ContractNames,
CurrentDeployment,
QueuedUpdate,
isBeacon,
isRollupUserFacet,
isRollupAdminFacet,
getLayer,
hasPostInitHook,
isBeaconOwnedByEOA,
isBeaconOwnedByRollup,
} from './types'
const ADMIN_SLOT =
'0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103'
const IMPLEMENTATION_SLOT =
'0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc'
const ROLLUP_CONSTRUCTOR_VAL = 42161
const getAdminFromProxyStorage = async (
hre: HardhatRuntimeEnvironment,
proxyAddress: string
) => {
let admin = await hre.ethers.provider.getStorageAt(proxyAddress, ADMIN_SLOT)
if (admin.length > 42) {
admin = '0x' + admin.substr(admin.length - 40, 40)
}
return admin
}
const POST_UPGRADE_INIT_SIG = '0x95fcea78'
const currentCommit = childProcess
.execSync('git rev-parse HEAD')
.toString()
.trim()
const ensureCleanGitTree = () => {
const tree = childProcess.execSync('git diff-index HEAD --').toString().trim()
const found = tree.split('\n').find(diff => diff.includes('contracts'))
if (found) {
throw new Error(
`You have local changes to ${found}. Commit/stash/ get rid of them`
)
}
}
export const initUpgrades = (
hre: HardhatRuntimeEnvironment,
rootDir: string
): {
updateImplementations: () => Promise<boolean>
verifyCurrentImplementations: () => Promise<boolean>
deployLogic: (contractNames: ContractNames[] | ContractNames) => Promise<void>
deployLogicAll: () => Promise<void>
transferAdmin: (proxyAddress: string, newAdmin: string) => Promise<void>
transferBeaconOwner: (
upgradableBeaconAddress: string,
newOwner: string
) => Promise<void>
removeBuildInfoFiles: () => Promise<void>
getDeployments: () => Promise<{
path: string
data: CurrentDeployments
}>
verifyDeployments: () => Promise<void>
} => {
const compileTask = hre.run('compile')
const getQueuedUpdates = async (): Promise<{
path: string
data: QueuedUpdates
}> => {
const network = await hre.ethers.provider.getNetwork()
const path = `${rootDir}/_deployments/${network.chainId}_queued-updates.json`
if (!existsSync(path)) {
console.log('New network; creating queued updates file')
writeFileSync(path, JSON.stringify({}))
return { path, data: {} }
}
const jsonBuff = readFileSync(path)
return { path, data: JSON.parse(jsonBuff.toString()) as QueuedUpdates }
}
const getDeployments = async (): Promise<{
path: string
data: CurrentDeployments
}> => {
const network = await hre.ethers.provider.getNetwork()
const path = `${rootDir}/_deployments/${network.chainId}_current_deployment.json`
if (!existsSync(path)) {
console.log('New network; need to set up _current_deployments.json file')
throw Error('No current deployments')
}
const jsonBuff = readFileSync(path)
return {
path,
data: JSON.parse(jsonBuff.toString()) as CurrentDeployments,
}
}
const createOrLoadTmpDeploymentsFile = async (): Promise<{
path: string
data: CurrentDeployments
}> => {
const { data: currentDeployments } = await getDeployments()
const val = await loadTmpDeployments()
if (val) return val
console.log('Creating a new tmp deployments file:')
const path = await tmpDeploymentsPath()
writeFileSync(path, JSON.stringify(currentDeployments))
return {
path,
data: currentDeployments,
}
}
const tmpDeploymentsPath = async () => {
const network = await hre.ethers.provider.getNetwork()
return `${rootDir}/_deployments/${network.chainId}_tmp_deployment.json`
}
const loadTmpDeployments = async (): Promise<
| {
path: string
data: CurrentDeployments
}
| undefined
> => {
const path = await tmpDeploymentsPath()
if (existsSync(path)) {
console.log(``)
const res = await prompts({
type: 'confirm',
name: 'value',
message:
'tmp deployments file found; do you want to resume deployments with it?',
initial: true,
})
if (res.value !== 'Yes') {
console.log('exiting')
process.exit(0)
}
const jsonBuff = readFileSync(path)
return {
path,
data: JSON.parse(jsonBuff.toString()) as CurrentDeployments,
}
}
}
// const getBuildInfoString = async (contractName: string) => {
// const names = await hre.artifacts.getAllFullyQualifiedNames()
// const contracts = names.filter(curr => curr.endsWith(`:${contractName}`))
// if (contracts.length !== 1) throw new Error('Contract not found')
// const info = await hre.artifacts.getBuildInfo(contracts[0])
// return JSON.stringify(info)
// }
const deployLogic = async (
contractNames: ContractNames[] | ContractNames
): Promise<void> => {
await compileTask
ensureCleanGitTree()
if (!Array.isArray(contractNames)) {
contractNames = [contractNames]
}
console.log('Deploying logic contracts for ', contractNames.join(','))
const signers = await hre.ethers.getSigners()
if (!signers.length) {
throw new Error(
'No signer - make sure a key is properly set (check hardhat config)'
)
}
const signer = signers[0]
console.log('Using signer', signer.address)
const { path, data: queuedUpdatesData } = await getQueuedUpdates()
for (const contractName of contractNames) {
if (queuedUpdatesData[contractName]) {
console.log()
const res = await prompts({
type: 'confirm',
name: 'value',
message: `Update already queued up for ${contractName}; would you redeploy it? ('Yes' to redeploy, otherwise we'll skip and used the queued update)`,
initial: true,
})
if (res.value.trim().toLowerCase() !== 'yes') {
console.log('Skipping redeploy and using the queued update')
continue
} else {
console.log('Redeploying ', contractName)
}
}
const layerOfContract = getLayer(contractName)
const currentLayer =
(
await hre.ethers.provider.getCode(
'0x0000000000000000000000000000000000000064'
)
).length > 2
? 2
: 1
if (layerOfContract !== currentLayer) {
throw new Error(
`Warning: trying to deploy ${contractName} onto the wrong layer!`
)
}
console.log('Deploying new logic for ', contractName)
const contractFactory = (
await hre.ethers.getContractFactory(contractName)
).connect(signer)
// handle Rollup's constructor:
const newLogic =
contractName === ContractNames.Rollup
? await contractFactory.deploy(ROLLUP_CONSTRUCTOR_VAL)
: await contractFactory.deploy()
const deployedContract = await newLogic.deployed()
const receipt = await deployedContract.deployTransaction.wait()
const newLogicData: QueuedUpdate = {
address: receipt.contractAddress,
deployTxn: receipt.transactionHash,
arbitrumCommitHash: currentCommit,
buildInfo: '' /* await getBuildInfoString(contractName) */,
}
queuedUpdatesData[contractName] = newLogicData
console.log(`Deployed ${contractName} Logic:`)
console.log(receipt)
console.log('')
writeFileSync(path, JSON.stringify(queuedUpdatesData))
}
}
const updateImplementations = async () => {
await compileTask
ensureCleanGitTree()
const res = await verifyCurrentImplementations()
if (!res) {
throw new Error(
'Verification of current implementations failed; cancelling update'
)
}
const { path: queuedUpdatesPath, data: queuedUpdatesData } =
await getQueuedUpdates()
const { path: deploymentsPath } = await getDeployments()
const { path: tmpDeploymentsPath, data: tmpDeploymentsJsonData } =
await createOrLoadTmpDeploymentsFile()
const { proxyAdminAddress } = tmpDeploymentsJsonData
const ProxyAdmin__factory = await hre.ethers.getContractFactory(
'ProxyAdmin'
)
let proxyAdmin = ProxyAdmin__factory.attach(proxyAdminAddress).connect(
hre.ethers.provider
)
const proxyAdminOwner = await proxyAdmin.owner()
const getSigner = async (networkName: string) => {
if (networkName === 'fork') {
await hre.network.provider.request({
method: 'hardhat_impersonateAccount',
params: [proxyAdminOwner],
})
await hre.network.provider.send('hardhat_setBalance', [
proxyAdminOwner,
'0x16189AD417E380000',
])
return hre.ethers.getSigner(proxyAdminOwner)
} else {
const signers = await hre.ethers.getSigners()
if (!signers.length) {
throw new Error(
'No signer - make sure a key is properly set (check hardhat config)'
)
}
return signers[0]
}
}
const signer = await getSigner(hre.network.name)
proxyAdmin = proxyAdmin.connect(signer)
if (proxyAdminOwner.toLowerCase() !== signer.address.toLowerCase()) {
throw new Error(
`Signer address ${signer.address} != ProxyAdmin owner ${proxyAdminOwner}`
)
}
const contractsToUpdate = Object.keys(queuedUpdatesData) as ContractNames[]
if (contractsToUpdate.length === 0) {
throw new Error(
'No logic implementations to upgrade to for current network / package'
)
}
console.log(`Updating ${contractsToUpdate.length} contracts`)
// TODO: explicitly check for storage layout clashes
contractsToUpdate.sort(a => (a === ContractNames.SequencerInbox ? -1 : 1))
for (const contractName of contractsToUpdate) {
const queuedUpdateData = queuedUpdatesData[contractName] as QueuedUpdate
const deploymentData = tmpDeploymentsJsonData.contracts[
contractName
] as CurrentDeployment
if (!deploymentData) {
console.warn(`Contract ${contractName} not recognized; skipping`)
continue
}
console.log(`Updating ${contractName} to new implementation`)
let upgradeTx: any
if (isBeaconOwnedByEOA(contractName)) {
// handle UpgradeableBeacon proxy owned by EOA
const UpgradeableBeacon = (
await hre.ethers.getContractFactory('UpgradeableBeacon')
)
.attach(deploymentData.proxyAddress)
.connect(signer)
upgradeTx = await UpgradeableBeacon.upgradeTo(queuedUpdateData.address)
} else if (isBeaconOwnedByRollup(contractName)) {
// handle UpgradeableBeacon proxy owned by Rollup
const rollupAddress =
tmpDeploymentsJsonData.contracts.Rollup.proxyAddress
const RollupAdmin = (
await hre.ethers.getContractFactory(ContractNames.RollupAdminFacet)
)
.attach(rollupAddress)
.connect(signer)
upgradeTx = await RollupAdmin.upgradeBeacon(
deploymentData.proxyAddress,
queuedUpdateData.address
)
} else if (
isRollupAdminFacet(contractName) ||
isRollupUserFacet(contractName)
) {
// Handle diamond proxy pattern
const userFacetAddress = isRollupUserFacet(contractName)
? queuedUpdateData.address
: tmpDeploymentsJsonData.contracts.RollupUserFacet.implAddress
const adminFacetAddress = isRollupAdminFacet(contractName)
? queuedUpdateData.address
: tmpDeploymentsJsonData.contracts.RollupAdminFacet.implAddress
const RollupAdmin = (
await hre.ethers.getContractFactory(ContractNames.RollupAdminFacet)
)
.attach(tmpDeploymentsJsonData.contracts.Rollup.proxyAddress)
.connect(signer)
upgradeTx = await RollupAdmin.setFacets(
adminFacetAddress,
userFacetAddress
)
} else {
// handle TransparentUpgradeableProxy
if (hasPostInitHook(contractName)) {
upgradeTx = await proxyAdmin.upgradeAndCall(
deploymentData.proxyAddress,
queuedUpdateData.address,
POST_UPGRADE_INIT_SIG
)
} else {
upgradeTx = await proxyAdmin.upgrade(
deploymentData.proxyAddress,
queuedUpdateData.address
)
}
}
const rec = await upgradeTx.wait()
console.log('Upgrade receipt:', rec)
// const buildInfo = await getBuildInfoString(contractName)
console.log(`Done updating ${contractName}`)
const newDeploymentData: CurrentDeployment = {
proxyAddress: deploymentData.proxyAddress,
implAddress: queuedUpdateData.address,
implDeploymentTxn: queuedUpdateData.deployTxn,
implArbitrumCommitHash: queuedUpdateData.arbitrumCommitHash,
implBuildInfo: '',
}
console.log('Setting new tmp: deployment data')
tmpDeploymentsJsonData.contracts[contractName] = newDeploymentData
writeFileSync(tmpDeploymentsPath, JSON.stringify(tmpDeploymentsJsonData))
delete queuedUpdatesData[contractName]
writeFileSync(queuedUpdatesPath, JSON.stringify(queuedUpdatesData))
console.log('')
}
console.log('Finished all deployments: setting data to current deployments')
writeFileSync(deploymentsPath, JSON.stringify(tmpDeploymentsJsonData))
// removing tmp file
unlinkSync(tmpDeploymentsPath)
return await verifyCurrentImplementations()
}
const verifyCurrentImplementations = async () => {
await compileTask
console.log('Verifying deployments:')
const { data: deploymentsJsonData } = await getDeployments()
const tmpDeploymentsJsonData = await loadTmpDeployments()
let success = true
const ProxyAdmin__factory = await hre.ethers.getContractFactory(
'ProxyAdmin'
)
const proxyAdmin = ProxyAdmin__factory.attach(
deploymentsJsonData.proxyAdminAddress
)
const proxyAdminOwner = await proxyAdmin.owner()
console.log('proxyAdmin owner:', proxyAdminOwner)
for (const _contractName in deploymentsJsonData.contracts) {
const contractName = _contractName as ContractNames
const _currentDeploymentData = deploymentsJsonData.contracts[contractName]
const _tmpDeploymentData =
tmpDeploymentsJsonData &&
tmpDeploymentsJsonData.data.contracts[contractName]
const deploymentData = _tmpDeploymentData
? _tmpDeploymentData
: _currentDeploymentData
if (isBeacon(contractName)) {
const UpgradeableBeacon = (
await hre.ethers.getContractFactory('UpgradeableBeacon')
).attach(deploymentData.proxyAddress)
const implementation = await UpgradeableBeacon.implementation()
const beaconOwner = await UpgradeableBeacon.owner()
if (
implementation.toLowerCase() !==
deploymentData.implAddress.toLowerCase()
) {
console.log(
contractName + ' Verification failed: bad implementation',
implementation,
deploymentData.implAddress
)
success = false
}
const expectedBeaconOwner = isBeaconOwnedByRollup(contractName)
? deploymentsJsonData.contracts.Rollup.proxyAddress
: proxyAdminOwner
if (beaconOwner.toLowerCase() !== expectedBeaconOwner.toLowerCase()) {
console.log(
`${contractName} Verification failed: bad admin`,
beaconOwner,
proxyAdminOwner
)
success = false
}
continue
}
if (isRollupAdminFacet(contractName) || isRollupUserFacet(contractName)) {
const Rollup = (
await hre.ethers.getContractFactory(ContractNames.Rollup)
).attach(deploymentsJsonData.contracts.Rollup.proxyAddress)
const facet = isRollupUserFacet(contractName)
? await Rollup.getUserFacet()
: await Rollup.getAdminFacet()
if (facet.toLowerCase() !== deploymentData.implAddress.toLowerCase()) {
console.log(
`${contractName} Verification failed; bad implementation`,
facet
)
success = false
}
continue
}
// check proxy admin
const admin = await getAdminFromProxyStorage(
hre,
deploymentData.proxyAddress
)
if (
admin.toLowerCase() !==
deploymentsJsonData.proxyAdminAddress.toLowerCase()
) {
console.log(
`${contractName} Verification failed: bad admin`,
admin,
deploymentsJsonData.proxyAdminAddress
)
success = false
}
// check implementation
let rawImplementation = await hre.ethers.provider.send(
'eth_getStorageAt',
[deploymentData.proxyAddress, IMPLEMENTATION_SLOT, 'latest']
)
rawImplementation =
rawImplementation.length % 2 === 1
? '0x0' + rawImplementation.substr(2)
: rawImplementation
let implementation = hre.ethers.utils.hexlify(rawImplementation)
if (implementation.length > 42) {
implementation =
'0x' + implementation.substr(implementation.length - 40, 40)
}
if (
implementation.toLowerCase() !==
deploymentData.implAddress.toLowerCase()
) {
console.log(
`${contractName} Verification failed; bad implementation`,
implementation,
deploymentData.implAddress
)
success = false
}
}
console.log(success ? 'Verified successfully :)' : 'Failed verification :/')
return success
}
const deployLogicAll = async () => {
await compileTask
const { data: deploymentsJsonData } = await getDeployments()
const contractsNames = Object.keys(
deploymentsJsonData.contracts
) as ContractNames[]
await deployLogic(contractsNames)
}
const transferAdmin = async (proxyAddress: string, newAdmin: string) => {
await compileTask
const proxyAdminAddr = await getAdminFromProxyStorage(hre, proxyAddress)
const ProxyAdmin__factory = await hre.ethers.getContractFactory(
'ProxyAdmin'
)
const proxyAdmin = ProxyAdmin__factory.attach(proxyAdminAddr).connect(
hre.ethers.provider
)
const proxyAdminOwner = await proxyAdmin.owner()
if (newAdmin.toLowerCase() === proxyAdminOwner.toLowerCase()) {
throw new Error('User trying to update admin to current admin address')
}
const signers = await hre.ethers.getSigners()
if (!signers.length) {
throw new Error(
'No signer - make sure a key is properly set (check hardhat config)'
)
}
const signer = signers[0]
if (signer.address.toLowerCase() !== proxyAdminOwner.toLowerCase()) {
throw new Error('User signer is not the owner of proxy admin')
}
const res = await proxyAdmin
.connect(signer)
.changeProxyAdmin(proxyAddress, newAdmin)
await res.wait()
}
const transferBeaconOwner = async (
upgradableBeaconAddress: string,
newOwner: string
) => {
await compileTask
const signers = await hre.ethers.getSigners()
if (!signers.length) {
throw new Error(
'No signer - make sure a key is properly set (check hardhat config)'
)
}
const signer = signers[0]
const UpgradeableBeacon = (
await hre.ethers.getContractFactory('UpgradeableBeacon')
)
.attach(upgradableBeaconAddress)
.connect(signer)
const beaconOwner = await UpgradeableBeacon.owner()
if (beaconOwner.toLowerCase() !== signer.address.toLowerCase()) {
throw new Error(
`Not connecetd as owner ${beaconOwner}, instead running as ${signer.address}`
)
}
console.log(
`You are about to transfer owner ship of ${upgradableBeaconAddress} to ${newOwner}. You sure? ('Yes' to proceeed)`
)
const confirm = await prompt('')
if (confirm !== 'Yes') {
console.log('Cancelling')
return
}
const res = await UpgradeableBeacon.transferOwnership(newOwner)
await res.wait()
console.log('ownership transfer complete')
}
const removeBuildInfoFiles = async () => {
console.log(
`You sure you want to remove build info files for the current network's current_deployments file? You might want to make sure they're backed up first. ('Yes' to continue)`
)
const res = await prompt('')
if (res !== 'Yes') {
console.log('exiting')
process.exit(0)
}
const { data, path } = await getDeployments()
for (const _contractName of Object.keys(data.contracts)) {
const contractName = _contractName as ContractNames
data.contracts[contractName].implBuildInfo = ''
}
writeFileSync(path, JSON.stringify(data))
}
const verifyDeployments = async () => {
const { data } = await getDeployments()
for (const _contractName of Object.keys(data.contracts)) {
const contractName = _contractName as ContractNames
const contract = data.contracts[contractName]
try {
// Handle Rollup Constructor
if (contractName === ContractNames.Rollup) {
await hre.run('verify:verify', {
address: contract.implAddress,
constructorArguments: [ROLLUP_CONSTRUCTOR_VAL],
contract: 'contracts/rollup/Rollup.sol:Rollup',
})
} else {
await hre.run('verify:verify', {
address: contract.implAddress,
})
}
} catch (err) {
console.log(`failed to verify ${contractName}`, err)
}
}
}
return {
updateImplementations,
verifyCurrentImplementations,
deployLogic,
deployLogicAll,
transferAdmin,
transferBeaconOwner,
removeBuildInfoFiles,
getDeployments,
verifyDeployments,
}
}