Hello! Welcome to the seventh lesson in our "Solidity Foundations" module.
In our last session, we explored how to define custom data types with structs and enums. We focused heavily on how their layout in storage, particularly the packing of struct members, directly impacts gas consumption. This principle of aligning our code with the EVM's low-level behavior is a recurring theme you've expressed interest in.
Introduction
Today, we'll extend this thinking to collection types. The learning outcome for this lesson is to declare and manipulate arrays and mappings.
These are Solidity's workhorses for managing groups of data. In DeFi, you'll constantly use them to track lists of users, positions, or token balances. For example, a mapping(address => uint) is the canonical way to represent token balances in an ERC-20 contract.
We will cover the syntax and common operations, but true to your goal of digging deeper, we will spend most of our time on how arrays and mappings are represented in EVM storage. Understanding their underlying mechanics is not just academic; it directly explains their performance characteristics, gas costs, and limitations, such as why you can't iterate over a mapping.
Part 1: Arrays in Solidity
Arrays are collections of elements of the same type, stored in a contiguous sequence. Solidity has two kinds of arrays:
- Fixed-Size Arrays: The size is known at compile-time.
// An array that will always hold 5 unsigned integers. uint[5] public myFixedArray; - Dynamic-Size Arrays: The size can change during runtime.
// An array of unsigned integers that can grow or shrink. uint[] public myDynamicArray;
You can learn more about the basic syntax and available members from the official documentation.
Types — Solidity 0.8.34-develop documentation
Please read this section from the Solidity documentation to get a handle on the basic syntax and properties of arrays.
Read the main subsection titled 'Arrays'. Focus on: The declaration syntax for fixed and dynamic arrays. The member functions for dynamic storage arrays: .length, .push(value), and .pop(). The section 'Allocating Memory Arrays', which explains that memory arrays are created with a fixed size using new and cannot be resized with .push() or .pop().
Arrays in Memory vs. Storage
A key distinction to grasp is the behavior of arrays in different data locations:
- Storage Arrays: A dynamic array declared as a state variable (i.e., in
storage) can be resized. The.push(value)and.pop()methods are available and modify the array's length. - Memory Arrays: Dynamic-length arrays can be created in
memory, but their size is fixed once allocated with thenewkeyword. You cannot use.push()or.pop()to change their size. You must calculate the required size beforehand.
This difference in behavior is a direct result of how the EVM handles storage versus memory. Let's dive into the storage layout.
Low-Level Deep Dive: How Dynamic Arrays Are Stored
The way Solidity stores dynamic arrays in storage is not intuitive but is designed to be flexible and avoid data collisions.
To understand this, we'll watch a detailed visual explanation.
Master Solidity: Dynamic Arrays and Memory Layout
This video by Jesper Kristensen provides an excellent visualization of the storage layout for dynamic arrays. It connects the high-level Solidity code to what's actually happening in the EVM's storage slots.
Watch the first part of the video, from the beginning to 08:44. Focus on these key points: The contract's storage slot for the array (let's call it p) stores the array's length. The actual elements are not stored sequentially in slots p+1, p+2, etc. Their location starts at a position determined by the hash of the array's slot: keccak256(p). Subsequent elements (element[1], element[2], etc.) are stored contiguously from this starting hash location.
To summarize the core concept:
- A dynamic array
myArrayis declared, and the compiler assigns it to storage slotp. - The value at
storage[p]is the number of elements in the array (its length). - The value of
myArray[0]is stored atstorage[keccak256(p)]. - The value of
myArray[1]is stored atstorage[keccak256(p) + 1], and so on.
This design ensures that no matter how large the array grows, it won't overwrite other state variables that might be declared after it in the contract. This hashing mechanism is a fundamental pattern for dynamic data structures in the EVM.
Part 2: Mappings in Solidity
Mappings are hash tables that store key-value pairs. They are incredibly efficient for lookups and are a cornerstone of many smart contract patterns, such as tracking token balances.
The syntax is mapping(KeyType => ValueType).
// A mapping from an address to a uint, perfect for balances.
mapping(address => uint) public balances;
// A mapping to a struct we defined in the last lesson.
mapping(uint => Position) public userPositions;
Some key properties of mappings:
- They can only be declared as state variables (in
storage). You cannot have a mapping inmemoryorcalldata. - All possible keys exist from the start and are initialized to a default value (e.g., 0 for
uint). You don't "add" keys; you just assign a value to them. - The keys themselves are not stored. This means mappings are not iterable; you cannot loop through the keys or values.
Low-Level Deep Dive: How Mappings Are Stored
The non-iterable nature of mappings is a direct consequence of their storage layout, which, like dynamic arrays, relies on hashing.
Let's watch the companion video that explains this mechanism.
Mastering Solidity Storage: Essential Mapping Secrets You Need to Know!
The same creator breaks down the storage layout for mappings, revealing why they are so efficient for lookups but impossible to iterate.
Watch from 10:55 to 22:01. Pay close attention to: How the storage slot where the mapping is declared (p) is left empty (it holds 0). How the storage location for a value is calculated by hashing the key and the mapping's slot number: keccak256(h(k) . p), where . is concatenation. Why this hashing scatters the data across the entire 2^256 slot address space.
The Solidity documentation confirms this mechanism.
Layout of State Variables in Storage and Transient Storage
This section of the docs provides the formal rules for how mappings and dynamic arrays are laid out in storage, reinforcing what you saw in the videos.
Read the section 'Mappings and Dynamic Arrays'. It provides the formulas for calculating storage locations for both data types.
The key formula for a mapping at slot p and a key k is:storage_location = keccak256(abi.encodePacked(k, p))
The value is then stored at that location: storage[storage_location].
Because each key is hashed to find its storage location, the values are spread out pseudo-randomly across the vast storage space. There's no list of keys to iterate over, which is why this is not possible in Solidity. To enable iteration, developers typically use a secondary data structure, like a dynamic array of keys, alongside the mapping.
Practical Implications: Gas Costs
Understanding these storage models isn't just theoretical. It has direct implications for gas usage. When should you use a mapping versus an array?
- Arrays are for ordered collections. If you need to iterate through all the elements, an array is your only choice. However, adding elements can be costly, especially if you need to maintain uniqueness or a specific order.
- Mappings are for direct lookups. If you need to fetch a value based on a known key, a mapping is almost always more gas-efficient than searching through an array.
Consider the simple act of adding a user to a list.

This dramatic difference highlights why choosing the right data structure is a critical gas optimization skill. For your goal of building DeFi protocols, where you often need to look up a user's balance or position by their address, mappings are the default and most efficient choice.
Conclusion
You have now learned to declare and manipulate Solidity's two primary collection types, and more importantly, you understand the low-level EVM mechanics that govern their behavior.
Key Takeaways:
- Arrays are used for ordered, iterable lists of data. Dynamic arrays can be resized in
storage(.push/.pop) but have a fixed size once created inmemory. - Mappings are used for efficient key-value lookups and are not iterable. They can only exist in
storage. - Storage Layout is Hashed: Both dynamic arrays and mappings use
keccak256to determine where their data is stored, preventing collisions with other state variables.- Dynamic Array: Length is at slot
p; data starts atkeccak256(p). - Mapping: Value for key
kis atkeccak256(abi.encodePacked(k, p)).
- Dynamic Array: Length is at slot
- This underlying design directly influences gas costs, making mappings significantly cheaper for direct lookups and appends than arrays.
In our next lesson, we will focus on handling errors using require, revert, and assert. You'll see how these constructs are used to validate inputs and state, for instance, to prevent a user from accessing an array element that is out of bounds, which would otherwise cause the entire transaction to fail.