Over the last year, the Tellor Oracle system has undergone three audits with two different auditors, been completely refactored several times, and has been critiqued and adjusted by some of the smartest minds in the space. It’s been a long road to getting a working system on mainnet, and this article should give you an overview of our experience and our approach to getting secure contracts launched.
From draft to production
Security audits for smart contracts are code reviews that help identify and eliminate bugs and/or vulnerabilities on your contracts, and ensure the code works as designed. Security audits are common in the crypto space because once code is deployed it cannot be changed. There have been many faulty smart contracts (the DAO, Parity Multisig, etc.), so doing everything you can to ensure security is paramount.
For a complex system like Tellor, getting the design of the system right is actually a completely separate problem from getting the code right. To give you some backstory, Tellor actually started out at a hackathon in mid-2018 as an idea to build a PoW system to choose reporters for data requests (very similar to Oraclize with a twist). Sadly, we didn’t win the hackathon, but we came out of it with a pretty cool idea: a decentralized oracle where miners submit off-chain price data. The only problem was that it wasn’t secure in the least. We switched to a model where parties would pay to read the data added by miners (where miners would get rewards over time as their inputted value was used). We actually even sent that version out for our first audit, and once again, the design was off. Keeping data private and charging for it on Ethereum is tough, if not impossible, because data on public blockchains is readable by anyone, so there’s a huge free-rider problem. Long story short, it passed on the code side, but the incentives weren’t quite right, so we went back to the drawing board. After a few more months of work, research, and talking with some of the smartest minds in the space, we came upon our solution now.
One thing that shocked me was just how important the incentive mechanisms and game theory behind the design are (they’re literally 95% of getting the build right). During our time in Binance Labs, we were in Full Node Berlin, where we had the opportunity to present Tellor, take feedback on our design and tweak it to get it to be audit ready. We were very excited to send this version to audit. We finally felt the incentives and design were finally right (see our whitepaper for more).
First off, our auditors were awesome! They were really hands on and seemed to really enjoy going through our code and hearing about our design. We were really pushing boundaries with our code and I really loved working with people that are fascinated by challenges. Like most firms, they gave us an initial review with a list of recommended updates that are usually classified as high, medium, and low priority. We generally try to address all the recommendations as promptly as possible, but there was definitely some back and forth since Solidity definitely has various opinions when it comes to best practice.
For example, during the first review, assembly code for obtaining the minimum and maximum of an array was recommended to save gas and we implemented the recommendation and added unit tests for it. However, during the second review phase, it was recommended that for readability purposes and to avoid problems with future solidity updates it would be best not to use assembly code, so we quickly and happily switched it back. We embraced the feedback!
Luckily, most of the feedback we received was on making our code more efficient or readable. We had to make a tough decision on the way we declared variables and we didn’t know if the auditors would like our structure. But we knew from the beginning that we wanted to keep our contracts upgradable but keep one eternal and continuing storage contract. One of the ways we decided to do this was by mapping the keccak 256 of the variable name to a variable of its type. Since we decided not to declare each variable independently but rather map the name of the variable to its type we essentially can add an infinite number of variables to each type (uint, address, bool). Declaring each variable limits future upgrades to the contracts since we would not be able to add variables to the storage (see more here..). We made the decision to sacrifice readability for upgradability. We documented the variables clearly on the storage contract, included getter functions for these, articulated our and our user needs to the auditors and kept our code structure. I don’t know if the auditors had previously seen this structure of declaring variables with a mapping on other contracts but I’m glad it was just a readability recommendation. We really pushed boundaries on the technical side and we felt quite proud that Certik, at one of the last meetings, jokingly mentioned that if their techs could conquer Tellor’s code, then they truly understand Solidity.
It was a long road (albeit not as long as some other projects) to get Tellor to the point where it is today. Hopefully you can learn from our journey, and our big recommendation to other projects is to just get stuff production ready (aka safe) and push it out. Things are different when you’re actually going live, so go through the auditing process and the whole mainnet launching process sooner rather than later. You’re not building just for yourself, so give it to users and go from there. The last thing anyone wants is a well polished project that no one uses. Work with the auditors to make it safe so you can work with users to make an impact.
Check out our website and subscribe for updates: www.tellor.io