Smart Contracts: WASM vs Solidity

With the upcoming launch of the PhronAI EVM-compatible chain, let’s explore WASM and EVM: the two most popular runtime environments that facilitate the execution of smart contracts on the blockchain.

Smart contracts are one of the more exciting developments of recent years, allowing users to create personalized use-cases whose execution is bereft of any kind of institutional intermediary or middleman. These contracts are pieces of specialized code that can be executed by users on demand; for example, two parties can potentially engage in an exchange of real estate through the help of a smart contract.

In this scenario, the smart contract will stipulate all of the requirements for the deal to be concluded. Once the pre-decided conditions occur, the property will change hands. The whole process will be completed and finalized by applying the smart contract without involving any real estate brokers, lawyers, or banks. In order to make smart contracts possible, it is necessary to employ a runtime environment known as virtual machines in which they can be executed. We’ll explore the two most popular ones, the Ethereum Virtual Machine (EVM) and WebAssembly (WASM).

The Building Blocks of Smart Contracts

Before we dive into the whole debate on EVM versus WASM, we should briefly explain the building blocks of a smart contract platform. This mechanism is actually very similar to how one would write and execute regular programs on a computer: the difference is that instead of running the programs on a local machine, they are executed by the blockchain!

  1. The high-level smart contract language in which the contract is written, e.g, Solidity, Vyper, ink!;

  2. The compiler translates the code into the actual smart contract. This list of instructions that inhabits the smart contract is called the bytecode and must follow a certain standard to be executable. These rules are pre-established in the virtual machine standard;

This bytecode is finally uploaded to the blockchain, where it is stored. When a user performs a smart contract call, the corresponding piece of bytecode is loaded into the execution environment (a virtual machine able to execute that particular type of bytecode), where the instructions constituting that call are executed.

The Languages Behind Smart Contracts

Solidity

Solidity is one of the programming languages which we can use to write smart contracts. It is the foundation upon which the Ethereum blockchain is built and is the most popular smart contract language currently in use. Some of the pros of Solidity are as follows:

  • Solidity offers inheritance properties for their contracts, including multiple-level inheritance properties;

  • It is quite easy to learn due to its fairly simple design and similarity to other popular languages;

  • It comes with a wide selection of reusable libraries and frameworks that allow a developer to reuse existing code;

  • It is the default smart contract programming language when working within the Ethereum blockchain, as it was designed solely with the purpose of working with Ethereum. This translates into considerable support from a large community that worked through many of the initial bugs and provided a more user-friendly / dev-friendly environment.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    event Transfer(address indexed from, address indexed to, uint256 value);
}

contract MyToken is IERC20 {
    string public name = "MyToken";
    string public symbol = "MTK";
    uint8 public decimals = 18;
    uint256 private _totalSupply;
    mapping(address => uint256) private _balances;

    constructor(uint256 initialSupply) {
        _totalSupply = initialSupply * 10 ** uint256(decimals);
        _balances[msg.sender] = _totalSupply;
        emit Transfer(address(0), msg.sender, _totalSupply);
    }

    function totalSupply() public view override returns (uint256) {
        return _totalSupply;
    }

    function balanceOf(address account) public view override returns (uint256) {
        return _balances[account];
    }

    function transfer(address recipient, uint256 amount) public override returns (bool) {
        require(recipient != address(0), "Transfer to zero address");
        require(_balances[msg.sender] >= amount, "Insufficient balance");

        _balances[msg.sender] -= amount;
        _balances[recipient] += amount;

        emit Transfer(msg.sender, recipient, amount);
        return true;
    }
}

Ink!

Smart contracts on the PhronAI L1 are written in ink!, an embedded domain-specific language (eDSL) built on top of Rust, and compiled to WASM. This language was developed particularly for writing smart contracts for the FRAME Contracts pallet developed by Parity Technologies.

The main advantage of ink! is that it leverages the power of Rust, which is a great modern language for systems and general-purpose programming, striking what seems to be the right balance between expressive power, type and memory safety and developer ergonomics.

Here are some of the advantages of ink!:

  • Its type system is very safe, eliminating a lot of the errors during the development of the contract;

  • Memory management in Rust and ink! prevents errors without introducing any performance overhead;

  • ink! and pallet_contracts introduce a way of natively creating upgradeable contracts by using a two step contract deployment process: uploading the code to the chain and instantiating;

  • Existing tools for Rust work for ink!;

  • Existing libraries may be used if they can be compiled with no_std feature – this is one of the bigger advantages of ink!;

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

use ink::prelude::string::String;
use ink::storage;

#[ink::contract]
mod my_token {
    #[ink(storage)]
    pub struct MyToken {
        total_supply: Balance,
        balances: storage::Mapping<AccountId, Balance>,
        name: String,
        symbol: String,
    }

    impl MyToken {
        #[ink(constructor)]
        pub fn new(initial_supply: Balance, name: String, symbol: String) -> Self {
            let caller = Self::env().caller();
            let mut balances = storage::Mapping::new();
            balances.insert(caller, &initial_supply);

            Self {
                total_supply: initial_supply,
                balances,
                name,
                symbol,
            }
        }

        #[ink(message)]
        pub fn total_supply(&self) -> Balance {
            self.total_supply
        }

        #[ink(message)]
        pub fn balance_of(&self, account: AccountId) -> Balance {
            self.balances.get(account).unwrap_or(0)
        }

        #[ink(message)]
        pub fn transfer(&mut self, to: AccountId, amount: Balance) -> Result<(), String> {
            let from = self.env().caller();
            let from_balance = self.balance_of(from);

            if from_balance < amount {
                return Err(String::from("Insufficient balance"));
            }

            self.balances.insert(from, &(from_balance - amount));
            let to_balance = self.balance_of(to);
            self.balances.insert(to, &(to_balance + amount));

            Ok(())
        }

        #[ink(message)]
        pub fn token_name(&self) -> String {
            self.name.clone()
        }

        #[ink(message)]
        pub fn token_symbol(&self) -> String {
            self.symbol.clone()
        }
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        #[ink::test]
        fn create_token_works() {
            let my_token = MyToken::new(1000, String::from("MyToken"), String::from("MTK"));
            assert_eq!(my_token.total_supply(), 1000);
            assert_eq!(my_token.token_name(), "MyToken");
            assert_eq!(my_token.token_symbol(), "MTK");
        }

        #[ink::test]
        fn transfer_works() {
            let mut my_token = MyToken::new(1000, String::from("MyToken"), String::from("MTK"));
            let accounts = ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();

            assert_eq!(my_token.balance_of(accounts.alice), 1000);
            assert_eq!(my_token.balance_of(accounts.bob), 0);

            my_token.transfer(accounts.bob, 100).expect("transfer failed");

            assert_eq!(my_token.balance_of(accounts.alice), 900);
            assert_eq!(my_token.balance_of(accounts.bob), 100);
        }
    }
}

WASM and EVM: similarities and differences

At this point it’s important to note that when talking about WASM, we actually mean a few things: ink! as a high-level language for writing smart contracts, WASM as a language/format for describing instructions for the virtual machine, the interpreter of this language embedded inside Substrate’s pallet_contracts and set of rules that dictate how the smart contracts interact with the rest of the chain. Similarly, by EVM we mean Solidity as the high-level smart contracts language, the EVM bytecode, the actual virtual machine executing the bytecode and its connection to the chain.

The philosophies behind EVM and WASM are different:

  • EVM was created from the ground-up as a solution targeted specifically for smart contracts. On that account, it contains many primitives specific to this domain but for the same reason it’s less reusable;

  • WASM leverages an existing, ubiquitous standard found in many web applications: this means a lot of solutions were already optimized for WASM but smart contract-specific functionality had to be added in, which does tend to increase the complexity of the whole solution.

At the same time, the key principles of smart contract development are the same on both platforms and almost all of the ideas have one-to-one mapping (albeit with different flavors). On both platforms the developers will create a contract that has some storage, a constructor, and a set of methods for reading and modifying the storage. The calls that modify the storage will cost a bit of the chain’s currency (a concept called ‘gas’) and an error in a transaction will cause it to be reverted.

Naturally, there are also notable differences between WASM and EVM. Below we present a list of those we find the most important:

  • In Solidity the storage entries are all 256 bits. This means that the variables are large enough to fit almost every conceivable value but this comes with a performance penalty. Also, the overflow is of course still possible and has been exploited in the past. In ink! the largest number type available without using additional libraries is 128 bits: it has performance benefits and is more than enough in most cases but developers coming from Solidity might find it limiting.

  • In Solidity a popular method for creating upgradeable contracts is to rely on proxy contracts: it is a powerful method but also introduces some risks. In ink! the default method for contract upgrades is to leverage the set_code_hash message, effectively swapping the bytecode of the contract for an upgraded version.

  • On EVM, each instantiation of the contract uploads the bytecode to the chain, which can result in increased costs when one has to deploy multiple instances of the same contract (e.g. different fungible tokens in a game). To alleviate that, a multi-token standard was created (ERC-1155). In ink! the two-step deployment process allows you to upload the bytecode once and create multiple instances for very little gas.

For an in-depth comparison of how the concepts from Solidity map to ink!, we recommend reading this article.

How does PhronAI support both WASM and EVM? And how do I choose?

With the creation of the PhronAI fully EVM-compatible Layer 2 blockchain, the developers can now choose whether they want to deploy in the EVM-land or in the Substrate world. There are now two chains running as part of PhronAI: the EVM-based Layer 2 and the Substrate-based Layer 1. The Layer 2 chain supports all of the languages and tooling available on the EVM, so Solidity developers will feel right at home. At the same time, the Layer 1 chain allows you to deploy smart contracts written in ink! and leverage all of its safety features.

For the majority of the developers the choice will most probably come down to familiarity with either one of the technologies: developers with experience in Solidity usually prefer to stick to the familiar territory and deploy on the L2 chain. Developers with previous experience in Rust or ink! may want to explore the features of Substrate on the L1 chain. At the same time, we encourage you to do your own research and choose the platform that is best suited for your use case. A good idea might be to get TZERO from the faucets and experiment on two chains until you can make an informed decision. In any case, we’ll be happy to see your project deploy on one or both of the chains!

Last updated