Best Practices for Ethereum Smart Contracts
An Ethereum smart contract is a type of account executed as a program with a code and data collection. It resides at a particular address on the Ethereum blockchain.
Functioning as a type of Ethereum account, smart contracts can hold a balance and send transactions over the network. However, it is noteworthy that they are deployed to the network instead of being controlled by a user. They run according to how they are programmed, and user accounts can interact with them by submitting transactions according to the specific smart contract functions.
Just like a regular contract, smart contracts define rules. However, the difference lies in its execution. Instead of just defining rules, smart contracts enforce them through code. Also, interactions with smart contracts are irreversible and they cannot be deleted by default.
Complex blockchain programs like Ethereum are highly experimental. There are constant changes and whenever new bugs or loopholes are discovered, new best practices are introduced. Hence, the security landscape is always changing and differs from one aspect to the other.
In this article, we will discuss all the best practices for Ethereum smart contracts in detail. Let’s begin by discussing the general best practices about the developers’ philosophies and approaches.
General Best Practices for Ethereum Smart Contracts
All kinds of best practices are important to ensure that your smart contract can defend itself against bugs and vulnerabilities when it comes to security. Some of these practices also depend on the kind of mindset and approaches that the developer has for securing the smart contract.
1. Be ready for failure.
All significant contracts are always prone to errors. Hence, you must be ready to deal with them and your contract must have the ability to respond to them. You can do so by:
- Pausing the contract or ‘breaking the circuit’ whenever things go wrong.
- Formulating an effective upgrade strategy with improvements and methods to fix bugs, loopholes, etc.
- Effectively managing the money at risk by limiting the maximum usage rate and managing the total amount well.
2. Ensure careful rollouts.
Careful rollouts can help you to detect and resolve bugs before the full production phase. It can be done by:
- Thoroughly testing contracts.
- Rolling out the contract in incremental phases with increased usage and testing in each phase.
- Providing bug bounties from as early as the alpha testnet releases.
- Adding tests at the discovery of every new attack vector.
3. Always keep the contracts simple.
If you make your contracts complex, you can expect more potential errors and bugs. Hence, keeping them simple is a sure shot way to reduce the chances of errors. You can keep contracts simple by implementing the following practices:
- You can make sure that the contract logic is simple.
- Wherever possible, use code or tools that you have already written before.
- You can modularize the code to make the contracts and functions small.
- Use blockchain only for those parts of your system that need decentralization.
- Wherever possible, give preference to clarity over performance.
4. Stay updated and keep track of new developments.
You must always stay up to date with any new security developments or changes. You can do so by:
- Regularly checking your contracts for new bugs and errors.
- Being open to adopting new security techniques.
- When using a tool or library, upgrading to its latest version as soon as possible.
5. Be attentive to blockchain properties.
Developers with enough programming experience can handle Ethereum programming conveniently. However, they must be aware of and attentive to certain pitfalls and blockchain properties by:
- Being careful about external contract calls as they can execute malicious code and tamper with the control flow.
- Keeping in mind that anyone can also view private data in smart contracts.
- Understanding that attackers can maliciously call public functions as they are public.
- Keeping in mind that on a blockchain, timestamps are imprecise and miners can alter or impact the time of a transaction’s execution in a margin of several seconds.
- Being aware of the block gas limits and gas costs.
- Being aware of approaches to random number generation on a blockchain is mostly gameable and non-trivial.
6. Consider fundamental trade-offs.
From the point of view of software engineering, an ideal smart contract system should be modular, support upgradeable components, and reuse code without duplicating it. However, from the security architecture’s standpoint, an ideal smart contract may or may not share the same approach. Hence, when assessing the security and structure of your smart contract system, you must find a balance between these trade-offs.
There can always be vital exceptions where your software engineering and security best practices won’t align; hence, finding a balance by making an optimal mix of the properties like duplication, reuse, modular, monolithic, upgradation and rigidity is crucial.
- Duplication and Reuse in Contracts
From the software engineering standpoint, reuse of contract code should be maximized where reasonable and should be done using proven previously deployed contracts you own. On the other hand, you should rely on duplication if previously self-owned contracts you have deployed aren’t available.
- Monolithic and Modular Contracts
Monolithic contracts keep all knowledge locally readable and identifiable, which is fine until it leads to the extreme locality of data and flow. It can cause trouble while optimizing the code review efficiency. Hence, the best practices from a security standpoint don’t align much with the software engineering standpoint for simple, short-lived and modular contracts. However, they align well in the case of complex contract systems.
- Rigid and Upgradeable Contracts
There is a fundamental trade-off between security and malleability in contracts. Malleable patterns make contracts complex and can also increase the risk of potential attacks. Hence, you must prefer simplicity over complexity if your smart contract performs limited functions for a pre-defined period.
Now that you know the general best practices developers can adopt in their philosophies and approaches let’s move on to Solidity best practices.
Solidity Best Practices for Ethereum Smart Contracts
Solidity is a programming language that is object-oriented and used to write smart contracts. Usually, solidity smart contracts are intended to run on Ethereum Virtual Machine (EVM). It is good to have a deep understanding of Solidity to write effective Ethereum smart contracts.
Let’s discuss some best practices that will help you to ensure that your Ethereum smart contracts written in Solidity are secure.
1. Enforce invariants withassert()
Whenever an assertation fails, an asset guard is triggered. For example, in a token issuance contract, the token to Ether issuance ratio may be fixed. You can ensure that this always happens with the help of assert().
However, note that asserts guards is combined with other techniques like allowing upgrades often so that you don’t end up being stuck with an assertion that is always failing.
2. Properly use assert(), require()
assert(), require() are convenience functions that can be used to look for conditions and throw an exception if the condition isn’t met. While the assert() function is used only to test for internal errors and verify variants, the require() function should validate return values from calls and ensure valid conditions.
3. Be aware of rounding with the integer division
Integer divisions always round down to the nearest integer. Hence, if you need more precision, you should use a multiplier or store both the denominator and the numerator.
4. Use modifiers only for checks
As the code inside a modifier is usually executed before the function body, it can violate the checks-effects-interactions pattern in case of any external calls or state changes. Also, as the code for modifier can be far from the function declaration, it can go unnoticed by developers. Hence, it is useful to use modifiers to replace the duplicate condition checks in multiple functions. Developers can also use require() or revert() inside the function. This practice can improve your smart contract code in terms of readability and make it easier to audit.
5. Be attentive to the trade-offs between abstract contracts and interfaces.
Both interfaces and abstract contracts provide a customizable and reusable approach.
Although interfaces are useful for designing contracts before implementation, they have limitations in storage access or inheritance from other interfaces. They also cannot have any functions implemented, which makes abstract contracts more practical than them. So, if a contract inherits from an abstract contract, it must implement all the non-implemented functions; otherwise, it will become abstract as well.
6. Keep all fallback functions simple.
Fallback functions are called when:
- A contract has access to only 2300 gas when called from .transfer() or .send()
- A contract is sent a message with no arguments or no matching functions
You can log an event in a fallback function to receive Ether from a .transfer() or .send().
7. Check data length in fallback functions.
You should always check the data length of fallback functions as, in addition to plain Ether transfers, they are also used when no other function matches. So, if the fallback function is intended to be used only for logging the received Ether, you must check that the data is empty; otherwise, it will become noticeable to the callers that your contract is used incorrectly.
8. Explicitly mark:
- Payable functions and state variables.
A payable modifier must be used by every function receiving Ether, starting from Solidity 0.4.0. It is also important to remember that the payable modifier is only applicable to calls from external contracts.
- Visibility in functions and state variables.
Functions can be external, internal, public or private. While external functions are a part of the contract interface, one can only access internal functions and state variables internally. On the other hand, public functions are a part of the contract interface but can be called internally or via messages. But, visibility of private functions and state variables is only there for the contract they are defined.
Explicitly labeling their visibility can make it simpler to detect incorrect assumptions about the variable’s accessibility or who can call the function.
9. Lock pragmas to specific compiler versions.
Pragma indicates the compiler version that the original authors intended. It is important to ensure that contracts are deployed with the compiler version and flags with which they have been tested the most. Locking the pragma lets you ensure that they don’t get deployed using a different compiler with higher risks of undetected bugs accidentally.
10. Use events to monitor contract activity.
Having a way to monitor your contract’s activity after deployment is useful. Looking at the contract’s transactions can be insufficient as message calls aren’t recorded on the blockchain.
An event is a way to log things that happen in a contract. Also, events can be used to trigger functions in the user interface and can be used to track the history of the contract’s transactions. All transactions that go directly or indirectly through a contract with events will eventually show up in the events list of that contract, along with the amount of money as well. Emitted events stay in the blockchain with other contract data and are always available for future audits.
11. Know that ‘Built-ins’ can be shadowed.
Shadowing ‘built-ins’ enables contracts to override their functionalities, which can mislead users of a contract. Hence, auditors and contract users should be aware of the entire smart contract source code of the application they intend to use.
12. Avoid using tx.origin
You should never use tx.origin for authorization as another contract with funds in your contract can call it via some method. Your contract will end up authorizing the transaction because your address at present is the tx.origin. It also limits interoperability. Instead, use msg.sender for authorization.
13. Timestamp Dependence
When using a timestamp for executing a critical function in a contract, there are three main considerations that you must take into account:
- Timestamp Manipulation
As we discussed earlier in this article, the timestamp of a block can be manipulated or influenced by a miner within the margin of a few seconds. Timestamps are not random; hence, you should avoid using them in that context because when timestamp is used by a contract to seed a random number, miners get a 15-second window before the block is validated, where they can precompute and post a timestamp more favorable to them.
- The 15-second Rule
Parity and Geth, widely known Ethereum protocol implementations, reject blocks with a timestamp of more than 15 seconds. However, if your time-independent event’s scale can maintain integrity while varying by 15 seconds, you can use block.timestamp safely.
While evaluating timestamp usage, it is useful to avoid using block.number as a timestamp.
- Avoid using block.number as a timestamp.
Estimating time delta using the block.number property is possible but not future proof as block times may change due to fork reorganizations.
Now that you know Solidity’s best practices for Ethereum smart contracts let’s move on to token implementation best practices.
Automate your business processes with Ethereum-powered smart contracts
LeewayHertz Ethereum Smart Contract Development
Token Implementation Best Practices for Ethereum Smart Contracts
For token implementation, not only should you comply with other best practices, but you must also be aware of some considerations and take them into account.
1. Comply with the latest standards.
Tokens’ smart contract should always follow and comply with an accepted and stable standard. For Ethereum, the currently accepted standards are:
- EIP721 (for non-fungible tokens)
2. Be aware of front-running attacks on EIP-20.
The potential for an approved spender to spend more than the intended amount can be created by the EIP-20 token’s approve()function. As a result, a front running attack can be used that enables the approved spender to call transferFrom() both before and after the approve() call is processed.
3. Prevent transferring tokens to the:
- 0x0 address
The “zero” address holds tokens of more than $80 million value at writing. Hence, you should avoid transferring tokens to the 0x0 address.
The “zero” address = 0x0000000000000000000000000000000000000000
- Contract address
You should also avoid transferring tokens to the same address as the smart contract because it can lead to massive potential losses as tokens get stuck at the contract address.
In the following example, you can see how you can implement both the above recommendations to create the following modifier while validating that the “to” address is neither the smart contract’s own address nor 0x0:
The modifier should then be applied to the “transfer” and “transferFrom” methods:
Now that you know the token implementation best practices let’s discuss documentation and procedures best practices for Ethereum smart contracts.
Documentation and Procedures Best Practices for Ethereum Smart Contracts
Creating proper documentation and following proper procedures is important when you’re launching a smart contract with substantial funds or a critical purpose.
1. Specifications and Rollout Plans
You should create documentation related to models, state machines, specs and diagrams that can help understand the purpose and intention of the system for auditors, the community and reviewers.
These documentations along with rollout plans and target dates can also end up revealing several bugs which can be fixed easily in a cost-effective manner.
Create documentations that include information about:
- compiler versions
- steps for verifying that the source code and the deployed bytecode match
- the flags used
- flags and compiler versions for different phases of the rollout process.
- where the current code is deployed
- the present status of the deployed code with performance statistics, outstanding issues, etc.
3. Known Issues
Create documentation of all issues that you know, such as:
- Known bugs
- Potential conflicts of interest
You should formulate a detailed document with the entire history of the smart contract, including:
- the testing phases it has gone through
- its usage stats
- length of testing
- discovered bugs
- information about people who have reviewed the code
- Reviewers’ feedback, etc.
You should have clear documentation about the following procedures to avoid all conflicts and confusion in the future.
- Wind down procedures in case of emergencies
- Action plans for when bugs are discovered
- The course of action in case of failure, including penalty funds and insurance details
- Detailed responsible disclosure policy, including the rules of bug bounty programs (if any) and responsible authorities for reporting bugs, etc.
6. Contact Information
All contact information, including the programmers, important stakeholders, and people to contact in case of issues or assistance, should be mentioned clearly in your official documentation.
Now that you know the documentation and procedures best practices let’s discuss security tools best practices for Ethereum smart contracts.
Security Tools Best Practices for Ethereum Smart Contracts
Choosing and implementing highly efficient security tools is a great way to ensure that your smart contract is secure and functioning well. Here are a few great tools with different functionalities that you can leverage for Ethereum smart contracts.
- Surya – offers several visual outputs and information about the smart contract’s structure and supports querying the function call graph.
- EVM Lab – A tool package that helps you interact with the Ethereum virtual machine (EVM).
- Solidity Visual Auditor – An extension that contributes a security-centric syntax with advanced Solidity code insights and semantic highlighting.
- Ethereum-graph-debugger – An EVM debugger that is graphical and displays the entire program control flow graph.
2. Static and Dynamic Analysis
Here are a few exceptional tools for static and dynamic analysis:
- Mythril – A useful and multipurpose tool for smart contract security.
- Slither – A static analysis framework that can detect common Solidity issues.
- Contract-Library – A security analysis tool and a decompiler for all deployed contracts
- MythX – A cloud service that uses symbolic analysis and input fuzzing to identify common security bugs and verify the smart contract’s code’s correctness.
- Manticore – A dynamic binary analysis tool that also offers EVM support.
- Echidna – The only fuzzer for Ethereum software that uses property testing to discover malicious inputs that can break smart contracts.
- Oyente – A tool for Ethereum code analysis to find common vulnerabilities
- Securify – A completely automated online static analyzer for smart contracts.
- Octopus – A security analysis tool for blockchain smart contracts. It supports EVMs as well.
- Vertigo – An effective tool for mutation testing of Ethereum smart contracts.
3. Test Coverage
Soliditycoverage is probably the most effective code coverage for Solidity testing.
4. Linters and Formatters
Linters and Formatters enhance code quality by implementing code composition and style rules, making it easier to read and review. Some effective Linters and Formatting tools are:
- Prettier with the Solidity plugin
Ethereum is a leading blockchain platform that also allows developers to build smart contracts. While several platforms allow smart contract development nowadays, Ethereum is easily one of the most popular ones.
Being a blockchain platform, it is always prone to quick changes and upgrades. As a result, developers have to constantly check on their smart contracts and keep upgrading them along with the platform to reduce security threats and failures.
As we have discussed in this article, there are several best practices that developers and teams can undertake to ensure high quality, highly efficient and highly secure smart contracts. Paying close attention to all general, solidity’s, documentation’s, security tools’ and token implementation’s best practices is crucial to develop Ethereum smart contracts successfully.
However, it is also noteworthy that developing smart contracts and maintaining them is not an easy task. It requires a lot of effort, careful work and expert supervision for seamless proceedings. Hence, it is always helpful to partner with an Ethereum development team or firm that can help you with all the complexities of developing Ethereum smart contracts.
LeewayHertz is one of the leading software development companies in the blockchain space. With highly experienced developers, our team of experts can help you with all kinds of Ethereum services that you require. If you are looking for a blockchain development partner for Ethereum smart contract development, please feel free to contact our experts.
Start a conversation by filling the form
All information will be kept confidential.
ERC20 tokens, executed as smart contracts on the Ethereum Virtual Machine , define rules of interaction and purchase with other tokens.
ERC20, ERC721, and ERC1155 are the prevalent token standards as approved by the Ethereum community.
With the help of NFT marketplace developed on Ethereum blockchain, you can leverage open transaction history for ownership verification.