Skip to main content

Mappings in Solidity are one of the most used types that the language provides. It is so widely used that they can be found not only in standards such as ERC-20 and ERC-721, but in almost every smart contract deployed in Ethereum blockchain.

In this article we will try to discover the caveats that must be taken into account when dealing with mappings, particularly from our DApp perspective.

A brief introduction

Mapping is a type that allows the storing of key-value pairs, something that developers coming from C#, JavaScript or Java know as Dictionaries, Hash Maps or Hash Tables.

Keys must be unique and used to access the value. Below is a mapping example:

contract Mappings {
    // Mapping declaration
    mapping (address => uint256) public balances;

    constructor Mappings() {
        // Assignment
        balances[0xFEC72b6c4ec23F7E9FbEd61E3FE0A8198e7BA81B] = 50;

        // Retrieval
        uint256 balance = balances[0xFEC72b6c4ec23F7E9FbEd61E3FE0A8198e7BA81B];
    }
}

Some peculiarities

Without entering into the low level characteristics too much and keeping the focus, below are two of the most important aspects of mapping which must be carefully taken into account when designing DApps.

For a deep dive into solidity maps, check out this article.

Cannot be iterated

Mapping function in solidity, by design, cannot be iterated or enumerated as in other languages. This is because they do not keep track of the elements so there is no length or count property. Most importantly, its elements store in a storage position based on the hash of the key.

No null value

Solidity does not support the concept of null or nil so if we try to access a key that does not contain a value we will get the default value, as opposed as other languages where we would get a null value.

This means that we will always get a value, the default value, even if the key does not exist. Following the balances example, we will get this:

const address ACCOUNT_WITH_BALANCE = 0xFEC72b6c4ec23F7E9FbEd61E3FE0A8198e7BA81B;
const address ACCOUNT_WITH_NO_BALANCE = 0x000000004ec23F7E9FbEd61E3FE0A81900000000;

// Balance will be 50
uint256 balance = balances[ACCOUNT_WITH_BALANCE];

// Balance will be 0, the uint256 default value
uint256 balance = balances[ACCOUNT_WITH_NO_BALANCE ];

What does all that mean?

It means that once you create a map and deploy it there is no way for us, as DApps developers, to know what the contents of the mapping are.

Following the balances example, where an account can have zero balance, we will always get a value for every single address we try.

For those cases where we get something different than zero, we can assume then that account is part of the mapping (or a user of our DApp), but what if we get a zero? Could we know if this is an account that has never operated the contract? or one that has been operating the contract but has zero balance?

No… we could not.


Why is that important at all?

The simplest use case is a user reporting a bug where some balance suddenly went to zero. Probably the very first step in the analysis would be to discover whether the user has ever operated at all.

In more advanced situations, we might need to migrate all the balances to another gas optimized version of the smart contract. Or we might just want to get some statistics, who knows…

The example is extremely over simplified and there are many strategies to mitigate that situation, but bear with me as we want to keep the example focused and simple.

One of the most common situations is dealing with these requirements once our DApp is in production. At that point, it is usually too late and the solutions typically imply extreme creativity and expertise.

That is why it is so important to consider alternatives even before designing our smart contracts. Let us see what some of those alternatives might be: To track or Not to track… that is the question.


To track

One of the options implies adding some centralization to our DApp. We can do that tracking via a backend service the activity that is taking place in the mapping.

In simple words and continuing with the balances example, store a copy of the balances in a separate store, like a table in a database.

We could have a table in a PostgreSQL database that could look like this:

AccountBalance
0xFEC72b6c4ec23F7E9FbEd61E3FE0A8198e7BA81B50
0xb794f5ea0ba39494ce839613fffba742795792680
0x71C7656EC7ab88b098defB751B7401B5f6d8976F200
Mapping tracking via SQL table example.

This approach is very simple to understand and enables us to retrieve information without having to query the blockchain. Even though querying the blockchain does not cost any gas, during peaks, it can become slow so creating a poor user experience.

However, this not only implies introducing a backend piece (in case it was not part of the DApp already) but also keeping the smart contract in sync with our database.

In the case that the smart contract is being accessed both from the Back End and directly from the blockchain, we will have to thoughtfully design and emit events so that our Back End component can both listen to them and update the database.


Not to track

Alternatively to the centralized approach, we can introduce a fully blockchain-oriented solution: Enumerable Mappings.

This approach consists of keeping track of the mapping keys in a separate array so that we can iterate over the array and use those indexes to access the mapping. A very simple implementation would look like this:

contract BalancesMap {
    mapping(address => uint256) public balances;
    address[] internal accounts;

    function add(address account, uint256 balance) public {
        balances[account] = balance;
        accounts.push(account);
    }

    function count() public view returns (uint) {
        return uint(accounts.length);
    }
    
    function getAccounts() public view returns (address[] memory) {
        return accounts;
    }
}

With this solution we can solve the previously stated problem where we did not know which keys were in use, as now we not only keep track of the keys for iterating later, but also we can get the list of keys straight away.

Conversely, we are introducing more complexity and in cases where we just need one account, we still have to iterate over the whole list. It also implies more storage usage, and consequently more gas.

I strongly suggest not implementing your own solution and rely on OpenZeppelin EnumerableMap instead.


The equilibrium

Both approaches, as we have seen, have pros and cons. But how can we decide which one is better?

In most cases, combining both is probably the answer. But that is not always possible. I usually follow the guidelines below when making a decision:

  • If our use-case is gas sensitive or we have some restrictions in terms of gas, then using tables is a better choice.
  • If your DApp requires some analytics or background processing, then using tables can be a requirement.
  • If our DApp is fully decentralized, use Enumerable Maps.
  • If we don’t have a Back End component or balances change too often (which makes syncing extremely complicated), using Enumerable Maps can be beneficial.

This guideline is far from being perfect and is intended to serve as a trigger for discussion. Each use case is unique and deserves careful consideration.


Conclusion

Mappings are extremely powerful and flexible, but present challenges that, if not considered from the start, can lead to extremely difficult situations to overcome. The strategy we choose depends largely on the use case we are trying to solve, nonetheless, doing a proper analysis in earlier stages can make our life much easier.

We are always looking for Web3 talent !

Mighty Block is one of the partners of Forte, a platform to enable game publishers to easily integrate blockchain technologies into their games. We believe blockchain will enable new economic and creative opportunities for gamers around the world and have assembled a team of proven veterans from across the industry (Kabam, Unity, GarageGames, ngmoco, Twitch, Disney), as well as a $100M developer fund & $725M funding, to help make it happen. That’s where you come into play.

Feel free to browse all our current open job opportunities in the following link 👇

Facundo La Rocca

Software Architect @ Mighty Block Passionate about Microservices, Cloud Computing and Blockchain

Leave a Reply

Discover more from Mighty Block |

Subscribe now to keep reading and get access to the full archive.

Continue reading