Comparing Modern Smart Contracting Environments

0
45
Photo by Florian Olivo on Unsplash

Writing a smart contract with Solidity, NEAR, ink!, Solana, and CosmWasm

Why Smart Contracts?

Smart contracts can be thought of as programs that run on a blockchain. What value can be created from running code on a decentralized blockchain? When you create a standard contract, you would rely on brokers and lawyers to create and handle disputes. In this case, you have to pay large amounts and trust a third party to ensure an agreement is enforced. Now, let’s compare this with a programmatic smart contract. Not only can any individual create a contract with code, the result of interpreting the contract should always be deterministic, assuming no bugs in the blockchain’s VM. This is very important for businesses because they can cut down on costs and time while being in a trustless and deterministic environment.

Smart contracts are gaining popularity and usage because these systems are permissionless. The value of this has been shown the most in DeFi because it has allowed anyone to create financial instruments as well as allowing anyone to use them. A great example of this is Aave, which is a token lending protocol that allows any user to earn interest for providing liquidity to the lending pool, to borrow tokens with collateral, or take a flash loan (which requires the loan to be paid back at the end of a transaction). All of these functionalities are quicker and more permissionless than having to go through a bank, which at minimum enforces you have a bank account, that your credit is good enough, and that you go through an approval process.

The alternatives to smart contracts are application-specific blockchains. These are separate chains with custom logic and their own validators which allows for more configurable security through its consensus mechanisms and validation logic. There are two primary frameworks for developing application-specific blockchains, which are Substrate and Cosmos-SDK.

It is important not to conflate application-specific blockchains with smart contracts, as they have specific use cases and drawbacks. There are some limitations of smart contracts, which is that they are only executed when transactions are sent to them, they are limited by the virtual machine or environment they are running on and the network parameters cannot be configured to fit the needs of each application.

Although application-specific blockchains can solve these problems, it comes with some significant drawbacks, such as:

  • There needs to be a community and ecosystem around an application before it launches, to ensure sufficient validators and security for users to trust
  • There is more experience required and development cost to launch a blockchain when compared to deploying a smart contract
  • These applications are not composable in their current state. The cross-chain communication protocols that exist today are in a very primitive state and require a lot of overhead, IBC just launched mid-February 2021 with the Cosmos-SDK Stargate upgrade (although the protocol is network agnostic) and XCMP, which is Polkadot’s equivalent, is still in development.

The ability for smart contracts or applications to directly interact with other smart contracts allows for them to be used as building blocks to avoid duplication and work synergistically.

Since smart contracts can more quickly and easily be used as building blocks for decentralized applications and has the lowest bar to entry, they enable individuals to create value easily as well as expanding what’s possible for anyone interacting with them.

Although Ethereum has dominated the smart contracting space and will remain dominant in the near future, there are some very interesting and scalable alternatives. At the time of writing this article, it costs ~$19 USD to do a simple ERC20 transfer or ~$58 USD to do a token swap through Uniswap, which is not feasible to an average consumer. The permissionless or cheap arguments have less weight when there is a higher bar set for financial requirements, which is why alternatives will be presented and compared in this article.

What this article will cover

This article will be primarily focused on the developer experience of Solidity, NEAR, ink!, CosmWasm and Solana. The following will be covered for each:

  • Overview of the protocol
  • Initializing a new contract
  • Environment setup and tooling available
  • Technical details around the smart contracting languages and VMs the code is run in
  • How smart contracts fit into the protocol
  • Writing contract logic and testing functionality

What this article will not cover

This will be primarily developer-focused so token economics of any of the protocols will not be covered.

The comparison will end at building and testing a smart contract in each, and will not cover deploying the contract on-chain, interacting with it from a frontend, or upgrading the contract code (for protocols that support). Cross contract calls in code will also not be covered, but I will describe how they would interact when necessary.

What contract will be built for comparison?

For the contract logic, I decided to write a plutocratic hosting contract where anyone can purchase a route and put their content on chain (in this case, a String). The idea is just that the highest bidder for each route will be able to show whatever content they choose, and this would be in the future be loaded through a frontend.

I chose to use this arbitrary logic to compare the different protocols because it covers the following common interactions you can do in most smart contracting environments:

  • Initialization of a contract
  • Query to retrieve data from the contract (get content)
  • Sending tokens to a contract and mutating state (purchase route)
  • Transferring tokens from contract
  • Accessing environment variables such as sender address, the value sent in the transaction, and tokens locked in contract

Solidity

Solidity is a smart contracting language that compiles to EVM bytecode. It’s by far the most used smart contracting language. Most networks providing support for the EVM. To tie this back to the protocols that are covered in this article, all four networks currently have or are working toward EVM support:

These options are not meant to be long-term solutions, but it is interesting to note even the most recent and modern networks are working toward EVM support for onboarding new developers and migrating existing apps. The dev UX for building contracts for any EVM-based chain or one with EVM support will be very similar, but for this section, it will be assumed that the code will be deployed on Ethereum.

The two primary languages used for EVM-based chains are Solidity and Vyper, and there is also Yul and Yul+ which are intermediate language options. I am choosing to create this example in Solidity because it is by far the most used language out of these options.

Solidity may not be the most modern or generally usable language when developing smart contracts, but given the dominance of Ethereum and EVM-based chains in the ecosystem, Solidity will be the language to use if you want to create value as quickly as possible. You will also have the best developer tooling and wallet support given the popularity. Since Solidity only compiles to EVM bytecode it will become obsolete as most modern solutions are moving away from the EVM (and for good reason).

For this article, we will be using Truffle as the framework for compiling and testing the contract. I will note that for development I used Remix as it is the de-facto IDE for developing Solidity smart contracts, and makes writing and testing much easier for developing quick projects like this.

There is a quickstart example here.

Now, let’s see what the contract looks like and walk through some important pieces after. The code for this example can be found here.

The first part of this contract describes the data that will be stored on-chain and in what format. The ContentRecord structure defines the values stored for each route. The

mapping(string => ContentRecord) values; 

describes that there will be some key-value storage, where the string key represents a route and ContentRecord value contains the content for the route along with the owner information. Finally, the last piece of data associated with the contract is the contractOwner and the address is marked as payable because funds can be sent to this address.

The next thing to cover is the function modifier, which may seem unfamiliar:

modifier onlyOwner {
  require(msg.sender == contractOwner, "Sender is not the contract owner");
  _;
}

Function modifiers can be used to wrap a function with some extra validation or logic, which can be added as attributes to any function. In this case, the modifier is checking that the message sender is the contract owner before executing the function. The _; syntax just indicates where the function logic that this attribute is applied on will be executed.

The functions and logic are pretty straightforward but to note a few important points about them:

  • The purchase function requires the payable modifier to indicate it can receive value transferred with the transaction
  • When doing a lookup of the mapping, any keys that do not exist will index a zeroed value, there is no concept of null pointers or optional returns for this
  • The withdraw function applies the onlyOwner modifier we created above, to only allow the contract owner from withdrawing from the contract. The funds are not used in the contract, so it’s fine to transfer everything out in this case.
  • The getRoute function applies the view modifier to indicate it will not modify any state. This function also specifies that it returns the string content in memory.

The Truffle test for this is located here and can be run with

truffle test

The Truffle test framework by default will spin up a development blockchain and preallocate accounts to be used within the test. This is great because you get to a full integration test that matches the interactions that would be made on an actual blockchain. These tests will interact with the network specified through the Ethereum JSON-RPC API. Almost all interactions with the chain and contracts go through this API, for better or for worse. Because everything goes through that API though, you can even choose to swap out the backend for whatever network you’d like which supports the Ethereum RPC-API (I did this when testing my work on Ethermint).

Awesome, we’ve now built out the contract in Solidity! Now to move on to the others for comparison (I promise the next ones won’t be as involved).

NEAR

NEAR is a Proof of Stake and sharded blockchain. NEAR’s VM executes Wasm code generated through their Rust SDK and their AssemblyScript SDK. For those unfamiliar, Rust is used commonly to generate Wasm code because it has fantastic support, is a memory-safe language, and is very efficient with low overhead. AssemblyScript is also commonly used for Wasm because of its familiarity being a variant of TypeScript while also being very efficient. They recommend using Rust for a better UX, but it’s great to have both supported in this case, as other Wasm VM blockchains usually just support Rust and it allows people unfamiliar with Rust to have a less steep learning curve.

I’ve set up both the Rust and AssemblyScript variants of the contract on GitHub, but we will just be covering the Rust contract in this article, as it will be the easiest to compare with the others.

NEAR’s sharding design allows it to scale horizontally instead of limiting decentralization by increasing the validator hardware requirements to scale vertically. Their sharding protocol is named Nightshade and the main difference of this compared to other sharded blockchains is that there is one chain that manages all transactions that happen in each block and validators of a shard only have to maintain and validate transactions and state modified in that given shard. The usual negative to sharding with regards to developer experience is that it is really hard for the protocol to maintain atomicity because a block on one shard may be orphaned while the other part of the transaction has been committed to the other shard chain. Nightshade aims to simplify and solve the issues relating to this and storage/state bloat, which is a very common bottleneck.

NEAR’s cross-chain calls all assume contracts do not live on the same shard (to avoid contracts co-locating on the same shard to avoid congestion and advantages) which means that all calls will be asynchronous. Although this adds complexity, it also allows for interacting with multiple contracts in parallel without having to synchronously await the response from each. Because cross-shard and cross-chain interactions currently have a lot of overhead and can span multiple blocks, this can be very useful for increasing the efficiency for individual users.

NEAR’s shards are currently planned to be homogeneous, which means that for each shard there will be the same runtime that executes transactions and the same validation logic. This is often necessary for smart contracting environments and also allows for dynamic resharding. There is no option in the current protocol design to feasibly create a chain/shard that is tweaked to fit the stakeholders of a large application, a point that Cosmos and Polkadot are explicitly designing to support.

There are a few examples to get started with NEAR here.

All code for the contract that will be compared is located here. Now on to the actual contract code!

Ignoring a few rough edges, that’s pretty clean! Now, let’s walk through some details of the contract and tie it back with the Solidity equivalent:

  • ContentTracker which is annotated with #[near_bindgen] defines the contract and its state
  • The Default implementation for ContentTracker is equivalent to the constructor in Solidity and is called when the contract is deployed.
  • The purchase function is annotated with #[payable], which is equivalent to the Solidity payable attribute
  • The token transfers, with the Promise::new(…).transfer(…); lines are examples of asynchronous calls that were mentioned above.
  • The transfers are scheduled, but the contract does not await the result to finish purchasing the route
  • The data structure LookupMap<String, ContentRecord> handles the key-value lookup which accesses storage and is roughly equivalent to the mapping in Solidity

There may be some confusing parts of the code if you are not familiar with Rust syntax, but hopefully, the functionality is clear by now. Testing this contract is as simple as running yarn test:cargo in the near directory. All tests are located in the same file as the contract and the functions can be called into natively using the testing_env! macro to initialize the environment variables. There is also a way to do full e2e tests by initializing the JS client and using the jest framework (or any equivalent) but I will consider that out of scope for this article to cover.

ink!

Ink! is Parity’s smart contracting language which compiles to Wasm. How this fits into the greater Polkadot ecosystem is that you can deploy these smart contracts to any Parachain, roughly equivalent to a shard, which supports the built-in contract pallet. There is uncertainty in the community about the plan for this, so details will be vague. Broadly, smart contracts are a smaller component of the ecosystem, and not inherently included in the network. I chose to cover this next because it is similar to NEAR in the sense that it has a shared security model and is sharded in some way, but it differs in the sense that Parachains are heterogeneous meaning they have their own state transition function, which is also compiled to Wasm and included in the relay chain.

The ability for applications to configure the state transition logic to fit the needs of their stakeholders is a great feature, but it does come with the uncertainty of acquiring a Parachain slot through an on-chain auction. I won’t go too in detail with Parachains, but just note that the ways to deploy code are building a Parachain/Parathread with Substrate, smart contracts with ink! (which we will cover next), or deploying an EVM-based contract to a Parachain that supports the EVM pallet.

The benefit of the Parachain runtimes and smart contracts compiling to Wasm is that the network can be upgraded without introducing a hard fork. These forkless upgrades help remove the overhead needed for node operators which also allows for more rapid updates.

All smart contracts deployed to the same Parachain can interact synchronously with each other. Unfortunately, the XCMP protocol mentioned earlier is still in development. The communication is limited to just things on a contract’s shard. Unfortunately, that means that smart contracts will have very little benefit from the sharded structure of the network if the contracts have benefit in being composable. As a smart contract developer in this ecosystem, you need to be sure that there will be Parachains that support the contracts pallet and choose one based on the network parameters needed but also the ecosystem of projects that will also be deployed there. The network has the flexibility to represent the stakeholders of the network, but it’s very much in control of the owners of the Parachain slots, which can be more volatile and harder to predict.

Here is a tutorial to get started with ink! and the contract code, which can be found here, looks like this:

Looking at this compared with the NEAR contract it’s very similar, but here are some points to note:

  • Contract code encapsulated in a module
  • Easy to generate events using the #[ink(event)] attribute and the ability to index the logs with #[ink(topic)]
  • Yes, other protocols have the ability to log, but specifically, the ease to define and index specific field data is very clean here
  • Errors that are associated with specific exit codes
  • Ability to express which values are lazily loaded from storage, along with the default lazy loading and caching with the builtin storage types
  • All types are quite accurately ported from the std library, which makes it very easily used for anyone familiar with Rust

Going through the process of building this made me want to give a big high five to the team building this, they really did an amazing job with this! Someone who isn’t experienced with Rust might have some issues with some rough edges, but overall it’s a great environment setup. They have an excellent developer UX given that it’s easy to read, has good error handling, easy and useful logging, great documentation, idiomatic Rust code, and very optimized.

As for testing the code, all tests are written within the module using Rust’s test framework and are run with cargo contract test. These are just unit tests with a mocked env setup, and to do full e2e tests you need to manually deploy and test the contract yourself. Hopefully, the tooling expands in the future to have the ability to run a test node within the testing framework to make integration tests more easily reproducible and with less overhead.

CosmWasm

The next smart contracting platform to cover is CosmWasm. CosmWasm is a Cosmos-SDK module, similar to the contract pallet in Polkadot, which allows you to add Wasm smart contracting support to any chain built with Cosmos-SDK. Cosmos’ network topology is different from the alternatives because it does not have a shared security model by default and instead of having a hierarchical structure, it is separated into sovereign zones which handle their own validator set. Cosmos-SDK is intended to create application-specific blockchains where the interoperability between applications is through IBC. This creates an environment where the state machines very much can match the stakeholders of the application/network, but it comes with the cost of having to onboard trusted validators for the network, launch a chain (more overhead than deploying a contract), and worse composability given the overhead and unstable nature of IBC.

Although CosmWasm can be plugged into any chain, the chain used for testnets is based on wasmd which is a fork of Gaia (the Cosmos hub chain). There is no mainnet launched for this yet.

CosmWasm is a great addition to the Cosmos space to allow for smart contracting and allow developers to build smaller and more composable applications when compared with developing and launching a Cosmos zone. Another point to make is that when building a Cosmos zone, you need to use Go as a programming language or you need to manually satisfy the ABCI interface when building an application on top of Tendermint. CosmWasm does open the door to Rust developers, as well as reducing the scope of what needs to be built when creating a decentralized application.

An interesting point about composability with CosmWasm is that synchronous contract calls are explicitly disallowed to avoid reentrancy attacks, and messages can be specified in the transaction return that will be executed after the current transaction fails. It makes certain interactions more difficult, especially when you rely on the result of calling another contract to finalize some state, but the motivation for disallowing is sound.

As for the code, it will look very familiar if you’ve ever written a Cosmos-SDK module. A tutorial to CosmWasm can be found here and the code for this contract is located here.

The first thing to note is the interactions with the state in cosmwasm/src/state.rs, which are the wrapper functions that help with interactions with the state.

This defines reading contract-specific state (in this case, config) and mappings of keys to values. This can be compared with the keeper of a Cosmos-SDK application.

Next, the errors are defined in cosmwasm/src/error.rs:

The message type definitions are kept in cosmwasm/src/msg.rs:

Where the HandleMsg enum defines the types for state transition logic (Msgs in Cosmos-SDK) and QueryMsg handles the query variants (Querier in Cosmos-SDK).

Finally, the core contract logic lives within cosmwasm/src/contract.rs:

These functions would loosely represent the Handlers within Cosmos-SDK.

Some points to note about the contract:

  • There is no default token denomination, so this contract will accept any type of token
  • Repurchasing any route requires the same token, for simplicity because I don’t have access to a price oracle
  • When a route is repurchased from a previous owner, the send message is included in the HandleResponse and will be executed within the same transaction

Writing a smart contract for CosmWasm is more verbose than the alternatives. This is partially due to the lack of procedural macro usage. It’s also unclear if the similarity with the architecture of Cosmos-SDK is based on a technical motivation or if it’s intended to be more familiar to onboard Cosmos developers, but it will be much more easily understood if coming from that background. The unfortunate part about this is that it isn’t very idiomatic Rust code, and is not very intuitive for someone to pick up and use. What compounds with this is that the internal types are barely documented, so when something is not clear, the likely course of action is to look through examples to find the usage.

It is also worth noting that there is also CosmWasm Plus which is designed for “real networks” and has libraries that overlap with the base CosmWasm libs, so maybe the motivation for that will also be more clear when diving in deeper.

Testing the contract is through native Rust tests, similar to NEAR and ink!, and it’s noted in the docs that if the tests pass, they will work on-chain.

CosmWasm is a fantastic component of the Cosmos ecosystem, and I hope the project can continue to improve to gain the adoption of a wider audience.

Solana

Solana was a very interesting blockchain to learn about because the protocol design is so drastically different from all alternatives. Many individual points would be out of the scope of this article to explain so I will list some of the interesting ones:

  • Rust or C/C++ smart contracts compiled through LLVM infrastructure
  • Compiles to BPF bytecode, which is loaded on chain
  • Consensus mechanism based on a synchronized clock called Proof of History
  • PoH expansion of PoS
  • Transactions timestamped alongside a VDF sample
  • Replaces low latency communication bottlenecks with local computation
  • Capable of sub-second confirmation of transactions
  • Parallel execution of smart contracts
  • The argument against Wasm based smart contracts is they are all currently single-threaded
  • Describes which state is Read/written and can parallelize based on similar semantics to a Read-write lock
  • SIMD like execution of instructions based on program ID
  • Storage optimized to take advantage of operating system
  • Accounts stored in memory-mapped files
  • Performance of their Cloudbreak storage is close to if all state could be kept in memory
  • Specifically not sharded (Vision is scaling as fast as possible without sharding, then add only when necessary)

If a lot of this is unfamiliar, just know that these strategies and design are quite far off the beaten path of blockchains, and it’s awesome! There are definitely tradeoffs to some of these things, but it’s undeniable that the Solana protocol has brought a ton of innovation to the blockchain ecosystem.

As for the smart contracting environment, smart contracts can be written in Rust or C and compiled to BPF which is the bytecode that gets deployed on-chain. Although C is supported, there is more tooling and resources around Rust, so it’s recommended to use that. There are two main ways to develop a Solana program: a vanilla Solana program using the Solana tooling or using Anchor which is a framework for writing Solana programs. For this article, I will be using Anchor because it abstracts a lot of tedious and janky logic as well as being closer to the alternatives compared in this article.

The first thing to walk through before starting to write the program is the account model. It is very different from other blockchains. Solana’s accounts are similar to files in a Linux operating system in the sense that they can hold any arbitrary data and also contain metadata about how they can be accessed. The differences from a file in an operating system are the following:

  • The files have a lifetime, based on how many lamports (or tokens) have been paid as rent
  • These accounts are indexed by a 256-bit public key
  • These accounts have a fixed size and cannot be resized. Yes, you read that correctly, I’ll talk more about the implications later.

Let’s talk about writing programs. Programs are just accounts that are marked as executable. Solana relies on accounts for storage because it cannot have an underlying store when parallelizing transactions. What this means, is that before you deploy your program you need to estimate how large the account needs to be if you want to upgrade the program (or leave it at the default which allocates 2x more data than currently needed), but also any state that is used in the application needs to be preallocated and initialized based on estimation as well. Currently, there is no plan in the short term to allow for resizable accounts, so this is an important point to keep in mind. Another important limitation is that programs have no return values, so if you ever need to retrieve a result from a program invocation, it needs to be written to an account and interpreted off-chain.

It’s also important to note that tokens are just a program in which the data is represented in an account, and each token account has one owner. The token denomination is just represented by a public key.

When executing a transaction, all accounts accessed in the transaction need to be specified upfront by the user. This poses a hurdle to any application developer because they need to be aware of all accounts that are used within the transaction, which gets much harder when considering cross-program invocations between applications. This makes usability and composability more difficult, but also keep in mind there are also much more tight restrictions on things like computation, invocation depth, call depth, transaction size limit, and stack frame size which often forces transactions and/or functions to be split up.

Now on to the contract. Unfortunately given the restrictions of the programming model in Solana, we actually cannot build the same functionality as the other options without moving application logic off-chain or modifying the functionality to be inefficient and limited by the fixed account size. This specific contract logic requires one of two things: some sort of mapping of keys to values that can be serialized to bytes or being able to store values in a persisted key-value store where values are stored separately. The latter is much preferred, and is used in the alternatives above because it means only what is used is loaded and avoids large serialization logic, but let me walk through why either isn’t possible:

Map:

  • The components of the std::hash lib required for a HashMap is not supported in Solana, as well as any other 3rd party hashing algorithms (I even tried to specifically add support for maps in Anchor)
  • Because of fixed account size, the account used as storage for this would need to preallocate an unreasonable amount of bytes to scale at all
  • Computation caps would be hit very quickly for (de)serialization and map functionality
  • Having a growable vector of (String, ContentData) compiles, but runs into all the other points

Key-value store:

  • No access to storage, only accounts
  • Mapping a Program address to an account with data requires off-chain application logic and there would not be a source of truth for a canonical “route” for our application

Because of the limitations, and because I time-boxed my time working on this, the contract I will walkthrough will only allow for an account to be initialized (representing a route) and the program will own a vault of tokens to be locked in the program until someone purchases it for more tokens of the same denomination. There will also not be a getter function for the route, given that there are no return values from Solana program invocations, meaning that this logic will all happen off-chain.

A getting started with Anchor can be found here.

And here is the contract code:

To explain at a high level what is happening, all accounts which will be accessed (analogous to storage) are annotated in a structure for each call with #[derive(Accounts)]. The functions for this contract are Initialize which initializes the account data for the initial owner and Purchase which allows anyone to purchase it for more tokens. The other data that can be passed into a function, which is just kept in memory and not persisted are the function parameters. You can see these parameters inside the initialize and purchase functions, along with the Context holding the accounts required for the transaction.

Next, the state for the contract is located in the ContentRecord struct which is annotated with #[account] to indicate that it represents the data layout for an account.

When transferring tokens from the program, the transfer call is signed by the program, and this signature is roughly equal to one with a private key, except only the program can generate it.

The functionality to withdraw funds does not apply here, because there is no contract owner. This means the scope of the contract is even smaller without a withdraw function and there are funds locked in the program’s vault which can’t be acquired by anyone.

Testing the contract is done through a mocha test framework, run with anchor test, and will handle deploying the contract and interacting with it through a solana-test-validator which is included in the Solana installation. These tests are great because it is full e2e testing and is roughly equal to how a frontend would interact with the program.

It might be more informative about how accounts are initialized and attached to a function call by looking through the test for this contract. One thing to note that will be important as the context is that the instructions included in a transaction are run before the transaction, which is essential to be able to create accounts used in the transaction.

In conclusion, in its current state Solana is not a Smart Contracting environment that makes it easy to jump in and build something. Solana can provide great scalability, but you would have to pick Solana only when the application fits with the programming model. It would be difficult for me to recommend someone to start with Solana because the developer experience and limitations are so much worse than alternatives due to the current protocol design.

Conclusion

And that’s it! If you’ve made it this far, thanks for reading! Let me know if there are any questions or anything else you’d like to be covered in the comments. Would you like to know more about other smart contracting languages/environments, the deployment process of getting the code on-chain, and/or interactions from a frontend to these contracts to understand the full stack better?

Join Coinmonks Telegram group and learn about crypto trading and investing

Also, Read


Comparing Modern Smart Contracting Environments was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.