We will continue the security-oriented tidbits by delving a little bit deeper into how re-entrancy attacks, a relatively unique trait to Ethereum, and improper data-caching can cause various types of attacks to manifest, including multi-million past incidents.
EVM State Machine
The EVM which all code is executed upon operates in a straightforward, deterministic manner; it is a stack-based machine that operates on the latest instruction at hand at any given point in time.
The execution of a particular function can be thought of as “single-threaded” whereby external calls halt the execution of the current contract until they conclude. As a result, any state changes that have been performed before such a call will be reflected within the blockchain whereas any changes slated to occur after the call concludes will not.
In a vulnerable contract, the above notions essentially cause the system to misbehave by “halting” it at an “illegal” state. Let us consider the example of a typical token that is meant to transfer funds between two parties.
The valid states of our “state-machine” revolve around the fact that a set token amount will be at any given point in time available to one party. For example, if person A transfers 100 units to person B, those 100 units must be either available to A or B.
Illegal Re-Entrancy States
To illustrate how re-entrancy attacks manifest, let us consider that in the above scenario the smart contract performing the unit transfers between individuals also performs an external call between the state changes that assign the newly acquired units to the target individual and subtract them from the original.
In the above sequence of actions when the external call is performed the contract will be in an “illegal” state with the two following traits:
- Both the recipient and the sender will have the transacted units readily available.
- The total supply of the token will be incorrect.
Given the above traits, one could theoretically exploit our token in myriad ways given that the total supply is no longer valid (i.e. causing percentage calculations to misbehave) and that double the original amount of tokens are available (i.e. leading to underflows or overflows).
Although the above example is grossly simplified, the basis of all re-entrancy attacks is an illegal state. It should also be noted that such re-entrancies can usually be replicated thus exaggerating their result. For example in the above scenario, 100 units could be duplicated each time by a factor of 2.
The most resilient solution to re-entrancy attacks is ensuring that your contract’s state-machine prohibits these states from ever occurring. An officially recognized pattern in Solidity is the Checks-Effects-Interactions pattern.
As its name implies, the recommended modus operandi of all functions of contract should be to first apply the necessary checks sanitizing its execution (i.e.
msg.sender is authorized and has enough funds). This ensures that the restrictions of a function are imposed before any state changes that may invalidate them.
Afterward, the function should perform all state changes it is meant to conduct. This ensures that the changes performed at the contract level are done so in a closed loop and cannot be frozen in-between. Finally, all external interactions should be performed in the desired sequence they are meant to be.
The sequence of events transpiring in the above pattern inherently guarantees that illegal states are not achievable as state changes are performed before the external calls that can “halt” them and overall the function’s execution is considered sane as all checks were performed at its beginning.
There are certain edge-cases, however, where the Checks-Effects-Interactions pattern cannot be applied due to the needs of the contract. A prime example of this is the Uniswap V2 pair contract which relies on
balanceOf measurements beyond the conclusion of an external call.
For such contracts to be guarded, a simple state-check needs to be enforced that permits only a single action to be in execution within the contract. This is done by setting a contract-wide flag to
true that is validated and set on each function.
While this security measure still permits illegal states to manifest, it prohibits malicious actors from taking advantage of them by disallowing any further actions with the contract until the original contract call concludes.
While in self-sufficient contract architectures this is a solid security measure, it should be noted that multi-contract architectures can still suffer from re-entrancy attacks in unrelated contracts that do interface with the illegal-state contract via getter function calls.
Re-entrancy attacks have been around for quite a while and are a well-known trait to the Ethereum developer community, however, this does not prevent them from taking form given that many can result from seemingly innocuous contract interactions, such as ERC-20
transfer invocations on malicious contracts.
My personal recommendation would be that each project should sanitize their contracts against re-entrancy attacks, perhaps via the use of static analyzers, and apply the Checks-Effects-Interactions pattern wherever applicable, including internal interactions that may not result in a re-entrancy as there is no gas-related drawback to applying the pattern.