Table of Contents
- Overview
- Constants
- Properties of a Gas Paying Token
- Configuring the Gas Paying Token
- Contract Modifications
- User Flow
- Upgrade
- Selection of
ETHER_TOKEN_ADDRESS
- Standard Config
- Fees
- Security Considerations
Custom gas token is also known as "custom L2 native asset". It allows for an L1-native ERC20 token to collateralize
and act as the gas token on L2. The native asset is used to buy gas which is used to pay for resources on the network
and is represented by EVM opcodes such as CALLVALUE
(msg.value
).
Both the L1 and L2 smart contract systems MUST be able to introspect on the address of the gas paying token to
ensure the security of the system. The source of truth for the configuration of the address of the token is
in the L1 SystemConfig
smart contract.
The terms "custom gas token", "gas paying token", "native asset" and "gas paying asset" can all be used interchangeably. Note, Ether is not a custom gas token, but may be used to pay gas. More on this in Configuring the Gas Paying Token.
Constant | Value | Description |
---|---|---|
ETHER_TOKEN_ADDRESS |
address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) |
Represents ether for gas paying asset |
DEPOSITOR_ACCOUNT |
address(0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001) |
Account with auth permissions on L1Block contract |
GAS_PAYING_TOKEN_SLOT |
bytes32(uint256(keccak256("opstack.gaspayingtoken")) - 1) |
Storage slot that contains the address and decimals of the gas paying token |
GAS_PAYING_TOKEN_NAME_SLOT |
bytes32(uint256(keccak256("opstack.gaspayingtokenname")) - 1) |
Storage slot that contains the ERC20 name() of the gas paying token |
GAS_PAYING_TOKEN_SYMBOL_SLOT |
bytes32(uint256(keccak256("opstack.gaspayingtokensymbol")) - 1) |
Storage slot that contains the ERC20 symbol() of the gas paying token |
The gas paying token MUST satisfy the standard ERC20 interface and implementation. It MUST be an L1 contract.
The gas paying token MUST NOT:
- Have a fee on transfer
- Have rebasing logic
- Have call hooks on transfer
- Have other than 18 decimals
- Have out of band methods for modifying balance or allowance
- Have a
name()
that is more than 32 bytes long - Have a
symbol()
this is more than 32 bytes long - Be a double entrypoint token
It MUST be enforced on chain that the token has exactly 18 decimals to guarantee no losses of precision when
depositing a token that has a different number of decimals. It MUST be enforced on chain that the
token's name
and symbol
are less than 32 bytes to guarantee they can each fit into a single storage
slot.
It MAY be possible to support ERC20 tokens with varying amounts of decimals in the future.
Ideally it is possible to have automated validation of custom gas paying tokens to know that the token satisfies the above properties. Due to the nature of the EVM, it may not always be possible to do so. ERC-165 isn't always used by ERC20 tokens and isn't guaranteed to tell the truth. USDT does not correctly implement the ERC20 spec. It may be possible to use execution traces to observe the properties of the ERC20 tokens but some degree of social consensus will be required for determining the validity of an ERC20 token.
The gas paying token is set within the L1 SystemConfig
smart contract. This allows for easy access to the
information required to know if an OP Stack chain is configured to use a custom gas paying token. The gas paying
token is set during initialization and cannot be modified by the SystemConfig
bytecode. Since the SystemConfig
is proxied, it is always possible to modify the storage slot that holds the gas paying token address directly
during an upgrade. It is assumed that the chain operator will not modify the gas paying token address unless they
specifically decide to do so and appropriately handle the consequences of the action.
The gas paying token address is network specific configuration, therefore it MUST be set in storage and not as an immutable. This ensures that the same contract bytecode can be used by multiple OP Stack chains.
If the address in the GAS_PAYING_TOKEN_SLOT
slot for SystemConfig
is address(0)
, the system is configured to
use ether
as the gas paying token, and the getter for the token returns ETHER_TOKEN_ADDRESS
. If the address in
the GAS_PAYING_TOKEN_SLOT
slot for SystemConfig
is not address(0)
, the system is configured to use a custom
gas paying token, and the getter returns the address in the slot.
flowchart LR
subgraph Layer One
OP[OptimismPortal]
subgraph SystemConfig
TA1[Token Address]
end
CO[Chain Operator] -->|"initialize()"| SystemConfig
SystemConfig -->|"setGasPayingToken()"| OP
end
subgraph Layer Two
subgraph L1Block
TA2[Token Address]
end
end
OP -->|"setGasPayingToken()"| L1Block
This interface encapsulates the shared interface.
interface IGasToken {
/// @notice Getter for the ERC20 token address that is used to pay for gas and its decimals.
function gasPayingToken() external view returns (address, uint8);
/// @notice Returns the gas token name.
function gasPayingTokenName() external view returns (string memory);
/// @notice Returns the gas token symbol.
function gasPayingTokenSymbol() external view returns (string memory);
/// @notice Returns true if the network uses a custom gas token.
function isCustomGasToken() external view returns (bool);
}
If a custom gas token is not used, then gasPayingToken()
should return (ETHER_TOKEN_ADDRESS, 18)
,
gasPayingTokenName
should return Ether
and gasPayingTokenSymbol
should return ETH
.
This interface applies to the following contracts:
L1Block
SystemConfig
The following contracts are aware internally if the chain is using a custom gas token but do not expose anything as part of their ABI that indicates awareness.
L1StandardBridge
L2StandardBridge
L1CrossDomainMessenger
L2CrossDomainMessenger
OptimismPortal
The OptimismPortal
is updated with a new interface specifically for depositing custom tokens.
The depositERC20Transaction
function is useful for sending custom gas tokens to L2. It is broken out into its
own interface to maintain backwards compatibility with chains that use ether
, to help simplify the implementation
and make it explicit for callers that are trying to deposit an ERC20 native asset.
function depositERC20Transaction(
address _to,
uint256 _mint,
uint256 _value,
uint64 _gasLimit,
bool _isCreation,
bytes memory _data
) public;
This function MUST revert when ether
is the L2's native asset. It MUST not be payable
, meaning that it will
revert when ether
is sent with a CALL
to it. It uses a transferFrom
flow, so users MUST first approve()
the OptimismPortal
before they can deposit tokens.
The following table describes the arguments to depositERC20Transaction
Name | Type | Description |
---|---|---|
_to |
address |
The target of the deposit transaction |
_mint |
uint256 |
The amount of token to deposit |
_value |
uint256 |
The value of the deposit transaction, used to transfer native asset that is already on L2 from L1 |
_gasLimit |
uint64 |
The gas limit of the deposit transaction |
_isCreation |
bool |
Signifies the _data should be used with CREATE |
_data |
bytes |
The calldata of the deposit transaction |
This function is the hot code path for all usage of the L1StandardBridge
and L1CrossDomainMessenger
, so it MUST
be as backwards compatible as possible. It MUST revert for chains using custom gas token when ether
is sent with
the CALL
. This prevents ether
from being stuck in the OptimismPortal
, since it cannot be used to mint native
asset on L2.
This function MUST only be callable by the SystemConfig
. When called, it creates a special deposit transaction
from the DEPOSITOR_ACCOUNT
that calls the L1Block.setGasPayingToken
function. The ERC20 name
and symbol
are passed as bytes32
to prevent the usage of dynamically sized string
arguments.
function setGasPayingToken(address _token, uint8 _decimals, bytes32 _name, bytes32 _symbol) external;
The StandardBridge
contracts on both L1 and L2 MUST be aware of the custom gas token. The ether
specific ABI on the
StandardBridge
is disabled when custom gas token is enabled.
The following methods MUST revert when custom gas token is being used:
bridgeETH(uint32,bytes)
bridgeETHTo(address,uint32,bytes)
receive()
finalizeBridgeETH(address, address, uint256, bytes)
The following legacy methods in L1StandardBridge
MUST revert when custom gas token is being used:
depositETH(uint32,bytes)
depositETHTo(address,uint32,bytes)
finalizeETHWithdrawal(address,address,uint256,bytes)
The following legacy methods in L2StandardBridge
MUST always revert when custom gas token is being used:
withdraw(address,uint256,uint32,bytes)
withdrawTo(address,address,uint256,uint32,bytes)
finalizeDeposit(address,address,address,address,uint256,bytes)
These methods are deprecated and subject to be removed in the future.
The CrossDomainMessenger
contracts on both L1 and L2 MUST NOT be able to facilitate the transfer of native asset
on custom gas token chains due to their tight coupling with ether
and the OptimismPortal
and L2ToL1MessagePasser
contracts.
It is possible to support this in the future with a redesign of the CrossDomainMessenger
contract.
It would need to have conditional logic based on being a custom gas token chain and facilitate transfer
of the ERC20 token to the end user on the other side. It adds a layer of extra state modifying calls
so it may not be worth adding.
The following methods MUST revert when CALLVALUE
is non zero:
sendMessage(address,bytes,uint32)
It should be impossible to call relayMessage
when CALLVALUE
is non zero, as it is enforced by sendMessage
.
The CrossDomainMessenger
also has the API for getting the custom gas token, namely gasPayingToken()
, which outputs
a tuple of the address and decimals of the custom gas token.
- The
L1CrossDomainMessenger
fetches this tuple from theSystemConfig
contract. - The
L2CrossDomainMessenger
fetches this tuple from theL1Block
contract.
The SystemConfig
is the source of truth for the address of the custom gas token. It does on chain validation,
stores information about the token and well as passes the information to L2.
The SystemConfig
is modified to allow the address of the custom gas paying token to be set during the call
to initialize
. Using a custom gas token is indicated by passing an address other than ETHER_TOKEN_ADDRESS
or address(0)
. If address(0)
is used for initialization, it MUST be mapped into ETHER_TOKEN_ADDRESS
before being forwarded to the rest of the system. When a custom gas token is set, the number of decimals
on the token MUST be exactly 18, the name of the token MUST be less than or equal to 32 bytes and the
symbol MUST be less than or equal to 32 bytes. If the token passes all of these checks,
OptimismPortal.setGasPayingToken
is called. The implementation of initialize
MUST not allow the chain
operator to change the address of the custom gas token if it is already set.
The L1Block
contract is upgraded with a permissioned function setGasPayingToken
that is used to set
information about the gas paying token. The DEPOSITOR_ACCOUNT
MUST be the only account that can call the
setter function. This setter is differentiated from the setL1BlockValues
functions because the gas paying
token is not meant to be dynamic config whereas the arguments to setL1BlockValues
are generally dynamic in nature.
Any L2 contract that wants to learn the address of the gas paying token can call the getter on the
L1Block
contract.
function setGasPayingToken(address _token, uint8 _decimals, byte32 _name, bytes32 _symbol) external;
The WETH9
predeploy is updated so that it calls out to the L1Block
contract to retrieve the name
and symbol
.
This allows for front ends to more easily trust the token contract. It should prepend Wrapped
to the name
and W
to the symbol
.
The user flow for custom gas token chains is slightly different than for chains that use ether
to pay for gas. The following tables highlight the methods that can be used for depositing and
withdrawing the native asset. Not every interface is included.
The StandardBridge
and CrossDomainMessenger
are symmetric in their APIs between L1 and L2
meaning that "sending to the other domain" indicates it can be used for both deposits and withdrawals.
Scenario | Method | Prerequisites |
---|---|---|
Native Asset Send to Other Domain | L1StandardBridge.bridgeETH(uint32,bytes) payable |
None |
Native Asset and/or Message Send to Other Domain | L1CrossDomainMessenger.sendMessage(address,bytes,uint32) payable |
None |
Native Asset Deposit | OptimismPortal.depositTransaction(address,uint256,uint64,bool,bytes) payable |
None |
ERC20 Send to Other Domain | L1StandardBridge.bridgeERC20(address,address,uint256,uint32,bytes) |
Approve L1StandardBridge for ERC20 |
Native Asset Withdrawal | L2ToL1MessagePasser.initiateWithdrawal(address,uint256,bytes) payable |
None |
There are multiple APIs for users to deposit or withdraw ether
. Depending on the usecase, different APIs should be
preferred. For a simple send of just ether
with no calldata, the OptimismPortal
or L2ToL1MessagePasser
should
be used directly. If sending with calldata and replayability on failed calls is desired, the CrossDomainMessenger
should be used. Using the StandardBridge
is the most expensive and has no real benefit for end users.
Scenario | Method | Prerequisites |
---|---|---|
Native Asset Deposit | OptimismPortal.depositERC20Transaction(address,uint256,uint256,uint64,bool,bytes) |
Approve OptimismPortal for ERC20 |
ERC20 Send to Other Domain | L1StandardBridge.bridgeERC20(address,address,uint256,uint32,bytes) |
Approve L1StandardBridge for ERC20 |
Native Asset Withdrawal | L2ToL1MessagePasser.initiateWithdrawal(address,uint256,bytes) payable |
None |
Users should deposit native asset by calling depositERC20Transaction
on the OptimismPortal
contract.
Users must first approve
the address of the OptimismPortal
so that the OptimismPortal
can use
transferFrom
to take ownership of the ERC20 asset.
Users should withdraw value by calling the L2ToL1MessagePasser
directly.
The following diagram shows the control flow for when a user attempts to send ether
through
the StandardBridge
, either on L1 or L2.
flowchart TD
A1(User) -->|Attempts to deposit/withdraw ether| A2(StandardBridge or CrossDomainMessenger)
A2 -->|Is chain using custom gas token?| A3{SystemConfig or L1Block}
A3 --> |Yes| A2
A2 --> |Revert| A1
The main difference is that the StandardBridge
and CrossDomainMessenger
cannot be used to send the native asset
to the other domain when an ERC20 token is the native asset. The StandardBridge
can still be used for bridging
arbitrary ERC20 tokens and the CrossDomainMessenger
can still be used for arbitrary message passing.
The custom gas token upgrade is not yet defined to be part of a particular network upgrade, but it will be scheduled as part of a future hardfork. On the network upgrade block, a set of deposit transaction based upgrade transactions are deterministically generated by the derivation pipeline in the following order:
- L1 Attributes Transaction calling
setL1BlockValuesEcotone
- User deposits from L1
- Network Upgrade Transactions
- L1Block deployment
- L2CrossDomainMessenger deployment
- L2StandardBridge deployment
- Update L1Block Proxy ERC-1967 Implementation Slot
- Update L2CrossDomainMessenger Proxy ERC-1967 Implementation Slot
- Update L2StandardBridge Proxy ERC-1967 Implementation Slot
The deployment transactions MUST have a from
value that has no code and has no known
private key. This is to guarantee it cannot be frontrun and have its nonce modified.
If this was possible, then an attacker would be able to modify the address that the
implementation is deployed to because it is based on CREATE
and not CREATE2
.
This would then cause the proxy implementation set transactions to set an incorrect
implementation address, resulting in a bricked contract. The calldata is not generated
dynamically to enable deterministic upgrade transactions across all networks.
The proxy upgrade transactions are from address(0)
because the Proxy
implementation
considers address(0)
to be an admin. Going straight to the Proxy
guarantees that
the upgrade will work because there is no guarantee that the Proxy
is owned by the
ProxyAdmin
and going through the ProxyAdmin
would require stealing the identity
of its owner, which may be different on every chain. That would require adding L2
RPC access to the derivation pipeline and make the upgrade transactions non deterministic.
from
:0x4210000000000000000000000000000000000002
to
:null
mint
:0
value
:0
gasLimit
: TODOdata
: TODOsourceHash
: TODO
from
:0x4210000000000000000000000000000000000003
to
:null
mint
:0
value
:0
gasLimit
:375,000
data
: TODOsourceHash
: TODO
from
:0x4210000000000000000000000000000000000004
to
:null
mint
:0
value
:0
gasLimit
:375,000
data
: TODOsourceHash
: TODO
from
:0x0000000000000000000000000000000000000000
to
:0x4200000000000000000000000000000000000015
mint
:0
value
:0
gasLimit
:50,000
data
: TODOsourceHash
: TODO
from
:0x0000000000000000000000000000000000000000
to
:0x4200000000000000000000000000000000000007
mint
:0
value
:0
gasLimit
:50,000
data
: TODOsourceHash
: TODO
from
:0x0000000000000000000000000000000000000000
to
:0x4200000000000000000000000000000000000010
mint
:0
value
:0
gasLimit
:50,000
data
: TODOsourceHash
: TODO
It was decided to use address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)
to represent ether
to push
ecosystem standardization efforts around using this address to represent ether
in DeFi protocols
or APIs.
There is currently no strong definition of what it means to be part of the standard config when using the OP Stack with custom gas token enabled. This will be defined in the future.
The OP Stack natively charges fees in terms of ether due to the fee formula taking into account the basefee and blobbasefee. When a custom gas token is used, fees are paid in the custom gas token but the conversion rate to ether is not taken into account as part of the protocol. It is assumed that the fees will be configured by the chain operator such that the revenue earned in custom gas token can be swapped into ether to pay for posting the data to L1.
The OptimismPortal
makes calls on behalf of users. It is therefore unsafe to be able to call the address of
the custom gas token itself from the OptimismPortal
because it would be a simple way to approve
an attacker's
balance and steal the entire ERC20 token balance of the OptimismPortal
.
Interop is supported between chains even if they use different custom gas tokens. The token address and the number of decimals are legible on chain. In the future we may add the ability to poke a chain such that it emits an event that includes the custom gas token address and its number of decimals to easily be able to introspect on the native asset of another chain.
The WETH9
predeploy at 0x4200000000000000000000000000000000000006
represents wrapped native asset and
not wrapped ether
. Portable and fungible ether
across different domains is left for a future project.