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
:
This command will create a new directory mytoken
with the following files inside:
lib.rs
- a Rust source file containing your contract's codeCargo.toml
- a manifest file explaining tocargo
how to build the contract (in this tutorial we won't need to modify it).gitignore
- in case you decide to usegit
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 will need to include a config macro:
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:
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:
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 Aleph Zero 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.
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:
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:
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
:
If you are not familiar with Rust's enum
s, 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:
The method can be called by any user to transfer some amount
of 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 enum
s, 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:
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:
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.
Compiling
Now it's time to build our contract:
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 contractmetadata.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!
Last updated