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

Accounts prototype (move side only) #20649

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

mystenmark
Copy link
Contributor

Prototype of the move-side implementation of sui accounts.

The basic design is this:

  • Modules must define a translation from the "user" type (e.g. Balance<T>) to a type that supports merging (e.g. MergableBalance<T>)
  • Mergable types are types constructed entirely from types defined in mergable.move
  • Things not shown/implemented in this PR:
    • The "mergable" types are what would be stored in the actual DF for a given account.
    • Merging will be done by rust code that traverses values and merges each subfield defined in mergable.rs
    • Merging could be done by the VM as well but who wants to invoke the VM to add two u128s together.

Things to note:

  • There are no untyped values anywhere (its not "ethy")
  • No special-casing in the framework for Balance or SUI - any type can use this system

Important TODOs:

  • Verifier changes are necessary to make transfer_to_account private in the same way that transfer is
  • PTB checker would have to verify that reserve is only called with an Input, not a result. I'm very open to better ideas of how to do reservations.

Things to not worry about:

  • No, events are not the right way to do this. I used them because they are there and they demonstrate what we care about, which is that types are preserved as data flows through the system.

Copy link

vercel bot commented Dec 16, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

3 Skipped Deployments
Name Status Preview Comments Updated (UTC)
multisig-toolkit ⬜️ Ignored (Inspect) Dec 16, 2024 9:36pm
sui-kiosk ⬜️ Ignored (Inspect) Dec 16, 2024 9:36pm
sui-typescript-docs ⬜️ Ignored (Inspect) Dec 16, 2024 9:36pm

@mystenmark mystenmark temporarily deployed to sui-typescript-aws-kms-test-env December 16, 2024 21:36 — with GitHub Actions Inactive
/// be created from a u64. This ensures that overflow is impossible unless
/// 2^64 Sums are created and added together - a practical impossibility.
public struct Sum has store {
value: u128,
Copy link
Contributor Author

@mystenmark mystenmark Dec 16, 2024

Choose a reason for hiding this comment

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

note that this is overkill for supporting Balance. We could have another type called CappedSum { u64 } for when values can't overflow.

But it would be dangerous to expose this type, since if it were improperly used by user code it could cause overflow.

So, we would have the option to special case Balance anyway and save 8 bytes if we want to, by having a public(package) CappedSum type.

entry fun reserve<T>(owner: address, limit: u64, ctx: &TxContext): Reservation<T> {
// TODO: handle sponsored transactions and (in the future) multi-agent transactions
assert!(ctx.sender() == owner, ENotAccountOwner);
return Reservation { owner, limit }
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
return Reservation { owner, limit }
Reservation { owner, limit }

/// Because types must explicitly implement a conversion from their ordinary type to a mergable
/// type (i.e. one made only of types defined in mergable.move), there is no need for an analogue
/// to `public_transfer`.
public fun transfer_to_account<T>(deposit: T, recipient: address) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Was thinking about this, and I unfortunately don't think it's safe if:

  1. recipient is an object ID (rather than an end-user address)
  2. We allow transfers to "wrapped" accounts (e.g., struct Obj has key { .., wrapped_account_id: ID })

The reason is that with wrapped Balance/Coin fields, you often want to maintain invariants that could be violated by a direct transfer. For example, in a classic CFMM pool

struct Pool has key {
  // ...
  coin1_account_id: ID, // before: Balance<T1>
  coin2_account_id: ID, // before: Balance<T2>
}

, you would want to maintain the invariant balance(coin1_account_id) = k * balance(coin2_account_id). With the current Balance-based approach, you leverage encapsulation to enforce this--e.g. only allow writes in swap, and only if the invariant are maintained. But with an implementation that used wrapped accounts, it would not be possible to enforce this invariant because of (2) (e.g., can just deposit directly to balance 1 without touching balance 2).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Two approaches come to mind for addressing this--the key ideas behind both are to enforce encapsulation on transfers to "object accounts":
A. transfer_to_account requires a &mut UID or &ID of object O to transfer to an object account for O (not needed for an address account, of course)
B. Object accounts must be explicitly created, and have a different/more structured API for transfers than addresses. E.g.,

struct Account has store {
  id: UID,
}

public fun create(ctx: &mut TxContext): Account

// could also split into more fine-grained caps
public fun transfer_to_account<T>(deposit: T, account: &mut Account)

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.

3 participants