Skip to main content

Precompiles

A precompile is a fixed contract address that, instead of running interpreted bytecode, triggers native runtime logic. From a contract's point of view it looks like an ordinary contract implementing an interface — you call it with a standard ABI-encoded call and get an ABI-encoded return — but the call is handled directly by Rust code in the runtime rather than by the PolkaVM or EVM interpreter.

This is how contracts on Polymesh reach native chain functionality: assets, compliance, settlement, and identity live in Substrate pallets, not in contract storage, and precompiles are the bridge.

Purpose​

On a general-purpose EVM chain, a token is just a contract someone deployed. On Polymesh, a fungible asset is a first-class chain object with an issuer, compliance rules, transfer restrictions, agents, and a settlement engine behind it. Precompiles let a contract work with those assets without the chain giving up any of that enforcement — a precompile call routes into the same pallet code an ordinary Substrate extrinsic would, with the same identity checks, the same compliance evaluation, and the same events.

The practical consequence: a Solidity contract can treat a Polymesh asset as an ERC-20 token, while the chain still refuses any transfer that would break a compliance rule.

Available precompiles​

PrecompileInterfaceAddress
Fungible AssetERC-20, ERC-3643 (partial), ERC-79430x<assetId>00080000
Non-Fungible AssetERC-721, ERC-721Metadata, ERC-165, ERC-79430x<assetId>00090000
Polymesh RuntimeIPolymeshRuntime0x00000000000000000000000000000000ffff0000

Precompiles are configured per runtime via the Precompiles associated type on pallet_revive::Config. All three are enabled on Mainnet, Testnet, and Develop.

The two asset precompiles serve every asset and collection automatically — there is no deployment, registration or issuer opt-in. The runtime precompile lives at one fixed address and covers the calls that are not scoped to a single asset: asset creation, ticker registration, DID onboarding and agent authorization.

Not yet available

There is no precompile for portfolios, settlement instructions, compliance rule management, claims, or ERC-1155 semantics. Contracts cannot call those pallets. This page is updated as precompiles are added — check the table above rather than assuming a capability exists.

This limit applies to contracts. An EVM wallet is not restricted to the precompiled surface: it can dispatch any extrinsic through runtime calls.

Address scheme​

The asset precompiles use pallet-revive's variable-prefix address matching, which lets a single precompile implementation serve many addresses by encoding data into the address itself. The runtime precompile uses fixed matching: it carries no data and answers at exactly one address.

A 20-byte precompile address is laid out as:

┌──────────────────────────┬────────────┬──────────┐
│ data (up to 16 bytes) │ matcher id │ reserved │
│ bytes 0..N │ 2 bytes │ 2 bytes │
└──────────────────────────┴────────────┴──────────┘

The trailing 4 bytes identify which precompile is being called (a big-endian u16 matcher id in bytes 16–17, then two zero bytes); the leading bytes carry whatever data that precompile needs. Every byte between the data and the matcher suffix must be zero.

Matcher idPrecompileData bytes
0x0008Fungible Asset16 (the asset's AssetId)
0x0009Non-Fungible Asset16 (the collection's AssetId)
0xffffPolymesh Runtime0 (one fixed address)

Because the data is embedded in the address, there is no registry to query and no deployment step — the address for a given asset or collection is computable offline, stable forever, and the same on every network where that asset exists.

Address collisions

Widening the data window trades collision resistance for address space: a keccak-derived address (an EOA, CREATE, or CREATE2 address) falls into a precompile's range with probability 2^-(8 × (20 - data_bytes)). At 16 data bytes that is 2^-32, and any such collision would have to also produce a valid AssetId to do anything. This is a property of the upstream pallet-revive matcher design, not something specific to Polymesh.

Shared call semantics​

These rules apply to every Polymesh precompile, so they are documented once here rather than repeated on each precompile's page.

Callers are resolved to Polymesh accounts​

A precompile receives a 20-byte EVM caller address. It resolves that to a 32-byte Polymesh AccountId32 through the runtime's AddressMapper (AccountId32Mapper) — the same mapping used everywhere else in pallet-revive. See Address mapping for how that works, and why calling map_account matters.

That resolved account is the origin the underlying pallet call runs as. It is subject to exactly the same requirements as any other Polymesh account: to act on assets it must be linked to an identity, hold the necessary permissions, and pass compliance.

A contract is an account, not a bystander

When a contract calls a precompile, the caller is the contract's own account — not the user who called the contract. A contract that holds or moves assets therefore needs its own identity linkage and permissions. Design for this explicitly; it is the most common source of surprise when porting an ERC-20-based design to Polymesh.

Failures are reverts, not silent falses​

When an underlying pallet call fails, the precompile reverts with a standard Solidity Error(string) carrying the pallet's own error message, prefixed with Extrinsic returned an error: . It does not return false.

This matters for contracts written defensively against non-compliant ERC-20 implementations: the "check the boolean return value" pattern is unnecessary here, because a returned true always means the operation actually happened. Wrap calls in try/catch if you need to handle failure without reverting your own transaction.

Delegate calls are rejected​

A contract cannot delegatecall into a precompile. Attempting it fails with PrecompileDelegateDenied. Precompiles act on behalf of their caller's account, and delegate-call semantics — running foreign code in your own storage and identity context — would break that model.

Read-only contexts reject state changes​

A contract that reaches a state-changing precompile method through staticcall — or any other read-only execution frame — fails with StateChangeDenied rather than appearing to succeed. This is what stops a precompile writing to Polymesh storage from inside a frame the EVM believes cannot write, since the EVM cannot roll back a pallet write it never saw.

eth_call and eth_estimateGas are not read-only frames. They are dry runs: the call executes normally against a throwaway overlay, and the result is discarded when the query returns. Simulating a write therefore reports success rather than reverting, exactly as it does on any Ethereum node, and nothing is committed. Treat a successful eth_call on a state-changing method as "this would have worked", not as evidence that it happened.

Two consequences worth designing around:

  • A dry run does not check a signature, so simulating a call from an address you do not control still reports what that address could do. Do not use eth_call as an authorization test.
  • A dry run does dispatch the underlying pallet call, so it surfaces genuine failures — failed compliance, missing permissions, insufficient balance — as a revert at estimation time.

Gas is charged from Polymesh weights​

Precompiles charge the worst-case weight of the underlying pallet call up front and refund down to actual consumption where the runtime can measure it. Gas estimates from Ethereum tooling reflect Polymesh's weight model (scaled by the runtime's GasScale), so a precompile call can be materially more expensive than an equivalent operation against a plain Solidity token — it is doing real compliance and settlement work.

The caller pays, and can be subsidised​

The runtime records a current fee payer for the call it is dispatching, which is what the Relayer pallet consults to decide whether a call is subsidised. A precompile sets that payer before dispatching the underlying pallet call and restores the previous value afterwards.

The payer is the precompile's immediate caller, resolved to its Polymesh account. When an account calls a precompile directly, that account pays. When a contract calls a precompile, the contract's own account pays — not the account that submitted the transaction. A contract that acts on behalf of users therefore needs its own subsidy, or its own POLYX, for the pallet calls it makes; it does not inherit the caller's.

This is about who pays for the pallet call, not about EVM gas: gas for the transaction itself is still paid by the account that submitted it.

Events are EVM logs​

Precompiles emit EVM logs, visible through eth_getLogs and standard Ethereum tooling. The underlying pallet also emits its normal Substrate events, so a single precompile call produces both: an EVM log for Ethereum-side tooling and the usual Asset/Settlement events for Substrate indexers.

Substrate-side indexers therefore need no precompile-specific handling — a transfer made through a precompile looks like any other transfer.

Interfaces and bytecode​

Each precompile's Solidity interface and the stub runtime bytecode are checked into the Polymesh repository:

PrecompileInterfaceRuntime bytecode
Fungible AssetFungibleAssetStub.solFungibleAssetStub.bin
Non-Fungible AssetNonFungibleAssetStub.solNonFungibleAssetStub.bin
Polymesh RuntimePolymeshRuntime.solPolymeshRuntime.bin

Each .sol file defines two things: the interface (IFungibleAsset, INonFungibleAsset, IPolymeshRuntime) and a stub contract of the same shape whose methods all revert with NotExecutable(). Import the interface to call a precompile from a contract; the stub exists only to produce the bytecode the precompile serves for explorer verification.

No ABI JSON is checked in. Generate one from the interface with solc --abi <file>, or forge inspect IFungibleAsset abi, then use it with viem, ethers or web3 against any address in the precompile's range.

Links point at the mainnet branch, which tracks the runtime deployed to Mainnet. A precompile added in a release that Mainnet has not yet taken will not be present there; the develop branch carries the newest interfaces.

Block explorer support​

Precompiles serve real bytecode for their address range: a checked-in stub contract whose ABI matches the precompile's interface. eth_getCode on a precompile address returns it, which lets explorers such as Blockscout verify the contract and render a proper method UI, split correctly into Read and Write tabs based on each method's declared state mutability.

The stub is only ever a carrier for code and an ABI — every method in it reverts with NotExecutable() if it were somehow executed directly. The real behaviour lives in the runtime.

Verifying one address per precompile per network is enough. Because every address in a precompile's range serves identical bytecode, Blockscout's twin-matching applies the verified ABI to the rest automatically.

The Polymesh repository ships helper scripts for this workflow under scripts/ (build_precompile_stub.sh, blockscout_find_precompile_addresses.sh, blockscout_check_precompile_code.sh, blockscout_verify_precompile.sh, blockscout_precompile_status.sh), with usage notes in precompiles/README.md.