Skip to content

List of Security Vulnerabilities

Rikard Hjort edited this page Jan 14, 2022 · 12 revisions

Table of Contents

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.

Integer Arithmetic

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; }

Floating Point Arithmetic

Fixed point numbers are not yet fully supported by solidity. User implementations may contain errors.

Reentrancy

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.

Access Control

There are a number of common mistakes relating to access control.

Default Visibility

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.

Authentication With tx.origin

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.

Signature Verification

If a smart contract system implements its own signature verification scheme, it may contain vulnerabilities.

Unprotected Functions

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.

Code Injection via delegatecall

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.

Signature Replay Attacks

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.

Unchecked External Calls

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.

Insufficient Gas Attacks

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.

DOS

This is a broad category of attacks where an attacker may render a contract inoperable, temporarily or permanently.

Unexpected Revert

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.

Block Gas Limit

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.

External Calls without Gas Stipends

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:

Offline Owner

Some systems may become inoperable if the owner or some other authority goes offline / loses their private key. This should be avoided.

Entropy Illusion

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.

Privacy Illusion

Developers may forget that everything on chain is public, and store private data in the open.

Miner Attacks

Transaction Ordering

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.

Timestamp Manipulation

Many contracts use block timestamps for various purposes. Keep in mind that miners can slightly adjust them to their advantage.

Unexpected Ether

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.

External Contract Referencing

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.

Uninitialized Storage Pointers

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.

Writes to Arbitrary Storage Locations

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.

Incorrect Interface

Typos or mistakes in the type signature of a function can lead to the fallback function being called instead.

Arbitrary Jumps with Function Variables

If a contract uses function variables, an attacker may be able to manipulate a function variable to point to an unexpected location.

Variable Shadowing

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.

Assert Violation

Properly functioning code should never violate an assert statement. This can occur if developers mistakenly use assert instead of require.

Dirty Higher Order Bits

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.

Complex Modifiers

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.

Outdated Compiler

Never use a compiler version that is significantly out of date. Implementations should specify an exact compiler version with pragma.

Use of Deprecated Solidity Functions

Avoid the use of deprecated solidity functions.

Function Selector Abuse

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

Experimental Language Features

Avoid experimental language features, as these are not properly tested and have contained vulnerabilities in the past.

Constructor call

Frontend (Off Chain) Attacks

These are possible vulnerabilities in frontends to ethereum contracts, not vulnerabilities in the contracts themselves. (Possibly out of scope.)

Short Address Attack

Frontends should validate any input used to make transactions on chain.

Historic Attacks

Constructor Names

Fixed in solidity v0.4.22

Call Depth Attack

Fixed in EIP 150

Constantinople Reentrancy

Solidity Abi Encoder v2 Bug

Fixed in solidity v0.5.7

Payable Multicall

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.

References