<Best Practices for Solidity Coding Standards: A Comprehensive Guide>
Written on
The aim of this article is not to reiterate the official Solidity Style Guide, which should be referred to, but to highlight the frequent deviations from this guide observed during code audits or reviews. Some of the points discussed may not appear in the style guide, yet they represent common stylistic errors made by Solidity developers.
First two lines
Add SPDX-License-Identifier
While your code may compile without this line, it will trigger a warning. Simply including it will eliminate that warning.
Correctly Set the Solidity Pragma Unless Creating a Library
You may have encountered pragmas such as:
pragma solidity ^0.8.0;
and
pragma solidity 0.8.26;
Which version should you use? If you are compiling and deploying the contract, you know your Solidity version, so it’s best to specify it clearly. However, if you’re developing a library for others to use, avoid fixing the pragma, as you cannot predict the compiler version your users will choose.
Imports
Specify Library Version in Import Statements
Instead of this:
import "@openzepplin/contracts/token/ERC20/ERC20.sol";
Use this:
import "@openzeppelin/[email protected]/token/ERC20/ERC20.sol";
To find the latest version, click on the branch dropdown on GitHub, select tags, and choose the most recent release. Always opt for the latest stable version.
Failing to version your imports may lead to your code breaking or behaving unpredictably when the underlying library changes.
Utilize Named Imports Instead of Importing Entire Namespaces
Instead of:
import "@openzeppelin/[email protected]/token/ERC20/ERC20.sol";
Use:
import {ERC20} from "@openzeppelin/[email protected]/token/ERC20/ERC20.sol";
Importing everything from a file can clutter the namespace, potentially leaving unused code that the compiler optimizer may not eliminate.
Eliminate Unused Imports
Tools like Slither can automatically flag these. Don’t hesitate to remove them; tidying up code is essential.
Contract Level
Implement Contract-Level NatSpec
NatSpec (natural language specification) serves to provide human-readable inline documentation. Here's an example:
/// @title Liquidity token for Foo protocol
/// @author Foo Incorporated
/// @notice General notes for non-technical readers
/// @dev Technical notes for Solidity developers
contract LiquidityToken {
}
Structure Contracts According to the Style Guide
Functions should be organized first by visibility and then by their state-modifying capabilities. The order should be: receive and fallback functions (if applicable), external functions, public functions, internal functions, and private functions. Within those categories, payable functions should be prioritized over non-payable, followed by view, and then pure functions.
contract ProperLayout {
// Type declarations
address internal owner;
uint256 internal _stateVar;
uint256 internal _stateVar2;
// Events
event Foo();
event Bar(address indexed sender);
// Errors
error NotOwner();
error FooError();
error BarError();
// Modifiers
modifier onlyOwner() {
if (msg.sender != owner) {
revert NotOwner();
}
_;
}
// Functions
constructor() {}
receive() external payable {}
fallback() external payable {}
// Function ordering by visibility
function foo() external payable {}
function bar() external {}
function baz() external view {}
function qux() external pure {}
function fred() public {}
function bob() public view {}
}
Constants
Substitute Magic Numbers with Constants
If you encounter a number like 100 in your code, clarify its purpose. Generally, numbers should be defined as constants at the beginning of the contract.
Use Solidity Keywords for Ether or Time Measurements
Instead of writing:
uint256 secondsPerDay = 60 * 60 * 24;
Use:
1 days
Similarly, replace:
require(msg.value == 10**18 / 10, "must send 0.1 ether");
with:
require(msg.value == 0.1 ether, "must send 0.1 ether");
Utilize Underscores for Large Number Readability
Instead of:
uint256 private constant BASIS_POINTS_DENOMINATOR = 10000;
Write:
uint256 private constant BASIS_POINTS_DENOMINATOR = 10_000;
Functions
Remove Virtual Modifiers from Non-Overridable Functions
The virtual modifier indicates that a function can be overridden. If you’re certain a function won’t be overridden, it’s unnecessary; simply delete it.
Order Function Modifiers Correctly
The proper order is visibility, mutability, virtual, override, and custom modifiers. For example:
// visibility (payability), [virtual], [override], [custom]
function foo() public payable onlyAdmin {}
function bar() internal view virtual override onlyAdmin {}
Properly Utilize NatSpec for Functions
Like contract-level NatSpec, function NatSpec outlines parameters and return values based on function arguments. This aids in describing argument names without lengthy variable names.
/// @notice Deposit ERC20 tokens
/// @param token The ERC20 token to deposit
/// @param amount The amount of tokens to deposit
/// @returns The amount of liquidity tokens received
function deposit(address token, uint256 amount) public returns (uint256) {}
You can inherit NatSpec from functions in parent contracts as well.
General Cleanliness
Remove Commented Out Code
This is straightforward; commented code adds unnecessary clutter.
Choose Variable Names Wisely
Naming is crucial for code readability. Here are some tips:
- Avoid generic terms like "user." Instead, use specific names such as "admin," "buyer," or "seller."
- The term "data" often lacks precision; prefer clearer names like "userAccount."
- Use consistent terminology for the same entity; don’t mix "depositor" and "liquidityProvider."
- Include units in variable names, e.g., "interestRatesBasisPoints" instead of just "interestRate."
- Use verbs in state-changing function names.
- Maintain consistency with underscores to differentiate internal variables from function arguments.
- Following the convention of using "get" for data retrieval and "set" for data modification can enhance clarity.
- After coding, revisit your variable and function names to ensure precision.
Additional Tricks for Organizing Large Codebases
- For numerous storage variables, define them in a single contract and inherit from that contract.
- Use structs to manage functions with many parameters.
- Consolidate imports into a single Solidity file before importing it to streamline code.
- Group related functions into libraries to reduce file size.
Mastering the organization of large codebases is an art, best learned by examining established projects.
Learn More with RareSkills
This checklist is utilized in our advanced Solidity bootcamp for code reviews.