Skip to content

Wasm (ink!)

Introduction

The pallet_contracts is a specialized pallet within the Polkadot SDK that enables smart contract functionality through a WebAssembly (Wasm) execution environment. For developing smart contracts for this pallet, ink! emerges as the primary and recommended language.

ink! is an embedded domain-specific language (eDSL) designed to develop Wasm smart contracts using the Rust programming language.

Rather than creating a new language, ink! is just standard Rust in a well-defined "contract format" with specialized #[ink(…)] attribute macros. These attribute macros tell ink! what the different parts of your Rust smart contract represent and ultimately allow ink! to do all the magic needed to create Polkadot SDK-compatible Wasm bytecode. Because of this, it inherits critical advantages such as:

  • Strong memory safety guarantees
  • Advanced type system
  • Comprehensive development tooling
  • Support from Rust's extensive developer community

Since ink! smart contracts are compiled to Wasm, they offer high execution speed, platform independence, and enhanced security through sandboxed execution.

Installation

ink! smart contract development requires the installation of cargo-contract, a command-line interface (CLI) tool that provides essential utilities for creating, testing, and managing ink! projects.

For step-by-step installation instructions, including platform-specific requirements and troubleshooting tips, refer to the official cargo-contract Installation guide.

Get Started

To create a new ink! smart contract project, use the cargo contract command:

cargo contract new INSERT_PROJECT_NAME

This command generates a new project directory with the following structure:

INSERT_PROJECT_NAME/
├── lib.rs          # Contract source code
├── Cargo.toml      # Project configuration and dependencies
└── .gitignore      # Git ignore rules

The lib.rs file includes a basic contract template with storage and message-handling functionality. Customize this file to implement your contract’s logic. The Cargo.toml file defines project dependencies, including the necessary ink! libraries and configuration settings.

Contract Structure

An ink! smart contract requires three fundamental components:

  • A storage struct marked with #[ink(storage)]
  • At least one constructor function marked with #[ink(constructor)]
  • At least one message function marked with #[ink(message)]

Default Template Structure

The following example shows the basic contract structure generated by running cargo contract new:

#![cfg_attr(not(feature = "std"), no_std, no_main)]

#[ink::contract]
mod flipper {

    /// Defines the storage of your contract.
    /// Add new fields to the below struct in order
    /// to add new static storage fields to your contract.
    #[ink(storage)]
    pub struct Flipper {
        /// Stores a single `bool` value on the storage.
        value: bool,
    }

    impl Flipper {
        /// Constructor that initializes the `bool` value to the given `init_value`.
        #[ink(constructor)]
        pub fn new(init_value: bool) -> Self {
            Self { value: init_value }
        }

        /// Constructor that initializes the `bool` value to `false`.
        ///
        /// Constructors can delegate to other constructors.
        #[ink(constructor)]
        pub fn default() -> Self {
            Self::new(Default::default())
        }

        /// A message that can be called on instantiated contracts.
        /// This one flips the value of the stored `bool` from `true`
        /// to `false` and vice versa.
        #[ink(message)]
        pub fn flip(&mut self) {
            self.value = !self.value;
        }

        /// Simply returns the current value of our `bool`.
        #[ink(message)]
        pub fn get(&self) -> bool {
            self.value
        }
    }
}

Storage

In an ink! contract, persistent storage is defined by a single struct annotated with the #[ink(storage)] attribute. This struct represents the contract's state and can use various data types for storing information, such as:

  • Common data types:

    • Boolean values (bool)
    • Unsigned integers (u8, u16, u32, u64, u128)
    • Signed integers (i8, i16, i32, i64, i128)
    • Tuples and arrays
  • Substrate-specific types:

    • AccountId - contract and user addresses
    • Balance - token amounts
    • Hash - cryptographic hashes
  • Data structures:

    • Struct - custom data structures
    • Vec - dynamic arrays
    • Mapping - key-value storage
    • BTreeMap- ordered maps
    • HashMap - unordered maps

Example of a storage struct using various supported types:

#[ink(storage)]
pub struct Data {
    /// A boolean flag to indicate a certain condition
    flag: bool,
    /// A vector to store multiple entries of unsigned 32-bit integers
    entries: Vec<u32>,
    /// An optional value that can store a specific integer or none
    optional_value: Option<i32>,
    /// A map to associate keys (as AccountId) with values (as unsigned 64-bit integers)
    key_value_store: Mapping<AccountId, u64>,
    /// A counter to keep track of some numerical value
    counter: u64,
}

For an in-depth explanation of storage and data structures in ink!, refer to the Storage & Data Structures section and the #[ink(storage)] macro definition in the official documentation.

Constructors

Constructors are functions that execute once when deploying the contract and are used to initialize the contract’s state. Each contract must have at least one constructor, though multiple constructors can provide different initialization options.

Example:

#[ink::contract]
mod mycontract {

    #[ink(storage)]
    pub struct MyContract {
        number: u32,
    }

    impl MyContract {
        /// Constructor that initializes the `u32` value to the given `init_value`.
        #[ink(constructor)]
        pub fn new(init_value: u32) -> Self {
            Self {
                number: init_value,
            }
        }

        /// Constructor that initializes the `u32` value to the `u32` default.
        #[ink(constructor)]
        pub fn default() -> Self {
            Self {
                number: Default::default(),
            }
        }
    }

    /* ... */
}

Note

In this example, new(init_value: u32) initializes number with a specified value, while default() initializes it with the type’s default value (0 for u32). These constructors provide flexibility in contract deployment by supporting custom and default initialization options.

For more information, refer to the official documentation for the #[ink(constructor)] macro definition.

Messages

Messages are functions that interact with the contract, allowing users or other contracts to call specific methods. Each contract must define at least one message.

There are two types of messages:

  • Immutable messages (&self) - these messages can only read the contract's state and cannot modify it
  • Mutable messages (&mut self) - these messages can read and modify the contract's state

Note

&self is a reference to the contract's storage.

Example:

#[ink(message)]
pub fn my_getter(&self) -> u32 {
    self.my_number
}

#[ink(message)]
pub fn my_setter(&mut self, new_value: u32) -> u32 {
    self.my_number = new_value;
}

Note

In the example above, my_getter is an immutable message that reads state, while my_setter is a mutable message that updates state.

For more information, refer to the official documentation on the #[ink(message)] macro.

Errors

For defining errors, ink! uses idiomatic Rust error handling with the Result<T,E> type. These errors are user-defined by creating an Error enum and all the necessary types. If an error is returned, the contract reverts

In ink!, errors are handled using idiomatic Rust practices with the Result<T, E> type. Custom error types are defined by creating an Error enum and specifying any necessary variants. If a message returns an error, the contract execution reverts, ensuring no changes are applied to the contract's state.

Example:

[derive(Debug, PartialEq, Eq)]
#[ink::scale_derive(Encode, Decode, TypeInfo)]
pub enum Error {
    /// Returned if not enough balance to fulfill a request is available.
    InsufficientBalance,
    /// Returned if not enough allowance to fulfill a request is available.
    InsufficientAllowance,
}

impl Erc20 {
    //...
    #[ink(message)]
    pub fn transfer_from(
        &mut self,
        from: AccountId,
        to: AccountId,
        value: Balance,
    ) -> Result<(),Error> {
        let caller = self.env().caller();
        let allowance = self.allowance_impl(&from, &caller);
        if allowance < value {
            return Err(Error::InsufficientAllowance)
        }
        //...
    }
    //...
}

Note

In this example, the Error enum defines custom error types InsufficientBalance and InsufficientAllowance. When transfer_from is called, it checks if the allowance is sufficient. If not, it returns an InsufficientAllowance error, causing the contract to revert. This approach ensures robust error handling for smart contracts.

Events

Events are a way of letting the outside world know about what's happening inside the contract. They are user-defined in a struct and decorated with the #[ink(event)] macro.

Events allow the contract to communicate important occurrences to the outside world. They are user-defined by creating a struct and annotating it with the #[ink(event)] macro. Each field you want to index for efficient querying should be marked with #[ink(topic)].

Example:

/// Event emitted when a token transfer occurs.
#[ink(event)]
pub struct Transfer {
    #[ink(topic)]
    from: Option<AccountId>,
    #[ink(topic)]
    to: Option<AccountId>,
    value: Balance,
}

impl Erc20 {
    //...
    #[ink(message)]
    pub fn transfer_from(
        &mut self,
        from: AccountId,
        to: AccountId,
        value: Balance,
    ) -> Result<(),Error> {
        //...
        self.env().emit_event(Transfer {
            from: Some(from),
            to: Some(to),
            value,
        });

        Ok(())
    }
}

Note

In this example, the Transfer event records the sender (from), the receiver (to), and the amount transferred (value). The event is emitted in the transfer_from function to notify external listeners whenever a transfer occurs.

For more details, check the Events section and the #[ink(event)] macro documentation.

Where to Go Next?

To deepen your knowledge of ink! development, whether you're exploring foundational concepts or advanced implementations, the following resources provide essential guidance:

  • Official ink! documentation — a thorough resource with guides, in-depth explanations, and technical references to support you in mastering ink! development

  • ink-examples repository — a curated collection of smart contract examples that demonstrate best practices and commonly used design patterns

Last update: November 21, 2024
| Created: August 27, 2024