LEARN X · ЗА 17 МИН

Solidity

Solidity за 17 минут: смарт-контракты Ethereum на одной странице — типы, функции, модификаторы, mapping, события, наследование, газ и безопасность.

Solidity — статически типизированный язык для смарт-контрактов виртуальной машины Ethereum (EVM). Код компилируется в байткод, разворачивается в блокчейн и исполняется детерминированно. Весь язык — в комментариях рабочего кода (компилятор 0.8+).

Структура контракта

// SPDX-License-Identifier: MIT
// Первая строка — лицензия (SPDX), иначе предупреждение компилятора.

// pragma задаёт версию компилятора: ^0.8.0 — от 0.8.0 до <0.9.0.
pragma solidity ^0.8.0;

// Импорт другого файла/контракта (как модули).
// import "./OtherContract.sol";

// contract — основная единица, похож на class.
contract Hello {
    // Тело контракта: переменные состояния, функции, события...
    string public greeting = "Привет, мир"; // строчный комментарий
    /* блочный
       комментарий */
}

Типы данных

contract Types {
    // Целые без знака: uint8..uint256 шагом 8. uint == uint256.
    uint256 count = 42;
    uint8 small = 255; // максимум для 8 бит

    // Целые со знаком.
    int256 temperature = -10;

    // Логический.
    bool isReady = true;

    // Адрес (20 байт). address payable умеет принимать ETH.
    address owner = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
    address payable wallet;

    // Байтовые типы фиксированной длины: bytes1..bytes32.
    bytes32 hash;

    // Динамические: bytes и string (хранятся в storage/memory).
    string name = "codechick";
    bytes data;

    // Целочисленное деление округляет вниз, дробных типов нет.
    uint256 half = 7 / 2; // == 3
}

Переменные состояния и видимость

Переменные состояния хранятся в блокчейне (storage) и стоят газа при записи.

contract Storage {
    // public — компилятор создаёт авто-геттер с тем же именем.
    uint256 public total;

    // private — доступ только из этого контракта (но данные видны в блокчейне!).
    uint256 private secret;

    // internal — этот контракт и наследники (по умолчанию для переменных).
    uint256 internal shared;

    // constant — известно при компиляции, дешевле по газу.
    uint256 public constant MAX_SUPPLY = 1000000;

    // immutable — задаётся один раз в конструкторе, потом не меняется.
    address public immutable creator;

    constructor() {
        creator = msg.sender;
    }
}

Функции

contract Functions {
    uint256 public value;

    // Параметры с типами, видимость, возвращаемый тип через returns.
    function add(uint256 a, uint256 b) public pure returns (uint256) {
        return a + b; // pure — не читает и не пишет состояние
    }

    // view — читает состояние, но не изменяет его.
    function getValue() public view returns (uint256) {
        return value;
    }

    // Меняет состояние — не помечается view/pure (стоит газа).
    function setValue(uint256 v) public {
        value = v;
    }

    // Можно вернуть несколько значений (кортеж).
    function minMax(uint256 a, uint256 b) public pure returns (uint256 lo, uint256 hi) {
        return a < b ? (a, b) : (b, a);
    }
}

Видимость и mutability функций

contract Visibility {
    // public — вызывается извне и изнутри.
    function p() public {}

    // external — только извне (через this.f() изнутри); дешевле для больших аргументов.
    function e() external {}

    // internal — этот контракт и наследники.
    function i() internal {}

    // private — только этот контракт.
    function pr() private {}

    // payable — функция может принимать ETH вместе с вызовом.
    function deposit() public payable {
        // msg.value — сколько wei прислали
    }

    // Сводка mutability:
    // pure    — не трогает состояние
    // view    — только читает
    // (ничего)— читает и пишет
    // payable — ещё и принимает ETH
}

Конструктор

contract Owned {
    address public owner;
    uint256 public createdAt;

    // constructor выполняется один раз при деплое и не хранится в блокчейне.
    constructor(uint256 _start) {
        owner = msg.sender;       // тот, кто развернул контракт
        createdAt = block.timestamp;
        // _start — аргумент деплоя
    }
}

Маппинги и массивы

contract Collections {
    // mapping(ключ => значение): хеш-таблица, все ключи как бы существуют со значением 0.
    mapping(address => uint256) public balances;

    // Вложенный mapping: владелец -> (оператор -> разрешено?).
    mapping(address => mapping(address => bool)) public approved;

    // Динамический массив.
    uint256[] public numbers;

    // Массив фиксированной длины.
    uint256[3] public triple;

    function demo() public {
        balances[msg.sender] = 100;   // запись по ключу
        numbers.push(7);              // добавить в конец
        uint256 len = numbers.length; // длина
        numbers.pop();                // удалить последний
        // У mapping нельзя узнать длину или перебрать ключи!
    }
}

Структуры и перечисления

contract Models {
    // struct — пользовательский составной тип.
    struct User {
        string name;
        uint256 balance;
        bool active;
    }

    // enum — именованные константы (0, 1, 2...).
    enum Status { Pending, Active, Closed }

    Status public status = Status.Pending;
    mapping(address => User) public users;

    function register(string memory _name) public {
        // Создаём структуру по именам полей.
        users[msg.sender] = User({name: _name, balance: 0, active: true});
        status = Status.Active;
    }
}

Модификаторы, require и revert

contract Guards {
    address public owner;
    constructor() { owner = msg.sender; }

    // modifier — переиспользуемая проверка; _ подставляет тело функции.
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner"); // условие + сообщение
        _;
    }

    // Кастомные ошибки (дешевле строк по газу, с 0.8.4).
    error InsufficientBalance(uint256 available, uint256 required);

    function withdraw(uint256 amount) public onlyOwner {
        uint256 bal = address(this).balance;
        // revert откатывает все изменения транзакции.
        if (amount > bal) revert InsufficientBalance(bal, amount);
        // require — то же самое для простых проверок.
        require(amount > 0, "Zero amount");
        // assert — для инвариантов, которые "никогда" не должны нарушаться.
        assert(bal >= amount);
    }
}

События

События пишутся в журнал транзакции — дёшево и удобно слушать с фронтенда.

contract Events {
    // indexed-поля (до 3) позволяют фильтровать события по значению.
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Log(string message);

    mapping(address => uint256) public balance;

    function send(address to, uint256 amount) public {
        balance[msg.sender] -= amount;
        balance[to] += amount;
        // emit вызывает событие.
        emit Transfer(msg.sender, to, amount);
        emit Log("transfer done");
    }
}

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

// Базовый контракт.
contract Animal {
    // virtual — метод можно переопределить в наследнике.
    function sound() public pure virtual returns (string memory) {
        return "...";
    }
}

// is — наследование (поддерживается множественное: is A, B).
contract Dog is Animal {
    // override — переопределяем виртуальный метод.
    function sound() public pure override returns (string memory) {
        return "Гав";
    }
}

// Абстрактный контракт и интерфейс.
interface IToken {
    // В interface все функции external и без тела.
    function transfer(address to, uint256 amount) external returns (bool);
}

Глобальный объект msg и контекст

contract Context {
    // msg.* — данные текущего вызова.
    function inspect() public payable returns (address, uint256) {
        address caller = msg.sender;  // кто вызвал (адрес)
        uint256 sent = msg.value;     // сколько wei прислали
        bytes memory raw = msg.data;  // сырые calldata
        raw;                          // (чтобы не было предупреждения)

        // block.* и tx.* — данные блока и транзакции.
        uint256 ts = block.timestamp; // время блока
        uint256 num = block.number;   // номер блока
        ts; num;
        return (caller, sent);
    }

    // Получить ETH контрактом — нужна receive или fallback.
    receive() external payable {}
    fallback() external payable {}
}

Газ и безопасность

Каждая операция стоит газ; транзакция откатывается, если газ кончился. Главный класс уязвимостей — reentrancy.

contract Safety {
    mapping(address => uint256) public balances;

    // ОПАСНО: внешний вызов до обновления состояния — атака повторного входа.
    function badWithdraw() public {
        uint256 amount = balances[msg.sender];
        (bool ok, ) = msg.sender.call{value: amount}(""); // вызовет fallback атакующего
        require(ok);
        balances[msg.sender] = 0; // обновили СЛИШКОМ поздно
    }

    // ПРАВИЛЬНО: "checks-effects-interactions" — сперва меняем состояние, потом перевод.
    function goodWithdraw() public {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "empty");
        balances[msg.sender] = 0;          // effect до interaction
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "transfer failed");
    }
    // В 0.8+ арифметика проверяет переполнение автоматически (revert при overflow).
}

Пример: контракт-хранилище с балансами

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Простой банк: вносим и выводим ETH, владелец может смотреть казну.
contract Bank {
    address public immutable owner;
    mapping(address => uint256) private balances;

    event Deposited(address indexed who, uint256 amount);
    event Withdrawn(address indexed who, uint256 amount);

    modifier onlyOwner() {
        require(msg.sender == owner, "only owner");
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    // Вносим ETH на свой счёт.
    function deposit() external payable {
        require(msg.value > 0, "send some ETH");
        balances[msg.sender] += msg.value;
        emit Deposited(msg.sender, msg.value);
    }

    // Выводим со своего счёта (безопасный порядок действий).
    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount, "not enough");
        balances[msg.sender] -= amount;            // сначала эффект
        (bool ok, ) = payable(msg.sender).call{value: amount}("");
        require(ok, "transfer failed");            // потом перевод
        emit Withdrawn(msg.sender, amount);
    }

    // Свой баланс.
    function myBalance() external view returns (uint256) {
        return balances[msg.sender];
    }

    // Вся казна контракта — только владелец.
    function treasury() external view onlyOwner returns (uint256) {
        return address(this).balance;
    }
}
Поддержать проект