deploy-utils.ts 12.4 KB
Newer Older
1
import assert from 'assert'
2
import { URLSearchParams } from 'url'
3 4 5 6

import { ethers, Contract } from 'ethers'
import { Provider } from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer'
7
import { sleep } from '@eth-optimism/core-utils'
8
import { HardhatRuntimeEnvironment } from 'hardhat/types'
9
import { Deployment, DeployResult } from 'hardhat-deploy/dist/types'
10 11 12
import 'hardhat-deploy'
import '@eth-optimism/hardhat-deploy-config'
import '@nomiclabs/hardhat-ethers'
13

14 15 16 17 18 19 20 21 22 23 24 25 26
/**
 * 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 ({
27 28
  hre,
  name,
29
  iface,
30 31 32 33
  args,
  contract,
  postDeployAction,
}: {
34
  hre: HardhatRuntimeEnvironment
35 36 37 38 39
  name: string
  args: any[]
  contract?: string
  iface?: string
  postDeployAction?: (contract: Contract) => Promise<void>
40
}): Promise<Contract> => {
41 42
  const { deployer } = await hre.getNamedAccounts()

43 44 45
  // 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.
46
  let result: Deployment | DeployResult = await hre.deployments.getOrNull(name)
47 48 49 50 51 52 53 54 55

  // 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
  }

56 57 58 59 60 61 62 63
  if (result) {
    console.log(`skipping ${name}, using existing at ${result.address}`)
  } else {
    result = await hre.deployments.deploy(name, {
      contract,
      from: deployer,
      args,
      log: true,
64
      waitConfirmations: numDeployConfirmations,
65
    })
66
    console.log(`Deployed ${name} at ${result.address}`)
67 68 69
    // 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)
70 71
  }

72 73 74 75 76
  // 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}`)
  }
77

78
  // Create the contract object to return.
79
  const created = asAdvancedContract({
80
    confirmations: numDeployConfirmations,
81 82 83 84 85 86 87 88
    contract: new Contract(
      result.address,
      iface !== undefined
        ? (await hre.ethers.getContractFactory(iface)).interface
        : result.abi,
      hre.ethers.provider.getSigner(deployer)
    ),
  })
89

90 91 92 93
  // Run post-deploy actions if necessary.
  if ((result as DeployResult).newlyDeployed) {
    if (postDeployAction) {
      await postDeployAction(created)
94 95
    }
  }
96 97

  return created
98 99
}

100
/**
101 102 103
 * 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.
104 105 106 107 108 109
 *
 * @param opts Options for the contract.
 * @param opts.hre HardhatRuntimeEnvironment.
 * @param opts.contract Contract to wrap.
 * @returns Wrapped contract object.
 */
110
export const asAdvancedContract = (opts: {
111
  contract: Contract
112 113
  confirmations?: number
  gasPrice?: number
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
}): 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)
Mark Tyneway's avatar
Mark Tyneway committed
133 134 135 136 137 138 139
    ;(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
      }
140

Mark Tyneway's avatar
Mark Tyneway committed
141 142 143 144
      // Now actually trigger the transaction (or call).
      const tx = await fn(...args, {
        gasPrice,
      })
145

Mark Tyneway's avatar
Mark Tyneway committed
146 147 148 149
      // 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
      }
150

Mark Tyneway's avatar
Mark Tyneway committed
151 152 153 154 155 156 157 158 159 160 161
      // 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')
162
          }
Mark Tyneway's avatar
Mark Tyneway committed
163 164
        } else if (receipt.confirmations >= (opts.confirmations || 0)) {
          return tx
165 166
        }
      }
Mark Tyneway's avatar
Mark Tyneway committed
167
    }
168 169 170 171 172
  }

  return contract
}

173 174 175 176 177 178 179 180 181 182
/**
 * 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.
 */
183
export const getContractFromArtifact = async (
184
  hre: HardhatRuntimeEnvironment,
185
  name: string,
186
  opts: {
187 188 189 190 191 192 193 194 195
    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.
196 197
  if (opts.iface) {
    const factory = await hre.ethers.getContractFactory(opts.iface)
198 199 200 201
    iface = factory.interface
  }

  let signerOrProvider: Signer | Provider = hre.ethers.provider
202 203 204
  if (opts.signerOrProvider) {
    if (typeof opts.signerOrProvider === 'string') {
      signerOrProvider = hre.ethers.provider.getSigner(opts.signerOrProvider)
205
    } else {
206
      signerOrProvider = opts.signerOrProvider
207 208 209
    }
  }

210 211 212 213 214 215 216
  let numDeployConfirmations: number
  try {
    numDeployConfirmations = hre.deployConfig.numDeployConfirmations
  } catch (e) {
    numDeployConfirmations = 1
  }

217
  return asAdvancedContract({
218
    confirmations: numDeployConfirmations,
219 220 221 222 223 224 225 226
    contract: new hre.ethers.Contract(
      artifact.address,
      iface,
      signerOrProvider
    ),
  })
}

227 228 229 230 231 232 233
/**
 * 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.
 */
234
export const getContractsFromArtifacts = async (
235
  hre: HardhatRuntimeEnvironment,
236 237 238 239 240 241 242 243 244 245 246 247 248
  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
}

249 250 251 252 253 254 255
/**
 * 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.
 */
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
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,
  })

273 274 275 276 277 278 279 280
  if (ethers.utils.isAddress(expected)) {
    assert(
      actual.toLowerCase() === expected.toLowerCase(),
      `[FATAL] ${variable} is ${actual} but should be ${expected}`
    )
    return
  }

281 282 283 284 285 286
  assert(
    actual === expected || (actual.eq && actual.eq(expected)),
    `[FATAL] ${variable} is ${actual} but should be ${expected}`
  )
}

287 288 289 290 291 292 293
/**
 * 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.
 */
294
export const getDeploymentAddress = async (
295
  hre: HardhatRuntimeEnvironment,
296 297 298 299 300
  name: string
): Promise<string> => {
  const deployment = await hre.deployments.get(name)
  return deployment.address
}
301

302 303 304 305 306 307
/**
 * JSON-ifies an ethers transaction object.
 *
 * @param tx Ethers transaction object.
 * @returns JSON-ified transaction object.
 */
308 309 310 311 312 313 314 315 316 317 318 319 320 321
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
      )
Matthew Slipper's avatar
Matthew Slipper committed
322
  )
323
}
324

325
/**
326
 * Helper for transferring a Proxy to a target contract.
327 328 329 330
 *
 * @param opts Options for executing the step.
 * @param opts.isLiveDeployer True if the deployer is live.
 * @param opts.proxy proxy contract.
331
 * @param opts.target target contract.
332 333 334 335 336 337
 */
export const doOwnershipTransfer = async (opts: {
  isLiveDeployer?: boolean
  proxy: ethers.Contract
  name: string
  transferFunc: string
338
  target: ethers.Contract
339 340
}): Promise<void> => {
  if (opts.isLiveDeployer) {
341 342
    console.log(`Setting ${opts.name} owner to target ${opts.target.address}`)
    await opts.proxy[opts.transferFunc](opts.target.address)
343 344
  } else {
    const tx = await opts.proxy.populateTransaction[opts.transferFunc](
345
      opts.target.address
346 347 348 349
    )
    console.log(`
    Please transfer ${opts.name} (proxy) owner to MSD
      - ${opts.name} address: ${opts.proxy.address}
350
      - target address: ${opts.target.address}
351 352 353
    `)
    printJsonTransaction(tx)
    printCastCommand(tx)
354
    await printTenderlySimulationLink(opts.target.provider, tx)
355 356 357
  }
}

358 359 360 361
/**
 * Check if the script should submit the transaction or wait for the deployer to do it manually.
 *
 * @param hre HardhatRuntimeEnvironment.
362
 * @param ovveride Allow manually disabling live transaction submission. Useful for testing.
363 364 365 366 367 368 369
 * @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) {
370
    console.log('Live deployer manually disabled')
371
    return false
372 373
  }
  const { deployer } = await opts.hre.getNamedAccounts()
374
  const ret =
375 376 377 378 379
    deployer.toLowerCase() === opts.hre.deployConfig.controller.toLowerCase()
  console.log('Setting live deployer to', ret)
  return ret
}

380
/**
381
 * Prints a direct link to a Tenderly simulation.
382 383 384 385
 *
 * @param provider Ethers Provider.
 * @param tx Ethers transaction object.
 */
386
export const printTenderlySimulationLink = async (
387 388
  provider: ethers.providers.Provider,
  tx: ethers.PopulatedTransaction
389
): Promise<void> => {
390
  if (process.env.TENDERLY_PROJECT && process.env.TENDERLY_USERNAME) {
391 392 393 394 395 396 397 398 399 400
    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()}`
    )
401 402
  }
}
403 404

/**
405
 * Prints a cast commmand for submitting a given transaction.
406 407 408
 *
 * @param tx Ethers transaction object.
 */
409
export const printCastCommand = (tx: ethers.PopulatedTransaction): void => {
410
  if (process.env.CAST_COMMANDS) {
411 412 413 414 415 416 417
    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} `)
    }
418 419
  }
}