Immutables, invariants, and upgradability

hmm… may be I am wrong… It is too late…
May be the applicable rule set (EVM_v1 or EVM_2) should depends on which storage is accessed (which exactly means “the code gets executed”).

hmm… If we assume that the (V) Victim should store anything in own storage, then it is ok: he has EVM_v1 and “expensive” SSTORE. The reentrance attack using transfer call will fail.
But what if it depends on some critical state stored in some EVM_v2 contract, that an attacker could manipulate cheep? Then the attack will succeed.

Then we need indeed a possibility to define a target EVM for a contract we deploying. Oh… it gets complicated :frowning:

Two really random thoughts:

  1. The gas limit on transfer was a poor precedent to set as it created a bad development practice that can be easily violated in an otherwise innocent change like this. We should avoid doing subtle little hacks like this in future because they are hard to reason about.
  2. It would instead be more beneficial if transfer literally would not allow a call back directly, by somehow shutting down execution or at least disallowing a re-entrancy more directly. That’s how most developers think of it in practice IMO.
4 Likes

Having worked at Intel for a few years, I observed that backward-compatibility was always a top-priority, very challenging and time-consuming to get right. As @lrettig points out, it is an explicit/implicit social contract with your developers/users, and in this case, fundamental to the immutability
(of behaviour) guarantee. This aspect may have been critical to the wide-spread adoption of x86 architecture because one can always buy the next generation processor with full confidence that the software they use/wrote (from n years ago) will continue to function as before.

2 Likes

This is similar to what was suggested in EIP-1283 Incident Report that we should consider introducing a contract-level reentrancy-/recursion-free CALL opcode. This could allow value transfers and state modifications (unlike STATICCALL) but prevent a contract-level indirect recursion.

I still think this is a bad idea. Preventing function-level recursion could be a useful thing to do, but preventing contract level recursion is far too blunt a tool. There are lots of cases where calling one’s caller is a useful thing to do, and next to no workaround for those cases if it’s prohibited.

I see. Given that STATICCALL should already reduce the reentrancy attack surface, introducing a variant of it that allows value transfers (for transfer/send) and only LOG opcodes in fallback functions seems too specific, wouldn’t it? This is essentially the second proposal listed here I suppose.

In the example contract illustrated by ChainSecurity, the dirty/cheaper SSTORE is executed by the victim’s contract (in updateSplit) when called by the attacker contract’s fallback function. So, even though the attacker contract is in EVM_v2 context, when it makes a call to the victim contract, the context should change to EVM_v1 (assuming victim contract was deployed with EVM_v1 i.e. without EIP-1283) and the legacy SSTORE will fail with OOG. The attacker shouldn’t be able to force the victim contract to execute in the newer post-EIP-1283 EVM_v2 context. What am I missing?

What do we mean by “consensus-level changes in order to function”?

If anyone is planning to work on this EVM versioning proposal, I will be interested in contributing. :slight_smile:

1 Like

Yes, you are right. Although for pure re-entrance only.
In general the Victim can depend on other contract’s storage and if it is in EVM_v2 scope, we have a problem.
We will need a possibility to enforce the target EVM version on deployment.

But if there many Victims in different EVMs depending on shared storage, accessible by Attacker, it will be a version conflict…

Versioning, in general, will be tricky to design and enforce I suspect. But if we would like to update existing developer-exposed interfaces/semantics without sacrificing backwards-compatibility, then the options are to (1) offer new interfaces for updated semantics (i.e. via new opcodes) or (2) update semantics of existing interfaces (i.e. new opcode behaviour) but provide a versioning system to allow developers to bind their code to specific semantics.

3 Likes

disagree.
Devs should create an re-entrance lock at the particular “entrance”, not at particular “exit” (call). At the “entrance” we know the function we would like to guard. Behind the “exit” it depends on callee and unknown to deployment time of caller’s contract.

We should publicly promote an explicit re-entrance lock usage. Assumption about reentrant behavior of other constructs, that were not developed as a re-entrancy lock, should be strongly discouraged.

Devs must use a lock on a function if there is something to guard and there is a call to other contract inside.

@fubuloubu, would you agree on the statement above?

For Vyper, we’ve discussed adding function-level recursion locks that would attempt and prevent mutal recursion between a set of contracts, but it would involve a lot of overhead and be too complex as to open a lot of attack surface in practice I think.

I really like the proposal of adding a callback-safe transfer opcode because it allows the developer an additional option to explicitly reduce their attack surface so they can protect themselves if a particular protocol would have safety issues that need to be protected. Re-entrancy is probably one of the most complex bugs possible with smart contracts, and I think giving protocol-level tools to protect against unintended behaviors is important to provide as it will actually mitigate the problem instead of band-aiding it as the 2300 gas stipend does.

This “callback-safe” version of transfer could allow STATICCALLs back but no mutating function calls. This might also be more broadly useful as an a method of calling, something like FINALCALL that does not allow mutating calls to itself after the call is forwarded e.g. “I don’t care what you do with this, but don’t come crawling back to me with it because I won’t be listening”.

I do agree with @Arachnid that this starts to break the “composable” behavior that developers tend to tout of Ethereum smart contracts, but it’s a trade of interoperability for safety that I think would be very helpful to developers.

I’ll caveat all of the above with “I am not a VM expert, and this all could be very difficult to design”.

1 Like

The backwards compatibility of x86 is a helpful example, and I’m thankful that @jpitts brought it up. I’ve heard @gcolvin speak about this before as well. But lest we compare ourselves too closely to Intel, I just want to point out a glaring difference: ours is an adversarial environment where the attacker can see, and execute, code on our “machine” at will. For this reason I think we should adopt a different set of principles and priorities in our design, and safety should be an even higher priority for us.

2 Likes

The honest answer to your question is that IMHO we should not have just one kind of “gas.” It should be multi-dimensional, and we should try to more accurately reflect the orthogonal costs of bandwidth, storage, compute, etc. I fear that monolithic gas is too great an abstraction for a functional, safe, efficient machine.

1 Like

Before this behaviour was discovered, nobody considered reducing the gas cost of SSTORE a potentially breaking change; reducing a cost is less likely to cause problems with contract execution than increasing it

What this issue taught us (yes, with the benefit of hindsight) is that even something as seemingly benign as reducing the gas cost of an opcode can have unintended knock-on effects that violate perceived “invariants.” Unless someone can generate some sort of formal proof that reducing gas costs in the future cannot have this effect, then I’m afraid we are stuck. We had months to evaluate this EIP and prepare for this hard fork and many intelligent people missed this potential issue, and the same could happen with any future change of this sort. And that’s just for reductions in gas cost - what about for other types of changes?

It seems to me that, fundamentally, we are faced with a stark choice between the following:

  1. Violate this “tacit social contract” (as I am referring to it) and accept whatever may come of that, including hacks and breakage, or
  2. Do not make changes to existing opcodes, no matter how benign

Versioning seems like a more practical approach, but will likely require consensus-level changes in order to function. On the other hand, it will also open the door to EWASM, which would require some kind of versioning anyway

Yes, this is promising and we need to give this more thought, but you’re right that it’s necessary for Ewasm anyway and it’s something we’ve begun to explore. We can continue that conversation here. In general I think we should not attempt to reinvent the wheel and should take as much as we can from existing package management systems such as npm, yarn, cargo, etc.

I can understand your point about multi-dimensional gas if you are talking about opcodes sampling. Yes, the sampled gas cost for some opcode is a “sum” of “orthogonal costs of bandwidth, storage, compute, etc”.

But once the “combined” gas cost is sampled into single number, I don’t see any reason for user to split it back into dimensions. Multi-dimensional gas should imply multidimensional gas price, but I don’t see who will need it. What should a user express by setting a network gas price higher than computational gas price? Unclear to me.

@lrettig, shouldn’t we better extend the topic to “Immutables, invariants and upgradability”?
Objects are: smart contracts, EVM and social contract around it.

currently we have it fragmented:

  • there are works on contract upgradability,
  • there are discussions on EVM upgradability,
  • social contract upgrades (like gas cost changes) are even not in discussion yet

I think all this stuff is in the same domain and tightly coupled.
It is really worth of thoughtful research and specification.

1 Like

Back to your original question:

Do you think there will be no need to tune single opcode’s cost in the future even if the hardware will change significant?

You’re right that multidimensional gas cost does not really help address this question. I think, yes, we probably do want to/need to be able to tune an opcode’s gas cost in the future. There are two ways we could tune:

  • Up, in case it’s too low, which would probably only happen to mitigate a DoS attack, which I would consider an emergency, and which in any case would definitely not increase the risk of re-entrancy
  • Down, in which case we might introduce a new, cheaper version of the opcode, or alternatively a new EVM version with a cheaper opcode

Agree, good point, will update the subject

I meant re-entrance locks. Unsure whether you mean the same with recursion locks.
Re-entrance locks are simple and intuitive in solidity, although quite expensive (exact this issue was targeted by EIP-1283).
I am wondering what do you mean by “too complex / a lot of overhead” in Viper exactly?

Copying over some relevant posts on this topic from the other thread:

CC @mandrigin, @Arachnid, @rajeevgopalakrishna

1 Like