Libraries in Solidity for code reusability and testing it
What are Libraries in Solidity
You might have heard of the DRY principle (don't repeat yourself). It is very essential in large programs to have the ability to reuse your code as it makes your code more maintainable and readable. In solidity practicing that principle might not be as straight forwards as in other programming languages.
Solidity provides the concept of Libraries to create reusable code that can be called from different contracts. You can think of library to be similar to static functions in a static class in other object oriented programming languages.
Libraries like contracts have to be deployed in order to be used. This also allows you to use libraries deployed by others, but be very cautious in using libraries not created and deployed by you as they can pose a security risk.
Lets look at how to create a library and use it, then we will look into how to deploy it and lastly how to test it. We will be using Truffle for our project. I'm assuming you have basic understanding of how to create contracts.
1. Create and use library
We will create a contract which uses library to maintain a mapping from a person name to age. We use library
keyword to create a library similar to contracts. Unlike contracts library can not have any storage variables, as its only meant for code reuse and not for state management.
// Code for StringToUintMap.sol
pragma solidity ^0.4.15;
library StringToUintMap {
struct Data {
mapping (string => uint8) map;
}
function insert(
Data storage self,
string key,
uint8 value) public returns (bool updated)
{
require(value > 0);
updated = self.map[key] != 0;
self.map[key] = value;
}
function get(Data storage self, string key) public returns (uint8) {
return self.map[key];
}
}
In the above library code we have the Data
struct which contains the mapping from string => uint8
and the functions are very simple wrapper on the mapping insert and get. And we pass in the Data
as storage so the EVM does not create a copy of it in memory but instead passes it by reference from the storage.
Lets create a contract that uses this library to manage mapping of persons name to their age.
// Code for PersonsAge.sol
pragma solidity ^0.4.15;
import { StringToUintMap } from "../libraries/StringToUintMap.sol";
contract PersonsAge {
StringToUintMap.Data private _stringToUintMapData;
event PersonAdded(string name, uint8 age);
event GetPersonAgeResponse(string name, uint8 age);
function addPersonAge(string name, uint8 age) public {
StringToUintMap.insert(_stringToUintMapData, name, age);
PersonAdded(name, age);
}
function getPersonAge(string name) public returns (uint8) {
uint8 age = StringToUintMap.get(_stringToUintMapData, name);
GetPersonAgeResponse(name, age);
return age;
}
}
First we import the library that we want to use. I usually place the library code under libraries hence we import it as
import { StringToUintMap } from "../libraries/StringToUintMap.sol";
In the contract we create a private variable of the struct type StringToUintMap.Data
. We have two contract functions, first is the addPersonAge
which takes in the name and age and calls the StringToUintMap.insert
passing in the struct instance from storage and at the end we emit a PersonAdded
event.
The getPersonAge
calls the StringToUintMap.get
to get the persons age from the mapping and then emits a GetPersonAgeResponse
event with the age.
So as you can see its pretty simple to create library code and use it. But the catch here is that when we deploy our contracts, we need to deploy the library code first and then point to that deployed library address before deploying the contract. This process is called linking.
2. Deploy library and contract
If you run truffle compile
in your project directory and then see the PersonAge.json
file you will find the following in the compiled bytecode
The compiler leaves a place holder for the address of the library code so either you or some tooling can fill it after deploying the library code. If you manually deploy the library code and just replace that with the library address the compiled binary would work.
Gladly you don't have to do any of it as truffle provides you the capabilities to do it easily.
Make changes in your migrations/2_deploy_contracts.js
files so it looks like following
const StringToUintMap = artifacts.require("./StringToUintMap.sol");
const PersonsAge = artifacts.require("./PersonsAge.sol");
module.exports = function(deployer) {
deployer.deploy(StringToUintMap);
deployer.link(StringToUintMap, PersonsAge);
deployer.deploy(PersonsAge);
};
Here we first import StringToUintMap
and PersonAge
then we give instructions to deployer to first deploy StringToUintMap using deployed.deploy(StringToUintMap)
.
Then we tell the deployer to link StringToUintMap with PersonAge contract using deployer.link(StringToUintMap, PersonAge)
. This should replace the placeholder in the compiled code that we saw earlier with the address of the deployed library. At the end we deploy our PersonAge contract as its linked.
If you create more contracts that use the library you have to make changes here to link them with the deployed library before you can deploy the contract which is using the library.
While in development phase you should never deploy or test using Ethereum Mainnet. For a private instance of Ethereum blockchain I use Ganache. It makes it really easy to start your own instance of Ethereum for testing and development and provides a nice UI to see whats happening in the blockchain.
Install and run Ganache and then figure out the RPC Server
Make changes in truffle.js
to point to Ganache RPC server for development.
module.exports = {
networks: {
development: {
host: "localhost", // Ganache RPC server URL
port: 7545, // Ganache RPC server Port
network_id: "*" // Match any network id
}
}
};
Now if you run truffle migrate
in your project you will see your contracts deployed to your Ganache and you can confirm that from the Transactions tab in Ganache UI
3. Test library
Before starting with testing make sure your deployment works otherwise you might get issues when testing. We can test a library using a solidity test. Lets create test\TestStringUintToMap.sol
. A simple test looks like following
pragma solidity ^0.4.15;
import "truffle/Assert.sol";
import { StringToUintMap } from "../libraries/StringToUintMap.sol";
contract TestStringToUintMap {
StringToUintMap.Data private _stringToUintMapData;
function testInsertNewKey() {
// Arrange
string memory key = "test1";
uint8 value = 10;
// Act
StringToUintMap.insert(_stringToUintMapData, key, value);
// Assert
Assert.equal(uint(_stringToUintMapData.map[key]), uint(value), "The key should be added");
}
function testUpdateKey() {
// Arrange
string memory key = "test2";
StringToUintMap.insert(_stringToUintMapData, key, 10);
// Act
uint8 newValue = 20;
bool updated = StringToUintMap.insert(_stringToUintMapData, key, newValue);
// Assert
Assert.isTrue(updated, "The value should be updated");
Assert.equal(uint(_stringToUintMapData.map[key]), uint(newValue), "The value should be updated");
}
function testGetValue() {
// Arrange
string memory key = "test3";
uint8 value = 10;
StringToUintMap.insert(_stringToUintMapData, key, value);
// Act
uint8 result = StringToUintMap.get(_stringToUintMapData, key);
// Assert
Assert.equal(uint(result), uint(value), "The key should have value");
}
}
As the test is written in solidity so we first import truffle/Assert.sol
which contains functions which help us asserting, then we import our library. We create the test contract TestStringToUintMap
and have a private storage variable
StringToUintMap.Data private _stringToUintMapData;
This will allow us to pass it to library code and test how it changed it. We create three different tests
testInsertNewKey: It calls the StringToUintMap.insert
to insert to the mapping and then we assert whether the key
and value
passed in got inserted.
testUpdateKey: Similar to the first test we insert and then later update the value
corresponding to the key
and assert whether the return value was as expected and the function indeed updated the value.
testGetValue: It first setups the test by inserting and then later calls StringToUintMap.get
to retrieve the value and asserts it is as expected.
Now if you run truffle test
you should see your tests running and if everything is fine it will succeed
How internal functions in library are different
Now if you change both the insert
and get
functions in StringToUintMap
to internal
instead of public
it makes the story a bit different. When compiling the library becomes the part of the compiled code of the contract and does not have to be deployed separately.
You wont see the library address placeholder in the compiled code. Hence you don't have to link the library as well. This also has the advantage that internal types can be passed to the library and the memory types are passed as reference instead of being copied. So if you don't have a compelling reason to make library functions public
then keep it internal
.
You can find all the code at Github