Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Research Area: ERC20 Token as the Gas Paying Token #67

Open
tynes opened this issue Feb 26, 2024 · 3 comments
Open

Research Area: ERC20 Token as the Gas Paying Token #67

tynes opened this issue Feb 26, 2024 · 3 comments

Comments

@tynes
Copy link
Contributor

tynes commented Feb 26, 2024

We would like to add support for the ability to use an L1 based ERC20 token as the native gas token on L2. This feature needs to specified before consideration for merge into the OP Stack.

Some considerations:

  • Minimal diff to the existing codebase
  • Branching logic in the deploy script or branching logic in the smart contract?
    • Is it better to deploy different L1 contracts for chains that use an ERC20 token as the L2 gas token or is it better to have the logic baked into the same contract and just use a deploy time flag to determine which code path is used?
  • Need to be mindful of how the design interacts with in protocol interoperability
  • The WETH predeploy cannot be backed by the custom gas token, it should always be WETH. This means that deposits of ether need to mint WETH at the predeploy
@tynes
Copy link
Contributor Author

tynes commented Mar 12, 2024

An initial + very minimal diff to the OptimismPortal to add custom gas token:

diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal.sol b/packages/contracts-bedrock/src/L1/OptimismPortal.sol
index 30e6bd471..b6f511756 100644
--- a/packages/contracts-bedrock/src/L1/OptimismPortal.sol
+++ b/packages/contracts-bedrock/src/L1/OptimismPortal.sol
@@ -14,6 +14,7 @@ import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol";
 import { ResourceMetering } from "src/L1/ResourceMetering.sol";
 import { ISemver } from "src/universal/ISemver.sol";
 import { Constants } from "src/libraries/Constants.sol";
+import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol";
 
 /// @custom:proxied
 /// @title OptimismPortal
@@ -64,6 +65,13 @@ contract OptimismPortal is Initializable, ResourceMetering, ISemver {
     /// @custom:network-specific
     SystemConfig public systemConfig;
 
+    /// @notice Address of the gas paying token.
+    /// @custom:network-specific
+    address public gasPayingToken;
+
+    /// @notice The total amount of gas paying asset in the contract.
+    uint256 internal _balance;
+
     /// @notice Emitted when a transaction is deposited from L1 to L2.
     ///         The parameters of this event are read by the rollup node and used to derive deposit
     ///         transactions on L2.
@@ -100,6 +108,7 @@ contract OptimismPortal is Initializable, ResourceMetering, ISemver {
             _l2Oracle: L2OutputOracle(address(0)),
             _systemConfig: SystemConfig(address(0)),
             _superchainConfig: SuperchainConfig(address(0))
+            _gasPayingToken: address(0),
         });
     }
 
@@ -107,10 +116,13 @@ contract OptimismPortal is Initializable, ResourceMetering, ISemver {
     /// @param _l2Oracle Contract of the L2OutputOracle.
     /// @param _systemConfig Contract of the SystemConfig.
     /// @param _superchainConfig Contract of the SuperchainConfig.
+    /// @param _gasPayingToken ERC20 token used to pay gas on L2.
+    ///        Set to `address(0)` for ether.
     function initialize(
         L2OutputOracle _l2Oracle,
         SystemConfig _systemConfig,
-        SuperchainConfig _superchainConfig
+        SuperchainConfig _superchainConfig,
+        address _gasPayingToken
     )
         public
         initializer
@@ -118,6 +130,7 @@ contract OptimismPortal is Initializable, ResourceMetering, ISemver {
         l2Oracle = _l2Oracle;
         systemConfig = _systemConfig;
         superchainConfig = _superchainConfig;
+        gasPayingToken = _gasPayingToken;
         if (l2Sender == address(0)) {
             l2Sender = Constants.DEFAULT_L2_SENDER;
         }
@@ -173,6 +186,15 @@ contract OptimismPortal is Initializable, ResourceMetering, ISemver {
         return _byteCount * 16 + 21000;
     }
 
+    /// @notice Retuns the balance of the contract in the smallest units.
+    function balance() public pure returns (uint256) {
+        if (gasPayingToken == address(0)) {
+            return address(this).balance;
+        } else {
+            return _balance;
+        }
+    }
+
     /// @notice Accepts value so that users can send ETH directly to this contract and have the
     ///         funds be deposited to their address on L2. This is intended as a convenience
     ///         function for EOAs. Contracts should call the depositTransaction() function directly
@@ -344,7 +366,20 @@ contract OptimismPortal is Initializable, ResourceMetering, ISemver {
         //   2. The amount of gas provided to the execution context of the target is at least the
         //      gas limit specified by the user. If there is not enough gas in the current context
         //      to accomplish this, `callWithMinGas` will revert.
-        bool success = SafeCall.callWithMinGas(_tx.target, _tx.gasLimit, _tx.value, _tx.data);
+        bool success = true;
+        // If either is the gas paying asset or if the custom gas token chain withdrawal is a call (no value)
+        if (gasPayingToken == address(0) || _tx.value == 0) {
+            success = SafeCall.callWithMinGas(_tx.target, _tx.gasLimit, _tx.value, _tx.data);
+        } else {
+            _balance -= _tx.value;
+
+            // this reverts on failure
+            SafeTransferLib.safeTransfer({
+                token: gasPayingToken
+                to: _tx.target,
+                value: _tx.value
+            });
+        }
 
         // Reset the l2Sender back to the default value.
         l2Sender = Constants.DEFAULT_L2_SENDER;
@@ -361,6 +396,39 @@ contract OptimismPortal is Initializable, ResourceMetering, ISemver {
         }
     }
 
+    // This is the entrypoint to depositing an ERC20 token as a custom gas token.
+    function depositERC20Transaction(
+        address _to
+        uint256 _mint,
+        uint256 _value,
+        uint64 _gasLimit,
+        bool _isCreation,
+        bytes memory _data
+    ) public metered(_gasLimit) {
+        // Can only be called if an ERC20 token is used for gas paying on L2
+        require(gasPayingToken != address(0), "OptimismPortal: only custom gas token");
+
+        // Pulls ownership of custom gas token to portal
+        SafeTransferLib.safeTransferFrom2({
+            token: gasPayingToken,
+            from: msg.sender,
+            to: address(this),
+            amount: _mint,
+        });
+
+        // Overflow protection here ensures safety on L2 from overflows in balance
+        _balance += _mint;
+
+        _depositTransaction({
+            _to: _to,
+            _mint: _mint,
+            _value: _value,
+            _gasLimit: _gasLimit,
+            _isCreation: _isCreation,
+            _data: _data
+        });
+    }
+
     /// @notice Accepts deposits of ETH and data, and emits a TransactionDeposited event for use in
     ///         deriving deposit transactions. Note that if a deposit is made by a contract, its
     ///         address will be aliased when retrieved using `tx.origin` or `msg.sender`. Consider
@@ -381,6 +449,28 @@ contract OptimismPortal is Initializable, ResourceMetering, ISemver {
         payable
         metered(_gasLimit)
     {
+        if (gasPayingToken != address(0)) {
+            require(msg.value == 0, "OptimismPortal: cannot send ETH with custom gas token");
+        }
+
+        _depositTransaction({
+            _to: _to,
+            _mint: msg.value,
+            _value: _value,
+            _gasLimit: _gasLimit,
+            _isCreation: _isCreation,
+            _data: _data
+        });
+    }
+
+    function _depositTransaction(
+        address _to,
+        uint256 _mint,
+        uint256 _value,
+        uint64 _gasLimit,
+        bool _isCreation,
+        bytes memory _data
+    ) internal {
         // Just to be safe, make sure that people specify address(0) as the target when doing
         // contract creations.
         if (_isCreation) {
@@ -406,7 +496,7 @@ contract OptimismPortal is Initializable, ResourceMetering, ISemver {
         // Compute the opaque data that will be emitted as part of the TransactionDeposited event.
         // We use opaque data so that we can update the TransactionDeposited event in the future
         // without breaking the current interface.
-        bytes memory opaqueData = abi.encodePacked(msg.value, _value, _gasLimit, _isCreation, _data);
+        bytes memory opaqueData = abi.encodePacked(_mint, _value, _gasLimit, _isCreation, _data);
 
         // Emit a TransactionDeposited event so that the rollup node can derive a deposit
         // transaction for this deposit.

@kahuang
Copy link

kahuang commented Mar 26, 2024

+1, need a standard here that is forwards compatible with interop/fault proofs etc

@drinkius
Copy link

We've previously deployed an Optimism-based rollup (pre-Bedrock) with ERC-20 as gas token, here is the implementation that we've come up with to keep the diff to a minimum

It seems that there might be more changes if we try to keep WETH predeploy address to always be same as L1 native token, in our implementation we avoided that and updates mostly were confined to contracts. Also, we should keep in mind that ERC-20 gas token rollups might be used on top of other L2s so not sure adhering to WETH naming might be good, it's better to have WNATIVE or something similar

I'm still not sure how to combine two implementations in one repo painlessly without disrupting existing rollup implementation and reusing existing tests, since really the logics might stay the same but slight differences in contracts' naming would require duplicating the calling logics

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants