Key Takeaways
- Blockchain technology is still highly experimental and the security landscape is constantly changing. It’s important to develop with a security mindset and use secure design patterns like rate limiters, exit strategies or circuit breakers to protect your contract against unexpected events.
- Several security design patterns are recommended for smart contract development. These include the Checks-Effects-Interaction Pattern, Circuit Breaker, Rate Limit, Speed Bumps, and Balance Limit Pattern. Each of these patterns serves to protect the contract from different vulnerabilities and ensure smooth operation.
- The iOlite project offers a plugin for developers that turns any programming language into Solidity code. This can be a useful tool for designing secure contracts. However, the main problem these patterns solve is the lack of execution control once a contract has been deployed, which is both a drawback and an advantage for smart contracts. Therefore, thorough testing of each smart contract is crucial before deployment.
Blockchain technology is still a highly experimental technology. Things move fast and you should expect constant changes in the security landscape, as new bugs and security risks are discovered and new best practices are developed. Following the security practices described in this article will help you better understand how to design flawless Ethereum smart contracts.
Developing with a security mindset is extremely important as the cost of failure can be high and change can be difficult. A basic defence mechanism against known vulnerabilities is therefore not enough. It’s recommended to use secure design patterns like rate limiters, exit strategies or circuit breakers to protect your contract against unexpected events.
Prepare For Failure
Any non-trivial contract will have errors in it. Your code must, therefore, be able to respond to bugs and vulnerabilities gracefully. When writing code, bear in mind the following security design patterns.
Checks-Effects-Interaction Pattern
This is the general guideline when coding any smart contract as it describes how to build up a function. You start with validating all the arguments and throwing appropriate errors when the args do not apply with the expected input. Next, you can make changes to the smart contract’s state and finally interact with other smart contracts.
Interacting with other contracts should always be the last step in your function as this mostly includes handing over the control to another contract. So, when we are handing over the control, it’s crucial that the current contract has finished its functionality and does not depend on the execution of the other contract. Here’s an example that comes from an Auction contract to set the end of the auction using a boolean.
Circuit Breaker
A circuit breaker, also referred to as an emergency stop, is capable of stopping the execution of functions inside the smart contract. A circuit breaker can be triggered manually by trusted parties included in the contract like the contract admin or by using programmatic rules that automatically trigger the circuit breaker when the defined conditions are met. The most common usage of a circuit breaker is when a bug is discovered.
The above code example defines two modifiers. The first modifier makes sure that all actions in the contract are suspended when a bug is discovered – only the admin is able to toggle this boolean. However, we want to be able to withdraw funds in case of a severe bug; therefore, the second modifier is meant to allow you to withdraw funds.
Rate Limit
The problem we are trying to solve is a request rush on a certain function that can hinder the correct operational performance of a smart contract. The solution is quite simple: we regulate how often a task can be executed within a period of time. We can also limit the amount of Ether an owner can withdraw from a contract to prevent a rapid drainage of funds. Or another example is limiting the number of issued tokens over time at the contract level – every month the contract will distribute 10,000 tokens.
This piece of code shows a very basic rate limit modifier that accepts an amount of time to stop the contract from executing the function until the actual time is greater than the rate limit timer.
Speed Bumps – Delay Contract Actions
Speed bumps are extremely useful when malicious events occur as it gives a smart contract owner time to act accordingly. For example, The DAO had a 27 days delay installed in which no functions could be executed, leaving the funds in the contract, increasing the likelihood of recovery. However, the speed bump was pretty useless for The DAO case as no recovery actions were present. Therefore, a speed bump pattern should be used in combination with a circuit breaker that blocks the contract except the withdraw function. This ensures users are able to withdraw funds before the malicious action is fulfilled.
This speed bump Solidity contract delays a withdrawal request for 4 weeks.
Balance Limit Pattern
It is generally a good idea to manage the amount of money at risk when coding smart contracts. A balance limit pattern is a fairly easy pattern to implement, yet so strong. The pattern will monitor the amount of Ether in your contract and reject payments when it the balance exceeds the predefined maximum.
This piece of code uses a modifier to limit the balance inside the contract. The modifier is applied to the ‘deposit’ function as that’s where the money is being accepted.
iOlite Plugin for Designing Secure Contracts
The iOlite project is a crowd intelligence platform for allowing non-programmers to write secure smart contracts with natural language that they can use to build blockchain applications.
In addition, the iOlite team has developed a plugin for developers that turns any programming language into Solidity code (first among different blockchain languages) using structs attached to snippets of code defined by Solidity experts.
The plugin for Visual Studio Code can be found on the marketplace. The full documentation can be viewed on Github.
The Bottom Line
Ethereum allows Solidity smart contracts to run autonomously on the blockchain. However, this requires highly secure code which implements all of the above design patterns.
Actually, the main problem that these patterns solve is the lack of execution control once a contract has been deployed. It’s both a drawback and an advantage for smart contracts. If you are a developer, be careful when deploying a smart contract and test each smart contract thoroughly — a single mistake can cost you millions.
Frequently Asked Questions on Smart Contract Safety and Best Practices
What are the key principles to consider when designing a smart contract?
When designing a smart contract, it’s crucial to consider principles such as simplicity, modularity, and data isolation. Simplicity ensures that the contract is easy to understand and less prone to errors. Modularity involves breaking down the contract into smaller, manageable parts, which makes it easier to update and maintain. Data isolation is about ensuring that each contract has its own state and logic, which reduces the risk of data manipulation.
How can I ensure the security of my smart contract?
Ensuring the security of your smart contract involves practices such as regular auditing, using established design patterns, and avoiding common pitfalls. Regular audits help identify and fix vulnerabilities. Established design patterns have been tested and proven to work, reducing the risk of errors. Avoiding common pitfalls, such as reentrancy attacks and integer overflow, can also enhance the security of your contract.
What are some common smart contract vulnerabilities?
Some common smart contract vulnerabilities include reentrancy attacks, integer overflow, and underflow, and timestamp dependence. Reentrancy attacks occur when an attacker repeatedly calls a function before the first function call is finished. Integer overflow and underflow happen when a number exceeds the maximum or minimum limit that can be stored in a variable. Timestamp dependence refers to the reliance on block.timestamp for critical functionalities, which can be manipulated by miners.
What are some best practices for testing smart contracts?
Best practices for testing smart contracts include writing comprehensive test cases, using automated testing tools, and performing manual reviews. Comprehensive test cases cover all possible scenarios and edge cases. Automated testing tools can help identify vulnerabilities and errors. Manual reviews involve a thorough examination of the contract by a human, which can catch errors that automated tools might miss.
How can I prevent reentrancy attacks in my smart contract?
Preventing reentrancy attacks involves practices such as using the Checks-Effects-Interactions pattern, avoiding calls to unknown contracts, and using mutex. The Checks-Effects-Interactions pattern ensures that interactions with other contracts are the last thing to happen in a function. Avoiding calls to unknown contracts reduces the risk of malicious behavior. Mutex provides a locking mechanism to prevent reentrancy.
What is the role of gas in smart contracts?
Gas in smart contracts serves as a measure of computational effort. It is used to limit the amount of work that can be done in a single transaction, preventing infinite loops and other resource-draining exploits. Developers must optimize their contracts to use gas efficiently, as users pay for gas in Ether.
What are some common smart contract design patterns?
Some common smart contract design patterns include the Factory pattern, the Withdrawal pattern, and the Access Restriction pattern. The Factory pattern involves creating contracts from a contract. The Withdrawal pattern allows users to withdraw funds from a contract. The Access Restriction pattern restricts access to certain functions to specific addresses.
How can I optimize the gas usage of my smart contract?
Optimizing the gas usage of your smart contract involves practices such as using appropriate data types, avoiding unnecessary computations, and minimizing the use of storage. Using appropriate data types can reduce the amount of gas used. Avoiding unnecessary computations can also save gas. Minimizing the use of storage, which is the most expensive operation in terms of gas, can significantly reduce gas costs.
What is the importance of contract upgradability?
Contract upgradability is important because it allows for the modification of the contract’s logic after it has been deployed. This is crucial for fixing bugs, adding new features, and improving the contract over time. However, upgradability must be implemented carefully to avoid introducing new vulnerabilities.
How can I implement contract upgradability in a secure way?
Implementing contract upgradability in a secure way involves practices such as using a proxy contract, separating logic and data, and ensuring proper access control. A proxy contract can be used to delegate calls to the logic contract, allowing the logic to be updated. Separating logic and data ensures that data is not lost during an upgrade. Proper access control ensures that only authorized addresses can upgrade the contract.
Fullstack Blockchain Developer at TheLedger.be with a passion for the crypto atmosphere.