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 SimpleAlloc and 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

→ View Full ERC20 Example

Gas Optimization Tips

  1. Use Poseidon over Keccak: 5-10x cheaper in circuits
  2. Batch operations: Multiple hashes in one call
  3. Minimize storage: Use memory for intermediate calculations
  4. 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

Libraries & Examples

Testnet Resources


Previous: Kusama ZK Introduction | Next: Poseidon Hash