Skip to content
Riccardo Malatesta edited this page Feb 11, 2026 · 22 revisions

Index

Gas optimizations

Avoid Using Boolean Variables for Storage

Storing boolean values (bool) in Solidity incurs unnecessary gas costs. Using uint256 (1 for true, 0 for false) eliminates additional Gwarmaccess (100 gas) and prevents costly Gsset operations (20,000 gas) when toggling from false to true. This simple adjustment can save up to 17,100 gas per instance.

Reference: OpenZeppelin Contracts.

Remediation

Replace bool with uint256 (1 for true, 0 for false).

// Before
bool public isActive = true;

// After
uint256 public isActive = 1; // 1 = true, 0 = false

Cache Array Length Before Looping

Failing to cache an array's length before a loop causes Solidity to repeatedly access arr.length, leading to redundant stack operations (DUP<N>). Storing the length in a variable optimizes execution by reducing stack manipulation, saving 3 gas per instance.

Remediation

Cache array length before loop.

// Before
uint256 length = arr.length;
for (uint256 i; i < length; i++) {
	// ... logic
}

// After
uint256 length = arr.length;
for (uint256 i; i < length; ) {
	// ... logic
	unchecked { ++i; }
}

Remove Explicit Default Value Assignments

Solidity automatically initializes variables to their default values. Explicitly setting them (e.g., uint256 x = 0;) wastes gas and increases bytecode size. Simply declaring the variable without an explicit assignment removes unnecessary operations.

Remediation

Remove explicit initialization.

// Before
uint256 x = 0;

// After
uint256 x;

Use Bitwise Shifting Instead of Multiplication and Division

Bitwise shifting (<< and >>) is more gas-efficient than multiplication (*) and division (/). The SHR opcode consumes only 3 gas, whereas DIV requires 5 gas. Additionally, shifting can bypass Solidity's division-by-zero restrictions

Remediation

Use bitwise shifts.

// Before
x = y * 8;

// After
x = y << 3; // Equivalent to y * 2^3

Prefer != 0 Over > 0 for Unsigned Integers

For unsigned integer comparisons, != 0 is cheaper than > 0. This minor change reduces gas consumption in conditions and validation checks.

Remediation

Use != 0.

// Before
require(x > 0);

// After
require(x != 0);

Optimize revert and require Strings to Reduce Gas Costs

Error messages in require() and revert() that exceed 32 bytes incur additional gas costs due to how Solidity handles string storage. Using custom errors instead of long revert strings significantly reduces gas consumption and bytecode size.

Remediation

Use custom errors or shorten strings.

// Before
revert("Long error message");

// After
error ShortError();
revert ShortError();

Postfix Increment/Decrement Increases Gas Costs

Using the prefix (++i / --i) increment or decrement is more gas-efficient than the postfix (i++ / i--) versions. The prefix form updates the value before returning it, whereas the postfix form first returns the original value and then updates it. When the return value is not needed, using the prefix version avoids unnecessary storage operations, reducing gas usage. However, ensure correctness when refactoring, as uint a = i++ and uint a = ++i produce different results.

Reference: Why does ++i cost less gas than i++?.

Remediation

Use prefix increment.

// Before
i++;

// After
++i;

Use constant or immutable for Unchanging Variables

If a variable does not need to change, declaring it as constant or immutable reduces gas costs by eliminating the need for SLOAD operations. constant is used for compile-time constants, while immutable allows assignment in the constructor but prevents modifications afterward. Using these keywords optimizes storage access and reduces transaction fees.

Remediation

Add immutable/constant.

// Before
uint256 public value;

// After
uint256 public immutable value;

Use external Instead of public for Functions When Possible

Since Solidity 0.6.9, public functions must transfer calldata parameters to memory, incurring additional gas costs. Using external instead avoids unnecessary memory allocation when the function is only called externally. external functions cannot be called internally, so ensure they are not required within the contract before making this change.

Remediation

Change the visibility from public to external.

// Before
function foo() public {}

// After
function foo() external {}

Mark Functions as payable When They Are Guaranteed to Revert for Normal Users

If a function has a modifier like onlyOwner and would revert when called by a normal user sending ETH, marking it as payable optimizes gas usage. Making the function payable prevents the compiler from inserting checks to ensure no ETH was sent, avoiding unnecessary opcodes such as CALLVALUE, DUP1, ISZERO, and REVERT. This optimization reduces gas costs by approximately 21 gas per call and lowers deployment costs.

Remediation

Add payable to the interested functions.

// Before
function withdraw() external onlyOwner {}

// After
function withdraw() external payable onlyOwner {}

Use Assembly to Check for address(0) to Reduce Gas Costs

Checking for address(0) using Solidity's higher-level syntax incurs additional gas costs due to function calls or storage reads. Using inline assembly is more efficient and can save 6 gas per instance by leveraging the iszero opcode directly.

Remediation

Implement inline assembly check.

// Before
require(addr != address(0));

// After
assembly {
	if iszero(addr) {
		revert(0, 0)
	}
}

Use require Instead of assert When Possible

When assert() fails, it triggers the INVALID (0xfe) opcode, consuming all remaining gas and reverting the transaction entirely. In contrast, require() uses the REVERT (0xfd) opcode, which allows unused gas to be returned. Using require() instead of assert() where appropriate can prevent unnecessary gas wastage while still enforcing conditions effectively.

Reference: Assert() vs Require() in Solidity - Key Difference & What to Use.

Remediation

Replace assert with require.

// Before
assert(condition);

// After
require(condition);

Using uint or int Smaller Than 32 Bytes Incurs Overhead

The Ethereum Virtual Machine (EVM) processes data in 32-byte (256-bit) chunks. When using smaller integer types like uint8 or uint16, the EVM must perform additional operations to adjust the size, leading to higher gas costs. To optimize gas efficiency, use uint256 unless explicit packing within a struct is required.

Reference: Layout of State Variables in Storage | Solidity Docs.

Remediation

Use uint256 unless packing.

// Before
uint8 smallVar = 100;

// After
uint256 normalVar = 100;

Use selfbalance() Instead of address(this).balance to Reduce Gas Costs

The BALANCE opcode, used when calling address(this).balance, has a minimum gas cost of 100. In contrast, SELFBALANCE is a more optimized opcode that only costs 5 gas, making it significantly more efficient for retrieving the contract's balance. Using selfbalance() within an inline assembly block minimizes gas costs while achieving the same functionality.

References: BALANCE | EVM Codes, SELFBALANCE | EVM Codes.

Remediation

Use selfbalance in assembly.

// Before
uint256 bal = address(this).balance;

// After
uint256 bal;
assembly { bal := selfbalance() }

Using constant for Keccak Variables Causes Extra Hashing and Higher Gas Costs

Declaring keccak256 hash values as constant results in additional hashing operations, increasing gas costs. Using immutable instead reduces gas consumption by approximately 20 gas, as the hash is computed only once during contract deployment. If the hash value does not need to be known at compile time, prefer immutable over constant to optimize gas efficiency.

Remediation

If possible, use immutable instead of constant.

// Before
bytes32 constant HASH = keccak256("hash");

// After
bytes32 immutable HASH = keccak256("hash");

Splitting require() Statements That Use && Can Reduce Gas Costs

Using a single require() statement with the && operator incurs additional gas costs due to stack operations and evaluation logic. Splitting the condition into two separate require() statements can save approximately 8 gas per instance by simplifying execution flow.

Remediation

Split require statements into multiple checks.

// Before
require(a && b);

// After
require(a);
require(b);

Using x = x + y Instead of x += y for State Variables Saves Gas

For state variables, using x += y or x -= y generates additional read and write operations compared to explicitly writing x = x + y. This optimization can save approximately 10 gas per instance by reducing unnecessary storage accesses.

Reference: StateVarPlusEqVsEqPlus.md.

Remediation

Use explicit assignment.

// Before
x += y;

// After
x = x + y;

Use unchecked{++i} or unchecked{i++} When Overflow Is Not Possible

Starting from Solidity 0.8.0, arithmetic operations include overflow checks by default, which increase gas costs. Wrapping increment (++i or i++) inside an unchecked block can save 30-40 gas per loop iteration when it is guaranteed that no overflow can occur.

Remediation

Wrap the increment operation inside an unchecked block when it is certain that no overflow can occur.

for (uint256 i = 0; i < n; ) {
	unchecked { ++i; }
	// loop body
}

Remove Redundant Event Fields to Save Gas

Including block.number or block.timestamp as event parameters is unnecessary, as these values are already recorded in the transaction logs by default. Removing them reduces event emission costs without losing essential information.

Remediation

Remove redundant fields.

// Before
event Log(uint256 value, uint256 timestamp);

// After
event Log(uint256 value); // timestamp is auto-added

Simplify Boolean Comparisons to Reduce Gas and Complexity

Comparing boolean variables to true or false is unnecessary and adds extra computation. Instead of if (x == true), simply use if (x), and instead of if (x == false), use if (!x). This reduces gas costs and improves code readability.

Remediation

Simplify boolean checks.

// Before
if (x == true) {}

// After
if (x) {}

Use >= or <= Instead of > or < to Reduce Gas Costs

Non-strict comparisons (>= and <=) are more gas-efficient than strict comparisons (> and <) because they avoid additional ISZERO checks. This optimization can save 15-20 gas per instance. Adjust thresholds accordingly when making this change.

Remediation

Use >=/<=.

// Before
require(x > 100);

// After
require(x >= 101);

Use private Instead of public for Constants to Reduce Deployment Gas

Marking constants as public generates an automatic getter function, which increases deployment costs by 3406-3606 gas. Since constant values can be retrieved from the verified contract source or through a dedicated getter function returning multiple values, using private avoids unnecessary storage and method ID table entries.

Remediation

Mark constants as private instead of public.

// Before
uint256 public constant VALUE = 100;

// After
uint256 private constant VALUE = 100;

Use a More Recent Solidity Version to Optimize Gas Usage

Upgrading to Solidity 0.8.10 or later provides multiple gas optimizations: skipping contract existence checks for external calls with return values (from 0.8.10), using cheaper custom errors instead of revert strings (from 0.8.4), improved struct packing and more efficient multiple storage reads (from 0.8.3), and automatic compiler inlining (from 0.8.2).

Remediation

Update pragma to a more recent version.

// Before
pragma solidity ^0.8.0;

// After
pragma solidity ^0.8.20;

Non-Critical issues

Add Descriptive Reason Strings to require() and revert() Statements

Providing clear error messages in require() and revert() improves code readability and debugging. When a condition fails, an informative message helps identify the issue quickly, making the contract easier to maintain and troubleshoot.

Reference: Error Handling: Assert, Require, Revert, and Exceptions.

Remediation

Include error messages in require/revert statements.

// Before
require(condition);

// After
require(condition, "Condition not met: insufficient balance");

Use Named Return Parameters to Improve Readability

Naming return parameters in function declarations increases code clarity and explicitness. It makes function outputs easier to understand and improves maintainability by providing context for returned values.

Remediation

Name return parameters in function declarations

// Before
function getUser() external returns (uint256);

// After
function getUser() external returns (uint256 userId);

Use bytes.concat() Instead of abi.encodePacked() for Solidity ≥ 0.8.4

Starting from Solidity 0.8.4, bytes.concat() provides a more readable alternative to abi.encodePacked() for concatenating bytes and bytesNN arguments. It offers the same functionality with a clearer name, improving code maintainability.

References: Solidity 0.8.4 Release Announcement, Remove abi.encodePacked #11593.

Remediation

Replace abi.encodePacked with bytes.concat for readability (Solidity ≥0.8.4).

// Before
bytes memory data = abi.encodePacked(a, b);

// After
bytes memory data = bytes.concat(a, b);

Use Explicit Imports for Improved Readability and Efficiency

To ensure only the necessary components are imported, use curly braces to specify individual imports. This makes the code more readable and helps avoid unnecessary dependencies.

Remediation

Use explicit imports with curly braces.

// Before
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

// After
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

Remove or Track TODO Comments to Maintain Code Quality

Unresolved TODO comments should be tracked in an issue backlog to ensure they are addressed before deployment. All TODOs must be completed or explicitly managed to prevent unfinished code from reaching production.

Remediation

Remove TODOs or track them in an issue tracker.

Add SPDX-License-Identifier to Avoid Legal and Usage Issues

Including an SPDX-License-Identifier at the top of each Solidity file clarifies the licensing terms, preventing potential legal disputes and ensuring proper code usage.

Remediation

Add the SPDX license identifier at the top of each Solidity file.

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

Add a pragma Statement to Ensure Compiler Compatibility

Without a pragma statement, the contract may be compiled with an unintended Solidity version, leading to compatibility issues and unpredictable behavior. Explicitly specifying the Solidity version ensures stability and consistency.

Remediation

Specify the Solidity version in files missing it.

// Add to the top of the file
pragma solidity ^0.8.20;

Add a Comment to Explain Empty Function Bodies

If a function has an empty body, include a comment to clarify its purpose. This improves code readability and helps other developers understand the intent behind the function.

Remediation

Add a comment to explain why a function is empty.

Replace Magic Numbers with Named Constants for Better Readability

Embedding numeric literals directly into the code reduces readability, maintainability, and security. Instead, define meaningful constants or variables to provide context and improve transparency.

Remediation

Use named constants for numeric values.

// Before
uint256 deadline = block.timestamp + 86400;

// After
uint256 constant DAY_IN_SECONDS = 86400;
uint256 deadline = block.timestamp + DAY_IN_SECONDS;

Use external Instead of public for Functions Not Called Internally

If a public function is never used within the contract, changing its visibility to external can reduce gas costs and improve contract efficiency. external functions are optimized for external calls and do not generate unnecessary internal access overhead.

Remediation

Change visibility from public to external if not called internally.

// Before
function getBalance() public view returns (uint256) {}

// After
function getBalance() external view returns (uint256) {}

Empty code blocks

Empty code blocks provide no functionality and can make the contract harder to read and maintain. They may also indicate incomplete implementations or leftover code that was not properly removed.

Remediation

To improve code clarity, remove any unnecessary empty blocks. If an empty block is required for structural reasons, add a comment explaining its purpose to prevent confusion during audits and future development.

Costly storage operations inside loops

Performing SSTORE operations inside loops is inefficient and significantly increases gas consumption. Each write to storage incurs a high gas cost, which can make transactions more expensive and even lead to out-of-gas errors if the loop iterates too many times.

Remediation

Cache values in memory by using local variables inside the loop and write to storage only once after the loop completes. This reduces redundant storage writes and improves contract efficiency.

Large Numeric Literals

Using large numeric literals directly in code can make it harder to read, understand, and maintain. Numbers with many digits increase the likelihood of typos and errors, making debugging more difficult.

Remediation

Use scientific notation (e notation) instead of long numeric literals. For example, replace 1000000000000000000 with 1e18 to represent one Ether in wei.

Inconsistent Integer Declarations

Using a mix of uint and uint256 or int and int256 within the same contract can lead to inconsistencies, making the code harder to read and maintain. While uint is an alias for uint256, explicitly specifying uint256 improves clarity, ensures consistency across the codebase, and aligns with best practices.

Remediation

It is reccomended to use uint256 and int256 explicitly instead of relying on shorthand types.

Lack of Event Emission for State Changes

Changing state variables without emitting an event makes it difficult for off-chain services, dApps, and users to track contract activity. Events provide a reliable way to monitor state changes without requiring expensive storage reads, improving transparency and auditability.

Remediation

It is advised to emit relevant events whenever an important state change occurs. This allows external listeners to efficiently track changes and respond accordingly without needing to query the blockchain.

Redundant abicoder v2 Pragma in Solidity 0.8.0+

The abicoder v2 pragma is unnecessary in Solidity 0.8.0 and later, as it is enabled by default. Keeping this pragma in the code has no effect and may cause confusion regarding its necessity.

Remediation

It is advised to remove pragma abicoder v2; from Solidity files when using Solidity 0.8.0 or higher.

Potential Type Safety Issues When Using abi.encodeWithSignature or abi.encodeWithSelector

Manually encoding function calls with abi.encodeWithSignature or abi.encodeWithSelector can introduce errors due to typos in function signatures or incorrect parameter ordering. These issues may lead to failed transactions or unintended behavior.

Remediation

To enhance type safety and prevent errors, use abi.encodeCall, which ensures that function signatures and argument types match the expected function definition.

Constants Should Use CONSTANT_CASE

Constant variables should follow the CONSTANT_CASE naming convention, where names are written in uppercase letters with underscores separating words. This improves readability, aligns with Solidity best practices, and makes constants easily distinguishable from regular variables.

Remediation

Rename constants to use CONSTANT_CASE (e.g., MAX_VALUE).

Inconsistent Formatting of Control Structures

Control structures, such as if, for, and while, should follow a consistent style to improve readability and maintainability. Opening braces should be placed on the same line as the condition to align with Solidity's best practices and commonly accepted style guides.

Remediation

To maintain a clean and consistent codebase, it is advised to format control structures as if (condition) { ... } instead of placing the opening brace on a new line.

Risk of Infinite Execution Due to while(true) Loops

Using while(true) creates a loop with no explicit termination condition, which can lead to infinite execution. In Solidity, this can cause transactions to run out of gas, resulting in failed execution and wasted gas fees.

Remediation

To prevent infinite loops, replace while(true) with a loop that has a well-defined termination condition. If an indefinite loop is necessary, ensure that there is an explicit break condition to allow for controlled exits.

Reduced Readability Due to Excessively Long Lines

Lines exceeding 164 characters can negatively impact readability, especially in code review tools like GitHub, where horizontal scrolling is required. Long lines make it harder to spot errors and understand logic at a glance.

Remediation

To improve code readability and maintainability, break long lines into multiple lines using proper formatting. Consider using indentation, line breaks, and helper variables to keep the code structured and easy to follow.

Inconsistent mapping Formatting Reduces Readability

Mappings in Solidity should be declared without spaces between mapping and the opening parenthesis to maintain consistency with the Solidity Style Guide. Inconsistent formatting can reduce readability and make code harder to review.

Reference: Solidity Style Guide - Mappings.

Remediation

To improve clarity and adhere to best practices, declare mappings in the following format: mapping(address => uint256) balances; instead of mapping (address => uint256) balances;.

Hard-Coded Addresses Reduce Flexibility and Maintainability

Using hard-coded addresses in a contract can lead to issues when deploying to different networks or environments. If an address changes due to an upgrade or redeployment, all instances of the contract that rely on the hard-coded value will need to be updated and redeployed, increasing the risk of errors and maintenance overhead.

Remediation

It is advised to replace hard-coded addresses with immutable variables that are initialized in the constructor.

Redundant Use of SafeMath in Solidity 0.8+

Starting from Solidity 0.8.0, built-in overflow and underflow checks are enabled by default, making the use of SafeMath unnecessary. Continuing to use SafeMath in newer Solidity versions adds unnecessary complexity and gas overhead without providing additional security benefits.

Remediation

To simplify the code and optimize gas efficiency, remove SafeMath and use native arithmetic operations directly. Solidity 0.8+ automatically reverts on overflow and underflow, ensuring safe arithmetic operations without requiring an external library.

Use of Exponentiation Instead of Scientific Notation

Using exponentiation (e.g., 10**18) in Solidity can make numerical values harder to read and understand at a glance. Scientific notation (1e18) is more concise, improves clarity, and is widely recognized in Solidity and other programming languages.

Remediation

It is advised to replace exponentiation with scientific notation. For example, use 1e18 instead of 10**18 when representing large numbers like wei-to-ether conversions. This approach makes the code more intuitive and reduces potential misunderstandings.

Low issues

Use a Fixed Solidity Version to Ensure Consistent Compilation

For non-library contracts, floating pragmas (^0.8.0) may introduce security risks by allowing compilation with unintended or vulnerable Solidity versions. Using a specific compiler version ensures that the contract is compiled consistently across different environments.

Reference: Version Pragma | Solidity Documentation.

Remediation

Use a fixed Solidity version to ensure consistent compilation.

// Before
pragma solidity ^0.8.0;

// After
pragma solidity 0.8.20;

Use SafeERC20 to Prevent Unsafe ERC20 Operations

ERC20 tokens have multiple implementations, some of which do not follow the standard correctly. Using OpenZeppelin's SafeERC20 helps prevent issues by handling failures safely. If SafeERC20 is not used, ensure each operation is wrapped in a require statement to check for successful execution.

Reference: ERC20 OpenZeppelin Documentation.

Remediation

Use SafeERC20 for ERC20 operations.

// Before
token.transferFrom(msg.sender, address(this), amount);

// After
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
SafeERC20.safeTransferFrom(token, msg.sender, address(this), amount);

Avoid Using Deprecated OpenZeppelin Library Functions

Some OpenZeppelin library functions have been deprecated and should be replaced with their modern equivalents. Using outdated functions may lead to compatibility issues and security risks. Always refer to the latest OpenZeppelin documentation to ensure best practices.

Reference: OpenZeppelin Contracts Issue #1064.

Remediation

Replace deprecated functions with modern equivalents.

// Before
_setupRole(DEFAULT_ADMIN_ROLE, admin);

// After
_grantRole(DEFAULT_ADMIN_ROLE, admin);

Avoid Using abi.encodePacked() with Dynamic Types When Hashing

Using abi.encodePacked() with dynamic types before hashing can lead to hash collisions due to improper padding. Instead, use abi.encode(), which ensures all values are padded to 32 bytes. If there is only one dynamic argument, consider casting it to bytes() or bytes32() before hashing. When concatenating multiple dynamic types, use bytes.concat() instead of abi.encodePacked().

Reference: Solidity ABI Specification - Non-Standard Packed Mode, How to Compare Strings in Solidity?.

Remediation

Replace abi.encodePacked() with abi.encode() when dealing with dynamic types. This prevents hash collisions due to improper padding. If there is only a single dynamic argument, consider casting to bytes() or bytes32() before hashing. When concatenating multiple dynamic types, use bytes.concat() instead of abi.encodePacked().

Use safeTransferOwnership Instead of transferOwnership for Safer Ownership Transfers

The transferOwnership function transfers contract ownership in a single step, which can lead to accidental ownership loss. Using a two-step process like safeTransferOwnership improves security by requiring the new owner to accept ownership explicitly.

Reference: OpenZeppelin Ownable2Step.

Remediation

Implement two-step ownership transfer.

// Before
transferOwnership(newOwner);

// After
import "@openzeppelin/contracts/access/Ownable2Step.sol";
safeTransferOwnership(newOwner);

Avoid Using Draft OpenZeppelin Contracts

Draft OpenZeppelin contracts may not be fully audited and are subject to changes, which can introduce security risks and instability. To ensure reliability, replace draft imports with stable, production-ready versions.

Remediation

Replace draft imports with stable versions.

// Before
import "@openzeppelin/contracts/drafts/ERC20Permit.sol";

// After
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";

Avoid Relying on block.timestamp for Critical Logic

The block timestamp is set by the miner and can be manipulated within a small range, making it unreliable for critical operations like time-based restrictions. This vulnerability, known as "selective packing", can be exploited to bypass contract logic. For better security, use an external timestamp source such as an oracle, which is less susceptible to manipulation.

References: Timestamp Dependence | Solidity Best Practices, What Is Timestamp Dependence?.

Remediation

Use oracle-based timestamps for critical logic.

// Before
require(block.timestamp > deadline, "Expired");

// After
uint256 oracleTimestamp = chainlinkOracle.getTimestamp();
require(oracleTimestamp > deadline, "Expired");

Avoid Making External Calls Inside Loops

Calling external contracts inside a loop can lead to denial-of-service (DoS) attacks if one of the calls fails or takes too long to execute. This can also result in excessive gas consumption, making transactions more expensive or even causing them to fail. To mitigate this risk, consider batching operations or restructuring the code to minimize external calls within loops.

Remediation

Restructure code to avoid external calls in loops.

Upgrade to a Recent Solidity Version to Avoid Security Risks

Using an outdated Solidity compiler version can expose the contract to known vulnerabilities and missing optimizations. Always use a recent version (≥0.8.10) to benefit from security patches, gas optimizations, and improved features.

Reference: Etherscan Solidity Bug Info.

Remediation

Upgrade to a recent Solidity version (Solidity ≥0.8.10)

Use Ownable2StepUpgradeable Instead of OwnableUpgradeable for Safer Ownership Transfers

Replacing OwnableUpgradeable with Ownable2StepUpgradeable improves security by introducing a two-step ownership transfer process. This prevents accidental loss of ownership and enhances contract safety.

Reference: Ownable2StepUpgradeable.

Remediation

Implement Ownable2StepUpgradeable instead of OwnableUpgradeable.

// Before
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

// After
import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";

Ensure ecrecover() Does Not Return address(0)

Using ecrecover() without checking for address(0) can lead to incorrect signature validation, as ecrecover() may return a random address instead of 0 for an invalid signature. Always include a check to reject zero addresses. It is also recommended to use OpenZeppelin's ECDSA.sol for safer and more reliable signature verification.

Remediation

Add a check for zero address.

address recovered = ecrecover(...);
require(recovered != address(0), "Invalid signature");

Use require Instead of assert to Prevent Gas Wastage

assert() consumes all remaining gas when it fails, whereas require() allows unused gas to be refunded. Use require() for input validation and conditions to prevent unnecessary gas loss and improve contract efficiency.

Reference: Require vs Assert in Solidity.

Remediation

Replace assert with require.

// Before
assert(condition);

// After
require(condition, "Condition failed");

Avoid Using Deprecated Chainlink Library Functions

Chainlink has deprecated functions like getTimestamp, getAnswer, latestRound, and latestTimestamp. Using outdated functions can lead to compatibility issues and missing improvements. It is recommended to update contracts to use the latest Chainlink Data Feeds functions, such as latestRoundData(), to ensure reliability and accuracy.

Reference: Chainlink Data Feeds API Reference.

Remediation

Update the contract to use the latest Chainlink Data Feeds functions, such as latestRoundData() or other recommended functions.

Ensure Compatibility with PUSH0 Opcode When Using Solidity ≥ 0.8.20

Solidity 0.8.20 introduces the PUSH0 opcode for gas optimization, but some EVM implementations, including certain Layer 2 chains, may not support it. Deploying contracts on incompatible chains could lead to failures. To avoid deployment issues, verify the target chain's support for PUSH0. If incompatibilities exist, consider downgrading the Solidity compiler to a version below 0.8.20 or use compiler flags to disable PUSH0 optimizations.

Remediation

Verify the target deployment chain's support for the PUSH0 opcode. If compatibility issues exist, downgrade the Solidity compiler to a version below 0.8.20 or use compiler flags to disable PUSH0 optimizations. Consider using solc with appropriate settings to ensure seamless deployment across different EVM implementations.

Remove or Implement Unused Error Declarations

Some errors are declared but never used in the contract. Unused errors can add unnecessary complexity and should be reviewed to determine if they are needed. If not required, they should be removed or commented out to keep the code clean and maintainable.

Remediation

It is advised to remove or implement unused errors.

Avoid Shadowing Built-In Global Symbols

Using variable or function names that shadow built-in global symbols like now, msg, or block can lead to confusion and unintended behavior. To improve readability and prevent potential issues, rename such variables and functions in order to avoid conflicts.

Remediation

It is advised to rename variables and functions to avoid shadowing.

Perform Multiplication Before Division to Prevent Precision Loss

In Solidity, integer division truncates decimals, which can lead to precision loss if division is performed before multiplication. To maintain accuracy, always reorder operations to multiply first and then divide.

Reference: Solidity Integer Division.

Remediation

Reorder operations to multiply first.

// Before
uint256 result = (a / b) * c;

// After
uint256 result = (a * c) / b;

Lack of Protection When Using block.timestamp for Swap Deadlines

Relying on block.timestamp for swap deadlines provides no security against manipulation. In Proof-of-Stake (PoS) networks, block proposers can predict and reorder transactions within a block, potentially executing swaps under more favorable conditions. This can expose users to front-running risks and manipulated execution timing.

Remediation

Allow users to specify deadline parameters rather than defaulting to block.timestamp. This ensures greater control over transaction execution and reduces the risk of manipulation.

Unused Internal Functions

Internal functions that are never called within the contract may indicate dead code, increasing contract size unnecessarily and potentially causing confusion during audits. Keeping unused functions can also introduce security risks if they become accessible through future upgrades or integrations.

Remediation

Remove any internal functions that are not in use. If the function is intended for external use, consider changing its visibility to public or external to clarify its purpose.

Potential Side Effects from Using Assembly in pure or view Functions

Using inline assembly within pure or view functions can lead to unintended side effects, potentially violating the expected behavior of these functions. Solidity enforces restrictions on state modifications in pure and view functions, but assembly can bypass these safeguards, leading to unexpected interactions with storage or execution context.

Remediation

Avoid using inline assembly in pure or view functions unless absolutely necessary. If assembly is required, thoroughly review the code to ensure no unintended state changes occur. Consider using Solidity's built-in functions instead of assembly whenever possible to maintain transparency and security.

Entire Transaction May Revert Due to require / revert Inside a Loop

Using require or revert inside a loop means that if any iteration fails, the entire transaction is reverted. This can be problematic in scenarios where partial progress should be preserved, such as batch processing or multi-step operations.

Remediation

It is advised to handle failed iterations individually instead of reverting the entire transaction.

decimals() is not a part of the ERC-20 standard

The decimals() function is not a part of the ERC-20 standard and was added later as an optional extension. Some valid ERC20 tokens do not support this interface, so it is unsafe to blindly cast all tokens to this interface and then call this function.

Remediation

Ensure that the token supports the decimals() function before calling it.

decimals() should be of type uint8

The decimals() function should be of type uint8 to ensure compatibility with the ERC-20 standard.

Remediation

Ensure that the decimals() function is of type uint8.

Fallback Lacking payable

The fallback function is not marked as payable, which means it cannot receive Ether. If the contract is expected to receive Ether, mark the fallback function as payable.

Remediation

Mark the fallback function as payable if it is expected to receive Ether.

symbol() is not a part of the ERC-20 standard

The symbol() function is not part of the original ERC-20 standard and was introduced later as an optional extension. Some ERC-20 tokens do not implement this function, which can lead to contract failures if the function is called without checking for support. Blindly casting all tokens to an interface that includes symbol() may result in unexpected behavior.

Remediation

Verify that the token supports the symbol() function before calling it. This can be done using ERC-165's supportsInterface or by handling missing functions gracefully with a try-catch statement when interacting with untrusted tokens.

Inaccurate Year Duration Assumption

Assuming a year is exactly 365 days in Solidity can lead to inaccuracies, as it does not account for leap years. Over time, these small discrepancies can accumulate, affecting contracts that rely on precise time-based calculations, such as vesting schedules or interest rate calculations.

Remediation

Use explicit time conversions:

// Before
uint256 year = 365 days;

// Better alternative:
uint256 year = 365.25 days; // Or use 1 years

Medium issues

Centralization Risk Due to Single Points of Control

Contracts with a single point of control pose centralization risks, making them vulnerable to malicious actions such as rug pulls or unauthorized upgrades. Contract owners must be trusted to act responsibly, but implementing security mechanisms can reduce these risks. To enhance security, consider using timelocks for administrative actions and multi-signature wallets (multi-sig) for privileged operations. These measures improve transparency and reduce the risk of a single entity having unchecked control.

Reference: UK Court Ordered Oasis to Exploit Own Security Flaw to Recover 120k wETH Stolen in Wormhole Hack.

Remediation

Implement timelock and multi-sig mechanisms for privileged operations.

// Using OpenZeppelin's TimelockController
import "@openzeppelin/contracts/governance/TimelockController.sol";

// Or Gnosis Safe multi-sig
import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol";

Use _safeMint Instead of _mint to Prevent NFT Loss

The _mint() function does not verify whether the recipient can receive ERC721 tokens, which can lead to lost or frozen NFTs if sent to an incompatible contract. Using _safeMint() ensures that the recipient is either an externally owned account (EOA) or a contract implementing IERC721Receiver, preventing this issue. This applies even when minting to msg.sender, as msg.sender might be a contract that does not support ERC721. Always use _safeMint() instead of _mint() to guarantee safe transfers.

References: EIP-721, OpenZeppelin Warning ERC721.sol#L271, Solmate _safeMint, OpenZeppelin _safeMint.

Remediation

Replace _mint with _safeMint to ensure safe transfers.

// Before
_mint(to, tokenId);
_mint(msg.sender, tokenId);

// After
_safeMint(to, tokenId, "");
_safeMint(msg.sender, tokenId, "");

// Recipient contract must implement:
function onERC721Received(address, address, uint256, bytes memory) 
	public pure returns (bytes4) {
	return this.onERC721Received.selector;
}

Replace latestAnswer with latestRoundData() for Reliable Price Feeds

The latestAnswer function is deprecated and does not return an error if no valid price is available, instead defaulting to 0. This can lead to inaccurate price feeds or potential denial-of-service issues. To ensure price accuracy and contract reliability, use latestRoundData() and include validation checks for stale or invalid prices.

References: Chainlink API Reference - latestAnswer, latestRoundData() Documentation.

Remediation

Use latestRoundData() with validation checks

(, int256 price,, uint256 updatedAt,) = 
	chainlinkFeed.latestRoundData();

require(updatedAt >= block.timestamp - 1 hours, "Stale price");
require(price > 0, "Invalid price");

Use OpenZeppelin's SafeERC20 Instead of SafeTransferLib.sol for Safer Transfers

Solmate's SafeTransferLib.sol does not verify whether a token address is a valid contract, leaving this responsibility to the caller. This increases the risk of honeypot attacks, where a malicious contract can trap funds. To enhance security, use OpenZeppelin's SafeERC20, which includes additional safety checks to prevent interacting with non-existent token contracts.

References: Solmate's SafeTransferLib.sol, Qubit Finance Hack - January 2022, OpenZeppelin SafeERC20.

Remediation

Use OpenZeppelin's SafeERC20 instead

// Before
import "solmate/utils/SafeTransferLib.sol";
SafeTransferLib.safeTransferFrom(token, from, to, amount);

// After
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
SafeERC20.safeTransferFrom(IERC20(token), from, to, amount);

Avoid Nested Loops to Prevent Denial of Service

Nested loops in Solidity can cause an exponential increase in gas consumption, potentially leading to transaction failures and denial of service. This can compromise the reliability and scalability of the protocol. To mitigate this issue, avoid nested loops whenever possible or implement pagination to process data in smaller batches.

Remediation

Avoid nested loops or implement pagination

Validate ECDSA.recover Output to Prevent Unintended Behavior

The ECDSA.recover function returns address(0) if the provided signature is invalid. If this output is not checked, it can lead to unintended behavior, such as unauthorized access or incorrect validation. To ensure security, always validate the recovered address and revert if it is address(0). Additionally, compare the recovered address with the expected signer to prevent unauthorized actions.

Reference: OpenZeppelin ECDSA.sol.

Remediation

Validate recovered address

address signer = ecrecover(hash, v, r, s);
require(signer != address(0), "Invalid signature");
require(signer == expectedSigner, "Unauthorized");

Check the Return Value of transfer and transferFrom to Prevent Silent Failures

Not all ERC20 token implementations revert on failure—some return false instead. If the return value of transfer or transferFrom is not checked, failed transfers might go unnoticed, leading to unintended behavior. To ensure safe transfers, use OpenZeppelin's SafeERC20, which correctly handles failures, or explicitly check the return value and revert if the transfer fails.

Remediation

Use SafeERC20 or check return values

// With SafeERC20
SafeERC20.safeTransfer(token, to, amount);

// Manual check
bool success = token.transfer(to, amount);
require(success, "Transfer failed");

Avoid Using block.number for Time Calculations Across EVM Chains

The interpretation of block.number varies across Layer 2 networks. On Optimism, it represents the L2 block number, while on Arbitrum, it reflects the L1 block number. These inconsistencies can cause logic errors in contracts that rely on block.number for time-sensitive operations. To ensure consistency across different chains, use block.timestamp instead of block.number for time-based calculations.

Remediation

Use timestamp-based durations instead

// Before
uint256 deadline = block.number + 100;

// After (assuming 15s blocks)
uint256 deadline = block.timestamp + 25 minutes; 

Validate Oracle Data Freshness to Prevent Stale Price Usage

Fetching price or data values from oracles without checking their timestamps can lead to outdated or incorrect values being used in contract operations. If an oracle is down, unresponsive, or delayed, it may return stale data, leading to vulnerabilities or incorrect calculations. To prevent this, always implement a staleness check by ensuring the data's timestamp is within an acceptable threshold (e.g., 1 hour).

Remediation

Add staleness checks for oracle data

(, int256 price,, uint256 updatedAt,) = 
	priceFeed.latestRoundData();

require(
	updatedAt >= block.timestamp - 2 hours,
	"Stale price data"
);

Use of tx.origin for Authorization

Using tx.origin instead of msg.sender for access control exposes the contract to phishing attacks. Malicious contracts can trick users into executing transactions that bypass security checks. To prevent this, always use msg.sender for validating authorized callers.

Reference: Solidity Docs: tx.origin.

Remediation

Replace tx.origin with msg.sender

Use Bounded Gas for External Calls to Prevent Gas Griefing Attacks

Forwarding all available gas in external calls (e.g., call{gas: ...}) can allow attackers to trigger out-of-gas failures, leading to denial-of-service vulnerabilities. To improve reliability, always set a bounded gas limit (e.g., gas: 100000) when making external calls.

Remediation

Use bounded gas for external calls

// Before
(bool success,) = to.call{value: amount}("");

// After
(bool success,) = to.call{value: amount, gas: 100000}("");

Avoid Using blockhash for Randomness to Prevent Manipulation

Using blockhash for randomness is insecure because miners can influence the outcome by selectively mining blocks. This makes it unsuitable for applications requiring fair and unpredictable randomness. To ensure secure randomness, use decentralized oracles such as Chainlink VRF, which provides verifiable and tamper-proof random values.

Remediation

Use Chainlink VRF for randomness

import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";

function requestRandomness() external {
	bytes32 requestId = requestRandomness(keyHash, fee);
}

function fulfillRandomness(bytes32, uint256 randomness) internal override {
	// Use secure random number
}

High issues

Use of delegatecall Inside Loops in Payable Function

Using delegatecall within a loop in a payable function can cause issues where each call retains the msg.value from the initial transaction. This can lead to unintended fund transfers and vulnerabilities. If delegatecall inside a loop is unavoidable, ensure that no msg.value is forwarded within loops, implement strict access controls, and use reentrancy guards to prevent attacks.

Reference: "Two Rights Might Make A Wrong" by samczsun.

Remediation

Avoid using delegatecall inside loops. If unavoidable:

  1. Ensure no msg.value is forwarded in loops
  2. Use strict access controls
  3. Implement reentrancy guards
  4. Explicitly handle ether transfers outside of loops to prevent unintended forwarding
// Add reentrancy guard
bool private locked;
modifier noReentrant() {
	require(!locked, "Reentrant call");
	locked = true;
	_;
	locked = false;
}

function safeDelegatecall(address target, bytes memory data) 
	external payable noReentrant {
	require(msg.value == 0, "Cannot forward ETH in delegatecall loop");

	(bool success,) = target.delegatecall(data);
	require(success, "Delegatecall failed");
}

function withdraw() external {
	payable(msg.sender).transfer(address(this).balance);
}

Arbitrary from Address in transferFrom / safeTransferFrom

Allowing any from address in transferFrom or safeTransferFrom can lead to unintended fund transfers if an attacker gains approval to move tokens on behalf of another address. Ensuring that msg.sender is either the from address or an approved operator prevents unauthorized token transfers. Using OpenZeppelin's SafeERC20 implementation further enhances security by handling transfer failures properly.

Remediation

Validate that msg.sender is either:

  • The from address, or
  • An approved operator
function transferFrom(address from, address to, uint256 amount) public {
	require(
		from == msg.sender || allowance[from][msg.sender] >= amount,
		"Unauthorized"
	);
	_transfer(from, to, amount);
}

// Or use OpenZeppelin's implementation
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
SafeERC20.safeTransferFrom(token, msg.sender, to, amount);

Outdated version of openzeppelin-contracts

Implementing an outdated version of @openzeppelin/contracts, specifically prior to version 4.9.5, introduces multiple high severity issues into the protocol's smart contracts, posing significant security risks. Immediate updating is crucial to mitigate vulnerabilities and uphold the integrity and trustworthiness of the protocol's operations. Check openzeppelin-contracts public reported and fixed security issues.

Remediation

Upgrade to the latest stable version of @openzeppelin/contracts (>= 5.2.0). Ensure all contract dependencies remain compatible after the upgrade and thoroughly test the changes before deployment.

Outdated version of openzeppelin-contracts-upgradeable

Implementing an outdated version of @openzeppelin/contracts-upgradeable, specifically prior to version 4.3.5, introduces multiple high severity issues into the protocol's smart contracts, posing significant security risks. Immediate updating is crucial to mitigate vulnerabilities and uphold the integrity and trustworthiness of the protocol's operations. Check openzeppelin-contracts public reported and fixed security issues.

Remediation

Upgrade to the latest stable version of @openzeppelin/contracts-upgradeable (>= 5.2.0). Ensure all contract dependencies remain compatible after the upgrade and thoroughly test the changes before deployment.

Avoid Using msg.value Inside Loops to Prevent Logic Errors

Reusing msg.value inside a loop can cause unintended behavior since the same value persists across iterations. This can break protocol logic, especially in cases involving multiple recipients or dynamic calculations. To ensure correct value distribution, track the remaining ETH explicitly and deduct the portion sent in each iteration.

Remediation

Track remaining ETH explicitly:

uint256 remainingValue = msg.value;
for (uint i = 0; i < iterations; i++) {
	uint256 portion = remainingValue / (iterations - i);
	(bool success,) = payable(recipient).call{value: portion}("");
	require(success, "Transfer failed");
	remainingValue -= portion;
}

Unsafe type casting

Casting from a larger type to a smaller type without proper validation can cause truncation, leading to unexpected behavior. If the value exceeds the target type's range, it may result in overflow or underflow, potentially introducing critical vulnerabilities. To prevent this, ensure that all type conversions check for validity before execution. Using OpenZeppelin's SafeCast library helps mitigate these risks by reverting on invalid casts.

Remediation

Use OpenZeppelin's SafeCast library:

import "@openzeppelin/contracts/utils/math/SafeCast.sol";

uint256 largeValue = 500;
uint32 smallValue = SafeCast.toUint32(largeValue); // Reverts if overflow

Uninitialized Storage Pointers

Uninitialized storage variables may reference unintended storage slots, leading to data corruption or potential exploits. When a storage pointer is declared but not explicitly assigned, it can point to an arbitrary location in contract storage, causing unpredictable behavior. To prevent this, always initialize storage variables explicitly before use.

Reference: Solidity Docs: Storage Pointers.

Remediation

Always initialize storage variables explicitly:

// Explicit initialization
struct Data {
	uint256 value;
}

function store() public {
	Data storage d; // INCORRECT
	
	Data storage d = data[msg.sender]; // CORRECT
	d.value = 100;
}

Price Manipulation Risk Due to Flash Loan Vulnerability in get_dy_underlying()

Using get_dy_underlying() as a price oracle is unsafe because it can be manipulated through flash loans, leading to inaccurate pricing and potential exploits. Since flash loans allow attackers to temporarily inflate or deflate asset values, relying on this function for pricing can expose contracts to severe financial risks. To mitigate this, use a secure oracle like Chainlink, which provides tamper-resistant pricing and includes staleness checks to prevent the use of outdated data.

Reference: Chainlink Data Feeds.

Remediation

Use Chainlink oracles with staleness checks:

// Replace vulnerable code
uint256 price = curvePool.get_dy_underlying(...);

// With secure oracle
(uint80 roundID, int256 price,, uint256 updatedAt,) = 
	chainlinkFeed.latestRoundData();
require(updatedAt >= block.timestamp - 1 hours, "Stale price");

Incorrect Price Calculation When Converting Between wstETH and stETH

Incorrect price calculation between wstETH and stETH. Multiply price by WstETH.stEthPerToken() to convert to ETH units.

Remediation

Multiply by conversion rate:

// Before (incorrect)
uint256 ethAmount = price * wstETHAmount;

// After (correct)
uint256 stEthPerToken = IWstETH(wstETH).stEthPerToken();
uint256 ethAmount = price * wstETHAmount * stEthPerToken / 1e18;

Unintended Execution Flow Due to return Statement in Yul Assembly

Using return in Yul assembly immediately halts execution, potentially skipping critical operations such as cleanup or state updates. This can introduce unexpected behavior or leave the contract in an inconsistent state. To ensure safe execution, always complete necessary operations before returning from an assembly block.

Reference: Inline Assembly | Solidity Docs.

Remediation

Avoid early returns in assembly blocks. Ensure all cleanup operations complete:

assembly {
	let result := delegatecall(...)
	// Perform all necessary operations
	switch result
	case 0 { revert(0, 0) }
	default { return(0, 0) } // Safe if all ops complete
}

RTLO character detected

The right-to-left override (RTLO) character (U+202E) can be used to visually obfuscate text, potentially misleading users or causing confusion in string representation. This could introduce risks in contract interactions or auditing processes. To mitigate this, ensure all strings and comments are free of RTLO characters. Additionally, implement a pre-commit hook to automatically detect RTLO characters and prevent their inclusion in the codebase.

Remediation

  1. Remove RTLO characters from all strings/comments
  2. Add pre-commit hook to detect RTLO:
# .pre-commit-config.yaml
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
	- id: check-byte-order-marker
	- id: check-merge-conflict
	- id: check-yaml
	- id: detect-private-key
	- id: end-of-file-fixer
	- id: mixed-line-ending
	- id: trailing-whitespace
	- id: check-ast

Risk of Inconsistent Behavior Due to Multiple Retryable Calls

Using multiple retryable ticket calls within a single function can lead to them being executed out of order, causing inconsistencies and unintended behavior. This can be mitigated by ensuring that each retryable call is associated with a unique, sequential identifier or nonce. To ensure correct execution order, use atomic transactions or sequence numbers to track and validate each retryable call.

Remediation

Use atomic transactions or sequence numbers:

uint256 public nonce;

function executeWithRetry() external {
	uint256 currentNonce = nonce++;
	// Include nonce in retryable data
	inbox.createRetryableTicket({
		data: abi.encode(currentNonce, ...)
	});
}

Locked Ether Due to Missing Withdraw Function

When a contract includes payable functions but lacks a method to withdraw Ether, funds can become locked, making them inaccessible to the contract owner or other authorized parties. This can result in a loss of control over the funds. To fix this, implement a withdraw function that allows Ether to be safely transferred out of the contract. Ensure proper access control to prevent unauthorized withdrawals.

Remediation

Add withdraw function with access control:

function withdrawETH(address payable to) external onlyOwner {
	uint256 balance = address(this).balance;
	(bool sent,) = to.call{value: balance}("");
	require(sent, "Failed to send Ether");
}

// Or use OpenZeppelin's Escrow pattern
import "@openzeppelin/contracts/utils/Escrow.sol";

Clone this wiki locally