Preventing Reentrancy Attacks in Smart Contracts

katelynsills

Kate Sills

Posted on October 5, 2020

Preventing Reentrancy Attacks in Smart Contracts

TLDR: Reentrancy attacks can be entirely prevented with eventual-sends. Eventual-sends (think JavaScript promises — promises actually come from eventual-sends!) allow you to call a function asynchronously and receive a promise, even if the function is on another machine, another blockchain, or another shard, making sharding and cross-chain contract communication much easier.

Photo by [Tim Gouw](https://unsplash.com/@punttim?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)Photo by Tim Gouw on Unsplash

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.

Interleaving Hazards

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[1] 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:

function makeBankAccount(balance) {
  stateHolder.updateState(balance);
  return {
    withdraw(amount) {
      balance -= amount;
      stateHolder.updateState(balance);
    },
    deposit(amount) {
      balance += amount;
      stateHolder.updateState(balance);
    },
    getBalance() {
      return balance;
    },
  };
}

const bankAccount = makeBankAccount(4000);
Enter fullscreen mode Exit fullscreen mode

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:

function makeStateHolder() {
  let state = undefined;
  const listeners = [];

  return {
    addListener(newListener) {
      listeners.push(newListener);
    },
    getState() {
      return state;
    },
    updateState(newState) {
      state = newState;
      listeners.forEach(listener => listener.stateChanged(newState));
    },
  };
}

const stateHolder = makeStateHolder();
Enter fullscreen mode Exit fullscreen mode

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:

const financeListener = {
  stateChanged(state) {
    if (state < 4000) {
      bankAccount.deposit(1000);
    }
  },
};
Enter fullscreen mode Exit fullscreen mode

The other listener just displays our account balance on our dashboard webpage (we’ll simulate this with a console.log 😃):

const webpageListener = {
  stateChanged(state) {
    console.log('DISPLAYED BALANCE', state);
  },
};
Enter fullscreen mode Exit fullscreen mode

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:

stateHolder.addListener(financeListener);
stateHolder.addListener(webpageListener);

bankAccount.withdraw(100);
Enter fullscreen mode Exit fullscreen mode

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:

  1. financeListener gets notified that the balance is $3,900 and deposits $1,000 in response.

  2. 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.

  3. financeListener gets notified that the balance is $4,900 and does nothing because the balance is over $4,000.

  4. webpageListener gets notified that the balance is $4,900, and displays $4,900.

  5. 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:

  1. The solution is not robust (the solution fails if conditions change slightly)

  2. The solution doesn’t solve all interleaving hazards

  3. 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

Emin Gün Sirer suggests:

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:

  1. 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.

  2. Less repeated implementation work (DRY): Instead of having umpteen different implementations of paging, there is just one that is used everywhere.

  3. 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.

You might have noticed that promises in JavaScript have a lot in common with eventual-sends. That’s not a coincidence — promises in JavaScript are direct descendants of eventual-sends, and come from work by Dean Tribble and Mark S. Miller of Agoric. (There’s a great video on the origin of promises that explains more).

In the late 1990s, Mark S. Miller, Dan Bornstein, and others created [the programming language E](http://www.erights.org/talks/promises/paper/tgc05.pdf), 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](http://dist-prog-book.com/chapter/2/futures.html#Joule)). 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](http://dist-prog-book.com/chapter/2/futures.html#brief-history). Image courtesy of Prasad, Patil, and Miller.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.

Let’s use JavaScript promises to prevent the interleaving hazard in our example. What we want to do is turn any immediate calls between the bankAccount object and our listeners into asynchronous calls. Now our stateHolder will notify the listeners asynchronously:

updateState(newState) {
  state = newState;
  listeners.forEach(listener => {
    Promise.resolve(listener).then(ev => ev.stateChanged(newState));
  });
},
Enter fullscreen mode Exit fullscreen mode

And we do the same thing to the deposit call in our financeListener:

const financeListener = {
  stateChanged(state) {
    if (state < 4000) {
      Promise.resolve(bankAccount).then(ba => ba.deposit(1000));
    }
  },
};
Enter fullscreen mode Exit fullscreen mode

In our new version that includes promises, our display updates correctly, and we’ve prevented our interleaving hazards!

There is one major distinction between JavaScript promises and eventual-sends: eventual-sends, unlike JavaScript promises, can be used with remote objects. For example, with eventual-sends we can read a file on a remote machine (the ‘~.’ is syntactic sugar) [2]:

const result = disk~.openDirectory("foo")~.openFile("bar.txt")~.read();
Enter fullscreen mode Exit fullscreen mode

Sharding

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 **[3]. 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 roundtripsPromise pipelining can substantially reduce the number of roundtrips

Conclusion

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.

Footnotes

[1] This example comes from Chapter 13 of Mark S. Miller’s thesis, *Robust Composition: Towards a Unified Approach to Access Control and Concurrency Control, *and was rewritten in JavaScript.

[2] The JavaScript promises in this example are only the equivalent to an eventual send for local objects, and for promises to local objects. For remote objects, a different API is required. For instance, Kris Kowal’s Q and Q-connection libraries allow for:

Promise.resolve(listener).invoke(‘stateChanged’, newState);

which we can write using the syntactic sugar for eventual sends:

listener~.stateChanged(newState);

[3] 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

💖 💪 🙅 🚩
katelynsills
Kate Sills

Posted on October 5, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related