How to Map a Substrate Account to an EVM Account on 3DPass

3DPass uses a bidirectional conversion system where the runtime's HashedAddressMapping converts EVM addresses to Substrate accounts via BlakeTwo256 hashing, while precompiles use a truncation helper to derive EVM addresses from the first 20 bytes of Substrate AccountId values.

To interact with the Ethereum Virtual Machine (EVM) on 3DPass, developers must understand how the Substrate-based chain translates between native 32-byte AccountId addresses and 20-byte EVM H160 addresses. This guide explains the specific Substrate to EVM account mapping on 3DPass mechanisms implemented in the 3dpass/3dp repository, including the runtime configuration and precompile utilities that handle these conversions.

How EVM to Substrate Conversion Works

The EVM pallet in 3DPass relies on the AddressMapping trait to translate Ethereum addresses into Substrate account identifiers. The runtime defines this mapping in runtime/src/lib.rs at line 41:

type AddressMapping = HashedAddressMapping<BlakeTwo256>;

This configuration directs the EVM pallet to use BlakeTwo256 hashing for address conversion. The implementation, located in pallets/evm/src/lib.rs (lines 12-21), operates through the following process:

  • The system prepends the bytes b"evm:" to the 20-byte EVM H160 address.
  • It hashes the resulting byte string using BlakeTwo256.
  • The 256-bit hash output becomes the 32-byte Substrate AccountId32.

The trait provides the into_account_id method, which the EVM runtime invokes to resolve Ethereum senders or contract addresses to Substrate accounts. Importantly, this trait defines only the EVM → Substrate direction; the reverse conversion requires a separate approach implemented in the precompiles.

How Substrate to EVM Conversion Works

For the reverse direction—converting a Substrate AccountId to an EVM address—3DPass precompiles use a consistent truncation strategy rather than a cryptographic hash.

The Truncation Helper in Precompiles

Every precompile exposing Substrate addresses to the EVM includes an account_id_to_address helper function. This utility extracts the first 20 bytes from the 32-byte AccountId to form the H160 address.

In the SerialNumbers precompile (precompiles/serial-numbers/src/lib.rs, lines 36-43), the implementation appears as:

fn account_id_to_address<Runtime: frame_system::Config>(account_id: &Runtime::AccountId) -> Address
where
    Runtime::AccountId: AsRef<[u8; 32]>,
{
    let bytes = account_id.as_ref();
    Address(H160::from_slice(&bytes[0..20]))
}

This identical pattern appears across other precompiles:

The truncation method assumes that the first 20 bytes of the hashed AccountId provide sufficient uniqueness for EVM interactions, creating a deterministic relationship from the Substrate account space to Ethereum addresses.

Implementing Substrate to EVM Mapping in Your Pallet

When building custom pallets that need to expose Substrate accounts to the EVM, replicate the truncation logic used by the precompiles:

use sp_core::H160;

pub fn substrate_to_evm<Runtime>(account_id: &Runtime::AccountId) -> H160
where
    Runtime: frame_system::Config,
    Runtime::AccountId: AsRef<[u8; 32]>,
{
    let bytes = account_id.as_ref();
    H160::from_slice(&bytes[0..20])
}

This function works with the default HashedAddressMapping configuration because the conversion leverages the raw bytes of the AccountId without requiring knowledge of the specific hashing algorithm used during the reverse conversion.

Custom Address Mapping Configuration

If your application requires a different conversion strategy—such as direct 1-to-1 mapping without hashing—you can implement a custom type that adheres to the AddressMapping trait.

Replace the default configuration in runtime/src/lib.rs:

type AddressMapping = MyDirectMapping;

Your custom type must implement:

pub trait AddressMapping<A> {
    fn into_account_id(address: H160) -> A;
}

Additionally, provide a complementary account_id_to_address function to handle the Substrate → EVM direction consistently across your runtime.

Summary

  • 3DPass uses HashedAddressMapping<BlakeTwo256> (defined in runtime/src/lib.rs) to convert EVM addresses to Substrate accounts by hashing b"evm:" + H160.
  • The into_account_id method handles the EVM → Substrate direction exclusively as part of the AddressMapping trait.
  • Precompiles use the account_id_to_address helper (found in SerialNumbers, Poscan, and Identity precompiles) to convert Substrate accounts to EVM addresses by truncating to the first 20 bytes.
  • Custom mapping implementations require replacing the AddressMapping type in the runtime and implementing both conversion directions to maintain consistency.

Frequently Asked Questions

How does the default EVM address mapping work in 3DPass?

The default implementation uses BlakeTwo256 hashing to map EVM addresses to Substrate accounts, as configured in runtime/src/lib.rs. The EVM pallet hashes the concatenation of the prefix b"evm:" and the 20-byte Ethereum address to generate a 32-byte AccountId. This provides a collision-resistant, deterministic mapping from the EVM space into Substrate's native account system.

Can I convert a Substrate account to an EVM address without using precompiles?

Yes. You can replicate the logic found in the precompiles' account_id_to_address helper function, which truncates the first 20 bytes of the 32-byte AccountId. Since this conversion is not part of the AddressMapping trait, you must implement this truncation manually in your pallet or runtime module to derive the corresponding H160 address.

Is it possible to change the address mapping strategy in 3DPass?

Absolutely. You can replace HashedAddressMapping<BlakeTwo256> with your own type that implements the pallet_evm::AddressMapping trait. This custom type must provide the into_account_id method for EVM-to-Substrate conversion, and you should implement a corresponding reverse conversion function to ensure consistent bidirectional mapping throughout your runtime.

Where can I find examples of address conversion in the 3DPass codebase?

The primary implementation resides in pallets/evm/src/lib.rs (lines 12-21) for EVM-to-Substrate conversion. For Substrate-to-EVM examples, examine the precompile directories: precompiles/serial-numbers/src/lib.rs (lines 36-43), precompiles/poscan/src/lib.rs (lines 44-51), and precompiles/identity/src/lib.rs (lines 116-124), all of which contain the account_id_to_address helper function.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →