Auditor’s Advice: EVM Limitations & Assembly Auditing Tips | Part 3/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 also marks the conclusion of a three-part series in which we will explore various aspects of an auditor’s (or developer’s, if we’re talking about internal auditing) duties, ranging from gas optimization to attack protection and EVM limits. We guarantee it will be enjoyable! More articles will be published soon!

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 II:

Why do you need to read this article?

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.

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!

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 — EVM limitations

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.

Since 2016, our team has accumulated a considerable number of observations, which we will provide here, along with numerous security advice. The techniques listed below can help you considerably increase the security of your project’s integration:

  • Contract bytecode size limit — 24kb;

  • Custom errors take up less space in bytecode than revert(“error text”) ;

  • Including the old encoder `pragma abicoder v1; Abicoder V2 is needed if there are arrays of arrays or arrays of structures in the contract. But even for simple arguments it makes way more complex bytecode — keep this in mind;

  • In general: public->external, memory->calldata;

  • Getters (including public variables) can be removed or merged to reduce the size of the contract;

  • You can also refuse short types (leave only in storage, use full types in local variables, calculations and parameters);

  • Modifiers can be used less frequently to reduce contract size;

  • To reduce bytecode, you can make Optimizer runs=1. But, unfortunately, it makes more expensive calls;

  • {} — blocks for restricting the visibility of local variables;

Setter Checks

  • There’s usually an Access Control;

  • Usually has to emit the event;

  • Storage must be updated: a function argument is written to storage (seen all three incorrect variants — they are quite popular 🙂);

  • In complex projects we consider situations how changing the address of one of the system components will affect the whole protocol;

Loops Checks

  • Check that they are not infinite and cannot become infinite (or a limited variant is available);

  • Loops coming from the end make an efficient check, not `uint i = N; i >= 0` (because the type uint always returns a positive value);

  • Before Solidity 0.5.0 continue inside do-while didn’t work properly: continue switches execution to do without checking the condition in while — this makes the do while loop infinite;

Timestamp/Block Dependency

  • Don’t measure time in blocks (unless we are talking about hard-forks — they are tied to block numbers). Sometimes blocks are slowed down due to TimeBomb, and switching to PoS will fix the time between blocks as 12 seconds instead of the current 13s on average (PoW);

  • Occasionally, contracts rely on very small intervals. In general, if a contract needs accuracy higher than 15 minutes, it can be dangerous;

  • Some contracts check that something happens no more than once per block. This is ok as an optimization (e.g., charging interest), but not ok as a protection measure against any reentrancy \ flash loans \ other attacks. It also breaks integrations — a lot of users will come through one contract.

Assembly Checks

  • The code itself shouldn’t break the memory:

a) “Mess up” the third or fourth slots;

b) “Corrupt” already allocated memory — starting from the fourth slot and up to free memory pointer (value in the third slot);

c) Very often break because of copying returndata to the beginning of memory, returndatacopy(0, 0, returndatasize()) — see the second code block in the section;

  • Assembly blocks should be labeled “memory-safe” if it is (and unlabeled if it does not follow memory conventions);

  • There are no overflow/underflow checks in assembly;

  • When writing/reading from an exact storage slot, consider the storage structure when inheriting. If B inherits from A, when using sstore() inside B, care must be taken not to accidentally overwrite the slots of A’s base contract;

  • Changes in designs in different versions of Solidity: selfdestruct is used instead of suicide since version 0.5.0 and keccak256 is used instead of sha3 starting from version 0.5.0.


II — Useful Resources

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

We at pessimistic sincerely hope you find our work useful and appreciate any feedback, so please do not hesitate to contact us! The best answers and questions may be included in the next blog post. We hope that this article was informative and useful for you!

By the way, several audits have been completed successfully! By the way, here 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.