-
Notifications
You must be signed in to change notification settings - Fork 0
Home
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.
Replace bool with uint256 (1 for true, 0 for false).
// Before
bool public isActive = true;
// After
uint256 public isActive = 1; // 1 = true, 0 = falseFailing 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.
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; }
}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.
Remove explicit initialization.
// Before
uint256 x = 0;
// After
uint256 x;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
Use bitwise shifts.
// Before
x = y * 8;
// After
x = y << 3; // Equivalent to y * 2^3For unsigned integer comparisons, != 0 is cheaper than > 0. This minor change reduces gas consumption in conditions and validation checks.
Use != 0.
// Before
require(x > 0);
// After
require(x != 0);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.
Use custom errors or shorten strings.
// Before
revert("Long error message");
// After
error ShortError();
revert ShortError();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++?.
Use prefix increment.
// Before
i++;
// After
++i;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.
Add immutable/constant.
// Before
uint256 public value;
// After
uint256 public immutable value;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.
Change the visibility from public to external.
// Before
function foo() public {}
// After
function foo() external {}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.
Add payable to the interested functions.
// Before
function withdraw() external onlyOwner {}
// After
function withdraw() external payable onlyOwner {}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.
Implement inline assembly check.
// Before
require(addr != address(0));
// After
assembly {
if iszero(addr) {
revert(0, 0)
}
}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.
Replace assert with require.
// Before
assert(condition);
// After
require(condition);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.
Use uint256 unless packing.
// Before
uint8 smallVar = 100;
// After
uint256 normalVar = 100;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.
Use selfbalance in assembly.
// Before
uint256 bal = address(this).balance;
// After
uint256 bal;
assembly { bal := selfbalance() }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.
If possible, use immutable instead of constant.
// Before
bytes32 constant HASH = keccak256("hash");
// After
bytes32 immutable HASH = keccak256("hash");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.
Split require statements into multiple checks.
// Before
require(a && b);
// After
require(a);
require(b);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.
Use explicit assignment.
// Before
x += y;
// After
x = x + y;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.
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
}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.
Remove redundant fields.
// Before
event Log(uint256 value, uint256 timestamp);
// After
event Log(uint256 value); // timestamp is auto-addedComparing 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.
Simplify boolean checks.
// Before
if (x == true) {}
// After
if (x) {}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.
Use >=/<=.
// Before
require(x > 100);
// After
require(x >= 101);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.
Mark constants as private instead of public.
// Before
uint256 public constant VALUE = 100;
// After
uint256 private constant VALUE = 100;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).
Update pragma to a more recent version.
// Before
pragma solidity ^0.8.0;
// After
pragma solidity ^0.8.20;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.
Include error messages in require/revert statements.
// Before
require(condition);
// After
require(condition, "Condition not met: insufficient balance");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.
Name return parameters in function declarations
// Before
function getUser() external returns (uint256);
// After
function getUser() external returns (uint256 userId);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.
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);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.
Use explicit imports with curly braces.
// Before
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// After
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";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.
Remove TODOs or track them in an issue tracker.
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.
Add the SPDX license identifier at the top of each Solidity file.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;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.
Specify the Solidity version in files missing it.
// Add to the top of the file
pragma solidity ^0.8.20;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.
Add a comment to explain why a function is empty.
Embedding numeric literals directly into the code reduces readability, maintainability, and security. Instead, define meaningful constants or variables to provide context and improve transparency.
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;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.
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 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.
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.
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.
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.
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.
Use scientific notation (e notation) instead of long numeric literals. For example, replace 1000000000000000000 with 1e18 to represent one Ether in wei.
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.
It is reccomended to use uint256 and int256 explicitly instead of relying on shorthand types.
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.
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.
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.
It is advised to remove pragma abicoder v2; from Solidity files when using Solidity 0.8.0 or higher.
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.
To enhance type safety and prevent errors, use abi.encodeCall, which ensures that function signatures and argument types match the expected function definition.
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.
Rename constants to use CONSTANT_CASE (e.g., MAX_VALUE).
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.
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.
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.
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.
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.
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.
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.
To improve clarity and adhere to best practices, declare mappings in the following format: mapping(address => uint256) balances; instead of mapping (address => uint256) balances;.
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.
It is advised to replace hard-coded addresses with immutable variables that are initialized in the constructor.
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.
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.
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.
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.
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.
Use a fixed Solidity version to ensure consistent compilation.
// Before
pragma solidity ^0.8.0;
// After
pragma solidity 0.8.20;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.
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);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.
Replace deprecated functions with modern equivalents.
// Before
_setupRole(DEFAULT_ADMIN_ROLE, admin);
// After
_grantRole(DEFAULT_ADMIN_ROLE, admin);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?.
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().
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.
Implement two-step ownership transfer.
// Before
transferOwnership(newOwner);
// After
import "@openzeppelin/contracts/access/Ownable2Step.sol";
safeTransferOwnership(newOwner);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.
Replace draft imports with stable versions.
// Before
import "@openzeppelin/contracts/drafts/ERC20Permit.sol";
// After
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";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?.
Use oracle-based timestamps for critical logic.
// Before
require(block.timestamp > deadline, "Expired");
// After
uint256 oracleTimestamp = chainlinkOracle.getTimestamp();
require(oracleTimestamp > deadline, "Expired");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.
Restructure code to avoid external calls in loops.
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.
Upgrade to a recent Solidity version (Solidity ≥0.8.10)
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.
Implement Ownable2StepUpgradeable instead of OwnableUpgradeable.
// Before
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
// After
import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";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.
Add a check for zero address.
address recovered = ecrecover(...);
require(recovered != address(0), "Invalid signature");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.
Replace assert with require.
// Before
assert(condition);
// After
require(condition, "Condition failed");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.
Update the contract to use the latest Chainlink Data Feeds functions, such as latestRoundData() or other recommended functions.
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.
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.
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.
It is advised to remove or implement unused errors.
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.
It is advised to rename variables and functions to avoid shadowing.
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.
Reorder operations to multiply first.
// Before
uint256 result = (a / b) * c;
// After
uint256 result = (a * c) / b;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.
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.
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.
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.
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.
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.
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.
It is advised to handle failed iterations individually instead of reverting the entire transaction.
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.
Ensure that the token supports the decimals() function before calling it.
The decimals() function should be of type uint8 to ensure compatibility with the ERC-20 standard.
Ensure that the decimals() function is of type uint8.
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.
Mark the fallback function as payable if it is expected to receive Ether.
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.
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.
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.
Use explicit time conversions:
// Before
uint256 year = 365 days;
// Better alternative:
uint256 year = 365.25 days; // Or use 1 yearsContracts 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.
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";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.
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;
}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.
Use latestRoundData() with validation checks
(, int256 price,, uint256 updatedAt,) =
chainlinkFeed.latestRoundData();
require(updatedAt >= block.timestamp - 1 hours, "Stale price");
require(price > 0, "Invalid price");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.
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);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.
Avoid nested loops or implement pagination
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.
Validate recovered address
address signer = ecrecover(hash, v, r, s);
require(signer != address(0), "Invalid signature");
require(signer == expectedSigner, "Unauthorized");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.
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");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.
Use timestamp-based durations instead
// Before
uint256 deadline = block.number + 100;
// After (assuming 15s blocks)
uint256 deadline = block.timestamp + 25 minutes; 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).
Add staleness checks for oracle data
(, int256 price,, uint256 updatedAt,) =
priceFeed.latestRoundData();
require(
updatedAt >= block.timestamp - 2 hours,
"Stale price data"
);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.
Replace tx.origin with msg.sender
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.
Use bounded gas for external calls
// Before
(bool success,) = to.call{value: amount}("");
// After
(bool success,) = to.call{value: amount, gas: 100000}("");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.
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
}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.
Avoid using delegatecall inside loops. If unavoidable:
- Ensure no
msg.valueis forwarded in loops - Use strict access controls
- Implement reentrancy guards
- 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);
}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.
Validate that msg.sender is either:
- The
fromaddress, 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);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.
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.
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.
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.
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.
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;
}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.
Use OpenZeppelin's SafeCast library:
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
uint256 largeValue = 500;
uint32 smallValue = SafeCast.toUint32(largeValue); // Reverts if overflowUninitialized 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.
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;
}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.
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 between wstETH and stETH. Multiply price by WstETH.stEthPerToken() to convert to ETH units.
Multiply by conversion rate:
// Before (incorrect)
uint256 ethAmount = price * wstETHAmount;
// After (correct)
uint256 stEthPerToken = IWstETH(wstETH).stEthPerToken();
uint256 ethAmount = price * wstETHAmount * stEthPerToken / 1e18;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.
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
}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.
- Remove RTLO characters from all strings/comments
- 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-astUsing 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.
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, ...)
});
}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.
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";