Commit 9b9f78c6 authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

Merge pull request #5233 from ethereum-optimism/fix/syscfg-prevent-low-gaslimit-3

contracts-bedrock: modularize deposit resource config
parents cb6c4238 0daba1ef
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -8,6 +8,7 @@ import (
"strings"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
......@@ -22,19 +23,45 @@ import (
"github.com/ethereum-optimism/optimism/op-chain-ops/state"
)
var proxies = []string{
"SystemConfigProxy",
"L2OutputOracleProxy",
"L1CrossDomainMessengerProxy",
"L1StandardBridgeProxy",
"OptimismPortalProxy",
"OptimismMintableERC20FactoryProxy",
}
var portalMeteringSlot = common.Hash{31: 0x01}
var (
// proxies represents the set of proxies in front of contracts.
proxies = []string{
"SystemConfigProxy",
"L2OutputOracleProxy",
"L1CrossDomainMessengerProxy",
"L1StandardBridgeProxy",
"OptimismPortalProxy",
"OptimismMintableERC20FactoryProxy",
}
// portalMeteringSlot is the storage slot containing the metering params.
portalMeteringSlot = common.Hash{31: 0x01}
// zeroHash represents the zero value for a hash.
zeroHash = common.Hash{}
// uint128Max is type(uint128).max and is set in the init function.
uint128Max = new(big.Int)
// The default values for the ResourceConfig, used as part of
// an EIP-1559 curve for deposit gas.
defaultResourceConfig = bindings.ResourceMeteringResourceConfig{
MaxResourceLimit: 20_000_000,
ElasticityMultiplier: 10,
BaseFeeMaxChangeDenominator: 8,
MinimumBaseFee: params.GWei,
SystemTxMaxGas: 1_000_000,
}
)
var zeroHash common.Hash
func init() {
var ok bool
uint128Max, ok = new(big.Int).SetString("ffffffffffffffffffffffffffffffff", 16)
if !ok {
panic("bad uint128Max")
}
// Set the maximum base fee on the default config.
defaultResourceConfig.MaximumBaseFee = uint128Max
}
// BuildL1DeveloperGenesis will create a L1 genesis block after creating
// all of the state required for an Optimism network to function.
func BuildL1DeveloperGenesis(config *DeployConfig) (*core.Genesis, error) {
if config.L2OutputOracleStartingTimestamp != -1 {
return nil, errors.New("l2oo starting timestamp must be -1")
......@@ -67,6 +94,26 @@ func BuildL1DeveloperGenesis(config *DeployConfig) (*core.Genesis, error) {
if err != nil {
return nil, err
}
portalABI, err := bindings.OptimismPortalMetaData.GetAbi()
if err != nil {
return nil, err
}
// Initialize the OptimismPortal without being paused
data, err := portalABI.Pack("initialize", false)
if err != nil {
return nil, fmt.Errorf("cannot abi encode initialize for OptimismPortal: %w", err)
}
if _, err := upgradeProxy(
backend,
opts,
depsByName["OptimismPortalProxy"].Address,
depsByName["OptimismPortal"].Address,
data,
); err != nil {
return nil, fmt.Errorf("cannot upgrade OptimismPortalProxy: %w", err)
}
sysCfgABI, err := bindings.SystemConfigMetaData.GetAbi()
if err != nil {
return nil, err
......@@ -75,7 +122,8 @@ func BuildL1DeveloperGenesis(config *DeployConfig) (*core.Genesis, error) {
if gasLimit == 0 {
gasLimit = defaultL2GasLimit
}
data, err := sysCfgABI.Pack(
data, err = sysCfgABI.Pack(
"initialize",
config.FinalSystemOwner,
uint642Big(config.GasPriceOracleOverhead),
......@@ -83,6 +131,7 @@ func BuildL1DeveloperGenesis(config *DeployConfig) (*core.Genesis, error) {
config.BatchSenderAddress.Hash(),
gasLimit,
config.P2PSequencerAddress,
defaultResourceConfig,
)
if err != nil {
return nil, fmt.Errorf("cannot abi encode initialize for SystemConfig: %w", err)
......@@ -94,7 +143,7 @@ func BuildL1DeveloperGenesis(config *DeployConfig) (*core.Genesis, error) {
depsByName["SystemConfig"].Address,
data,
); err != nil {
return nil, err
return nil, fmt.Errorf("cannot upgrade SystemConfigProxy: %w", err)
}
l2ooABI, err := bindings.L2OutputOracleMetaData.GetAbi()
......@@ -119,24 +168,6 @@ func BuildL1DeveloperGenesis(config *DeployConfig) (*core.Genesis, error) {
return nil, err
}
portalABI, err := bindings.OptimismPortalMetaData.GetAbi()
if err != nil {
return nil, err
}
// Initialize the OptimismPortal without being paused
data, err = portalABI.Pack("initialize", false)
if err != nil {
return nil, fmt.Errorf("cannot abi encode initialize for OptimismPortal: %w", err)
}
if _, err := upgradeProxy(
backend,
opts,
depsByName["OptimismPortalProxy"].Address,
depsByName["OptimismPortal"].Address,
data,
); err != nil {
return nil, err
}
l1XDMABI, err := bindings.L1CrossDomainMessengerMetaData.GetAbi()
if err != nil {
return nil, err
......@@ -264,6 +295,7 @@ func deployL1Contracts(config *DeployConfig, backend *backends.SimulatedBackend)
if gasLimit == 0 {
gasLimit = defaultL2GasLimit
}
constructors = append(constructors, []deployer.Constructor{
{
Name: "SystemConfig",
......@@ -274,6 +306,7 @@ func deployL1Contracts(config *DeployConfig, backend *backends.SimulatedBackend)
config.BatchSenderAddress.Hash(), // left-padded 32 bytes value, version is zero anyway
gasLimit,
config.P2PSequencerAddress,
defaultResourceConfig,
},
},
{
......@@ -297,6 +330,7 @@ func deployL1Contracts(config *DeployConfig, backend *backends.SimulatedBackend)
predeploys.DevL2OutputOracleAddr,
config.PortalGuardian,
true, // _paused
predeploys.DevSystemConfigAddr,
},
},
{
......@@ -342,6 +376,7 @@ func l1Deployer(backend *backends.SimulatedBackend, opts *bind.TransactOpts, dep
deployment.Args[3].(common.Hash),
deployment.Args[4].(uint64),
deployment.Args[5].(common.Address),
deployment.Args[6].(bindings.ResourceMeteringResourceConfig),
)
case "L2OutputOracle":
_, tx, _, err = bindings.DeployL2OutputOracle(
......@@ -362,6 +397,7 @@ func l1Deployer(backend *backends.SimulatedBackend, opts *bind.TransactOpts, dep
deployment.Args[0].(common.Address),
deployment.Args[1].(common.Address),
deployment.Args[2].(bool),
deployment.Args[3].(common.Address),
)
case "L1CrossDomainMessenger":
_, tx, _, err = bindings.DeployL1CrossDomainMessenger(
......@@ -421,6 +457,15 @@ func l1Deployer(backend *backends.SimulatedBackend, opts *bind.TransactOpts, dep
func upgradeProxy(backend *backends.SimulatedBackend, opts *bind.TransactOpts, proxyAddr common.Address, implAddr common.Address, callData []byte) (*types.Transaction, error) {
var tx *types.Transaction
code, err := backend.CodeAt(context.Background(), implAddr, nil)
if err != nil {
return nil, err
}
if len(code) == 0 {
return nil, fmt.Errorf("no code at %s", implAddr)
}
proxy, err := bindings.NewProxy(proxyAddr, backend)
if err != nil {
return nil, err
......
......@@ -8,6 +8,7 @@ import (
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
......@@ -100,6 +101,30 @@ func TestBuildL1DeveloperGenesis(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "Wrapped Ether", name)
sysCfg, err := bindings.NewSystemConfig(predeploys.DevSystemConfigAddr, sim)
require.NoError(t, err)
cfg, err := sysCfg.ResourceConfig(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, cfg, defaultResourceConfig)
owner, err = sysCfg.Owner(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, owner, config.FinalSystemOwner)
overhead, err := sysCfg.Overhead(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, overhead.Uint64(), config.GasPriceOracleOverhead)
scalar, err := sysCfg.Scalar(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, scalar.Uint64(), config.GasPriceOracleScalar)
batcherHash, err := sysCfg.BatcherHash(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, common.Hash(batcherHash), config.BatchSenderAddress.Hash())
gasLimit, err := sysCfg.GasLimit(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, gasLimit, uint64(config.L2GenesisBlockGasLimit))
unsafeBlockSigner, err := sysCfg.UnsafeBlockSigner(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, unsafeBlockSigner, config.P2PSequencerAddress)
// test that we can do deposits, etc.
priv, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")
require.NoError(t, err)
......
......@@ -16,7 +16,7 @@
"l1BlockTime": 15,
"l1GenesisBlockNonce": "0x0",
"cliqueSignerAddress": "0x0000000000000000000000000000000000000000",
"l1GenesisBlockGasLimit": "0xe4e1c0",
"l1GenesisBlockGasLimit": "0x1c9c380",
"l1GenesisBlockDifficulty": "0x1",
"finalSystemOwner": "0x0000000000000000000000000000000000000111",
"portalGuardian": "0x0000000000000000000000000000000000000112",
......@@ -29,7 +29,7 @@
"l1GenesisBlockTimestamp": "0x0",
"l1GenesisBlockBaseFeePerGas": "0x3b9aca00",
"l2GenesisBlockNonce": "0x0",
"l2GenesisBlockGasLimit": "0xe4e1c0",
"l2GenesisBlockGasLimit": "0x1c9c380",
"l2GenesisBlockDifficulty": "0x1",
"l2GenesisBlockMixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"l2GenesisBlockNumber": "0x0",
......
......@@ -79,7 +79,7 @@ func MakeDeployParams(t require.TestingT, tp *TestParams) *DeployParams {
L1GenesisBlockNonce: 0,
CliqueSignerAddress: common.Address{}, // proof of stake, no clique
L1GenesisBlockTimestamp: hexutil.Uint64(time.Now().Unix()),
L1GenesisBlockGasLimit: 15_000_000,
L1GenesisBlockGasLimit: 30_000_000,
L1GenesisBlockDifficulty: uint64ToBig(1),
L1GenesisBlockMixHash: common.Hash{},
L1GenesisBlockCoinbase: common.Address{},
......@@ -90,7 +90,7 @@ func MakeDeployParams(t require.TestingT, tp *TestParams) *DeployParams {
FinalizationPeriodSeconds: 12,
L2GenesisBlockNonce: 0,
L2GenesisBlockGasLimit: 15_000_000,
L2GenesisBlockGasLimit: 30_000_000,
L2GenesisBlockDifficulty: uint64ToBig(0),
L2GenesisBlockMixHash: common.Hash{},
L2GenesisBlockNumber: 0,
......
......@@ -75,7 +75,7 @@ func DefaultSystemConfig(t *testing.T) SystemConfig {
L1GenesisBlockNonce: 4660,
CliqueSignerAddress: addresses.CliqueSigner,
L1GenesisBlockTimestamp: hexutil.Uint64(time.Now().Unix()),
L1GenesisBlockGasLimit: 8_000_000,
L1GenesisBlockGasLimit: 30_000_000,
L1GenesisBlockDifficulty: uint642big(1),
L1GenesisBlockMixHash: common.Hash{},
L1GenesisBlockCoinbase: common.Address{},
......@@ -85,7 +85,7 @@ func DefaultSystemConfig(t *testing.T) SystemConfig {
L1GenesisBlockBaseFeePerGas: uint642big(7),
L2GenesisBlockNonce: 0,
L2GenesisBlockGasLimit: 8_000_000,
L2GenesisBlockGasLimit: 30_000_000,
L2GenesisBlockDifficulty: uint642big(1),
L2GenesisBlockMixHash: common.Hash{},
L2GenesisBlockNumber: 0,
......
This diff is collapsed.
......@@ -69,17 +69,18 @@
➡ contracts/L1/SystemConfig.sol:SystemConfig
=======================
| Name | Type | Slot | Offset | Bytes | Contract |
|---------------|-------------|------|--------|-------|--------------------------------------------|
| _initialized | uint8 | 0 | 0 | 1 | contracts/L1/SystemConfig.sol:SystemConfig |
| _initializing | bool | 0 | 1 | 1 | contracts/L1/SystemConfig.sol:SystemConfig |
| __gap | uint256[50] | 1 | 0 | 1600 | contracts/L1/SystemConfig.sol:SystemConfig |
| _owner | address | 51 | 0 | 20 | contracts/L1/SystemConfig.sol:SystemConfig |
| __gap | uint256[49] | 52 | 0 | 1568 | contracts/L1/SystemConfig.sol:SystemConfig |
| overhead | uint256 | 101 | 0 | 32 | contracts/L1/SystemConfig.sol:SystemConfig |
| scalar | uint256 | 102 | 0 | 32 | contracts/L1/SystemConfig.sol:SystemConfig |
| batcherHash | bytes32 | 103 | 0 | 32 | contracts/L1/SystemConfig.sol:SystemConfig |
| gasLimit | uint64 | 104 | 0 | 8 | contracts/L1/SystemConfig.sol:SystemConfig |
| Name | Type | Slot | Offset | Bytes | Contract |
|-----------------|----------------------------------------|------|--------|-------|--------------------------------------------|
| _initialized | uint8 | 0 | 0 | 1 | contracts/L1/SystemConfig.sol:SystemConfig |
| _initializing | bool | 0 | 1 | 1 | contracts/L1/SystemConfig.sol:SystemConfig |
| __gap | uint256[50] | 1 | 0 | 1600 | contracts/L1/SystemConfig.sol:SystemConfig |
| _owner | address | 51 | 0 | 20 | contracts/L1/SystemConfig.sol:SystemConfig |
| __gap | uint256[49] | 52 | 0 | 1568 | contracts/L1/SystemConfig.sol:SystemConfig |
| overhead | uint256 | 101 | 0 | 32 | contracts/L1/SystemConfig.sol:SystemConfig |
| scalar | uint256 | 102 | 0 | 32 | contracts/L1/SystemConfig.sol:SystemConfig |
| batcherHash | bytes32 | 103 | 0 | 32 | contracts/L1/SystemConfig.sol:SystemConfig |
| gasLimit | uint64 | 104 | 0 | 8 | contracts/L1/SystemConfig.sol:SystemConfig |
| _resourceConfig | struct ResourceMetering.ResourceConfig | 105 | 0 | 32 | contracts/L1/SystemConfig.sol:SystemConfig |
=======================
➡ contracts/legacy/DeployerWhitelist.sol:DeployerWhitelist
......
......@@ -4,6 +4,7 @@ pragma solidity 0.8.15;
import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import { SafeCall } from "../libraries/SafeCall.sol";
import { L2OutputOracle } from "./L2OutputOracle.sol";
import { SystemConfig } from "./SystemConfig.sol";
import { Constants } from "../libraries/Constants.sol";
import { Types } from "../libraries/Types.sol";
import { Hashing } from "../libraries/Hashing.sol";
......@@ -44,10 +45,15 @@ contract OptimismPortal is Initializable, ResourceMetering, Semver {
uint64 internal constant RECEIVE_DEFAULT_GAS_LIMIT = 100_000;
/**
* @notice Address of the L2OutputOracle.
* @notice Address of the L2OutputOracle contract.
*/
L2OutputOracle public immutable L2_ORACLE;
/**
* @notice Address of the SystemConfig contract.
*/
SystemConfig public immutable SYSTEM_CONFIG;
/**
* @notice Address that has the ability to pause and unpause withdrawals.
*/
......@@ -135,19 +141,22 @@ contract OptimismPortal is Initializable, ResourceMetering, Semver {
}
/**
* @custom:semver 1.2.0
* @custom:semver 1.3.0
*
* @param _l2Oracle Address of the L2OutputOracle contract.
* @param _guardian Address that can pause deposits and withdrawals.
* @param _paused Sets the contract's pausability state.
* @param _config Address of the SystemConfig contract.
*/
constructor(
L2OutputOracle _l2Oracle,
address _guardian,
bool _paused
) Semver(1, 2, 0) {
bool _paused,
SystemConfig _config
) Semver(1, 3, 0) {
L2_ORACLE = _l2Oracle;
GUARDIAN = _guardian;
SYSTEM_CONFIG = _config;
initialize(_paused);
}
......@@ -197,6 +206,21 @@ contract OptimismPortal is Initializable, ResourceMetering, Semver {
// Intentionally empty.
}
/**
* @notice Getter for the resource config. Used internally by the ResourceMetering
* contract. The SystemConfig is the source of truth for the resource config.
*
* @return ResourceMetering.ResourceConfig
*/
function _resourceConfig()
internal
view
override
returns (ResourceMetering.ResourceConfig memory)
{
return SYSTEM_CONFIG.resourceConfig();
}
/**
* @notice Proves a withdrawal transaction.
*
......
......@@ -28,44 +28,34 @@ abstract contract ResourceMetering is Initializable {
}
/**
* @notice Maximum amount of the resource that can be used within this block.
* This value cannot be larger than the L2 block gas limit.
*/
int256 public constant MAX_RESOURCE_LIMIT = 20_000_000;
/**
* @notice Along with the resource limit, determines the target resource limit.
*/
int256 public constant ELASTICITY_MULTIPLIER = 10;
/**
* @notice Target amount of the resource that should be used within this block.
*/
int256 public constant TARGET_RESOURCE_LIMIT = MAX_RESOURCE_LIMIT / ELASTICITY_MULTIPLIER;
/**
* @notice Denominator that determines max change on fee per block.
*/
int256 public constant BASE_FEE_MAX_CHANGE_DENOMINATOR = 8;
/**
* @notice Minimum base fee value, cannot go lower than this.
*/
int256 public constant MINIMUM_BASE_FEE = 1 gwei;
/**
* @notice Maximum base fee value, cannot go higher than this.
* It is possible for the MAXIMUM_BASE_FEE to raise to a value
* that is so large it will consume the entire gas limit of
* an L1 block.
*/
int256 public constant MAXIMUM_BASE_FEE = int256(uint256(type(uint128).max));
/**
* @notice Initial base fee value. This value must be smaller than the
* MAXIMUM_BASE_FEE.
* @notice Represents the configuration for the EIP-1559 based curve for the deposit gas
* market. These values should be set with care as it is possible to set them in
* a way that breaks the deposit gas market. The target resource limit is defined as
* maxResourceLimit / elasticityMultiplier. This struct was designed to fit within a
* single word. There is additional space for additions in the future.
*
* @custom:field maxResourceLimit Represents the maximum amount of deposit gas that
* can be purchased per block.
* @custom:field elasticityMultiplier Determines the target resource limit along with
* the resource limit.
* @custom:field baseFeeMaxChangeDenominator Determines max change on fee per block.
* @custom:field minimumBaseFee The min deposit base fee, it is clamped to this
* value.
* @custom:field systemTxMaxGas The amount of gas supplied to the system
* transaction. This should be set to the same number
* that the op-node sets as the gas limit for the
* system transaction.
* @custom:field maximumBaseFee The max deposit base fee, it is clamped to this
* value.
*/
uint128 public constant INITIAL_BASE_FEE = 1 gwei;
struct ResourceConfig {
uint32 maxResourceLimit;
uint8 elasticityMultiplier;
uint8 baseFeeMaxChangeDenominator;
uint32 minimumBaseFee;
uint32 systemTxMaxGas;
uint128 maximumBaseFee;
}
/**
* @notice EIP-1559 style gas parameters.
......@@ -102,20 +92,25 @@ abstract contract ResourceMetering is Initializable {
function _metered(uint64 _amount, uint256 _initialGas) internal {
// Update block number and base fee if necessary.
uint256 blockDiff = block.number - params.prevBlockNum;
ResourceConfig memory config = _resourceConfig();
int256 targetResourceLimit = int256(uint256(config.maxResourceLimit)) /
int256(uint256(config.elasticityMultiplier));
if (blockDiff > 0) {
// Handle updating EIP-1559 style gas parameters. We use EIP-1559 to restrict the rate
// at which deposits can be created and therefore limit the potential for deposits to
// spam the L2 system. Fee scheme is very similar to EIP-1559 with minor changes.
int256 gasUsedDelta = int256(uint256(params.prevBoughtGas)) - TARGET_RESOURCE_LIMIT;
int256 gasUsedDelta = int256(uint256(params.prevBoughtGas)) - targetResourceLimit;
int256 baseFeeDelta = (int256(uint256(params.prevBaseFee)) * gasUsedDelta) /
(TARGET_RESOURCE_LIMIT * BASE_FEE_MAX_CHANGE_DENOMINATOR);
(targetResourceLimit * int256(uint256(config.baseFeeMaxChangeDenominator)));
// Update base fee by adding the base fee delta and clamp the resulting value between
// min and max.
int256 newBaseFee = Arithmetic.clamp({
_value: int256(uint256(params.prevBaseFee)) + baseFeeDelta,
_min: MINIMUM_BASE_FEE,
_max: MAXIMUM_BASE_FEE
_min: int256(uint256(config.minimumBaseFee)),
_max: int256(uint256(config.maximumBaseFee))
});
// If we skipped more than one block, we also need to account for every empty block.
......@@ -128,11 +123,11 @@ abstract contract ResourceMetering is Initializable {
newBaseFee = Arithmetic.clamp({
_value: Arithmetic.cdexp({
_coefficient: newBaseFee,
_denominator: BASE_FEE_MAX_CHANGE_DENOMINATOR,
_denominator: int256(uint256(config.baseFeeMaxChangeDenominator)),
_exponent: int256(blockDiff - 1)
}),
_min: MINIMUM_BASE_FEE,
_max: MAXIMUM_BASE_FEE
_min: int256(uint256(config.minimumBaseFee)),
_max: int256(uint256(config.maximumBaseFee))
});
}
......@@ -145,7 +140,7 @@ abstract contract ResourceMetering is Initializable {
// Make sure we can actually buy the resource amount requested by the user.
params.prevBoughtGas += _amount;
require(
int256(uint256(params.prevBoughtGas)) <= MAX_RESOURCE_LIMIT,
int256(uint256(params.prevBoughtGas)) <= int256(uint256(config.maxResourceLimit)),
"ResourceMetering: cannot buy more gas than available gas limit"
);
......@@ -168,6 +163,14 @@ abstract contract ResourceMetering is Initializable {
}
}
/**
* @notice Virtual function that returns the resource config. Contracts that inherit this
* contract must implement this function.
*
* @return ResourceConfig
*/
function _resourceConfig() internal virtual returns (ResourceConfig memory);
/**
* @notice Sets initial resource parameter values. This function must either be called by the
* initializer function of an upgradeable child contract.
......@@ -175,7 +178,7 @@ abstract contract ResourceMetering is Initializable {
// solhint-disable-next-line func-name-mixedcase
function __ResourceMetering_init() internal onlyInitializing {
params = ResourceParams({
prevBaseFee: INITIAL_BASE_FEE,
prevBaseFee: 1 gwei,
prevBoughtGas: 0,
prevBlockNum: uint64(block.number)
});
......
......@@ -5,6 +5,7 @@ import {
OwnableUpgradeable
} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { Semver } from "../universal/Semver.sol";
import { ResourceMetering } from "./ResourceMetering.sol";
/**
* @title SystemConfig
......@@ -49,12 +50,12 @@ contract SystemConfig is OwnableUpgradeable, Semver {
uint64 public constant MINIMUM_GAS_LIMIT = 8_000_000;
/**
* @notice Fixed L2 gas overhead.
* @notice Fixed L2 gas overhead. Used as part of the L2 fee calculation.
*/
uint256 public overhead;
/**
* @notice Dynamic L2 gas overhead.
* @notice Dynamic L2 gas overhead. Used as part of the L2 fee calculation.
*/
uint256 public scalar;
......@@ -65,10 +66,17 @@ contract SystemConfig is OwnableUpgradeable, Semver {
bytes32 public batcherHash;
/**
* @notice L2 gas limit.
* @notice L2 block gas limit.
*/
uint64 public gasLimit;
/**
* @notice The configuration for the deposit fee market. Used by the OptimismPortal
* to meter the cost of buying L2 gas on L1. Set as internal and wrapped with a getter
* so that the struct is returned instead of a tuple.
*/
ResourceMetering.ResourceConfig internal _resourceConfig;
/**
* @notice Emitted when configuration is updated
*
......@@ -79,7 +87,7 @@ contract SystemConfig is OwnableUpgradeable, Semver {
event ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data);
/**
* @custom:semver 1.0.1
* @custom:semver 1.1.0
*
* @param _owner Initial owner of the contract.
* @param _overhead Initial overhead value.
......@@ -87,6 +95,7 @@ contract SystemConfig is OwnableUpgradeable, Semver {
* @param _batcherHash Initial batcher hash.
* @param _gasLimit Initial gas limit.
* @param _unsafeBlockSigner Initial unsafe block signer address.
* @param _config Initial resource config.
*/
constructor(
address _owner,
......@@ -94,13 +103,23 @@ contract SystemConfig is OwnableUpgradeable, Semver {
uint256 _scalar,
bytes32 _batcherHash,
uint64 _gasLimit,
address _unsafeBlockSigner
) Semver(1, 0, 1) {
initialize(_owner, _overhead, _scalar, _batcherHash, _gasLimit, _unsafeBlockSigner);
address _unsafeBlockSigner,
ResourceMetering.ResourceConfig memory _config
) Semver(1, 1, 0) {
initialize({
_owner: _owner,
_overhead: _overhead,
_scalar: _scalar,
_batcherHash: _batcherHash,
_gasLimit: _gasLimit,
_unsafeBlockSigner: _unsafeBlockSigner,
_config: _config
});
}
/**
* @notice Initializer.
* @notice Initializer. The resource config must be set before the
* require check.
*
* @param _owner Initial owner of the contract.
* @param _overhead Initial overhead value.
......@@ -108,6 +127,7 @@ contract SystemConfig is OwnableUpgradeable, Semver {
* @param _batcherHash Initial batcher hash.
* @param _gasLimit Initial gas limit.
* @param _unsafeBlockSigner Initial unsafe block signer address.
* @param _config Initial ResourceConfig.
*/
function initialize(
address _owner,
......@@ -115,9 +135,9 @@ contract SystemConfig is OwnableUpgradeable, Semver {
uint256 _scalar,
bytes32 _batcherHash,
uint64 _gasLimit,
address _unsafeBlockSigner
address _unsafeBlockSigner,
ResourceMetering.ResourceConfig memory _config
) public initializer {
require(_gasLimit >= MINIMUM_GAS_LIMIT, "SystemConfig: gas limit too low");
__Ownable_init();
transferOwnership(_owner);
overhead = _overhead;
......@@ -125,6 +145,21 @@ contract SystemConfig is OwnableUpgradeable, Semver {
batcherHash = _batcherHash;
gasLimit = _gasLimit;
_setUnsafeBlockSigner(_unsafeBlockSigner);
_setResourceConfig(_config);
require(_gasLimit >= minimumGasLimit(), "SystemConfig: gas limit too low");
}
/**
* @notice Returns the minimum L2 gas limit that can be safely set for the system to
* operate. The L2 gas limit must be larger than or equal to the amount of
* gas that is allocated for deposits per block plus the amount of gas that
* is allocated for the system transaction.
* This function is used to determine if changes to parameters are safe.
*
* @return uint64
*/
function minimumGasLimit() public view returns (uint64) {
return uint64(_resourceConfig.maxResourceLimit) + uint64(_resourceConfig.systemTxMaxGas);
}
/**
......@@ -188,7 +223,7 @@ contract SystemConfig is OwnableUpgradeable, Semver {
* @param _gasLimit New gas limit.
*/
function setGasLimit(uint64 _gasLimit) external onlyOwner {
require(_gasLimit >= MINIMUM_GAS_LIMIT, "SystemConfig: gas limit too low");
require(_gasLimit >= minimumGasLimit(), "SystemConfig: gas limit too low");
gasLimit = _gasLimit;
bytes memory data = abi.encode(_gasLimit);
......@@ -207,4 +242,60 @@ contract SystemConfig is OwnableUpgradeable, Semver {
sstore(slot, _unsafeBlockSigner)
}
}
/**
* @notice A getter for the resource config. Ensures that the struct is
* returned instead of a tuple.
*
* @return ResourceConfig
*/
function resourceConfig() external view returns (ResourceMetering.ResourceConfig memory) {
return _resourceConfig;
}
/**
* @notice An external setter for the resource config. In the future, this
* method may emit an event that the `op-node` picks up for when the
* resource config is changed.
*
* @param _config The new resource config values.
*/
function setResourceConfig(ResourceMetering.ResourceConfig memory _config) external onlyOwner {
_setResourceConfig(_config);
}
/**
* @notice An internal setter for the resource config. Ensures that the
* config is sane before storing it by checking for invariants.
*
* @param _config The new resource config.
*/
function _setResourceConfig(ResourceMetering.ResourceConfig memory _config) internal {
// Min base fee must be less than or equal to max base fee.
require(
_config.minimumBaseFee <= _config.maximumBaseFee,
"SystemConfig: min base fee must be less than max base"
);
// Base fee change denominator must be greater than 0.
require(_config.baseFeeMaxChangeDenominator > 0, "SystemConfig: denominator cannot be 0");
// Max resource limit plus system tx gas must be less than or equal to the L2 gas limit.
// The gas limit must be increased before these values can be increased.
require(
_config.maxResourceLimit + _config.systemTxMaxGas <= gasLimit,
"SystemConfig: gas limit too low"
);
// Elasticity multiplier must be greater than 0.
require(
_config.elasticityMultiplier > 0,
"SystemConfig: elasticity multiplier cannot be 0"
);
// No precision loss when computing target resource limit.
require(
((_config.maxResourceLimit / _config.elasticityMultiplier) *
_config.elasticityMultiplier) == _config.maxResourceLimit,
"SystemConfig: precision loss with target resource limit"
);
_resourceConfig = _config;
}
}
......@@ -16,6 +16,8 @@ import { ProxyAdmin } from "../universal/ProxyAdmin.sol";
import { OptimismMintableERC20Factory } from "../universal/OptimismMintableERC20Factory.sol";
import { PortalSender } from "./PortalSender.sol";
import { SystemConfig } from "../L1/SystemConfig.sol";
import { ResourceMetering } from "../L1/ResourceMetering.sol";
import { Constants } from "../libraries/Constants.sol";
/**
* @title SystemDictator
......@@ -79,6 +81,7 @@ contract SystemDictator is OwnableUpgradeable {
bytes32 batcherHash;
uint64 gasLimit;
address unsafeBlockSigner;
ResourceMetering.ResourceConfig resourceConfig;
}
/**
......@@ -160,6 +163,8 @@ contract SystemDictator is OwnableUpgradeable {
* initialized upon deployment.
*/
constructor() {
ResourceMetering.ResourceConfig memory rcfg = Constants.DEFAULT_RESOURCE_CONFIG();
// Using this shorter variable as an alias for address(0) just prevents us from having to
// to use a new line for every single parameter.
address zero = address(0);
......@@ -177,7 +182,7 @@ contract SystemDictator is OwnableUpgradeable {
PortalSender(zero),
SystemConfig(zero)
),
SystemConfigConfig(zero, 0, 0, bytes32(0), 0, zero)
SystemConfigConfig(zero, 0, 0, bytes32(0), 0, zero, rcfg)
)
);
}
......@@ -244,7 +249,8 @@ contract SystemDictator is OwnableUpgradeable {
config.systemConfigConfig.scalar,
config.systemConfigConfig.batcherHash,
config.systemConfigConfig.gasLimit,
config.systemConfigConfig.unsafeBlockSigner
config.systemConfigConfig.unsafeBlockSigner,
config.systemConfigConfig.resourceConfig
)
)
);
......
......@@ -3,16 +3,32 @@ pragma solidity 0.8.15;
import { OptimismPortal } from "../L1/OptimismPortal.sol";
import { L2OutputOracle } from "../L1/L2OutputOracle.sol";
import { AddressAliasHelper } from "../vendor/AddressAliasHelper.sol";
import { SystemConfig } from "../L1/SystemConfig.sol";
import { ResourceMetering } from "../L1/ResourceMetering.sol";
import { Constants } from "../libraries/Constants.sol";
contract EchidnaFuzzOptimismPortal {
OptimismPortal internal portal;
bool internal failedToComplete;
constructor() {
ResourceMetering.ResourceConfig memory rcfg = Constants.DEFAULT_RESOURCE_CONFIG();
SystemConfig systemConfig = new SystemConfig({
_owner: address(1),
_overhead: 0,
_scalar: 10000,
_batcherHash: bytes32(0),
_gasLimit: 30_000_000,
_unsafeBlockSigner: address(0),
_config: rcfg
});
portal = new OptimismPortal({
_l2Oracle: L2OutputOracle(address(0)),
_guardian: address(0),
_paused: false
_paused: false,
_config: systemConfig
});
}
......
......@@ -3,6 +3,7 @@ pragma solidity 0.8.15;
import { ResourceMetering } from "../L1/ResourceMetering.sol";
import { Arithmetic } from "../libraries/Arithmetic.sol";
import { StdUtils } from "forge-std/Test.sol";
import { Constants } from "../libraries/Constants.sol";
contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils {
bool internal failedMaxGasPerBlock;
......@@ -24,6 +25,20 @@ contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils {
__ResourceMetering_init();
}
function resourceConfig() public pure returns (ResourceMetering.ResourceConfig memory) {
return _resourceConfig();
}
function _resourceConfig()
internal
pure
override
returns (ResourceMetering.ResourceConfig memory)
{
ResourceMetering.ResourceConfig memory rcfg = Constants.DEFAULT_RESOURCE_CONFIG();
return rcfg;
}
/**
* @notice Takes the necessary parameters to allow us to burn arbitrary amounts of gas to test
* the underlying resource metering/gas market logic
......@@ -34,12 +49,16 @@ contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils {
uint256 cachedPrevBoughtGas = uint256(params.prevBoughtGas);
uint256 cachedPrevBlockNum = uint256(params.prevBlockNum);
ResourceMetering.ResourceConfig memory rcfg = resourceConfig();
uint256 targetResourceLimit = uint256(rcfg.maxResourceLimit) /
uint256(rcfg.elasticityMultiplier);
// check that the last block's base fee hasn't dropped below the minimum
if (cachedPrevBaseFee < uint256(MINIMUM_BASE_FEE)) {
if (cachedPrevBaseFee < uint256(rcfg.minimumBaseFee)) {
failedNeverBelowMinBaseFee = true;
}
// check that the last block didn't consume more than the max amount of gas
if (cachedPrevBoughtGas > uint256(MAX_RESOURCE_LIMIT)) {
if (cachedPrevBoughtGas > uint256(rcfg.maxResourceLimit)) {
failedMaxGasPerBlock = true;
}
......@@ -51,11 +70,11 @@ contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils {
if (_raiseBaseFee) {
gasToBurn = bound(
_gasToBurn,
uint256(TARGET_RESOURCE_LIMIT),
uint256(MAX_RESOURCE_LIMIT)
uint256(targetResourceLimit),
uint256(rcfg.maxResourceLimit)
);
} else {
gasToBurn = bound(_gasToBurn, 0, uint256(TARGET_RESOURCE_LIMIT));
gasToBurn = bound(_gasToBurn, 0, targetResourceLimit);
}
_burnInternal(uint64(gasToBurn));
......@@ -63,13 +82,13 @@ contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils {
// Part 3: we run checks and modify our invariant flags based on the updated params values
// Calculate the maximum allowed baseFee change (per block)
uint256 maxBaseFeeChange = cachedPrevBaseFee / uint256(BASE_FEE_MAX_CHANGE_DENOMINATOR);
uint256 maxBaseFeeChange = cachedPrevBaseFee / uint256(rcfg.baseFeeMaxChangeDenominator);
// If the last block used more than the target amount of gas (and there were no
// empty blocks in between), ensure this block's baseFee increased, but not by
// more than the max amount per block
if (
(cachedPrevBoughtGas > uint256(TARGET_RESOURCE_LIMIT)) &&
(cachedPrevBoughtGas > uint256(targetResourceLimit)) &&
(uint256(params.prevBlockNum) - cachedPrevBlockNum == 1)
) {
failedRaiseBaseFee = failedRaiseBaseFee || (params.prevBaseFee <= cachedPrevBaseFee);
......@@ -81,7 +100,7 @@ contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils {
// If the last block used less than the target amount of gas, (or was empty),
// ensure that: this block's baseFee was decreased, but not by more than the max amount
if (
(cachedPrevBoughtGas < uint256(TARGET_RESOURCE_LIMIT)) ||
(cachedPrevBoughtGas < uint256(targetResourceLimit)) ||
(uint256(params.prevBlockNum) - cachedPrevBlockNum > 1)
) {
// Invariant: baseFee should decrease
......@@ -104,11 +123,11 @@ contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils {
Arithmetic.clamp(
Arithmetic.cdexp(
int256(cachedPrevBaseFee),
BASE_FEE_MAX_CHANGE_DENOMINATOR,
int256(uint256(rcfg.baseFeeMaxChangeDenominator)),
int256(uint256(params.prevBlockNum) - cachedPrevBlockNum)
),
MINIMUM_BASE_FEE,
MAXIMUM_BASE_FEE
int256(uint256(rcfg.minimumBaseFee)),
int256(uint256(rcfg.maximumBaseFee))
)
);
}
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import { ResourceMetering } from "../L1/ResourceMetering.sol";
/**
* @title Constants
* @notice Constants is a library for storing constants. Simple! Don't put everything in here, just
......@@ -24,4 +26,24 @@ library Constants {
* non-zero to reduce the gas cost of message passing transactions.
*/
address internal constant DEFAULT_L2_SENDER = 0x000000000000000000000000000000000000dEaD;
/**
* @notice Returns the default values for the ResourceConfig. These are the recommended values
* for a production network.
*/
function DEFAULT_RESOURCE_CONFIG()
internal
pure
returns (ResourceMetering.ResourceConfig memory)
{
ResourceMetering.ResourceConfig memory config = ResourceMetering.ResourceConfig({
maxResourceLimit: 20_000_000,
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 8,
minimumBaseFee: 1 gwei,
systemTxMaxGas: 1_000_000,
maximumBaseFee: type(uint128).max
});
return config;
}
}
......@@ -32,8 +32,6 @@ contract SetPrevBaseFee_Test is Portal_Initializer {
// In order to achieve this we make no assertions, and handle everything else in the setUp()
// function.
contract GasBenchMark_OptimismPortal is Portal_Initializer {
uint128 internal INITIAL_BASE_FEE;
// Reusable default values for a test withdrawal
Types.WithdrawalTransaction _defaultTx;
......@@ -86,8 +84,6 @@ contract GasBenchMark_OptimismPortal is Portal_Initializer {
1
);
INITIAL_BASE_FEE = op.INITIAL_BASE_FEE();
// Fund the portal so that we can withdraw ETH.
vm.deal(address(op), 0xFFFFFFFF);
}
......@@ -103,7 +99,7 @@ contract GasBenchMark_OptimismPortal is Portal_Initializer {
}
function test_depositTransaction_benchmark_1() external {
setPrevBaseFee(vm, address(op), INITIAL_BASE_FEE);
setPrevBaseFee(vm, address(op), 1 gwei);
op.depositTransaction{ value: NON_ZERO_VALUE }(
NON_ZERO_ADDRESS,
ZERO_VALUE,
......@@ -124,16 +120,9 @@ contract GasBenchMark_OptimismPortal is Portal_Initializer {
}
contract GasBenchMark_L1CrossDomainMessenger is Messenger_Initializer {
uint128 internal INITIAL_BASE_FEE;
function setUp() public virtual override {
super.setUp();
INITIAL_BASE_FEE = op.INITIAL_BASE_FEE();
}
function test_sendMessage_benchmark_0() external {
vm.pauseGasMetering();
setPrevBaseFee(vm, address(op), INITIAL_BASE_FEE);
setPrevBaseFee(vm, address(op), 1 gwei);
// The amount of data typically sent during a bridge deposit.
bytes
memory data = hex"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
......@@ -153,11 +142,8 @@ contract GasBenchMark_L1CrossDomainMessenger is Messenger_Initializer {
}
contract GasBenchMark_L1StandardBridge_Deposit is Bridge_Initializer {
uint128 internal INITIAL_BASE_FEE;
function setUp() public virtual override {
super.setUp();
INITIAL_BASE_FEE = op.INITIAL_BASE_FEE();
deal(address(L1Token), alice, 100000, true);
vm.startPrank(alice, alice);
L1Token.approve(address(L1Bridge), type(uint256).max);
......@@ -165,7 +151,7 @@ contract GasBenchMark_L1StandardBridge_Deposit is Bridge_Initializer {
function test_depositETH_benchmark_0() external {
vm.pauseGasMetering();
setPrevBaseFee(vm, address(op), INITIAL_BASE_FEE);
setPrevBaseFee(vm, address(op), 1 gwei);
vm.resumeGasMetering();
L1Bridge.depositETH{ value: 500 }(50000, hex"");
}
......@@ -179,7 +165,7 @@ contract GasBenchMark_L1StandardBridge_Deposit is Bridge_Initializer {
function test_depositERC20_benchmark_0() external {
vm.pauseGasMetering();
setPrevBaseFee(vm, address(op), INITIAL_BASE_FEE);
setPrevBaseFee(vm, address(op), 1 gwei);
vm.resumeGasMetering();
L1Bridge.bridgeERC20({
_localToken: address(L1Token),
......
......@@ -28,6 +28,9 @@ import { L1ChugSplashProxy } from "../legacy/L1ChugSplashProxy.sol";
import { IL1ChugSplashDeployer } from "../legacy/L1ChugSplashProxy.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { LegacyMintableERC20 } from "../legacy/LegacyMintableERC20.sol";
import { SystemConfig } from "../L1/SystemConfig.sol";
import { ResourceMetering } from "../L1/ResourceMetering.sol";
import { Constants } from "../libraries/Constants.sol";
contract CommonTest is Test {
address alice = address(128);
......@@ -158,6 +161,7 @@ contract Portal_Initializer is L2OutputOracle_Initializer {
// Test target
OptimismPortal internal opImpl;
OptimismPortal internal op;
SystemConfig systemConfig;
event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success);
event WithdrawalProven(
......@@ -169,7 +173,25 @@ contract Portal_Initializer is L2OutputOracle_Initializer {
function setUp() public virtual override {
super.setUp();
opImpl = new OptimismPortal({ _l2Oracle: oracle, _guardian: guardian, _paused: true });
ResourceMetering.ResourceConfig memory config = Constants.DEFAULT_RESOURCE_CONFIG();
systemConfig = new SystemConfig({
_owner: address(1),
_overhead: 0,
_scalar: 10000,
_batcherHash: bytes32(0),
_gasLimit: 30_000_000,
_unsafeBlockSigner: address(0),
_config: config
});
opImpl = new OptimismPortal({
_l2Oracle: oracle,
_guardian: guardian,
_paused: true,
_config: systemConfig
});
Proxy proxy = new Proxy(multisig);
vm.prank(multisig);
proxy.upgradeToAndCall(
......
......@@ -9,6 +9,7 @@ import { OptimismPortal } from "../L1/OptimismPortal.sol";
import { Types } from "../libraries/Types.sol";
import { Hashing } from "../libraries/Hashing.sol";
import { Proxy } from "../universal/Proxy.sol";
import { ResourceMetering } from "../L1/ResourceMetering.sol";
contract OptimismPortal_Test is Portal_Initializer {
event Paused(address);
......@@ -1045,10 +1046,12 @@ contract OptimismPortalUpgradeable_Test is Portal_Initializer {
}
function test_params_initValuesOnProxy_succeeds() external {
(uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = OptimismPortal(
payable(address(proxy))
).params();
assertEq(prevBaseFee, opImpl.INITIAL_BASE_FEE());
OptimismPortal p = OptimismPortal(payable(address(proxy)));
(uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = p.params();
ResourceMetering.ResourceConfig memory rcfg = systemConfig.resourceConfig();
assertEq(prevBaseFee, rcfg.minimumBaseFee);
assertEq(prevBoughtGas, 0);
assertEq(prevBlockNum, initialBlockNum);
}
......
......@@ -4,6 +4,7 @@ pragma solidity 0.8.15;
import { Test } from "forge-std/Test.sol";
import { ResourceMetering } from "../L1/ResourceMetering.sol";
import { Proxy } from "../universal/Proxy.sol";
import { Constants } from "../libraries/Constants.sol";
contract MeterUser is ResourceMetering {
constructor() {
......@@ -14,6 +15,19 @@ contract MeterUser is ResourceMetering {
__ResourceMetering_init();
}
function resourceConfig() public pure returns (ResourceMetering.ResourceConfig memory) {
return _resourceConfig();
}
function _resourceConfig()
internal
pure
override
returns (ResourceMetering.ResourceConfig memory)
{
return Constants.DEFAULT_RESOURCE_CONFIG();
}
function use(uint64 _amount) public metered(_amount) {}
function set(
......@@ -29,6 +43,11 @@ contract MeterUser is ResourceMetering {
}
}
/**
* @title ResourceConfig
* @notice The tests are based on the default config values. It is expected that
* the config values used in these tests are ran in production.
*/
contract ResourceMetering_Test is Test {
MeterUser internal meter;
uint64 initialBlockNum;
......@@ -38,42 +57,15 @@ contract ResourceMetering_Test is Test {
initialBlockNum = uint64(block.number);
}
/**
* @notice The INITIAL_BASE_FEE must be less than the MAXIMUM_BASE_FEE
* and greater than the MINIMUM_BASE_FEE.
*/
function test_meter_initialBaseFee_succeeds() external {
uint256 max = uint256(meter.MAXIMUM_BASE_FEE());
uint256 min = uint256(meter.MINIMUM_BASE_FEE());
uint256 initial = uint256(meter.INITIAL_BASE_FEE());
assertTrue(max >= initial);
assertTrue(min <= initial);
}
/**
* @notice The MINIMUM_BASE_FEE must be less than the MAXIMUM_BASE_FEE.
*/
function test_meter_minBaseFeeLessThanMaxBaseFee_succeeds() external {
uint256 max = uint256(meter.MAXIMUM_BASE_FEE());
uint256 min = uint256(meter.MINIMUM_BASE_FEE());
assertTrue(max > min);
}
function test_meter_initialResourceParams_succeeds() external {
(uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = meter.params();
ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig();
assertEq(prevBaseFee, meter.INITIAL_BASE_FEE());
assertEq(prevBaseFee, rcfg.minimumBaseFee);
assertEq(prevBoughtGas, 0);
assertEq(prevBlockNum, initialBlockNum);
}
function test_meter_maxValue_succeeds() external {
uint256 max = uint256(meter.MAX_RESOURCE_LIMIT());
uint256 target = uint256(meter.TARGET_RESOURCE_LIMIT());
uint256 elasticity = uint256(meter.ELASTICITY_MULTIPLIER());
assertEq(max / elasticity, target);
}
function test_meter_updateParamsNoChange_succeeds() external {
meter.use(0); // equivalent to just updating the base fee and block number
(uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = meter.params();
......@@ -116,8 +108,9 @@ contract ResourceMetering_Test is Test {
}
function test_meter_updateNoGasDelta_succeeds() external {
uint64 target = uint64(uint256(meter.TARGET_RESOURCE_LIMIT()));
meter.use(target);
ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig();
uint256 target = uint256(rcfg.maxResourceLimit) / uint256(rcfg.elasticityMultiplier);
meter.use(uint64(target));
(uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = meter.params();
assertEq(prevBaseFee, 1000000000);
......@@ -126,12 +119,14 @@ contract ResourceMetering_Test is Test {
}
function test_meter_useMax_succeeds() external {
uint64 target = uint64(uint256(meter.TARGET_RESOURCE_LIMIT()));
uint64 elasticity = uint64(uint256(meter.ELASTICITY_MULTIPLIER()));
meter.use(target * elasticity);
ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig();
uint64 target = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier);
uint64 elasticityMultiplier = uint64(rcfg.elasticityMultiplier);
meter.use(target * elasticityMultiplier);
(, uint64 prevBoughtGas, ) = meter.params();
assertEq(prevBoughtGas, target * elasticity);
assertEq(prevBoughtGas, target * elasticityMultiplier);
vm.roll(initialBlockNum + 1);
meter.use(0);
......@@ -140,10 +135,12 @@ contract ResourceMetering_Test is Test {
}
function test_meter_useMoreThanMax_reverts() external {
uint64 target = uint64(uint256(meter.TARGET_RESOURCE_LIMIT()));
uint64 elasticity = uint64(uint256(meter.ELASTICITY_MULTIPLIER()));
ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig();
uint64 target = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier);
uint64 elasticityMultiplier = uint64(rcfg.elasticityMultiplier);
vm.expectRevert("ResourceMetering: cannot buy more gas than available gas limit");
meter.use(target * elasticity + 1);
meter.use(target * elasticityMultiplier + 1);
}
// Demonstrates that the resource metering arithmetic can tolerate very large gaps between
......@@ -153,9 +150,11 @@ contract ResourceMetering_Test is Test {
// At 12 seconds per block, this number is effectively unreachable.
vm.assume(_blockDiff < 433576281058164217753225238677900874458691);
uint64 target = uint64(uint256(meter.TARGET_RESOURCE_LIMIT()));
uint64 elasticity = uint64(uint256(meter.ELASTICITY_MULTIPLIER()));
vm.assume(_amount < target * elasticity);
ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig();
uint64 target = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier);
uint64 elasticityMultiplier = uint64(rcfg.elasticityMultiplier);
vm.assume(_amount < target * elasticityMultiplier);
vm.roll(initialBlockNum + _blockDiff);
meter.use(_amount);
}
......@@ -182,6 +181,15 @@ contract CustomMeterUser is ResourceMetering {
});
}
function _resourceConfig()
internal
pure
override
returns (ResourceMetering.ResourceConfig memory)
{
return Constants.DEFAULT_RESOURCE_CONFIG();
}
function use(uint64 _amount) public returns (uint256) {
uint256 initialGas = gasleft();
_metered(_amount, initialGas);
......@@ -224,10 +232,11 @@ contract ArtifactResourceMetering_Test is Test {
vm.roll(1_000_000);
MeterUser base = new MeterUser();
minimumBaseFee = uint128(uint256(base.MINIMUM_BASE_FEE()));
maximumBaseFee = uint128(uint256(base.MAXIMUM_BASE_FEE()));
maxResourceLimit = uint64(uint256(base.MAX_RESOURCE_LIMIT()));
targetResourceLimit = uint64(uint256(base.TARGET_RESOURCE_LIMIT()));
ResourceMetering.ResourceConfig memory rcfg = base.resourceConfig();
minimumBaseFee = uint128(rcfg.minimumBaseFee);
maximumBaseFee = rcfg.maximumBaseFee;
maxResourceLimit = uint64(rcfg.maxResourceLimit);
targetResourceLimit = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier);
outfile = string.concat(vm.projectRoot(), "/.resource-metering.csv");
try vm.removeFile(outfile) {} catch {}
......
......@@ -3,36 +3,58 @@ pragma solidity 0.8.15;
import { CommonTest } from "./CommonTest.t.sol";
import { SystemConfig } from "../L1/SystemConfig.sol";
import { ResourceMetering } from "../L1/ResourceMetering.sol";
import { Constants } from "../libraries/Constants.sol";
contract SystemConfig_Init is CommonTest {
SystemConfig sysConf;
function setUp() public virtual override {
super.setUp();
ResourceMetering.ResourceConfig memory config = ResourceMetering.ResourceConfig({
maxResourceLimit: 20_000_000,
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 8,
minimumBaseFee: 1 gwei,
systemTxMaxGas: 1_000_000,
maximumBaseFee: type(uint128).max
});
sysConf = new SystemConfig({
_owner: alice,
_overhead: 2100,
_scalar: 1000000,
_batcherHash: bytes32(hex"abcd"),
_gasLimit: 9_000_000,
_unsafeBlockSigner: address(1)
_gasLimit: 30_000_000,
_unsafeBlockSigner: address(1),
_config: config
});
}
}
contract SystemConfig_Initialize_TestFail is CommonTest {
contract SystemConfig_Initialize_TestFail is SystemConfig_Init {
function test_initialize_lowGasLimit_reverts() external {
vm.expectRevert("SystemConfig: gas limit too low");
uint64 minimumGasLimit = sysConf.minimumGasLimit();
ResourceMetering.ResourceConfig memory cfg = ResourceMetering.ResourceConfig({
maxResourceLimit: 20_000_000,
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 8,
minimumBaseFee: 1 gwei,
systemTxMaxGas: 1_000_000,
maximumBaseFee: type(uint128).max
});
// The minimum gas limit defined in SystemConfig:
uint64 MINIMUM_GAS_LIMIT = 8_000_000;
vm.expectRevert("SystemConfig: gas limit too low");
new SystemConfig({
_owner: alice,
_overhead: 0,
_scalar: 0,
_batcherHash: bytes32(hex""),
_gasLimit: MINIMUM_GAS_LIMIT - 1,
_unsafeBlockSigner: address(1)
_gasLimit: minimumGasLimit - 1,
_unsafeBlockSigner: address(1),
_config: cfg
});
}
}
......@@ -57,6 +79,70 @@ contract SystemConfig_Setters_TestFail is SystemConfig_Init {
vm.expectRevert("Ownable: caller is not the owner");
sysConf.setUnsafeBlockSigner(address(0x20));
}
function test_setResourceConfig_notOwner_reverts() external {
ResourceMetering.ResourceConfig memory config = Constants.DEFAULT_RESOURCE_CONFIG();
vm.expectRevert("Ownable: caller is not the owner");
sysConf.setResourceConfig(config);
}
function test_setResourceConfig_badMinMax_reverts() external {
ResourceMetering.ResourceConfig memory config = ResourceMetering.ResourceConfig({
maxResourceLimit: 20_000_000,
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 8,
systemTxMaxGas: 1_000_000,
minimumBaseFee: 2 gwei,
maximumBaseFee: 1 gwei
});
vm.prank(sysConf.owner());
vm.expectRevert("SystemConfig: min base fee must be less than max base");
sysConf.setResourceConfig(config);
}
function test_setResourceConfig_zeroDenominator_reverts() external {
ResourceMetering.ResourceConfig memory config = ResourceMetering.ResourceConfig({
maxResourceLimit: 20_000_000,
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 0,
systemTxMaxGas: 1_000_000,
minimumBaseFee: 1 gwei,
maximumBaseFee: 2 gwei
});
vm.prank(sysConf.owner());
vm.expectRevert("SystemConfig: denominator cannot be 0");
sysConf.setResourceConfig(config);
}
function test_setResourceConfig_lowGasLimit_reverts() external {
uint64 gasLimit = sysConf.gasLimit();
ResourceMetering.ResourceConfig memory config = ResourceMetering.ResourceConfig({
maxResourceLimit: uint32(gasLimit),
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 8,
systemTxMaxGas: uint32(gasLimit),
minimumBaseFee: 1 gwei,
maximumBaseFee: 2 gwei
});
vm.prank(sysConf.owner());
vm.expectRevert("SystemConfig: gas limit too low");
sysConf.setResourceConfig(config);
}
function test_setResourceConfig_badPrecision_reverts() external {
ResourceMetering.ResourceConfig memory config = ResourceMetering.ResourceConfig({
maxResourceLimit: 20_000_000,
elasticityMultiplier: 11,
baseFeeMaxChangeDenominator: 8,
systemTxMaxGas: 1_000_000,
minimumBaseFee: 1 gwei,
maximumBaseFee: 2 gwei
});
vm.prank(sysConf.owner());
vm.expectRevert("SystemConfig: precision loss with target resource limit");
sysConf.setResourceConfig(config);
}
}
contract SystemConfig_Setters_Test is SystemConfig_Init {
......
......@@ -2,18 +2,23 @@ pragma solidity 0.8.15;
import { Test } from "forge-std/Test.sol";
import { SystemConfig } from "../../L1/SystemConfig.sol";
import { ResourceMetering } from "../../L1/ResourceMetering.sol";
import { Constants } from "../../libraries/Constants.sol";
contract SystemConfig_GasLimitLowerBound_Invariant is Test {
SystemConfig public config;
function setUp() public {
ResourceMetering.ResourceConfig memory cfg = Constants.DEFAULT_RESOURCE_CONFIG();
config = new SystemConfig({
_owner: address(0xbeef),
_overhead: 2100,
_scalar: 1000000,
_batcherHash: bytes32(hex"abcd"),
_gasLimit: 8_000_000,
_unsafeBlockSigner: address(1)
_gasLimit: 30_000_000,
_unsafeBlockSigner: address(1),
_config: cfg
});
// Set the target contract to the `config`
......
......@@ -17,6 +17,10 @@ const deployFn: DeployFunction = async (hre) => {
'L2OutputOracleProxy'
)
const Artifact__SystemConfigProxy = await hre.deployments.get(
'SystemConfigProxy'
)
const portalGuardian = hre.deployConfig.portalGuardian
const portalGuardianCode = await hre.ethers.provider.getCode(portalGuardian)
if (portalGuardianCode === '0x') {
......@@ -41,6 +45,7 @@ const deployFn: DeployFunction = async (hre) => {
L2OutputOracleProxy.address,
portalGuardian,
true, // paused
Artifact__SystemConfigProxy.address,
],
postDeployAction: async (contract) => {
await assertContractVariable(
......@@ -53,6 +58,11 @@ const deployFn: DeployFunction = async (hre) => {
'GUARDIAN',
hre.deployConfig.portalGuardian
)
await assertContractVariable(
contract,
'SYSTEM_CONFIG',
Artifact__SystemConfigProxy.address
)
},
})
}
......
import assert from 'assert'
import { DeployFunction } from 'hardhat-deploy/dist/types'
import '@eth-optimism/hardhat-deploy-config'
import { ethers } from 'ethers'
import { assertContractVariable, deploy } from '../src/deploy-utils'
const uint128Max = ethers.BigNumber.from('0xffffffffffffffffffffffffffffffff')
const deployFn: DeployFunction = async (hre) => {
const batcherHash = hre.ethers.utils
.hexZeroPad(hre.deployConfig.batchSenderAddress, 32)
......@@ -18,6 +23,14 @@ const deployFn: DeployFunction = async (hre) => {
batcherHash,
hre.deployConfig.l2GenesisBlockGasLimit,
hre.deployConfig.p2pSequencerAddress,
{
maxResourceLimit: 20_000_000,
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 8,
systemTxMaxGas: 1_000_000,
minimumBaseFee: ethers.utils.parseUnits('1', 'gwei'),
maximumBaseFee: uint128Max,
},
],
postDeployAction: async (contract) => {
await assertContractVariable(
......@@ -41,6 +54,14 @@ const deployFn: DeployFunction = async (hre) => {
'unsafeBlockSigner',
hre.deployConfig.p2pSequencerAddress
)
const config = await contract.resourceConfig()
assert(config.maxResourceLimit === 20_000_000)
assert(config.elasticityMultiplier === 10)
assert(config.baseFeeMaxChangeDenominator === 8)
assert(config.systemTxMaxGas === 1_000_000)
assert(ethers.utils.parseUnits('1', 'gwei').eq(config.minimumBaseFee))
assert(config.maximumBaseFee.eq(uint128Max))
},
})
}
......
import assert from 'assert'
import { ethers } from 'ethers'
import { ethers, BigNumber } from 'ethers'
import { DeployFunction } from 'hardhat-deploy/dist/types'
import { awaitCondition } from '@eth-optimism/core-utils'
import '@eth-optimism/hardhat-deploy-config'
......@@ -100,6 +100,18 @@ const deployFn: DeployFunction = async (hre) => {
),
gasLimit: hre.deployConfig.l2GenesisBlockGasLimit,
unsafeBlockSigner: hre.deployConfig.p2pSequencerAddress,
// The resource config is not exposed to the end user
// to simplify deploy config. It may be introduced in the future.
resourceConfig: {
maxResourceLimit: 20_000_000,
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 8,
minimumBaseFee: ethers.utils.parseUnits('1', 'gwei'),
systemTxMaxGas: 1_000_000,
maximumBaseFee: BigNumber.from(
'0xffffffffffffffffffffffffffffffff'
).toString(),
},
},
}
......
......@@ -17,6 +17,8 @@ import {
getCastCommand,
} from '../src/deploy-utils'
const uint128Max = ethers.BigNumber.from('0xffffffffffffffffffffffffffffffff')
const deployFn: DeployFunction = async (hre) => {
const { deployer } = await hre.getNamedAccounts()
......@@ -254,6 +256,14 @@ const deployFn: DeployFunction = async (hre) => {
'gasLimit',
hre.deployConfig.l2GenesisBlockGasLimit
)
const config = await SystemConfigProxy.resourceConfig()
assert(config.maxResourceLimit === 20_000_000)
assert(config.elasticityMultiplier === 10)
assert(config.baseFeeMaxChangeDenominator === 8)
assert(config.systemTxMaxGas === 1_000_000)
assert(ethers.utils.parseUnits('1', 'gwei').eq(config.minimumBaseFee))
assert(config.maximumBaseFee.eq(uint128Max))
},
})
......
......@@ -234,7 +234,7 @@ const deployFn: DeployFunction = async (hre) => {
)
const resourceParams = await OptimismPortal.params()
assert(
resourceParams.prevBaseFee.eq(await OptimismPortal.INITIAL_BASE_FEE()),
resourceParams.prevBaseFee.eq(ethers.utils.parseUnits('1', 'gwei')),
`OptimismPortal was not initialized with the correct initial base fee`
)
assert(
......
......@@ -19,6 +19,6 @@ This invariant asserts that there is no chain of calls that can be made that wil
## Deposits of any value should always succeed unless `_to` = `address(0)` or `_isCreation` = `true`.
**Test:** [`FuzzOptimismPortal.sol#L41`](../contracts/echidna/FuzzOptimismPortal.sol#L41)
**Test:** [`FuzzOptimismPortal.sol#L57`](../contracts/echidna/FuzzOptimismPortal.sol#L57)
All deposits, barring creation transactions and transactions sent to `address(0)`, should always succeed.
# `ResourceMetering` Invariants
## The base fee should increase if the last block used more than the target amount of gas
**Test:** [`FuzzResourceMetering.sol#L139`](../contracts/echidna/FuzzResourceMetering.sol#L139)
**Test:** [`FuzzResourceMetering.sol#L158`](../contracts/echidna/FuzzResourceMetering.sol#L158)
If the last block used more than the target amount of gas (and there were no empty blocks in between), ensure this block's baseFee increased, but not by more than the max amount per block.
## The base fee should decrease if the last block used less than the target amount of gas
**Test:** [`FuzzResourceMetering.sol#L150`](../contracts/echidna/FuzzResourceMetering.sol#L150)
**Test:** [`FuzzResourceMetering.sol#L169`](../contracts/echidna/FuzzResourceMetering.sol#L169)
If the previous block used less than the target amount of gas, the base fee should decrease, but not more than the max amount.
## A block's base fee should never be below `MINIMUM_BASE_FEE`
**Test:** [`FuzzResourceMetering.sol#L160`](../contracts/echidna/FuzzResourceMetering.sol#L160)
**Test:** [`FuzzResourceMetering.sol#L179`](../contracts/echidna/FuzzResourceMetering.sol#L179)
This test asserts that a block's base fee can never drop below the `MINIMUM_BASE_FEE` threshold.
## A block can never consume more than `MAX_RESOURCE_LIMIT` gas.
**Test:** [`FuzzResourceMetering.sol#L170`](../contracts/echidna/FuzzResourceMetering.sol#L170)
**Test:** [`FuzzResourceMetering.sol#L189`](../contracts/echidna/FuzzResourceMetering.sol#L189)
This test asserts that a block can never consume more than the `MAX_RESOURCE_LIMIT` gas threshold.
## The base fee can never be raised more than the max base fee change.
**Test:** [`FuzzResourceMetering.sol#L181`](../contracts/echidna/FuzzResourceMetering.sol#L181)
**Test:** [`FuzzResourceMetering.sol#L200`](../contracts/echidna/FuzzResourceMetering.sol#L200)
After a block consumes more gas than the target gas, the base fee cannot be raised more than the maximum amount allowed. The max base fee change (per-block) is derived as follows: `prevBaseFee / BASE_FEE_MAX_CHANGE_DENOMINATOR`
## The base fee can never be lowered more than the max base fee change.
**Test:** [`FuzzResourceMetering.sol#L192`](../contracts/echidna/FuzzResourceMetering.sol#L192)
**Test:** [`FuzzResourceMetering.sol#L211`](../contracts/echidna/FuzzResourceMetering.sol#L211)
After a block consumes less than the target gas, the base fee cannot be lowered more than the maximum amount allowed. The max base fee change (per-block) is derived as follows: `prevBaseFee / BASE_FEE_MAX_CHANGE_DENOMINATOR`
## The `maxBaseFeeChange` calculation over multiple blocks can never underflow.
**Test:** [`FuzzResourceMetering.sol#L203`](../contracts/echidna/FuzzResourceMetering.sol#L203)
**Test:** [`FuzzResourceMetering.sol#L222`](../contracts/echidna/FuzzResourceMetering.sol#L222)
When calculating the `maxBaseFeeChange` after multiple empty blocks, the calculation should never be allowed to underflow.
# `SystemConfig` Invariants
## The gas limit of the `SystemConfig` contract can never be lower than the hard-coded lower bound.
**Test:** [`SystemConfig.t.sol#L39`](../contracts/test/invariants/SystemConfig.t.sol#L39)
**Test:** [`SystemConfig.t.sol#L44`](../contracts/test/invariants/SystemConfig.t.sol#L44)
......@@ -5,8 +5,10 @@
**Table of Contents**
- [Gas Stipend](#gas-stipend)
- [Default Values](#default-values)
- [Limiting Guaranteed Gas](#limiting-guaranteed-gas)
- [Rationale for burning L1 Gas](#rationale-for-burning-l1-gas)
- [On Preventing Griefing Attacks](#on-preventing-griefing-attacks)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
......@@ -36,6 +38,17 @@ the L2 gas, where `gas spent` is the amount of L1 gas spent processing the depos
of this credit is greater than the ETH value of the requested guaranteed gas
(`requested guaranteed gas * L2 gas price`), no L1 gas is burnt.
## Default Values
| Variable | Value |
| ------------------------------- | ----------------- |
| Max Resource Limit | 20,000,000 |
| Elasticity Multiplier | 10 |
| Base Fee Max Change Denominator | 8 |
| Minimum Base Fee | 1 gwei |
| Maximum Base Fee | type(uint128).max |
| System Tx Max Gas | 1,000,000 |
## Limiting Guaranteed Gas
The total amount of guaranteed gas that can be bought in a single L1 block must be limited to
......@@ -123,3 +136,20 @@ The payable version (Option 2) will likely have discount applied to it (or conve
premium applied to it).
For the initial release of bedrock, only #1 is supported.
## On Preventing Griefing Attacks
The cost of purchasing all of the deposit gas in every block must be expensive
enough to prevent attackers from griefing all deposits to the network.
An attacker would observe a deposit in the mempool and frontrun it with a deposit
that purchases enough gas such that the other deposit reverts.
The smaller the max resource limit is, the easier this attack is to pull off.
This attack is mitigated by having a large resource limit as well as a large
elastcity multiplier. This means that the target resource usage is kept small,
giving a lot of room for the deposit base fee to rise when the max resource limit
is being purchased.
This attack should be too expensive to pull off in practice, but if an extremely
wealthy adversary does decide to grief network deposits for an extended period
of time, efforts will be placed to ensure that deposits are able to be processed
on the network.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment