Solidity Libraries: library, using for, and Where pure Lives
What the library keyword actually declares, what it forbids, what using for buys you in notation, and why nearly every function inside a library is marked pure.
TL;DR
- A
libraryis a contract with no state of its own and nothing to remember between calls. It is a set of functions, and that is the entire idea. - What a library may not have is the definition: no state variables, no constructor, no inheritance, no ether.
using SafeERC20 for IERC20;is notation.token.safeTransfer(to, value)becomesSafeERC20.safeTransfer(token, to, value). The value in front of the dot becomes the first argument, and nothing else changes.internallibrary functions are compiled into the calling contract.externalones live at their own deployed address. Most libraries you will read are entirely internal.pureis everywhere in libraries because a function that touches no state can be checked by reading it alone. That is what makes a five hundred line library approachable.
What is a library in Solidity?
A library is a contract that holds no state of its own and is never deployed for anybody to hold a balance in. It is a collection of functions and nothing else, with nothing remembered between one call and the next. The keyword library simply replaces contract on the declaration line.
The word sounds heavy, so it helps to see how small one can be. This is the whole of Uniswap V2's Math, twenty three lines:
// a library for performing various math operations
library Math {
function min(uint x, uint y) internal pure returns (uint z) {
z = x < y ? x : y;
}
// babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
function sqrt(uint y) internal pure returns (uint z) {
if (y > 3) {
z = y;
uint x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
}
}
Two functions, one of which fits on a single line. Both are used by the pair contract Uniswap V2 deploys for every trading pair, so this file sits under a great deal of value while staying short enough to read in one sitting.
A library is usually smaller than the word suggests, because the things that make a contract long, storage, an owner, a constructor, are precisely the things it is not allowed to have.
What can a library not have?
No state variables, no constructor, no inheritance, and no ability to receive ether. That list is not a set of restrictions bolted onto an ordinary contract. It is the definition. Take those four things away from a contract and what remains is a library.
Constants are the apparent exception. Here is the top of OpenZeppelin's Strings:
library Strings {
using SafeCast for *;
bytes16 private constant HEX_DIGITS = "0123456789abcdef";
uint8 private constant ADDRESS_LENGTH = 20;
Two values that look like state and are not. A constant is compiled into the code rather than written to a storage slot, so nothing here differs between one call and the next. The library still remembers nothing.
Notice the second line as well. Libraries lean on other libraries constantly, which is why Strings can import Bytes, Math and SafeCast and still hold no state of its own.
What does using SafeERC20 for IERC20 actually do?
It attaches a library's functions to a type so they can be written as methods. After using SafeERC20 for IERC20;, the expression token.safeTransfer(to, value) compiles to SafeERC20.safeTransfer(token, to, value). The receiver moves into the first argument slot. That is the entire transformation, and nothing else about the call changes.
The library says so in its own header comment:
* To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
And the function it is talking about starts like this:
function safeTransfer(IERC20 token, address to, uint256 value) internal {
if (!_safeTransfer(token, to, value, true)) {
revert SafeERC20FailedOperation(address(token));
}
}
Three parameters, the first of which is an IERC20. That first parameter is the one using SafeERC20 for IERC20; fills in from whatever sits to the left of the dot.
This matters for reading more than for writing. Sooner or later you will meet token.safeTransfer(...), go looking for safeTransfer in the IERC20 interface, and find nothing, because it was never there. The answer is one using line near the top of the file, and once you know to look the dead end stops being one.
What is the difference between internal and external library functions?
Where the compiled code ends up. An internal library function is compiled into every contract that uses it, so there is no separate thing on chain. An external or public one lives at a deployed library address and is reached by delegatecall, which means the library must be deployed and linked separately.
Every function in Uniswap's Math above is internal, as is every function in Strings and SafeERC20 that a calling contract is meant to use. That is the common case by a wide margin: most libraries are source organisation with no independent existence on chain.
Libraries also use private for their own plumbing:
function _safeApprove(IERC20 token, address spender, uint256 value, bool bubble) private returns (bool success) {
That one is not part of the library's surface. It exists for the internal functions above it, and nothing outside the file can reach it. The split you already know from public, private, internal, external applies here unchanged, with external carrying the extra consequence of a separate deployment.
So scan a library's declarations for the word external. If it is absent, there is no deployment and no link step to think about.
Why is almost every function in a library pure?
Because a library has no state to read. With no storage of its own, a function can only work with the arguments handed to it, and pure is the word for exactly that. The payoff for a reader is large: a pure function can be verified by reading it alone, with no reference to anything else.
Uniswap's SafeMath is three functions and three lines of body:
function add(uint x, uint y) internal pure returns (uint z) {
require((z = x + y) >= x, 'ds-math-add-overflow');
}
function sub(uint x, uint y) internal pure returns (uint z) {
require((z = x - y) <= x, 'ds-math-sub-underflow');
}
function mul(uint x, uint y) internal pure returns (uint z) {
require(y == 0 || (z = x * y) / y == x, 'ds-math-mul-overflow');
}
Hand add the numbers 5 and 9 and you get 14. Hand it two whose sum does not fit and you get a revert. Which contract it was compiled into, who is calling, what the chain looks like today: none of it enters the calculation. That is what view and pure promise, and why maths helpers are the easiest place to start in an unfamiliar codebase.
SafeERC20 is the counterexample, because none of its functions are pure: it calls out to a token. So library does not automatically mean pure. It means there is no state of the library's own, and when a library also makes no external calls, pure is what falls out.
How do you read a five hundred line library?
You do not read it. You list the declarations, read the names, and open only the function you came for. A library has no shared state and no setup order, so nothing accumulates as you go. Each function stands by itself, which means you can take one and leave the rest.
Strings is five hundred and thirty two lines. Here is the procedure, and it takes about a minute:
- List the declarations. Grep the file for lines beginning with
function, or fold everything in your editor. You now have a table of contents nobody wrote. - Read the names, not the bodies. In
Stringsthey arrive in families:toString,toStringSigned,toHexString, thenparseUint,tryParseUint,parseInt,parseAddress. Learning the convention of one family tells you what the rest of that family does. - Open exactly one function, the one whose name matches what you came for. Because it is
pure, everything it depends on is in its own parameter list.
Compare that with an inherited function, where a three line body can send you through three files before you learn what it does. Inheritance reading is a chase, described step by step in Solidity inheritance. Library reading is a lookup. The difference is state: a parent brings its storage with it, and a library brings nothing.
Why does SafeERC20 exist at all?
Because ERC-20 was widely adopted before its return values settled. Some tokens return a boolean from transfer, and some return nothing whatsoever, and a call that returns nothing is not the same as a call that failed. SafeERC20 handles both shapes behind a single function name.
The library states the problem in its own header:
* @title SafeERC20
* @dev Wrappers around ERC-20 operations that throw on failure (when the token
* contract returns false). Tokens that return no value (and instead revert or
* throw on failure) are also supported, non-reverting calls are assumed to be
* successful.
That is a compatibility statement. A contract that wants to work with every token in circulation has to cope with both conventions, and writing that logic once beats writing it at every call site.
The shape it settles on is worth recognising. Every operation comes in two forms. One reverts with the library's own named error:
error SafeERC20FailedOperation(address token);
And one hands the outcome back as a boolean and lets the caller decide:
function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {
return _safeTransfer(token, to, value, false);
}
Same work, two answers. When a library gives you doX and tryDoX side by side, the author expected two kinds of caller: one that wants the transaction to stop, and one that wants to carry on and handle the result itself.
Delete the line and ask what breaks
- Delete
using SafeERC20 for IERC20;and everytoken.safeTransfer(...)in the file stops compiling. Behaviour does not change; the fix is to writeSafeERC20.safeTransfer(token, ...)at each call site. The line was notation. - Add an ordinary state variable to
Stringsand the file stops compiling.constantsurvives because it is baked into the code, but a storage slot is the one thing a library may not own. - Change
internaltoexternalon Uniswap'sMath.minand it is no longer compiled into the pair contract. It becomes a separate deployment reached bydelegatecall, and one that has to be linked at deploy time. - Delete
purefromSafeMath.addand it still compiles and returns the same number. What you lose is the compiler's promise that the answer depends on nothing butxandy, which was the reason three lines were enough to trust it.
Related questions
What is the difference between a library and a contract in Solidity? A library declares functions and constants only. It has no storage, no constructor, no inheritance and cannot hold ether, so there is nothing for it to remember and nothing for it to own. A contract can do all of those things.
Can a library have state variables?
No. A library may declare constant values, which are compiled into the code rather than stored, but it cannot declare a variable that occupies a storage slot. This is the restriction from which every other property of a library follows.
What does using for do in Solidity?
It lets a library's functions be written as methods on a type. using L for T; means a value v of type T can call v.f(args), which the compiler turns into L.f(v, args). It affects notation only.
Do you have to deploy a library?
Only if it has public or external functions. A library whose functions are all internal is compiled directly into each contract that uses it, so there is nothing separate to deploy and nothing to link.
Can a library inherit from another contract?
No. A library cannot inherit, and nothing can inherit from a library. It can still call other libraries, which is how Strings uses Bytes, Math and SafeCast without taking on any of their state, since none of them have any.
Why do contracts use safeTransfer instead of transfer?
Compatibility. ERC-20 is implemented inconsistently: some tokens return a boolean and some return nothing, so a direct transfer call has to handle two different return shapes. SafeERC20 writes that handling once so the calling contract does not repeat it.
Where to go next
A library is defined by what it cannot do, and everything useful follows from that. No storage means no context, no context means pure, and pure means the function in front of you is the whole story. That is why a five hundred line library reads more easily than a forty line contract three inheritance levels deep.
Libraries are one of three ways Solidity lets contracts share code without duplicating it. The other two are covered in interface vs abstract contract, and the wider method all of this sits inside is how to read a smart contract.
Tagged