This pallet provides EVM compatibility for Substrate chains by bridging Ethereum-style transactions with Substrate FRAME calls.
The EVM Adapter pallet enables Substrate chains to process Ethereum transactions without requiring a full EVM execution environment. It works by:
- Receiving Ethereum Transactions: Accepts transactions formatted as Ethereum EIP-1559 transactions
- Signature Verification: Verifies ECDSA signatures and recovers the signer address
- Address Mapping: Maps EVM addresses (AccountId20) to Substrate accounts (AccountId32) using blake2 hashing
- Call Decoding: Interprets the transaction data and converts it to FRAME calls
- Dispatching: Executes the decoded call on the Substrate chain
The to address in an Ethereum transaction represents a pallet name:
- First 8 characters of the pallet name are encoded as bytes
- Padded with zeros to create a valid 20-byte address
- Example: "Balances" becomes
0x42616c616e636573000000000000000000000000
The data field contains:
- First 4 bytes: Function selector (keccak256 hash of function signature, first 4 bytes)
- Remaining bytes: ABI-encoded arguments
Transfer - transfer(address,uint256)
- Function selector:
0xa9059cbb - Arguments:
address: Destination address (32 bytes, last 20 bytes used)uint256: Amount to transfer (32 bytes)
- Maps to:
pallet_balances::Call::transfer_allow_death
// Using web3.js or ethers.js
const web3 = new Web3(rpcUrl);
// Balances pallet address
const balancesPalletAddress = "0x42616c616e636573000000000000000000000000";
// Encode transfer call
const transferData = web3.eth.abi.encodeFunctionCall({
name: 'transfer',
type: 'function',
inputs: [{
type: 'address',
name: 'to'
}, {
type: 'uint256',
name: 'value'
}]
}, [recipientAddress, '1000000000000000000']); // 1 token
// Send transaction
const tx = {
from: senderAddress,
to: balancesPalletAddress,
data: transferData,
gas: 21000,
};
await web3.eth.sendTransaction(tx);The pallet uses the same address mapping as the ETH RPC adapter:
EVM Address (20 bytes) -> Substrate Account (32 bytes):
1. Pad address to 32 bytes (add 12 zero bytes at the end)
2. Apply blake2_256 hash
3. Use hash as AccountId32
Substrate Account (32 bytes) -> EVM Address (20 bytes):
1. Take first 20 bytes of AccountId32
This ensures consistency between the adapter and the pallet.
The pallet verifies ECDSA signatures using secp256k1:
- Reconstructs the message hash from transaction fields
- Recovers the public key using
secp256k1_ecdsa_recover - Derives the Ethereum address from the public key
- Maps the address to a Substrate account for dispatch
To add support for additional pallets:
- Add the pallet name to the
decode_callfunction - Implement a decoder function (e.g.,
decode_your_pallet_call) - Map function selectors to FRAME calls
- Return the constructed call
Example:
fn decode_call(transaction: &EthereumTransaction) -> Result<T::RuntimeCall, Error<T>> {
let pallet_name = Self::pallet_name_from_address(transaction.to)
.ok_or(Error::<T>::UnsupportedPallet)?;
match pallet_name.as_slice() {
b"Balances" => Self::decode_balances_call(transaction),
b"YourPallet" => Self::decode_your_pallet_call(transaction),
_ => Err(Error::<T>::UnsupportedPallet),
}
}This pallet is designed to work with the subeth ETH RPC adapter. The adapter forwards eth_sendTransaction and eth_sendRawTransaction requests to this pallet via the transact extrinsic.
- Simplified RLP encoding: Uses SCALE encoding for transaction hashing instead of proper RLP
- Limited pallet support: Currently only supports Balances pallet
- No EVM bytecode execution: This is a translation layer, not a full EVM
- Signature format: Simplified message hash construction
- Add proper RLP encoding/decoding
- Support more pallets (Staking, Democracy, etc.)
- Add support for contract creation (integration with pallet-evm)
- Implement transaction pool for unsigned transactions
- Add gas metering and fee conversion
- Support EIP-2930 and legacy transaction types
MIT-0