Creating your first contract

As now your machine is ready for development, it's time we build our first smart contract. The example contract we are going to develop in this tutorial is a simplified version of the ERC20 token.

Boxes marked like this contain some basic information about the Rust programming language. If you are familiar with Rust, please feel free to skip those.

Boxes marked like this contain general remarks about smart contract development in ink!

While not strictly necessary for completing this tutorial, they may prove useful down the line.

Introduction

With all the important tools in place, you are now ready to develop your first ink! smart contract!

The example contract we are going to build in this tutorial is a much simpler version of ERC20 token. Our contract, when instantiated, will create a pool with a new type of fungible token that can be transferred between accounts. The contract will hold a registry of accounts with their balances and provide methods to query balances and transfer tokens.

Note that the token we are creating has nothing to do with the chain's native currency! To that end, the chain's internal mechanisms won't ensure the correctness of transactions: it's all on you, the Creator of the contract, to make sure that the logic makes sense.

Contract creation

Let's start with generating a contract template with cargo contract:

cargo contract new mytoken
cd  mytoken

This command will create a new directory mytoken with the following files inside:

  • lib.rs - a Rust source file containing your contract's code

  • Cargo.toml - a manifest file explaining to cargo how to build the contract (in this tutorial we won't need to modify it)

  • .gitignore - in case you decide to use git to version control your contract

We are going to be working only with lib.rs, the remaining two files can be left as they are. If you look inside lib.rs you're going to find the simplest hello-world contract - a flipper, which holds a single boolean value and allows flipping it. You are encouraged to take a look at the code, but don't worry if you find some parts mysterious. We're going to modify the code step by step and explain everything along the way.

Implementation

A smart contract written in ink! is in fact just a regular Rust code that makes use of ink! macros (lines that look like #[ink...]). The role of these macros is to modify the compilation process to produce, instead of a normal program that can be run on your computer, a WASM smart contract that can be deployed to the Phron blockchain. On top of the file, you will need to include a config macro:

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

This scary looking macro basically instructs the compiler to not include the standard library (ink! will use its own set of primitives suited for smart contract development). It also allows to disable emitting the main symbol. The rest of the file contains a definition of a module, prefixed with the main ink! macro:

#[ink::contract]
mod mytoken {
    //...
}

This macro tells ink! that module mytoken is actually a definition of a smart contract and that ink! should look inside that module for various components of a contract. It also gives you some handy type aliases like Balance and Account. Additionally, it enforces some invariants that we don't really need to worry about now:

  • a contract needs to have exactly one struct marked as #[ink::storage]

  • a contract needs to have at least one function marked as #[ink::constructor]

Rust modules can be thought of as collections of types and functions, grouping them in a single large scope.

Storage

The first component is the contract storage. It contains data that is stored on the blockchain and holds the state of the contract. In our case, this is going to be a mapping between users and the number of tokens they own, together with a single number holding the total supply or our new token. That data needs to be enclosed in a single Rust struct that is prefixed with the corresponding ink! storage macro:

#[ink::contract]
mod mytoken {
    use ink::storage::Mapping;

    #[ink(storage)]
    #[derive(Default)]
    pub struct Mytoken {
        total_supply: Balance,
        balances: Mapping<AccountId, Balance>,
    }
}

Here we are declaring a Rust struct that will be instantiated as follows:

let my_token = Mytoken {
    total_supply: somevalue1,
    balances: somevalue2,
}

And its fields will be accessed like my_token.total_supply.

There exists a handy shortcut where instead of writing: Mytoken { total_supply: total_supply, balances: balances } you can write Mytoken { total_supply, balances } (assuming total_supply and balances are variables that exist in your code).

Here we are using a Mapping data structure provided by the ink::storage crate. Please note that when writing ink! smart contracts you cannot use data structures from the Rust standard library. Fortunately, ink! provides a handy replacement for that in a form of key-value map optimized for being stored on-chain.

You need to be quite conservative when choosing what to store in this struct, as it will incur some fees (in case of Phron these are very small but it's still worth being aware what you allocate). For example, storing a mapping between addresses and balances is fine. However, if your contract handles images, you will probably want to store these off-chain and only commit hashes to the contract's storage.

Note that we also instruct ink! to create an implementation of the Default trait for us. In our case, it will allow the compiler to initialize the total_supply field to 0 (the Default value for a Balance) and the balances to an empty Mapping (which incidentally is the corresponding Default implementation).

Constructor

The next step is implementing a constructor of our contract. It needs to be placed inside an impl block for our newly defined struct Mytoken and again prefixed with the right ink! macro:

A contract can have an arbitrary non-zero number of constructors, as long as each of them is marked with the #[ink(constructor)] macro.

#[ink::contract]
mod mytoken {   
    // ... (storage definition)

    impl Mytoken {
        #[ink(constructor)]
        pub fn new(total_supply: Balance) -> Self {
            let mut balances = Mapping::default();
            let caller = Self::env().caller();
            balances.insert(caller, &total_supply);
            Self {
                total_supply,
                balances,
            }
        }
    }
}

Our constructor takes a single argument: the initial supply of our newly created token, and deposits all that supply to the account of the contract creator (the account which calls the constructor).

The contract's constructor is very similar to a regular Rust struct contructor. Note that we are first creating an empty Mapping by invoking the Default::default() function and only then inserting the first entry, assigning all of the supply to the caller of the constructor.

We can use the Self::env().caller() method to access the address of an account that called our contract. In context of the constructor this will be the contract's creator/owner.

The impl block will contain the methods operating on a struct of the same name (in our case: Mytoken). In some languages you'd write those methods in the class/struct body. Rust chooses to separate the definition and implementation for some added flexibility it provides.

Additionally, note that in Rust we write single line comments as a double-slash (//).

Messages

Just like our contract constructor defined above is in fact a regular Rust constructor prefixed with an ink! macro, the callable methods of our contract (called messages by ink!) are normal Rust methods annotated with another ink! macro:

#[ink::contract]
mod mytoken {  
    // ... (storage definition)
    
    impl Mytoken { 
        // ... (constructor definition)
    
        #[ink(message)]
        pub fn total_supply(&self) -> Balance {
            self.total_supply
        }
        
        #[ink(message)]
        pub fn balance_of(&self, account: AccountId) -> Balance {
            self.balances.get(&account).unwrap_or_default()
        }
    }
}

The methods marked by #[ink(message)] need to be declared as public (pub fn).

Here we defined two methods for accessing the storage of our contract: reading the total supply and the number of tokens held by a particular account. These methods are read-only, they don't modify the contract storage and can be called without submitting a transaction to the blockchain.

The balance_of method first retrieves a value from the balances mapping for a given account. The result comes wrapped in an Option struct: we use the unwrap_or_default() method to either retrieve the actual value or a default for the Balance type (which conveniently is 0).

We use the self keyword to access the instance of the struct a given method is called on (some languages choose to use the this keyword with the same semantics).

Functions in Rust are declared using the fn keyword, followed by the function's name, a list of its arguments along with their types (arg: Type) and the return type after an arrow. Note that you don't have to use the return keyword if you simply want to return the last line.

Nota bene: the code becomes arguably more elegant if you don't overuse the short-circuiting return statement.

The pub keyword marks a given function as public, i.e. accessible from outside the module where it's declared.

Errors

Before we look at the transfer function, we need to learn Rust's idiomatic way of handling errors. In contrast to some languages, Rust chooses to forgo the notion of exception in favor of algebraic error handling. Each method that can potentially fail will have a Result<T, E> type, which is defined as the following:

pub enum Result<T, E> {
  Ok(val: T),    // T is the expected type of the computation
  Err(msg: E),   // E is an error type of your choice
}

If you're not familiar with Rust's generic types, here we can say that Result is a type with type parameters T and E (which are basically placeholders for types). Later in your code you can instantiate the Result with any types as T and E, for example: Result<u128, str>: in this case the val will be an unsigned, 128-bit integer and the msg will be a string. Note that for each instantiation, you will be required to stick to the choice of T and E. However, in any place you are creating an instance of this type, you can choose any types you please (it should be clear once we see the usage examples).

In our contract, we will define a custom Error struct that we'll use as the Err part of the Result:

#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Error {
    InsufficientBalance,
}

In this introductory tutorial, please don't worry about the scary macros: we will describe them in detail in later tutorial. For now, it's enough to copy them over 😊 In the transfer implementation below you'll see an example of using this method in practice.

If you are not familiar with Rust's enums, here we are defining an Error type with just one variant (or constructor) that takes no arguments: InsufficientBalance. When instantiating this error, you will write Error::InsufficientBalance and its type will be Error (e.g. if you need to specify that in a type signature of a function).

The last piece we need is a method for transferring tokens between accounts:

mod mytoken {  
    // ...
    
    impl Mytoken { 
        // ... constructor definition
        // ... error definition
        
        #[ink(message)]
        pub fn transfer(&mut self, to: AccountId, value: Balance) -> Result<(), Error> {
            let from = self.env().caller();
            let from_balance = self.balance_of(from);
            if from_balance < value {
                return Err(Error::InsufficientBalance);
            }

            self.balances.insert(from, &(from_balance - value));
            let to_balance = self.balance_of(to);
            self.balances.insert(to, &(to_balance + value));
            Ok(())
        }
    }
}

The method can be called by any user to transfer some amountof their tokens to their chosen recipient. If the user tries to transfer more tokens than they own, the method exits without performing any changes. Note that inside transfer method we make use of balance_of method we previously defined.

The most important difference between this and our previous methods is the fact that transfer modifies the contract storage. This fact needs to be indicated by using &mut self instead of &self as the first argument. This requirement is enforced by the compiler - if you happen to forget mut, your contract simply won't build and the compiler will give you a suggestion to use mut. So no need to worry about deploying a buggy contract.

To build on our previous explanation of enums, note that we are declaring the return type to be Result<(), Error>.

Our Ok variant contains a (), which is roughly the equivalent of void in C(++) and 'no value returned' in plain English.

The Err variant needs to have our previously-defined Error type inside, so when we return it, we instantiate it as Err(Error::InsufficientBalance).

For the purpose of this tutorial, we can assume that the references (&) work very similarly to languages like C++: instead of copying the value or giving it away, you're only 'lending it out' to some other function. As you will later see, the lending/borrowing intuition is actually very well formalised in the Rust language.

To denote that the borrower may make some changes to the value it temporarily acquires, we need to mark the reference as &mut.

Fortunately, as mentioned above, Rust's compiler offers very helpful tips on how to resolve issues in this area: it is likely they will be enough to create a contract that correctly compiles. If you want to read more on this topic, please consult The Rust Book.

Tests

Like every other program, our smart contract should be tested. This part of the development process is also very similar to how it's done in regular Rust. The tests are performed off-chain and ink! provides a handful of useful tools that help to simulate the on-chain environment in which our contract will live in future.

Here we demonstrate a very minimal test suite with a basic sanity check of each implemented method. The following code should be placed in the same lib.rs file as the contract:

#[cfg(test)]
mod tests {
    use super::*;

    #[ink::test]
    fn total_supply_works() {
        let mytoken = Mytoken::new(100);
        assert_eq!(mytoken.total_supply(), 100);
    }

    #[ink::test]
    fn balance_of_works() {
        let mytoken = Mytoken::new(100);
        let accounts =
            ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
        assert_eq!(mytoken.balance_of(accounts.alice), 100);
        assert_eq!(mytoken.balance_of(accounts.bob), 0);
    }

    #[ink::test]
    fn transfer_works() {
        let mut mytoken = Mytoken::new(100);
        let accounts =
            ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();

        assert_eq!(mytoken.balance_of(accounts.bob), 0);
        assert_eq!(mytoken.transfer(accounts.bob, 10), Ok(()));
        assert_eq!(mytoken.balance_of(accounts.bob), 10);
    }
}

We will not go into details of this code as it should be pretty self-explanatory after implementing the contract above.

The test suite can be run by invoking cargo test in the terminal while inside the mytoken folder:

cargo test
   Compiling mytoken v0.1.0 (/home/user/ink/mytoken)
    Finished test [unoptimized + debuginfo] target(s) in 1.08s
     Running unittests lib.rs (target/debug/deps/mytoken-668aad4b5e4b8a01)

running 3 tests
test tests::balance_of_works ... ok
test tests::total_supply_works ... ok
test tests::transfer_works ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

The 4.0 version of ink! introduced end-to-end tests to the smart contract development flow: we are going to cover that in a later tutorial as a recommended additional way of ensuring your contract's correctness.

Summary

Finally, let's combine all the pieces of our contract into the final version. We can also add some doc comments (///...) to describe what these pieces do: here they are omitted for the sake of brevity but it is recommended to include them. This information will be visible to the users interacting with our contract through the Contracts UI.

mytoken/lib.rsCopy

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

#[ink::contract]
mod mytoken {
    use ink::storage::Mapping;

    #[ink(storage)]
    #[derive(Default)]
    pub struct Mytoken {
        total_supply: Balance,
        balances: Mapping<AccountId, Balance>,
    }

    #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
    #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
    pub enum Error {
        InsufficientBalance,
    }

    impl Mytoken {
        #[ink(constructor)]
        pub fn new(total_supply: Balance) -> Self {
            let mut balances = Mapping::default();
            let caller = Self::env().caller();
            balances.insert(caller, &total_supply);
            Self {
                total_supply,
                balances,
            }
        }

        #[ink(message)]
        pub fn total_supply(&self) -> Balance {
            self.total_supply
        }

        #[ink(message)]
        pub fn balance_of(&self, owner: AccountId) -> Balance {
            self.balances.get(owner).unwrap_or_default()
        }

        #[ink(message)]
        pub fn transfer(&mut self, to: AccountId, value: Balance) -> Result<(), Error> {
            let from = self.env().caller();
            let from_balance = self.balance_of(from);
            if from_balance < value {
                return Err(Error::InsufficientBalance);
            }

            self.balances.insert(from, &(from_balance - value));
            let to_balance = self.balance_of(to);
            self.balances.insert(to, &(to_balance + value));
            Ok(())
        }
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        #[ink::test]
        fn total_supply_works() {
            let mytoken = Mytoken::new(100);
            assert_eq!(mytoken.total_supply(), 100);
        }

        #[ink::test]
        fn balance_of_works() {
            let mytoken = Mytoken::new(100);
            let accounts =
                ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
            assert_eq!(mytoken.balance_of(accounts.alice), 100);
            assert_eq!(mytoken.balance_of(accounts.bob), 0);
        }

        #[ink::test]
        fn transfer_works() {
            let mut mytoken = Mytoken::new(100);
            let accounts =
                ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();

            assert_eq!(mytoken.balance_of(accounts.bob), 0);
            assert_eq!(mytoken.transfer(accounts.bob, 10), Ok(()));
            assert_eq!(mytoken.balance_of(accounts.bob), 10);
        }
    }
}

Compiling

Now it's time to build our contract:

cargo +nightly contract build --release

The resulting files will be placed in mytoken/target/ink/ folder. If the compilation is successful you will find there the following 3 files:

  • mytoken.wasm is a binary WASM file with the compiled contract

  • metadata.json containing our contracts ABI (Application Binary Interface)

  • mytoken.contract which bundles the above two for more convenient interaction with the chain explorer

We are now ready to deploy our mytoken contract to Phron Testnet!

Last updated