Seven Simple Solidity Blocks to Build Your dApp

4.7/5 - (4 votes)
Solidity Structure of a Smart Contract – Basic Elements

In this article, we’ll take a detailed look at the structure of a smart contract (docs).

💡 We’ll learn about state variables, functions, function modifiers, events, errors, struct types, and enum types.

We have recognized most of these elements in action in the previous articles, especially in the ones where we had real examples of smart contracts.

Building on the previously seen context of use, we’ll spend a few more words on each of them and provide an additional context of their applicability. It’s always useful to know the proverbial nuts and bolts of the stuff we’re working with, as well as stretch our imagination for possibly other uses than just the most common ones.

Structure of a Contract

For those of us who have some experience with programming with object-oriented languages, OOL, there is a noticeable similarity between smart contracts and classes in OOL.

As mentioned in the introduction, contracts may contain any of the state variables, functions, function modifiers, events, errors, struct types, and enum types.

There is a useful Solidity feature we haven’t used before, and it enables us to form a new smart contract based on some other, more general but useful contract. This feature is called inheritance. We’ll just touch on the concepts in this article but will also go into further detail in a future one.

Info: “Object-oriented language (OOL) is a high-level computer programming language that implements objects and their associated procedures within the programming context to create software programs. Object-oriented language uses an object-oriented programming technique that binds related data and functions into an object and encourages reuse of these objects within the same and other programs.”

Other special kinds of contracts are libraries and interfaces.

  • 📚 Libraries are contracts that are deployed only once at a specific address (just like a brick-and-mortar library in the real world), and then their “knowledge”, i.e. functionality and data structures are reused whenever needed.
  • đŸ–Ĩī¸ Interfaces look like abstract contracts, but they cannot implement any functions. Instead, they contain only the declarations of data structures and function signatures, instructing an inheriting contract on how the function should look when it’s implemented.

State Variables

State variables, or more generally, objects hold the values that are permanently stored in the contract (EVM) memory area called storage. As a reminder, there are three memory areas: storage, memory, call data, plus stack.

An example of a state variable declaration looks like this:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract SimpleStorage {
    uint storedData; // State variable
    // ...
}

Functions

Functions are constructs of a smart contract that give it the desired behavior, or in other words, functions are executable units of code.

They can be defined inside or outside a contract, depending on the project structure and behavior requirements.

Here we have an example of a simple helper function (a frequently used utility function) that just takes an argument, multiplies it by 2, and returns the result to the caller would look like this:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;

contract SimpleAuction {
    function bid() public payable { // Function
        // ...
    }
}

// Helper function defined outside of a contract
function helper(uint x) pure returns (uint) {
    return x * 2;
}

The function definition says a few more things about the function, apart from its behavior. The function is defined as pure, meaning it cannot access any of the state variables, i.e. its outer context is made unavailable. the function returns a result of type uint, i.e. the unsigned integer.

Function calls can be made either from other smart contracts or clients, which we recognize as external function calls, or from other same-contract functions, which we recognize as internal function calls.

Besides, functions can be defined with one of the several different levels of visibility towards other contacts. In general, functions are defined with (input) parameters (also known as formal parameters, variables, or named placeholders) and accept arguments in their place (actual parameters, real values).

With that in mind, we see that functions communicate with each other by accepting arguments and returning values.

Function Modifiers

According to the official Solidity documentation, “Function modifiers can be used to amend the semantics of functions in a declarative way”.

This means that the function definition is supplemented with a function modifier that additionally defines how a function can interact with its other context.

ℹī¸ Info: Semantics is the branch of linguistics and logic concerned with meaning, branching into two main areas: logical semantics and lexical semantics. Lexical semantics is concerned with the analysis of word meanings and relations between them.

💡 Info: Declarative way expresses what should be done, or the desired behavior, without specifying how it should be achieved or implemented (imperative).

The definition above means that we can use one of the function modifiers to change the way the function behaves in terms of reading and modifying the state variables, only reading the state variables, or being isolated from the state variables, i.e. working only with its input arguments.

Events

Events are most commonly described as convenience interfaces with the EVM logging facilities.

In case this description seems a bit vague or dry, let’s put it in other words: events enable us as developers to deploy signal sending from contract functions and use these signals to monitor the contract.

👀 Contract monitoring is useful in several ways, e.g. we can use them to trigger an action upon receiving the signal, forming a chain of actions, during the debugging process, or just to monitor and even learn how a smart contract works.

More on events can be found following the link: https://docs.soliditylang.org/en/v0.8.15/contracts.html#events.

Errors

Error constructs represent a useful and simple approach that enables us to define descriptive names and data for circumstances when an error occurs.

As shown in previous articles on Solidity source code examples, error constructs are commonly used with revert statements to assist in clarifying the cause of an error and, consequentially, the state reversal.

By using error constructs, we can precisely determine the details of an error, the error name, and the data that caused it. Errors use fewer resources than string descriptions and allow us to encode additional data. Even more, we can use NatSpec to describe the error.

An example of an error construct is:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

/// Not enough funds for transfer. Requested `requested`,
/// but only `available` available.
error NotEnoughFunds(uint requested, uint available);

contract Token {
    mapping(address => uint) balances;
    function transfer(address to, uint amount) public {
        uint balance = balances[msg.sender];
        if (balance < amount)
            revert NotEnoughFunds(amount, balance);
        balances[msg.sender] -= amount;
        balances[to] += amount;
        // ...
    }
}

Struct Types

Struct types are Solidity data types that we define when we need a custom, complex data type.

Struct types are formed by grouping variables together and used by instancing the custom struct type later in the source code:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract Ballot {
    struct Voter { // Struct
        uint weight;
        bool voted;
        address delegate;
        uint vote;
    }
}

Enum Types

Enum types are special custom types that are based on a finite, limited set of constant values. In our example a few articles before, we used the Enum type for keeping track of a state our contract is in:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract Purchase {
    enum State { Created, Locked, Inactive } // Enum
}

Conclusion

Throughout this article, we made an additional step in familiarizing ourselves with the basic elements of the Solidity programming language.

As we advance our knowledge, we’ll get to know each of the mentioned elements even better. So far, we went through several, important sections.

First, we had a brief overview of the structure of a smart contract.

Second, we focused on the state variables and reminded ourselves what they are.

Third, we formally refreshed our knowledge on the topic of functions.

Fourth, a nice reminder of the function modifiers, along with a few definitions blazed in front of our eyes.

Fifth, although it wasn’t a posh event, it was an interesting refresher on how can we set them up! Just remember, dress appropriately 🙂

Sixth, errors are common in life, since nobody’s perfect. How to handle errors is an entirely different thing. Now we know how and are ready to face them!

Seventh, what should we do with struct types? We learned that trick, let’s set up a trap and use it to capture custom data structure.

Eighth, we learned what enum types are and how to use them to keep a record of our smart contract’s current state.


What’s Next?

This tutorial is part of our extended Solidity documentation with videos and more accessible examples and explanations. You can navigate the series here (all links open in a new tab):