Events, indexed-параметры, require/assert/revert, custom errors Solidity 0.8+
События — это единственный дешёвый способ записи данных из контракта; правильная обработка ошибок определяет, насколько понятен контракт при сбоях
События позволяют контракту записывать информацию в лог транзакции. Лог не хранится в storage EVM (поэтому дешевле), но доступен через фильтрацию блокчейна — ноды, The Graph, ethers.js.
Типичные применения: уведомление фронтенда об изменениях, аудит-трейл для off-chain систем, реакция на действия (мониторинг), замена хранения данных в storage когда нужна только история.
Объявление и вызов события:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Token {
mapping(address => uint256) public balances;
// Объявление события
event Transfer(address indexed from, address indexed to, uint256 amount);
event Minted(address indexed to, uint256 amount, string note);
function mint(address to, uint256 amount) external {
balances[to] += amount;
emit Minted(to, amount, "Initial mint");
emit Transfer(address(0), to, amount); // Mint = transfer from zero address
}
}Каждый emit записывает строку в лог транзакции. Это стоит газ (LOG0-LOG4 опкоды), но значительно дешевле SSTORE.
Параметр с модификатором indexed индексируется и может использоваться для фильтрации событий по значению. Без indexed параметр записывается в data-часть лога и не фильтруется.
Ограничение: максимум 3 indexed параметра на событие. Типы: любые значимые типы, bytes32. Динамические типы (string, bytes) при индексировании хэшируются — хранится keccak256, а не сам текст.
Практический пример фильтрации в ethers.js (как это используется на фронтенде):
// Все переводы на конкретный адрес
const filter = token.filters.Transfer(null, userAddress);
const events = await token.queryFilter(filter, fromBlock, toBlock);Именно поэтому from и to в ERC-20 Transfer событии помечаются indexed — это позволяет эффективно получать историю транзакций адреса.
Solidity предоставляет три механизма прерывания выполнения.
require(condition, message) — проверяет входные данные и предусловия. При ложном условии выбрасывает revert с сообщением. Нужен для: валидации аргументов, проверки прав доступа, проверки достаточности баланса. Возвращает неизрасходованный газ.
assert(condition) — проверяет инварианты, которые никогда не должны нарушаться. При ложном условии выбрасывает Panic(0x01). Используется для проверки логических ошибок в самом коде (не пользовательского ввода). Исторически assert потреблял весь газ — сейчас в 0.8.x тоже возвращает остаток газа.
revert(message) — безусловный откат с сообщением. Полезен для сложных условий, которые неудобно записать в require.
function withdraw(uint256 amount) external {
// require: проверяем пользовательское условие
require(amount > 0, "Amount must be positive");
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
// assert: проверяем инвариант — после списания баланс не должен быть больше начального
assert(balances[msg.sender] <= type(uint256).max);
(bool success, ) = payable(msg.sender).call{value: amount}("");
if (!success) revert("Transfer failed");
}Кастомные ошибки — эффективная альтернатива строковым сообщениям. Определяются как именованные типы, могут содержать параметры, кодируются компактно (4 байта selector + данные vs длинная строка).
Использование кастомных ошибок экономит газ при деплое и при revert (меньше данных в returndata) и улучшает читаемость кода.
// Объявление кастомных ошибок
error Unauthorized(address caller, address owner);
error InsufficientBalance(uint256 requested, uint256 available);
error DeadlineExpired(uint256 deadline, uint256 current);
contract Vault {
address public owner;
mapping(address => uint256) public balances;
modifier onlyOwner() {
if (msg.sender != owner) revert Unauthorized(msg.sender, owner);
_;
}
function withdraw(uint256 amount) external {
uint256 available = balances[msg.sender];
if (amount > available) revert InsufficientBalance(amount, available);
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
function timedAction(uint256 deadline) external {
if (block.timestamp > deadline) revert DeadlineExpired(deadline, block.timestamp);
// выполняем действие
}
}Для декодирования кастомных ошибок в ethers.js или Tenderly нужен ABI контракта — ошибки попадают в него как обычные события. При работе с кастомными ошибками в тестах можно использовать vm.expectRevert(abi.encodeWithSelector(MyError.selector, param1, param2)) в Foundry.
Несколько практических правил, которые улучшают контракт:
Проверяйте условия в начале функции — до изменений state (Checks-Effects-Interactions). Если что-то пойдёт не так, газ на выполненные изменения не будет потрачен.
Используйте кастомные ошибки с параметрами вместо строковых сообщений в новом коде — это дешевле и информативнее для отладки.
При внешних вызовах всегда проверяйте success: (bool success, bytes memory data) = addr.call{value: amount}(""); и обрабатывайте !success через revert.
Никогда не глушите ошибки внешних вызовов без явной причины — если call вернул false, это означает, что что-то пошло не так на стороне получателя.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.