How to… avoid common mistakes in dApp development
In this article Inal Kardanov discusses best practices for developing decentralized applications (dApps) using the Ride programming language, and gives tips for avoiding some of the most common mistakes.
The development of dApps for Web 3.0 is often far from a trivial task. Several aspects of this process are different from regular app development:
• Cost of mistakes. A mistake in a dApp can often lead to the loss of funds, including those belonging to the dApp’s users
• Open-source code. Even if you don’t want your code to be available to other developers and users, it will be stored on the blockchain and always available for decompiling (with Waves Protocol, it’s even easy to do this in the blockchain explorer)
• Many interconnected elements. Some dApps operate using the logic of other dApps
• Protocol updates. The Waves Protocol update to version 1.2 and activation of function 14 (if it is accepted for mainnet) will completely change the way the script invocation transaction works. Basically, a dApp’s environment can change, which is hard to imagine for regular development.
Let’s consider some of the most common mistakes made by dApp developers, and discuss ways to avoid them.
Always check signatures
One of the most common mistakes that developers make is the use of the check format case _ => true
in smart account scripts or in dApps’ @Verifier
function. For instance, one might think that the following script prohibits only transfer transactions, permitting all other transaction types:
But the devil is in the details. A script like this completely prohibits transfer transactions from that account but permits all other transactions by any user. Any individual or even a script can run a transaction from that account by entering the account’s public key in the senderPublicKey
field and adding no signatures.
Always check the presence of a signature and its accuracy:
One should look out for this both on mainnet and on testnet: on both networks, there are scripts that watch all blockchain transactions and withdraw all tokens from accounts with the vulnerability described above.
Understand the difference between @Verifier and @Callable
Some dApp developers wrongly believe that @Verifier
checks incoming transactions to a dApp’s address. For instance, you might see a script like this:
But this script’s function is not to call this account’s methods, but to call any dApp from this dApp’s account without even supplying a signature. For example, any user can call another account and transfer all of this dApp account’s tokens to it.
Remember that a dApp’s account can perform operations and send transactions, and these operations are controlled by the @Verifier
function.
Check transactions before sending
InvokeScript transactions can return an error. While previously transactions like this were not recorded to the blockchain, with the release of Version 1.2 (which at the time of writing is only available on stagenet) the situation has changed. Now, InvokeScript transactions and smart asset transactions are written to the blockchain even if they return an error, and users pay transaction fees.
You cannot be 100% sure that a transaction will be successfully executed and written to the blockchain, as the blockchain state changes quite quickly and new transactions are added to the UTX pool, which can change the branch the script will follow. You can maximize the probability that a transaction will be successfully executed by preliminary validation. A node’s REST API has the method debug/validate, which accepts and validates a transaction. This method returns the same transaction script execution result that would be returned if the transaction were added to the block right now.
Use this method for preliminary validation before sending a transaction with the broadcast method.
Important: This API method requires a key that cannot be obtained for public nodes. Therefore, use a node whose API key you know.
Be careful with keys
While developing dApps, many operations involve the key-value storage of the accounts. Keys in the storage are often composite. For instance, the key voting_12_vote_3MEEsWQtsS5WV2SczdEvipY3Ch5LuSHuLWa
can store a vote cast by the account 3MEEsWQtsS5WV2SczdEvipY3Ch5LuSHuLWa
in a vote with id=12
. A key for a record like this in the storage can be implemented in Ride in the following way:
Often, while creating a key, developers make the mistake of writing to one key and trying to read from another. For instance, they could forget to add _
. To avoid this mistake, instead of copying and pasting, always use separate functions for creating keys. And, of course, write tests for your dApps.
Use default values
Another common mistake related to key storage (among other things), is attempting to read values from variables of the Union(T|Unit)
type, using value()
or extract()
where default values could be used instead.
For instance, if a function attempts to read the vote cast by a user from an account’s storage, this data may not yet be in place. Therefore, use the function valueOrElse
or pattern matching:
Also keep in mind that not only can you call your dApp’s functions from your user interface, but anyone can do so in any possible way. Using default values can help here, as well.
Keep your transactions under control
In dApp operation, several interdependent transactions often need to run in sequence. For instance, if you use a commitment scheme, the reveal phase has to follow the commit phase. If you send a transaction earlier than the commit transaction is added to the blockchain, your script will return an error, and the user will pay a fee but won’t obtain the expected result.
Rarely, a situation can occur on the Waves blockchain when, due to a fork, the last block or microblock will roll back, which can lead to disruption in the sequence of interdependent transactions. For instance, if you send a transaction for the commit phase, wait until it is added to the last (liquid) block and immediately send a reveal transaction. A situation can occur when the last block or microblock will roll back and the commit transaction will fall out of the blockchain. As a result, the reveal phase transaction will be invalid.
If you use the function waitForTx from the library waves-transactions, it only waits for the transaction to be written to the last liquid block, which can lead to problems. If you have interdependent transactions, it’s safer to use the function waitForTxWithNConfirmations and wait for one or two confirmations after the first transaction has been written to the block.
For more info on Ride go to https://github.com/wavesplatform/ride-introduction