Enums (short for “enumerables”) are found in most programming languages and stem from the need for tracking state in a program without relying on magic numbers or space-heavy strings. It is useful to think of them as “booleans”, except with more than 2 positions.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
contract enumExample {
enum dayOfTheWeek {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
}
dayOfTheWeek public currentDay;
function getDayOfTheWeek() public view returns (dayOfTheWeek) {
//tells you the current numerical position
return currentDay;
}
function changeDay(dayOfTheWeek _day) public {
//sets the dayOfTheWeek to the value you enter
currentDay = _day;
}
function tuesday() public {
//sets the dayOfTheWeek to Tuesday
currentDay = dayOfTheWeek.Tuesday;
}
}
Enums are a custom (simple) data type in Solidity that allow the developer to attach readable labels to numbers, and are commonly used for tracking state and state changes in a space-efficient manner.
They are a user-defined data type that first has to be defined at the state level, and then individually instantiated inside variables.
Defining Enums
To use an enum, we first must define it at the state (contract) level with the enum
keyword, followed by a comma-delimited set of enum states.
Let’s see an example of an enum that tracks possible states of a transaction:
pragma solidity ^0.8.23;
contract Enums {
enum TxState {
Inactive, // Default state
Pending, // First state
Active, // Second state
Cancelled // Third state
}
}
Note that in many other languages enums are defined in UPPER_SNAKE_CASE, while in Solidity it doesn’t matter which convention is used. Enums in Solidity are conventionally defined with PascalCase.
Enums can be defined with up to 256 different states, though no practical scenario would ever call for that many.
Using Enums
So far, we have only defined our new enum, but we haven’t created any instances of it yet. This is the same process as structs, where we first define the data type, and then create individual instances of it.
Declaring at the State Level
When we assign an enum value we must remember to use the Enum.State
syntax. So, if we want to create a TxState
instance with an initial value of Pending
, then we use TxState.Pending
as the value:
contract Enums {
// Definition
enum TxState {
Inactive, // Default state
Pending, // First state
Active, // Second state
Cancelled // Third state
}
// Instantiation (state level)
TxState public someTx = TxState.Pending;
TxState public someOtherTx;
}
Here we have instantiated two TxState
variables, one with an initial value and one without.
However, if you deploy and call these variables, you’ll see that we get 1
and 0
for them, respectively. This is because enums are actually uint8
variables that only have their enum labels before they are compiled to their native integer form.
That also means that the zeroth state TxState.Inactive
is the default value of a TxState
enum. This is true of all enums, where the first state listed is the default state if no value is given.
This is very important to remember if you combine enums and mappings, as all unmapped values report their default value, and can present an attack vector if their default value is used for passing security checks.
Declaring at the Local Level
Enums (on their own) are simple data types equivalent to a uint8
, so we don’t need to use memory
when using them inside functions:
// Instantiation (state level)
TxState public someTx = TxState.Pending;
TxState public someOtherTx;
// Shows how to declare a local enum variable
function someFunction() public view {
TxState tempTx = TxState.Inactive;
}
// Shows how to assign a new value to an enum state variable
function assignState(TxState newState) public {
someOtherTx = newState;
}
// Shows how to return an enum value
function getState() public view returns(TxState) {
return someOtherTx;
}
Here we have two functions, one that creates a local TxState
variable, and one that assigns a new value to the someOtherTx
state variable.
If we deploy this contract, then what do we pass as the input for assignState()
? We simply use the enum state’s integer value, so 0
is TxState.Inactive
, 1
is TxState.Pending
, 2
is TxState.Active
, and so on.
You’ll also notice if we call getState()
that we get the enum’s uint8
value instead of its TxState
label, even though we declared TxState
as the return type. This is so the compiler can ensure a valid value is being returned.
Pro-Tip to Make Enums Easier
You might think that always using SomeEnum.SomeState
will become tiresome and use up a significant amount of horizontal space in your window, and you would be correct.
Fortunately, we can declare constant
state variables that store each of the enum values, and use them in our contract instead:
contract Enums {
// Definition
enum TxState {
Inactive, // Default state
Pending, // First state
Active, // Second state
Cancelled // Third state
}
// Re-Defining Enum Values as Constants
TxState constant INACTIVE = TxState.Inactive;
TxState constant PENDING = TxState.Pending;
TxState constant ACTIVE = TxState.Active;
TxState constant CANCELLED = TxState.Cancelled;
// Now we can just use `PENDING` instead of `TxState.Pending`
TxState public someTx = PENDING;
TxState public someOtherTx;
}
Because constant
variables are replaced by their assigned values during compilation, they don’t take up any storage slots in the contract and they don’t cost any additional gas fees to read. Therefore, there are no downsides to this technique.
Enums and Control Structures
Enums can be used in conditional control structures just like any other simple data type:
TxState public someTx = TxState.Pending;
// Cycles to the next TxState each time it is called
function nextState() public {
if (someTx == TxState.Pending) {
someTx = TxState.Active;
} else if (someTx == TxState.Active) {
someTx = TxState.Cancelled;
} else if (someTx == TxState.Cancelled) {
someTx = TxState.Inactive;
} else {
someTx = TxState.Pending;
}
}
// Uses the constant values defined earlier
function nextState2() public {
if (someTx == PENDING) {
someTx = ACTIVE;
} else if (someTx == ACTIVE) {
someTx = CANCELLED;
} else if (someTx == CANCELLED) {
someTx = INACTIVE;
} else {
someTx = PENDING;
}
}
// Compact single-line version
function nextState3() public {
if (someTx == PENDING) someTx = ACTIVE;
else if (someTx == ACTIVE) someTx = CANCELLED;
else if (someTx == CANCELLED) someTx = INACTIVE;
else someTx = PENDING;
}
This example shows both the native syntax for comparing enum values as well as the constant-value replacement trick we introduced.
Enums are typically used in conditional control structures where more than two logical paths exist, but in some rare situations they can be used as part of loop conditions.
Security Considerations
Because the first value defined in an enum is its default value, an inexperienced developer can inadvertently expose a serious security vulnerability if they combine enums with mappings.
Let’s say we have a contract wallet that tracks a hierarchy of roles that can interact with the wallet, with Admin
having full access to the contract’s ETH balance:
contract ContractWallet {
enum Role {
Admin,
User,
Guest
}
mapping(address => Role) public role;
constructor() {
role[msg.sender] = Role.Admin;
}
modifier onlyAdmin(address _user) {
require(isAdmin(_user), "Unauthorized user");
_;
}
function isAdmin(address _user) public view returns(bool){
return role[_user] == Role.Admin;
}
function addAdmin(address _user) public onlyAdmin(msg.sender) {
role[_user] = Role.Admin;
}
function withdrawAllETH(address payable destination) public onlyAdmin(msg.sender) {
uint totalBalance = address(this).balance;
(bool success, ) = destination.call{value: totalBalance}("");
require(success, "ETH transfer failed");
}
}
You’ll notice if we call isAdmin()
for literally any address that it will always return true
. Therefore, anyone can call withdrawAllETH()
, and steal all funds from the contract at any time.
There’s a very simple fix to this in the enum definition:
enum Role {
Undefined, // Place a default dummy value here!
Admin,
User,
Guest
}
Now, all addresses that haven’t been added to the system will be mapped to Role.Undefined
by default.
Enums are a highly specialized data structure created to replace magic numbers with human-readable labels. While they are uncommon in real-world Solidity applications, they are very powerful in the scenarios that call for them.
The thing to keep in mind about enums is that they are basically just uint8
values with labels, and they are compiled to their underlying uint8
values. Therefore, we must use their uint8
values when passing them as function inputs, and we will receive their uint8
values when receiving them as function outputs.