This blog post provides a gentle introduction to Sui, a new Move-based blockchain. We compare Sui with Aptos and highlight some important differences for deveopers to know, especially for secure smart contract development.
Introduction
Sui is a proof-of-stake smart contract blockchain platform targeting the Move↗ language. Sui is unique in that each object has its own ledger and transactions don’t necessarily have to be in sequence. This is claimed to be a breakthrough optimization and has important implications on object handling on the Sui blockchain.
The Move language used on the Sui blockchain was originally developed at Facebook as part of the Libra blockchain (later Diem). That project was discontinued, and the team that made it split into two groups making Sui↗ and Aptos↗, both layer 1 blockchains running Move. There are a few other blockchains also building on Move.
Sui is intended to allow real-time use cases of digital assets, such as items in a game. The transaction needs to be able to happen quickly enough for a reasonable playing experience.
On the technical side, the validators and client are written in Rust, with governance partially written in Move on-chain. There is also a Sui Move standard library pre-deployed on-chain, which smart contracts can invoke. A limited version of the original Move standard library is also available.
To help explain Sui, we’ll compare Sui Move to Aptos Move and core Move.
Sui Object Model
Accounts
It’s common for blockchains (and Aptos) to use accounts to manage matters of permission and ownership. Accounts are essentially addresses. Accounts are associated with private key(s), which can be used to sign transactions. Resources or objects, as they are called in Sui, are for the most part held at an account.
For example, holding a Coin
resource at your account on Sui would
allow you to sign transactions from that account using the
SUI
(currency) out of that Coin
to pay for the gas cost of running
some function.
For a more in-depth discussion of resources and accounts, and how it works on Aptos, check out our blog post on Aptos.
Objects
Objects in core, Aptos, and Sui Move are used to represent assets like USDT, the administrator permissions to a smart contract, chips in an on-chain poker game, and any other data that the smart contract is tracking. Needless to say, objects and how they are managed are central to Move smart contracts, whether on Aptos or Sui.
An object at its core is just a collection of related data represented
as a struct
in Move. A Move struct
can have four types of abilities:
key
, store
, copy
, and drop
.
Abilities
key
makes an object able to be stored on-chain. If an object does not have key, it has to be stored under another object or destroyed before the contract finishes execution.store
allows an object to be stored under another object. Think ofstruct
s like boxes, andstore
allows this particular box to fit inside other boxes.copy
allows an object to be copied. Non-copyable objects can’t be copied, but manual “copies” can theoretically still be created by the creating contract.drop
allows an object to be quietly destroyed. Simply allowing the object to go out of scope will destroy it.
On Sui, in order for a Move object to be stored on-chain under an
account, it must be also be a Sui object. Sui objects have the key
ability (since they are stored on-chain) and a special first member of
the struct named id
with type UID
. A bytecode verifier ensures that
any struct
with the key
ability has the special first member.
Object IDs are implemented with UID
s in Sui Move. In Sui, UID
s are
unique and are used to reference objects both from on-chain smart
contracts and off-chain in the Sui client. In Sui, to pass an object
into a Move contract, you must refer to the object by its UID
.
The UID
is intended to be unique per on-chain object. The UID
can be
used in either a RPC call or the Sui command line. The UID
can also be
used to request information about the state of the object on-chain.
The most significant difference between Sui Move and core/Aptos Move is that Sui does not leverage the Move global storage.
In Aptos/core Move, any object with the key
ability can be stored at
an account (does not need UID
). An Aptos smart contract can access
object of types that it defines under any account at any point in its
execution, even if the caller is not the account that is accessed. This
is typically done through borrow_global
, borrowing a reference to an
object type it defines.
Whereas in the case of Sui, objects are passed to the runtime directly
at the top level call and no external references can be borrowed during
execution. To store objects, you use the
transfer::transfer(object, address)
function. A complete example is
available in the UID
swapping section of this article.
In Sui, all objects a smart contract can access are passed in from
outside Move. This is similar to Solana’s predeclared accessed accounts
per transaction. Only the owner of the account can pass objects under
that account into Move (directly or indirectly, by reference). Objects
stored under other accounts other than the sender are therefore
inaccessible (except shared and frozen objects). The ownership of
objects is central in determining who can do what. The owner of a
USDTCoin
can spend it. The owner of the WithdrawPermission
can
withdraw money from a flash lender.
Sui objects can also be in some other special states for situations beyond simple ownership by account. At any point in Sui Move execution, a Sui object can be in one of six states.
Sui object states
- Inside a Move contract
- The Sui object was either passed by value into a Move contract
or was created inside the Move contract. It must be transferred
to some account (if it has
key
) or destroyed before the end of Move execution.
- The Sui object was either passed by value into a Move contract
or was created inside the Move contract. It must be transferred
to some account (if it has
- Frozen/immutable (global read-only)
- The Sui object can be passed by read-only reference into Move contracts by anyone.
- Shared (global read-write)
- The Sui object can be passed by read-write reference into Move contracts by anyone.
- Owned by an address
- Only that address may pass in the object into Move (as reference or directly).
- In core Move, the declaring contract can use types it declares from any address regardless of caller, whereas in Sui only the caller’s owned objects may be passed into Move.
- In core Move only one object of a type can be stored at an address, but there is no such restriction in Sui.
- Owned by another object
- This is not implemented as of the time of writing (November 2022), and the mechanics are unclear.
- According to the current documentation, the object would still be accessible directly on-chain.
- Wrapped
- The object has both
key
andstore
and is stored under (directly as a member or nested under multiple levels of objects) another object that is stored on-chain somewhere. - The object isn’t directly accessible on-chain. The object is stored in another wrapper object. To recover the original object, you must destroy the wrapper object (unwrapping).
- The object has both
Object IDs
Object UID
s are supposed to be globally unique. They are created
through object::new(&mut TxContext)
and destroyed through
object::delete(UID)
.
Other Sui differences
TxContext
In Aptos, the signer
of a transaction is directly received as an
argument passed into the function that was called from outside of Move.
In Sui the TxContext
type can be declared as an argument of a function
just like you would with signer
, and the signer
can be obtained from
TxContext
using a library function. Also, TxContext
is used to
create UID
s in Sui Move. As of this writing, TxContext
is only used
for those purposes.
Global storage and the acquires keyword
Aptos uses the global storage operators (move_to
, move_from
, etc.)
from core Move
. Functions that use global storage must have the
appropriate acquires
annotation.
Unsurprisingly, acquires
keyword is not used in Sui, along with the
global storage operators since Sui does not use the Move global storage.
The init(&mut TxContext) function
Aptos supports an initialization function called upon module deploy with
a similar signature. Sui will call the init
function declared with
this signature upon module deploy.
Interesting behavior and potential vulnerabilities
Object ownership transfer
Sui objects (with key+store) stored at an account can be freely transfered between accounts without involvement of the creating contract. This could potentially be unintuitive and leads to problems in contracts that enforce invariants through an account holding some combination of contract-created objects. Objects with only the key ability are more restricted. They can only be transferred by the creating module.
Object freezing
Sui objects stored at an account can be frozen without the involvement
of the creating contract if that object also has store
. A frozen
object is global and immutable. It can be passed to the smart contract
by anyone, which may be surprising to developers who expect the object
to only come from a single account.
Reentrancy
In Move↗, Reentrancy isn’t possible since dynamic callbacks are not possible. This is a classic bug in Ethereum/Solidity contracts where a smart contract calls a callback function that calls back into the contract again, potentially operating on a partially updated state of that smart contract. The function call hasn’t finished at this point; it is waiting on callback to finish. You can find more details at this Consensys article.
Integer overflows
In Move, integer overflows are automatically reverted. Any transaction that causes an integer overflow cannot succeed. However, bitwise operators cannot overflow, even if the end result seems unintuitive. For example, bitshifting outside the variable bit width is allowed, and the shifted-in bits are zero. We discuss this in our Aptos Move article.
Sui object UID swapping
You may know that UID
s are supposed to uniquely identify objects on
the chain. However, if you control the contract that created two
objects, and can therefore destruct/recreate them, you can change the
type of an object on-chain.
struct Cat has key {
id: UID,
}
struct Dog has key {
id: UID,
}
public entry fun transmute(cat: Cat, dog: Dog, ctx: &mut TxContext) {
let Cat {
id: cat_id,
} = cat;
let Dog {
id: dog_id,
} = dog;
let new_cat = Cat {
id: dog_id,
};
let new_dog = Dog {
id: cat_id,
};
transfer::transfer(new_cat, tx_context::sender(ctx));
transfer::transfer(new_dog, tx_context::sender(ctx));
}
Let’s try this contract on-chain.
First we check the types of our cat and our dog:
$ sui client object --id 0x1ae0eb5f3b972449ff2a4134e799f447c662939e
----- Move Object (0x1ae0eb5f3b972449ff2a4134e799f447c662939e[1]) -----
Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
Version: 1
Storage Rebate: 12
Previous Transaction: DfF7MGmkZcl4YuLT7gnLXsyfmZhNv/d/72V9L0eOBc0=
----- Data -----
type: 0xccfefb2987fabb577cb4e5cb726d7d3a08c7f2e2::objid::Cat
id: 0x1ae0eb5f3b972449ff2a4134e799f447c662939e
$ sui client object --id 0x49dc71ac8bb7b649c8ff21d255295da72d791707
----- Move Object (0x49dc71ac8bb7b649c8ff21d255295da72d791707[1]) -----
Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
Version: 1
Storage Rebate: 12
Previous Transaction: DfF7MGmkZcl4YuLT7gnLXsyfmZhNv/d/72V9L0eOBc0=
----- Data -----
type: 0xccfefb2987fabb577cb4e5cb726d7d3a08c7f2e2::objid::Dog
id: 0x49dc71ac8bb7b649c8ff21d255295da72d791707
Then we call transmute(cat, dog)
$ sui client call --package 0xccfefb2987fabb577cb4e5cb726d7d3a08c7f2e2 --module objid --gas-budget 10000 --function transmute --args 0x1ae0eb5f3b972449ff2a4134e799f447c662939e 0x49dc71ac8bb7b649c8ff21d255295da72d791707
----- Certificate ----
<snip>
----- Transaction Effects ----
Status : Success
Mutated Objects:
- ID: 0x1ae0eb5f3b972449ff2a4134e799f447c662939e , Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
- ID: 0x37d38af0f4cac30679658c3a75cbbf90169d82d7 , Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
- ID: 0x49dc71ac8bb7b649c8ff21d255295da72d791707 , Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
and check the types of our cat and dog again:
$ sui client object --id 0x1ae0eb5f3b972449ff2a4134e799f447c662939e
----- Move Object (0x1ae0eb5f3b972449ff2a4134e799f447c662939e[2]) -----
Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
Version: 2
Storage Rebate: 12
Previous Transaction: kTbgQmSch/kav+EW2abSfM3w4Seujv3UK7Yn4ZUV34Q=
----- Data -----
type: 0xccfefb2987fabb577cb4e5cb726d7d3a08c7f2e2::objid::Dog
id: 0x1ae0eb5f3b972449ff2a4134e799f447c662939e
$ sui client object --id 0x49dc71ac8bb7b649c8ff21d255295da72d791707
----- Move Object (0x49dc71ac8bb7b649c8ff21d255295da72d791707[2]) -----
Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
Version: 2
Storage Rebate: 12
Previous Transaction: kTbgQmSch/kav+EW2abSfM3w4Seujv3UK7Yn4ZUV34Q=
----- Data -----
type: 0xccfefb2987fabb577cb4e5cb726d7d3a08c7f2e2::objid::Cat
id: 0x49dc71ac8bb7b649c8ff21d255295da72d791707
This can’t really be used to attack anything on-chain except in very
unusual circumstances. Some contract would have to refer to a foreign
object by UID
and make assumptions about its type in such a way that
changing it would violate the assumptions.
However, this can potentially be a problem for off-chain applications, which assume the type of objects on-chain.
Hiding and restoring objects with contract control
Normally, it’s possible to move a Sui object stored at an account into
another object if that Sui object has store
. However, if you have
control over the contract that created the object, you can destruct it
and store the UID
inside another object, even if that object does not
have store
.
struct Cat has key { // notice: no `store` capability!
id: UID,
}
struct Tomb has key {
id: UID,
cat_id: UID,
}
public entry fun entomb(cat: Cat, ctx: &mut TxContext) {
let Cat { // destruct Cat to extract its UID
id: cat_id
} = cat;
let tomb = Tomb {
id: object::new(ctx),
cat_id: cat_id,
};
transfer::transfer(tomb, tx_context::sender(ctx));
}
public entry fun resurrect(tomb: Tomb, ctx: &mut TxContext) {
let Tomb { // get the cat UID out of the tomb, destructing the tomb in the process
id: tomb_id,
cat_id: cat_id,
} = tomb;
object::delete(tomb_id);
let cat = Cat { // create a new cat with the same UID as before
id: cat_id,
};
transfer::transfer(cat, tx_context::sender(ctx));
}
Let’s try this with the cat from before. Let’s take a look at the cat:
$ sui client object --id 0x49dc71ac8bb7b649c8ff21d255295da72d791707
----- Move Object (0x49dc71ac8bb7b649c8ff21d255295da72d791707[2]) -----
Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
Version: 2
Storage Rebate: 12
Previous Transaction: kTbgQmSch/kav+EW2abSfM3w4Seujv3UK7Yn4ZUV34Q=
----- Data -----
type: 0xccfefb2987fabb577cb4e5cb726d7d3a08c7f2e2::objid::Cat
id: 0x49dc71ac8bb7b649c8ff21d255295da72d791707
Now let’s hide away the cat with entomb(cat)
:
$ sui client call --package 0xccfefb2987fabb577cb4e5cb726d7d3a08c7f2e2 --module objid --gas-budget 10000 --function entomb --args 0x49dc71ac8bb7b649c8ff21d255295da72d791707
----- Certificate ----
<snip>
----- Transaction Effects ----
Status : Success
Created Objects:
- ID: 0x342fdc3c73baba9d4c7717ff899ef74c3fe73833 , Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
Mutated Objects:
- ID: 0x37d38af0f4cac30679658c3a75cbbf90169d82d7 , Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
Wrapped Objects:
- ID: 0x49dc71ac8bb7b649c8ff21d255295da72d791707
Notice how our cat is considered a wrapped object now. We’ll check that the cat can no longer be accessed by its ID:
$ sui client object --id 0x49dc71ac8bb7b649c8ff21d255295da72d791707
Object deleted at reference (0x49dc71ac8bb7b649c8ff21d255295da72d791707, SequenceNumber(3), o#5858585858585858585858585858585858585858585858585858585858585858).
Now we bring the cat back into existence with resurrect(tomb)
:
$ sui client call --package 0xccfefb2987fabb577cb4e5cb726d7d3a08c7f2e2 --module objid --gas-budget 10000 --function resurrect --args 0x342fdc3c73baba9d4c7717ff899ef74c3fe73833
----- Certificate ----
<snip>
----- Transaction Effects ----
Status : Success
Mutated Objects:
- ID: 0x37d38af0f4cac30679658c3a75cbbf90169d82d7 , Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
Deleted Objects:
- ID: 0x342fdc3c73baba9d4c7717ff899ef74c3fe73833
Unwrapped Objects:
- ID: 0x49dc71ac8bb7b649c8ff21d255295da72d791707 , Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6
Let’s take a look at the cat:
$ sui client object --id 0x49dc71ac8bb7b649c8ff21d255295da72d791707
----- Move Object (0x49dc71ac8bb7b649c8ff21d255295da72d791707[4]) -----
Owner: Account Address ( 0xf302c0f8afe0d7da1f33d27b87da88f1f48b78c6 )
Version: 4
Storage Rebate: 12
Previous Transaction: /u9PrwRWlpM2S31y59iyIEMKHa1/GBNeo5s/b6olZzE=
----- Data -----
type: 0xccfefb2987fabb577cb4e5cb726d7d3a08c7f2e2::objid::Cat
id: 0x49dc71ac8bb7b649c8ff21d255295da72d791707
The security implications of this are very similar to UID
swapping.
On-chain programs are unlikely to be vulnerable, but off-chain programs
can potentially be confused by objects going in and out of existence.
Update (1st December, 2022): Both UID swapping and object hiding were actually possible due to a bug in the Sui bytecode verifier. It was patched 30 minutes after this post was published in this commit↗. We applaud the Mysten Labs team for their prompt response.
Conclusion
Sui Move is a new chain still in rapid development but already has breakthrough performance improvements and a well-designed on-chain language. Sui Move has few unintuitive behaviors that could lead to vulnerabilities. As auditors and builders of the wider crypto ecosystem, we will watch Sui with anticipation.