Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] [move] Shared Coin Standard #4680

Merged
merged 6 commits into from
Oct 21, 2022
Merged

[RFC] [move] Shared Coin Standard #4680

merged 6 commits into from
Oct 21, 2022

Conversation

punwai
Copy link
Contributor

@punwai punwai commented Sep 16, 2022

Motivation

In many traditional smart contract platforms, it is common for protocols to be able to debit a user's account, provided that the user delegates them the permission to do so. Here are some examples of where this is useful:

  • Marketplace Bidding and Auctioning -- Where you want the resource owner to be able to execute the sale without you having to sign the transaction (This is a major one)
  • Subscriptions and any kind other kinds of automated payments -- Where you want to be automatically debited every month.

This is natively possible with the Ethereum ERC-20 standard, where a user would be able to give certain addresses the permission to transfer coins from their wallet. However, in Sui's programming model, where coins are owned objects, this is inherently not possible.

What this RFC contains

This RFCl introduces the safe module, which includes the Safe struct -- essentially a coin with shared access controls.Safe is a shared object, but only a specific owner address will be able to freely remove funds from it. Anybody is able to add funds to the safe. The owner is able to create Capability structs which allow the holder to withdraw a fixed amount of coins from the Safe. Once a Capability is created, the owner is able to revoke the access at any time.

Technical Considerations

Safe currently uses a bitmap to allow revocation of Capabilities at any time in the future. Each additional capability, on average, adds one bit to the side of the Safe. I personally don't think that the cost that this adds is of any concern, and if it is, they can always just move their funds to a new Safe.

Possible Expansions

It is also easy to create different kinds of access controls. We can also have things like SubscriptionCapability that allows withdrawals at every 10 checkpoints or something.

We are thinking of also creating a proposal for the LootBox standard -- an analogous standard for NFT delegation.

Proposers: @punwai @GrantStenger @justinwlin | kinetic.xyz

@punwai punwai changed the title [SIP] [move] Safe (Shared Coin) Standard [SIP-1] [move] Safe (Shared Coin) Standard Sep 16, 2022
@punwai punwai changed the title [SIP-1] [move] Safe (Shared Coin) Standard [SIP] [move] Safe (Shared Coin) Standard Sep 16, 2022
@punwai punwai changed the title [SIP] [move] Safe (Shared Coin) Standard [SIP] [move] Shared Coin Standard Sep 16, 2022
@punwai punwai requested a review from damirka September 16, 2022 13:30
@Jordan-Mysten
Copy link
Contributor

I think the goals here make sense, and I do think we need some solution to allow coins to be spent by other parties. In general I'd be really curious about the UI implications of the Safe proposed here. For other chains, approval itself doesn't alter coin balance, but from what I can tell here it looks like here it would actually alter the balance for users directly.

I imagine if this is something we're providing natively, we'd probably want ways to display Safes within the wallet, similar to how we display coins.

Copy link
Collaborator

@sblackshear sblackshear left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like the idea, and think that this will be useful! Some details to be worked out here around revocation--see inline comments.

///
struct Safe<phantom T> has key {
id: UID,
owner: address,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think here, removing owner and instead adding a separate OwnerCapability { id: UID, safe_id: ID } that serves the same purpose might buy some valuable flexibility. This enables use-cases like:

  • Transferrable Safes (which in turn enable key rotation)
  • Safes protected by a smart contract wallet or managed by a DAO
  • KELP-like social recovery schemes for the funds in a vault

id: UID,
owner: address,
balance: Balance<T>,
permissions_bitmap: vector<u64>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/deps/move-stdlib/sources/bit_vector.move might be useful here--I think it is desirable to separate the (rather generic) implementation of the bitvector data structure from the (rather specific) implementation of Safe.

Copy link
Contributor Author

@punwai punwai Oct 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was looking at the BitVector implementation, and was wondering whether it could potentially be less efficient than just storing u64s? First observation is that it stores a vector of bools, which I presume is a byte each rather than a bit (you probably know best). But my biggest concern is that it has a fixed size at instantiation, which I don't think is idela, as the variance in the usages of Safe is likely huge (a few power users, many just buy an NFT here and there). I'll change to BitVector for now, but I'll keep the old commit unsquashed incase.

}

/// Safe Owner Only
public fun revoke_capability<T>(safe: &mut Safe<T>, capability: &Capability<T>, ctx: &TxContext) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this probably wants to take the nonce as input. Otherwise, a malicious Capability holder can prevent you from revoking their capability by making it a single-owner object, or by wrapping it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this (in turn) means that either the Safe must remember the amount of each capability it issued (otherwise, it won't know what to subtract during revocation), or must only support a revoke_all API.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I've just realized that this is a problem - a fix was just pushed in.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the Safe have to remember the amount of each capability? Currently revoking just completely turns off the capability.

Copy link
Contributor Author

@punwai punwai Sep 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, another thing we might want to consider is whether it is desirable for the author to change the amounts which they allow capabilities to use. In this case, we could instead store a vector of u64 amounts in Safe that represents how much each capability could take out.

create_capability_(safe, withdraw_amount, ctx)
}
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might also be useful to have API's for:

  • allowing the owner (or more likely, anyone?) to deposit more funds into a Safe
  • allowing a Capability owner to voluntarily relinquish permissions
  • allowing anyone to read a Safe's balance
  • deleting an empty Safe

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 is just pushed in through another commit. The rest I'll add!

id: object::new(ctx),
safe_id: object::uid_to_inner(&safe.id),
nonce: safe.next_nonce,
amount: withdraw_amount,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible (though debatable whether it's desirable) for a Safe to have "overdraft protections"--it can remember how much it's promised via issuing Capability's and make sure not to promise more than is in the balance.

Probably not be desired for all use-cases, but interesting to think about as an option.

///
/// @ownership: Owned
///
struct Capability<phantom T> has store, key {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we call this something like AmountCap, that could open the door for issuing other kinds of capabilities in the future (e.g., a cap that gives you ownership of a share of the funds rather than a fixed amount). Maybe that wants to be a different standard, but could be an interesting building block.

Unrelated observation: a FlashLender does not look so different than a Safe--just a different revocability policy (you must self-revoke by end of tx + pay to do so) https://github.com/MystenLabs/sui/blob/main/sui_programmability/examples/defi/sources/flash_lender.move#L13

@punwai
Copy link
Contributor Author

punwai commented Sep 17, 2022

@Jordan-Mysten I completely agree -- the wallet should be responsible for displaying amounts in Safes. I think what would make sense is if we implement was Sam says (OwnerCapability), then we could query all Safes that the user owns along with the coin. Happy to add this into the wallet as well!

@sblackshear sblackshear changed the title [SIP] [move] Shared Coin Standard [RFC] [move] Shared Coin Standard Oct 1, 2022
@punwai punwai force-pushed the safe branch 4 times, most recently from 295c202 to 3b87a65 Compare October 1, 2022 22:42
@punwai
Copy link
Contributor Author

punwai commented Oct 1, 2022

@sblackshear ready!

@punwai punwai force-pushed the safe branch 3 times, most recently from 2f353ae to c6c471d Compare October 2, 2022 02:18
Copy link
Collaborator

@sblackshear sblackshear left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like these improvements! A few more tweaks--the biggest conversation is around the bitmap.

crates/sui-framework/sources/safe.move Outdated Show resolved Hide resolved
}

/// Revoke a `TransferCapability` as an `OwnerCapability` holder
public fun revoke_transfer_capability<T>(safe: &mut Safe<T>, capability: &OwnerCapability<T>, capability_nonce: u64, _ctx: &TxContext) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public fun revoke_transfer_capability<T>(safe: &mut Safe<T>, capability: &OwnerCapability<T>, capability_nonce: u64, _ctx: &TxContext) {
public entry fun revoke_transfer_capability<T>(safe: &mut Safe<T>, capability: &OwnerCapability<T>, capability_nonce: u64) {

crates/sui-framework/sources/safe.move Outdated Show resolved Hide resolved
id: object::new(ctx),
balance,
next_nonce: 0,
permissions_bitmap: bit_vector::new(1000),
Copy link
Collaborator

@sblackshear sblackshear Oct 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at how this is used, I'm wondering if it wouldn't be simpler (and more space-efficient, in the typical case) to just use a vector<ID> here.

A BitVector of length 1000 occupies 8 + 8 + (8 * 1000) = 8016 bytes even when there are no active capabilities, and can only grow in size over time because we never reuse "capability nonces". It also requires an extra 8 bytes in the capability to store the nonce.

By contrast, a vector<ID> will be proportional to the number of outstanding capabilities at the current point in time (we'd need to have ~286 active capabilities simultaneously before it got as big as the empty permissions_bitmap), shrinks as the number of active capabilities shrinks and we also won't need the 8 byte nonce inside each capability anymore. Checking capability membership is a a large set is less efficient than checking membership in a large bitmap, but I think this is likely to be a wash for small-to-medium capability counts (which is what I'd expect to see in practice).

The set approach would also allow you to support a revoke_all API (useful if there is e.g., a known vuln) by simply emptying out the vector.

Finally, knowing the ID's of each outstanding capability makes it easier for the owner of the Safe discover where their delegated permissions are (e.g., by using an ID lookup).

Copy link
Contributor Author

@punwai punwai Oct 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other idea is to revert to the previous bitmap implementation that I had, it adds on a u64 every time that it needs to extend the length of the bitmap, and this means that on average, each capability adds on only 1 bit. This of course does not have the benefit of being able to clean up space when revoking an ID. We would also be able to support a revoke_all API by simply looping until next_nonce and disabling all the capabilities. But of course, this does not have the benefits of easier lookups for users.

Advantages:

  • More space efficient than both suggested implementations
  • Can support revoke_all
  • inclusion checks are O(1)
    Disadvantages:
  • Harder to do lookups

Copy link
Contributor Author

@punwai punwai Oct 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sblackshear Leaving this choice up to you!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your other bitmap would indeed be more space-efficient than the bitvector library (sorry for suggesting you switch to that and then criticizing the choice lol, I forgot how inefficient that library is...).

I would vote for going with the vector<ID> approach mostly because it's (a) very simple and (b) not ever-growing--I think both the vector and the bitvector approach can comfortably support hundreds of simultaneous capabilities, which seems good enough. A Safe that needs thousands would probably want to use one of the forthcoming maps or something else.

crates/sui-framework/tests/safe_tests.move Show resolved Hide resolved
@github-actions github-actions bot added the Type: Documentation Improvements or additions to documentation label Oct 12, 2022
@github-actions github-actions bot removed the Type: Documentation Improvements or additions to documentation label Oct 12, 2022
@punwai
Copy link
Contributor Author

punwai commented Oct 12, 2022

Apologies for the late reply -- changes have been pushed!

@punwai
Copy link
Contributor Author

punwai commented Oct 12, 2022

Changes ready for review.

@sblackshear sblackshear enabled auto-merge (rebase) October 12, 2022 21:18
@sblackshear
Copy link
Collaborator

Thanks for this Pun! Looks good to go.

@sblackshear
Copy link
Collaborator

Looks like this needs some license headers in the new .move files

@sblackshear
Copy link
Collaborator

And updates to the snapshot tests because the additions to the framework changed genesis.

Copy link
Collaborator

@kchalkias kchalkias left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job @punwai. Good to go after resolving the failed CI tests.

punwai and others added 6 commits October 20, 2022 22:27
change to comments

add error names

add comments

changes

f

f
@sblackshear sblackshear merged commit 02cae19 into MystenLabs:main Oct 21, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants