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:https://medium.com/media/da9a1ee8b6e7152ff1db29f421897912
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:https://medium.com/media/abec87e2005d37e5dbbb7922183e95c1
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:https://medium.com/media/b3cc6a19a3585d10855da978cd2aa6cc
The other listener just displays our account balance on our dashboard webpage (we’ll simulate this with a console.log 😃):https://medium.com/media/351ede3bf6af81337d5b761846a041d5
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:https://medium.com/media/6015abbaf6a15537d135cf30dd636e4c
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” (
) 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 (
): 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.
In the late 1990s, Mark S. Miller, Dan Bornstein, and others created the programming language E, which is an object-oriented programming language for secure distributed computing. E’s interpretation and implementation of promises were a major contribution. E inherited concepts from Joule (Tribble, Miller, Hardy, & Krieger, 1995). Promises were even present in the Xanadu project back in 1988. More information on the history of promises can be found in the textbook Programming Models for Distributed Computation. Image courtesy of Prasad, Patil, and Miller.
And we do the same thing to the deposit call in our financeListener:https://medium.com/media/a05ebe342f8d73931ffb43c7c58a4478
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.
Promise pipelining can substantially reduce the number of roundtrips
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.