Security
Ethereum smart contracts are extremely flexible, capable of both holding large quantities of tokens (often in excess of $1B) and running immutable logic based on previously deployed smart contract code. While this has created a vibrant and creative ecosystem of trustless, interconnected smart contracts, it is also the perfect ecosystem to attract attackers looking to profit by exploiting vulnerabilities in smart contracts and unexpected behavior in Ethereum. Smart contract code usually cannot be changed to patch security flaws, assets that have been stolen from smart contracts are irrecoverable, and stolen assets are extremely difficult to track. The total of amount of value stolen or lost due to smart contract issues is easily in the $1B. Some of the larger due to smart contract coding errors include:
- Parity multi-sig issue #1 - $30M lost
- Parity multi-sig issue #2 - $300M locked
- TheDAO hack, 3.6M ETH! Over $1B in today's ETH prices
Prerequisites
This will cover smart contract security so make sure you're familiar with smart contracts before tackling security.
How to write more secure Smart Contract code
Before launching any code to Mainnet, it is important to take sufficient precaution to protect anything of value your smart contract is entrusted with. In this article, we will discuss a few specific attacks, provide resources to learn about more attack types, and leave you with some basic tooling and best practices to ensure your contracts function correctly and securely.
Audits are not a silver bullet
Years prior, the tooling for writing, compiling, testing, and deploying smart contracts was very immature, leading many projects to write Solidity code in haphazard ways, throw it over a wall to an auditor who would investigate the code to ensure it functions securely and as expected. In 2020, the development processes and tooling that support writing Solidity is significantly better; leveraging these best practices not only ensures your project is easier to manage, it is a vital part of your project's security. An audit at the end of writing your smart contract is no longer sufficient as the only security consideration your project makes. Security starts before you write your first line of smart contract code, security starts with proper design and development processes.
Smart Contract Development Process
At a minimum:
- All code stored in a version control system, such as git
- All code modifications made via Pull Requests
- All Pull Requests have at least one reviewer. If you are a solo project, consider finding another solo author and trade code reviews!
- A single command compiles, deploys, and runs a suite of tests against your code using a development Ethereum environment (See: Truffle)
- You have run your code through basic code analysis tools such as Mythril and Slither, ideally before each pull request is merged, comparing differences in output
- Solidity does not emit ANY compiler warnings
- Your code is well-documented
There is much more to be said for development process, but these items are a good place to start. For more items and detailed explanations, see the process quality checklist provided by DeFiSafety. DefiSafety is an unofficial public service publishing reviews of various large, public Ethereum dApps. Part of the DeFiSafety rating system includes how well the project adheres to this process quality checklist. By following these processes:
- You will produce more secure code, via reproducible, automated tests
- Auditors will be able to review your project more effectively
- Easier onboarding of new developers
- Allows developers to quickly iterate, test, and get feedback on modifications
- Less likely your project experiences regressions
Attacks and vulnerabilities
Now that you are writing Solidity code using an efficient development process, let's look at some common Solidity vulnerabilities to see what can go wrong.
Re-entrancy
Re-entrancy is one of the largest and most significant security issue to consider when developing Smart Contracts. While the EVM cannot run multiple contracts at the same time, a contract calling a different contract pauses the calling contract's execution and memory state until the call returns, at which point execution proceeds normally. This pausing and re-starting can create a vulnerability known as "re-entrancy".
Here is a simple version of a contract that is vulnerable to re-entrancy:
1// THIS CONTRACT HAS INTENTIONAL VULNERABILITY, DO NOT COPY2contract Victim {3 mapping (address => uint256) public balances;45 function deposit() external payable {6 balances[msg.sender] += msg.value;7 }89 function withdraw() external {10 uint256 amount = balances[msg.sender];11 (bool success, ) = msg.sender.call.value(amount)("");12 require(success);13 balances[msg.sender] = 0;14 }15}16Prikaži sveKopiraj
To allow a user to withdraw ETH they have previously stored on the contract, this function
- Reads how much balance a user has
- Sends them that balance amount in ETH
- Resets their balance to 0, so they cannot withdraw their balance again.
If called from a regular account (such as your own Metamask account), this functions as expected: msg.sender.call.value() simply sends your account ETH. However, smart contracts can make calls as well. If a custom, malicious contract is the one calling withdraw()
, msg.sender.call.value() will not only send amount
of ETH, it will also implicitly call the contract to begin executing code. Imagine this malicious contract:
1contract Attacker {2 function beginAttack() external payable {3 Victim(VICTIM_ADDRESS).deposit.value(1 ether)();4 Victim(VICTIM_ADDRESS).withdraw();5 }67 function() external payable {8 if (gasleft() > 40000) {9 Victim(VICTIM_ADDRESS).withdraw();10 }11 }12}13Prikaži sveKopiraj
Calling Attacker.beginAttack() will start a cycle that looks something like:
10.) Attacker's EOA calls Attacker.beginAttack() with 1 ETH20.) Attacker.beginAttack() deposits 1 ETH into Victim34 1.) Attacker -> Victim.withdraw()5 1.) Victim reads balances[msg.sender]6 1.) Victim sends ETH to Attacker (which executes default function)7 2.) Attacker -> Victim.withdraw()8 2.) Victim reads balances[msg.sender]9 2.) Victim sends ETH to Attacker (which executes default function)10 3.) Attacker -> Victim.withdraw()11 3.) Victim reads balances[msg.sender]12 3.) Victim sends ETH to Attacker (which executes default function)13 4.) Attacker no longer has enough gas, returns without calling again14 3.) balances[msg.sender] = 0;15 2.) balances[msg.sender] = 0; (it was already 0)16 1.) balances[msg.sender] = 0; (it was already 0)17Prikaži sve
Calling Attacker.beginAttack with 1 ETH will re-entrancy attack Victim, withdrawing more ETH than it provided (taken from other users' balances, causing the Victim contract to become under-collateralized)
How to deal with re-entrancy (the wrong way)
One might consider defeating re-entrancy by simply preventing any smart contracts from interacting with your code. You search stackoverflow, you find this snippet of code with tons of upvotes:
1function isContract(address addr) internal returns (bool) {2 uint size;3 assembly { size := extcodesize(addr) }4 return size > 0;5}6Kopiraj
Seems to make sense: contracts have code, if the caller has any code, don't allow it to deposit. Let's add it:
1// THIS CONTRACT HAS INTENTIONAL VULNERABILITY, DO NOT COPY2contract ContractCheckVictim {3 mapping (address => uint256) public balances;45 function isContract(address addr) internal returns (bool) {6 uint size;7 assembly { size := extcodesize(addr) }8 return size > 0;9 }1011 function deposit() external payable {12 require(!isContract(msg.sender)); // <- NEW LINE13 balances[msg.sender] += msg.value;14 }1516 function withdraw() external {17 uint256 amount = balances[msg.sender];18 (bool success, ) = msg.sender.call.value(amount)("");19 require(success);20 balances[msg.sender] = 0;21 }22}23Prikaži sveKopiraj
Now in order to deposit ETH, you must not have smart contract code at your address. However, this is easily defeated with the following Attacker contract:
1contract ContractCheckAttacker {2 constructor() public payable {3 ContractCheckVictim(VICTIM_ADDRESS).deposit(1 ether); // <- New line4 }56 function beginAttack() external payable {7 ContractCheckVictim(VICTIM_ADDRESS).withdraw();8 }910 function() external payable {11 if (gasleft() > 40000) {12 Victim(VICTIM_ADDRESS).withdraw();13 }14 }15}16Prikaži sveKopiraj
Whereas the first attack was an attack on contract logic, this is an attack on Ethereum contract deployment behavior. During construction, a contract has not yet returned its code to be deployed at its address, but retains full EVM control DURING this process.
It is technically possible to prevent smart contracts from calling your code, using this line:
1require(tx.origin == msg.sender)2Kopiraj
However, this is still not a good solution. One of the most exciting aspects of Ethereum is its composability, smart contracts integrate with and building on each other. By using the line above, you are limiting the usefulness of your project.
How to deal with re-entrancy (the right way)
By simply switching the order of the storage update and external call, we prevent the re-entrancy condition that enabled the attack. Calling back into withdraw, while possible, will not benefit the attacker, since the balances
storage will already be set to 0.
1contract NoLongerAVictim {2 function withdraw() external {3 uint256 amount = balances[msg.sender];4 balances[msg.sender] = 0;5 (bool success, ) = msg.sender.call.value(amount)("");6 require(success);7 }8}9Kopiraj
The code above follows the "Checks-Effects-Interactions" design pattern, which helps protect against re-entrancy. You can read more about Checks-Effects-Interactions here
How to deal with re-entrancy (the nuclear option)
Any time you are sending ETH to an untrusted address or interacting with an unknown contract (such as calling transfer()
of a user-provided token address), you open yourself up to the possibility of re-entrancy. By designing contracts that neither send ETH nor call untrusted contracts, you prevent the possibility of re-entrancy!
More attack types
The attack types above cover smart-contract coding issues (re-entrancy) and Ethereum oddities (running code inside contract constructors, before code is available at the contract address). There are many, many more attack types to be aware of, such as:
- Front-running
- ETH send rejection
- Integer overflow/underflow
Further reading:
- Consensys Smart Contract Known Attacks - A very readable explanation of the most significant vulnerabilities, with sample code for most.
- SWC Registry - Curated list of CWE's that apply to Ethereum and smart contracts
Security tools
While there is no substitute for understanding Ethereum security basics and engaging a professional auditing firm to review your code, there are many tools available to help highlight potential issues in your code.
Smart Contract Security
Slither - Solidity static analysis framework written in Python 3.
MythX - Security analysis API for Ethereum smart contracts.
Mythril - Security analysis tool for EVM bytecode.
SmartContract.Codes - Search engine for verified solidity source codes.
Manticore - A command line interface that uses a symbolic execution tool on smart contracts and binaries.
Securify - Security scanner for Ethereum smart contracts.
ERC20 Verifier - A verification tool used to check if a contract complies with the ERC20 standard.
Formal Verification
Information on Formal Verification
- How formal verification of smart-contacts works July 20, 2018 - Brian Marick
- How Formal Verification Can Ensure Flawless Smart Contracts Jan 29, 2018 - Bernard Mueller
Using tools
Two of the most popular tools for smart contract security analysis are:
Both are useful tools that analyze your code and report issues. Each has a [commercial] hosted version, but are also available for free to run locally. The following is a quick example of how to run Slither, which is made available in a convenient Docker image trailofbits/eth-security-toolbox
. You will need to install Docker if you don't already have it installed.
$ mkdir test-slither$ curl https://gist.githubusercontent.com/epheph/460e6ff4f02c4ac582794a41e1f103bf/raw/9e761af793d4414c39370f063a46a3f71686b579/gistfile1.txt > bad-contract.sol$ docker run -v `pwd`:/share -it --rm trailofbits/eth-security-toolboxdocker$ cd /sharedocker$ solc-select 0.5.11docker$ slither bad-contract.sol
Will generate this output:
ethsec@1435b241ca60:/share$ slither bad-contract.solINFO:Detectors:Reentrancy in Victim.withdraw() (bad-contract.sol#11-16):External calls:- (success) = msg.sender.call.value(amount)() (bad-contract.sol#13)State variables written after the call(s):- balances[msg.sender] = 0 (bad-contract.sol#15)Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilitiesINFO:Detectors:Low level call in Victim.withdraw() (bad-contract.sol#11-16):- (success) = msg.sender.call.value(amount)() (bad-contract.sol#13)Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#low-level-callsINFO:Slither:bad-contract.sol analyzed (1 contracts with 46 detectors), 2 result(s) foundINFO:Slither:Use https://crytic.io/ to get access to additional detectors and Github integrationPrikaži sve
Slither has identified the potential for re-entrancy here, identifying the key lines where the issue might occur and giving us a link for more details about the issue:
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities
allowing you to quickly learn about potential problems with your code. Like all automated testing tools, Slither is not perfect, and it errs on the side of reporting too much. It can warn about a potential re-entrancy, even when no exploitable vulnerability exists. Often, reviewing the DIFFERENCE in Slither output between code changes is extremely illuminating, helping discover vulnerabilities that were introduced much earlier than waiting until your project is code-complete.
Further reading
Smart Contract Security Best Practices Guide
- consensys.github.io/smart-contract-best-practices/
- GitHub
- Aggregated collection of security recommendations and best practices
Smart Contract Security Verification Standard (SCSVS)
Know of a community resource that helped you? Edit this page and add it!
Related tutorials
- Secure development workflow
- How to use Slither to find smart contract bugs
- How to use Manticore to find smart contract bugs
- Security guidelines
- Token security