El contrato atacante utiliza la funcion revert() para interrumpir el correcto funcionamiento de nuestro contrato.
Introducción
Importancia de la seguridad en el desarrollo de contratos
Buenas prácticas
Vulnerabilidades con variables globales
Identificación del usuario: problema con tx.origin
Dependencia con timestamp
Vulnerabilidades del almacenamiento
Overflow y underflow
Variables privadas
Problemas con llamadas externas
DelegateCall
Gas insuficiente en Solidity
Ataques con transferencias
Forzar envío de Ethers
Reentrancy simple
Reentrancy cruzado
Denegación de servicio
Denegación por reversión
Denegación por límite de gas
Despedida
Desafío
Continúa aprendiendo
You don't have access to this class
Keep learning! Join and start boosting your career
Denial-of-service or DoS attacks are well known in the IT world. They totally or partially interrupt a service such as a web server, a database server or some kind of network where information is exchanged.
Blockchain is no exception, smart contracts can be victims of this type of attacks. Contracts can be blocked in such a way that millions of dollars are lost and can never be recovered.
Let's see what this vulnerability is about and how to prevent it through a simple example:
We will use a smart contract from a game whose objective is to become the "King" by sending more ETH than the previous King. The dethroned King will regain his ETH by losing the throne. The vulnerability will cause the smart contract to become unusable forever and no one else can claim to be King.
NOTE: The example of this vulnerability was originally taken and adapted from Solidity by examples.
Let's start by analyzing the game contract that will be breached. Its function claimThrone()
is the one in charge of all the logic of it.
// SPDX-License-Identifier: MITpragma solidity ^0.8.13; contract KingOfEther { address public king; uint public balance; function claimThrone() external payable { require(msg.value > balance, "Need to pay more to become the king"); // We return ETH to the dethroned King(bool sent, ) = king.call{value: balance}(""); require(sent, "Failed to send Ether"); // We set the new Kingbalance = msg.value; king = msg.sender; } } }
In this part, the attacking contract. Its receive()
function is responsible for denying service forever from the other contract.
contract Attack { KingOfEther kingOfEther; constructor(KingOfEther _kingOfEther) { // Instantiate KingOfEther with its addresskingOfEther = KingOfEther(_kingOfEther); } // Function that will reverse the transaction whenever an attempt is made to return ETH to the contract.
// Making it so that no one else can claim the throne.receive() external payable { revert(); } // The attack originates when we send more ETH than the previous King and the Attack contract becomes the new one. function attack() public payable { kingOfEther.claimThrone{value: msg.value}(); } } }
KingOfEther
contract.claimThrone()
function.claimThrone()
function. Alice gets back 1 ETH and Bob is the new King.Attack
contract that receives by parameter the address of KingOfEther
.Attack.attack
with 3 ETH.TIP: I encourage you to deploy the contracts yourself in environments like Remix to explore the vulnerability and see that indeed the breached contract becomes unusable.
Attack
became the new King. No one else can claim the throne because, when the KingOfEther
contract tries to return its ETH to the attacking contract, Attack responds with a revert()
through its receive()
function.
The attack generates that only the attacking contract loses its ETH, since the dethroned King recovers them before setting the attacking contract as King.
Although, as we have said, "only the attacking contract loses its ETH", it may seem a meaningless attack, since it is caused only by wanting to harm a project and to make the contract unusable. Even so, there are scenarios where there are also economic losses of other users.
KingOfEther
expects a positive response by performing the call()
and returning the ETH to its respective owner. The revert()
of the attacker contract causes that in each call an error always occurs and a new King can never be set.
It should be noted that the vulnerability can be exposed not only through the receive()
function plus a revert()
. There could simply be no fallback()
function in the attacking contract and cause the same damage of rendering the breached contract unusable.
Look at the main KingOfEther
function:
function claimThrone() external payable { require(msg.value > balance, "You need to pay more to be the new King."); (bool sent, ) = king.call{value: balance}(""); require(sent, "The ETH submission has failed."); balance = msg.value; king = msg.sender; }
It takes care of both returning the ETH to its owner and setting the new King afterwards.
This vulnerability can be avoided by making a division of responsibility. On the one hand, the logic for determining the new King. On the other hand, the withdrawal of the ETH by the users who were dethroned.
contract KingOfEther { address public king; uint public balance; mapping(address => uint) public balances; function claimThrone() external payable { require(msg.value > balance, "You need to pay more to be the new King."); balances[king] += balance; balance = msg.value; king = msg.sender; } function withdraw() public { require(msg.sender != king, "The current King cannot withdraw his funds."); uint amount = balances[msg.sender]; balances[msg.sender] = 0; (bool sent, ) = msg.sender.call{value: amount}(""); require(sent, "Sending ETH has failed."); } } }
Managing the balances of each account is always best done with mapping
. The withdraw()
function will allow users to withdraw their money (except for the current King) and, even if some kind of denial occurs, they will be able to get their ETH back.
Each user is responsible for their ETH. The withdrawal of the same must be done by themselves and it will no longer be the smart contract itself that will be responsible for returning them to them.
In this way, the DoS attack will only affect the attacked contract itself, who will be the only one who cannot recover his ETH.
There are a lot of people out there looking to do harm; even if they also get hurt financially, they are looking to destroy a project or community that may be behind it.
Always look for best practices when writing code. As in all software development, there are recommended design patterns for writing code. The Single Responsibility Principle of the SOLID pattern also applies to Web3 development.
Contributed by: Kevin Fiorentino (Platzi Contributor).
Contributions 2
Questions 3
El contrato atacante utiliza la funcion revert() para interrumpir el correcto funcionamiento de nuestro contrato.
Un punto clave para entender cómo funciona y que no se hizo mención: La función receive() external payable
del contrato Atacante está estandarizada y será llamada cada vez que se envíe ETH al contrato. Cuando el contrato Atacante se convierte en el mejorPostor
, siempre se hará un llamado a receive()
y siempre devolverá error y el contrato se bloquea para siempre.
Pueden ver algo de documentación que encontré.
No deja de ser una completa perdida del dinero también para el Atacante, ya que nunca recuperará sus ETH. Es un ataque por maldad y generar daños a un proyecto.
Want to see more contributions, questions and answers from the community?