Ir al contenido principal

ERC-20 con mecanismos de seguridad

erc-20
Principiante
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
    Premint1.000
    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:

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:

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: @omahs(opens in a new tab), 17 de febrero de 2024

¿Le ha resultado útil este tutorial?