Need help understanding Bitcoin DeFi?
→ START HERE
Need help understanding Bitcoin DeFi?
→ START HERE
Need help understanding Bitcoin DeFi?
→ START HERE
Need help understanding Bitcoin DeFi?
→ START HERE
Need help understanding Bitcoin DeFi?
→ START HERE

Web3 Programming Languages: Clarity vs. Solidity

Nearly 50,000 developers joined Web3 in 2023. How do newcomers choose which Web3 programming language to learn?

Type
Topic(s)
Clarity
Published
February 19, 2024
Author(s)
Developer Advocate
Contents

If you want to build Web3 apps, the good news is that you have a lot of different blockchains and Web3 programming languages to choose from. In this article, we compare two popular Web3 smart contract programming languages—Clarity and Solidity—so you can make an informed decision on whether you want to build smart contracts in the Bitcoin or Ethereum ecosystem respectively.

Clarity vs. Solidity: An Introduction

There are no Web3 applications without smart contracts—automated programs that execute when predefined events or actions occur. These contracts are deployed on blockchains, and blockchains differ in a number of respects, ranging from which is the most decentralized and secure (Bitcoin) to which has the larger number of Web3 app users (Ethereum).

But they also differ in the programming languages that developers create smart contract applications with. How different can these languages be, you ask? Let’s compare two popular languages to find out.

If you’re a visual learner, check out this workshop that we gave to Developer DAO:

What Is Clarity?

Clarity is the programming language for creating smart contracts and apps on Stacks, a Bitcoin L2 for smart contracts. The Bitcoin blockchain itself is written in Bitcoin Script, a language with limited functionality that does not support smart contracts. So in order to build apps on Bitcoin, you need Bitcoin layers that offer a fully expressive programming environment while inheriting Bitcoin’s security, stability, and capital. That’s where Clarity comes in.

Clarity was designed to make the handling of assets on a blockchain as safe, secure, and predictable as possible while also empowering developers to build the next generation of blockchain applications. It was created 6 years after Solidity, and incorporated many lessons learned in those early years of Web3 app development, namely the importance of secure, predictable smart contracts.

Clarity was developed and is maintained by a multidisciplinary collaboration of engineers and scientists from the Stacks foundation, Hiro, Algorand, computer scientists at Princeton and Stanford, and others.

What Is Solidity?

Solidity was the first-ever smart contract programming language, and today it is the most widely used language in Web3. Solidity is the language for creating apps on Ethereum, and developers can also use it on any blockchain compatible with the Ethereum Virtual Machine (EVM), of which there are several, including Ethereum L2s, Solana, Avalanche, and more.

Solidity was initially proposed by Ethereum co-founder Gavin Wood before it was fully developed by the Ethereum Core developers and what later became the Solidity team, led by Christian Reitwiessner.

Solidity gained popularity through its first-mover advantage as the first language to let developers build blockchain apps, and its adoption accelerated in 2017 with the launch of the first projects that congested the Ethereum chain, such as the NFT project Cryptokitties. Solidity has built a solid reputation as a programming language for building all kinds of decentralized applications, and today Solidity is the most popular Web3 language, with more than 7,800 monthly active developers using it on the Ethereum blockchain alone.

Building With Clarity vs. Building With Solidity

On one hand, you have Solidity, the most popular language in Web3 today. It has a strong developer community and robust documentation. The language is also compatible with a number of different blockchains, thanks to newer Web3 entrants building off Ethereum’s foundation.

On the other hand, you have Clarity, a much newer language that learned from Solidity’s flaws. Clarity takes a fundamentally different approach with its design and prioritizes security and predictability, and Clarity contracts settle on the Bitcoin blockchain itself—the most secure and decentralized blockchain in use today. Clarity was created to answer the question: how can a language empower developers to write the most secure and predictable smart contracts, given that these programs will be handling billions of dollars of real-world financial assets? 

So how do these two languages stack up? Let’s take a look at the high-level similarities and differences between these two languages:

Clarity vs. Solidity: Security

Smart contracts manage billions of dollars in value—there is $64B locked in DeFi apps alone. The open source nature of blockchain applications combined with the valuable assets they manage mean they are highly attractive targets for hackers.

Solidity, the pioneering smart contract language, came with several security blindspots that have since been exploited—the most infamous being the DAO hack in which 3.6M ETH were stolen and the entire Ethereum chain state was reversed in a hard fork. However, the hacks did not stop, as Rekt’s leaderboard shows (the largest hack coming in 2022, worth $624M).

The staggering values smart contracts handle call for predictability and guarantees in execution.

Turing Completeness

Solidity is Turing complete, a technical term in computer science which describes a system that can theoretically solve any computation problem. More practically, it means Solidity is capable of things like executing infinite loops and conditional jumps.

This has important security ramifications because it means developers cannot predict all of the potential execution paths their code will take. If you can’t predict how your code will behave, that introduces new attack vectors and new bug possibilities that you didn’t anticipate during testing because you fundamentally cannot test for them.

Clarity, in contrast, is Turing incomplete. It is decidable. While theoretically this means it may be possible to build certain functionalities in Solidity that you can’t in Clarity, practically we have found this not to be the case. 

Being decidable also comes with important benefits. Decidability means that developers can statically explore and analyze all outcomes of their code, and this has several ramifications:

  1. It is easier to debug your code when you can reason as to how the code will behave and analyze all of the possible execution outcomes.
  2. Certain classes of hacks and exploits are fundamentally impossible, such as reentrancy attacks.
  3. You can predict contract termination as well as runtime costs. When you prompt users to execute a transaction, you can provide them with a precise gas and cost estimate, which you cannot do with the same degree of accuracy in Ethereum. 

The Ethereum community has worked hard to empower developers to write secure vulnerabilities (for instance, there are smart contract security guidelines and dossiers on known attacks and vulnerabilities), but despite that work, those security concerns still exist because Solidity allows them. Clarity simply removes a lot of those vulnerabilities altogether. 

To Compile, or Not to Compile

Clarity is an interpreted language and does not use a compiler. This means that the smart contract code on the blockchain is published publicly in a human-readable format (What You See Is What You Get). This has several benefits:

  1. It avoids any risk of compiler bugs, i.e. the process by which human-readable code is translated into machine code.
  2. It makes it easier for any developer or user to verify and validate the source code of the smart contract.
  3. It makes it easier for developers to learn from each other and boosts the overall composability of the ecosystem (developers can take the code of a smart contract, or part of it, and reuse it in a new application).

Clarity vs. Solidity: Syntax and Design Principles 

Solidity is a statically-typed, object-oriented, high-level language, strongly influenced by JavaScript, C++, and Python. Clarity is a statically-typed, functional language, strongly influenced by LISP and Scala.

At a high level, Clarity was designed with the principles of increased security, predictability, and transparency—principles chosen as a result of the lessons learned after the first 6 years of smart contract applications.

Let’s take a closer look at the two languages to see how those decisions play out.

Syntax Overview

Clarity is a functional programming language with syntactic similarities to LISP-like languages. Clarity requires precision and explicit composition. There is no inheritance, and developers must spell out what exactly the code should do. This brings the benefit of predictability and eases the auditing process. No cross-referencing inherited behavior from a 3rd-party library, which inherits behavior from a different 3rd-party library.

Solidity is an object-oriented language that uses an imperative style of programming (like JavaScript, C++ and Python). The language supports inheritance and user-defined types and is contract-oriented. Since Solidity is a compiled language, developers must use an ABI (Application Binary Interface) to call specific functions in a smart contract and get data back.

Let’s look at how these differences play out in code, looking at a sample To Do app.

In Solidity, a to-do app can be structured around a contract that maintains a list of to-do items, each with a unique identifier and a completion status.


contract TodoList {
  
  struct Todo {
    uint id;
    string task;
    bool completed;
  }
  
  Todo[] public todos;
  uint public nextId = 0;
  
  function create(string memory _task) public {
    todos.push(Todo(nextId, _task, false));
    nextId++;
  }
  
  function update(uint _id, string memory _task) public {
    Todo storage todo = todos[_id];
    todo.task = _task;
  }
  
  function toggleCompleted(uint _id) public {
    Todo storage todo = todos[_id];
    todo.completed = !todo.completed;
  }
  
  function get(uint _id) public view returns (string memory, bool) {
    Todo storage todo = todos[_id];
    return (todo.task, todo.completed);
  }
}

In Clarity, data structures are defined differently, and the language does not support dynamic arrays in the same way Solidity does. However, you can use maps and a counter to achieve similar functionality.


;; Define a map to store to-dos, keyed by an integer ID
(define-map todos 
  { id: uint } 
  { task: (string-ascii 128), completed: bool }
)

;; Counter for the next to-do ID
(define-data-var next-id uint u0)

;; Function to add a new to-do
(define-public (create (task (string-ascii 128)))
  (let
    (
      (id (var-get next-id))
    )
    (map-set todos { id: id } { task: task, completed: false })
    (var-set next-id (+ id u1))
    (ok id)
  )
)

;; Function to update a to-do's task
(define-public (update (id uint) (task (string-ascii 128)))
  (begin
    (
      map-set 
        todos 
        { id: id } 
        (merge 
          (default-to 
            { task: "", completed: false} 
            (map-get? todos { id: id })
          ) 
          { task: task }
        )
    )
    (ok true)
  )
)

;; Function to toggle a to-do's completion status
(define-public (toggle-completed (id uint))
  (match 
    (map-get? todos { id: id }) 
    todo
    (
      let
        (
          (new-completed (not (get completed todo)))
        )
        (map-set todos 
          { id :id } 
          (merge todo {completed: new-completed})
        )
        (ok new-completed)
    )
    (err (err u"Todo not found"))
  )
)

;; Function to retrieve a to-do
(define-read-only (get-todo (id uint))
  (map-get? todos { id: id })
)

Syntax Differences

Let’s take a closer look at the differences between these two languages by looking at how each executes similar functions.

Native Asset Transfer

First, let’s look at how Solidity and Clarity handle transactions with their native asset.

Sending Native Tokens in Solidity

contract SendETH {
  function transfer(address payable _to) public payable {
    _to.transfer(msg.value);
  }
}

Here the Solidity contract showcases a more familiar syntax for developers coming from JavaScript or C-like languages, with its use of declarative variables and functions. Solidity employs the <code-rich-text>payable<code-rich-text> modifier to enable functions and addresses to handle and receive ETH. The <code-rich-text>transfer<code-rich-text> method is used to send ETH from the contract to another address. This method is a part of the payable address's interface, allowing for a straightforward transfer of funds.

Sending Native Tokens in Clarity

(define-public (send-stx (recipient principal) (amount uint))
  (stx-transfer? amount tx-sender recipient)
)

Here we have a Clarity contract. Clarity utilizes a LISP-inspired syntax, where expressions are enclosed in parentheses, and the function name precedes its arguments. This approach is evident in the <code-rich-text>stx-transfer?<code-rich-text> function, which is a built-in feature of Clarity for transferring STX tokens. The function takes the amount to be transferred and the recipient's address as arguments, showcasing Clarity's direct and functional style of programming.

Minting an NFT

Now let’s take a look at the process of minting non-fungible tokens (NFTs) with smart contracts written in Clarity and Solidity.

Minting an NFT in Solidity

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract MyNFT is ERC721 {
  constructor() ERC721("WrappedETH", "wETH") {}

  function mint(address to, uint256 tokenId) public {
    _mint(to, tokenId);
  }
}

Solidity utilizes the ERC-721 standard for NFTs, which defines a set of rules and functions for NFT operations. The <code-rich-text>mint<code-rich-text> function in Solidity typically involves specifying the address to receive the NFT and a unique token ID. Solidity's use of declarative programming and object-oriented concepts facilitates a straightforward implementation of the ERC-721 standard.

Minting an NFT in Clarity

;; Define the NFT
(define-non-fungible-token bitcoin-szn uint)

;; Mint function
(define-public (mint (recipient principal) (token-id uint))
  (nft-mint? bitcoin-szn token-id recipient)
)

In the Clarity NFT minting example, the <code-rich-text>define-non-fungible-token<code-rich-text> command creates a new NFT type called bitcoin-szn, with each token uniquely identified by a <code-rich-text>uint<code-rich-text>. The minting functionality is encapsulated in the <code-rich-text>mint<code-rich-text> function, which is publicly accessible and takes two parameters: <code-rich-text>recipient<code-rich-text>, the address to receive the NFT, and <code-rich-text>token-id<code-rich-text>, the unique identifier for the NFT being minted.

The <code-rich-text>nft-mint?<code-rich-text> operation attempts to mint a new <code-rich-text>bitcoin-szn<code-rich-text> token, assigning it to the recipient. This process is straightforward yet powerful, demonstrating Clarity's emphasis on an explicit and functional programming style, where the function's purpose and its arguments are clearly delineated.

Interfaces

Lastly, let’s take a look at the concept of interfaces in Solidity and Clarity (in Clarity, interfaces are done with a <code-rich-text>use-trait<code-rich-text> function). An interface is a contract that defines a set of functions without implementing them. It specifies what methods a contract must have, allowing different contracts to interact with each other through a common set of functions. Think of it as a blueprint for a contract, ensuring that any contract that implements the interface adheres to a specific structure and behavior.

Interfaces in Solidity 

// Defining an interface
interface IToken {
  function transfer(address recipient, uint256 amount) external returns (bool);
}

// Implementing an interface in a contract
contract Token is IToken {
  function transfer(address recipient, uint256 amount) external override returns (bool) {
    // Implementation details
  }
}

In Solidity, interfaces are defined using the <code-rich-text>interface<code-rich-text> keyword. Contracts that implement an interface must implement all of its methods. Solidity's interfaces allow for the definition of a set of methods that other contracts must implement, without specifying how these methods are implemented. This enables a form of polymorphism, where different contracts can be interacted with through the same interface, despite potentially having different underlying implementations. For example, a wallet contract can be designed to interact with a particular token standard (ERC-20 or SIP-10), and this enables the wallet to send tokens that use each standard without knowing the specifics of each individual token implementation.

Interfaces in Clarity

In Clarity, the concept of <code-rich-text>use-trait<code-rich-text> allows contracts to define and enforce interfaces that other contracts must implement, facilitating a form of contract interaction and composition that ensures a level of abstraction and interoperability.

This mechanism is somewhat akin to interfaces in object-oriented programming languages but tailored to the smart contract environment. It ensures that different contracts adhere to the same method signatures, creating a standard that makes contracts composable and interoperable.

Let’s take a closer look at a code example:


(define-trait token-trait
  (
    (transfer? (principal principal uint) (response uint uint))
  )
)

Here is our blueprint for defining a trait. This one says, “if you want to act like a token in our system, you must know how to perform a transfer." The transfer method must take in two principals (addresses of the sender and recipient) and an amount (how many tokens to transfer), and it will return a boolean indicating success or failure, along with a possible error code.


(impl-trait .path-to-token-contract.token-trait)

(define-public (transfer (recipient principal) (amount uint))
  (begin
    ;; Implementation details here...
    (ok true)
  )
)

When another contract declares it implements <code-rich-text>token-trait<code-rich-text>, it's making a promise: "I will provide a <code-rich-text>transfer<code-rich-text> function just like the one described in <code-rich-text>token-trait<code-rich-text>." This is crucial for interoperability. Other contracts, or users, can interact with this contract knowing it supports certain actions, like transferring tokens.


(use-trait token-trait .path-to-token-contract.token-trait)

(define-public (perform-transfer (token-contract ) (recipient principal) (amount uint))
  (contract-call? token-contract transfer recipient amount)
)

By using <code-rich-text>use-trait<code-rich-text>, a contract declares, "I expect to work with any contract that behaves like <code-rich-text>token-trait<code-rich-text>." It's like saying, "I only want to talk to contracts that know how to transfer tokens as defined in our blueprint." This expectation is crucial for creating flexible and modular systems.

For more interactive coding samples that explore the differences between Clarity and Solidity, check out this website.

Design Differences

Reentrancy 

Reentrancy happens when a smart contract allows external contract calls to occur before the complete execution of the initial call. For example, Contract A calls into Contract B and is waiting for some condition to be met before updating its state, while Contract B calls back into Contract A (to, for instance, recursively withdraw or drain funds). 

If you aren’t very careful with your contract design, reentrancy can make your code do things you never planned for, whether a bug or an exploit by a malicious actor. Solidity allows reentrancy and recently added an opt-in <code-rich-text>noReentracy<code-rich-text> guard that developers may or may not use, but the responsibility is on the developer. Clarity doesn’t allow for reentrancy at all.

Default Permissions

In smart contracts, there are several types of permissions when it comes to functions: private, read-only, or public. Private functions can only be called by the current contract; read-only functions can be called externally (but cannot change the chain state); and public functions can be called externally by another principal (i.e. another user or smart contract).

Solidity functions are public by default, which means that any external contract can call into them. Clarity functions, on the other hand, are private by default, and developers must explicitly state that any given function is public. This again imposes an intentional opt-out protection on developers, warding off less secure contract design.

Clarity vs. Solidity: Developer Ecosystem

We’ve talked about the differences between the two languages, but what kind of resources and support can you expect to find in each ecosystem as you learn a new programming language?

Developer Community

The Ethereum ecosystem has the largest developer community in Web3, with 7,800+ monthly active developers building hundreds of different decentralized projects. 71% of contract code in all of Web3 is deployed on Ethereum first. Other blockchains such as Cosmos, Fantom, Binance Smart Chain, and Avalanche are all Solidity-compatible, and developers have resources and support from those communities too.

The Bitcoin ecosystem has, by comparison, fewer developers, with 1,000+ monthly active developers of its own. But that number is growing. 40% of Bitcoin developers are working on Bitcoin L2s, such as Stacks, which has 150+ monthly active developers of its own.

Dev Tooling and Resources 

The large developer community in Ethereum provides you with access to a large library of developer resources for learning Solidity, as well as a wide variety of tooling, such as Hardhat, Alchemy, Remix, and more. The ecosystem also offers a number of grant programs, and despite the market downturn, Web3 companies are still raising over $1B every quarter across all ecosystems, so there is funding to be had for founders that tap into the right opportunities.

If you choose to learn Clarity, the Clarity Language resource hub provides you with a compendium of tutorials, templates, and jobs for Clarity developers. Web3 developers that choose to learn Clarity will find a rich library of developer tooling from us at Hiro, and they can turn to Stacks Grants for early funding.

Clarity vs. Solidity: Which Web3 Programming Language Offers More Opportunities?

Both the Bitcoin and Ethereum ecosystems are leading blockchain ecosystems: together they have accounted for 40% of all Web3 developers, which has held steady since 2015. In terms of smart contract programming languages, Solidity comes with a robust ecosystem of resources and community support, benefits that come with being the oldest smart contract programming language.

Clarity is a language optimized for secure and predictable applications. While much newer, in many ways, Clarity is better suited for the business of smart contracts.

Ultimately, the Web3 programming language you choose to learn is dependent on which blockchain you want to build on— a holistic decision, with many different factors, such as users, market shares, and funding. Learn more about smart contract development in our free book.

Download a Developer’s Guide to Smart Contracts
Copy link
Mailbox
Hiro news & product updates straight to your inbox
Only relevant communications. We promise we won’t spam.

Related stories