-
Notifications
You must be signed in to change notification settings - Fork 134
List of Security Vulnerabilities
Table of Contents
- Integer Arithmetic
- Floating Point Arithmetic
- Reentrancy
- Access Control
- Code Injection via delegatecall
- Signature Replay Attacks
- Unchecked External Calls
- DOS
- Entropy Illusion
- Privacy Illusion
- Miner Attacks
- Unexpected Ether
- External Contract Referencing
- Uninitialized Storage Pointers
- Writes to Arbitrary Storage Locations
- Incorrect Interface
- Arbitrary Jumps with Function Variables
- Variable Shadowing
- Assert Violation
- Dirty Higher Order Bits
- Complex Modifiers
- Outdated Compiler
- Use of Deprecated Solidity Functions
- Function Selector Abuse
- Experimental Language Features
- Constructor call
- Frontend (Off Chain) Attacks
- Historic Attacks
- Payable Multicall
- References
This page contains a comprehensive list of common smart contract security vulnerabilities, compiled from various sources. We use it as our reference list for security audits. In this page we only include basic information. Follow the links given in each section for more information.
Neither the EVM nor Solidity (before v0.8) provide builtin error reporting for arithmetic overflow/underflow. Consequently, applications need to check for these cases themselves. Furthermore, one cannot make the (seemingly reasonable) assumption that x != -x
, because of this case.
Note that since Solidity version 0.8, arithmetic operations revert on overflow and underflow. The developer can choose to bypass these checks by using the unchecked
keyword, for example with unchecked { x = a + b; }
- Consensys Best Practices: Integer Overflow and Underflow
- DASP: Arithmetic Issues
- Sigmaprime: Arithmetic Over/Under Flows
- Solidity Documentation: Two's Complement / Underflows / Overflows
- Solidity Documentation: Checked or Unchecked Arithmetic
- SWC Registry: Integer Overflow and Underflow
- Trail of Bits: Integer Overflow
Fixed point numbers are not yet fully supported by solidity. User implementations may contain errors.
When a contract calls an external function, that external function may itself call the calling function. This can have unexpected effects. To prevent this sort of attack, a contract can implement a lock in storage that prevents re-entrant calls.
- Consensys Best Practices: Reentrancy
- DASP: Reentrancy
- Sigmaprime: Reentrancy
- Solidity Documentation: Reentrancy
- SWC Registry: Reentrancy
- Trail of Bits: Reentrancy
There are a number of common mistakes relating to access control.
The default visibility for Solidity functions is public
. Developers may forget to specify the visibility for a function that is intended to be private, leading to possible vulnerabilities.
- SWC Registry: State Variable Default Visibility
- SWC Registry: Function Default Visibility
- Sigmaprime: Default Visibilities
Solidity has a global variable, tx.origin
, which returns the address of the account that originally sent the call. Using this variable for authentication leaves the contract vulnerable to a phishing-like attack.
- Sigmaprime: tx.origin Authentication
- SWC Registry: Authentication Through tx.origin
- Solidity Documentation: tx.origin
If a smart contract system implements its own signature verification scheme, it may contain vulnerabilities.
Contracts may implement other (more complex) forms of access control themselves. Errors in this code can lead to functions that should be private being accessible by an attacker. In particular, one should always check that any selfdestruct
calls and ether withdrawals can only be made by those who are intented to be able to.
- SWC Registry: Unprotected SELFDESTRUCT
- SWC Registry: Unprotected Ether Withdrawal
- Trail of Bits: Unprotected Functions
Solidity allows calling external contracts via the DELEGATECALL
opcode, which executes the code of an external contract in the persistent context of the present contract. Certain contracts perform DELEGATECALL
calls using user-provided call data, which can effectively give full control to an attacker.
If a smart contract system performs any sort of signature verification, it may be vulnerable to signature replay attacks. (Keep in mind that any signature sent to a contract via calldata will be publicly available.) Keeping track of processed signatures in storage is a simple way to prevent such attacks. Furthermore, in some cases, signatures may be malleable, i.e. an attacker may be able to modify them (so that they may be replayed) without destroying their validity.
- SWC Registry: Missing Protection Against Signature Replay Attacks
- SWC Registry: Signature Malleability
In Solidity, there are multiple ways to call an external contract and send ether. The function transfer
reverts if the transfer fails. However, the functions call
and send
return false. Programmers may mistakenly expect call
and send
to revert, and fail to check for their return value.
- DASP: Unchecked Low Level Calls
- Sigmaprime: Unchecked CALL Return Values
- Trail of Bits: Unchecked External Calls
- SWC Registry: Unchecked Call Return Value
If a function makes a call to an external subroutine with call
, an attacker may be able to cause the function to only partially execute by sending a (precise) insufficient amount of gas.
This is a broad category of attacks where an attacker may render a contract inoperable, temporarily or permanently.
An attacker may be able to exploit the fact that transfer
(alternatively require(addr.send(amount))
) reverts on failure to prevent a function from ever completing execution.
In cases where the users of a system can manipulate how much computation (gas) is necessary for the execution of some function, it may be possible to DOS the system by causing the required gas to exceed the block gas limit. This is often the case in systems that loop over an array or mapping that can be enlarged by users at little cost.
- Consensys Best Practices: DOS with Block Gas Limit
- SWC Registry: DOS with Block Gas Limit
- Solidity Documentation: Gas Limit and Loops
In some cases, developers may want to make a transfer and continue execution regardless of the result. One way to achieve this is with call.value(v)()
, however this may allow the recipient to consume all the gas of the calling function, preventing execution from continuing. See example 1 here:
Some systems may become inoperable if the owner or some other authority goes offline / loses their private key. This should be avoided.
The EVM does not have support for uncertainty/random number generation. Contracts may try to simulate uncertainty in a way that is in fact predictable and exploitable.
- DASP: Bad Randomness
- Sigmaprime: Entropy Illusion
- Trail of Bits: Bad Randomness
- SWC Registry: Weak Sources of Randomness from Chain Attributes
- Solidity Documentation: Private Information and Randomness
Developers may forget that everything on chain is public, and store private data in the open.
Miners can see transactions for a short time before they get included in a block, and exploit this information. They can also alter the order of transactions within a block. Users also have some influence over this process by setting gas prices. This can often pose a security risk, especially in systems where users are bidding or otherwise competing for something.
- DASP: Front-Running
- Sigmaprime: Race Conditions / Front-Running
- Consensys Best Practices: Front-Running
- Trail of Bits: Race Conditions
- SWC Registry: Transaction Order Dependence
Many contracts use block timestamps for various purposes. Keep in mind that miners can slightly adjust them to their advantage.
- DASP: Time Manipulation
- Sigmaprime: Block Timestamp Manipulation
- Consensys Best Practices: Timestamp Dependence
- SWC Registry: Timestamp Dependence
Certain contracts behave erroneously when their account contains ether. It is always possible to forcibly send ether to a contract (without triggering its fallback function), using selfdestruct
, or by mining to the account.
- Sigmaprime: Unexpected Ether
- Consensys Best Practices: Forcibly Sending Ether
- Trail of Bits: Forced Ether Reception
- Solidity Documentation: Sending and Receiving Ether
When a contract delegates some of its functionality to an external contract whose address is either inaccessible or subject to change, a benign implementation may be swapped out for a malicious one.
Local variables in solidity functions default to storage
or memory
depending on their type. Uninitialized local storage variables can point to unexpected storage locations in the contract, leading to intentional or unintentional vulnerabilities.
In general, writes to arbitrary storage locations should be avoided. This can occur, for example, if a storage array is written to at an index specified by a user.
Typos or mistakes in the type signature of a function can lead to the fallback function being called instead.
If a contract uses function variables, an attacker may be able to manipulate a function variable to point to an unexpected location.
If multiple variables with the same name are declared in different scopes, there may be unintended effects. This is easy to miss in the case where a contract inherits from a contract implemented in a separate file.
- Trail of Bits: Variable Shadowing
- SWC Registry: Incorrect Inheritance Order
- SWC Registry: Shadowing State Variables
Properly functioning code should never violate an assert
statement. This can occur if developers mistakenly use assert
instead of require
.
- SWC Registry: Assert Violation
- SWC Registry: Requirement Violation
- Solidity Documentation: Error Handling
If keccak256(msg.data)
or some similar hash functionality is used (for example to log past calls), be aware that functions of types that don't occupy 32 bytes may be called with identical arguments but different hashes.
Use modifiers only for input validation with require
. Modifiers should not contain any substantive logic, because that logic will be executed before any input validation done at the start of function bodies.
Never use a compiler version that is significantly out of date. Implementations should specify an exact compiler version with pragma
.
Avoid the use of deprecated solidity functions.
To call a function on another contract, the standard ABI way to do so is to pass as calldata a function "selector", followed by the encoded arguments. You can read more here, but a short example follows.
In solidity, a call may look like otherContract.foo("hello")
, but in reality, the call becomes address(otherContract).call(abi.encodeWithSignature("foo(string)", "hello"))
which in practice becomes
0xf31a69690000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000568656c6c6f000000000000000000000000000000000000000000000000000000
The first 4 bytes 0xf31a6969
, are called the function selector, and consist of the first 4 bytes of the Keccak256 hash of the string "foo(string)". All ABI-compliant contracts on Ethereum begin by looking at these bytes of the calldata and jump to the corresponding function body.
If a contract performs a call to an external contract, and the user can influence any part of the method signature (such as the function name, or its type), they can call any function on the external contract, simply by manipulating the string until a selector matching the desired one is found.
This was behind the Poly Network hack in August 2021, where an attacker crafted messages on one chain which got processed on another chain. The contracts assumed well-behaved transactions, and the attacker managed to trick the contracts into calling privileged functions on yet other contracts. https://twitter.com/kelvinfichter/status/1425217056660721666
Avoid experimental language features, as these are not properly tested and have contained vulnerabilities in the past.
These are possible vulnerabilities in frontends to ethereum contracts, not vulnerabilities in the contracts themselves. (Possibly out of scope.)
Frontends should validate any input used to make transactions on chain.
Fixed in solidity v0.4.22
- Sigmaprime: Constructors with Care
- Trail of Bits: Wrong Constructor Name
- SWC Registry: Incorrect Constructor Name
Fixed in EIP 150
Fixed in solidity v0.5.7
This is present when Multicall is used on any contract which reads the value of msg.value
.
Multicall is a pattern to call several contract endpoints in one transaction, using delegatecall
.
A contract endpoint may implicitly assume that it is called in a single transaction, by looking at msg.value
.
Since the value is defined per transaction, calling the same endpoint twice in one multicall means that the same msg.value
may be read several times, even though the value was only transferred once.
For example, if a token contract accepts ETH in exchange for tokens in a swap
function, and the contract implements multicall, an attacker may call swap several times in one transaction.
Let's say the attacker sends along 1 ETH in the multicall transaction, which would normally give them 100 tokens.
Each call to the swap
function will read msg.value
and transfer 100 tokens to the attacker.
If the attacker calls swap
10 times in one multicall, they will get 1,000 tokens in exchange for 1 ETH.