Skip links
solidity libraries

Libraries in Solidity

A brief tutorial explaining what Solidity libraries are, their benefits, and how to use them effectively in your smart contracts. It includes practical examples and step-by-step instructions to help you integrate libraries into your projects.

What is a Library?

We can think of libraries as contracts with reusable blocks of code. The key difference is that libraries have no storage and cannot store ether. The purpose of libraries is to avoid unnecessary code repetition, allowing developers to reuse external code. By reusing code, libraries minimize the amount of repeated logic that needs to be deployed onchain, resulting in lower gas fees. Libraries can also be embedded directly into contracts as internal functions, further optimizing gas usage and execution speed.

The possibility to be used by different contracts without needing redeployment can increase the security of our contracts, for example by using tested libraries such as those provided by OpenZeppelin. Using these well-audited libraries reduce the risk of vulnerabilities in common functionalities, enabling developers to focus on their specific application logic.

The use of libraries also makes the developer’s job easier, as it makes the code simpler and cleaner, allowing both the development team and other developers to understand it better. This simplicity stems from code reuse, which reduces redundancy and enhances readability.

How visibility affect Library functions

Library functions accept the same visibility modifiers as regular contracts. However, when developing libraries, it’s crucial to understand the distinctions between them, as this differences will affect both deployment and execution. This flexibility in function visibility enables developers to precisely tailor the accessibility and behavior of library functions to meet their specific requirements.
Comment

Libraries with external functions are deployed independently. This means that contracts use them via delegatecall, executing the library ‘s logic within the context of the calling contract and allowing the library to access and modify the storage of the contract. Although this generates a slightly higher gas cost, it can be more efficient when using the library for multiple contracts.

Public functions in libraries behave similarly to external ones, allowing calls from outside the library and requiring separate deployment. In contrast, private functions are only accessible within the library itself, preventing direct access from contracts using the library.

On the other hand, libraries with internal functions are included directly in the calling contract at compile time, as if the library were part of the contract itself. This causes a lower cost of gas in the calls, but increases the cost and size of the deployment.

Contracts utilizing libraries that contain both internal and external functions will have a hybrid structure: internal functions will be directly embedded, while external functions will still be deployed as separate contracts. This mixed approach can offer strategic benefits in certain situations. For example, developers can designate frequently used, simple functions as internal to optimize gas efficiency, while more complex or less frequently accessed functions can be made external to manage contract size effectively. This flexibility allows for a careful balance between performance optimization and code organization.

delegatecall is a low-level function that allows one contract to call another contract and run its code within the context of the calling contract

How to implement a library

To understand the implementation let’s look at a simple example in parts. This is a basic library with a function that receives two values and adds them together.

/AddLibrary.sol

pragma solidity ^0.8.9;

library AddLibrary {
    function add(uint a, uint b) public pure returns (uint){
        return a + b;
    }
}

To use the library in our contract, we must first import it. Once imported, we can utilize its functions and structures within our contract code.

/Add.sol

pragma solidity ^0.8.9;
import "./AddLibrary.sol";

contract Add {
    function add(uint a, uint b) public pure returns (uint256){
        return AddLibrary.add(a, b);
    }
}

In Solidity, the calling contract can modify its storage while executing library code. This is possible because the contract executes the library code within its own context using delegatecall. To interact with the contract’s state, you must use the storage keyword for parameters representing state variables.

Additionally, libraries can define custom data structures using struct. These structs can be referenced from the contract that imports the library. When a contract uses a library-defined struct, it gains access to a reusable and consistent data structure.

In this case we can see a library which defines a simple struct with the data type to store the points and a method to add points to it.

/PointsLibrary.sol

pragma solidity ^0.8.9;

library PointsLibrary {
    struct Player {
        uint score;
    }

    function incrementScore(Player storage _player, uint points) external {
        _player.score += points;
    }
}

In this example, we call the library function with the parameter that will be modified. This parameter, which represents a storage variable, allows the library function to directly alter the contract’s state.

/Points.sol

pragma solidity ^0.8.9;
import "./PointsLibrary.sol";

contract Points {
    mapping(uint => PointsLibrary.Player) public players;
    
    function foo(uint _points) external {
        PointsLibrary.incrementScore(players[0], _points);
    }
}

There’s a useful tip for making library function calls shorter and more intuitive. Instead of passing the storage variable as the first parameter every time we call a library function, we can define the library’s usage using the using keyword. This keyword is followed by the library name and then the data type to which we want to attach the library functions. Here’s an example of how this works:

pragma solidity ^0.8.9;
import "./PointsLibrary.sol";

contract Points {
    using PointsLibrary for PointsLibrary.Player;
    mapping(uint => PointsLibrary.Player) public players;
    
    function foo(uint _points) external {
        players[0].incrementScore(_points);
    }
}

Wrap up

Libraries in Solidity are powerful tools that developers often underutilize. They facilitate the creation of reusable code, enhance contract readability, and optimize gas costs, which are crucial factors in blockchain development. Despite their potential to significantly improve code quality and reduce deployment expenses, libraries remain underappreciated in the Ethereum development community. Increasing awareness and adoption of libraries among developers could lead to more efficient, secure, and cost-effective smart contracts. This, in turn, would generate a positive impact on the entire Ethereum ecosystem, fostering innovation and elevating the quality of decentralized applications.

Useful Resourses