SafeMath Library for Inline Assembly/Yul in Solidity Smart Contracts
Table of contents
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
andresult / 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 andrhs
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.
Subscribe to my newsletter
Read articles from Nour Elden directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by