Building a Versioned and Developer-Friendly Smart Contract API on a Polkadot SDK Chain
*Making Rust Smart Contract Development on Polkadot Seamless and Powerful *
Unlocking the Runtime for Smart Contracts Polkadot is more than a network of blockchains, it’s an interoperability layer where specialised chains contribute unique capabilities to a shared ecosystem. The Polkadot SDK lets teams build fully customisable blockchains, and with protocols like XCM and ISMP those chains can interact with each other seamlessly.
In theory, this means a smart contract could tap into assets, services, and logic from anywhere in the Polkadot Network. In practice, few contract developers have done this. Not because it’s impossible, but because the interface between immutable smart contracts and rapidly evolving runtimes is fragile and hard to get right.
The Pop API set out to change that. Its mission:
Give smart contracts a stable, versioned, developer-friendly way to call directly into the runtime — and, by extension, into the broader Polkadot ecosystem — without forcing developers to become Substrate experts or fear breaking changes.
The Pop API is built around three design goals:
- Simplicity: Developers can seamlessly interact with the runtime, allowing them to build advanced applications without needing deep blockchain or Substrate expertise.
- Stability: A versioned API that ensures smart contracts stay compatible even if the runtime evolves.
- Efficiency: Optimising for minimal contract size, having the lowest possible deployment and execution costs.
Pop API as Chain Extension
Pallet-Contracts (WASM) + Chain Extensions + ink! v5
The Pop API is implemented as a single, generic chain extension; a runtime component that smart contracts can call to dispatch an extrinsic (execute a function that changes state) or read state. This design means:
- It is not tied to any specific pallet or feature.
- Any chain can adopt it.
- New runtime features can be exposed without having to create an additional chain extension.
The extension offers two main functions:
- Dispatch
- ReadState
Making the Most of 4 Bytes
Calls into the chain extension carry a single u32 identifier, four bytes that pack all routing and versioning information:
[func_id, version, module_index, call_index]
- func_id: Which chain extension function to call (Dispatch or ReadState).
- version: Which API version.
- module_index: Which pallet to route to.
- call_index: Which specific dispatchable or read state method within that pallet.
Status Codes and Error Handling
The runtime returns another u32, the status code:
0→ Success.>0→ Encoded error.
This is a SCALE-encoded Error type, derived from DispatchError but extended with:
pub enum Error {
...
DecodingFailed = 254,
Unknown {
dispatch_error_index: u8,
error_index: u8,
error: u8,
} = 255,
}This ensures any runtime error can be returned to a contract, even unknown errors from newer runtime versions.
Use Cases
From the start, the Pop API focuses on delivering practical, high-value runtime capabilities to contracts. The initial use cases include:
- Fungibles: creating, minting, burning, transferring, and querying fungible assets.
- Non-Fungibles: creating, minting, burning, transferring, and querying NFTs.
- Messaging: sending and receiving ISMP/XCM messages with automatic callback.
Strengths and What We Learn
The Pop API is flexible, versioned, efficient, and developer-friendly through one-liner APIs and rich error reporting. But there are trade-offs only fully understood over time. This forum discussion highlights a core challenge: the Polkadot SDK is not stable enough at the error surface level. There are no automated tests in the SDK that flag breaking changes to errors, meaning a pallet update can silently change error variants or their encoding.
To be confident nothing breaks after a new release, full integration tests for every pallet the Pop API touches have to be written and maintained, just to detect such changes. Combined with global versioning in the original Pop API, this means that a breaking error change in any one use case can force a version bump for the entire API, even if 95% of it is unaffected.
And on top of that, the all-in-one surface of the chain extension means unrelated areas are tightly coupled. Changes in one area carry a higher risk of impacting others, and the cost of version bumps is amplified.
Pop API as Precompile
Pallet-Revive (PolkaVM) + Precompiles + ink! v6
Then the ecosystem shift toward Polkadot Hub and Solidity compatibility via pallet-revive on PolkaVM introduces a new model for exposing runtime functionality as precompiles: “virtual contracts” at deterministic H160 addresses using a Solidity ABI. R0GUE choose to adopt this approach to align with the latest PolkaVM technology, which means embracing precompiles and Solidity compatibility, and in turn gaining the ability to serve both ink! and Solidity contracts with the same APIs — opening interesting cross-language possibilities.

Precompiles
With chain extensions, contracts call a single generic entry point and pass 4 bytes as selector, plus SCALE-encoded parameters. The runtime decodes the selector, charges weight, dispatches the call, and returns a u32 status code; reads write results into the contract’s output buffer.
With precompiles, contracts call a specific H160 address and invoke a named function. Routing happens via the function selector, and parameters/returns use the Solidity ABI. Success paths return typed values directly, while failures revert with an ABI error payload.
Btw, YES, ink! speaks Solidity

Changes to the API Precompiles naturally lead the Pop API away from the all-in-one surface of the original chain extension, which in hindsight has only one advantage: a smaller auditing surface. Now, each use case — fungibles, non-fungibles, messaging — lives in its own precompile with its own interface and version. The versioning model remains the same: each precompile is versioned so older contracts continue to work as newer versions are introduced.
The new Pop API also addresses one of the biggest lessons from error handling. Instead of a “return anything” approach, each precompile now exposes specific, typed errors relevant to its domain, plus the Unknown variant used in the previous Pop API. This allows developers to detect and surface any error without exposing the full flexibility and fragility of the underlying SDK. The design is stable against upstream changes and does not break when pallets evolve.
Conclusion & Looking Ahead
The shift to revive didn’t make things easy. The implementation had to be completely reworked, and the process brought its fair share of delays. But in the end the Pop API resulted in the strongest solution possible. A combination of a carefully designed APIs and the architectural advantages that precompiles introduced.
Precompiles in the Polkadot SDK will bring new challenges: maintaining stability on top of a fast-moving SDK, and exposing as much functionality as possible without blocking improvements to those features. Pop API’s versioned precompiles should prove valuable here. And with the addition of the Unknown error variant, developers can still surface every error, even those not fully abstracted by the precompile. Thanks to the smaller surface area of precompiles, the API has been able to preserve the per-use-case approach while making it easier to evolve.
Now, a new chapter is opening for Polkadot, one where smart contracts take center stage. This is where things get exciting. A Polkadot network where powerful runtime features, cross-chain composability, and the full reach of XCM and ISMP are available to every contract developer. The building blocks are here. The possibilities are enormous. Let’s build!
