-
Notifications
You must be signed in to change notification settings - Fork 96
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
base: main
Are you sure you want to change the base?
Changes from 19 commits
00ce042
d503636
9558d46
115c61e
0644021
2d41c3d
b911eb4
34e18e4
bb7ea4e
e1e004f
3cf1a9a
90826bf
4317849
8ff5f88
60ecdec
178e253
6de833d
61762db
e1c8541
f3262e1
7703c51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
/// @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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. support arrays? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the definition? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This commit should address your comment: |
||
// 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 | ||
|
||
|
@@ -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 | ||
|
@@ -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. |
There was a problem hiding this comment.
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