Hello! Welcome to the sixth lesson in our "Solidity Foundations" module.
In our last session, we took a deep dive into the EVM's data architecture, distinguishing between the persistent storage, volatile memory, and read-only calldata. We established that storage is a vast key-value store of 32-byte slots and that understanding its layout is paramount for optimization.
Introduction
Today, we'll build directly on that foundation. The learning outcome for this lesson is to define and use structs and enums to model domain data.
These are user-defined types that allow you to create more complex and readable data structures than the built-in value types. For your goal of building DeFi protocols, structs are indispensable for modeling concepts like loans, user positions, or trade orders. Enums are perfect for managing the state of these complex objects.
Crucially, we won't just learn the syntax. We'll leverage our knowledge of storage to see how these types are laid out at a low level, enabling you to design them for maximum gas efficiency from the start—a skill that directly aligns with your interest in EVM internals.
Let's begin with structs.
What are Structs?
A struct is a composite data type that groups several variables under a single name. Think of it as creating a custom "blueprint" for a concept in your application. For instance, instead of tracking a user's address, balance, and reputation as separate variables, you can group them into a User struct.

To get familiar with the basic syntax of defining, creating, and interacting with structs, the following video provides a clear and concise walkthrough.
This tutorial by EatTheBlocks will walk you through the essential syntax for working with structs in Solidity.
Please watch from the beginning until 06:10. This covers: Declaring a struct (01:17 - 02:46) Creating instances of a struct (02:46 - 05:17) Reading and updating struct fields (05:17 - 06:10) Focus on the two different ways to instantiate a struct: with positional arguments and with named arguments.
As you saw, you can create a new struct instance like this:
// Let's define a struct relevant to DeFi
struct Position {
address owner;
uint collateralAmount;
uint debtAmount;
}
// Instantiate using positional arguments (order matters)
Position memory myPosition = Position(msg.sender, 1000, 500);
// Instantiate using named arguments (order doesn't matter, more readable)
Position memory anotherPosition = Position({
owner: msg.sender,
debtAmount: 200,
collateralAmount: 400
});
// Access and update fields
anotherPosition.debtAmount = 250;
This seems straightforward, but the real power—and potential for optimization—lies in how these structs are stored.
Structs and Storage Layout: The Gas Optimization Angle
In the last lesson, we discussed how the EVM packs multiple small variables into a single 32-byte storage slot to save gas. This principle applies directly to the members of a struct. The order in which you declare fields in a struct can have a significant impact on how many storage slots it occupies, and therefore, how much gas it costs to interact with.
The following resources provide excellent examples of this concept in action.
The RareSkills Book of Solidity Gas Optimization: 80+ Tips
First, let's read a short section from 'The RareSkills Book of Solidity Gas Optimization'. It clearly illustrates the difference between a packed and an unpacked struct.
Read section 4, 'Pack structs'. It presents two contracts, Unpacked_Struct and Packed_Struct, and explains why one is more gas-efficient than the other.
Ethereum Virtual Machine Internals – Part 2
Now, let's reinforce this with a similar example from the NetSPI article we reviewed in the last lesson. It quantifies the gas savings, which is particularly insightful.
Read the section titled 'Structs'. Pay close attention to the S1 (expensive) and S2 (cheaper) struct definitions and the resulting gas costs for the modify() vs. modify2() functions.
Let's synthesize what you've just read. Consider these two structs:
// Unpacked Struct (Less Efficient)
struct Unpacked {
uint128 a; // Slot 0 (16 bytes)
uint256 b; // Slot 1 (32 bytes) - Can't fit in slot 0
uint128 c; // Slot 2 (16 bytes) - Starts a new slot
}
// Total storage slots used: 3
// Packed Struct (More Efficient)
struct Packed {
uint128 a; // Slot 0 (16 bytes)
uint128 c; // Slot 0 (16 bytes) - Fits alongside 'a'
uint256 b; // Slot 1 (32 bytes) - Starts a new slot
}
// Total storage slots used: 2
By simply reordering the fields in Packed, we allow the compiler to place a and c into the same 32-byte storage slot. This reduces the number of expensive SSTORE operations required to write to the struct, directly lowering gas costs.
Rule of thumb: When defining a struct, group smaller data types together where possible so they can be packed into single 32-byte slots.
Enums for Managing States
An enum (short for enumeration) is a way to create a user-defined type that is restricted to a set of predefined constant values. They are incredibly useful for representing states, categories, or choices in your contract, making your code more readable and less error-prone than using raw integers.
For a DeFi lending protocol, you might define the status of a loan like this:
enum LoanStatus {
Pending, // 0
Active, // 1
Repaid, // 2
Liquidated // 3
}
// Then you can use it in a struct
struct Loan {
uint amount;
address borrower;
LoanStatus status;
}
Loan myLoan;
function activateLoan() internal {
myLoan.status = LoanStatus.Active;
}
function checkStatus() external view returns (LoanStatus) {
return myLoan.status;
}
Behind the scenes, enum members are represented by integers, starting from 0. LoanStatus.Pending is 0, LoanStatus.Active is 1, and so on. Because they are represented by small integer types (usually uint8), they are very gas-efficient and can be tightly packed in storage.
Let's see how they are stored at the EVM level.
Ethereum Virtual Machine Internals – Part 2
The NetSPI article also has a section dedicated to how enums are stored.
Read the short section titled 'Enums'. It explains that enums are stored as single-byte values, making them ideal for packing.
The key insight is that an enum with up to 256 members will only take up a single byte (uint8). This makes them a highly efficient tool for state management in your contracts.
Conclusion
You have now learned how to create custom, complex data types in Solidity that are essential for modeling real-world domain data. More importantly, you've connected this high-level language feature to the low-level reality of the EVM's storage model.
Key Takeaways:
- Structs group related variables into a single, logical unit. They are fundamental for representing complex objects like user accounts or financial positions.
- Enums create a type with a finite set of named constants, perfect for managing the state of an object in a readable and secure way.
- Storage Packing is Key: The order of fields within a struct matters immensely. By grouping smaller types together, you enable the compiler to pack them into fewer storage slots, leading to significant gas savings.
- Enums are stored as small integers (e.g.,
uint8), making them highly efficient and easily packable within structs or alongside other state variables.
In our next lesson, we will declare and manipulate arrays and mappings. These are Solidity's primary collection types, and they are frequently used to store multiple instances of the structs we've just learned to define, such as a mapping(address => User) or an array of Loan structs. Your understanding of struct packing will be immediately applicable as you design these larger data collections.