Skip to main content

Balances Precompile

Address: 0x0000000000000000000000000000000000000802

The Balances precompile exposes native pallet_balances transfers to EVM clients. Unlike a plain ETH transfer, it accepts a raw 32-byte AccountId32 as destination, which means an EVM wallet can send tokens to any Substrate account — including Sr25519 and Ed25519 accounts that do not have an EVM address.


Overview

PropertyValue
Address0x0000000000000000000000000000000000000802
Index2050 (0x802)
Sourceframe/evm/precompile/balances/
Cratepallet-evm-precompile-balances
StatusImplemented (MVP)

Why This Precompile Exists

On Orbinum, EVM accounts are embedded in AccountId32 as [H160 | 0x00 × 12]. A plain ETH transfer only reaches the 20-byte EVM namespace. Native Substrate accounts — for example, a Sr25519 keypair used in Polkadot.js or Talisman — occupy an arbitrary 32-byte AccountId32 that does not correspond to any EVM address. Without this precompile, there is no way to send tokens to such an account from an EVM transaction.

The Balances precompile bridges this gap by routing the transfer through pallet_balances::Mutate::transfer, which operates on the full AccountId32 space.

EVM tx (MetaMask)
└── call 0x0802 with transfer(dest_bytes32, amount)
└── pallet_balances: caller AccountId32 → dest AccountId32

Caller Identity

The caller's AccountId32 is derived from the EVM H160 sender via AddressMapping:

AccountId32 = H160 ++ [0x00; 12]

Authentication is provided by the EVM transaction signature — the precompile does not perform additional signature verification.


Function Reference

transfer

Transfers tokens from the caller to dest. The caller account may be reaped (destroyed) if its balance drops below the existential deposit.

function transfer(bytes32 dest, uint256 value) external;
ParameterTypeDescription
destbytes32Raw 32-byte AccountId32 of the recipient
valueuint256Amount to transfer in the chain's native denomination. Must fit in u128.

Selector: 0x6a467394

note

This corresponds to pallet_balances::transfer_allow_death. The sender account may be reaped if its balance reaches zero.


transferKeepAlive

Transfers tokens from the caller to dest, but guarantees that the caller account is not reaped. The call reverts if the remaining balance would fall below the existential deposit.

function transferKeepAlive(bytes32 dest, uint256 value) external;
ParameterTypeDescription
destbytes32Raw 32-byte AccountId32 of the recipient
valueuint256Amount to transfer. Must fit in u128.

Selector: 0xac6ac4c4

note

This corresponds to pallet_balances::transfer_keep_alive. Prefer this variant for faucets and smart contracts where the sender should remain funded.


ABI Encoding

Both functions take (bytes32, uint256). The encoding is straightforward:

BytesField
[0..3]4-byte selector (0x6a467394 or 0xac6ac4c4)
[4..35]bytes32 — raw AccountId32 (big-endian, zero-padded left if shorter)
[36..67]uint256 — amount in native denomination, big-endian, must fit in u128

Total calldata length: 68 bytes. Shorter inputs are rejected with an error.

Values exceeding u128::MAX (2^128 − 1) are rejected.


Gas

A flat base cost of 5 000 gas is charged per call, deducted before any argument decoding. This covers the storage reads and writes performed by pallet_balances and prevents free DoS attempts.


Usage Examples

Solidity

interface IBalances {
function transfer(bytes32 dest, uint256 value) external;
function transferKeepAlive(bytes32 dest, uint256 value) external;
}

IBalances balances = IBalances(0x0000000000000000000000000000000000000802);

// Send 10 ORB to a Sr25519 account (Alice's AccountId32)
bytes32 alice = 0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d;
balances.transferKeepAlive(alice, 10e18);

ethers.js / viem

import { ethers } from "ethers";

const ABI = [
"function transfer(bytes32 dest, uint256 value) external",
"function transferKeepAlive(bytes32 dest, uint256 value) external",
];

const balances = new ethers.Contract(
"0x0000000000000000000000000000000000000802",
ABI,
signer
);

// Alice's raw AccountId32 (Sr25519 dev account)
const alice = "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d";

await balances.transferKeepAlive(alice, ethers.parseEther("10"));

Raw calldata (manual)

Selector:  6a467394
Dest: d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d
Value: 0000000000000000000000000000000000000000000000008ac7230489e80000
└── 10 ORB = 10 × 10¹⁸ = 0x8AC7230489E80000

Encoding an SS58 Address

If you have a Substrate address in SS58 format (e.g. from Polkadot.js or Talisman), decode it to its raw 32-byte AccountId32 before passing it to the precompile.

import { decodeAddress } from "@polkadot/util-crypto";

// "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" → Uint8Array(32)
const accountId32 = decodeAddress("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY");
const dest = "0x" + Buffer.from(accountId32).toString("hex");
// → "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"

Compatible Wallets

The precompile broadens token reachability across the full account space. The table below shows which address format each wallet produces and how it maps to the bytes32 dest parameter.

Recipient wallets (destination address)

WalletAddress formatKey typeNotes
MetaMask0x… (EVM)secp256k1Encode as AccountId32 = [H160 | 0x00×12]
Rainbow0x… (EVM)secp256k1Same as MetaMask
Coinbase Wallet0x… (EVM)secp256k1Same as MetaMask
Rabby0x… (EVM)secp256k1Same as MetaMask
Polkadot.js ExtensionSS58 (5…)Sr25519 / Ed25519 / ECDSADecode SS58 → raw 32 bytes
TalismanSS58 (5…)Sr25519Decode SS58 → raw 32 bytes
SubWalletSS58 (5…)Sr25519Decode SS58 → raw 32 bytes
Nova WalletSS58 (5…)Sr25519Decode SS58 → raw 32 bytes
Ledger (Substrate app)SS58 (5…)Ed25519Decode SS58 → raw 32 bytes

EVM addresses can be passed directly to the precompile as bytes32 by left-padding the 20-byte H160 to 32 bytes ([H160 | 0x00 × 12]). SS58 addresses must first be decoded to their raw 32-byte AccountId32 — see Encoding an SS58 Address above.

Caller wallets (transaction sender)

The precompile is called via a standard EVM transaction, so the sender must always be an EVM wallet. The precompile itself handles delivery to any AccountId32 recipient.

WalletSupported as caller
MetaMask
Rainbow
Coinbase Wallet
Rabby
Any EVM-compatible wallet
Polkadot.js / Talisman (Substrate-only mode)❌ — must go through an EVM transaction

Limitations

  • The value parameter must fit in a u128. Values exceeding u128::MAX are rejected. In practice this is not a constraint — the maximum representable amount is approximately 3.4 × 10³⁸, far above any realistic balance.
  • The precompile does not check whether dest corresponds to a real account. Tokens sent to an unregistered AccountId32 are credited to that account and can be claimed later by whoever controls the key.
  • No cross-asset support — only the chain's native token is transferred.