SafeMath Library for Inline Assembly/Yul in Solidity Smart Contracts

Nour EldenNour Elden
4 min read

Introduction

After solving the Node Guardians Quest ‘Yul basics’, I've gained insights into writing assembly code for a SafeMath library. Since v0.8.0, Solidity supports overflow and underflow checks for arithmetic operations, but inline assembly does not. This discrepancy necessitates a custom implementation to ensure the same level of safety when working with assembly code. This library presents a SafeMath library written in inline assembly to provide these essential checks, preventing potential vulnerabilities in smart contracts that rely on low-level arithmetic operations.

Contract Overview

The SafeMath library includes four arithmetic functions (add, sub, mul, div) with overflow and underflow checks using assembly. Custom errors enhance readability and debugging.

Contract Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract SafeMath {
    // Custom errors
    error AdditionOverflow(int256 lhs, int256 rhs);
    error AdditionUnderflow(int256 lhs, int256 rhs);
    error SubtractionOverflow(int256 lhs, int256 rhs);
    error SubtractionUnderflow(int256 lhs, int256 rhs);
    error MultiplicationOverflow(int256 lhs, int256 rhs);
    error DivisionByZero();
    error DivisionOverflow(int256 lhs, int256 rhs);

    /// @notice Returns lhs + rhs.
    /// @dev Reverts on overflow / underflow.
    function add(int256 lhs, int256 rhs) public pure returns (int256 result) {
        assembly {
            result := add(lhs, rhs)
            // Check for overflow when both inputs are positive
            if and(sgt(lhs, 0), sgt(rhs, 0)) {
                if slt(result, lhs) {
                    mstore(0, 0x7e1eb7fc00000000000000000000000000000000000000000000000000000000) // AdditionOverflow selector
                    mstore(32, lhs)
                    mstore(64, rhs)
                    revert(0, 96)
                }
            }
            // Check for underflow when both inputs are negative
            if and(slt(lhs, 0), slt(rhs, 0)) {
                if sgt(result, lhs) {
                    mstore(0, 0x5d29679300000000000000000000000000000000000000000000000000000000) // AdditionUnderflow selector
                    mstore(32, lhs)
                    mstore(64, rhs)
                    revert(0, 96)
                }
            }
        }
    }

    /// @notice Returns lhs - rhs.
    /// @dev Reverts on overflow / underflow.
    function sub(int256 lhs, int256 rhs) public pure returns (int256 result) {
        assembly {
            result := sub(lhs, rhs)
            if and(sgt(lhs, 0), slt(rhs, 0)) {
                if slt(result, lhs) {
                    mstore(0, 0x43ce8a4200000000000000000000000000000000000000000000000000000000) // SubtractionOverflow selector
                    mstore(32, lhs)
                    mstore(64, rhs)
                    revert(0, 96)
                }
            }
            // Check for overflow when lhs is negative and rhs is positive
            if and(slt(lhs, 0), sgt(rhs, 0)) {
                if sgt(result, lhs) {
                    mstore(0, 0x8c10517700000000000000000000000000000000000000000000000000000000) // SubtractionUnderflow selector
                    mstore(32, lhs)
                    mstore(64, rhs)
                    revert(0, 96)
                }
            }
        }
    }

    /// @notice Returns lhs * rhs.
    /// @dev Reverts on overflow.
    function mul(int256 lhs, int256 rhs) public pure returns (int256 result) {
        // Convert this to assembly
        assembly {
            // Check if either input is zero
            if or(iszero(lhs), iszero(rhs)) { revert(0, 0) }
            // Perform multiplication
            result := mul(lhs, rhs)
            // Check for overflow
            if iszero(eq(sdiv(result, lhs), rhs)) {
                mstore(0, 0x0ac8e39d00000000000000000000000000000000000000000000000000000000) // MultiplicationOverflow selector
                mstore(32, lhs)
                mstore(64, rhs)
                revert(0, 96)
            }
            if iszero(eq(sdiv(result, rhs), lhs)) {
                mstore(0, 0x0ac8e39d00000000000000000000000000000000000000000000000000000000) // MultiplicationOverflow selector
                mstore(32, lhs)
                mstore(64, rhs)
                revert(0, 96)
            }
        }
    }

    /// @notice Returns lhs / rhs.
    /// @dev Reverts on division by zero and overflow.
    function div(int256 lhs, int256 rhs) public pure returns (int256 result) {
        assembly {
            // Check for division by zero or division of zero
            if or(iszero(rhs), iszero(lhs)) {
                // If rhs is zero, it's division by zero. If lhs is zero, we can return early.
                if iszero(rhs) {
                    // Store "Division by zero" error
                    mstore(0, 0x18b69439) // DivisionByZero selector
                    revert(0, 4)
                }
                // If lhs is zero, return zero (no need to revert)
                result := 0
            }
            // 2. Check if lhs == INT256_MIN && rhs == -1 -> revert
            if and(
                eq(lhs, 0x8000000000000000000000000000000000000000000000000000000000000000),
                eq(rhs, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)
            ) {
                // Store "Division overflow" error
                mstore(0, 0x91aa53a800000000000000000000000000000000000000000000000000000000) // DivisionOverflow selector
                mstore(32, lhs)
                mstore(64, rhs)
                revert(0, 96)
            }
            // Perform the division
            result := sdiv(lhs, rhs)
        }
    }
}

let’s break down the Multiplication function:

  • The multiplication function performs checks to prevent overflow during multiplication. The primary formula used to verify correctness is result / lhs != rhs and result / rhs != lhs. If either condition is met, it indicates an overflow, causing the function to revert.

let’s break down the Division function:

  • Division by Zero Check: Ensures rhs it's not zero.

  • INT256_MIN/-1 Check: Prevents overflow when lhs is INT256_MIN and rhs is -1.

    • eq(lhs, 0x8000...0000): Checks if the left-hand side is equal to INT256_MIN (the smallest possible int256).

    • eq(rhs, 0xffff...ffff): Checks if the right-hand side is equal to -1 in two's complement representation.

This part prevents the overflow that would occur when dividing INT256_MIN by -1, as a result (2^255) is too large to be represented in an int256.

  • Division Operation: Performs signed division using sdiv.

Usage

If you are developing smart contracts and using assembly/Yul code, this SafeMath library will definitely assist you, especially with complex arithmetic operations. By ensuring overflow and underflow checks, offers a reliable way to handle arithmetic in low-level assembly, which is crucial for maintaining contract integrity and security.

This is up-to-date. assembly-safeMath-library

Conclusion

While this project demonstrates high code quality and rigorous testing, it has not been audited and should be used cautiously.


I have used Claude.ai to help me understand some complex arithmetic operations. It's much better than ChatGPT.

0
Subscribe to my newsletter

Read articles from Nour Elden directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nour Elden
Nour Elden