Solidity smart contracts common vulnerabilities

Alberto Molina
Coinmonks
Published in
13 min readMay 4, 2022

--

As a developer, sometimes we focus too much on the functionality and performance offered by our applications and we tend to forget about security. Security is always important but it is paramount when it comes to smart contracts deployed on public blockchains (like Ethereum).

Our bytecode is accessible to everyone, our contracts’ state variables can be seen by anyone and any valid account can interact with our contracts, meaning that, we are completely exposed to the world.

Even the tiniest vulnerability will be noticed by someone and potentially exploited (depending on the reward to cost ratio of course….), which is why I would like to share with other developers the list of the most common smart contract vulnerabilities that I managed to put together from multiple sources.

  • Saving unencrypted confidential data on the blockchain
  • Integer overflow and underflow (solved since solidity 0.8)
  • Unchecked call return values
  • Re-entrancy attacks
  • Denial Of Service attacks
  • Front Running attacks
  • Replay signatures attacks
  • Function default visibility
  • Floating pragma
  • Loop through long arrays
  • Wrong inheritance
  • Unexpected ether balance
  • Access outside array limits
  • Delegate calls to untrusted sources
  • (Regular) calls to untrusted sources
  • Insecure randomness
  • Block Timestamp manipulation
  • Contracts with zero code
  • Uninitialized Storage Pointers
  • Unupgradable smart contracts
  • Initializable logic implementations
  • Different contracts at the same address

Saving unencrypted confidential data on the blockchain

The blockchain is accessible to anyone meaning that absolutely nothing about it is confidential, if by mistake, you save passwords (or similar information) on it, you are in big trouble.

Solution: Do NEVER store confidential information on a blockchain unless you encrypt it or hash it.

Integer overflow and underflow (solved since solidity 0.8)

This is probably one of the easiest vulnerabilities to understand and forget at the same time.

Solidity offers different types of integers based on the maximum available value: int8 (8 bits, maximum value 2⁸-1), int16 (16 bits, maximum value 2¹⁶-1), …

If you execute a calculation that returns a result larger than the maximum value or smaller than 0, the solidity will not throw an exception but will instead jump all the way to the other end of the range, for instance, for a uint8, 2⁸-1 + 4 = 3…

Solution: Fortunately this vulnerability has been solved since solidity 0.8. If you are still using a lower solidity version, you can use the SafeMath library from OpenZeppelin or check for integer overflows/underflows yourself.

Unchecked call return values

At some point in your code, you might invoke a function from a third-party smart contract. This execution might fail or return an unexpected result.

Solution: Fortunately call operations always return a “success” boolean and data (it depends on the invoked function of course). Never forget to check those values before proceeding with your code logic.

Re-entrancy attacks

This is probably the most well-known vulnerability in the list.

Whenever you invoke an external contract function, always keep in ming that this contract might invoke your contract function in return (creating sort of an invocation loop).

This can be potentially dangerous if for example, you are sending funds to an external account (that happens to be an external contract), but you did not update its balance before sending them. Each time the external contract will “re-enter” your function, it will receive the same amount of funds. By the time the execution finishes, you will have probably sent multiple times the amount you expected to send.

Solution: You can either use the “Non reentrancy” pattern and apply it to your sensitive functions (there is an openZepelin contract you can use) or simply implement your functions following this pattern :

  • Check: check the input parameters, access rights, etc…
  • Update: update the state variables
  • Interact: call the external contracts / accounts

Denial Of Service attacks

Let’s suppose that at some point in your code logic you invoke an external contract’s function and then if the invocation is successful, continue with your code execution. If for some reason, a malicious developer has implemented the external contract function so that it always fails, the final part of your function will NEVER be executed… this can be extremely problematic, it is indeed a very simple way of performing a DoS attack on your contract’s function…

Solution: Always assume that external calls can fail. If possible implement a “pull” pattern instead of a “push” one.

In a “push” pattern, contracts invoke your function which will invoke a third-party contract. That way a malicious contract could block other users.

In a “pull” pattern, contracts invoke your function which will in return invoke the calling contract’s function only. That way, if a malicious developer tries to DoS your function, it will only block itself and not your other users.

Front Running attacks

Ethereum is a distributed network that relies on a large set of miners to validate and add blocks to the ledger. Those miners will have access to the transactions that users want to add to the blockchain.

If your dapp offers a reward to the first user that submits certain information (secret info or similar), a miner could simply wait for an honest user to submit the information, let the transaction languish in the transaction pool, create its own transaction with the secret info originally provided by the honest user, and get the reward instead of him/her.

Solution: There is no magical solution for this, just keep it in mind when implementing your dapp…

Replay signatures attacks

Signatures are a way for an account to send another’s account transactions to the blockchain. The original account will sign a message then the delivery account will send the message to a smart contract, that way it is the delivery account that pays for the transaction fees and not the original account.

Of course your smart contract must have functions capable of validating signatures and performing the required tasks.

Nevertheless, if a signed message is valid and your smart contract will execute its content, keep in mind that whoever has access to it (literally anyone monitoring the blockchain) could send it multiple times, meaning that your smart contract functionality might be executed multiple times for the same message, which was not the original account’s intention when it signed the message…

Solution: Add a nonce per account to the signed messages. Messages signed by the same account with an already used nonce are not accepted. Every time an account signes a new message, it will have to increment the nonce. The EIP-712 standard can help you with this.

Function default visibility

Function's default visibility is public, meaning that anyone can execute it. If you implement a function that is not supposed to be executed by external accounts but you forget to set its visibility to internal or private, you might be in trouble.

Solution: Always define functions and state variables visibility.

Floating pragma

If you leave a floating pragma in your code (pragma solidity ≥ 0.7.0 < 0.9.0.), you will not be sure of which version is been used to compile your code which means that you might encounter unexpected behaviors.

Solution: You should lock the solidity pragma to a specific solidity version so that you can be sure how the contract will behave once deployed.

Loop through long arrays

If you are used to other programming languages you might be tempted to use arrays more than you actually should.

Keep in mind that executing functions in Ethereum costs gas (money), and transactions have a gas limit by definition (the gas limit of a single block). If for some reason your smart contract uses a very long array, and at some point, you need to iterate through it, you might reach the gas limit making the function unexecutable….

Solution: Always try to use mappings when you expect a long list of values.

Wrong inheritance

Solidity supports multiple inheritances which introduce the ambiguity called the “Diamond Problem”. Basically, if multiple parent contracts implement the same function, which one will be inherited by your contract?.

Solidity uses C3 Linearization to set the priorities among the parent contracts. If you do not pay attention to this you might have unexpected behaviors.

Solution: You should understand how “C3 Linearization “ works. As a general rule of thumb, add your parent contracts going from the most generic one to the most specific one.

Unexpected ether balance

You might think that the only way for your contract to receive ether is by either implementing “payable” functions and/or a receive function. But even if your contract has absolutely no “payable” functions and no receive function it might still receive either in the following two situations :

  • As a result of a “self-destruct” of another contract. Every time a contract is self-destructed it can decide to which account it wants to send its current balance.
  • As a result of newly mined ether. Every time a miner adds a block to the blockchain it will receive a reward in ether, the miner can decide to which account the ether should be assigned to.

These two types of ether transfer are not going to be caught by your receive function anyways….

Solution: Never assume you can store in a state variable the exact amount of ether your contract owns by simply reacting to the received transactions. Best case scenario you will be able to store the minimum amount it owns.

Access outside array limits

In early versions of Solidity, storage dynamic array lengths could be modified directly (without actually adding or removing elements from it). The problem is that state variables share the same storage space, meaning that if you let users modify an array length they can potentially access any slot in storage space, they could replace any state variable in your contract, even those he/she is not supposed to have access to…

Solution: Use a solidity version that does not have this vulnerability or simply do not let users modify storage array’s lengths.

Delegate calls to untrusted sources

This is probably the biggest security vulnerability you can have. “Delegate Calls” will invoke an external contract function and execute it within your contract’s context (as opposed to regular calls that run in the callee’s context). The external contract will then have full access to your state variables and will be able to do whatever it wants.

Solution: Use delegate calls carefully…

(Regular) calls to untrusted sources

Calling untrusted contracts can also have some unexpected results. Keep in mind that calling the same contract’s function twice can return two different results, for instance, you call an untrusted contract function “check_condition_X()” that may return true initially, but if you call it immediately after again (nothing in your contract has change in the meantime and we are still in the same transaction) it might return false (the untrusted contract state might have changed because of your first invocation).

Solution: If you need an externally provided value in your code, instead of calling the external contract each time, save the value in a local variable.

Insecure randomness

By definition, smart contracts must be deterministic, they should return the same result every time we run them, which means that also by definition, randomness is not possible.

Solution: I guess the easiest solution would be to not include randomness at all in your smart contract but if you have to, then it will depend on how sensitive the random value must be. You could potentially use global variables (block timestamp, block number, …) or use an oracle like chainlink to get secure randomness.

Block Timestamp manipulation

Miners chose the timestamp of the block they mine, they cannot set any timestamp they want, but they have certain flexibility. If you are implementing a smart contract that requires date and/or time values to work properly, miners could have the possibility to hack it.

Solution: You can work with data and time as long as you are not too restrictive. As a general rule of thumb, you should be able to tolerate at least a 15 seconds variance.

Contracts with zero code

It is possible to check whether an account corresponds to a smart contract or it is externally owned. The way to do that is by checking the “bytecode size” related to the account (using assembly). Accounts have several properties in Ethereum (balance, nonce, etc…), and one of them is “bytecode”, which is empty for an externally owned account and contains some data for smart contracts.

However, during a new contract deployment, the “constructor” function of the contract will be executed before the smart contract is created, meaning that during that execution process, the smart contract address will not have any related “bytecode”. If a smart contract invokes your function while it is been deployed, it will not be detected as a smart contract but as an externally owned account.

Solution: If you need to restrict access to your smart contract the best way would be to have a whitelist but if you really need to prevent smart contracts from invoking your code, maybe the best way would be to check if “msg.sender” corresponds to “tx.origin” since the original account of a transaction must always be an externally owned account.

Uninitialized Storage Pointers

This is mostly good practice. Always initialize your state variable (from the constructor or initializer function if you are using transparent proxies), otherwise, the default empty values might lead to unexpected behaviors.

Solution: Fortunately since solidity version 0.5.0 storage pointers must be initialized.

Unupgradable smart contracts

Smart contracts are by definition immutable (they can be “self-destructed” by not modified) and unstoppable, meaning that if for some reason you deploy a buggy smart contract, you will not be able to fix it and redeploy a new version…

Solution: Always use upgradable patterns for your smart contracts (transparent proxies, admin proxies, beacon proxies, etc…). You do not need to implement them, you can reuse the functionality offered by openzeppelin for example. You can also consider the possibility of adding the “Pausable” pattern to some of your contract’s functions. “Pausable” function can be paused (meaning nobody can invoke them), this will give you more time to fix a bug and deploy a new version.

Initializable logic implementations

In order to have upgradable smart contracts, The “proxy-pattern” is used very often. The idea is simple, in a nutshell: a “logic” contract (with all the EVM byte code) is deployed, then one or many “proxies” contracts are deployed and configured to delegate all their calls to the logic contract. Proxies do not contain any logic, all the code is in the logic contract, however proxies keep their own state variable.

In many of these cases it is necessary to “initialize” the proxies. Initializing a contract is normally done using the “constructor” function that only runs once (during deployment), but if the “proxy-pattern” is implemented, constructors can not be used, since they would initialize the logic and not the proxies. That is why, in order to initialize a Proxy, logic contracts expose an initialize method which can be called by anyone, and sets the initial state of the contract. Proxies call this method (using a delegate call like for any other logic method) and initialize their own state.

The vulnerability comes from the fact that the initialize method can be called by anyone, meaning that anyone could potentially initialize the logic contract itself. This is not a problem at first, since proxies rely on the logic contract for the executable byte code, not for the state since they keep their own, initializing the logic state would not really have an impact on any of the proxies. However, in some cases, in could happen that an attacker could set the logic contract state in a way that gave him the possibility to destroy it, for example, if the logic contract makes a delegate call to an address included in its state which can only be set by an administrator. The administrator account will be probably set through the initialize method, an attacker could then claim the logic contract administration then set the address of the delegate call to a contract that would simply execute the “self-destruct” command. If that happened, the logic contract would be removed and proxies would be left “pointing” to an empty contract.

Solution: The solution is actually pretty simple. When deploying the logic contract, a constructor method should be included and the purpose of it would be to simply set the initializer flag to false. The idea is that the initialize method should not be executable in the context of the logic contract itself but only through delegate calls.

Different contracts at the same address

Contracts are supposed to be immutable, their bytecode cannot be changes after deployment. This is actually true but there are a couple of tricks that we can apply to deploy different logic at the same address.

The first one is well-know, and it is not a trick, it is a design pattern if you will : using proxies. A proxy is a smart contract that delegates its logic to an “implementation” contract, changing the implementation address to a different smart contract will effectively “change” the proxy’s logic (or bytecode). The truth is that the proxy’s bytecode is not changing but its “underlying” logic is.

The second one is what we can call an “actual trick” and can lead to potential hacks if we are not careful with it : removing and deploying at the same address. We can indeed remove a smart contract by invoking the “selfdestruct” opcode. Once the contract is deployed, we can redeploy a new contract at the same address. In order to do that we need to apply the following system:

  • Deploy a “Deployer-Deployer” contract.
  • Deployer-Deployer contract will deploy using the “create2” opcode a “Deployer” contract at a predefined address (based on the Deployer bytecode and a salt).

Deployer contract is a “live-factory”, meaning that it uses “create” opcode to deploy a smart contract using the bytecode it receives as an input.

  • Deployer contract will deploy a contract (CONTRACT A) using the “create” opcode at a pseudo-random address determined by the Deployer address and a nounce (nounce = 0 since it is the first deployment).
  • We run “self-destruct” on CONTRACT A.
  • We run “self-destruct” on Deployer.
  • We redeploy Deployer using Deployer-Deployer and the same salt. Deployer will be redeployed at the same address and its nounce will be reset to 0.
  • Deployer contract will deploy a contract (CONTRACT B) using the “create” opcode at a pseudo-random address determined by the Deployer address and a nounce (nounce = 0 since it is the first deployment).

Result => CONTRACT A and CONTRACT B will be deployed at the same address and yet contain different byte code.

Solution: if a smart contract does not include the possibility to “self-destruct” you can be sure it will never be replaced.

Join Coinmonks Telegram Channel and Youtube Channel learn about crypto trading and investing

Also, Read

--

--