Solidity

Solidity - это язык программирования, используемый для написания смарт-контрактов на платформе Ethereum. 

Типы данных в Solidity:

Целочисленные типы (Integer Types):

uint: беззнаковое целое число.

int: знаковое целое число.

uint8, uint16, uint32, uint64, uint128, uint256: целые числа заданной длины без знака.

int8, int16, int32, int64, int128, int256: целые числа заданной длины со знаком.

int public age;

uint public age;

Адреса (Address):

address: специальный тип для адресов Ethereum.

address public owner =0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 

Логический тип (Boolean Type):

bool: принимает значение true или false.

bool public isTrue;

Строки (String):

string: динамическая строка переменной длины.

bytes: динамический массив байт.

bytes public name = "Test";

string public title = "To pay";

Массивы (Arrays):

fixedArray[]: массив фиксированной длины.

dynamicArray[]: динамический массив переменной длины.

int[] public arr;

Структуры (Structures):

struct: пользовательский тип данных, который может содержать различные типы данных.

struct User {

        address owner;

        uint256 value;

        uint256 timestamp;

    }

Перечисления (Enums):

enum Status {

        Create,

        Accept,

        Done,

        Fall

    }

Мапы (Mappings):

mapping: структура данных, ассоциирующая ключи с значениями.

mapping (address=>uint) public client;

Функции (Functions):

contract Test {

    struct User {

        address owner;

        uint256 value;

        uint256 timestamp;

        string text;

    }

    mapping(address => User) public client;

    function pay(string memory text) public payable {

        User memory _user = User(msg.sender, msg.value, block.timestamp, text);

        client[msg.sender] = _user;

    }

    function transfer(address to) public {

        address payable _to = payable(to);

        _to.transfer(address(this).balance);

    }

}.

Прочие (Miscellaneous):

address payable: специальный тип для адресов Ethereum с возможностью отправки эфира.

block: глобальные переменные, предоставляющие информацию о текущем блоке.

msg: глобальные переменные, предоставляющие информацию о текущем вызове или транзакции.

В Solidity существуют два основных вида циклов: for и while.

Цикл for:

function exampleForLoop() public pure returns (uint) {

    uint sum = 0;

    for (uint i = 1; i <= 10; i++) {

        sum += i;

    }

    return sum; // возвращает сумму чисел от 1 до 10

}

Цикл while:

function exampleWhileLoop() public pure returns (uint) {

    uint i = 0;

    uint sum = 0;

    while (i <= 10) {

        sum += i;

        i++;

    }

    return sum; // возвращает сумму чисел от 0 до 10

}

Операторы условия в Solidity:

Операторы сравнения:

==: равно

!=: не равно

>: больше

<: меньше

>=: больше или равно

<=: меньше или равно

Логические операторы:

&&: логическое "и"

||: логическое "или"

!: логическое "не"

Тернарный оператор:

? :: тернарный оператор используется для создания условного выражения. Он имеет следующий синтаксис: условие ? выражение1 : выражение2. Если условие истинно, возвращается выражение1, в противном случае возвращается выражение2.

Примеры использования:

uint a = 5;

uint b = 10;

bool result;

// Примеры операторов сравнения

result = (a == b);  // false

result = (a != b);  // true

result = (a > b);   // false

result = (a < b);   // true

result = (a >= b);  // false

result = (a <= b);  // true

// Примеры логических операторов

result = (a < 10 && b > 5);  // true

result = (a < 10 || b < 5);  // true

result = !(a == b);          // true

// Пример тернарного оператора

uint max = (a > b) ? a : b;  // max равно большему из a и b

Оператор if:

function exampleElseIf(uint x) public pure returns (uint) {

    if (x < 5) {

        return 1;

    } else if (x < 10) {

        return 2;

    } else {

        return 3;

    }

}

Оператор switch:

switch (выражение) {

    case значение1:

        // код, который нужно выполнить, если выражение равно значению1

        break;

    case значение2:

        // код, который нужно выполнить, если выражение равно значению2

        break;

    ...

    default:

        // код, который нужно выполнить, если ни одно из значений не соответствует выражению

        break;

}

Виды функции:

contract MyContract {

    // Публичная функция

    function myFunction() public returns (uint) {

        return 42;

    }

    // Приватная функция

    function myPrivateFunction() private returns (uint) {

        return 100;

    }

}

Анонимные функции:

pragma solidity ^0.8.0;

contract MyContract {

    function executeFunction(function(uint) external callback) public returns (uint) {

        return callback(42);

    }

}

Область видимости:

Public:

Переменные или функции с областью видимости public могут быть доступны извне контракта.

Для публичных переменных Solidity генерирует автоматически созданные геттеры, которые позволяют другим контрактам или внешним аккаунтам читать значение этой переменной.

Публичные функции могут быть вызваны извне контракта.

Пример:

pragma solidity ^0.8.0;


contract MyContract {

    uint public myPublicVariable = 42;


    function myPublicFunction() public pure returns (string memory) {

        return "Hello, world!";

    }

}

Private:

Переменные или функции с областью видимости private могут быть доступны только изнутри контракта, в котором они определены.

Они не могут быть вызваны или прочитаны извне контракта.

Пример:

pragma solidity ^0.8.0;


contract MyContract {

    uint private myPrivateVariable = 42;


    function myPrivateFunction() private pure returns (string memory) {

        return "Hello, private function!";

    }

}

Internal:

Переменные или функции с областью видимости internal могут быть доступны изнутри контракта и из наследующих контрактов.

Они не могут быть вызваны или прочитаны извне контракта или его наследников.

Пример:

pragma solidity ^0.8.0;


contract MyContract {

    uint internal myInternalVariable = 42;


    function myInternalFunction() internal pure returns (string memory) {

        return "Hello, internal function!";

    }

}

External:

Переменные или функции с областью видимости external могут быть вызваны только извне контракта.

Они не могут быть вызваны изнутри контракта, в котором они определены.

Пример:

pragma solidity ^0.8.0;

contract MyContract {

    function myExternalFunction() external pure returns (string memory) {

        return "Hello, external function!";

    }

}

Принципы объектно-ориентированного программирования (ООП):

Наследование:

В Solidity контракты могут наследовать функциональность и свойства других контрактов. Это позволяет создавать иерархии контрактов, где дочерние контракты могут использовать и переопределять функции и переменные базовых контрактов.


Пример:

pragma solidity ^0.8.0;


contract Animal {

    function makeSound() public pure virtual returns (string memory) {

        return "Some generic sound";

    }

}


contract Dog is Animal {

    function makeSound() public pure override returns (string memory) {

        return "Woof";

    }

}


Инкапсуляция:

Инкапсуляция позволяет скрыть детали реализации и предоставить интерфейс для взаимодействия с контрактом. В Solidity это достигается с помощью модификаторов доступа (например, public, private, internal, external) для переменных и функций.


Пример:

pragma solidity ^0.8.0;


contract Bank {

    uint private balance;


    function deposit(uint amount) public {

        balance += amount;

    }


    function withdraw(uint amount) public {

        require(amount <= balance, "Insufficient balance");

        balance -= amount;

    }


    function getBalance() public view returns (uint) {

        return balance;

    }

}


Полиморфизм:

Полиморфизм в Solidity позволяет объектам одного типа вести себя по-разному в зависимости от контекста. Это может быть достигнуто с помощью переопределения функций в дочерних контрактах.

Абстракция:

Абстракция позволяет скрыть сложные детали реализации и предоставить простой интерфейс для взаимодействия. Это может быть достигнуто с помощью абстрактных контрактов, которые определяют интерфейс без реализации.


Пример:

pragma solidity ^0.8.0;


abstract contract Animal {

    function makeSound() public pure virtual returns (string memory);

}


contract Dog is Animal {

    function makeSound() public pure override returns (string memory) {

        return "Woof";

    }

}

События (events):

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;


contract MyContract {

    // Определение события с указанием параметров

    event Transfer(address indexed _from, address indexed _to, uint _value);


    // Пример функции, которая вызывает событие

    function transfer(address _to, uint _value) public {

        // Проверка достаточности средств

        require(msg.sender.balance >= _value, "Insufficient balance");


        // Перевод средств

        payable(_to).transfer(_value);


        // Вызов события

        emit Transfer(msg.sender, _to, _value);

    }

}

В этом примере:

Transfer - это определенное событие, которое содержит три параметра: _from, _to и _value. Параметр _from является адресом отправителя, _to - адресом получателя, а _value - количеством передаваемых токенов (или эфира).

Функция transfer позволяет отправлять эфир другому адресу. Перед отправкой средств выполняется проверка достаточности баланса отправителя. Затем вызывается функция transfer для отправки эфира, и после этого вызывается событие Transfer с передачей параметров _from, _to и _value.


Модификаторы (Modifiers):

pragma solidity ^0.8.0;


contract MyContract {

    address public owner;


    // Модификатор для проверки, что функция вызвана владельцем контракта

    modifier onlyOwner() {

        require(msg.sender == owner, "Only contract owner can call this function");

        _; // это позволяет выполнить тело функции, к которой применяется модификатор

    }

    // Установка владельца контракта

    constructor() {

        owner = msg.sender;

    }

    // Функция, доступная только владельцу

    function changeOwner(address _newOwner) public onlyOwner {

        owner = _newOwner;

    }

}

В этом примере:

Модификатор onlyOwner проверяет, что вызывающий адрес соответствует адресу владельца контракта. Если это не так, функция завершится с ошибкой.

Затем модификатор вызывает внутренние функции с помощью _;, если проверка пройдена.

Функция changeOwner является общедоступной, но её выполнение доступно только владельцу контракта из-за использования модификатора onlyOwner.

Модификаторы делают код более читаемым и безопасным, позволяя централизовать и переиспользовать код для проверки различных условий в различных функциях контракта. Они также помогают соблюдать принципы DRY (Don't Repeat Yourself) и улучшают общую безопасность и эффективность контракта.

Проверка значений на соответствие:

 function transaction(address to) public {

        address payable _to = payable(to);

        // require(to==owner, "You are not owner");

        // assert(to==owner);

        if(to!=owner){

            revert("You are not owner");

        }

        _to.transfer(address(this).balance); 

    }

Библиотеки (Libraries) в Solidity:

pragma solidity ^0.8.0;


// Библиотека для работы с целыми числами

library MathLib {

    // Функция для сложения двух чисел

    function add(uint256 a, uint256 b) external pure returns (uint256) {

        return a + b;

    }


    // Функция для вычитания одного числа из другого

    function sub(uint256 a, uint256 b) external pure returns (uint256) {

        require(b <= a, "Subtraction result cannot be negative");

        return a - b;

    }

}


contract MyContract {

    // Импорт библиотеки

    using MathLib for uint256;


    // Пример использования библиотеки для сложения чисел

    function addNumbers(uint256 x, uint256 y) external pure returns (uint256) {

        return x.add(y);

    }


    // Пример использования библиотеки для вычитания чисел

    function subNumbers(uint256 x, uint256 y) external pure returns (uint256) {

        return x.sub(y);

    }

}


В этом примере:


MathLib - это библиотека, которая содержит функции add и sub для выполнения операций сложения и вычитания целых чисел. Они определены как внешние функции и имеют ключевое слово external.

Контракт MyContract использует библиотеку MathLib с помощью директивы using, которая позволяет вызывать функции библиотеки на объектах типа uint256.

Функции addNumbers и subNumbers в контракте MyContract вызывают функции add и sub из библиотеки MathLib для выполнения операций сложения и вычитания чисел.

Таким образом, использование библиотек позволяет создавать общие и полезные функции, которые могут быть использованы в различных контрактах, сокращая дублирование кода и улучшая его модульность.

В Solidity существует два типа библиотек: внутренние (internal) и внешние (external). Давайте рассмотрим их различия и примеры использования каждого типа:

Внутренние библиотеки (Internal Libraries):

Внутренние библиотеки определяются непосредственно внутри контракта и могут использоваться только внутри этого контракта.

Использование внутренних библиотек полезно, когда функции библиотеки нужны только внутри данного контракта и не должны быть доступны другим контрактам.

Пример внутренней библиотеки:

pragma solidity ^0.8.0;

contract MyContract {

    // Внутренняя библиотека для работы с целыми числами

    library MathLib {

        // Функция для сложения двух чисел

        function add(uint256 a, uint256 b) internal pure returns (uint256) {

            return a + b;

        }

    }

    // Пример использования внутренней библиотеки для сложения чисел

    function addNumbers(uint256 x, uint256 y) external pure returns (uint256) {

        return MathLib.add(x, y);

    }

}

Внешние библиотеки (External Libraries):

Внешние библиотеки представляют собой отдельные контракты, которые могут быть импортированы и использованы в других контрактах.

Использование внешних библиотек позволяет повторно использовать функциональность и типы данных в различных контрактах.

Пример внешней библиотеки:

pragma solidity ^0.8.0;


// Внешняя библиотека для работы с целыми числами

library MathLib {

    // Функция для сложения двух чисел

    function add(uint256 a, uint256 b) external pure returns (uint256) {

        return a + b;

    }

}


Контракт, использующий внешнюю библиотеку:

pragma solidity ^0.8.0;


import "./MathLib.sol";


contract MyContract {

    // Пример использования внешней библиотеки для сложения чисел

    function addNumbers(uint256 x, uint256 y) external pure returns (uint256) {

        return MathLib.add(x, y);

    }

}

Управление памятью в Solidity:

Ключевые слова для указания области памяти:

memory: Это ключевое слово используется для указания временной области памяти, которая будет выделена во время выполнения функции и уничтожена после её завершения. Переменные, объявленные с ключевым словом memory, хранятся в памяти.

storage: Это ключевое слово используется для указания постоянной области памяти, которая хранится на блокчейне и сохраняется между вызовами функций контракта.

Копирование данных между памятью и хранилищем:

Переменные, хранящиеся в области памяти (memory), могут быть копированы в хранилище (storage) и наоборот. Это происходит в основном внутри функций, когда данные должны быть сохранены на блокчейне или загружены из него.

Передача аргументов в функции:

При вызове функций в Solidity аргументы, передаваемые в функцию, могут быть указаны как memory, если они должны быть скопированы в память во время выполнения функции, или как storage, если они должны быть загружены из хранилища.

Пример использования управления памятью в Solidity:

pragma solidity ^0.8.0;


contract MemoryManagement {

    // Функция для копирования данных из memory в storage

    function copyData(uint256[] memory data) external {

        // Копируем данные из memory в storage

        uint256[] storage storageData = data;

    }


    // Функция для суммирования чисел в массиве в memory

    function sum(uint256[] memory numbers) external pure returns (uint256) {

        uint256 result = 0;

        for (uint256 i = 0; i < numbers.length; i++) {

            result += numbers[i];

        }

        return result;

    }

}

В этом примере:

Функция copyData принимает массив чисел в качестве аргумента memory, затем копирует его содержимое в переменную storageData, которая хранится в хранилище.

Функция sum принимает массив чисел в качестве аргумента memory, выполняет суммирование чисел и возвращает результат. Так как в данном случае нет необходимости сохранять данные между вызовами функций, массив чисел остаётся в памяти memory.

Правильное управление памятью в Solidity помогает оптимизировать расход газа и обеспечивает эффективную работу смарт-контрактов на блокчейне Ethereum.

Инлайн ассемблерный код (Inline Assembly):

В Solidity есть возможность использовать инлайн ассемблерный код (Inline Assembly), который позволяет встраивать низкоуровневые операции напрямую в смарт-контракты. Это особенно полезно, когда требуется выполнить сложные операции, которые нельзя реализовать с помощью стандартного высокоуровневого языка.

Вот пример использования инлайн ассемблера в Solidity:

pragma solidity ^0.8.0;

contract InlineAssemblyExample {

    // Функция для вычисления остатка от деления двух чисел, используя инлайн ассемблер

    function remainder(uint256 a, uint256 b) public pure returns (uint256) {

        uint256 result;

        assembly {

            // a % b

            // загружаем значения a и b в регистры

            let remainder := div(a, b)

            // результат помещаем в переменную result

            result := remainder

        }

        return result;

    }

}

В этом примере используется инлайн ассемблер для выполнения операции остатка от деления двух чисел. Операция div выполняет целочисленное деление, а результат сохраняется в переменной remainder.

Хотя инлайн ассемблер предоставляет гибкость в выполнении низкоуровневых операций, его использование также сопряжено с риском ошибок и усложнением чтения и поддержки кода. Поэтому обычно рекомендуется использовать инлайн ассемблер только в крайне необходимых случаях, когда стандартные средства Solidity не могут выполнить нужные операции.

Временные ограничения в Solidity:

Тайм-ауты (Timeouts):

Вы можете использовать временные ограничения для установки временных интервалов, в течение которых определенная функция может быть вызвана.

Это полезно, например, когда вы хотите предотвратить повторное голосование или выполнение каких-либо действий в течение определенного периода времени.

Блокировка средств (Locking Funds):

Вы можете установить временные ограничения для блокировки средств на определенный период времени.

Например, это может быть полезно при реализации токенов с возможностью временной блокировки для выполнения некоторых операций.

Контроль частоты вызовов функций (Function Call Rate Control):

Вы можете использовать временные ограничения для ограничения частоты вызовов определенных функций в вашем контракте.

Это может быть полезно для предотвращения спама или ограничения возможности злоумышленников вызывать определенные функции слишком часто.

Пример использования временных ограничений в Solidity для реализации тайм-аута:


pragma solidity ^0.8.0;


contract TimeoutExample {

    uint256 private lastCallTime;

    uint256 private constant timeout = 1 hours;


    // Функция, которая может быть вызвана только раз в час

    function callOncePerHour() public {

        require(block.timestamp >= lastCallTime + timeout, "Function can only be called once per hour");

        

        // Выполнение функции

        // ...


        lastCallTime = block.timestamp;

    }

}

В этом примере callOncePerHour можно вызывать только раз в час, так как мы используем временное ограничение, чтобы проверить, прошло ли уже достаточно времени с момента последнего вызова функции.

Интерфейсы (Interfaces):

В Solidity интерфейсы (Interfaces) представляют собой контрактные абстракции, которые определяют структуру и поведение контракта без реализации его функциональности. Они позволяют разделить интерфейс и реализацию контрактов, что делает код более модульным, облегчает разработку и обновление контрактов, а также позволяет контрактам взаимодействовать друг с другом.

Основные аспекты интерфейсов в Solidity:

Определение интерфейса:

Интерфейс определяется с использованием ключевого слова interface.

В интерфейсе описываются только сигнатуры функций без их реализации.

Реализация интерфейса:

Контракты могут реализовывать интерфейсы, указывая это в своём определении с использованием ключевого слова contract.

Для реализации интерфейса контракт должен реализовать все функции, описанные в интерфейсе.

Использование интерфейсов:

Интерфейсы могут быть использованы для создания экземпляров контрактов, вызова их функций и передачи в качестве аргументов в другие функции.

Пример интерфейса и его реализации:

// Определение интерфейса

interface Token {

    function transfer(address recipient, uint256 amount) external returns (bool);

}

// Реализация контракта, который использует интерфейс

contract MyContract {

    Token public tokenContract;

    // Принимаем адрес токен-контракта через конструктор

    constructor(address _tokenAddress) {

        tokenContract = Token(_tokenAddress);

    }

    // Функция для передачи токенов

    function transferTokens(address _recipient, uint256 _amount) external {

        tokenContract.transfer(_recipient, _amount);

    }

}

В этом примере:

Определяется интерфейс Token, который содержит сигнатуру функции transfer.

Контракт MyContract реализует этот интерфейс, принимая адрес токен-контракта через свой конструктор.

Функция transferTokens в контракте MyContract использует интерфейс Token для вызова функции transfer на токен-контракте.

Использование интерфейсов позволяет создавать гибкие и модульные контракты, которые могут взаимодействовать друг с другом, не завися от конкретной реализации.

В Solidity декларативные переменные и функции обычно относятся к способу объявления переменных и функций с указанием специальных ключевых слов для указания области памяти, в которой они должны быть хранены, и других особенностей. Это помогает явно определить поведение переменных и функций в контракте и управлять ресурсами, такими как память и хранилище.


Переопределение функций в дочерних классах

В Solidity ключевое слово virtual используется в контексте функций, чтобы указать, что функция может быть переопределена в наследующих контрактах. Когда функция объявляется как virtual, это означает, что она может быть переопределена (или перекрыта) в дочерних контрактах.

Когда вы хотите, чтобы функция могла быть переопределена в наследниках, вы объявляете её с ключевым словом virtual в базовом контракте. Затем в дочерних контрактах вы можете использовать ключевое слово override, чтобы переопределить эту функцию.

abstract contract Owner {

    function get() public virtual returns (uint) {

        return 12;

    }

}


abstract contract Payment {

    function get() public virtual returns (uint) {

        return 12;

    }

}


contract Test is Owner, Payment {

    function get() public override(Owner, Payment) returns (uint) {

        return Payment.get(); // или return Owner.get();

    }

}


Признаки "плохого кода"

Кратко рассмотрим 12 признаков, когда код можно улучшить: 1. Duplicated Code  — иногда повторяющийся код не всегда несет в себе пользу. Выде...