Ir al contenido principal

ERC-20 con mecanismos de seguridad

erc-20
Beginner
Ori Pomerantz
15 de agosto de 2022
9 minuto leído minute read

Introducción

Una de las cosas más positivas de Ethereum es que no hay una autoridad central que pueda modificar o deshacer sus transacciones. Y, sin embargo, una de las grandes trabas de Ethereum es que no hay una autoridad central con el poder de deshacer los errores del usuario o las transacciones ilícitas. En este artículo, descubrirá algunos de los errores comunes que los usuarios cometen con los tókenes ERC-20, al igual que cómo crear contratos ERC-20 que ayuden a los usuarios a evitar esos errores, o le otorguen algo de poder a una autoridad central (por ejemplo, para congelar cuentas).

Observe que aunque utilizaremos el contrato del token ERC-20 OpenZeppelin(opens in a new tab), este artículo no lo explica en gran detalle. Puede encontrar aquí esta información.

Si quieres ver el código fuente completo:

  1. Abre el IDE Remix(opens in a new tab).
  2. Haga click en el ícono github de clonar (clone github icon).
  3. Cone el repositorio de GitHub https://github.com/qbzzt/20220815-erc20-safety-rails.
  4. Abre contratos > erc20-safety-rails.sol.

Creando un contrato ERC-20

Antes de agregar la funcionalidad del riel de seguridad, necesitamos un contrato ERC-20. En este artículo usaremos el Asistente de Contratos de OpenZeppelin(opens in a new tab). Ábrelo en otra ventana del navegador y sigue estas instrucciones:

  1. Selecciona ERC-20.

  2. Ingresa estos ajustes:

    ParámetroValor
    NombreSafetyRailsToken
    SímboloSAFE
    Premint1000
    CaracterísticasNinguno
    Control de accesoOwnable
    UpgradabilityNinguno
  3. Desplácese hasta arriba y haga click en Open in Remix (Abrir en Remix, para Remix) o en Download (Descargar) para utilizar un entorno diferente. Doy por sentado que está usando Remix, si usa algo diferente, realice únicamente los cambios apropiados.

  4. Ahora tenemos un contrato ERC-20 totalmente funcional. Puedes expandir .deps > npm para ver el código importado.

  5. Compile, despliegue y familiarícese con el contrato para ver si funciona como un contrato ERC-20. Si necesitas aprender cómo utilizar Remix, usa este tutorial(opens in a new tab).

Errores comunes

Los errores

Los usuarios algunas veces envían tokens a la dirección incorrecta. Como no podemos leer sus mentes para saber lo que hacían, hay dos tipos de error que suceden mucho y son fácilmente detectables:

  1. Envianr los tókenes a la dirección propia del contrato. Por ejemplo, el token OP de Optimism(opens in a new tab) gestionado para acumular más de 120.000(opens in a new tab) tókenes OP en menos de dos meses. Esto representa una cantidad significativa de poder que, supuestamente, las personas perdieron.

  2. Enviar los tókenes a una dirección vacía, que no corresponde a una cuenta de propiedad externa o a un contrato inteligente. Como no tenemos las estadísticas de la frecuencia con la que esto sucede, un incidente podría haber costado 20.000.000 tókenes(opens in a new tab).

Evitar transferencias

El contrato ERC-20 de OpenZeppelin incluye un gancho _beforeTokenTransfer(opens in a new tab), que se invoca antes de transferir un token. Por defecto, este gancho no hace nada, pero podemos dotarle de nuestra propia funcionalidad, como los chequeos que revierten si hay algún problema.

Para usar este gancho, añada esta función antes de la constructora:

1 function _beforeTokenTransfer(address from, address to, uint256 amount)
2 internal virtual
3 override(ERC20)
4 {
5 super._beforeTokenTransfer(from, to, amount);
6 }
Copiar

Algunas partes de esta función pueden resultarle nuevas si no está muy familiarizado con Solidity:

1 internal virtual
Copiar

La palabra clave virtual significa que como hemos heredado funcionalidades de ERC-20 y anulado esta función, otros contratos pueden heredarla de nosotros y anular esta función.

1 override(ERC20)
Copiar

Debemos especificar de manera explícita que estamos anulando(opens in a new tab) la definición del token ERC20 de _beforeTokenTransfer. Por lo general, las definiciones explícitas son mucho mejores, desde una perspectiva de seguridad, que las implícitas. No podemos olvidar que hemos hecho algo si lo tenemos a la vista. Esta es también la razon por la que necesitamos especificar qué _beforeTokenTransfer de la superclase estamos anulando.

1 super._beforeTokenTransfer(from, to, amount);
Copiar

Esta línea llama la función de _beforeTokenTransfer del contrato o los contratos heredados que la tienen. En este caso, eso es solo ERC20, Ownable no tiene este gancho. Aunque actualmente ERC20._beforeTokenTransfer no hace nada, lo invocamos en caso de que se le añada alguna funcionalidad en el futuro (y así decidimos implementar nuevamente el contrato, porque los contratos no cambian una vez implementados).

Codificar los requisitos

Queremos añadir estos requisitos a la función:

  • La dirección to no puede ser igual a address(this), la dirección propia del contrato ERC-20.
  • La dirección to no puede estar vacía, esta debe ser:
    • Unas cuentas de propiedad externa (EOA). No podemos revisar si una dirección es una EOA directamente, pero podemos revisar el saldo de ETH de una dirección. Las EOAs casi siempre tienen un balance, incluso si ya no se encuentran en uso - es difícil vaciarlas hasta el último wei.
    • Un contrato inteligente. Probar si una dirección es un contrato inteligente es un poco complicado. Hay un código de operación que revisa la longitud externa del código, llamado EXTCODESIZE(opens in a new tab), pero no está disponible directamente en Solidity. Debemos usar Yul(opens in a new tab), que es un ensamblaje de EVM, para tal fin. Hay otros valores que podemos usar desde Solidity (<address>.code y <address>.codehash(opens in a new tab)), pero cuestan más.

Repasemos el nuevo código línea por línea:

1 require(to != address(this), "Can't send tokens to the contract address");
Copiar

Este es el primer requisito, revisa que to y this(address) no sean lo mismo.

1 bool isToContract;
2 assembly {
3 isToContract := gt(extcodesize(to), 0)
4 }
Copiar

Así es como revisamos si una dirección es un contrato. No podemos recibir salidas directamente de Yul, en vez de esto, definimos una variable para almacenar el resultado (isToContract en este caso). Según el funcionamiento de Yul, cada código de operación se considera una función. Por tanto, primero invocamos EXTCODESIZE(opens in a new tab) para obtener el tamaño del contrato y después GT(opens in a new tab) para revisar que no sea cero (estamos trabajando con números enteros sin firmar, por lo que no puede ser negativo). Luego escribimos el resultado en isToContract.

1 require(to.balance != 0 || isToContract, "Can't send tokens to an empty address");
Copiar

Finalmente, tenemos la revisión verdadera para direcciones vacías.

Acceso administrativo

Algunas veces es útil tener un administrador que puede deshacer los errores. Para reducir el potencial de abuso, este administrador puede ser una multifirma(opens in a new tab), por lo que varias personas deben estar de acuerdo con una acción. En este artículo tenemos dos características administrativas:

  1. Congelar y descongelar cuentas. Esto puede ser útil, por ejemplo, cuando una cuenta puede verse afectada.
  2. Limpieza de activos.

Los fraudes algunas veces envían tókenes fraudulentos al contrato de un token real para obtener la legitimidad. Por ejemplo, consulte aquí(opens in a new tab). El contrato ERC-20 legítimo es 0x4200....0042(opens in a new tab). El fraude que pretende ser legítimo es 0x234....bbe(opens in a new tab).

También puede que las personas envíen tókenes ERC-20 legítimos a nuestro contrato por error, lo cual es otra razón para querer tener una manera de eliminarlos.

OpenZeppelin proporciona dos mecanismos para activar el acceso administrativo:

Para simplificar la explicación, en este artículo utilizaremos Ownable.

Congelar y descongelar contratos

Congelar y descongelar contratos requiere varios cambios:

  • El mapeo(opens in a new tab) de direcciones a booleanos(opens in a new tab) para hacer un seguimiento de las direcciones que están congeladas. Todos los valores son inicialmente cero, el cual interpretan como falso los booleanos. Esto es precisamente lo que queremos; ya que, por defecto, las cuentas no están congeladas.

    1 mapping(address => bool) public frozenAccounts;
    Copiar
  • Eventos(opens in a new tab) para informar a cualquier interesado cuando una cuenta se congela o descongela. Desde un punto de vista técnico, no se requieren eventos para estas acciones, aunque le ayudan al código fuera de la cadena a ser capaz de escuchar estos eventos y saber lo que está ocurriendo. Se considera una buena práctica en contratos inteligentes, emitirlos cuando sucede algo que puede ser relevante para alguien más.

    Los eventos están indexados, por tanto, es posible buscar totas las veces que una cuenta se ha congelado o descongelado.

    1 // When accounts are frozen or unfrozen
    2 event AccountFrozen(address indexed _addr);
    3 event AccountThawed(address indexed _addr);
    Copiar
  • Funciones para el congelamiento y descongelamiento de cuentas. Al ser estas dos funciones son prácticamente idénticas, solo hablaremos de la función para congelar.

    1 function freezeAccount(address addr)
    2 public
    3 onlyOwner
    Copiar

    Las funciones marcadas como públicas(opens in a new tab) pueden activarse desde otros contratos inteligentes o directamente mediante una transacción.

    1 {
    2 require(!frozenAccounts[addr], "Account already frozen");
    3 frozenAccounts[addr] = true;
    4 emit AccountFrozen(addr);
    5 } // freezeAccount
    Copiar

    Si la cuenta ya está congelada, revierte. De lo contrario, congélela y emit un evento.

  • Cambie _beforeTokenTransfer para evitar que el dinero pase desde una cuenta congelada. Tenga en cuenta que el dinero todavía puede transferirse a la cuenta congelada.

    1 require(!frozenAccounts[from], "The account is frozen");
    Copiar

Limpieza de activos

Para publicar tókenes ERC-20 retenidos por este contrato, necesitamos activar una función en el contrato del token al que pertenece, siendo transfer(opens in a new tab) o approve(opens in a new tab). En este caso no tiene sentido el gasto de gas en asignaciones, también podemos transferir directamente.

1 function cleanupERC20(
2 address erc20,
3 address dest
4 )
5 public
6 onlyOwner
7 {
8 IERC20 token = IERC20(erc20);
Copiar

Esta es la sintaxis necesaria para crear un objeto para un contrato cuando recibimos la dirección. Podemos hacer esto porque tenemos la definición para tokens ERC20 como parte del código fuente (ver la línea 4) y ese archivo incluye la definición para IERC20(opens in a new tab), la interfaz para un contrato ERC20 de OpenZeppelin.

1 uint balance = token.balanceOf(address(this));
2 token.transfer(dest, balance);
3 }
Copiar

Se trata de una función de limpieza, por lo que supuestamente no queremos dejar ningún token. En lugar de obtener el saldo del usuario manualmente, también podríamos automatizar el proceso.

Conclusión

Esta no es una solución perfecta, ya que no existe una solución perfecta para un problema ocurrido cuando un usuario hace un fallo. Sin embargo, usar este tipo de comprobaciones puede al menos prevenir algunos errores. La capacidad de congelar cuentas, a pesar de ser peligrosa, puede utilizarse para limitar el daño de ciertos actos de piratería, negando al hacker los fondos robados.

Última edición: @Karym_GG(opens in a new tab), 23 de febrero de 2024

¿Le ha resultado útil este tutorial?