diff --git a/integration-tests/test/ovm-self-upgrades.spec.ts b/integration-tests/test/ovm-self-upgrades.spec.ts index 2f07ddc38fe9c..d0461517ef256 100644 --- a/integration-tests/test/ovm-self-upgrades.spec.ts +++ b/integration-tests/test/ovm-self-upgrades.spec.ts @@ -1,104 +1,84 @@ import { expect } from 'chai' -import { Wallet, utils, BigNumber, Contract } from 'ethers' -import { Direction } from './shared/watcher-utils' -import { OptimismEnv } from './shared/env' - -import { getContractInterface } from '@eth-optimism/contracts' -import { l2Provider, OVM_ETH_ADDRESS } from './shared/utils' +/* Imports: External */ import { ethers } from 'hardhat' +import { Wallet, Contract } from 'ethers' +import { + getContractInterface, + getChugSplashActionBundle, + ChugSplashAction, + isSetStorageAction, + predeploys, +} from '@eth-optimism/contracts' + +/* Imports: Internal */ +import { OptimismEnv } from './shared/env' - -// TODO: use actual imported Chugsplash type - -interface SetCodeInstruction { - target: string // address - code: string // bytes memory -} - -interface SetStorageInstruction { - target: string // address - key: string // bytes32 - value: string // bytes32 -} - -type ChugsplashInstruction = SetCodeInstruction | SetStorageInstruction - -// Just an array of the above two instruction types. -type ChugSplashInstructions = Array - -const isSetStorageInstruction = (instr: ChugsplashInstruction): instr is SetStorageInstruction => { - return !instr["code"] -} - -describe('OVM Self-Upgrades', async () => { - let env: OptimismEnv - let l2Wallet: Wallet - let OVM_UpgradeExecutor: Contract - - const applyChugsplashInstructions = async (instructions: ChugSplashInstructions) => { - for (const instruction of instructions) { - let res - if (isSetStorageInstruction(instruction)) { - res = await OVM_UpgradeExecutor.setStorage( - instruction.target, - instruction.key, - instruction.value - ) - } else { - res = await OVM_UpgradeExecutor.setCode( - instruction.target, - instruction.code - ) - } - await res.wait() // TODO: promise.all +const executeAndVerifyChugSplashBundle = async ( + ChugSplashDeployer: Contract, + actions: ChugSplashAction[] +): Promise => { + const bundle = getChugSplashActionBundle(actions) + + const res1 = await ChugSplashDeployer.approveTransactionBundle( + bundle.root, + bundle.actions.length, + { + gasLimit: 8000000, + gasPrice: 0, } - } - - const checkChugsplashInstructionsApplied = async (instructions: ChugSplashInstructions) => { - for (const instruction of instructions) { - // TODO: promise.all this for with a map for efficiency - if (isSetStorageInstruction(instruction)) { - const actualStorage = await l2Provider.getStorageAt( - instruction.target, - instruction.key - ) - expect(actualStorage).to.deep.eq(instruction.value) - } else { - const actualCode = await l2Provider.getCode( - instruction.target - ) - expect(actualCode).to.deep.eq(instruction.code) + ) + await res1.wait() + + for (const rawAction of bundle.actions) { + const res2 = await ChugSplashDeployer.executeAction( + rawAction.action, + rawAction.proof, + { + gasPrice: 0, } + ) + await res2.wait() + + const action = actions[rawAction.proof.actionIndex] + if (isSetStorageAction(action)) { + expect( + await ChugSplashDeployer.provider.getStorageAt( + action.target, + action.key + ) + ).to.deep.equal(action.value) + } else { + expect( + await ChugSplashDeployer.provider.getCode(action.target) + ).to.deep.equal(action.code) } } +} - const applyAndVerifyUpgrade = async (instructions: ChugSplashInstructions) => { - await applyChugsplashInstructions(instructions) - await checkChugsplashInstructionsApplied(instructions) - } - +describe.only('OVM Self-Upgrades', async () => { + let env: OptimismEnv + let l2Wallet: Wallet + let ChugSplashDeployer: Contract before(async () => { env = await OptimismEnv.new() l2Wallet = env.l2Wallet - - OVM_UpgradeExecutor = new Contract( - '0x420000000000000000000000000000000000000a', - getContractInterface('OVM_UpgradeExecutor', true), + ChugSplashDeployer = new Contract( + predeploys.ChugSplashDeployer, + getContractInterface('ChugSplashDeployer', true), l2Wallet ) }) describe('setStorage and setCode are correctly applied', () => { it('Should execute a basic storage upgrade', async () => { - const basicStorageUpgrade: ChugSplashInstructions = [ + await executeAndVerifyChugSplashBundle(ChugSplashDeployer, [ { - target: OVM_ETH_ADDRESS, - key: '0x1234123412341234123412341234123412341234123412341234123412341234', - value: '0x6789123412341234123412341234123412341234123412341234678967896789', - } - ] - await applyAndVerifyUpgrade(basicStorageUpgrade) + target: predeploys.OVM_ETH, + key: `0x${'12'.repeat(32)}`, + value: `0x${'32'.repeat(32)}`, + }, + ]) }) it('Should execute a basic upgrade overwriting existing deployed code', async () => { @@ -107,25 +87,92 @@ describe('OVM Self-Upgrades', async () => { ).deploy() await DummyContract.deployTransaction.wait() - const basicCodeUpgrade: ChugSplashInstructions = [ + await executeAndVerifyChugSplashBundle(ChugSplashDeployer, [ { target: DummyContract.address, - code: '0x1234123412341234123412341234123412341234123412341234123412341234', - } - ] - await applyAndVerifyUpgrade(basicCodeUpgrade) + code: `0x${'12'.repeat(32)}`, + }, + ]) }) it('Should execute a basic code upgrade which is not overwriting an existing account', async () => { - // TODO: fix me? Currently breaks due to nil pointer dereference; triggerd by evm.StateDB.SetCode(...) in ovm_state_manager.go ? - // More recent update: I cannot get this to error out any more. - const emptyAccountCodeUpgrade: ChugSplashInstructions = [ + await executeAndVerifyChugSplashBundle(ChugSplashDeployer, [ + { + target: `0x${'56'.repeat(20)}`, + code: `0x${'12'.repeat(32)}`, + }, + ]) + }) + + it('should set code and set storage in the same bundle', async () => { + await executeAndVerifyChugSplashBundle(ChugSplashDeployer, [ + { + target: `0x${'56'.repeat(20)}`, + code: `0x${'12'.repeat(32)}`, + }, + { + target: `0x${'56'.repeat(20)}`, + key: `0x${'12'.repeat(32)}`, + value: `0x${'12'.repeat(32)}`, + }, + ]) + }) + + it('should set code multiple times in the same bundle', async () => { + await executeAndVerifyChugSplashBundle(ChugSplashDeployer, [ + { + target: `0x${'56'.repeat(20)}`, + code: `0x${'12'.repeat(32)}`, + }, + { + target: `0x${'56'.repeat(20)}`, + code: `0x${'34'.repeat(32)}`, + }, + { + target: `0x${'56'.repeat(20)}`, + code: `0x${'56'.repeat(32)}`, + }, + ]) + }) + + it('should set storage multiple times in the same bundle', async () => { + await executeAndVerifyChugSplashBundle(ChugSplashDeployer, [ + { + target: `0x${'56'.repeat(20)}`, + key: `0x${'12'.repeat(32)}`, + value: `0x${'12'.repeat(32)}`, + }, + { + target: `0x${'56'.repeat(20)}`, + key: `0x${'34'.repeat(32)}`, + value: `0x${'12'.repeat(32)}`, + }, + { + target: `0x${'56'.repeat(20)}`, + key: `0x${'56'.repeat(32)}`, + value: `0x${'12'.repeat(32)}`, + }, + ]) + }) + + it.skip('should set storage multiple times with different addresses in the same bundle', async () => { + await executeAndVerifyChugSplashBundle(ChugSplashDeployer, [ + { + target: `0x${'57'.repeat(20)}`, + key: `0x${'12'.repeat(32)}`, + value: `0x${'12'.repeat(32)}`, + }, + { + target: `0x${'58'.repeat(20)}`, + key: `0x${'34'.repeat(32)}`, + value: `0x${'12'.repeat(32)}`, + }, { - target: '0x5678657856785678567856785678567856785678', - code: '0x1234123412341234123412341234123412341234123412341234123412341234', - } - ] - await applyAndVerifyUpgrade(emptyAccountCodeUpgrade) + target: `0x${'59'.repeat(20)}`, + key: `0x${'56'.repeat(32)}`, + value: `0x${'12'.repeat(32)}`, + }, + ]) }) }) }) diff --git a/integration-tests/test/ovmcontext.spec.ts b/integration-tests/test/ovmcontext.spec.ts index 1f633a5c487e3..16640b974b1f8 100644 --- a/integration-tests/test/ovmcontext.spec.ts +++ b/integration-tests/test/ovmcontext.spec.ts @@ -105,16 +105,21 @@ describe('OVM Context: Layer 2 EVM Context', () => { for (let i = start; i < tip.number; i++) { const block = await L2Provider.getBlockWithTransactions(i) - const [, returnData] = await OVMMulticall.callStatic.aggregate([ + const [, returnData] = await OVMMulticall.callStatic.aggregate( [ - OVMMulticall.address, - OVMMulticall.interface.encodeFunctionData('getCurrentBlockTimestamp'), + [ + OVMMulticall.address, + OVMMulticall.interface.encodeFunctionData( + 'getCurrentBlockTimestamp' + ), + ], + [ + OVMMulticall.address, + OVMMulticall.interface.encodeFunctionData('getCurrentBlockNumber'), + ], ], - [ - OVMMulticall.address, - OVMMulticall.interface.encodeFunctionData('getCurrentBlockNumber'), - ], - ], {blockTag: i}) + { blockTag: i } + ) const timestamp = BigNumber.from(returnData[0]) const blockNumber = BigNumber.from(returnData[1]) diff --git a/packages/contracts/chugsplash-deploy/deploy-l2.json b/packages/contracts/chugsplash-deploy/deploy-l2.json new file mode 100644 index 0000000000000..88943474fc625 --- /dev/null +++ b/packages/contracts/chugsplash-deploy/deploy-l2.json @@ -0,0 +1,59 @@ +{ + "contracts": { + "OVM_L2ToL1MessagePasser": { + "address": "0x4200000000000000000000000000000000000000", + "source": "OVM_L2ToL1MessagePasser" + }, + "OVM_L1MessageSender": { + "address": "0x4200000000000000000000000000000000000001", + "source": "OVM_L1MessageSender" + }, + "OVM_DeployerWhitelist": { + "address": "0x4200000000000000000000000000000000000002", + "source": "OVM_DeployerWhitelist", + "variables": { + "owner": "{{ env.DEPLOYER_WHITELIST_OWNER }}" + } + }, + "OVM_ECDSAContractAccount": { + "address": "0x4200000000000000000000000000000000000003", + "source": "OVM_ECDSAContractAccount" + }, + "OVM_SequencerEntrypoint": { + "address": "0x4200000000000000000000000000000000000005", + "source": "OVM_SequencerEntrypoint" + }, + "OVM_ETH": { + "address": "0x4200000000000000000000000000000000000006", + "source": "OVM_ETH", + "variables": { + "l1TokenGateway": "{{ env.L1_ETH_GATEWAY_ADDRESS }}", + "messenger": "{{ contracts.OVM_L2CrossDomainMessenger }}", + "name": "Ether", + "symbol": "ETH" + } + }, + "OVM_L2CrossDomainMessenger": { + "address": "0x4200000000000000000000000000000000000007", + "source": "OVM_L2CrossDomainMessenger", + "variables": { + "libAddressManager": "{{ contracts.Lib_AddressManager }}" + } + }, + "OVM_ProxyEOA": { + "address": "0x4200000000000000000000000000000000000009", + "source": "OVM_ProxyEOA" + }, + "Lib_AddressManager": { + "address": "0x4200000000000000000000000000000000000008", + "source": "Lib_AddressManager", + "variables": { + "_owner": "0x1212121212121212121212121212121212121212" + } + }, + "ERC1820Registry": { + "address": "0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24", + "source": "ERC1820Registry" + } + } +} diff --git a/packages/contracts/contracts/chugsplash/L2/ChugSplashDeployer.sol b/packages/contracts/contracts/chugsplash/L2/ChugSplashDeployer.sol new file mode 100644 index 0000000000000..b37be7d4bba56 --- /dev/null +++ b/packages/contracts/contracts/chugsplash/L2/ChugSplashDeployer.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: MIT +// @unsupported: evm +pragma solidity >0.5.0 <0.8.0; +pragma experimental ABIEncoderV2; + +/* Library Imports */ +import { Lib_ExecutionManagerWrapper } from "../../optimistic-ethereum/libraries/wrappers/Lib_ExecutionManagerWrapper.sol"; +import { Lib_MerkleTree } from "../../optimistic-ethereum/libraries/utils/Lib_MerkleTree.sol"; + +/** + * @title ChugSplashDeployer + */ +contract ChugSplashDeployer { + + /********* + * Enums * + *********/ + + enum ActionType { + SET_CODE, + SET_STORAGE + } + + + /*********** + * Structs * + ***********/ + + struct ChugSplashAction { + ActionType actionType; + address target; + bytes data; + } + + struct ChugSplashActionProof { + uint256 actionIndex; + bytes32[] siblings; + } + + + /************* + * Variables * + *************/ + + // Address that can approve new transaction bundles. + address public owner; + bytes32 public currentBundleHash; + uint256 public currentBundleSize; + uint256 public currentBundleTxsExecuted; + + + /********************** + * Function Modifiers * + **********************/ + + /** + * Marks a function as only callable by the owner. + */ + modifier onlyOwner() { + // require( + // msg.sender == owner, + // "ChugSplashDeployer: sender is not owner" + // ); + _; + } + + + /******************** + * Public Functions * + ********************/ + + /** + * Changes the owner. Only callable by the current owner. + * @param _owner New owner address. + */ + function setOwner( + address _owner + ) + public + onlyOwner + { + owner = _owner; + } + + function hasActiveBundle() + public + view + returns ( + bool + ) + { + return ( + currentBundleHash != bytes32(0) + && currentBundleTxsExecuted < currentBundleSize + ); + } + + function approveTransactionBundle( + bytes32 _bundleHash, + uint256 _bundleSize + ) + public + onlyOwner + { + require( + hasActiveBundle() == false, + "ChugSplashDeployer: previous bundle has not yet been fully executed" + ); + + currentBundleHash = _bundleHash; + currentBundleSize = _bundleSize; + currentBundleTxsExecuted = 0; + + // TODO: Set system status to "upgrading". + } + + function executeAction( + ChugSplashAction memory _action, + ChugSplashActionProof memory _proof + ) + public + { + // TODO: Do we need to validate enums or does solidity do it for us? + // TODO: Do we need to check gas limit? + + require( + hasActiveBundle() == true, + "ChugSplashDeployer: there is no active bundle" + ); + + // Make sure that the owner did actually sign off on this action. + require( + Lib_MerkleTree.verify( + currentBundleHash, + keccak256( + abi.encode( + _action.actionType, + _action.target, + _action.data + ) + ), + _proof.actionIndex, + _proof.siblings, + currentBundleSize + ), + "ChugSplashDeployer: invalid action proof" + ); + + if (_action.actionType == ActionType.SET_CODE) { + // When the action is SET_CODE, we expect that the data is exactly the bytecode that + // the user wants to set the code to. + Lib_ExecutionManagerWrapper.ovmSETCODE( + _action.target, + _action.data + ); + } else { + // When the action is SET_STORAGE, we expect that the data is actually an ABI encoded + // key/value pair. So we'll need to decode that first. + (bytes32 key, bytes32 value) = abi.decode( + _action.data, + (bytes32, bytes32) + ); + + Lib_ExecutionManagerWrapper.ovmSETSTORAGE( + _action.target, + key, + value + ); + } + + currentBundleTxsExecuted++; + if (currentBundleSize == currentBundleTxsExecuted) { + currentBundleHash = bytes32(0); + // TODO: Set system status to "done upgrading/active". + } + } +} diff --git a/packages/contracts/contracts/optimistic-ethereum/OVM/predeploys/OVM_UpgradeExecutor.sol b/packages/contracts/contracts/optimistic-ethereum/OVM/predeploys/OVM_UpgradeExecutor.sol deleted file mode 100644 index f08d63d664296..0000000000000 --- a/packages/contracts/contracts/optimistic-ethereum/OVM/predeploys/OVM_UpgradeExecutor.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MIT -// @unsupported: evm -pragma solidity >0.5.0 <0.8.0; - -/* Library Imports */ -import { Lib_ExecutionManagerWrapper } from "../../libraries/wrappers/Lib_ExecutionManagerWrapper.sol"; - -/** - * @title OVM_UpgradeExecutor - * @dev The OVM_UpgradeExecutor is the contract which authenticates and executes (i.e. - * calls the relevant Execution Manager upgrade functions) upgrades to the OVM State. - * This enables us to update the predeploy and execution contracts directly from within - * L2 when there is a new release. - * - * Compiler used: optimistic-solc - * Runtime target: OVM - */ -contract OVM_UpgradeExecutor { - function setCode( - address _address, - bytes memory _code - ) - external - { - Lib_ExecutionManagerWrapper.ovmSETCODE( - _address, - _code - ); - } - - function setStorage( - address _address, - bytes32 _key, - bytes32 _value - ) - external - { - Lib_ExecutionManagerWrapper.ovmSETSTORAGE( - _address, - _key, - _value - ); - } -} diff --git a/packages/contracts/contracts/optimistic-ethereum/libraries/wrappers/Lib_ExecutionManagerWrapper.sol b/packages/contracts/contracts/optimistic-ethereum/libraries/wrappers/Lib_ExecutionManagerWrapper.sol index f0e2337215fee..84a0074d38b46 100644 --- a/packages/contracts/contracts/optimistic-ethereum/libraries/wrappers/Lib_ExecutionManagerWrapper.sol +++ b/packages/contracts/contracts/optimistic-ethereum/libraries/wrappers/Lib_ExecutionManagerWrapper.sol @@ -137,8 +137,10 @@ library Lib_ExecutionManagerWrapper { return abi.decode(returndata, (uint256)); } - /* - * Performs a safe ovmSETCODE call. + /** + * Calls the ovmSETCODE opcode. Only callable by the upgrade deployer. + * @param _address Address to set the code of. + * @param _code New code for the address. */ function ovmSETCODE( address _address, @@ -156,7 +158,10 @@ library Lib_ExecutionManagerWrapper { } /** - * Performs a safe ovmSETSTORAGE call. + * Calls the ovmSETSTORAGE opcode. Only callable by the upgrade deployer. + * @param _address Address to set a storage slot for. + * @param _key Storage slot key to modify. + * @param _value Storage slot value. */ function ovmSETSTORAGE( address _address, diff --git a/packages/contracts/package.json b/packages/contracts/package.json index f367d3cbdaaa0..a564e6ba5ced2 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -50,7 +50,8 @@ "@openzeppelin/contracts": "^3.3.0", "@typechain/hardhat": "^1.0.1", "ganache-core": "^2.13.2", - "glob": "^7.1.6" + "glob": "^7.1.6", + "merkletreejs": "^0.2.18" }, "devDependencies": { "@eth-optimism/hardhat-ovm": "^0.0.3", @@ -71,7 +72,6 @@ "hardhat-deploy": "^0.7.4", "lodash": "^4.17.20", "merkle-patricia-tree": "^4.0.0", - "merkletreejs": "^0.2.12", "mkdirp": "^1.0.4", "mocha": "^8.3.0", "random-bytes-seed": "^1.0.3", diff --git a/packages/contracts/src/chugsplash/config.ts b/packages/contracts/src/chugsplash/config.ts new file mode 100644 index 0000000000000..fb81b2d5fabd8 --- /dev/null +++ b/packages/contracts/src/chugsplash/config.ts @@ -0,0 +1,144 @@ +/* External Imports */ +import { cloneDeep, isPlainObject } from 'lodash' + +type SolidityVariable = + | string + | number + | Array + | { + [name: string]: SolidityVariable + } + +export interface ChugSplashConfig { + contracts: { + [name: string]: { + address?: string + source: string + variables?: { + [name: string]: SolidityVariable + } + } + } +} + +/** + * Parses any template strings found inside of a variable. Will parse recursively if the variable + * is an array or a plain object. Keys inside plain objects can also be templated in. + * @param variable Variable to replace. + * @param env Environment variables to inject into {{ env.X }} template strings. + * @param addresses Contract addresses to inject into {{ contract.X }} template strings. + * @returns Modified variable with template strings replaced. + */ +const parseVariable = ( + variable: SolidityVariable, + env: { + [name: string]: string + } = {}, + addresses: { + [name: string]: string + } = {} +): SolidityVariable => { + if (typeof variable === 'string') { + // "{{ }}" is a template string and needs to be replaced with the desired value. + const match = /{{ (.*?) }}/gm.exec(variable) + if (match && match.length == 2) { + if (match[1].startsWith('env.')) { + const templateKey = match[1].replace('env.', '') + const templateVal = env[templateKey] + if (templateVal === undefined) { + throw new Error( + `[chugsplash]: key does not exist in environment: ${templateKey}` + ) + } else { + return templateVal + } + } else if (match[1].startsWith('contracts.')) { + const templateKey = match[1].replace('contracts.', '') + const templateVal = addresses[templateKey] + if (templateVal === undefined) { + throw new Error( + `[chugsplash]: contract does not exist: ${templateKey}` + ) + } else { + return templateVal + } + } else { + throw new Error( + `[chugsplash]: unrecognized template string: ${variable}` + ) + } + } else { + return variable + } + } else if (Array.isArray(variable)) { + // Each array element gets parsed individually. + return variable.map((element) => { + return parseVariable(element, env, addresses) + }) + } else if (isPlainObject(variable)) { + // Parse the keys *and* values for objects. + variable = cloneDeep(variable) + for (const [key, val] of Object.entries(variable)) { + delete variable[key] // Make sure to delete the original key! + variable[parseVariable(key, env, addresses) as string] = parseVariable( + val, + env, + addresses + ) + } + return variable + } else { + // Anything else just gets returned as-is. + return variable + } +} + +// TODO: Change this when we break this logic out into its own package. +// const proxyArtifact = hre.artifacts.readArtifactSync('ChugSplashProxy') + +/** + * Replaces any template strings inside of a chugsplash config. + * @param config Config to update with template strings. + * @param env Environment variables to inject into {{ env.X }}. + * @returns Config with any template strings replaced. + */ +export const parseConfig = ( + config: ChugSplashConfig, + deployerAddress: string, + env: any = {} +): ChugSplashConfig => { + // TODO: Might want to do config validation here. + + // Make a copy of the config so that we can modify it without accidentally modifying the + // original object. + const parsed = cloneDeep(config) + + // Make sure this field is definitely defined. + parsed.contracts = parsed.contracts || {} + + // Generate a mapping of contract names to contract addresses. Used to inject values for + // {{ contract.X }} template strings. + const addresses = {} + for (const contractNickname of Object.keys(parsed.contracts)) { + addresses[contractNickname] = parsed.contracts[contractNickname].address + // ethers.utils.getCreate2Address( + // deployerAddress, + // ethers.utils.keccak256(ethers.utils.toUtf8Bytes(contractNickname)), + // ethers.utils.keccak256(proxyArtifact.bytecode) + // ) + } + + for (const [contractNickname, contractConfig] of Object.entries( + parsed.contracts + )) { + for (const [variableName, variableValue] of Object.entries( + contractConfig.variables || {} + )) { + parsed.contracts[contractNickname].variables[ + variableName + ] = parseVariable(variableValue, env, addresses) + } + } + + return parsed +} diff --git a/packages/contracts/src/chugsplash/core.ts b/packages/contracts/src/chugsplash/core.ts new file mode 100644 index 0000000000000..06f389cfd102e --- /dev/null +++ b/packages/contracts/src/chugsplash/core.ts @@ -0,0 +1,172 @@ +/* External Imports */ +import { fromHexString, toHexString } from '@eth-optimism/core-utils' +import { ethers } from 'ethers' +import MerkleTree from 'merkletreejs' + +/* Internal Imports */ +import { parseConfig } from './config' +import { computeStorageSlots, getStorageLayout } from './storage' + +export enum ChugSplashActionType { + SET_CODE, + SET_STORAGE, +} + +export interface RawChugSplashAction { + actionType: ChugSplashActionType + target: string + data: string +} + +export interface SetCodeAction { + target: string + code: string +} + +export interface SetStorageAction { + target: string + key: string + value: string +} + +export type ChugSplashAction = SetCodeAction | SetStorageAction + +export interface ChugSplashActionBundle { + root: string + actions: Array<{ + action: RawChugSplashAction + proof: { + actionIndex: number + siblings: string[] + } + }> +} + +export const isSetStorageAction = ( + action: ChugSplashAction +): action is SetStorageAction => { + return ( + (action as SetStorageAction).key !== undefined && + (action as SetStorageAction).value !== undefined + ) +} + +export const toRawChugSplashAction = ( + action: ChugSplashAction +): RawChugSplashAction => { + if (isSetStorageAction(action)) { + return { + actionType: ChugSplashActionType.SET_STORAGE, + target: action.target, + data: ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'bytes32'], + [action.key, action.value] + ), + } + } else { + return { + actionType: ChugSplashActionType.SET_CODE, + target: action.target, + data: action.code, + } + } +} + +export const getChugSplashActionBundle = ( + actions: ChugSplashAction[] +): ChugSplashActionBundle => { + const rawActions = actions.map((action) => { + return toRawChugSplashAction(action) + }) + + const getLeafHash = (action: RawChugSplashAction): string => { + return ethers.utils.keccak256( + ethers.utils.defaultAbiCoder.encode( + ['uint8', 'address', 'bytes'], + [action.actionType, action.target, action.data] + ) + ) + } + + const elements = rawActions.map((action) => { + return getLeafHash(action) + }) + + const filledElements = [] + for (let i = 0; i < Math.pow(2, Math.ceil(Math.log2(elements.length))); i++) { + if (i < elements.length) { + filledElements.push(elements[i]) + } else { + filledElements.push(ethers.utils.keccak256(ethers.constants.HashZero)) + } + } + + const bufs = filledElements.map((element) => { + return fromHexString(element) + }) + + const tree = new MerkleTree( + bufs, + (el: Buffer | string): Buffer => { + return fromHexString(ethers.utils.keccak256(el)) + } + ) + + return { + root: toHexString(tree.getRoot()), + actions: rawActions.map((action, idx) => { + return { + action: action, + proof: { + actionIndex: idx, + siblings: tree.getProof(getLeafHash(action), idx).map((element) => { + return element.data + }), + }, + } + }), + } +} + +export const getBundleFromConfig = async ( + hre: any, //HardhatRuntimeEnvironment, + deployment: string | any, + deployerAddress: string +): Promise => { + const config = parseConfig( + typeof deployment === 'string' ? require(deployment) : deployment, + deployerAddress, + process.env + ) + + const actions: ChugSplashAction[] = [] + for (const [contractNickname, contractConfig] of Object.entries( + config.contracts + )) { + const artifact = hre.artifacts.readArtifactSync(contractConfig.source) + const storageLayout = await getStorageLayout(hre, contractConfig.source) + + const target = + config.contracts[contractNickname].address || contractNickname + + // Push an action to deploy this contract. + actions.push({ + target: target, + code: artifact.deployedBytecode, + }) + + // Push a `SET_STORAGE` action for each storage slot that we need to set. + for (const slot of computeStorageSlots( + storageLayout, + contractConfig.variables + )) { + actions.push({ + target: target, + key: slot.key, + value: slot.val, + }) + } + } + + return getChugSplashActionBundle(actions) +} diff --git a/packages/contracts/src/chugsplash/index.ts b/packages/contracts/src/chugsplash/index.ts new file mode 100644 index 0000000000000..ea567d3e3994c --- /dev/null +++ b/packages/contracts/src/chugsplash/index.ts @@ -0,0 +1,3 @@ +export * from './config' +export * from './core' +export * from './storage' diff --git a/packages/contracts/src/chugsplash/storage.ts b/packages/contracts/src/chugsplash/storage.ts new file mode 100644 index 0000000000000..c1206f3dc63e1 --- /dev/null +++ b/packages/contracts/src/chugsplash/storage.ts @@ -0,0 +1,280 @@ +/* External Imports */ +import { remove0x } from '@eth-optimism/core-utils' +import { BigNumber, ethers } from 'ethers' + +// Represents the JSON objects outputted by the Solidity compiler that describe the structure of +// state within the contract. See +// https://docs.soliditylang.org/en/v0.8.3/internals/layout_in_storage.html for more information. +interface SolidityStorageObj { + astId: number + contract: string + label: string + offset: number + slot: number + type: string +} + +// Represents the JSON objects outputted by the Solidity compiler that describe the types used for +// the various pieces of state in the contract. See +// https://docs.soliditylang.org/en/v0.8.3/internals/layout_in_storage.html for more information. +interface SolidityStorageType { + encoding: 'inplace' | 'mapping' | 'dynamic_array' | 'bytes' + label: string + numberOfBytes: number + key?: string + value?: string + base?: string + members?: SolidityStorageObj[] +} + +// Container object returned by the Solidity compiler. See +// https://docs.soliditylang.org/en/v0.8.3/internals/layout_in_storage.html for more information. +export interface SolidityStorageLayout { + storage: SolidityStorageObj[] + types: { + [name: string]: SolidityStorageType + } +} + +interface StorageSlotPair { + key: string + val: string +} + +export const getStorageLayout = async ( + hre: any, //HardhatRuntimeEnvironment, + name: string +): Promise => { + const { sourceName, contractName } = hre.artifacts.readArtifactSync(name) + const buildInfo = await hre.artifacts.getBuildInfo( + `${sourceName}:${contractName}` + ) + const output = buildInfo.output.contracts[sourceName][contractName] + + if (!('storageLayout' in output)) { + throw new Error( + `Storage layout for ${name} not found. Did you forget to set the storage layout compiler option in your hardhat config? Read more: https://github.com/ethereum-optimism/smock#note-on-using-smoddit` + ) + } + + return (output as any).storageLayout +} + +/** + * Encodes a single variable as a series of key/value storage slot pairs using some storage layout + * as instructions for how to perform this encoding. Works recursively with struct types. + * @param variable Variable to encode as key/value slot pairs. + * @param storageObj Solidity compiler JSON output describing the layout for this + * @param storageTypes Full list of storage types allowed for this encoding. + * @param nestedSlotOffset For nested data structures, keeps track of a value to be added onto the + * keys for nested values. + * @returns Variable encoded as a series of key/value slot pairs. + */ +const encodeVariable = ( + variable: any, + storageObj: SolidityStorageObj, + storageTypes: { + [name: string]: SolidityStorageType + }, + nestedSlotOffset = 0 +): Array => { + const variableType = storageTypes[storageObj.type] + + // Slot key will be the same no matter what so we can just compute it here. + const slotKey = + '0x' + + remove0x( + BigNumber.from( + parseInt(storageObj.slot as any, 10) + nestedSlotOffset + ).toHexString() + ) + .padStart(64 - storageObj.offset * 2, '0') + .padEnd(64, '0') + + if (variableType.encoding === 'inplace') { + if ( + variableType.label === 'address' || + variableType.label.startsWith('contract') + ) { + if (!ethers.utils.isAddress(variable)) { + throw new Error(`invalid address type: ${variable}`) + } + + // Addresses are right-aligned. + const slotVal = + '0x' + + remove0x(variable) + .padStart(64, '0') + .toLowerCase() + + return [ + { + key: slotKey, + val: slotVal, + }, + ] + } else if (variableType.label === 'bool') { + // Do some light parsing here to make sure "true" and "false" are recognized. + if (typeof variable === 'string') { + if (variable === 'false') { + variable = false + } + if (variable === 'true') { + variable = true + } + } + + if (typeof variable !== 'boolean') { + throw new Error(`invalid bool type: ${variable}`) + } + + // Booleans are right-aligned and represented as 0 or 1. + const slotVal = '0x' + (variable ? '1' : '0').padStart(64, '0') + + return [ + { + key: slotKey, + val: slotVal, + }, + ] + } else if (variableType.label.startsWith('bytes')) { + if (!ethers.utils.isHexString(variable, variableType.numberOfBytes)) { + throw new Error(`invalid bytesN type`) + } + + // BytesN are **left** aligned (eyeroll). + const slotVal = + '0x' + + remove0x(variable) + .padEnd(64, '0') + .toLowerCase() + + return [ + { + key: slotKey, + val: slotVal, + }, + ] + } else if (variableType.label.startsWith('uint')) { + if ( + remove0x(BigNumber.from(variable).toHexString()).length / 2 > + variableType.numberOfBytes + ) { + throw new Error( + `provided ${variableType.label} is too big: ${variable}` + ) + } + + // Uints are right aligned. + const slotVal = + '0x' + + remove0x(BigNumber.from(variable).toHexString()) + .padStart(64, '0') + .toLowerCase() + + return [ + { + key: slotKey, + val: slotVal, + }, + ] + } else if (variableType.label.startsWith('struct')) { + // Structs are encoded recursively, as defined by their `members` field. + let slots = [] + for (const [varName, varVal] of Object.entries(variable)) { + slots = slots.concat( + encodeVariable( + varVal, + variableType.members.find((member) => { + return member.label === varName + }), + storageTypes, + nestedSlotOffset + storageObj.slot + ) + ) + } + return slots + } + } else if (variableType.encoding === 'bytes') { + if (storageObj.offset !== 0) { + throw new Error(`offset not supported for string/bytes types`) + } + + // ref: https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#bytes-and-string + const strUtf8 = ethers.utils.toUtf8Bytes(variable) + if (strUtf8.length < 32) { + const slotVal = ethers.utils.hexlify( + ethers.utils.concat([ + ethers.utils + .concat([strUtf8, ethers.constants.HashZero]) + .slice(0, 31), + ethers.BigNumber.from(strUtf8.length * 2).toHexString(), + ]) + ) + + return [ + { + key: slotKey, + val: slotVal, + }, + ] + } else { + throw new Error('large strings (>31 bytes) not supported') + } + } else if (variableType.encoding === 'mapping') { + console.log(variable, variableType) + + throw new Error('mapping types not yet supported') + } else if (variableType.encoding === 'dynamic_array') { + throw new Error('array types not yet supported') + } else { + throw new Error( + `unknown unsupported type ${variableType.encoding} ${variableType.label}` + ) + } +} + +/** + * Computes the key/value storage slot pairs that would be used if a given set of variable values + * were applied to a given contract. + * @param storageLayout Solidity storage layout to use as a template for determining storage slots. + * @param variables Variable values to apply against the given storage layout. + * @returns An array of key/value storage slot pairs that would result in the desired state. + */ +export const computeStorageSlots = ( + storageLayout: SolidityStorageLayout, + variables: any = {} +): Array => { + let slots: StorageSlotPair[] = [] + for (const [variableName, variableValue] of Object.entries(variables)) { + // Find the entry in the storage layout that corresponds to this variable name. + const storageObj = storageLayout.storage.find((entry) => { + return entry.label === variableName + }) + + // Complain very loudly if attempting to set a variable that doesn't exist within this layout. + if (!storageObj) { + throw new Error( + `variable name not found in storage layout: ${variableName}` + ) + } + + // Encode this variable as series of storage slot key/value pairs and save it. + slots = slots.concat( + encodeVariable(variableValue, storageObj, storageLayout.types) + ) + } + + // TODO: Deal with packed storage slots. + + const seen = {} + for (const slot of slots) { + if (seen[slot.key]) { + throw new Error(`packed storage slots not supported`) + } else { + seen[slot.key] = true + } + } + + return slots +} diff --git a/packages/contracts/src/contract-deployment/config.ts b/packages/contracts/src/contract-deployment/config.ts index 8eaf7f3076f75..ea5f3365cc700 100644 --- a/packages/contracts/src/contract-deployment/config.ts +++ b/packages/contracts/src/contract-deployment/config.ts @@ -233,8 +233,8 @@ export const makeContractDeployConfig = async ( '0x0000000000000000000000000000000000000000', // will be overridden by geth when state dump is ingested. Storage key: 0x0000000000000000000000000000000000000000000000000000000000000008 ], }, - OVM_UpgradeExecutor: { - factory: getContractFactory('OVM_UpgradeExecutor', undefined, true), + ChugSplashDeployer: { + factory: getContractFactory('ChugSplashDeployer', undefined, true), params: [], }, 'OVM_ChainStorageContainer:CTC:batches': { diff --git a/packages/contracts/src/contract-dumps.ts b/packages/contracts/src/contract-dumps.ts index c42171f4cd987..53c678c5465fb 100644 --- a/packages/contracts/src/contract-dumps.ts +++ b/packages/contracts/src/contract-dumps.ts @@ -152,7 +152,7 @@ export const makeStateDump = async (cfg: RollupDeployConfig): Promise => { 'OVM_ExecutionManager', 'OVM_StateManager', 'OVM_ETH', - 'OVM_UpgradeExecutor', + 'ChugSplashDeployer', ], deployOverrides: {}, waitForReceipts: false, @@ -169,7 +169,7 @@ export const makeStateDump = async (cfg: RollupDeployConfig): Promise => { 'OVM_ETH', 'OVM_ECDSAContractAccount', 'OVM_ProxyEOA', - 'OVM_UpgradeExecutor', + 'ChugSplashDeployer', ] const deploymentResult = await deploy(config) diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index ba9a1959a5686..71c73b7258268 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -2,3 +2,4 @@ export * from './contract-defs' export { getLatestStateDump, StateDump } from './contract-dumps' export * from './contract-deployment' export * from './predeploys' +export * from './chugsplash' diff --git a/packages/contracts/src/predeploys.ts b/packages/contracts/src/predeploys.ts index ab9ad20caf43b..d658220246cda 100644 --- a/packages/contracts/src/predeploys.ts +++ b/packages/contracts/src/predeploys.ts @@ -17,6 +17,6 @@ export const predeploys = { OVM_L2CrossDomainMessenger: '0x4200000000000000000000000000000000000007', Lib_AddressManager: '0x4200000000000000000000000000000000000008', OVM_ProxyEOA: '0x4200000000000000000000000000000000000009', - OVM_UpgradeExecutor: '0x420000000000000000000000000000000000000a', + ChugSplashDeployer: '0x420000000000000000000000000000000000000a', ERC1820Registry: '0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24', } diff --git a/packages/contracts/test/contracts/OVM/execution/OVM_ExecutionManager/ovmSETCODE.spec.ts b/packages/contracts/test/contracts/OVM/execution/OVM_ExecutionManager/ovmSETCODE.spec.ts index c7a3bd40950db..f48c0ee531b93 100644 --- a/packages/contracts/test/contracts/OVM/execution/OVM_ExecutionManager/ovmSETCODE.spec.ts +++ b/packages/contracts/test/contracts/OVM/execution/OVM_ExecutionManager/ovmSETCODE.spec.ts @@ -13,7 +13,7 @@ import { } from '../../../../helpers' import { predeploys } from '../../../../../src/predeploys' -const UPGRADE_EXECUTOR_ADDRESS = predeploys.OVM_UpgradeExecutor +const UPGRADE_EXECUTOR_ADDRESS = predeploys.ChugSplashDeployer const UPGRADED_ADDRESS = '0x1234123412341234123412341234123412341234' const UPGRADED_CODE = '0x1234' const UPGRADED_CODEHASH = ethers.utils.keccak256(UPGRADED_CODE) diff --git a/packages/contracts/test/contracts/OVM/execution/OVM_ExecutionManager/ovmSETSTORAGE.spec.ts b/packages/contracts/test/contracts/OVM/execution/OVM_ExecutionManager/ovmSETSTORAGE.spec.ts index 85aa214386508..603cc40fb9df6 100644 --- a/packages/contracts/test/contracts/OVM/execution/OVM_ExecutionManager/ovmSETSTORAGE.spec.ts +++ b/packages/contracts/test/contracts/OVM/execution/OVM_ExecutionManager/ovmSETSTORAGE.spec.ts @@ -13,7 +13,7 @@ import { } from '../../../../helpers' import { predeploys } from '../../../../../src/predeploys' -const UPGRADE_EXECUTOR_ADDRESS = predeploys.OVM_UpgradeExecutor +const UPGRADE_EXECUTOR_ADDRESS = predeploys.ChugSplashDeployer const UPGRADED_ADDRESS = '0x1234123412341234123412341234123412341234' const sharedPreState = { diff --git a/packages/contracts/test/helpers/test-runner/test-runner.ts b/packages/contracts/test/helpers/test-runner/test-runner.ts index 7b7ba1874bc26..33a840847261f 100644 --- a/packages/contracts/test/helpers/test-runner/test-runner.ts +++ b/packages/contracts/test/helpers/test-runner/test-runner.ts @@ -221,8 +221,8 @@ export class ExecutionManagerTestRunner { ) await AddressManager.setAddress( - 'OVM_UpgradeExecutor', - predeploys.OVM_UpgradeExecutor + 'ChugSplashDeployer', + predeploys.ChugSplashDeployer ) const DeployerWhitelist = await getContractFactory( diff --git a/yarn.lock b/yarn.lock index 07d5a890722e9..7325c01a36de8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8706,7 +8706,7 @@ merkle-patricia-tree@^4.0.0, merkle-patricia-tree@^4.1.0: rlp "^2.2.3" semaphore-async-await "^1.5.1" -merkletreejs@^0.2.12, merkletreejs@^0.2.18: +merkletreejs@^0.2.18: version "0.2.18" resolved "https://registry.yarnpkg.com/merkletreejs/-/merkletreejs-0.2.18.tgz#205cc4f79e134c9bc887bdbd440b9e78787c4823" integrity sha512-f8bSFaUDPZhot94xkjb83XbG1URaiNLxZy6LWTw2IzbQeCA4YX/UxublGxXdLQIYXbWkDghq6EqwG5u4I7ELmA==