PolkaVM Smart Contracts
PolkaVM is a RISC-V based virtual machine for Substrate blockchains, offering gas-efficient smart contract execution. This guide covers building ZK-friendly contracts on PolkaVM.
What is PolkaVM?
PolkaVM is an alternative to the EVM designed for the Polkadot/Kusama ecosystem:
- RISC-V based: Standard instruction set, easy to target from many languages
- Gas efficient: Lower costs for cryptographic operations
- Rust native: First-class Rust support with no_std
- ZK optimized: Better performance for verification circuits
Development Setup
Online IDE (No Setup Required)
RevX.dev - Browser-based PolkaVM IDE:
- Write, compile, and deploy Rust contracts directly in your browser
- Pre-configured toolchain - no local installation needed
- Built-in deployment to Paseo testnet
- Perfect for learning and rapid prototyping
Recommended for beginners: Start with RevX.dev to experiment, then set up local tooling for production.
AI-Assisted Development
Claude Code AI Guide to PolkaVM - Comprehensive AI prompts and patterns:
- Contract templates and boilerplate generation
- Debugging assistance prompts
- Gas optimization suggestions
- ABI encoding/decoding helpers
- Common patterns and best practices
Local Setup (Prerequisites)
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install PolkaVM toolchain
cargo install polkatool
# Install required target
rustup target add riscv64emac-unknown-none-polkavm
Project Structure
my-polkavm-contract/
├── Cargo.toml
├── build.sh
├── src/
│ ├── lib.rs
│ └── zk.rs
└── ts/
├── main.ts
└── package.json
Example: Poseidon Hash Contract
This contract implements the Poseidon hash function on PolkaVM.
Rust Implementation (src/lib.rs)
#![allow(unused)] #![no_main] #![no_std] fn main() { extern crate alloc; use alloc::vec::Vec; use simplealloc::SimpleAlloc; mod zk; use zk::{poseidon, Fr}; #[global_allocator] pub static mut GLOBAL: SimpleAlloc<{ 1024 * 10 }> = SimpleAlloc::new(); use uapi::{input, HostFn, HostFnImpl as api, ReturnFlags}; use ethabi::{decode, ParamType, Token}; // Function selector: hash(uint256[2]) const HASH_SELECTOR: [u8; 4] = [0x56, 0x15, 0x58, 0xfe]; #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { unsafe { core::arch::asm!("unimp"); core::hint::unreachable_unchecked(); } } #[no_mangle] #[polkavm_derive::polkavm_export] pub extern "C" fn deploy() {} #[no_mangle] #[polkavm_derive::polkavm_export] pub extern "C" fn call() { input!(selector: &[u8; 4]); match selector { &HASH_SELECTOR => { input!(buffer: &[u8; 4 + 32 + 32]); // Decode ABI input let param_types = &[ParamType::FixedArray( Box::new(ParamType::Uint(256)), 2 )]; let decode_result = decode(param_types, &buffer[4..]).unwrap(); if let Token::FixedArray(array_tokens) = &decode_result[0] { if let (Token::Uint(a), Token::Uint(b)) = (&array_tokens[0], &array_tokens[1]) { // Convert to field elements let mut a_bytes = [0u8; 32]; let mut b_bytes = [0u8; 32]; a.to_big_endian(&mut a_bytes); b.to_big_endian(&mut b_bytes); let fr_a = Fr::from_limbs(bytes_to_limbs(&a_bytes)); let fr_b = Fr::from_limbs(&bytes_to_limbs(&b_bytes)); // Compute Poseidon hash let hash_result = poseidon(&[fr_a, fr_b]); // Return result let result_limbs = hash_result.to_limbs(); let result_bytes = limbs_to_bytes(&result_limbs); api::return_value(ReturnFlags::empty(), &result_bytes); } } } _ => panic!("Unknown function"), } } fn bytes_to_limbs(bytes: &[u8; 32]) -> [u64; 4] { let mut limbs = [0u64; 4]; for i in 0..4 { let start_idx = (3 - i) * 8; limbs[i] = u64::from_be_bytes([ bytes[start_idx], bytes[start_idx+1], bytes[start_idx+2], bytes[start_idx+3], bytes[start_idx+4], bytes[start_idx+5], bytes[start_idx+6], bytes[start_idx+7] ]); } limbs } fn limbs_to_bytes(limbs: &[u64; 4]) -> [u8; 32] { let mut result = [0u8; 32]; for i in 0..4 { let limb_bytes = limbs[3-i].to_be_bytes(); for j in 0..8 { result[i*8 + j] = limb_bytes[j]; } } result } }
Poseidon Implementation (src/zk.rs)
Key components of the Poseidon hash:
#![allow(unused)] fn main() { // BN254 field element in Montgomery form pub struct Fr([u64; 4]); // Constants for BN254 curve const MODULUS: [u64; 4] = [ 0x43e1f593f0000001, 0x2833e84879b97091, 0xb85045b68181585d, 0x30644e72e131a029, ]; // S-box: x^5 for BN254 fn sbox(x: &Fr) -> Fr { let x2 = x.mul(&x); let x4 = x2.mul(&x2); x4.mul(x) } // Poseidon parameters: t=3, 8 full rounds, 57 partial rounds const T: usize = 3; const NROUNDS_F: usize = 8; const NROUNDS_P: usize = 57; pub fn poseidon(inputs: &[Fr; 2]) -> Fr { let mut state = [Fr::zero(), inputs[0], inputs[1]]; // Apply rounds (constants and MDS matrix omitted for brevity) // ... state[0] } }
Build Script (build.sh)
#!/bin/bash
set -e
# Build for PolkaVM target
cargo build --release --target riscv64emac-unknown-none-polkavm
# Extract the contract binary
cp target/riscv64emac-unknown-none-polkavm/release/erc20 .
# Generate TypeScript bindings
cd ts
yarn install
Deployment
1. Compile and Build
./build.sh
2. Deploy to Testnet
cd ts/
export AH_PRIV_KEY="your_private_key"
yarn erc20
Output:
contract address is: 0xD670137a3CfAaF08B5395CDaaEaFf617672896D2
3. Call the Contract
Using cast (Foundry):
cast call <CONTRACT_ADDRESS> \
"hash(uint256[2]):(uint256)" \
"[123,456]" \
--rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io
Expected output:
10422317022970317265083564129873630166...
Solidity Interface
Call the deployed contract from Solidity:
pragma solidity ^0.8.30;
interface IPoseidonT3 {
function hash(uint256[2] memory input) external pure returns (uint256);
}
contract MyContract {
function computeHash(uint256 a, uint256 b)
external view returns (uint256)
{
return IPoseidonT3(POSEIDON_ADDRESS).hash([a, b]);
}
}
Example: ERC20 Token Contract
Beyond cryptographic primitives, PolkaVM can implement full ERC20 tokens in Rust. The polkavm-erc20-in-rust repository provides a complete example.
Key Features
- Full ERC20 Implementation:
transfer,approve,transferFrom,balanceOf,allowance - Solidity Compatible: Works with existing Solidity tools and interfaces
- Rust Performance: Optimized with
SimpleAllocand LTO - TypeScript Deployment: Complete deployment and testing scripts
Project Structure
polkavm-erc20-in-rust/
├── src/
│ └── erc20.rs # Main contract implementation
├── ts/
│ ├── main.ts # Deployment script
│ └── erc20.ts # Interaction helpers
├── build.sh # Build script
└── Cargo.toml # Dependencies
Function Selectors
#![allow(unused)] fn main() { const NAME_SELECTOR: [u8; 4] = [0x06, 0xfd, 0xde, 0x03]; // name() const SYMBOL_SELECTOR: [u8; 4] = [0x95, 0xd8, 0x9b, 0x41]; // symbol() const DECIMALS_SELECTOR: [u8; 4] = [0x31, 0x3c, 0xe5, 0x67]; // decimals() const TOTAL_SUPPLY_SELECTOR: [u8; 4] = [0x18, 0x16, 0x0d, 0xdd]; // totalSupply() const BALANCE_OF_SELECTOR: [u8; 4] = [0x70, 0xa0, 0x82, 0x31]; // balanceOf(address) const TRANSFER_SELECTOR: [u8; 4] = [0xa9, 0x05, 0x9c, 0xbb]; // transfer(address,uint256) const APPROVE_SELECTOR: [u8; 4] = [0x09, 0x5e, 0xa7, 0xb3]; // approve(address,uint256) const TRANSFER_FROM_SELECTOR: [u8; 4] = [0x23, 0xb8, 0x72, 0xdd]; // transferFrom(address,address,uint256) }
Storage Pattern
#![allow(unused)] fn main() { // Balance storage key: 7-byte prefix + 20-byte address pub fn get_balance_key(sender: &[u8; 20]) -> [u8; 27] { let mut key = [0u8; 27]; key[0..7].copy_from_slice(BALANCE); key[7..27].copy_from_slice(sender); key } // Allowance storage key: 9-byte prefix + 20-byte owner + 20-byte spender pub fn get_allowance_key(owner: &[u8; 20], spender: &[u8; 20]) -> [u8; 49] { let mut key = [0u8; 49]; key[0..9].copy_from_slice(ALLOWANCE); key[9..29].copy_from_slice(owner); key[29..49].copy_from_slice(spender); key } }
Build and Deploy
# Build the contract
./build.sh
# Deploy via TypeScript
cd ts
mv env.example .env # Add your private key
yarn install
yarn erc20
Example Output
contract address is: 0xAA00B7111CB9dd5074Db32E1B7917dD01ceb39dE
Name: name
Symbol: symbol
Decimals: 18
Total Supply: 1234000000000000000000
Transfer successful: 1233 tokens sent
Gas Optimization Tips
- Use Poseidon over Keccak: 5-10x cheaper in circuits
- Batch operations: Multiple hashes in one call
- Minimize storage: Use memory for intermediate calculations
- LeanIMT: Efficient Merkle tree updates
Common Issues
Memory Allocation
PolkaVM has limited memory. Use SimpleAlloc:
#![allow(unused)] fn main() { #[global_allocator] pub static mut GLOBAL: SimpleAlloc<{ 1024 * 10 }> = SimpleAlloc::new(); }
ABI Decoding
Ensure proper ABI encoding for inputs:
#![allow(unused)] fn main() { // Input format: [selector, encoded_params...] // hash(uint256[2]): selector + offset + [a, b] }
Field Arithmetic
Always validate field elements are in range:
#![allow(unused)] fn main() { require(value < SNARK_SCALAR_FIELD, "Value out of range"); }
Testing
Local Testing
# Test with known inputs
cast call <ADDRESS> "hash(uint256[2]):(uint256)" "[0,0]"
# Expected: 552037174761081804855249776048373...
cast call <ADDRESS> "hash(uint256[2]):(uint256)" "[1,2]"
# Expected: 324046091733193931345823963991450...
Integration Tests
// ts/main.ts
import { Contract } from 'ethers';
const contract = new Contract(address, abi, provider);
const hash = await contract.hash([123, 456]);
console.log('Hash:', hash.toString());
Resources
Development Tools
- RevX.dev - Browser-based PolkaVM IDE
- Claude Code AI Guide - AI-assisted development prompts
- PolkaVM Documentation
- pallet-revive Docs
Libraries & Examples
- Poseidon Paper
- PoseidonPolkaVM
- ERC20 in Rust - Complete ERC20 implementation
- Rust Contract Template
Testnet Resources
Previous: Kusama ZK Introduction | Next: Poseidon Hash