deploy-utils.ts 8.75 KB
Newer Older
1 2 3 4 5
import assert from 'assert'

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

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

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

58
  // Always wait for the transaction to be mined, just in case.
59 60
  await hre.ethers.provider.waitForTransaction(result.transactionHash)

61
  // Create the contract object to return.
62 63
  const created = asAdvancedContract({
    confirmations: hre.deployConfig.numDeployConfirmations,
64 65 66 67 68 69 70 71
    contract: new Contract(
      result.address,
      iface !== undefined
        ? (await hre.ethers.getContractFactory(iface)).interface
        : result.abi,
      hre.ethers.provider.getSigner(deployer)
    ),
  })
72

73 74 75 76
  // Run post-deploy actions if necessary.
  if ((result as DeployResult).newlyDeployed) {
    if (postDeployAction) {
      await postDeployAction(created)
77 78
    }
  }
79 80

  return created
81 82
}

83
/**
84 85 86
 * 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.
87 88 89 90 91 92
 *
 * @param opts Options for the contract.
 * @param opts.hre HardhatRuntimeEnvironment.
 * @param opts.contract Contract to wrap.
 * @returns Wrapped contract object.
 */
93
export const asAdvancedContract = (opts: {
94
  contract: Contract
95 96
  confirmations?: number
  gasPrice?: number
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
}): 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

  // Override each function call to also `.wait()` so as to simplify the deploy scripts' syntax.
  for (const fnName of Object.keys(contract.functions)) {
    const fn = contract[fnName].bind(contract)
    ;(contract as any)[fnName] = async (...args: any) => {
118 119 120
      // 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
121 122 123 124
      if (contract.interface.getFunction(fnName).constant) {
        gasPrice = 0
      }

125
      // Now actually trigger the transaction (or call).
126 127 128 129
      const tx = await fn(...args, {
        gasPrice,
      })

130
      // Meant for static calls, we don't need to wait for anything, we get the result right away.
131 132 133 134
      if (typeof tx !== 'object' || typeof tx.wait !== 'function') {
        return tx
      }

135 136
      // Wait for the transaction to be included in a block and wait for the specified number of
      // deployment confirmations.
137 138 139 140 141 142 143
      const maxTimeout = 120
      let timeout = 0
      while (true) {
        await sleep(1000)
        const receipt = await contract.provider.getTransactionReceipt(tx.hash)
        if (receipt === null) {
          timeout++
144 145
          if (timeout > maxTimeout) {
            throw new Error('timeout exceeded waiting for txn to be mined')
146
          }
147
        } else if (receipt.confirmations >= (opts.confirmations || 0)) {
148 149 150 151 152 153 154 155 156
          return tx
        }
      }
    }
  }

  return contract
}

157 158 159 160 161 162 163 164 165 166
/**
 * 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.
 */
167
export const getContractFromArtifact = async (
168
  hre: HardhatRuntimeEnvironment,
169
  name: string,
170
  opts: {
171 172 173 174 175 176 177 178 179 180
    iface?: string
    signerOrProvider?: Signer | Provider | string
  } = {}
): Promise<ethers.Contract> => {
  const artifact = await hre.deployments.get(name)
  await hre.ethers.provider.waitForTransaction(artifact.receipt.transactionHash)

  // Get the deployed contract's interface.
  let iface = new hre.ethers.utils.Interface(artifact.abi)
  // Override with optional iface name if requested.
181 182
  if (opts.iface) {
    const factory = await hre.ethers.getContractFactory(opts.iface)
183 184 185 186
    iface = factory.interface
  }

  let signerOrProvider: Signer | Provider = hre.ethers.provider
187 188 189
  if (opts.signerOrProvider) {
    if (typeof opts.signerOrProvider === 'string') {
      signerOrProvider = hre.ethers.provider.getSigner(opts.signerOrProvider)
190
    } else {
191
      signerOrProvider = opts.signerOrProvider
192 193 194
    }
  }

195 196
  return asAdvancedContract({
    confirmations: hre.deployConfig.numDeployConfirmations,
197 198 199 200 201 202 203 204
    contract: new hre.ethers.Contract(
      artifact.address,
      iface,
      signerOrProvider
    ),
  })
}

205 206 207 208 209 210 211
/**
 * 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.
 */
212
export const getContractsFromArtifacts = async (
213
  hre: HardhatRuntimeEnvironment,
214 215 216 217 218 219 220 221 222 223 224 225 226
  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
}

227 228 229 230 231 232 233
/**
 * 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.
 */
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
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,
  })

251 252 253 254 255 256 257 258
  if (ethers.utils.isAddress(expected)) {
    assert(
      actual.toLowerCase() === expected.toLowerCase(),
      `[FATAL] ${variable} is ${actual} but should be ${expected}`
    )
    return
  }

259 260 261 262 263 264
  assert(
    actual === expected || (actual.eq && actual.eq(expected)),
    `[FATAL] ${variable} is ${actual} but should be ${expected}`
  )
}

265 266 267 268 269 270 271
/**
 * 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.
 */
272
export const getDeploymentAddress = async (
273
  hre: HardhatRuntimeEnvironment,
274 275 276 277 278
  name: string
): Promise<string> => {
  const deployment = await hre.deployments.get(name)
  return deployment.address
}
279 280

export const jsonifyTransaction = (tx: ethers.PopulatedTransaction): string => {
Matthew Slipper's avatar
Matthew Slipper committed
281 282 283 284 285 286 287 288 289 290
  return JSON.stringify(
    {
      to: tx.to,
      data: tx.data,
      value: tx.value,
      chainId: tx.chainId,
    },
    null,
    2
  )
291
}