Utilities - OpenZeppelin Docs (original) (raw)

Packing

The storage in the EVM is shaped in chunks of 32 bytes, each of this chunks is known as a slot, and can hold multiple values together as long as these values don’t exceed its size. These properties of the storage allow for a technique known as packing, that consists of placing values together on a single storage slot to reduce the costs associated to reading and writing to multiple slots instead of just one.

Commonly, developers pack values using structs that place values together so they fit better in storage. However, this approach requires to load such struct from either calldata or memory. Although sometimes necessary, it may be useful to pack values in a single slot and treat it as a packed value without involving calldata or memory.

The Packing library is a set of utilities for packing values that fit in 32 bytes. The library includes 3 main functionalities:

With these primitives, one can build custom functions to create custom packed types. For example, suppose you need to pack an address of 20 bytes with a bytes4 selector and an uint64 time period:

function _pack(address account, bytes4 selector, uint64 period) external pure returns (bytes32) {
    bytes12 subpack = Packing.pack_4_8(selector, bytes8(period));
    return Packing.pack_20_12(bytes20(account), subpack);
}

function _unpack(bytes32 pack) external pure returns (address, bytes4, uint64) {
    return (
        address(Packing.extract_32_20(pack, 0)),
        Packing.extract_32_4(pack, 20),
        uint64(Packing.extract_32_8(pack, 24))
    );
}

Storage Slots

Solidity allocates a storage pointer for each variable declared in a contract. However, there are cases when it’s required to access storage pointers that can’t be derived by using regular Solidity. For those cases, the StorageSlot library allows for manipulating storage slots directly.

bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

function _getImplementation() internal view returns (address) {
    return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;
}

function _setImplementation(address newImplementation) internal {
    require(newImplementation.code.length > 0);
    StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
}

The TransientSlot library supports transient storage through user defined value types (UDVTs), which enables the same value types as in Solidity.

bytes32 internal constant _LOCK_SLOT = 0xf4678858b2b588224636b8522b729e7722d32fc491da849ed75b3fdf3c84f542;

function _getTransientLock() internal view returns (bool) {
    return _LOCK_SLOT.asBoolean().tload();
}

function _setTransientLock(bool lock) internal {
    _LOCK_SLOT.asBoolean().tstore(lock);
}

| | Manipulating storage slots directly is an advanced practice. Developers MUST make sure that the storage pointer is not colliding with other variables. | | --------------------------------------------------------------------------------------------------------------------------------------------------------- |

One of the most common use cases for writing directly to storage slots is ERC-7201 for namespaced storage, which is guaranteed to not collide with other storage slots derived by Solidity.

Users can leverage this standard using the SlotDerivation library.

using SlotDerivation for bytes32;
string private constant _NAMESPACE = "<namespace>" // eg. example.main

function erc7201Pointer() internal view returns (bytes32) {
    return _NAMESPACE.erc7201Slot();
}

Base64

Base64 util allows you to transform bytes32 data into its Base64 string representation.

This is especially useful for building URL-safe tokenURIs for both ERC-721 or ERC-1155. This library provides a clever way to serve URL-safe Data URI compliant strings to serve on-chain data structures.

Here is an example to send JSON Metadata through a Base64 Data URI using an ERC-721:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {Base64} from "@openzeppelin/contracts/utils/Base64.sol";

contract Base64NFT is ERC721 {
    using Strings for uint256;

    constructor() ERC721("Base64NFT", "MTK") {}

    // ...

    function tokenURI(uint256 tokenId) public pure override returns (string memory) {
        // Equivalent to:
        // {
        //   "name": "Base64NFT #1",
        //   // Replace with extra ERC-721 Metadata properties
        // }
        // prettier-ignore
        string memory dataURI = string.concat("{\"name\": \"Base64NFT #", tokenId.toString(), "\"}");

        return string.concat("data:application/json;base64,", Base64.encode(bytes(dataURI)));
    }
}

Multicall

The Multicall abstract contract comes with a multicall function that bundles together multiple calls in a single external call. With it, external accounts may perform atomic operations comprising several function calls. This is not only useful for EOAs to make multiple calls in a single transaction, it’s also a way to revert a previous call if a later one fails.

Consider this dummy contract:

// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract Box is Multicall {
    function foo() public {
        // ...
    }

    function bar() public {
        // ...
    }
}

This is how to call the multicall function using Ethers.js, allowing foo and bar to be called in a single transaction:

// scripts/foobar.js

const instance = await ethers.deployContract("Box");

await instance.multicall([
    instance.interface.encodeFunctionData("foo"),
    instance.interface.encodeFunctionData("bar")
]);