Migrating from Solidity
The similarities and differences between Ink! and Solidity
Last updated
The similarities and differences between Ink! and Solidity
Last updated
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!
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:
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
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:
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.
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:
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.
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:
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 struct
s 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.
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).
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.
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:
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.
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 assert
s, require
s 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:
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)]
.
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):
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:
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.
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:
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:
Then you can define your functions' return types as:
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:
use one of the helper methods to, for example, fail immediately if the result was not Ok
:
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.