Protegiendo los Smarts Contracts

jueves, 15 de marzo de 2018

Ya hemos escrito en este blog, en distintas ocasiones, sobre temas como Blockchain, Bitcoins, Ethereum y Smart Contracts, e incluso hemos realizado un #11PathsTalks dedicado al tema. Son terminologías que todos los que estamos interesados en este sector conocemos. El concepto Smart Contracts seguramente sea el que menos hemos oído hablar, siendo un buen recurso a tener presente es el blog bit2me.

Los contratos inteligentes son, al fin y al cabo, programas que se ejecutan en el Blockchain. Sabiendo que una de las grandes bondades del Blockchain es su inmutabilidad (su fortaleza para que lo que se escriba no pueda ser alterado), no podemos dejar pasar una pregunta fundamental:

¿Qué sucedería si un programa que desarrollamos en Solidity para ejecutar en el Blockchain de Ethereum tuviera vulnerabilidades? Se supone que una vez puesto allí no puede ser modificado, ¿se puede hacer algo? O peor aún, ¿qué sucedería si alguna persona malintencionada detecta que nuestro Smart Contract posee vulnerabilidades y las aprovecha transfiriéndose Ethers o adueñándose de nuestro contrato?  ¿Se podrían volver atrás las transacciones?

La seguridad en los contratos inteligentes es algo a tomarse muy en serio en el momento en que comenzamos a pensar en nuestro proyecto.  Este tipo de tecnologías tienen la seguridad en el desarrollo de software como un tema crítico que no se puede dejar pasar por alto. Varias debilidades sobre cómo se escribe el código de un Smart Contract ya fueron analizadas anteriormente en otro post nuestro, hoy tenemos la intención de repasar algunos casos interesantes.

Tipos de datos
El siguiente contracto inteligente llamado “test_uint” nos permite analizar qué sucede por ejemplo con los tipos de datos y las implicancias en cuanto al manejo y las comprobaciones de los valores.

Ejemplo test_uint

Podemos ver que se ha seleccionado un tipo de datos entero sin signo de 256 bits. Pero, ¿qué sucede con los valores máximos y mínimos? La función set_minimo() disminuye en 1 el valor de la variable mínimo y la función set_maximo() aumenta en 1 el valor de la variable máximo. Si el valor de la variable mínimo es 0, cuando disminuimos en 1, automáticamente se realiza un desplazamiento y el valor pasa a ser el máximo disponible en el tipo de variable uint. Se produce el mismo caso cuando el valor de la variable máximo lo aumentamos en 1, la variable pasa a tener el valor 0. Ahora, imaginemos el caso en que se realizará alguna operación de agregar dinero a una cuenta, si no se controla correctamente los valores y si no se definen de forma adecuada los tipos de datos, podría darse el caso de que alguien llegue al valor máximo sólo enviando dinero a otra cuenta.

Visibilidad
Según las funciones hay cuatro tipo de definiciones:
  • Función definida como external: las funciones sólo podrán ser llamadas desde funciones de otros contractos externos, no podrán ser llamadas desde funciones dentro del mismo contracto.
  • Función definidas como public: pueden ser llamadas por cualquier función dentro del mismo contrato, por funciones que heredaron a partir de éste o usuarios externos.
  • Función definida como private: podrán ser llamadas desde el mismo contrato. 
  • Función definida como internal: podrán ser llamadas como contratos heredados. Ciertas funciones, por una definición errónea de su visibilidad, puede que no sean invocadas en ciertos momentos y así no permitir el funcionamiento adecuado de nuestro contrato, impactando en la disponibilidad del mismo.

Delegatecall / Call / Callcode
Analicemos el siguiente ejemplo propuesto por Zeppelin sobre las función delegatecall. Tiene como propósito usar el código de librería que está almacenado en otro contrato.

Ejemplo Delegate Call

En este ejemplo, el atacante podría quedarse con la propiedad de este contrato, debido a que, en el momento de la llamada de la función delegada, se carga el código de otro contrato dentro del entorno del actual. Es por ello, que en la documentación misma de Solidity se recomienda que las tres funciones (call, delegatecall y callcode) se usen. Estas son funciones de muy bajo nivel y deben usarse sólo como medida de último recurso ya que pueden afectar la seguridad del programa.

Aquí os dejo otro pequeño código para verificar lo comentado delegatecall

Recomendaciones
Para poder alcanzar un nivel de seguridad adecuado en nuestros contratos inteligentes, deberíamos incluir la seguridad dentro de todo el ciclo de vida del desarrollo de estos proyectos. Si no es incluida desde un principio, es muy probable que luego nos olvidemos de añadir controles necesarios. Tal y como hemos vivido, olvidarse algo aquí es muy crítico, ya que no tenemos luego posibilidad de cambiar el contrato. Tendríamos entonces que finalizar el contrato actual y publicar uno nuevo, con todo lo que eso implica.

Desde el punto de vista técnico, más detalladamente, Consensys ha desarrollado diferentes recomendaciones con los conocimientos mínimos que debería tener un programador de Solidity:
  • Llamadas externas: utilizarlas con mucha precaución
  • Marcar los contratos no confiables: cuando interactuamos con contratos externos hay que tener especial manejo en las interacciones con funciones, métodos, variables externas, y realizar los controles necesarios
  • No realizar cambios de estados después de realizar llamadas externas: debemos asumir que ante una llamada externa se podría estar ejecutando código malicioso que luego nos podría afectar en nuestro contrato
  • Realizar manejo de errores en las llamadas externas
  • Recordar que todo lo que está en Blockchain es público
  • No realizar devoluciones o otro tipo de operaciones después de llamadas externas, ya que podrían no terminar de ejecutarse
  • No asumir que los contratos son creados con balance cero

También, en la página de Solidity, encontraremos recomendaciones de seguridad que se deberían seguir:
  • Información privada y aleatoriedad: toda la información que pongamos en el Blockchain es pública, hay que tener especial cuidado con la implementación de funciones aleatorias
  • Reentradas: tener en cuenta que una función de un contrato podría volver a ser invocada por el mismo usuario antes de que la misma finalizara. Podría tener impactos graves, por ejemplo, en la transferencia de Ethers
  • Bucles y límites de gas: siempre que utilicemos bucles tenemos que poner límites a los mismos para que no consuma todo el gas
  • Aplicar controles en el envío y recepción de Ethers

Recuerda siempre mantener los contratos inteligentes escritos de forma simple y pequeños, pero sobre todo, realizarle pruebas y verificar su seguridad de manera periódica.

Fabián Chiera
Chiief Security Ambassador en Panamá

1 comentario:

  1. algún buen curso sobre BloackChain sobre desarrollo y seguridad en este tema que recomienden, Saludos.

    ResponderEliminar