You don't have access to this class

Keep learning! Join and start boosting your career

Aprovecha el precio especial y haz tu profesión a prueba de IA

Antes: $249

Currency
$209
Suscríbete

Termina en:

0 Días
7 Hrs
40 Min
22 Seg

Denegación por reversión

12/15
Resources

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.

DoS in blockchain

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:

Explanation of the breached contract.

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.

Contracts involved

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

Procedure to breach the contract.

  1. Deploy the KingOfEther contract.
  2. Send 1 ETH with Alice via the claimThrone() function.
  3. Send 2 ETH with Bob through the claimThrone() function. Alice gets back 1 ETH and Bob is the new King.
  4. Deploy the Attack contract that receives by parameter the address of KingOfEther.
  5. Call Attack.attack with 3 ETH.
  6. The new King will be the attacking contract and no one else will be able to claim the reign.

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.

What happened here?

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.

Why did this happen?

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.

How can this be avoided?

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.

Conclusions

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

Sort by:

Want to see more contributions, questions and answers from the community?

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.