Storage optimization in Solidity smart contracts is an essential consideration given the relatively high costs associated with writing data to the blockchain. Every write operation has a gas fee, and more extensive storage usage equates to increased deployment and execution costs. The peaq network provides EVM compatibility, so the same best practices that apply to Ethereum also apply here—but within peaq’s unique economy and infrastructure.

Key topics covered in this guide include:

  • How the EVM (on peaq) stores data in 256-bit storage slots.
  • Why mappings can be more efficient than arrays in certain scenarios.
  • Additional strategies like data packing, using bytes effectively, and using events instead of on-chain storage.
  • Practical coding examples in Solidity.

Prerequisites

  • Basic Solidity Knowledge
  • An understanding of how the EVM handles storage, memory, and call data, as well as the associated gas costs for these operations.
  • Familiar that peaq network is EVM-compatible and that the best practices for storage optimization hold true in its environment, with potential additional benefits from the underlying Substrate-based infrastructure.

Optimizations

Below are some high-level strategies and examples to help you optimize your smart contract storage usage on the peaq network’s EVM.

Use Mappings Instead of Arrays (When Feasible)

Arrays (especially dynamic arrays) often require more operations to manage (e.g., iteration, boundary checks) and can grow unbounded if not carefully restricted. Mappings store data more sparsely, saving storage when you do not need sequential elements.

// Example: Using a mapping instead of a dynamic array

// Less optimal: using a dynamic array
contract TestArray {
    uint256[] public items;
    
    function addItem(uint256 value) external {
        items.push(value);
    }
    
    // Accessing an element
    function getItem(uint256 index) external view returns (uint256) {
        return items[index];
    }
}

// More optimal: using a mapping
contract TestMapping {
    mapping(uint256 => uint256) public items;
    uint256 public itemCount;
    
    function addItem(uint256 value) external {
        items[itemCount] = value;
        itemCount++;
    }
    
    // Accessing an element
    function getItem(uint256 index) external view returns (uint256) {
        return items[index];
    }
}

Explanation:

  • In the above example, TestMapping is more flexible with sparse data and can save gas.
  • You can maintain a separate counter (itemCount) to track indices, mimicking array-like behavior.

Use the Right Data Types and Pack Them

The EVM stores data in 256-bit slots. When two or more variables fit into a single 256-bit slot, they can be packed together to reduce overall storage usage.

contract DataPacking {
    // All of these will fit into a single 256-bit slot if ordered properly
    uint128 public valueA; // 128 bits
    uint64  public valueB; // 64 bits
    uint64  public valueC; // 64 bits
    
    // Another slot
    address public owner;   // 160 bits
    bool    public isActive; // 8 bits
    uint88  public counter;  // 88 bits
}

Best Practice:

  • Group smaller types together so they can occupy the same slot.
  • Reordering the variables to minimize wasted space in each 256-bit slot can substantially reduce gas costs.

Store Data Off-Chain or Use Events When Appropriate

If you only need data for historical or informational purposes (i.e., you don’t need it to stay in contract storage for on-chain logic), consider storing it off-chain or emitting it in events. Events are cheaper than storing data on-chain and still let you retrieve the data from transaction logs.

contract UseEvents {
    // Instead of storing all these logs on-chain...
    // mapping(uint256 => string) public logs;  // This can grow unbounded
    
    // Emit an event to record data
    event LogRecorded(uint256 indexed id, string info);
    
    function recordLog(uint256 _id, string memory _info) external {
        emit LogRecorded(_id, _info);
    }
}

Note:

  • Data in events is not accessible to contracts directly (only via off-chain indexing), so only move data to events if your contract does not need to rely on it for future state changes.
  • Checkout our indexing solutions to see how to query these events.

Consider Using bytes Arrays for Packed Storage

In some scenarios (especially with strings or variable-length data), storing in a single bytes array can be more efficient than storing an array of fixed-size data types, as it packs data continuously without leaving empty space.

contract BytesStorage {
    bytes public data; 
    
    function appendData(bytes memory newData) external {
        // Append in memory, then store
        data = abi.encodePacked(data, newData);
    }
}

Carefully Manage Storage Reads and Writes

Each storage write operation costs gas, so optimizing writes—and reducing the number of expensive SSTORE operations—can lower costs.

Strategies:

  • Minimize writes by caching frequently updated values in memory and writing only once when necessary.
  • Use local variables (memory) rather than reading from storage multiple times in a function. Each storage read is more expensive than a memory read.
contract StorageWrites {
    uint256 public counter;
    
    function incrementCounter(uint256 times) external {
        uint256 temp = counter; // Only one storage read
        for (uint256 i = 0; i < times; i++) {
            temp++;  // Increment in memory
        }
        counter = temp; // Single storage write
    }
}

Reset Storage Slots to Zero When Possible

The EVM provides a gas refund for clearing storage slots (i.e., writing zero to a previously non-zero slot). Although refunds are capped, this can still result in net savings when performing large transactions.

contract StorageCleanup {
    mapping(address => uint256) public balances;
    
    function withdrawAll() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
        
        // Clear the slot
        balances[msg.sender] = 0;
        
        // Transfer or other business logic
        // ...
    }
}

Use Structs Wisely

Grouping related variables into structs can help you manage your data more systematically. However, remember that each struct field occupies storage in 256-bit slots, and you can still leverage the same packing principles within a struct.

struct UserInfo {
    uint8 age;
    uint64 score;
    bool isActive;
    // Note: reordering could pack these into fewer slots
}

contract ManageUsers {
    mapping(address => UserInfo) public users;
    
    function setUserInfo(uint8 _age, uint64 _score, bool _active) external {
        // Single store operation if we read the struct first into memory, modify it, then write once
        UserInfo memory tempUser = users[msg.sender];
        tempUser.age = _age;
        tempUser.score = _score;
        tempUser.isActive = _active;
        
        users[msg.sender] = tempUser;
    }
}

Summary

Optimizing storage in Solidity smart contracts not only reduces gas costs for your end-users but also contributes to the overall efficiency of the peaq network. By adopting these best practices—using mappings, packing data, leveraging events instead of persistent storage, and resetting unused slots—you can ensure your dApps remain economical and performant.

Key Takeaways:

  • Mappings often outperform arrays, especially for sparse data.
  • Data Packing aligns smaller data types to minimize wasted storage space.
  • Events are a cheaper alternative to on-chain storage if you only need data for off-chain retrieval.
  • Minimizing Writes to contract storage lowers gas costs significantly.
  • Storage Slot Cleanup can yield gas refunds and improve contract efficiency.

As you develop on the peaq network’s EVM, keep these strategies in mind to create secure, cost-effective, and efficient smart contracts. By following these guidelines, you will optimize your contracts’ storage usage, reducing costs and enhancing the user experience for your dApps.