Solidity Security – Understanding Race Conditions in Contract Development

contract-developmentSecuritysolidity

I've come across an example of how a race condition can happen in a scenario like this that I don't understand how it could occur:

Bob creates RaceCondition(100, token). Alice trusts RaceCondition with
all its tokens. Alice calls buy(150) Bob sees the transaction, and
calls changePrice(300). The transaction of Bob is mined before the one
of Alice and as a result, Bob received 300 tokens.

The example code has something like this:

contract RaceCondition{
    address private owner;
    uint public price;
    ERC20 token;

    function RaceCondition(uint _price, ERC20 _token)
        public 
    {
        owner = msg.sender;
        price = _price;
        token = _token;
    }

    // If the owner sees someone calls buy
    // he can call changePrice to set a new price
    // If his transaction is mined first, he can
    // receive more tokens than excepted by the new buyer
    function buy(uint new_price) payable
        public
    {
        require(msg.value >= price);

        // we assume that the RaceCondition contract
        // has enough allowance
        token.transferFrom(msg.sender, owner, price);

        price = new_price;
        owner = msg.sender;
    }

    function changePrice(uint new_price){
        require(msg.sender == owner);
        price = new_price; 
    }

}

However, I don't understand how a race condition can happen with the given example.

  1. Is the example implying that even though Bob had sent in his transaction later than Alice, Bob likely paid a higher gas fee so that his transaction gets mined before Alice's?
    • If this was the case, are we assuming that Alice's and Bob's transactions are sent to the same block which could allow this to happen? But this would mean Bob has to make his transaction within split seconds right after Alice's transaction to be in the same block and also be in time enough to have his transaction mined before hers?
  2. The example mentioned that Bob can call changePrice after Alice called buy to receive more tokens than expected. But I don't see how this can happen:
    • Assuming Bob managed to get his call to changePrice(300) to be locked in before Alice's buy(150) call, Alice's buy(150) call would throw exception because require(msg.value >= price); would return false, wouldn't it? I.e., 150 >= 300 is false, hence, reverts, wouldn't it?

Best Answer

Looks like a horrible example.

To begin with, there is zero talk about how much Ethers the users are sending with their transactions. That's what matters, because the code makes checks against msg.value. It simply looks like the writer has mixed up msg.value with the price (or new_price) parameter.

I'm not sure if you can call anything in the blockchain a race condition. I guess, maybe, you can, if two transactions try to do the same thing which only one can do. But the term "race conditions" is usually used when there is some degree of parallelism, and the blockchain has zero parallelism when executing transactions.

Anyway, if in the example there was a function which buys as many tokens as the caller can get with X Ethers, it would be a different story. Then an attacker (owner) could enter a transaction which makes the token price higher, and this transaction gets mined before the buyer's. This basically happens in decentralized exchanges all the time, in a bit more complicated manner.

Furthemore, transaction are not "sent to a block". They are sent to a pool of transactions, and the miners decide in which block to include which transaction.

Related Topic