Make the most out of your audit
Audits are costly. The expert reviewers taking care of your contracts have several years of experience and one or more very expensive degrees, and their time is not only finite, but also very valuable as more and more projects require their services to secure their platforms.
In this scenario, it would be a waste to pay for a 5 day audit and have a couple of them wasted on simple issues and roadblocks that could be easily prevented. The auditors' time and brainpower would be better spent on looking at the code to think about all the amazing ways that things can go wrong.
Always keep in mind, an auditor already has very limited time on your project (budget and time constraints lurking in), which means he will, to the best of his good will, not find every issue every single time. Attackers, on the other hand, have unlimited time to find bugs in the code. They don’t have to deliver a report in 5 days and spend time on paperwork.
In this apocalyptic scenario where the auditor is your best friend, the best decision a client can make is to make the process as smooth and efficient as possible, to help the smart contract auditor make the most of their time on your project.
In order to help you be the best client ever, we at Inference put up a small list of tips you can follow to make sure your code is in the best shape possible for its upcoming review.
Do not accept unnecessary coins (and parameters)
Let’s think for a second about the good old web2 world. You are developing an application. You know what you wouldn’t do? Accept user input when you do not need it. No sane person would, right? Back to cutting-edge, decentralized, smoking hot web3. Why would you accept coins (e.g. tez or eth) on an entrypoint that does not need them?
Users make mistakes. They will send unnecessary coins along with the contract call, and once they realize their mistake, they will ask you to have those back. Unless you are prepared with some payback functionality, those coins might be locked and lost in the contract forever. And while we can all agree it is definitely a mistake on the users’ side, rest assured they won’t like that you cannot pay them back. It is a very simple way to lose clients and reputation, while the cost to avoid this is close to nothing.
Another recurrent mistake we often encounter, which is very similar and thus included in the same paragraph, is the inclusion of unnecessary parameters in function definitions. Sending additional parameters in a smart contract call costs more gas: if the parameter is not needed for the smart contract logic, it is better left out.
Let us get back to the web2 example from the previous paragraph. We now do not accept user input when it is not necessary. Good for us. What else wouldn’t we want to do? Maybe forget to check that the input we do need is inside some good ranges of values that our program can accept without crashing or misbehaving. That’s a good idea, especially in a world where requests cost actual money (gas), besides representing a security risk every time that unchecked input is accepted and processed by the contract’s logic.
And what about the shiny web3 world? Idem. Your code takes a deadline as a parameter? Better check that it is not in the past. Getting a range of values? A reasonable check would be to make sure that the lower bound is effectively lower than the upper bound. User want to transfer an asset? Before looping through seven different maps, causing gas costs to skyrocket, better implement the check to assert whether the user own the asset or not.
A general rule of development that applies perfectly as a best practice even in the new wave of web3 is to fail early and fail often. Be sure to follow that to stay on the safe side.
Maps, maps everywhere
The following example is a Tezos specific one, but gas exhaustion as a vector for DoS attacks is applicable to many other chains.
Maps are expensive (and for the very same reasons, so are lists and sets. We decided to write “map” instead of all of them in this paragraph, but they are perfectly interchangeable for the following discussion). Every time you call a smart contract entrypoint, you deserialize its code and its storage (but not big_maps, keep this in mind). This means that the bigger your map gets, the more costly it is to interact with your smart contract, even if the functionality called doesn’t even need to access the map. Cherry on top, maps can potentially get big enough that the simple attempt to access a value will run up your gas costs so much that your contract is now unusable. It is basically a DoS, as your contract will revert any contract call that requires deserialization of the map.
There are two basic approaches to avoid this problem:
- Limit the size of your map. Users don’t always need the possibility to add an infinite amount of entries in a map.
- Use a big map. No deserialization DoS, although keep in mind you cannot loop over a big map, and they also incur costs on other facets. Be sure to check out the documentation to understand if they are appropriate to your use case.
As DRY as the desert
The next tip is taken from general best practices of software development, because it is always important to remember the basics, no matter what complicated service you are trying to build.
From an auditor’s standpoint, it is easier to look at 500 lines of code in 3 days than to look at 5’000 in 3 days. We can all agree the previous sentence is fairly obvious. It is even easier if those lines do not perform almost the same functionality over and over again.
Auditors suffer from the same vices and flaws as all other humans. If you force one of them to look at almost the same sentence (or code) several times, they will, inevitably, start to skim over it sooner rather than later. We have all experienced this phenomenon. Remember the last time you had to check your report for mistakes and typos before submitting it: the first time you read it carefully; the second time, less carefully. Third time? You are skimming over it.
Following the oldest principle of coding, don’t repeat yourself (DRY). If all your entrypoints need to perform some action, let’s make that into a single function that each entrypoint can call.
The burden of knowing everything
When you are an auditor, sometimes you are tasked with a contract that creates a picture to share with friends. The next day, you have some DeFi protocol based on UniswapV3, talking about ticks and tick-spacing and price fluctuations. And next Monday, you have crypto platypuses that need to fight aliens to save the Earth.
In all this, keep in mind that auditors generally love to study and, for sure, they are experts in cybersecurity, programming languages, attack vectors and common pitfalls and mistakes. But they are not, at least not always or not all of them, experts on platypuses.
That is why auditors need good documentation. The best gift you can give to your auditor is some documentation before the assessment starts, so that they will be able to start on day one already up to the challenge, maximizing their time on your project without wasting it on asking simple questions regarding the smart contracts under review.
And the second best gift? Commenting code. Developers are the greatest artists of this century, and as artists they sometimes get lost in their own genius solutions.
Auditors have great respect for beautiful code, but sometimes super-performing code is also quite unreadable for someone that was not even there when it was designed. Please comment your code thoroughly so that your assigned auditor can quickly pick it up and appreciate it fully.
Let us strike this point once more. Documenting is so important: while auditors are (usually) very smart and pick things up extremely quickly, they are still regular human beings that need to study in order to know things. We wish we could “drink and know things”, but that is only for dwarves in fantasy.
Lots of words in this blog post, so let’s include a short conclusion to help you have a quick glance at what was discussed, and how you can set your project up for success before its next audit:
- Accepting unnecessary coins? Prevent users from locking their funds in your contract
- Accepting unnecessary parameters? Keep gas costs to the minimum and stay on the safe side, only ask for as many parameters as you need.
- Constantly check for values taken from the users, as they might be poorly formatted or outside the acceptable range of your use case. Remember to fail early and fail often
- Data structures can grow to very large sizes, causing deserialization costs to rise so high that they can potentially lock your contract in an unusable state. Read the documentation to find the data structure that best fits your needs.
- DRY: repetitive actions should be included in functions, so that the auditor reviews as little code as possible (in terms of line of code). Remember: repetitiveness and focus are mortal enemies, and you need your auditor as sharp as possible.
- Document well, document often, and share documentation in the most timely manner possible. Give it to the auditors in advance, so that they can study your project beforehand, they will love you for it.
If you would like even more information on how to properly set up your smart contracts before receiving an audit, check our Preparation Checklist.
And of course, if you would like your smart contract to be audited by us, send an email to [email protected]