EIP-5283: Semaphore for Reentrancy Protection
A Precompile-based parallelizable reentrancy protection using the call stack
Abstract
This EIP proposes adding a precompiled contract that provides a semaphore function for creating a new type of reentrancy protection guard (RPG). This function aims to replace the typical RPG based on modifying a contract storage cell. The benefit is that the precompile-based RPG does not write to storage, and therefore it enables contracts to be forward-compatible with all designs that provide fine-grained (i.e. cell level) parallelization for the multi-threaded execution of EVM transactions.
Motivation
The typical smart contract RPG uses a contract storage cell. The algorithm is simple: the code checks that a storage cell is 0 (or any other predefined constant) on entry, aborting if not, and then sets it to 1. After executing the required code, it resets the cell back to 0 before exiting. This is the algorithm implemented in OpenZeppelin's ReentrancyGuard. The algorithm results in a read-write pattern on the RPG's storage cell. This pattern prevents the parallelization of the execution of the smart contract for all known designs that try to provide fine-grained parallelization (detecting conflicts at the storage cell level).
Several EVM-based blockchains have successfully tested designs for the parallelization of the EVM. The best results have been obtained with fine-grained parallelization where conflicts are detected by tracking writes and reads of individual storage cells. The designs based on tracking the use of accounts or contracts provide only minor benefits because most transactions use the same EIP-20 contracts.
To summarize, the only available RPG construction today is based on using a contract storage cell. This construction is clean but it is not forward-compatible with transaction execution parallelization.
Specification
Starting from an activation block (TBD) a new precompiled contract Semaphore
is created at address 0x0A
. When Semaphore
is called, if the caller address is present more than once in the call stack, the contract behaves as if the first instruction had been a REVERT
, therefore the CALL returns 0. Otherwise, it executes no code and returns 1. The gas cost of the contract execution is set to 100, which is consumed independently of the call result.
Rationale
The address 0x0A
is the next one available within the range defined by EIP-1352.
Sample usage
Parallelizable storage-based RPGs
The only way to parallelize preexistent contracts that are using the storage RPG construction is that the VM automatically detects that a storage variable is used for the RPG, and proves that it works as required. This requires static code analysis. This is difficult to implement in consensus for two reasons. First, the CPU cost of detection and/or proving may be high. Second, some contract functions may not be protected by the RPG, meaning that some execution paths do not alter the RPG, which may complicate proving. Therefore this proposal aims to protect future contracts and let them be parallelizable, rather than to parallelize already deployed ones.
Alternatives
There are alternative designs to implement RPGs on the EVM:
- Transient storage opcodes (
TLOAD
/TSTORE
) provide contract state that is kept between calls in the same transaction but it is not committed to the world state afterward. These opcodes also enable fine-grained parallelization. - An opcode
SSTORE_COUNT
that retrieves the number ofSSTORE
instructions executed. It enables also fine-grained execution parallelization, butSSTORE_COUNT
is much more complex to use correctly as it returns the numberSSTORE
opcodes executed, not the number of reentrant calls. Reentrancy must be deducted from this value. - A new
LOCKCALL
opcode that works similar toSTATICALL
but only blocks storage writes in the caller contract. This results in cheaper RPG, but it doesn't allow some contract functions to be free of the RPG.
All these alternative proposals have the downside that they create new opcodes, and this is discouraged if the same functionality can be implemented with the same gas cost using precompiles. A new opcode requires modifying compilers, debuggers and static analysis tools.
Gas cost
A gas cost of 100 represents a worst-case resource consumption, which occurs when the stack is almost full (approximately 400 addresses) and it is fully scanned. As the stack is always present in RAM, the scanning is fast.
Note: Once code is implemented in geth, it can be benchmarked and the cost can be re-evaluated, as it may result to be lower in practice. As a precompile call currently costs 700 gas, the cost of stack scanning has a low impact on the total cost of the precompile call (800 gas in total).
The storage-based RPG currently costs 200 gas (because of the savings introduced in EIP-1283. Using the Semaphore
precompile as a reentrancy check would currently cost 800 gas (a single call from one of the function modifiers). While this cost is higher than the traditional RPG cost and therefore discourages its use, it is still much lower than the pre-EIP-1283 cost. If a reduction in precompile call cost is implemented, then the cost of using the Semaphore
precompile will be reduced to approximately 140 gas, below the current 200 gas consumed by a storage-based RPG. To encourage to use of the precompile-based RPG, it is suggested that this EIP is implemented together with a reduction in precompile calls cost.
Backwards Compatibility
This change requires a hard fork and therefore all full nodes must be updated.
Test Cases
A call to second()
directly from a transaction does not revert, but a call to first()
does revert.
Security Considerations
Needs discussion.
Copyright
Copyright and related rights waived via CC0.