The Tezos blockchain is one of the main projects you need on your radar for 2020. Its liquid proof of stake system makes it increasingly attractive for people looking for steady revenues. Its token has more than doubled in price since last December, making it interesting for investors. And there are many new projects planned to be deployed on the blockchain in 2020 (an NFT-based game, USDTez, and the new tzBTC to name a few).
You have some experience with the Ethereum blockchain and you’re thinking about taking the plunge and getting your feet wet with Ligo. Congratulations! However, you’ll quickly notice that even if they’re both labeled “blockchain,” programming on the Tezos blockchain and the Ethereum blockchain can be as different as programming in two different languages. Some concepts that you know from Solidity do not apply (and can even be counterproductive) when coding with Ligo. Likewise, some concepts in Ligo do not exist in Solidity.
In this article, we’ll look at the main differences, pitfalls, and things to keep in mind when you are used to Solidity and want to join us and become a Tezos smart contract developer.
I’ll assume that you have a (moderately) good knowledge of Solidity and JavaScript. No knowledge of Ligo is required, although it may help you to better understand the comparisons.
The type system for simple values in Solidity and Ligo are very similar. Ligo has integers, strings, addresses, nat (the unsigned integer of Solidity), booleans, etc. One of the perks of types of Ligo is that you can declare your own types, it allows for a better structure and organization of your code. For example, it may be clearer to sometimes create a new type instead of using a primitive type (
username
instead of string
, score
instead of nat
).The complex types present more differences: Ligo has tuples, sets, and lists but no arrays. Sets can be seen as unordered arrays, containing unique values of the same type, while lists can be seen as ordered arrays containing values of the same type. Ligo also provides a number of methods to read, modify, or return information about the sets and lists, which are very handy.
Ligo also offers
records
which are very useful and similar to structs
in Solidity. Ligo doesn’t have enums, but variants that give you all the functionalities of enums and additional benefits! A special kind of variant called option
will spare you a lot of bugs when verifying the right value is provided.Another major difference between Solidity and Ligo is in its maps (the mappings of Solidity), which is the subject of the next paragraph.
Ligo offers maps and big maps, which behave essentially in the same way. Big maps are more suitable for containing a large amount of data. They resemble mappings of Solidity with a few benefits.
In Solidity, all the keys of a mapping exist by default, if you try to get the value associated with a key that doesn’t exist, you will get the default value of its type. This is not the case in Ligo. Thanks to pattern matching, you know precisely when you query a value in a map whether it returns something or not. If nothing is returned, you get an option with the value None. You aren’t left wondering whether the
0
or the empty string returned is the actual value or the default value because the key doesn’t exist!In Ligo, it’s possible to iterate over a map (this is not possible natively in Solidity with mappings). When iterating over a map, you can just check the returned values, you can modify them or you can “fold” them. One feature that takes some time to get used to when coming from Solidity (or a language like JavaScript) is that the functions you will use are not methods of the instance of the map (or the set/list) but methods of the Map (list/set) object. For example, if you want to know the size of a map you wouldn’t write
my_map.size()
but Map.size(my_map)
. To add an element to a set, you wouldn’t write my_set.add(el)
but Set.add(el, my_set)
.I put these two different cases together as they are little highlights that will make your job easier and your code safer.
Solidity is infamous for lacking any sort of native string manipulation — you can’t slice them, concatenate them, etc. Rejoice, because Ligo lets you do it! You get three methods that will help you work with strings.
String.length()
will return the length of the string, String.sub()
will return a substring of a string and strings can easily be concatenated in ReasonLigo with the ++
operation (string1 ++ string2
).I particularly enjoy working with sets and lists when writing smart contracts in Ligo. Sets are unordered lists of unique values of the same type. A few functions help you manipulate them easily (
Set.add()
, Set.remove()
) and you can be sure you have unique non-repeated values in them.Lists are ordered collections of elements of the same type. If you need to store some values in a certain order, this is what you use. Different methods help you work with lists, like
List.iter()
, List.map()
and List.fold()
. These types add another level of security and robustness to your smart contract.One of the highlights of being a smart contract developer is deploying your contract to the blockchain and letting it start its self-contained life!
When you write a smart contract in Solidity, you usually start by building the constructor. This function is in charge of initializing storage during the deployment of the contract. This doesn’t exist in Ligo. There’s no constructor function. Instead, you provide the initial storage while deploying the smart contract! In the Tezos lingo, it is called “originating a contract”.
The logic of using a smart contract on the Tezos blockchain is somewhat different from that of a contract on the Ethereum blockchain. In Solidity, you have multiple access points of your contract that you can call directly to modify or return some data. This creates the need for a “fallback function” in case users try to call a function that doesn’t exist.
In Ligo, there’s only one single access point for your smart contract called the entry point. This may sound a little weird at the beginning (it did to me) but it’s actually a robust system that wipes out the need for a fallback function. Your main entry point receives the name of the function you’re trying to call as a parameter and will use pattern matching to ensure that the function exists. Then, it will call the matching function called a “pseudo-entry point.” If it doesn’t exist, it will just fail. In Solidity, calling a function that doesn’t exist triggers the fallback function. In Ligo, it throws an error.
Note: The main entry point of your smart contract in Ligo only accepts two parameters: one parameter sent with the transaction and the storage. It’s not possible to provide multiple parameters like in Solidity
function myFunc(param1 uint, param2 uint, param3 string)
. However, you can send a record to the function. This restriction is helpful in avoiding common bugs in Solidity when passing parameters in the wrong order.One of the other main differences between Solidity and Ligo is in the transaction flow. In Solidity, when you send a transaction to a smart contract, the state of the smart contract is updated along the way until the transaction reaches the end of the function. If there’s an error at any point in the flow (for example thrown by an
assert
or require
exception), the state is reverted to its original value before the transaction happened. This leaves your code open to reentrancy attacks if you are not careful about when and how you update your state.Reentrancy attacks are unlikely with a smart contract on the Tezos blockchain. In Ligo, the main entry point always returns two things: a list of transactions to execute and the new state. At no point during the transaction flow is the storage of your smart contract altered. If there’s an error, the transaction will just stop with nothing changed. Since the function returns the whole storage, pattern matching will make sure that you return correct storage with the expected values. If a property of your storage is supposed to be a nat, it will be impossible to store an integer instead.
Pattern matching is among the features of a smart contract in Ligo that will help you make it bug-free. Below are two examples of pattern matching, but this is a built-in feature that will prove itself useful all over your smart contract!
In the pseudo-entry points
As mentioned earlier, there is only one “true” entry point in your Ligo smart contract. Through pattern matching, it will be in charge of redirecting the transaction call to the right function. Let’s check the example provided on the Ligo website:
When the transaction reaches the smart contract, it’s evaluated against its name and its parameters. If the type of the parameter matches, it will go through the switch pattern to be redirected to its corresponding function. After the function processes the transaction, the new storage is returned.
In updating maps
Pattern matching is a powerful ally when you want to update a map.
In Solidity, you have to take a wild guess when updating a mapping and assume the key has been initialized with a value (because all keys exist by default). Sometimes it’s a desirable pattern — for example, if you want to create a new key because adding a value to a key that was not previously initialized in a mapping will just create it.
In Ligo, it’s impossible to add a value to a key that doesn’t exist in a map. Every search and update of a map goes through pattern matching. Imagine we have a mapping of the type
map(address, tez)
that stores the number of tezzies owned by our users. This is how you would update the value for a provided address:switch (Map.find_opt (user, wallets)) {
| Some (wallet) => Map.update(user, wallet + 1tez, wallets)
| None => failwith ("No wallet found") : return_type
}
Here’s what goes on in this little piece of code.
Map.find_opt
is a function that returns an option type, i.e a variant type that contains only two options. Either there’s something (Some
) or there is nothing (None
). If a value associated with the key was found, the value is returned with Some
. Otherwise, None
is returned (in this case, we just throw an error, but you could also create the key/value pair or complete another action). It’s then impossible to try to update the value of a key that doesn’t exist.In Solidity, you would write
wallets[user] += 1 ether
because by default the key exists and you have no control over its existence. The wallet could also be equal to 0ether
and you wouldn’t know if it is a desirable value or just the default value. In Ligo, if the value of a key is 0tez
, you know that it was set to be equal to 0 on purpose.One of the most puzzling differences between Ligo and Solidity for me, when I started writing smart contracts for the Tezos blockchain, was how to get the values from the contract storage out to my dapp. As mentioned earlier, the entry points of a Ligo contract always return the whole storage and a list of operations. There is no getter function to implement if you want to return one single value. Also, the storage in your Ligo smart contract is one single variable (generally a record), instead of distinct separate variables as in Solidity.
I only got a satisfying answer to this question when I started playing with libraries for dapps, like the excellent Taquito. For every smart contract developer coming from Solidity and wondering how Tezos smart contracts export their values to use them in a dapp, the simple answer is: they don’t. They won’t return any value but you can read them from outside. The Tezos node exposes the contract storage and libraries like Taquito give you access to the whole storage (for example with
await contractInstance.storage()
if using Taquito).Solidity has to automatically create getter functions for your variables, while this is not a concern with Ligo. In Ligo every value of the storage is always one promise away from you!
Now that I’ve spent the whole article trying to show you how amazing writing smart contracts for the Tezos blockchain is, let’s see a few things that you’re going to miss as a Solidity developer when switching to Ligo.
Obviously, the Tezos ecosystem hasn’t yet reached the maturity of the Ethereum one and a lot of tools and features are yet to be developed. Here are a few that will hopefully make their way into the smart contracts and the Tezos ecosystem:
Tezos.stream.subscribeOperation
) but I tried it on a sandboxed node and half of the storage updates didn’t register. As of today, the most reliable way to get notified of storage updates is a good old setInterval
with a function that compares the values of the freshly fetched storage and the old one.As you can imagine, switching from Solidity to Ligo requires patience and a lot of reading. You have to learn how to use a new programming language for smart contracts that’s pretty different from Solidity. You need to find new tools to help you test your smart contracts and develop your dapps. You need to learn how to interact with smart contracts on Tezos from your application.
If you are a self-taught developer like me, it will take you some time to figure out how everything works together. However, it’s a very rewarding process. During these past months, I’ve learned a lot about functional programming, smart contracts, Tezos, and dapps in general. If you continue writing smart contracts in Solidity, a glimpse of another approach to creating them may help you become a better developer. Just be cautious — writing smart contracts for Tezos is highly addictive, as soon as you realize how much secure, robust, and elegant they are!
Don’t hesitate to leave your opinions or suggestions!