Alert Source Discuss
🚧 Stagnant Standards Track: Core

EIP-5283: Semaphore for Reentrancy Protection

A Precompile-based parallelizable reentrancy protection using the call stack

Authors Sergio D. Lerner (@SergioDemianLerner)
Created 2022-07-17
Discussion Link https://ethereum-magicians.org/t/eip-5283-a-semaphore-for-parallelizable-reentrancy-protection/10236
Requires EIP-20, EIP-1283, EIP-1352

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

pragma solidity ^0.8.0;

abstract contract ReentrancyGuard2 {

    uint8 constant SemaphoreAddress = 0x0A;
    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is supported.      
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
    }

    function _nonReentrantBefore() private {
    	assembly {
            if iszero(staticcall(1000,SemaphoreAddress,  0, 0, 0, 0)) {
                revert(0, 0)
            }
        }
    }
}

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:

  1. 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.
  2. An opcode SSTORE_COUNT that retrieves the number of SSTORE instructions executed. It enables also fine-grained execution parallelization, but SSTORE_COUNT is much more complex to use correctly as it returns the number SSTORE opcodes executed, not the number of reentrant calls. Reentrancy must be deducted from this value.
  3. A new LOCKCALL opcode that works similar to STATICALL 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

contract Test is ReentrancyGuard2 {
    function second() external nonReentrant {
    }
    function first() external nonReentrant {
        this.second();
    }
}

A call to second() directly from a transaction does not revert, but a call to first() does revert.

Security Considerations

Needs discussion.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Sergio D. Lerner (@SergioDemianLerner), "EIP-5283: Semaphore for Reentrancy Protection [DRAFT]," Ethereum Improvement Proposals, no. 5283, July 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5283.