Continuing the topic of blockchain and cryptocurrency, let's talk about the languages that are used in this industry. Most of them are well-known representatives: Java, C/C++, Python, Ruby, etc. They are used to describe common interfaces and protocols. However, there are highly specialized tasks where the use of popular languages is ineffective. An example is smart contracts and the Solidity language for the Ethereum platform.
Solidity Basics
Memory types
An important thing you need to understand before starting Solidity development is where and how is all the data stored? This will determine what methods of working with memory you will use and how much gas the end user of your smart contract will pay for it!
- Storage is the permanent memory of a smart contract, which stores all global variables in it. This is the most precious memory! Because the data stored in it is recorded on the blockchain forever. This is a kind of storage (database) of your smart contract.
- Memory is temporary memory that will be allocated only at the time the smart contract functions are called. This is a kind of RAM necessary for calculations. For example, it stores the values of variables that you pass to local functions or the values of variables that these functions return. It costs much less than storage.
- Stack - This memory operates on LIFO (last in, first out) principles. It is mainly used by EVM for all calculations. It costs the same as memory. The use of this type of memory is best left to the discretion of the compiler.
Location of data in memory
For arrays and structures, Solidity automatically determines, depending on the context, where this data should be located - in storage or in memory. For example, variables that are passed to a function, declared in a function and returned by a function are stored in memory, and global variables are stored in storage.
However, you can override this behavior by using the storage and memory keywords and this will affect how the assignment operator works.
- When we assign an existing variable with type memory to a variable with type storage, we copy data from memory to storage. In this case, a new storage is not created (since it was already allocated when creating the contract)!
- When we assign an existing variable with type storage to a variable with type memory, we copy data from storage to memory. This allocates new memory!
- When a storage variable is created locally within a function and an object is assigned to it by searching from the list of global variables, it simply refers to the data already placed in the storage storage. In this case, a new storage is not created!
We can only override the memory location of function parameters and local variables. Whenever we assign a variable with type storage to a variable with type memory, we create a copy and its further modification does not affect the state of the contract.
contract ExampleStore { /** * @dev Define the structure of the object */ struct Item { uint price; uint units; } /** * @dev When creating a contract, storage memory is allocated * to store an array of our Item structures */ Item[] public items; /** * @dev Create an instance of an object in memory * and add it to the array stored in storage */ function newItem(uint _price, uint _units) public { Item memory item = Item(_price, _units); items.push(item); } /** * @dev Return a reference to a specific object * from an existing array living in storage. */ function getUsingStorage(uint _itemIdx) public returns (uint) { Item storage item = items[_itemIdx]; return item.units; } /** * @dev Return a copy of a specific object * from an existing array living in storage. */ function getUsingMemory(uint _itemIdx) public returns (uint) { Item memory item = items[_itemIdx]; return item.units; } /** * @dev Take a reference to a specific object from an existing array in storage * and change its value (the value in storage changes) */ function addItemUsingStorage(uint _itemIdx, uint _units) public { Item storage item = items[_itemIdx]; item.units += _units; } /** * @dev Take a copy of a specific object from an existing array in storage * and change its value (the value in storage does NOT change) */ function addItemUsingMemory(uint _itemIdx, uint _units) public { Item memory item = items[_itemIdx]; item.units += _units; } }
Solidity programming language course - training from scratch, lessons
As of August 2022, there are almost 166 million unique addresses in the Ethereum blockchain. An address in Ethereum has similar characteristics to postal addresses. Through the use of public key cryptography.
Address uniqueness
An Ethereum address is the last 20 bytes of the hash (hash function keccak-256) of the public key.
Since hashing functions are deterministic, this means that for different input data there will be a different hash, while for the same input data the hash function will always return the same hash. Therefore, a unique hash is created for the unique private key =>.
Personal and secret
Your mailbox key is not only unique, but also private and secret. We have already become acquainted with uniqueness, now let’s look at “personal” and “secret”.
Personal - only you own the key that opens your mailbox. You keep the key secret by attaching it to your key ring along with all your other keys.
Secret (private) - you and only you know what this physical key can be used for. If I give you my set of keys, you will not know which key to use to open my mailbox.
Similarly in Ethereum, your private key is stored in your wallet. Only you should know it and never share it.
Secret (private) key management
In the real world, you can open your mailbox with a unique physical key. Your mailbox has a built-in lock with a unique secret key attached to it to open it.
In Ethereum, you can use your account with a unique private key.
Note: private or secret key? Essentially it's the same thing. If you talk about “public key”, then pair it with “private key”. If you say “public key”, then say “private key”. Depends on your preferences on how to use Russian. In what follows, the “private” key will be used.
In the world of cryptography, "private" and "private" keys are interchangeable terms. The public key is derived from the private key, so they are related.
Private keys in Ethereum allow you to send ether by signing transactions. The only exception is smart contracts, as we will see later.
Different types of addresses
An Ethereum address is the same as a postal address: it represents the recipient of a message.
The address in the payment portion of an Ethereum transaction is the same as the recipient's account in a bank transfer.
Types of addresses in Ethereum: Externally owned accounts, contract accounts
External owned accounts (EOAs): controlled by private keys.
The private key gives control over the ether in the account and over the authentication processes required by the account when interacting with smart contracts. They (private keys) are used to create digital signatures that are required for transactions involving the spending of any funds in the account.
Contract accounts (smart contract accounts, CA): self-governing with their own code.
Unlike EOA, smart contracts do not have public or private keys. Smart contracts are not supported by a private key, but by an inherent code. You could say that they are “in control of themselves.”
The address of each smart contract is determined during the execution of the smart contract creation transaction, as the result of a function from the transaction source and nonce. An Ethereum smart contract address can be used in a transaction as a recipient, sending funds to the smart contract or calling one of the smart contract functions.
Importing source files
When your smart contract consists of several files and in one of these files you use the functions of another, then it is important to indicate in it which particular source code file you are using. This is done using the import control construct. Usually it is indicated immediately between the smart contract version and the block describing the contract itself. Moreover, you can specify both the URL link to the smart contract and the path to the file on your computer.
// Importing OpenZeppelin's ERC-721 Implementation import "https://github.com/OpenZeppelin/openzeppelin-solidity/contracts/token/ERC721/ERC721.sol"; // Importing OpenZeppelin's SafeMath Implementation import "https://github.com/OpenZeppelin/openzeppelin-solidity/contracts/utils/math/SafeMath.sol";
Working at Ambisafe Software
My interest in Solidity also stems from my desire to work for Ambisafe Software, because the company's ideas resonate with my worldview. The guys are already changing the world economy, and since childhood I have wanted to participate in something global and significant. I want to live in a world where people can open their own businesses without bribes, bureaucracy and nepotism. And now Ambisafe Software is bringing the world closer to this ideal. I want this to continue to exist.
Currently I am helping to write contracts for a project that sells real estate via blockchain. Ambisafe Software brings together brilliant minds and I think that in fact it is not so important what project you work on, but what matters is who exactly you work with: with such people we will make any project outstanding.
Topics: Solidity, career
Contract structures
Functions
Function Declaration
All smart contract code must be divided into logical blocks, each of which performs a specific task. Such logical blocks are usually called functions and are designated in the code by the keyword function.
Functions can be located:
- inside the contract (contract block),
- inside the library (library block),
- at the top level, outside the contract and libraries.
Feature Visibility
- external - External functions are part of a contract, which means they can be called from other contracts and through transactions. Reading comes directly from calldata. An external function f cannot be called from within (i.e. f() doesn't work, but this.f() does).
- internal - These functions and state variables can only be accessed from the current contract or its descendants without using this. This is the default visibility level for state variables.
- public - public functions are part of the contract interface and can be called either inside the contract as internal, or called externally as external. In this case, the array is copied into memory, thereby requiring more memory than using internal or external.
- private - private functions and state variables are visible only to the contracts in which they are defined. But they are not visible in the contracts inherited from them.
Function modifiers
In addition, Solidity has such a thing as a function modifier. This is a kind of named precondition allocated in advance into a separate block of code. They are denoted by the modifier keyword. If you apply one of these predefined modifiers to your function, your function will only be executed if the conditions specified in its modifier successfully pass the tests.
Below is an example of a function and modifier:
pragma solidity >=0.7.0 <0.9.0; contract MyContractName { address public seller; modifier onlySeller() { // Modifier require( msg.sender == seller, “Only seller can call this.” ); _; } function abort() public onlySeller { // Modifier usage // … } }
Types of functions
Functions are internal and external. Inner functions can only be called from the current contract in which the function is declared. External functions consist of an address and a signature, which can be passed through the return value of the external function call.
function () {internal|external} [pure|view|payable]
- are the parameters that you pass to the function. If you don't pass anything to the function, then no parameters are specified.
- {internal|external} is an indication of whether the function is internal or external. If not specified, then by default the function will be considered internal. If the function is external, then you must explicitly set it to external.
- [pure|view|payable] is a definition of the nature of the function, they can be as follows: pure - a function that makes a calculation only based on the arguments passed to it, while it does not read or change the state variables of the contract itself.
- view is a function available only for reading data, which guarantees that the state variables in it will not change (set by default, unless a different nature of the function is specified).
- payable - allows a function to receive ether when it is called.
There are 2 ways to call a function, either by its name f(), or using this.f(). However, if you did not declare a function with that name, or deleted it using the delete keyword before calling it, this will throw an exception. As a rule, if an internal function is called, the f() format is used, and if an external function is called, the this.f() format is used.
Built-in methods of external functions
For external functions you can use 2 built-in methods:
- .address - returns the address of the function contract.
- .selector - Returns the ABI function selector.
Previously, 2 more methods were used: .gas (uint) and .value (uint). They were deprecated in Solidity 0.6.2 and removed in Solidity 0.7.0. Instead, use {gas: ...} and {value: ...} to specify the amount of gas or amount of wei sent to the function, respectively.
pragma solidity >=0.6.4 <0.9.0; contract Example { function f() public payable returns (bytes4) { assert(this.f.address == address(this)); return this.f.selector; } function g() public { this.f{gas: 10, value: 800}(); } }
Special functions fallback and receive
- The fallback function is executed when the contract is called if none of its other functions match the given signature, i.e. essentially, if a function that does not exist in the contract is called. Fallback is also executed if a contract is called without transmitting any data or ether is transferred, but the contract does not have a receive function that would process it. The fallback function always accepts any data by default, but in order to accept ether, it must be marked as payable.
- The receive function is executed when the contract is called with an empty calldata value. This is a function that is called when transmitting ether to a contract address, for example, using the send and transfer functions. If, when transmitting ether, the receive function does not exist, but the fallback function is defined in the contract, then it will be called. If the fallback function is not defined, then the contract will not be able to receive ether, and all attempts to transfer ether to this contract will cause an exception.
contract TestPayable { uint x; uint y; // This function is executed when sending any message to this contract // including receiving the sent ether (when the receive function is not defined). // Any call with non-empty calldata to this contract will execute // the fallback function (even if ether is sent along with the call). fallback() external payable { x = 1; y = msg.value; } // This function is executed when ether is sent to this contract, // i.e. for all calls to contract with empty calldata. receive() external payable { x = 2; y = msg.value; } }
Virtual and override functions
Basic functions can be overridden in legacy contracts to change their behavior. Such functions should be marked as virtual.
A virtual function to be overridden must be marked with the override keyword.
Rules for overriding override functions:
- visibility can only be changed from external to public,
- if the virtual function was not payable, then it can be redefined in pure and view,
- view can be redefined to pure,
- payable cannot be overridden to any other visibility modifier.
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract Base { function foo() virtual external view {} } contract Inherited is Base { function foo() override public pure {} }
Events
Events allow you to conveniently display the necessary information about the occurrence of an event in the logs of the Ethereum virtual machine.
To define an event in code, use the event keyword. And to trigger this event, the emit keyword is used.
pragma solidity >=0.7.0 <0.9.0; contract MyContractName { event HighestBidIncreased(address bidder, uint amount); // Event function bid() public payable { // … emit HighestBidIncreased(msg.sender, msg.value); // Triggering event } }
Errors
You can describe possible errors in advance and assign names to them using the error keyword. If an atypical situation occurs in your code, you can call an error by its name in the same way as a function using the revert keyword. This is much cheaper than using string error descriptions and allows additional data to be passed.
pragma solidity >=0.7.0 <0.9.0; /// 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; // … } }
Structures
A structure is essentially a description of an object with a set of its properties, which is specified by the struct keyword. For example, you want to describe the characteristics of a character in your game. To do this, you create a Human structure and inside it describe its characteristics in the form of variables. Structures are allowed to be placed both outside the contract and inside it.
pragma solidity >=0.7.0 <0.9.0; contract Game { struct Human { // Struct uint weight; uint age; bool is Merried; address delegate; } }
Transfers
This is an object that can only store values predetermined in it. It is specified by the enum keyword. For example, let's define an enum object in our program, in which we will store a set of 3 colors. And then we will create a variable with the type of this object and assign it one of the values of this set.
pragma solidity >=0.7.0 <0.9.0; contract MyContractName { // define enum colors enum Color { Red, Green, Yellow } // Enum // set enum value Color constant defaultColor = Color.Green; }
Variables
Variable types
- State Variable is a state variable whose values are stored in the contract storage.
- Local Variable - variables that exist before the function is executed.
- Global Variable - global variables that allow you to obtain information about the blockchain.
Value types
Boolean
A Boolean variable that can store only 2 states: true and false. Example:
bool public paused = false;
Operators used: !, &&, ||, ==, !=
Integers
int / uint - signed and unsigned integer data type. Most often it is indicated with the number of bytes reserved for the declared variable. For example, int / uint is analogous to int256 / uint256 m
int / uint - signed and unsigned integer data type. Most often it is indicated with the number of reserved bits from 8 to 256 for the declared variable. For example, int/uint is the equivalent of int256/uint256.
// variables int* int8 = From -128 to 127 int16 = From -32,768 to 32,767 int32 = From -2,147,483,648 to 2,147,483,647 int64 = From -9,223,372,036,854,775,808 to 9,223,372,036 854 775 807 int128 = From -170141183460469231731687303715884105728 to 170141183460469231731687303715884105727 int256 = From -5789604461865809 7711785492504343953926634992332820282019728792003956564819968 to 57896044618658097711785492504343953926634992332820282019728792 003956564819967 // variables uint* uint8 = From 0 to 255 uint16 = From 0 to 65,535 uint32 = From 0 to 4,294,967,295 uint64 = From 0 to 18 446 744 073 709 551 615 uint256 = From 0 to 115792089237316195423570985008687907853269984665640564039457584007913129639935
Operators used: - Comparison: <=, <, ==, !=, >=, > - Bitwise: &, |, ^, ~ - Shift: <<, >> - Arithmetic: +, -, unary -, * , /, %, **
Fixed Point Numbers
Fixed-point numbers are not yet fully supported in Solidity. This means that you can declare this type, but you cannot assign it or you cannot assign other variables from the value of this type.
They are defined by the keywords fixedMxN and ufixedMxN for signed and unsigned numbers, respectively. Where instead of M the number of bits occupied by the stored value is entered from 8 to 256, and N is the number of available decimal places from 0 to 80. Moreover, ufixed and fixed are analogous to ufixed128x18 and fixed128x18, respectively.
Operators used: - Comparison: <=, <, ==, !=, >=, > - Arithmetic: +, -, unary -, *, /, %
Address
This is a fixed 20 byte value (the length of the address in Ethereum), which is the basis in all contracts. Using this type, you can specify participants, smart contact addresses, and addresses of your own tokens.
address nameReg = 0xdfc4bccf1aec515932c2d1ae499f92bb4ce04113;
The address can also be declared as payable, in which case it becomes possible to send ether to this address using two additional methods that it acquires: transfer and send.
address payable nameReg = 0xdfc4bccf1aec515932c2d1ae499f92bb4ce04113;
If you plan to declare a variable with the address type to which you want to transfer ether, then make this variable with the address payable type.
Operators used: <=, <, ==, !=, >=, >
Contract type
Each contract has its own type. Contracts can be converted to and from an address type. Moreover, converting a contract to an address payable type is only possible if the contract itself has a “receive or payable fallback” function.
If you declare a local contract variable, you can call functions of that contract. You can also create contract instances using the word new, this means that the contract will be created anew. By passing the contract variable C to the type(C) function, you can see the contract information.
Arrays
Arrays can have static or dynamic sizes. An array T of fixed size k is written as T[k], and a dynamic array is written as T[]. For example, an array of 5 dynamic arrays is written as uint[][5]. Note that this is the reverse notation compared to most other languages (they would do it the other way around).
Array elements can be variables of any type, including mapping and struct. If you mark arrays as public, Solidity will create a getter for them (a function for getting elements from it). The numeric index will be a required parameter for this getter. Accessing a non-existent array element raises an error. With the .push() and .push(value) methods you can add a new element to the end of an array, and calling the .push() method will add an empty element and return a reference to it.
Arrays of bytes and string types.
Arrays of type bytes and string are special arrays. For example, bytes is similar to byte[] but tightly packed in memory and callback functions, while string is equal to bytes but does not allow length or index access.
You can use bytes instead of byte[], because it's cheaper because byte[] adds 31 bytes between elements. Use bytes for raw byte data of arbitrary length (UTF-8). And if you can limit the length to an arbitrary number of bytes from 1 to 32, then use the bytes1 .. bytes32 types, since it is much cheaper. If you want to concatenate multiple bytes variables, use the built-in bytes.concat function.
bytes s = "Storage"; function f(bytes calldata c, string memory m, bytes16 b) public view { bytes memory a = bytes.concat(s, c, c[:2], "Literal", bytes(m), b); assert((s.length + c.length + 2 + 7 + bytes(m).length + 16) == a.length); }
Allocation of Memory arrays
Dynamic memory arrays can be created using the new operator. Unlike storage arrays, they cannot be resized, so you must calculate the required size for them in advance.
uint[] memory a = new uint[](7); bytes memory b = new bytes(len);
Array Methods
- length - allows you to understand the number of elements in the array,
- push() - allows you to add a new empty element to the end of the array, returns a link to the added element,
- push(x) - allows you to add a new element "x" to the end of the array, returns nothing,
- pop - removes the last element of the array.
Array slices
This is a representation of a continuous part of an array starting and ending with a specific index.
This is called as x[start:end], where start is the starting index, and end is the ending index, which ends the representation of part of the array. Moreover, the indication of start and end is optional, i.e. if you do not specify start, it will be considered the value 0, and if you do not specify end, then this will be considered the last element of the array.
Strings
In string variables, you can store string values that are enclosed in single or double quotes. They also have support for escape characters such as \n, \xNN, etc.
string public constant name = “This is my string value;
Solidity does not have string manipulation capabilities, but there are third party string libraries. For example, you can compare two strings by their hash: keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2)) or concatenate two strings using bytes.concat(bytes(s1), bytes(s2) ).
Unicode literals
Regular strings are specified in ASCII encoding, but if you plan to use UTF-8 in the string, you need to specify the unicode keyword before the double or single quotes.
string memory a = unicode»Hello