Events — объявление и emit
Как объявлять events в Solidity, вызывать emit и выбирать indexed-поля для фильтрации logs.
Event записывает факт транзакции
event — это описание записи, которую контракт может оставить во время выполнения транзакции. Такая запись попадает в logs и потом читается через RPC, block explorer, frontend или индексатор.
emit — это команда, которая создаёт конкретный log. Контракт меняет state отдельно, а event только сообщает внешнему миру, что произошло.
Пишем контракт с событиями
Создайте файл contracts/DonationBox.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract DonationBox {
address public immutable owner;
uint256 public totalDonated;
mapping(address => uint256) public donatedBy;
event DonationReceived(
address indexed donor,
uint256 amount,
uint256 totalDonated
);
event Withdrawn(
address indexed owner,
address indexed to,
uint256 amount
);
error OnlyOwner();
error ZeroAmount();
error ZeroAddress();
error NothingToWithdraw();
error EthTransferFailed();
constructor() {
owner = msg.sender;
}
function donate() external payable {
if (msg.value == 0) {
revert ZeroAmount();
}
donatedBy[msg.sender] += msg.value;
totalDonated += msg.value;
emit DonationReceived(msg.sender, msg.value, totalDonated);
}
function withdraw(address payable to) external {
if (msg.sender != owner) {
revert OnlyOwner();
}
if (to == address(0)) {
revert ZeroAddress();
}
uint256 amount = address(this).balance;
if (amount == 0) {
revert NothingToWithdraw();
}
(bool success, ) = to.call{value: amount}("");
if (!success) {
revert EthTransferFailed();
}
emit Withdrawn(msg.sender, to, amount);
}
}Этот контракт принимает пожертвования, хранит сумму по каждому адресу и записывает события, которые приложение может показать в истории действий.
Как это работает
event DonationReceived(...) — объявляет формат события. Это описание попадёт в ABI контракта.
address indexed donor — делает адрес донора фильтруемым. Frontend сможет быстро найти все пожертвования конкретного адреса.
uint256 amount — обычный аргумент события. Он попадёт в data, но по нему нельзя эффективно фильтровать logs через topics.
emit DonationReceived(msg.sender, msg.value, totalDonated) — создаёт log после изменения storage. В событие попадает уже обновлённый totalDonated.
event Withdrawn(...) — отдельное событие для другого действия. Лучше делать разные events для разных бизнес-событий, а не один универсальный Action.
emit Withdrawn(msg.sender, to, amount) — записывает, кто вывел ETH, куда и сколько.
Indexed-поля
indexed нужен для фильтрации logs. Внешнее приложение может запросить события только по конкретному donor, owner или to, если эти поля помечены как indexed.
В одном событии можно индексировать до трёх аргументов. Не стоит ставить indexed на всё подряд: индексированные динамические типы вроде string и bytes хранятся в topics как hash, а не как исходное значение.
Хорошие кандидаты для indexed: address, uint256 id, bytes32 role, bytes32 orderHash.
Events не заменяют State
Events нужны для истории, интерфейсов и индексаторов. Они дешевле записи в storage, но контракт не может прочитать старые logs из своего Solidity-кода.
Если значение нужно для будущей логики контракта, храните его в state: mapping, массиве или переменной. Если значение нужно только внешнему приложению, часто достаточно event.
В DonationBox баланс пожертвований хранится в donatedBy и totalDonated, а события помогают восстановить историю пожертвований снаружи.
Частые ошибки
Думать, что emit меняет состояние → event только создаёт log. State меняют присваивания, изменения storage и переводы ETH.
Не индексировать адреса → без indexed frontend не сможет быстро фильтровать events по пользователю через topics.
Эмитить событие до проверки результата → сначала выполните действие и убедитесь, что оно не упало, затем вызывайте emit.
Что дальше
- Events и логи — как хранятся данные — разберите, где logs лежат после транзакции
- Исторические события через getLogs — прочитайте events через ethers v6
- Вызов write-функций — отправьте транзакцию и получите receipt с logs