Skip to main content
Version: v6

Polkadot SDK Precompiles Title Picture

Precompiles in Polkadot SDK

Precompiles act on-chain like regular contracts: they have an address and contracts can interact with them as if they were other contracts. But their code is not stored on-chain, instead they are implemented outside the sandboxed environment in which contracts are normally executed.

This makes precompiles a very efficient option to execute e.g. cryptographic functionality. But they need to be added by the chain operator. Since they run outside the sandbox, bugs or exploits here can have grievous consequences.

The execution engine for ink! contracts, pallet-revive, supports precompiles. Some precompiles are enabled by default, others are shipped with Polakdot SDK, but are optional.

An important distinction is that precompiles can be written in a way that they take either raw bytes as input, or in a way that they expose a complete Solidity interface, requiring also the Solidity ABI and encoding as the calling convention.

Primitive Precompiles

The pallet-revive ships with a number of precompiles that are enabled by default:

NameAddressCalled viaEnabled by default?Implemented in ink!?
EcRecover0x1Raw bytes
Sha2560x2Raw bytes
Ripemd1600x3Raw bytesNot yet
Identity0x4Raw bytesNot yet
Modexp0x5Raw bytesNot yet
Bn128Add0x6Raw bytesNot yet
Bn128Mul0x7Raw bytesNot yet
Bn128Pairing0x8Raw bytesNot yet
Blake2F0x9Raw bytesNot yet
PointEval0aRaw bytesNot yet
System0x900Solidity interface
Storage0x901Solidity interface

The Polkadot SDK contains a number of additional precompiles that can be enabled at will:

NameAddressCalled viaEnabled by default?Implemented in ink!?
AssetsPrecompile0x0120Solidity interfaceNot yet
XcmPrecompile0xA0000Raw bytes

Add your own precompiles

It's possible to extend the pallet-revive with custom precompiles. This is not relevant if you are only deploying your contracts to a chain that you don't control.

But if you are building a blockchain with Polkadot SDK and want to give users the ability to access specific functionality of your blockchain runtime in their smart contracts, then that's the way to go.

Through this, smart contract developers can utilize the business logic primitives of the chain to build a new application on top of it. Think for example of a decentralized exchange blockchain. This chain would in its simplest form have an order book to place bids and asks — there is no need for taking untrusted, Turing-complete, programs from the outside. The parachain could decide to expose the order book into smart contracts though, giving external developers the option of building new applications that utilize the order book. For example, to upload trading algorithms as smart contracts to the chain. Smart contracts here are an opportunity to up the user engagement and drive usage of the chain's native token. And the billing for utilizing the chain comes already built-in with the pallet — users have to pay gas fees for the execution of their smart contract.

For example, on the Polkadot testnet Westend the pallet-revive is configured to use these additional precompiles (here):

type Precompiles = (
ERC20<Self, InlineIdConfig<0x120>, TrustBackedAssetsInstance>,
ERC20<Self, InlineIdConfig<0x320>, PoolAssetsInstance>,
XcmPrecompile<Self>,
);

Develop your own Precompile

If you are looking to develop your own custom precompile, here are some starting points:

Our ink-node contains a simple demo precompile. You can find its source code here and its Solidity interface specification here.

#![cfg_attr(not(feature = "std"), no_std, no_main)]

/// This trait is an implementation of the Solidity interface found at
/// <https://github.com/use-ink/ink-node/blob/main/runtime/src/IDemo.sol>.
///
/// Note that it's also possible to just implement the interface partially.
/// This can be useful if you just want to expose part of the precompile
/// functionality
#[ink::contract_ref(abi = "sol")]
pub trait System {
/// Simple echo function.
///
/// If `mode = 0`, the function reverts.
/// If `mode > 0`, the input `message` is echoed back to the caller.
///
/// # Note
///
/// This signature is the ink! equivalent of the following Solidity signature
///
/// ```solidity
/// function echo(uint8 mode, bytes message) external view returns (bytes);
/// ```
#[ink(message)]
#[allow(non_snake_case)]
fn echo(&self, mode: u8, message: ink::sol::DynBytes) -> ink::sol::DynBytes;
}

#[ink::contract]
mod precompile_demo {
use super::System;
use ink::prelude::vec::Vec;

#[ink(storage)]
pub struct PrecompileDemo;

impl PrecompileDemo {
/// Initializes contract.
#[ink(constructor)]
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {}
}

/// Calls the `echo` function from `ink-node`'s `DemoPrecompile`.
#[ink(message)]
pub fn call_echo(&self, data: Vec<u8>) -> Vec<u8> {
const DEMO_PRECOMPILE_ADDR: [u8; 20] =
hex_literal::hex!("00000000000000000000000000000000000B0000");
let system_ref: super::SystemRef = ink::Address::from(DEMO_PRECOMPILE_ADDR).into();
let in_bytes = ink::sol::DynBytes(data);
let out_bytes = system_ref.echo(1, in_bytes);
out_bytes.0
}
}

#[cfg(all(test, feature = "e2e-tests"))]
mod e2e_tests {
use super::*;
use ink_e2e::ContractsBackend;

type E2EResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;

#[ink_e2e::test]
async fn call_echo_works(mut client: ink_e2e::Client<C, E>) -> E2EResult<()> {
// given
let mut constructor = PrecompileDemoRef::new();
let contract = client
.instantiate("precompile_demo", &ink_e2e::bob(), &mut constructor)
.submit()
.await
.expect("instantiate failed");
let call_builder = contract.call_builder::<PrecompileDemo>();

// when
let data = vec![0x1, 0x2, 0x3, 0x4];
let expected = data.clone();
let call_echo = call_builder.call_echo(data);
let res = client
.call(&ink_e2e::bob(), &call_echo)
.submit()
.await
.expect("call_echo failed");

// then
assert_eq!(res.return_value(), expected);

Ok(())
}
}
}

If you want to look further, the source code of the AssetPrecompile and the XcmPrecompile is also a good inspiration.