EIP-5283: Semaphore for Reentrancy Protection
A Precompile-based parallelizable reentrancy protection using the call stack
作者 | Sergio D. Lerner |
---|---|
讨论-To | https://ethereum-magicians.org/t/eip-5283-a-semaphore-for-parallelizable-reentrancy-protection/10236 |
状态 | Draft |
类型 | Standards Track |
分类 | Core |
创建日期 | 2022-07-17 |
依赖 | 20, 1283, 1352 |
英文版 | https://eips.ethereum.org/EIPS/eip-5283 |
目录
摘要
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.
动机
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.
规范
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.
基本原理
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)
}
}
}
}
Parallellizable storage-based RPGs
The only way to paralellize prexistent 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 is works as required. This requires static code analysys. 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 paralellize 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.
向后兼容性
This change requires a hard fork and therefore all full nodes must be updated.
测试用例
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.
参考文献
Please cite this document as:
Sergio D. Lerner, "EIP-5283: Semaphore for Reentrancy Protection [DRAFT]," Ethereum Improvement Proposals, no. 5283, July 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5283.