Smart contracts – best practices
- Writing the smart contracts
Using the smart contracts
- How to make an offline “cold wallet”
- How to make utility tokens on top of Universa
- How to register multiple revisions in a single transaction
- How to verify “the contract is yours”/“token balance”
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 “bearer” 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”? I.e. make a “bearer contract”, or “finder-keeper contract”?
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,
Note: this is mostly a theoretical example, and should not be used in practice directly. Even during registering such a contract in the Universa Network, you reveal it at least to the nodes; any single malicious (or at least a little bit watchful) node can spot a “bearer contract” passing through the network, and re-register it to the node owner.
Using the smart contracts
How to make an offline “cold wallet”
For major financial institutions dealing with crypto assets of large worth, it is rather regular to use a “cold wallet” concept, when the primary amount of worth is stored/controlled on an offline/disconnected computer. When such a computer has completely no access to the Internet, it cannot be broken, and the assets cannot be stolen. How to make the similar with Universa?
Let’s imagine two processes and three primary roles.
Alice is our “Holder”, which uses the cold wallet to keep the assets/smart contracts on a safe offline place. Bob is our “Sender”, who initially sends (and changes the owner) some smart contracts (or even specifically, tokens) to Alice . Charlie is our “Receiver”; at some point Alice wants to send (and in particularly, change the owner) some smart contracts from her offline “cold wallet” to Charlie.
Alice gets two separate computers; one will act as a “cold wallet”, not connected to internet; one will act as a “hot wallet”, connected to Internet and performing the contract registrations in Universa.
On both computers, she creates a private key, a “cold wallet private key” and a “hot wallet private key” accordingly. She gets the key address of the cold wallet private key; that will be used in smart contracts.
On “hot wallet computer”, she reserves a necessary amount ot UTNs/Us to be able to register the contracts. All these Us should be owned by her “hot wallet private key”.
Receiving the assets
Bob wants to send some assets to Alice:
- Bob changes the assets to be owned by Alice’s “cold wallet address”, and registers this change in Universa. (Note he doesn’t need any operations from Alice side for that, in particular ones involving her “cold wallet private key”).
- Bob delivers the smart contracts to Alice’s “hot wallet computer”.
- On the “hot wallet computer” (which has access to Universa to check the contracts), Alice checks that the smart contracts are approved.
- After that, Alice transfers the received smart contracts to “cold wallet computer” using some offline means (like, the USB thumb drive).
Sending the assets
Alice wants to send some assets to Charlie:
- On the “cold wallet computer” (ideally: even on the “hot wallet computer”, causing as little operations on “cold wallet computer” as possible), Alice prepares a new transaction pack containing the ownership change from Alice to Charlier for the assets she is sending (for the tokens, that will be a split-join operation). If it was prepared on the “hot wallet computer”, she transfers it to the “cold wallet computer” (using something like the USB thumb drive). Then she signs it on the “cold wallet computer” with her “cold wallet private key”.
- Alice delivers this transaction pack to “hot wallet computer” using some offline means (like, the USB thumb drive).
- On “hot wallet computer”, Alice makes the “compound” containing the Us required to register this transaction; the Us are owned by the “hot wallet private key”. She signs the compound with the “hot wallet private key” (which is nowhere related to the “cold wallet private key”), and registers it in Universa.
- After successful registration, Alice delivers the signed smart contracts to Charlie (using CryptoCloud-based chat in Web Client, or even some non-Universa means).
Besides the procedure above, please also take your usual steps to ensure your security.
- Use and regularly update the antivirus software on “hot wallet computer” (and preferrably on “cold wallet computer too” – you’ll need to update it without connecting it to the Internet though!), to prevent any virus being able to infect your USB thumb drive.
- The computer used for “hot swap wallet” (and obviously, the computer used for the “cold swap wallet”!) should NOT be used for any non-Universa-related activities, such as (but not limited to) casual Internet browsing, gaming, social networks, etc.
To prevent any viruses substituting the data, you may want to double-check the assets/amount (if tokens)/new owner address just before signing the data on the “cold swap computer”. Please always make sure you are signing the transaction which you do want to perform, not any misgenerated one! Though, the address structure makes it almost impossible for malicious party to generate an address resembling the original address too close; so the substituted address will likely be different significantly.
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!
How to verify “the contract is yours”/“token balance”
When writing the custom client software for specific business logic (be that general-purpose contract handling, special-purpose contract handling, or token ““wallet”-style contract handling), you may often want to handle/display, for the current user, some contracts as “belonging to the user”, or “controlled by the user”. For the tokens, this may involve calculating the proper token balance, if the tokens “belong to the user”. How to properly test that?
Depending on the particular business needs, you need to choose the particular aspects you are testing/displaying, among the ones below:
1. Is the contract actually APPROVED by the network right now?
Your client may need to check the item state of the contract more or less often. If it is very sure that the client is the only one who can change/register the contract, it may remember the item state and cache it for a long time; but if it is possible that there are other clients who can alter the same contract – or there are other users/private keys who can do it – you may need to recheck the state regularly and/or on demand.
2. “Belonging” or “Controlling”?
Do you care about the contracts “being owned” by the user, or “being controlled” by them? You need to decide and distinguish. For the typical tokens, this is often virtually the same – the one who is listed as an “owner”, is also the only one who can change the owner, and/or split-join the token. In most complex cases, it may be otherwise:
- It is possible to make the smart contracts (or even tokens) for which not just the current owner, but someone else (like the central-issuing authority) can change their owner or split them.
- It is even possible to make the smart contract (or tokens) for which the owner user cannot even control the token (change its owner or split it).
To test the “belonging”, it is usually sufficient to test the
owner field of the contract. To the test the “controlling”, one may need to try to execute the fake Change Owner operation (as a most basic action to confirm being able to control), create the transaction pack, do not register it, but test for errors.
3. Are you the only one?
No matter if you choose “belonging” or “controlling” on the previous step, it may be important to display if you are the sole user being able to do that, or if some other users may exist.
If you decide to display the “sole belonging”/“sole controlling”:
- If you chose “belonging”: does the
ownerfield contain the SimpleRole matching your address? Or it is a ListRole? (so it is possible the others may control it too). If it is some other role, it is definitely more complex contract to understand than usual.
- If you chose “controlling”: the verification may be more complex; but in general you should test whether the ChangeOwner and SplitJoin (if available) permissions (for typical tokens) are currently eventually mapped to the SimpleRole (matching your address).
4. Are you the owner unconditionally?
You may be the owner (belonger) and/or controller of the contract; this may be defined via a SingleRole, or maybe even via a ListRole (so it is a multisig key). But even them, it may happen that the current owner (or the current “controller”, i.e. “ones who has the ChangeOwner and SplitJoin permissions”) owns it with some reference/requirement added. In such a case, if any reference exists linked to the according role (they may be too hard to parse their inner logic fully) – you may want to display that it is likely you can use this contract/token but only on some conditions that you should know. And attempt to create the dummy ChangeOwner/SplitJoin transaction without actually registering it, may verify whether you are currently seem to be able to control it; but as the reference may include time-restricted factors (e.g. you can control it only before some date, or some other date), the subsequent attempt to make the very same SplitJoin transaction may fail even if the first attempt succeeded.