On January 15th, a group of key stakeholders chose to halt the Ethereum “Constantinople” upgrade. It was only a day before Constantinople was supposed to take effect, but Chain Security had released a blog post that pointed out that the new reduced gas costs would bypass some previously “reliable” defenses against reentrancy attacks. The Ethereum community worked quickly and transparently to postpone the upgrade so that more investigation could be done.
We wanted to take this opportunity to bring attention to the class of problems that reentrancy attacks are part of, and how certain designs can eliminate the entire class of problems altogether.
Ethereum’s reentrancy attacks are just one part of a larger class of problems, called interleaving hazards. We might think that because Ethereum runs sequentially, it can’t possibly have interleaving hazards. But surprisingly, even entirely sequential programs can have interleaving hazards.
Here’s an example that is entirely synchronous and sequential, but has a major interleaving hazard. In this example, we have a bank account that we can deposit to and withdraw from:
Whenever we do something that changes the balance, we want to update the state with our new balance and notify our listeners. We do this with a stateHolder:
Let’s say we have two listeners. One is a financial application that deposits to our account if our balance drops below a certain level:
The other listener just displays our account balance on our dashboard webpage (we’ll simulate this with a console.log 😃):
Nothing to worry about here, right? Let’s see what happens when we execute it. We add the listeners and withdraw $100 from our account:
Our bank account starts off with a balance of $4000. Withdrawing $100 updates the balance to be $3900, and we notify our listeners of the new balance. The financeListener deposits $1000 in reaction to the news, making the balance $4,900. But, our website shows a balance of $3,900, the wrong balance! 😱
Why does this happen? Here’s the sequence of events:
- financeListener gets notified that the balance is $3,900 and deposits $1,000 in response.
- The deposit triggers a state change and starts the notification process again. Note that the webpageListener is still waiting to be notified about the first balance change from $4000 to $3900.
- financeListener gets notified that the balance is $4,900 and does nothing because the balance is over $4,000.
- webpageListener gets notified that the balance is $4,900, and displays $4,900.
- webpageListener finally gets notified that the balance is $3,900 and updates the webpage to display $3,900 — the wrong balance.
We’ve just shown that even entirely synchronous programs — programs that have nothing to do with smart contracts or cryptocurrencies — can still have major interleaving hazards.
How can we eliminate interleaving hazards?
A number of people have proposed solutions for interleaving hazards, but many of the proposed solutions have the following flaws:
- The solution is not robust (the solution fails if conditions change slightly)
- The solution doesn’t solve all interleaving hazards
- The solution restricts functionality in a major way
Let’s look at what people have proposed for Ethereum.
Resource constraints as a defense against interleaving hazards
Consensys’ “Recommendations for Smart Contract Security in Solidity” states the following:
someAddress.send()and someAddress.transfer() are considered safe against reentrancy. While these methods still trigger code execution, the called contract is only given a stipend of 2,300 gas which is currently only enough to log an event… Using send() or transfer() will prevent reentrancy but it does so at the cost of being incompatible with any contract whose fallback function requires more than 2,300 gas.
As we saw in the Constantinople upgrade, this defense fails if the gas required to change state is less than 2,300 gas. Over time, we would expect the required gas to change, as it did with the Constantinople update, so this is not a robust approach (flaw #1).
Call external functions last, after any changes to state variables in your contract
Solidity’s documentation recommends the following:
“Write your functions in a way that, for example, calls to external functions happen after any changes to state variables in your contract so your contract is not vulnerable to a reentrancy exploit.”
However, in the example above, all of the calls to the external listener functions in withdraw and deposit happen after the state change. Yet, there is still an interleaving hazard (flaw #2). Furthermore, we might want to call multiple external functions, which would be then be vulnerable to each other, making reasoning about vulnerabilities a huge mess.
Don’t Call Other Contracts
do not perform external calls in contracts. If you do, ensure that they are the very last thing you do. If that’s not possible, use mutexes to guard against reentrant calls. And use the mutexes in all of your functions, not just the ones that perform an external call.
This is obviously a major restriction in functionality (flaw #3). If we can’t call other contracts, we can’t actually have composability. Furthermore, mutexes can result in deadlock and are not easily composable themselves.
It’s hard to avoid programming overcomplicated monoliths if none of your programs can talk to each other.
— “The Art of Unix Programming”
What do we mean by composability and why do we want it?
StackOverflow gives us an excellent explanation of composability:
“A simple example of composability is the Linux command line, where the pipe character lets you combine simple commands (ls, grep, cat, more, etc.) in a virtually unlimited number of ways, thereby “composing” a large number of complex behaviors from a small number of simpler primitives.
There are several benefits to composability:
- More uniform behavior: As an example, by having a single command that implements “show results one page at a time” (
more) you get a degree of paging uniformity that would not be possible if every command were to implement their own mechanisms (and command line flags) to do paging.
- Less repeated implementation work (DRY): Instead of having umpteen different implementations of paging, there is just one that is used everywhere.
- More functionality for a given amount of implementation effort: The existing primitives can be combined to solve a much larger range of tasks than what would be the case if the same effort went into implementing monolithic, non-composable commands.”
There are huge benefits to composability, but we haven’t yet seen a smart contract platform that is able to easily compose contracts without interleaving hazards. This needs to change.
What is the composable solution?
We can solve interleaving hazards by using a concept called eventual-sends. An eventual-send allows you to call a function asynchronously, even if it’s on another machine, another blockchain, or another shard. Essentially, an eventual-send is an asynchronous message that immediately returns an object (a promise) that represents the future result. As the 2015 (prior to the DAO attack) Least Authority security review of Ethereum pointed out, Ethereum is extremely vulnerable to reentrancy attacks and if Ethereum switched to eventual-sends, they would eliminate their reentrancy hazards entirely.
And we do the same thing to the deposit call in our financeListener:
In our new version that includes promises, our display updates correctly, and we’ve prevented our interleaving hazards!
In addition to eliminating re-entrancy attacks such the DAO attack, eventual-sends allow you to compose contracts over shards and even over blockchains, because your execution model is already asynchronous. If we are going to scale and interoperate, the future for blockchain must be asynchronous.
Limitations and Tradeoffs
There are a few tradeoffs in choosing eventual-sends. For instance, debugging in an asynchronous environment is generally harder, but work has already been done to allow developers to browse the causal graph of events in an asynchronous environment.
Another limitation is that asynchronous messages seem less efficient. As Vitalik Buterin has pointed out, interacting with another contract might require multiple rounds of messaging. However, eventual-sends make things easier by enabling promise pipelining . An eventual-send gives you a promise that will resolve in the future, and you can do an eventual-send to that promise, thus composing functions and sending messages without having to wait for a response.
Agoric smart contracts use eventual-sends which eliminate the entire class of interleaving hazards. Compared to other proposed solutions, eventual-sends are more robust, more composable, and enable much more functionality, including even enabling communication across shards and across blockchains.
Thus, smart contract platforms *can* prevent reentrancy vulnerabilities. Instead of relying on fragile mechanisms such as gas restrictions, we need to scrap synchronous communication between smart contracts and use eventual-sends.
which we can write using the syntactic sugar for eventual sends:
 Miller M.S., Tribble E.D., Shapiro J. (2005) Concurrency Among Strangers. In: De Nicola R., Sangiorgi D. (eds) Trustworthy Global Computing. TGC 2005. Lecture Notes in Computer Science, vol 3705. Springer, Berlin, Heidelberg