Os eventos de solidity são a coisa mais próxima de uma declaração “print” ou “console.log” no Ethereum. Vamos explicar como eles funcionam e quando usá-los. Também entraremos em muitos detalhes técnicos frequentemente omitidos em outros recursos.
Aqui está um exemplo mínimo para emitir um evento de solidity.
contract ExampleContract { // We will explain the significance of the indexed parameter later. event ExampleEvent(address indexed sender, uint256 someValue); function exampleFunction(uint256 someValue) public { emit ExampleEvent(sender, someValue); } }
Talvez os eventos mais conhecidos sejam aqueles emitidos pelos tokens ERC20 quando são transferidos. O remetente, o destinatário e o valor são registrados em um evento.
emit Transfer(from, to, amount);
Isso não é redundante? Já podemos examinar as transações anteriores para ver as transferências e, em seguida, examinar os dados de chamada da transação para ver as mesmas informações.
Isso está correto, pode-se excluir eventos e não afetar a lógica de negócios do contrato inteligente. No entanto, esta não seria uma maneira eficiente de olhar para a história.
O cliente Ethereum não possui uma API para listar transações por “tipo”, por exemplo “todas as transações de transferência de um ERC20”. Aqui estão suas opções se você quiser consultar transações:
getTransaction
getTransactionFromBlock
A API getTransactionFromBlock
só pode informar quais transações ocorreram em um bloco específico, não pode direcionar vários blocos de endereço de contrato inteligente.
getTransaction
só pode inspecionar transações para as quais você conhece o hash da transação.
Os eventos, por outro lado, podem ser recuperados com muito mais facilidade.
Aqui estão as opções do cliente Ethereum:
eventos
eventos.todososeventos
getPastEvents
Cada um deles requer a especificação do endereço do contrato inteligente que o consultador deseja examinar e retorna um subconjunto (ou todos) dos eventos que um contrato inteligente emitiu de acordo com os parâmetros de consulta especificados.
Aqui está o principal insight de por que você usaria Eventos para rastrear transações em vez das próprias transações: Ethereum não fornece um mecanismo para obter todas as transações para um contrato inteligente, mas fornece um mecanismo para obter todos os eventos de um contrato inteligente.
Por que é isso? Tornar os eventos rapidamente recuperáveis requer sobrecarga de armazenamento adicional. Se a Ethereum fizesse isso para todas as transações, isso tornaria a cadeia consideravelmente maior. Com eventos, os programadores de solidity podem ser seletivos sobre que tipo de informação vale a pena pagar pela sobrecarga de armazenamento adicional, para permitir a recuperação rápida fora da cadeia.
Aqui está um exemplo de uso da API descrita acima. Nesse código, o cliente se inscreve em eventos de um contrato inteligente. Esses exemplos estão todos em Javascript.
Esse código aciona um retorno de chamada toda vez que um token ERC20 emite um evento de transferência.
const { ethers } = require("ethers"); // const provider = your provider const abi = [ "event Transfer(address indexed from, address indexed to, uint256 value)" ]; const tokenAddress = "0x..."; const contract = new ethers.Contract(tokenAddress, abi, provider); contract.on("Transfer", (from, to, value, event) => { console.log(`Transfer event detected: from=${from}, to=${to}, value=${value}`); });
Se quisermos ver os eventos retroativamente, podemos usar o código a seguir. Neste exemplo, olhamos para o passado para transações de aprovação em um token ERC20.
const ethers = require('ethers'); const tokenAddress = '0x...'; const filterAddress = '0x...'; const tokenAbi = [ { "anonymous": false, "inputs": [ { "indexed": true, "name": "from", "type": "address" }, { "indexed": true, "name": "to", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "Transfer", "type": "event" } ]; const tokenContract = new ethers.Contract(tokenAddress, tokenAbi, provider); // this line filters for Approvals for a particular address. const filter = tokenContract.filters.Approval(filterAddress, null, null); tokenContract.queryFilter(filter).then((events) => { console.log(events); });
Se você quiser procurar uma troca entre dois endereços conhecidos específicos (se tal transação existir), o arquivo ethers.js. código seria o seguinte:
tokenContract.filters.Transfer(address1, address2, null);
Aqui está um exemplo semelhante em web3.js em vez de ethers.js. Observe que os parâmetros de consulta fromBlock
e toBlock
foram adicionados (para indicar que nos preocupamos apenas com eventos entre esses blocos) e demonstraremos a capacidade de ouvir vários endereços sendo o remetente. Os endereços são combinados com a condição "OR".
const Web3 = require('web3'); const web3 = new Web3('https://rpc-endpoint'); const contractAddress = '0x...'; // The address of the ERC20 contract const contractAbi = [ { "anonymous": false, "inputs": [ { "indexed": true, "name": "from", "type": "address" }, { "indexed": true, "name": "to", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "Transfer", "type": "event" } ]; const contract = new web3.eth.Contract(contractAbi, contractAddress); const senderAddressesToWatch = ['0x...', '0x...', '0x...']; // The addresses to watch for transfers from const filter = { fromBlock: 0, toBlock: 'latest', topics: [ web3.utils.sha3('Transfer(address,address,uint256)'), null, senderAddressesToWatch, ] }; contract.getPastEvents('Transfer', { filter: filter, fromBlock: 0, toBlock: 'latest', }, (error, events) => { if (!error) { console.log(events); } });
O exemplo acima funciona porque o evento Aprovar (e Transferir) no ERC20 define o remetente a ser indexado. Aqui está a declaração do evento ERC20 Approval no Solidity.
event Approval(address indexed owner, address indexed spender, uint256 value);
Se o argumento "proprietário" não fosse indexado, o código javascript anterior falharia silenciosamente. A implicação aqui é que você não pode filtrar eventos ERC20 que tenham um value
específico (quantia de token) para a transferência, porque isso não é indexado. Você deve extrair todos os eventos e filtrá-los lado do javascript; não pode ser feito no cliente Ethereum.
Um argumento indexado para uma declaração de evento é chamado de tópico .
A melhor prática geralmente aceita para eventos é registrá-los sempre que ocorrer uma alteração de estado potencialmente consequente. Alguns exemplos incluem:
Alteração do titular do contrato
éter em movimento
Conduzindo um comércio
Nem toda mudança de estado requer um evento. A pergunta que os desenvolvedores do Solidity devem se fazer é “alguém teria interesse em recuperar ou descobrir essa transação rapidamente?”
Isso requer algum julgamento subjetivo. Lembre-se, um parâmetro não indexado não pode ser pesquisado diretamente, mas ainda pode ser um dado útil quando acompanhado por um parâmetro indexado. Uma boa maneira de obter uma intuição para isso é observar como as bases de código estabelecidas projetam seus eventos
Como regra geral, os valores de criptomoeda não devem ser indexados e um endereço deve ser, mas essa regra não deve ser aplicada cegamente. Os índices apenas permitem que você obtenha rapidamente valores exatos, não um intervalo de valores.
Um exemplo disso seria adicionar um evento quando os tokens são gerados porque as bibliotecas subjacentes já emitem esse evento.
Os eventos mudam de estado; eles alteram o estado do blockchain armazenando o log. Portanto, eles não podem ser usados em funções de exibição (ou puras).
Eventos não são tão úteis para depuração como console.log e print são em outras linguagens; como os próprios eventos mudam de estado, eles não são emitidos se uma transação for revertida.
Para argumentos não indexados, não há limite intrínseco para o número de argumentos, embora, é claro, haja tamanhos de contrato e limites de gás aplicáveis. O seguinte exemplo sem sentido é solidez válida:
contract ExampleContract { event Numbers(uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256); }
Da mesma forma, não há limite intrínseco para o comprimento de strings ou arrays armazenados em um log.
No entanto, não pode haver mais de três argumentos indexados (tópicos) em um evento. Um evento anônimo pode ter 4 argumentos indexados (abordaremos essa distinção mais tarde).
Um argumento com zero eventos também é válido.
Os seguintes eventos se comportam de forma idêntica
event NewOwner(address newOwner); event NewOwner(address);
Em geral, incluir o nome da variável seria ideal porque a semântica por trás do exemplo a seguir é muito ambígua (não é assim que você deve declarar eventos!)
event Trade(address,address,address,uint256,uint256);
Podemos adivinhar que os endereços correspondem ao remetente e os endereços do token, enquanto os uint256es correspondem aos valores, mas isso é difícil de decifrar.
É convencional colocar o nome de um evento em maiúscula, mas o compilador não exige isso. Esta seria uma declaração muito melhor:
event Trade(address trader, address token1, address token2, uint256 token1Amount, uint256 token2Amount);
Quando um evento é declarado em um contrato pai, ele pode ser emitido pelo contrato filho. Os eventos são internos e não podem ser modificados para serem privados ou públicos.
Aqui está um exemplo:
contract ParentContract { event NewNumber(uint256 number); function doSomething(uint256 number) public { emit NewNumber(number); } } contract ChildContract is ParentContract { function doSomethingElse(uint256 number) public { emit NewNumber(number); // valid } }
Da mesma forma, os eventos podem ser declarados em uma interface e usados no filho, como no exemplo a seguir.
interface IExampleInterface { event Deposit(address indexed sender, uint256 amount); } contract ExampleContract is IExampleInterface { function deposit() external payable { emit Deposit(msg.sender, msg.value); // also valid } }
A EVM (Ethereum Virtual Machine) identifica eventos com o keccak256 de sua assinatura.
Para solidity versões 0.8.15 ou superior, você também pode recuperar o seletor usando o membro .selector.
pragma solidity ^0.8.15; contract ExampleContract { event SomeEvent(uint256 blocknum, uint256 indexed timestamp); function selector() external pure returns (bool) { // true return SomeEvent.selector == keccak256("SomeEvent(uint256,uint256)"); } }
O seletor de eventos é, na verdade, um tópico em si (discutiremos isso mais adiante em uma seção posterior).
Marcar variáveis como indexadas ou não não altera o seletor.
Os eventos podem ser marcados como anônimos, caso em que não terão um seletor. Isso significa que o código do lado do cliente não pode isolá-los especificamente como um subconjunto como nossos exemplos anteriores. A única maneira de o código do lado do cliente ver um evento anônimo é obter todos os eventos do contrato inteligente.
pragma solidity ^0.8.15; contract ExampleContract { event SomeEvent(uint256 blocknum, uint256 timestamp) anonymous; function selector() public pure returns (bool) { // ERROR: does not compile, anonymous events don't have selectors return SomeEvent.selector == keccak256("SomeEvent(uint256,uint256)"); } }
Como a assinatura do evento é usada como um dos índices, uma função anônima pode ter quatro tópicos indexados, pois a assinatura da função é “liberada” como um dos tópicos.
Um evento anônimo pode ter até quatro tópicos indexados. Um evento não anônimo pode ter até três.
contract ExampleContract { // valid event SomeEvent(uint256 indexed, uint256 indexed, address indexed, address indexed) anonymous; }
Eventos anônimos raramente são usados na prática.
Esta seção descreve eventos no nível de montagem do EVM. Esta seção pode ser ignorada para programadores novos no desenvolvimento de blockchain .
Para recuperar todas as transações que aconteceram com um contrato inteligente, o cliente Ethereum teria que escanear cada bloco, o que seria uma operação de I/O extremamente pesada; mas o Ethereum usa uma otimização importante.
Os eventos são armazenados em uma estrutura de dados Bloom Filter para cada bloco. Um Filtro Bloom é um conjunto probabilístico que responde com eficiência se um membro está no conjunto ou não. Ao invés de varrer todo o bloco, o cliente pode perguntar ao filtro bloom se algum evento foi emitido no bloco. Isso permite que o cliente escaneie o blockchain muito mais rapidamente para encontrar eventos.
Os Filtros Bloom são probabilísticos: às vezes eles retornam incorretamente que um item é um membro do conjunto, mesmo que não seja. Quanto mais membros forem armazenados em um Filtro Bloom, maior a chance de erro e maior deve ser o filtro Bloom (em termos de armazenamento) para compensar isso. Por causa disso, o Ethereum não armazena todas as transações em um Bloom Filter. Há muito menos eventos do que transações. Isso mantém o tamanho do armazenamento no blockchain gerenciável.
Quando o cliente obtém uma resposta de associação positiva de um filtro bloom, ele deve verificar o bloco para verificar se o evento ocorreu. No entanto, isso só acontecerá para um pequeno subconjunto de blocos, portanto, em média, o cliente Ethereum economiza muito cálculo verificando primeiro o filtro bloom quanto à presença do evento.
Na representação intermediária do Yul, a distinção entre argumentos indexados (tópicos) e argumentos não indexados torna-se clara.
As seguintes funções yul estão disponíveis para emitir eventos (e seu opcode EVM tem o mesmo nome). A tabela é copiada da documentação do yul com alguma simplificação.
Código de operação | Uso |
---|---|
log0(p, s) | log sem tópicos e dados mem[p…(p+s)) |
log1(p, s, t1) | log com tópico t1 e data mem[p…(p+s)) |
log2(p, s, t1, t2) | log com os tópicos t1, t2 e data mem[p…(p+s)) |
log3(p, s, t1, t2, t3) | log com tópicos t1, t2, t3 e data mem[p…(p+s)) |
log4(p, s, t1, t2, t3, t4) | log com tópicos t1, t2, t3, t4 e data mem[p…(p+s)) |
Um log pode ter até 4 tópicos, mas um evento solidity não anônimo pode ter até 3 argumentos indexados. Isso ocorre porque o primeiro tópico é usado para armazenar a assinatura do evento. Não há opcode ou função Yul para emitir mais de quatro tópicos.
Os parâmetros não indexados são simplesmente codificados em abi na região da memória [p…(p+s)) e emitidos como uma longa sequência de bytes.
Lembre-se de que, em princípio, não havia limite para quantos argumentos não indexados um evento no Solidity pode ter. A razão subjacente é que não há limite explícito de quanto tempo leva a região de memória apontada nos dois primeiros parâmetros do código log op. É claro que existem limites fornecidos pelo tamanho do contrato e pelos custos do gás de expansão de memória.
Os eventos são substancialmente mais baratos do que gravar em variáveis de armazenamento. Os eventos não devem ser acessíveis por contratos inteligentes, portanto, a relativa falta de despesas gerais justifica um custo menor de gás.
A fórmula de quanto custa um evento de gás é a seguinte ( source ):
375 + 375 * num_topics + 8 * data_size + mem_expansion cost
Cada evento custa pelo menos 375 gás. Um adicional de $ 375 é pago para cada parâmetro indexado. Um evento não anônimo tem o seletor de eventos como parâmetro indexado, de modo que o custo é incluído na maioria das vezes. Então pagamos 8 vezes o número de palavras de 32 bytes escritas na cadeia. Como essa região é armazenada na memória antes de ser emitida, o custo de expansão da memória também deve ser contabilizado.
Em geral, a intuição de que quanto mais você registra, mais você paga em gasolina é precisa.
Os eventos servem para que os clientes recuperem rapidamente transações que possam ser de seu interesse. Embora não alterem a funcionalidade do contrato inteligente, eles permitem que o programador especifique quais transações devem ser recuperadas rapidamente. Isso torna mais fácil para os dapps resumir rapidamente informações importantes.
Os eventos são relativamente baratos em termos de gás em comparação com o armazenamento, mas o fator mais importante em seu custo é o número de parâmetros indexados, supondo que o codificador não use uma quantidade excessiva de memória.
Gosta do que vê aqui? Veja nosso Solidity Bootcamp avançado para saber mais.
A imagem principal deste artigo foi gerada pelo AI Image Generator do HackerNoon por meio do prompt "Events in Solidity"