A race condition is a situation where two threads access the same value in memory at the same timemutably and non-atomically. Mutably means each thread may change the value. Non-atomically means the mutation consists of multiple steps that can be interrupted in the middle.
If thread 1 is interrupted in the middle of mutating the value, and the CPU switches to thread 2, then thread 2 is given an unfinalized, invalid state of that value, which leads to undefined behavior.
I realized this is the same as reentrancy while watching in this video
youtu.be/QAzuAn3nFGo?t=255 where it presents a "naive" way of mitigating race conditions, that appears to me as identical to OpenZeppelin's ReentrencyGuard (
github.com/OpenZeppelin/open…).
An EVM contract (let's call it "A") may call into another contract ("B"); this is equivalent to a CPU switches to a different thread. Contract B may then call back to contract A; this is equivalent to thread 2 accessing the same memory value as thread 1 was mutating. If contract A calls B while in the middle of mutating a value in its own state, it exposes the value to B in an unfinalized, invalid state.
In real hardware, it's not possible to prevent race conditions with purely software-level solutions. This is because modern compilers and CPUs may apply optimizations to software code, such as reordering instructions, that breaks the solution. Instead, hardware-level solutions such as atomic instructions are necessary.
However in specific blockchain VMs, e.g. EVM which executes instructions sequentially without reordering, purely software solutions are possible.
Other VMs, such as
@CosmWasm, and our grug-vm that
@dango is based on, a contract is not allowed to call another contract (i.e. "switching to another thread") until is finishes mutating its own state and fully exits. Thus, reentrancy is fundamentally impossible.