BTC
ETH
HTX
SOL
BNB
View Market
简中
繁中
English
日本語
한국어
ภาษาไทย
Tiếng Việt

In-depth analysis of the development of smart contracts: a comparative study of Move and Rust

链捕手
特邀专栏作者
2022-09-08 08:40
This article is about 22576 words, reading the full article takes about 33 minutes
​In-depth study of the emerging smart contract programming language Move, compared with the existing Rust-based model on Solana.
AI Summary
Expand
​In-depth study of the emerging smart contract programming language Move, compared with the existing Rust-based model on Solana.

Original title: "Smart Contract Development—Move vs.Rust

Compilation of the original text: Guo Qianwen, Chain Catcher

Compilation of the original text: Guo Qianwen, Chain Catcher

first level title

1 Introduction

Recently, discussions about Aptos and Sui have been in full swing. The two are emerging high-performance L1 public chains. The Move smart contract programming language is an indispensable part of these new chains. Some developers are actively turning to Move, declaring it the future of smart contract development. Others were more cautious, arguing that Move couldn't offer much that was too new compared to existing programming languages.

Encryption investors are also wondering how the uniqueness of these L1 public chains can compete with Solana, which is currently the main player in high-performance L1 and is known for using Rust as smart contract programming.

But the discussions we have seen so far have not reached a certain depth, and we can truly understand the impact of these new technologies on us. This applies at both poles of the discussion - Move skeptics dismiss Move as nothing and fail to appreciate its subtler (but important) side, but Move proponents, overhyping Move, fail to see what it is make it great. This brings about a huge middle ground and ambiguity, causing outside spectators, encryption developers, and investors to pay attention to this topic, but they are not sure of their own views.

In this post, I'll do a deep technical dig into Move, its novel programming model, the Sui blockchain and how it leverages the power of Move, and how it compares to Solana and its programming model. To highlight Move's features, I'll compare Solana/Rust to Sui/Move. Because understanding is easier when you compare one thing to another with which you are already familiar.There are other variants of Move, such as Aptos Move, which are slightly different in some ways. The point of this article is not to discuss the subtle differences between the different variants of Move, but to show the general advantages of Move and how it compares to the Solana programming model. So for simplicity, I'm only using one variant (Sui Move) in this article. Therefore, I amBut even so, all of the main benefits of Move discussed in this article apply to all Move integrations (which natively support Move bytecode), including Aptos. I chose Sui just because I am more familiar with it, and I think it is more intuitive and easier to present in the form of an article.

first level title

2. Solana programming model

On Solana, programs (smart contracts) are stateless and cannot themselves access (read or write) any state that persists across transactions. In order to access or maintain state, programs need to use accounts. Each account has a unique address (the public key of the Ed25519 key pair), which can store arbitrary data.

We can think of Solana's account space as a global key-value store, where the key is the account address (pubkey) and the value is the account data. Programs operate on 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, that program is allowed to modify its data. Programs cannot modify accounts they do not own (but are allowed to read them). During operation, this kind of dynamic check can be performed by comparing the account status before and after the execution of the program. If there is any illegal modification, the transaction will fail.

Each account also has a private key associated with it (the corresponding public key is its address), and users who have access to this private key can use it to sign transactions. Using this mechanism, we implement permissions and ownership functions in Solana smart contracts - for example, in order to obtain certain funds, smart contracts can require users to provide the necessary signatures.

When calling other programs, the customer needs to specify which accounts the program will access when calling. In this way, the transaction processing runtime can schedule non-overlapping transactions to execute in parallel while ensuring data consistency. This is one of the design features of Solana that makes it high throughput.

Programs can call other programs through CPI calls. These calls work basically the same as calls from the client - the caller program needs to specify the account that the callee program will access, and the callee program will do the input checks, just like calling from the client (since it doesn't trust the caller program).

These are the fundamental building blocks of secure smart contract programming on Solana. To some extent, you can think of Solana programs as programs in the operating system, and accounts as files. Anyone can freely execute any program, and even deploy their own programs. When programs (smart contracts) run, they read and write files (accounts). All files can be read by all programs, but only programs that have ownership rights to the files can write to them. Programs can also execute other programs, but they don't have any trust in each other -- whoever executes a program needs to assume that the input is potentially malicious. Since the OS is globally accessible to anyone, native signature verification support was built into the program to implement permissions and ownership functions for the user... Not a perfect analogy, but kind of interesting.

first level title

3. The programming model of Move

In Solana, this is equivalent to publishing all smart contracts as modules in a program. This means that all smart contracts (modules) are included in the same class system and can call each other directly without going through an intermediate API or interface. This is very important and its implications will be thoroughly discussed in this article.

secondary title

3.1 Object

Note that the object concept below is specific to the Sui variant of Move. In Move's other integrations (such as Aptos or Diem/core Move), the situation may be slightly different. However, there are similar solutions in other Move variants that achieve the same thing (persistence of state), which are not much different.

The main reason for introducing the Sui variant here is that the code samples later in the article are all based on the Sui variant of Move, and its objects such as the global storage mechanism in core Move are more intuitive and easy to understand. Importantly, all of the main benefits of Move discussed in this article apply to all Move integrations (which support Move bytecode natively), including Aptos.

Objects are struct instances 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 that belong to the user. Only users who own the object can use it in transactions. Ownership metadata is completely transparent and is handled by the runtime. It is implemented using public key cryptography - each own object is associated with a public key (stored in the object metadata at runtime), and any time you want to use the object in a transaction, you need to provide the corresponding signature (now supports Ed25519, which will soon support ECDSA and K-of-N multi-signature).

Shared objects are similar to owned objects, but they do not have an owner associated with them. Therefore, you don't need to own any private keys to use them in transactions (anyone can use them). Any own object can be shared (by its owner), and once an object is shared, it remains shared forever - it can never be transferred or become an own object again.

The Move programming model is very intuitive and simple. Each smart contract is a module consisting of function and structure definitions. Structures are instantiated in functions and can be passed to other modules through function calls. To make a structure persistent across transactions, we make it an object that can be owned, shared, or immutable (Sui only, slightly different in other Move variants).

first level title

4. Security of Move

  • We have seen that in Move:

  • You can pass any object you own (or share) to any function in any module

  • Anyone can publish a (potentially hostile) module

There is no notion of a module owning a structure, which would give the owner module the sole power to change the structure, as is the case with Solana accounts - structures can flow into other modules, or be embedded in other structures.

The question is, why is this practice safe? What's stopping people from posting a malicious module, taking a shared object (like an AMM pool), sending it into a malicious module, and continuing to drain their funds?

That's the novelty of Move...let's talk resources.

secondary title

4.1 Structure

image

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

  • So far so good - it's also how you define a struct in Rust. But in Move, structs are unique. Compared with traditional programming languages, Move modules have more space in how to use types. The structure defined in the code snippet above will be subject to the following restrictions:

  • It can only be instantiated ("packed") and destroyed ("unpacked") within the module that defines the struct -- that is, you cannot instantiate or destroy a struct instance from any function in any other module.

  • Fields of a struct instance can only be accessed (and thus can be changed) within its module.

  • A struct instance cannot be cloned or copied outside of its module

cannot store a struct instance in a field of another struct instance

This means that if we deal with an instance of this struct in a function of another module, we cannot mutate its fields, clone it, store it in another struct's field, or discard it (it has to be passed through a function call to other places). Here's the thing: the modules of the structure implement functions that can be called from our modules to do these things. But other than that, we can't do these things directly for external types. This gives the module complete control over how its types are and are not used.

We seem to lose a lot of flexibility due to these constraints. This is also true - in traditional programming, dealing with such structures would be very cumbersome, but in fact, this is exactly what we want in smart contracts. Smart contract development is, after all, about the programming of digital assets (resources). If you look at the structure described above, that's exactly what it is - it's a resource. It cannot be created out of thin air at will, it cannot be copied, and it cannot be accidentally destroyed. So we do lose some flexibility here, but the flexibility we lose is exactly what we want, as this makes manipulating resources intuitive and safe.

image

Furthermore, Move allows us to relax some of these limitations by adding capabilities to the structure. There are four capabilities: key, store, copy and delete. You can add any combination of these abilities to a structure.

  • Here's what they do:

  • Keys - Allows a struct to be an object (exclusive to Sui, the core Move case is slightly different). As mentioned earlier, objects are persistent, and if they are self-owned objects, they need to be signed by the user before they can be used in smart contract calls. When using the key capability, the first field of the structure must be an object ID with type UID. This will give it a globally unique ID that can be referenced by.

  • storage - allows embedding the struct as a field in another struct

  • copy - allows arbitrary copying/cloning of the structure from anywhere

Essentially, every structure in Move is a default resource. Capabilities give us the power to finely relax these constraints to behave more like traditional structures.

secondary title

4.2 Coins

image

You can find the complete module implementation in the Sui code repository (Link)。

Link

Coin types have key and storage functions. key means it is available as an object. This allows users to own coins directly (as a top-level object). When you own a coin, no one else can even reference it in transactions (let alone spend it) except you. Storage means that a coin can be embedded as a field in another struct, which is useful for composability.

Since there is no discard function, coins cannot be accidentally discarded (destroyed) within the function. This is a very nice feature - it means you can't accidentally lose a coin. If you're implementing a function that takes a coin as a parameter, at the end of the function you need to explicitly do something with it - transfer it to the user, embed it in another object, or send it into another object by calling function (which again needs to do something with it). Of course, it is possible to burn a coin by calling the coin::burn function in the coin module, but you need to do it purposefully (you can't do it by accident).

No cloning capability means that no one can duplicate coins, creating new supply out of thin air. Creating new supply can be done with the coin::mint function and can only be called by the owner of the coin's treasury capability object.

In Move, the security of a resource is defined by its type. Given that Move has a global type system, this makes for a more natural and safer programming model where resources can be passed directly in and out of untrusted code.

secondary title

4.3 Bytecode Verification

As mentioned earlier, mobile smart contracts are released as modules. Anyone is allowed to create and upload any arbitrary module to the blockchain for execution by anyone. We've also seen that Move has rules for how structs are used.

So, what guarantees that these rules are obeyed by any module? What's stopping people from uploading modules with specially crafted bytecode, such as accepting a coin object, and then directly mutating its internal fields to bypass these rules? By doing this, the number of all coins can be increased illegally. The syntax of the bytecode alone certainly allows this.

Bytecode verification can help prevent this type of abuse. The Move Verifier is a static analysis tool that analyzes Move bytecode and determines whether it obeys the required type, memory, and resource safety rules. All code uploaded to the chain needs to pass a validator. When you try to upload a Move module to the chain, nodes and validators will first run through validators before allowing submissions. If any module tries to bypass Move's security rules, it will be rejected by the validator and will not be published.

Move bytecode and validators are the core innovations of Move. It implements an intuitive resource-centric programming model not possible elsewhere. Most critically, it allows structured types to cross trust boundaries without losing their integrity.

In Move, types do exist across modules - the type system is global. This means no CPI calls, account encoding/decoding, account ownership checks, etc. are required - you just call functions and arguments directly from another module. The type and resource safety of the entire smart contract is guaranteed by bytecode verification at compile/release time, and does not need to be implemented at the smart contract level like Solana, and then checked at runtime.

first level title

Now we've seen how Move programming works, and why it's fundamentally safe. So let’s take a deeper look at how this impacts smart contract programming from a composability, ergonomics, and security standpoint. Here, I will compare the development of Move/Sui with the EVM and Rust/Solana/Anchor to help understand the benefits that Move's programming model brings.

secondary title

5.1 Flash Loans

A flash loan is a type of loan in DeFi where the loan amount must be repaid in the same transaction as the borrow. The main benefit of this is that loans can be completely uncollateralized since transactions are atomic. This can be used to arbitrage between assets without having to have a principal.

The main difficulty in achieving this is - how do you guarantee from the flash loan smart contract that the loan amount will be repaid in the same transaction? In order for the loan to be uncollateralized, the transaction needs to be atomic - that is, if the loan amount is not repaid in the same transaction, the entire transaction needs to fail.

  • The EVM has dynamic scheduling, so reentrancy can be used to achieve this, as follows:

  • Lightning Loan users create and upload custom smart contracts, and when the contract is invoked, the control will be passed to the Lightning Loan smart contract by calling

  • Then, the flash loan smart contract will send the requested loan amount 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 loan amount to perform the operations it needs (such as arbitrage).

  • After the custom smart contract completes its operation, it needs to return the loaned amount to the flash loan smart contract.

  • In this way, the executionOperation() of the custom smart contract is completed, and the control will be returned to the flash loan smart contract, which will check whether the loan amount has been returned correctly.

If the custom smart contract does not return the loan amount correctly, the entire transaction will fail.

This achieves the desired functionality nicely, but the problem is that it relies on reentrancy, which we would very much like not to have in smart contract programming. Because reentrancy is inherently dangerous and the root cause of many vulnerabilities, including the infamous DAO hack.

  • Solana does this better because it doesn't allow reentrancy. But without reentrancy, how can flash loans be implemented on Solana if the flash loan smart contract cannot call back to a custom smart contract? Thanks to instruction introspection. On Solana, each transaction consists of multiple instructions (smart contract calls), and from any instruction you can check other instructions (their program IDs, instruction data, and accounts) that exist in the same transaction. This makes it possible to implement flash loans as follows:

  • The lightning loan smart contract implements borrow and repay instructions

  • Users create a flash loan transaction by stacking calls to borrow and repay instructions in the same transaction. When the borrowing order is executed, it will use the order introspection to check whether the repayment order is scheduled at the later stage of the same transaction. If the invocation of the redemption instruction does not exist or is invalid, the transaction will fail at this stage.

  • Between the calls to borrow and repay, the borrowed funds are free to be used by any other in-between instructions.

At the end of the transaction, the repay instruction call will return the funds to the Lightning Lender smart contract (the existence of this instruction will be checked in the reflection of the borrow instruction)

This solution is good enough, but still not ideal. Instruction introspection is somewhat of a special case, not commonly used in Solana, and its use requires developers to master a large number of concepts, and its implementation itself is also very technical, because there are some nuances that need to be properly considered. There is also a technical limitation - the repayment instruction needs to exist statically in the transaction, so it is not possible to call the repayment dynamically through a CPI call during transaction execution. This is not a big deal, but it somewhat limits the flexibility of the code when integrating with other smart contracts, and also pushes more complexity to the client.

Move also prohibits dynamic scheduling and reentrancy, but unlike Solana, it has a very simple and natural flash loan solution. Move's linear type system allows the creation of structures that are guaranteed to be consumed exactly once during transaction execution. This is the so-called "Hot Potato" pattern - a structure with no key, store, delete, or clone functions. Modules that implement this pattern will usually have a function that instantiates the structure and a function that destroys the structure. Since the "hot potato" struct has no discard, key, or store functionality, its destroy function is guaranteed to be called to consume it. Although we could pass it to any other function in any module, ultimately it needs to end up in the destroy function. Because there is no other way to dispose of it, and the validator requires it to be disposed of at the end of the transaction (it cannot be discarded arbitrarily, as there is no discard function).

  • Let’s see how we can use this to implement flash loans.

  • The Lightning Loan smart contract implements a "hot potato" receipt (Receipt) structure

  • When a loan is made by calling the loan function, it sends the caller two objects - the requested funds (one coin) and a receipt, which is a record of the loan amount that needs to be repaid.

  • Borrowers can then use the received funds for the operations they need (such as arbitrage).

  • After the borrower completes its intended operation, it needs to call the repayment function, which will receive the borrowed funds and the receipt as parameters. This function is guaranteed to be called within the same transaction, since the caller has no other way to get rid of the receipt instance (it is not allowed to be discarded or embedded in another object, which is required by the validator).

The repayment function checks that the correct amount has been returned by reading the loan information embedded in the receipt.

Move's resource safety features enable flash loans in Move without using reentrancy or introspection. They guarantee that the receipt cannot be modified by untrusted code, and it needs to be returned to the repayment function at the end of the transaction. This way, we can guarantee that the correct amount of funds is returned in the same transaction.

Flash Loan is a great example of how Move's linear type system and resource safety guarantees allow us to express functionality in ways that other programming languages ​​cannot.

secondary title

5.2 Mint Authority Lock

  • The "minting authority lock" smart contract extends the functionality of token minting, allowing multiple whitelisted parties (authorities) to mint tokens. The desired functionality of this smart contract is as follows (for both Solana and Sui implementations):

  • The original token minting authority creates a "minting lock" which will allow our smart contract to oversee minting. The caller becomes the administrator of the mintlock.

  • An administrator can create additional minting authorizations for this lock, which can be delegated to other parties and allow him to use this lock to mint tokens at any time.

  • Each minting authorization has a daily limit on the number of tokens that can be minted.

  • Admins can ban (and unblock) any minting authority at any time.

An administrator's capabilities can be transferred to another party.

This smart contract can be used, for example, to transfer the minting power of tokens to other users or smart contracts, while the original minting authority (administrator) still retains control over the minting. Otherwise, we would have to hand over full control of minting to another party, which is not ideal since we would just have to trust it not to abuse that power. And it is not possible to give permission to multiple parties.

Full implementations of these smart contracts can be found here (Solana) and here (Sui).

NOTE: Please do not use this code in production! This is sample code for educational purposes only. While I've tested its functionality, I haven't done a thorough audit or security testing.

image

Now let's take a look at the code and see how the implementation is different. Below is a side-by-side code screenshot of the full Solana and Sui implementation of this smart contract.

It can be noticed that for the same function, Solana's implementation is more than twice the size of Sui (230 LOC vs 104). This is a big deal because less code usually means fewer bugs and less development time.

So, where did Solana get these extra rows? If we look closely at Solana's code, we can divide it into two parts - instruction implementation (smart contract logic) and account checking. The instruction implementation is relatively close to what we have on the Sui --- Solana has 136 lines, and Sui has 104 lines. The extra line count is due to the reference of two CPI calls (approximately 10 LOC each). The most important difference is in the account check (marked in red in the screenshot above), which is required (critical in fact) on Solana, but not on Move. Account checks account for approximately 40% (91 LOC) of this smart contract.

Move does not require an account check. The reduction of LOC can bring benefits, but it is also necessary to eliminate account checks. Because it turns out that implementing these checks correctly is very tricky, and if you make even one mistake, it can often lead to major breaches and loss of user funds. In fact, some of the largest (in terms of loss of user funds) Solana smart contract vulnerabilities were account substitution attacks caused by improper account checks.
-Wormhole ($336 million) - https://rekt.news/wormhole-rekt/
-Cashio ($48 million) - https://rekt.news/cashio-rekt/

- Crema Finance ($8.8 million) - https://rekt.news/crema-finance-rekt/

image

So how is Move able to be as safe without these checks? Let's take a closer look at what these checks actually do. Here is the account check required by the mint_to command (permission holders mint tokens by calling this command):

There are 6 checks (marked in red):

1. Check whether the provided lock account is owned by the smart contract and is of the MintLock type. The incoming lock is required because it is used for CPI calls to the token program for minting (which stores permissions).

2. Check whether the provided minting permission account belongs to the provided lock. The minting authority account holds the authority state (its public key, whether it is banned, etc.).

3. Check that the caller of the instruction has the required key for the permission (required authority signed the transaction).

4. The token target account needs to be passed in, because the token program will change it (increase the balance) in the CPI call. The minting check isn't strictly necessary here, as the CPI call will fail if the wrong account is passed in, but it's still good practice.

5. Similar to 4.

  • 6. Check whether the token program account is passed in correctly.

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

  • Account Ownership Checks (1, 2, 4, 5)

  • Account type check (1, 2, 4, 5)

  • Account instance check (whether the correct instance of an account type is passed in) (2, 5)

  • Account Signature Check(3)

Program account address check(6)

image

But in Move, there is no account check or anything like that, only function signatures:

The mint_balance function requires only four parameters. Among these four parameters, only lock and cap represent objects (somewhat similar to accounts).

In Solana, we need to declare 6 accounts and manually implement various checks on them, while in Move, we only need to pass in 2 objects, and no explicit checks are required. How is this achieved?

In Move, some of these checks are done transparently by runtime, some are done statically by the validator at compile time, and some are not needed at all in construction.

Account Ownership Checks - Move has a type system, so this design is unnecessary. A Move structure can only be changed by functions defined in its module, not directly. Bytecode verification ensures that struct instances can flow freely into untrusted code (other modules) without being illegally altered.

Account type check - not necessary since the Move type exists throughout the smart contract. Type definitions are embedded into the module binary (published on the blockchain and executed by the virtual machine). The validator will check that, during compile/publish, when our function is called, the correct type is passed.

Account instance check - in Move (and sometimes on Solana too) you do this in the function body. In this particular case, this is unnecessary because the generic type parameter T of the lock and cap parameter types enforces that the incoming cap (minting ability/authority) object correctly matches its lock (each coin type T can only have a lock).

Account Signature Checks - We don't handle signing directly in Sui. Objects can be owned by users. Minting authority is granted by ownership of the Minting Authority ability object (created by the admin). Passing a reference to this object in the mint_balance function will allow us to mint. Owned objects can only be used in transactions by their owners. In other words, signature checking of objects is done transparently by the runtime.

Essentially, Move leverages bytecode verification to make the programming model for digital assets more natural. Solana's model revolves around account ownership, signatures, CPI calls, PDAs, etc. But when we step back and think about it, we realize that we don't want to deal with these problems. They don't have any relation to the digital assets themselves - rather, we have to use them because this enables us to implement the desired functionality in Solana's programming model.

On Solana, there is no bytecode verification to ensure finer-grained type or resource safety, and you cannot allow any program to change any account, so it is necessary to introduce the concept of account ownership. For similar reasons (no type/resource safety across program calls), there is no notion of user-owned objects that can enter and exit the program, instead we use account signatures to prove permissions. Since sometimes programs also need to be able to provide account signatures, we have PDA...

Move's native abstraction of resources allows us to directly manipulate resources without introducing any low-level building blocks such as PDAs. Type and resource safety guarantees across smart contract boundaries are ensured by validators and do not need to be implemented manually.

secondary title

5.3 Limitations of Solana Composability

I would like to give one more example to highlight some of the pain points of smart contract composability on Solana.

We saw in the mint permission lock example that we need to declare more inputs on Solana compared to Sui (mint_to calls for 6 accounts on Solana vs. 2 objects on Sui). Obviously, dealing with 6 accounts is more troublesome than dealing with 2 objects, especially if you consider that you also need to implement account checks for accounts. In theory this part is manageable, but what happens when we start composing multiple different smart contracts together in a single call?

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

It has the right to mint a certain token from the minting authority lock program, and can mint

When it is called, it will use its authority to mint a user-specified amount of tokens, swap it for a different token using the AMM, and send it to the user in the same command

image

The focus of this example is to illustrate how the minting authority lock smart contract and the AMM smart contract will be combined. An account check invoked by a command might look like this:

17 accounts. 5-6 programs per CPI call (minting and swapping), plus program accounts.

image

On Sui, the signature of an equivalent function looks like this:

There are only 3 objects.

Why are we passing far fewer objects on Sui compared to accounts on Solana (3 vs. 17)? Basically, because in Move we can embed (wrap) them. The safety guarantees of the type system allow us to do this.

image

Below is a comparison between a Solana account and Sui objects, which hold the state of an AMM pool.

We can see that on Solana we store addresses (Pubkeys) of other accounts, which are like pointers and do not store actual data. In order to access these accounts, they need to be passed in individually, and we also need to manually check that the correct account is passed in. In Move, we are able to embed structures within each other and access their values ​​directly. We can mix and match types from any module while they retain their resource and type safety guarantees, both thanks to Move's global type system and resource safety, both driven by bytecode verification.

However, when composing multiple smart contracts, many accounts have to be passed (and thus checked), which creates considerable implementation complexity and has security implications. The relationships between these accounts can be quite intricate, to the extent that it is difficult to keep track of all the necessary account checks and whether they are implemented correctly.

Actually, that's what I think happened in the Cashio breach ($48 million). Below is a breakdown of the (inadequate) account check that led to the vulnerability. As you can see, these account checks get a little more complicated. Developers have good intentions to check correctly, but at some point, the mental pressure becomes too great and it becomes very error prone. The more accounts you have, the more error prone you are.

Move's global type system and more natural programming model means we can drive smart contract composition with greater safety before reaching the limits of psychological stress.

Move was designed with TCB reduction in mind - Move made a number of decisions to minimize TCB as much as possible. The bytecode verifier removes many of the checks performed by the Move compiler from the TCB, whereas in Rust/Anchor there are many more components that need to be trusted, so the surface area for fatal security bugs is larger.

first level title

Can we have Move on Solana, and how?

secondary title

6.1 Is there a globally type-safe Anchor?

image

Before we get started, let's take a brief look at Anchor and do a little thought experiment. Maybe we can somehow upgrade the Anchor to provide some of the benefits we get from Move? Maybe we can get type-safe native support for cross-procedural calls? After all, the Anchor instruction is already similar to the entry function of Move:

image

Maybe we can extend Anchor so that the account can be passed directly to the command parameter.

Can we avoid account checks?

In this case, we want the type checking to be done by the run rather than the program - the run will read the Anchor account discriminator (or its equivalent) and be able to check that the account passed in conforms to the required discriminator (Anchor account's first 8 bytes).

Solana does not distinguish between different instruction calls of the same program, this is implemented manually by the program (in this case, the heavy lifting is done by the Anchor). So, in order to do this, the runtime must somehow know about the different instructions, their signatures, type information.
Solana programs are compiled to SBF (Solana Bytecode Format, a variant of eBPF), and uploaded to the chain (and executed) this way. SBF itself doesn't embed any type or function information that would help us. But maybe we can modify SBF to allow instructions and type information to be embedded in the binary? This way the required instructions and signature information can be read from the binary by the runtime.

We can indeed do this. This will require a considerable amount of engineering, especially given that we need to maintain backwards compatibility with older programs, but here's what we get:

- account ownership and type checking is done by run instead of program

- For accounts whose addresses are known at compile time (such as program accounts), we can avoid passing them in from the client, now they can be passed in by run.

- If we 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, reloading them dynamically with runtime (based on embedded constraint information).

We still don't get:

- Embedded accounts. We still have to use Pubkeys to refer to other accounts and not be able to embed them directly. This means that we do not get rid of the account bloat problem described in Section 5.3.

-When making cross-program calls, account type checking still needs to be done dynamically at runtime, rather than statically at compile time like in Move.

NOTE: This is just a thought experiment. It does not mean that it can be completed safely, nor does it mean that it is difficult to achieve, nor does it advertise that these benefits are worth the effort of engineering.

These benefits are certainly nice, but they don’t fundamentally change anything from a smart contract development perspective. Doing type checking at runtime instead of in the program may bring some performance benefits, and it is not necessary to manually pass the address account from the client at compile time, improving ergonomics to a certain extent (this can also be alleviated by tooling). But we're still ultimately dealing with Solana's programming model, which itself provides more help in handling digital assets - we still don't have native resource security, we can't embed accounts, so there's still account bloat, we're still dealing with account signing and PDAs...

We want to have a natural programming model, but at the same time we're dealing with untrusted code. While we can safely handle untrusted code on Solana, there is a compromise in the programming model. Bytecode verification makes it possible for us to have both. Without it, we really don't seem to be able to improve the programming model.

secondary title

6.2 Solana Bytecode Format

As mentioned earlier, SBF (Solana Bytecode Format), the compilation and on-chain storage format of Solana smart contracts, is based on eBPF. Using eBPF on Solana instead of any other bytecode format (such as WASM) is mainly because Solana's requirements for secure and high-performance smart contract execution are consistent with the kernel sandbox program execution requirements of eBPF design (it also needs security and high performance)

On the face of it, eBPF is indeed a solid choice. High performance, designed around security, program size and number of instructions are limited, has a bytecode verifier... looks great.

But let's see what this means in practice. Maybe we can somehow leverage eBPF validators to improve the security of our smart contracts? Here are some of the things eBPF validators do:

- does not allow infinite loops

- Check if the program is a DAG (Directed Acyclic Graph)

-No out-of-bounds jumps allowed

- Check parameter types when making various helper function calls (helper functions are defined in the kernel, e.g. for modifying network packets).

Well, disallowing out-of-bounds jumps seems useful, but otherwise limited. In fact, mandating that a program must be a DAG and have no infinite loops is problematic, since it greatly limits the program's operability (we don't have Turing completeness). The reason this is needed in eBPF programs is that the verifier needs to be sure that the program terminates within a certain number of instructions (so that the program doesn't cause the kernel to terminate; this is the famous Halting problem), and gas metering is not a option as it would affect performance too much.

While this trade-off is great for implementing high-performance firewalls, it's not so great for smart contract development. The vast majority of eBPF validators cannot be reused on Solana programs. In fact, Solana doesn't use the original eBPF validator at all, it uses a (more basic) custom validator that basically checks for correct instructions and out-of-bounds jumps.

At the same time, eBPF is designed to allow up to 5 parameters to be passed to a function for calling. This means that the Rust standard library cannot be compiled directly to eBPF. Or the stack size is limited to 512 bytes, which reduces the size of arguments we can pass to a function without heap allocation.

So, even if Rust compiles to LLVM, has an eBPF backend for LLVM, and even supports the Rust compiler for eBPF use, you still can't make Solana smart contracts compile to eBPF as they should. This is why the Solana team had to make several modifications to the Rust codebase and the eBPF LLVM backend (e.g. passing arguments via the stack).

Since some of these changes are native support upstream (either Rust or LLVM), the Solana team currently maintains forks of both Rust and LLVM. When you execute cargo build-bpf (the typical command to build a Solana smart contract), Cargo will pull this Solana-specific version of rustc (the compiler for the Rust programming language) to compile the smart contract (the original rustc will not work) .

This is how SBF was born - Solana requires some requirements that are not compatible with eBPF. The Solana team is currently working on upstreaming SBF as a separate LLVM backend and adding it as a Rust target to avoid maintaining a separate fork.

Therefore, while eBPF can serve as a format for smart contracts, it is not as ideal as it seems on the surface. It required some modifications, and the original validator wasn't very useful.

In the discussion about Move and Solana/SBF, a misconception is that some people think that the main ideas of Move should be applicable to SBF, because it is based on eBPF, and perhaps can use its validator to do static account change checks instead of in Do dynamic checking at runtime.

In my opinion, this is a dubious claim. Even if it were possible to prove that programs don't alter accounts in eBPF that they don't own, which is exactly what Move is doing, it's certainly not Move's main idea.

The main idea of ​​Move is to create a resource-centric programming model that can naturally interact with untrusted code.

  • In practice, this means:

  • global type safety

  • Resource security (key, clone, stash, discard)

  • embeddable resources

  • ...

Resources flow safely to and from untrusted code

It is very difficult to introduce the main Move ideas into eBPF/SBF. Enforcing properties such as "this untrusted code cannot drop a T" is impossible without major changes to eBPF. This requires so much modification that you end up with a new bytecode that looks more like Move than eBPF.

In fact, a similar line of thought was what led to the birth of Move in the first place. The Move team (then at Diem) initially considered starting with other formats such as WASM, JVM or CLR, but adding this after the fact was too difficult - linearity/capacity was unconventional. So Move was designed from the ground up with the idea of ​​performing these checks efficiently through lightweight validator channels.

If you think about it, this isn't actually that surprising. After all, in the end, smart contract programming is not system programming, backend programming, or any other traditional programming, it's a completely different type of programming. So it's no surprise that the capabilities of existing bytecode and instruction formats cannot be exploited, since they were designed with completely different use cases in mind.

I'm not criticizing Solana for using eBPF. In fact, I think it's a pretty solid pick and a good judgment call by the team given the context. In hindsight, the team may have chosen WASM over eBPF, which would have avoided the aforementioned issues of compiling smart contracts into eBPF, since WASM has first-class support in Rust (though WASM may have other issues), but It can be seen that the team may feel that eBPF is a safer choice given the emphasis on performance. Plus, at the time these design choices were made, Move hadn't even been announced, and creating a new language from scratch certainly wasn't a reasonable option for a startup. Ultimately, Solana manages to deliver a successful high-performance L1, and that's what matters.

  • There are three ways to get Move on Solana:

  • Add the Move vm as a local loader (alongside the SBF vm)

  • Run the Move virtual machine as a program (such as Neon)

Compile Move to SBF (like Solang)

Let's discuss (3) first. The idea here is to have an LLVM front end for Move so that it compiles to SBF. A Move smart contract compiled to SBF is executed transparently, just like a smart contract built in Rust (or any other language that can be compiled to SBF), and does not require any distinction or knowledge of Move at runtime. From an operational perspective, this would be a very elegant solution, since it would require no changes to it or its security assumptions.

But I think developing smart contracts in this way will be worse than using Anchor directly. What you get with (3) is the Move syntax in the Solana programming model. This means that all the important advantages of Move discussed in Chapter 5 (global type safety, global resource safety, embeddable objects...) will no longer exist. Instead, we'll still have to deal with account checks, CPI calls, PDAs, etc., just like in Rust. And, since Move doesn't support macros, it's impossible to implement a framework like Anchor using eDSL to simplify some of this work, so the code will be similar (but probably worse) to vanilla Rust. The Rust standard library and ecosystem are also unavailable, so things like account serialization and deserialization have to be reimplemented in Move.

Move is not well suited for use with other programming models. This is because it was specifically designed to compile to Move bytecode and pass the validator. This is necessary given the custom rules around the ability and borrow checkers. Its bytecode verification is so specific that other languages ​​have little chance of compiling to Move bytecode and passing the verifier. Because Move is built around this very specific kind of bytecode verification, it's not as flexible as languages ​​like Rust.

Stripping the bytecode gives up all of Move's major advantages. Although the type, resource, and memory safety features of Move are preserved at the program level, they are not preserved globally. And program-level safety doesn't bring many new results -- we've already achieved them with Rust.

Move's smart contract ecosystem also doesn't work on Solana - the programming model is so different that significant parts of the smart contracts have to be rewritten. Considering all of this, I predict that implementing Move with (3) will not be accepted.

As for (1), the idea here is (together with the SBF loader) to add support for the Move loader at runtime. Move smart contracts will be stored as Move bytecode on-chain and executed by the Move VM (just like in Sui). This means we will have an ecosystem of SBF smart contracts and an ecosystem of Move smart contracts, the former running on the current Solana programming model and the latter running on an (arguably more advanced) Move model .

With this approach, it is possible to keep all the benefits of the interactions between Move smart contracts, but one challenge here is to enable Move smart contracts to interact with SBF smart contracts and vice versa - you need a pair of Move and For those who have a deep understanding of Solana, validators must also be tuned.

There is also the disadvantage of needing to maintain two different loaders at runtime. This has security implications as it means the attack surface is doubled - a single loader error could mean the entire chain is exploited. Early support for MoveVM was actually added to Solana back in 2019 (#5150), but was later removed due to security concerns (#11184).

As for (2), the idea is to run the entire Move VM as one Solana program (smart contract). The Move VM is implemented in Rust, so it will probably compile to SBF (unless it uses threads or other unsupported API). While this sounds crazy, Neon has implemented a similar approach, running the EVM as a Solana program. The benefit of this approach is that no modifications to the run are required and the same security assumptions can be maintained.

There is no direct way to bring the main functionality of Move to Solana. While it's possible to build an LLVM front end and compile Move to SBF, this won't do much as the programming model will remain the same. As the thought experiment in Section 6.1 illustrates, the programming model cannot be improved without some sort of bytecode verification. Changing eBPF/SBF to support bytecode verification will be very difficult. It seems like the only reasonable option is to somehow get MoveVM running. But this means that there will be two ecosystems operating on different programming models, and getting them to interoperate properly is extremely challenging.

secondary title

6.4. Performance of Move

Move's bytecode is not a general-purpose bytecode language. It has a very "assertive" type system, which is quite advanced to allow all necessary validation. This means lower performance compared to other bytecode formats such as eBPF/SBF, which are closer to native code, which one might consider to be a problem for its use in high-performance L1.

However, so far, smart contract execution has not been a bottleneck on either Solana (average 3k TPS at the time of writing) or Sui (based on the initial e2e benchmarks the team did). The main way to improve transaction processing performance is through parallel execution. Both Solana and Sui implement this by requiring prior declaration of dependencies and parallel scheduling of execution of transactions that depend on different sets of objects/accounts.

With all of this in mind, I hope Move's performance won't be a significant hindrance for the foreseeable future.

7. Other functions of Move

secondary title

7.1 Validator

Move has a formal verification tool for smart contracts called Move Prover. With this tool, you can judge whether different invariants hold for your smart contract. Behind the scenes, verification conditions are translated into SMT formulas, which are then checked using an SMT solver. This is very different from fuzzing, which is trial and error by walking the input space, for example. For example, fuzzing and unit/integration testing can still provide a false positive if they fail to test for a particular input or combination of inputs, indicating a bug in the program. Verifiers, on the other hand, essentially provide formal proofs that specified invariants hold for the provided program. It's like checking the program against all possible inputs, but it doesn't need to.

Below is an example of a validator (taken from the White Paper "Fast and Reliable Formal Verification of Smart Contracts with Move Prover").

image

secondary title

7.2. Wallet security

Since Sui requires that all objects that transactions will access are passed in function arguments (not dynamically loaded from global state), and the move function signatures are stored in the bytecode itself along with the type information, we can have the wallet send The user provides more meaningful information, explaining the content of the transaction.

image

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

We can see from the signature of the function that this transaction will access the user's 3 assets (asset types). Not only that, according to & and &mut keywords (or no keywords), we can also know that asset 1 can be read, asset 2 can be changed (but not transferred or destroyed), and asset 3 may be changed, transferred or destroyed. destroy.

The wallet can display this information to the user, who can then become more aware of what actions a transaction might have on the asset. If something is unusual, for example, a transaction call from a Web3 application is touching some asset or coin it shouldn't, the user can observe this and decide not to proceed with the transaction.

This is not possible on Solana because accounts contain arbitrary data from an operational point of view. You need an external description of the account (specific to the application) to interpret it, which is not necessarily provided by the smart contract issuer. Additionally, the concept of asset ownership does not exist in the Solana runtime, and each smart contract needs to implement this semantics manually (usually using account signatures and PDAs), which means there is no general way to track this.

secondary title

7.3 Simple and complex transactions

Specifically for Sui, there is an interesting optimization at the consensus level that allows certain types of transactions to abandon full consensus in favor of a simpler algorithm based on Byzantine Consistent Broadcast. The advantage of this is that these transactions can be parallelized at the consensus level, eliminating head-of-line blocking, and achieving near-instant finality—essentially realizing the scalability of web2.

This is due to Sui's distinction between owned and shared objects (see Section 3.1). Transactions involving only own objects (called simple transactions) do not require full consensus on Sui. Since own objects cannot be used in transactions other than the sender, and the sender can only send one transaction at a time, this in itself means that these transactions do not need to be ordered with reference to other transactions (total ordering and causal ordering) - we know that transactions Objects referenced in cannot be affected by other transactions, and the transaction cannot affect other objects. Therefore, we don't care about the ordering of that transaction relative to other transactions happening in parallel on the chain - it's actually irrelevant. Sui is able to exploit this fact to greatly optimize the processing of simple transactions, achieving finality in a few hundred milliseconds. But the downside is that the sender can only send one transaction at a time. On the other hand, transactions involving any number of shared objects, known as complex transactions, always require full consensus.

Certain types of applications can make good use of simple transactions, given that the creation, transfer, and modification of own objects can be done entirely through simple transactions. Good examples are NFTs (including mass coinage) and web3 games. These use cases benefit greatly from low-latency finality and elimination of peer blocking, enabling better user experience and scalability.

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

The Sui documentation has more details on simple and complex transactions.

https://docs.sui.io/devnet/learn/sui-compared

https://docs.sui.io/devnet/learn/how-sui-works#system-overview

first level title

8. Conclusion

This article explores and compares the programming models of Solana and Sui in depth, and also discusses the Move programming language.

The second chapter is a summary of the Solana programming model, while the third chapter introduces Sui Move and its programming model. Chapter 4 goes on to explain how type and resource safety in Move work. The implications of the Move functionality for smart contract development are not immediately apparent, so in Chapter 5 I provide a more thorough comparison of Solana and SuiMove using real-life examples. Chapter 6 discusses eBPF/SBF, showing that it is not easy to get the Move functionality or Move itself to work on Solana. Chapter 7 discusses some of Sui's Move-related features.

Smart contract programming is about programming digital assets. It can be said that this is a new type of programming, which is very different from other types of programming (such as systems, backends...) that we have seen so far. Because of this, existing programming languages ​​and programming models are naturally not well suited to this use case.

The crux of the matter is that we want a programming model that works naturally with resources but at the same time interacts with untrusted code. Solana makes a compromise here. It enables smart contracts to have the necessary programmability in a trustless environment, but its programming model is not natural for programming with resources. Bytecode verification makes it possible to have both properties. In a way, it turns untrusted code into trusted code.

Move is a new programming language for smart contract development. Its core innovation is its bytecode, which is purposely designed to be verifiable. While bytecode verification itself is not a new concept, the verification that Move does is indeed an innovation. Through its bytecode and verification, Move implements a smart contract programming model with first-class support for resources and guarantees safe programming in an untrusted environment.

I think Move is to smart contract development what React is to front-end development. Saying "what can be done in Move can be done in Rust" is like saying "what can be done in React can be done in jQuery". Of course it is possible to implement a jQuery-based application that is comparable to a React application, but it is not practical. React introduces the concept of virtual DOM, which is completely comprehensible to developers, but makes front-end development faster, scalable, and simpler. Likewise, Move's bytecode verification is a low-level technology that is also easy to understand for developers, but it provides a more ergonomic, composable, and safer smart contract development. Move also significantly lowers the barrier to entry for smart contract developers due to its security and more intuitive programming model.

If Move manages to gain traction (and there are early indications it will), it could pose a serious threat to Solana. There are two reasons.

First of all the development time of the Move smart contract is much faster. Developing a smart contract from scratch in Move can be 2-5 times faster than in Rust. Therefore, the development of the Move ecosystem can surpass that of Solana. Due to the open and permissionless nature of the blockchain, there is no severe lock-in effect and liquidity can move easily. Solana developers may be forced to adopt Move purely due to economic considerations - either switch to Move or be surpassed by Move developers because they can develop more secure smart contracts faster. If you want to hire a smart contract developer, you can hire a Rust developer who can build one smart contract, or hire a Move developer who can build two more secure smart contracts in the same amount of time. This is similar to what React did to front-end development.

Second, Move has a much lower barrier to entry than Rust or Solidity. Because the Move syntax is simpler and the programming model is more intuitive. Some developers can't do smart contract development in Rust or Solidity, but might be able to in Move. Because there are fewer concepts to learn, non-smart contract developers can enter Move more than entering Rust (Rust itself is a complex language, coupled with Solana concepts, such as PDA, will cause a lot of confusion for beginners) or Solidity (you need to be familiar with very fine details of the language like reentrancy to be able to develop secure smart contracts) is much easier. Even if existing Solana and Solidity developers don't switch to Move, the market of developers not yet entering the space is orders of magnitude larger than the number of existing developers in the space. Because of Move's lower barriers to entry and faster development, it has better product-market fit than Rust or Solidity and can take a bigger slice of the pie. If new developers start pouring in, I want them to start with Move, not Rust or Solidity. This is also similar to how React is in the web industry.

Because of this, I fully expect Solana to add first-class support for Move in the medium to long term. But it's not easy. To get the main benefits of Move, Move bytecode needs to be natively supported, which means that simply compiling Move to eBPF/SBF is not possible (see Section 6.3). In order to maintain the existing ecosystem, both operations need to be supported. The main technical challenge is how to achieve proper interoperability between operations. This requires a deep understanding of Move and Solana, so I hope the Solana team will give a direct push to this with support from the Move team.

Move originated from Meta (née Facebook)'s Diem project. The Move team, led by Sam Blackshear, was tasked with figuring out how to handle smart contracts. After digging into the problem, they found that programming smart contracts is all about digital assets (resources), but none of the existing languages ​​support this use case, and decided to build a new programming language from scratch.

smart contract
Welcome to Join Odaily Official Community