Auditor’s Advice: Math, Solidity & Gas Optimizations | Part 1/3

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

Today we also kick off 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 II:

Part III:

Why do we actually need to optimize gas?

Gas refers to the fees required to execute operations on the Ethereum network. By optimizing gas usage, you can significantly reduce the costs associated with deploying and interacting with the smart contract. This is particularly important for users who are paying transaction fees and can drive user adoption and satisfaction.

So, optimizing gas usage is integral to a thorough security audit. It helps identify potential vulnerabilities and ensures efficient use of computational resources. By scrutinizing gas consumption, auditors can verify that the contract is functioning optimally and uncover any potential security flaws or inefficiencies.

Why do we actually need to audit the code?

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 — Gas Optimization Tips

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:

  • Modifiers: each inclusion of `_` in a modifier inserts the function body into bytecode. If a function has several modifiers, it may significantly increase the size of the contract and the cost of deployment. If you need to save money, you can combine modifiers or put the checks into a separate function;

  • In older versions of solidity, reading the storage length of the array (array.length) in the loop condition means reading from storage at each iteration;

  • Sometimes when auditing code, you may notice unnecessary copying (calldata->storage; calldata->memory; memory <-> storage) when assigning or passing arguments to a function. For example, you should always mark reference-type arguments of external functions as calldata, not memory; sometimes you may even use storage references in internal calls;

  • You can change the order of storage variables or fields in a structure somewhere to use storage packing, and it will be useful. However, sometimes reading and writing with storage packing is not always cheaper than without it. You can save a lot of gas when writing a storage array of structures with packed fields;

  • In Solidity, some data types have a higher gas cost than others. And that is what is often required of a smart contract developer and that’s why you should understand the gas utilization of the available data types;

  • Custom errors is cheaper to use than revert(“error text”). There is more information in this article: soliditylang.org/blog/2021/04/21/custom-errors.

Useful Resources

www.semanticscholar.org/paper/GASTAP%3A-A-Gas-Analyzer-for-Smart-Contracts-Albert-Gordillo/d10384f967089f1a68e81e6d4be40d7dafa715b6
www.semanticscholar.org/paper/GASTAP%3A-A-Gas-Analyzer-for-Smart-Contracts-Albert-Gordillo/d10384f967089f1a68e81e6d4be40d7dafa715b6

We would like to convey our heartfelt appreciation to the authors of these resource resources! Examine them out:

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


II — Solidity Auditing Tips

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:

  • Functions: Internal calls preserve the context (msg.sender, msg.value, etc). For example, an internal call to `transfer(…)` inside a token transfers tokens from the address of the caller, not from the balance of the token contract itself;

  • Functions: external > public

  • Variables: Visibility of private and internal does not hide data. Variable values are readable from off-chain;

  • Variables: Public visibility for variables creates getters and the code of getters takes up space in the contract. When optimizing gas, you can remove Public visibility for some variables (unused or already read in some other functions) and thus reduce gas consumption during contract execution;

  • Constants: Magic constants are dangerous, so make full-fledged constants with a normal name. Immutable is also ok;

  • Constants: Function signatures should be checked using 4bytes;

  • Ether: payable — can be redundant (the function can receive ether but does not process it);

  • Ether: fallback vs receive; receive is sufficient for receiving money. It often includes a check that the broadcast comes from one of the addresses (e.g. WETH);

  • Ether: It is impossible to limit the consumption of ether (selfdestruct, mining), so you cannot rely on the exact value of the balance. This is also true for tokens;

  • Ether: Three Ether sending options: send, call, transfer:

a) Specifics of use address.transfer() — throws on failure, forwards 2,300 gas stipend (not adjustable), safe against reentrancy, should be used in most cases as it’s the safest way to send ether;

b) Specifics of use address.send() — returns false on failure, returns false on failure, should be used in rare cases when you want to handle failure in the contract;

c) Specifics of use address.call.value().gas()() — returns false on failure, forwards all available gas (adjustable), not safe against reentrancy, should be used when you need to control how much gas to forward when sending ether or to call a function of another contract;

  • It’s bad form to work directly with gas: When working with the tx.gasprice variable, it is necessary to remember that its value is set by the user at the moment of transaction execution;

  • It’s bad form to work directly with gas: release of a specific amount of gas through .gas(X) / {gas: X}

  • .transfer VS .send VS .call — call is a good standard, but it brings its own set of problems (reentrancy etc);

  • It is easy to make collisions, for example with arrays: abi.encodePacked([1,2],[3]) == abi.encodePacked([1],[2,3])) so check them out carefully. Technically, similar can be done with abi.encode, e.g. via structures.

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:


III — Arithmetic Errors

Since our team has been working since 2016, we have accumulated quite a few observations, which we will share below along with several security advices.

Following the tips below can significantly improve the security of your project’s integration:

Overflow/Underflow

  • Use SafeMath and analogs for Solidity <0.8 (and do not use for more recent ones), keep in mind that a.add(b).mul(c) == (a+b)*c ;

  • Very carefully check all unchecked sections for Solidity >=0.8 ;

  • It is better to do all calculations in the uint256 type, because, for example, in the case of `uint256(a) = uint16(b) * uint32(c) / uint16(d)` the right part may overflow, because the intermediate value may not fit into uint32 (for each operation the maximum of operand types is taken, i.e. the result of multiplication of uint16 and uint32 will be uint32);

  • Type conversion — always check that the number is converted normally. Recommendation: use libraries like SafeCast ;

MulDiv — Loss of Accuracy in Calculations

  • It’s almost always multiplication first, then division;

  • When calculating fractions, don’t forget that you can accidentally get zero. e.g. `balanceOf(user) / totalSupply() == 0`. You need to multiply by a suitable multiplier (often 1e18).

  • It is better to put the additional multiplier in a constant like `uint constant private HUNDRED_PERCENT=1e18;` ;

  • There are times when you need to calculate the sum of fractions (i.e., the common denominator of all fractions). It is correct to add first, then divide;

  • Instead of `a/b > c/d` it is often better to use `a*d > c*b`.

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:


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.