What a Solidity library is

A Solidity library is a special contract-like unit that cannot hold persistent state in the usual way and is meant to be reused by other contracts. You can think of it as a named collection of functions with strong compiler support for safe reuse.

Libraries are useful in two broad scenarios:

  1. Pure utility logic
  2. Functions that operate only on inputs and return outputs, such as math helpers, encoding helpers, or validation routines.

  1. Structured storage helpers
  2. Functions that operate on a struct stored in contract storage, often used to encapsulate complex state transitions while keeping the main contract small.

A key benefit is that library functions can be called internally, which often results in inlined code and lower runtime overhead. In some cases, libraries can also be deployed separately and linked, but that is more relevant for larger codebases and older compilation patterns.


When to use a library instead of a contract

A library is a good fit when you need reusable logic but do not want the overhead of inheritance or the coupling of a shared base contract.

Use a library when:

  • The code is stateless or nearly stateless.
  • Multiple contracts need the same helper functions.
  • You want to keep business logic separate from storage-heavy contract code.
  • You need functions that operate on a storage struct in a controlled way.

Avoid a library when:

  • The logic needs its own lifecycle, permissions, or external interface.
  • You want polymorphism or override behavior.
  • The code depends heavily on contract-specific state and configuration.

A practical rule: if the code is “mechanical” and reusable, a library is often the right abstraction. If it is “behavioral” and contract-specific, a contract or abstract contract may be better.


Library function types

Solidity libraries commonly expose three kinds of functions:

Function typeTypical useCan read state?Can write state?
pureDeterministic helpersNoNo
viewRead-only helpersYesNo
storage-reference functionsMutate a passed-in storage structIndirectlyYes

The third category is the most distinctive. A library can define functions that accept a storage pointer, allowing the library to update a struct stored in the calling contract.


A practical example: reusable token balance accounting

Suppose you are building a protocol with several contracts that need the same balance bookkeeping logic. Instead of repeating the same checks and updates, you can isolate the logic in a library.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

library BalanceLib {
    struct BalanceData {
        mapping(address => uint256) balances;
        uint256 totalDeposits;
    }

    error InsufficientBalance();
    error ZeroAmount();

    function deposit(BalanceData storage self, address account, uint256 amount) internal {
        if (amount == 0) revert ZeroAmount();
        self.balances[account] += amount;
        self.totalDeposits += amount;
    }

    function withdraw(BalanceData storage self, address account, uint256 amount) internal {
        if (amount == 0) revert ZeroAmount();

        uint256 current = self.balances[account];
        if (current < amount) revert InsufficientBalance();

        unchecked {
            self.balances[account] = current - amount;
            self.totalDeposits -= amount;
        }
    }

    function balanceOf(BalanceData storage self, address account) internal view returns (uint256) {
        return self.balances[account];
    }
}

contract Vault {
    using BalanceLib for BalanceLib.BalanceData;

    BalanceLib.BalanceData private balances;

    function deposit() external payable {
        balances.deposit(msg.sender, msg.value);
    }

    function withdraw(uint256 amount) external {
        balances.withdraw(msg.sender, amount);
        payable(msg.sender).transfer(amount);
    }

    function balanceOf(address account) external view returns (uint256) {
        return balances.balanceOf(account);
    }
}

This pattern keeps the contract focused on external behavior while the library owns the accounting rules.


Why storage-reference libraries are powerful

The most useful library pattern in Solidity is a function that takes MyStruct storage self. This lets you encapsulate state transitions without exposing the underlying layout everywhere.

Advantages

  • Encapsulation: The main contract does not need to manage every field directly.
  • Reuse: Multiple contracts can share the same state logic.
  • Auditability: Related operations live together.
  • Reduced duplication: Fewer chances to introduce inconsistent updates.

Important constraint

The library does not own the storage. The calling contract does. That means the library must be designed carefully to avoid assumptions about layout, initialization, or access control.

A library should generally not decide who may call a function. That responsibility belongs to the contract. The library should decide how the state changes once the call is authorized.


Using using for to improve readability

The using for directive attaches library functions to a type, making calls feel like method invocations.

using BalanceLib for BalanceLib.BalanceData;

After that, you can write:

balances.deposit(msg.sender, msg.value);

instead of:

BalanceLib.deposit(balances, msg.sender, msg.value);

This improves readability, especially when a library is tightly associated with a specific struct or value type.

Best practice

Use using for when the library functions are conceptually part of the type. Avoid it if the association is weak or if it makes the code harder to follow for new contributors.


Pure utility libraries for domain logic

Not all libraries need storage references. Many of the best libraries are pure helpers that encode domain rules.

Examples include:

  • fee calculation
  • fixed-step rounding
  • address normalization
  • deadline validation
  • basis-point conversions
  • array search helpers

A simple example:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

library FeeMath {
    uint256 internal constant BPS_DENOMINATOR = 10_000;

    function applyBps(uint256 amount, uint256 bps) internal pure returns (uint256) {
        require(bps <= BPS_DENOMINATOR, "invalid bps");
        return (amount * bps) / BPS_DENOMINATOR;
    }

    function subtractFee(uint256 amount, uint256 bps) internal pure returns (uint256) {
        uint256 fee = applyBps(amount, bps);
        return amount - fee;
    }
}

This kind of library is ideal when the logic is reused across vaults, marketplaces, or staking contracts.


Library design best practices

1. Keep libraries focused

A library should solve one narrow problem well. Avoid turning it into a dumping ground for unrelated helpers. For example, a FeeMath library should not also contain address utilities or access-control helpers.

2. Prefer explicit inputs and outputs

Libraries are easiest to reason about when their dependencies are visible in the function signature. Avoid hidden assumptions about global state or external calls.

3. Use custom errors or clear revert messages

A library should fail predictably. If it validates inputs, make the failure mode obvious and consistent.

4. Document storage assumptions

If a library works with a struct, document what each field means and whether any invariants must hold before calling the function.

5. Keep external calls out of libraries when possible

Libraries are best for deterministic logic and state updates. External calls introduce reentrancy, dependency, and failure complexity that is usually better handled in the contract layer.


Library vs inheritance vs internal helper contract

Choosing the right reuse mechanism matters. The comparison below can help.

PatternBest forProsTrade-offs
LibraryStateless helpers, shared storage logicReusable, modular, often gas-efficientLess flexible than inheritance
InheritanceShared behavior with override supportNatural for polymorphic designCan create tight coupling and complex linearization
Internal helper functionsSmall local reuse inside one contractSimple, no extra abstractionNot reusable across contracts

If you need shared logic across many contracts, a library is usually cleaner than a base contract. If you need override hooks or shared access control behavior, inheritance may be more appropriate.


Common pitfalls

Assuming a library can manage its own state

A library cannot behave like a standalone stateful service. If you need persistent independent state, use a contract.

Mixing access control into utility code

Do not place permission checks inside a generic math or encoding library. That makes the library less reusable and harder to test.

Overusing linked libraries

External library linking can complicate deployment and verification. Prefer internal library functions unless you have a strong reason to link separately.

Writing overly broad libraries

A large “utils” library often becomes hard to maintain. Split by domain: FeeMath, OrderValidation, PositionAccounting, and so on.

Forgetting upgrade implications

If a library manipulates a storage struct, changing the struct layout later can break assumptions in multiple contracts. Treat the struct definition as part of the library’s public contract.


Testing libraries effectively

Libraries should be tested as thoroughly as contracts because they often contain the core business rules.

Recommended testing approach

  • Test pure functions with normal, edge, and invalid inputs.
  • Test storage-reference functions through a small harness contract.
  • Verify invariants after each state transition.
  • Include fuzz tests for arithmetic and boundary conditions.

A harness contract is especially useful for storage libraries because it lets you exercise the library in a realistic storage context without cluttering production code.


Practical guidelines for production projects

If you are introducing libraries into a real codebase, follow these rules:

  1. Start with repeated logic.
  2. Extract only code that appears in multiple places or is clearly domain-specific.

  1. Keep contracts responsible for authorization.
  2. Libraries should transform state, not decide policy.

  1. Use structs to group related state.
  2. This makes storage libraries easier to understand and safer to extend.

  1. Prefer internal functions unless linking is necessary.
  2. Internal libraries are simpler to deploy and verify.

  1. Document invariants and expected call order.
  2. This is especially important for accounting, order books, and position management.


Conclusion

Solidity libraries are a practical tool for building cleaner, more maintainable smart contracts. They shine when you need reusable logic, especially for deterministic helpers and structured state manipulation. The best libraries are small, focused, and explicit about their assumptions.

Used thoughtfully, libraries reduce duplication, improve auditability, and help you separate business rules from contract plumbing. That separation is a major advantage in larger Solidity systems where correctness and clarity matter as much as gas efficiency.

Learn more with useful resources