4

I was wondering why is the eternal storage approach (first code snippet) still used? I know it's meant to make contracts upgradable but it seems too expensive to me.

contract Storage {

   mapping(bytes32 => uint256)    private uIntStorage;
   mapping(bytes32 => string)     private stringStorage;
   mapping(bytes32 => address)    private addressStorage;
   mapping(bytes32 => bytes)      private bytesStorage;
   mapping(bytes32 => bool)       private boolStorage;
   mapping(bytes32 => int256)     private intStorage;

   function getAddress(bytes32 _key) public view returns (address) {
       return addressStorage[_key];
   }

   function getUint(bytes32 _key) public view returns (uint) {
       return uIntStorage[_key];
   }

   function getString(bytes32 _key) public view returns (string) {
       return stringStorage[_key];
   }

   function getBytes(bytes32 _key) public view returns (bytes) {
       return bytesStorage[_key];
   }

   function getBool(bytes32 _key) public view returns (bool) {
       return boolStorage[_key];
   }

   function getInt(bytes32 _key) public view returns (int) {
       return intStorage[_key];
   }


   function setAddress(bytes32 _key, address _value) public {
       addressStorage[_key] = _value;
   }

   function setUint(bytes32 _key, uint _value) public {
       uIntStorage[_key] = _value;
   }

   function setString(bytes32 _key, string _value) public {
       stringStorage[_key] = _value;
   }

   function setBytes(bytes32 _key, bytes _value) public {
       bytesStorage[_key] = _value;
   }

   function setBool(bytes32 _key, bool _value) public {
       boolStorage[_key] = _value;
   }

   function setInt(bytes32 _key, int _value) public {
       intStorage[_key] = _value;
   }
 }

Consider this struct.

 struct Test {
   address user;
   uint96 amount;
   uint48 timestamp;
   uint48 expiry;
 }

Saving one struct element to a mapping would need 3 storage slots: 1 for the mapping key 1 for user and amount 1 for timestamp and expiry

Now consider saving the same struct in the eternal storage shown before. We would need 4 slots for the mapping keys and 4 slots for the actual values.

So 3 slots vs 8 slots.

Keeping in mind the a SSTORE operations costs 20000 gas, the struct method costs 60k gas vs 160k gas of the eternal storage.

Supposing my calculations are correct, this seems too much of a difference to me, especially from a customer perspective. So why is this method still used?

EDIT: As Jesse clarified below, mapping keys do not cost any SSTORE operation. Even deleting them from the calculation, we would still have double costs for the eternal storage, 4 slots, compared to the struct, 2 slots. The difference would be even bigger for more complex structs.

  • 1
    The struct is packed to fit into a couple 32-byte slots. You could just do the same yourself and then call setUint (or better yet, add a setBytes32). As for the answer to your question, you gave it in your second sentence. I'm not sure what more you're looking for. – user19510 Dec 16 '18 at 0:49
  • Wouldn't packing the data myself make the code too complex and unreadable? Is it considered a good practice? Thanks for the advice – AleG Dec 16 '18 at 9:46
  • It's hard to make suggestions without knowing the scenario. Sometimes the difference in gas price isn't a big deal. Sometimes it's a big deal and worth optimizing. In most cases, aiming for "upgradability" is a mistake in the first place. All I'm saying is that technically it's possible to pack the struct yourself just like the compiler does. – user19510 Dec 16 '18 at 17:30
  • In what production code have you seen this used? I certainly wouldn't choose to do this unless there's some really good reason I haven't thought of. Fully general upgradability isn't usually a good idea anyway – Tjaden Hess Dec 16 '18 at 17:51
  • I saw this in a bunch of articles speaking about upgradability. What I'm developing is a dapp that enables recurring payments and I thought upgradability was a good thing. Saving payments data in the storage contract and the logic in another contract in order to be able to upgrade the logic. Do you suggest sticking to basic non-upgradable smart contracts? Is upgradability in Ethereum a bad thing? Any advice would be much appreciated – AleG Dec 16 '18 at 22:34
3

They're not mutually exclusive concepts.

That code snippet originates here, to the best of my knowledge: https://blog.colony.io/writing-upgradeable-contracts-in-solidity-6743f0eecc88/

The gist of the idea is the separation of storage and logic. Logic contracts that could be replaced without loss of data or the need to migrate data. This approach is based on establishing a very general-purpose storage vault that is singularly concerned with persistence. The goal is to make the storage contract so simple it will never need to be replaced even in the case of application requirements evolving in unanticipated ways. A fully realized implementation usually involves access control and transferable ownership, so future logic contracts can take over.

There's no prohibition on clients packing data in more tightly if, for example, two values will fit neatly into a single 32-byte word. So, to the extent that clients don't mind a little packing and unpacking, the storage footprint can be the same. There's also no prohibition on making software clients responsible for the packing/unpacking operations so contracts don't have to. Also, consider that blockchain is not the ideal place for bulky meta-data - ideally, only data that is crucial to internal logic. https://blog.b9lab.com/the-joy-of-minimalism-in-smart-contract-design-2303010c8b09

Eternal storage is not the only approach to upgradeability. Proxy contracts are a new popular choice. There are tradeoffs in terms of readability, complexity and even limitations to upgradeability. None of the patterns are "do whatever you want" propositions.

Regarding the desirability of contract upgradeability, this can only be addressed on a case-by-case basis. It may be a sign that the long-term requirements are not well understood or the confidence in the implementation is not where it should be. It certainly needs to be defendable from the standpoint that it's not really an immutable set of rules if a party, usually centralized, can arbitrarily change the rules. This is why you don't usually see it in contracts such as tokens. Users would probably find upgradeability to be an anti-feature.

For more complex apps, the reasoning might be along the lines of a defense against defects or accommodation of future requirements. These are potentially awkward defenses. Why hasn't quality assurance established confidence? Why is a contract being used at all if the features are known to be changeable? What if users don't agree with the devs about what is a desirable upgrade? What constrains the developers from deploying a selfish upgrade or otherwise disagreeable upgrade? Without taking sides, because it's a case-by-case issue, these are dimensions to think about. Upgradeability will substantially increase the complexity of most contracts and the debate.

I feel a shout out should go out to Ali Azam for his excellent talk at DevCon IV about the work he is doing on trustless upgrades. In his model, upgrades are proposed by devs but adopted on an opt-in basis by individual users. In my opinion, this goes right to the heart of the matter. What sort of upgrade should be possible, and under what conditions? Who decides? How do we know the upgrade won't break something? What if we prefer the original implementation?

Hope it helps.

  • Spitballing... a storage contract could contain a nextOwner address (the contract to be voted on by users). Users could review the new contract on EtherScan and vote for it. A majority of users or user balances in favor would result in the new contract being approved and changing the address would clear previous votes. It could get messy and might not be immune to manipulation (e.g., adding users for the sole purpose of voting) but might work for certain contracts. – SuperCodeBrah Sep 6 at 2:56
2

When storing data into a Solidity mapping, there is no cost for storing the key. The key is not actually stored anywhere. The sha3 hash of the key is used as the storage address for the value.

For example, uintStorage[0x123] = 989898; only executes one SSTORE instruction.

This is also the reason why it's not possible to loop over all the values in a mapping; the keys are not stored at all.

  • Thank you for clarifying this point. Correcting this, we would still have double costs for the eternal storage compared to the struct: 4 vs 2 slots. And this is just a simple struct. – AleG Dec 16 '18 at 0:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.