-
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
Language feature: disallow state-changing effects after an external call by default #12996
Comments
Just a comment on the name of the modifier: I don't like opinionated words like "safe" or "protected" because they give a false impression of safety or protection, we should find a more neutral one. Also more generally: Also wrt. free functions, it might be a good idea to explicitly pass/provide something like a "context" as a parameter to functions that allows external calls (or maybe even state changes without external calls). In this case, we could have a special property on this context that allows state changes after an external call. By default, the context would transform into a "constant state" context after an external call - there are the linear types again. Also something to consider more specifically to this issue: Is it OK to do another external call after the first external call? What about a delegatecall? Even if we do not use delegatecall, how do we distinguish a "data contract" that is associated to the current contract from a potentially malicious external contract? After all these questions, I'm more and more inclined that this should rather be "off-loaded" to user code and the compiler should instead provide the required features for the type system. |
You are right about the name of the modifier - after thinking through it today maybe we even do not need a modifier but can use the There are three types of reentrancy:
For the cross-contract reentrancy, this one can happen when a state from one contract is used in another contract, but that state is not fully updated before getting called. The conditions required for the cross-contract reentrancy to be possible are as follows:
The best outcome for this language feature would be that one could write the contracts without bothering about these three types by default. Of course, we can always argue to "offload" all the responsibility to the developers but past experience and events showcase blatantly that this does not work as intended. Coming back to your context idea - I really like it but couldn't we simply achieve that via the
I'm not yet sure what would be the best design here tbh. |
I would like to quickly add some further thoughts behind this language feature: The main question is whether we should build the language in a via negativa or via positiva way. I strongly tend based on the experience and events over the last 6 years to the first one. If you look at all the exploits that happened over the last 6 years (including DAO) we haven't got really smarter w.r.t. to reentrancy attacks as it seems (I also blame a little bit the auditors who push for the low-level calls instead e.g. transfer). Too many devs are still not paranoid enough! And Solidity already started implementing such safety features via |
I think this shouldn't go into the language with IMO, reentrancy guards could be added by default for every external call, and bypassed if the call is in an |
#13124 is a dup of this issue in general but offers two different approaches:
Copying what I said in the other issue: You could combine both approaches, eg have the aggressive approach by default which can be bypassed by the user if the external call is in an unchecked block, and the compiler can also choose to optimize it away if it detects the conditions in the conservative approach. |
Standard reachability analysis should be able to determine quite easily whether any given line can modify state or call a function in another contract. Then Solidity just needs to ensure that every function has a partition between state modification first, and external calls second. If you want the user to have to explicitly signify whether a function updates state, you already have that: non- |
I personally really dislike the idea of function modifiers for reentrancy. It's too binary and does not represent the amount of different use cases. Moreover:
Edit:
|
What I proposed is already basically what Slither does (minus the modifiers), but Solidity should give these warnings rather than Slither, because this is such a serious source of security vulnerabilities. Yes, in the general case precise reachability analysis is uncomputable, unless you use conservative logic that looks to see whether something might be reachable given some specific runtime control flow (then reachability analysis becomes simple and computable). Re. the modifiers, there's a reason why Solidity made visibility modifiers and mutation modifiers required a few versions ago. Being explicit establishes a contract between the programmer and the compiler. And what I explained in my 2nd paragraph is exactly what Solidity already does (albeit with inverted logic) with mutation modifiers for pure/view functions. So basically the machinery is all already there in the compiler to implement this simply. |
It will be much easier to implement robust automatic runtime protections against reentrancy attacks at runtime once EIP-1153 is merged, because it will allow for flags to be set whose lifecycle is bound to a transaction. However, I believe the vast majority of reentrancy attack vulnerabilities can be detected and prevented through static analysis. |
Are you confident that's gonna go in? I personally am not.
Agree that potential reentrancy can be detected with syntactic static analysis. But what about false positives? I think there are multiple ideas here that are complementary and provide different levels of safety and user experience. We need to get more people into this discussion, not only from the team. We also need to gather actual data on how many / what type of contracts would benefit from each proposed solution, and how many would be made worse / more annoying. |
This is a great idea. Omni NFT was also an reentrancy attack and it would be great to have it here. |
Adding a mutex to functions that write after external calls would not address cases where separate functions read from stale values or overwrite the same storage value. Take an example from the SAILFISH paper where adding a mutex is insufficient:
Rather than strictly identifying instances that do not follow check-effects-interact, it would make sense to look for function calls that allow for control flow to be controlled externally (as opposed to explicitly indicated like an if-else statement). I think this is clear if one examines the output of
Upon examination, after If we treat reentrancy as an undefined behavior problem, I think we could find a happy medium that is not cumbersome for developers yet eliminates undefined behavior. This would encourage specification and hopefully reduce false positives/ fighting the compiler. In order to introduce these sort of enhancements, I think it would make sense to consider the points raised in this issue and move towards a modular compiler design. People could opt in to passes that have more advanced analysis than the Solidity compiler and harden their code as they see fit (this is a common practice for other compiled languages). In addition, pruning infeasible paths would likely be slow and only required to run on "release" builds. P.S. I also like the use of TLOAD for reentrancy protection and think it would give developers less of a reason to circumvent compiler-generated protections, e.g. turning off reentrancy mitigations to reduce SLOADs. |
@pcaversaccio an awesome list -- thanks for compiling this!
@0xalpharush Correct, it's insufficient to prevent state changes if a parent frame in the call stack is a call to an external contract. You have to prevent state changes if a call was made to an external contract at any previous point in the same transaction. This is why I referred to EIP-1153, since it will provide a supported mechanism for binding state to the life of a transaction. However, perhaps this can be approximated by calculating bytes32 transactionId = keccak256(abi.encode(block.chainid, block.number)); then modifiers could be created for functions that call external contracts and functions that update internal state as follows: contract X {
bytes32 private extContractLastCalled_transactionId;
modifier extContractCaller() {
extContractLastCalled_transactionId = keccak256(abi.encode(block.chainid, block.number));
_;
}
modifier updateState() {
require (keccak256(abi.encode(block.chainid, block.number)) != extContractLastCalled_transactionId,
"Can't update state after calling external contract in the same transaction");
_;
}
} I think this would work, but it's not ideal, because
|
Would replacing the mutex modifier with this and having the compiler automatically include it in every call be a reasonable compromise? |
Actually I think bytes32 transactionId = keccak256(abi.encode(block.chainid, block.number, tx.origin)); Although this still doesn't handle the case of a single EOA submitting multiple transactions to be mined in a single block. Is there any other way to reliably differentiate different transactions? |
Just as a quick reference for comparison, Vyper uses a simple The main point is the following:
|
Linking this EIP initiative by @SergioDemianLerner since it can be relevant to this discussion. The proposal is to implement (through a hard fork) a reentrancy guard as a precompile. I don't think we should solve this issue via a hard fork. It's not an EVM problem but a compiler problem. Even if we had such a pre-compile, the discussed issue would still exist by default. |
@pcaversaccio right, a hard fork would solve the problem in the wrong way. Reentrance can and should be statically determined. Slither already does this (although I don't know what degree of complexity it handles). |
With ERC777 it can be the other way around see: https://twitter.com/transmissions11/status/1496944873760428058 |
Good point, and correct, I pointed that out here: OpenZeppelin/openzeppelin-contracts#2620 (comment) The ERC777 standard is insecure by default, and needs to be deprecated. But presumably there should be a compiler directive in case anyone really wanted to turn off Checks-Effects-Interactions order checking for a given function. |
My EIP-5283 represents the simplest hard-fork that provides the mutex functionality that is future-proof for EVM parallel tx execution with fine-grained parallelization. The sooner we introduce such a feature, the higher the number of deployed contracts will be parallelizable. |
@
Thank you for reply. I agree with your thoughts! |
Thank you for doing this post |
Since EIP-1153 is confirmed to be included in the upcoming Cancun-Deneb upgrade, we now have transient opcodes at hand that can 'easily' be used to disable reentrancy by default. I really think this is very important and should be included in the |
For the sake of documentation, linking a very interesting write-up by @0xalpharush on the discussed matter: https://gist.github.com/0xalpharush/15d903ec43334b081caece21a0bd7a20. Also, @jtriley-eth provided interesting thoughts for reflection. |
This issue has been marked as stale due to inactivity for the last 90 days. |
Don't close this issue as this is still very important. |
This issue has been marked as stale due to inactivity for the last 90 days. |
Don't close this issue as this is still very important. |
This issue has been marked as stale due to inactivity for the last 90 days. |
Don't close this issue as this is still very important. |
This issue has been marked as stale due to inactivity for the last 90 days. |
Don't close this issue as this is still very important. @cameel this is a super important feature and it seems like it's not getting the attention it should internally in the Solidity team IMHO over the last 2 years. |
Hey @pcaversaccio! Appreciate you proposing these changes and keeping the issue alive. From our internal discussions, we understand the desire to have such functionality within the Solidity compiler. However, given our current priorities the team will continue focusing on making via-IR viable and specifying the next iteration of Solidity for which we will keep this proposal in mind. At the moment, we won't be introducing such functionality which serves as grounds for closing the issue. In the meantime, you can make use of various static analysis tools available that can perform the desired safety checks outside the compiler. Some of them are even mentioned in the thread above. |
Sad to read that comment @mehtavishwa30. This is really a bad decision and will lead to even more millions of billions of frozen and/or locked and/or robbed funds. |
|
Just came from @pcaversaccio tweet as seen here and this really does need the attention that it deserves. It's been 2 years ffs. The North Korean guys are probably laughing at us right now! |
Just to be clear here: this issue is about performing source level analysis in order to disallow state-changing effects after an external call unless a specific syntactic marker is used and not about mitigating reentrancy-attacks in general and (while the discussion in the issue has over time shifted towards it as a different solution to the underlying problem) also not about builtin reentrancy guards by default. |
Abstract
Generally disallowing state-changing effects after an
external
function call and enabling the possibility to mark functions that specifically do this.Motivation
I started this discussion on Twitter after another reentrancy attack (Cc: @chriseth). Reentrancy attacks are ubiquitous and even though there are toolings available (e.g. Slither) that conduct a static analysis it requires an initial setup as well as a proper understanding of how to interpret the results. In order to make the Solidity development more secure and sustainable I feel it's time to finally introduce such a language feature that disallows state-changing effects after an
external
function call and mark functions that specifically do this.Specification
All state-changing effects after an
external
function call are disallowed. If you want to allow however such a possibility, we introduce a new modifierunprotected
whose default value isfalse
.Backwards Compatibility
The code that previously compiled fine will not compile anymore if there is a state-change effect after an
external
call and the new modifierunprotected
is unknown, so it's a breaking change.The text was updated successfully, but these errors were encountered: