@hyperlane-xyz/core
Version:
Core solidity contracts for Hyperlane
560 lines (514 loc) • 16.7 kB
text/typescript
import assert from 'assert'
import { URLSearchParams } from 'url'
import { ethers, Contract } from 'ethers'
import { Provider } from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer'
import { awaitCondition, sleep } from '@eth-optimism/core-utils'
import { HardhatRuntimeEnvironment } from 'hardhat/types'
import { Deployment, DeployResult } from 'hardhat-deploy/dist/types'
import 'hardhat-deploy'
import '@eth-optimism/hardhat-deploy-config'
import '@nomiclabs/hardhat-ethers'
/**
* Wrapper around hardhat-deploy with some extra features.
*
* @param opts Options for the deployment.
* @param opts.hre HardhatRuntimeEnvironment.
* @param opts.contract Name of the contract to deploy.
* @param opts.name Name to use for the deployment file.
* @param opts.iface Interface to use for the returned contract.
* @param opts.args Arguments to pass to the contract constructor.
* @param opts.postDeployAction Action to perform after the contract is deployed.
* @returns Deployed contract object.
*/
export const deploy = async ({
hre,
name,
iface,
args,
contract,
postDeployAction,
}: {
hre: HardhatRuntimeEnvironment
name: string
args: any[]
contract?: string
iface?: string
postDeployAction?: (contract: Contract) => Promise<void>
}) => {
const { deployer } = await hre.getNamedAccounts()
// Hardhat deploy will usually do this check for us, but currently doesn't also consider
// external deployments when doing this check. By doing the check ourselves, we also get to
// consider external deployments. If we already have the deployment, return early.
let result: Deployment | DeployResult = await hre.deployments.getOrNull(name)
// Wrap in a try/catch in case there is not a deployConfig for the current network.
let numDeployConfirmations: number
try {
numDeployConfirmations = hre.deployConfig.numDeployConfirmations
} catch (e) {
numDeployConfirmations = 1
}
if (result) {
console.log(`skipping ${name}, using existing at ${result.address}`)
} else {
result = await hre.deployments.deploy(name, {
contract,
from: deployer,
args,
log: true,
waitConfirmations: numDeployConfirmations,
})
console.log(`Deployed ${name} at ${result.address}`)
// Only wait for the transaction if it was recently deployed in case the
// result was deployed a long time ago and was pruned from the backend.
await hre.ethers.provider.waitForTransaction(result.transactionHash)
}
// Check to make sure there is code
const code = await hre.ethers.provider.getCode(result.address)
if (code === '0x') {
throw new Error(`no code for ${result.address}`)
}
// Create the contract object to return.
const created = asAdvancedContract({
confirmations: numDeployConfirmations,
contract: new Contract(
result.address,
iface !== undefined
? (await hre.ethers.getContractFactory(iface)).interface
: result.abi,
hre.ethers.provider.getSigner(deployer)
),
})
// Run post-deploy actions if necessary.
if ((result as DeployResult).newlyDeployed) {
if (postDeployAction) {
await postDeployAction(created)
}
}
return created
}
/**
* Returns a version of the contract object which modifies all of the input contract's methods to
* automatically await transaction receipts and confirmations. Will also throw if we timeout while
* waiting for a transaction to be included in a block.
*
* @param opts Options for the contract.
* @param opts.hre HardhatRuntimeEnvironment.
* @param opts.contract Contract to wrap.
* @returns Wrapped contract object.
*/
export const asAdvancedContract = (opts: {
contract: Contract
confirmations?: number
gasPrice?: number
}): Contract => {
// Temporarily override Object.defineProperty to bypass ether's object protection.
const def = Object.defineProperty
Object.defineProperty = (obj, propName, prop) => {
prop.writable = true
return def(obj, propName, prop)
}
const contract = new Contract(
opts.contract.address,
opts.contract.interface,
opts.contract.signer || opts.contract.provider
)
// Now reset Object.defineProperty
Object.defineProperty = def
for (const fnName of Object.keys(contract.functions)) {
const fn = contract[fnName].bind(contract)
;(contract as any)[fnName] = async (...args: any) => {
// We want to use the configured gas price but we need to set the gas price to zero if we're
// triggering a static function.
let gasPrice = opts.gasPrice
if (contract.interface.getFunction(fnName).constant) {
gasPrice = 0
}
// Now actually trigger the transaction (or call).
const tx = await fn(...args, {
gasPrice,
})
// Meant for static calls, we don't need to wait for anything, we get the result right away.
if (typeof tx !== 'object' || typeof tx.wait !== 'function') {
return tx
}
// Wait for the transaction to be included in a block and wait for the specified number of
// deployment confirmations.
const maxTimeout = 120
let timeout = 0
while (true) {
await sleep(1000)
const receipt = await contract.provider.getTransactionReceipt(tx.hash)
if (receipt === null) {
timeout++
if (timeout > maxTimeout) {
throw new Error('timeout exceeded waiting for txn to be mined')
}
} else if (receipt.confirmations >= (opts.confirmations || 0)) {
return tx
}
}
}
}
return contract
}
/**
* Creates a contract object from a deployed artifact.
*
* @param hre HardhatRuntimeEnvironment.
* @param name Name of the deployed contract to get an object for.
* @param opts Options for the contract.
* @param opts.iface Optional interface to use for the contract object.
* @param opts.signerOrProvider Optional signer or provider to use for the contract object.
* @returns Contract object.
*/
export const getContractFromArtifact = async (
hre: HardhatRuntimeEnvironment,
name: string,
opts: {
iface?: string
signerOrProvider?: Signer | Provider | string
} = {}
): Promise<ethers.Contract> => {
const artifact = await hre.deployments.get(name)
// Get the deployed contract's interface.
let iface = new hre.ethers.utils.Interface(artifact.abi)
// Override with optional iface name if requested.
if (opts.iface) {
const factory = await hre.ethers.getContractFactory(opts.iface)
iface = factory.interface
}
let signerOrProvider: Signer | Provider = hre.ethers.provider
if (opts.signerOrProvider) {
if (typeof opts.signerOrProvider === 'string') {
signerOrProvider = hre.ethers.provider.getSigner(opts.signerOrProvider)
} else {
signerOrProvider = opts.signerOrProvider
}
}
let numDeployConfirmations: number
try {
numDeployConfirmations = hre.deployConfig.numDeployConfirmations
} catch (e) {
numDeployConfirmations = 1
}
return asAdvancedContract({
confirmations: numDeployConfirmations,
contract: new hre.ethers.Contract(
artifact.address,
iface,
signerOrProvider
),
})
}
/**
* Gets multiple contract objects from their respective deployed artifacts.
*
* @param hre HardhatRuntimeEnvironment.
* @param configs Array of contract names and options.
* @returns Array of contract objects.
*/
export const getContractsFromArtifacts = async (
hre: HardhatRuntimeEnvironment,
configs: Array<{
name: string
iface?: string
signerOrProvider?: Signer | Provider | string
}>
): Promise<ethers.Contract[]> => {
const contracts = []
for (const config of configs) {
contracts.push(await getContractFromArtifact(hre, config.name, config))
}
return contracts
}
/**
* Helper function for asserting that a contract variable is set to the expected value.
*
* @param contract Contract object to query.
* @param variable Name of the variable to query.
* @param expected Expected value of the variable.
*/
export const assertContractVariable = async (
contract: ethers.Contract,
variable: string,
expected: any
) => {
// Need to make a copy that doesn't have a signer or we get the error that contracts with
// signers cannot override the from address.
const temp = new ethers.Contract(
contract.address,
contract.interface,
contract.provider
)
const actual = await temp.callStatic[variable]({
from: ethers.constants.AddressZero,
})
if (ethers.utils.isAddress(expected)) {
assert(
actual.toLowerCase() === expected.toLowerCase(),
`[FATAL] ${variable} is ${actual} but should be ${expected}`
)
return
}
assert(
actual === expected || (actual.eq && actual.eq(expected)),
`[FATAL] ${variable} is ${actual} but should be ${expected}`
)
}
/**
* Returns the address for a given deployed contract by name.
*
* @param hre HardhatRuntimeEnvironment.
* @param name Name of the deployed contract.
* @returns Address of the deployed contract.
*/
export const getDeploymentAddress = async (
hre: HardhatRuntimeEnvironment,
name: string
): Promise<string> => {
const deployment = await hre.deployments.get(name)
return deployment.address
}
/**
* JSON-ifies an ethers transaction object.
*
* @param tx Ethers transaction object.
* @returns JSON-ified transaction object.
*/
export const printJsonTransaction = (tx: ethers.PopulatedTransaction): void => {
console.log(
'JSON transaction parameters:\n' +
JSON.stringify(
{
from: tx.from,
to: tx.to,
data: tx.data,
value: tx.value,
chainId: tx.chainId,
},
null,
2
)
)
}
/**
* Mini helper for transferring a Proxy to the MSD
*
* @param opts Options for executing the step.
* @param opts.isLiveDeployer True if the deployer is live.
* @param opts.proxy proxy contract.
* @param opts.dictator dictator contract.
*/
export const doOwnershipTransfer = async (opts: {
isLiveDeployer?: boolean
proxy: ethers.Contract
name: string
transferFunc: string
dictator: ethers.Contract
}): Promise<void> => {
if (opts.isLiveDeployer) {
console.log(`Setting ${opts.name} owner to MSD`)
await opts.proxy[opts.transferFunc](opts.dictator.address)
} else {
const tx = await opts.proxy.populateTransaction[opts.transferFunc](
opts.dictator.address
)
console.log(`
Please transfer ${opts.name} (proxy) owner to MSD
- ${opts.name} address: ${opts.proxy.address}
- MSD address: ${opts.dictator.address}
`)
printJsonTransaction(tx)
printCastCommand(tx)
await printTenderlySimulationLink(opts.dictator.provider, tx)
}
}
/**
* Check if the script should submit the transaction or wait for the deployer to do it manually.
*
* @param hre HardhatRuntimeEnvironment.
* @param ovveride Allow manually disabling live transaction submission. Useful for testing.
* @returns True if the current step is the target step.
*/
export const liveDeployer = async (opts: {
hre: HardhatRuntimeEnvironment
disabled: string | undefined
}): Promise<boolean> => {
if (!!opts.disabled) {
console.log('Live deployer manually disabled')
return false
}
const { deployer } = await opts.hre.getNamedAccounts()
const ret =
deployer.toLowerCase() === opts.hre.deployConfig.controller.toLowerCase()
console.log('Setting live deployer to', ret)
return ret
}
/**
* Mini helper for checking if the current step is a target step.
*
* @param dictator SystemDictator contract.
* @param step Target step.
* @returns True if the current step is the target step.
*/
export const isStep = async (
dictator: ethers.Contract,
step: number
): Promise<boolean> => {
return (await dictator.currentStep()) === step
}
/**
* Mini helper for checking if the current step is the first step in target phase.
*
* @param dictator SystemDictator contract.
* @param phase Target phase.
* @returns True if the current step is the first step in target phase.
*/
export const isStartOfPhase = async (
dictator: ethers.Contract,
phase: number
): Promise<boolean> => {
const phaseToStep = {
1: 1,
2: 3,
3: 6,
}
return (await dictator.currentStep()) === phaseToStep[phase]
}
/**
* Mini helper for executing a given step.
*
* @param opts Options for executing the step.
* @param opts.isLiveDeployer True if the deployer is live.
* @param opts.SystemDictator SystemDictator contract.
* @param opts.step Step to execute.
* @param opts.message Message to print before executing the step.
* @param opts.checks Checks to perform after executing the step.
*/
export const doStep = async (opts: {
isLiveDeployer?: boolean
SystemDictator: ethers.Contract
step: number
message: string
checks: () => Promise<void>
}): Promise<void> => {
const isStepVal = await isStep(opts.SystemDictator, opts.step)
if (!isStepVal) {
console.log(`Step already completed: ${opts.step}`)
return
}
// Extra message to help the user understand what's going on.
console.log(opts.message)
// Either automatically or manually execute the step.
if (opts.isLiveDeployer) {
console.log(`Executing step ${opts.step}...`)
await opts.SystemDictator[`step${opts.step}`]()
} else {
const tx = await opts.SystemDictator.populateTransaction[
`step${opts.step}`
]()
console.log(`Please execute step ${opts.step}...`)
console.log(`MSD address: ${opts.SystemDictator.address}`)
printJsonTransaction(tx)
printCastCommand(tx)
await printTenderlySimulationLink(opts.SystemDictator.provider, tx)
}
// Wait for the step to complete.
await awaitCondition(
async () => {
return isStep(opts.SystemDictator, opts.step + 1)
},
30000,
1000
)
// Perform post-step checks.
await opts.checks()
}
/**
* Mini helper for executing a given phase.
*
* @param opts Options for executing the step.
* @param opts.isLiveDeployer True if the deployer is live.
* @param opts.SystemDictator SystemDictator contract.
* @param opts.step Step to execute.
* @param opts.message Message to print before executing the step.
* @param opts.checks Checks to perform after executing the step.
*/
export const doPhase = async (opts: {
isLiveDeployer?: boolean
SystemDictator: ethers.Contract
phase: number
message: string
checks: () => Promise<void>
}): Promise<void> => {
const isStart = await isStartOfPhase(opts.SystemDictator, opts.phase)
if (!isStart) {
console.log(`Start of phase ${opts.phase} already completed`)
return
}
// Extra message to help the user understand what's going on.
console.log(opts.message)
// Either automatically or manually execute the step.
if (opts.isLiveDeployer) {
console.log(`Executing phase ${opts.phase}...`)
await opts.SystemDictator[`phase${opts.phase}`]()
} else {
const tx = await opts.SystemDictator.populateTransaction[
`phase${opts.phase}`
]()
console.log(`Please execute phase ${opts.phase}...`)
console.log(`MSD address: ${opts.SystemDictator.address}`)
printJsonTransaction(tx)
await printTenderlySimulationLink(opts.SystemDictator.provider, tx)
}
// Wait for the step to complete.
await awaitCondition(
async () => {
return isStartOfPhase(opts.SystemDictator, opts.phase + 1)
},
30000,
1000
)
// Perform post-step checks.
await opts.checks()
}
/**
* Prints a direct link to a Tenderly simulation.
*
* @param provider Ethers Provider.
* @param tx Ethers transaction object.
*/
export const printTenderlySimulationLink = async (
provider: ethers.providers.Provider,
tx: ethers.PopulatedTransaction
): Promise<void> => {
if (process.env.TENDERLY_PROJECT && process.env.TENDERLY_USERNAME) {
console.log(
`https://dashboard.tenderly.co/${process.env.TENDERLY_PROJECT}/${
process.env.TENDERLY_USERNAME
}/simulator/new?${new URLSearchParams({
network: (await provider.getNetwork()).chainId.toString(),
contractAddress: tx.to,
rawFunctionInput: tx.data,
from: tx.from,
}).toString()}`
)
}
}
/**
* Prints a cast commmand for submitting a given transaction.
*
* @param tx Ethers transaction object.
*/
export const printCastCommand = (tx: ethers.PopulatedTransaction): void => {
if (process.env.CAST_COMMANDS) {
if (!!tx.value && tx.value.gt(0)) {
console.log(
`cast send ${tx.to} ${tx.data} --from ${tx.from} --value ${tx.value}`
)
} else {
console.log(`cast send ${tx.to} ${tx.data} --from ${tx.from} `)
}
}
}