Passer au contenu principal

Réduire la taille des contrats pour ne pas dépasser la limite

soliditycontrats intelligentsstockagetruffle
Intermédiaire
Markus Waas
soliditydeveloper.com(opens in a new tab)
26 juin 2020
7 minutes de lecture minute read

Pourquoi existe-t-il une limite ?

Le 22 novembre 2016,(opens in a new tab) la fourche Spurious Dragon a introduit EIP-170(opens in a new tab), qui a ajouté une limite de taille des contrats intelligents de 24,576 kb. Pour vous, en tant que développeur Solidity, cela signifie que lorsque vous ajoutez de plus en plus de fonctionnalités à votre contrat, à un moment donné, vous atteindrez la limite et, lors du déploiement, vous rencontrerez cette erreur :

Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on Mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries.

Cette limite a été apportée pour empêcher les attaques par déni de service (DOS). Tout appel vers un contrat est relativement peu coûteux en gaz. Cependant, l'impact d'un appel de contrat sur les nœuds Ethereum augmente de manière exponentielle en fonction de la taille du code du contrat appelé (lecture du code sur le disque, pré-traitement du code et ajout des données à la preuve de Merkle). Dans une situation où l'attaquant n'a besoin que de peu de ressources pour donner beaucoup de travail aux autres nœuds, il y a un risque d'attaques DOS.

À l'origine, le problème était moins préoccupant, car la limite naturelle de la taille des contrats était la limite de gaz par bloc. Bien entendu, un contrat doit être déployé dans une transaction qui contient tout le bytecode du contrat. Si vous n'incluez ensuite que cette seule transaction dans un bloc, vous pourrez utiliser tout le gaz, mais il ne sera pas illimité. Depuis la mise à niveau de Londres, la limite de gaz de bloc a pu varier entre 15M et 30M unités selon la demande du réseau.

Relever le défi

Malheureusement, il n'existe pas de moyen simple d'obtenir la taille du bytecode de vos contrats. Pour vous aider, le plugin truffle-contract-size(opens in a new tab) est un excellent outil si vous utilisez Truffle.

  1. npm install truffle-contract-size
  2. Ajoutez le plugin au fichier truffle-config.js : plugins: ["truffle-contract-size"]
  3. Lancez truffle run contract-size

Cela vous aidera à comprendre comment vos changements affectent la taille totale des contrats.

Dans les prochaines lignes, nous examinerons certaines méthodes classées en fonction de leur impact potentiel. Considérez ça comme perdre du poids. La meilleure stratégie pour atteindre son poids cible (dans notre cas, 24 kb) est de se concentrer en premier lieu sur les méthodes à fort impact. Dans la plupart des cas, il suffit de revoir son régime alimentaire pour y parvenir, mais il faut parfois aller un peu plus loin. Ensuite, vous pouvez ajouter à cela un peu d'exercice (impact modéré) ou même des suppléments (impact faible).

Impact important

Séparez vos contrats

Cela devrait toujours être votre première approche. Comment peut-on séparer le contrat en plusieurs petits contrats ? Cela vous oblige généralement à mettre en place une bonne architecture pour vos contrats. Des contrats plus petits sont toujours à préférer du point de vue de la lisibilité du code. Pour fractionner vos contrats, demandez-vous :

  • Quelles sont les fonctions qui vont ensemble ? Chaque ensemble de fonctions peut faire l'objet d'un contrat distinct.
  • Quelles sont les fonctions qui ne requièrent pas de lire l'état du contrat ou seulement une partie spécifique de son état ?
  • Pouvez-vous séparer le stockage et les fonctionnalités ?

Bibliothèques

Une façon simple de séparer le code fonctionnel du stockage est d'utiliser une bibliothèque(opens in a new tab). Ne déclarez pas les fonctions de la bibliothèque comme internes, sinon elles seront ajoutées au contrat(opens in a new tab) directement, lors de la compilation. Mais si vous utilisez des fonctions publiques, celles-ci seront en fait dans un contrat de bibliothèque séparé. Pensez à utiliser « using for(opens in a new tab) » pour rendre l'utilisation des bibliothèques plus pratique.

Proxies

Une méthode plus avancée consiste à utiliser le système de proxy. Les bibliothèques utilisent DELEGATECALL en coulisse, qui exécute simplement la fonction d'un autre contrat avec l'état du contrat appelant. Consultez cette publication de blog(opens in a new tab) pour en savoir plus sur les systèmes de proxy. Cela vous offrira plus de fonctionnalités, comme permettre les mise à niveau, par exemple, mais ils ajoutent aussi beaucoup de complexité. Je ne les ajouterais pas uniquement pour réduire la taille des contrats, à moins que ce ne soit votre seule option pour une raison quelconque.

Impact modéré

Supprimez des fonctions

Cela devrait être évident. Les fonctions augmentent considérablement la taille d'un contrat.

  • Externe : Souvent, nous ajoutons beaucoup de fonctions « view », pour des raisons de commodité. Ce n'est pas vraiment un problème, jusqu'à ce que vous atteigniez la taille limite. Quand cela arrive, vous devriez vraiment penser à supprimer tous les éléments qui ne sont absolument pas essentiels.
  • Interne : Vous pouvez également supprimer les fonctions internes/privées et mettre le code dans la ligne, à condition qu'elles ne soient appelées qu'une fois.

Évitez les variables inutiles

Un simple changement comme celui-ci :

1function get(uint id) returns (address,address) {
2 MyStruct memory myStruct = myStructs[id];
3 return (myStruct.addr1, myStruct.addr2);
4}
Copier
1function get(uint id) returns (address,address) {
2 return (myStructs[id].addr1, myStructs[id].addr2);
3}
Copier

représente une différence de 0,28 kb. Il y a de fortes chances que vous puissiez trouver de nombreuses situations similaires dans vos contrats, et elles peuvent engendrer une augmentation significative du poids total.

Abrégez les messages d'erreur

Les longs messages d'annulation et, en particulier, les nombreux messages différents d'annulation peuvent alourdir le contrat. Utilisez plutôt des codes d'erreur courts et décodez-les dans votre contrat. Un long message peut ainsi devenir beaucoup plus court :

1require(msg.sender == owner, "Only the owner of this contract can call this function");
2
Copier
1require(msg.sender == owner, "OW1");
Copier

Utiliser des erreurs personnalisées au lieu de messages d'erreur

Des erreurs personnalisées ont été introduites dans Solidity 0.8.4(opens in a new tab). Ils sont un excellent moyen de réduire la taille de vos contrats, car ils sont encodés ABI en tant que sélecteurs (comme les fonctions).

1error Unauthorized();
2
3if (msg.sender != owner) {
4 revert Unauthorized();
5}
Copier

Envisagez une valeur d'exécution faible dans l'optimiseur

Vous pouvez également modifier les paramètres de l'optimiseur. La valeur par défaut à 200 signifie qu'il va essayer d'optimiser le bytecode comme si une fonction était appelée 200 fois. Si vous la définissez à 1, vous demandez simplement à l'optimiseur d'optimiser dans le cas où chaque fonction n'est exécutée qu'une seule fois. Une fonction optimisée pour une seule exécution signifie qu'elle est optimisée pour le déploiement lui-même. Sachez que cela augmente les coûts en gaz pour l'exécution des fonctions, donc vous ne voudrez peut-être pas le faire.

Impact faible

Évitez de passer des structures aux fonctions

Si vous utilisez ABIEncoderV2(opens in a new tab), il peut être utile de ne pas passer de structures à une fonction. Au lieu de passer le paramètre comme structure...

1function get(uint id) returns (address,address) {
2 return _get(myStruct);
3}
4
5function _get(MyStruct memory myStruct) private view returns(address,address) {
6 return (myStruct.addr1, myStruct.addr2);
7}
Copier
1function get(uint id) returns(address,address) {
2 return _get(myStructs[id].addr1, myStructs[id].addr2);
3}
4
5function _get(address addr1, address addr2) private view returns(address,address) {
6 return (addr1, addr2);
7}
Copier

... passez les paramètres requis directement. Dans l'exemple ci-dessus, nous avons économisé 0,1 kb de plus.

Spécifiez des visibilités appropriées pour vos fonctions et variables

  • Des fonctions ou des variables uniquement appelées de l'extérieur ? Déclarez-les external au lieu de public.
  • Des fonctions ou des variables uniquement appelées au sein du contrat ? Déclarez-les private ou internal au lieu de public.

Retirez les modificateurs

Les modificateurs, surtout lorsqu'ils sont utilisés de manière intensive, peuvent avoir un impact significatif sur la taille du contrat. Envisagez de les supprimer et d'utiliser plutôt des fonctions.

1modifier checkStuff() {}
2
3function doSomething() checkStuff {}
Copier
1function checkStuff() private {}
2
3function doSomething() { checkStuff(); }
Copier

Ces conseils devraient vous aider à réduire considérablement la taille du contrat. Encore une fois, je ne saurais trop insister sur le fait qu'il faut toujours privilégier le fractionnement des contrats, si possible, afin d'obtenir un impact maximal.

Dernière modification: @SuperDelphi(opens in a new tab), 15 août 2023

Ce tutoriel vous a été utile ?