Smart contract security
Ostatnia edycja: , Invalid DateTime
Smart contracts are extremely flexible, and capable of controlling large amounts of value and data, while running immutable logic based on code deployed on the blockchain. This has created a vibrant ecosystem of trustless and decentralized applications that provide many advantages over legacy systems. They also represent opportunities for attackers looking to profit by exploiting vulnerabilities in smart contracts.
Public blockchains, like Ethereum, further complicate the issue of securing smart contracts. Deployed contract code usually cannot be changed to patch security flaws, while assets stolen from smart contracts are extremely difficult to track and mostly irrecoverable due to immutability.
Although figures vary, it is estimated that the total amount of value stolen or lost due to security defects in smart contracts is easily over $1 billion. This includes high-profile incidents, such as the DAO hack(opens in a new tab) (3.6M ETH stolen, worth over $1B in today’s prices), Parity multi-sig wallet hack(opens in a new tab) ($30M lost to hackers), and the Parity frozen wallet issue(opens in a new tab) (over $300M in ETH locked forever).
The aforementioned issues make it imperative for developers to invest effort in building secure, robust, and resilient smart contracts. Smart contract security is serious business, and one that every developer will do well to learn. This guide will cover security considerations for Ethereum developers and explore resources for improving smart contract security.
Prerequisites
Make sure you’re familiar with the fundamentals of smart contract development before tackling security.
Guidelines for building secure Ethereum smart contracts
1. Design proper access controls
In smart contracts, functions marked public
or external
can be called by any externally owned accounts (EOAs) or contract accounts. Specifying public visibility for functions is necessary if you want others to interact with your contract. Functions marked private
however can only be called by functions within the smart contract, and not external accounts. Giving every network participant access to contract functions can cause problems, especially if it means anyone can perform sensitive operations (e.g., minting new tokens).
To prevent unauthorized use of smart contract functions, it is necessary to implement secure access controls. Access control mechanisms restrict the ability to use certain functions in a smart contract to approved entities, such as accounts responsible for managing the contract. The Ownable pattern and role-based control are two patterns useful for implementing access control in smart contracts:
Ownable pattern
In the Ownable pattern, an address is set as the “owner” of the contract during the contract-creation process. Protected functions are assigned an OnlyOwner
modifier, which ensures the contract authenticates the identity of the calling address before executing the function. Calls to protected functions from other addresses aside from the contract owner always revert, preventing unwanted access.
Role-based access control
Registering a single address as Owner
in a smart contract introduces the risk of centralization and represents a single point-of-failure. If the owner’s account keys are compromised, attackers can attack the owned contract. This is why using a role-based access control pattern with multiple administrative accounts may be a better option.
In role-based access control, access to sensitive functions is distributed between a set of trusted participants. For instance, one account may be responsible for minting tokens, while another account performs upgrades or pauses the contract. Decentralizing access control this way eliminates single points of failure and reduces trust assumptions for users.
Using multi-signature wallets
Another approach for implementing secure access control is using a multi-signature account to manage a contract. Unlike a regular EOA, multi-signature accounts are owned by multiple entities and require signatures from a minimum number of accounts—say 3-of-5—to execute transactions.
Using a multisig for access control introduces an extra layer of security since actions on the target contract require consent from multiple parties. This is particularly useful if using the Ownable pattern is necessary, as it makes it more difficult for an attacker or rogue insider to manipulate sensitive contract functions for malicious purposes.
2. Use require(), assert(), and revert() statements to guard contract operations
As mentioned, anyone can call public functions in your smart contract once it is deployed on the blockchain. Since you cannot know in advance how external accounts will interact with a contract, it is ideal to implement internal safeguards against problematic operations before deploying. You can enforce correct behavior in smart contracts by using the require()
, assert()
, and revert()
statements to trigger exceptions and revert state changes if execution fails to satisfy certain requirements.
require()
: require
are defined at the start of functions and ensures predefined conditions are met before the called function is executed. A require
statement can be used to validate user inputs, check state variables, or authenticate the identity of the calling account before progressing with a function.
assert()
: assert()
is used to detect internal errors and check for violations of “invariants” in your code. An invariant is a logical assertion about a contract’s state that should hold true for all function executions. An example invariant is the maximum total supply or balance of a token contract. Using assert()
ensures that your contract never reaches a vulnerable state, and if it does, all changes to state variables are rolled back.
revert()
: revert()
can be used in an if-else statement that triggers an exception if the required condition is not satisfied. The sample contract below uses revert()
to guard the execution of functions:
1pragma solidity ^0.8.4;23contract VendingMachine {4 address owner;5 error Unauthorized();6 function buy(uint amount) public payable {7 if (amount > msg.value / 2 ether)8 revert("Not enough Ether provided.");9 // Perform the purchase.10 }11 function withdraw() public {12 if (msg.sender != owner)13 revert Unauthorized();1415 payable(msg.sender).transfer(address(this).balance);16 }17}18Pokaż wszystko
3. Test smart contracts and verify code correctness
The immutability of code running in the Ethereum Virtual Machine means smart contracts demand a higher level of quality assessment during the development phase. Testing your contract extensively and observing it for any unexpected results will improve security a great deal and protect your users in the long run.
The usual method is to write small unit tests using mock data that the contract is expected to receive from users. Unit testing is good for testing the functionality of certain functions and ensuring a smart contract works as expected.
Unfortunately, unit testing is minimally effective for improving smart contract security when used in isolation. A unit test might prove a function executes properly for mock data, but unit tests are only as effective as the tests that are written. This makes it difficult to detect missed edge cases and vulnerabilities that could break the safety of your smart contract.
A better approach is to combine unit testing with property-based testing performed using static and dynamic analysis(opens in a new tab). Static analysis relies on low-level representations, such as control flow graphs(opens in a new tab) and abstract syntax trees(opens in a new tab) to analyze reachable program states and execution paths. Meanwhile, dynamic analysis techniques, such as fuzzing, execute contract code with random input values to detect operations that violate security properties.
Formal verification is another technique for verifying security properties in smart contracts. Unlike regular testing, formal verification can conclusively prove the absence of errors in a smart contract. This is achieved by creating a formal specification that captures desired security properties and proving that a formal model of the contracts adheres to this specification.
4. Ask for an independent review of your code
After testing your contract, it is good to ask others to check the source code for any security issues. Testing will not uncover every flaw in a smart contract, but getting an independent review increases the possibility of spotting vulnerabilities.
Audits
Commissioning a smart contract audit is one way of conducting an independent code review. Auditors play an important role in ensuring that smart contracts are secure and free from quality defects and design errors.
That said, you should avoid treating audits as a silver bullet. Smart contract audits won't catch every bug and are mostly designed to provide an additional round of reviews, which can help detect issues missed by developers during initial development and testing. You should also follow best practices for working with auditors(opens in a new tab), such as documenting code properly and adding inline comments, to maximize the benefit of a smart contract audit.
Bug bounties
Setting up a bug bounty program is another approach for implementing external code reviews. A bug bounty is a financial reward given to individuals (usually whitehat hackers) that discover vulnerabilities in an application.
When used properly, bug bounties give members of the hacker community incentive to inspect your code for critical flaws. A real-life example is the “infinite money bug” that would have let an attacker create an unlimited amount of Ether on Optimism(opens in a new tab), a Layer 2(opens in a new tab) protocol running on Ethereum. Fortunately, a whitehat hacker discovered the flaw(opens in a new tab) and notified the team, earning a large payout in the process(opens in a new tab).
A useful strategy is to set the payout of a bug bounty program in proportion to the amount of funds at stake. Described as the “scaling bug bounty(opens in a new tab)”, this approach provides financial incentives for individuals to responsibly disclose vulnerabilities instead of exploiting them.
5. Follow best practices during smart contract development
The existence of audits and bug bounties doesn’t excuse your responsibility to write high-quality code. Good smart contract security starts with following proper design and development processes:
Store all code in a version control system, such as git
Make all code modifications via pull requests
Ensure pull requests have at least one independent reviewer—if you are working solo on a project, consider finding other developers and trade code reviews
Use a development environment for testing, compiling, deploying smart contracts
Run your code through basic code analysis tools, such as Mythril and Slither. Ideally, you should do this before each pull request is merged and compare differences in output
Ensure your code compiles without errors, and the Solidity compiler emits no warnings
Properly document your code (using NatSpec(opens in a new tab)