This document introduces the Foundry–Polkadot integration, built on top of pallet-revive. This integration allows Solidity developers to test contracts against Polkadot’s EVM and the PolkaVM (RISC-V) backends directly from forge test.
It covers:
- The Revive dual-execution architecture
- Foundry’s dual-bytecode compilation model
- The CLI interface for execution mode selection
- The
vm.polkadotcheatcode for dynamic runtime switching - Guidelines for production and experimental testing
Note on Terminology: "Polkadot runtime" refers to the execution environment activated by the
--polkadotflag. Internally, this environment is powered by pallet-revive, which supports two backends: EVM and PolkaVM.
Revive is a Substrate pallet designed to support two distinct execution backends:
| Bytecode | VM | Description |
|---|---|---|
| EVM | EVM Interpreter | Fully compatible with Ethereum. |
| PVM | PolkaVM (RISC-V) | Limited Ethereum compatibility. |
Developers can compile a single Solidity source into both bytecode formats. The type of bytecode deployed determines the backend used for its execution.
To support multiple backends, a dual compilation model is implemented within Foundry.
When tests are run in this environment, contracts are compiled to both EVM and PVM bytecode simultaneously. This enables the test runner to switch execution environments between the standard Foundry EVM and the Polkadot runtime during test execution.
Foundry executes tests inside a local REVM (EVM), where cheatcodes like vm.store and vm.prank manipulate the internal database.
In foundry-polkadot, the test contract (implementing Test or DSTest) continues to run inside Foundry's REVM. However, specific operations are intercepted to support Polkadot execution:
| Opcode | Action | Behavior |
|---|---|---|
CREATE / CREATE2 |
Intercept | The contract is deployed inside the Polkadot runtime (pallet-revive) instead of the Foundry REVM. |
CALL / STATICCALL / DELEGATECALL |
Intercept | The call is executed inside the Polkadot runtime (pallet-revive), skipping the Foundry REVM execution. |
- Compatibility: Test logic and assertions run in the standard Foundry environment.
- Execution Environment: Contract execution occurs inside the Polkadot runtime logic (pallet-revive).
- Cheatcodes: Standard Foundry cheatcodes remain functional for setup and assertions.
Hybrid testing requires state management between environments. This integration uses a model where the Foundry REVM and the Polkadot runtime synchronize state via access logs.
Instead of migrating the entire database, a diff-based state bridging mechanism is used:
- Polkadot Runtime Execution: Execution in Polkadot mode (pallet-revive) produces storage diffs.
- Sync to REVM: These diffs are applied to the Foundry REVM.
- Sync to Polkadot: Setup performed in REVM (via cheatcodes) is applied directly to the Polkadot runtime state.
This ensures that cheatcodes like vm.store function correctly and state changes in the contract are reflected in test assertions.
The CLI controls the runtime using the --polkadot flag, following standard Foundry conventions.
# Standard Foundry behavior (Local Foundry REVM)
forge test
# Polkadot EVM runtime (Default for Polkadot mode)
forge test --polkadot
# Explicitly select Polkadot EVM
forge test --polkadot=evm
# Experimental: Polkadot PVM runtime
forge test --polkadot=pvmDesign Principles:
- No Redundancy: Compiler flags are handled automatically.
- Consistency: Usage mirrors standard Foundry commands.
- Configuration: Compiler settings remain in
foundry.toml, while runtime selection is handled via CLI.
The CLI sets the default mode for the test run. The vm.polkadot cheatcode allows toggling environments dynamically within a specific test.
interface Vm {
/// Switch INTO or OUT OF Polkadot runtime.
/// backend: "evm", or "pvm"
function polkadot(bool enable, string memory backend) external;
/// Auto-detect backend from CLI flags.
function polkadot(bool enable) external;
}Switch to Polkadot runtime (default backend):
vm.polkadot(true);
// Subsequent calls run in Polkadot runtimeReturn to standard REVM:
vm.polkadot(false);
// Subsequent calls run in local Foundry REVMNote: When switching VMs, contracts must be registered for migration using
vm.makePersistentif they need to persist across execution boundaries.
The following tables describe how CLI flags interact with cheatcodes in different scenarios.
Command: forge test
Context: The test runs entirely in the local Foundry EVM. Polkadot features are disabled.
| Cheatcode Action | Resulting Environment |
|---|---|
| None | Foundry REVM |
vm.polkadot(true) |
❌ Invalid (Mode not enabled) |
Command: forge test --polkadot=evm or forge test --polkadot
Context: The test runs in Polkadot mode, deploying EVM bytecode by default. You can switch back to local Foundry REVM or to PVM bytecode deployment.
| Cheatcode Action | Resulting Environment |
|---|---|
| None | Polkadot EVM |
vm.polkadot(false) |
Foundry REVM |
vm.polkadot(true, "pvm") |
Polkadot PVM |
Command: forge test --polkadot=pvm
Context: The test runs in Polkadot mode, deploying PVM bytecode by default. You can switch back to local REVM or to EVM bytecode deployment.
| Cheatcode Action | Resulting Environment |
|---|---|
| None | Polkadot PVM |
vm.polkadot(false) |
Foundry REVM |
vm.polkadot(true, "evm") |
Polkadot EVM |
Below is an example of a test that bridges the environments.
// Simple contract to be tested
contract Simple {
function get() public pure returns (uint256) {
return 6;
}
}
contract FooTest is Test {
function testSimple() public {
// 1. Deploy: Intercepted -> Deployed to Polkadot runtime
Simple testContract = new Simple();
// 2. Call: Intercepted -> Executed in Polkadot runtime
uint256 number = testContract.get();
// 3. Assert: Executed in local REVM
assertEq(6, number);
}
}Execution Flow with forge test --polkadot (Default: EVM Backend):
- The test starts in Foundry's EVM.
new Simple()is identified as aCREATEopcode. The system intercepts it and deploys the contract into the Polkadot runtime (using EVM bytecode).testContract.get()is identified as aCALL. It is executed in the Polkadot runtime.- The return value
6is passed back to the test runner. assertEqruns in Foundry REVM to verify the result.
- Production: Use EVM mode (
--polkadot=evm). It is stable, deterministic, and compatible with Ethereum. - Research: Use PVM mode (
--polkadot=pvm) to explore RISC-V capabilities. This mode is experimental.
Tests on standard open-source projects have shown a 90-100% pass rate using the Polkadot EVM backend. However, the following limitations exist:
- Gas Model: The gas metering in
foundry-polkadotis not fully aligned with Polkadot's production gas model. Tests relying on precise gas checks may fail. - Balance Types: Ethereum uses
u256for balances, while Polkadot usesu128. Tests involving amounts exceedingu128::MAXwill fail in the Polkadot runtime. - PVM Integration Maturity: The PVM backend is experimental. Tests may not work, for example when using libraries or proxy patterns.