Storage and memory secrets in Solidity

This time, we will talk about storage locations in Solidity programming language, specifically about the storage and memory locations. Not knowing what they represent and how they work can cause issues in our smart contracts.

Storage

Storage in smart contracts holds data between function calls. We can imagine that storage would be as a hard drive in the computer. Even if we turn it off, the data stays and isn't erased. On the blockchain, what we write in storage is stored.

Storage by default

State variables

By default, Solidity will keep in storage smart contract's state variables.

contract StorageContract {
  struct LuckyNumber {
    uint256 number;
    string reason;
  }

  mapping(address => LuckyNumber) luckyNumbers;
}

In this example smart contract's state variables luckyNumbers are kept in storage, and data will persist between function calls.

When we add and get the lucky number, we have a predictable outcome.

function addLuckyNumber(LuckyNumber memory luckyNumber) external {
  require(luckyNumber.number != 0, "Lucky number can't be 0!");
  require(luckyNumbers[msg.sender].number == 0, "You already have set lucky number. Edit it if you have another one.");

  luckyNumbers[msg.sender] = luckyNumber;
}

function getMyLuckyNumber() external view returns(uint256) {
  require(luckyNumbers[msg.sender].number != 0, "You don't have a lucky number set yet.");

  LuckyNumber memory luckyNumber = luckyNumbers[msg.sender];

  return luckyNumber.number;
}

Local function variables

Local function variables of struct, array, or mapping are saved in storage by default. It means that if we declare these values in our functions, they are kept in storage, which can cause unexpected issues that are hard to track.

If we add a function editLuckyNumber to our code example and mark a local copy as storage it will edit the state variable that we expect.

function editLuckyNumber(uint256 luckyNumber) external {
  require(luckyNumber != 0, "Lucky number can't be 0!");
  require(luckyNumbers[msg.sender].number != 0, "You don't have a lucky number set yet.");

  LuckyNumber storage _luckyNumber = luckyNumbers[msg.sender];
  _luckyNumber.number = luckyNumber;
}

Memory

In memory, Solidity keeps all locally defined value types, which can be uint, string, etc., but not an array, a struct, or a mapping. Function arguments are kept in memory as well. Remember that memory can't be used at the smart contract level, only locally in functions.

function multiplyByItself(uint256 number) external pure returns(uint256) {
  uint256 result = number * number;

  return result;
}

In this example, the function argument number that we pass in our function is stored in memory. Also, locally defined variable of the result is stored in memory and will be released as soon as the function's execution ends.

Pitfall using memory and storage

One of the major pitfalls of wrong usage of the storage and memory keywords in the Solidity programming language is that we declare a variable either storage or memory without thinking it through. First, keeping data in storage will consume more gas because we need to pay for the block space. Second, we should ask ourselves whether we need to access data that we keep between the function calls. By function calls, there can be even two different functions.

If we define the _luckyNumber in function editLuckyNumber using the memory keyword, it will edit this function locally only, and changes won't be written to the blockchain.

function editLuckyNumber(uint256 luckyNumber) external {
  require(luckyNumber != 0, "Lucky number can't be 0!");
  require(luckyNumbers[msg.sender].number != 0, "You don't have a lucky number set yet.");

  LuckyNumber memory _luckyNumber = luckyNumbers[msg.sender];
  _luckyNumber.number = luckyNumber;
}

This function's outcome will result in editing the lucky number not working because we update it only locally.

TL;DR

Storing data using Solidity language in our smart contracts is a crucial thing. Life is easier with value types, but with arrays, structs, and mappings, it is more tricky. That's why it is essential to ask whenever we want to save these variables. Do we want data to persist between calling the smart contract or being saved locally while executing the function?

Links