Flow Community Rewards are here. Earn points for engaging in the ecosystem, spend points on prizes. Learn more.
Developers
October 31, 2023
How we could have prevented this Solidity Vulnerability: The DAO
Joshua Hannan
How we could have prevented this Solidity Vulnerability: The DAO

Welcome to my series of blogs about common and high-profile vulnerabilities in Solidity smart contracts. In this series, I’ll explain how different vulnerabilities have been exploited in Solidity smart contracts and how choosing languages like Solidity actually sets developers up for difficulty from the beginning. Solidity requires developers to learn all of the extra coding patterns and best practices to write secure code. These coding patterns and best practices are difficult to understand and implement while keeping your contract performant and clear.. You can’t trust all developers to do that. Developers should get to focus on building awesome experiences instead of fixing security footguns. I would know. I’m one of them. 

I’ve been in the web3 smart contract world since early 2017, establishing myself as a solidity developer and security auditor while working on projects like modular libraries and the first interactive ICO implementation. I’ve been around for most of the hacks that have happened to smart contracts so I know exactly how it can negatively affect users and developers. I joined the Flow team in 2019 and have been working on designing more secure smart contracts experiences ever since.

The DAO Hack

The first hack I’ll discuss is ‘The DAO’, a significant event in the history of cryptocurrency. The DAO, or Decentralized Autonomous Organization, was a blockchain-based investment fund that raised $150 million in Ether through a token sale in 2016. The ETH was held in a smart contract that was collectively managed by token holders. Unfortunately, the DAO was hacked, and the hacker managed to drain most of the funds from the smart contract. The hack used what is now a well known "reentrancy attack," where the hacker recursively withdrew funds from the smart contract without their balance being updated.

Here is a basic example of a Solidity contract that can be hacked with reentrancy. The DAO was slightly more complex, but the principle is the same.

Contract
GroupEtherBank {

       uint256 public withdrawalLimit = 1 ether;
       mapping(address => uint256) public lastWithdrawTime;
       mapping(address => uint256) public balances;

       function depositFunds() public payable {
              balances[msg.sender] += msg.value;
       }

       function withdrawFunds (uint256 _weiToWithdraw) public {
           // make sure the caller has enough balance to withdraw
           require(balances[msg.sender] >= _weiToWithdraw);
           // make sure they aren’t withdrawing more than the limit
           require(_weiToWithdraw <= withdrawalLimit);
           // Make sure they have waited long enough to withdraw
           require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
           // Send the Ether to the caller
           require(msg.sender.call.value(_weiToWithdraw)());
           // Update the caller’s balance and last withdraw time
           balances[msg.sender] -= _weiToWithdraw;
           lastWithdrawTime[msg.sender] = now;
       }
}

The withdraw method first validates that the caller is authorized to withdraw their tokens before sending the ETH with msg.sender.call and updating the balances in the contract. Seems fine, right? Nope! I told you not to trust me!

The vulnerability lies in Solidity’s strange built-in fallback function.  

The fallback function is a function that can be included in a smart contract that executes some code when ether is sent to it without a corresponding function call.

contract FallbackExample {

    boolean public calledFallbackFunction;

    fallback() external payable {
        calledFallbackFunction = true;
    }
}

Creative use of fallback functions has been the vector by which many Solidity smart contracts have been hacked.

The contract that got hacked doesn’t have a fallback function, but the contract that hacks it does. In the Bank contract’s withdraw function, the balances and other values are updated after the Ether is sent instead of before.

   require(msg.sender.call.value(_weiToWithdraw)());
    // Update the caller’s balance and last withdraw time
    balances[msg.sender] -= _weiToWithdraw;

The attacker deployed a smart contract that masqueraded as an "investor" and deposited some ETH into the DAO. This entitled the hacker to later call the withdraw() function in the DAO's smart contract. When the withdraw() function was eventually called, the DAO's contract sent ETH to the hacker. However, the hacker's smart contract intentionally did not have a receive() function, so when it received ETH from the withdraw request, the hacker's fallback function got triggered. This allowed the hacker’s fallback function to repeatedly call the withdraw method to withdraw funds from the DAO contract. Since the send happens before the balance is updated, it never is reached, and all the tokens get withdrawn from the DAO.

Reentrancy Attacks

From https://www.geeksforgeeks.org/reentrancy-attack-in-smart-contracts/#

Reentrancy comes in various forms and is a common vulnerability of smart contracts, and it can exist in smart contracts on various blockchain platforms, other languages such as Vyper and Rust are also susceptible to this problem. Of course, you can just tell developers to use a correct coding pattern like moving the balance update to before the Ether transfer to avoid it. But as we’ve established, most developers are lazy and can’t be trusted. Case in point, I wrote a paragraph of this blog using an AI. Try to guess which one! Just kidding, pay attention to the actual content! (We’re also easily distracted)

Fortunately, smart contract engineering standards have improved a lot since 2016 and there is a new programming paradigm called resource-oriented programming that helps fix many of these problems, as well has a greater reliance on a strong type system and other improvements that were not obvious back then. Resource oriented languages like Cadence and Move have emerged in recent years for blockchains like Flow, Aptos, and Sui and make smart contract developers lives much easier.

Solving Reentrancy with Cadence

How does a resource oriented programming language like Cadence help lazy developers like me avoid vulnerabilities like the DAO hack? As you’ll see in most of the articles in this series, the second you start developing in Cadence, you gain a superpower. It doesn’t require any extra work besides learning the language. It just comes by default. That superpower is that in order to avoid these vulnerabilities, you are not required to have read an obscure corner of a documentation page for every feature or use specific safe libraries. You get security and safety simply because you are using a better language.

I know, who would have thought that some languages are just better suited to different domains than others, especially when they are built with 10 years of knowledge gained from using the other ones!

In a strongly typed resource-oriented language like Cadence, security protections are built-in to the language itself, so a developer would have to actively intend to build most of these vulnerabilities into their smart contracts to be susceptible to them. This allows developers to focus on what truly matters, a great experience for their users.

First of all, Cadence doesn’t have a fallback function, so this exact version of this vulnerability is not even possible to begin, but even if there was something like it, Cadence has additional protections to make it safer for developers and users.

Let me explain with an example of how this Group Bank smart contract would work in Cadence. I’ll use simplified Cadence code to make this easier to understand for newcomers.

The biggest difference between a language like Solidity and resource oriented languages is how ownership of assets is tracked. In most languages, including Solidity, ownership is tracked via a ledger in a central location. When you transfer tokens, your balance is reduced in the ledger and the address you are transferring tokens to has its balance increased.

In a resource oriented language, assets are actually represented as objects, called resources, that are directly stored in your account’s storage. This is much more analogous to the real world because you can think of assets in a resource oriented language like physical objects or cash that you store in your house or your purse, respectively. When you want to transfer ownership of the assets, you literally move them from your account’s storage to the account storage of the person you are transferring to. Even MORE decentralization!

Caption: Comparing assets to how they work in the real world is actually helpful when thinking about many different aspects of designing smart contracts in Cadence!

What does this have to do with preventing re-entrancy, you might ask? 

An important consideration is that with this model, each asset stores its own metadata, including balance. So an object in Cadence that tracks a fungible token balance, like Ether, would look something like this:

access(all) resource Vault {
 
   access(all) var balance: UFix64

}

Just like a cash bill has a number on it that says how much it is worth, each Vault has a balance field on it that indicates how much it is worth. But, unlike real cash, I can combine multiple Vaults into a single vault that has a sum of the combined balances! You unfortunately cannot do that by smashing two bills together. Trust me, I’ve tried.


The same works in reverse, when transferring tokens, one vault is split into two vaults so you can keep the tokens that you do not transfer, and the second vault can be moved to a different account.

Caption: Disclaimer: Do not try to reproduce in real life by ripping a cash bill in half.

Here is the function, defined within the Vault resource that is called to achieve this:

       // Only the person who literally has possession of the Vault object can call this
       access(Withdrawable) fun withdraw(amount: UFix64): @Vault {
            self.balance = self.balance - amount
            return <-create Vault(balance: amount)
        }

Compare that to the withdraw function in the solidity contract above:

// Two operations are required to move tokens
require(msg.sender.call.value(_weiToWithdraw)());
balances[msg.sender] -= _weiToWithdraw;

As you can see in our Cadence code, since we actually have to return the withdrawn Vault in the return statement of the function, there is no way for a function to re-enter the withdraw method. Additionally, there is a built in pre-condition to the fungible token standard that requires that there has to be enough balance to withdraw, so that would also check and fail the withdraw condition before the attacker is able to drain all the funds. 

Bank Contract in Cadence

Here is how a simple Bank like the one above could be built in Cadence:

access(all) contract GroupFLOWBank {

    /// Dictionary (Mapping) that stores each users' deposits
    /// Key: The ID of the user who has deposited
    /// Value: The Vault that the user deposited
    access(contract) let deposits: @{UInt64: FlowToken.Vault}

    /// Any user can call deposit to store their vault here
    /// They get a resource object back that they can use to withdraw their deposit
    access(all) fun deposit(from: @FlowToken.Vault): @DepositManager {
        let manager <- create DepositManager()

        // Store the depositor's vault at the UUID of their DepositManager resource
        self.deposits[manager.uuid] <- from

        // Return their manager to them
        return <-manager
    }

    /// Object that the owner of a deposited vault stores in their private account
    /// that gives only them access to only the vault that they deposited
    access(all) resource DepositManager {
        access(all) fun withdraw(amount: UFix64): @FlowToken.Vault {
            // Get a reference to the vault that they deposited
            let depositedVaultRef = (&GroupFLOWBank.deposits[self.uuid] as &FlowToken.Vault?)!

            // Return the amount of FLOW they want to withdraw
            return depositedVaultRef.withdraw(amount: amount)
        }
    }
}

This might be a little confusing to you if you’ve never seen a resource-oriented programming language before. In a Cadence contract, most functionality is defined within resources, so it is not public by default because resources are stored in an account’s private storage.

In the Bank contract, each user’s deposited tokens are stored in their own vault instead of a global pool and only they can call their withdraw method because the withdraw method is defined in the resource object that they store in their private account storage, not in a public function like it is in Solidity! Also, since the withdraw method is tied to the resource, it can only access the corresponding Vault stored with their resource’s UUID. Additionally, because the reduction of the balance is integral to the withdrawal process, a reentrant function could not call the withdraw method again because the tokens have already been subtracted and moved away. The malicious withdrawal would simply fail. 

Conclusion

I hope after reading this, you can understand a little more about why resource-oriented programming is a safer and more robust way to build smart contracts. The programming languages we use should help protect us from potential problems, not leave us helpless to make simple coding mistakes that could cost us millions of dollars. I’ve only scratched the surface of what resource-oriented programming makes possible, but I’ll be exploring more in future posts, so subscribe and stay tuned!

I highly recommend checking out the Cadence documentation and tutorials to learn more about this programming paradigm. See you next time!