Smart contracts – best practices
- Writing the smart contracts
- Using the smart contracts
Smart contracts – best practices
See also: Software development – best practices
Writing the smart contracts
How to convert between a role and a reference
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
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
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)?
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”
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”?
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,
Using the smart contracts
How to make utility tokens on top of Universa
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.
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.
Can it be done in Universa in a way easier for user?
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:
But what happens one layer below?
Inside KTN architecture
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.
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.
How to register multiple revisions in a single transaction
Have you ever thought about registering multiple revisions in a single transaction?
Usages for this can be numerous:
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.
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,..
Atomic operations with referential triggers.
Well you know that a referential constraint in your smart contract (most likely, in its
definition.referencessection) 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_ownerwill 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
ANY_OFwith multiple roles, one being the role of the
ownerand 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_subjectcontaining 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
name = arbiter_seizureand
all_of = ["ref.state.origin == ABCD1234", "ref.state.seizure_mode == 1"]). Thus your
change_ownerpermission 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_modeis set in that contract, it will stay set! Wouldn’t it be safer if we set
1; referred to this contract; and set the
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.
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!