Skip to main content
Version: v6

Polkadot SDK Precompiles Title Picture

Rust contracts on pallet-revive

pallet-revive supports contracts written in Rust and Solidity. Solidity is supported by compiling the source code with Parity's revive compiler. Rust is supported by providing a low-level FFI interface and compiling for PolkaVM. You can view the low-level interface here. See the HostFn trait for functions exposed to smart contracts.

Low-level Rust interface

pallet-revive's low-level Rust interface can be used to write contracts, those will be very low-level though with developers having to take care of everything by themselves. In this section we'll explain what "everything" implies.

We believe a fitting comparison is the analogy of writing a web application in something like Ruby on Rails vs C, crafting your own HTTP responses and doing byte-level manipulation.

There are certainly niches where "going low" makes sense, but due to the complexity, the overwhelming majority of developers will neither want nor need to use it. It's certainly not a way that can be recommended to newcomers or as a general approach. For Polkadot SDK natives: this would be similar to developing a parachain without FRAME.

Nevertheless, it's interesting and examples of such low-level contracts can be found in the pallet-revive fixtures directory. Another cool example is the nightmarket project, written entirely in low-level Rust for pallet-revive.

ink! vs. low-level Rust

ink! is a toolkit to make the development of Rust smart contracts for pallet-revive easy and efficient. To highlight some key abstractions that make smart contract development much easier than with "plain" Rust:

ABI Support

ink! supports two ABIs: Solidity and our own ABI format, which uses SCALE.

Support for the Solidity ABI means you can interact with ink! contracts as if they were Solidity contracts. You can cross-contract call them or use them with MetaMask or Hardhat.

Our custom ABI uses the Polkadot native SCALE encoding, which makes it highly efficient. The Solidity ABI encoding is bloated and not Polkadot native. By using SCALE, ink! contracts play well with Polkadot-native tooling and libraries (PAPI, polkadot-js, …).

If you write low-level Rust contracts, you will not have any ABI support or dispatching logic whatsoever. This means, you'll have to handcraft on your own how your contract is called and how it reacts to that. Building a frontend for your contract will require you to build the logic for calling the contract by yourself.

Types

We've baked the classic Polkadot SDK types for Balance, AccountId, etc. into ink! (see here for how). For developers coming from parachain development, ink! will feel natural and they'll recognize the same guiding principles.

ink! provides a high-level type system built directly on top of Rust’s own type system. This means you can use enums, structs, generics, traits, associated types, iterators, Result, pattern matching, and all other expressive features that Rust offers in a smart contract context. You can use any struct, tuple, etc. that supports SCALE encoding in events, storage, as function arguments, as return types, etc.

ink! automatically derives the logic for encoding, decoding, allocating, storing, and loading these types from contract storage.

In contrast, low-level Rust contracts using the raw pallet-revive FFI have no type support at all. All values (arguments, return values, storage items) must be manually serialized and deserialized. It is entirely up to you to ensure that your types remain consistent over time. You also need to design your own storage layout and perform manual byte-level manipulation.

This leads to a large surface area for subtle bugs, broken upgrades, and ABI mismatches.

Developer Ergonomics

ink! focuses on reducing boilerplate: macros generate dispatch code, storage bindings, events, constructors, metadata, error handling, and typed interfaces for cross-contract calls. You write Rust code while ink! expands the complex parts for you.

This starts with simple things like constructors

Pure Rust contracts must implement all of these things manually: You must define your own entrypoints, argument parsers, dispatch tables, error codes, and metadata. Even simple tasks such as emitting events or reading a storage value become complex and require multiple lines of low-level code.

This slows down development dramatically and increases the likelihood of bugs.

Datastructures & Storage

ink! provides high-level, contract-optimized datastructures such as Mapping, Lazy, StorageVec, and others built specifically for smart-contract workloads. They automatically handle storage layout, lazy loading, SCALE encoding, hashing, and key derivation.

These data structures expose an idiomatic Rust interface, mimicking the APIs found in Rust's std::collections.

Low-level Rust contracts provide no datastructures whatsoever. You must define your own storage layout and manually read and write byte slices to storage. Bread and butter data structures (like maps) require full custom implementations, including all serialization concerns.

Documentation & Tooling

ink! comes with comprehensive documentation:

It is supported by sophisticated tooling such as cargo-contract and pop-cli, the ink! analyzer, phink! (a fuzzer by SRLabs), type bindings for polkadot-js, PAPI, dedot, and other established libraries.

For low-level Rust on pallet-revive there is no dedicated documentation, guides, or tooling beyond the raw FFI functions. Developers must construct their own workflows for compilation, deployment, metadata definition, debugging, and verification. There is no support group for any of that, whereas for ink! there is an active Telegram channel with over 370 members and Substrate Stack Exchange.

UI Frameworks

ink! contracts can be used directly with existing Polkadot UI stacks such as polkadot-js, PAPI, dedot, Substrate Connect, or custom TypeScript frameworks that rely on SCALE metadata.

Furthermore, there are custom tools around ink!, like inkathon.xyz, which has become the go-to tool for rapid prototyping and hackathons.

Because ink! generates complete metadata, frontends can introspect message signatures, events, storage items, and types without any manual work.

Pure Rust contracts expose no metadata, so no UI framework can interact with them by default. Building a frontend requires hand-crafted encoding/decoding logic, manual ABI definitions, and custom integration layers. No automatic type generation is possible.

Testing

ink! provides an E2E testing environment, entirely in Rust. This means you can test interactions with your contract and make assertions for the resulting state, events, balances, etc. The E2E testing framework can even be used to fuzz-test your contract!

Low-level Rust contracts have no testing framework. You must implement your own simulated environment or write integration tests that call into compiled RISC-V bytecode manually. This means substantially more work and minimal safety guarantees.

Cross-contract calls

ink! offers a type-safe cross-contract calling system where Rust traits can be used to specify contract interface to either Solidity or ink! contracts. ink! automatically generates the bindings, ABI selectors, SCALE encoding logic, error types, and call wrappers based on those traits. Through that you get strong typing: if you are trying to use the wrong type during cross-contract calls, the compiler will catch it.

Pure Rust contracts must build cross-contract calls manually: encoding arguments, constructing selectors, decoding return values, and performing error handling. None of this is type-checked, and a single mismatched byte can cause the call to fail silently.

Comparison Table

ink!Pure RustSolidity
LanguageRust (safe, statically typed) with ink! macros for higher-level abstractionsRust (no macros or framework)Solidity (purpose-built for smart contracts)
Safety ModelRust guarantees (ownership, no nulls, memory safety, …)Rust safety, but high bug risk due to DIY framework (e.g. for storage interactions)Manual safety patterns; reentrancy common
Toolingcargo-contract, ink! analyzer, pop-cli, phink!, …NoneHardhat, Foundry, Truffle, Remix
UI frameworksink!athon, dedot, PAPI, Typink, polkadot-js, …NoneVery mature Web3 ecosystem: ethers.js, wagmi, web3.js
DocumentationExtensive, verbose docs on use.ink/docs, crate-level docs for all crates (ink_env, ink_storage, …)NoneWidely available
Polkadot SDK IntegrationNative support for typical polkadot-sdk types (Balance, AccountId, VersionedXcm, etc.)NoneNone
Developer ErgonomicsHigh: macros for storage, events, traits, testingVery low: everything must be implemented manuallyExtensive tooling & IDE support
Testing EnvironmentsElaborate End-to-End testing frameworkMust implement your own test harnessFoundry/Hardhat; very mature
Contract ModelFunction-based message/constructor modelNo model provided; must implement dispatch & ABIFunction-based ABI, msg.sender model
DatastructuresHigh-level datastructures, optimized for smart contracts (small footprint, lazy loading)NoneBasic mappings, arrays, structs
Storage Handlingink::storage, Mapping`, derive macros for layout and codecsManual storage layout + serializationSolidity storage keywords & slot model
ABI / MetadataSupports ink! metadata (SCALE-based) + Solidity ABIFully manual ABI definitionsNative ABI; industry standard
Cross-Contract CallsRust traits as type-safe interfaces for calling Solidity or ink! contractsManual SCALE (or other) encoding/decoding, not type-safeBuilt into EVM
Contract VerificationSupported through ink! metadata and tooling; verifiable via Docker containerNo support, must build verification tooling manuallyEtherscan-style source verification
Zero KnowledgeCan integrate with Polkadot-native ZK primitives at the Rust level (example)Possible, but fully manualGrowing zkEVM ecosystem
ProductivityHighHighest flexibility, lowest productivityHigh
Recommended Use CasePolkadot smart contractsHighly efficient low-level contracts (e.g. for isolated business logic parts)EVM-chain smart contracts, L1/L2 ecosystems