Calling a Precompile
A precompile is a contract at a fixed address that runs runtime code instead of EVM bytecode. Orbinum's two precompiles reach the shielded pool and native balances from any EVM wallet or contract. This page calls the Balances precompile from Solidity and from ethers v6; the same rules apply to ShieldedPool.
Orbinum precompiles
| Address | Name | Functions | Reference |
|---|---|---|---|
0x0000000000000000000000000000000000000801 | ShieldedPool | shield, payable: deposits ORB into the shielded pool | ShieldedPool Precompile |
0x0000000000000000000000000000000000000802 | Balances | transfer, transferKeepAlive: pay ORB to any AccountId32 | Balances Precompile |
No package publishes their interfaces; declare them in your project. Every registered address, including the cryptographic ones, is in Precompile Addresses.
The interface
interface IBalances {
function transfer(bytes32 dest, uint256 value) external;
function transferKeepAlive(bytes32 dest, uint256 value) external;
}
| Parameter | Meaning |
|---|---|
dest | The recipient's 32-byte AccountId32: an sr25519 or ed25519 public key, or an EVM account's mapped form, the H160 followed by 12 zero bytes |
value | Amount in planck (10¹⁸ per ORB); must fit in 128 bits |
The precompile debits msg.sender: an externally owned account pays from its own balance, a
contract from the contract's balance. transfer lets the transfer reap the sender and
transferKeepAlive refuses to; with an existential deposit of 0 no account is ever reaped, so the
two behave the same and either can take the sender's balance to zero.
From Solidity
A contract that holds ORB and lets only its owner pay it out, to any account, including one with no EVM address:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IBalances {
function transfer(bytes32 dest, uint256 value) external;
function transferKeepAlive(bytes32 dest, uint256 value) external;
}
/// Holds ORB and lets its owner pay any AccountId32, including accounts with no EVM address.
contract Payouts {
address private constant BALANCES = 0x0000000000000000000000000000000000000802;
address public immutable owner;
error NotOwner();
error PayoutFailed();
event Paid(bytes32 indexed dest, uint256 amount);
constructor() {
owner = msg.sender;
}
/// Funds the contract: send ORB to its address.
receive() external payable {}
/// Pays `amount` of the contract's own ORB to `dest`.
function payout(bytes32 dest, uint256 amount) external {
if (msg.sender != owner) revert NotOwner();
// A low-level call: `ok` lets the contract handle a failed transfer, whose
// revert returns the unused gas.
(bool ok, ) = BALANCES.call(
abi.encodeCall(IBalances.transferKeepAlive, (dest, amount))
);
if (!ok) revert PayoutFailed();
emit Paid(dest, amount);
}
}
| Line | Why |
|---|---|
receive() | Funds the contract with a plain ORB transfer to its address; the precompile pays from that balance |
msg.sender != owner | Without it, anyone could call payout and empty the contract |
BALANCES.call(...) | Returns ok instead of reverting the whole call. A high-level IBalances(BALANCES).transferKeepAlive(...) also reaches the precompile, since the address holds stub code, and bubbles up its revert reason |
if (!ok) revert | A failed transfer returns false; reverting keeps the contract's state consistent |
emit Paid | The precompile emits no EVM log; the contract emits its own if indexers need one |
The transfer itself appears as a Substrate Balances.Transfer event, not as an EVM log.
From ethers v6
An externally owned account calls the precompile directly. addressToAccountIdHex from
@orbinum/protocol turns an SS58 address, an AccountId32 hex or an H160 into the bytes32
dest:
import { ethers } from 'ethers';
import { addressToAccountIdHex } from '@orbinum/protocol';
const provider = new ethers.JsonRpcProvider('https://rpc-1.testnet.orbinum.io');
const signer = new ethers.Wallet(process.env.ORBINUM_PRIVATE_KEY!, provider);
const balances = new ethers.Contract(
'0x0000000000000000000000000000000000000802',
[
'function transfer(bytes32 dest, uint256 value)',
'function transferKeepAlive(bytes32 dest, uint256 value)',
],
signer,
);
const dest = addressToAccountIdHex('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
if (!dest) throw new Error('not an SS58 address, AccountId32 or H160');
const tx = await balances.transferKeepAlive(dest, ethers.parseEther('1'));
const receipt = await tx.wait();
console.log('included in block', receipt?.blockNumber);
A top-level call has no extcodesize check, so ethers.Contract works as with any contract. An H160
passed to addressToAccountIdHex becomes its mapped AccountId32, the address followed by 12 zero
bytes, not left-padded. The signer pays gas in ORB; test ORB comes from the
faucet.
How failures look
Both precompiles fail the same way: the call reverts with an Error(string) reason, decodable like
a Solidity require message, and the unused gas is returned.
eth_call and eth_estimateGas report the reason, for example
execution reverted: transfer failed: Token(FundsUnavailable), so estimating before sending shows
why a call would fail. A contract calling the precompile receives ok == false and the encoded
reason as return data. A mined transaction that fails shows status: 0 in its receipt.
The revert strings are listed in Balances Precompile and ShieldedPool Precompile.
Call rules
| Rule | Balances | ShieldedPool |
|---|---|---|
STATICCALL | Refused | Refused |
DELEGATECALL and CALLCODE | Refused | Refused |
Value (msg.value) | Refused | Required: it is the shielded amount |
| Caller | msg.sender, mapped to its AccountId32 | The precompile's own mapped account dispatches shield |
Refusing delegated calls means a contract moves only its own balance through Balances, never the
balance of an account that called it. Proxies still work: an upgradeable proxy's implementation
makes a normal CALL to the precompile, with the proxy as msg.sender.
Related
- Balances Precompile — full ABI, gas and encoding
- ShieldedPool Precompile —
shieldand its origin model - Precompile Addresses — every registered address
- EVM Compatibility — unregistered precompiles and other differences
- EVM ↔ Substrate Mapping — how an H160 maps to an
AccountId32 - Network & Tooling — endpoints and tool configuration