0

This is the classic reentry attack:

function withdrawBalance() public {
    uint amountToWithdraw = userBalances[msg.sender];
    (bool success, ) = msg.sender.call.value(amountToWithdraw)(""); 
    require(success);
    userBalances[msg.sender] = 0;
}

I don't understand how this works. It seems one transaction is not completed before the other. I know that in the Dao attack a contract using a fallback function on call.value() rigged the system to stall before adjusting the state variable userBalances[msg.sender], but if this is merely due to the extra time created via this fallback function, it suggests an improbable but possible problem.

Say I have 10 tokens in contractBalance, and users can put them into their contract balance.

function takeAllTokens(uint x) public {
    require(x < contractBalance);
    uint amountToTake = contractBalance;
    uint y = x + 1; // just to give it a couple pico seconds
    contractBalance -= x;
    userBalances[msg.sender] = contractBalance;
}

If transactions are not processed completely in sequence, two people arriving simultaneously asking for 10 tokens would each end up getting 10 tokens, more than is in the contractBalance in this example.

Is this only impossible because in this latter case, the time needed to go from one line to the next is so short, that while possible, its probability is considered impossible?

1

I will try sort out some misconceptions in a way that hopefully helps.

This doesn't do what you think it does.

uint y = x + 1; // just to give it a couple pico seconds

Intuitive ideas about temporal time don't apply. Consider how transactions are run redundantly by every node now, and in the future (syncing). What is the start time in temporal terms? It's not really definable, is it? Same for duration. If it could be measured, it would be different on every node, and it would not be deterministic, therefore not something a contract can access.

While it is obviously not the case in the physical world, a contract function, and transactions should be logically understood to execute instantaneously.

There is no parallelism and no possibility of two transactions running simultaneously. They are ordered in the blocks and each executes in the context of the previous, finished transaction - single-threaded sequential execution.

Transactions in a block are executed, in order, instantaneously on or near the temporal time indicated by the block timestamp ... when the block was discovered.

Re-entrance is not about timing. It is about the transfer of flow control. Vulnerable contracts expect flow control to return, but it might not. The contract that has received flow control may get up to mischief before eventually returning. That can cause all sorts of logic errors because the attacker may be able to call any function while the contract is in an inconsistent state.

The defense is to put all validation first, then update the state as required, and last (always last) allow flow control to pass to another contract. Putting the house in order first blocks most trickery.

Hope it helps.

| improve this answer | |
  • 1
    Typo. "No parallelism" – Rob Hitchens Dec 5 '19 at 14:18
  • Such a hack would have to be limited to a block's worth of gas, I suppose. This is good news because it implies it's not necessary to adjust the state immediately; rather, just before information is either pulled or pushed outside the function (flow and control are transferred to another agent). There are cases where making the state adjustment immediately--the next line--is difficult. – Eric Falkenstein Dec 5 '19 at 14:34
  • 2
    I'd add that reentrancy is transfering the control flow while the contract is in an inconsistent state. – Ismael Dec 5 '19 at 14:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.