Foundry–Polkadot: Testing Directly in pallet-revive
Foundry–Polkadot v1.5.1 is out with two major features:
- pallet-revive testing —
forge test --polkadotnow 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.storeworking 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.makePersistentto 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) |
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") |
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):
- The test starts in Foundry EVM.
new Simple()is identified as aCREATEopcode. The system intercepts it and deploys the contract into pallet-revive.testContract.get()is identified as aCALL. It is executed in pallet-revive.- The return value
6is passed back to the test runner. assertEqruns 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:
- Gas model — Not fully aligned with Polkadot’s production gas model. Tests relying on precise gas checks may fail.
- Numeric types — Ethereum uses
u256for balances, block numbers, and timestamps. Polkadot usesu128for balances andu64for block numbers/timestamps. Values exceeding these limits are clamped with a warning. - PVM backend maturity — The PVM backend is experimental. Tests may fail when using libraries or proxy patterns.