Gestión de errores en Solidity

Solidity

En este tutorial vamos a aprender cómo gestionar los errores que se pueden dar en tus funciones de Solidity, de forma que puedas minimizar los efectos negativos y revertir el estado del Smart Contract.

Consecuencias de los errores en Solidity

Los errores se pueden dar bien porque los lances controladamente bajo determinadas condiciones o bien porque estés intentado usar variables no declaradas o porque estés accediendo a un elemento de un array que no existe, entre otras muchas cosas.

Cuando ocurre un error, se frenará la ejecución de la función en el momento en el que se da el error, por lo que las siguientes instrucciones no se ejecutarán. Tal y como te estarás imaginando esto podría suponer un gran problema.

En caso de que por ejemplo se haya transferido cierta cantidad de ETH al contrato para luego realizar alguna operación, un error entre dichas operaciones puede suponer un gran quebradero de cabeza. Afortunadamente, Solidity revertirá el valor de las variables de estado que hayan sido modificadas en la función. En el siguiente ejemplo, la variable x revertirá su valor cuando se produzca algún error:

pragma solidity ^0.8.17;

contract MiContrato
{
    uint x;

    function ejemplo() external
    {
        x = 2;

        // Error
    }
}

El otro gran problema de los errores es el GAS consumido. Las operaciones en Solidity consumen GAS, por lo que, cuando ocurra un error, el GAS consumido se perderá irremediablemente, así que estarás perdiendo dinero. Sin embargo y pese a todo, seguramente quieras lanzar un error en muchos casos, como cuando un parámetro de una función no es del tipo deseado o se encuentra fuera de cierto rango de valores soportado.

Funciones de control de errores en Solidity

En Solidity existen diferentes funciones que te permitirán tanto lanzar errores controladamente como gestionar aquellos errores inesperados que se produzcan.

La sentencia throw

La sentencia throw se usaba hasta la versión 0.5 de Solidity, estando actualmente en desuso. De hecho era la única sentencia de gestión de errores que existía en Solidity hasta su versión 4.10.

Actualmente no debes usar la sentencia throw, aunque es interesante que conozcas su existencia en caso de que revises código creado con versiones anteriores de Solidity.

La sentencia revert

La sentencia revert permite lanzar un error controladamente especificando el motivo por el cual ha sucedido. El uso de la sentencia revert es útil cuando la condición a verificar es medianamente compleja.

En el siguiente ejemplo lanzamos un error usando la sentencia revert cuando la variable x es mayor que 10:

pragma solidity ^0.8.17;

contract MiContrato
{
    uint x;

    function testRevert(uint _x) pure external
    {
        if (_x > 10) {
            revert("El valor especificado debe ser menor o igual que 10");
        }
    }
}

La sentencia require

La sentencia require nos permitirá especificar la condición a comprobar en la propia sentencia. Acepta dos argumentos; el primero de ellos es la condición a comprobar, mientras que el segundo será el mensaje de error que se lanzará. Esta es la sentencia des control de errores que usarás con más frecuencia en Solidity.

En el siguiente ejemplo lanzamos un error usando la sentencia require cuando la variable x es mayor que 10:

pragma solidity ^0.8.17;

contract MiContrato
{
    uint x;

    function testRequire(uint _x) pure external
    {
        require(_x > 10, "El valor especificado debe ser menor o igual que 10");
    }
}

La sentencia assert

La sentencia assert te permitirá únicamente especificar la condición para la cual se lanzará un error, sin la posibilidad de devolver un mensaje.

Sin embargo, existe una diferencia semántica entre las sentencias assert y require. La función require está orientada a la gestión de aquellos errores esperados que ocurren normalmente en el ciclo de ejecución de tus funciones. Son errores que has tenido en cuenta que podrían suceder de antemano. Por otro lado, la sentencia assert está orientada a la gestión de aquellos errores inesperados, por lo que suelen indicar la existencia de un bug o la necesidad de refactorizar tu código para la correcta gestión de estos errores. Son errores que nunca deberían ocurrir.

En el siguiente ejemplo lanzamos un error usando la sentencia assert cuando la variable x es mayor que 10:

pragma solidity ^0.8.17;

contract MiContrato
{
    uint x;

    function testRequire(uint _x) pure external
    {
        assert(_x > 10);
    }
}

Crea un contrato con gestión de errores

Vamos a crear un contrato de ejemplo con Solidity en el que usaremos las sentencias require, revert y assert. El contrato se encargará de gestionar el balance de una cuenta, aceptando tanto depósitos como retiradas del balance de la cuenta:

pragma solidity ^0.8.13;

contract Cuenta {
    uint public balance;

    function ingresar(uint _cantidad) public
    {
        uint balanceAnterior = balance;
        uint nuevoBalance = balance + _cantidad;

        require(nuevoBalance >= balanceAnterior, "El nuevo balance debe ser superior al anterior");

        balance = nuevoBalance;

        assert(balance >= balanceAnterior);
    }

    function retirar(uint _cantidad) public
    {
        uint balanceAnterior = balance;

        require(balance >= _cantidad, "La cantidad a retirar debe ser menor o igual el balance");

        if (balance < _cantidad) {
            revert("La cantidad a retirar debe ser menor o igual el balance");
        }

        balance -= _cantidad;

        assert(balance <= balanceAnterior);
    }
}

En el método ingresar calculamos el futuro balance y lo guardamos en la variable nuevoBalance. El nuevo balance debe ser siempre superior al anterior, por lo que en caso de que esto no sea así, lanzamos un error mediante la sentencia require.

Al final de la función, usamos la sentencia assert tras incrementar el valor del balance del contrato para comprobar que el valor del balance sea superior al anterior. De este modo modo nos protegemos frente a posibles errores inesperados o vulnerabilidades que puedan provocar  que el balance de los usuarios se esfume.

En la función retirar, comprobamos primero la existencia de fondos suficientes en la cuenta. Para ello usamos la sentencia require para comprobar que el balance actual de la cuenta es mayor o igual a la cantidad a retirar. También hemos usado la sentencia revert a modo de ejemplo para garantizar dicha condición.

Tras reducir el balance del contrato, comprobamos la presencia de errores inesperados mediante la sentencia assert, comprobando que en efecto la cantidad a retirar se haya deducido del balance de la cuenta.

Comprueba errores en otro contrato

Para finalizar este tutorial, vamos a comprobar también la presencia de posibles errores en otros contratos. En el mudo real, interactuarás con otros contratos que también pueden fallar, por lo que es importante comprobar que funcionan adecuadamente.

Vamos a crear dos contratos, que será el ContratoA y el ContratoB. En el contrato ContratoB creamos la función ejemplo, que devolverá un error. En el contrato ContratoA creamos la función ejecutarEjemplo que llamará a la función ejemplo del contrato ContratoB:

pragma solidity ^0.8.17;

contract ContratoA
{
    function ejecutarEjemplo() external
    {
        ContratoB contratoB = new ContratoB();
        contratoB.ejemplo();
    }
}

contract ContratoB
{
    uint x;

    function ejemplo() pure external
    {
        revert("Algun error");
    }
}

Si compilas estos contratos en Remix y ejecutas la función ejecutarEjemplo del contrato ContratoA, verás que se muestra el error que hemos definido en la función ejemplo del contrato ContratoB. Los errores se propagan, por lo que el resultado es el mismo que en el caso de que el error se produjese en el contrato desde el que llamas a la función del otro contrato.

La transacción fallará en ambos contratos. La transacción fallará al completo, por lo que cualquier cambio en el estado de ambos contratos se revertirá al estado previo a al ejecución.

Existe un modo de recuperarte del error, aunque para ello tendrás que usar la sentencia call para ejecutar la función de otro contrato. La función call es una función de bajo nivel que permite llamar a otros Smart Contracts:

pragma solidity ^0.8.17;

contract ContratoA
{
    function ejecutarEjemplo() external
    {
        ContratoB contratoB = new ContratoB();
        address(contratoB).call(abi.encodePacked("ejemplo()"));
    }
}

contract ContratoB
{

    function ejemplo() pure external
    {
        revert("Algun error");
    }
}

Si compilas estos contratos con Remix y ejecutas la función ejecutarEjemplo, verás que no se muestra ningún error. Son embargo, has de saber que el uso de call no es recomendable, ya que es vulnerable a los ataques de reentrada,t ambién conocidos como ataques de reentrancy. La sentencia call debe evitarse en la medida de lo posible.

Esto ha sido todo.


Avatar de Edu Lazaro

Edu Lázaro: Ingeniero técnico en informática, actualmente trabajo como desarrollador web y programador de videojuegos.

👋 Hola! Soy Edu, me encanta crear cosas y he redactado esta guía. Si te ha resultado útil, el mayor favor que me podrías hacer es el de compatirla en Twitter 😊

Si quieres conocer mis proyectos, sígueme en Twitter.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

“- Hey, Doc. No tenemos suficiente carretera para ir a 140/h km. - ¿Carretera? A donde vamos, no necesitaremos carreteras.”