Skip to content

Solidity: Best Practices

Kerala Blockchain Academy edited this page Jun 21, 2024 · 5 revisions

Coding Style

This guide outlines the current style conventions for the Solidity programming language. While inspired by the structure and recommendations of Python's PEP 8 style guide, these conventions are tailored explicitly for Solidity.

This guide does not dictate how to write Solidity code but rather promotes consistency and readability across Solidity projects.

Code Layout

Indentation

Consistent indentation is crucial for code readability. Use 4 spaces for each indentation level. While tabs are not recommended, the most important factor is to maintain consistency throughout your codebase. Avoid mixing tabs and spaces.

Blank Lines

Think of blank lines as visual separators in your code.

  • Top-Level Declarations: Separate top-level declarations (contracts, interfaces, libraries) within the Solidity file with two blank lines.
  • Functions: Using a single blank line to separate function declarations within a contract.
  • Related One-Liners: Blank lines may be omitted for visual compactness in special cases, such as groups of simple, related functions (e.g., in abstract contracts)

Source File Encoding

To ensure compatibility and avoid unexpected characters, it is recommended to use either UTF-8 or ASCII encoding for your Solidity source files.

Order of Functions

A well-defined function order enhances code readability and makes it easier to identify callable functions. It also simplifies the process of locating the constructor and fallback functions.

Grouping by Visibility

Organise your functions based on their visibility, following this order:

  • constructor
  • receive function (if exists)
  • fallback function (if exists)
  • external
  • public
  • internal
  • private

Placement of View and Pure Functions

Place view and pure functions at the end of each visibility group. This convention helps distinguish state-changing functions from those that only read from the blockchain state.

Whitespace in Expressions

Whitespace is essential for code readability, but too much can clutter your code. Follow these guidelines to maintain a clean and consistent style:

Avoid Extra Whitespace

  • Inside Parentheses, Brackets, and Braces: Do not add spaces immediately inside parentheses, brackets, or braces, except when writing single-line function declarations.
  • Before Punctuation: Avoid placing spaces immediately before commas or semicolons.
  • Over-Alignment: Don't use more than one space around an assignment operator (=) or other operators (+, -, etc.) to align with other lines of code. This can make your code harder to maintain.
  • receive and fallback Functions: For compactness, omit whitespace within receive and fallback functions.

Following these guidelines will make your code easier to read and maintain.

Control Structures

For code readability, consistent formatting of control structures, such as if, else, while, and for, as well as contract, library, function, and struct bodies, is important.

Braces and Indentation

  • Opening Brace: The opening brace should be on the same line as the declaration and preceded by a single space.
  • Closing Brace: The closing brace should be on its own line, aligned with the start of the declaration.
  • Single-Statement Bodies: If a control structure or function body contains only one statement, you may omit the braces if the statement fits on a single line.

Whitespace Around Control Structures

  • Keywords and Parentheses: Place a single space between control structure keywords (if, while, for) and their corresponding parenthetic blocks.
  • Parentheses and Braces: Similarly, use a single space between the parenthetic block and the opening brace.

Special Case: Else Clauses

  • Placement After if: In the case of if blocks with an else or else...if clause, the else keyword should be on the same line as the closing brace of the if block.

By adhering to these conventions, your code will be more visually organised and more accessible to follow.

Function Declaration

Formatting function declarations consistently is essential for code clarity and maintainability.

Short Function Declarations

  • Opening Brace: Place the opening brace of the function body on the same line as the function declaration, preceded by a single space.
  • Closing Brace: Align the closing brace with the start of the function declaration.

Long Function Declarations

  • Arguments on New Lines: For functions with multiple or complex arguments, place each argument on its own line, indented to the same level as the function body.
  • Parentheses and Brace on New Lines: In this case, place the closing parenthesis and opening brace on separate lines, also aligned with the function declaration.

Modifiers

  • Order: Arrange modifiers in the following order: visibility, mutability, virtual, override, followed by any custom modifiers.
  • New Lines for Long Declarations: If a function declaration with modifiers becomes lengthy, place each modifier on its own line.

Constructor Functions (Inherited Contracts)

  • New Lines for Base Constructors: If a constructor function needs to call base constructors with arguments, and the declaration becomes long or difficult to read, place each base constructor call on a new line, similar to modifiers.

Single-Statement Functions

  • One-Line Option: You can optionally write the entire declaration on one line for short functions with a single statement.

Mappings and Variable Declarations

Consistent formatting of mappings and variable declarations contributes to code clarity and prevents potential errors.

Mappings

  • No Space After Keyword: Do not insert a space between the mapping keyword and its type when declaring a mapping variable.
  • No Space in Nested Mappings: Similarly, in nested mappings, avoid spaces between nested mapping keywords and their types.

Variable Declarations

  • Array Declarations: Do not include a space between the array type and the opening square bracket ([]).
  • String Quotes: Always use double quotes ("") to enclose string literals, not single quotes ('').

General Note

These guidelines aim to improve the readability and maintainability of your Solidity code. While they cover common scenarios, there might be exceptions depending on the context. As a developer, use your best judgment to ensure your code is clear and consistent.

Order of Layout

A well-organized layout of contract elements improves the readability and maintainability of your Solidity code.

Overall File Structure

  • SPDX License Identifier: Include the SPDX license identifier at the top of the file.
  • Pragma Statement: Declare the pragma directive to specify the Solidity compiler version.
  • Import Statements: Group all import statements together.
  • Interfaces: Define interfaces before libraries or contracts.
  • Libraries: Declare libraries before contracts.
  • Contracts: Place contract definitions at the end.

Inside Contracts, Libraries, or Interfaces

  • Type Declarations: Define custom types (structs, enums) first.
  • State Variables: Declare state variables next.
  • Events: Define events before functions.
  • Functions: Order functions according to visibility (as described in a previous section).

Naming Conventions

Well-chosen names enhance code understanding and maintainability. While not strict rules, these conventions aim to improve readability by conveying information through names.

Naming Styles

To avoid confusion, the following names will be used to refer to different naming styles.

  • b (single lowercase letter)
  • B (single uppercase letter)
  • lowercase: All lowercase letters
  • lower_case_with_underscores: Lowercase letters with underscores
  • UPPERCASE: All uppercase letters
  • UPPER_CASE_WITH_UNDERSCORES: Uppercase letters with underscores
  • CapitalizedWords (or CapWords): Words capitalized (e.g., MyContract)
  • camelCase: Similar to CapWords but starting with lowercase (e.g., findSum)
  • Capitalized_Words_With_Underscores: Words capitalised with underscores

Letters to Avoid

  • l - Lowercase "L"
  • O - Uppercase "O"
  • I - Uppercase "i"

These can be confused with numbers or other letters.

Specific Naming Recommendations

  • Contracts and Libraries: Use CapWords. Filenames should match the contract/library name.
  • Structs: Use CapWords.
  • Events: Use CapWords.
  • Functions: Use camelCase.
  • Function Arguments: Use camelCase. For library functions operating on custom data, name the first argument self.
  • Local and State Variables: Use camelCase.
  • Constants: Use UPPER_CASE_WITH_UNDERSCORES.
  • Modifiers: Use camelCase.
  • Enums: Use CapWords.

Best Practices

  1. Keep it Simple: Prioritize clean, concise code and modular design. Avoid unnecessary complexity to minimise errors. Leverage existing tools and libraries when possible.
  2. Stay Up-to-Date: Actively monitor security developments and promptly address vulnerabilities. Upgrade tools and libraries to the latest versions.
  3. Test Thoroughly: Conduct comprehensive testing, both manual and automated, using a variety of environments (e.g., private networks, public testnets).
  4. Optimise Variable Usage: To improve gas efficiency, favour fixed-size variables (e.g., bytes32, bytes30) over dynamic ones (e.g., string, bytes). Consider using bytes instead of strings when feasible.
  5. Maintain Determinism: Ensure your contract logic is predictable and has a known maximum execution cost. Avoid unbounded loops or recursions that could consume excessive gas.
  6. Use Arrays Wisely: Arrays are not designed for large datasets in Solidity. For extensive data, prefer fixed-size arrays over dynamic ones, and consider off-chain storage.
  7. Memory Caching: Optimize state variable access using local variables for intermediate calculations and writing to storage only once.
  8. Explicit Visibility: Always define visibility (public, external, internal, private) for functions and state variables to control access and enhance security.
  9. External Calls: Use external for functions called only externally (cheaper due to direct call data access).Use public for functions called internally (arguments are copied to memory).Minimize external calls to untrusted contracts to avoid potential security risks and unexpected errors.
  10. Favor Pull over Push: When interacting with external contracts, prefer pull mechanisms (e.g., users withdrawing funds) over push mechanisms (e.g., automatically sending funds) to reduce transaction failures.
  11. Libraries: Utilize libraries for code reusability, especially in larger contracts. However, avoid them in smaller contracts due to potential overhead.
  12. Integer Division and Rounding: Be mindful of integer division rounding down. Consider using multipliers or storing the numerator and denominator separately for higher precision.
  13. Modifiers: Employ modifiers to enforce conditions before function execution. They can take arguments and include require statements for error handling.
  14. Access Restriction: Control access to functions using modifiers to ensure only authorised parties can interact with certain functionalities.
  15. Trapped Ether: If your contract receives payments, implement a mechanism for the owner to withdraw or transfer the accumulated ether.
  16. Checks-Effects-Interactions Pattern: Follow this pattern to minimise risks when interacting with external contracts. Perform checks first, then modify state variables, and lastly, interact with other contracts.
  17. Modifiers for Checks Only: Use modifiers exclusively for checks, avoiding state changes or external calls within them.
  18. Integer Overflow/Underflow: Since Solidity 0.8.0, these issues are addressed by default. Exercise caution with integer operations for earlier versions and consider using the SafeMath library from OpenZeppelin.
  19. Lock Pragmas: Specify the exact compiler version and flags (Optimizer Settings or Experimental Features) used for testing to ensure consistent deployment behaviour.
  20. Events for Monitoring: Emit events to track contract activity and trigger actions in user interfaces.
  21. Fallback Functions: Keep fallback functions simple, using either revert() or event logging.
  22. Forced Ether Transfers: Be aware that Ether can be sent to any address, even contracts, without invoking code. Design contracts accordingly.
  23. Abstract Contracts vs. Interfaces: Understand the tradeoffs between abstract contracts (more flexible) and interfaces (simpler). Use interfaces for design and abstract contracts for partial implementation.
  24. Shadowing Built-ins: Avoid overriding built-in functions like msg and revert(), which can lead to unexpected behaviour.
  25. Blockchain Properties: Be aware of Ethereum’s unique blockchain properties.
    • External contract calls can be malicious, execute harmful code, and alter control flow.
    • Public functions are accessible to anyone and can be exploited.
    • Private data is not private on a public blockchain.
    • Gas costs matter. Optimise code and consider block gas limits.
    • Timestamps are not reliable for precision or randomness.
    • Blockchain interaction is open to all.
    • Use standard libraries like OpenZeppelin, solmate, solady without modification.
  26. Hybrid Systems: Consider a hybrid approach for large datasets with blockchain storing proof of data and off-chain databases holding the actual data.
  27. Gas Optimization Tips: Use fixed-size variables, minimise external calls, remove unnecessary variables, avoid default initialisations, write concise require messages, and prefer mappings over arrays when appropriate.
  28. Maximum Contract Size: Adhere to the 24 KB contract size limit. If necessary, split large contracts into multiple smaller ones.


Clone this wiki locally