Foundry–Polkadot: Testing Directly in pallet-revive

Foundry–Polkadot: Testing Directly in pallet-revive

Foundry–Polkadot v1.5.1 is out with two major features:

  • pallet-revive testingforge test --polkadot now executes contracts directly inside pallet-revive, supporting both EVM and PolkaVM (RISC-V) backends.
  • anvil-polkadot — a Substrate-based local node with an Ethereum-compatible RPC API.

This document covers the testing integration.

Pallet-revive Architecture

Pallet-revive supports two execution backends:

Bytecode VM Description
EVM EVM Interpreter Executes EVM bytecode inside the REVM interpreter.
PVM PolkaVM (RISC-V) Executes PVM bytecode inside the PolkaVM interpreter.

Developers can compile a single Solidity source into both bytecode formats. The bytecode deployed determines which backend executes it.

How It Works

In standard Foundry, all contracts execute inside Foundry EVM. In Foundry-Polkadot, the test contract still runs in Foundry EVM, but contract deployment and calls are intercepted and routed to pallet-revive:

Opcode Behaviour
CREATE / CREATE2 Contract deployed inside pallet-revive instead of Foundry EVM.
CALL / STATICCALL / DELEGATECALL Call executed inside pallet-revive instead of Foundry EVM.

This means:

  • Test logic, assertions, and cheatcodes run in Foundry EVM as usual.
  • Contract execution happens inside pallet-revive.
  • State is synchronised between the two environments using diff-based bridging - changed storage slots are synced after each call, keeping cheatcodes like vm.store working correctly.

CLI

forge test                  # Standard Foundry behaviour (Foundry EVM)
forge test --polkadot       # pallet-revive with EVM backend (default)
forge test --polkadot=evm   # Same as above, explicit
forge test --polkadot=pvm   # pallet-revive with PVM backend (experimental)

Switching Backends: vm.polkadot

Within a test, you can switch where contracts are deployed and executed using the vm.polkadot cheatcode - for example, to compare contract behaviour across backends or to deploy specific contracts in Foundry EVM instead of pallet-revive.

interface Vm {
    /// Switch INTO or OUT OF Polkadot runtime (pallet-revive).
    /// backend: "evm", or "pvm"
    function polkadot(bool enable, string calldata backend) external;

    /// Auto-detect backend from CLI flags.
    function polkadot(bool enable) external;
}

Note: Contracts must be registered with vm.makePersistent to survive backend switches.

Execution Matrix

The following table describes how CLI flags interact with cheatcodes in different scenarios.

Scenario 1: Standard Foundry EVM

Command: forge test
Context: The test runs entirely in Foundry EVM. Polkadot features are disabled.

Cheatcode Action Resulting Environment
None Foundry EVM
vm.polkadot(true) :cross_mark: Invalid (Mode not enabled)

Scenario 2: Polkadot EVM

Command: forge test --polkadot=evm or forge test --polkadot
Context: The test runs in Polkadot mode, deploying EVM bytecode by default to pallet-revive. You can switch back to Foundry EVM deployment.

Cheatcode Action Resulting Environment
None Polkadot EVM
vm.polkadot(false) Foundry EVM
vm.polkadot(true, "pvm") :cross_mark: Not supported - contracts are not compiled to PVM in EVM mode

Scenario 3: Polkadot PVM

Command: forge test --polkadot=pvm
Context: The test runs in Polkadot mode, deploying PVM bytecode to pallet-revive. Both EVM and PVM bytecodes are compiled, so you can switch between backends.

Cheatcode Action Resulting Environment
None Polkadot PVM
vm.polkadot(false) Foundry EVM
vm.polkadot(true, "evm") Polkadot EVM

Example

contract Simple {
    function get() public pure returns (uint256) {
        return 6;
    }
}

contract FooTest is Test {
    function testSimple() public {
        // Deploy: intercepted → deployed to pallet-revive
        Simple testContract = new Simple();

        // Call: intercepted → executed in pallet-revive
        uint256 number = testContract.get();

        // Assert: runs in Foundry EVM
        assertEq(6, number);
    }
}

Execution flow with forge test --polkadot (Default: EVM Backend):

  1. The test starts in Foundry EVM.
  2. new Simple() is identified as a CREATE opcode. The system intercepts it and deploys the contract into pallet-revive.
  3. testContract.get() is identified as a CALL. It is executed in pallet-revive.
  4. The return value 6 is passed back to the test runner.
  5. assertEq runs in Foundry EVM to verify the result.

Limitations

Tests on standard open-source projects show a 90–100% pass rate with the Polkadot EVM backend. Known limitations:

  1. Gas model — Not fully aligned with Polkadot’s production gas model. Tests relying on precise gas checks may fail.
  2. Numeric types — Ethereum uses u256 for balances, block numbers, and timestamps. Polkadot uses u128 for balances and u64 for block numbers/timestamps. Values exceeding these limits are clamped with a warning.
  3. PVM backend maturity — The PVM backend is experimental. Tests may fail when using libraries or proxy patterns.
2 Likes