What is Ink! and Why It’s Used in Polkadot?
Ink! is a powerful smart contract language designed specifically for the Polkadot ecosystem. Ink! is the fastest way to get started in Polkadot ecosystem. It is one of the cleanest smart contract language and a joy to write in.
While Polkadot is famous as a layer-0 infrastructure layer for building layer-1 Blockchains (aka parachains), it is absolutely not needed to start there. In fact, unless there is a specific need, we recommend to understand building smart contracts for Substrate-compatible Blockchains — and ink! is the best candidate for that.
Mature toolkits exist to build, deploy, and manage Ink!-based smart contracts on Substrate-based blockchains. Instead of a brand new language, Ink! uses Rust as a foundational layer. This makes writing contracts a seamless and 'native' experience.
Ultimately, these contracts can be compiled into webAssembly (Wasm), making them performant, interoperable across different Blockchains in Polkadot, and more. These contracts can be deployed on most parachains that support smart contracts, including popular testnets and mainnets like Moonbeam, Astar, and Rococo. They can also aid to custom Blockchain development — but more on that later.
Why Ink! is Native to Polkadot
Ink! and Rust: How Are They Related?
Ink! is an Embedded Domain Specific Language (eDSL) built on top of Rust. Simply put, it extends Rust with specialized macros and annotations to enable blockchain-specific functionalities.
Macro-based Design
Ink! uses macros like #[ink::contract]
, #[ink::storage]
, and #[ink::message]
to define the core components of a smart contract. These macros tell the Ink! compiler how to process our code for blockchain use.
Domain-Specific Enhancements
Ink! provides tools for interacting with the blockchain environment, such as accessing the caller's account ID or transferring tokens.
Think of Ink! as Rust with superpowers for blockchain. It's still Rust at its core but tailored for Web3 development.
The Anatomy of an ink! Smart Contract
Before we dive deeper into ink!, let's highlight the key pieces of a minimal ink! smart contract.
For this example, we take the Flipper contract. It has a single boolean variable (value
) that can be toggled between true
and false
. It may not have all the bells and whistles, but we aim to detail the most important bits of an ink! contract.
But first, let's get the CLI that will help us with setting up and managing smart contracts written with ink!.
This enable us to quickly start up ink! templates. In a suitable folder, let's create a template ink! contract.
Consequently, this will create a flipper folder with two key files:
Cargo.toml
: This is the standard config file for Rust projects.lib.rs
: The crux of the contract.For simplicity, we will replace the content in lib.rs
with an even simpler version of the flipper contract.
Breaking It Down
If the std
feature is not enabled in the `Cargo.toml
` file, the contract switches to no_std
, which excludes Rust's standard library from the compilation process. This is a crucial step for WebAssembly (Wasm) compatibility, as Wasm's lightweight design prioritizes performance and portability.
This declares the module as an Ink! smart contract. Every contract needs exactly one of these.
Storage: #[ink(storage)]
marks the Flipper struct as the storage for this contract. Each contract must have one storage struct to persist data on-chain. While we will explore all storage options in depth in a later chapter, it's important to note that the storage struct is where the contract keeps its persistent state. Whether it's a simple value like this boolean or more complex data structures, all on-chain data resides here — ensuring it remains available between contract interactions.
This is the primary constructor that allows you to explicitly set the initial value of the flipper. It's more flexible as it lets you create a Flipper instance with either true or false as the starting value.
default()
- This is a convenience constructor that creates a Flipper instance with a default value (false
). It delegates to the new constructor by using Default::default()
which returns false for bool types in Rust.
Having multiple constructors is a common pattern in smart contracts for a few reasons:
This pattern is particularly useful in smart contract contexts where we want to give users flexibility in how they deploy and initialize contracts while still maintaining safe defaults.
Messages: #[ink(message)]
functions define how users interact with the contract.
flip
toggles the value by switching the boolean stored in the contract's state. This is a mutable operation that changes the contract's persistent storage, making it the core action of this contract.
get
retrieves the current value of the boolean. This is a read-only operation, meaning it doesn't alter the state but instead provides external users with the current status of the contract. This separation of state-changing and query functions is a fairly common and important design principle in smart contract development.
Deploying Ink! Contracts
First, we compile the contract with cargo-contract
.
This will generate the .contract
and .wasm
files in the target directory.
.contract
file includes both the contract's bytecode and metadata..wasm
, which is stored onchain when the contract is deployed..contract
file can be used to upload the contract to local nodes or parachains supporting smart contracts. More on that in coming chapters!