Std Storage

Std Storage is a library that makes manipulating storage easy.

To use Std Storage, import the following in your test contract:

import {stdStorage, StdStorage} from "forge-std/Test.sol";              

Add the following line in your test contract:

using stdStorage for StdStorage;

Then, access Std Storage via the stdstore instance.

Functions

Query functions:

  • target: Set the address of the target contract
  • sig: Set the 4-byte selector of the function to static call
  • with_key: Pass an argument to the function (can be used multiple times)
  • depth: Set the position of the value in the tuple (e.g. inside a struct)
  • enable_packed_slots: Allow the use of packed storage slots

Terminator functions:

  • find: Return the slot number
  • checked_write: Set the data to be written to the storage slot(s)
  • read_<type>: Read the value from the storage slot as <type>

Simple Example

playerToCharacter tracks info about players’ characters.

// MetaRPG.sol

struct Character {
    string name;
    uint256 level;
}

mapping (address => Character) public playerToCharacter;

Let’s say we want to set the level of our character to 120.

// MetaRPG.t.sol

stdstore
    .target(address(metaRpg))
    .sig("playerToCharacter(address)")
    .with_key(address(this))
    .depth(1)
    .checked_write(120);

Packed-Slot Example

balanceOf() returns the balance of a user in a gas-optimized ERC20 implementation. enable_packed_slots() also works with ERC7201 Namespaced Storage Slots, proxy patterns, and packed slots as shown in the example below:

// AgoraDollar.sol
contract AgoraDollar {
    /// @notice The Erc20AccountData struct
    /// @param isFrozen A boolean to indicate if the account is frozen
    /// @param balance A uint248 to store the balance of the account
    struct Erc20AccountData {
        bool isFrozen;
        uint248 balance;
    }

    /// @notice The Erc20CoreStorage struct
    /// @param accountData A mapping of address to Erc20AccountData to store account data
    /// @custom:storage-location erc7201:AgoraDollarErc1967Proxy.Erc20CoreStorage
    struct Erc20CoreStorage {
        /// @dev _account The account whose data we are accessing
        /// @dev _accountData The account data for the account
        mapping(address _account => Erc20AccountData _accountData) accountData;
    }

    /// @notice The ```ERC20_CORE_STORAGE_SLOT_``` is the storage slot for the Erc20CoreStorage struct
    /// @dev keccak256(abi.encode(uint256(keccak256("AgoraDollarErc1967Proxy.Erc20CoreStorage")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 internal constant ERC20_CORE_STORAGE_SLOT_ =
        0x455730fed596673e69db1907be2e521374ba893f1a04cc5f5dd931616cd6b700;

    /// @notice The ```getPointerToErc20CoreStorage``` function returns a pointer to the Erc20CoreStorage struct
    /// @return $ A pointer to the Erc20CoreStorage struct
    function getPointerToErc20CoreStorage() internal pure returns (Erc20CoreStorage storage $) {
        /// @solidity memory-safe-assembly
        assembly {
            $.slot := ERC20_CORE_STORAGE_SLOT_
        }
    }

    /// @notice The ```balanceOf``` function returns the token balance of a given account
    /// @param _account The account to check the balance of
    /// @return The balance of the account
    function balanceOf(address _account) external view returns (uint256) {
        return getPointerToErc20CoreStorage().accountData[_account].balance;
    }
}

Let’s say we want a function to set the balance of an address to some amount on the AUSD contract.

// TestAgoraDollar.t.sol

function seedUserWithAusd(address _to, uint248 _amount) internal {
    stdstore
        .enable_packed_slots()
        .target(address(Constants.Mainnet.AUSD_ERC20)) //0x00000000eFE302BEAA2b3e6e1b18d08D69a9012a
        .sig("balanceOf(address)")
        .with_key(_to)
        .checked_write(_amount);
}

Known issues

  • Slot(s) may not be found if the tuple contains types shorter than 32 bytes