WrappedEVM Eth RPC compatibility layer

Here is a scheme for categorizing Ethereum compatibility for some given blockchain. The key is to assess the layers of Program Equivalence:

  • Level 0: Full compatibility (Essentially a chain code fork)
  • Level 1: Bytecode level compatibility
  • Level 2: Code compile-level language compatibility (Eg. Solang)
  • Level 3: RPC/endpoint extrinsic compatibility (Eg. Ability to sign using Metamask / Ability to read data using the same block explorers)

While Frontier’s pallet-evm and pallet-ethereum already provide Level 0 compatibility to Substrate, there’s instances where we might prefer native Substrate execution over Frontier’s SputnikVM Runner, as a workaround to its inherent performance penalties. So we want to be able to navigate this scheme as a spectrum of compatibility with legacy EVM standards.

The easiest place to start is at Level 3. From there, climbing up the compatibility levels implies that things can go in a couple different directions, namely:

  1. EVMless Execution: Re-routing Eth RPCs for native Subtrate execution under multiple FRAME pallets.
  2. Wasm Contract Execution: Re-routing Eth RPCs for execution under pallet-contracts.

I recently learned about a community effort that is apparently being called WrappedEVM, including teams like Interlay, Centrifuge and HydraDX. So I’m starting this thread as a way to coordinate those efforts.

EVMless Execution (pallet-evmless)

The dispatch precompile is made available by pallet-evm. It an obvious initial step in this direction, as it opens up the possibility of triggering FRAME execution via Eth RPCs. It is already available and a really low-hanging-fruit for teams aiming to go down this route.

It is still a bit cumbersome, however, because the RPC side is still partially “contaminated” with Substrate, as it requires the actual FRAME calls to be SCALE encoded.

For some cases, we might want the Eth RPCs to believe they’re interacting entirely with an EVM ABI, while staying completely agnostic in regards to the Substrate realm. This starts from the assumption that there are already pallets deployed into the Runtime that are able to cover for all the expected functionality that the ABIs expose.

This would require some mechanism for the Runtime to receive the EVM dispatchables and re-route them for native Substrate execution. This can be achieved with a stripped down version of pallet-evm (let’s call it pallet-evmless), that provides a simplified EVM shell Runner. Then, for each Solidity use-case, different precompiles would be written ad-hoc, either tighly or loosely coupling with the pallets responsible for execution. Unique’s evm-coder crate can be leveraged here so that developers can focus on writing their precompile logic in Rust without worrying too much about the low-level ABI details.

Ideally, we should standardize pallet-evmless as a new upstream feature on Frontier.

Note: I’m using the term EVMless to describe this specific path of action, while WrappedEVM is being used as the umbrella term for the overall initiative (which also includes the pallet-contracts path described below).

Wasm Contract Execution (pallet-contracts)

While still a WIP, there’s a few tools in our community that have great potential to onboard Solidity devs into Substrate:

evm-coder

Maintained by Unique Network, evm-coder is a Rust library for seamless call translation between Rust and Solidity code. Not only it can be quite helpful writing precompiles based on Solidity contracts (on the EVMless context described above), but it can also be used while writing ink! contracts as well.

Sol2Ink

Sol2ink is able to parse compilable Solidity interfaces into ink! traits and compilable Solidity contracts into ink! contracts, while leveraging the power of OpenBrush. The effort is led by SuperColony, but the repository seems stale for the past few months at the time of writing.

Solang

Maintained by the HyperLedger Foundation, Solang is a Solidity compiler for Solana (which we don’t really care about here) and Substrate. It is able to compile Solidity contracts for execution under pallet-contracts. The project is under active development, with frequent contributions from Parity’s Core Engineering.

pallet-contracts

Be it some Solidity contract:

  • transpiled to ink! and then compiled into Wasm via Sol2ink or evm-coder
  • compiled straight into Wasm via Solang

While deploying them into pallet-contracts, we might want to still interact with them via Eth RPCs. This would imply new features being added into pallet-contracts. While theoretically feasible, there might be a few challenges to be tackled along the way:

  • ABI compatibility: currently, ink! and Solang are not following the EVM ABI calling conventions. Making this happen (if even possible) would imply changing the ink! and Solang calling conventions, which opens a pandora box of breaking changes between different versions and can be seen as tabu. There’s also the problem that the Wasm contracts use types as arguments and return values that don’t exist in the EVM ABI. In this case we would probably just specify them as as bytes and live with the fact that a Dapp needs custom code to encode/decode them. Bottom line is: this topic needs more research.

Edit: as added by @lach in comment below , evm-coder can potentially help solve some of these issues of ABI compatibility for ink!.

  • EVM tooling compatibility: Tooling might inspect the code. It might be as little as checking for some magic byte and as sophisticated as generating constructor code. It doesn’t matter if it is just a trivial check. That tool will not work as we will be feeding it wasm code. We would need to try to upstream compatibility fixes for the tooling projects.
  • Contract creation: A contract running on pallet-contracts contains a dedicated “construction” entry point. When creating a new contract, input data to is passed to this constructor as data in the transaction. This is different from EVM, where the code passed as data is the constructor itself. There are certainly some ways so work around this but it will require Solidity developers to do things slightly different from what they are used to.

So there will likely be no perfect on-size-fits-all solution, and we will have to deal with fragmentation at some point. Which is arguably unavoidable, as we can already see in some Eth L2s breaking EVM bytecode and RPC compatibility (e.g.: zkSync and ERC1167).

Nevertheless, this is still a path worth pursuing, as onboarding Ethereum dApps and devs will be really important for the vision of Hybrid Chains in our Ecosystem.

5 Likes

ABI compatibility: currently, ink! and Solang are not following the EVM ABI calling conventions. Making this happen (if even possible) would imply changing the ink! and Solang calling conventions, which opens a pandora box of breaking changes between different versions and can be seen as tabu.

Regarding mentioned evm-coder - it is not dependent on substrate code, so it can be used inside of ink! contracts as well. I.e (example code, implemented using not-yet-existing evm_coder_ink adapter):

struct Contract;

/// Ink calls
impl Contract {
    #[ink(message)]
    pub fn hello_ink(&self) -> String {
        "Hello from ink! side of a contract!".into()
    }

    // Forward ethereum calls to evm_coder
    #[ink(message)]
    pub fn evm_call(&mut self, calldata: Vec<u8>) -> evm_coder_ink::Result {
        let call = MyContractCall::parse(&calldata)?;
        let value = self.env().transferred_balance();
        let caller = self.env().caller();
        self.call(evm_coder::Msg {
            call,
            value,
            caller,
        }).into()
    }
}

/// Evm calls
#[evm_coder::solidity_interface(name = MyContract)]
impl Contract {
    pub fn hello_evm(&self) -> String {
        "Hello from evm side of a contract!".into()
    }
}

2 Likes

that’s awesome, thanks for this info @lach
I edited the OP to make sure I’m covering these aspects of evm-coder.