К основному содержимому

Введите слово из вопроса или названия статьи.

3 · Под капотом

Обзор языка Solidity: типы, функции, ошибки и события на примерах

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

4 мин чтенияОбновлено:
В этой статье

Solidity - основной язык смарт-контрактов для Ethereum и совместимых сетей. Он похож на JavaScript и C++, но с важными отличиями: каждая операция стоит газа, код нельзя поправить после развёртывания, а ошибка может стоить денег. Здесь мы не будем учить программированию с нуля; цель - научиться читать контракты вроде нашего и понимать, что в них происходит. Предварительно полезно прочитать что такое смарт-контракт.

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

solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";

contract Escrow is Ownable2Step {
    uint16 public constant PLATFORM_FEE_BPS = 100;   // константа
    uint256 public dealCount;                         // переменная состояния

    event OfferCreated(uint256 indexed dealId);       // событие
    error ZeroAmount();                               // ошибка

    function createOffer(uint256 amount) external returns (uint256) { ... }
}
  1. Лицензия и версия компилятора (pragma) в начале. Эскроу собран версией 0.8.24 (как проверить).
  2. Импорты из библиотек (OpenZeppelin).
  3. Контракт - как класс: состояние + функции.
  4. Наследование (is): контракт перенимает код родителей.

Типы данных

ТипОписаниеПример
uint256, uint96, uint16целые без знака разных размеровсумма, ставка
int256целые со знакомредко
boolистина/ложьфлаг funded
addressадрес (20 байт)покупатель
address payableадрес, которому можно отправить ETHполучатель
bytes3232 произвольных байтахеш условий
string, bytesдинамические данныередко в деньгах
enumнабор именованных значенийStatus.Active
structсоставной типсделка Deal
mapping(K => V)таблицаdeals[id]

Дробных чисел нет (почему). Размеры влияют на упаковку в хранилище.

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

solidity
enum Status { None, Offered, Active, Disputed, Settled }

struct Deal {
    address buyer;
    uint96 amount;
    Status status;
    // ...
}

mapping(uint256 => Deal) private _deals;

Статусы образуют конечный автомат: сделка переходит из одного состояния в другое по строгим правилам (разбор).

Функции и видимость

solidity
function confirmDelivery(uint256 dealId) external nonReentrant { ... }

Видимость:

  • external - вызывается только извне;
  • public - извне и изнутри;
  • internal - внутри контракта и наследников;
  • private - только внутри этого контракта (но данные всё равно читаемы).

Изменяемость:

  • view - читает состояние, не меняет;
  • pure - не читает и не меняет;
  • payable - может принимать ETH.

Данные: storage, memory, calldata

Для ссылочных типов указывается место:

  • storage - постоянная память контракта (дорого);
  • memory - временная в рамках вызова;
  • calldata - входные данные транзакции, только для чтения (дёшево).
solidity
function createOffer(OfferParams calldata p) external payable { ... }

calldata для параметров функции экономит газ, потому что данные не копируются.

Модификаторы

Модификатор - повторно используемая «обёртка» над функцией:

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

_ - место, где выполняется тело функции. Встречаются onlyOwner, whenNotPaused, nonReentrant (шаблоны доступа, реентерабельность).

Ошибки

Раньше использовали require(cond, "текст"). Современный способ - пользовательские ошибки:

solidity
error WrongStatus(Status expected, Status actual);

if (d.status != expected) revert WrongStatus(expected, d.status);

Они компактнее по газу и несут параметры, понятные интерфейсу. Откат отменяет все изменения транзакции.

События

solidity
event DealSettled(uint256 indexed dealId, Outcome outcome, uint256 buyerAmount, ...);
emit DealSettled(dealId, outcome, buyerAmount, sellerAmount, arbiterFee, platformFee);

События пишутся в журнал транзакции и дёшевы. Сайты и индексаторы читают их, чтобы построить историю (подробнее). Поля indexed позволяют быстро фильтровать.

Глобальные переменные

ПеременнаяЧто это
msg.senderкто вызвал
msg.valueсколько ETH отправлено
block.timestampвремя блока (секунды)
address(this)адрес самого контракта
solidity
uint64 deadline = uint64(block.timestamp) + 7 days;

Единицы времени 1 days, 7 days - просто числа секунд.

Работа с ETH и токенами

Отправка основной монеты:

solidity
(bool ok, ) = payable(to).call{value: amount, gas: 50_000}("");

Токены ERC-20 - через интерфейс IERC20 и обёртку SafeERC20, учитывающую нестандартные реализации (особенности, реализация). Порядок «сначала состояние, потом внешние вызовы» защищает от атак.

Чтение контракта: быстрый алгоритм

  1. Найдите структуры данных и статусы - это модель.
  2. Составьте список внешних функций - это интерфейс.
  3. Для каждой посмотрите кто может вызвать и в каком состоянии.
  4. Найдите места, где уходят деньги (call, transfer) и проверьте порядок.
  5. Прочитайте события: они расскажут, что контракт считает важным.

Что почитать дальше

Если хотите писать, а не только читать: ERC-20 с нуля и руководство по Foundry. Внутреннее устройство - в разделе об EVM.

Упоминается в статьях

Готовы попробовать?

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