Introduction
Writing deployable smart contracts on the Celo Blockchain is influenced largely by the Solidity programming language. Being skillful in any profession depends on deep is our knowledge of the subject. Understanding Solidity’s data types will increase your proficiency in the language. I have prepared this guide to dive deeper into mapping in Solidity. We will explore a couple of ways you can use mapping while blending it with other Solidity types to give you the desired result.
Prerequisites
To get the best from this tutorial, you need to have general programming knowledge and be at least a beginner in Solidity programming. I also assume that you know how to write unit testing otherwise, I recommend starting the following articles.
Requirements
Basically, you need not install any tools if you are familiar with online IDE like Remix. But I am fond of working with the command line, so VSCode is my favorite.
If you intend to use a Code editor such as VSCode, follow the steps below to set up a hardhat project.
- Install Hardhat
mkdir mappingInSolidity && cd mappingInSolidity
npm install hardhat
- Start the Hardhat project
npx hardhat
- Follow the instructions in the terminal to complete the setup.
Note: You do not need to install all the dependencies given in the terminal. We only need hardhat to compile the contracts which is done by the
npm install hardhat
command.
Contents
- What is mapping in Solidity
- Mapping Conventions and best practices
- Array in Mapping
- Mapping Example
- Enum in Mapping: Using Enum as a Key
- Enum in Mapping: Using Enum both as a Key and value
- Mapping in Struct
- Struct in Mapping
- Nested Mapping
- Interacting through Mapping
1. What is mapping in Solidity
Mapping is a reference type just like arrays and structs in Solidity. It is one of the data types well-used in Solidity and is used to persist data across functions in smart contracts i.e. storage. Mapping is represented as a key-value pair in storage where a generic data type is mapped to another generic or user-defined type.
What are generic types?
These are built-in types in Solidity. Examples are uint256
, uint128
, bytes
, bytes32
, and so on.
What are user-defined types
Just as in C++, you can create your own type in solidity using the generic types. To create a user-defined type, Solidity provides a keyword struct
. To create a user-defined type, use the following syntax.
struct <TypeName> {
}
You use the struct
keyword followed by the desired name. A good practice is to start with an Uppercase such as:
struct MyDataType {
uint128 value;
string surname;
bytes32 password;
}
The syntax of mapping can be written as:
mapping(_KeyType => _ValueType) <visibility> <identifier>;
Where:
_KeyType
− can be any built-in types plus bytes and string except for reference type or complex objects such as struct, mapping, and array.
_ValueType
− can be any type.
2. Mapping Conventions and best practices
Things to note about mapping
-
Mapping can only have the type of storage and is generally used for state variables.
-
Visibility in mapping is optional in earlier versions of Solidity but it defaults to
private
in later versions. -
When Mapping is marked public, Solidity automatically creates a getter function for it.
-
Mapping cannot be directly iterated. But we have special ways of doing that.
-
Complex types such as arrays or struct cannot be used as keys.
-
You cannot return the full contents in a mapping at once.
Declaring a mapping
Precautions must be taken when declaring a mapping.
- In mapping declarations, do not separate the keyword
mapping
from its type by whitespace.
The code below displays examples of mapping. Examine it vividly to identify the best and wrong ways of declaring mapping.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
contract MappingConventionsAndBestPractices {
struct Data {
bytes somedata;
}
enum Steps {START, STOP}
// Good
mapping(uint => uint) good;
mapping(address => bool) isAddress;
mapping(uint => mapping(bool => Data[])) public data;
mapping(uint => mapping(uint => Steps)) steps;
// Wrong
mapping (uint => uint) wrong;
mapping( address => bool ) isAddress2;
mapping (uint => mapping (bool => Data[])) public data2;
mapping(uint => mapping (uint => Steps)) steps2;
}
3. Array in Mapping
In the example code below, I show how arrays can be used inside the mapping.
It explains:
- Declaring an array of hashes in a
mapping
. - Adding, replacing, fetching, and deleting items in an array in a
mapping
. - A generic type
uint256
is used as the key referencing the value array ofbytes32
values. - Different numbers within
2^256-1
cannot have values other than a list of hashes. Solidity will automatically create a getter function for thehashes
with typeuint256
as an argument. So if we give any number within the uint256 range, we get an array ofbytes32
in return. - We can loop through the array in the mapping.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
contract ArrayInMapping {
mapping(uint256 => bytes32[]) public hashes;
/**@dev Retrieves a hash from the list of hashes in a mapping
@param hashId : Index or position of the hash in the array of hashes
@param outerKey : outer index or key in the mapping
Note:
*/
function getHash(uint outerKey, uint hashId) public view returns(bytes32 _hash) {
_hash = hashes[outerKey][hashId];
}
/**@dev Retrieves an array of hashes from the mapping using a number of type uint as a key
@param outerKey : outer index or key in the mapping
*/
function getHash(uint outerKey) public view returns(bytes32[] memory _hashes) {
_hashes = hashes[outerKey];
}
/** @dev Adds an item to the end of an array in a mapping
*/
function addItem(uint outerKey, bytes32 newHash) public {
hashes[outerKey].push(newHash);
}
/** @dev Replace an item in an array in mapping at a specific index
*/
function replaceItem(uint outerKey, uint index, bytes32 newHash) public {
hashes[outerKey][index] = newHash;
}
/**@dev Deletes an array of hashes in a mapping
*/
function deleteHashes(uint outerKey) public {
delete hashes[outerKey];
}
/**@dev Deletes a hash in an array in mapping
Note: This method leaves a gap in the array
*/
function deleteHash(uint outerKey, uint hashId) public {
delete hashes[outerKey][hashId];
}
}
4. Mapping Example
Here is an example showing how mapping can be used with a few generic types: bytes
, string
, bytes32
, and uint
. The code displays how we can fetch data of different types from mapping. I strongly recommend going through the code to fully understand how mapping was used.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
contract MappingExamples {
// Maps uint to bytes
mapping(uint256 => bytes) public nameCodes;
// uint256 to string
mapping(uint256 => string) public names;
// bytes to bytes32
mapping(bytes => bytes32) public nameHashes;
// bytes32 to uint
mapping(bytes32 => uint) public ids;
function getNameCode(uint key) public view returns(bytes memory) {
return nameCodes[key];
}
function getName(uint key) public view returns(string memory) {
return names[key];
}
function getNameHash(string memory key) public view returns(bytes32) {
return nameHashes[bytes(key)];
}
function getNameCode(bytes32 key) public view returns(uint) {
return ids[key];
}
}
5. Enum in Mapping: Using Enum as a Key
This is one of the patterns I love using when coding in Solidity. The below code demonstrates how to use an enum in mapping. An enumerator otherwise called enum
is used as a key.
Here is how enums are declared in solidity
enum <identifier> { MEMBER1, MEMBER2, ..., MEMBERn }
-
In the contract below, we use the enum
Form
to denote the different classes of students while thestudents
mapping shows the number of students in each class. -
The modifier
ensureIndexTally
ensures the argument supplied does not exceed the length of the members of the enumForm
else we get an error.
Note: Similar to arrays, enums are zero-based so the index of the last item in our enum
Form
will be 5.
- In the constructor, we accept an array of the number of students in the classes
Note: The length of the
umberOfStudents
cannot be greater than 6 else it will not deploy.
- Since we have the length of the
umberOfStudents
array tally with the enumForm
, we can run a loop to simplify the process instead of adding them one after the other.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
/**@title EnumVsMapping:
*/
contract EnumInMappingEnumAsKey {
enum Form { A, B, C, D, E, F}
///@dev Using enum as the key, and uint256 type for value
mapping(Form => uint256) public students;
modifier ensureIndexTally(uint8 classID) {
require(classID < 6, "ID Out of bound");
_;
}
constructor(uint[6] memory numberOfStudents) {
for (uint i = 0; i < numberOfStudents.length; ++i) {
students[Form(i)] = numberOfStudents[i];
}
}
function fetchTotalStudentInAClass(uint8 classID) public view ensureIndexTally(classID) returns(uint256) {
return students[Form(classID)];
}
/**@dev Change the number of students in a class
*/
function updateNumberOfStudents(uint8 classID, uint newNumberOfStudent) public ensureIndexTally(classID){
students[Form(classID)] = newNumberOfStudent;
}
}
6. Enum in Mapping: Using Enum both as a Key and value
The EnumInMappingEnumToEnum
demonstrates how to use an enum in mapping both for key and value.
In the contract below, we use the enum Class
to denote the different classes in an inter-house sport, while Positions
represents each Class’s performances.
Note: In
enum Class {BLUE, YELLOW, GREEN, RED}
Clas.BLUE IS equivalent to 0.
Class.YELLOW = 1.
Class.GREEN = 2.
Class.RED = 3.
-
The modifier
ensureIndexTally
enforces a check that ensures the argument supplied does not exceed the length of the members in theClass
enum else we get an error. -
In the construction, we want to be sure each of the houses has no position set yet. Although, by default in the mapping, each house has
NONE
as a position. But for demonstration purposes, it is ok to do that.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
contract EnumInMappingEnumToEnum {
enum Class {BLUE, YELLOW, GREEN, RED}
enum Position {NONE, FIRST, SECOND, THIRD, FOURTH}
///@dev Using enum for both key and value.
mapping(Class => Position) public performances;
modifier ensureIndexTally(uint8 classID) {
require(classID < 4, "ID Out of bound");
_;
}
constructor() {
for (uint i = 0; i < 4; i++) {
performances[Class(i)] = Position.NONE; // Each house will have no positon
}
}
/// @dev retrives the performance of a particular house
function getPerformance(uint8 classID) public ensureIndexTally(classID) returns(string memory) {
return _getPosition(uint8(performances[Class(classID)]));
}
function _getPosition(uint8 index) internal returns(string memory _position) {
_position = index == 4 ? "FOURTH" : index == 3? "THIRD" : index == 2? "SECOND" : "FIRST";
}
/**@dev Change the number of students in a class
*/
function updatePerformance(uint8 classID, uint positionID) public ensureIndexTally(classID){
// Ensure the positionID tally with the length of the Position enum;
performances[Class(classID)] = Position(positionID);
}
}
7. Mapping in Struct
The following code displays how user-defined type i.e.struct
can be used in a mapping.
-
We declared a
struct
as a reference type to the data we will need in the storage. -
Inside the struct, we also declared a mapping. Even though a mapping is a storage reference, yet we do not have any data in storage until we explicitly created a top storage reference as a state variable named
data
. -
The
data
storage reference creates a slot in storage withData
as the type of data we can keep in the storage. -
The
Profile
struct was used as the value with an address as the key or reference. If an address is given, aProfile
object is returned. -
I used the
signUp
function as an example to show how we can input data to the mapping via the state variabledata
. It simply signs up a new user. Another functiondeleteAccount
allows users to remove themselves from the system whilegetUserInfo
returns information about specific user addresses.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
contract MappingInStruct {
error AlreadySignedUp();
struct Profile {
string surname;
uint age;
uint balance;
bytes32 dataIntegrity;
bool isUser;
}
// Structured data that contain users' balances and profiles
struct Data {
mapping(address => Profile) profiles;
}
Data private data;
///@dev Signs up a new user
function signUp(string memory surname,uint8 age) public payable {
if(data.profiles[msg.sender].isUser) revert AlreadySignedUp();
data.profiles[msg.sender] = Profile(surname, age, msg.value, keccak256(abi.encodePacked(surname, msg.sender, age)),true);
}
///@dev Get user's profile by their address.
function getUserInfo(address who) public view returns(Profile memory _prof) {
_prof = data.profiles[who];
}
///@notice Users can delete their account at will
function deleteAccount() public {
require(data.profiles[msg.sender].isUser, "Not registered");
delete data.profiles[msg.sender];
}
}
8. Struct in Mapping
The example contract below shows how to deep-nest struct in a struct and use the root struct as a value in the mapping. The contract displays how to register students in a school. The power to register students is given to a special account registrar
. In a real-world scenario, a registrar account could be the school admin where centralization plays or a multisig account in a decentralized world.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
contract StructInMapping {
enum Remark {NONE, POOR, AVERAGE, GOOD, EXCELLENT}
enum Term {FIRST, SECOND, THIRD}
enum Classes {FORM1, FORM2, FORM3, FORM4, FORM5}
uint public totalStudents;
address public registrar;
struct StudentData {
Bios bio;
OfficialInfo officialInfo;
}
struct Wallet {
uint totalAmountInWallet;
uint spent;
}
struct Bios {
string surname;
string lastName;
uint age;
uint height;
string stateOfOrigin;
uint dateOfBirth;
}
struct OfficialInfo {
uint dateRegistered;
Classes classAdmitted;
uint tuitionFee;
uint otherFee;
bool admitted;
Wallet wallet;
ClassActivities activity;
}
struct ClassActivities {
uint tests;
uint examScores;
uint grade;
Remark remark;
}
///@dev Maps student address to their data
mapping(address => mapping(Term => StudentData)) public students;
constructor(address _registrar) {
registrar = _registrar;
}
function registerStudent(
string memory surname,
string memory lastName,
string memory stateOfOrigin,
uint8 class,
uint age,
uint height,
uint tuitionFee,
uint dateOfBirth,
address studentID
) public {
require(msg.sender == registrar, "Only registrar is allowed");
require(class < 5, "Out of bound");
students[studentID][Term.FIRST] = StudentData(
Bios(surname, lastName, age, height, stateOfOrigin, dateOfBirth),
OfficialInfo(
block.timestamp,
Classes(class),
tuitionFee,
0,
true,
Wallet(0, 0),
ClassActivities(0, 0, 0, Remark.NONE)
)
);
}
}
9. Nested Mapping
The following contract shows how to nest mapping inside another mapping using the struct Player
as the final value. It’s a simple game I built to show how nested mapping works.
Note: Do not use these contracts in production. Even though I introduced a few necessary checks, yet it can contain potential bugs that may cause severe loss if not properly audited.
- Anyone can be a player by calling the
enterGame
function. - There can be a maximum of 3 players in round after which the game auto locks itself until the admin account explicitly calls the
selectWinner
function, the game will be in locked mode. Using an index, the admin can randomly pick a winner. In production, you’d want to use an unbias means of seleecting a winner such as getting random value into your contracts through an oracle.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
contract NestedMapping {
error GameAlreadyOpened();
error GameClosed();
/**Admin address that rolls the dice.
Often, an oracle is used such as the Chainlink Keeper
*/
address public admin;
bool public gameOpen;
uint public totalPlayerInRound;
// Player information
struct Player {
bool isPlayed;
uint256 winnings;
address account;
}
// Current round of the game
uint256 public currentRound;
// Nested mappings
// Mapping round to position to players
mapping(uint256 => mapping(uint => Player)) private players;
///@dev Only admin can do certain things
modifier onlyAdmin() {
require(msg.sender == admin, "Not authorized");
_;
}
constructor(address _admin) {
require(_admin != address(0), "Admin cannot be the zero address");
admin = _admin;
}
/**@dev Admin selects winner.
After winnings are splitted, the game is open again
*/
function selectWinner(uint winningPosition) public onlyAdmin {
require(!gameOpen, "Player in current round not complete");
Player memory winner = players[currentRound][winningPosition];
uint balances = address(this).balance;
uint256 boardShare = balances - ((20 * balances)/100); // Winner takes 80% of total game balances while 20% goes to the boardman
(bool sent,) = admin.call{value: boardShare}("");
if(sent) {
(bool success,) = winner.account.call{value: balances - boardShare}("");
if(success) gameOpen = true;
}
}
// Player enters current round of game.
function enterGame() public payable {
totalPlayerInRound ++;
if(!gameOpen) revert GameClosed();
require(msg.value > 5 ether, "Insufficient bet");
require(!players[currentRound][totalPlayerInRound].isPlayed, "Player already in game");
players[currentRound][totalPlayerInRound] = Player(true, 0, msg.sender);
if(totalPlayerInRound == 3) {
totalPlayerInRound = 0;
gameOpen = false;
currentRound ++;
}
}
}
10. Interacting through Mapping
Mapping is like a hash table that stores data as key-value pairs where the key can be of any generic or built-in type except for reference types such as struct
and arrays
. It is similar to a dictionary. Solidity did not provide a way to loop through mapping but developers have somehow found ways to achieve this. In the code highlighted below, I show you can loop through a mapping.
-
First, we initialize a counter and an mapping in addition to the mapping we intend to iterate over.
-
The value in the
counterToAddress
mapping points to the key in the next mapping -
We use the
mappings
to intend to iterate over the mapping: -
The
addToMappings(address target)
can be used to add several items to themappings
while we used theiterateOverMappings
to loop over it.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
contract IterationInMapping {
uint counter; // you should initialize this to 0 in the constructor
mapping (uint => address) private counterToAddress;
mapping (address => uint) private mappings;
constructor() {
counter = 0;
}
function addToMappings(address target) public {
counter ++;
counterToAddress[counter] = target;
mappings[counterToAddress[counter]] = counter;
}
function iterateOverMappings() public view returns(uint currentValue) {
for (uint i=0; i < counter; i++) {
currentValue = mappings[counterToAddress[i]];
}
}
}
Compilation
Copy the contracts to Remix IDE to compile or run the following command if you’re using Hardat.
npx hardhat compile
Deployment
To set up your environment deployment onto Celo Alfajores, Follow this guide
Final Note
Your understanding of mapping in solidity is paramount to coding effectively using the Solidity language and writing optimized smart contracts. A lot of times, mapping is preferred to array in storage because it presents data in hash form compared to an array. They can also be expensive where they are deep nested.
Next step
Improve your skills in smart contracts through consistent learning. Start with each data type in Solidity. Gain a deeper understanding of how they work. You’re a step away from becoming a white hat.
About the Author
Isaac Jesse, aka Bobelr is a full-stack web3 developer with proficiency in smart contracts development. He was an ambassador and DevAm for several projects like Algorand etc.