-
Notifications
You must be signed in to change notification settings - Fork 5.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[codegen] Enforce Re-Entrancy Protections by Default (Solidity 0.9.X
)
#13845
Comments
Linking my issue here for obvious reasons #12996. |
Thanks @pcaversaccio for the additional context, updated original issue with additional background. |
Unfortunately, I don't think this is the right approach at the language level. Reentrancy is an important problem but there are multiple ways to deal with it and I think that this particular solution is too opinionated towards only one of them. For example:
Making these decisions for the user is perfectly fine for a library like In fact, I'd say that for use cases where this mechanism would be a good fit using I'm going to close the issue since I don't think we'll implement it the way presented there. This of course does not close the discussion on the topic, though the forum is probably a better place for that. Overall, I don't think we have consensus to go for any particular solution right now so we need more discussion and feedback from the community. Feel free to also drop in on one of our team calls if you want to discuss possible solutions with the team. |
Thanks a lot for the detailed feedback @cameel. Here are a couple of thoughts:
After inspecting issue #4990, its proposed solution would be inadequate. The issue lies in that we need conditional behavior for re-entrancy guards to work properly across both non-mutating and mutating functions of a contract, as showcased in the exhibit above. As such, I think this exhibit is separate from #4990 for which it was closed for. Read-only re-entrancy vulnerabilities are real and have occurred in the past, including a now-patched vulnerability in Curve Finance as well as the Market.XYZ hack which resulted from code suffering from the same flaw. I understand the aversion to this change, however, it would benefit the developer community significantly and increase the collective bar of security, in turn increasing the adoption of the Solidity language (as well as smart contract development) even more. |
Abstract
As the security space around EVM-based smart contracts matures, we can observe a recurring pattern in security vulnerabilities across all EVM spectrums; a significant portion of them arise from re-entrancy vulnerabilities,
With this issue, we aim to introduce a built-in re-entrancy check across all functions of a generated contract by default, permitting programmers to explicitly mark their functions as re-entrant via the newly introduced
reentrant
keyword, being declared akin tooverride
and co.Motivation
Rationale
Re-entrancy attacks are one of the most common root causes of multi-million dollar exploits we can observe by going through historical exploits. Additionally, the concept of a re-entrancy is very hard to grasp when coming from a traditional programming background due to the unique nature of the EVM.
Solidity is an evolving language that attempts to cater to the wider EVM development community and has historically introduced tools to aid in developing using the language more securely, such as built-in arithmetic checks introduced in
0.8.X
.With this change, we aim to introduce built-in re-entrancy protections by default with the ability to bypass these protections explicitly, empowering seasoned developers with maximum flexibility while protecting newcomers from EVM-related caveats they may not be aware of.
Proposal
The proposed keyword (
reentrant
) is meant to mark a function explicitly re-entrant. As a result, code generation of Solidity would need to introduce a breaking change that will cause the "entrypoint" of the bytecode to evaluate the reentrancy flag.For the proposal to function properly, a
NON_REENTRANT_FLAG_OFFSET
compiler-literal would need to be introduced that signifies a storage slot's offset that is meant to indicate the re-entrant flag that is validated. This offset should be preferrably located in the upper-half of thetype(uint256).max
range that a smart contract's storage slot space supports to ensure no conflicts with existing implementations.Keyword vs. Existing Syntax
Upon additional feedback from @pcaversaccio and issue #12996, I would like to add some additional insight as to why a new keyword was chosen over the existing syntax. The original issue revolved around the concept of a new keyword and switched over to the idea of using
unchecked
to perform external calls without triggering any re-entrancy safety checks.The
unchecked
keyword is meant to be utilized in the locale it is declared in (i.e. an upper-mostunchecked
block will not affect the statements of internal calls it makes), as such, such a solution is not viable if we want the new re-entrancy feature to be compatible with existing programming paradigms such as inheritance.We are faced with either breaking the existing behaviour of
unchecked
to apply to internal call chains or introducing a new keyword. The latter appears to be more explicit and easier to grasp for security auditors and developers alike, however, feedback is appreciated.Example Showcase
To illustrate how the generated bytecode would be altered, let us take a subset of the
WETH9
contract:The above contract contains two functions that do mutate the state (
fallback
&deposit
) as well as one function that does not mutate the state (balanceOf
).Its compilation with current tools would result in the following bytecode in pseudo-code format:
Given that the compiler can detect which parts of the generated bytecode will mutate the state and which will not (based on the
view
/pure
keywords), we have two cases of code generation:reentrant
Functions Definedreentrant
Functions DefinedNo
reentrant
Function CaseFor the first case, the bytecode generation module would inject a blanket check at the beginning of
main
that would validate the re-entrant state of the contract. Afterwards, the bytecode generation module would introduce an assignment to the re-entrant flag solely in theif-else
clauses that execute code mutating the state.Identifying the correct points of injection should be trivial as the compiler is already aware of which functions mutate the state via the
view
andpure
keywords. To illustrate how the bytecode generated would look like, let us take the first case with the originalMockWETH9
smart contract code:The bytecode generator can further optimize the gas cost of the injection by performing the re-entrant flag assignment conditionally via a temporary variable which will hold the value of
storage[NON_REENTRANT_FLAG_OFFSET]
that is evaluated at the very start of themain
block.reentrant
Function CaseThis case would simply require the blanket check in the
main
code block showcased above to be relocated to allif-else
bodies that do NOT have thereentrant
modifier set. To illustrate how thereentrant
keyword would be used, let us adjust the originalMockWETH9
code to now permit re-entrancy solely for thedeposit
function:The bytecode generation would look like the following:
A yet-to-be defined behaviour arises if we declare the
fallback
function asreentrant
when it invokes thedeposit
function which has not been declared so. To ensure maximal compatibility with existing programming paradigms, we believe that thereentrant
keyword should mark a function as re-entrant regardless of its internal call chain. As a result, if we have the following code:The function
fallback
will be re-entrant even if it invokesdeposit
which we have not marked so. This ensures compatibility with libraries / smart contract dependencies as otherwise users who wish to set their functions asreentrant
deliberately would have to reflect that modifier to the full call-chain. Additionally, given that the introduction of the keyword is a concious and deliberate choice by the developer(s), we consider them to be fully aware of the implications of thereentrant
keyword.Advanced Usage
We are aware that re-entrancy is indeed desirable in a set of limited use cases, the most common being proxy implementations that follow a fragmented logic pattern and thus invoke themselves externally (i.e. Diamond standard cross-facet invocations). To accommodate for these implementations, we propose the introduction of an argument to the
reentrant
keyword similarly to how arguments are present for theoverride
keyword.In detail, we advise the introduction of a single, optional
address
argument which marks a function asreentrant
but solely for a particularaddress
. In the case of the Diamond standard, for example, we can introduce thereentrant(address(this))
syntax to ensure that the facets of the Diamond can invoke each other without compromising the wider security guarantees of the system.Additionally, this syntax permits complex smart contract systems that are meant to invoke one-another mid-execution to still function post-
0.9.X
securely. Multi-address support can be introduced, however, it should be delayed until a sufficient use-case is illustrated by the development community that cannot be solved by better programming practices.Specification
While the specification of how the new
reentrant
keyword will operate can be extracted from the above text, we would also like to highlight which sections of the official Solidity documentation would require adjustments to accommodate for this change. Reference specification can be produced upon request for all chapters outlined below should this feature request gain traction.Contracts Section
A new "Re-Entrancy" chapter would need to be introduced that describes how re-entrancy behaves post-
0.9.X
(in that it is prohibited) and how developers can make use of thereentrant
keyword to bypass this security measure. A warning chapter should be introduced as well ensuring that the developers are well aware of the security guarantees they are nullifying by using the keyword.Cheatsheet
The
Modifiers
section would need to be expanded with the newreentrant
keyword and how it is meant to be used.Language Grammar
An identical rule to the
override
specifier would need to be introduced specifying how the newreentrant
keyword is meant to be parsed when reading Solidity code using machines.Layout of State Variables In Storage
This chapter should specify the newly reserved
NON_REENTRANT_FLAG_OFFSET
as a matter of specification. Overwriting the storage area of the flag via overlap or a storage slot hash collision due to the usage of upgradeable patterns and standards such as EIP-1967 should be of negligible concern with a likelihood akin to that of general hash collisions.Solidity v0.9.0 Breaking Changes
This chapter should, as its namesake indicates, highlight the breaking change of how
reentrant
behaves and how contracts compiled inpragma solidity ^0.9.0
will have re-entrancy protections enforced by default.Backwards Compatibility
As the code generation's behaviour will change to enforce re-entrancy protection by default, this is a breaking change requiring a minor semver bump to prompt developers to get accustomed to the new security measure.
General Concerns
Security Bypass
Given that the change illustrated by this issue would rely on the storage space of the smart contract, developers will be able to explicitly unset the re-entrant flag via
assembly
blocks that access the low-level nature of the EVM and write to theNON_REENTRANT_FLAG_OFFSET
storage slot. Such code is considered malicious by nature and should be flagged by auditors as well as potential static analyzers that aid them.Bytecode & Gas Increase
The issue attempts to explain the proposed change in the Solidity language in a way that minimizes the gas footprint as well as bytecode size impact. Nevertheless, both of these numbers will increase for all contracts compiled beyond
0.9.X
.We believe the security guarantees achieved by this change to be worth the extra units of gas and size, which is evidenced by the developer community itself via the common usage of libraries implementing this trait such as
ReentrancyGuard
by OpenZeppelin.Resources
The pseudo-code of the bytecode was generated by the ethervm decompiler and was consequently manually edited to illustrate the smaller subset of
MockWETH9
as well as the adjustments thatreentrant
and post-0.9.X
compilation would cause.The text was updated successfully, but these errors were encountered: