Why ERC827 can make you vulnerable to reentrancy attacks — and how to prevent them

Leonard von Kleist
ChainSecurity
Published in
4 min readJun 1, 2018

The ERC20 standard is currently the most widely used standard for tokens on the Ethereum blockchain. However, unlike transactions using Ethereum’s native currency Ether, an ERC20 token only allows the transfer of value, not value and data. One possible solution that has been gaining popularity is the ERC827 standard. However, along with the new opportunities of ERC827 come new vulnerabilities.

ERC827 vs ERC20: What’s the difference?

ERC827 is an extension of ERC20. The three functions that are new in ERC827 are: approveAndCall(), transferAndCall(), and transferFromAndCall().

The difference between the ERC827 functions and their ERC20 counterparts is that in addition to what they do in ERC20, they also call _to.call(_data) on the _to contract to whom the money is being sent. Let’s look at transferAndCall() as an example.

Here is the code for the transferAndCall() function in ERC827:

We see that transferAndCall() calls ERC20’s transfer()function (ERC827 inherits from ERC20), and makes a call to the contract passed in with the _to parameter. If the sender doesn’t have enough funds, or if this function call does not return successfully, transferAndCall() reverts (for more information check out https://github.com/ethereum/EIPs/issues/827).

Reentrancy in ERC827

The aforementioned call to the _to contract introduces a new and potentially unexpected control flow transfer. If the _to contract has a fallback function it will be executed and could, for example, be used to perform a reentrancy attack. This can be best explained with an example:

Let’s look at OpenZeppelin’s IndividuallyCappedCrowdsale, a contract already used in production by many projects on the Ethereum blockchain. IndividuallyCappedCrowdsale is supposed to limit individual contributions to a crowdsale, e.g. a single account may not contribute more than 0.5 ETH. At the moment, this code makes use of the transfer() function as specified in the ERC20 standard, so it is safe. However, if this code is used with ERC827 tokens and the new transferAndCall()function, it would be vulnerable to reentrancy.

Why is this the case? IndividuallyCappedCrowdsale inherits from Crowdsale. In Crowdsale.sol, the functionbuyTokens() is defined as follows:

buyTokens() calls _processPurchase(), which calls _deliverTokens(), which then calls transfer().

After _processPurchase() has executed successfully, buyTokens()calls _updatePurchasingState()to update the record of how many Wei the user has contributed, ensuring that no one can spend more than allowed by their individual sales cap. If line 122 in the above code were to call transferAndCall(_beneficary, _tokenAmount, "")instead of transfer(_beneficary, _tokenAmount), this would make the contract vulnerable to a reentrancy attack. As we saw above, the ERC827 transferAndCall() function not only transfers tokens to the receiving account, but also calls that account’s fallback function (because no _data parameter is passed to transferAndCall()). Now, suppose that the attacking account’s fallback function contains another call to buyTokens(). This call would be executed before _updatePurchasingState(), allowing the attacker to buy as many tokens as he wants, bypassing the individual sales cap.

Suppose the attacking contract (let’s call it Eve) wants to call buyTokens() twice. The chain of events would be:

Interaction between IndividuallyCappedCrowdsale(ICC) and Eve

As we can see, because transferAndCall()calls Eve’s fallback function before _updatePurchasingState()is called, Eve could make as many calls to buyTokens()as she wants, with_updatePurchasingState()only being executed after all the tokens have been transferred. In this case, it would be advisable to call _updatePurchasingState()before _processPurchase(), so that if someone tries to call buyTokens()reentrantly, the sales cap will still be enforced correctly.

To help you better understand how this vulnerability works, we’ve replicated it ourselves. Have a look at the code on GitHub here.

In the code on GitHub, we have a contract called VulnerableCrowdsale for an ERC827 Token called CarelessCrowdsaleCoin. The crowdsale allows its owner to decide how many Wei any account is allowed to contribute. This is called the account’s sales cap. It inherits from a modified version of OpenZeppelins Crowdsale contract, which uses the new ERC827 transferAndCall()function instead of the old ERC20 transfer():

This simple change introduces a reentrancy vulnerability. Can you see it?

Suppose there’s a malicious contract, Eve, with a sales cap of 100 (i.e. Eve can’t buy more than 100 Wei worth of CarelessCrowdsaleCoin). BuyTokens()checks that Eve hasn’t reached her sales cap yet, calls transferAndCall()to transfer the tokens, and then updates Eve’s entry in the contributions[] mapping. However, recall that the transferAndCall() function also makes a call to Eve when it transfers the tokens. Eve has a fallback function that calls buyTokens() again, so she can buy more tokens before her entry in contributions[] is updated.

This means that she can bypass her sales cap.

When we run our test cases, we see that the sales cap works effectively when Eve tries to buy 200 tokens at once:

But fails if eve buys 200 tokens using several reentrant calls from her fallback function:

We also see that if we change the buyTokens() function so that _updatePurchasingState()is called before_processPurchase(), the reentrancy attack doesn’t work anymore(Try this out for yourself!)

Takeaway

Code that is safe for ERC20 may not be safe for ERC827, so make sure that you carefully look at the changes between the two standards (Hint: transferAndCall, transferFromAndCall and approveAndCall) and double check your code for possible reentrancy vulnerabilities before you make the switch.

--

--