Solidity Enums (Syntax Examples)

whiteboard crypto logo
Published by:
Whiteboard Crypto
on

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.Inactive1 is TxState.Pending2 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.

whiteboard crypto logo

WhiteboardCrypto is the #1 online resource for crypto education that explains topics of the cryptocurrency world using analogies, stories, and examples so that anyone can easily understand them. Growing to over 870,000 Youtube subscribers, the content has been shared around the world, played in public conferences and universities, and even in Congress.