Skip to main content

Overview

Libraries are similar to contracts but are designed for code reuse. They are deployed once and their code is reused by multiple contracts using DELEGATECALL, making them gas-efficient.

Creating a Library

Use the library keyword instead of contract:
PriceConverter.sol
library PriceConverter {
    function getPrice() internal view returns (uint256) {
        // Implementation
    }
    
    function getConversionRate(uint256 ethAmount) internal view returns (uint256) {
        uint256 ethPrice = getPrice();
        uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1e18;
        return ethAmountInUsd;
    }
}
Library functions are typically marked as internal or public. Internal functions are only accessible by the calling contract.

Library Characteristics

What Libraries Cannot Do

  • Cannot have state variables
  • Cannot inherit or be inherited
  • Cannot receive Ether
  • Cannot be destroyed

What Libraries Can Do

  • Define functions (including view and pure functions)
  • Define structs, enums, and events
  • Use other libraries
  • Modify state of the calling contract (via DELEGATECALL)

Using Libraries

There are two ways to use library functions:

1. Direct Library Calls

import {PriceConverter} from "./PriceConverter.sol";

contract FundMe {
    function fund() public payable {
        uint256 valueInUsd = PriceConverter.getConversionRate(msg.value);
    }
}

2. Using…For Syntax

The using...for directive attaches library functions to a type:
FundMe.sol
import {PriceConverter} from "./PriceConverter.sol";

contract FundMe {
    using PriceConverter for uint256;
    
    function fund() public payable {
        // msg.value is uint256, so we can call library functions on it
        uint256 valueInUsd = msg.value.getConversionRate();
    }
}
The using...for syntax makes code more readable by allowing you to call library functions as if they were methods on the type.

How Using…For Works

When you use using PriceConverter for uint256:
uint256 valueInUsd = PriceConverter.getConversionRate(msg.value);
Traditional library call passing msg.value as argument.

Real-World Example: PriceConverter

Here’s a complete library that interacts with Chainlink price feeds:
PriceConverter.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

import {AggregatorV3Interface} from "@chainlink/[email protected]/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";

library PriceConverter {
    function getPrice() internal view returns (uint256) {
        // Sepolia ETH/USD price feed address
        AggregatorV3Interface priceFeed = AggregatorV3Interface(
            0x694AA1769357215DE4FAC081bf1f309aDC325306
        );
        (, int256 price,,,) = priceFeed.latestRoundData();
        // Price has 8 decimals, convert to 18 decimals
        return uint256(price * 1e10);
    }

    function getConversionRate(uint256 ethAmount) internal view returns (uint256) {
        uint256 ethPrice = getPrice();
        uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1e18;
        return ethAmountInUsd;
    }

    function getVersion() internal view returns (uint256) {
        return AggregatorV3Interface(
            0x694AA1769357215DE4FAC081bf1f309aDC325306
        ).version();
    }
}

Using the Library in a Contract

FundMe.sol
import {PriceConverter} from "./PriceConverter.sol";

contract FundMe {
    using PriceConverter for uint256;
    
    uint256 public constant MINIMUM_USD = 5e18;
    
    function fund() public payable {
        // Clean syntax: call getConversionRate on msg.value
        require(
            msg.value.getConversionRate() >= MINIMUM_USD,
            "didn't send enough ETH"
        );
    }
}
Without using...for, you would need to write:
require(PriceConverter.getConversionRate(msg.value) >= MINIMUM_USD);
The using...for syntax makes it more readable.

Library Function Parameters

When using using...for, the type becomes the first parameter of library functions:
library Math {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        return a + b;
    }
}

contract Calculator {
    using Math for uint256;
    
    function calculate() public pure returns (uint256) {
        uint256 x = 5;
        // x becomes the first parameter (a), 3 is the second parameter (b)
        return x.add(3); // Returns 8
    }
}

Global Using…For (Solidity 0.8.19+)

You can attach libraries globally in newer versions:
using PriceConverter for uint256 global;

contract FundMe {
    // No need to declare 'using' again
    function fund() public payable {
        require(msg.value.getConversionRate() >= MINIMUM_USD);
    }
}

Common Standard Libraries

OpenZeppelin Libraries

import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol";

contract Example {
    using SafeMath for uint256;
    
    function safeAdd(uint256 a, uint256 b) public pure returns (uint256) {
        return a.add(b); // Reverts on overflow
    }
}
SafeMath is no longer needed in Solidity 0.8.0+ because overflow/underflow checking is built-in.

Benefits of Libraries

  1. Code Reusability - Write once, use in multiple contracts
  2. Gas Efficiency - Deployed once, reused via DELEGATECALL
  3. Clean Code - using...for makes code more readable
  4. Separation of Concerns - Keep utility functions separate from contract logic
  5. Upgradability - Can deploy new library versions without changing contract code

Library vs Contract

FeatureLibraryContract
State VariablesNoYes
Receive EtherNoYes
InheritanceNoYes
DeployedOnce, reusedEach instance
Use CaseUtility functionsBusiness logic

Key Takeaways

  • Libraries provide reusable utility functions without state
  • Use library keyword instead of contract
  • using...for attaches library functions to types for cleaner syntax
  • Library functions become available as methods on the specified type
  • Libraries are gas-efficient because they’re deployed once and reused
  • Common use cases: math operations, type conversions, data validation

Build docs developers (and LLMs) love