Sui Improvement Proposals 2.0

Paul Fidika
11 min readNov 2, 2022

--

I’ve been working with Sui Move for about a month now. I’d like to make some suggestions for improvements:

More Accurate Terminology

‘Shared object’ and ‘owned object’ can be a bit of a misnomer. More accurate terminology is ‘multi-writer’ and ‘single-writer’ objects.

Many ‘shared objects’ can in fact be ‘owned’ by someone, like:

Here the Sui-defined owner is everyone, while the module-defined owner is whatever address is inside of the ‘owner’ property.

New terminology:

  • Single-Writer Object: an object that can only be used in a transaction by one person; the Sui-defined owner.
  • Multi-Writer Object: an object that anyone can reference mutably in a transaction.
  • Sui-defined owner: the account specified in Sui’s ownership system.
  • Module-defined owner: the module’s system of ownership; if one exists, and however that is defined. Origin-byte is calling this ‘logical ownership’ I believe.
  • Nano-Sui: the smallest fractional unit of a Sui-coin (9 decimals of precision). I like this better than ‘Myst’.

Pass Shared Objects by Value (not just reference)

Currently shared (multi-writer) objects can only be passed by reference, not by value (consumed). This prevents shared-objects from being deleted when they are no longer needed. It’s important shared-objects be deletable, otherwise Sui’s head will be filled up with lots of useless shared-object wrappers from past transactions. People will be incentivized to delete shared objects so that they can claim the storage refund.

For safety, this pass-by-value behavior should only be allowed by the module that defined the shared object. The follow operations should be available:

  • Delete the object
  • unshare the object (more on this below)

Storing a shared object, either with a wrapper struct, or by placing it inside of a dynamic field, should not be allowed, unless the object is unshared first.

Deleting can be handled in the usual way:

let SharedObject { id, field1: _ } = shared_object;
object::delete(id);

Note that, currently in Sui, every uid must be deleted after it has been taken from a destructured object; it cannot be used to create a new object with that same id. This sort of type-transforming behavior might be interesting however.

(There was a github issue by Sam Blackshear for this but I can’t find it???)

Unshare Objects

This could be something like:

transfer::take_shared(shared_object, new_owner);

again, this would require the shared object to be passed by value, which can only be done by the module defining the object. Furthermore, this operation consumes the object, which means that any storing (wrapping or dynamic field) operations will have to be done as a separate transaction afterwards. (This limits composability but might be more practical.)

This would allow objects to switch from multi-writer mode, back to single-writer mode, as needed. For example, if you have a game item that you want to allow a friend to use, but then you want to be able to take full ownership of it again at some later point in time. If shared objects can be deleted, this behavior is already possible by simple destructuring the object, deleting its id, and then packing it into a new owned object (although it will have a different UID).

This is behavior is already sort of possible, in an inelegant way; you can create a wrapper object around T (your owned object), then share Wrapper { T }, add functions that allow for anyone to borrow T temporarily. Then you no longer want to share T, you remove T from the wrapper. However, this is rather clunky and complicates everything and has poor composability (you cannot use entry functions; you would need to wrap entry functions with another module that borrows a reference to T…).

Ideally the unshared object’s UID should stay the same, but if you must change its UID, that’s okay too.

We may also want to add a transfer::is_shared_object(object_ref) function as well, that returns a bool depending on whether an object is shared or owned. This would enable some conditional behavior.

Unfreeze Objects

A common pattern I’ve noticed is something like:

public fun use_sword(sword: &mut Sword, config: &GameConfig) {

this GameConfig struct can be a big struct that specifies parameters, such as how much damage a sword should do, how much durability it should have, etc.

Ideally, this GameConfig should be a frozen, immutable object. Then, if the sword is also an owned (single writer) object, then all transactions using this function can be simple transactions (no need for consensus). This is ideal.

However, what if the developer wants to tweak something in the GameConfig? In that case, they would have to create a new GameConfig, and convince everyone to use this new config file instead, although the old GameConfig will always be around, meaning that users can ‘cheat’ and continue to use the old game-configuration. What is the developer to do? I suppose they could deploy a new, updated module, then migrate everyone over to that…

The other way around it would be to make the GameConfig a shared (multi-writer) object, that only the game devs can edit. Unfortunately, this forces all game transactions to now go through consensus (which takes longer and costs more), for no reason really.

A good middle-ground would be to add an ‘unfreeze’ ability.

transfer::unfreeze_object(object);

This should only be doable by the module that defines the resource. Furthermore, the transaction should have to be finalized before any edits can be done. In this case, effectively ‘frozen’ objects are not truly immutable they are merely ‘rarely ever written to’. In the case of a GameConfig, which is only changed once every few weeks, this works great; the object can be unfrozen, edited, then re-frozen, in 2 or 3 transactions.

Enumerable Table

Because table and object_table both use the same key and value types for all entries, it’s possible to create an index of keys, which can just be a simple vector. This would make it possible to iterate over all items in the table.

For bag, object_bag, dynamic_field, and dynamic_object_field, these allow heterogenous key and value types, so an index is not really possible unfortunately.

Move’s strict static-type system, where every type must be specified on compile, makes it impossible to be able to fully explore an arbitrary dynamic_field at runtime.

Notes on Static Typing in Move

I’ve seen a couple people asking for type-reflection, like converting the type of an object to a string at runtime. As far as I can tell, without major changes to the Move VM, this sort of behavior would be useless even if you got it. The Move VM does not allow any sort of conditional types, it does not allow branches where different types are used for the same variable, it does not allow types to be used generically. For example:

It would be nice if the Move VM could support this sort of behavior, and most new Move developers would expect this code to work, but adding this sort of behavior would be a big ask (I think?).

Dynamic_fields are kind of a clever way of working around this limitation within the Move VM, but they’re also severely limiting, in that the application code must now memorize, and get right, the type of value corresponding to each key when reading stored dynamic fields. This limits its practicality.

Expanding Dynamic Field Functions

As mentioned above, the above code will not work within Move. However, we can enable this sort of behavior at a lower level using native functions; essentially rather than doing these actions within Move, we do them within the Rust-layer that exists inside the Move VM.

Here’s a list of potential dynamic field operations:

  • dynamic_field::drop(parent, key): drops the value and key, assuming the value has the ‘drop’ ability, otherwise this aborts.
  • dynamic_object_field::remove_and_transfer(parent, key, owner): this removes the value at ‘key’, drops key, then transfers the value to the new owner, using polymorphic transfer.
  • dynamic_field::rekey(parent, old_key, new_key): changes the key for value from old_key to new_key
  • dynamic_field::move(old_parent, old_key, new_parent, new_key): moves the value from a dynamic field on the old_parent, to a dynamic field on the new_parent.

Script Transactions

Diem had the notion of ‘scripts’, which are like ephemeral modules that are only executed once. Sui has the concept of transaction bundles, which put several entry-function calls together into one atomic unit (meaning that if one of the calls fails, all the other transactions revert), which is great, but it’s not the same thing.

It’s true that a user can deploy a module, then call it, but that’s two transactions, and modules are all immutable on Sui, meaning they cannot be deleted after the fact in exchange for the storage refund. That means that any script-transactions using this ‘hack’ will be slower and more expensive than they need to be, and they’ll also clog up Sui’s head. It would be better if Sui brought back script transactions.

Multiple Signers for a Transaction

Are there plans to support this in the future? It would require refactoring tx_context::signer() to return a vector of addresses rather than a single address.

Rotating Private Keys

This was an original feature from Diem, and I’d like it if it were possible for Sui to rotate keypairs. Basically, every user has an address (their first public key), which stays constant, and then they can rotate their keypair whenever they need to. This means that if someone thinks their private key has been compromised, they can rotate their key to a new one before the attacker can use their old private key. Currently, as in Solana, the only way to change your private key would be to transfer all of your resources from your current account to a newly generated account; which is impossible mostly since not all resources are transferrable, and also infeasible, because accounts will eventually own thousands of resources.

Timestamps

Down to the millisecond would be ideal

https://github.com/MystenLabs/sui/issues/226

Greater Balance Precision (u128)

Consider mkaing Balance.amount u128 rather than u64. This is because Ethereum and most ERC20 tokens have 18 decimals of precision, which is definitely excessive, but if you’re bridging from Ethereum to Sui you’ll want to be able to represent exact balances. With u64’s we’d be forced to truncate at least a few of those precision-decimals in order to accommodate larger balances of ETH.

Restricted-Transfer Problem:

The gist of this is; allow key-only objects to be stored within a dynamic_object_field, but only by the module that defined the object. This allows for key-only objects to be used as dynamic_object_fields. Once you add the ‘store’ ability to your objects because you want to enable this behavior, this allows any module to wrap the object, polymorphic transfer it, and put it inside any dynamic_field, which may be undesirable to the module-author.

Status: Mysten Labs has debated this idea, and mostly rejected it.

https://github.com/MystenLabs/sui/issues/5584

Misc. Expansions for Move / The Move VM

  • Make strings (both ascii and utf8) into primitive object types, so that they can be used as arguments to entry functions and developers don’t need to perform an extra step on every vector<vector<u8>> input.
  • Make options primitive objects, so that they can be used as arguments to entry functions, such as an optional string that can be filled in with a default value. Ideally, options should also be able to store references (although this would require a different type of non-storable, temporary-option that would has to be dropped at the end of the transaction). For example, if I’m calling into a function, like public fun optional_parameter(maybe: Option<&mut SomeObject>) this will not work. My intention here is to allow this function accept an optional mutable reference to some object, but because structs (including Option) cannot store references, this is impossible.
  • Add the Modulo operator natively to Move; I’m surprised this was omitted.
  • Shorthand vector initialization. We should be able to do let vector = [1, 2, 3, 4, 5, 6], which is much more concise than having to do 6 separate push calls.

Developer Experience Suggestions:

  • Add pre-built binaries for various platforms (Linux, Windows, Mac) for the Sui CLI for each release.
  • Add a Sui faucet into the cli, which is ideally called automatically when you generate a new keypair on chain. (It should be rate-limited in some way however — I imagine that was the logic behind the Discord faucet.)

Transaction Memos

In an entry transaction, be able to attach arbitrary data to the ctx (Transaction Context) as a sequence of bytes. This data can be read by on-chain and off-chain modules.

For example, an exchange might instruct depositors to do a simple transfer of coins to a fixed address (sui::transfer::transfer(coin, address)), and attach a memo to the transaction that specifies the hashed account-number that deposit was intended for.

You could additionally use it to supply custom module-specific security. For example, suppose you want to deposit coin into another person’s vault, and this vault requires permission. It would work something like:

public entry fun deposit(vault: &mut Vault, coin: Coin<Sui>, ctx: &TxContext) {
if (vault.require_permission) {
assert!(check_permission(vault, ctx), ENOT_PERMISSION);
};
coin::merge_into(&mut vault.balance, coin);
}

public fun check_permission(vault: &mut Vault, ctx: &TxContext): bool {
let signature_bytes = tx_context::borrow_memo(ctx);
let owner = &vault.owner;
let nonce = &vault.nonce;
let sender = tx_context::sender(ctx);
// Check that the bytes match a signature from address
...
}

the permission-checker do a cryptographic check like “did the owner-address produce a valid signature of (sender_address + vault_nonce)”? If yes, then return true, otherwise return false.

The bytes could be included explicitly as an argument in the deposit function, but then we’d have to add arguments to both our functions, and a lot of the time we won’t be using those bytes, so callers would be specifying lots of empty u8 vectors for no reason.

Additionally we could allow functions to attach variables to ctx, and then pass those down to other functions further down on the chain, without needing to explicitly pass every variable. This would work much like contexts in React do, where you wrap functions in a context, and inside of that wrapper all children can access variables within the wrapping-context. (Because ctx does not have a UID, we are currently unable to use dynamic fields for this purpose.)

Use A Dual-Native-Coin Model

Similar to xDai (now Gnosis-chain) have two native coins:

  1. Sui-resource: represents a fractional ownership of Sui’s total capacity. This is used to pay for transaction fees and storage. Could possibly be a stablecoin, like dollars.
  2. Sui-ownership: this token is used to stake, representing control of the network. Supply of this is constant and does not increase or decrease. The sui-resource coins used in transactions are paid to ownership holders.

If the resource coin is a stablecoin, we could say 1 coin = $1 USD (permanently). Then the cost of the network varies through time, i.e., some days 1 coin = 1,000 transactions, but under heavy usage 1 coin = only 100 transactions. Some days 100 KBs of storage = 1 coin, but other days 100 KBs of storage = 10 coins.

If the resource coin is not a stablecoin, and its price is allowed to vary, it should be locked to some other resource, i.e., 1 resource coin = 100 KBs of storage.

The Sui-ownership coins represent ownership and control of the network. If there are 1,000 resource coins consumed in a day by transactions, totally $1,000 USD, then those are divided up and given proportionally to the ownership-holders.

Resource-coins could be thought of as a currency (dollars) used to buy a company’s services, while Ownership-coins could be thought of as equity (shares) in that company.

I can’t think of any downside to this model, other than the SEC being more likely to raise a stink because the ownership-coins look more like a security now. But we should design our systems around logic and function, not arbitrary governmental whims.

--

--