Smart Contract Audit Checklist

When you deploy a smart contract to the blockchain, it becomes a permanent fixture. Unlike traditional software, you can’t just release a quick update or patch to fix an issue. The code is immutable, meaning it’s unchangeable once it’s live. This permanence is a double-edged sword: it ensures transparency and trustlessness, but leaves no room for error.

Smart contracts often handle valuable assets, which means any flaw in the code can have serious financial repercussions. A tiny bug isn’t just a technical glitch—it can lead to significant losses for you and your users. In some cases, vulnerabilities have been exploited to steal millions of dollars worth of cryptocurrency. That’s why getting it right the first time is crucial.

Ensure your apps are secure with our comprehensive smart contract audit checklist. As a trusted provider of blockchain development services with years of hands-on experience, we’ll guide you through the whole process of developing and auditing smart contracts. In this article, we’ll cover:

Security Considerations

Given that smart contracts often handle significant financial assets and execute critical functions without human intervention, any vulnerability can have devastating consequences. Unlike traditional software, you can’t easily update a smart contract once it’s deployed, which means vulnerabilities are permanent and exploitable by anyone. A single security flaw can lead to massive financial losses, erode user trust, and irreparably damage your project’s reputation.

Reentrancy Vulnerabilities:

  • Use the checks-effects-interactions pattern to prevent reentrancy attacks
  • Avoid updating the state after external interactions, as this opens up the possibility for reentrancy attacks
  • Use modifiers like nonReentrant from OpenZeppelin’s ReentrancyGuard to prevent multiple entries into a function
  • Use functions like transfer or send instead of low-level call to limit the gas sent to external contracts, reducing reentrancy risks
  • Minimize and thoroughly review any external calls to untrusted contracts, ensuring they don’t introduce vulnerabilities
pragma solidity ^0.8.0;

contract ChecksEffectsInteractions {

    mapping(address => uint) balances;

    function deposit() public payable {
        balances[msg.sender] = msg.value;
    }

    function withdraw(uint amount) public {
        require(balances[msg.sender] >= amount);

        balances[msg.sender] -= amount;

        msg.sender.transfer(amount);
    }
}
The checks-effects-interactions pattern in action: At first the withdraw function performs all required checks (available amount), then applies effects (balance change after withdrawal). Only after that will it perform the actual transfer, thus preventing reentrancy attack

Integer Overflows and Underflows:

  • Implement SafeMath libraries to handle arithmetic operations safely
  • Review all arithmetic operations to ensure they won’t unintentionally cause overflows or underflows
  • Choose variable types that suit the range of values you expect, reducing the risk of overflows
  • Write tests that cover edge cases for your arithmetic operations to catch any potential issues early
pragma solidity ^0.8.0;

contract SafeMathExample {
    function safeAdd(uint256 a, uint256 b) public pure returns (uint256) {
        uint256 result = a + b; // Overflow check is automatic in Solidity 0.8+
        return result;
    }
}
Using Solidity 0.8+ built-in overflow checks to safely perform addition without the need for external libraries
pragma solidity >=0.7.0 <0.9.0;

contract SafeMathExample {
    function safeAdd(uint256 a, uint256 b) public pure returns (uint256) {
        uint256 result = a + b;
 assert(c >= a);
        return result;
    }
}
If the code is supposed to run on pre 0.8 versions, overflow check must be added

Access Control and Authorization:

  • Use access modifiers like onlyOwner to restrict function access
  • Implement an ownership model using OpenZeppelin’s Ownable contract
  • Review all access control logic carefully to ensure correctness
  • Utilize role-based access control (RBAC) if multiple permission levels are needed
  • Initialize ownership properly to prevent unauthorized access post-deployment
  • Avoid hardcoding addresses; use variables that can be updated if necessary
  • Emit events for critical actions like ownership transfers for transparency
  • Protect constructor and initializer functions to prevent re-initialization attacks
  • Implement multi-signature requirements for highly sensitive functions
  • Document access control policies clearly within the codebase
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";

contract AccessControlledContract is Ownable {
    uint256 private value;

    // Only the owner can set the value
    function setValue(uint256 _value) public onlyOwner {
        value = _value;
    }

    // Anyone can read the value
    function getValue() public view returns (uint256) {
        return value;
    }
}
Using OpenZeppelin’s Ownable contract to restrict access to the setValue function with the onlyOwner modifier

Randomness and Predictability:

  • Avoid using block variables (e.g., block.timestamp, blockhash, block.number) for randomness
  • Understand the limitations of on-chain randomness sources and their susceptibility to manipulation
  • Consider using oracles or off-chain solutions like Chainlink VRF for secure and verifiable randomness
  • Implement commit-reveal schemes to add a layer of security when randomness is required
  • Utilize verifiable random functions (VRFs) for provably fair and tamper-proof random number generation
  • Be cautious with miner-influenced variables, as miners can manipulate them within certain bounds
  • Test the randomness implementation thoroughly to ensure it meets the security requirements
  • Stay updated on best practices and known vulnerabilities related to randomness in smart contracts
  • Document the randomness approach used in the contract for transparency and future audits
  • Consider the cost and complexity trade-offs of off-chain vs. on-chain randomness solutions
pragma solidity ^0.8.0;

contract InsecureRandom {
    function getRandomNumber() public view returns (uint256) {
        // Vulnerable to manipulation by miners
        return uint256(keccak256(abi.encodePacked(block.timestamp, block.difficulty, msg.sender)));
    }
}
Insecure randomness using block variables can be manipulated by miners
pragma solidity ^0.8.0;

contract OddEvenGame {
    uint public yourAnswer;

    function oddOrEven(bool yourGuess) external payable returns (bool) {
        yourAnswer = block.timestamp % 2;

        if (yourGuess == (yourAnswer > 0)) {
            uint fee = msg.value / 10;
            payable(msg.sender).transfer(msg.value * 2 - fee);
            return true;
        } else {
            return false;
        }
    }
}
Block timestamp variable could be manipulated by miners in order to win the game every time

External Calls and Interactions:

  • Review all external calls: Ensure they are necessary and properly secured
  • Implement comprehensive testing: Include unit tests and integration tests for external interactions

Denial of Service (DoS) Attacks:

  • Optimize gas usage in functions to prevent transactions from failing due to exceeding gas limits
  • Avoid unbounded loops or loops that depend on user input, which could lead to DoS vulnerabilities
  • Limit the amount of data processed per transaction to manageable sizes
  • Use pull over push patterns when sending Ether or tokens to multiple addresses
  • Implement emergency stop mechanisms (circuit breakers) to halt operations in case of an attack
  • Set reasonable gas limits and monitor gas consumption regularly
  • Beware of external calls that may consume excessive gas or fail unexpectedly
  • Validate and sanitize all inputs to prevent misuse that could lead to DoS
  • Use mapping structures over arrays when possible to avoid iteration over large datasets
  • Test for edge cases where functions may consume excessive gas or behave unexpectedly
pragma solidity ^0.8.0;

contract InefficientContract {
    uint256[] public data;

    function addData(uint256[] memory _data) public {
        for (uint256 i = 0; i < _data.length; i++) {
            data.push(_data[i]);
        }
    }
}
An inefficient function that may consume excessive gas when processing large arrays
pragma solidity ^0.8.0;

contract EfficientContract {
    mapping(uint256 => uint256) public data;
    uint256 public dataCount;

    function addData(uint256[] memory _data) public {
        uint256 count = dataCount;
        for (uint256 i = 0; i < _data.length; i++) {
            data[count] = _data[i];
            count++;
        }
        dataCount = count;
    }
}
An optimized function using mappings to handle data more efficiently, reducing gas consumption

Timestamp Dependence:

  • Avoid using block timestamps (block.timestamp) for critical logic such as randomness, locking mechanisms, or time-based conditions
  • Understand the limitations and potential manipulation of block timestamps by miners
  • Use alternative approaches like block numbers for time-sensitive logic when possible
  • Implement time buffers or grace periods to mitigate minor timestamp manipulations
  • Be cautious when using now, as it is an alias for block.timestamp and carries the same risks
  • Document any use of timestamps thoroughly, explaining why it is safe or necessary
  • Consider using trusted time oracles if precise timing is essential for contract functionality
  • Test contract behavior under different timestamp scenarios to identify potential vulnerabilities
  • Stay updated on best practices related to timestamp dependence in smart contracts
  • Review Solidity compiler warnings about potential timestamp vulnerabilities
pragma solidity ^0.8.0;

contract Auction {
    uint256 public auctionEndTime;
    uint256 constant TIME_BUFFER = 15 minutes;

    constructor(uint256 _biddingTime) {
        auctionEndTime = block.timestamp + _biddingTime;
    }

    function bid() public payable {
        require(block.timestamp <= auctionEndTime + TIME_BUFFER, "Auction already ended");
        // Bidding logic
    }
}
Including a time buffer to account for possible timestamp manipulation by miners

Gas Limit and Block Gas Limit:

  • Leverage gas-efficient coding patterns and Solidity features
  • Ensure that functions do not exceed the block gas limit, which would cause transactions to fail
  • Avoid operations with high gas consumption, such as unbounded loops or extensive storage writes
  • Optimize data structures and algorithms to reduce gas usage
  • Use events over storage when possible to save gas costs
  • Break down large functions into smaller, manageable units if necessary
  • Monitor gas costs of functions during development and testing
  • Stay informed about network gas limits, as they can change over time
  • Consider user experience, as high gas costs can deter users from interacting with your contract
function processArray(uint256[] memory data) public {
    for (uint256 i = 0; i < data.length; i++) {
        // Perform operations
    }
}
A gas-heavy loop: Processing large arrays in a single transaction can exceed the block gas limit

Code Quality and Best Practices

By prioritizing code readability and maintainability, you will improve the quality of your smart contracts while contributing to a more secure and robust blockchain ecosystem. Clear and well-organized code will facilitate auditing, collaboration, and future enhancements.This ultimately ensures your contracts remain reliable and effective over time.

Code Readability and Maintainability:

  • Use clear and descriptive naming conventions for variables, functions, contracts, and events
  • Apply consistent code formatting throughout the contract
  • Follow established style guides, such as the official Solidity Style Guide
  • Use comments and documentation to explain complex logic and intentions
  • Organize code logically, grouping related functions and definitions
  • Avoid deeply nested code structures to enhance readability
  • Use modifiers and helper functions to reduce code duplication
  • Keep functions concise, focusing on a single responsibility
  • Leverage inheritance and interfaces for cleaner architecture
  • Regularly refactor code to improve clarity and remove redundancies
// Nested if statements (less readable)
if (condition1) {
    if (condition2) {
        if (condition3) {
            // Do something
        } else {
            // Handle else
        }
    }
}

// Refactored using require statements (more readable)
require(condition1, "Condition1 failed");
require(condition2, "Condition2 failed");
require(condition3, "Condition3 failed");
// Do something
Refactor nested if statements into a series of require statements

Proper Use of Libraries and Inheritance:

  • Incorporate trusted libraries like OpenZeppelin for common functionalities to enhance security and efficiency
  • Utilize inheritance to promote code reuse and organization, making contracts more modular and maintainable
  • Leverage well-tested libraries to avoid reinventing the wheel and reduce potential bugs
  • Understand the functionality and limitations of the libraries you incorporate
  • Keep libraries updated to benefit from security patches and improvements
  • Use interfaces and abstract contracts to define standard behaviors
  • Avoid deep inheritance hierarchies that can complicate code understanding
  • Be cautious with multiple inheritance and the diamond problem in Solidity; use patterns like the Solidity 0.6+ virtual override system to manage it
  • Import libraries correctly and manage dependencies securely
  • Document any custom modifications to external libraries for clarity
  • Commenting and Documentation:

    • Document all functions and complex logic thoroughly to enhance understanding and maintainability
    • Include comments explaining the purpose and functionality of code segments, especially for complex or non-obvious logic
    • Use the Solidity NatSpec (Natural Specification) format for standardized documentation
    • Provide clear descriptions of function parameters and return values
    • Explain any assumptions, limitations, or side effects associated with functions or code blocks
    • Keep comments up-to-date with code changes to prevent discrepancies
    • Avoid redundant comments that merely restate the code; focus on the ‘why’ rather than the ‘what’
    • Document events and modifiers to clarify their purpose and usage
    • Include usage examples where appropriate to demonstrate how functions should be called
    • Maintain external documentation (e.g., README, Wikis) for broader context and project-level information
    /**
     * @dev Emitted when the ownership is transferred.
     * @param previousOwner The address of the previous owner.
     * @param newOwner The address of the new owner.
     */
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    
    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        require(msg.sender == owner, "Caller is not the owner");
        _;
    }
    
    Documenting events and modifiers to clarify their purpose and usage

    Modularity and Code Reuse:

    • Break down code into smaller, reusable components to enhance readability and maintainability
    • Ensure modules are self-contained and testable, promoting easier debugging and verification
    • Adopt the Single Responsibility Principle (SRP), where each module or function has a single purpose
    • Use libraries and contracts to encapsulate common functionalities
    • Utilize interfaces and abstract contracts to define clear contracts between components
    • Implement modular testing strategies, testing each component independently
    • Encapsulate functionality and restrict access to internal components as appropriate
    • Promote loose coupling between modules, reducing interdependencies
    • Leverage design patterns, such as factory or proxy patterns, to organize code effectively
    • Regularly refactor code to improve modularity and eliminate redundancies
    pragma solidity ^0.8.0;
    
    library MathUtils {
        function max(uint256 a, uint256 b) internal pure returns (uint256) {
            return a >= b ? a : b;
        }
    
        function min(uint256 a, uint256 b) internal pure returns (uint256) {
            return a <= b ? a : b;
        }
    
        // Additional utility functions...
    }
    
    contract ExampleContract {
        using MathUtils for uint256;
    
        function getMaxValue(uint256 x, uint256 y) public pure returns (uint256) {
            return x.max(y);
        }
    }
    
    Using a MathUtils library to provide utility functions that can be reused across contracts

    Testing and Validation:

    • Write comprehensive unit tests covering various input scenarios for each function
    • Write integration tests to check interactions between different contracts and external systems
    • Aim for high test coverage; strive for 100% where possible

    Compliance and Standards:

    • Follow relevant standards (e.g., ERC-20, ERC-721) meticulously to ensure compatibility
    • Stick to the official Solidity style guide for consistency
    • Stay updated with the latest best practices
    • Incorporate community-vetted improvements into your code

    Common Pitfalls to Avoid:

    • Overconfidence: Always have your code reviewed by others to catch overlooked issues
    • Skipping the Testing Phase: Allocate sufficient time and resources for thorough testing; do not bypass this critical phase
    • Ignoring Compiler Warnings: Address all compiler warnings before considering the code ready for deployment
    • Not Keeping Up with Updates: Stay current with the latest developments in blockchain technology and smart contract standards

    Tools and Resources for Code Review

    Integrating effective tools and resources into your development workflow is essential for building secure and reliable smart contracts. By utilizing automated security tools like MythX and Slither, leveraging development frameworks such as Truffle and Hardhat, and committing to ongoing education, you ensure that your skills and projects remain robust and up-to-date in the rapidly evolving blockchain landscape.

    Automated Security Analysis Tools:

    • Integrate tools like MythX and Slither into your development workflow to catch potential issues
    • Use static analysis tools to detect vulnerabilities and code quality issues early
    • Automate security scanning as part of your continuous integration (CI) pipeline
    • Understand the limitations of automated tools and supplement them with manual reviews
    • Stay updated with new features and updates of these tools to leverage their full potential
    • Use complementary tools like Mythril, Echidna, and Manticore for comprehensive analysis
    • Configure tools appropriately to match your project’s needs and reduce false positives
    • Regularly run these tools throughout development, not just at the end

    Development and Testing Frameworks:

    • Use frameworks like Truffle and Hardhat for efficient development, testing, and deployment
    • Leverage built-in testing environments to write and run automated tests
    • Utilize deployment scripts to manage contract deployment across different networks
    • Take advantage of debugging tools provided by these frameworks
    • Integrate plugins and extensions to enhance functionality
    • Manage dependencies effectively using package managers and configuration files
    • Use network forking features to test contracts against live blockchain states
    • Implement gas reporting to optimize contract functions
    • Maintain organized project structures for better code management
    • Stay updated with the latest versions to benefit from new features and security patches

    Building & Securing Smart Contracts

    As a blockchain development agency, we provide end-to-end smart contract solutions, from initial design to final deployment. Our hands-on experience in creating secure and efficient blockchain solutions gives us a unique perspective on smart contract security.

    Developing Innovative Protocols: BAMM

    We’ve developed BAMM, a Layer 3 transport protocol optimized for transferring small amounts of data between anonymous peers. Designed as a foundation for web3 decentralized applications, BAMM leverages the Proof-of-Stake consensus mechanism to validate transactions. Here’s how BAMM enhances user privacy:

    • End-to-End Encryption: Messages are encrypted on the sender’s device and remain unreadable by anyone except the intended recipient, even if intercepted.
    • Data Integrity: Messages are guaranteed to arrive intact. Any tampering renders them unreadable, and they are discarded.
    • Onion Routing: Messages are wrapped in multiple layers of encryption and routed through a network of intermediary nodes. Each node removes a layer, revealing the next destination without exposing the final recipient. This hides user metadata like IP addresses and device identifiers, enhancing privacy and anonymity.
    • Proof-of-Stake: Every node must stake currency before joining the network, which incentivizes honest behavior and discourages malicious actors from attempting to compromise user privacy.

    BAMM facilitates direct, secure communication between users without relying on a central server. This decentralization removes a single point of failure and ensures robust user privacy.

    Building Secure Dapps: Tingl

    We also developed Tingl, an anonymous web3 messenger that showcases our commitment to security and privacy. In Tingl, user data and message metadata reside on the blockchain for maximum security, while large media files are stored off-chain for efficiency. For a fully decentralized storage solution, integrating IPFS is also possible.

    Here’s a glimpse into what we’ve implemented for Tingl:

    • Pay-Per-View Messages: Files become available only after the sender is paid. Users can securely share images and files up to 2MB.
    • Guest Chat: Facilitating one-time communication, Tingl users can host a guest chat for someone without an account.
    • Sale Web Link: Enabling secure transactions, Tingl users can create a web link with content previews and options for buyers to purchase the content.
    • Superlike: Users can send a “superlike” via a paid on-chain transaction, rewarding the message author.

    Why This Matters to You

    Our experience with BAMM and Tingl means we’ve navigated the complexities of smart contract development and security firsthand. We understand the pitfalls and challenges you might face, because we’ve been there ourselves. This deep expertise allows us to provide not just theoretical advice, but practical solutions tailored to your specific needs.

    By choosing us for your smart contract code review, you’re tapping into a wealth of knowledge and experience. We know what it takes to build secure, efficient, and user-friendly blockchain applications. Let us help you ensure your smart contracts are rock-solid, so you can focus on what you do best—innovating and growing your business. Contact us today for a free, non-binding consultation.

    See how we developed
    an anonymous web3 messenger
    with exclusive chat privacy

    Please enter your business email isn′t a business email