Skip to main content

Command Palette

Search for a command to run...

A Complete Guide to Storage in Solidity Smart Contracts

Published
5 min read
A Complete Guide to Storage in Solidity Smart Contracts
N

Hi! I am Navya, a dedicated coding enthusiast, deeply interested in Web3 technologies and exploring the future of decentralized applications and blockchain innovation.

Introduction

In Solidity, a smart contract interacts with four primary data locations: storage, memory, calldata, and stack. Among these, storage is essentially the “database” tied to the smart contract, where values are persisted after a transaction completes. This is why storage is responsible for maintaining the smart contract’s state variables.

What is Storage?

Storage in Solidity consists of an enormous number of slots (theoretically 2^256 slots), each 32 bytes in size. When reading or writing data to the storage, we handle entire slots rather than individual bytes. This is why storage operations are centered around these slots.

Storage Variable Visibility

In Solidity, storage variables (also known as state variables) can have three levels of visibility:

  1. Public: A public state variable automatically generates an external “getter” function, making the variable accessible from other contracts that inherit it.

  2. Internal: Internal variables behave similarly to public ones, except no getter function is generated. This is the default visibility setting.

  3. Private: Private variables cannot be accessed by derived contracts. However, even private variables can still be read from off-chain applications, so they are not truly private.

Storage Layout and Packing

The way variables are stored in Solidity depends on their data types and definitions. Let’s look at the differences between value types, fixed arrays, and reference types:

Value Types

Data types like uint256, address, and bool are stored in the order they are declared, with each variable typically occupying one slot. However, if two adjacent variables are smaller than 32 bytes combined, Solidity will pack them into a single slot. For example, a bool and an address can fit into one slot if their combined size is less than 32 bytes.

For instance:

uint128 num1;   // 16 bytes, uses 1 slot
address addr1;  // 20 bytes, packed with num1 in 1 slot
bool flag;      // 1 byte, will occupy its own slot
uint256 largeNum; // 32 bytes, occupies a full slot

Fixed-Size Arrays

Fixed-size arrays behave similarly to individual value types, but they cannot be packed with other variables, regardless of size.

Reference Types

Reference types, such as dynamic arrays, mappings, and structures, follow a different approach:

  • Dynamic arrays: Store their length in one slot, and the array elements are stored starting at the position defined by the hash keccak256(p), where p is the position of the array's length.

  • Mappings: Mappings are stored in a hashed format, with the position calculated by keccak256(h(k) . p), making packing impossible.

  • Structures: Structures are stored similarly to fixed-size arrays, with each new structure starting in a new slot, and internal variables being packed together if possible.

Bytes and Strings

For byte arrays and strings, storage behavior depends on their size:

  • If the size is 31 bytes or less, both the value and its length are stored in a single slot.

  • If the size is 32 bytes or more, the data is stored similarly to dynamic arrays, with the slot storing 2 * length + 1 to distinguish between short and long data.

Inheritance and Storage

When dealing with inheritance in Solidity, state variables from parent contracts are inherited by child contracts. The order of the variables in the storage layout is determined by the C3 linearization algorithm. This is important for maintaining compatibility, especially when upgrading contracts. Introducing new state variables in parent contracts may shift the layout and overwrite existing variables, leading to unintended consequences.

For example:

contract Parent1 {
  uint256 num1;    // Slot 0
  address addr1;   // Slot 1
}

contract Parent2 {
  uint128 num2;    // Slot 2
  bool flag;       // Slot 3
}

contract Child is Parent1, Parent2 {
  uint256 childNum;    // Slot 4
  address childAddr;   // Slot 5
}

When the inheritance structure changes, the storage layout of the child contract shifts:

contract Parent3 {
  uint256 newNum;      // Slot 4 (shifts previous variables)
}

contract Child is Parent1, Parent2, Parent3 {
  uint256 childNum;    // Slot 6
  address childAddr;   // Slot 7
}

In this case, newNum from Parent3 takes over slot 4, pushing childNum and childAddr down, potentially leading to incorrect behavior.

Gas Costs and Best Practices

Storage operations in Solidity are expensive in terms of gas, since storing data on the blockchain is permanent. Some key considerations regarding gas costs include:

  • Cold and warm accesses: Cold reads (accessing a storage variable for the first time in a transaction) cost 2,100 gas, while subsequent warm reads cost only 100 gas. Similarly, cold writes are significantly more expensive than warm writes.

  • Refunds: Resetting a storage variable to zero can refund up to 20% of the transaction’s gas cost, but overuse can backfire due to diminishing refunds after the limit is reached.

Here’s an example illustrating gas costs:

uint256 internal value;

function gasCostExample() external {
    uint256 firstRead = value; // Cold Read: 2,100 gas
    uint256 secondRead = value; // Warm Read: 100 gas

    value = 100;  // Cold Write: 22,100 gas if value was 0
    value = 200;  // Warm Write: 2,900 gas because value was 100
    value = 200;  // Warm Write: 100 gas because value didn't change

    value = 0;    // Reset: 2,900 gas with up to 20% refund
}

Best Practices

  1. Minimize storage usage: Store only essential data in storage to reduce costs.

  2. Plan your storage layout: When upgrading contracts, use patterns like eternal storage or unstructured storage to avoid overlapping state variables.

  3. Use local variables: If you need to read the same storage variable multiple times in a transaction, copy it into a local variable to reduce gas costs.

  4. Pack related variables: Group variables smaller than 32 bytes together if you use them in the same transaction, but avoid packing unrelated variables as it may increase gas costs.

By following these practices, you can optimize gas consumption and ensure smoother contract upgrades, making your Solidity smart contracts more efficient and maintainable.