RIP-7755: Cross-L2-Call

Contract standard for cross-L2 calls facilitation


Metadata
Status: DraftStandards Track: CoreCreated: 2024-08-11
Authors
Wilson Cusack (@WilsonCusack), Jack Chuma (@jackchuma)

Abstract


Contracts for facilitating request, fulfillment, and fulfillment reward of cross-L2 calls.

Motivation


Cross-chain actions are an increasingly important part of crypto user experience. Today, most solutions for Ethereum layer 2s (L2s) have one or more of the following drawbacks.

  1. Reliance on privatized relayers with offchain access and incentives.
  2. Reliance on protocols outside of Ethereum and its rollups.
  3. High-level, intent-based systems that do not allow specifying exact calls to make.

Ethereum L2s, which all write state to a shared execution environment, are uniquely positioned to offer an alternative. Ethereum L2 users should have access to a public, decentralized utility for making cross L2 calls.

From any L2 chain, users should be able to request a call be made on any other L2 chain. Users should be able to guarantee a compensation for this call being made, and thus be able to control the likelihood this call will be made.

User should have full assurance that compensation will only be paid if the call was made. This assurance should depend ONLY on onchain information.

Specification


To only rely on onchain information, we use

  1. Layer 1 (L1), i.e. Ethereum Mainnet, blockhashes or beacon roots on the L2.
    • We take as an assumption that every Ethereum L2 should have a trusted L1 blockhash or state representation in the execution environment.
  2. Ethereum L2 blockhashes or state roots on L1.

Using these inputs, on any Ethereum L2, we can trustlessly verify ERC-1186 storage proofs of any other Ethereum L2.

Our contracts' job, then, is to represent call requests and fulfillment in storage on each chain.

Structs


Flow Diagrams

Happy Case

image

  1. User calls to an RIP7755Outbox contract with CrossChainRequest and reward funds
  2. RIP7755Outbox emits event for fulfillers to discover
  3. Fulfiller relays CrossChainRequest to RIP7755Inbox contract, including any funds possibly needed to successfully complete the call
  4. If included, RIP7755Inbox makes a precheck call to validate fulfillment condition(s)
  5. RIP7755Inbox makes the call as specified by CrossChainRequest
  6. RIP7755Inbox write to storage the FulfillmentInfo receipt of the call
  7. After CrossChainRequest.finalityDelaySeconds have elapsed, the fulfiller can submit the proof
  8. If the proof is valid and the call was successfully made, fulfiller is paid reward

RIP7755Outbox Contract

On the origin chain, there is an outbox contract to receive cross-chain call requests and payout rewards on proof of their fulfillment.


RIP7755Inbox Contract

On the destination chain, there is an inbox contract to store a receipt of the call fulfillment.


PrecheckContract Interface

On the destination chain, any valid precheck contract must adhere to the following interface.


Storage Proof Validation

The implementation details for successful storage proof validation will vary depending on the destination chain. However, all implementations will adhere to the following fundamental pattern:

  1. Verify that the beacon root used for the proof corresponds to the root exposed in the source chain's execution environment.
  2. Validate the L1 execution client's state root against the beacon root.
  3. Validate the destination chain's output contract storage root against the L1 execution client's state root.
  4. Validate the destination chain's state root against the destination chain's output contract storage root.
  5. Validate the destination chain's inbox contract storage root against the destination chain's state root.
  6. Validate the FulfillmentInfo struct at the correct storage key against the destination chain's inbox contract storage root.

It is important to note that not all L2 chains directly store their state root on L1. In certain cases, the L2 chain stores an abstract "output root" which must be connected to its state root in some manner. In these instances, the storage proof validation necessitates an intermediate step between steps 4 and 5. This step involves providing the destination chain's state root along with custom logic to derive the output root using that state root and any other required information. This step is only considered successful if the derived output root matches the value proven to be stored in the destination chain's output contract on L1.

Example Usage


These examples are not intended to be comprehensive of every detail. First example is more verbose, in hopes of giving helpful understanding for all examples.

Transfer native asset across chains.

User at Address A on Chain X wants to send 0.1 ether to Address B on Chain Y.

On Chain X, Address A calls requestCrossChainCall on a RIP7755Outbox contract of their choosing. CrossChainRequest.calls contains a single call.


The CrossChainRequest includes info about the destination chain and RIP7755Inbox contract the user wants the call to be made through.

The destination chain has a 7 day challenge period, and so the user sets CrossChainRequest.finalityDelaySeconds to a 7 day equivalent for maximum security.

When calling to requestCrossChainCall on origin chain, the user sends 0.1001 ether in value, which matches CrossChainRequest.rewardAmount. The excess above 0.1 ether is intended to exceed the gas cost of the call on destination chain and serve as a compensation to the fulfiller. This reward amount would need to provide sufficient incentive for the fulfiller to wait CrossChainRequest.finalityDelaySeconds, in this case 7 days, to get their reward.

Include custom exclusivity period for a specified fulfiller

To enhance the previous example, we introduce a precheck condition where Address A authorizes only a specific fulfiller, referred to as Fulfiller A, to submit the transaction to the destination chain for a specified period of time.

To implement this, custom exclusivity logic must be incorporated into a precheck smart contract (PCSM) deployed on the destination chain. The CrossChainRequest will include the PCSM address concatenated with the encoded data for exclusivity validation in the extraData field.


Once Address A invokes requestCrossChainCall on the origin chain, if an unintended fulfiller attempts to submit the transaction on the destination chain within the exclusivity period, the fulfill function call will revert due to the precheck failure. This ensures that only Fulfiller A can call fulfill before the expirationTimestamp.

Transfer ERC20 asset across chains.

User at Address A on Chain X wants to send 100 USDC to Address B on Chain Y.

ERC20 transfers have unique challenges in our paradigm, because

  1. The caller needs to specify the exact calls to make.
  2. The calls must be made through the inbox contract.

We show two example solutions below.

1. With known fulfiller address

The following example requires the caller to know ahead of time the fulfillers address. This is not ideal because (1) the calls will only work for one fulfiller (2) requires offchain pre-coordination.

  • Origin Chain:
    • Pre-steps:
      • Address A calls to USDC contract on origination chain to approve RIP7755Outbox to move 100 USDC.
    • Address A calls to outbox, with a two calls in CrossChainRequest.calls
      • 
        
      • 
        
  • Destination Chain
    • Pre-steps
      • Fulfiller calls to USDC contract on destination chain to approve RIP7755Inbox to move 100 USDC.
    • Fulfiller calls fulfill on RIP7755Inbox
      • In the first call of CrossChainRequest.calls, 100 USDC is sent from fulfiller to RIP7755Inbox.
      • In second call, 100 USDC is sent from RIP7755Inbox to Address B.

2. With helper contract

We could also solve the challenge by introducing a helper contract for facilitating the ERC20 transfer. This helper contract would accept calls in the format transfer(address asset, address to, uint256 amount) and then would use tx.origin to determine the from for the ERC20 transferFrom call (and check msg.sender is some known RIP7755Inbox contract). This would rely on fulfillers pre-depositing ERC20s in this helper contract, or having approvals set so it can pull funds at any time.

[!NOTE]
In future drafts, we may specify an implementation of HelperContract. It may also be convenient for fulfillers to be able to auth with signature. This could maybe be accomplished via some context that could be passed to fulfill and stored in RIP7755Inbox for the duration of the call.

  • Origin Chain:
    • Pre-steps:
      • Address A calls to USDC contract on origination chain to approve RIP7755Outbox to move 100 USDC.
    • Address A calls to outbox, with a one call in CrossChainRequest.calls
      • 
        
  • Destination Chain
    • Pre-steps
      • Fulfiller calls to USDC contract on destination chain to approve HelperContract to move 100 USDC.
    • Fulfiller calls fulfill on RIP7755Inbox
      • Call transfers 100 USDC to Address B via HelperContract.

ERC20 swap on Chain B using assets from Chain A.

TODO

Pay gas on Chain A for a smart account transaction on Chain B.

TODO

Example _validate implementation


OP Stack

The following library is an example of how storage proof validation can be implemented for an OP Stack chain.


Where the StateValidator structs are defined as follows:


This implementation example is designed with the assumption that the chain it is deployed on supports EIP-4788. It adheres to the general storage proof validation pattern previously outlined. The following lines highlight the specifics that are unique to the OP Stack:


This example leverages Optimism's AnchorStateRegistry contract to utilize the most recent anchor state available at the time of proof. The anchor state represents the finalized state of the destination L2 chain. To verify the state root of the destination chain, we must provide the pre-image of the chain's anchor state output root. As of this writing, the pre-image consists of four bytes32 values: version, l2StateRoot, l2MessagePasserStorageRoot, and l2BlockHash.

The l2StateRoot and l2BlockHash are extracted and derived from the RLP-encoded block headers array. Specifically, the l2StateRoot is the fourth element in this array, while the l2BlockHash is the hash of the entire RLP-encoded block headers array. With this information, we can compute the expected output root.

If the derived output root does not match the value stored in the L1 storage slot, the proof is deemed invalid.

Arbitrum

The following library is an example of how storage proof validation can be implemented for Arbitrum.


The process of verifying this proof is similar to the OP Stack example, but with specific nuances for Arbitrum. Below are the lines of code that are tailored for Arbitrum's unique architecture:


This step is crucial for verifying the authenticity of the destination L2 chain's state root. In Arbitrum, the confirmData field within an RBlock is utilized as the L1 storage value that we have proven. Similar to the OP Stack example, we must derive the expected storage value on L1 using the state root. Specifically, this is achieved by hashing the l2BlockHash together with the sendRoot found in the proof data structure. Given that the L2 state root is included in the pre-image of the block hash, a match between the derived value and the verified storage value confirms the authenticity of the state root.