It is important to understand smart contracts in the domain in which they live: decentralized, asynchronous networks. As a result of living in this ecosystem, there are security considerations that are not always obvious and can lead to issues. To illustrate, we are going to look into two related functions of the ERC20 standard: approve and transferFrom. Here is code for the approve function from OpenZeppelin:
function approve(address _spender, uint256 _value) public returns (bool) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
The approve function allows a token owner to say that they have approved a transfer of their token to another account. Then, in response to different events, a future transfer can take place. How this happens depends on the application, but such as the token sale, by approving a transfer, a blockchain application can later call transferFrom and move the tokens, perhaps to accept payment and then perform actions. Let's look at that code:
function transferFrom(address _from,address _to,uint256 _value) public returns (bool) {
require(_to != address(0)); // check to make sure we aren't transfering to nowhere.
// checks to ensure that the number of tokens being moved is valid.
require(_value <= balances[_from]);
require(_value <= allowed[_from][msg.sender]);
// execute the transfer.
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
//Record the transfer to the blockchain.
emit Transfer(_from, _to, _value);
// let the calling code or app know that the transfer was a success.
return true;
}
The two functions work together. The user wishing to use the app uses approve to allow payment, and the app calls transferFrom in order to accept. But because of the asynchronous nature of the calls, it is possible for flaws to exist.
Imagine an app where users can pay tokens in order to join a digital club—40 tokens for a basic membership and 60 tokens for an enhanced membership. Users can also trade the tokens to other people or sell them as they wish. The ideal case for these two functions is where a user approves 40 tokens and the application registers this and calls transferFrom to move the 40 tokens, and then grants access as part of the smart contract. So far so good.
It's important to keep in mind that each action here takes time, and the order of events is not fixed. What actually happens is that the user sends a message to the network, triggering approve, the application sends another message, triggering transferFrom, and then everything resolves when the block is mined. If these transactions are out of order (transferFrom executing before approve), the transaction will fail. Moreover, what if the user changes their mind and decides to change their approval from 40 to 60? Here is what the user intends:
- User: approve 40 (block 1)
- User: approve 60 (block 1)
- App: transferFrom 60 to App (block 1)
- App: Grant enhanced membership (block 2)
In the end, the user paid 60 tokens and got what they wanted. But because each of these events are asynchronous and the order is decided by the miners, this order is not guaranteed. Here, is what might happen instead:
- User: approve 40 (block 1)
- App: transferFrom 40 to App (block 1)
- User: approve 60 (block 2, as the miners did not include it in block 1)
- App: transferFrom 60 to App (Block 2)
Now the user has paid 100 tokens without meaning to. Here is yet another permutation:
- User: approve 40 (block 1)
- User: approve 60 (block 1)
- App: transferFrom 40 to app (block 2)
- App: Grants basic membership (block 2)
- App: transferFrom 60 to app (block 3) | fails
At the end of this sequence, the user still has 20 tokens approved, and the attempt to get the enhanced membership has failed. While an app can and should be written without these issues by doing such things as allowing upgraded membership for 20 tokens and checking the max approval before transferFrom is called, this attention to detail is not guaranteed or automatic on the part of application authors.
The important thing to understand is that race conditions and ordering issues are extremely important in Ethereum. The user does not control the order of events on a blockchain, nor does an app. Instead, it is the miners that decide which transactions occur in which blocks and in which order. In Ethereum, it is the gas price that affects the priority that miners give transactions. Other influences can involve the maximum block gas limit, the number of transactions already in a block, and whether or not a miner that successfully solves a block has even seen the transaction on the network. For these reasons, smart contracts cannot assume that the order of events is what is expected.