La función de selfdestruct(address)
elimina todo el código de bytes de la dirección del contrato y envía todo el éter almacenado a la dirección especificada. Si esta dirección especificada también es un contrato, no se llama a ninguna función (incluida la alternativa).
En otras palabras, un atacante puede crear un contrato con una función selfdestruct()
, enviarle ether, llamar a selfdestruct(target)
y forzar el envío de ether a un objetivo.
Veamos cómo puede verse este ataque. Creamos un contrato inteligente simple. Nota: Creé este contrato basado en Solidity by example .
// SPDX-License-Identifier: MIT pragma solidity ^0.8.10; contract EtherGame { uint public targetAmount = 5 ether; address public winner; function play() public payable { require(msg.value == 1 ether, "You can only send 1 Ether"); uint balance = address(this).balance; require(balance <= targetAmount, "Game is over"); if (balance == targetAmount) { winner = msg.sender; } } function claimReward() public { require(msg.sender == winner, "Not winner"); (bool sent, ) = msg.sender.call{value: address(this).balance}(""); require(sent, "Failed to send Ether"); } } contract Attack { EtherGame etherGame; constructor(EtherGame _etherGame) { etherGame = EtherGame(_etherGame); } function attack() public payable { address payable addr = payable(address(etherGame)); selfdestruct(addr); } }
Este contrato representa un juego simple en el que los jugadores envían 1 ether al contrato con la esperanza de ser el que alcance el umbral igual a 5 eth.
Cuando se alcanza el 5 º, el juego finaliza y el primer jugador que alcanza el hito puede reclamar una recompensa.
En este caso, un atacante puede, por ejemplo, enviar al contrato 5 eth o cualquier otro valor que empuje el saldo del contrato por encima del umbral. Esto bloquearía todas las recompensas en el contrato para siempre.
Esto se debe a que nuestra instrucción if en la función play()
verifica si el saldo del ganador es igual a 5 eth.
Esta vulnerabilidad surge del mal uso de this.balance
. Su contrato debe evitar depender de los valores exactos del saldo del contrato porque puede ser manipulado artificialmente.
Si se requieren valores exactos de éter depositado, se debe usar una variable autodefinida que se incremente en funciones pagaderas, para rastrear de manera segura el éter depositado. Esto puede evitar que su contrato se vea afectado por el éter forzado enviado a través de una llamada de selfdestruct()
.
Veamos cómo se ve la versión segura del contrato.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.10; contract EtherGame { uint public targetAmount = 5 ether; address public winner; uint public balance; function play() public payable { require(msg.value == 1 ether, "You can only send 1 Ether"); uint balance += msg.value; require(balance <= targetAmount, "Game is over"); if (balance == targetAmount) { winner = msg.sender; } } function claimReward() public { require(msg.sender == winner, "Not winner"); (bool sent, ) = msg.sender.call{value: address(this).balance}(""); require(sent, "Failed to send Ether"); } } contract Attack { EtherGame etherGame; constructor(EtherGame _etherGame) { etherGame = EtherGame(_etherGame); } function attack() public payable { address payable addr = payable(address(etherGame)); selfdestruct(addr); } }
Aquí, ya no tenemos ninguna referencia a this.balance
. En su lugar, hemos creado una nueva variable, balance
, que realiza un seguimiento de la cantidad actual de eth.
Publicado anteriormente aquí.