ERC-20 con mecanismos de seguridad
Introducción
Una de las grandes ventajas de Ethereum es que no hay una autoridad central que pueda modificar o deshacer sus transacciones. Uno de los grandes problemas con Ethereum es que no hay una autoridad central con el poder de deshacer los errores de los usuarios o las transacciones ilícitas. En este artículo aprenderá sobre algunos de los errores comunes que los usuarios cometen con los tokens ERC-20, así como la forma de crear contratos ERC-20 que ayuden a los usuarios a evitar esos errores o que den a una autoridad central algo de poder (por ejemplo, para congelar cuentas).
Tenga en cuenta que aunque utilizaremos el contrato de token ERC-20 de OpenZeppelinopens in a new tab, este artículo no lo explica en gran detalle. Puede encontrar esta información aquí.
Si desea ver el código fuente completo:
- Abra el IDE de Remixopens in a new tab
- Haga clic en el icono para clonar de GitHub (
). - Clone el repositorio de GitHub
https://github.com/qbzzt/20220815-erc20-safety-rails. - Abra contracts > erc20-safety-rails.sol.
Creación de un contrato ERC-20
Antes de que podamos añadir la funcionalidad de mecanismo de seguridad, necesitamos un contrato ERC-20. En este artículo usaremos el Asistente de Contratos de OpenZeppelinopens in a new tab. Ábralo en otro navegador y siga estas instrucciones:
-
Seleccione ERC20.
-
Introduzca estos ajustes:
Parámetro Valor Nombre SafetyRailsToken Símbolo SAFE Premint 1000 Funciones Ninguno Control de acceso Ownable Upgradability Ninguno -
Desplácese hacia arriba y haga clic en Abrir en Remix (para Remix) o en Descargar para usar un entorno diferente. Voy a suponer que está usando Remix; si usa otra cosa, simplemente haga los cambios apropiados.
-
Ahora tenemos un contrato ERC-20 totalmente funcional. Puede expandir
.deps>npmpara ver el código importado. -
Compile, despliegue e interactúe con el contrato para ver que funciona como un contrato ERC-20. Si necesita aprender a usar Remix, use este tutorialopens in a new tab.
Errores comunes
Los errores
Los usuarios a veces envían tokens a la dirección equivocada. Aunque no podemos leerles la mente para saber qué querían hacer, hay dos tipos de errores que ocurren con frecuencia y son fáciles de detectar:
-
Enviar los tokens a la propia dirección del contrato. Por ejemplo, el token OP de Optimismopens in a new tab llegó a acumular más de 120 000opens in a new tab tokens OP en menos de dos meses. Esto representa una cantidad significativa de riqueza que la gente presumiblemente acaba de perder.
-
Enviar los tokens a una dirección vacía, una que no corresponde a una cuenta de propiedad externa o a un contrato inteligente. Aunque no tengo estadísticas sobre la frecuencia con que esto ocurre, un incidente podría haber costado 20 000 000 de tokensopens in a new tab.
Prevención de transferencias
El contrato ERC-20 de OpenZeppelin incluye un hook, _beforeTokenTransferopens in a new tab, que se llama antes de que se transfiera un token. Por defecto, este hook no hace nada, pero podemos añadirle nuestra propia funcionalidad, como comprobaciones que se revierten si hay un problema.
Para usar el hook, añada esta función después del constructor:
1 function _beforeTokenTransfer(address from, address to, uint256 amount)2 internal virtual3 override(ERC20)4 {5 super._beforeTokenTransfer(from, to, amount);6 }Algunas partes de esta función pueden ser nuevas para usted si no está muy familiarizado con Solidity:
1 internal virtualLa palabra clave virtual significa que, al igual que heredamos la funcionalidad de ERC20 y sobrescribimos esta función, otros contratos pueden heredar de nosotros y sobrescribir esta función.
1 override(ERC20)Tenemos que especificar explícitamente que estamos sobrescribiendoopens in a new tab la definición del token ERC20 de _beforeTokenTransfer. En general, las definiciones explícitas son mucho mejores, desde el punto de vista de la seguridad, que las implícitas: no puede olvidar que ha hecho algo si lo tiene justo delante. Esa es también la razón por la que necesitamos especificar qué _beforeTokenTransfer de la superclase estamos sobrescribiendo.
1 super._beforeTokenTransfer(from, to, amount);Esta línea llama a la función _beforeTokenTransfer del contrato o contratos de los que heredamos que la tienen. En este caso, solo es ERC20, ya que Ownable no tiene este hook. Aunque actualmente ERC20._beforeTokenTransfer no hace nada, la llamamos por si se añade funcionalidad en el futuro (y entonces decidimos volver a desplegar el contrato, porque los contratos no cambian después del despliegue).
Codificación de los requisitos
Queremos añadir estos requisitos a la función:
- La dirección
tono puede ser igual aaddress(this), la dirección del propio contrato ERC-20. - La dirección
tono puede estar vacía, tiene que ser una de estas dos:- Una cuenta de propiedad externa (EOA). No podemos comprobar directamente si una dirección es una EOA, pero podemos comprobar el saldo de ETH de una dirección. Las EOA casi siempre tienen saldo, incluso si ya no se usan; es difícil vaciarlas hasta el último wei.
- Un contrato inteligente. Comprobar si una dirección es un contrato inteligente es un poco más difícil. Hay un código de operación que comprueba la longitud del código externo, llamado
EXTCODESIZEopens in a new tab, pero no está disponible directamente en Solidity. Tenemos que usar Yulopens in a new tab, que es el ensamblador de la EVM, para ello. Hay otros valores que podríamos usar de Solidity (<address>.codey<address>.codehashopens in a new tab), pero cuestan más.
Repasemos el nuevo código línea por línea:
1 require(to != address(this), "No se pueden enviar tokens a la dirección del contrato");Este es el primer requisito: comprobar que to y this(address) no son lo mismo.
1 bool isToContract;2 assembly {3 isToContract := gt(extcodesize(to), 0)4 }Así es como comprobamos si una dirección es un contrato. No podemos recibir la salida directamente de Yul, así que en su lugar definimos una variable para guardar el resultado (isToContract en este caso). La forma en que funciona Yul es que cada código de operación se considera una función. Así que primero llamamos a EXTCODESIZEopens in a new tab para obtener el tamaño del contrato, y luego usamos GTopens in a new tab para comprobar que no es cero (estamos tratando con enteros sin signo, así que, por supuesto, no puede ser negativo). Luego escribimos el resultado en isToContract.
1 require(to.balance != 0 || isToContract, "No se pueden enviar tokens a una dirección vacía");Y finalmente, tenemos la comprobación real para las direcciones vacías.
Acceso administrativo
A veces es útil tener un administrador que pueda deshacer errores. Para reducir el potencial de abuso, este administrador puede ser una multifirmaopens in a new tab para que varias personas tengan que estar de acuerdo en una acción. En este artículo tendremos dos características administrativas:
-
Congelar y descongelar cuentas. Esto puede ser útil, por ejemplo, cuando una cuenta puede estar comprometida.
-
Limpieza de activos.
A veces los estafadores envían tokens fraudulentos al contrato del token real para ganar legitimidad. Por ejemplo, véase aquíopens in a new tab. El contrato ERC-20 legítimo es 0x4200....0042opens in a new tab. La estafa que pretende serlo es 0x234....bbeopens in a new tab.
También es posible que la gente envíe tokens ERC-20 legítimos a nuestro contrato por error, que es otra razón para querer tener una forma de sacarlos.
OpenZeppelin proporciona dos mecanismos para habilitar el acceso administrativo:
- Los contratos
Ownableopens in a new tab tienen un único propietario. Las funciones que tienen el modificadoropens in a new tabonlyOwnersolo pueden ser llamadas por ese propietario. Los propietarios pueden transferir la propiedad a otra persona o renunciar a ella por completo. Los derechos de todas las demás cuentas suelen ser idénticos. - Los contratos
AccessControlopens in a new tab tienen control de acceso basado en roles (RBAC)opens in a new tab.
Para simplificar, en este artículo usamos Ownable.
Congelar y descongelar contratos
Congelar y descongelar contratos requiere varios cambios:
-
Un mapeoopens in a new tab de direcciones a booleanosopens in a new tab para hacer un seguimiento de qué direcciones están congeladas. Todos los valores son inicialmente cero, que para los valores booleanos se interpreta como falso. Esto es lo que queremos porque, por defecto, las cuentas no están congeladas.
1 mapping(address => bool) public frozenAccounts; -
Eventosopens in a new tab para informar a cualquier persona interesada cuando una cuenta se congela o descongela. Técnicamente, no se requieren eventos para estas acciones, pero ayuda al código fuera de la cadena a poder escuchar estos eventos y saber qué está pasando. Se considera una buena práctica que un contrato inteligente los emita cuando sucede algo que podría ser relevante para otra persona.
Los eventos están indexados, por lo que será posible buscar todas las veces que una cuenta ha sido congelada o descongelada.
1 // Cuando las cuentas se congelan o descongelan2 event AccountFrozen(address indexed _addr);3 event AccountThawed(address indexed _addr); -
Funciones para congelar y descongelar cuentas. Estas dos funciones son casi idénticas, por lo que solo repasaremos la función de congelación.
1 function freezeAccount(address addr)2 public3 onlyOwnerLas funciones marcadas como
publicopens in a new tab pueden ser llamadas desde otros contratos inteligentes o directamente mediante una transacción.1 {2 require(!frozenAccounts[addr], "La cuenta ya está congelada");3 frozenAccounts[addr] = true;4 emit AccountFrozen(addr);5 } // freezeAccountSi la cuenta ya está congelada, se revierte. De lo contrario, la congela y
emite un evento. -
Cambie
_beforeTokenTransferpara evitar que se mueva dinero de una cuenta congelada. Tenga en cuenta que todavía se puede transferir dinero a la cuenta congelada.1 require(!frozenAccounts[from], "La cuenta está congelada");
Limpieza de activos
Para liberar los tokens ERC-20 que posee este contrato, debemos llamar a una función en el contrato del token al que pertenecen, ya sea transferopens in a new tab o approveopens in a new tab. No tiene sentido gastar gas en asignaciones en este caso, es mejor que transfiramos directamente.
1 function cleanupERC20(2 address erc20,3 address dest4 )5 public6 onlyOwner7 {8 IERC20 token = IERC20(erc20);Esta es la sintaxis para crear un objeto para un contrato cuando recibimos la dirección. Podemos hacer esto porque tenemos la definición para los tokens ERC20 como parte del código fuente (véase la línea 4), y ese archivo incluye la definición para IERC20opens in a new tab, la interfaz para un contrato ERC-20 de OpenZeppelin.
1 uint balance = token.balanceOf(address(this));2 token.transfer(dest, balance);3 }Esta es una función de limpieza, por lo que presumiblemente no queremos dejar ningún token. En lugar de obtener el saldo del usuario manualmente, también podemos automatizar el proceso.
Conclusión
Esta no es una solución perfecta; no hay una solución perfecta para el problema de «el usuario cometió un error». Sin embargo, usar este tipo de comprobaciones puede, al menos, evitar algunos errores. La capacidad de congelar cuentas, aunque es peligrosa, puede utilizarse para limitar el daño de ciertos hackeos al denegar al hacker los fondos robados.
Vea aquí más de mi trabajoopens in a new tab.
Última actualización de la página: 4 de septiembre de 2025