Automated Smart Contract Audit Guide. Part 1: Preparing for an Audit


    Our company is engaged in auditing the security of smart contracts, and the use of automated tools is very serious. How much they can help in identifying suspicious places, what is worth using them, what they can do and what are the specifics of work in this area? These and related questions are the subject of this article. And the material will be attempts to work with real contracts with the help of the most interesting representatives and recipes for the launch of this extremely disparate and wildly interesting software. At first I wanted to make one article, but after some time the amount of information became too large, so it was decided to make a cycle of articles, one for each autoanalyzer. A list from which we will take tools is presented, for example, here., but if during the time of writing other interesting tools come across, I will describe and test them with pleasure.

    I must say that the audit tasks turned out to be extremely interesting, since so far, developers have not paid much attention to the economic aspects of algorithms, and internal optimization. And the audit of smart contracts added some interesting attack vectors that need to be considered when searching for errors. Also, as it turned out, quite a lot of tools for automatic testing appeared: static analyzers, bytecode analyzers, fuzzers, parsers and many other good software.

    The purpose of the article: to promote the spread of secure contract codes and allow developers to quickly and easily get rid of the stupid bugs that are often the most offensive. When the protocol itself is completely reliable, and solves a serious problem, the presence of a stupid mistake, forgotten during the testing phase, can seriously ruin a project’s life. Therefore, let us learn how to use, at a minimum, tools that allow “little blood” to get rid of well-known problems.

    Looking ahead, I must say that the most frequently encountered critical bugs that we encountered in audits are still logical implementation problems, and not typical vulnerabilities, such as access rights, integer overflow, reentrancy. A large, full audit of solutions is impossible without experienced developers who are able to audit the high-level logic of contracts, their lifecycle, aspects of actual operation and compliance with the task, and not just typical attack patterns. It is high-level logic that often becomes a source of critical bugs.

    But warnings, typical holes and mistakes left out of carelessness that should not be missed - the lot of automatic analyzers, they should cope with these tasks better than people. It is this thesis and will be subjected to verification.

    Features of the audit of the code of smart contracts

    Smart contract code auditing is a fairly specific area. Despite its small size, a smart contract in Ethereum is a full-fledged program, capable of organizing complex branches, cycles, decision trees, and even to automate seemingly simple transactions require thinking through all possible branches at every step. From this point of view, blockchain development is extremely low-level, very resource-intensive and extremely reminiscent of the development of system and embedded software in C / C ++ and assembly languages. That is why we love to see at interviews of developers of low-level algorithms, network stack, high-load services, all who dealt with low-level optimization and code auditing.

    From the developer's point of view, Solidity is also quite specific, although it is easy to read by almost any programmer and in the first steps it seems extremely simple. Solidity code is fairly easy to read; it is familiar to any developer who owns C / C ++ syntax and OOP, such as JavaScript.

    Here, the simplicity of the code is the key to survival, nothing heavy works, so the whole arsenal of low-level development is used in the work — algorithms that allow efficient use of resources and save memory: Merkle trees, Bloom filters, “lazy” loading resources, expanding cycles, manual garbage collection and much more.
    A small amount of source code and the resulting bytecode.

    A separate smart contract is limited by the volume of the byte-code, each byte costs some amount of gas, and the maximum is limited from above, so you can push about 10Kb into the blockchain (at the moment), it will not work anymore.Here's a good article on how much it costs to deploy a contract and how much gas costs. Therefore, a lot of stuff will not succeed. If you exaggerate, then a few thousand lines of “average” code is the maximum. Several dozen methods, the absence of aggregation and generally complex logic is extremely characteristic of contracts. All that does not fit, requires you to isolate the code in separate libraries, change and complicate the order of calculations in the network. Solidity developers may be happy to shove a bunch of code into one contract, but simply have to arrange their contract systems correctly, creating separate library classes with their own storage. And such separate “classes” are conveniently laid out in separate files, and, therefore, it is quite pleasant to read the contract code, everything is quite well structured initially - otherwise it will not work. As an example, I recommend to look at how ERC721 is made in openzeppelin-solidity .

    Gas, gas, gas

    Gas introduces an additional layer of logic into the execution of the contract code, which requires an audit. Moreover, unlike the traditional code, one and the same code segment can spend different amounts of gas. A table of EVM opcodes and their cost is useful for understanding gas limitations, here it is .

    To demonstrate why it is necessary to devote a lot of time to evaluating gas, consider such a piece of pseudo code (of course unreal, bullet in the cycle with ether is a bad idea):

    // функция просто записывает код события для аккаунта в блокчейн
    function fixSomeAccountAction(uint _actionId) public onlyValidator {
        // … 
    // юзер дергает функцию, которая суммирует награды за каждый тип действия и выплачивает награду
    function receivePaymentForSavedActions() {
        // ...
        for (uint256 i = 0; i < events[msg.sender].length; i++) {
            // берем actionId из массива 
            uint actionId = events[msg.sender][i];
            // вычсляем награду за данный вид action
            uint payment = getPriceByEventId(actionId);
            if (payment > 0) {
                paymentAccumulators[msg.sender] += payment;
            emit LogEventPaymentForAction(msg.sender, actionId, payment);
            // …
            // delete “events[msg.sender][i]” from array 

    the fact is that the cycle in the contract is executed events [msg.sender] .length times, and each iteration is an entry in the blockchain (transfer () and emit ()). If the length of the array is small, then the cycle works a dozen times, handing out payment for each action. But, if the array of events [msg.sender] is large, then there will be a lot of iterations and the gas spent will be rested against the hard-boiled maximum gas limit (~ 8,000,000). The transaction will fall, and now it will never work, since there is no way to reduce the length of the array events [msg.sender] in the contract. If a cycle does not just calculate a single unit value, but writes to the blockchain (for example, some commissions, payments for actions are paid), then the allowed number of iterations is quite substantially limited. Judge for yourself - limit: 8 000 000 write new 256-bit values: 20 000. Ie you can save or update metadata only for a couple of hundred 256-bit addresses with some metadata Tokens for an address where there are already tokens, you spend on recording 4 times less gas (5,000 vs 20,000).

    Therefore, do not be surprised that the issue of gas in smart contracts is so closely related to the security of contracts, because the situation when funds are stuck in a contract for all practical purposes is not much different from the situation when they were stolen. The fact that the ADD instruction costs 3 gas, and SSTORE (saving to storage): 20,000 says that the most expensive resource in the blockchain is storage, and the problems of contract code optimization are in many ways aligned with the low-level development tasks in C and ASM for embedded systems, where storage is also a very limited resource.

    Beautiful blockchain

    This is a very positive paragraph about why the blockchain is so good from a security point of view for the auditor. Determinism of the execution of a contract code is the key to successful debugging and reproducing bugs and vulnerabilities. Technically, any call to a contract code can be replicated on any platform up to a bit, it allows tests to work everywhere and be extremely simple to support, and incident investigation reliable and undeniable. Now we always know who called which function, with which parameters, which code processed it, and what the result was. All this is completely deterministic, i.e. reproduced anywhere, even in JS on a web page. If we talk about Ethereum, then any test case is extremely easy to write in convenient JavaScript, including fuzzing parameters, and works great anywhere where there are Node.js.

    All these beautiful words, however, should not relax the developers, because, as mentioned above, the most serious mistakes are logical, and for them the determinism of execution is an orthogonal property.

    Contract Build Environment

    To write an article, I took an old experimental contract for booking housing from the Smartz designer: The contract allows you to create a record of the object (apartment or hotel room), set the price and the date of delivery, after which the contract waits for payment and, if received, records the booking act, keeping the funds on the balance until the guest enters the room and will not confirm entry. At this point, the owner of the number gets paid. The contract is essentially a state machine, the state and transitions of which can be viewed in Booking.sol. We did it pretty quickly, changed the development process and did not have time to do a lot of tests, it is far from a new version of the compiler and more or less rich internal logic. So let's see how the analyzers deal with it, what errors they find, and, if necessary, add our own.

    Work with different versions of solc

    Different analyzers will have to be used differently - some are launched from the docker, others use ready-made compiled byte-code, and the auditor itself also has to deal not with a pair, but with dozens of contracts with different versions of the compiler. Therefore, different versions solcneed to be able to “podsovyvat” in different ways, and in the host system, and within the docker-image, and inside the truffle, so I’ll give these several options for dirty hacks:

    1 way: inside the truffle

    For this, no tricks are needed, because since truffle version 5.0.0, you can specify the version of the compiler directly in truffle.js, as in this diff .

    Now truffle will download the necessary compiler itself and run it. Many thanks to the team for this, Solidity is young, changes in the language are serious, and moving from version to version for the auditor is unacceptable - this way you can make new mistakes and disguise the old ones.

    2 way: replacing / usr / bin / solc in the analyzer docker container
    If the analyzer is distributed as a Dockerfile, you can replace it when building the docker image by adding a line to Dockerfile that pulls the solcrequired version directly from the image that pulls out of the network and replaces / usr / bin / solc:

    COPY --from=ethereum/solc:0.4.19 /usr/bin/solc /usr/bin

    3 way: replacing / usr / bin / solc

    The dirtiest way to the forehead, if there is no way out at all, you can despicably replace the / usr / bin / solc binary with a script like this (remember to save the original file):

    # run Solidity compiler of given version, pass all parameters
    # you can run “SOLC_DOCKER_VERSION=0.4.20 solc --version”
    docker run \
        --entrypoint "" \
        --tmpfs /tmp \
        -v $(pwd):/project \
        -v $(pwd)/node_modules:/project/node_modules \
        -w /project \
        ethereum/solc:$SOLC_DOCKER_VERSION \
        /usr/bin/solc \

    It downloads and caches the docker image with the required version solc, switches to the current directory and starts /usr/bin/solcwith the parameters passed. Not a very good way, but perhaps for some tasks, it will suit you.

    Flattening code

    Now we will deal with source codes. Of course, in theory, autoanalyzers (especially for static source code analysis) should collect a contract, pull up all dependencies, put everything together in one monolith and analyze it. But, as I have already said, changes from version to version can be serious, and I constantly stumbled upon the need to add an additional directory to the docker, configure it inside the path, and all this so that he correctly pulled the necessary imports. Some analyzers understand everything, the latter are not, therefore, it is more convenient for a analyzer to eat a single file to merge everything into one file and analyze only it, as a universal option so that you don’t have to worry.

    For this, use a regular truffle-flattener .

    This is the standard npm module, used very simply:

    truffle-flattener contracts/Booking.sol > contracts/flattened.sol

    If you need to customize flattening somehow, you can write your flattener, for example, we used the python-based version before:

    Let's start the analysis.

    Using the example of the old man, we will continue the analysis. The contract has an old version of the compiler “0.4.20”, and I deliberately took the old contract to solve problems with the compiler. Worse the situation is that the auto-analyzer, for example, studying byte-code, may depend on this version of solc, and here the discrepancies in the versions can greatly affect the results or even break everything. so if even if you are doing everything kosher, using the latest versions, you can still fly to the analyzer, sharpened by the previous version of the compiler.
    Compiling and running tests

    To get started, just pull the project from the github and try to compile:

    git clone
    cd constructor-eth-booking
    npm install
    truffle compile

    Surely you have a problem with the compiler version. And there are also problems with autoanalyzers, so use any means to get the compiler 0.4.20 and build the project. I just registered the correct version of the compiler in truffle.js and it all came together as above.

    Also run

    truffle-flattener contracts/Booking.sol > contracts/flattened.sol

    as mentioned in the paragraph about flattening, contracts/flattened.solwe will be the ones to analyze for different analyzers.
    Conclusion to the introductory part

    Now, having flattened.sol and the ability to use solcan arbitrary version, you can proceed to the analysis. I will omit the problems with running truffle and tests, there is a lot of documentation on this issue, sort it out yourself. Of course, tests must be run and successfully run. Also, in order to check the logic, the auditor often has to add his own tests, checking potentially leaky places, for example, checking the functionality of a contract at the boundaries of arrays, covering all variables, even those strictly intended for data storage, with tests, etc. There are a lot of recommendations here, besides this is exactly the product that our company supplies to the market, so the study of logic is a purely human task.

    We will go to analyzers that are interesting from our point of view, try to slip our contract with them, and artificially introduce vulnerabilities into it in order to evaluate how autoanalyzers react to them. The next article will be devoted to the Slither analyzer, but in general, the action plan is approximately as follows:

    Part 1. Introduction. Compilation, flattening, versions of Solidity (this article)
    Part 2. Slither
    Part 3. Mythril
    Part 4. Manticore
    Part 5. Echidna
    Part 6. Unknown tool 1
    Part 7. Unknown tool 2

    Such a set of analyzers turned out because it is important for an auditor to be able to use different types of analysis - static and dynamic, and to conduct them requires completely different approaches. Our task is to learn how to use the basic tools in each type of analysis and understand which one to use.

    Perhaps in the process of detailed research, there will be new candidates for consideration, or the order of articles will change, so stay tuned. To go to the next part, click here.

    Also popular now: