Reentrancy Attacks & Ethereum Multiverse Dilemma : How DAO Hackers Exploit Solidity Fallback Function With Example & Prevention Strategy

10 min read

Re-Entrancy Attacks

Chainlink released a new article exploring one of the most infamous Solidity hacks of all time. This event occurred during the formative years of Ethereum smart contract development and was meant to compromise a DAO (decentralized autonomous organization) called The DAO.

Understanding this report will help other blockchains prevent similar attacks (called reentrancy attack) from happening again.

A Brief History of The DAO Hack

By 2015, the nascent Ethereum community was starting to talk about DAOs—decentralized autonomous organizations. The idea behind these blockchain-powered communities was that they could coordinate human effort through the execution of verifiable code (specifically, smart contracts running on the Ethereum blockchain) and through decentralized decision-making on the community’s protocols. In 2016, when the Ethereum mainnet was about a year old, a DAO called “The DAO” was created. It was a decentralized, community-controlled investment fund. It raised $150m worth of ether (about 3.54 million ETH) by selling its own community token. People purchased The DAO’s community token by depositing ETH, and that ETH became the investment funds that The DAO would invest on behalf of its community of token-holding investors.

As it was so early in the evolution of Ethereum, smart contracts, and DAOs, there was a lot of excitement about these unprecedented ways to organize and coordinate human activity.  

Unfortunately, less than 3 months after The DAO’s launch, it was attacked by a “blackhat” hacker. Over the next few weeks, the hacker proceeded to drain most of the $150m worth of ETH from The DAO’s smart contract. The hacker used what has come to be called a “reentrancy” attack. The name is descriptive, and we will dive more into the structure and technique of this attack shortly. But as you can imagine, this was a very serious breach of The DAO, a violation of the investors’ trust, and a significant blow to the credibility of Ethereum.

It also created very deep ideological rifts. While industry participants and critics watched the funds being drained from The DAO, there was extensive debate about the best way to respond. On one end of the ideological spectrum was the view that the promise of cryptographically enabled blockchains was their immutability and tamper-resistance. Intervention, even for the right reasons, was still tampering. A truly trustless and tamper-resistant system would require no intervention, even if the consequences were serious—that is the price to be paid for decentralized tamper resistance. 

On the other hand, people’s savings were being stolen in slow motion and the damage to the public’s confidence and optimism about blockchain technology demanded an intervention, even if it was just to protect people from losing their life savings, and the ethical obligation to prevent theft.

As these debates raged on, a “whitehat” hacker group was assembled to act as a counterstrike force. They were in the for-intervention camp, and they used the same hack—the reentrancy exploit—to attempt to drain The DAO faster than the attacker. The idea was to rescue the funds so that they could be refunded back to investors. And lots of refunds were made to DAO members, though many of the investors had exited the protocol through “escape pods” that let them extract their investment on the way out.

Meanwhile, since the hacker was still draining millions out, the Ethereum core team was faced with a very challenging decision. One way to thwart the hacker would be to fork the Ethereum blockchain. This would be like changing history, where an alternate reality played out. In this example, by forking Ethereum, the new fork would operate as though the hack never happened, and the hacker’s ill-gotten ETH would only be valid on the legacy fork of the network.  If users adopted the new fork and abandoned the old one, the hacker’s ETH would be worth very little. This fork would invalidate the historical blocks that stored the transactions of the hacker’s attack. But this extreme-sounding step was completely contrary to the principles underpinning Ethereum—this sort of intervention is the kind of centralized, “unilateral” action that Ethereum was meant to do away with.

Those who voted in favor of the fork were supporting a world where there would be two parallel Ethereum blockchains. This vote won out with 85% of the votes, and the fork happened (despite some miners resisting as there was no actual defect in the Ethereum protocol). That’s why there are now two Ethereum chains—Ethereum Classic and the Ethereum chain that we know today. Both have their native ETH tokens, and these tokens have very different prices in the market. You can see the Ethereum Foundation’s announcement on the fork here.

The DAO was historically significant, and then the hack and the resulting decisions also became historical. 

But how exactly did the hack work? Let’s explore.

What Is a Reentrancy Attack in Solidity?

Applications that run on the Ethereum blockchain are called “smart contracts” (though there is no legal effect to them). Smart contracts are pieces of code, most commonly written in a language called Solidity, that are executed on the blockchain and can interact with external user accounts and other smart contracts on the Ethereum network. This free interaction between, and composability of, smart contracts is at the heart of their design. Moving payments between accounts is also at the heart of the philosophy. These principles are reflected in the way the Solidity language executes on the Ethereum Virtual Machine

The reentrancy attack exploits the way something called “fallback” functions work. Fallback functions are special constructs in Solidity that are triggered in specific situations. The features of fallback functions are:

  1. They are unnamed.
  2. They are externally called (i.e. they cannot be called from inside the contract in which they are written).
  3. There can be zero or one fallback functions per contract—no more.
  4. They are automatically triggered when another contract calls a function in the fallback’s enclosing smart contract, but that called function name does not match or exist.
  5. They can also be triggered if ETH is sent to the fallback’s enclosing contract, there is no accompanying “calldata” (a data location like memory or storage), and there is no receive() function declared—in this circumstance a fallback must be marked payable for it to trigger and receive the ETH.
  6. Fallback functions can include arbitrary logic in them.

It is the fifth and sixth features of fallback functions that are exploited by the reentrancy hack. The hack also relies on a certain order of operations in the victim contract. So let’s explore how this happens.

In the illustration below, the red and green boxes are smart contracts, and just to make it interesting, I’ve presented the reentrancy attack against the backdrop of The DAO and its famous hack. This is a highly simplified version, stripped down to the essentials to understand reentrancy only, and the code below is not similar to The DAO’s code.

In our illustration, The DAO’s smart contract maintains a state variable called Balances that tracks each investor’s investment in The DAO. This is separate from the smart contract’s ETH balance, which is not stored in a state variable.

The hacker deploys a smart contract that acts as the “investor,” and this contract deposits some ETH into The DAO. This entitles the hacker to later call the withdraw() function in The DAO’s smart contract. When the withdraw() function is eventually called, The DAO’s contract sends ETH to the hacker. But the hacker’s smart contract intentionally does not have a receive() function, so when it receives ETH from the withdraw request, the hacker’s fallback function gets triggered. This fallback function could have been empty and still received the ETH, but instead it has some malicious code in it. 

This code, immediately upon execution, calls The DAO’s smart contract’s withdraw() function again. This sets off a loop of calls because at this point the first call to withdraw() is still executing. It will only finish executing when the hacker contract’s fallback function finishes, but that instead has re-called withdraw(), which kicks off a nested cycle of calls between the hacker contract and The DAO’s smart contract.

A diagram of how the reentrancy attack works

Each time withdraw() is called, The DAO’s smart contract tries to send the hacker an amount of ETH equivalent to the hacker’s deposit. But, crucially, it does not update the hacker’s account balance until after the ETH-sending transaction finishes. But the ETH sending transaction cannot finish until the hacker’s fallback function finishes executing. So the DAO’s contract keeps sending more and more ETH to the hacker without decrementing the hacker’s balance—thus draining The DAO’s funds. 

This will become a little easier to follow in the code walkthrough below.

Reentrancy Attack Code Example

Let’s start with The DAO’s code, in which a specific order of operations creates a vulnerability to a reentrancy attack.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.10;

 contract Dao {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        require(msg.value >= 1 ether, "Deposits must be no less than 1 Ether");
        balances[msg.sender] += msg.value;
    }

    function withdraw() public {
        // Check user's balance
        require(
            balances[msg.sender] >= 1 ether,
            "Insufficient funds.  Cannot withdraw"
        );
        uint256 bal = balances[msg.sender];

        // Withdraw user's balance
        (bool sent, ) = msg.sender.call{value: bal}("");
        require(sent, "Failed to withdraw sender's balance");

        // Update user's balance.
        balances[msg.sender] = 0;
    }

    function daoBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

Note the following:

  • The smart contract maintains a mapping of investor addresses and ETH balances. The invested ETH is held in the contract’s balance itself, which is different from the balances state variable.
  • deposit() requires a minimum contribution of 1 ETH, and once a contribution is received, it increments the investor’s balance.
  • The withdraw() function sends the withdrawn ETH to the investor (using msg.sender.call) before it resets their balance to zero. The send transaction does not finish executing until the hacker’s fallback function finishes executing, so the hacker’s balance is not set to zero until the fallback function finishes. This is the major vulnerability in The DAO’s contract.

Now let’s examine the hacker’s smart contract, which executed the exploit.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.10;

interface IDao {
    function withdraw() external ;
    function deposit()external  payable;
 }

contract Hacker{
    IDao dao; 

    constructor(address _dao){
        dao = IDao(_dao);
    }

    function attack() public payable {
        // Seed the Dao with at least 1 Ether.
        require(msg.value >= 1 ether, "Need at least 1 ether to commence attack.");
        dao.deposit{value: msg.value}();

        // Withdraw from Dao.
        dao.withdraw();
    }

    fallback() external payable{
        if(address(dao).balance >= 1 ether){
            dao.withdraw();
        }
    }

    function getBalance()public view returns (uint){
        return address(this).balance;
    }
}

Things to note here:

  • The attack() function deposits the hacker’s “investment” in The DAO and then kicks off the attack by calling The DAO contract’s withdraw() function, which we deconstructed in the previous paragraph.
  • The fallback function includes the malicious code. It checks that The DAO’s contract still has some ETH left in it and then calls The DAO contract’s withdraw() function. We saw in the previous paragraph that The DAO contract’s withdraw() function does not update the hacker’s account balance so long as the ETH-sending transaction is still executing. And that transaction keeps executing because the hacker’s fallback function keeps calling withdraw(). This drains The DAO contract’s ETH balance without updating the balances state variable. 
  • Once The DAO contract’s ETH balance is drained, the fallback() function will no longer execute the withdraw() function, and that will (finally!) complete the execution of the fallback() function, which will conclude the ETH-send transaction. Only then will the hacker’s account balance be reset to zero, by which time The DAO will have no ETH left.

Fixing Reentrancy Vulnerability

There are a few ways to fix a reentrancy vulnerability, but in our example, the simplest fix is actually to change the order of operations in The DAO’s withdraw() function so the caller’s balance is reset to 0 before The DAO contract sends them their ether using the low-level call function. It would look like this:

Contract Dao {
…

     function withdraw() public {
        // Check user's balance
        require(
            balances[msg.sender] >= 1 ether,
            "Insufficient funds.  Cannot withdraw"
        );
        uint256 bal = balances[msg.sender];

        // Update user's balance.
        balances[msg.sender] = 0;

        // Withdraw user's balance
        (bool sent, ) = msg.sender.call{value: bal}("");
        require(sent, "Failed to withdraw sender's balance");

        // Update user's balance.
        balances[msg.sender] = 0;
    }
}

This way, when the low-level call() function triggers the hacker contract’s fallback() function, and that function tries to re-enter the withdraw() function, the hacker’s balance is zero at the point of re-entry and the require() method will evaluate to false, thus reverting the transaction right there. This will then cause the original call to call() to move to return, and since it failed the value of sent will be false, which will cause the next line (require(sent, “Failed to withdraw sender’s balance”);) to revert.  

The hacker would have withdrawn their deposit and nothing more.

Another option would be for The DAO contract to use function modifiers to “lock” the withdraw() function while it is still executing so that any re-entry is blocked by the lock. We’d achieve this by adding these lines to The DAO contract.

Contract Dao {
   bool internal locked;

   modifier noReentrancy() {
        require(!locked, "No reentrancy");
        locked = true;
        _;
        locked = false;
    }

 //……
    function withdraw() public noReentrancy { 

    // withdraw logic goes here…

    }
}

This reentrancy guard uses what’s known as a mutex (mutually exclusive) flag pattern to protect the withdraw()withdraw() function from being invoked while a previous invocation has not completed. So when the hacker contract’s fallback()fallback() function tries to re-enter The DAO via the withdraw()withdraw() function, the function modifier will get triggered and its require()require() function will cause a revert with the message “No reentrancy”.

Conclusion

This is a highly simplified walkthrough of the concept of reentrancy attacks, using example code rather than production samples. Still, this should give the reader enough information to mitigate the risk of another Solidity attack in their systems.

Via this site

Author

Chris Munch

Chris Munch is a professional cryptocurrency and blockchain writer with a background in software businesses, and has been involved in marketing within the cryptocurrency space. With a passion for innovation, Chris brings a unique and insightful perspective to the world of crypto and blockchain. Chris has a deep understanding of the economic, psychological, marketing and financial forces that drive the crypto market, and has made a number of accurate calls of major shifts in market trends. He is constantly researching and studying the latest trends and technologies, ensuring that he is always up-to-date on the latest developments in the industry. Chris’ writing is characterized by his ability to explain complex concepts in a clear and concise manner, making it accessible to a wide audience of readers.