Smart Contract Development — Move vs. Rust

An in-depth look into Move, a novel programming language for smart contract development, and how it compares to the existing Rust-based model used on Solana.

Krešimir Klas
53 min readSep 6, 2022

1. Introduction

In recent weeks and months, there’s been a lot of buzz around Aptos and Sui, the emerging high-performance L1s, and the Move smart contract programming language which is integral to these new chains. Some developers are already aggressively switching to Move swearing it’s the future of smart contract development, while others are a bit cautious having the impression that Move is just another smart contract programming language that fundamentally doesn’t offer much over the existing programming models. Crypto investors are also wondering what’s so special about these new L1s and how they stack up against Solana, currently the main player in the high-performance L1 category, which notably uses Rust as the basis for smart contract programming.

But the discussions we’ve seen so far don’t go into the necessary depth required to fully appreciate what this new technology brings us. And this is true on both ends of the discussion — Move skeptics downplay Move without fully appreciating some of the more subtle (but very important) aspects of it, while Move fans praise Move to no end but fail to fully articulate what exactly makes it so great. This leaves a large middle ground and a lot of ambiguity due to which outside onlookers, crypto developers, and investors who’ve been following this discussion cannot form their opinions with confidence.

I have recently done an in-depth look into Move and having experience with smart contract development on Solana I’m in a good position to give a technical overview of these new technologies and share some deeper insights that would otherwise not be available to people interested in this discussion on the outside.

In this article, I’m taking a deep technical dive into Move, its novel programming model, the Sui blockchain and how it leverages Move’s features, and how it compares to Solana and its programming model.

In order to highlight the features of Move, I will compare Solana/Rust to Sui/Move. The main reason I do that is that it’s much easier to understand something when you compare it to another thing that you’re already familiar with rather than try to understand it on its own. It’s important to note that there are also other variants of Move like the Aptos Move which do certain things slightly differently. The point of this article is not to discuss the nuances between different variants of Move but to show the general benefits of Move and how it compares to the Solana programming model, so for simplicity I’ve decided to stick to using only one variant (the Sui Move) throughout this article. Because of this, certain Move concepts I introduce in this article (namely objects and related functionality) only apply to the Sui variant of Move and not the others. While the other variants of Move don’t necessarily have these concepts, they achieve the same functionality using different mechanisms (e.g. global storage). But even so, all the main Move benefits discussed in this article apply to all Move integrations (that support Move bytecode natively), including Aptos. The reason I chose Sui specifically is simply that I’m more familiar with it and also I feel it’s a bit more intuitive and easier to present in article form.

Once again, even though throughout this article I do comparisons between Solana and Sui, my intention is not to throw shade on anyone or anything but simply to highlight different aspects of these technologies and their various benefits and trade-offs for easier understanding.

2. The Solana Programming Model

In order to fully appreciate the points being made in this article, some familiarity with the Solana programming model is required. If you’re not familiar with Solana’s programming model, I recommend reading my article on Solana smart contract programming which covers all the concepts required to follow this one.

For a quick refresher, I’m also going to do a brief summary here. If you’re already familiar with Solana’s programming model, you may skip this chapter.

On Solana, the programs (smart contracts) are stateless in that they cannot on their own access (read or write) any state that persists throughout transactions. To access or persist state, the programs need to use accounts. Each account has a unique address (the public key of an Ed25519 key pair) and can store arbitrary data.

We can think of Solana’s account space as a global key-value store where keys are account addresses (pubkeys) and values are account data. The programs then operate on top of this key-value store by reading and modifying its values.

Accounts have a notion of ownership. Each account is owned by one (and only one) program. When an account is owned by a program, the program is allowed to mutate its data. Programs are not allowed to mutate accounts that they don’t own (but are allowed to read from them). These checks are done dynamically by the runtime by comparing the account state before and after the program execution and fail the transaction if an illegal mutation has been made.

Each account also has a private key associated with it (the corresponding public key is its address) and the user who has access to this private key can sign transactions with it. Using this mechanism we implement authority and ownership functionalities in Solana smart contracts — e.g. to access certain funds the smart contract can require that the necessary signature is provided by the user.

In order to do a program call, the client needs to specify which accounts this program will access during the call. This is so that the transaction processing runtime can schedule non-overlapping transactions to be executed in parallel while guaranteeing data consistency. This is one of Solana’s design features that enables its high throughput.

Programs can call other programs via CPI calls. These calls work pretty much the same as the calls coming from the client side— the caller program needs to specify accounts that the callee program will be accessing and the callee program will do all the same input checks as if it were called from a client (it doesn’t trust the caller program).

PDA accounts are a special kind of account that enable programs to provide account signatures without owning or storing private keys. PDAs guarantee that only the program which the PDA has been generated for can create a signature for it (and no other users and programs). This is useful when a program needs to interact with another program via CPI calls and provide authority (e.g. implementing a vault). PDAs guarantee that no one except for the program has direct access to the program’s resources. PDAs can also be used for creating accounts at deterministic addresses.

These are the basic building blocks of secure smart contract programming on Solana. Again, if you feel like any of these concepts are unclear I strongly recommend reading my article on Solana smart contract programming which goes into each of these (and other stuff) in a bit more depth.

In a way, you can think of Solana programs as programs in an operating system and accounts as files where anyone can freely execute any program and even deploy their own programs. When programs (smart contracts) are run they will read from and write to files (accounts). All files are available to all programs for reading, but only the programs which have ownership permission on a file can write to it. Programs can also execute other programs but they don’t trust each other in any way — no matter who executed a program, it needs to assume the inputs are potentially adversarial. Since this OS is globally accessible by anyone, native signature verification support is added to the programs in order to enable authority and ownership functionality for users… It’s not a perfect analogy but it’s a fun one.

3. The Move Programming Model

In Move, smart contracts are published as modules. Modules consist of functions and custom types (structs). Structs consist of fields that can be primitive types (u8, u64, bool…) or other structs. Functions can call other functions — either in the same module or other modules if they’re made public.

To put this in the context of Solana, this is as if all smart contracts are published as modules within a single program. This means that all smart contracts (modules) are contained within the same type system and can call each other directly without going through an intermediate API or an interface. This is very significant and the implications of this will be thoroughly discussed throughout this article.

3.1. Objects

Before we continue, it’s important to note that the following notion of objects is specific to the Sui variant of Move and things might be slightly different in other integrations of Move (e.g. Aptos or Diem/core Move). Even so, in other Move variants there are similar solutions that achieve the same thing (persistence of state) which are not too dissimilar. The main reason I introduce Sui objects here is because the code samples later in the article are based on the Sui variant of Move and also because objects are a bit more intuitive to understand than e.g. the global storage mechanism in core Move. But the important thing is that all of the main benefits of Move discussed in this article apply to all Move integrations (that natively support Move bytecode) including Aptos. For learning how the Sui and Aptos variants are different, the Why We Created Sui Move article and the Move on Aptos doc page are a good start.

Objects are struct instances that are stored by the runtime and persist state across transactions.

There are three different types of objects (in Sui):

  • owned objects
  • shared objects
  • immutable objects

Owned objects are objects which belong to users. Only the user who owns the object can use it in a transaction. The ownership metadata is fully transparent and handled by the runtime. It’s implemented using public key cryptography — each owned object is associated with a public key (stored in the object’s metadata in the runtime) and anytime you want to use an object in a transaction, you need to provide the corresponding signature (Ed25519 is supported now with ECDSA and K-of-N multisig support coming soon).

Shared objects are similar to owned objects but they don’t have an owner associated with them. Therefore you don’t have to possess any private keys to be able to use them in a transaction (anyone can use them). Any owned object can be shared (by its owner) and once an object is shared it will forever remain shared — it can never be transferred or become an owned object again.

Immutable objects are objects which cannot be mutated. Once an object has been marked immutable, its fields can never again be modified. Similar to shared objects, these don’t have an owner and can be used by anyone.

The Move programming model is very intuitive and simple. Each smart contract is a module and consists of function and struct definitions. Structs are instantiated in functions and can be passed into other modules via function calls. In order for a struct to be persisted across transactions, we turn it into an object which can be owned, shared, or immutable (Sui specific, this is slightly different in other Move variants). That’s it!

4. Move Safety

So we have seen that in Move:

  • you can pass any object that you own (or shared) to any function in any module
  • anyone is allowed to publish a (potentially adversarial) module
  • there’s no notion of modules owning structs which would give the owner module the sole authority over mutating it like it’s the case with Solana’s accounts — the structs can flow into other modules and be embedded into other structs also

The question now is, what makes this safe? What’s stopping someone from publishing an adversarial module, taking a shared object (like an AMM pool), and sending it into the adversarial module which will then proceed to drain it of funds?

In Solana, there’s a notion of account ownership where only the program which owns an account is ever allowed to mutate it. But in Move, there’s no notion of modules owning objects and you can send objects into arbitrary modules — not only a reference to the object but the whole object itself, by value. And there are no specific checks done by the runtime to ensure this object hasn’t been illegally modified while it’s passing through the untrusted module. So what is keeping this object safe? Where is the guarantee that the object isn’t misused by untrusted code coming from?

Well, in this lies the novelty of Move… Let’s talk about resources.

4.1. Structs

Defining a struct type is pretty much what you’d expect:

struct Foo {
x: u64,
y: bool
}

So far so good — this is how you’d define a struct in Rust also. But there is something unique about structs in Move, and that is that in Move, modules have much more control over how their types can and can’t be used than conventional programming languages do. A struct defined as in the code snippet above will have the following restrictions:

  • It can only be instantiated (“packed”) and destroyed (“unpacked”) inside the module that defines the struct — i.e. you cannot instantiate or destroy a struct instance from inside any function in any other module
  • Fields of a struct instance can only be accessed (and therefore mutated) from its module
  • You cannot clone or duplicate a struct instance outside its module
  • You cannot store a struct instance in a field of some other struct instance

This means that if you’re handling an instance of this struct in a function in another module, you wouldn’t be able to mutate its fields, clone it, store it in a field in another struct, or drop it (you’d have to pass it on somewhere else via a function call). It may be the case that the struct’s module implements functions that do those things that can be called from our module, but otherwise we can’t do any of those things directly for an external type. This gives modules full control over how their types can and can’t be used.

Now, it seems like with these restrictions we have lost a lot of flexibility. And that’s true — dealing with such structs would be very cumbersome in conventional programming, but this is, in fact, very much what we want in smart contracts. Smart contract development is about programming digital assets (resources) after all. And if you look at the struct described above, this is exactly what it is — it’s a resource. It cannot be arbitrarily created out of thin air, it cannot be duplicated, and it cannot be destroyed by accident. So it’s true that we’ve lost some flexibility here, but the flexibility we’ve lost is exactly the kind of flexibility we want to lose. This makes working with resources intuitive and safe.

Furthermore, Move allows us to loosen up some of these restrictions by adding capabilities to structs. There are four capabilities: key, store, copy, and drop. You can add any combination of these capabilities to a struct:

struct Foo has key, store, copy, drop {
id: UID,
x: u64,
y: bool
}

Here’s what they do:

  • key — this allows a struct to become an object (Sui specific, core Move is slightly different). As explained earlier, objects are persisted and, in the case of owned objects, require user signatures to be used in a smart contract call. When the key capability is used, the first field of the struct must be the ID of the object with the type UID. This will give it a globally unique ID that can be used to reference it.
  • store — this allows the struct to be embedded as a field in another struct
  • copy — this allows the struct to be copied/cloned arbitrarily from anywhere
  • drop — this allows the struct to be destroyed arbitrarily from anywhere

In essence, every struct in Move is a resource by default. Capabilities give us the power to granularly loosen up these restrictions and make them behave more like conventional structs.

4.2. Coin

To illustrate this a bit better, let’s look at the Coin type as an example. Coin implements ERC20 / SPL Token-like functionality in Sui and is a part of the Sui Move Library. This is how it’s defined:

// coin.move
struct Coin<phantom T> has key, store {
id: UID,
balance: Balance<T>
}
// balance.move
struct Balance<phantom T> has store {
value: u64
}

You can find the full module implementation in the Sui codebase (link).

The Coin type has the key and store capabilities. Key means that it can be used as an object. This allows users to own Coins directly (as a top-level object). When you own a Coin, nobody but you can even reference it in a transaction (let alone use it). Store means that the Coin can be embedded as a field in another struct. This is useful for composability.

Since there’s no drop capability, the Coin cannot be accidentally discarded (destroyed) in a function. This is a very nice feature — it means that you can’t lose a Coin by accident. If you’re implementing a function that receives a Coin as an argument, by the end of the function you need to do something with it explicitly — either transfer it to a user, embed it in another object, or send it into another function via a call (which again needs to do something with it). It’s of course possible to destroy a Coin by calling the coin::burn function in the coin module, but you need to do that purposefully (you won’t do that accidentally).

No clone capability means that nobody can duplicate the Coin and thus create new supply out of thin air. Creating new supply can be done through the coin::mint function and it can only be called by the owner of the treasury capability object for that coin (this object is initially transferred to the currency creator).

Also, note that thanks to generics each different coin will be its own distinct type. And since two coins can be added together only through the coin::join function (and not by directly accessing their fields), this means it’s simply not possible to add values of coins of different types (coin A + coin B) — there’s no function of that signature. The type system protects us here from doing bad accounting.

In Move, the resource safety of a resource is defined by its type. Considering that Move has a global type system, this enables a more natural and safer programming model where resources can be passed in and out of untrusted code directly while preserving their safety. This may not seem like a big deal at first glance, but in fact, this carries significant benefits for smart contract composability, ergonomics, and safety. This will be discussed more thoroughly in chapter 5.

4.3. Bytecode Verification

As mentioned earlier, Move smart contracts are published as modules. And anyone is allowed to create and upload any arbitrary module to the blockchain to be executed by anyone. We have also seen that Move has certain rules on how structs are allowed to be used.

So what is guaranteeing that these rules are being respected by arbitrary modules? What’s preventing someone from uploading a module with specially crafted bytecode which will e.g. receive a Coin object and then proceed to bypass those rules by mutating its inner fields directly? By doing this you could illegally inflate the amount of coins you have. The bytecode syntax alone sure allows this.

This kind of abuse is prevented by bytecode verification. The Move verifier is a static analysis tool that analyzes Move bytecode and determines whether it respects the required type, memory, and resource safety rules. All code uploaded onto the chain needs to pass the verifier. When you attempt to upload a Move module onto the chain, the nodes and validators will first run it through the verifier before it’s allowed to be committed. If any module tries to go around Move’s safety rules, it will be rejected by the verifier and won’t be published. That’s why it’s not possible to break the type or resource safety rules with specially crafted bytecode — the verification will prevent you from uploading such module to the chain!

The Move bytecode and the verifier are the core novelty of Move. It’s what enables an intuitive programming model centered around resources that isn’t possible otherwise. The key thing is that it allows for structured types to be passed across trust boundaries without losing their integrity.

On Solana, smart contracts are programs, while in Move they are modules. This may seem like it’s just a semantic difference but it is not, and it carries a tremendous significance. The difference is that on Solana there’s no type safety across program boundaries — each program loads instances by manually decoding them from raw account data and this involves doing critical safety checks manually. There’s also no native resource safety. Instead, resource safety has to be implemented by each smart contract individually. This does allow for sufficient programmability but it hinders composability and ergonomics a great deal compared to Move’s model where there is a native support for resources and they can safely flow in and out of untrusted code.

In Move, the types do exist across modules — the type system is global. This means that there’s no need for CPI calls, account encoding/decoding, account ownership checks, etc. — you just call a function in another module with the arguments directly. Type and resource safety across smart contracts is guaranteed by bytecode verification at compile/publish time and does not need to be implemented on the smart contract level and then checked during runtime like on Solana.

5. Solana vs. Move

Now that we’ve seen how Move programming works and what makes it fundamentally safe, let’s take a deeper look into what kind of implications this has for smart contract programming from the perspectives of composability, ergonomics, and safety. Here I will compare Move/Sui development with EVM and Rust/Solana/Anchor in order to help appreciate what Move’s programming model brings to the table.

5.1. Flash Lending

Let’s start with the flash lending example. Flash loans are a type of loan in DeFi where the loaned amount must be repaid within the same transaction it is borrowed. The main benefit of this is that since the transaction is atomic, the loan can be fully uncollateralized. This can be used e.g. to arbitrage between assets without needing to have the principal amount to execute the arbitrage.

The main difficulty with implementing this is — how do you guarantee from within the flash loan smart contract that the loaned amount will be paid back in the same transaction? In order for the loan to be able to be uncollateralized, the transaction needs to be atomic — i.e. if the loaned amounts are not paid back in the same transaction, the whole transaction needs to fail.

EVM has dynamic dispatch so it’s possible to implement this using reentrancy as follows:

  • Flash loan user creates and uploads a custom smart contract which when called will pass control to the flash loan smart contract by calling it
  • Flash loan smart contract will then send the requested loan amounts to the custom smart contract and call the executeOperation() callback function in the custom smart contract
  • The custom smart contract will then use the received loaned amounts to execute its desired operations (e.g. arbitrage)
  • After the custom smart contract is finished with its operations, it needs to return the loaned amount back to the flash loan smart contract
  • With that, the custom smart contract’s executionOperation() will finish, and the control will be returned to the flash loan smart contract which will check whether the loaned amount has been correctly returned
  • If the custom smart contract hasn’t returned the loaned amounts correctly, the whole transaction will fail

This implements the required functionality perfectly well, but the problem is that it relies on reentrancy and we would very much like to ban it from even being possible in smart contract programming. This is because reentrancy is inherently very dangerous and was the root cause of many vulnerabilities including the infamous DAO hack.

Solana does better here as it does not allow reentrancy. But without reentrancy and the flash loan smart contract calling back into the custom smart contract being possible, how do you implement flash loans on Solana? Well, it is possible thanks to instruction introspection. On Solana, each transaction consist of multiple instructions (smart contract calls), and from any instruction you can inspect other instructions (their program ID, instruction data, and accounts) present in the same transaction. This makes it possible to implement flash loans as follows:

  • Flash loan smart contract implements borrow and repay instructions
  • Users create a flash loan transaction by stacking the borrow and repay instructions calls together in the same transaction. The borrow instruction will, when executed, check that the repay instruction is scheduled later in the same transaction using instruction introspection. If the repay instruction call is not present or invalid, the transaction will fail at this stage
  • Between the borrow and repay calls, the borrowed funds can be used arbitrarily by any other instructions that are in between
  • At the end of the transaction, the repay instruction call will return the funds to the flash lender smart contract (existence of this instruction is checked using introspection in the borrow instruction)

For the curious, there’s a prototype implementation of this (link).

The solution works well enough but it’s still not ideal. Instruction introspection is somewhat of a special-case thing and not something that’s commonly used in Solana, so its usage has overhead both in terms of the number of concepts that a developer needs to master and the technicalities of the implementation itself as there are a few nuances that need to be properly considered. There is also a technical limitation — since the repay instruction needs to be present statically in the transaction, it’s not possible to call repay dynamically during transaction execution via a CPI call. This is hardly a deal-breaker but it somewhat limits the code flexibility when integrating this with other smart contracts and also pushes more complexity onto the client side.

Edit: There’s also another approach to implementing flash lending without instruction introspection — you can have the flash lending instruction in the lending smart contract do a CPI call into your arbitrary smart contract with the funds and then after the CPI call returns it would check that the funds have been correctly returned. This approach is implemented by the SPL Lending program (link). There’s a different issue with this though — there’s no generic way to aggregate (combine) multiple loans in a single call.

Move also forbids dynamic dispatch and reentrancy but, unlike Solana, has a very simple and natural solution for flash lending. Move’s linear type system allows for creation of structs that are guaranteed to be consumed exactly once during transaction execution. This is the so-called “hot potato” pattern — a struct that has no key, store, drop, or clone capabilities. A module that implements such a pattern will usually have one function which instantiates the struct and another which destroys it. Since the “hot potato” struct doesn’t have a drop, key, or store capability, it’s guaranteed that its “destroy” function will be called in order to consume it. Even though we can pass it around to any number of other functions in any module, eventually it needs to end up in the “destroy” function. This is simply because there’s no other way to get rid of it, and the verifier requires that something is done with it by the end of the transaction (it cannot just be discarded arbitrarily since there’s no drop capability).

Let’s see how this can be leveraged in order to implement flash lending:

  • Flash lending smart contract implements a “hot potato” Receipt struct
  • When a loan is made by calling the loan function, it will send two objects to the caller — the requested funds (a Coin) and a Receipt which is a record of the loaned amount to be repaid
  • The borrower can then use the received funds for its desired operations (e.g. arbitrage)
  • After the borrower is done with its intended operations, it needs to call the repay function which will receive the borrowed funds and the Receipt as arguments. This function is guaranteed to be called in the same transaction since there’s no other way for the caller to get rid of the Receipt instance (it’s not allowed to be dropped or embedded into another object, this is asserted by the verifier)
  • The repay function checks whether the correct amount has been returned by reading the loan information embedded in the receipt.

An example implementation of this can be found here.

Move’s resource safety features make flash lending possible in Move without the use of reentrancy or introspection. They guarantee that a Receipt cannot be modified by untrusted code and that it needs to be returned to the repay function by the end of the transaction. With this we can guarantee that the correct amount of funds is returned in within the same transaction.

The functionality is fully implemented using basic language primitives and the Move implementation doesn’t suffer from integration overhead as Solana implementation does with transactions needing to be specially crafted. Also, no complexity is pushed to the client side.

Flash lending is a good example of how Move’s linear type system and resource safety guarantees allow us to express functionalities in a way not possible in other programming languages.

5.2. Mint Authority Lock

To further highlight the advantages of Move’s programming model I’ve implemented a “Mint Authority Lock” smart contract both in Solana (Anchor) and Sui Move to do a comparison.

What the “Mint Authority Lock” smart contract does is it extends the functionality of a token mint to, instead of just one, allow multiple whitelisted parties (authorities) to mint tokens. The required functionality of the smart contract is as follows (applies both to the Solana and Sui implementations):

  • The original token mint authority creates a “mint lock” which will enable our smart contract to regulate the minting of tokens. The caller becomes the admin of the mint lock.
  • The admin can then create additional mint authorities for the lock that can be given to other parties and allows them to use the lock to mint tokens when they please.
  • Each mint authority has a daily limit for the amount of tokens that can be minted by it.
  • The admin can ban (and unban) any mint authority at any time.
  • The admin capability can be transferred to another party.

This smart contract can be used e.g. to give mint capabilities for a token to some other user or smart contract while the original mint authority (admin) still retains the control of the mint. Without this we’d have to pass the full control of the mint to the other party which is not ideal since we’d have to trust it not to abuse it. And giving permission to multiple parties would not be possible.

The full implementation of these smart contracts can be found here (Solana) and here (Sui).

Note: Please don’t use this code in production! This is example code meant for educational purposes only. Although I’ve tested it for functionality I haven’t done a thorough audit or security testing.

Now let’s look at the code and see how the implementations differ. Here are the side-by-side code screenshots for the full Solana and Sui implementations of this smart contract:

What is immediately noticeable is that, for the same functionality, Solana implementation is more than twice the size compared to Sui (230 LOC vs 104). This is already a big deal since less code generally means fewer bugs and shorter development time.

So where are these extra lines coming from on Solana? If we take a closer look at the Solana code, we can group it into two sections — the instruction implementation (smart contract logic) and the account checks. The instruction implementation somewhat closely matches what we have on Sui — 136 LOC vs 104 on Sui. The additional lines can be attributed to the boilerplate of the two CPI calls (~10 LOC each). The most significant difference though is due to the account checks (section marked red in the screenshot above) which are required (critical in fact) on Solana, but not required in Move. The account checks make up ~40% of this smart contract (91 LOC).

Move doesn’t require account checks. And this is not only beneficial because of the LOC reduction. Removing the necessity of doing account checks is very significant because implementing those checks correctly has proven to be very tricky, and if you make even a single mistake there it will often lead to critical vulnerabilities and loss of user funds. In fact some of the biggest (in terms of lost user funds) Solana smart contract exploits were account substitution attacks caused by improper account checks:

Clearly, getting rid of those checks would be a big deal.

So how is Move able to do without these checks while being just as safe? Let’s take a closer look at what the checks actually do. Here are the account checks required for the mint_to instruction (authority holder calls this to mint tokens through the lock):

There are 6 checks (highlighted in red):

  1. Checks that the provided lock account is owned by this smart contract and is of the MintLock type. It’s necessary to pass in the lock because it‘s used for the CPI call to the Token Program for minting (it stores the authority).
  2. Checks that the provided mint authority account belongs to the provided lock. The mint authority account holds the authority state (its public key, whether it’s been banned, etc.)
  3. Checks that the instruction caller owns the required keys for this authority (the required authority signed the transaction).
  4. It’s required to pass in the token destination account because the Token Program will mutate it in the CPI call (add balance). The mint check is not strictly necessary here because if a wrong account is passed in the CPI call would fail, but it’s good practice to do the check nonetheless.
  5. Similar to 4.
  6. Checks that the Token Program account is passed in correctly.

We can see that the account checks (in this example) fall into these five categories:

  • account ownership checks (1, 2, 4, 5)
  • account type checks (1, 2, 4, 5)
  • account instance checks (whether the correct instance of a certain account type has been passed in) (2, 5)
  • account signature checks (3)
  • program account address check (6)

Note: This doesn’t cover all types of account checks, but it’s enough to illustrate the point.

In Move though, there are no account checks or anything equivalent required. We just have the function signature:

The mint_balance function requires only four arguments. Out of these four only lock and cap represent objects (somewhat comparable to accounts).

So how is it possible that in Solana we need to declare 6 accounts and also manually implement various checks for them, while in Move we need to pass in only 2 objects and no explicit checks are necessary?

Well, in Move, some of these checks are done transparently by the runtime, some of them are done statically at compile time by the verifier, and some of them are simply not necessary by construction. Let’s see:

  • account ownership checks — not necessary due to the design of Move’s type system. A Move struct can be mutated only through functions defined in its module and never directly. Bytecode verification guarantees that struct instances can freely flow into untrusted code (other modules) without being mutated illegally.
  • account type checks — not necessary because Move types exist across smart contracts. Type definitions are embedded in module binaries (which is what is published on the blockchain and executed by the VM). The verifier will check that the correct types are being passed when our functions are being called during compile / publish time.
  • account instance checks — in Move (and on Solana sometimes also) you’d do this in the function body. In this particular example, it’s not necessary because the generic type parameter T for lock and cap argument types enforce that the passed in cap (mint capability / authority) object correctly matches its lock (there can be only one lock per Coin type T).
  • account signature checks — we don’t directly deal with signatures in Sui. Objects can be owned by users. The mint authority is granted by ownership of the mint capability object (created by the admin). Passing a reference to this object into the mint_balance function will allow us to mint. Owned objects can only be used in a transaction by their owners. In other words, the object signature check is done transparently by the runtime.

Essentially, Move leverages bytecode verification in order to enable a programming model much more natural for programming digital assets. Solana’s model revolves around account ownership, signatures, CPI calls, PDAs, etc. But if we take a step back and think about it, we see that we don’t really want to be dealing with those. They’ve got nothing to do with digital assets in and of themselves — rather it’s that we have to use them because it’s what allows us to implement the desired functionality within Solana’s programming model.

On Solana, since there’s no bytecode verification to guarantee type or resource safety on a more granular level, you can’t allow any program to mutate any account, so introducing the notion of account ownership is necessary. For similar reasons (no type/resource safety across program calls) there’s no notion of user-owned objects that can flow in and out of programs, but instead, we deal with account signatures to prove authority. And since sometimes programs also need to be able to provide account signatures, we have PDAs…

While you can have the same cross-program type and resource safety on Solana as in Move, you have to implement it manually using low-level building blocks (account signatures, PDAs…). Ultimately what we’re doing is we’re using low-level primitives to model programmable resources (linear types). And this is what account checks are — they’re the overhead of implementing type safety and modeling resources manually.

Move has a native abstraction for resources and allows us to work with resources directly without needing to introduce any low-level building blocks such as PDAs. Type and resource safety guarantees across smart contract boundaries are asserted by the verifier and are not needed to be implemented manually.

5.3. The Limitations of Solana Composability

I want to give another example that will highlight some pain points with smart contract composability on Solana.

We’ve seen on the Mint Authority Lock example that we’re required to declare many more inputs on Solana compared to Sui (6 accounts on Solana vs. 2 objects in Sui for the mint_to call). Clearly, having to deal with 6 accounts is more cumbersome than dealing with 2 objects, especially if you consider that we also need to implement account checks for the accounts. Arguably this is still manageable, but what happens when we start composing multiple different smart contracts together within a single call?

Suppose we want to create a smart contract that does the following:

  • it owns a mint authority from the Token Mint Lock program (as described in the previous section) for a certain token mint
  • when it’s called it will use its authority to mint a user-specified amount of tokens, swap them using an AMM for a different token, and send them to the user all in the same instruction

The point of this example is to illustrate how the Mint Authority Lock smart contract and an AMM smart contract would be composed together. This is a purely hypothetical example that probably doesn’t have any use in real life, but will serve to illustrate the point — real-life examples of composing smart contracts aren’t too dissimilar from this.

The account checks for an instruction call that does this could look something like this:

That’s 17 accounts. We have about 5–6 per CPI call (mint and swap) plus program accounts.

On Sui the signature of an equivalent function would be this:

That’s only 3 objects.

So how can it be that we’re passing so much less objects on Sui vs. accounts on Solana (3 vs. 17)?

Fundamentally, the reason is because in Move we’re able to embed (wrap) them. The safety guarantees of the type system allow us to do this.

Here’s a comparison between what a Solana account and a Sui object that hold the state of an AMM pool could look like:

We can see that on Solana we store addresses (Pubkeys) of other accounts which are like pointers and don’t store the actual data. In order to access these accounts they need to be passed in separately and we also need to manually check that the right accounts have been passed in. In Move, we’re able to embed structs into each other and access their values directly. We can mix and match types from any module while they preserve their resource and type safety guarantees. This is again possible thanks to Move’s global type system and resource safety enabled by bytecode verification.

But the main issue with passing around a large number of accounts is not even developer ergonomics. Having to pass around (and thus check) many accounts when composing multiple smart contracts creates a considerable implementation complexity and has security implications. The relationships between these accounts may be quite intricate and at a certain point it becomes quite difficult to keep track of all the necessary account checks and whether they have been done correctly.

In fact, this is what I believe happened in the Cashio exploit ($48M). Here’s a breakdown of the (insufficient) account checks that enabled the exploit. As you can see, these account checks can get somewhat intricate. The developer probably had the best intentions to do the checks properly, but at a certain point the mental overhead becomes too big and it becomes very easy to slip up. The more accounts there are, the higher the chance of having a bug.

Move’s global type system and a more natural programming model mean that we can push smart contract composition much further with more safety before we hit the limits of mental overhead.

As a side note, there’s another thing worth considering in terms of the security of Move compared to Rust/Anchor that may not be immediately obvious. This is that the TCB (trusted computing base) for Move is much smaller than Rust/Anchor. Smaller TCB means that fewer components that go into smart contract compilation and execution have to be trusted. This reduces the surface area for vulnerabilities that could affect smart contracts — bugs outside of TCB don’t affect smart contract safety or security.

Move has been designed with reducing TCB in mind — a number of decisions were made to reduce the TCB as much as possible. The bytecode verifier takes many of the checks performed by the Move compiler out of the TCB, whereas in Rust/Anchor many more components have to be trusted and the surface area for security-critical bugs is much larger.

6. Move on Solana

Clearly, Move is amazing. We also know that Solana has been designed to allow other programming languages to be used to develop smart contracts for it. The question now is — can we have Move on Solana and how?

6.1. Anchor with Global Type Safety?

Before we start looking into Move on Solana, let's take a brief look into Anchor and do a little thought experiment. Maybe we can somehow upgrade Anchor to provide some of the benefits we get from Move? Maybe we can get native support for type safety across program calls? After all, Anchor instructions already resemble Move entry functions:

// Function signatures of the "mint to" function from the
// "Mint Authority Lock" smart contract from the previous chapter.
// Sui Move
public entry fun mint_balance<T>(
lock: &mut TreasuryLock<T>,
cap: &mut MintCap<T>,
amount: u64,
ctx: &mut TxContext
) {
// Anchor
pub fn mint_to(ctx: Context<MintTo>, amount: u64) -> Result<()> {

Maybe if we somehow extend Anchor to allow for accounts to directly be passed in instruction arguments:

// Anchor modified
pub fn mint_to(
lock: &mut MintLock,
authority: &mut MintAuthority,
amount: u64
) -> Result<()> {

we could avoid doing account checks?

In this case, we want the type checks to be done by the runtime instead of the program — the runtime would read Anchor’s account discriminators (or equivalent) and be able to check that the passed in account matches the required discriminator (first 8 bytes of an Anchor account).

But remember that Solana doesn’t discriminate between different instruction calls into the same program, this is implemented manually by the program (in this case the heavy lifting is done by Anchor). So in order to do this, the runtime would somehow have to know about the different instructions, their signatures, and have information on the types also.

Solana programs compile to SBF (Solana Bytecode Format, a modification of eBPF) and are uploaded to the chain (and executed) as such. And SBF itself doesn’t embed any type or function information that could help us here. But maybe we could modify SBF to allow for instruction and type information to be embedded in the binary? Then the required information about instructions and signatures could be read by the runtime from the binary.

We could indeed do that. It would be a considerable engineering undertaking to implement this, especially if you consider that we need to keep backwards compatibility with old programs, but here are the benefits that we would get:

  • Account ownership and type checks are done by the runtime instead of in the program.
  • For accounts with addresses known at compile time (e.g. program accounts) we can avoid passing them in from the client, these can instead now be injected by the runtime.
  • If we also somehow manage to embed account constraints into the binary, we can further reduce the number of accounts that have to be passed in by the client by dynamically loading them recursively with the runtime (based on the embedded constraints info).

We still don’t get:

  • Embedded accounts. We still have to use Pubkeys to reference other accounts instead of being able to embed them directly. This means that we don’t get rid of account bloat described in section 5.3.
  • When doing cross-program calls, the account type checks still need to be done dynamically during runtime instead of statically at compile time like in Move

Note: Keep in mind this is all just a thought experiment. I’m not claiming that this can be done safely, how difficult it would be to implement, or that the benefits would be worth the engineering effort.

While these benefits indeed are nice, they don’t fundamentally change much from a smart contract development perspective. Doing type checks in the runtime instead of programs probably has some performance benefits and not having to pass accounts with addresses known at compile time manually from the client improves ergonomics somewhat (this can also be alleviated with tooling). But while these ergonomics and performance improvements do help, we’re ultimately still dealing with Solana’s programming model which in and of itself doesn’t offer much to help us work with digital assets — we still don’t have native resource safety, we can’t embed accounts so there’s still account bloat, we’re still dealing with account signatures and PDAs…

Ideally, we’d want all smart contracts to live within a single type system and be able to pass objects in and out of them freely like in Move. But because the other smart contracts cannot be trusted we can’t do that directly. To go around this, Solana has program separation and account ownership — each program manages its own accounts and they interact via CPI calls. This is safe and allows for sufficient programmability, but the resulting programming model is not ideal — there’s no global type system and without it no meaningful resource safety also.

We want a natural programming model but at the same time, we’re dealing with untrusted code. While on Solana we can work with untrusted code safely, it compromises on the programming model. Bytecode verification is what makes it possible to have both at the same time. So it really does seem like without it we can’t improve the programming model much…

6.2. The Solana Bytecode Format

As mentioned earlier, SBF (Solana Bytecode Format), what Solana smart contracts compile to and are stored on-chain as, is based on eBPF. The main motivation behind using eBPF on Solana as the base instead of any other bytecode format (like WASM) was that the requirements for safe and performant smart contract execution Solana has, match the requirements for sandboxed program execution within the kernel that eBPF was designed for (it also needs to be safe and performant). You can read more about the initial design decisions behind the Solana smart contract environment in this article from Anatoly Yakovenko (note that this article is from 2018 and some of the things discussed there are outdated).

On paper, eBPF does seem like a solid choice. It’s performant, it’s designed around safety, the program size and number of instructions are limited, it has a bytecode verifier… Looks promising!

But let's see what this means in practice. Maybe we can leverage the eBPF verifier in some way to improve the safety of our smart contracts? Here are some of the things that the eBPF verifier does:

  • disallows unbounded loops
  • checks that the program is a DAG
  • disallows out-of-bounds jumps
  • checks argument types when doing various helper function calls (the helpers are defined in the kernel, used e.g. to modify a network packet)

Well, disallowing out-of-bounds jumps seems useful, but the other stuff not so much. In fact, enforcing that the program must be a DAG and has no unbounded loops is problematic as it significantly limits programmability (we don’t have Turing completeness). The reason this is needed in eBPF programs is that the verifier needs to determine the program terminates within a certain amount of instructions (so that a program cannot halt the kernel; this is the famous halting problem), and gas metering is not an option because it would hinder performance too much.

While this trade-off is great for implementing a high-performance firewall, it’s not that great for smart contract development, and a large majority of the eBPF verifier cannot be reused for Solana programs. In fact, Solana doesn’t even use the original eBPF verifier at all. It uses a (more basic) custom verifier that mainly just checks that instructions are correct and for out-of-bounds jumps.

Aside from the verifier, there are a few other eBPF specifics that are a bit problematic for compiling smart contracts. Like the fact that eBPF by design allows at most 5 arguments to be passed to a function call. This effectively means that Rust standard library cannot be compiled to eBPF directly. Or that the stack size is limited to 512 bytes which reduces the size of arguments we can pass to a function without heap allocating.

So even though Rust compiles to LLVM, there is an eBPF backend for LLVM, and there’s even support for the Rust compiler to target eBPF, you still won’t be able to enable Solana smart contracts to compile to eBPF as it is. That’s why the Solana team had to make multiple changes both to the Rust codebase and the eBPF LLVM backend (e.g. Support passing arguments via the stack).

Since some of these changes are inherently not upstream-able (neither to Rust nor the LLVM), the Solana team currently maintains forks of both Rust and LLVM with those changes. When you do cargo build-bpf (the canonical command to build Solana smart contracts), Cargo will pull this Solana-specific version of rustc to do the smart contract compilation (the original rustc wouldn’t work).

And that’s how SBF came to life — some of the requirements that Solana needs are not compatible with eBPF. Solana team is currently working towards upstreaming SBF as a separate LLVM backend and adding it as a Rust target in order to avoid having to maintain separate forks.

For the curious, here are some of the relevant GitHub discussions:

So while eBPF can work as a format for smart contracts, it is not as ideal as it seems on paper. It needs to be patched up a bit and the original verifier is not of great use.

There’s also a misconception that I’ve seen floating around in the discussions on Move and Solana/SBF that I feel is very important to address. Some comments are being made that the main Move ideas should work for SBF because it’s based on eBPF and its verifier could potentially be leveraged to make the account mutation checks be done statically instead of dynamically in the runtime.

This is a dubious statement in my opinion. Even if it’s possible to prove that programs don’t mutate accounts they don’t own in eBPF, and Move indeed does that sort of thing, it is certainly not the *main* Move idea.

The main Move idea is to enable a programming model centered around resources that can interact with untrusted code in a natural way.

In practice this means:

  • global type safety
  • resource safety (key, clone, store, drop)
  • embeddable resources
  • resources flowing in and out of untrusted code safely

The sections in chapter 5. (Flash Lending, Mint Authority Lock, The Limitations of Solana Composability) illustrate how significant this is in terms of smart contract safety, composability, and ergonomics. The impact of this goes far beyond skipping some runtime safety checks to improve the performance of cross-program calls. This is not to be underestimated.

Now, if you’re wondering how difficult it would be to bring the main Move ideas to eBPF/SBF, the answer is — very difficult. Enforcing properties like “this untrusted code shouldn’t be able to drop a T” will not be possible without significant modifications to eBPF. In fact, it will require so much modification that you will end up with a new bytecode that looks more like Move than eBPF. This would certainly be a big research undertaking.

In fact, a similar line of thinking is what led to the creation of Move in the first place. The Move team (then at Diem) initially considered starting from other formats like WASM, JVM, or the CLR, but it’s just too hard to add this after the fact — linearity/abilities are highly unconventional. So Move was designed from scratch with the idea of efficiently enforcing these checks with lightweight verifier passes.

And if you think about it, this is actually not so surprising. At the end of the day, smart contract programming is not systems programming, backend programming, or any kind of other conventional programming. It’s an entirely different type of programming so it’s not surprising that the features of existing bytecodes and instruction formats cannot be reused here since they’ve been designed with entirely different use cases in mind.

To be perfectly clear, I’m not criticizing Solana for going with eBPF. In fact, I think it’s a really solid choice and good judgment from the team considering the context. Arguably, and in hindsight, the team might have gone e.g. with WASM instead of eBPF which would have avoided the issues with compiling smart contracts to eBPF mentioned earlier since WASM has first-class support in Rust (there could be different issues with WASM though), but I can see how the team could have felt that eBPF was a safer choice considering the emphasis on performance. Also, Move wasn’t even announced when these design choices were being made, and creating a new language from scratch would certainly not be a reasonable option for a startup. At the end of the day, Solana managed to deliver a successful high-performance L1 and that’s what ultimately matters.

6.3. Running Move on Solana

Extending eBPF/SBF to support Move features seems difficult and we would probably end up with something similar to Move bytecode anyways. Instead of trying to improve SBF, maybe we should just somehow get Move to run on Solana directly? After all, Solana is very open to supporting multiple programming languages for smart contract development, and even Anatoly is encouraging for Move to be integrated in some of his tweets.

There are seemingly three ways to get Move on Solana:

  1. Add the Move VM as a native loader (alongside the SBF VM)
  2. Run the Move VM as a program (like Neon)
  3. Compile Move to SBF (like Solang)

Let’s discuss (3) first. The idea here is to build an LLVM frontend for Move in order to compile it to SBF. Move smart contracts compiled to SBF could be executed transparently the same way that smart contracts built in Rust (or any other language that compiles to SBF) do, and the runtime wouldn’t have to have any distinction or knowledge about Move. This would be a really elegant solution from the perspective of the runtime as it would require no changes to it or its security assumptions.

But from the smart contract side, this does not have much impact. While there are genuine reasons to build an LLVM frontend for Move, e.g. for performance or portability, doing it to execute Move smart contracts using the SBF runtime doesn’t bring much merit in my opinion. In fact, I would argue that developing smart contracts this way would be worse than just using Anchor.

What you get with (3) is Move syntax within Solana’s programming model. This means that all of the significant benefits of Move discussed in chapter 5. (global type safety, global resource safety, embeddable objects…) would not be there. Instead, we would still have to deal with account checks, CPI calls, PDAs, etc. just the same as in Rust. And since Move doesn’t support macros, implementing a framework like Anchor with an eDSL to streamline some of this would not be possible so the code would be similar to raw Rust (but probably worse). The Rust standard library and ecosystem are also not available so things like account serialization and deserialization would have to be re-implemented in Move.

Move is not very amenable to be used with other programming models. This is because it has been specially designed to be able to compile to the Move bytecode and pass the verifier. This is necessary due to all the custom rules around abilities and the borrow checker. The bytecode verification that is done is so specific that other languages have almost no chance of compiling to the Move bytecode and passing the verifier. Because Move has been designed around this very specific bytecode verification, it is not as flexible as Rust for example.

Stripping away the bytecode gives up all of the main benefits of Move. As the “Resources: A Safe Language Abstraction for Money” paper states:

The distinguishing feature of Move is an executable bytecode representation with resource safety guarantees for all programs. This is crucially important given the open deployment model for contracts — recall that any contract must tolerate arbitrary interactions with untrusted code. Source-level linearity has limited value if it can be violated by untrusted code at the executable level (e.g., untrusted code that duplicates a source-level linear type).

While the type, resource, and memory safety features of Move would be preserved on the program level, they wouldn’t be preserved globally. And program-level safety doesn’t bring much new — we already have this with Rust to a large degree.

Move’s ecosystem of smart contracts also couldn’t be used on Solana — the programming models are so different that significant parts of smart contracts would have to be rewritten.

Considering all of this, I expect that the usage of Move implemented with (3) wouldn’t catch on— it would be just too cumbersome to use compared to Anchor. It would probably be even more cumbersome than raw Rust in some ways.

As for (1), the idea here is to (alongside the SBF loader) add support for a Move loader in the runtime. Move smart contracts would be stored as Move bytecode on-chain and executed by the Move VM (just like in Sui). This means that we would have an ecosystem of SBF smart contracts and an ecosystem of Move smart contracts where the former would operate on the current Solana programming model, while the latter on an (arguably superior) Move model.

With this approach, it would be possible to keep all the benefits of Move for the Move smart contracts interacting with each other, but a big difficulty here is to get the Move smart contracts to be able to interact with the SBF smart contracts and vice versa. Implementing this would be challenging — you need someone who deeply understands Move and Solana. The verifier would also have to be adapted.

There’s also the downside of needing to maintain two different loaders in the runtime. This has security implications as it means that the attack surface is twice the size — a bug in any loader could mean that the whole chain gets exploited. As a side note, early support for the Move VM was actually added to Solana back in 2019 (#5150) but has later been removed (#11184) due to security concerns (see this thread).

As for (2), the idea is to run the whole Move VM as a Solana program (smart contract). The Move VM is implemented in Rust so it might be possible to compile it to SBF (unless it uses threads or some other unsupported API). As crazy as this sounds, a similar approach has been implemented by Neon to run the EVM as a Solana program. The benefit of this approach is that no changes need to be made to the runtime and it can keep the same security assumptions.

I’m not familiar with the technical details of the Move VM so I can’t comment much on the feasibility of this and what limitations it would have. The first thing that comes to mind is that the verifier would also have to be run as a program which means within the compute budget. This approach would also suffer from the same interoperability issues between SBF and Move smart contracts as (1).

There’s no straightforward way to bring the main features of Move to Solana. While it is possible to build an LLVM frontend and compile Move to SBF, this is not going to do much as the programming model will remain the same. As illustrated by the thought experiment in section 6.1., there’s not much you can do to improve the programming model without some kind of bytecode verification. Changing eBPF/SBF to support bytecode verification would be very difficult. It seems like the only reasonable option is to somehow get the Move VM to run. But this means that there’s going to be two ecosystems operating on different programming models, and getting them to interoperate properly is very challenging.

6.4. On Move Performance

The Move bytecode is not a general bytecode language. It has a very opinionated type system and in order to allow for all the necessary verification it is quite high level. This could mean lower performance compared to other bytecode formats such as eBPF/SBF which are much closer to native code, and one may argue that this would be an issue for the use in high-performance L1s.

But smart contract execution has not been a bottleneck so far neither on Solana (which at the time time of writing is averaging out at 3k TPS) nor Sui (based on the initial e2e benchmarks done by the team). The main driver for increasing performance of transaction processing is parallelized execution. Both Solana and Sui implement this by requiring dependencies to be declared a priori and scheduling the execution of transactions that depend on different sets of objects/accounts in parallel. Thanks to this and because there are bottlenecks in other places, namely the networking layer, transaction execution is, in terms of TPS, still orders of magnitudes away from showing up on the critical path.

Furthermore, there’s nothing preventing Move from being AOT compiled or JIT-ed to increase the performance once the TX execution does show up on the critical path. This is where building an LLVM frontend for Move would be beneficial. Also, further optimizations unique to Move can potentially be gained thanks to Move’s inherent amenability to static analysis.

Considering all of this, I expect that Move's performance will not be a significant blocker for the foreseeable future.

7. Other Move Features

In this chapter I will describe a few other features of Move that are perhaps not central to the discussion in this article but are still relevant.

7.1. The Prover

Move has a formal verification tool for smart contracts called the Move Prover. With this tool, you’re able to assert whether different invariants hold for your smart contract or not. Behind the scenes, the verification conditions are translated into SMT formulas and then checked using an SMT solver. This is very different e.g. from fuzz testing where it’s trial and error by walking the input space. For example, fuzz and unit/integration tests can still provide a false positive if they fail to test a specific input or a combination of inputs that would show that a program has a bug. The Prover on the other hand essentially provides formal proof that the specified invariants hold for the provided program. This is like checking the program against all possible inputs but without having to do so.

The Move Prover is very fast so it can be integrated into the regular development workflow just like a type checker or a linter would.

Here’s an example of what a prover spec looks like (taken from the “Fast and Reliable Formal Verification of Smart Contracts with the Move Prover” whitepaper):

[…] This adds the specification of the transfer function, a helper function bal for use in specs, and two global memory invariants. The first invariant states that a balance can never drop underneath a certain minimum. The second invariant refers to an update of global memory with pre and post state: the balance on an account can never decrease in one step more than a certain amount.

For more details on the Prover, I recommend the following whitepapers:

7.2. Wallet Safety

Since Sui requires that all objects that a transaction will access are passed in the function arguments (no dynamic loading from the global state), and Move function signatures along with type information are stored in the bytecode itself, we can have wallets provide more meaningful information to the user about what the transaction will do before the user signs it.

For example, if we have a function with the following signature:

public entry fun foo(
asset1: &Asset,
asset2: &mut Asset,
asset3: Asset
)

we can tell just from the function signature that this transaction will access 3 of the user’s assets (of type Asset). Not only that, but based on the & and &mut keywords (and lack thereof) we can also tell that the asset1 can be just read from, asset2 can be mutated (but not transferred or destroyed), while asset3 can potentially be mutated, transferred, or destroyed.

A wallet can display this information to the user who then has a more meaningful insight into what the transaction might do with the assets. If something doesn’t look right, e.g. a transaction call coming from a web3 app is touching some assets or coins that it shouldn’t, the user can observe this and decide not to proceed with the transaction. There’s a great article from

covering this very topic (link).

Wallets can additionally simulate transactions too which would give even more information to the users about its effects. Sui’s object-centric programming model and the fact that type information is native to the runtime means that it’s possible to interpret object changes without having any specific application-level knowledge of the smart contract.

This is not possible on Solana for example because from the runtime’s perspective accounts contain arbitrary data. You would need an external description of the accounts (application-specific) to be able to interpret them, which may or may not be made available by the smart contract publisher. Also a notion of asset ownership doesn’t exist in the Solana runtime and each smart contract needs to implement this semantic manually (normally using account signatures and PDAs) which means that there’s no generic way to track this.

7.3 Simple and Complex Transactions

Specific to Sui, there’s an interesting optimization on the consensus level that allows for certain types of transactions to forgo full consensus and instead be committed using a simpler algorithm based on Byzantine Consistent Broadcast. The advantage is that these transactions can be parallelized on the consensus level eliminating head-of-line blocking and committed with near instant finality — achieving basically web2 scalability.

This is possible due to Sui’s distinction between owned and shared objects (see section 3.1.). Transactions involving only owned objects (referred to as simple transactions) do not require full consensus on Sui. Since owned objects can’t be used in a transaction by anyone else but the sender and the sender can only send one transaction at a time, this inherently means that these transactions don’t have to be ordered in reference to other transactions (total vs causal ordering) — we know for a fact that the objects referenced in the transaction cannot be affected by other transactions and also that this transaction cannot affect other objects. Thus we don’t care about ordering this transactions with respect to other transactions happening in parallel on the chain — it’s effectively irrelevant. Sui is able to leverage this fact to greatly optimize the processing of simple transactions achieving finality within a few hundred ms. The downside is that senders are allowed to send only one transaction at a time. Transactions involving any number of shared objects (referred to as complex transactions), on the other hand, always require full consensus.

Considering that creation, transfers, and modifications of owned objects can be done fully with simple transactions, certain types of applications can leverage simple transactions really well. Good examples are NFTs (including mass minting) and web3 gaming. These use cases benefit a lot from low latency finality and elimination of head-of-line blocking achieving better user experience and scalability. A more comprehensive list of single-writer-friendly apps can be found here.

Some other types of apps must rely on complex transactions though. This includes most of DeFi apps. For example, an AMM liquidity pool would need to be a shared object because any kind of exchange order execution requires full consensus and total ordering. It’s because, fundamentally, if multiple orders come from different users at the same time, we need to agree on whose order will be executed first, and that determines what execution price each user will get.

Then there are applications that can use a mix of simple and complex transactions. These applications require complex transactions to be able to achieve their desired functionality but can leverage simple transactions for certain operations for better efficiency. A price oracle can be designed like this for example. We can have multiple publishers submit price data for a market using simple transactions and then an authority to aggregate the prices (e.g. stake weighted median) using a complex transaction. It’s not possible to implement a price oracle without relying on complex transactions at some point (fundamentally because using the published prices in other transactions requires agreement on ordering and thus a full consensus), but at least we can optimize the publisher writes with simple transactions.

The Sui docs have more details about simple and complex transactions:

8. Final Remarks

This article is a deep dive into Solana’s and Sui’s programming models, how they compare, and the Move programming language.

Chapter 2. is a summary of Solana’s programming model while chapter 3. introduces Sui Move and its programming model. Chapter 4. then goes on to explain how the type and resource safety work in Move. The significance of Move’s features on smart contract development is not immediately obvious, so in chapter 5. a more thorough comparison between Solana and Sui Move is done using real-life examples. Chapter 6. discusses eBPF/SBF and shows that getting Move features or Move itself to work on Solana is no easy task. Chapter 7. touches on a few other Move features.

Smart contract programming is about programming digital assets. And it’s safe to say that this is a new type of programming that is distinct from other types of programming we’ve seen so far (e.g. systems, backend…). Because of that, it’s not surprising that the existing programming languages and programming models don’t fit this use case very well.

The crux of the issue is that we want a programming model that is natural for working with resources but at the same time we’re interacting with untrusted code. Solana does a compromise here where it does enable smart contracts with the necessary programmability in an untrusted environment, but its programming model is not very natural for programming with resources. Bytecode verification is what makes it possible to have both. In a way, it turns untrusted code into trusted.

Move is a novel programming language for smart contract development. The core novelty of it is its bytecode which is purposefully designed to be amenable to verification. While bytecode verification itself is not a novel concept, the kind of verification that Move does is. Through its bytecode and verification, Move enables a smart contract programming model that has first-class support for resources and can guarantee safe programmability in an untrusted environment.

The implications of this on smart contract development are not obvious at first glance but, as illustrated in chapter 5., they are indeed very significant in terms of smart contract ergonomics, composability, and safety. Move presents a significant step-up over Solana’s Rust-based programming model.

So much so that I would argue that Move will do for smart contract development what React did for frontend development, and saying that “what you can do with Move you can do with Rust” is akin to saying “what you can do with React you can do with jQuery”. Of course, it’s possible to implement a jQuery-based app that is equivalent to a React app, but it’s not practical. React introduced the concept of virtual DOM which is totally transparent to the developer but allows for much faster, scalable, and simple development of frontends. In a similar manner, Move’s bytecode verification is an underlying technology that is also totally transparent to the developer, but one that offers a more ergonomic, composable, and safer smart contract development. Due to its safety and a more intuitive programming model, Move also considerably lowers the bar to entry for smart contract developers.

If Move manages to gain traction (which there are early signs that it will), it could pose a considerable threat to Solana. This is because of two reasons.

The first is that the development times for Move smart contracts are much faster. It appears that developing a smart contract from scratch in Move could be up to 2–5 times faster than in Rust. This is especially true when composing smart contracts which is trivial in Move but can be complex on Solana. Because of that, the development of the Move ecosystem could outpace Solana. Due to the open and permissionless nature of blockchains, there are no strong lock-in effects and the liquidity can easily move around. Solana developers could be forced to adopt Move purely because of economical reasons — you can either switch to Move or get outpaced by Move developers who develop safer smart contracts faster. If you’re hiring a smart contract developer, you can either hire a Rust developer which will build one smart contract or a Move developer which will build two that are safer in the same amount of time. This is similar to the effect React had on frontend development.

The second reason is that the bar to entry for Move is much lower than Rust or Solidity. Because Move syntax is much simpler and the programming model is more intuitive, there’s a whole class of developers who wouldn’t be able to do smart contract development in Rust or Solidity but might be able to in Move. And since there are fewer concepts to learn, onboarding a non smart contract developer to Move is much easier than onboarding them to Rust (which is a complex language on its own, plus the Solana concepts like PDAs which cause a lot of confusion for beginners) or Solidity (where you need to be familiar with very subtle details of the language like reentrancy in order to be able to develop safe smart contracts). Even if the existing Solana and Solidity developers don’t switch to Move, the market of developers that have not yet entered the space is orders of magnitude greater than the number of existing developers in the space. Since Move has a lower bar to entry and allows for faster development, it has a much better product market fit than Rust or Solidity and could take a much larger slice of that pie. If new developers start coming in en-masse I would expect them to start on Move rather than Rust or Solidity. This is again similar to what happened with React in the web industry.

Because of that I fully expect that a first-class support for Move will be added to Solana in the mid to long-term. But this is not an easy task. In order to get the main benefits of Move, the Move bytecode needs to be natively supported. This means that simply compiling Move to eBPF/SBF is not going to cut it (see section 6.3.). In order to keep the existing ecosystem, both runtimes will need to be supported. The main technical challenge is to enable a proper interoperability between the runtimes. This requires deep knowledge both of Move and Solana so I expect that it would require the Solana team to drive this directly with support from someone on the Move team.

Move originated at Meta (née Facebook) for the Diem project. The Move team led by Sam Blackshear was tasked to figure out what to do about smart contracts. After looking more closely at the problem they’ve seen that smart contract programming is all about programming digital assets (resources), but none of the existing languages natively support that use case. The decision was made to build a new programming language from scratch.

I’d like to emphasize that the decision to create a new language is not at all an obvious one as it takes years of engineering effort to get it off the ground and in most cases it’s better to use existing solutions. The Move team had correctly foreseen that a smart contract language that is safe, has first-class support for resources, and at the same time is flexible enough can be built and this alone displays a high degree of expertise. It was a bold move by the team and the engineering leadership at Novi/Meta who supported the project (there would be a number of directors and VP’s involved in such a decision). Meta since shut down their Diem efforts and at the end wasn’t able to reap the results of its investment in Move, but it’s a great contribution to the broader crypto community nonetheless.

All in all, Move is an amazing piece of technology that I believe will have a tremendous impact on how we develop smart contracts. Well done Move team!

--

--