Smart contracts – best practices


See also: Software development – best practices

Writing the smart contracts

How to convert between a role and a reference

Problem

You know that any permission (normally defined in definition.permissions) may depend on a role (which can actually be mode = all or mode = any and contain multiple actual roles). It can also contain a requires condition with all_of or any_of fields being the lists of references to be checked (and to be satisfied either simultaneously, for all_of, or to be satisfied if at least a single one reference is satisfied, for any_of). But can you adapt a reference to a role (so, e.g., a mode = any complex role will utilize a reference check)? Or vice versa, can you adapt a role to a reference (so, e.g., a requires.any_of check will utilize a role check)?

Solution

Adapting a role to a reference check

For example, how to adapt an owner role to a reference check in the permission?

Use the following in the reference condition:

this can_play this.owner
Adapting a reference to a role

For example, how to adapt an external_reference reference to the role?

It is less known, but the reference can be used not just inside the permissions, but as an extra check in some role as well!

- role1:
  type: simple
  required:
    any_of: [external_reference]

How to write a “finder keeper” contract: change owner to “anybody”

Problem

Imagine you want to perform a giveaway, and give out some token (or some other contract) to the first person who finds it and registers themselves as an owner. How to do that? Usually you change the contract owner to a specific new owner (address/public key), and after that only the owner is capable of using it; if you register some “nobody” as a new owner, you lose your own ownership right, but then only the registered “nobody” (i.e. noone!) will be able to use it. How to register a contract to “anybody”?

Solution

Registering it to “nobody” is registering to a role (like, SimpleRole) that will never be satisfied. Registering it to “anybody” is, instead, registering it to a SimpleRole that is always satisfied. You can create a SimpleRole that has a bound reference, which is always true; like, ref.origin defined.

Using the smart contracts

How to make utility tokens on top of Universa

Problem

Imagine a new service called KTN is going to launch a new blockchain-powered game selling/exchanging some, say, “virtual kittens”; and this game will have an internal currency called “virtual dollars”.

For this purpose, at least two new different kinds of contracts are to be created: VKTN is a smart contract for a kitten, and VUSD is a smart contract for “virtual dollars”.

VUSD is going to be a “utility token”, and, in the KTN service, this will be the primary currency to pay for all blockchain operations – for example, for breeding virtual kittens or changing their owner. Whenever you register something in KTN blockchain, you spend VUSD to pay for that.

It seems somewhat trivial if the KTN service builds up a whole new blockchain from scratch.

graph LR Alice["Alice"] Bob["Bob"] KTN("KTN blockchain") Alice-- sends a kitten -->Bob Alice-. pays VUSD .->KTN

It is a bit less trivial if some existing blockchain like Ethereum is used as a backbone. Because the user also have to pay the Ethereum “gas” fees, so the user has to deal with both VUSD tokens and also obtain some ETH for fees.

graph LR Alice["Alice"] Bob["Bob"] subgraph Blockchain layer KTN("KTN service") Ethereum("Ethereum blockchain") KTN---Ethereum end Alice-- sends a kitten -->Bob Alice-. "pays VUSD" .->KTN Alice-. "pays ETH (spends gas)" .->Ethereum

Can it be done in Universa in a way easier for user?

Solution

Yes!

It can be abstracted in such a way that the user won’t even know they deal with Universa and UTN tokens – and still, UTN tokens will be used to reserve U needed for network functioning>

As the KTN team has anyway to develop the whole stack for that, they will develop a new service for dealing with cross-blockchain exchanges; and similarly to Universa’s URS, let’s think they called it “KURS” (“K for 🐱kittens!”).

In Universa, the amount of energy/resources required to deal with the contract registration is called “U”; in Ethereum, it is “gas”; for KTN, let’s call it “milk”.

So the user has to spend some VUSD to “buy some milk for kittens”. This may be a separate operation, similar to “reserving some U” in Universa, making it possible to reserve a large amount of U in advance; this may be an immediate operation (so in a glimpse, VUSD is spend to “buy some milk” – and only the amount of milk needed for this operation – and the milk is immediately wasted for KTN network functioning – like with Ethereum gas); doesn’t matter, only the whole idea matters.

From user’s viewpoint

So, from the user’s point of view, this will look this way:

sequenceDiagram participant Alice participant Bob participant KTN as KTN Alice->>+Bob: sends a kitten Alice->>+KTN: sends the transaction, pays VUSD for “milk” KTN-->>-Alice: registers the “send a kitten” transaction Bob-->>-Alice: thanks!

But what happens one layer below?

Inside KTN architecture

sequenceDiagram participant Alice participant KURS participant KTN Alice->>+KURS: pays VUSD for milk KURS-->>-Alice: returns “milk” Alice->>+KTN: sends the transaction, provides milk KTN-->>-Alice: registers the “send a kitten” transaction

Just like if the KTN service had created a whole independent blockchain, the user just thinks they spent some VUSD and got some “milk” in return; and they are using “milk” to pay for the network.

But what is most important: now there is only one single place, KURS, that needs to deal with UTN tokens directly; know about them (e.g., purchase them on the exchange) and reserve U!

And note, that even Alice thinks it is KTN service that handles the whole transaction, actually it is the Universa network that handles it.

So let’s dive deeper.

Inside KURS

sequenceDiagram participant Alice participant KURS participant URS Alice->>+KURS: pays VUSD for milk Note over KURS: exchange VUSD internally to UTN KURS->>+URS: reserve some U on behalf of Alice URS-->>-KURS: return some U owned by Alice KURS-->>-Alice: returns “milk” and (behinds the scenes) U

From the UI level, Alice just purchased some milk from KURS. But, behind the scenes, KURS called the UUTN service at URS and reserved some U.

It is KURS who internally maintains the appropriate balance of UTN tokens to handle all the transactions of KTN players in Universa network. It is only KURS who has to purchase the UTN tokens and deal with them directly. And whenever the user thinks they purchase some “milk” (in terms of KTN), the KURS actually reserves some U for them (in terms of Universa).

Thankfully, UUTN service has a feature of reserving the U smart contracts for a different party; so it is KURS whose private key is used to pay for U reservation, but it is Alice whose private key will now own these Us.

So now Alice can “send the transaction” in KTN service; but behind the scenes, she already has got some Us reserved, and can use them to actually send the transaction in Universa network.

sequenceDiagram participant Alice participant KTN participant Universa Alice->>+KTN: (according to UI) sends the transaction, provides milk Alice->>+Universa: (actually) sends the transaction, provides U Universa-->>-Alice: (actually) registers the transaction KTN-->>-Alice: (according to UI) registers the “send a kitten” transaction

How to register multiple revisions in a single transaction

Problem

Have you ever thought about registering multiple revisions in a single transaction?

Usages for this can be numerous:

  1. Batching/caching the multiple general-purpose changes.

    Imagine your smart contract tracks some object changes that happen just way to irregularly: there can be hours or days without a single change in the contract, and then there can be a dozen or a hundred of changes within several seconds.

    Like, you have a hosting service, and each smart contract tracks the user visits of the control panel and administrative actions taken – normally the user won’t visit it for weeks, but then visits it and does many operations at once, signs them with their own private key (and thus each change is externally signed by them) and wants all of them reflected in the Universa distributed ledger.

    If your service has many users doing many operations at once, attempting to register every control panel action in Universa may put a heavy load on your servers, both network load (for each action, you have to send the registration request to Universa) and CPU load (you also have to handle the Universa network responses, wait for the network consensus, postpone registering all subsequent changes until the consensus is reached, etc etc).

    In such a scenario, it makes some sense to not register every contract change, but probably cache and batch it, e.g.: collect all changes until either 1 second elapsed since the latest received change, or 10 seconds elapsed since the earliest non-registered change. This lets your system ensure than the worst-case registration delay is 10 seconds while best-case is still 1 second.

    Note this would also somewhat decreases the cost of the registration comparing to registering multiple changes independently, but only slightly: if you want the most cost-effective registration, you must collect your changes on the middleware level, and make a single new revision with all the changes, if possible.

  2. Batching multiple token (split-join) operations at once.

    Even more specific case for multiple operations are the token split-join operations. You may run a “virtual bank” or “crypto currency exchange”; and you may do many internal operations in it: intra-client transfers, matching the exchange sell/buy orders, etc. As all of these operations are internal and happen in your own backend, you definitely trust them already; the blockchain registrations are intended for public transparency only.

    So, to decrease the network/processing load on your servers, you may want to implement the batching (with best case and worst case) similarly to the previous case. In revision 17 you spend (and sign) some ABC token giving out 127.45 of them to user Alice; in revision 18 you are also giving 223.56 ABC tokens to user Bob; in revision 19 Alice has already returned 785 ABC tokens to you and you join them to the pool,..

  3. Atomic operations with referential triggers.

    Well you know that a referential constraint in your smart contract (most likely, in its definition.references section) may refer to a field in some other contract. This constraint can be used then in any of the permissions in your contract, so some permission can be dependent on an external contract; that external contract acts as a trigger/flag to permit some operations on your contract.

    For example, you are building an estate property registration system, where anyone can register changing ownership of their townhouse or condo. Normally, the definition.permissions.change_owner will contain just the owner, right? But for legal reasons, you are requested to implement the government (or some other arbiter) right to seize any of these estate properties from some state offenders. This seizure can be implemented as easy as changing the change_owner permission to ANY_OF with multiple roles, one being the role of the owner and another being the role of the arbiter/seizure agency.

    But this seizure can have a more complex procedure itself, and have an independent smart contract (with many other incapsulated rules and constraints). For example, each seizure requires a new revision of the smart contract with Origin ID=ABCD1234, and this revision must have a field, say, state.seizure_mode = 1 (in real life, there may be more complex conditions, like, state.seizure_subject containing the public key or the address of the offender whose property is being seized).

    In this case, you may add this “seizure reference” to the definition of your property smart contract (something like: some reference in definition.references has name = arbiter_seizure and where condition containing all_of = ["ref.state.origin == ABCD1234", "ref.state.seizure_mode == 1"]). Thus your change_owner permission will require either to owner to do this, or a seizure contract (with seizure_mode = 1) to be referred (see the “How to convert between a role and a reference” practice if you need to use a role and a reference in the same condition).

    But wait, after the seizure_mode is set in that contract, it will stay set! Wouldn’t it be safer if we set seizure_mode to 1; referred to this contract; and set the seizure_mode back to 0? Ideally, in a single transaction, to prevent any race conditions. Can’t we do a single transaction that makes a new revision of “seizure contract” setting it as needed to allow seizure; make a new revision of the “property contract” referring to this “flag-on” revision; and immediately make a second revision of the “seizure contract” making it “flag-off”?

    Note this “seizure contract” is just an example of some centralized flag that can control some of referential constraints in the definition of your contracts; there may be more uses for it.

Solution

Since version 3.9.1 of the codebase, a single transaction can contain multiple revision changes. Just get them all in a single transaction pack, and register it. That’s it!