Search…
Creating your first contract
As your computer 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 much simpler version of the ERC20 token.
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.
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 cargo how to build the contract
  • .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 Aleph Zero blockchain. On top of the file, you can find the line importing ink! together with an additional config macro:
#![cfg_attr(not(feature = "std"), no_std)]
use ink_lang as ink;
These two lines are mandatory boilerplate, they will look the same in every ink! contract. 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.

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::{traits::SpreadAllocate, Mapping};
#[ink(storage)]
#[derive(SpreadAllocate)]
pub struct Mytoken {
total_supply: u32,
balances: Mapping<AccountId, u32>,
}
}
Here we are using a Mapping data structure provided by 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.

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:
#[ink::contract]
mod mytoken {
// ... (storage definition)
use ink_lang::utils::initialize_contract;
impl Mytoken {
#[ink(constructor)]
pub fn new_token(supply: u32) -> Self {
initialize_contract(|contract: &mut Self| {
let caller = Self::env().caller();
contract.balances.insert(&caller, &supply);
contract.total_supply = supply;
})
}
}
}
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 constructor is very similar to a regular Rust struct contructor, with one small caveat: the Mapping in the contract storage must be initialized using a special initialize_contract library function, just like in the example above. For the same reason the definition of the storage struct from the previous code snippet was prefixed with #[derive(SpreadAllocate)] marco. Further explanation of reasons behind that can be found here.

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) -> u32 {
self.total_supply
}
#[ink(message)]
pub fn balance_of(&self, account: AccountId) -> u32 {
match self.balances.get(&account) {
Some(value) => value,
None => 0,
}
}
}
}
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 last piece we need is a method for transferring tokens between accounts:
mod mytoken {
// ...
impl Mytoken {
// ...
#[ink(message)]
pub fn transfer(&mut self, recipient: AccountId, amount: u32) {
let sender = self.env().caller();
let sender_balance = self.balance_of(sender);
if sender_balance < amount {
return;
}
self.balances.insert(sender, &(sender_balance - amount));
let recipient_balance = self.balance_of(recipient);
self.balances.insert(recipient, &(recipient_balance + amount));
}
}
}
The method can be called by any user to transfer some amountof their tokens to the 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.

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 crate::mytoken::Mytoken;
use ink_env::{test, DefaultEnvironment};
use ink_lang as ink;
#[ink::test]
fn total_supply_works() {
let mytoken = Mytoken::new_token(1000);
assert_eq!(mytoken.total_supply(), 1000);
}
#[ink::test]
fn balance_of_works() {
let accounts = test::default_accounts::<DefaultEnvironment>();
test::set_caller::<DefaultEnvironment>(accounts.alice);
let mytoken = Mytoken::new_token(1000);
assert_eq!(mytoken.balance_of(accounts.alice), 1000);
assert_eq!(mytoken.balance_of(accounts.bob), 0);
}
#[ink::test]
fn transfer_works() {
let accounts = test::default_accounts::<DefaultEnvironment>();
test::set_caller::<DefaultEnvironment>(accounts.alice);
let mut mytoken = Mytoken::new_token(1000);
assert_eq!(mytoken.balance_of(accounts.alice), 1000);
assert_eq!(mytoken.balance_of(accounts.bob), 0);
mytoken.transfer(accounts.bob, 100);
assert_eq!(mytoken.balance_of(accounts.alice), 900);
assert_eq!(mytoken.balance_of(accounts.bob), 100);
}
}
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

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. This information will be visible to the users interacting with our contract.
mytoken/lib.rs
#![cfg_attr(not(feature = "std"), no_std)]
use ink_lang as ink;
#[ink::contract]
mod mytoken {
use ink_storage::{traits::SpreadAllocate, Mapping};
#[ink(storage)]
#[derive(SpreadAllocate)]
pub struct Mytoken {
total_supply: u32,
balances: Mapping<AccountId, u32>,
}
use ink_lang::utils::initialize_contract;
impl Mytoken {
/// Creates a token contract with the given initial supply belonging to the contract creator.
#[ink(constructor)]
pub fn new_token(supply: u32) -> Self {
initialize_contract(|contract: &mut Self| {
let caller = Self::env().caller();
contract.balances.insert(&caller, &supply);
contract.total_supply = supply;
})
}
/// Total supply of the token.
#[ink(message)]
pub fn total_supply(&self) -> u32 {
self.total_supply
}
/// Current balance of the chosen account.
#[ink(message)]
pub fn balance_of(&self, account: AccountId) -> u32 {
match self.balances.get(&account) {
Some(value) => value,
None => 0,
}
}
/// Transfers an amount of tokens to the chosen recipient.
#[ink(message)]
pub fn transfer(&mut self, recipient: AccountId, amount: u32) {
let sender = self.env().caller();
let sender_balance = self.balance_of(sender);
if sender_balance < amount {
return;
}
self.balances.insert(sender, &(sender_balance - amount));
let recipient_balance = self.balance_of(recipient);
self.balances.insert(recipient, &(recipient_balance + amount));
}
}
}
#[cfg(test)]
mod tests {
use crate::mytoken::Mytoken;
use ink_env::{test, DefaultEnvironment};
use ink_lang as ink;
#[ink::test]
fn total_supply_works() {
let mytoken = Mytoken::new_token(1000);
assert_eq!(mytoken.total_supply(), 1000);
}
#[ink::test]
fn balance_of_works() {
let accounts = test::default_accounts::<DefaultEnvironment>();
test::set_caller::<DefaultEnvironment>(accounts.alice);
let mytoken = Mytoken::new_token(1000);
assert_eq!(mytoken.balance_of(accounts.alice), 1000);
assert_eq!(mytoken.balance_of(accounts.bob), 0);
}
#[ink::test]
fn transfer_works() {
let accounts = test::default_accounts::<DefaultEnvironment>();
test::set_caller::<DefaultEnvironment>(accounts.alice);
let mut mytoken = Mytoken::new_token(1000);
assert_eq!(mytoken.balance_of(accounts.alice), 1000);
assert_eq!(mytoken.balance_of(accounts.bob), 0);
mytoken.transfer(accounts.bob, 100);
assert_eq!(mytoken.balance_of(accounts.alice), 900);
assert_eq!(mytoken.balance_of(accounts.bob), 100);
}
}

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 Aleph Zero Testnet!
Copy link
Outline
Implementation
Compiling