diff --git a/packages/contracts/src/chugsplash/actions.ts b/packages/contracts/src/chugsplash/actions.ts index 66fdc7600fc..180e79a1e9e 100644 --- a/packages/contracts/src/chugsplash/actions.ts +++ b/packages/contracts/src/chugsplash/actions.ts @@ -1,4 +1,4 @@ -/* External Imports */ +/* Imports: External */ import { fromHexString, toHexString } from '@eth-optimism/core-utils' import { ethers } from 'ethers' import MerkleTree from 'merkletreejs' diff --git a/packages/contracts/src/chugsplash/config.ts b/packages/contracts/src/chugsplash/config.ts index 2667dc75247..0a902bc2af6 100644 --- a/packages/contracts/src/chugsplash/config.ts +++ b/packages/contracts/src/chugsplash/config.ts @@ -3,6 +3,7 @@ import * as Handlebars from 'handlebars' import { ethers } from 'ethers' type SolidityVariable = + | boolean | string | number | Array diff --git a/packages/contracts/src/chugsplash/hardhat-tools.ts b/packages/contracts/src/chugsplash/hardhat-tools.ts new file mode 100644 index 00000000000..719dc8ded11 --- /dev/null +++ b/packages/contracts/src/chugsplash/hardhat-tools.ts @@ -0,0 +1,56 @@ +/* Imports: External */ +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +/* Imports: Internal */ +import { computeStorageSlots, getStorageLayout } from './storage' +import { ChugSplashConfig, parseChugSplashConfig } from './config' +import { + ChugSplashAction, + ChugSplashActionBundle, + getChugSplashActionBundle, +} from './actions' + +/** + * Generates a ChugSplash action bundle from a config file. + * @param hre Hardhat runtime environment, used to load artifacts + storage layouts. + * @param config Config file to convert into a bundle. + * @param env Environment variables to inject into the config file. + * @returns Action bundle generated from the parsed config file. + */ +export const makeActionBundleFromConfig = async ( + hre: HardhatRuntimeEnvironment, + config: ChugSplashConfig, + env: { + [key: string]: string | number | boolean + } = {} +): Promise => { + // Parse the config to replace any template variables. + const parsed = parseChugSplashConfig(config, env) + + const actions: ChugSplashAction[] = [] + for (const [contractName, contractConfig] of Object.entries( + parsed.contracts + )) { + const artifact = hre.artifacts.readArtifactSync(contractConfig.source) + const storageLayout = await getStorageLayout(hre, contractConfig.source) + + // Add a SET_CODE action for each contract first. + actions.push({ + target: contractConfig.address, + code: artifact.deployedBytecode, + }) + + // Add SET_STORAGE actions for each storage slot that we want to modify. + const slots = computeStorageSlots(storageLayout, contractConfig.variables) + for (const slot of slots) { + actions.push({ + target: contractConfig.address, + key: slot.key, + value: slot.val, + }) + } + } + + // Generate a bundle from the list of actions. + return getChugSplashActionBundle(actions) +} diff --git a/packages/contracts/src/chugsplash/index.ts b/packages/contracts/src/chugsplash/index.ts index 8bafc869d65..1d288f94bb2 100644 --- a/packages/contracts/src/chugsplash/index.ts +++ b/packages/contracts/src/chugsplash/index.ts @@ -1,3 +1,4 @@ export * from './actions' export * from './config' export * from './storage' +export * from './hardhat-tools' diff --git a/packages/contracts/test/chugsplash/hardhat-tools.spec.ts b/packages/contracts/test/chugsplash/hardhat-tools.spec.ts new file mode 100644 index 00000000000..3843bc4541c --- /dev/null +++ b/packages/contracts/test/chugsplash/hardhat-tools.spec.ts @@ -0,0 +1,326 @@ +import { expect } from '../setup' + +/* Imports: External */ +import hre from 'hardhat' +import { ethers } from 'ethers' +import { remove0x } from '@eth-optimism/core-utils' + +/* Imports: Internal */ +import { + ChugSplashActionType, + getContractDefinition, + makeActionBundleFromConfig, +} from '../../src' +import { NON_NULL_BYTES32, NON_ZERO_ADDRESS } from '../helpers' + +describe('ChugSplash hardhat tooling', () => { + describe('makeActionBundleFromConfig', () => { + it('should make a bundle from config with one contract and no variables', async () => { + const bundle = await makeActionBundleFromConfig(hre, { + contracts: { + MyContract: { + address: `0x${'11'.repeat(20)}`, + source: 'Helper_StorageHelper', + variables: {}, + }, + }, + }) + + expect(bundle.actions.length).to.equal(1) + expect(bundle.actions[0].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_CODE, + target: `0x${'11'.repeat(20)}`, + data: getContractDefinition('Helper_StorageHelper').deployedBytecode, + }) + }) + + it('should make a bundle from config with two contracts and no variables', async () => { + const bundle = await makeActionBundleFromConfig(hre, { + contracts: { + MyContract1: { + address: `0x${'11'.repeat(20)}`, + source: 'Helper_StorageHelper', + variables: {}, + }, + MyContract2: { + address: `0x${'22'.repeat(20)}`, + source: 'Helper_StorageHelper', + variables: {}, + }, + }, + }) + + expect(bundle.actions.length).to.equal(2) + expect(bundle.actions[0].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_CODE, + target: `0x${'11'.repeat(20)}`, + data: getContractDefinition('Helper_StorageHelper').deployedBytecode, + }) + expect(bundle.actions[1].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_CODE, + target: `0x${'22'.repeat(20)}`, + data: getContractDefinition('Helper_StorageHelper').deployedBytecode, + }) + }) + + it('should make a bundle from config with one contract with variables', async () => { + const bundle = await makeActionBundleFromConfig(hre, { + contracts: { + MyContract1: { + address: `0x${'11'.repeat(20)}`, + source: 'Helper_StorageHelper', + variables: { + _uint8: 123, + _bytes32: NON_NULL_BYTES32, + }, + }, + }, + }) + + expect(bundle.actions.length).to.equal(3) + expect(bundle.actions[0].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_CODE, + target: `0x${'11'.repeat(20)}`, + data: getContractDefinition('Helper_StorageHelper').deployedBytecode, + }) + expect(bundle.actions[1].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_STORAGE, + target: `0x${'11'.repeat(20)}`, + data: ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'bytes32'], + [ + ethers.constants.HashZero, + '0x000000000000000000000000000000000000000000000000000000000000007b', + ] + ), + }) + expect(bundle.actions[2].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_STORAGE, + target: `0x${'11'.repeat(20)}`, + data: ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'bytes32'], + [ + '0x000000000000000000000000000000000000000000000000000000000000000a', + NON_NULL_BYTES32, + ] + ), + }) + }) + + it('should make a bundle from config with two contracts with variables', async () => { + const bundle = await makeActionBundleFromConfig(hre, { + contracts: { + MyContract1: { + address: `0x${'11'.repeat(20)}`, + source: 'Helper_StorageHelper', + variables: { + _uint8: 123, + _bytes32: NON_NULL_BYTES32, + }, + }, + MyContract2: { + address: `0x${'22'.repeat(20)}`, + source: 'Helper_StorageHelper', + variables: { + _address: NON_ZERO_ADDRESS, + _bool: true, + }, + }, + }, + }) + + expect(bundle.actions.length).to.equal(6) + expect(bundle.actions[0].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_CODE, + target: `0x${'11'.repeat(20)}`, + data: getContractDefinition('Helper_StorageHelper').deployedBytecode, + }) + expect(bundle.actions[1].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_STORAGE, + target: `0x${'11'.repeat(20)}`, + data: ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'bytes32'], + [ + ethers.constants.HashZero, + '0x000000000000000000000000000000000000000000000000000000000000007b', + ] + ), + }) + expect(bundle.actions[2].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_STORAGE, + target: `0x${'11'.repeat(20)}`, + data: ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'bytes32'], + [ + '0x000000000000000000000000000000000000000000000000000000000000000a', + NON_NULL_BYTES32, + ] + ), + }) + expect(bundle.actions[3].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_CODE, + target: `0x${'22'.repeat(20)}`, + data: getContractDefinition('Helper_StorageHelper').deployedBytecode, + }) + expect(bundle.actions[4].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_STORAGE, + target: `0x${'22'.repeat(20)}`, + data: ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'bytes32'], + [ + '0x000000000000000000000000000000000000000000000000000000000000000e', + `0x000000000000000000000000${remove0x(NON_ZERO_ADDRESS)}`, + ] + ), + }) + expect(bundle.actions[5].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_STORAGE, + target: `0x${'22'.repeat(20)}`, + data: ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'bytes32'], + [ + '0x000000000000000000000000000000000000000000000000000000000000000c', + '0x0000000000000000000000000000000000000000000000000000000000000001', + ] + ), + }) + }) + + it('should make a bundle from config with one contract and templated variables', async () => { + const bundle = await makeActionBundleFromConfig( + hre, + { + contracts: { + MyContract1: { + address: `0x${'11'.repeat(20)}`, + source: 'Helper_StorageHelper', + variables: { + _uint8: `{{ env.MY_UINT8_VALUE }}`, + _bytes32: `{{ env.MY_BYTES32_VALUE }}`, + }, + }, + }, + }, + { + MY_UINT8_VALUE: 123, + MY_BYTES32_VALUE: NON_NULL_BYTES32, + } + ) + + expect(bundle.actions.length).to.equal(3) + expect(bundle.actions[0].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_CODE, + target: `0x${'11'.repeat(20)}`, + data: getContractDefinition('Helper_StorageHelper').deployedBytecode, + }) + expect(bundle.actions[1].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_STORAGE, + target: `0x${'11'.repeat(20)}`, + data: ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'bytes32'], + [ + ethers.constants.HashZero, + '0x000000000000000000000000000000000000000000000000000000000000007b', + ] + ), + }) + expect(bundle.actions[2].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_STORAGE, + target: `0x${'11'.repeat(20)}`, + data: ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'bytes32'], + [ + '0x000000000000000000000000000000000000000000000000000000000000000a', + NON_NULL_BYTES32, + ] + ), + }) + }) + + it('should make a bundle from config with two contracts with variables and templated variables', async () => { + const bundle = await makeActionBundleFromConfig( + hre, + { + contracts: { + MyContract1: { + address: `0x${'11'.repeat(20)}`, + source: 'Helper_StorageHelper', + variables: { + _uint8: 123, + _bytes32: NON_NULL_BYTES32, + }, + }, + MyContract2: { + address: `0x${'22'.repeat(20)}`, + source: 'Helper_StorageHelper', + variables: { + _address: `{{ env.MY_ADDRESS_VALUE }}`, + _bool: `{{ env.MY_BOOLEAN_VALUE }}`, + }, + }, + }, + }, + { + MY_ADDRESS_VALUE: NON_ZERO_ADDRESS, + MY_BOOLEAN_VALUE: true, + } + ) + + expect(bundle.actions.length).to.equal(6) + expect(bundle.actions[0].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_CODE, + target: `0x${'11'.repeat(20)}`, + data: getContractDefinition('Helper_StorageHelper').deployedBytecode, + }) + expect(bundle.actions[1].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_STORAGE, + target: `0x${'11'.repeat(20)}`, + data: ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'bytes32'], + [ + ethers.constants.HashZero, + '0x000000000000000000000000000000000000000000000000000000000000007b', + ] + ), + }) + expect(bundle.actions[2].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_STORAGE, + target: `0x${'11'.repeat(20)}`, + data: ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'bytes32'], + [ + '0x000000000000000000000000000000000000000000000000000000000000000a', + NON_NULL_BYTES32, + ] + ), + }) + expect(bundle.actions[3].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_CODE, + target: `0x${'22'.repeat(20)}`, + data: getContractDefinition('Helper_StorageHelper').deployedBytecode, + }) + expect(bundle.actions[4].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_STORAGE, + target: `0x${'22'.repeat(20)}`, + data: ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'bytes32'], + [ + '0x000000000000000000000000000000000000000000000000000000000000000e', + `0x000000000000000000000000${remove0x(NON_ZERO_ADDRESS)}`, + ] + ), + }) + expect(bundle.actions[5].action).to.deep.equal({ + actionType: ChugSplashActionType.SET_STORAGE, + target: `0x${'22'.repeat(20)}`, + data: ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'bytes32'], + [ + '0x000000000000000000000000000000000000000000000000000000000000000c', + '0x0000000000000000000000000000000000000000000000000000000000000001', + ] + ), + }) + }) + }) +})