TL;DR: PET is a test suite for Polkadot/Kusama relay chains and system parachains, with growing coverage for ecosystem parachains. E2E tests for accounts, proxy, multisig, governance, staking, and more now exist alongside the original XCM tests.a Acala, Hydration, and Bifrost have E2E coverage; most ecosystem chains don’t yet.
Repository: GitHub - open-web3-stack/polkadot-ecosystem-tests: Polkadot Ecosystem Tests powered by Chopsticks · GitHub
Parachain teams are welcome to use and contribute.
Overview
PET is an automated test suite that forks live mainnet state and runs end-to-end test scenarios against it: real storage, real runtimes, real call data. It covers XCM transfers, balance operations, proxy call filtering, governance flows, and more.
Tests can run against a proposed runtime (via a WASM override) to catch bugs before they’re enacted. Scheduled CI also runs every 6 hours against the current production runtime, catching state-dependent issues and flagging when an enacted upgrade has changed behavior.
Both modes have caught real problems: broken transfers, broken XCM messages between parachains, inconsistent proxy call filter lists, missing pallet events, and a pallet-balances bug that silently prevented users from adding proxies or creating multisigs. For those interested in the technical details, more on that example below.
Background and history
Prior forum discussions (Multichain XCM integration test, Network Test Framework) and an early Parity prototype (polkadot-integration-tests) established the motivation: runtime upgrades were breaking cross-chain interactions. In January 2024, Bastian Köcher (@bkchr) opened polkadot-fellows/runtimes#171 after XCM changes broke parachain messaging:
We want to ensure that nothing breaks unintentionally. […] Recently we have seen that changes to XCM broke messages between parachains in the ecosystem. This was detected on Kusama, so Kusama actually has done its job, but these things could also be detected before they are on Kusama to make everyone’s lives easier.
Development was funded via the Ecosystem Test Environment Bounty (Referendum #804). Bryan Chen (@xlc) and the Acala team built it, directly inspired by Acala’s own e2e-tests repo, which had already caught multiple XCM issues before runtime upgrades reached production.
Bryan introduced PET on the forum in July 2024. At the time it had 12 networks, 8 network pairs, and 24 tests, all XCM-focused. The community reviewed it, several parachain teams began contributing test scenarios, and the Fellowship called for broader participation.
Coverage has since expanded from XCM to include end-to-end tests for accounts, proxy, multisig, governance, staking, and more on relay chains and system parachains.
Current situation
PET has broad coverage for relay chains and system parachains (Asset Hub, Bridge Hub, Coretime, People, Collectives). Beyond XCM, E2E suites now exist for accounts/balances, proxy, multisig, governance, staking, nomination pools, vesting, bounties, identity, and more.
Some ecosystem parachains are covered too. XCM test scenarios exist from Acala, Astar, and Hydration. E2E tests (accounts, preimage) have been added for Acala, Hydration, and Bifrost on the Polkadot side, and for Karura, Basilisk, and Bifrost on Kusama.
But most ecosystem parachains aren’t covered yet. The LiquidityRestrictions bug detailed in the end of the post, inconsistencies in proxy call filter lists, XCM breakage from relay chain upgrades — these go undetected until users hit them.
PET is powered by Chopsticks, which forks live mainnet state at known-good block numbers. Tests use Vitest with snapshot testing. Snapshots record the output of extrinsic execution (events, balances, storage changes) and detect when that output changes. Volatile data (timestamps, block hashes) is redacted to prevent flaky tests. When a snapshot changes unexpectedly, CI creates a GitHub issue notifying the relevant team. Tests can also be triggered on demand via the bot trigger issue.
Supported chains: Polkadot relay, Asset Hub, Bridge Hub, Coretime, People, Collectives, plus ecosystem parachains Acala, Hydration, Moonbeam, Astar, and Bifrost. Kusama side has corresponding canary networks.
What PET tests
XCM transfers: test runners for xcmPallet transfers (relay ↔ parachain, parachain ↔ parachain) and xTokens transfers. These cover the full message flow: construction, sending, routing, execution on the destination, balance verification on both sides.
E2E test suites: suites for the core pallets used by relay chains and system parachains. All test logic lives in packages/shared/src/, parameterized so the same code runs on both Polkadot and Kusama.
The accounts suite covers balance operations, deposits, locks, freezes, and reserves. The staking suite covers bond/unbond/nominate/payout flows and validator selection. Multisig, governance, vesting, nomination pools, and identity have analogous suites.
The proxy suite is worth singling out because of proxy call filtering: for every proxy type in each chain, it checks which calls are permitted and which are rejected. A coverage checker script verifies completeness per chain. Parachains with custom proxy types should verify their call filter coverage here.
Snapshots also catch unintentional changes from runtime upgrades. If an event structure or extrinsic side effect changes, the diff surfaces it in CI. Some tests are shipped with deliberately empty snapshots to document known upstream issues (e.g. missing events in pallet_identity and pallet_conviction_voting); when the upstream fixes land, the snapshot mismatch prompts an update.
Goal
The goal: every parachain should have E2E coverage for its critical flows — staking, proxy, multisig, governance, XCM — testing both the happy path and failure cases (insufficient funds, bad origins, incorrect parameters, state that blocks the operation). CI catches regressions automatically; tests run against proposed runtimes before upgrades are enacted.
How to get there
The E2E suites are chain-agnostic and parameterized. Running them on an ecosystem parachain means instantiating the shared module with the chain’s config. Only the suites relevant to a chain’s pallets need to be included; if a chain doesn’t have staking, the staking suite is simply not instantiated.
Here’s the full accounts test file for Hydration:
import { hydration } from '@e2e-test/networks/chains'
import {
accountsE2ETests, createAccountsConfig, createDefaultDepositActions,
manualLockAction, manualReserveAction, registerTestTree, type TestConfig,
} from '@e2e-test/shared'
const testConfig: TestConfig = { testSuiteName: 'Hydration Accounts' }
const accountsCfg = createAccountsConfig({
expectation: 'failure',
actions: {
reserveActions: [manualReserveAction()],
lockActions: [manualLockAction()],
depositActions: createDefaultDepositActions(),
},
})
registerTestTree(accountsE2ETests(hydration, testConfig, accountsCfg))
25 lines. The shared module handles everything; the chain-specific file provides the config and any overrides. The same pattern applies to proxy, multisig, governance, and the other suites.
Adding a new chain to PET requires:
- A chain config in
packages/networks/src/chains/ - RPC endpoints in
packages/networks/src/pet-chain-endpoints.json - CI workflow entries in
.github/workflows/ci.ymland.github/workflows/update-snapshot.yml - Test files that instantiate the shared modules, often under 30 lines each
Documentation: For Test Writers.
How we can help
If you run into issues adding your chain (configuring the chain definition, figuring out which suites apply, dealing with chain-specific quirks), open an issue or reach out to @rockbmb directly. You can also find us in the Polkadot Technical Fellowship Open Channel on Matrix.
Technical details: LiquidityRestrictions, a bug your chain might have
pallet-balances has a bug from the SDK’s incomplete migration from Currency traits to fungible traits (polkadot-sdk#226). If a user has staked most of their balance (creating a freeze via fungible), operations still going through Currency::reserve (proxy::add_proxy, multisig::as_multi, referenda::submit) fail with a spurious balances.LiquidityRestrictions error, even with enough spendable funds.
This hit Moonbeam in production. The SDK fix (polkadot-sdk#8108) was merged in September 2025, with a deeper fix (polkadot-sdk#9560) migrating pallet_proxy off Currency::reserve entirely.
PET’s accounts suite (issue #401) detects this. Each chain’s config specifies whether to expect the bug or the fix. Polkadot relay currently expects success. When a chain upgrades, the configuration flips, and the test becomes a regression guard.
pallet-balances has two balance management APIs:
| Concept | API | Status |
|---|---|---|
| Locks / Reserves | Currency trait |
Deprecated, still used by pallet_proxy, pallet_multisig, others |
| Freezes / Holds | fungible traits |
The replacement |
The bug was in Currency::ensure_can_withdraw. When a pallet called reserve(), the check was:
ensure!(new_balance >= Self::account(who).frozen, Error::LiquidityRestrictions);
This compares the new free balance (after the reserve) against the frozen amount. Reserves move funds from free to reserved — total balance unchanged. The frozen constraint is on the total balance:
|__total (free + reserved)_______________|
|__on_hold/reserved__|___free____________|
|__________frozen___________|
|__on_hold__|__ed__|
|__untouchable__|__spendable__|
With free = 5, frozen = 10, reserved = 10, total = 15 — 5 spendable tokens, a reserve should succeed. The old code sees new_free = 0 < frozen = 10 and returns LiquidityRestrictions.
The test:
- Credit an account with 1,000,000 × ED
- Reserve 700,000 × ED (via staking bond, nomination pool creation, or manual storage write)
- Freeze 700,000 × ED (via vested transfer or manual storage write)
- Attempt an action that uses
Currency::reserveinternally:proxy::add_proxy,multisig::as_multi, orreferenda::submit - Assert the outcome against the chain’s expected state
Test cases are combinatorially generated: every combination of reserve action × lock action × deposit action. Each chain gets its own matrix.
For chains where the fix hasn’t been adopted yet, the test asserts the specific error:
expect(client.api.errors.balances.LiquidityRestrictions.is(moduleError)).toBe(true)
And verifies the account had enough funds, proving the error was wrong:
expect(account.data.free.toBigInt()).toBeGreaterThanOrEqual(actionDeposit)
This only shows up when both balance accounting systems coexist in the same account — multiple pallets interacting against realistic state. Unit tests won’t catch it.