paint-brush
Événements Solidity : un aperçu completpar@rareskills
650 lectures
650 lectures

Événements Solidity : un aperçu complet

par RareSkills13m2023/05/31
Read on Terminal Reader

Trop long; Pour lire

Les événements de solidité sont ce qui se rapproche le plus d'une déclaration "print" ou "console" dans Ethereum. Nous vous expliquerons comment ils fonctionnent et quand les utiliser. Nous entrons également dans de nombreux détails techniques souvent omis dans d'autres ressources. Voici un exemple minimal pour émettre un événement de solidité.
featured image - Événements Solidity : un aperçu complet
RareSkills HackerNoon profile picture
0-item
1-item

Evénements solidaires

Les événements de solidité sont ce qui se rapproche le plus d'une déclaration "print" ou "console.log" dans Ethereum. Nous vous expliquerons comment ils fonctionnent et quand les utiliser. Nous aborderons également de nombreux détails techniques souvent omis dans d'autres ressources.


Voici un exemple minimal pour émettre un événement de solidité.

 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); } }



Les événements les plus connus sont peut-être ceux émis par les jetons ERC20 lors de leur transfert. L'expéditeur, le destinataire et le montant sont enregistrés dans un événement.


 emit Transfer(from, to, amount);


N'est-ce pas redondant ? Nous pouvons déjà parcourir les transactions passées pour voir les transferts, puis nous pourrions examiner les données d'appel de la transaction pour voir les mêmes informations.


C'est correct, on pourrait supprimer des événements et n'avoir aucun effet sur la logique métier du contrat intelligent. Cependant, ce ne serait pas une manière efficace de regarder l'histoire.


Table des matières

  • Evénements solidaires
  • Récupérer les transactions plus rapidement
  • A l'écoute des événements
  • Événements indexés par solidité vs événements non indexés
  • Bonnes pratiques des événements solidaires
  • Les événements ne peuvent pas être utilisés dans les fonctions
  • Combien d'arguments un événement peut-il prendre ?
  • Les noms de variables dans les événements sont facultatifs mais recommandés
  • Les événements peuvent être hérités via des contrats parents et des interfaces
  • Sélecteur d'événement
  • Événements anonymes
  • Rubriques avancées sur les événements
  • Coût du gaz pour émettre un événement de solidité
  • Conclusion


Récupérer les transactions plus rapidement

Le client Ethereum n'a pas d'API pour répertorier les transactions par "type", par exemple "toutes les transactions de transfert d'un ERC20". Voici vos options si vous souhaitez interroger les transactions :


  • getTransaction

  • getTransactionFromBlock


L'API getTransactionFromBlock ne peut vous dire que les transactions qui se sont produites sur un bloc particulier, elle ne peut pas cibler un contrat intelligent adresse plusieurs blocs.


getTransaction ne peut inspecter que les transactions dont vous connaissez le hachage de transaction.


Les événements, en revanche, peuvent être récupérés beaucoup plus facilement.


Voici les options du client Ethereum :


  • événements

  • events.allEvents

  • getPastEvents


Chacun d'entre eux nécessite de spécifier l'adresse du contrat intelligent que le demandeur souhaite examiner et renvoie un sous-ensemble (ou tous) des événements qu'un contrat intelligent a émis en fonction des paramètres de requête spécifiés.


Voici l'idée clé de la raison pour laquelle vous utiliseriez les événements pour suivre les transactions plutôt que les transactions elles-mêmes : Ethereum ne fournit pas de mécanisme pour obtenir toutes les transactions d'un contrat intelligent, mais il fournit un mécanisme pour obtenir tous les événements d'un contrat intelligent.


Pourquoi est-ce? Rendre les événements rapidement récupérables nécessite une surcharge de stockage supplémentaire. Si Ethereum faisait cela pour chaque transaction, cela rendrait la chaîne considérablement plus grande. Avec les événements, les programmeurs de solidité peuvent être sélectifs quant au type d'informations pour lesquelles il vaut la peine de payer les frais de stockage supplémentaires, afin de permettre une récupération rapide hors chaîne.


A l'écoute des événements

Voici un exemple d'utilisation de l'API décrite ci-dessus. Dans ce code, le client s'abonne aux événements d'un contrat intelligent. Ces exemples sont tous en Javascript.


Exemple 1 : écoute des événements ERC20 Transfer.

Ce code déclenche un rappel chaque fois qu'un jeton ERC20 émet un événement de transfert.

 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}`); });


Exemple 2 : Filtrage d'un agrément ERC20 pour une adresse précise

Si nous voulons regarder les événements rétroactivement, nous pouvons utiliser le code suivant. Dans cet exemple, nous nous tournons vers le passé pour les transactions d'approbation dans un jeton 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); });



Si vous vouliez rechercher un échange entre deux adresses connues particulières (si une telle transaction existe), le fichier ethers.js. le code serait le suivant :


 tokenContract.filters.Transfer(address1, address2, null);


Voici un exemple similaire dans web3.js au lieu de ethers.js. Notez que les paramètres de requête fromBlock et toBlock sont ajoutés (pour signifier que nous ne nous soucions que des événements entre ces blocs), et nous démontrerons la possibilité d'écouter plusieurs adresses en tant qu'expéditeur. Les adresses sont combinées avec la condition "OU".


 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); } });


Événements indexés par solidité vs événements non indexés

L'exemple ci-dessus fonctionne car l'événement Approve (and Transfer) dans ERC20 définit l'expéditeur à indexer. Voici la déclaration de l'évènement ERC20 Approval dans Solidity.


 event Approval(address indexed owner, address indexed spender, uint256 value);


Si l'argument "propriétaire" n'était pas indexé, le code javascript précédent échouait silencieusement. L'implication ici est que vous ne pouvez pas filtrer les événements ERC20 qui ont une value spécifique (montant de jeton) pour le transfert, car cela n'est pas indexé. Vous devez récupérer tous les événements et les filtrer côté javascript ; cela ne peut pas être fait dans le client Ethereum.


Un argument indexé pour une déclaration d'événement est appelé un sujet .


Bonnes pratiques des événements solidaires

La meilleure pratique généralement acceptée pour les événements consiste à les consigner chaque fois qu'un changement d'état potentiellement conséquent se produit. Voici quelques exemples :


  • Changer le propriétaire du contrat

  • Éther en mouvement

  • Faire un commerce


Tous les changements d'état ne nécessitent pas un événement. La question que les développeurs de Solidity devraient se poser est « est-ce que quelqu'un aurait intérêt à récupérer ou découvrir rapidement cette transaction ?

Indexer les bons paramètres d'événement

Cela nécessite un certain jugement subjectif. N'oubliez pas qu'un paramètre non indexé ne peut pas être recherché directement, mais peut néanmoins constituer une donnée utile lorsqu'il est accompagné d'un paramètre indexé. Un bon moyen d'avoir une intuition pour cela est de regarder comment les bases de code établies conçoivent leurs événements



En règle générale, les montants de crypto-monnaie ne doivent pas être indexés, et une adresse doit l'être, mais cette règle ne doit pas être appliquée aveuglément. Les index vous permettent uniquement d'obtenir rapidement des valeurs exactes, pas une plage de valeurs.

Éviter les événements redondants

Un exemple de cela serait d'ajouter un événement lorsque les jetons sont créés car les bibliothèques sous-jacentes émettent déjà cet événement.

Les événements ne peuvent pas être utilisés dans les fonctions d'affichage

Les événements changent d'état; ils modifient l'état de la blockchain en stockant le journal. Par conséquent, ils ne peuvent pas être utilisés dans des fonctions de vue (ou pures).


Les événements ne sont pas aussi utiles pour le débogage que console.log et print le sont dans d'autres langages ; comme les événements eux-mêmes changent d'état, ils ne sont pas émis si une transaction est annulée.


Combien d'arguments un événement peut-il prendre ?

Pour les arguments non indexés, il n'y a pas de limite intrinsèque au nombre d'arguments, bien qu'il y ait bien sûr des limites de taille de contrat et de gaz qui s'appliquent. L'exemple absurde suivant est une solidité valide :


 contract ExampleContract { event Numbers(uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256); }


De même, il n'y a pas de limite intrinsèque à la longueur des chaînes ou des tableaux stockés dans un journal.


Cependant, il ne peut pas y avoir plus de trois arguments indexés (sujets) dans un événement. Un événement anonyme peut avoir 4 arguments indexés (nous reviendrons sur cette distinction plus tard).

Un argument avec zéro événement est également valide.


Les noms de variable dans les événements sont facultatifs mais recommandés

Les événements suivants se comportent de manière identique

 event NewOwner(address newOwner); event NewOwner(address);


En général, inclure le nom de la variable serait idéal car la sémantique derrière l'exemple suivant est très ambiguë (ce n'est pas ainsi qu'il faut déclarer des événements !)


 event Trade(address,address,address,uint256,uint256);


On peut deviner que les adresses correspondent à l'expéditeur, et les adresses de jeton, tandis que les uint256es correspondent aux montants, mais c'est difficile à déchiffrer.


Il est classique de mettre une majuscule au nom d'un événement, mais le compilateur ne l'exige pas. Ce serait une bien meilleure déclaration :


 event Trade(address trader, address token1, address token2, uint256 token1Amount, uint256 token2Amount);


Les événements peuvent être hérités via des contrats parents et des interfaces

Lorsqu'un événement est déclaré dans un contrat parent, il peut être émis par le contrat fils. Les événements sont internes et ne peuvent pas être modifiés pour être privés ou publics.


Voici un exemple:

 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 } }


De même, les événements peuvent être déclarés dans une interface et utilisés dans l'enfant, comme dans l'exemple suivant.


 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 } }


Sélecteur d'événement

L'EVM (Ethereum Virtual Machine) identifie les événements avec le keccak256 de leur signature.

Pour les versions 0.8.15 ou ultérieures de Solidity, vous pouvez également récupérer le sélecteur à l'aide du membre .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)"); } }


Le sélecteur d'événement est en fait un sujet lui-même (nous en discuterons plus en détail dans une section ultérieure).

Marquer des variables comme indexées ou non ne change pas le sélecteur.


Événements anonymes

Les événements peuvent être marqués comme anonymes, auquel cas ils n'auront pas de sélecteur. Cela signifie que le code côté client ne peut pas les isoler spécifiquement en tant que sous-ensemble comme nos exemples précédents. Le seul moyen pour le code côté client de voir un événement anonyme est d'obtenir tous les événements du contrat intelligent.


 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)"); } }


Étant donné que la signature d'événement est utilisée comme l'un des index, une fonction anonyme peut avoir quatre rubriques indexées, puisque la signature de la fonction est « libérée » comme l'une des rubriques.


Un événement anonyme peut avoir jusqu'à quatre sujets indexés. Un événement non anonyme peut en avoir jusqu'à trois.


 contract ExampleContract { // valid event SomeEvent(uint256 indexed, uint256 indexed, address indexed, address indexed) anonymous; }


Les événements anonymes sont rarement utilisés dans la pratique.


Rubriques avancées sur les événements

Cette section décrit les événements au niveau de l'assemblage de l'EVM. Cette section peut être ignorée pour les programmeurs débutants dans le développement de la blockchain .


Détail de mise en œuvre : Filtres Bloom

Pour récupérer chaque transaction qui s'est produite avec un contrat intelligent, le client Ethereum devrait analyser chaque bloc, ce qui serait une opération d'E/S extrêmement lourde ; mais Ethereum utilise une optimisation importante.


Les événements sont stockés dans une structure de données Bloom Filter pour chaque bloc. Un filtre Bloom est un ensemble probabiliste qui répond efficacement si un membre est dans l'ensemble ou non. Au lieu de scanner l'intégralité du bloc, le client peut demander au filtre bloom si un événement a été émis dans le bloc. Cela permet au client de scanner la blockchain beaucoup plus rapidement pour trouver des événements.


Les filtres Bloom sont probabilistes : ils renvoient parfois à tort qu'un élément est membre de l'ensemble même s'il ne l'est pas. Plus il y a de membres stockés dans un filtre Bloom, plus le risque d'erreur est élevé et plus le filtre Bloom doit être grand (en termes de stockage) pour compenser cela. Pour cette raison, Ethereum ne stocke pas toutes les transactions dans un filtre Bloom. Il y a beaucoup moins d'événements que de transactions. Cela permet de maintenir la taille de stockage sur la blockchain gérable.


Lorsque le client reçoit une réponse d'adhésion positive d'un filtre de bloom, il doit analyser le bloc pour vérifier que l'événement a bien eu lieu. Cependant, cela ne se produira que pour un petit sous-ensemble de blocs, donc en moyenne, le client Ethereum économise beaucoup de calculs en vérifiant d'abord la présence d'événements dans le filtre bloom.


Événements Yul

Dans la représentation intermédiaire Yul, la distinction entre les arguments indexés (topics) et les arguments non indexés devient claire.


Les fonctions yul suivantes sont disponibles pour émettre des événements (et leur opcode EVM porte le même nom). Le tableau est copié de la documentation yul avec quelques simplifications.

code d'opération

Usage

log0(p, s)

log sans topic ni data mem[p…(p+s))

log1(p, s, t1)

log avec topic t1 et data mem[p…(p+s))

log2(p, s, t1, t2)

log avec les sujets t1, t2 et data mem[p…(p+s))

log3(p, s, t1, t2, t3)

log avec les sujets t1, t2, t3 et data mem[p…(p+s))

log4(p, s, t1, t2, t3, t4)

log avec les sujets t1, t2, t3, t4 et data mem[p…(p+s))


Un journal peut avoir jusqu'à 4 sujets, mais un événement de solidité non anonyme peut avoir jusqu'à 3 arguments indexés. En effet, le premier sujet est utilisé pour stocker la signature de l'événement. Il n'y a pas d'opcode ou de fonction Yul pour émettre plus de quatre sujets.


Les paramètres non indexés sont simplement codés abi dans la région de mémoire [p…(p+s)) et émis sous la forme d'une longue séquence d'octets.


Rappelez-vous plus tôt qu'il n'y avait en principe aucune limite quant au nombre d'arguments non indexés qu'un événement dans Solidity peut avoir. La raison sous-jacente est qu'il n'y a pas de limite explicite sur la durée de la région de mémoire pointée dans les deux premiers paramètres du code de l'opération de journalisation. Il y a bien sûr des limites fournies par la taille du contrat et les coûts du gaz d'extension de la mémoire.

Coût du gaz pour émettre un événement de solidité

Les événements sont nettement moins chers que l'écriture dans des variables de stockage. Les événements ne sont pas destinés à être accessibles par des contrats intelligents, de sorte que le manque relatif de frais généraux justifie un coût du gaz inférieur.

La formule du coût d'un événement en essence est la suivante ( source ) :


 375 + 375 * num_topics + 8 * data_size + mem_expansion cost


Chaque événement coûte au moins 375 essences. Un supplément de 375 est payé pour chaque paramètre indexé. Un événement non anonyme a le sélecteur d'événement comme paramètre indexé, de sorte que le coût est inclus la plupart du temps. Ensuite, nous payons 8 fois le nombre de mots de 32 octets écrits dans la chaîne. Étant donné que cette région est stockée en mémoire avant d'être émise, le coût d'extension de la mémoire doit également être pris en compte.


En général, l'intuition selon laquelle plus vous vous connectez, plus vous payez en essence, est exacte.

Conclusion

Les événements permettent aux clients de récupérer rapidement les transactions susceptibles de les intéresser. Bien qu'ils ne modifient pas la fonctionnalité des contrats intelligents, ils permettent au programmeur de spécifier les transactions qui doivent être rapidement récupérables. Cela permet aux dapps de résumer rapidement les informations importantes.


Les événements sont relativement bon marché en termes de gaz par rapport au stockage, mais le facteur le plus important de leur coût est le nombre de paramètres indexés, en supposant que le codeur n'utilise pas une quantité excessive de mémoire.

Apprendre encore plus

Vous aimez ce que vous voyez ici ? Consultez notre Solidity Bootcamp avancé pour en savoir plus.


L'image principale de cet article a été générée par le générateur d'images AI de HackerNoon via l'invite "Events in Solidity"