Closed Nielsbishere closed 2 years ago
Related issue: #715.
I agree that some kind of const mechanism for parameters/variables is something we should really have in the language. The devil lies in the details :)
Some general remarks:
view
, it has full access to contract's storage. Even if you try to limit it with syntax, it's always possible to read/write arbitrary storage locations with mstore
/mload
in inline assembly.immutable
is not the best name for this. It's already used for a different mechanism where a state variable not only cannot be changed but its value is hard-coded in contract's bytecode at construction time. Parameters can't work like that so reusing it would be misleading.
immutable
mechanism originally proposed using the keyword for parameters. The thing that was implemented is different from that initial proposal though and I think we should find a new keyword.Overall, this issue is very underspecified. There are a lot of important questions that need to be answered to make it implementable:
public
function called internally vs externally).const_cast<>
in C++?memory
/storage
references is the reference itself immutable or only the value it points at? Or both?view
function with pure
one.foo(uint)
and foo(uint immutable)
and have the compiler choose the right one?
interface
s.using ... for uint immutable
to get a library function attached only to immutable values?calldata
parameters since calldata is inherently immutable? Or should they implicitly get the same semantics even without the keyword?@cameel interesting points, you're absolutely right about sstore/mstore. In OSes this is generally solved by something like VirtualProtect, which allows specifying ranges as read/write. Though something like that would require EVM changes that are probably too big of a change, but I don't see a good alternative (something like PushProtectRange and PopProtectRange). I thought about a "noassembly" function modifier that'd disallow inline assembly calls to prevent people from mstore/sstore, but that won't prevent people from doing it anyways and pretending it's generated with the noassembly function modifier. When resolving the function; params constness should be factored in, to make sure that if it's swapped someone can't remove them (so it's part of the function signature). I'd say that this applies to memory, storage and calldata (always const). Allowing this on ints/uints might be neat but personally I don't see the use for this.
Thanks for a detailed answer. This gives a much better picture of what you're thinking of. I was hoping it was more about a general concept of constness of parameters but looks like do mostly care about protecting specific areas of memory/storage.
I have more to say on specific points but before we get deep into that maybe let's talk about the most important point - security - because I really don't think this can ever work as a security feature without support at EVM level. Can you give a more detailed scenario where this would protect your contract? It seems to me that you're thinking about it in the context of a proxy that makes a delegatecall to a contract that it doesn't fully trust? In such a scenario the bottom line is that any syntax-level protection can be defeated because the contract you delegate to does not have to be written in Solidity at all. It can be written in another language or even hand-crafted in assembly/Yul giving full access to the delegated storage.
Also, even if EVM had something like PushProtectRange
, protecting specific ranges is very problematic due to how the compiler allocates storage. Value types are easy because they're stored contiguously starting at zero. Problem starts when you add reference types. A dynamically-sized array only has one slot in the contiguous range (length is stored there) and the actual data is at a "random" storage location (computed by hashing the position of the length slot). A dynamic array of dynamic arrays will have every sub-array at a different "random" storage location. And, worst of all, a mapping not only spreads its values all over storage but also does not store the keys so the compiler cannot even tell where they are. Trying to protect a deeply nested dynamic array would be expensive and in case of mapping it would be simply impossible.
So really, I only see any const feature as a good practice that lets you detect honest bugs but not as something that will let you protect your storage/memory against code you do not trust.
@cameel That's a shame. Basically the idea would be that this would give more of a guarantee to users that functions can't randomly be swapped out to something that can for example steal their funds; providing a better trust between devs and users. If for example only 2 out of all variables have to be modified, then exposing the entire storage struct as readwrite could mean that if the dev was either hacked or willingly wanted to steal users funds, they could. Or if multiple structs are passed and only one should be modified. But I understand that it's not really possible, perhaps this should be fixed at the contract implementation instead. For example by having a version id and letting the user specify a version id that resolves the function and not allowing functions by version id to be changed, only new ones to be added.
@Nielsbishere
There are ways to minimize security issues or trust with upgradeable contracts. Here are some ways:
diamondCut
function. So the ability to upgrade can be removed after some time has passed.@Nielsbishere
perhaps this should be fixed at the contract implementation instead. For example by having a version id and letting the user specify a version id that resolves the function and not allowing functions by version id to be changed, only new ones to be added.
This does not solve the problem. If new functions can be added then new functions can be added that can manipulate any of the state in the contract. For example a new function can be added that steals all users` funds.
@mudgen
The rest sounds good and you're right about the adding new functions thing. It's a bit complicated to wrap my head around making it in a way that doesn't expose everything to new functions. Can't I copy something from storage to memory and then pass it to a function if it doesn't need to read it? Or is copying heavy or impossible.
Because otherwise I could make it that it's not invoked through the fallback function but two functions that invoke an address you've added. One is for modifying critical data and the other for only reading it and modifying some other data that isn't as sensitive.
@Nielsbishere
Can't I copy something from storage to memory and then pass it to a function if it doesn't need to read it? Or is copying heavy or impossible.
I'm not really understanding what the point of this is. Can you clarify?
Because otherwise I could make it that it's not invoked through the fallback function but two functions that invoke an address you've added. One is for modifying critical data and the other for only reading it and modifying some other data that isn't as sensitive.
With that you made me think of a way to have a diamond with immutable functions that read/write state that can't be messed with by upgradeable functions or new functions. This is how:
DiamondCut
event and include them in what the Loupe functions return.That's it. Pretty simple.
With this strategy it is possible for a diamond to have immutable functions with state variables that can only be modified by the immutable functions, and at the same time have upgradeable functions and have the ability to add new functions.
@mudgen the point is basically not passing a storage struct but instead a memory struct, so if modifications are done it won't affect anything on chain.
That sounds pretty good, I'll look into it.
@Nielsbishere Okay, thanks.
Governance through tokens is a debated topic though, because wealthy investors can just buy everything and rule it. But I don't see a good way to do this without requiring KYC (and even that could be manipulated).
That's a good point. Thanks.
Abstract
Consider something like EIP-2535 (Diamonds) that allows for swapping out functions for upgrades. This introduces some trust required from the user to make sure functions stay the same as they were before (or at least don't turn malicious). Without immutable as a variable modifier that's hard because every storage or memory variable passed can be modified by it; regardless of if it's needed. I think reducing this possible attack surface could be good for upgradable contracts.
If I'm missing a keyword or solution that does this already, I'd be glad to hear about it.
Motivation
Let's consider the example above, but assume that somehow the owner intentionally or unintentionally swapped out this function for a malicious one;
This would mean that even though mySwappableCheckFunction only reads from system, it will still maintain the access to it. Meaning that intentionally or not someone can write to it. Marking this as immutable would solve the problem while also adding the nice syntax sugar of not allowing modification to something that you indicated shouldn't be modified.
Solution:
Specification
Should not compile, while not assigning to myTest should. I'm not sure about the EVM if it should block this access at bytecode level, but I think having support for this at Solidity level is already very nice (since the user can verify generated bytecode with the open source version to see if it has been altered).
Backwards Compatibility
N/A. Older versions would just remain as they are