Introducing Evm-coder - a library for seamless call translation between Rust and Solidity

Having frontier’s pallet-evm in chain alone is good; however, what’s the point of having a chain, if you don’t have your code on it, and what’s the point of having pallet-evm without the ability to communicate with the chain data itself? There is an EVM mechanism called precompiles to implement such communication.

Precompile is an EVM contract that is implemented in native code instead of bytecode; users can’t deploy such a contract, and precompile can communicate with some internal chain APIs.

However, writing precompiles is complicated because of the required solidity ABI knowledge and some of the EVM oddities. Alas, writing EVM precompiles requires large amounts of boilerplate for encoding/decoding argument and return types, as well as hardcoding selectors/selector hashes in your own precompile code.

What if there was something to write precompiles as easily as with plain solidity?

Introducing evm-coder!

Evm-coder aims to simplify the process of writing precompiles for EVM by providing a high-level abstraction for encoding/decoding arguments and return types. With this library, developers can focus on writing their precompile logic in Rust without worrying too much about the low-level details of Solidity’s ABI. Furthermore, this makes it easy for developers to create precompiles that are compatible with existing Ethereum standards, as well as to develop new contracts from scratch.

Given plain Rust code, with a couple of attributes placed throughout the code, you will get working EVM precompile, solidity API declarations (for contract developers), and contract stubs for some other purposes, i.e. ABI generation for end users.

Use-cases

Bringing some of your pallets to the EVM world. This is the most basic use case, and this is what evm-coder was designed for, and for which task it was already used for a long time.

Providing EVM compatibility for Ink! contracts. Evm-coder is not tied to Substrate and can be used in other environments. Ink! contracts can embed evm-coder inside and parse/execute incoming EVM calls.

Analysis/indexing tasks. Evm-coder is also not tied to execution, and you may use it for research or similar purposes!

Workflow

Let’s have a partial WETH-like contract implemented over pallet_balances. Some of the code is omitted for the shortness of this post; more complete examples you can see on the project’s GitHub page.

/// A struct, which will be passed as &self in precompile code.
/// Can be used to provide some runtime data; however, here, we have nothing to do with it.
struct Balances<T>(PhantomData<T>);
/// Generate implementations needed for #[solidity_interface] implementation to work.
frontier_contract! {
	macro_rules! Balances_result {}
	impl<T> Contract for Balances<T> {}
}

#[solidity_interface(
	// Name of a generated interface.
	// Visible in both generated solidity interfaces and used in generated WERC20Call enum.
	name = WERC20,
	// Optional expected ERC165 selector. If provided - compilation will fail on selector mismatch.
	// Can be used to make sure contract backward compatibility doesn't break and external
	// interface remains the same.
	expect_selector = 0x942e8b22,
)]
impl<T: pallet_balances::Config + pallet_evm::Config> Balances<T> {
	// The following doc comment will be added to the resulting solidity interface definitions:
	/// @dev Returns the name of the token.
	fn name() -> String {
		"Wrapped Token".to_owned()
	}
	fn decimals() -> u8 {
		0
	}
	fn balance_of(owner: Address) -> U256 {
		let owner = <T as pallet_evm::Config>::EvmAddressMapping::into_account_id(owner.0);
		<frame_system::Account<T>>::get(owner).data.free.into()
	}
	// Caller here is not an argument, but who has sent the transaction.
	fn transfer(caller: Caller, to: Address, amount: U256) -> Result<bool> {
		let source = <T as pallet_evm::Config>::EvmAddressMapping::into_account_id(caller.0);
		let dest = <T as pallet_evm::Config>::EvmAddressMapping::into_account_id(to.0);

		<pallet_balances::Pallet<T> as frame_support::traits::Currency>::transfer(
			source,
			dest,
			amount.try_into().ok_or("amount overflow")?,
			ExistenceRequirement::AllowDeath,
		)
		.ok_or("transfer failed");
		Ok(true)
	}
	...
}

// Generate a test, which will print the interface definition to stdout when run.
generate_stubgen!(gen_iface, Balances<()>, false);

With this code, we will get generated enum, similar to #[frame_support::pallet]'s generated code for #[pallet::call], which allows us to implement any necessary machinery around call introspection, i.e. CallFilter’s, as well as transform the call, and reencode it back.

enum WERC20Call {
    Name,
    Decimals,
    BalanceOf {
    	owner: Address,
    },
    Transfer {
    	to: Address,
        amount: U256,
    },
}

Some constants for further introspection of the encoded data.

impl WERC20Call {
	...
	// Solidity call signature, text representation
	const TRANSFER_SIGNATURE: SignatureUnit = "transfer(address,uint256)";
	// keccak256(TRANSFER_SIGNATURE)[0:4]
	const TRANSFER: Bytes4 = 0x1fa501;
	...
	// ERC165 contract selector
	fn interface_id() -> Bytes4 {
		Self::NAME ^ Self::DECIMALS ^ ...
	}
}

Call parser itself

impl Call for WERC20Call {
	fn parse(reader: AbiReader) -> Result<Self> {
		...
	}
}

Code, which calls originally defined methods declared in impl block. The parsed call can be altered between the parsing and execution phases.

impl Callable<WERC20Call> for Balances {
	fn call(&mut self, msg: Msg<WERC20Call>) -> ResultWithPostInfo {...]
}

And if we will run the gen_iface test, which we created using the generate_stubgen! macro:

interface ERC20 is ERC165 {
    /// @dev Returns the name of the token.
    function name() external view returns (string memory);
    ...
    function transfer(address to, uint256 amount) external returns (bool);
}

The precompile is ready to use! Only basic evm-coder features were used in this example; there was no inheritance, events, runtime data, code reuse patterns, #[derive(AbiType)] for passing complex structures between evm and the precompile, and many other things. For more complex examples, check out the project page.
Also, you can look at the unique-chain usage of this crate, as we have been using it in production for a couple of years now: unique-chain/erc.rs at 86290e7790af2d3f35121831128cbaa920939fd4 · UniqueNetwork/unique-chain · GitHub.

Planned features

  • #[solidity_callable] - reverse side of #[solidity_interface], allow to generate bindings to user contract, which can be callable from precompile, may be used for safeTransfer method family implementation.
  • Standalone ABI file generation - currently, you need to use a solidity compiler to do so from auto-generated stubs, although evm-coder knows everything required to generate ABI .json file itself.
  • Standard integrations. Currently, users of this library need to implement some integration stuff by themselves. At unique-network, we have pallet_evm_coder_substrate for this purpose, in which we implement custom #[derive(Weighted)] attribute and conversions between evm-coder and substrate+frontier errors unique-chain/pallets/evm-coder-substrate at master · UniqueNetwork/unique-chain · GitHub.
5 Likes

This is an interesting approach. We do it differently that we simply use solidity contracts to do solidity work such as emit events and only use custom precompile to access runtime.

Also you could generate the selector from function signature with macro instead of hardcode them.

1 Like

@xlc, this is still hardcode because this data can be derived from method arguments. And then, you would have to not only hardcode it in the selector but also in the interface definitions.

The ability to derive this data from contract definitions makes evm-coder a better fit for complex contracts with many methods/struct arguments/APIs with long maintenance that have a need to maintain deprecated (= not visible, but still callable) methods.

And for an example how the ability to generate interfaces looks in practice and production, with this: unique-chain/erc.rs at 86290e7790af2d3f35121831128cbaa920939fd4 · UniqueNetwork/unique-chain · GitHub
We get this generated for free: unique-chain/UniqueRefungible.sol at 86290e7790af2d3f35121831128cbaa920939fd4 · UniqueNetwork/unique-chain · GitHub