1

I've been reading into strategies for writing upgradeable contracts. A pattern that has come up numerous times is to seperate your contracts business logic from its storage so that upgrades can occur without loss of data (Writing upgradeable contracts in solidity, Upgradeable solidity contract design).

In this article, its said that the eternal storage is a "a simple and extensible way to store any kind of data from simple values, to arrays and complex object type data." However, given the provided storage contract and mock User contract below, I'm having a hard time understanding the best way to move the User storage into the EternalStorage contract given that it contains things like arrays, mappings, structs, etc. Any thoughts/suggestions?

pragma solidity ^0.4.24;

contract UserRegistry {
  struct User {
      address addr;
      uint points;
      address[] friendsList;
      mapping(address => bool) friends;
  }

  mapping(address => User) users;
  mapping(address => mapping(address => uint)) public gamesPlayedTogether; // user address => (friendAddress => games played together)

  function createUser() public {
      User memory user = User(msg.sender, 0, new address[](0));
      users[msg.sender] = user;
  }

  // Various other business logic
}

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;
  }
}

1 Answer 1

1

I would change some details in the mock client. It's close to the Mapped Structs with Index pattern over here: Are there well-solved and simple storage patterns for Solidity?.

It might be good to understand this pattern for referential integrity: https://medium.com/@robhitchens/enforcing-referential-integrity-in-ethereum-smart-contracts-a9ab1427ff42 before you set out, then ...

Make it stateless ...

Reduce everything to key/value pairs with bytes32 keys. This is a rough minimal sketch optimized for readability:

contract StatelessUserRegisty {

    Storage dataStore;

    constructor() public {
        dataStore = new Storage();
    }

    function userKey(address userAddr) public pure returns(bytes32 userID) {
        return keccak256(abi.encodePacked(userAddr));
    }

    function isUser(address userAddr) public view returns(bool isIndeed) {
        return dataStore.getBool(userKey(userAddr));
    }

    function createUser(address userAddr) public returns(bool success) {
        require(!isUser(userAddr));
        dataStore.setBool(userKey(userAddr),true);
        return true;
    }

    function updateUserPoints(address userAddr, uint userPoints) public returns(bool success) {
        require(!isUser(userAddr));
        dataStore.setUint(userKey(userAddr),userPoints);
        return true;
    }

}

I got away with a bit of a cheat because there is only 1 bool and one uint. What if there were multiple uint variables?

Keep hashing away ...

bytes32 key1 = keccak256(userAddr, "first");
bytes32 key2 = keccak256(userAddr, "second");

You can take that a step further and add row or index to the mix for arrays and mappings.

Having played around with this sort of pattern quite a bit, I've come to think of these application-specific storage contracts as something separate from application logic - just the essential create, retrieve, update, delete and referential integrity assurances. Then, consider letting application contracts "own" one or more data controllers.

ApplicationContract => Data Controller => EternalStorage

Orderly replacement of the data controller and/or the application contract is another topic (hint: don't break it!). The design anticipates that one might need to extend the schema at some point.

Hope it helps.

2
  • Thank you! This definitely helps. To use this pattern, are we forced to lose the notion of the User struct then?
    – Jordan
    Jun 30, 2018 at 2:24
  • Pretty much. You can use a mapped structure with index as eternal storage to the extent that your confident you will not want to change the layout. I don't know of a way to keep the layout flexible without reducing things to a catch-all key/value store, where the supported types are scalar. You'll see a lot of "upgradeable" patterns that don't support layout add-ons. In the end it's about the extent of upgradeability you're aiming for. Jun 30, 2018 at 3:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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