Cointime

Download App
iOS & Android

Solidity Storage. How Does It Work?

The Storage is one of the four data locations a solidity smart contract has (the others are : memory, calldata and stack). In simple words, it is the “database” associated to the smart contract, values are persisted after the transaction finishes, which is why it contains the contract’s “state” variables.

Storage definition

Each smart contract storage contains (theoretically) 2**256 slots that are 32 bytes long (technically an infinite amount of slots). A slot is kind of the “basic unit” of the storage, when reading or writing from/to the storage we have to deal with slots and not with individual bytes.

Variables visibility

Storage variables (also called “state” variables) can have the following visibility definitions:

  • Public: when a state variable is set as “public”, solidity will automatically generate an external function to return its value (a “getter” function). The variable will be accessible from any smart contract inheriting from the smart contract where the variable is defined (derived contracts).
  • Internal: internal variables are exactly like public variables except that solidity will not automatically generate the “getter” function. This is the default visibility.
  • Private: private variables are NOT accessible from derived contracts.

It is important to note that storage variables can be read from off-chain applications independently of their visibility definition, “private” variables are not really private….

Storage layout

Variables can be stored on storage in different ways, depending on their data type, the order in which they were defined and sometimes even on their value.

Value types

Value types (uint256, address, bool, …) are stored in the order they are defined.

By default each variable takes one full slot, this can however be a little bit different if its data type is less than 32 bytes long and the previous/next variable is also less than 32 bytes long, in that case, if both variable can fit into a single slot (their combined length is less than or equal to 32 bytes long) solidity packs them.

Fixed size arrays are stored in a similar way, with the only peculiarity that they cannot be packed with any variable defined before or after.

For instance:

uint256 u1; // 32 bytes-long
uint256 u2; // 32 bytes-long
address a1; // 20 bytes-long
bool b1;    // 1 bytes-long
address a2; // 20 bytes-long
bool[5] arr1; // 5 bytes-long
bool b2; // 1 bytes-long

u1 and u2 will take their own slots (slot 0 and slot 1).

a1 and b1 will be packed into a single slot, since their combined length is 21 bytes (slot 2)

a2 will take its own slot too, since it does not fit in the remaining 11 bytes from slot 3 (slot 3).

arr1 will start in a new slot because it is a fix-size array (even if technically it could fit in the previous slots remaining 12 bytes) and its values will be packed (slot 4).

b2 will start in a new slot (despite been only 1 byte, which could fit into the previous slot) because the previous variable was a fix-size array (slot 5).

Reference Types

Reference types are : dynamic-size arrays, mappings and structures.

  • Dynamic-size arrays: They take their “own” slot (like a uint256 data type would) which contains the length of the array. The values of the array are stored in order starting at position keccak256(p) and can be packed together (where “p” is the array “own” slot position)
  • Mappings: Stored in a similar way to dynamic-size arrays, only that the mapping “own” slot does not store anything (mappings do not have length as opposed to arrays) and values are stored at position keccak256(h(k) . p) (where “p” is the mapping “own” slot position, “k” is the mapping’s key we are accessing, “h” is a padding function for keys that are less than 32 bytes and “.” is a concatenation function). Mappings values are kind of scattered all over the storage, which is why packaging is not possible.
  • Structures: Structures are stored in exactly the same way as fix-size arrays, they start on a new slot and the next variable after the struct starts on a new slot too. Variables inside the structure can be packed together.

Bytes & Strings

Bytes and Strings (which are basically fancy bytes) storage policy depends on their size.

  • Less or equal to 31 bytes (short bytes/strings): length and value store in a single slot like:
Value: stored in the higher-order bytes (left aligned).
Length: stored in the lowest-order byte (rightmost byte) as length * 2.
  • More or equal to 32 bytes (long bytes/strings): stored in a very similar way to dynamic-size arrays, with the only difference that the “own” slot does not store the length but : 2 * length + 1 (This is done to make the difference between short and long bytes/strings, by simply looking at the last bit of the byte/string own slot, if bit = 0 then SHORT, if bit = 1 then LONG)

Inheritance

The solidity compiler accepts multiple inheritance and uses the C3 linearization algorithm (beyond the scope of this blog) to determine the final “linear” hierarchy of parent smart contracts.

State variables defined in parent smart contracts get inherited by their children to form the final storage layout, the order of those variable is determined by the C3 linearization algorithm result.

contract parent_1
{
  uint256 p1_u;
  address p1_a;
}

contract parent_2
{
  uint256 p2_u;
  address p2_a;
}

contract child is parent_1, parent_2
{
  uint256 u;
  address a;
}

// FINAL STORAGE LAYOUT FOR "child":

uint256 p1_u; // Slot 0
address p1_a; // Slot 1
uint256 p2_u; // Slot 2
address p2_a; // Slot 3
uint256 u;    // Slot 4
address a;    // Slot 5

This is very important to keep in mind when upgrading smart contracts (using transparent proxies or any other pattern) because if we add state variables to parent contracts, or new contracts with state variables to the inheritance, those new state variables might “shift” down the previous ones which may lead to undesired and unexpected consequences.

contract parent_3
{
  uint256 p3_u;
  address p3_a;
}

contract child is parent_1, parent_2, parent-3
{
  uint256 u;
  address a;
}

// FINAL STORAGE LAYOUT FOR "child":

uint256 p1_u; // Slot 0
address p1_a; // Slot 1
uint256 p2_u; // Slot 2
address p2_a; // Slot 3
uint256 p3_u; // Slot 4 : p3_u will contain the value u had
address p3_a; // Slot 5 : p3_a will contain the value a had
uint256 u;    // Slot 6 : u will be 0
address a;    // Slot 7 : a will be 0x

Gas cost

Saving data in storage means saving data on the blockchain forever (or until you remove it) which is why dealing with storage in ethereum is very expensive in terms of gas.

Removing data from storage on the other hand allows for some transaction gas to be refunded, this is done to encourage developers to “release” storage that is not needed anymore.

Another important Gas related policy to keep in mind when dealing with storage is the concept of “cold and warm” accesses. Since EIP-2929, the EVM makes the difference between the first time we access a storage variable within a transaction (cold access, it does not matter if it is a read or write access) and the rest (warm access):

  • Reading from storage (SLOAD opcode only) : Cold read costs 2'100 gas, Warm read costs 100 gas.
  • Writing to storage (SSTORE opcode only): If we are setting a variable from 0 to a non-zero value it will cost 22'100 gas if it is a cold write or 20'000 if it is a warm write. If we are changing a previously set variable value, it will cost 5'000 for a cold write and 2'900 for a warm write. If he value we are writing is the same one the variable already has, then the gas cost will only be 2'200 for a cold write and 100 for a warm write.
  • Refunds: If we reset a storage variable to 0, we can get a 20% transaction gas refund (or up to 4'800 gas per reset variable).
/* Disclaimer : this code is only to illustrate the gas costs, 
 it does not make much sense for itself... */

uint256 _u1;

function gasCostRead() external returns (uint256)
{
  uint256 uCold = _u1; // Cold Read : Sload = 2'100 gas
  uint256 uWarm = _u1; // Warm Read : Sload = 100 gas
  retun u1;
}

function gasCostWrite() external
{
  _u1 = 3; /* Cold Write : Sstore = 22'100 gas (if _u1 was 0), 
                                    5'000 gas (if _u1 was neither 0 nor 3
                                    2'200 (if _u1 was 3) */
  _u1 = 4; // Warm Write : Sstore = 2'900 gas because _u1 was 3 
  _u1 = 4; // Warm Write : Sstore = 100 gas because _u1 was 4 already
  _u1 = 0; // Warm Write : Sstore = 2'900 gas because _u1 was 4 + 20% refund (max 4'800 gas)
  _u1 = 3; // Warm Write : Sstore = 20'000 gas because _u1 was 0
}

Best Practices

When dealing with storage variables

  • Secure writing access: Be sure who has the right to change each and every storage variable since this will affect your contract’s state…
  • Store as little as possible: Storing data in storage is pretty expensive as we saw earlier. In order to make your contract as cheap as possible, only save in it information that can not be stored anywhere else.
  • Define your storage layout pattern in advance: Upgrading smart contracts can lead to storage variables overlapping, which can render your contracts useless. Before deploying even the first version, be sure you are using a layout pattern that will give you the possibility to painlessly upgrade it in the future (check: eternal storage, unstructured storage, gap pattern, …)
  • Access data as little as possible: Accessing storage variables is also expensive despite cold reads been relatively cheap. As a general rule of thumb, if you are going to read the same storage variable multiple times within the same transaction (and NOT modify it), just copy it into a local variable and read it from there.
  • Packing variables: Defining variables that are less than 32 bytes next to each other can lead to solidity packaging them together, which can be a good idea if your are using them in your code at the same time (within the same transaction) because it will help you save gas. HOWEVER, if these variables are completely “unrelated”, using them can turn out to be even more expensive than if they were not packed together!! This happens because solidity works with 32 bytes words, and in order to extract the variable it needs from a packed slot, it will have to run some extra tasks.
  • Set to zero whenever it makes sense/possible: We saw earlier that resetting a storage variable to zero will entitle the user that submitted the transaction to a refund, which means a lower gas cost. It can thus be a good idea to reset variables if it makes sense from a business point of view. HOWEVER resetting to many variables can actually lead to the opposite result, this happens because if the refund is already at 20%, another extra reset will only be able to refund 20% of the cost added by the reset operation itself!! which means that the total cost of the transaction will actually increase by 80% of the cost added by the reset operation…..
Comments

All Comments

Recommended for you

  • Iranian Military: Ready to Respond Decisively to 'Enemy's Breach of Promises'

    On April 21, local time, Abdollahi, commander of the Khatam al-Anbiya Central Command of the Iranian Armed Forces, stated that Iran is prepared to respond decisively to the 'enemy's breach of promises.' Abdollahi emphasized that the current Iranian military possesses 'authority, readiness, and comprehensive strategic capabilities.' He noted that the Islamic Revolutionary Guard Corps and other defense forces have demonstrated combat capabilities in relevant operations, putting 'Israel and the United States in a difficult and fatigued position,' forcing them to 'seek a ceasefire.' Abdollahi also stressed that the Iranian armed forces maintain a high level of unity with the government and the people under the supreme leader's unified command, and will respond 'decisively, resolutely, and promptly' to any threats and actions. (CCTV News)

  • Another Iranian Oil Tanker Returns to Iran After Breaking US Blockade

    On April 21, according to CCTV News, maritime intelligence company 'TankerTrackers' reported that a tanker belonging to the National Iranian Tanker Company returned to Iran after unloading approximately 2 million barrels of crude oil in Indonesia, crossing the relevant maritime blockade line. The tanker is currently en route to Iran's main oil export hub, Khark Island, and is expected to arrive on April 22 local time. It is reported that the tanker set sail from Iran in late March, heading towards the Riau Islands of Indonesia.

  • White House: US and Iran on the Verge of Reaching an Agreement

    On April 21, White House Press Secretary Kayleigh McEnany stated in an interview with Fox News on the evening of the 20th that the United States and Iran are on the "verge of reaching an agreement." McEnany remarked, "The US has never been closer to achieving a truly good deal." However, she did not disclose any information regarding the current status of the negotiations. McEnany noted that even if an agreement is not reached, President Trump has multiple options and is not afraid to utilize these measures. Previous actions have demonstrated that Trump is not just "bluffing."

  • Kelp DAO Attacker Transfers 30,800 ETH to Special Address

    On April 21, news emerged that, according to monitoring by PeckShield, the Kelp DAO attacker transferred 30,800 ETH to a special address starting with 0x00000, possibly indicating a destruction action.

  • Trump: 'Midnight Hammer' Completely Dismantled Iran's Nuclear Dust Base

    On April 21, U.S. President Trump stated that the 'Midnight Hammer' operation has completely destroyed the 'nuclear dust' base within Iran. As a result, the cleanup will be a long and arduous process. The fake news media, including CNN and other corrupt media networks and platforms, have failed to give our great pilots the credit they deserve, instead always attempting to belittle and undermine them. They are losers!!! (Dongxin News Agency)

  • BTC Drops Below $76,000

    Market data shows that BTC has dropped below $76,000, currently priced at $75,999.63, with a 24-hour increase of 1.68%. The market is experiencing significant volatility, so please ensure proper risk management.

  • Japan Officially Allows Export of Lethal Weapons Through Cabinet Resolution

    On April 21, according to Kyodo News, the Japanese government officially revised the 'Three Principles on Transfer of Defense Equipment' and its operational guidelines during a cabinet meeting, which will, in principle, allow the export of lethal weapons. (Xinhua News Agency)

  • Trump Claims Iran Will Negotiate

    On April 21, during a phone interview with CNN, U.S. President Trump stated that Iran "will negotiate" and expressed confidence in potential talks set to take place in Pakistan. Trump remarked, "They will negotiate; if they don't, they will face unprecedented problems." He also expressed hope that both sides could reach a "fair agreement" and emphasized that Iran "will not have nuclear weapons." Additionally, he defended military actions against Iran by stating there was "no choice" and claimed that they would ultimately "wrap things up."

  • Amazon to Invest Additional $5 Billion in Anthropic

    On April 21, Amazon announced on Monday that it will invest an additional $5 billion in the artificial intelligence company Anthropic, bringing the total investment to as much as $20 billion. Anthropic develops the Claude chatbot and programming tools, and plans to invest over $100 billion in Amazon's cloud technology and chips over the next decade.

  • Three U.S. Carrier Strike Groups May Deploy Simultaneously in the Middle East

    On April 21, according to CCTV, the U.S. military is expected to deploy three carrier strike groups simultaneously in the Middle East in the coming days. Currently, the USS Lincoln strike group is stationed in the Gulf of Oman, near the Strait of Hormuz, participating in maritime blockade operations; the USS Ford strike group is located in the northern Red Sea; and the USS Bush strike group, which is taking a route around Africa, is heading north from the southeast of Africa and is expected to enter the Arabian Sea—this carrier may replace the USS Ford in its mission. In the short term, the U.S. military may have three aircraft carriers in the Middle East.