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

Add opcm upgrades spec #468

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
313 changes: 219 additions & 94 deletions specs/experimental/op-contracts-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,148 +18,262 @@ of governance approved [contract releases] can be found on the

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

**Table of Contents**

- [Overview](#overview)
- [Getter Methods](#getter-methods)
- [Deployment](#deployment)
- [Interface](#interface)
- [`Proxy.sol`](#proxysol)
- [`deploy`](#deploy)
- [Getter Methods](#getter-methods)
- [Implementation](#implementation)
- [Batch Inbox Address](#batch-inbox-address)
- [Contract Deployments](#contract-deployments)
- [Interface](#interface)
- [`deploy`](#deploy)
- [Implementation](#implementation)
- [Batch Inbox Address](#batch-inbox-address)
- [Contract Deployments](#contract-deployments)
- [Upgrading](#upgrading)
- [Interface](#interface-1)
- [`upgrade`](#upgrade)
- [Implementation](#implementation-1)
- [`NewChainConfig` struct](#newchainconfig-struct)
- [Requirements on the OP Chain contracts](#requirements-on-the-op-chain-contracts)
- [Adding game types](#adding-game-types)
- [Interface](#interface-2)
- [`addGameType`](#addgametype)
- [Implementation](#implementation-2)
- [Security Considerations](#security-considerations)
- [Chain ID Source of Truth](#chain-id-source-of-truth)
- [Chain ID Frontrunning](#chain-id-frontrunning)
- [Chain ID Value](#chain-id-value)
- [Proxy Admin Owner](#proxy-admin-owner)
- [Upgradeability (ABI Changes)](#upgradeability-abi-changes)
- [Safely using `DELEGATECALL`](#safely-using-delegatecall)
- [Atomicity of upgrades](#atomicity-of-upgrades)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Deployment
## Overview

The OP Contracts Manager is a proxied contract deployed at `0xTODO`. It can be deployed as follows:
The OP Contracts Manager refers to a series of contracts, of which a new singleton is deployed
for each new release of the OP Stack contracts.

TODO.
The OP Contracts Manager corresponding to each release can be used to:

## Interface
1. Deploy a new OP chain.
2. Upgrade the contracts for an existing OP chain from the previous release to the new release.
3. Orchestrate adding a new game type on a per-chain basis

Version 1.0.0 of the OP Contracts Manager deploys the `op-contracts/v1.6.0`
contracts release.
## Getter Methods

### `Proxy.sol`
The following interface defines the available getter methods:

The OP Contracts Manager is a proxied contract using the standard `Proxy.sol` contract that lives in
the Optimism monorepo. Therefore the OP Contracts Manager will have the same interface as the
`Proxy.sol`, in addition to other methods defined in this specification.
```solidity
/// @notice Returns the latest approved release of the OP Stack contracts are named with the
/// format `op-contracts/vX.Y.Z`.
function l1ContractsRelease() external view returns (string memory);
/// @notice Represents the interface version so consumers know how to decode the DeployOutput struct
function OUTPUT_VERSION() external view returns (uint256);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we mixing snake case and camel case in the interface? We generally agreed on not doing that because it introduces tech debt if we move from something being an immutable to not. This couples the implementation details to the interface

/// @notice Addresses of the Blueprint contracts.
function blueprints() external view returns (Blueprints memory);
/// @notice Maps an L2 chain ID to an L1 batch inbox address
function chainIdToBatchInboxAddress(uint256 _l2ChainId) external pure returns (address);
/// @notice Addresses of the latest implementation contracts.
function implementations() external view returns (Implementations memory);
/// @notice Address of the ProtocolVersions contract shared by all chains.
function protocolVersions() external view returns (address);
/// @notice Address of the SuperchainConfig contract shared by all chains.
function superchainConfig() external view returns (address);
/// @notice Maps an L2 Chain ID to the SystemConfig for that chain.
function systemConfigs(uint256 _l2ChainId) external view returns (address);
/// @notice Semver version specific to the OPContractsManager
function version() external view returns (string memory);
```
blmalone marked this conversation as resolved.
Show resolved Hide resolved

The privileged methods of the OP Contracts Manager will be held by the L1 ProxyAdmin owner, as
specified by the [standard configuration].
## Deployment

### `deploy`
### Interface

The `deploy` method is the only non-view method in the contract. It is used to
deploy the full set of L1 contracts required to setup a new OP Stack chain that
complies with the [standard configuration]. It has the following interface:
#### `deploy`

```solidity
struct Roles {
maurelian marked this conversation as resolved.
Show resolved Hide resolved
address proxyAdminOwner;
address systemConfigOwner;
address batcher;
address unsafeBlockSigner;
address proposer;
address challenger;
}
The `deploy` method is used to deploy the full set of L1 contracts required to setup a new OP Stack
chain that complies with the [standard configuration]. It has the following interface:

function deploy(
uint256 l2ChainId,
uint32 basefeeScalar,
uint32 blobBasefeeScalar,
Roles roles
) external returns (SystemConfig)
```solidity
/// @notice Deploys a new OP Chain
/// @param _input DeployInput containing chain specific config information.
maurelian marked this conversation as resolved.
Show resolved Hide resolved
/// @return DeployOutput containing the new addresses.
function deploy(DeployInput calldata _input) external returns (DeployOutput memory)
```

The `l2ChainId` has the following restrictions:

- It must not be equal to 0.
- It must not be equal to the chain ID of the chain the OP Contracts Manager is
deployed on.
deployed on.
- It must not be equal to a chain ID that is already present in the
[ethereum-lists/chains] repository. This is not enforced onchain, but may matter
for future versions of OP Contracts Manager that handle upgrades.
[ethereum-lists/chains] repository. This is not enforced onchain, but may matter
for future versions of OP Contracts Manager that handle upgrades.

On success, the following event is emitted:

```solidity
event Deployed(uint256 indexed l2ChainId, SystemConfig indexed systemConfig);
event Deployed(uint256 indexed outputVersion, uint256 indexed l2ChainId, address indexed deployer, bytes deployOutput);
```

This method reverts on failure. This occurs when:

- The input `l2ChainId` does not comply with the restrictions above.
- The resulting configuration is not compliant with the [standard configuration].

### Getter Methods
### Implementation

The following interface defines the available getter methods:
#### Batch Inbox Address

The chain's [Batch Inbox] address is computed at deploy time using the recommend approach defined
in the [standard configuration]. This improves UX by removing an input, and ensures uniqueness of
the batch inbox addresses.

#### Contract Deployments
blmalone marked this conversation as resolved.
Show resolved Hide resolved
blmalone marked this conversation as resolved.
Show resolved Hide resolved

All contracts deployed by the OP Contracts Manager are deployed with CREATE2.

For singletons the following salt is used:

```solidity
keccak256(abi.encode(l2ChainId, saltMixer))
Copy link
Contributor

@blmalone blmalone Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation of salts have now been simplied as per this pr: ethereum-optimism/optimism#13273

Now all salts are computed the same way. So both proxied and non-proxied contracts have their salts computed as:

keccak256(abi.encode(_l2ChainId, _saltMixer, _contractName));

```

For `Proxy` contracts, the following salt is used:

```solidity
/// @notice The logic address and initializer selector for an implementation contract.
struct Implementation {
address logic; // Address containing the deployed logic contract.
bytes4 initializer; // Function selector for the initializer.
keccak256(bytes.concat(bytes32(uint256(l2ChainId)), saltMixer, contractName))
```

The `saltMixer` value is provided as a field in the `DeployInput` struct.

This provides the following benefits:

- Contract addresses for a chain can be derived as a function of chain ID without any RPC calls.
- Chain ID uniqueness is enforced for free, as a deploy using the same chain ID
will result in attempting to deploy to the same address, which is prohibited by
the EVM.
- This property is contingent on the proxy and `AddressManager` code not
changing when OP Contracts Manager is upgraded. Both of these are not planned to
change.
- The OP Contracts Manager is not responsible for enforcing chain ID uniqueness, so it is acceptable
if this property is not preserved in future versions of the OP Contracts Manager.

## Upgrading

### Interface

#### `upgrade`

The `upgrade` method is used by the Upgrade Controller to upgrade the full set of L1 contracts for
all chains that it controls.

It has the following interface:

```solidity
/// @notice The full set of inputs to deploy a new OP Stack chain.
struct DeployInput {
Roles roles;
uint32 basefeeScalar;
uint32 blobBasefeeScalar;
uint256 l2ChainId;
// The correct type is AnchorStateRegistry.StartingAnchorRoot[] memory,
// but OP Deployer does not yet support structs.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

support arrays? Roles is a struct

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The limitation is that neither the setters, nor getters on the Input/Output contracts that op-deployer uses be a complex/reference type, because those I/O contracts are simulated as predeploys in go code, and that simulation does not have the ability to handle the more complicated encodings.

That however is an implementation detail specific to the monorepo, which would not be required for another implementer to know, so I've removed these comments which had been copied from the impl source.

I've also removed the monorepo specific type wrappers from the DeployInput and replaced them with their simple type definitions (ie. Duration -> uint64, Claim -> bytes32).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bytes startingAnchorRoots;
// The salt mixer is used as part of making the resulting salt unique.
string saltMixer;
uint64 gasLimit;
// Configurable dispute game parameters.
GameType disputeGameType;
Claim disputeAbsolutePrestate;
uint256 disputeMaxGameDepth;
uint256 disputeSplitDepth;
Duration disputeClockExtension;
Duration disputeMaxClockDuration;
}

/// @notice Returns the latest approved release of the OP Stack contracts.
/// @notice Release strings follow semver and are named with the
/// format `op-contracts/vX.Y.Z`.
function latestRelease() external view returns (string memory);
function upgrade(ISystemConfig[] _systemConfigs, IProxyAdmin[] _proxyAdmins, NewChainConfig[] _newConfigs) public;
```

/// @notice Maps a release version to a contract name to its implementation data.
function implementation(
string memory release,
string memory contractName
) external view returns (Implementation memory);
For each chain successfully upgraded, the following event is emitted:

/// @notice Maps an L2 Chain ID to the SystemConfig address for that chain.
/// @notice All contracts for a chain can be found from its SystemConfig.
function systemConfig(uint256 chainId) external view returns (SystemConfig);
```solidity
event Upgraded(uint256 indexed l2ChainId, SystemConfig indexed systemConfig, address indexed upgrader);
```

## Implementation
This method reverts if the upgrade is not successful for any of the chains.

### Batch Inbox Address
### Implementation

The chain's [Batch Inbox] address is computed at deploy time using the recommend approach defined
in the [standard configuration]. This improves UX by removing an input, and ensures uniqueness of
the batch inbox addresses.
The high level logic of the upgrade method is as follows:

### Contract Deployments
1. The Upgrade Controller Safe will `DELEGATECALL` to the `OPCM.upgrade()` method.
2. For each `_systemConfig`, the list of addresses in the chain is retrieved.
3. For each address, a two step upgrade is used where:
1. the first upgrade is to an `InitializerResetter` which resets the `initialized` value.
1. the implementation is updated to the final address and `upgrade()` is called on that address.

All contracts deployed by the OP Contracts Manager are deployed with CREATE2, with a
salt equal to either:
This approach requires that all contracts have an `upgrade()` function which sets the `initialized`
value to `true`. The `upgrade` function body should be empty unless it is used to set a new state
variable added to that contract since the last upgrade.

- The L2 chain ID, or
- `keccak256(bytes.concat(bytes32(uint256(l2ChainId)), contractName))`.
#### `NewChainConfig` struct
blmalone marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the definition?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the following content below:


By way of example, if an upgrade is adding a new variable address foo to the SystemConfig contract, for
an upgrade named Example, the struct could have the following definition:

struct NewChainConfigForExampleUpgrade {
  address systemConfigFoo;
}

We could create a structure in this document wherein each upgrade is expected to document the specific struct required, but I think this is sufficient instruction for an implementer, and I'm hesitant to add that overhead to each upgrade.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


The former is used when only a single instance of a given contract is deployed for a chain.
The latter is used when deploying multiple instances of a given contract for a chain,
which is the case of all `Proxy` contracts. For these, the `contractName`
is the name of the implementation contract that will be used with the proxy.
This struct is used to pass the new chain configuration to the `upgrade` method, and so it will
vary for each release of the OP Contracts Manager, based on what (if any) new parameters are added.

This provides the following benefits:
In practice, this struct is likely to be have a unique name for each release of the OP Contracts
Manager.

- Contract addresses for a chain can be derived as a function of chain ID without any RPC calls.
- Chain ID uniqueness is enforced for free, as a deploy using the same chain ID
will result in attempting to deploy to the same address, which is prohibited by
the EVM.
- This property is contingent on the proxy and `AddressManager` code not
changing when OP Contracts Manager is upgraded. Both of these are not planned to
change.
- The OP Contracts Manager is not responsible for enforcing chain ID uniqueness, so it is acceptable
if this property is not preserved in future versions of the OP Contracts Manager.
By way of example, if an upgrade is adding a new variable `address foo` to the `SystemConfig` contract, for
an upgrade named `Example`, the struct could have the following definition:

```solidity
struct NewChainConfigForExampleUpgrade {
address systemConfigFoo;
}
```

#### Requirements on the OP Chain contracts

In general, all contracts used in an OP Chain SHOULD be proxied with a single shared implementation.
This means that all values which are not constant across OP Chains SHOULD be held in storage rather
than the bytecode of the implementation.

Any contracts which do not meet this requirement will need to be deployed by the `upgrade()`
blmalone marked this conversation as resolved.
Show resolved Hide resolved
function, increasing the cost and reducing the number of OP Chains which can be atomically upgraded.

## Adding game types

Because different OP Chains within a Superchain may use different dispute game types, and are
expected to move from a permissioned to permissionless game over time, an `addGameType()` method is
blmalone marked this conversation as resolved.
Show resolved Hide resolved
provided to enable adding a new game type to multiple games at once.

### Interface

#### `addGameType`

The `addGameType` method is used to orchestrate the actions required to add a new game type to one
or more chains.

```solidity
struct NewGameConfig {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should define the game types and the definitions per game type. This spec is not clear enough to enable a second implementation or a reviewer to know that an impl is correct

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having just learned that the Proofs contracts refactor has been deprioritized, I will need to rewrite this section. Will let you know when that's done.

Copy link
Contributor Author

@maurelian maurelian Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit should address your comment:
7703c51

// fields will vary depending on the game type
}

function addGameType(ISystemConfig[] _systemConfigs, NewGameConfig[] _newGames) public;
```

### Implementation

The high level logic of the `addGameType` method is as follows (for each chain):

1. The Upgrade Controller Safe will `DELEGATECALL` to the `OPCM.addGameType()` method.
1. A new Proxy contract will be deployed, with the implementation set to the `Creator` contract for that game type.
1. Calls `setImplementation()` on the `DisputeGameFactory`
1. Calls `upgrade()` on the `AnchorStateRegistry` to set the new game type to add a new entry to the `anchors` mapping.
The `upgrade()` method should revert if it would overwrite an existing entry.
maurelian marked this conversation as resolved.
Show resolved Hide resolved

## Security Considerations

Expand All @@ -170,10 +284,10 @@ once per chain ID, because contract addresses are a function of chain ID. Howeve
future versions of OP Contracts Manager may:

- Change the Proxy code used, which would allow a duplicate chain ID to be deployed
if there is only the implicit check.
if there is only the implicit check.
- Manage upgrades, which will require "registering" existing pre-OP Contracts Manager
chains in the OP Contracts Manager. Registration will be a privileged action, and the [superchain registry] will be
used as the source of truth for registrations.
chains in the OP Contracts Manager. Registration will be a privileged action, and the [superchain registry] will be
used as the source of truth for registrations.

This means, for example, if deploying a chain with a chain ID of 10—which is OP
Mainnet's chain ID—deployment will execute successfully, but the entry in OP
Expand Down Expand Up @@ -207,10 +321,21 @@ The proxy admin owner is a very powerful role, as it allows upgrading protocol
contracts. When choosing the initial proxy admin owner, a Safe is recommended
to ensure admin privileges are sufficiently secured.

### Upgradeability (ABI Changes)
### Safely using `DELEGATECALL`

Because a Safe will `DELEGATECALL` to the `upgrade()` and `addGameType()` methods, it is
critical that no storage writes occur. This should be enforced in multiple ways, including:

- By static analysis of the `upgrade()` and `addGameType()` methods during the development process.
- By simulating and verifying the state changes which occur in the Upgrade Controller Safe prior to execution.

### Atomicity of upgrades

Although atomicity of a superchain upgrade is not essential for many types of upgrade, it will
at times be necessary. It is certainly always desirable for operational reasons.

This contract is upgradeable, and breaking changes are expected, as upgrades
are required to update the contracts release that is deployed. This is because
the required inputs to the `deploy` method may change as new contract releases
are supported. Therefore, if calling this contract from another contract, be
sure to account for future breaking changes to the ABI.
maurelian marked this conversation as resolved.
Show resolved Hide resolved
For this reason, efficiency should be kept in mind when designing the upgrade path. When the size of
the superchain reaches a size that nears the block gas limit, upgrades may need to be broken up into
stages, so that components which must be upgrade atomically can be. For example, all
`OptimismPortal` contracts may need to be upgraded in one transaction, followed by another
transaction which upgrades all `L1CrossDomainMessenger` contracts.
Loading