Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] feat: ChugSplash v0 #469

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
136 changes: 136 additions & 0 deletions packages/contracts/chugsplash-deploy/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import hre from 'hardhat'
import { cloneDeep, isPlainObject } from 'lodash'
import { ethers } from 'ethers'

type SolidityVariable =
| string
| number
| Array<SolidityVariable>
| {
[name: string]: SolidityVariable
}

export interface ChugSplashConfig {
contracts: {
[name: 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)
})
} 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) as string] = parseVariable(val, env)
}
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)

// 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(config.contracts || {})) {
addresses[contractNickname] = ethers.utils.getCreate2Address(
deployerAddress,
ethers.utils.keccak256(ethers.utils.toUtf8Bytes(contractNickname)),
ethers.utils.keccak256(proxyArtifact.bytecode)
)
}

for (const [contractNickname, contractConfig] of Object.entries(
config.contracts || {}
)) {
for (const [variableName, variableValue] of Object.entries(
contractConfig.variables || {}
)) {
parsed.contracts[contractNickname].variables[
variableName
] = parseVariable(variableValue, env, addresses)
}
}

return parsed
}
139 changes: 139 additions & 0 deletions packages/contracts/chugsplash-deploy/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import hre from 'hardhat'
import { Contract, ethers } from 'ethers'
import * as dotenv from 'dotenv'

import { parseConfig } from './config'
import { computeStorageSlots, SolidityStorageLayout } from './storage'

enum ChugSplashActionType {
SET_CODE,
SET_STORAGE,
}

interface ChugSplashAction {
type: ChugSplashActionType
target: string
data: string
}

export const getStorageLayout = async (
hre: any, //HardhatRuntimeEnvironment,
name: string
): Promise<SolidityStorageLayout> => {
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
}

export const getDeploymentBundle = async (
hre: any, //HardhatRuntimeEnvironment,
deploymentPath: string,
deployerAddress: string
): Promise<{
hash: string,
actions: ChugSplashAction[]
}> => {
const config = parseConfig(
require(deploymentPath),
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)

// Push an action to deploy this contract.
actions.push({
type: ChugSplashActionType.SET_CODE,
target: contractNickname,
data: 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({
type: ChugSplashActionType.SET_STORAGE,
target: contractNickname,
data: ethers.utils.defaultAbiCoder.encode(
['bytes32', 'bytes32'],
[slot.key, slot.val]
),
})
}
}

return {
hash: '0x' + 'FF'.repeat(32),
actions
}
}

export const createDeploymentManager = async (
hre: any, //HardhatRuntimeEnvironment,
owner: string
): Promise<Contract> => {
const factory = await hre.ethers.getContractFactory('ChugSplashDeployer')
const instance = await factory.deploy(owner)
await instance.deployTransaction.wait()
return instance
}

const main = async (hre: any) => {
dotenv.config()

const [owner] = await hre.ethers.getSigners()

// 1. Create a ChugSplashDeployer
const deployer = await createDeploymentManager(hre, await owner.getAddress())

// 2. Generate the bundle of actions (SET_CODE or SET_STORAGE)
const bundle = await getDeploymentBundle(
hre,
'./deployments/old-deploy.json',
deployer.address
)

// 3. Approve the bundle of actions.
await deployer.approveTransactionBundle(
bundle.hash,
bundle.actions.length
)

// 4. Execute the bundle of actions.
for (const action of bundle.actions) {
console.log(`Executing chugsplash action`)
console.log(`Target: ${action.target}`)
console.log(`Type: ${action.type === 0 ? 'SET_CODE' : 'SET_STORAGE'}`)
await deployer.executeAction(
action.type,
action.target,
action.data,
8_000_000 // TODO: how to handle gas?
)
}

// 5. Verify the correctness of the deployment?
}

// misc improvements:
// want to minimize the need to perform unnecessary actions
// want to be able to perform multiple actions at the same time

main(hre)
92 changes: 92 additions & 0 deletions packages/contracts/chugsplash-deploy/deployments/mainnet.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"contracts": {
"OVM_ChainStorageContainer:CTC:batches": {
"source": "OVM_ChainStorageContainer",
"variables": {
"owner": "{{ contracts.OVM_CanonicalTransactionChain }}"
}
},
"OVM_ChainStorageContainer:CTC:queue": {
"source": "OVM_ChainStorageContainer",
"variables": {
"owner": "{{ contracts.OVM_CanonicalTransactionChain }}"
}
},
"OVM_ChainStorageContainer:SCC:batches": {
"source": "OVM_ChainStorageContainer",
"variables": {
"owner": "{{ contracts.OVM_CanonicalTransactionChain }}"
}
},
"OVM_CanonicalTransactionChain": {
"source": "OVM_CanonicalTransactionChain",
"variables": {
"forceInclusionPeriodSeconds": "{{ env.CTC_FORCE_INCLUSION_PERIOD_SECONDS }}",
"forceInclusionPeriodBlocks": "{{ env.CTC_FORCE_INCLUSION_PERIOD_BLOCKS }}",
"maxTransactionGasLimit": "{{ env.CTC_MAX_TRANSACTION_GAS_LIMIT }}",
"batches": "{{ contracts.OVM_ChainStorageContainer:CTC:batches }}",
"queue": "{{ contracts.OVM_ChainStorageContainer:CTC:queue }}",
"ovmExecutionManager": "{{ contracts.OVM_ExecutionManager }}",
"ovmSequencer": "{{ env.SEQUENCER_ADDRESS }}",
"ovmSequencerEntrypoint": "0x4200000000000000000000000000000000000005"
}
},
"OVM_StateCommitmentChain": {
"source": "OVM_StateCommitmentChain",
"variables": {
"FRAUD_PROOF_WINDOW": "{{ env.SCC_FRAUD_PROOF_WINDOW }}",
"SEQUENCER_PUBLISH_WINDOW": "{{ env.SCC_SEQUENCER_PUBLISH_WINDOW }}",
"batches": "{{ contracts.OVM_ChainStorageContainer:SCC:batches }}",
"ovmBondManager": "{{ contracts.OVM_BondManager }}",
"ovmFraudVerifier": "{{ contracts.OVM_FraudVerifier }}",
"ovmCanonicalTransactionChain": "{{ contracts.OVM_CanonicalTransactionChain }}",
"ovmProposer": "{{ env.PROPOSER_ADDRESS }}"
}
},
"OVM_ExecutionManager": {
"source": "OVM_ExecutionManager",
"variables": {
"ovmSafetyChecker": "{{ contracts.OVM_SafetyChecker }}",
"gasMeterConfig": {
"minTransactionGasLimit": "{{ env.EM_MIN_TRANSACTION_GAS_LIMIT }}",
"maxTransactionGasLimit": "{{ env.EM_MAX_TRANSACTION_GAS_LIMIT }}",
"maxGasPerQueuePerEpoch": "{{ env.EM_MAX_GAS_PER_QUEUE_PER_EPOCH }}",
"secondsPerEpoch": "{{ env.EM_SECONDS_PER_EPOCH }}"
},
"globalContext": {
"ovmCHAINID": "{{ env.OVM_CHAIN_ID }}"
},
"transactionContext": {
"ovmNUMBER": "0xdefa017defa017defa017defa017defa017defa017defa017defa017defa017d"
}
}
},
"OVM_SafetyChecker": {
"source": "OVM_SafetyChecker"
},
"OVM_FraudVerifier": {
"source": "OVM_FraudVerifier",
"variables": {
"ovmStateCommitmentChain": "{{ contracts.OVM_StateCommitmentChain }}",
"ovmCanonicalTransactionChain": "{{ contracts.OVM_CanonicalTransactionChain }}",
"ovmStateTransitionerFactory": "{{ contracts.OVM_StateTransitionerFactory }}",
"ovmBondManager": "{{ contracts.OVM_BondManager }}"
}
},
"OVM_BondManager": {
"source": "mockOVM_BondManager"
},
"OVM_StateManagerFactory": {
"source": "OVM_StateManagerFactory"
},
"OVM_StateTransitionerFactory": {
"source": "OVM_StateTransitionerFactory"
},
"OVM_L1MultiMessageRelayer": {
"source": "OVM_L1MultiMessageRelayer"
},
"OVM_L1CrossDomainMessenger": {
"source": "OVM_L1CrossDomainMessenger"
}
}
}