The nitty-gritty of Ethereum and Solidity: Libraries.

Alberto Molina
Coinmonks
7 min readNov 4, 2023

--

Libraries in Solidity serve the same purpose as they do in any other programming language: to facilitate code reuse across different applications, in this case, “smart contracts.”

Adhering to development best practices, it is essential to avoid code duplication. In simpler terms, shared functionality should be consolidated into a single location that all dependent objects reference. This is precisely the role that libraries in Solidity fulfill. While achieving the same results is possible through “regular” smart contracts, this blog will delve into the advantages of using libraries.

Library definition

The first concept we must grasp is: what exactly is a library in Solidity? Put simply, a library is essentially a smart contract. This makes perfect sense since smart contracts are the only code containing objects that EVM blockchains can manage. Therefore a library has not only some bytecode, but can also have an address, a nonce and a balance, although the only thing we will care about is the bytecode and potentially the address.

A library can contain methods of any visibility (public, internal, …), modifiers (only for internal use, they cannot be exported and used by other smart contracts) and custom data types (structures and enums) but cannot contain state variables since libraries are not meant to be used as independent smart contracts but only as reusable code providers.

Libraries can be classified into two categories: embedded and external.

Embedded

An embedded library is a type of library that does not undergo deployment on the blockchain as a standalone smart contract. Consequently, it lacks an address, balance, or nonce. Instead, it serves as a code snippet that, during the compilation phase, becomes integrated into the smart contract(s) that reference it. In essence, it functions as a Solidity structure that is inherited by other smart contracts. This inheritance is somewhat akin to a parent-child relationship seen in inheritance, but there are distinctions that we will explore towards the conclusion of this article.

A library is embedded into the contract if all library functions are internal.

External

An external library is a type of library that is deployed onto the blockchain as a smart contract. However, this “smart contract” library possesses some distinct characteristics :

  • State changing methods can only be invoked by another smart contract through a “delegate call”.

During the deployment of the library, the library’s address is stored as an immutable constant within the library itself. It’s important to note that constants are not considered part of a smart contract’s state. As mentioned earlier, libraries cannot have their own state variables.

Whenever an external or public method of the library is called, the current context’s address is compared to the library’s stored address. If they match, the call is reverted.

  • Non-state changing methods, such as view and pure functions, can be invoked without any restrictions.
  • Libraries should not have ether (their balance should always be 0). Consequently, library methods cannot be declared as payable, and receive and fallback functions cannot be implemented within a library.

It’s important to note that these restrictions do not offer an absolute guarantee that a library will not have funds associated with it. There may be scenarios where funds were sent to the library’s address prior to the library’s deployment or during the deletion of a smart contract.

The key point here is that libraries are not designed to actively manage their own funds, which is why efforts are made to prevent anyone from sending ethers to them. While it doesn’t completely eliminate the possibility of funds being associated with a library, it ensures that libraries are primarily focused on providing reusable code rather than handling monetary transactions.

  • A library cannot be deleted. This is not strictly a restriction in and of itself but rather a consequence of the initial restriction mentioned in the list. Given that state-changing methods can only be invoked in “delegate” mode, if any library method employs the “selfdestruct” opcode, it will never execute within the library’s context. Instead, it will execute within the context of the calling smart contract, resulting in the deletion of the calling smart contract itself.
  • Modifiers can only be employed by the methods within the library itself. This limitation stems from the fact that modifiers essentially serve as code snippets that are “copy-pasted” by the compiler wherever they are referenced. Libraries are designed to provide callable functionality. Therefore, if you intend to define a modifier’s functionality within a library, you must implement a library function and subsequently invoke it from your smart contract’s modifier. Below is an example illustrating this concept:
library Lib{
function modifierContent() external {
// ---------------------
}
}

contract Contract{
using Lib for *;
modifier actualModifier(){
Lib.modifierContent();
_;
}
function SomeFunc() external actualModifier(){
// -------------
}
}

The two last features not only apply to external libraries but also embedded ones.

Smart contract byteCode size

At first glance it might seem obvious that, from a smart contract byte code size point of view, external libraries are a better option than embedded ones since they get deployed and their byte code does not get added to the smart contract.

However, this is not always the case, as there are situations where embedded libraries can result in a smaller bytecode footprint for your smart contract. Let’s delve into the reasons behind this:

External libraries do not inject their bytecode directly into the smart contracts utilizing them. Nevertheless, this doesn’t imply that no additional code is introduced. Invoking functions from external libraries requires the inclusion of some “boilerplate” code to orchestrate the delegate call and manage its response.

In contrast, embedded libraries only incorporate the bytecode of the methods that are actively used, either directly or indirectly, by the smart contracts.

There might be instances where the “boilerplate” code introduced by external libraries proves to be more substantial in terms of bytecode size than what would be added by an internal library. As a general rule of thumb regarding bytecode size:

  • Small libraries should typically be embedded within the smart contract.
  • Large libraries are more efficiently externalized as separate contracts.

Libraries in Solidity

How to create a library

Creating a library in Solidity is quite straightforward. Instead of using the keyword “contract,” you simply use the keyword “library” and then proceed to implement your methods just as you would with a regular contract.

library MyLibrary{
// code
}

How to use a library

To utilize a library from a smart contract, you should employ the “using X for Y” syntax, where:

  • X: represents the name of the library you intend to utilize.
  • Y: denotes the data type to which you want to associate the library functions.

You have the option to attach the library functions to all types within the contract by using: “using X for *”. Alternatively, if you decide to attach the library functions to a specific type, the library member functions will receive the object they are invoked on as their first parameter.

library MyLibrary {
function isEven(uint256 self)
public
pure
returns (bool)
{
return (self % 2 == 0);
}
}

contract MyContract {
using MyLibrary for uint256;

function checkParity(uint256 _value) public pure returns(bool){
return _value.isEven();
}
}

Difference between libraries and parent contracts

In Solidity, you can reuse code in your contracts by using libraries or contract inheritance. But there are some important differences between the two:

Libraries are collections of functions that can be called from contracts or from other libraries. When a contract calls a library function, the code for the library function is run in the context of the contract that called it. This is done with “delegateCall.” This means that the library has no storage and can’t change the state of the calling contract or access its storage directly using variable names as it has no way to know their name, HOWEVER, a library can technically change a contract’s state using assembly as you can see in the example below.

library Library{
function Mstore(bytes32 _input) external{
assembly{
sstore(0, _input)
}
}

}

contract Contract{
uint256 public value;

using Library for *;

function setValue(uint256 _input)external{
Library.Mstore(bytes32(_input));
}

}

Contract inheritance, on the other hand, means making a new contract that “inherits” the code and storage variables of an existing contract, called the “base” contract. When you make a contract that is based on another contract, the code of the base contract is copied into the new contract, and the new contract has access to all of the storage variables and functions of the base contract.

Methods signatures

Another crucial distinction between libraries and contracts lies in how function selectors are computed.

In a regular contract, a function selector consists of the first 4 bytes of the function signature, which is the result of applying keccak256 to the function name along with its input parameters.

On the other hand, external library functions offer more flexibility in terms of argument types compared to external contract functions. This is because libraries can be attached to data types within a contract and applied to the contract’s storage variables. Consequently, the computation of function signatures, in some cases, differs as follows:

  • If no input argument is specified, the signature is computed just like for a contract.
  • If non-storage value type, arrays, string or bytes argument is specified, the signature is computed just like for a contract.
  • If a non-storage struct argument is specified, its fully qualified name is used to compute the signature.
  • If a storage argument is specified, the computed signature is obviously going to be different than a contract one simply because external contract’s function cannot have storage variables as input arguments…

Let’s just see a few examples in the code below.

You can copy-paste it in remix and test it yourself

library MyLibrary {
struct Struct{
uint256 value;
bool flag;
}

//bytes4(keccak256(methodName()))
function methodName() public pure{
}

//bytes4(keccak256(methodName_2(uint256)))
function methodName_2(uint256 _input) public pure{
}

//bytes4(keccak256(methodName_3(MyLibrary.Struct)))
function methodName_3(Struct calldata st) public pure{
}

//bytes4(keccak256(methodName_4(uint256[] storage)))
function methodName_4(uint256[] storage self) public pure{
}

}

contract MyContract {
using MyLibrary for *;

struct Struct{
uint256 value;
bool flag;
}

//bytes4(keccak256(methodName()))
function methodName() public pure returns(bytes4){
return this.methodName.selector;
}

//bytes4(keccak256(methodName_2(uint256)))
function methodName_2(uint256 _input) public pure returns(bytes4){
return this.methodName_2.selector;
}

//bytes4(keccak256(methodName_3((uint256,bool))))
function methodName_3(Struct calldata st) public pure returns(bytes4){
return this.methodName_3.selector;
}

// Test library methods selectors
function libraryMethodName() public pure returns(bytes4){
return MyLibrary.methodName.selector;
}

function libraryMethodName_2() public pure returns(bytes4){
return MyLibrary.methodName_2.selector;
}

function libraryMethodName_3() public pure returns(bytes4){
return MyLibrary.methodName_3.selector;
}

function libraryMethodName_4() public pure returns(bytes4){
return MyLibrary.methodName_4.selector;
}
}

--

--