Ethereum apps handle financial value, making security crucial. Smart contracts have definitely had their fair share of attacks as a new, experimental technology.
I have created a list of nearly all known threats and vulnerabilities to help prevent further attacks. Although this list can cover known threats, new vulnerabilities are still being constantly discovered, and as such this should only be the start of your engineering research into smart contract security.
We will look at known attacks in this chapter that can be used to exploit vulnerabilities in smart contracts.
Front-running aka transaction-ordering dependence
Concordia University claims that front-running is “a course of action in which a person profits from advance access to privileged market information about potential transactions and trades.” This awareness of future market activities can contribute to manipulation.
For example, realizing that there is going to be a very large inventory of a particular token, a bad actor may buy the token in advance and sell the token for a profit when the excessive purchase order increases the price.
Front-running attacks have been a concern in financial markets for a long time, and the issue is coming up again in cryptocurrency markets because of the open nature of blockchain.
Because the solution to this problem varies by contract, it can be difficult to protect against it. Possible solutions involve batching transactions and using a pre-commit scheme (i.e. allowing users to submit later details).
DoS with block gas limit
The blocks all have a gas cap in the blockchain of the Ethereum. One of the advantages of a block gas cap is that it prevents an infinite transaction loop from being generated by hackers, but if a transaction’s gas usage reaches that limit, the transaction will fail. In a few different ways, this can lead to a DoS attack.
Sending funds to an array of addresses is a situation in which the block gas cap can be a concern. This can easily go wrong, even without any malicious intent. It is only by allowing too many customers to pay that the fuel cap can be maxed out and the transaction stopped from ever succeeding.
This situation can lead to an assault as well. Say a bad actor wants to create a significant number of addresses, with a small amount of money from the smart contract being paid for each address. The payment can be stopped permanently if it is done effectively, perhaps even stopping more transactions from occurring.
Using a pull-payment system over the existing push-payment system would be an effective solution to this issue. To do this, split each payment into your own transaction and call the feature to the receiver.
If you really need to loop through an array of unspecified duration for some reason, at least expect it to take several blocks and allow multiple transactions — as seen in this example:
In some cases, even if you don’t loop through a range of unspecified duration, your contract may be threatened with a block gas cap. Before a payment can be processed, an intruder will fill many blocks using a sufficiently high gas price.
This assault is achieved at a very high gas price by making multiple payments. If the fuel price is high enough and the transactions consume ample gas, they can fill entire blocks and impede the processing of other transactions.
Ethereum transactions allow the recipient to pay gas to deter spam attacks, but there may be enough opportunity in some cases to go through such an attack. For example, on a gambling Dapp, Fomo3D, a block stuffing attack was used. The app had a countdown timer, so users could win a jackpot by being the last to buy a key — except when a user bought a key, the timer would be extended. The next 13 blocks in a row are stuffed by an intruder who bought a key so they could win the jackpot.
It is necessary to carefully consider whether it is safe to implement time-based behavior in your application to avoid these attacks from occurring.
DoS with (unexpected) revert
DoS (denial-of-service) attacks can occur in functions when you attempt to send funds to a client and the functionality is dependent on a positive transfer of funds.
This can be troublesome if the funds are sent to a smart contract created by a bad actor, as they can simply create a fallback feature that will undo all transactions.
As you can see in this instance, if an attacker bids back all transactions from a smart contract with a fallback feature, they can never be reimbursed, so nobody can ever make a higher bid.
This can be dangerous without the involvement of an intruder. Of example, through iterating through the list, you might want to pay for an array of users and, of course, you would want to make sure that each user is paid properly. When one payment fails, the dilemma here is that the role is reversed and no one is paid.
Using a pull-payment system over the existing push-payment system would be an effective solution to this issue. To do this, split each payment into their own transaction, and call the operation to the receiver.
Forcibly sending Ether to a contract
Occasionally, the ability of users to submit Ether to a smart contract is ignored. Unfortunately for these cases, a contract fallback function can be bypassed and Ether can be forcibly sent.
While it seems that any payment to the Vulnerable contract should be reversed, there are still a few ways to send Ether forcefully.
The first approach is to name a contract with the Vulnerable contract address set as the beneficiary the “selfdestruct” form. It works because the fallback feature won’t be caused by “selfdestruct.”
Another approach is to pre-calculate the address of a contract and send Ether to the address before even deploying the contract. Surprisingly enough, this can be achieved.
Insufficient gas griefing
Griefing is a type of attack that is often committed in video games, where a malicious user unintendedly plays a game to annoy other players, called trolling. This type of attack is also used to stop transactions as intended.
This attack can be carried out on contracts that accept information and use it on another contract in a subcall. For multi-signature wallets as well as payment relayers, this approach is often used. If the subcall fails, either reversing the entire transaction or continuing the execution.
Let’s take as an example a simple relayer contract. The relayer contract, as shown below, allows someone to make and sign a transaction without executing the transaction. It is also used when a customer is unable to pay for the transaction-related fuel.
The client performing the transaction, the forwarder, can effectively censor transactions using just enough gas to execute the transaction, but not enough gas to achieve the subcall.
This could be avoided in two ways. The first solution is to allow only trusted users to relay transactions. The other option, as seen below, is to allow the forwarder to supply ample fuel.
Reentrancy is an attack that can occur when an error in a contract system may cause a service communication to continue many times when otherwise it should be forbidden. When used maliciously, this can be used to steal funds from a smart contract. In reality, the attack vector used in the DAO hack was reentrancy.
When a weak function is the same function that an attacker tries to call recursively, a single-function reentrancy attack occurs.
Here, we can see that the balance is only adjusted after the transfer of funds. This can cause a hacker to call the function several times before the smart contract is set to 0.
A reentrance attack by cross-function is a more complicated version of the same operation. Cross-function reentrancy happens when a compromised function shares a state that can be abused by an attacker.
In this example, a hacker can take advantage of this contract by providing a “transfer()” fallback function to transfer spent funds before the “withdraw()” function setting the balance to 0.
Using “send” or “transfer” instead of “call” to transfer funds in a smart contract. Unlike the other features, the problem with using “call” is that it has no gas cap of 2300. For external function calls, this means “call” can be used, which can be used to execute reentrancy attacks.
The effective form of avoidance is to mark untrusted functions
Therefore, for maximum use of protection the checks-effects-interactions pattern. For ordering smart contract features, this is a basic thumb rule.
It should begin with the feature checks — e.g.,
The next step is the effects of the contract should be performed — e.g., state modifications.
Eventually, we were able to perform interactions with other smart contracts — e.g., external function calls.
This design is successful against reentrancy as the changed contract state would prevent malicious interactions from being conducted by bad actors.
Since the balance is set to 0 before any transactions take place, there is nothing to submit after the first transaction if the contract is called recursively.
In this chapter, we will look at known vulnerabilities in smart contracts and how to prevent them. In the Smart Contract Weakness Classification, almost all of the vulnerabilities mentioned here can be found.
Integer overflow and underflow
Whole forms have total values of solidity. For example:
Bugs that surpass the maximum value (overflow) or go below the minimum value (underflow) can occur. You go back to zero if you surpass the maximum value, and if you go below the minimum value, it will bring you back to the maximum value.
Because smaller forms of integer — like
uint16, etc. — have smaller maximum values, overflow can be easier to cause; hence, they should be used more carefully.
Possibly, using the OpenZeppelin SafeMath library when performing mathematical operations is the best solution available for overflow and underflow bugs.
The timestamp of a block, accessed by
block.timestamp, a miner can be manipulated. There are three things that you should consider when conducting a contract procedure with a timestamp.
If a timestamp is used in an attempt to generate randomness, within 15 seconds of block validation, a miner will post a timestamp, giving them the ability to set the timestamp as a value that would increase their chances of benefiting from the feature.
For example, a lottery application may pick a random bidder in a group using the block timestamp. A miner will enter the lottery and then adjust the timestamp to a value that will give them better chances of winning the lottery.
Therefore, timelines should not be used to establish randomness.
The 15-second rule
The reference specification of Ethereum, the “Yellow Paper,” does not define a limit as to how many blocks in time will shift — it just has to be larger than its parent’s timestamp. That being said, in the future, common protocol implementations would reject blocks with timestamps of more than 15 seconds, so as long as the time-dependent event can differ safely by 15 seconds, a block timestamp is safe to use.
block.number as a timestamp
The time difference between incidents can be calculated using
block.number and the average time of the frame. But block times can change and break the functionality, so avoiding this use is best.
Authorization through tx.origin
tx.origin Is a global solidity parameter that returns the address sent by the transaction. It’s important you never use
tx.origin for authorisation as another contract may use a fallback feature to call your contract and obtain authorisation as the approved address is stored in
tx.origin. Consider this example:
Here we can see the
TxUserWallet contract authorizes the
transferTo() function with
Now, if somebody fooled you into sending Ether to the contract address of
TxAttackWallet, they might steal your funds by searching
tx.origin for the address that sent the payment.
msg.sender for authorization to avoid this type of attack.
Choosing one version of the compiler and sticking with it is considered best practice. Contracts can unintentionally be implemented with a floating pragma using an obsolete or unstable compiler version— which can cause bugs, putting the protection of your smart contract at risk. The pragma also informs developers what version to use if they implement your contract for open-source projects. The selected version of the compiler should be checked extensively and considered for known bugs.
In the case of libraries and packages, the exception where using a floating pragma is appropriate. Otherwise, to compile locally, programmers would need to manually patch the pragma.
Function default visibility
Visibility of the feature can be specified as public, private, internal or external. Consideration of the best visibility for your smart contract feature is critical.
A programmer forgets or forgets to use a visibility variable to trigger most smart contract attacks. The function is then set by definition as public, which may result in unintentional changes to the system.
Outdated compiler version
Developers frequently find and fix bugs and vulnerabilities in existing software. It is therefore necessary to use the latest version of the compiler.
Unchecked call-return value
If a low-level call’s return value is not checked, the execution will resume, even if an error is thrown by the call system. This can lead to unexpected behaviour, upsetting the stability of the system. An attacker may even trigger a missed call, who may be able to exploit the request further.
In Solidity, you can either use low-level calls such as
address.send(), or you can use contract calls such as
ExternalContract.doSomething(). Low-level calls will never throw an exception — instead they will return
false if they encounter an exception, whereas contract calls will automatically throw.
Be sure to check the return value in case you use low-level calls to manage potential failed calls.
Unprotected Ether withdrawal
Bad actors may be able to withdraw some or all of the Ether from a contract without appropriate access controls. This can be done by misnaming a feature meant to be a constructor, allowing everyone access to the contract being reinitialized. To avoid this weakness, only allow those allowed or intended to cause withdrawals and properly name your builder.
Unprotected selfdestruct instruction
In contracts with a method of
selfdestruct, malicious actors will self-destruct the contract if there are incomplete or inadequate access controls. It is important to consider whether the functionality of self-destruct is absolutely necessary. Try using a multisig authorization to stop an attack if necessary.
This attack has been used in the assault on the Parity. An anonymous client in the “library” smart contract found and exploited a loophole, making himself the holder of the contract. The intruder then continued with the contract being self-destructed. This contributed to the blocking of funds in 587 separate wallets, containing 513,774.16 Ether in total.
State variable default visibility
Developers are common to specifically declare visibility of function, but not so common to declare visibility of variable. State variables can have one of three identifiers of visibility:
private. Luckily, the default visibility for variables is internal and not public, but even if you intend on declaring a variable as internal, it’s important to be explicit so there are no incorrect assumptions as to who can access the variable.
Uninitialized storage pointer
Data is stored in the EVM as either
calldata. It is critical that both are well understood and initialized correctly. Incorrect initialization of data storage points, or simply leaving them uninitialized, can lead to vulnerabilities in the contract.
As of Solidity
0.5.0, uninitialized storage pointers are no longer an issue because uninitialized storage pointer contracts will no longer be compiled. That being said, knowing what space indicators you should use in some circumstances is still relevant.
0.4.10, the following functions were created:
revert(). Here, the assert function will be discussed and how to use it.
It was said officially, the
assert() function It is intended to state invariants; informally said,
assert() is an excessively assertive bodyguard defending your contract while stealing your gas during the process. Properly functioning contracts should never enter a state of default. If you have an inadequate argument, you’ve either improperly used
assert() or your contract contains a bug that puts it in an invalid state.
If the condition checked in the
assert() is not actually an invariant, it’s suggested that you replace it with a
Use of deprecated gunctions
With the passing of time, solidity functions are discarded and often replaced through better functions. Not using deprecated functions is critical, as it can lead to unintended effects and compilation errors.
Here is a list of functions and alternatives that are deprecated. Most alternatives are simple aliases, and if used as a substitute for their deprecated equivalent, they will not interrupt current behavior.
Delegatecall to untrusted callee
Delegatecall is a special variant of a message call. It is almost the same as a standard message call except that the target address is executed in the calling contract and
msg.value remain the same. Essentially,
delegatecall delegates other agreements to adjust the processing of the calling document.
delegatecall gives so much leverage over a contract, it’s very important to use it only with trustworthy contracts, like your own. If the target address comes from the feedback of the client, make sure it is a trusted contract.
For smart contracts, people often assume that using a cryptographic signature scheme verifies the signatures are unique; however, this is not the case. Without the private key, signatures in Ethereum can be modified and remain valid. For example, elliptic key cryptography consists of three variables— v, r, and s — and you can get a valid signature with an invalid private key if these values are changed in the correct way.
Never use a signature in a signed message hash to verify whether previously signed messages have been processed by the contract to avoid the signature malleability issue because malicious users will identify and recreate your signature.
Incorrect constructor name
0.4.22, the only way to define a builder was to construct a contract name feature. This has been problematic in some situations. For example, if a smart contract with a different name is recycled but the function of the constructor is not modified as well, it simply becomes a normal, callable function.
Now you can describe the builder with modern versions of Solidity with the
constructor keyword, preventing this weakness effectively. The solution to this problem is therefore simply to use modern versions of solidity compilers.
Shadowing state variables
The same parameter can be used twice in Solidity, but it can lead to unintended side effects. This is particularly hard when it comes to working with multiple contracts. Take the example below:
Here, we can see
SuperContract, and the variable
a is defined twice with different values. Now, say we use
a to perform some function in
SubContract. Functionality inherited from
SuperContract will no longer work since the value of
a has been modified.
It is important to check the entire smart contract structure for ambiguities in order to avoid this vulnerability. Checking for compiler warnings is also important, as these ambiguities can be highlighted as long as they are in the smart contract.
Weak sources of randomness from chain attributes
There are some applications in Ethereum that rely on the generation of random numbers for equality. Random-number generation, however, is very difficult in Ethereum, and it is worth considering some pitfalls.
Using chain attributes such as
block.difficulty can seem like a good idea, as pseudorandom values are often generated. Nevertheless, the problem lies in a miner’s ability to change these principles. For example, there is sufficient motivation for a miner to create multiple alternative blocks in a gambling app with a multimillion-dollar jackpot, only selecting the block that will result in a jackpot for the miner. Of course, managing the blockchain like that comes at a substantial cost, but if the stakes are high enough, it can definitely be achieved.
There are a few ways to prevent mineral exploitation in random-number generation:
- A commitment scheme such as RANDAO, a DAO in which all DAO participants produce the random number
- External sources via oracles — e.g., Oraclize
- Bitcoin hashes are used because the network is more distributed and blocks are more difficult to mine
Missing protection against signature-replay attacks
Sometimes in smart contracts, to increase usability and gas costs, it is necessary to perform signature verification. Nonetheless, care must be taken when carrying out the confirmation of signatures.
The contract should only allow new hashes to be processed to defend against signature-replay attacks. It prevents malicious users from copying the signature of other users many times.
Follow these recommendations in order to be extra safe with signature verification:
Store all hash messages processed by the contract — then test the hash messages against existing messages before performing the function
Include the contract address in the hash to ensure that the message is used only in a single contract
Never create the hash message with the signature. See “Signature malleability.”
require() method is intended to validate conditions such as inputs or variables of the contract state or to validate the return values of existing contract calls. Inputs can be received by callers or they can be returned by callees to validate existing calls. In the case that an input breach occurred due to a callee’s return value, one of two things probably went wrong:
- The contract includes an error that provided the data.
- The requirement condition is too strong
Next consider whether the state of the specification is too high to solve this problem. Weaken it if necessary to allow any legitimate external input. If the problem is not the state of the specification, the contract must include a loophole that provides external input. Ensure that this contract does not provide invalid data.
Write to an arbitrary storage location
Writing to sensitive storage locations should be accessed only by approved addresses. If there are insufficient authorization checks throughout the contract, sensitive data may be overwritten by a malicious client. Even if authorization checks are in place to write sensitive data, however, an attacker may still be able to overwrite sensitive data of insensitive data. This could give an attacker access to overwrite important variables like the owner of the contract.
Not only do we want to secure sensitive data stores with authorization criteria to prevent this from happening, but we also want to make sure that writing to one data structure can not unintentionally overwrite entries from another data structure.
Incorrect inheritance order
It is possible to inherit from multiple sources in Solidity, which can create confusion if not properly understood. This confusion is known as the diamond problem: if two simple contracts have the same purpose, which one should be given priority? Thankfully, Solidity gracefully addresses this problem— as long as the designer knows the solution.
Using reverse-C3 linearization, the solution Solidity provides the diamond problem. It ensures that the succession will be linearized from right to left so that the order of inheritance matters. Beginning with more general contracts and finishing with more limited contracts is recommended in order to avoid problems.
Arbitrary jump with a function-type variable
Solidity respects feature forms. It implies that a form of parameter can be assigned to a corresponding signature element. As every other function, the function can then be called from the variable. Users should not be able to change the variable of the function, but this is possible in some situations.
If the smart contract uses certain guidance for assembly,
mstore for example, an attacker may point the variable function to any other function. This can give the hacker the ability to break the contract’s functionality— and potentially even drain the contract funds.
Since inline assembly is a low-level way to access the EVM, most important safety features are bypassed. It is therefore important to use assembly only if it is required and understood correctly.
Presence of unused variables
Although it is permitted, avoiding unused variables is best practice. Unused variables can lead to several different issues:
- Increase in computations (unnecessary gas consumption)
- Indication of bugs or malformed data structures
- Decreased code readability
Deleting all unused variables from a code base is highly recommended.
Unexpected Ether balance
Because Ether can always be sent to a contract — see “Forcibly sending Ether to a smart contract” — It is vulnerable to attack if a contract assumes a particular balance.
Assuming we have a contract that prohibits all functions from being performed if the contract includes some Ether. If a malicious client wants to manipulate this by sending Ether violently, they can trigger a DoS, making the contract unusable. For this reason, for the balance of Ether in a contract, it is necessary never to use strict equality tests.
It is always possible to read smart contract code for Ethereum. As such, treat it. Even if your code on Etherscan is not checked, attackers can still decompile or even simply check transactions to and from Etherscan to analyze it.
An example of a problem here would be a guessing game where the user must guess a stored private variable in order to win the Ether in the contract. Of course, this is incredibly trivial to hack (to the point that you shouldn’t do it because it’s almost definitely a much trickier honeypot contract).
The use of unencrypted off-chain passwords, such as API keys, with Oracle calls is another common problem. If it is possible to determine your API key, malicious actors can either actually use it for themselves or take advantage of other mechanisms such as exhausting your permissible API calls and causing Oracle to return an error page that may or may not cause problems depending on the contract structure.
Faulty contract detection
Most contracts do not want to deal with other contracts. A simple way of preventing this is to test if any code is stored in the calling account. Contract accounts initiating calls during development, however, will still not reveal that they store code, essentially bypassing contract detection.
Unclogged blockchain reliance
Most contracts depend on long-term calls, but Ethereum can be spammed for a decent amount of time with very small Gwei transactions, relatively cheaply.
For example, Fomo3D (a countdown game where the last investor wins the jackpot, but each investment adds time to the countdown) has been won by a consumer who has completely blocked the blockchain for a short period of time, refusing to allow others to invest until the timer runs out and he wins (see “DoS with block gas limit”).
There are nowadays most croupier gaming contracts depending on past blockhashes to offer RNG. For the most part, this is not a terrible source of RNG, and it even accounts for the hashes pruning which takes place after 256 lines. But many of them simply nullify the bet at that point. This would allow somebody to make bets on many of these similarly operating contracts with some outcome as the winner for them all, check the croupier’s submission while it’s still pending, and, if it’s unfavorable, just clog the blockchain before pruning takes place and they could get their bets back.
Inadherence to standards
It is important to follow standards in terms of smart contract growth. Standards are set to eliminate flaws, which can lead to unintended consequences if overlooked.
Take, for example, the original BNB token from Binance. It was sold as an ERC20 token, but later it was pointed out that for a few reasons it was not fully compatible with ERC-20:
- It prevented sending to 0x0
- It blocked transfers of 0 value
- It didn’t return true or false for success or fail
The key cause of concern with this inappropriate implementation is that it will behave in unexpected ways if it is used with a smart contract that requires an ERC-20 token. It might even be forever locked in the deal.
Although standards aren’t always perfect and may become antiquated soon, they encourage the safest smart contracts.
There are many ways to hack your smart contracts, as you can see. Until building, it is important that you fully understand every vector of attack and vulnerability.