Раздел 30 · Web3 на Rust

Solidity: язык контрактов

senior~40 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Solidity: язык контрактов

Мы разобрали EVM изнутри: стек, память, storage, газ. Писать контракты прямо в опкодах никто не станет, поверх машины живёт язык. Solidity это самый распространённый язык контрактов Ethereum, и почти каждая его особенность это прямое следствие того, как устроена машина под ним. Кто видел EVM, тот понимает Solidity не как «ещё один Си-подобный язык», а как тонкую обёртку над storage и газом.

Идея

Solidity компилируется в тот самый байткод EVM из урока 26. Синтаксис похож на JavaScript и на статически типизированный язык, но не обманывайся: за каждой строкой стоит газ и storage-слоты. Контракт это как класс: поля (состояние в storage), методы (функции), конструктор (запускается один раз при деплое).

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

contract Bank {
    // поля контракта живут в storage, это дорого и постоянно
    mapping(address => uint256) private balances;
    address public immutable owner;

    // событие: дешёвый способ рассказать миру о том, что случилось
    event Deposited(address indexed who, uint256 amount);

    // конструктор: один запуск при деплое, дальше кода конструктора в цепи нет
    constructor() {
        owner = msg.sender;
    }
}

Типы: значение против ссылки, и три места хранения

Типы делятся на значимые (uint256, bool, address, bytes32, маленькие uintN) и ссылочные (массивы, struct, mapping, string, bytes). Значимые копируются при присваивании, ссылочные требуют указать, где они лежат.

И вот главное, чего нет в обычных языках: у ссылочных данных есть место хранения: storage, memory или calldata. Это ровно те три области EVM из прошлого урока:

  • storage это постоянное состояние контракта, дорогое (SSTORE стоил тысячи газа), переживает транзакции.
  • memory это временный буфер на время вызова, дешёвый, стирается по завершении.
  • calldata это неизменяемые аргументы транзакции, самая дешёвая область для чтения.
// storage-ссылка: правки идут прямо в состояние контракта
function topUp(address user, uint256 amount) internal {
    balances[user] += amount; // это SSTORE, дорого
}

// calldata: аргумент только читаем, не копируем, минимальный газ
function sumAll(uint256[] calldata items) external pure returns (uint256 total) {
    for (uint256 i = 0; i < items.length; i++) {
        total += items[i];
    }
}

Спутать место это классическая ошибка: взял storage-ссылку думая, что это копия, и внезапно поменял состояние. Компилятор во многом страхует, но модель держать в голове надо.

Функции: видимость и мутабельность

У функции два ортогональных набора модификаторов. Видимость (кто может звать):

  • external только снаружи (транзакцией или другим контрактом),
  • public и снаружи, и изнутри,
  • internal только изнутри и из наследников,
  • private только из этого контракта.

И мутабельность (что функция делает с состоянием):

  • view читает состояние, но не меняет,
  • pure не трогает состояние вообще,
  • payable умеет принимать эфир,
  • без пометки: может менять состояние.
// payable: функция принимает эфир, msg.value это сколько прислали
function deposit() external payable {
    balances[msg.sender] += msg.value;
    emit Deposited(msg.sender, msg.value);
}

// view: только читает, вызов извне не стоит газа
function balanceOf(address user) external view returns (uint256) {
    return balances[user];
}

Эти пометки не косметика: view и pure компилятор проверяет и запрещает менять состояние, а забытый payable заставит контракт отвергать эфир.

Модификаторы: переиспользуемые проверки

Модификатор это обёртка вокруг функции, чаще всего проверка доступа. Тело функции встаёт на место _.

error NotOwner(); // кастомная ошибка, дешевле строки в require

modifier onlyOwner() {
    if (msg.sender != owner) revert NotOwner();
    _; // сюда подставится тело функции
}

function withdrawAll() external onlyOwner {
    uint256 amount = balances[owner];
    balances[owner] = 0; // сначала эффект: обнуляем баланс в storage
    // и только потом внешний вызов (урок The DAO не забыт: если бы порядок
    // был обратный, получатель мог бы повторно войти в withdrawAll,
    // пока баланс ещё не списан)
    (bool ok, ) = owner.call{value: amount}("");
    require(ok, "transfer failed");
}

Порядок строк в withdrawAll не случайность, а паттерн checks-effects-interactions: сначала проверки (модификатор), затем изменения своего состояния, и только последним внешний вызов. Ровно та ошибка порядка, что утопила The DAO (урок 24).

премиум-урок

Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.

Оплата пока не подключена: премиум и курсы открываются промокодом.