Skip to content

Commit

Permalink
fix: use creation bytecode instead of the DEPLOY_CODE_PREFIX for et…
Browse files Browse the repository at this point in the history
…herscan compatibility
  • Loading branch information
sam-goldman committed Nov 7, 2022
1 parent c84eb6c commit 273d4c3
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 220 deletions.
7 changes: 7 additions & 0 deletions .changeset/spotty-rivers-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@chugsplash/contracts': patch
'@chugsplash/core': patch
'@chugsplash/plugins': patch
---

Use creation bytecode instead of the `DEPLOY_CODE_PREFIX` to deploy implementation contracts for Etherscan compatibility
42 changes: 10 additions & 32 deletions packages/contracts/contracts/ChugSplashManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ import { IProxyAdapter } from "./IProxyAdapter.sol";
import { ProxyUpdater } from "./ProxyUpdater.sol";
import { Create2 } from "./libraries/Create2.sol";
import { MerkleTree } from "./libraries/MerkleTree.sol";
import {
ReentrancyGuardUpgradeable
} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";

/**
* @title ChugSplashManager
*/
contract ChugSplashManager is OwnableUpgradeable {
contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable {
/**
* @notice Emitted when a ChugSplash bundle is proposed.
*
Expand Down Expand Up @@ -136,12 +139,6 @@ contract ChugSplashManager is OwnableUpgradeable {
bytes32 internal constant EIP1967_IMPLEMENTATION_KEY =
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

/**
* @notice "Magic" prefix. When prepended to some arbitrary runtime bytecode and used to create
* a contract, the appended bytecode will be deployed as given.
*/
bytes13 internal constant DEPLOY_CODE_PREFIX = 0x600D380380600D6000396000f3;

/**
* @notice Address of the ChugSplashRegistry.
*/
Expand Down Expand Up @@ -397,8 +394,9 @@ contract ChugSplashManager is OwnableUpgradeable {

/**
* @notice Executes a specific action within the current active bundle for a project. Actions
* can only be executed once. If executing this action would complete the bundle, will
* mark the bundle as completed and make it possible for a new bundle to be approved.
* can only be executed once. A re-entrancy guard is added to prevent an implementation
* contract's constructor from calling another contract which in turn calls back into
* this function.
*
* @param _action Action to execute.
* @param _actionIndex Index of the action in the bundle.
Expand All @@ -408,7 +406,7 @@ contract ChugSplashManager is OwnableUpgradeable {
ChugSplashAction memory _action,
uint256 _actionIndex,
bytes32[] memory _proof
) public {
) public nonReentrant {
uint256 initialGasLeft = gasleft();

require(
Expand Down Expand Up @@ -788,34 +786,14 @@ contract ChugSplashManager is OwnableUpgradeable {
* interacting with a proxy whose storage has not fully been initialized.
*
* @param _target Target that corresponds to the implementation.
* @param _code Runtime bytecode of the implementation to be deployed.
* @param _code Creation bytecode of the implementation contract.
*/
function _deployImplementation(string memory _target, bytes memory _code) internal {
// TODO: Add a re-entrancy guard to this function if we move away from using
// `DEPLOY_CODE_PREFIX`. There is currently no risk of re-entrancy because the prefix
// guarantees that no sub-calls can be made in the implementation contract's constructor. In
// the future, we might want to move away from the prefix to add support for constructors
// that can run arbitrary creation bytecode. It will then become become necessary to add a
// re-entrancy guard to prevent a constructor from calling another contract which in turn
// calls back into deployImplementation or setStorage.

// Create the deploycode by prepending the magic prefix to the runtime bytecode.
bytes memory deploycode = abi.encodePacked(DEPLOY_CODE_PREFIX, _code);

address implementation;
assembly {
implementation := create(0x0, add(deploycode, 0x20), mload(deploycode))
implementation := create2(0x0, add(_code, 0x20), mload(_code), 0x0)
}

// Check that the code was actually deployed correctly. It might be impossible to fail this
// check. Should only happen if the contract creation from above runs out of gas but this
// parent execution thread does NOT run out of gas. Seems like we should be doing this check
// anyway though.
require(
_getAccountCodeHash(implementation) == keccak256(_code),
"ChugSplashManager: code was not correctly deployed"
);

// Set the target to its newly deployed implementation.
implementations[_target] = implementation;
}
Expand Down
69 changes: 22 additions & 47 deletions packages/contracts/test/ChugSplashManager.t.sol

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions packages/core/src/config/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export const makeActionBundleFromConfig = async (
config: ChugSplashConfig,
artifacts: {
[name: string]: {
deployedBytecode: string
creationCode: string
storageLayout: SolidityStorageLayout
immutableVariables: string[]
}
Expand All @@ -162,7 +162,7 @@ export const makeActionBundleFromConfig = async (
// Add a DEPLOY_IMPLEMENTATION action for each contract first.
actions.push({
target: referenceName,
code: artifact.deployedBytecode,
code: artifact.creationCode,
})

// Next, add a SET_IMPLEMENTATION action for each contract.
Expand Down
177 changes: 47 additions & 130 deletions packages/plugins/src/hardhat/artifacts.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import path from 'path'

import * as semver from 'semver'
import {
SolidityStorageLayout,
ContractConfig,
ChugSplashConfig,
} from '@chugsplash/core'
import { add0x, remove0x } from '@eth-optimism/core-utils'
import { SolidityStorageLayout, ChugSplashConfig } from '@chugsplash/core'
import { remove0x } from '@eth-optimism/core-utils'
import { ethers, utils } from 'ethers'

// TODO
Expand Down Expand Up @@ -92,15 +88,45 @@ export const getStorageLayout = async (
return (output as any).storageLayout
}

export const generateRuntimeBytecode = async (
provider: ethers.providers.JsonRpcProvider,
export const getCreationCode = async (
parsedConfig: ChugSplashConfig,
referenceName: string
): Promise<string> => {
const contractConfig = parsedConfig.contracts[referenceName]
const { sourceName, contractName, bytecode, abi } = getContractArtifact(

const { bytecode } = getContractArtifact(contractConfig.contract)

const { constructorArgTypes, constructorArgValues } =
await getConstructorArgs(parsedConfig, referenceName)

const creationCodeWithConstructorArgs = bytecode.concat(
remove0x(
utils.defaultAbiCoder.encode(constructorArgTypes, constructorArgValues)
)
)

return creationCodeWithConstructorArgs
}

export const getConstructorArgs = async (
parsedConfig: ChugSplashConfig,
referenceName: string
): Promise<{ constructorArgTypes: any[]; constructorArgValues: any[] }> => {
const contractConfig = parsedConfig.contracts[referenceName]

const { sourceName, contractName, abi } = getContractArtifact(
contractConfig.contract
)

const constructorFragment = abi.find(
(fragment) => fragment.type === 'constructor'
)
const constructorArgTypes = []
const constructorArgValues = []
if (constructorFragment === undefined) {
return { constructorArgTypes, constructorArgValues }
}

const buildInfo = await getBuildInfo(sourceName, contractName)
const output = buildInfo.output.contracts[sourceName][contractName]
const immutableReferences: {
Expand All @@ -109,14 +135,6 @@ export const generateRuntimeBytecode = async (
start: number
}[]
} = output.evm.deployedBytecode.immutableReferences
const deployedBytecode = output.evm.deployedBytecode.object

if (Object.keys(immutableReferences).length === 0) {
return add0x(deployedBytecode)
}

// Maps a variable's AST ID to its ABI encoded value
const astIdToAbiEncodedValue = {}

// Maps a constructor argument name to the corresponding variable name in the ChugSplash config
const constructorArgNamesToImmutableNames = {}
Expand Down Expand Up @@ -146,79 +164,28 @@ export const generateRuntimeBytecode = async (
node.name
)
constructorArgNamesToImmutableNames[constructorArgName] = node.name

let typeString: string
if (node.typeDescriptions.typeString.startsWith('contract')) {
typeString = 'address'
} else if (node.typeDescriptions.typeString.startsWith('enum')) {
typeString = 'uint8'
} else {
typeString = node.typeDescriptions.typeString
}
const abiEncodedValue = utils.defaultAbiCoder.encode(
[typeString],
[contractConfig.variables[node.name]]
)
astIdToAbiEncodedValue[node.id] = remove0x(abiEncodedValue)
}
}
}
}
}

let bytecodeInjectedWithImmutables = deployedBytecode
for (const [astId, referenceArray] of Object.entries(immutableReferences)) {
for (const { start, length } of referenceArray) {
bytecodeInjectedWithImmutables = bytecodeInjectedWithImmutables
.substring(0, start * 2)
.concat(astIdToAbiEncodedValue[astId])
.concat(
bytecodeInjectedWithImmutables.substring(
start * 2 + length * 2,
bytecodeInjectedWithImmutables.length
)
)
}
}

const constructorFragment = abi.find(
(fragment) => fragment.type === 'constructor'
)
const constructorArgTypes = []
const constructorArgValues = []
constructorFragment.inputs.forEach((fragment) => {
constructorArgTypes.push(fragment.type)
if (constructorArgNamesToImmutableNames.hasOwnProperty(fragment.name)) {
constructorFragment.inputs.forEach((input) => {
constructorArgTypes.push(input.type)
if (constructorArgNamesToImmutableNames.hasOwnProperty(input.name)) {
constructorArgValues.push(
contractConfig.variables[
constructorArgNamesToImmutableNames[fragment.name]
constructorArgNamesToImmutableNames[input.name]
]
)
} else {
throw new Error(
`Detected a non-immutable constructor argument, "${fragment.name}", in ${contractConfig.contract}. Please remove it or make the corresponding variable immutable.`
`Detected a non-immutable constructor argument, "${input.name}", in ${contractConfig.contract}. Please remove it or make the corresponding variable immutable.`
)
}
})
const creationBytecodeWithConstructorArgs = bytecode.concat(
remove0x(
utils.defaultAbiCoder.encode(constructorArgTypes, constructorArgValues)
)
)
const bytecodeDeployedWithConstructorArgs = await provider.call({
data: creationBytecodeWithConstructorArgs,
})

if (
add0x(bytecodeInjectedWithImmutables) !==
bytecodeDeployedWithConstructorArgs
) {
throw new Error(
`ChugSplash cannot generate the deployed bytecode for ${contractConfig.contract}. Please report this error.`
)
}

return bytecodeDeployedWithConstructorArgs
return { constructorArgTypes, constructorArgValues }
}

export const getDeployedBytecode = async (
Expand All @@ -229,60 +196,6 @@ export const getDeployedBytecode = async (
return deployedBytecode
}

export const getConstructorArgValues = async (
contractConfig: ContractConfig
): Promise<any[]> => {
const { sourceName, contractName, abi } = getContractArtifact(
contractConfig.contract
)
const constructorFragment = abi.find(
(fragment) => fragment.type === 'constructor'
)
if (
constructorFragment === undefined ||
constructorFragment.inputs.length === 0
) {
return []
}
const buildInfo = await getBuildInfo(sourceName, contractName)

// Maps a constructor argument name to the corresponding variable name in the ChugSplash config
const constructorArgNamesToImmutableNames = {}

for (const source of Object.values(buildInfo.output.sources)) {
for (const contractNode of (source as any).ast.nodes) {
if (contractNode.nodeType === 'ContractDefinition') {
for (const node of contractNode.nodes) {
if (
node.nodeType === 'VariableDeclaration' &&
node.mutability === 'immutable'
) {
const constructorArgName =
getConstructorArgNameForImmutableVariable(
contractConfig.contract,
contractNode.nodes,
node.name
)
constructorArgNamesToImmutableNames[constructorArgName] = node.name
}
}
}
}
}

const constructorArgTypes = []
const constructorArgValues = []
constructorFragment.inputs.forEach((fragment) => {
constructorArgTypes.push(fragment.type)
constructorArgValues.push(
contractConfig.variables[
constructorArgNamesToImmutableNames[fragment.name]
]
)
})
return constructorArgValues
}

export const getNestedConstructorArg = (variableName: string, args): string => {
let remainingArguments = args[0]
while (remainingArguments !== undefined) {
Expand All @@ -304,9 +217,13 @@ export const getConstructorArgNameForImmutableVariable = (
for (const node of nodes) {
if (node.kind === 'constructor') {
for (const statement of node.body.statements) {
if (statement.expression.nodeType !== 'Assignment') {
if (statement.expression.nodeType === 'FunctionCall') {
throw new Error(
`Please remove the "${statement.expression.expression.name}" call in the constructor for ${contractName}.`
)
} else if (statement.expression.nodeType !== 'Assignment') {
throw new Error(
`disallowed statement constructor for ${contractName}: ${statement.expression.nodeType}`
`disallowed statement in constructor for ${contractName}: ${statement.expression.nodeType}`
)
}
if (statement.expression.leftHandSide.name === variableName) {
Expand Down
8 changes: 6 additions & 2 deletions packages/plugins/src/hardhat/deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ import {
import { getChainId } from '@eth-optimism/core-utils'

import {
getConstructorArgValues,
getContractArtifact,
getStorageLayout,
getBuildInfo,
getConstructorArgs,
} from './artifacts'
import { writeHardhatSnapshotId } from './utils'

Expand Down Expand Up @@ -273,6 +273,10 @@ export const deployChugSplashConfig = async (
const metadata =
buildInfo.output.contracts[sourceName][contractName].metadata
const { devdoc, userdoc } = JSON.parse(metadata).output
const { constructorArgValues } = await getConstructorArgs(
parsedConfig,
referenceName
)
const artifact = {
contractName,
address: contractConfig.address,
Expand All @@ -282,7 +286,7 @@ export const deployChugSplashConfig = async (
receipt: finalDeploymentReceipt,
numDeployments: 1,
metadata,
args: await getConstructorArgValues(contractConfig),
args: constructorArgValues,
bytecode,
deployedBytecode: await hre.ethers.provider.getCode(
contractConfig.address
Expand Down
Loading

0 comments on commit 273d4c3

Please sign in to comment.