Smart Contract Security Challenges

Deric Cheng
Dev Education at Web3U

Unlike most use cases of more traditional programming languages, Solidity contracts tend to transfer substantial amounts of value as one of their core functions, and as a result are exposed to a variety of high-risk attacks from attackers seeking to drain funds from these immutable actors. 

As a result, it’s highly recommended that developers audit or hire an auditing agency prior to launching smart contracts to the mainnet - once it’s been published, it can be very difficult to retroactively fix security vulnerabilities!

The types of potential vulnerabilities, and ways in which these attacks can be executed, are an entire course by itself. Let’s do a quick overview of major vulnerabilities so you’ll have an idea of what to keep an eye out for: 

Reentrancy Attacks

This type of attack is both extremely dangerous, allowing a vulnerable smart contract to be drained of all its ether, and extremely easy to accidentally commit. Reentrancy attacks occur because of two critical features of Solidity: 

  1. Smart contracts execute imperatively - that is, they wait for each line to finish before executing the next line.
  2. Smart contracts can call external, untrusted contracts and wait for the result before proceeding.

Therefore, when a vulnerable contract A makes an external call to another untrusted contract B, it’s possible that the other contract B can be maliciously changed to make a recursive call back to the original contract A. If the call from contract A to B involves sending any amount of Ether, this infinite loop can effectively drain contract A of all its resources before the function finishes.

Here’s a simple example: 

-- CODE language-js line-numbers -- contract A { function withdraw() external { uint256 amount = balances[msg.sender]; (bool success, ) = msg.sender.call.value(amount)(""); require(success); balances[msg.sender] = 0; }}


Typically, the msg.sender is a regular user, such as a Metamask account. In this case, the withdraw() function will simply withdraw some money, update the balances, and conclude. However, if the msg.sender is a malicious contract B, when contract A runs msg.sender.call, contract B can be set up such that it will immediately call contract A again. In that situation, the withdraw() function will be repeatedly called until contract A has no resources or the EVM stack fills up. Pretty dangerous, right? 

For some examples on how to prevent this, take a look at this article here.

Frontrunning

Interestingly enough, smart contracts and transactions become fully public not when they’re confirmed on the blockchain, but the second that you submit them to the network as a pending transaction. These pending transactions are shared throughout the network in the mempools of Ethereum nodes, allowing the miner of a block to select the transactions with the highest gas fees. 

One side effect of this design is that the intended outcome of a smart contract is visible to all for a period of time before it’s confirmed into the blockchain. Say you have a smart contract that when run, will execute an arbitrage that will earn you 1 ETH, and that costs 0.05 ETH to deploy. Malicious actors watching the mempool may see this transaction, recognize the opportunity, and copy your smart contract, submitting it with a gas fee of 0.06 ETH. Then, they’ve successfully “front-run” your contract, stealing your arbitrage opportunity by submitting their transaction first.

In practice, these attacks are often carried out by the miners themselves, resulting in a phenomenon known as MEV (miner extractable value) that is worth thousands of ETH on a daily basis. Unfortunately, they’re fairly difficult to avoid, but a variety of cutting-edge practices are described in this article here. 

Integer Overflow & Underflow

This is a common attack in many programming languages! Here’s the general gist of how this type of attack might happen in Solidity:

  1. Solidity smart contracts are built using 256 bits as the word size, which equate to roughly 4.3 billion. 
  2. When reducing the value of an unsigned integer of 0, it will loop back around to the maximum value.
  3. Therefore, an underflow attack can be executed by having a malicious address that is recorded by the smart contract to have zero balance attempt to send 1 unit of Ether, forcing its balance to cycle all the way back to the maximum value allowed: 4.3 billion. 
  4. Then, because the smart contract believes the address to have a balance of 4.3 billion ether, it may permit any withdrawals from that account to continue until the smart contract is drained of funds.

As you can see, this hack is easily exploitable for any smart contract that internally tracks address balances. To avoid this, simply make sure you’re using a 0.8 version of the Solidity compiler, which automatically checks for overflows and underflows. 

Jeez…What Else? 

Unfortunately, dozens of other major exploits. As many developers have said, “It takes a week to learn Solidity, and three years to avoid writing critical vulnerabilities on a regular basis”. 

It’s sufficient to recognize that it’s highly likely you’ll end up with security issues on any moderately-complicated smart contract that you write, and that if you plan on storing any significant value in the contract you should absolutely have the code audited by a professional agency. 

Happy coding!