Sui Improvement Proposals
Sui is very novel, but I fear it’s too inflexible to be practical. The following ideas are suggestions for improving Sui’s programming model; I don’t go into how to implement these proposals.
Restrict The Transfer Function
sui::transfer::transfer
is just too powerful; this function allows any resource-owner to transfer any resource to anyone. There are many cases when this might not be desirable.
💡 Example: USDC. Circle (issuer of USDC) maintains the ability to freeze user-balances at the request of legal authorities, in order to fight money laundering and theft. This is a must-have feature for Circle.
On Aptos, the aptos_framework::coin
module has a function, coin::initialize<CoinType>()
which creates a new coin type and returns a freeze_cap<CoinType>
to the creator address.
A user’s coin is stored inside of a CoinStore struct, which has a ‘frozen’ state, which defaults to false.
Anyone with that freeze_cap (usually the creator address) can call coin::freeze_coin_store<CoinType>(address, &freeze_cap)
to freeze the CoinStore of the specified user address. Once a CoinStore is frozen, any calls by that user to coin::transfer
, coin::deposit
, and coin::withdraw
will fail. This means that the user’s coin is inaccessible. The creator can reverse the process with a call to coin::unfreeze_coin_store
.
This is a very simple, intuitive implementation. For an indexer, it’s trivial to see who has the ability to freeze or unfreeze coin stores, and to see which user’s CoinStores are frozen.
Sui has a much more complex attempt at implementing the same behavior, called regulated_coin:
First, it defines a RegulatedCoin struct, which has a balance and a ‘creator’ address; this is the REAL owner. If 0x123 owns an instance of RegulatedCoin on Sui (let’s call it r_coin), but r_coin.creator == 0x789, then 0x123 will be unable to do a lot with regulated_coin, because he’s not the “true” owner. That is to say, regulated_coin defines and maintain its own ‘owner’ field, separate from Sui’s concept of ownership.
When someone creates a RegulatedCoin type, they receive a treasury_cap, and creates a shared object called Registry. This registry contains a vector of banned addresses.
If 0x789 wants to send a RegulatedCoin to 0x123, he must call my_coin::transfer(®istry, &mut coin, value, 0x123)
, where ‘coin’ is an instance of RegulatedCoin; this function takes the coin’s balance and wraps it inside of a Transfer struct, which is transferred to its new owner, 0x123. This transfer struct includes a “to” field, which will be 0x123’s address. Note also that the ®istry is required; the transfer function will check against this registry to make sure neither the sender nor receiver’s addresses are on the ban list.
0x123 must then call my_coin::accept_transfer(®istry, &mut my_coin_balance, transfer_struct)
, which will remove the balance form the Transfer wrapper, and place it inside of his my_coin_balance. Because 0x123 is the ‘true’ owner of the Transfer struct, if 0x123 were to transfer it to 0x456, 0x456 would be unable to unwrap the Transfer struct using accept_transfer because their address isn’t on it; 0x123 is instead.
Notice how much worse this solution is compared to Aptos’ solution:
- This solution is more complex, and more difficult to understand. It essentially circumvents Sui’s built-in ownership model and makes up its own ownership model.
- There is now a single ban-list vector which must be kept in memory and constantly checked against, whereas Aptos has a bunch of small unrelated objects, allowing better memory-concurrency. The use of a shared object also means that all transfers must now go through Sui’s consensus, which means these transactions cannot take advantage of one of Sui’s biggest strengths.
- Vectors in Move can only hold 65,536 entries, meaning eventually Circle will be unable to ban new RegulatedCoin addresses simply because its ban-list vector is full.
- Transfer now require two transactions; first a transfer transaction from the sender, and then an accept_transfer transaction from the receiver. In Aptos, only the sender needs to do a transaction (assuming the receiver already has a CoinStore available to be deposited into).
More importantly; this doesn’t really solve the problem. This entire ban list can be circumvented by a simple ‘proxy ownership service’. 0x1 could deploy a module which accepts all regulated_coins sent to it, setting itself (0x1) as the ‘owner’, and then it immediately uses sui::transfer::transfer (not abc_coin::transfer) to send these coins back to the original sender. These regulated coin owners are then free to use sui::transfer::transfer to send regulated coins to each other as they please, even if they’re banned. When they want to take full ownership of the regulated coin again, they simply sui::transfer::transfer it back to 0x1, requesting it to be sent to an unbanned address they control. 0x1 sets the new unbanned address as the new owner, rather than itself, and immediately transfer it, ending the proxy ownership.
This system effectively defeats the entire purpose of regulated_coin existing.
The fundamental problem is that module-creators have no real control over the resources they create after they create them and give them to a user; the user has complete, irrevocable control at that point.
Proposal:
💡 Restrict sui::transfer::transfer to being used only within the module that defines the struct being transferred. This will allow module-authors to control how their resources are transferred.
Module-authors can choose to build-in a my_module::transfer() function if they want, or to make an object completely non-transferrable.
Downside:
I realize that Sui wants generic, system-defined asset-transfers, so perhaps we can give my_module::transfer
functions some special status within Sui, or require transfer functions to satisfy a certain specification (spec file).
The beauty of sui::transfer::transfer
is that it allows ownership to be transferred generically; I don’t need to know what the asset type is that I’m transferring, or where it’s coming from. This allows Sui to be very generic.
Having to replace the universal use sui::transfer::transfer
with use coin_address::coin_name::transfer
for EVERY struct type I intend to use would make Sui less generic, and less useful. So perhaps we should standardize every module’s transfer function signature, but leave the implementation logic up to the module authors; for example, for a non-transferrable resource, the implementing body would simply be assert!(false, 0);
. For regulated coins, we would simply use regular coins with a ‘frozen’ boolean property that can be checked inside of its own transfer function, similar to the Aptos example above.
Allow Modules To Use Struct-Types They Defined, Even Without User Permission
This is related to the above problem; once a module creates and transfer a resource to a user, it has no control over it.
In traditional move, a module can use global-memory operators like borrow_global_mut<MyResource>(address)
or move_from<MyResource>(address)
, which allows modules to grab resources that they defined from a user’s global storage, and modify them, without the user signing the transaction. This is amazingly flexible.
For example:
This allows 0x1 to create a Budget object, and give it to 0x2. 0x2 can then later use this Budget object to redeem Money from 0x1’s account. Furthermore, 0x1 can revoke 0x2’s Budget at any later time.
How can we reproduce this in Sui? We could obviously just make Money and Budget shared objects, and then add an ‘owner’ field to both them, which we always check against to figure out who the “real” owner is, but for gas efficiency and parallelization we always want to use owned-objects whenever possible; it seems silly to create a “Shared Object” that can only really be used by one or two persons.
Proposal 1:
💡 Bring back Move’s global storage operators. The sui::test_scenario package already has a bunch of these methods, like take_owned or take_owned_by_id specifically for Sui. These could be very useful in production.
Downside:
I like that Sui consists solely of pure functions, in the sense that functions are solely dependent upon their inputs, and do not use or modify any global storage. This also provides safety guarantees that a function can, at worst, only steal the objects you gave it as inputs. In Aptos, if you sign a malicious transaction, that function now has your signer object, which it can use to do literally anything with your authority, including draining your wallet.
That is to say, Sui functions are easier to parallelize (in terms of memory), the state of the blockchain is easier to shard, and transactions are easier to simulate and are safer to sign blindly.
Perhaps we can accommodate this:
Proposal 2:
💡 Allow structs to contain mutable and immutable references to other structs stored in global storage.
This would look something like this:
The reason why this behavior wasn’t included in the original version of Move was because they couldn’t figure out how to serialize references. However in Sui Move references to other global storage objects is easy because every struct with the key or store ability has a unique global identifier.
These references would essentially act as storable keys, which can be recalled from global storage later, allowing the the owner of these keys to access other people’s globally-stored objects.
It could get risky if these stored references could be used across modules; imagine Coin giving a reference to its balance to a function in malicious_module; the function then uses the balance and ends execution. Currently, the balance-reference is then dropped from memory, and that module cannot use the balance again unless we call it and give it another reference. BUT if the malicious_module could store this reference, it could access the user’s coin balance again and again in the future, stealing funds in unpredictable ways.
Because of this, we should have the rule that a module can only store references to other structs which it also defines, as is the case in our above example.
But wait; I have even more wacky ideas if you don’t like that one.
Proposal 3:
💡 Allow functions to define type-signatures that can accept objects that are neither owned by the transaction sender, nor are shared objects. This is limited to only struct types that are also defined by the module.
This would work something like this:
Note that the sui::object::owner
function does not exist yet, but I imagine it would take a reference to an object and then return the address of the owner of that object.
Anyway, in the above function money
is marked with a ‘!’ in its type, which means that it does not need to be owned by the transaction sender; it can be a Money object (as defined in this module) that is owned by anyone. The spend_money function then makes sure that the budget_cap authorized address and the Money object’s owner address match up; this prevents anyone with a budget_cap from using anyone else’s money.
Instead of checking the owner address, we could instead link the object id; either method would work perfectly well.
But basically this allows modules to define functions where users can sign a transaction which takes someone else’s owned objects and modifies them. In this case, protecting ownership is up to the module to implement correctly, rather than up to Sui.
I think this is a very flexible system that opens up a world of new possibilities, and I can’t really think of any downsides either, other than that you might be able to write-lock someone else’s owned objects by creating one of these transactions and then getting it 33%+ confirmed but not going all the way to 66%+ to get it finalized, which would effectively prevent that user from using that object until that transaction expires. But this is more of a problem with how Sui consensus works than with how Move works; the transaction-verification process should prevent these sorts of blocking-actions.
Oh but don’t worry; I’m not out of stupid ideas yet!
Proposal 4:
(Still working on this one…)
Further
Please drop a comment and give me your feedback and thoughts on these ideas! You can contact me at paul@fidika.com, Telegram: @PaulFidika, or Twitter: PaulFidika
Also checkout my company OpenRails: https://github.com/orgs/open-rails/repositories