Idiomatic way to allow only 1 instance of object for each address

I would like to know the Idiomatic way to allow only 1 instance of the object for each address.

My current solution is to use dynamic_object_field.

Here is an example to help reproduce the problem,

    struct Bank<phantom T> has key {
        id: UID,
        vault: Balance<T>,
    }

    struct Account has key, store {
        id: UID,
        balance: u64,
    }

    public entry fun add_bank<T>(ctx: &mut TxContext) {
        transfer::share_object(
            Bank<T> {
                id: object::new(ctx),
                vault: balance::zero<T>(),
            }
        );
    }

    public entry fun deposit<T>(bank: &mut Bank, depositor_coin: &mut Coin<T>, amount: u64, ctx: &mut TxContext) {
        let sender = tx_context::sender(ctx);
        let deposit_coin = coin::split<T>(depositor_coin, amount, ctx);

        if (!ofield::exists_(&bank.id, sender)) {
            ofield::add(&mut bank.id, sender, Account {
                id: object::new(ctx),
                balance: amount,
            });
        } else {
            let account = ofield::borrow_mut<address, Account>(&mut bank.id, sender);
            account.balance = account.balance + amount;
        }

        balance::join<T>(&mut bank.vault, coin::into_balance<T>(deposit_coin));
    }

If we simply transfer the ownership of the Account object to the user, it will make the user can have multiple Account objects.
So to make the Account object only created once for each address, I use the address of the user as the key of the dynamic field. If a field with the name of the address of the user exists, that means the user already has the Account object otherwise we will create one for the user.

Is this the idiomatic way to solve this case? Or there’s a better way to do it?

8 Likes

Your pattern satisfies your brief, but it requires a shared point of coordination (the Bank) which means you will not be able to take advantage of the faster single-owner transaction path, and it can be circumvented by a person generating multiple addresses.

For these reasons, we usually avoid patterns like this on Sui, because our object model is more similar to UTXO than Accounts.

Could you share what it is you’re trying to achieve? There is perhaps a more idiomatic way to do it.

6 Likes

I’ve been thinking about the downside of this pattern, especially the single-owner transaction path but an object like Bank should be a shared object. Let’s say it has a total_balance field to count the total balance of all user accounts, when the user going to deposit some amount of coin, the total_balance field needs to be mutated.

Actually, an address that has many Accounts in a Bank is fine. I just wondering what would be the best solution for the 1 account for 1 address problem.

Another question, Is there any limit to how many dynamic fields an object can have?

1 Like

You’re right that a Bank that needs to maintain a total_balance could benefit from being shared, but this does not require a restriction of one account per address – that’s the bit that I am interested in (why the package you are building requires that each address can hold only one account).

In Sui’s model, it is more typical to allow an address to hold multiple accounts and have the Bank track the Account’s IDs, rather than require that an address can only hold one account. This pattern is more flexible and it makes it easier to transfer accounts.

Re: limits for dynamic fields. There is no limit on the number of dynamic fields that an object can have, but there are limits on how big each object (individually, without its dynamic fields) can be, and how many objects can be touched in a single transaction.

Cc @ade who is currently working on limits.

1 Like

This case is just an example because I’m wondering about the solution to this case. There might be a case when we want to store the date when the user registered as a customer in a bank for the first time. We will probably need a Customer object to store that date and also related user information. This Customer object couldn’t be more than 1 which this goes against the recommended concept.

But I understand the part that we need to avoid this kind of design.

So the size of an object is only counted from the defined fields (non-dynamic fields)?

Got it, there definitely are some cases where you might want to enforce this restriction on what addresses can do with objects (how many of them they can have, and whether they can transfer them), KYC (Know Your Customer) is a legitimate case.

So the size of an object is only counted from the defined fields (non-dynamic fields)?

That’s correct – but note that it is not a count of “one” per field, it is the cumulative size of all its non-dynamic fields (so for example, a struct that contains a vector may have a dynamic size).

1 Like

Exactly, some cases are unavoidable.

It’s clear now, thank you for your time!