Libraries in Solidity for code reusability and testing it

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
Place holder for library address

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
Ganache 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

Deploying the library and contracts

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

Testing the library

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

Reference