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-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.