Ayúdanos a actualizar esta página.

🌏

Disponemos de una nueva versión de esta página, pero solo está en inglés por ahora. Ayúdanos a traducir la última versión.

Esta página está incompleta. Si eres un experto en el tema, por favor edita esta página y esparce tu sabiduría.

Seguridad

Última edición: , Invalid DateTime
Edit page

Los contratos Inteligentes de Ethereum son extremadamente flexibles, capaces de contener grandes cantidades de tokens (A menudo más de $1B) y ejecutar una lógica inmutable basada en el código de contrato inteligente previamente desplegado. Aunque esto ha creado un ecosistema vibrante y creativo de contratos inteligentes sin confianza e interconectados, es también el ecosistema perfecto para atraer atacantes que buscan beneficios explotando las vulnerabilidades de los contratos inteligentes y los comportamientos inesperados en Ethereum. El código del contrato inteligente normalmente no se puede modificar para "poner parches" a los fallos de seguridad, por lo que los activos robados de contratos inteligentes son irrecuperables, y los activos robados son extremadamente difíciles de rastrear. La cantidad total de valor robado o perdido debido a problemas de contratos inteligentes asciende fácilmente a $1B USD. Algunos de los mayores debido a errores de codificación de contrato inteligente incluyen:

Requisitos previos

Esto cubrirá la seguridad de los contratos inteligentes, así que asegúrate de que estás familiarizado con los contratos inteligentes antes de abordar la seguridad.

Cómo escribir un código de contrato inteligente más seguro

Antes de lanzar cualquier código para la red principal, es importante tomar las precauciones suficientes para proteger cualquier recurso de valor que se confíe a su contrato inteligente. En este artículo abordaremos algunos ataques específicos, proporcionaremos recursos para aprender sobre más tipos de ataque y te informaremos acerca de algunas herramientas básicas y prácticas recomendadas para asegurarte de que tus contratos funcionen de forma correcta y segura.

Las auditorías no siempre son milagrosas

Hace algunos años, las herramientas para escribir, compilar, probar e implementar contratos inteligentes eran muy nuevas, lo que llevó a muchos proyectos a escribir código de Solidity de maneras imprecisas. A continuación, se lo mostraban a un auditor para que investigase el código y garantizase que funcionaría con el nivel de seguridad esperado. En 2020, los procesos de desarrollo y las herramientas que apoyan la redacción con Solidity son significativamente mejores. Aprovechar estas prácticas recomendadas no solo asegura que tu proyecto sea más fácil de gestionar, sino que es una parte vital de la seguridad de tu proyecto. Una auditoría al final de la escritura de tu contrato inteligente ya no basta como única consideración de seguridad. La seguridad comienza antes de escribir la primera línea de código del contrato inteligente, la seguridad comienza con el diseño y los procesos de desarrollo adecuados.

Proceso de desarrollo de contratos inteligentes

Como mínimo:

  • Todo el código almacenado en un sistema de control de versiones, como git
  • Todas las modificaciones de código hechas a través de solicitudes de pull
  • Todas las solicitudes de pull tienen, al menos, un revisor. Si tu proyecto es individual, plantéate encontrar a otro autor de proyecto individual para realizar revisiones de código.
  • Un solo comando compila, implementa y ejecuta un conjunto de pruebas con respecto a tu código mediante un entorno de desarrollo Ethereum (consulta: Truffle)
  • Has ejecutado tu código a través de herramientas de análisis de código básicas como Mythril y Slither, idealmente antes de que cada solicitud de pull se fusione, comparando diferencias en la salida
  • Solidity no emite ninguna advertencia del compilador
  • Tu código está bien documentado

Hay mucho más que decir sobre el proceso de desarrollo, pero estos puntos conforman un buen punto de partida. Para obtener más artículos y explicaciones detalladas, consulta la lista de verificación de calidad de proceso proporcionada por DeFiSafety. DefiSafety es un servicio público no oficial que publica reseñas de varias dapps de Etherum grandes y públicas. Parte del sistema de calificación de DeFiSafety incluye cómo se adhiere el proyecto a esta lista de verificación de calidad de proceso. Siguiendo estos procesos:

  • Producirás un código más seguro, mediante pruebas automatizadas reproducibles
  • Los clientes podrán revisar tu proyecto de forma más eficaz
  • Incorporación más fácil de nuevos desarrolladores
  • Permite a los desarrolladores iterar, probar y obtener comentarios sobre las modificaciones
  • Es menos probable que tu proyecto experimente regresiones

Ataques y vulnerabilidades

Ahora que estás escribiendo código de Solidity mediante un proceso de desarrollo eficiente, veamos algunas vulnerabilidades comunes de Solidity para ver qué puede fallar.

Re-entrancy

El ''Re-entrancy'' es uno de los mayores y más importantes problemas de seguridad a tener en cuenta al desarrollar contratos inteligentes. Mientras que la EVM no puede ejecutar varios contratos al mismo tiempo, un contrato que llama a un contrato diferente pausa el estado de ejecución y memoria del contrato de llamada hasta que la llamada regrese, en cuyo punto la ejecución continúa normalmente. Esta pausa y el consiguiente reinicio puede crear una vulnerabilidad conocida como "Re-entrancy".

Esta es una versión simple de un contrato que es vulnerable a la "Re-entrancy":

1// ESTE CONTRATO TIENE VULNERABILIDAD INTENCIONAL, NO COPIAR
2contract Victim {
3 mapping (address => uint256) public balances;
4
5 function deposit() external payable {
6 balances[msg.sender] += msg.value;
7 }
8
9 function withdraw() external {
10 uint256 amount = balances[msg.sender];
11 (bool success, ) = msg.sender.call.value(amount)("");
12 require(success);
13 balances[msg.sender] = 0;
14 }
15}
16
Mostrar todo
📋 Copiar

Para permitir a un usuario retirar ETH que ha almacenado previamente en el contrato, esta función

  1. Lee cuánto saldo tiene un usuario
  2. Envía la cantidad del saldo en ETH
  3. Reinicia su saldo a 0, para que no puedan retirar su saldo de nuevo.

Si se llama desde una cuenta normal (como tu propia cuenta Metamask), esta función, como se esperaba, msg.sender.call.value() simplemente envía su cuenta ETH. Sin embargo, los contratos inteligentes también pueden realizar llamadas. Si un contrato malicioso es el que llama a retiro (), msg.sender.call. alue() no sólo enviará una cantidad de ETH, sino que también llamará implícitamente al contrato para comenzar a ejecutar el código. Imaginemos este contracto malicioso:

1contract Attacker {
2 function beginAttack() external payable {
3 Victim(VICTIM_ADDRESS).deposit.value(1 ether)();
4 Victim(VICTIM_ADDRESS).withdraw();
5 }
6
7 function() external payable {
8 if (gasleft() > 40000) {
9 Victim(VICTIM_ADDRESS).withdraw();
10 }
11 }
12}
13
Mostrar todo
📋 Copiar

Al llamar a Attacker.beginAttack(), se iniciará un ciclo que se parecerá a lo siguiente:

10.) La EOA del atacante llama a Attacker.beginAttack() con 1 ETH
20.) Attacker.beginAttack() le deposita 1 ETH a la Víctima
3
4 1.) Atacante -> Victim.withdraw()
5 1.) La víctima lee el balanceOf[msg.sender]
6 1.) La víctima envía ETH al Atacante (lo que ejecuta la función predeterminada)
7 2.) Atacante -> Victim.withdraw()
8 2.) La Víctima lee el balanceOf[msg.sender]
9 2.) La Víctima envía ETH al Atacante (lo que ejecuta la función predeterminada)
10 3.) Atacante -> Victim.withdraw()
11 3.) La Víctima lee el balanceOf[msg.sender]
12 3.) La Víctima envía ETH al Atacante (lo que ejecuta la función predeterminada)
13 4.) El Atacante ya no tiene el combustible necesario, regresa sin llamar de nuevo
14 3.) balances[msg.sender] = 0;
15 2.) balances[msg.sender] = 0; (ya era 0)
16 1.) balances[msg.sender] = 0; (ya era 0)
17
Mostrar todo

Llamar al Attacker.beginAttack con 1 ETH hará que vuelva a entrar el ataque a la Víctima, extrayendo más ETH del proporcionado (tomado de los balances de otros usuarios, causando que el contrato de la Víctima sea sub-colateralizado)

Cómo lidiar con la reentrada (la forma incorrecta)

Uno podría considerar derrotar la reentrada simplemente impidiendo que cualquier contrato inteligente interactúe con tu código. Si buscas stackoverflow, encuentras el segmento de código con muchos votos positivos:

1function isContract(address addr) internal returns (bool) {
2 uint size;
3 assembly { size := extcodesize(addr) }
4 return size > 0;
5}
6
📋 Copiar

Parece tener sentido: Los contratos tienen código, si la persona que llama tiene algún código, no permite que deposite. Vamos a añadirlo:

1// ESTE CONTRATO TIENE VULNERABILIDAD INTENCIONAL, NO COPIAR
2contract ContractCheckVictim {
3 mapping (address => uint256) public balances;
4
5 function isContract(address addr) internal returns (bool) {
6 uint size;
7 assembly { size := extcodesize(addr) }
8 return size > 0;
9 }
10
11 function deposit() external payable {
12 require(!isContract(msg.sender)); // <- NEW LINE
13 balances[msg.sender] += msg.value;
14 }
15
16 function withdraw() external {
17 uint256 amount = balances[msg.sender];
18 (bool success, ) = msg.sender.call.value(amount)("");
19 require(success);
20 balances[msg.sender] = 0;
21 }
22}
23
Mostrar todo
📋 Copiar

Ahora, para poder depositar ETH, no necesitas tener un contrato inteligente en tu dirección. Sin embargo, esto se contradice fácilmente con el siguiente contrato de Atacante:

1contract ContractCheckAttacker {
2 constructor() public payable {
3 ContractCheckVictim(VICTIM_ADDRESS).deposit(1 ether); // <- Nueva línea
4 }
5
6 function beginAttack() external payable {
7 ContractCheckVictim(VICTIM_ADDRESS).withdraw();
8 }
9
10 function() external payable {
11 if (gasleft() > 40000) {
12 Victim(VICTIM_ADDRESS).withdraw();
13 }
14 }
15}
16
Mostrar todo
📋 Copiar

Mientras que el primer ataque fue un ataque a la lógica contractual, este es un ataque al comportamiento de distribución del contrato de Ethereum. Durante la construcción, un contrato aún no ha devuelto su código para ser implementado en su dirección, pero conserva el control completo de EVM DURANTE este proceso.

Es técnicamente posible evitar que los contratos inteligentes llamen a su código utilizando esta línea:

1require(tx.origin == msg.sender)
2
📋 Copiar

Sin embargo, esta todavía no es una buena solución. Uno de los aspectos más emocionantes de Ethereum es su composición, los contratos inteligentes se integran y construyen entre sí. Al usar la línea de arriba, estás limitando la utilidad de tu proyecto.

Cómo lidiar con la re-entrada (la forma correcta)

Simplemente cambiando el orden de la actualización de almacenamiento y llamada externa, prevenimos la condición de re-entrada que permitió el ataque. Pedir de nuevo el retiro, si es posible, no beneficiaría al atacante, ya que el almacenamiento de balances estará establecido en 0.

1contract NoLongerAVictim {
2 function withdraw() external {
3 uint256 amount = balances[msg.sender];
4 balances[msg.sender] = 0;
5 (bool success, ) = msg.sender.call.value(amount)("");
6 require(success);
7 }
8}
9
📋 Copiar

El código anterior sigue el patrón de diseño "Chequeo-Efectos-Interacciones", el cual ayuda a proteger contra re-entrada. Puedes leer más acerca de Chequeo-Efectos-Interacciones aquí

Cómo lidiar con la re-entrada (la forma incorrecta)

Cada vez que estás enviando ETH a una dirección no confiable, o interactuando con un contrato desconocido (tal como llamar a transferir() de una dirección de token provista por un usuario), te abres a ti mismo a la posibilidad de re-entrada. Al diseñar contratos que no envían ETH ni llaman contratos no confiables, previenes la posibilidad de que se produzca una re-entrada.

Más tipos de ataques

Los tipos de ataques anteriores cubren problemas de codificación de contrato inteligente (de re-entrada) y las peculiaridades de Ethereum (ejecutar códigos dentro de constructores de contratos, antes de que el código esté disponible en la dirección del contrato). Existen muchos más ataques a los que se debe prestar atención, por ejemplo:

  • Inicio de ejecución
  • ETH enviar rechazo
  • Desbordamiento/bajo flujo entero

Más información:

Herramientas de seguridad

Aunque no hay sustituto para entender los conceptos básicos de seguridad de Ethereum y comprometer a una empresa de auditoría profesional para revisar su código, hay muchas herramientas disponibles para ayudarte a diagnosticar los posibles problemas de tu código.

Seguridad de contratos inteligentes

Slither: **Entorno de trabajo de análisis estático de Solidity escrito en Python 3.**

MythX: **API de análisis de seguridad para contratos inteligentes de Ethereum.**

Mythril: Herramienta de análisis de seguridad para el bytecode de la EVM.

SmartContract.Codes: Motor de búsqueda para códigos fuente verificados de Solidity.

Manticore: Una interfaz de línea de comandos que utiliza una herramienta de ejecución simbólica en contratos inteligentes y binarios.

Securify: Escáner de seguridad para contratos inteligentes de Ethereum.

ERC20 Verifier: Una herramienta de verificación utilizada para comprobar si un contrato cumple con el estándar ERC20.

Verificación formal

Información sobre la verificación formal

Uso de herramientas

Dos de las herramientas más populares para el análisis de seguridad de contratos inteligentes son:

Ambas son herramientas útiles que analizan tu código e informan sobre problemas. Cada una tiene una versión alojada [commercial], pero también están disponibles de forma gratuita para ejecutarse localmente. El siguiente es un ejemplo rápido de cómo ejecutar Slither, que está disponible en una imagen Docker conveniente trailofbits/eth-security-toolbox. Necesitarás instalar Docker si aún no lo tienes instalado.

$ mkdir test-slither
$ curl https://gist.githubusercontent.com/epheph/460e6ff4f02c4ac582794a41e1f103bf/raw/9e761af793d4414c39370f063a46a3f71686b579/gistfile1.txt > bad-contract.sol
$ docker run -v `pwd`:/share -it --rm trailofbits/eth-security-toolbox
docker$ cd /share
docker$ solc-select 0.5.11
docker$ slither bad-contract.sol

Generará esta salida:

ethsec@1435b241ca60:/share$ slither bad-contract.sol
INFO:Detectors:
Reentrancy in Victim.withdraw() (bad-contract.sol#11-16):
External calls:
- (success) = msg.sender.call.value(amount)() (bad-contract.sol#13)
State variables written after the call(s):
- balances[msg.sender] = 0 (bad-contract.sol#15)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities
INFO:Detectors:
Low level call in Victim.withdraw() (bad-contract.sol#11-16):
- (success) = msg.sender.call.value(amount)() (bad-contract.sol#13)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#low-level-calls
INFO:Slither:bad-contract.sol analyzed (1 contracts with 46 detectors), 2 result(s) found
INFO:Slither:Use https://crytic.io/ to get access to additional detectors and Github integration
Mostrar todo

Slither ha identificado la re-entrada potencial aquí, mediante la identificación de las líneas clave donde el problema podría ocurrir y proporcionando un enlace con más información acerca del problema:

Referencia: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities

permitiéndote conocer rápidamente los posibles problemas de tu código. Al igual que todas las herramientas de pruebas automatizadas, Slither no es perfecta y peca de informar demasiado. Puede advertir sobre una posible reentrada, incluso cuando no existe una vulnerabilidad explotable. A menudo, revisar la DIFERENCIA en la salida de Slither entre los cambios de código es extremadamente esclarecedor, ya que contribuye a descubrir las vulnerabilidades que se introdujeron mucho antes sin tener que esperar hasta que el código de tu proyecto esté completo.

Más lectura

Guía de prácticas recomendadas de seguridad para contratos inteligentes

Estándar de verificación de seguridad de contrato inteligente (SCSVS)

¿Conoces algún recurso en la comunidad que te haya servido de ayuda? Edita esta página y añádelo.