Shared Storage

When an Extendable is extended, it makes use of delegatecall to access functions that exist in separate Extension contracts. However due to the nature of delegation, it makes it harder for contracts to define storage variables in a safe way. For example, let's look at the contract below.

contract Example {
    string example = "example";
    
    function show() public returns(string storage) {
        return example;
    }
}

This contract defines a variable example that exists in the contract storage and the function show() reads from that variable to return it. However if you were build an Extension in a similar fashion, it would result in errors. This is because of how Solidity and the EVM addresses this storage.

Normal Storage

Typical storage variables are declared and defined in the scope of the contract as shown in the above example. This tells EVM what type the variable is, and what order these variables should be laid out in memory. The order in which the variables are defined is the order in which they are organised in smart contract storage, and they are given a slot to designate that order.

The first variable occupies slot 0 and and the next occupies slot 1 etc. Each slot is a 32-byte block. Since some variables can be larger than 32-bytes (such as strings), the way slot assignment works is slightly more complicated, but in essence will follow a sequential rule.

If you attempt to follow such a model using Extensions, what happens? Every Extension would define variables independently, but when an Extendable contract performs a delegate call, all these variables suddenly all occupy slot 0 onwards! This results in a terrible collision of variables and inevitable corruption of data.

We can avoid this problem by using dynamic slot assignment.

Shared Storage

In order to use delegatecall successfully, we have to introduce shared storage. That is storage variables that are defined in different contracts but ultimately end up sharing the same storage space in a single Extendable contract. As alluded to, we must use a dynamic slot assignment method.

Instead of allowing Solidity to decide how to assign slots to each storage variable we intend to use, Extensions will define their own slots to store variables. Since we must make sure that any Extension's variables must never overlap with another, we must choose a slot that does not collide with any other. How do we do that? Since the storage space extends to 2^256 - 1 slots, we have an astronomical address space to assign variables. We then create a pseudorandom slot number that is deterministically generated from a human readable name and assign our variables to begin from that slot number. Due to the unlikelyhood of hash collisions, we can also be similarly confident that address spaces will not collide. More details on this topic here.

Extensions must first define a storage library contract that is imported by Extensions that require access to some variables, and define those variables within.

struct YourVariables {
    string yourString;
    uint256 yourUint;
}

library StorageTemplate {
    bytes32 constant private STORAGE_NAME = keccak256("your_unique_storage_identifier");

    function _getState()
        internal 
        view
        returns (YourVariables storage state) 
    {
        bytes32 position = keccak256(abi.encodePacked(address(this), STORAGE_NAME));
        assembly {
            state.slot := position
        }
    }
}

First define the variables that you need to store in a struct. Then replicate the storage library and give your library a STORAGE_NAME. This name should be as unique as possible and best practices are giving it a human readable name that relates to the variables being stored.

Now Extensions simply import the library and call _getState() to be able to access the storage variables.

Changing the structure of your variables is a very dangerous procedure.

If you are upgrading an Extension and adding more variables, ensure that you only append to the struct.

If you are making non-append changes to the struct, you may have to perform a backup or migration of existing state to avoid risk of data corruption.

Last updated