Auditor’s Advice: Solidity Checklist & Reentrancy Attack | Part 2/3

We continue our series of educational articles and today we’ll look at some specific tips for auditing during the development of smart contracts on Solidity!

Today we also continue a unique 3-part series in which we will discuss various facets of an auditor’s (or developer’s, if we’re talking about internal auditing) work, from gas optimization to attack protection and EVM limitations. We guarantee it will be entertaining!

By the way, there are some vacant slots now so if your project needs an audit — feel free to write to us, visit our public reports page hereLet’s get in touch: gm@pessimistic.io!

Part I:

Part III:


Why do you need to read this article?

Smart contracts are contracts that self-execute because the conditions of the agreement are put directly into the code. They are unchangeable and run on a decentralized blockchain network. Because of their nature, securing and auditing them is critical for a variety of reasons.

To begin with, security is critical in any decentralized application (DApp) because it handles with money transactions and user data. Even tiny vulnerabilities or programming errors in smart contracts might have disastrous implications, such as fund loss or illegal data access. We can ensure that smart contracts are resistant to attacks and that potential gaps are closed by securing them.

Several security procedures must be followed in order to achieve this. To begin, secure coding standards such as Solidity should be implemented. Using safe math libraries and avoiding obsolete or experimental features are examples of best practices. To avoid unauthorized access, contract functions and variables should be correctly marked as public, private, or internal.

Furthermore, audits are critical for detecting vulnerabilities and logic problems in smart contracts. Auditing entails a thorough examination of the contract’s codebase and planned functionality to ensure that it aligns with the project’s goals and does not present any risks. Auditors look for security flaws, potential reentry difficulties, inappropriate data processing, and other programming faults in the contract.

In recent months we have been actively developing our own Slither detectors (check out our Slitherin tool) to help with code review and audit process. Please let us know if you have discovered an issue/bug/vulnerability via our custom Slither detectors. You may contact us via opening a PR/Issue or directly, whichever is more convenient for you!

Audits are essential for catching potential security risks and preventing potential exploits that malicious actors might leverage. They provide an extra layer of assurance by identifying flaws that might have been overlooked during development. Additionally, audits help meet regulatory requirements while increasing credibility and trust among users and investors, as they demonstrate a commitment to security.

Moreover, audits can help evaluate the economic and game-theoretic aspects of smart contracts. Ensuring the contract’s logic and incentives align with the desired outcomes is crucial in decentralized systems. By auditing the contract, we can verify that the contract behaves as intended and that the economic variables, such as token issuance mechanisms or staking mechanisms, align with the project’s goals.

Apart from the technical aspects, audits also provide an opportunity for natural language review. The codebase is often reviewed by experts who can identify and fix any ambiguities or inconsistencies in the code, making it more readable and understandable.

Launching a project without securing and auditing smart contracts is a significant risk. It can expose the project to potential vulnerabilities, leading to financial losses, reputational damage, and legal consequences. Investors and users are becoming increasingly cautious and aware of the risks involved in decentralized systems, and they tend to favor projects that have been audited by reputable firms.

In summary, securing and auditing smart contracts before the launch of a project is critical to ensure the security, reliability, and credibility of the decentralized application. It mitigates potential vulnerabilities, enhances confidence among users and investors, aligns economic incentives, and helps meet regulatory requirements.

We can confidently say that such tips can be read publicly in a few places, and our blog is one of those places. The following will be our observations — only dry facts for auditors, tricks and the best life-hacks shared by our best auditors.

Everything you see below is based on our personal experience. And today, dear readers, it will be made available to you!

We finished our own research a few months ago; please read it if you haven’t already:

You will also find a list of tools and research for self-study, and we strongly recommend that you read it separately for better understanding! Let’s get started!


I — Reentrancy Checks

In simple terms, a Reentrancy attack occurs between two smart contracts, where an attacking smart contract exploits the code in a vulnerable contract to drain it of its funds.

The exploit works by having the attacking smart contract repeatedly call the withdraw function before the vulnerable smart contract has had time to update the balance.

Since our team has been working since 2016, we have amassed a large number of observations, which we will offer here, along with various security recommendations. The techniques listed below can help you considerably increase the security of your project’s integration:

  • Description of patterns for Slither: reentrancy-eth (attack while sending ether), reentrancy-no-eth (attack for any external function without sending), reentrancy-benign (attack possible, but no harm done), reentrancy-events (display events in the wrong order, which can cause problems for third parties). To prevent such attacks, check-effects-interactions pattern should be used in all cases;

  • Check out: Reentrancy Attacks on Smart Contracts DistilledRead-only Reentrancy: In-DepthSlither Detectors by Pessimistic.io;

  • It is very important to identify all the places where execution is passed to insecure addresses: msg.sender, token specified by the user, etc. Trace from which places we “get there”CEI violations can be quite invisible if internal methods or inheritance are used;

  • If a non-reentrant modifier is used, it should normally be on all non-view methods. Keep in mind that non-reentrant does not protect the contract system — only each contract individually;

  • Sometimes you may encounter read-only reentrancy attacks, possible while reading asset data. Attacks are performed during callback calls, when the values of all new token balances are not yet recorded;

  • We must remember that view methods may not work correctly if we are already inside this contract (integrating contracts may have problems).

Useful Resources

We would like to express our sincere gratitude to the Authors of all the resource materials! Check them out:

We also finished our own research a few months ago; please read it if you haven’t already:


II — Other Checks

Nobody can deny that the foundation of any secure integration is a unique approach to code writing. As a result, this article will concentrate solely on those areas that might be quite valuable in keeping your code safe and secure. We finished our own research a few months ago; please read it if you haven’t already:

The integration of your project will be substantially more secure if you implement the below recommendations:

Unused Return Value

  • call/delegatecall/staticcall/callcode/send — always check return value;

  • SafeMath and analogs — often `x+=y;` turns into `x.add(y)` instead of `x=x.add(y);`

  • Many standards and projects return a bool (ERC20) or error code (Compound), these should be checked;

Access Control

  • Access Control — verify that functions have adequate caller verification: For example you can’t use tx.origin to verify the caller (med), and it is very bad practice to separate contracts from EoA;

  • Almost always setters must be protected by some sort of role;

  • initialize() and its analogs must be protected by something too;

  • Sometimes contract functions should only be called from another contract of the same project. In such a case there must be a check “onlyThatContract”;

  • When using OpenZeppelin TimelockController, the deplayer gets admin roles for proposer and executor roles. In the documentation it is recommended to strip it of admin roles immediately after the deployment, otherwise it is overpowered — the deplayer can control the whole governance process bypassing the timelock;

Calls to “Custom” Addresses

  • Through an arbitrary call in a C1 contract, C2 can be called, which cares that it is C1 that calls it. This bypasses the msg.sender checks in C2. For example, let there be a system of two contracts C1 and C2:

a) C1.foo() makes an arbitrary call (in addition to possibly something else);

b) C1.bar() does all sorts of checks and calls C2.xyz();

c) C2.xyz() does something very important and is protected by onlyC1then the checks from C1.bar can be bypassed via C1.foo()->C2.xyz() (onlyC1 will not notice the trick;

  • If contract A has assets (ether or tokens) on contract A and contract A makes a delegatecall to “user” contract B, contract B can withdraw assets from contract A’s balance sheet;

  • We cannot rule out the case where there are no tokens, but there is a native currency. If the project is located on a specific chain where the native currency is a precompiled contract (e.g. Moonbeam and MOVR & GLMR tokens), then a delegatecall can also be performed to it. Vulnerability example;

  • The use of fallback()-function in the contract code should be very careful. Thus, the presence of fallback()-function without revert and conditions allows to execute for this contract a function with any signature that is not presented in the source code of the contract (may be malicious);

  • Potential DoS: the address can simply drop or “eat” all the gas, the calling contract should not somehow lag or freeze from this (relevant for the State Machine pattern). Also relevant for `.send()`, `.transfer()`. ;

  • Delegatecall to a spoofed address can remove the calling contract or break storage completely;

  • Starting with 0.5, the compiler inserts a check of the called address (that it is a contract and not EoA). But for low-level calls (.call, .delegatecall and others) and assembly there is no such check. Calling a non-contract address will not create any errors, but the logic will not even start.

Useful Resources

We would like to express our sincere gratitude to the Authors of all the resource materials! Check them out:


In conclusion, we would like to say that we hope that this article was informative and useful for you! Thank you for reading! 🙂

The most important thing we wanted to tell you and show you is that a new instrument does not mean good and an old instrument does not mean bad. It’s all about knowing how to use it and being willing to look for something new. We hope we’ve got you interested in that!

In recent months we have been actively developing our own Slither detectors (check out our Slitherin tool) to help with code review and audit process. Please let us know if you have discovered an issue/bug/vulnerability via our custom Slither detectors. You may contact us via opening a PR/Issue or directly, whichever is more convenient for you!

What instruments should we review? What would you be interested in reading about? Please leave your comments, we will be happy to answer them, and the best answers and questions may be included in the next article!

By the way, there are some vacant slots now so if your project needs an audit — feel free to write to us, visit our public reports page here.


Support is very important to me, with it I can do what I love — educating users!

If you want to support my work, please, consider donating me:

Stay safe!

Subscribe to Officer's Blog
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.