Skip to content

Commit

Permalink
feat(core): Add support for deploying stateless contracts
Browse files Browse the repository at this point in the history
  • Loading branch information
RPate97 committed Mar 30, 2023
1 parent 65f34be commit bff091a
Show file tree
Hide file tree
Showing 26 changed files with 400 additions and 191 deletions.
2 changes: 1 addition & 1 deletion packages/contracts/contracts/ChugSplashDataTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ struct ChugSplashTarget {
*/
enum ChugSplashActionType {
SET_STORAGE,
DEPLOY_IMPLEMENTATION
DEPLOY_CONTRACT
}

/**
Expand Down
90 changes: 53 additions & 37 deletions packages/contracts/contracts/ChugSplashManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -196,16 +196,16 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable {
);

/**
* @notice Emitted when an implementation contract is deployed by this contract.
* @notice Emitted when a contract is deployed.
*
* @param referenceNameHash Hash of the reference name.
* @param implementation Address of the deployed implementation.
* @param bundleId ID of the bundle in which the implementation was deployed.
* @param contractAddress Address of the deployed contract.
* @param bundleId ID of the bundle in which the contract was deployed.
* @param referenceName String reference name.
*/
event ImplementationDeployed(
event ContractDeployed(
string indexed referenceNameHash,
address indexed implementation,
address indexed contractAddress,
bytes32 indexed bundleId,
string referenceName
);
Expand Down Expand Up @@ -542,6 +542,11 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable {
"ChugSplashManager: invalid bundle target proof"
);

// continue if the target does not use a proxy
if (target.proxyTypeHash == keccak256('no-proxy')) {
continue;
}

// Get the proxy type and adapter for this reference name.
address adapter = recorder.adapters(target.proxyTypeHash);

Expand Down Expand Up @@ -573,10 +578,12 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable {
}

// Upgrade the proxy's implementation contract to the adapter.
(bool success, ) = adapter.delegatecall(
abi.encodeCall(IProxyAdapter.initiateExecution, (target.proxy))
);
require(success, "ChugSplashManger: failed to initiate execution");
if (target.proxyTypeHash != keccak256('no-proxy')) {
(bool success, ) = adapter.delegatecall(
abi.encodeCall(IProxyAdapter.initiateExecution, (target.proxy))
);
require(success, "ChugSplashManger: failed to initiate execution");
}
}

// Mark the bundle as initiated.
Expand All @@ -590,11 +597,11 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable {

/**
* @notice Executes multiple ChugSplash actions within the current active bundle for a project.
* Actions 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
* Actions can only be executed once. A re-entrancy guard is added to prevent a
* contract's constructor from calling another contract which in turn
* calls back into this function. Only callable by the executor.
*
* @param _actions Array of SetStorage/DeployImplementation actions to execute.
* @param _actions Array of SetStorage/DeployContract actions to execute.
* @param _actionIndexes Array of action indexes.
* @param _proofs Array of Merkle proofs for each action.
*/
Expand All @@ -603,6 +610,7 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable {
uint256[] memory _actionIndexes,
bytes32[][] memory _proofs
) public nonReentrant {
bytes32 noProxyTypeHash = keccak256('no-proxy');
uint256 initialGasLeft = gasleft();

require(
Expand Down Expand Up @@ -648,24 +656,29 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable {
// Get the adapter for this reference name.
address adapter = recorder.adapters(action.proxyTypeHash);

require(adapter != address(0), "ChugSplashManager: proxy type has no adapter");
require(action.proxyTypeHash == noProxyTypeHash || adapter != address(0), "ChugSplashManager: proxy type has no adapter");
require(action.proxyTypeHash != noProxyTypeHash || action.actionType != ChugSplashActionType.SET_STORAGE, "ChugSplashManager: cannot set storage in non-proxied contracts");

// Set the proxy's implementation to be a ProxyUpdater. Updaters ensure that only the
// ChugSplashManager can interact with a proxy that is in the process of being updated.
// Note that we use the Updater contract to provide a generic interface for updating a
// variety of proxy types.
(bool success, ) = adapter.delegatecall(
abi.encodeCall(IProxyAdapter.initiateExecution, (action.proxy))
);
require(success, "ChugSplashManager: failed to set implementation to an updater");
// Note no adapter is necessary for non-proxied contracts as they are not upgradable and
// cannot have state.
if (action.proxyTypeHash != noProxyTypeHash) {
(bool success, ) = adapter.delegatecall(
abi.encodeCall(IProxyAdapter.initiateExecution, (action.proxy))
);
require(success, "ChugSplashManager: failed to set implementation to an updater");
}

// Mark the action as executed and update the total number of executed actions.
bundle.actionsExecuted++;
bundle.actions[actionIndex] = true;

// Next, we execute the ChugSplash action by calling deployImplementation/setStorage.
if (action.actionType == ChugSplashActionType.DEPLOY_IMPLEMENTATION) {
_deployImplementation(action.referenceName, action.data);
// Next, we execute the ChugSplash action by calling deployContract/setStorage.
if (action.actionType == ChugSplashActionType.DEPLOY_CONTRACT) {
_deployContract(action.referenceName, action.data);
} else if (action.actionType == ChugSplashActionType.SET_STORAGE) {
(bytes32 key, uint8 offset, bytes memory val) = abi.decode(
action.data,
Expand All @@ -687,7 +700,7 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable {
* @notice Completes the bundle by upgrading all proxies to their new implementations. This
* occurs in a single transaction to ensure that all proxies are initialized at the same
* time. Note that this function will revert if it is called before all of the SetCode
* and DeployImplementation actions have been executed in `executeChugSplashAction`.
* and DeployContract actions have been executed in `executeChugSplashAction`.
* Only callable by the executor.
*
* @param _targets Array of ChugSplashTarget objects.
Expand Down Expand Up @@ -987,39 +1000,42 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable {
}

/**
* @notice Deploys an implementation contract, which will later be set as the proxy's
* implementation address. Note that we wait to set the proxy's implementation address
* until the very last call of the bundle to avoid a situation where end-users are
* interacting with a proxy whose storage has not fully been initialized.
* @notice Deploys a contract using the CREATE2 opcode.
*
* If the user is deploying a proxied contract, then we deploy the implementation contract
* first and later set the proxy's implementation address to the implementation
* contract's address. Note that we wait to set the proxy's implementation address until
* the very last call of the bundle to avoid a situation where end-users are interacting
* with a proxy whose storage has not been fully initialized.
*
* @param _referenceName Reference name that corresponds to the implementation.
* @param _code Creation bytecode of the implementation contract.
* @param _referenceName Reference name that corresponds to the contract.
* @param _code Creation bytecode of the contract.
*/
function _deployImplementation(string memory _referenceName, bytes memory _code) internal {
function _deployContract(string memory _referenceName, bytes memory _code) internal {
// Calculate the salt for the Create2 call. Note that there can be address collisions
// between implementations if their reference names are the same, but this is avoided with
// off-chain tooling by skipping implementations that have the same reference name and
// creation bytecode.
bytes32 salt = keccak256(bytes(_referenceName));
bytes32 salt = bytes32(0);

// Get the expected address of the implementation contract.
address expectedImplementation = Create2.compute(address(this), salt, _code);
// Get the expected address of the contract.
address expectedAddress = Create2.compute(address(this), salt, _code);

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

// Could happen if insufficient gas is supplied to this transaction, should not happen
// otherwise. If there's a situation in which this could happen other than a standard OOG,
// then this would halt the entire contract.
require(
expectedImplementation == implementation,
"ChugSplashManager: implementation was not deployed correctly"
expectedAddress == actualAddress,
"ChugSplashManager: contract was not deployed correctly"
);

emit ImplementationDeployed(_referenceName, implementation, activeBundleId, _referenceName);
recorder.announce("ImplementationDeployed");
emit ContractDeployed(_referenceName, actualAddress, activeBundleId, _referenceName);
recorder.announce("ContractDeployed");
}

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/contracts/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export const OZ_UUPS_OWNABLE_PROXY_TYPE_HASH = ethers.utils.keccak256(
export const OZ_UUPS_ACCESS_CONTROL_PROXY_TYPE_HASH = ethers.utils.keccak256(
ethers.utils.toUtf8Bytes('oz-access-control-uups')
)
export const NO_PROXY_TYPE_HASH = ethers.utils.keccak256(
ethers.utils.toUtf8Bytes('no-proxy')
)

export const CHUGSPLASH_SALT = '0x' + '13'.repeat(32)

Expand Down
44 changes: 20 additions & 24 deletions packages/core/src/actions/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from '../languages/solidity/storage'
import { ArtifactPaths } from '../languages/solidity/types'
import {
getImplAddress,
getContractAddress,
readContractArtifact,
getCreationCodeWithConstructorArgs,
readBuildInfo,
Expand All @@ -27,7 +27,7 @@ import {
ChugSplashBundles,
ChugSplashTarget,
ChugSplashTargetBundle,
DeployImplementationAction,
DeployContractAction,
RawChugSplashAction,
SetStorageAction,
} from './types'
Expand All @@ -49,15 +49,15 @@ export const isSetStorageAction = (
}

/**
* Checks whether a given action is a DeployImplementation action.
* Checks whether a given action is a DeployContract action.
*
* @param action ChugSplash action to check.
* @returns `true` if the action is a DeployImplementation action, `false` otherwise.
* @returns `true` if the action is a DeployContract action, `false` otherwise.
*/
export const isDeployImplementationAction = (
export const isDeployContractAction = (
action: ChugSplashAction
): action is DeployImplementationAction => {
return (action as DeployImplementationAction).code !== undefined
): action is DeployContractAction => {
return (action as DeployContractAction).code !== undefined
}

/**
Expand All @@ -81,9 +81,9 @@ export const toRawChugSplashAction = (
[action.key, action.offset, action.value]
),
}
} else if (isDeployImplementationAction(action)) {
} else if (isDeployContractAction(action)) {
return {
actionType: ChugSplashActionType.DEPLOY_IMPLEMENTATION,
actionType: ChugSplashActionType.DEPLOY_CONTRACT,
proxy: action.proxy,
proxyTypeHash: action.proxyTypeHash,
referenceName: action.referenceName,
Expand Down Expand Up @@ -116,9 +116,7 @@ export const fromRawChugSplashAction = (
offset,
value,
}
} else if (
rawAction.actionType === ChugSplashActionType.DEPLOY_IMPLEMENTATION
) {
} else if (rawAction.actionType === ChugSplashActionType.DEPLOY_CONTRACT) {
return {
referenceName: rawAction.referenceName,
proxy: rawAction.proxy,
Expand Down Expand Up @@ -215,15 +213,15 @@ export const makeBundleFromTargets = (
/**
* Generates an action bundle from a set of actions. Effectively encodes the inputs that will be
* provided to the ChugSplashManager contract. This function also sorts the actions so that the
* SetStorage actions are first and the DeployImplementation actions are last.
* SetStorage actions are first and the DeployContract actions are last.
*
* @param actions Series of DeployImplementation and SetStorage actions to bundle.
* @param actions Series of DeployContract and SetStorage actions to bundle.
* @return Bundled actions.
*/
export const makeBundleFromActions = (
actions: ChugSplashAction[]
): ChugSplashActionBundle => {
// Sort the actions to be in the order: SetStorage then DeployImplementation
// Sort the actions to be in the order: SetStorage then DeployContract
const sortedActions = actions.sort((a1, a2) => {
if (isSetStorageAction(a1)) {
// Keep the order of the actions if the first action is SetStorage.
Expand Down Expand Up @@ -359,21 +357,20 @@ export const makeActionBundleFromConfig = async (
const storageLayout =
compilerOutput.contracts[sourceName][contractName].storageLayout

// Skip adding a `DEPLOY_IMPLEMENTATION` action if the implementation has already been deployed.
// Skip adding a `DEPLOY_CONTRACT` action if the contract has already been deployed.
if (
(await provider.getCode(
getImplAddress(
getContractAddress(
parsedConfig.options.projectName,
referenceName,
creationCodeWithConstructorArgs
)
)) === '0x'
) {
// Add a DEPLOY_IMPLEMENTATION action.
// Add a DEPLOY_CONTRACT action.
actions.push({
referenceName,
proxy: contractConfig.proxy,
proxyTypeHash: proxyTypeHashes[contractConfig.proxyType],
proxyTypeHash: proxyTypeHashes[contractConfig.kind],
code: creationCodeWithConstructorArgs,
})
}
Expand All @@ -398,7 +395,7 @@ export const makeActionBundleFromConfig = async (
actions.push({
referenceName,
proxy: contractConfig.proxy,
proxyTypeHash: proxyTypeHashes[contractConfig.proxyType],
proxyTypeHash: proxyTypeHashes[contractConfig.kind],
key: segment.key,
offset: segment.offset,
value: segment.val,
Expand Down Expand Up @@ -429,11 +426,10 @@ export const makeTargetBundleFromConfig = (

targets.push({
referenceName,
proxyTypeHash: proxyTypeHashes[contractConfig.proxyType],
proxyTypeHash: proxyTypeHashes[contractConfig.kind],
proxy: contractConfig.proxy,
implementation: getImplAddress(
implementation: getContractAddress(
parsedConfig.options.projectName,
referenceName,
creationCodeWithConstructorArgs
),
})
Expand Down
14 changes: 7 additions & 7 deletions packages/core/src/actions/execute.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ethers } from 'ethers'
import { Logger } from '@eth-optimism/common-ts'

import { fromRawChugSplashAction, isDeployImplementationAction } from './bundle'
import { fromRawChugSplashAction, isDeployContractAction } from './bundle'
import {
BundledChugSplashAction,
ChugSplashBundles,
Expand Down Expand Up @@ -175,11 +175,11 @@ export const executeTask = async (args: {
}
}

// Find the indices of the first DeployImplementation and SetImpl actions so we know where to
// Find the indices of the first DeployContract and SetImpl actions so we know where to
// split up our batches. Actions have already been sorted in the order: SetStorage then
// DeployImplementation.
// DeployContract.
const firstDepImpl = actionBundle.actions.findIndex((action) =>
isDeployImplementationAction(fromRawChugSplashAction(action.action))
isDeployContractAction(fromRawChugSplashAction(action.action))
)

logger.info(`[ChugSplash]: initiating execution...`)
Expand All @@ -198,10 +198,10 @@ export const executeTask = async (args: {
await executeBatchActions(actionBundle.actions.slice(0, firstDepImpl))
logger.info(`[ChugSplash]: executed SetStorage actions`)

// Execute DeployImplementation actions in batches.
logger.info(`[ChugSplash]: executing DeployImplementation actions...`)
// Execute DeployContract actions in batches.
logger.info(`[ChugSplash]: executing DeployContract actions...`)
await executeBatchActions(actionBundle.actions.slice(firstDepImpl))
logger.info(`[ChugSplash]: executed DeployImplementation actions`)
logger.info(`[ChugSplash]: executed DeployContract actions`)

logger.info(`[ChugSplash]: completing execution...`)
await (
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { BigNumber } from 'ethers'
*/
export enum ChugSplashActionType {
SET_STORAGE,
DEPLOY_IMPLEMENTATION,
DEPLOY_CONTRACT,
}

/**
Expand Down Expand Up @@ -51,9 +51,9 @@ export interface SetStorageAction {
}

/**
* DeployImplementation action data.
* DeployContract action data.
*/
export interface DeployImplementationAction {
export interface DeployContractAction {
referenceName: string
proxy: string
proxyTypeHash: string
Expand All @@ -68,7 +68,7 @@ export interface ChugSplashBundles {
/**
* ChugSplash action.
*/
export type ChugSplashAction = SetStorageAction | DeployImplementationAction
export type ChugSplashAction = SetStorageAction | DeployContractAction

/**
* ChugSplash action that is part of a bundle.
Expand Down
Loading

0 comments on commit bff091a

Please sign in to comment.