Skip to content

Solidity: Best Practices

tr1sm0s1n edited this page May 22, 2024 · 5 revisions

Coding Style

This section provides current conventions for the Solidity language. The structure and many recommendations are taken from Python’s pep8 style guide.

The end goal is not to learn how to write or how not to write Solidity; the style conventions are solely intended to establish consistency when coding Solidity.

Code Layout

Indentation

Use 4 spaces per indentation level. Spaces are the preferred indentation method. Mixing tabs and spaces should be avoided.

Blank Lines

Surround top-level declarations in the Solidity file with two blank lines. Within a contract, surround function declarations with a single blank line. Blank lines may be omitted between groups of related one-liners (such as stub functions for an abstract contract).

Maximum Line Length

Keeping the line length to a maximum of 79 (or 99) characters, as recommended by PEP 8, helps readers easily parse the code.

Wrapped lines should conform to the following guidelines.

  1. The first argument should not be attached to the opening parenthesis.
  2. One, and only one, indent should be used.
  3. Each argument should fall on its line.
  4. The terminating element, );, should be placed on the final line by itself.

Source File Encoding

UTF-8 or ASCII encoding is preferred.

Order of Functions

Ordering helps readers identify which functions they can call while finding the constructor and fallback definitions easier.

Functions should be grouped according to their visibility and order:

  • constructor
  • receive function (if exists)
  • fallback function (if exists)
  • external
  • public
  • internal
  • private
  • Within a grouping, place the view, and pure functions last.

Whitespace in Expressions

Avoid extraneous whitespace in the following situations:

  • Immediately inside parenthesis, brackets, or braces, except for single-line function declarations.
  • Immediately before a comma, semicolon.
  • More than one space around an assignment or operator to align with another.
  • Don’t include whitespace in the receive and fallback functions.

Control Structures

The braces denoting the body of a contract, library, functions, and structs should:

  • open on the same line as the declaration.
  • close on their own line at the same indentation level as the beginning of the declaration.
  • have an opening brace preceded by a single space.
  • The same recommendations apply to the control structures if, else, while, and for.

Additionally, there should be a single space between the control structures if, while, and for, and the parenthetic block representing the conditional, as well as a single space between the conditional parenthetic block and the opening brace.

For control structures whose body contains a single statement, omitting the braces is ok if the statement is contained on a single line.

For if blocks that have an else or else...if clause, the else should be placed on the same line as the if’s closing brace. This is an exception compared to the rules of other block-like structures.

Function Declaration

For short function declarations, it is recommended that the opening brace of the function body be kept on the same line as the function declaration.

The closing brace should be at the same indentation level as the function declaration.

The opening brace should be preceded by a single space.

The modifier order for a function should be:

  • Visibility
  • Mutability
  • Virtual
  • Override
  • Custom modifiers

For long function declarations, it is recommended to drop each argument onto its own line at the same indentation level as the function body. The closing parenthesis and opening bracket should be placed on their own line as well at the same indentation level as the function declaration.

If a long function declaration has modifiers, then each modifier should be dropped to its own line.

Multiline output parameters and return statements should follow the same style recommended for wrapping long lines found in the Maximum Line Length section.

For constructor functions on inherited contracts whose bases require arguments, it is recommended to drop the base constructors onto new lines in the same manner as modifiers if the function declaration is long or hard to read.

When declaring short functions with a single statement, it is permissible to do it on a single line.

Mappings

In variable declarations, do not separate the keyword mapping from its type by a space. Do not separate any nested mapping keyword from its type by whitespace.

Variable Declarations

Declarations of array variables should not have a space between the type and the brackets.

Strings should be quoted with double quotes instead of single quotes.

Note: These guidelines are intended to improve readability. Authors should use their best judgment as this unit does not try to cover all possible ways of code writing.

Order of Layout

Layout contract elements in the following order:

  • SPDX License Identifier statement
  • Pragma statement
  • Import statements
  • Interfaces
  • Libraries
  • Contracts

Inside each contract, library or interface, use the following order:

  • Type declarations
  • State variables
  • Events
  • Functions

Naming Conventions

The naming convention can be used to convey metadata about variables, contracts, functions, etc. The naming recommendations given here are intended to improve the readability, and thus they are not rules, but rather guidelines to try and help convey the most information through the names of things.

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
  • lower_case_with_underscores
  • UPPERCASE
  • UPPER_CASE_WITH_UNDERSCORES
  • CapitalizedWords (or CapWords)
  • mixedCase (differs from CapitalizedWords by initial lowercase character!)
  • Capitalized_Words_With_Underscores

Letters to Avoid

  • l - Lowercase letter el
  • O - Uppercase letter oh
  • I - Uppercase letter eye

Never use any of these for single-letter variable names. They are often indistinguishable from the numerals one, zero, and sometimes other letters.

Contract and Library Names

Contracts and libraries should be named using the CapWords style. Examples: SimpleToken, MyContract, StudentDetails, etc.

Contract and library names should also match their filenames.

If a contract file includes multiple contracts and/or libraries, then the filename should match the core contract, though it is not a strict recommendation.

Struct Names

Structs should be named using the CapWords style. Examples: MyBook, Student, Details, etc.

Event Names

Events should be named using the CapWords style. Examples: NewBook, Add, StudentAdded, etc.

Function Names

Functions should use camelCase. Examples: getBalance, transfer, getDetails, addStudent, changeOwner, etc.

Function Argument Names

Function arguments should use camelCase. Examples: firstName, status, projectPhase, senderAddress, newOwner, etc.

When writing library functions that operate on custom data, the data should be the first argument and should always be named self.

Local and State Variable Names

Use camelCase. Examples: firstName, middleName, lastName, etc.

Constants

Constants should be named with all capital letters and underscores separating words. Examples: MAX_COUNT, MAX_VALUE, etc.

Modifier Names

Use camelCase. Examples: onlyOwner, isContract, etc.

Enums

Enums should be named using the CapWords style, in the style of simple type declarations. Examples: ProjectPhase, Light, Door, etc.

Best Practices

1. Keep it simple

Write clean code and keep the contract simple. A clean code will allow us to understand and identify any problem that we could encounter easily. The solidity styling standards we learned in the earlier section can be used as a reference when coding.

Complexity increases the likelihood of errors.

  • Ensure that the contract logic is simple.
  • Modularize code to keep contracts and functions small.
  • Use already-written tools or code wherever possible (eg. don't roll your own random number generator).
  • Prefer clarity to performance whenever possible.
  • Only use the blockchain for the parts of your project that require decentralization or immutability.

2. Stay up to date

Keep track of new security developments.

  • Check your contracts for any new bug as soon as it is discovered.
  • Upgrade to the latest version of any tool or library as soon as possible.
  • Adopt new security techniques that appear useful.

3. Test it in every way you can

Test contracts thoroughly, and add test cases whenever new attack vectors are discovered.

Do manual testing, set up your own blockchain network, deploy the smart contract, then record the transactions and analyze the results with the smart contract logic. Repeat the same with any public test network.

Do testing using a framework, you should write test cases for all the functions in the smart contract, the test cases should be in alignment with the logic of the smart contract.

4. Use of dynamic variables

Variable types like string and bytes occupy a large memory. Instead of using these, fixed-size variables like bytes32, bytes30, etc, can lead to better gas optimization. As for strings, replacing them with bytes will be a reliable method.

5. Keep contracts deterministic

You should be able to look at your smart contract functions and understand how many statements need to be executed to run it. If you can’t make an estimate or it multiplies based on a control structure, you might have problems. The below code is a bad practice for a smart contract.

function findCharacter(string paragraph, bytes1 char) public returns (bool) {
    for (i=0; i<paragraph.length; i++) {
        if (paragraph[i]==char) {
            if (findCharacter(paragraph, "!")) {
                return true;
            }
        }
    }
}

6. When using arrays

Arrays in Solidity are not meant to hold a lot of information.

If you have designed your contract with an array that can grow over time then you may just break the contract. When your array becomes large enough, it will consume much gas over your gas limit (especially in the case of multi-dimensional arrays), and even if you increase that you may hit the block gas limit. If you find you are using arrays that way, consider using an off-chain database instead.

Avoid arrays with unknown iterations whenever possible. Always prefer fixed size over dynamic arrays. It is not possible to resize memory arrays. Keep in mind that dynamically sized memory arrays cannot be assigned to fixed-size memory arrays.

7. Memory caching

Instead of performing operations on state variables, do it on local ones and write to state once.

8. Always specify Solidity visibility modifiers

Public functions can be called by anyone (by functions from inside the contract, by functions from inherited contracts, or by outside users). External functions can be accessed by another contract.

private and internal: private means that the function can only be called from inside the contract, while internal proves a more relaxed restriction allowing contracts that inherit from the parent contract to use that function.

Even though, keep your functions private or internal unless there is a need for outside interaction.

The same can be said for the state variables, explicitly specifying the visibility of each state variable.

9. External calls

The best practice is to use external call if you expect that function will only be called externally, and public if you need to call the function internally. The difference is that in a public function, arguments are copied to memory, while in an external function, they are read directly from call data which is cheaper than memory allocation.

Internal calls are executed via jumps in the code and array arguments are passed internally by pointers to memory. The internal function expects its arguments to be located in memory when the compiler generates the code for it.

Avoid calling contract functions externally. Calls to untrusted contracts can lead to several unexpected risks or errors. External calls may execute malicious code in that contract or any other contract on which it depends.

10. Favor pull over push for external calls

External calls can fail accidentally or deliberately. To minimize the damage caused by such failures, it is often better to isolate each external call into its own transaction that can be initiated by the recipient of the call. This is especially relevant for payments, where it is better to let users withdraw funds rather than push funds to them automatically. (This also reduces the chance of problems with the gas limit.) Avoid combining multiple ether transfers in a single transaction.

11. Libraries

The libraries are a great way to achieve code reusability. It is a different type of contract, that doesn’t have any storage or event log and cannot hold ether (doesn’t allow payable functions and cannot have a fallback function).

A good practice is to use libraries in large contracts because they provide clean and smaller code. Avoid using them in small contracts because calling library functions is overhead, and it may be cheaper to implement the library functionality in your contract.

One of the most used libraries is SafeMath which is written by OpenZeppelin and is designed to support safe math operations that help prevent overflow. The libraries within the OpenZeppelin have gone through many iterations rectifying their error, so can be used as a reference.

12. Beware of rounding with integer division

All integer division rounds down to the nearest integer. If you need more precision, consider using a multiplier, or store both the numerator and denominator.

Hopefully, in the near future, Solidity will implement fixed-point types.

Using a multiplier prevents rounding down; this multiplier needs to be accounted for when working with x in the future:

uint multiplier = 10;
uint x = (5 * multiplier) / 2;

Storing the numerator and denominator means you can calculate the result of numerator/denominator off-chain:

uint numerator = 5;
uint denominator = 2;

13. Modifiers

Modifiers are a very important part of writing smart contracts. Use them if you want to ensure that certain conditions are met before proceeding to execute the rest of the code in the method. Modifiers can also receive arguments and allow you to write require() inside, which is a way of saying that if the condition is not true, you should throw an exception.

You can reuse the modifier as many times as you want, apply multiple modifiers to a single method, and call the same modifier more than once in a single method.

14. Access Restriction

Allow authorized parties only to access certain functions. We can use modifiers to achieve this result.

15. Trapped Ether in contract

If you are accepting payments to your contract you need to have a way to transfer that balance out of the contract. You can set your smart contract to receive payments (payable) but in that case, you should have functions that allow the owner to transfer the balance of the contract to themselves or others.

16. Use the Checks-Effects-Interactions Pattern

If you are making a call to an untrusted external contract, avoid state changes after the call. This pattern is also sometimes known as the checks-effects-interactions pattern.

Most functions will first perform some checks (who called the function, are the arguments in range, did they send enough Ether, does the person have tokens, etc.). These checks should be done first. As the second step, if all checks are passed, effects to the state variables of the current contract should be made. Interaction with other contracts should be the very last step in any function.

17. Use modifiers only for checks

The code inside a modifier is usually executed before the function body, so any state changes or external calls will violate the Checks-Effects-Interactions pattern. Moreover, these statements may also remain unnoticed by the developer, as the code for modifiers may be far from the function declaration.

18. Integer Overflow and Underflow

If an integer value reaches the maximum uint value (2^256) it will circle back to zero which checks for the condition. This may or may not be relevant, depending on the implementation. Think about whether or not the uint value has an opportunity to approach such a large number. Think about how the uint variable changes state, and who has the authority to make such changes. If any user can call functions that update the uint value, it's more vulnerable to attack. If only an admin has access to change the variable's state, you might be safe. If a user can increment by only 1 at a time, you are probably also safe because there is no feasible way to reach this limit.

The same is true for underflow. If a uint is made to be less than zero, it will cause underflow and get set to its maximum value.

Be careful with the smaller data-types like uint8, uint16, uint24...etc: they can even more easily hit their maximum value.

Note: Be aware there are around 20 cases of overflow and underflow.

Solved By Default from 0.8.0 versions onwards.

19. Lock pragmas to specific compiler version

Contracts should be deployed with the same compiler version and flags that they have been tested the most with. Locking the pragma helps ensure that contracts do not accidentally get deployed using, for example, the latest compiler which may have higher risks of undiscovered bugs. Contracts may also be deployed by others and the pragma indicates the compiler version intended by the original authors.

20. Use events to monitor contract activity

It can be useful to have a way to monitor the contract's activity after it is deployed. One way to accomplish this is to look at all the transactions of the contract. However, that may be insufficient, as message calls between contracts are not recorded on the blockchain. Moreover, it shows only the input parameters, not the actual changes being made to the state. So, in this situation, you can use events to flag certain activities that are happening in the smart contract. Also, events could be used to trigger functions in the user interface.

21. Use Fallback functions

Fallback functions are triggered when the function signature does not match any of the available functions in a Solidity contract. Keep fallback functions simple, either use revert() or just log an event.

22. Remember that Ether can be forcibly sent to an account

Beware of coding an invariant that strictly checks the balance of a contract.

An attacker can forcibly send ether to any account and this cannot be prevented (not even with a fallback function that does a revert()).

The attacker can do this by creating a contract, funding it with 1 Wei, and invoking selfdestruct(victimAddress). No code is invoked in victimAddress, so it cannot be prevented. This is also true for block reward which is sent to the address of the miner, which can be any arbitrary address.

Also, since contract addresses can be precomputed, ether can be sent to an address before the contract is deployed.

23. Be aware of the tradeoffs between abstract contracts and interfaces

Both interfaces and abstract contracts provide one with a customizable and reusable approach for smart contracts. Interfaces, which were introduced in Solidity 0.4.11, are similar to abstract contracts but cannot have any functions implemented. Interfaces also have limitations such as not being able to access storage or inherit from other interfaces which generally makes abstract contracts more practical.

Although, interfaces are certainly useful for designing contracts prior to implementation. Additionally, it is important to keep in mind that if a contract inherits from an abstract contract it must implement all non-implemented functions via overriding or it will be abstract as well.

24. Be aware that 'Built-ins' can be shadowed

It is currently possible to shadow built-in globals in Solidity. This allows contracts to override the functionality of built-ins such as msg and revert(). Although this is intended, it can mislead users of a contract as to the contract's true behavior.

contract PretendingToRevert {
    function revert() internal view {}
}

contract ExampleContract is PretendingToRevert {
    function somethingBad() public {
        revert();
    }
}

25. Be aware of blockchain properties

While much of your programming experience will be relevant to Ethereum programming, there are some pitfalls to be aware of.

  • Be extremely careful about external contract calls, which may execute malicious code and change control flow.
  • Understand that your public functions are public, and may be called maliciously and in any order. The private data in smart contracts is also viewable by anyone.
  • Keep gas costs and the block gas limit in mind.
  • Be aware that timestamps are imprecise on a blockchain, miners can influence the time of execution of a transaction within a margin of several seconds.
  • Randomness is non-trivial on the blockchain, most approaches to random number generation are gameable on a blockchain.
  • Always remember that anyone can interact with the blockchain directly without going through the system.
  • Always use standard libraries as is (for example OpenZeppelin). Do not modify the library code. Upgrade if a newer version is available.

26. Hybrid database/blockchain system

Until blockchain technology gets better, it does not make sense to use the blockchain as a database. You can store record indexes in a smart contract and use the database to keep all of the contextual information. And in case you are worried about data integrity, you may also store the hash of the data store in the database.

Example:

struct registration {
    uint studentID;
    uint courseID;
    uint sessionID;
}

The rest of the associated data can be stored in other storage mediums.

27. Some points for gas optimization

  • Prefer fixed-sized variables to dynamic-sized.
  • Make fewer external calls.
  • Delete storage variables that you don’t require.
  • Don’t initialize variables with the default value of the type. For example, the default value of uint is 0, no need to initialize it with 0.
  • Write reason strings with require but keep it short.
  • Use events to store data not required to persist.
  • Prefer mapping over arrays when appropriate.

28. Maximum size for smart contracts

To avoid the colossal size of a smart contract deployed into the Ethereum blockchain, a size restriction of 24 KB was introduced per contract address. Because large data size will affect the performance of the protocols in Ethereum, but in some cases you might want more space for writing your code, then you can split these into multiple contracts.

Clone this wiki locally