'EXTSLOAD' opcode proposal

I wanted to gather feedback around proposing a new EXTSLOAD opcode, which would allow a contract to read a storage position from another account.

While I understand that this could be regarded as a Bad Idea ™ since it promotes breaking encapsulation, I think it naturally follows EXTCODEHASH. Once you have validated that an account holds a particular kind of contract using EXTCODEHASH, you can safely use EXTSLOAD to check a storage position from that account, knowing that it will have the semantics you expect.

The gas cost for this operation could be much cheaper than actually performing a call to execute a getter from a contract, since it does not require executing (nor loading!) any code on the queried contract, but just retrieving a single storage location.

As for high-level usage of this opcode, this means that getters for a contract (assuming it will rely on a single implementation) could be implemented as methods from Solidity libraries that use this opcode behind the scenes to check the code hash and subsequently retrieve the value from storage.

contract Box {
  uint256 value;
  constructor (uint256 _value) public { value = _value; }
}

library BoxReader {
  function getValue(Box target) internal {
    // optionally use extcodehash on Box to validate it matches the code
    // use extsload on Box to retrieve the value
  }
}

contract Reader {
  using BoxReader for Box; 

  Box box;
  constructor() { box = new Box(); }

  function readBox() public {
    uint256 value = box.getValue();
    // ...
  }
}

Besides gas cost savings, the actual use case that drove me to this opcode was using EXTCODEHASH for validating implementation contracts behind DELEGATECALL proxy contracts. On a system that relies on this pattern for upgradeability, using EXTCODEHASH for checking that a contract has the expected code does not cut it, since it may be a proxy sitting in front of the actual implementation contract. This means that the validation would actually be a 3-step process:

  • EXTCODEHASH the target account to check it is indeed a proxy,
  • EXTSLOAD to retrieve the implementation contract’s address,
  • and then EXTCODEHASH the retrieved implementation address and compare it against the desired one

Given I’m not that familiar with the internals of EVM implementations, I wanted to gather some feedback around this before formally proposing it as an EIP. Thanks in advance for any comments!

2 Likes

This is a bad idea. If a storage is marked as “private” this of course means that you can read it off-chain but it is private for a reason: external contracts are not allowed to read any storage. If someone wants to implement reading storage (either for all accounts or for some) they can just implement a function which other accounts can call. This would yield about 700 gas for the call plus 200 gas for the SLOAD and some extra overhead for the function selector / memory expansion.

Hey Jochem! I understand that reading storage can be implemented as a getter if the contract wants to expose that value. The point of the proposal is coming up with a cheap way (in terms of gas) to go around getters when dealing within a set of closed and well-known contracts, since it removes the need to set up a new context for the CALL, loading the code, running the function selector, etc.

It’s worth noting that the gas cost for EXTSLOAD would have to be quite high - much higher than SLOAD because it requires the target account and it’s storage to be loaded. That’s at least 50% of the cost of performing a call (the remaining being loading the other account’s code and a small amount of time creating a new EVM frame). So it would become more expensive to use EXTSLOAD by about the second or third value loaded that way.

related:

may be

1 Like

For reference, I wrote up an EIP which outlines this and did a rough implementation in go-ethereum to map it out:

For what it’s worth, it’s generally a bad idea, but it has some nice upsides. I don’t think the gas would actually be substantially higher after looking at it implemented. Go-ethereum is well setup to handle it. The question is, do we break the existing call oriented interface design model which is already well established and going pretty well.

1 Like

Yes and no, you could still workaround that limitation using a witness, and with that you could read any storage position of any account. Anyway, EXTSLOAD could still break the assumptions of some systems, reading a storage slot using a witness has his limitations (front running, witness update, etc), and this opcode would remove all those limitations, maybe opening the door to “game” some smart contracts?

The main problem here is that, in my opinion, a beginning solidity dev learns up to some extent that internal/private variables cannot be read from other accounts. This does not even imply the knowledge of how CALLs the other contracts work (e.g. invoking their code, shifting the environment to this storage, etc.) - from a “normal” imperative programming approach (e.g. Java) it is clear that something which is “external” which might be vague in the Ethereum context (in practice this is hence another account/address/contract) and which has private/internal variables (in solidity hence not exposed via internal/private - but this holds form EVM in general) it cannot be read from said account.

Since most developers currently use Solidity (this is an assumption but I am pretty sure it holds) it is strongly implies that external accounts cannot read these internal/private variables. Since variables are non-exposed by default this means a developer has to expose them via public (or in case of a function public/external) to allow other accounts to read this.

Adding EXTSLOAD directly violates this assumption. In my opinion this is a much larger issue that “detailed” settings e.g. EIP 1283 where we introduce a re-entrancy in case of a fallback (it is implied a fallback cannot be used to exploit via re-entrancy since it cannot SSTORE). This implies that you can immediately read other accounts storage. This results in a gigantic attack vector where developers first assumed that you cannot read storage slot X from an external account and now you suddenly can (directly - e.g. without tricks). This is the reason I strongly oppose this addition.

Given this reasoning I would love to know the reasoning from @spalladino knowing where, in practice, this opcode is actually useful, besides basically verifying setups via CREATE2. I think by now it is clear that there is a safe way to setup CREATE2 contracts e.g. in a way where the code is either present or it is selfdestructed (overwriting code is not possible, even if the code would equal the currently deployed code).