Pop Network

Pop Network

Pop Network is a Polkadot parachain purpose-built for smart contract development, created by R0GUE with funding from the Polkadot Treasury. It provides a stable, versioned API for smart contracts that leverages powerful runtime functionality without the need to learn pallet development. We delivered 60+ merged PRs across the project's full lifecycle, from early foundational infrastructure and API design through to the major architectural migration from chain extensions to pallet_revive precompiles.

The Challenge

Some of Polkadot's most powerful capabilities — cross-chain messaging, composable runtime primitives — are accessible only to parachain developers who build with FRAME pallets. Smart contract developers, the largest pool of Web3 builders, are locked out. They can't easily send XCM messages, interact with Asset Hub tokens, or compose with runtime functionality from within a contract. The learning curve for pallet development and XCM is steep, and the tools to bridge that gap didn't exist.

Pop Network set out to solve this by providing a clean abstraction layer: smart contracts written in ink! — Rust-based contracts targeting Polkadot's native execution environment — call a purpose-built API, the API dispatches into runtime pallets, and the complexity stays hidden. The challenge was making this work across two fundamentally different execution models — first pallet_contracts with chain extensions targeting WebAssembly, then pallet_revive with precompiles targeting PolkaVM — as Polkadot's smart contract infrastructure evolved beneath the project.

Our Work

Our work spanned the project's full lifecycle — from initial advisory on API design and foundational infrastructure work through to the major architectural migration that defined the project's final form.

API & Chain Extension Architecture

The API is the core abstraction — composed of runtime pallets and a corresponding ink! library providing a stable, developer-facing SDK for contracts. A key design decision was implementing the API as standard FRAME pallets: incoming calls are decoded as versioned RuntimeCall variants and dispatched through FRAME's native dispatch system, leveraging its weight accounting, call filtering, and error handling. Versioning was built into call encoding from the outset — critical where deployed contracts are immutable.

Our early involvement was mostly advisory, with initial contributions rounding out the NFTs offering and adding the first cross-chain messaging capability. The more substantial work came later with the chain extension architecture and the precompile migration.

As the API surface grew, the monolithic chain extension approach became unsustainable — each new pallet would have required its own extension implementation. We rearchitected it as a trait-driven pipeline where each stage is independently implementable and the entire extension surface declared via tuple composition at compile time. The rearchitecture consolidated dispatch and state-read logic into reusable function implementations, ensuring every runtime interaction was properly weight-charged, filtered, and error-handled. New pallets could be exposed to contracts simply by adding them to versioned runtime enums, with the extension handling the rest generically. A companion security fix ensured calls blocked at the runtime level (such as privileged balance operations) were equally blocked through the chain extension.

Messaging API

Building on the initial cross-chain messaging capability, the generalised messaging pallet abstracted over both XCM and ISMP transports behind a protocol-agnostic MessageId-based lifecycle. Contracts create cross-chain messages, receive a message identifier, and can poll its status before finally retrieving any response. A deposit-hold mechanism ensures the pallet is economically self-sustaining without subsidising cross-chain spam. Callback support was added subsequently, allowing asynchronous responses from ISMP and XCM queries to be dispatched directly back to contracts — with prepaid callback weight to ensure guaranteed execution.

Migration to pallet_revive Precompiles

The largest body of work was migrating the API layer from pallet_contracts chain extensions to pallet_revive precompiles — a fundamental shift in execution model driven by Polkadot's move toward PolkaVM and Ethereum compatibility. The SDK uplift to stable2506 brought in the Polkadot SDK release that introduced precompile support in the upstream pallet-revive, which the fungibles and messaging migrations were built against.

The fungibles precompile migration introduced pallet-api-vnext as an additive crate alongside the existing API — avoiding disruption to existing consumers during the transition. The fungibles API wraps pallet-assets, exposing the full token lifecycle through two precompiles: Fungibles as a single entry point for managing any asset, and Erc20 providing per-token endpoints that Ethereum wallets and tooling can interact with natively using the standard ERC-20 interface.

A key architectural improvement was the shift to precompile-based interfaces — #[ink::trait_definition] traits invoked via contract_ref! for ink!, with corresponding Solidity interfaces generated from .sol files. This made the API type-safe, discoverable, and idiomatic for both ink! and Solidity developers.

The messaging precompile migration was the more complex of the two — a three-tier precompile structure (Messaging, Ismp, Xcm) partly driven by the lack of interface inheritance in the alloy Solidity macros, but also to provide varying levels of abstraction depending on use case. The asynchronous contract callback mechanism supports dual encoding (SCALE or Solidity ABI, chosen at request time) and if a callback fails, the response is stored for manual polling as a fallback. Deposits are held separately for messaging and callback execution weight, ensuring guaranteed dispatch.

Both migrations are backed by comprehensive test suites — pallet-level unit tests covering precompile dispatch, weight charging, error handling, and deposit lifecycle, alongside integration tests that deploy compiled .polkavm contracts and exercise the full end-to-end flow against a real runtime.

Infrastructure & Network Operations

Alongside the API work, we laid some of the project's operational foundations:

Technical Highlights

A closer look at the key abstractions and design decisions behind the work.

Composable Chain Extension

The chain extension pipeline decomposes into a series of traits for standardised runtime interaction — Decode for decoding data read from contract memory, Matches for resolving a chain extension function from call data, Environment for providing access to the execution environment and Function for the implementation. Function implementations in the form of DispatchCall and ReadState provide a composable type system via tuples at compile time. Weight accounting is made explicit to enforce security, with each function implementation charging weight before performing the operation it accounts for. Runtime state reads are formalised via a Readable trait, allowing each module to define its own Read enum of query variants with benchmarked weights for proper fee charging — ensuring contracts can read runtime state while the chain accurately accounts for the execution cost.

Versioning spans both sides of the boundary: the contract-side library encodes calls as a 4-byte key [function, version, module, call] via ChainExtensionMethod::build(), and the Processor trait within the chain extension allows their extraction before the full decoding pass, prepending them to construct valid versioned runtime calls without additional payload overhead. The version then flows through VersionedRuntimeCall, VersionedRuntimeRead, and VersionedError enums, allowing the runtime to evolve its API while maintaining backward compatibility with deployed contracts.

A MockEnvironment enables pure unit tests without contract runtime setup, keeping the test suite fast and deterministic.

Chain Extension → Precompile Migration

The migration from chain extensions to precompiles required a considered rearchitecture across every layer of the stack:

Chain Extension (v0)Precompiles (vNext)
Call mechanismChainExtensionMethod::build(u32)contract_ref! via precompile address
Contract interfaceFree functions#[ink::trait_definition] traits
EncodingSCALESolidity ABI or SCALE
Addressing4-byte function identifierDeterministic EVM address
Error handlingStatusCode(u32)Solidity custom errors with 4-byte selectors

Address resolution uses two schemes: fixed_address(n) for general-purpose precompiles and prefixed_address(prefix, id) for per-asset ERC-20 endpoints where the asset identifier is inlined into the contract address. Each precompile version is a separate module registered at its own precompile index — and therefore its own address — ensuring current and future versions coexist without breaking existing consumers. The reworked contract-side library introduces additional lightweight client-side validation before hitting the precompile — checking for zero addresses, zero values, and sender-is-recipient issues — failing fast with typed errors to save gas on calls that would inevitably revert. Error handling uses a three-layer system: shared runtime error types, per-module versioned Error enums with Solidity 4-byte selectors computed at compile time, and a dual-path decoder handling both Solidity custom errors and SCALE payloads.

Protocol-Agnostic Messaging

The messaging pallet introduces MessageId as an application-level handle, with transport-specific creation paths converging on a uniform poll/get/remove lifecycle. A deposit-hold mechanism backed by pallet_balances hold reasons charges callers proportionally to message size, ensuring economic sustainability and block execution security. The callback system uses a Callback struct carrying destination contract address, encoding format (Scale or SolidityAbi), function selector, and gas/storage deposit limits — dispatching asynchronous responses to contracts implementing callback typed response traits. The precompiles defined as Solidity interfaces use function overloading for optional callbacks. A dual-trait pattern provides the equivalent on the ink! side — separate *Callback traits for each transport keeping both interfaces idiomatic and allowing contracts to opt into callback support only when needed. For protocol types that are inherently SCALE-encoded (VersionedXcm, Location), the design wraps them in SolBytes<Vec<u8>> to carry SCALE-native data as opaque blobs within the Solidity ABI boundary.

Outcome

We delivered across the full lifecycle of the project — from advisory and foundational infrastructure through chain extension architecture to the major precompile migration for pallet_revive. The precompile work was cutting-edge, developed alongside active upstream changes in both pallet_revive and ink! — requiring us to adapt to a moving target while maintaining API stability, a challenge that drew directly on the versioning and abstraction patterns established in the earlier chain extension work.

Pop Network is open source. Explore the project on GitHub.

Pop Network makes it easy for smart contract developers to use the Power of Polkadot.

github.com/r0gue-io/pop-node

Industry

Web3 / Smart Contracts

Let's start something

Interested in what's possible? We'd like to hear about it.

Get in touch

By browsing this website, you accept our Privacy Policy.