Introduction to Solidity language and smart contracts

In the past posts, we have seen how to interacts with smart contracts from web app, api. We have also deployed a smart contract with the help of openzeppelin. But we didn't learn about how to write a smart contract completly from scratch, learn the basics of solidity.

This is what we will do in this post and next ones. In this one we will cover the basics of how a smart contract is structured.

Structure of a smart contract

A smart contract also call Contracts in Solidity are similar to classes in object-oriented programming languages. They can contain functions and data variables, which can be used to store and retrieve values on the blockchain.

Here is a list of what a contract can declare :

  • State variables
  • Functions
  • Functions modifiers
  • Events
  • Errors
  • Structs
  • Enums

Let's see in details what each of them works.

State variables

These are variables that are permanently stored in contract storage.

They can have one the following types :

Boolean : A boolean value can have either true or false as its value.

bool myBool = true;

Integer : An integer is a whole number without a fractional component, and can be signed or unsigned.

int myInt = -123;
uint myUint = 456;

Address : An address is a 20-byte unique identifier for an Ethereum account.

address payable myAddress = 0x1234567890123456789012345678901234567890;

String : A string is a sequence of characters.

string myString = "Hello, World!";

Bytes : Bytes are fixed-length arrays of bytes.

bytes32 myBytes32 = 0x1234567890123456789012345678901234567890123456789012345678901234;

Array : An array is a collection of values of the same type.

uint[] myArray = [1, 2, 3];

Struct : A struct is a user-defined composite data type that groups together variables of different types.

struct Person {
  string name;
  uint age;
}
Person myPerson = Person("John Doe", 30);

Enum : An enum is a user-defined type consisting of a set of named constants.

enum Color {Red, Green, Blue}
Color myColor = Color.Green;

This types can also be combined with one the visibility modifiers

public : Public state variables differ from internal ones only in that the compiler automatically generates getter functions for them, which allows other contracts to read their values.

internal : Internal state variables can only be accessed from within the contract they are defined in and in derived contracts. They cannot be accessed externally.

private : Private state variables are like internal ones but they are not visible in derived contracts.

By default if visibility is not specified, the state variable is internal.

Functions and function modifiers

Functions are the main way to interact with the blockchain. They can be called by other contracts or directly by transactions. They are usually defined inside a contract, but they can also be defined outside of contracts. They can accept parameters and return values.

contract SimpleVote {
    function vote() public {
      ...
    }
}

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

Like state variables, functions can also have one the following visibility modifiers :

external External functions are part of the contract interface, which means they can be called from other contracts and via transactions. An external function f cannot be called internally (i.e. f() does not work, but this.f() works).

public Public functions are part of the contract interface and can be either called internally or via message calls.

internal Internal functions can only be accessed from within the current contract or contracts deriving from it. They cannot be accessed externally. Since they are not exposed to the outside through the contract’s ABI, they can take parameters of internal types like mappings or storage references.

private Private functions are like internal ones but they are not visible in derived contracts.

They can also be coupled with modifiers. Modifiers can be used to change the behaviour of functions in a declarative way. For example, you can use a modifier to automatically check a condition prior to executing the function.

contract Purchase {
    address public seller;

    // Modifier definition
    modifier onlySeller() {
        require(
            msg.sender == seller,
            "Only seller can call this."
        );
        _;
    }

    function abort() public view onlySeller { // Modifier usage
        // ...
    }
}

In this example, the modifier onlySeller is used to restrict the access to the abort function to the seller only.

Events

Events are a way to emit information from a smart contract to the outside world. Events are defined in the contract as a type of function that can be triggered by the contract's internal state changes.

When an event is triggered, it generates a log entry in the Ethereum blockchain, which can be listened to and interpreted by external applications, such as user interfaces or other smart contracts.

// Define an event

contract MyContract {

  event NewTrade(address trader, uint256 amount, uint256 timestamp);


  function trade(uint256 amount) public {
    // ... perform the trade logic ...

    // Emit the event
    emit NewTrade(msg.sender, amount, block.timestamp);
  }
}

In this example, we define an event called NewTrade that takes three parameters: the trader who initiated the trade, the amount of tokens traded, and the timestamp of the trade. The indexed keyword is used to allow for efficient filtering of the logs.

In the trade function, after performing the trade logic, we emit the event by calling emit NewTrade(...) with the appropriate parameters. This will generate a log entry in the blockchain that external applications can listen to and respond to.

Overall, events provide a way for contracts to communicate their state changes to the outside world.

Errors

Errors allows to define descriptive name and fata in case of failure. They are similar to exceptions in other languages. This are much cheaper than string description and allow to encode additional information and tfor hey can be used in revert statements.

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;
        // ...
    }
}

In this exemple we define an error called NotEnoughFunds that takes two parameters: the amount requested and the amount available. The error is then used in the transfer function to revert the transaction if the sender does not have enough funds.

Structs

Structs are a way to define a custom data type that can be used to group together related data. They are similar to tuples in other languages.

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

Enums

Enums can be used to create custom types with a finite set of ‘constant values’.

contract Ballot {
  enum Phase { Init, Regs, Vote, Done }
  Phase public state = Phase.Init;
}

Conclusions

In this article we have seen the basics of how a contract is structued. We didn't go in depth on every section because this is just an introduction to the language. In further articles we will deep dive into all the concepts we have seen here.