Commit 1b917041 authored by Mark Tyneway's avatar Mark Tyneway Committed by Kelvin Fichter

contracts: update `OVM_GasPriceOracle`

Add a setter for L1 base fee. The trusted oracle can update it over
time. Eventually this will become trustless, but this is a quick and
easy approach for now.

Also add a setter/getter for the per batch overhead and the scalar.

Also emit events for when the values are updated. This will make it much
easier to track historical gas prices over time.

Add tests for new functionality. L2 geth will consume the new value in
the `OVM_GasPriceOracle`

Add getters that make it easy for users to know the L1 costs.
- `getL1Fee` returns the L1 fee given the current L1 base fee known by
  the L2 node
- `getL1GasUsed` counts the bytes and creates a sum for the gas cost of
  submitting the data to L1
parent 78ad23db
---
'@eth-optimism/contracts': patch
---
Add getter and setter to `OVM_GasPriceOracle` for the l1 base fee
......@@ -3,6 +3,12 @@ import * as fs from 'fs'
import * as path from 'path'
import * as mkdirp from 'mkdirp'
const ensure = (value, key) => {
if (typeof value === 'undefined' || value === null || Number.isNaN(value)) {
throw new Error(`${key} is undefined, null or NaN`)
}
}
/* Internal Imports */
import { makeL2GenesisFile } from '../src/make-genesis'
;(async () => {
......@@ -10,24 +16,82 @@ import { makeL2GenesisFile } from '../src/make-genesis'
const outfile = path.join(outdir, 'state-dump.latest.json')
mkdirp.sync(outdir)
const env = process.env
// An account that represents the owner of the whitelist
const whitelistOwner = env.WHITELIST_OWNER
// The gas price oracle owner, can update values is GasPriceOracle L2 predeploy
const gasPriceOracleOwner = env.GAS_PRICE_ORACLE_OWNER
// The initial overhead value for the GasPriceOracle
const gasPriceOracleOverhead = parseInt(
env.GAS_PRICE_ORACLE_OVERHEAD || '2750',
10
)
// The initial scalar value for the GasPriceOracle. The actual
// scalar is scaled downwards by the number of decimals
const gasPriceOracleScalar = parseInt(
env.GAS_PRICE_ORACLE_SCALAR || '1500000',
10
)
// The initial decimals that scale down the scalar in the GasPriceOracle
const gasPriceOracleDecimals = parseInt(
env.GAS_PRICE_ORACLE_DECIMALS || '6',
10
)
// The initial L1 base fee in the GasPriceOracle. This determines how
// expensive the L1 portion of the transaction fee is.
const gasPriceOracleL1BaseFee = parseInt(
env.GAS_PRICE_ORACLE_L1_BASE_FEE || '1',
10
)
// The initial L2 gas price set in the GasPriceOracle
const gasPriceOracleGasPrice = parseInt(
env.GAS_PRICE_ORACLE_GAS_PRICE || '1',
10
)
// The L2 block gas limit, used in the L2 block headers as well to limit
// the amount of execution for a single block.
const l2BlockGasLimit = parseInt(env.L2_BLOCK_GAS_LIMIT, 10)
// The L2 chain id, added to the chain config
const l2ChainId = parseInt(env.L2_CHAIN_ID, 10)
// The block signer address, added to the block extradata for clique consensus
const blockSignerAddress = env.BLOCK_SIGNER_ADDRESS
// The L1 standard bridge address for cross domain messaging
const l1StandardBridgeAddress = env.L1_STANDARD_BRIDGE_ADDRESS
// The L1 fee wallet address, used to restrict the account that fees on L2 can
// be withdrawn to on L1
const l1FeeWalletAddress = env.L1_FEE_WALLET_ADDRESS
// The L1 cross domain messenger address, used for cross domain messaging
const l1CrossDomainMessengerAddress = env.L1_CROSS_DOMAIN_MESSENGER_ADDRESS
ensure(l2BlockGasLimit, 'L2_BLOCK_GAS_LIMIT')
ensure(l2ChainId, 'L2_CHAIN_ID')
ensure(blockSignerAddress, 'BLOCK_SIGNER_ADDRESS')
ensure(l1StandardBridgeAddress, 'L1_STANDARD_BRIDGE_ADDRESS')
ensure(l1FeeWalletAddress, 'L1_FEE_WALLET_ADDRESS')
ensure(l1CrossDomainMessengerAddress, 'L1_CROSS_DOMAIN_MESSENGER_ADDRESS')
// Basic warning so users know that the whitelist will be disabled if the owner is the zero address.
if (process.env.WHITELIST_OWNER === '0x' + '00'.repeat(20)) {
if (env.WHITELIST_OWNER === '0x' + '00'.repeat(20)) {
console.log(
'WARNING: whitelist owner is address(0), whitelist will be disabled'
)
}
const genesis = await makeL2GenesisFile({
whitelistOwner: process.env.WHITELIST_OWNER,
gasPriceOracleOwner: process.env.GAS_PRICE_ORACLE_OWNER,
initialGasPrice: 0,
l2BlockGasLimit: parseInt(process.env.L2_BLOCK_GAS_LIMIT, 10),
l2ChainId: parseInt(process.env.L2_CHAIN_ID, 10),
blockSignerAddress: process.env.BLOCK_SIGNER_ADDRESS,
l1StandardBridgeAddress: process.env.L1_STANDARD_BRIDGE_ADDRESS,
l1FeeWalletAddress: process.env.L1_FEE_WALLET_ADDRESS,
l1CrossDomainMessengerAddress:
process.env.L1_CROSS_DOMAIN_MESSENGER_ADDRESS,
whitelistOwner,
gasPriceOracleOwner,
gasPriceOracleOverhead,
gasPriceOracleScalar,
gasPriceOracleL1BaseFee,
gasPriceOracleGasPrice,
gasPriceOracleDecimals,
l2BlockGasLimit,
l2ChainId,
blockSignerAddress,
l1StandardBridgeAddress,
l1FeeWalletAddress,
l1CrossDomainMessengerAddress,
})
fs.writeFileSync(outfile, JSON.stringify(genesis, null, 4))
......
// SPDX-License-Identifier: MIT
pragma solidity >0.5.0 <0.8.0;
/* Internal Imports */
import { iOVM_GasPriceOracle } from "./iOVM_GasPriceOracle.sol";
/* External Imports */
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol";
/**
* @title OVM_GasPriceOracle
......@@ -10,15 +14,28 @@ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
* currently is. This measure is used by the Sequencer to determine what fee to charge for
* transactions. When the system is more congested, the l2 gas price will increase and fees
* will also increase as a result.
*
* All public variables are set while generating the initial L2 state. The
* constructor doesn't run in practice as the L2 state generation script uses
* the deployed bytecode instead of running the initcode.
*/
contract OVM_GasPriceOracle is Ownable {
contract OVM_GasPriceOracle is Ownable, iOVM_GasPriceOracle {
/*************
* Variables *
*************/
// Current l2 gas price
// Current L2 gas price
uint256 public gasPrice;
// Current L1 base fee
uint256 public l1BaseFee;
// Amortized cost of batch submission per transaction
uint256 public overhead;
// Value to scale the fee up by
uint256 public scalar;
// Number of decimals of the scalar
uint256 public decimals;
/***************
* Constructor *
......@@ -28,12 +45,10 @@ contract OVM_GasPriceOracle is Ownable {
* @param _owner Address that will initially own this contract.
*/
constructor(
address _owner,
uint256 _initialGasPrice
address _owner
)
Ownable()
{
setGasPrice(_initialGasPrice);
transferOwnership(_owner);
}
......@@ -50,8 +65,135 @@ contract OVM_GasPriceOracle is Ownable {
uint256 _gasPrice
)
public
override
onlyOwner
{
gasPrice = _gasPrice;
emit GasPriceUpdated(_gasPrice);
}
/**
* Allows the owner to modify the l1 base fee.
* @param _baseFee New l1 base fee
*/
function setL1BaseFee(
uint256 _baseFee
)
public
override
onlyOwner
{
l1BaseFee = _baseFee;
emit L1BaseFeeUpdated(_baseFee);
}
/**
* Allows the owner to modify the overhead.
* @param _overhead New overhead
*/
function setOverhead(
uint256 _overhead
)
public
override
onlyOwner
{
overhead = _overhead;
emit OverheadUpdated(_overhead);
}
/**
* Allows the owner to modify the scalar.
* @param _scalar New scalar
*/
function setScalar(
uint256 _scalar
)
public
override
onlyOwner
{
scalar = _scalar;
emit ScalarUpdated(_scalar);
}
/**
* Allows the owner to modify the decimals.
* @param _decimals New decimals
*/
function setDecimals(
uint256 _decimals
)
public
override
onlyOwner
{
decimals = _decimals;
emit DecimalsUpdated(_decimals);
}
/**
* Computes the L1 portion of the fee
* based on the size of the RLP encoded tx
* and the current l1BaseFee
* @param _data Unsigned RLP encoded tx, 6 elements
* @return L1 fee that should be paid for the tx
*/
function getL1Fee(bytes memory _data)
public
view
override
returns (
uint256
)
{
uint256 l1GasUsed = getL1GasUsed(_data);
uint256 l1Fee = SafeMath.mul(l1GasUsed, l1BaseFee);
uint256 divisor = 10**decimals;
uint256 unscaled = SafeMath.mul(l1Fee, scalar);
uint256 scaled = SafeMath.div(unscaled, divisor);
return scaled;
}
/**
* Computes the amount of L1 gas used for a transaction
* The overhead represents the per batch gas overhead of
* posting both transaction and state roots to L1 given larger
* batch sizes.
* 4 gas for 0 byte
* https://github.com/ethereum/go-ethereum/blob/9ada4a2e2c415e6b0b51c50e901336872e028872/params/protocol_params.go#L33
* 16 gas for non zero byte
* https://github.com/ethereum/go-ethereum/blob/9ada4a2e2c415e6b0b51c50e901336872e028872/params/protocol_params.go#L87
* This will need to be updated if calldata gas prices change
* Account for the transaction being unsigned
* Padding is added to account for lack of signature on transaction
* 1 byte for RLP V prefix
* 1 byte for V
* 1 byte for RLP R prefix
* 32 bytes for R
* 1 byte for RLP S prefix
* 32 bytes for S
* Total: 68 bytes of padding
* @param _data Unsigned RLP encoded tx, 6 elements
* @return Amount of L1 gas used for a transaction
*/
function getL1GasUsed(bytes memory _data)
public
view
override
returns (
uint256
)
{
uint256 total = 0;
for (uint256 i = 0; i < _data.length; i++) {
if (_data[i] == 0) {
total += 4;
} else {
total += 16;
}
}
uint256 unsigned = SafeMath.add(total, overhead);
return SafeMath.add(unsigned, 68 * 16);
}
}
// SPDX-License-Identifier: MIT
pragma solidity >0.5.0 <0.8.0;
/**
* @title iOVM_GasPriceOracle
*/
interface iOVM_GasPriceOracle {
/**********
* Events *
**********/
event GasPriceUpdated(uint256);
event L1BaseFeeUpdated(uint256);
event OverheadUpdated(uint256);
event ScalarUpdated(uint256);
event DecimalsUpdated(uint256);
/********************
* Public Functions *
********************/
function setGasPrice(uint256 _gasPrice) external;
function setL1BaseFee(uint256 _baseFee) external;
function setOverhead(uint256 _overhead) external;
function setScalar(uint256 _scalar) external;
function setDecimals(uint256 _decimals) external;
function getL1Fee(bytes memory _data) external returns (uint256);
function getL1GasUsed(bytes memory _data) external returns (uint256);
}
......@@ -14,8 +14,16 @@ export interface RollupDeployConfig {
whitelistOwner: string
// Address that will own the L2 gas price oracle.
gasPriceOracleOwner: string
// Initial value for the L2 gas price.
initialGasPrice: number
// Overhead value of the gas price oracle
gasPriceOracleOverhead: number
// Scalar value of the gas price oracle
gasPriceOracleScalar: number
// L1 base fee of the gas price oracle
gasPriceOracleL1BaseFee: number
// L2 gas price of the gas price oracle
gasPriceOracleGasPrice: number
// Number of decimals of the gas price oracle scalar
gasPriceOracleDecimals: number
// Initial value for the L2 block gas limit.
l2BlockGasLimit: number
// Chain ID to give the L2 network.
......@@ -52,7 +60,11 @@ export const makeL2GenesisFile = async (
},
OVM_GasPriceOracle: {
_owner: cfg.gasPriceOracleOwner,
gasPrice: cfg.initialGasPrice,
gasPrice: cfg.gasPriceOracleGasPrice,
l1BaseFee: cfg.gasPriceOracleL1BaseFee,
overhead: cfg.gasPriceOracleOverhead,
scalar: cfg.gasPriceOracleScalar,
decimals: cfg.gasPriceOracleDecimals,
},
L2StandardBridge: {
l1TokenBridge: cfg.l1StandardBridgeAddress,
......
......@@ -4,6 +4,8 @@ import { expect } from '../../../setup'
import { ethers } from 'hardhat'
import { ContractFactory, Contract, Signer } from 'ethers'
import { calculateL1GasUsed, calculateL1Fee } from '@eth-optimism/core-utils'
describe('OVM_GasPriceOracle', () => {
const initialGasPrice = 0
let signer1: Signer
......@@ -22,9 +24,12 @@ describe('OVM_GasPriceOracle', () => {
let OVM_GasPriceOracle: Contract
beforeEach(async () => {
OVM_GasPriceOracle = await Factory__OVM_GasPriceOracle.deploy(
await signer1.getAddress(),
initialGasPrice
await signer1.getAddress()
)
OVM_GasPriceOracle.setOverhead(2750)
OVM_GasPriceOracle.setScalar(1500000)
OVM_GasPriceOracle.setDecimals(6)
})
describe('owner', () => {
......@@ -45,6 +50,12 @@ describe('OVM_GasPriceOracle', () => {
await expect(OVM_GasPriceOracle.connect(signer1).setGasPrice(0)).to.not.be
.reverted
})
it('should emit event', async () => {
await expect(OVM_GasPriceOracle.connect(signer1).setGasPrice(100))
.to.emit(OVM_GasPriceOracle, 'GasPriceUpdated')
.withArgs(100)
})
})
describe('get gasPrice', () => {
......@@ -61,7 +72,7 @@ describe('OVM_GasPriceOracle', () => {
})
it('is the 1st storage slot', async () => {
const gasPrice = 1234
const gasPrice = 333433
const slot = 1
// set the price
......@@ -77,4 +88,205 @@ describe('OVM_GasPriceOracle', () => {
)
})
})
describe('setL1BaseFee', () => {
it('should revert if called by someone other than the owner', async () => {
await expect(OVM_GasPriceOracle.connect(signer2).setL1BaseFee(1234)).to.be
.reverted
})
it('should succeed if called by the owner', async () => {
await expect(OVM_GasPriceOracle.connect(signer1).setL1BaseFee(0)).to.not
.be.reverted
})
it('should emit event', async () => {
await expect(OVM_GasPriceOracle.connect(signer1).setL1BaseFee(100))
.to.emit(OVM_GasPriceOracle, 'L1BaseFeeUpdated')
.withArgs(100)
})
})
describe('get l1BaseFee', () => {
it('should return zero at first', async () => {
expect(await OVM_GasPriceOracle.l1BaseFee()).to.equal(initialGasPrice)
})
it('should change when setL1BaseFee is called', async () => {
const baseFee = 1234
await OVM_GasPriceOracle.connect(signer1).setL1BaseFee(baseFee)
expect(await OVM_GasPriceOracle.l1BaseFee()).to.equal(baseFee)
})
it('is the 2nd storage slot', async () => {
const baseFee = 12345
const slot = 2
// set the price
await OVM_GasPriceOracle.connect(signer1).setGasPrice(baseFee)
// get the storage slot value
const priceAtSlot = await signer1.provider.getStorageAt(
OVM_GasPriceOracle.address,
slot
)
expect(await OVM_GasPriceOracle.l1BaseFee()).to.equal(
ethers.BigNumber.from(priceAtSlot)
)
})
})
// Test cases for gas estimation
const inputs = [
'0x',
'0x00',
'0x01',
'0x0001',
'0x0101',
'0xffff',
'0x00ff00ff00ff00ff00ff00ff',
]
describe('getL1GasUsed', async () => {
for (const input of inputs) {
it(`case: ${input}`, async () => {
const overhead = await OVM_GasPriceOracle.overhead()
const cost = await OVM_GasPriceOracle.getL1GasUsed(input)
const expected = calculateL1GasUsed(input, overhead)
expect(cost).to.deep.equal(expected)
})
}
})
describe('getL1Fee', async () => {
for (const input of inputs) {
it(`case: ${input}`, async () => {
await OVM_GasPriceOracle.setGasPrice(1)
await OVM_GasPriceOracle.setL1BaseFee(1)
const decimals = await OVM_GasPriceOracle.decimals()
const overhead = await OVM_GasPriceOracle.overhead()
const scalar = await OVM_GasPriceOracle.scalar()
const l1BaseFee = await OVM_GasPriceOracle.l1BaseFee()
const l1Fee = await OVM_GasPriceOracle.getL1Fee(input)
const expected = calculateL1Fee(
input,
overhead,
l1BaseFee,
scalar,
decimals
)
expect(l1Fee).to.deep.equal(expected)
})
}
})
describe('setOverhead', () => {
it('should revert if called by someone other than the owner', async () => {
await expect(OVM_GasPriceOracle.connect(signer2).setOverhead(1234)).to.be
.reverted
})
it('should succeed if called by the owner', async () => {
await expect(OVM_GasPriceOracle.connect(signer1).setOverhead(0)).to.not.be
.reverted
})
it('should emit event', async () => {
await expect(OVM_GasPriceOracle.connect(signer1).setOverhead(100))
.to.emit(OVM_GasPriceOracle, 'OverheadUpdated')
.withArgs(100)
})
})
describe('get overhead', () => {
it('should return 2750 at first', async () => {
expect(await OVM_GasPriceOracle.overhead()).to.equal(2750)
})
it('should change when setOverhead is called', async () => {
const overhead = 6657
await OVM_GasPriceOracle.connect(signer1).setOverhead(overhead)
expect(await OVM_GasPriceOracle.overhead()).to.equal(overhead)
})
it('is the 3rd storage slot', async () => {
const overhead = 119090
const slot = 3
// set the price
await OVM_GasPriceOracle.connect(signer1).setOverhead(overhead)
// get the storage slot value
const priceAtSlot = await signer1.provider.getStorageAt(
OVM_GasPriceOracle.address,
slot
)
expect(await OVM_GasPriceOracle.overhead()).to.equal(
ethers.BigNumber.from(priceAtSlot)
)
})
})
describe('setScalar', () => {
it('should revert if called by someone other than the owner', async () => {
await expect(OVM_GasPriceOracle.connect(signer2).setScalar(1234)).to.be
.reverted
})
it('should succeed if called by the owner', async () => {
await expect(OVM_GasPriceOracle.connect(signer1).setScalar(0)).to.not.be
.reverted
})
it('should emit event', async () => {
await expect(OVM_GasPriceOracle.connect(signer1).setScalar(100))
.to.emit(OVM_GasPriceOracle, 'ScalarUpdated')
.withArgs(100)
})
})
describe('scalar', () => {
it('should return 1 at first', async () => {
expect(await OVM_GasPriceOracle.scalar()).to.equal(1500000)
})
it('should change when setScalar is called', async () => {
const scalar = 9999
await OVM_GasPriceOracle.connect(signer1).setScalar(scalar)
expect(await OVM_GasPriceOracle.scalar()).to.equal(scalar)
})
it('is the 4rd storage slot', async () => {
const overhead = 111111
const slot = 4
// set the price
await OVM_GasPriceOracle.connect(signer1).setScalar(overhead)
// get the storage slot value
const priceAtSlot = await signer1.provider.getStorageAt(
OVM_GasPriceOracle.address,
slot
)
expect(await OVM_GasPriceOracle.scalar()).to.equal(
ethers.BigNumber.from(priceAtSlot)
)
})
})
describe('decimals', () => {
it('is the 5th storage slot', async () => {
const decimals = 6
const slot = 5
// get the storage slot value
const priceAtSlot = await signer1.provider.getStorageAt(
OVM_GasPriceOracle.address,
slot
)
expect(await OVM_GasPriceOracle.decimals()).to.equal(
ethers.BigNumber.from(priceAtSlot)
)
})
})
})
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