Solidity Tutorial : all about Mappings

0
48

Solidity Tutorial: all about Mappings

The School Cloackroom by Malcolm Evans, a cartoon drawing used as an analogy to explain mappings in Solidity (taken from https://www.cagle.com/tag/cloakroom/)

Table of Content

  • Introduction to mappings
  • What mappings are used for?
  • How mappings are represented in storage?
  • Key and Values types allowed
  • Operations on Mappings
  • Nested Mappings
  • Mappings as function parameters
  • Structs as value types of mappings
  • Mappings Limitations

Introduction to mappings

Mappings in Solidity are similar to the concept of hash map in Java or dictionary in C and Python. They act like hash tables, although they are slightly different.

There is nothing better than an analogy with the real world to understand what are mappings in Solidity and how they behave. The following example was taken from a Reddit post.

A hashmap (= mapping) is like a cloakroom. You hand your coat over and get a ticket. Whenever you give that ticket back, you immediately get your coat. You can have a lot of coats, but you still get your coat back immediately. There is a lot of magic going on inside the cloakroom, but you don’t really care as long as you get your coat back immediately.

Therefore, mapping as a data structure enables to find the location corresponding to a given key in an efficient way.

What mappings are used for?

Mappings are useful for associations, like associating a unique Ethereum address to a specific balance. This is the case of the standard ERC20 contract. The smart contract keeps track of how many tokens a user owns by using a mapping.

contract ERC20 is Context, IERC20 {    
using SafeMath for uint256;
using Address for address;
    mapping (address => uint256) private _balances;
    ...
}

Another example where a mapping could be used would be to manage and keep track wich users / addresses are allowed to send ether to the contract.

mapping(address => bool) allowedToSend;

Here is another example of how to define a mapping in Solidity. The code sample below would enable to associate a user level to an ethereum address for a simple game written in Solidity.

mapping(address => uint) public userLevel;

How mappings are represented in Storage?

Mappings are represented differently in storage than other variable types.

As we said, mappings enable to find the location of a value in storage, given a specific key. Hashing the key is a good start !

In Solidity, mappings values are represented by the hash of their keys. The 32 byte hash is an hexadecimal value that can be converted to a decimal number. This number represents the slot number where the value for a specific key is hold in storage.

Let’s look at a basic example with the code snippet below.

contract StorageTest {
uint256 a; // slot 0
uint256[2] b; // slots 1-2
    struct Entry {
uint256 id;
uint256 value;
}
Entry c; // slots 3-4
Entry[] d; // slot 5 for length, keccak256(5)+ for data
    mapping(uint256 => uint256) e;
mapping(uint256 => uint256) f;
}

In the above code:

  • the “location” for e is slot 6
  • the location for f is slot 7

but nothing is actually stored at those locations ! There’s no length to be stored, and individual values need to be located elsewhere.

In fact when mapping is declared, space is reserved for it sequentially like any other type, but the actual values are stored in a different slot.

Finding the location in storage of a mapping key

To find the location of a specific value within a mapping, the key (1st !) and the slot of the mapping variable (2nd) are hashed together (after being concatenated).

The hash obtained correspond to the location in storage where the value associated to this key in the mapping is stored. In fact, a 32 byte hash contains hexadecimal characters, that can be converted to a decimal number (that represents the storage slot for a specific key in the mapping). Here is an example.

mapping(address => uint) public balances; // slot 2

The variable balances is located in slot 2 in storage. As seen before, we concatenate both the key with the slot number, as in the formula below:

So in conclusion:

the token balance associated with the address 0x123456…7890 can be found in the contract’s storage slot nb 11233648340…332173
NB: Note the value of slot and keys are represented in hexadecimals., and left padded with empty bytes.

Mapping do not have a length

As seen before, there is no concept of a key and a value “by itself”.

The data for a key is not stored in a mapping, but rather its keccak256 hash is used for storing the value the key data references to.

Because of this, mappings do not have a length.

NB: the same principle applies to nested mappings. We will talk about it later.

Key and value types allowed

The table below lists all the possible variable types that can be used when declaring a mapping.

mapping (KeyType => ValueType) mappingName;

Operations on Mappings

Getter - Reading value for a specific key

To obtain the value associated to a specific key as follow.

function currentLevel(address userAddress)
public
view
returns (uint)
{
return userLevel[userAddress];
}

However, there is a better for reading a value a mapping. Like for any other variable, the keyword public creates automatically a getter function in Solidity.

The same applies to mapping. By adding the keyword public in the mapping definition, this will create a getter function. You will only need to pass the key as a parameter to the getter to return the _ValueType.

mapping(address => uint) public userLevel;

Setter — Writing value for a specific key

Following our example for the getter function, you can set the value for a specific key as follow:

function setUserLevel(address _user, uint _level)
public
{
userLevel[_user] = _level;
}

The code above enables to set the level of an “imaginary” game for a specific user.

Finding the storage slot for a specific key

The code snippet below offer the functionality describe above. It enables to find in which storage slot the value associated to a specific key is stored.

function findMapLocation(uint256 slot, uint256 key) public pure returns (uint256) {
return uint256(keccak256(abi.encode(key, slot)));
}

Nested Mappings

If you have a look at the table above, for keys and value types allowed, you will notice that:

in a mapping, a key can be associated to a value of type mapping

So mappings can be nested !

A great practical example of a nested mapping can be found in the ERC20 smart contract. An address can grant other addresses the right to spend a certain amount of tokens.

mapping (address => mapping (address => uint256)) private _allowances;

In relational database architecture, this relationship is called one-to-many: an owner can grant multiple spenders to spend some tokens on his behalf.

Getter function for nested mapping

If the _ValueType of a mapping turns up to be a mapping too, then the getter function will have a single parameter for every _KeyType recursively.

Let’s go back to our previous allowances example from the ERC20 contract. The function allowance(...) defined in line 125 takes two parameters: owner and spender. Based on our explanation based, we can understand that these are our single parameter defined for every key type recursively.

function allowance(address owner, address spender) 
public
view
virtual override
returns (uint256)
{
return _allowances[owner][spender];
}

How nested mappings are represented in storage?

A nested mapping follows the same pattern as a regular mapping, but in a recursive fashion.

  • Take the hash of the key + slot => we get our resulting result (=key 2)
  • Take the resulting hash and hash it with our key 2 result

Mappings as function parameters

This is a difficult topic, but we will attempt to discuss about it.

Mappings can be passed as parameters inside functions, but they must satisfy the following two requirements.

  • Mapping can be used as parameter only for private and internal functions.
  • The data location for the function parameter (our mapping) can only be storage.

Iterating through a mapping

Why you cannot iterate through a mapping?

Because the 32 bytes hash obtained via keccak256(key, slot) is an hexadecimal value that can be converted to decimals, this lead to an immense possible range. The decimal number obtained can be somewhere in the range of 0 to 2²⁵⁶.

This is why you cannot iterate through a mapping. There would be too many possible keys ( = also read slots) to iterate. As a result:

mappings are virtually initialised. In other words, all possible variables are initialised by default.

So by default, when a mapping is declared, every possible key is mapped to a value whose bytes representation are all zeros.

How to iterate through a mapping in Solidity?

We have already seen that because of their nature, it is impossible to iterate through a mapping. The reason being that when a mapping is defined, all its key are virtually initialised.

In other words, every possible key exist by default, and is mapped to a default zero / null value.

However, it is possible to implement a data structure on top of a mapping, so that we can iterate through it.

Let’s take an example that use an uint as a key. The solution is to have a counter that tells you the length of the mapping where the last value is stored.

contract IterableMapping {
    mapping(uint => address) someList;
uint public totalEntries = 0;
    function addToList() public returns(uint) {
someList[totalEntries] =
address(0xABaBaBaBABabABabAbAbABAbABabababaBaBABaB);
return ++totalEntries;
}
}

A note on Struct as a value type for mappings

An important side note worth mentioning relates to mappings that have a struct as a value type.

If the struct contains an array, it will not be returned via the getter function created by the “public” keyword. You have to create your own function for that, that will return an array.

Mappings Limitations

Mappings do not come without pitfalls. Here are some of them.

  • You can’t not define variables as the _ValueType of a mapping. Remix will throw a TypeError as shown below:
  • As seen before, you can’t iterate in a mapping directly
  • It is impossible to retrieve a list of values or keys, like you would do it in Java for instance. The reason is still the same: all variables already initialised by default. So it’s internally not possible.
  • Mappings can’t be passed as parameters inside public and external functions in smart contracts.
  • Mappings can’t be used as return value for function
Get Best Software Deals Directly In Your Inbox

Solidity Tutorial : all about Mappings was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.