Introduction to Writing RISC-V Contracts in Rust on Polkadot
Source: Dev.to
Smart Contracts & the Evolution of Web3 Execution
Smart contracts have long been the primary method for developing applications in Web3.
However, the underlying technology hasn’t changed much over the years. The EVM (Ethereum Virtual Machine) has long been the default option for smart‑contract development. It served its purpose well, bootstrapping an entire industry of decentralized applications and becoming the de‑facto standard for interacting with Web3 infrastructure.
The Problem with the EVM
- The EVM is an emulation layer, not an actual machine.
- It is a virtual machine designed for a specific network, which brings inherent limitations.
- Although it is “general‑purpose” in a sense, it does not give developers the full capabilities of a typical tech stack or the high‑performance tooling that powers the rest of the internet.
If we want a new standard for Web3 software, it needs to be on par with (or ideally better than) the status quo. This isolation holds back the next generation of complex, high‑compute applications. We should be able to create and deploy applications the same way we do in the conventional world—on Web3.
Why the Current Model Is Slow
- Developer experience – Write high‑level code → compile to bytecode → EVM executes instruction‑by‑instruction.
- Performance – Execution on a custom VM is slower than native execution.
- Optimization gap – By relying on a custom VM, we lose decades of hardware‑architecture optimizations (e.g., RISC‑V).
Introducing PolkaVM
PolkaVM is a RISC‑V‑based virtual machine that lets developers write smart contracts in Rust, Go, and any language that can be compiled by LLVM—just like traditional software. It represents a shift to “bare‑metal” execution while still:
- Executing deterministic logic.
- Maintaining persistent state.
- Granting developers flexibility beyond the bounds of a custom sandbox.
How PolkaVM Works with Ethereum Tooling
- Revive (pallet‑revive) provides Solidity support on Polkadot.
- Revive is a compiler pipeline that targets PolkaVM (RISC‑V) while keeping Ethereum tooling compatibility as a first‑class goal.
- An Ethereum JSON‑RPC adapter (often called an eth‑rpc adapter) runs on the node, allowing tools like MetaMask, Hardhat, or Foundry to speak normal Ethereum JSON‑RPC. The node translates those calls into the underlying Polkadot runtime and contract environment.
When you send a transaction:
- The adapter and runtime plumbing route it into the PolkaVM execution engine.
- The engine leverages the performance and security properties of the underlying RISC‑V VM.
Reference Guides
- Polkadot Hub RPC node guide (Ethereum RPC adapter) – Smart contract functionality / Ethereum tooling compatibility
- Get Started – Smart Contracts on Polkadot – Overview of how contracts work on Polkadot Hub
Writing Contracts for PolkaVM
Language Options
- Solidity (via Revive)
- Rust, Go, or any language that compiles to RISC‑V
No‑Std Environment
Writing for PolkaVM is similar to writing for an embedded environment: you operate on “bare metal” without an operating system (OS).
no_std: The Rust standard library (std) assumes OS services (threads, files, allocation plumbing). In a no‑std context, you cannot usestddirectly.core: Always available.alloc: Available if you provide a global allocator.
Memory Management Overview
| Mechanism | Description | Trade‑off |
|---|---|---|
| Linear memory (LIFO) | Fast allocation by moving a pointer. | Compiler must know sizes up‑front; dynamic structures are harder. |
| Dynamic memory (heap) | Supports growable structures like Vec and String. | Requires an allocator to manage free space and fragmentation. |
In a standard environment the allocator is provided automatically. In a no_std environment you start without one, but you can add one (e.g., picoalloc) to manage a fixed chunk of memory as a heap.
// Designate a fixed region of memory for the allocator to manage.
#[global_allocator]
static mut ALLOC: picoalloc::Mutex = ...;
With a global allocator in place, the alloc crate becomes usable, allowing types such as Vec even in a no_std contract.
Interacting with the Blockchain
Contracts communicate with the blockchain via a host interface (exposed in the Revive stack through the pallet-revive-uapi crate). Host functions cover storage, token transfers, events, and chain context.
Representative Host Functions
| Host Function | Purpose |
|---|---|
set_storage / get_storage | Write/read contract storage (key/value). |
call / instantiate | Call another contract / deploy a new contract instance. |
deposit_event | Emit an event (topics + data). |
caller / origin | Get the caller and origin addresses. |
call_data_size / call_data_copy | Read transaction input (calldata). |
return_value | Return data (and optionally revert) and stop execution. |
balance / balance_of | Query balances. |
Reference: pallet-revive-uapi docs
Storage Model in Rust
When writing a PolkaVM contract in Rust, storage is a raw key‑value store:
- Keys – Fixed‑size byte arrays (commonly 32 bytes).
- Values – Variable‑size byte vectors.
You can use the alloc crate (once a global allocator is set) to work with familiar collections (Vec, String, etc.) while still adhering to the deterministic, on‑chain execution model.
Summary
- The EVM’s emulation‑layer design limits performance and developer ergonomics.
- PolkaVM brings RISC‑V‑level performance and LLVM‑based language support to Web3 smart contracts.
- Through Revive and an Ethereum RPC adapter, existing Ethereum tooling remains compatible.
- Contracts are written in a
no_stdenvironment but can still leverage dynamic memory via a custom allocator. - Interaction with the blockchain is performed through a well‑defined set of host functions exposed by
pallet-revive-uapi.
By moving to a “bare‑metal” execution model, developers can build the next generation of high‑compute, complex Web3 applications without sacrificing the deterministic guarantees required on‑chain.
Fixed‑length Byte Arrays
For example:
const VALUE_KEY: [u8; 32] = [0u8; 32];
This key can then be stored using a host function such as set_storage:
api::set_storage(
StorageFlags::empty(),
&VALUE_KEY,
&value_bytes,
);
Ethereum‑style ABI Selectors
The ABI defines how functions are identified within a contract.
In Ethereum‑style ABIs a 4‑byte selector targets a specific function.
How a selector is derived
- Take the text signature, e.g.
"flip()". - Hash it with Keccak‑256.
- Use the first 4 bytes of the hash.
That 4‑byte ID acts as the “key” that selects a function entry point.
Using a Solidity interface in Rust
Rust contracts can adopt a Solidity interface as their ABI, which gives you:
- Ethereum tooling compatibility – tools like Foundry’s
castcan talk to your contract via an Ethereum JSON‑RPC endpoint. - Cross‑contract interoperability – other contracts (Rust or Solidity) can call into yours using the same ABI expectations.
You have two ways to define an Ethereum‑compatible ABI:
- Create an external Solidity file that defines your contract’s ABI.
- Use the
sol!macro from thealloycrate.
Example: Using sol! with an External File
Assume we have a Solidity interface that contains a public function flip().
Import it in Rust:
sol!("Flipper.sol");
The macro expands at compile time into Rust types with selectors.
A simplified generated snippet looks like this:
// Generated code (simplified)
struct flipCall {}
impl SolCall for flipCall {
// 4‑byte selector for flip()
const SELECTOR: [u8; 4] = keccak256("flip()")[0..4];
}
Benefits
- Type safety – decode into Rust types instead of hand‑parsing bytes.
- Standard selectors – the same selector scheme as the EVM ABI.
You can reference the selector in your dispatch loop as:
Flipper::flipCall::SELECTOR
When the selector matches, invoke the corresponding Rust function to perform the work.
PVM Build Process
- Rust → ELF – the Rust compiler produces a standard ELF binary (
riscv64emac-unknown-none-polkavm). - Refinement (Polkatool) – a specialized linker performs tree shaking (removing dead code) and other optimizations, outputting the final
.polkavmfile.
The .polkavm artifact is what gets deployed on‑chain.
Building a Flipper Contract & Deploying to the Polkadot Testnet
The contract will store a single bool value and let anyone flip it.
1. Prerequisites
| Tool | Install command |
|---|---|
| Git | Download from |
| Rust | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
| Foundry | curl -L https://foundry.paradigm.xyz | bash |
foundryup |
2. Create a Wallet
# Generate a new private key
cast wallet new
Import the generated key for later signing:
cast wallet import pvm-account --private-key
3. Fund the Wallet (Polkadot Testnet – Paseo / Asset Hub)
- Open the Polkadot Faucet.
- Choose “Polkadot testnet (Paseo)” and ensure the chain is Assethub.
- Paste the address you just generated.
- Click “Get Funds” (it may take a few minutes).
4. Clone the Template Project
git clone -b template https://github.com/CrackTheCode016/flipper-pvm flipper
cd flipper
Note: A CLI tool (
cargo-pvm-contract) is under development to automate project scaffolding. For this tutorial we work directly from the template.
5. Project Layout
flipper/
├─ src/
│ └─ flipper.rs # contract logic (needs implementation)
├─ Flipper.sol # Solidity ABI interface
└─ Cargo.toml
The template already includes:
- An allocator via picoalloc
- A panic handler
- The Solidity interface definition
The core logic (storage handling, event emission, dispatch loop) is left as todo!() placeholders.
6. Implement the Missing Pieces
Open src/flipper.rs and replace the todo!() blocks as follows.
a. Reading the Stored Value
fn get_value() -> bool {
// Allocate a 32‑byte buffer (Solidity stores bool as uint8 in a 32‑byte slot)
let mut value_bytes = vec![0u8; 32];
let mut output = value_bytes.as_mut_slice();
match api::get_storage(StorageFlags::empty(), &VALUE_KEY, &mut output) {
Ok(_) => {
// The bool is stored in the last byte
output[31] != 0
}
Err(_) => false, // Default to false if the key is not set
}
}
b. Writing the Stored Value
fn set_value(value: bool) {
let mut value_bytes = [0u8; 32];
// Store the bool in the last byte (0 = false, 1 = true)
value_bytes[31] = if value { 1 } else { 0 };
api::set_storage(StorageFlags::empty(), &VALUE_KEY, &value_bytes);
}
c. Emitting the Flipped Event
fn emit_flipped(new_value: bool) {
// Build the event struct (generated by `sol!`)
let _event = Flipper::Flipped { new_value };
// First topic is the event signature hash
let topics = [Flipper::Flipped::SIGNATURE_HASH.0];
// Encode the bool as a 32‑byte word (last byte holds the value)
let mut data = [0u8; 32];
data[31] = if new_value { 1 } else { 0 };
api::deposit_event(&topics, &data);
}
d. The Main Dispatch Entry Point (call)
#[no_mangle]
pub extern "C" fn call() {
// 1️⃣ INPUT HANDLING
let call_data_len = api::call_data_size();
let mut call_data = vec![0u8; call_data_len as usize];
api::call_data_copy(&mut call_data, 0);
// 2️⃣ SELECTOR EXTRACTION
if call_data.len() {
let current = get_value();
let new_value = !current;
set_value(new_value);
emit_flipped(new_value);
}
// ── get() ────────────────────────────────────────
Flipper::getCall::SELECTOR => {
let current = get_value();
// 4️⃣ RETURN ENCODING (bool as uint256)
let mut return_data = [0u8; 32];
return_data[31] = if current { 1 } else { 0 };
api::return_value(ReturnFlags::empty(), &return_data);
}
// ── Unknown selector ─────────────────────────────────
_ => {
api::return_value(ReturnFlags::REVERT, b"Unknown selector");
}
}
}
7. Build & Deploy
# Build the ELF binary
cargo build --release
# Run the Polkatool linker to produce the .polkavm artifact
polkatool link -o flipper.polkavm target/riscv64emac-unknown-none-polkavm/release/flipper
Upload flipper.polkavm to the Polkadot testnet using your preferred RPC client (e.g., cast send or a Polkadot UI).
Once deployed, you can interact with the contract:
# Call flip()
cast send "flip()" --private-key
# Call get()
cast call "get()" --rpc-url
You should see the boolean toggle on each flip() call and the current value returned by get().
🎉 You now have a fully functional Flipper contract running on Polkadot’s PVM! 🎉
Feel free to explore further—add more functions, emit additional events, or integrate with other on‑chain logic. Happy hacking!
Build the contract
cargo build
If the build succeeds you’ll find flipper.debug.polkavm in the target directory.
Set your RPC endpoint
export ETH_RPC_URL="https://services.polkadothub-rpc.com/testnet"
Deploy the contract
cast send --account pvm-account \
--create "$(xxd -p -c 99999 target/flipper.debug.polkavm)"
A successful deployment will output something similar to:
blockHash 0xf215391078dd412eb90095e5c5ad4454a761299ef51ce9ad44e0dc5178f957ee
blockNumber 2
contractAddress
cumulativeGasUsed 0
effectiveGasPrice 50000000000
from 0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac
gasUsed 295239...
status 1 (success)...
to 0xc01Ee7f10EA4aF4673cFff62710E1D7792aBa8f3
Copy the contractAddress and export it for later use:
export RUST_ADDRESS=
Interact with the contract
Check the current value (should be false)
cast call $RUST_ADDRESS "get() returns (bool)"
Flip the value
cast send --account pvm-account $RUST_ADDRESS "flip()"
Verify the new value (should be true)
cast call $RUST_ADDRESS "get() returns (bool)"
Explorer example
You can view the deployed contract on the Polkadot test‑net explorer:
https://polkadot.testnet.routescan.io/address/0x2044DB81C13954e157Cb9F1E006006a70b7CEA89
Why PolkaVM?
PolkaVM showcases a future where Web3 applications are full‑scale programs running on real technology stacks, not just mini‑apps. Building on Web3 should expand our design possibilities rather than force compromises.
Working at this low level of smart‑contract development opens new avenues for computationally expensive applications, where every byte counts. Contracts written in Rust or Solidity can call these optimized PolkaVM contracts, benefiting from lower costs and more practical execution models.