Migrating from Solidity

The similarities and differences between Ink! and Solidity

We encourage the Readers to first follow our Aleph Zero smart contracts basics tutorial to get a general understanding of Ink!.

It is worth pointing out in the beginning that there are many more similarities than differences between Ink! and Solidity. Both are imperative programming languages that allow you to write contracts similarly: as a module that defines a set of methods ('messages') that return some information about the contract and/or modify the contract's state and both run in environments that utilize the concept of gas (a fee for running program instructions that prevents the contract from running indefinitely).

Of course, the syntax is slightly different and Ink's type system may take some getting used to, but any Solidity developer should be able to jump right in and start developing contracts in the Aleph Zero ecosystem. Almost all of the concepts have a one-to-one correspondence between Ink! and Solidity, so converting the contracts should be a breeze. You can also try automatic translation using the Sol2Ink tool, which can produce satisfactory results for simpler contracts.

If you use one of the automatic translation tools, make sure to inspect the resulting code very carefully!

Migrating a simple contract

Let's take a look at a classic 'Hello world' example of a smart contract, the Flipper: it is the most basic contract imaginable and its sole purpose is to hold a boolean value in the state and expose a message that allows callers to 'flip' it. Here is how it could look in Solidity:

contract MyContract {
    bool private _theBool;
    event UpdatedBool(bool indexed _theBool);

    constructor(bool theBool_) {
        require(theBool_ == true, "theBool_ must start as true");

        _theBool = theBool_;
    }

    function setBool(bool newBool) public returns (bool boolChanged) {
        if _theBool == newBool {
               boolChanged = false;
        } else {
            boolChanged = true;
        }

        _theBool = newBool;
        // emit an event
        UpdatedBool(newBool);
    }
}

Now, the funny thing is that if you want to make this basic contract in Ink!, you only need to type one command:

cargo contract new flipper

cargo contract is a tool for managing and building Ink! smart contracts and its new command will scaffold a basic flipper. To build the contract you can run:

cargo contract build --release

And just like that, you will have the target/release/ink/flipper.contract file ready to be deployed.

That said, let's dive a little deeper and convert the contract manually to better understand the process.

File layout

Solidity keeps the storage and logic of the contract inside of the same class. In Ink! you group all of that using a module. Inside the module you:

  • define your storage struct

  • define the implementation of messages/methods

  • define any additional structs and traits

As such, the layout of the file will look like this:

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

use ink_lang as ink;

#[ink::contract]
mod flipper {
    #[ink(storage)]
    pub struct Flipper {
        // storage elements go here
    }

    #[ink(event)]
    pub struct SomeEvent {
        // event fields
    }
    
    // more definitions: additional structs, errors, events, trait implementations

    impl Flipper { // name needs to match the name of the storage struct
        #[ink(constructor)]
        pub fn new( /* constructor arguments */ ) -> Self {
            // constructor implementation
        }
        
        // additional messages/methods: getters, setters, etc.
    }
}

This layout will be pretty much constant across all of the contracts you write. Note that you almost never need to type all of the above yourself: cargo contract new takes care of all the scaffolding. Note the #[ink::contract] above module declaration: it enforces certain invariants (that we mention below) and defines some handy aliases.

Storage struct

Ink! defines the storage struct the Rust way: as a separate entity inside the module. The data-logic separation has a few useful implications that are beyond the scope of this tutorial: it suffices to say that this idea is battle-tested in the Rust world. To define your storage, you will write the following code:

#[ink(storage)]
pub struct Flipper {
    the_bool: bool,
}

The #[ink(storage)] macro tells the compiler that this is indeed the contract's storage. The contract needs to have exactly one such struct: don't worry, the compiler will instruct you if you forget to define it or define more than one.

Of course, you are able to define as many structs as you want but only one of them can be marked with #[ink(storage)].

As an aside, you need to be quite conservative with putting elements in the contract's storage. Smart contracts on the Aleph Zero blockchain are exceptionally cheap but the storage fees are still something to consider during your development. For example, you may want to store your images off-chain and only store hashes on the blockchain.

Method implementations

Instead of putting the logic in the class/struct body, Rust (and therefore: Ink!) chooses to have a dedicated impl block that contains all of the methods (including the constructors).

impl Flipper { /* method definitions */ }

The name needs to match the name of your contract's storage struct. Inside, you can put three types of methods:

  • methods marked as #[ink(message)] which will be what your contract's users can call;

  • methods marked as #[ink(constructor)] which are the constructors of your contract;

  • regular, unmarked methods that won't be callable as part of your contract's interface but can be used as helpers by the other methods.

Constructor(s)

Solidity uses a dedicated constructor keyword, and a contract is allowed to have only one. In contrast, Ink! allows you to define any non-zero number of constructors. Each constructor is a public method with the #[ink(constructor)] macro which returns an instance of the contract's storage struct:

#[ink(constructor)]
pub fn new(initial_state: bool) -> Self {
    Self {
        the_bool: initial_state,
    }
}

Note that while Solidity constructors work 'in-place': you assign the values to the storage elements. In Ink! you need to explicitly create and return the storage struct.

This stems from the fact that the constructor is generally a method like any other: it needs the macro but doesn't even need to have a special name. Of course, in order to make the interface understandable to your user, it's best to opt for methods called 'new', 'initialize', 'create', etc.

You can do any required initialization inside of the constructor. A common operation is initializing a mapping, if your contract has one in its storage. In that case, you will use the Default::default() method to initialize the mapping and then put it as one of the fields in the returned struct.

Instead of using Self, you can just use the name of your storage struct (in our case: Flipper). The Rust community prefers the former for its more generic semantics.

Messages/methods

Here we will see some syntactic differences between Ink! and Solidity:

  • in Solidity, you declare the messages as public methods on your contract class;

  • Ink! puts the methods in the impl block but you still need to mark them as pub;

  • in Solidity, you have two ways of returning a value from a function:

    • declare the return variable (returns(bool someval)) and assign to it: a concept familiar to those of us who remember the golden days of Pascal;

    • use the return keyword to immediately exit from a function, returning a value;

  • Ink! also has two ways of returning values, arguably more functional in nature:

    • the return keyword;

    • implicitly returning the last expression in a function (this one being the 'purer' variant and preferred in the Rust/Ink! community);

  • Ink! messages need the #[ink(message)] macro to be callable by your contract's users;

  • Solidity relies heavily on asserts, requires and similar exception-based special functions to handle failure.

  • Ink! does expose assert!(...) as a macro but the preferred way of handling errors is much more powerful: using the Result return type (more on that below).

  • In Ink!, you need to explicitly state whether a function will be allowed to mutate the state of the contract. If it needs to mutate the storage elements, it needs to have &mut self as the first argument. Otherwise, it will have &self (messages in Ink! need to have self passed as a reference, so in either case, the ampersand will be there).

Here is a basic form of our set_bool message/method:

#[ink(message)]
pub fn set_bool(&mut self, new_bool: bool) -> bool {
    let bool_changed = true;

    if self.the_bool == new_bool{
        bool_changed = false;
    } else {
        bool_changed = true;
    }

    self.the_bool = new_bool;

    // implicit return
    bool_changed
}

Note that for the implicit return to work, you must not include a semicolon on the last line.

Similarly to Solidity, you can make your methods payable: you will need to change the macro to #[ink(message, payable)].

Events

Your contracts can emit events just like in Solidity (although when using multiple contracts you will need to use a simple workaround, as described here). You start by declaring your event struct and marking it with a macro (you may be starting to see a pattern here):

#[ink(event)]
pub struct UpdatedBool {
    #[ink(topic)]
    the_bool: bool,
}

Use the #[ink(topic)] macro to mark the fields of your choice indexable (this is the same as in Solidity).

To emit the event, you can use the following call:

self.env().emit_event(UpdatedBool {
    the_bool: new_bool
});

It creates the event struct and immediately passes it to the emit_event function. self.env() is a method that gives you access to several useful things, including the caller's address (self.env().caller()) and the account of the contract itself (self.env().account_id()). The Solidity counterparts of these calls are msg.sender and address(this), respectively.

Error handling

As previously mentioned, Ink! developers do not rely on throwing exceptions or stopping the execution with require macros. What we do instead is use the Result type, which can be one of two things: Ok(somevalue) when the computation was successful, or Err(someerror) when it wasn't. It is defined roughly as:

pub enum Result<T, E> {
    Ok(val: T),
    Err(msg: E),
}

With T and E being generic types for the actual value and the error type, respectively. The most common usage is to define an error type like:

pub enum MyError {
    NegativeNumber,
    WrongId(id: u32),
    BadCaller(caller: Vec<u8>),
    // etc...
}

Then you can define your functions' return types as:

pub fn some_function(x: i32) -> Result<u32, MyError> {
    if x < 0 {
        Err(MyError::NegativeNumber)
    } else {
        Ok((x + 1) as u32)
    }
}

Please don't mind the contrived nature of this example: it is only there to illustrate a common pattern when dealing with errors.

Now, as a caller of this function, you can choose to do two things:

  • explicitly examine the result and decide on what actions to take:

match some_function(12) {
    Ok(v) => yay(),       // the computation succeeded
    Err(e) => sad_pepe(), // there was an error
}
  • use one of the helper methods to, for example, fail immediately if the result was not Ok:

some_function(-1).unwrap()

Further reading

We have only scratched the surface here but hopefully, this guide has shed some light on the process of migrating contracts from Solidity and shown that it's easier than it might seem.

For a very detailed guide, please see this chapter of Ink's documentation.

Last updated