Romper la inmutabilidad de la cadena de bloques: cómo el modelo de proxy puede lograr actualizaciones de contratos inteligentes

El patrón de proxy permite que los contratos inteligentes actualicen su lógica mientras mantienen sus direcciones en cadena y valores de estado. La llamada al contrato de proxy ejecutará el código del contrato lógico mediante delegarCall para modificar el estado del contrato de proxy.

Este artículo proporcionará una descripción general de los tipos de contratos de proxy, los incidentes y recomendaciones de seguridad relacionados, y las mejores prácticas para usar contratos de proxy.

Introducción a contratos actualizables y modo proxy

Todos conocemos la función "no manipulable" de la cadena de bloques, y el código del contrato inteligente no se puede modificar después de implementarse en la cadena de bloques.

Entonces, cuando los desarrolladores desean actualizar el código del contrato para actualizaciones de lógica, correcciones de errores o actualizaciones de seguridad, deben implementar un nuevo contrato y se generará una nueva dirección de contrato.

Para resolver este problema, puede utilizar el modo proxy.

El modo proxy realiza la capacidad de actualización del contrato sin cambiar la dirección de implementación del contrato, que actualmente es el modo de actualización de contrato más común.

El modo proxy es un sistema de contrato actualizable, incluido el contrato de proxy y el contrato de implementación lógica.

El contrato de proxy maneja la interacción del usuario y el almacenamiento de datos y estado del contrato. La llamada del usuario al contrato de proxy ejecutará el código del contrato lógico a través de la llamada delegada (), cambiando así el estado del contrato de proxy. La actualización se realiza actualizando la dirección de contrato lógica registrada en la ranura de almacenamiento predeterminada del contrato de proxy.

Los tres modos de proxy más convencionales son proxy transparente, proxy UUPS y proxy Beacon.

Proxy transparente

En el modo de proxy transparente, la función de actualización se implementa en el contrato de proxy. El rol de administrador del contrato de proxy tiene la autoridad directa para operar el contrato de proxy para actualizar la dirección de implementación lógica correspondiente al proxy. Las personas que llaman sin privilegios de administrador delegarán sus llamadas al contrato de implementación.

Nota: El administrador del contrato de proxy no puede ser un rol clave en la implementación lógica del contrato, ni siquiera puede ser un usuario común, porque el administrador del proxy no puede interactuar con el contrato de implementación.

Apoderado UUPS

En el modo UUPS (Universal Upgradeable Proxy Standard), la función de actualización del contrato se implementa en el contrato lógico. Dado que el mecanismo de actualización se almacena en el contrato lógico, la versión actualizada puede eliminar la lógica relacionada con la actualización para prohibir futuras actualizaciones. En este modo, todas las llamadas al contrato de proxy se reenvían al contrato de implementación lógica.

Proxy de baliza

El modo de proxy Beacon permite que varios contratos de proxy compartan la misma implementación lógica haciendo referencia al contrato Beacon. El contrato Beacon proporciona la dirección del contrato de implementación lógica para el contrato de proxy llamado. Al actualizar a una nueva dirección de implementación lógica, solo es necesario actualizar la dirección registrada en el contrato Beacon.

Uso indebido de proxy e incidentes de seguridad

Los desarrolladores pueden utilizar contratos de modo proxy para implementar sistemas de contratos actualizables. Sin embargo, el modo proxy también tiene ciertos umbrales operativos y, si se usa incorrectamente, puede traer problemas de seguridad devastadores para el proyecto. Las siguientes secciones muestran incidentes relacionados con el uso indebido de proxy y los riesgos de centralización que plantean los proxies.

Divulgación de clave administrada por proxy

El administrador del proxy controla el mecanismo de actualización del modo de proxy transparente; si se filtra la clave privada del administrador, el atacante puede actualizar el contrato lógico y ejecutar su propia lógica maliciosa en el estado del proxy.

El 5 de marzo de 2021, PAID Network sufrió un ataque de "minting" causado por una gestión deficiente de la clave privada. La Red PAGADA fue explotada por un atacante que robó la clave privada del administrador del proxy y activó un mecanismo de actualización para cambiar el contrato lógico.

Después de la actualización, el atacante puede destruir el PAGADO del usuario y generar un lote de PAGADO para sí mismo, que puede venderse más tarde. No hay ninguna vulnerabilidad de seguridad en el código en sí, pero el atacante obtuvo la clave privada para actualizar el contrato del administrador.

** Implementación de proxy UUPS sin inicializar **

Para el modo de proxy UUPS, durante la inicialización del contrato de proxy, la persona que llama pasa los parámetros iniciales al contrato de proxy, y luego el contrato de proxy llama a la función initialize() en el contrato lógico para lograr la inicialización.

La función initialize() generalmente está protegida con el modificador "inicializador" para restringir que la función se llame solo una vez. Después de llamar a la función initialize(), desde la perspectiva del contrato de proxy, se inicializa el contrato lógico.

Sin embargo, desde la perspectiva del contrato lógico, el contrato lógico no se inicializa porque initialize() no se llama directamente en el contrato lógico. Dado que el contrato lógico en sí no está inicializado, cualquiera puede llamar a la función initialize() para inicializarlo, establecer la variable de estado en un valor malicioso y, potencialmente, hacerse cargo del contrato lógico.

El impacto de la adopción de un contrato lógico depende del código de contrato en el sistema. En el peor de los casos, un atacante puede actualizar el contrato lógico en el modo de proxy UUPS a un contrato malicioso y ejecutar una llamada de función de "autodestrucción", lo que puede hacer que todo el contrato de proxy se vuelva inútil, y los activos del contrato se perderán. ser permanentemente destruido perdido.

caso

① Parity Multisig Freeze: el contrato lógico no se inicializa. El atacante desencadena la inicialización de muchas billeteras y bloquea ether en el contrato llamando a selfdestruct().

② Harvest Finance, Teller, KeeperDAO y Rivermen utilizan contratos lógicos no inicializados, lo que permitirá a los atacantes establecer los parámetros de inicialización de los contratos de forma arbitraria y ejecutar selfdestruct() durante la llamada delegada() para destruir el contrato de proxy.

Conflicto de almacenamiento

En un sistema de contrato actualizable, el contrato de proxy no declara variables de estado, sino que utiliza ranuras de almacenamiento pseudoaleatorias para almacenar datos importantes.

Los contratos proxy almacenan los valores de las variables de estado del contrato lógico en relación con el lugar donde se declararon. Si el contrato de proxy declara sus propias variables de estado y tanto el proxy como el contrato lógico intentan utilizar la misma ranura de almacenamiento, se producirá un conflicto de almacenamiento.

El contrato de proxy proporcionado por la biblioteca OpenZeppelin no declara variables de estado en el contrato, pero según el estándar EIP 1967, guarda el valor que debe almacenarse (como la dirección de administración) en una ranura de almacenamiento específica para evitar conflictos.

caso

El 23 de julio de 2022, hora de Beijing, la plataforma de música descentralizada Audius fue pirateada. El incidente fue causado por la introducción de una nueva lógica en el contrato de proxy, lo que resultó en conflictos de almacenamiento.

El contrato de proxy declara una variable de estado de dirección proxyAdmin y su valor se leerá incorrectamente cuando se ejecute el código de contrato lógico.

El valor de proxyAdmin personalizado por la parte del proyecto se consideró erróneamente como el valor de initialized e initialized, por lo que el modificador del inicializador devolvió un resultado incorrecto, lo que permitió al atacante llamar a la función initialize() nuevamente y otorgarse la autoridad para administrar el contrato. Luego, los atacantes cambiaron los parámetros de votación y aprobaron su propuesta maliciosa para robar los activos de Audius.

Llamar delegar a llamar() en contrato lógico o contrato no confiable

Suponga que deleguecall() existe en un contrato lógico y el contrato no valida correctamente el destino de la llamada. En este caso, un atacante puede explotar esta función para ejecutar llamadas a contratos maliciosos que controla, para subvertir implementaciones lógicas o para ejecutar lógica personalizada.

De manera similar, si hay una función address.call() sin restricciones en el contrato lógico, una vez que el atacante proporcione de manera malintencionada los campos de dirección y datos, se puede usar como un contrato de proxy.

caso

Ataques Pickle Finance, Furucombo y dYdX.

En estos incidentes, el contrato vulnerable fue aprobado por el token del usuario, y hay una llamada ()/delegatecall () en el contrato proporcionado por el usuario para llamar a la dirección y los datos del contrato, el atacante podrá llamar al contrato de función transferFrom() para retirar los saldos de los usuarios. Durante el incidente de dYdX, dYdX realizó su propio ataque de sombrero blanco para proteger los fondos.

Mejores prácticas

generalmente

(1) Use el modo proxy solo cuando sea necesario

No es necesario actualizar todos los contratos. Como se muestra arriba, hay muchos riesgos involucrados en el uso del patrón proxy. La propiedad "actualizable" también plantea problemas de confianza, ya que los administradores de proxy pueden actualizar los contratos sin el consentimiento de la comunidad. Recomendamos integrar el patrón de proxy en los proyectos solo cuando sea necesario.

(2) No modifique la biblioteca proxy

La biblioteca de contratos de proxy es compleja, especialmente la parte que se ocupa de la gestión del almacenamiento y los mecanismos de actualización. Cualquier error en la modificación afectará el trabajo de los contratos proxy y lógicos. Una gran cantidad de errores de alta gravedad relacionados con el agente que encontramos durante nuestras auditorías se debieron a modificaciones incorrectas de la biblioteca del agente. El incidente de Audius es un excelente ejemplo de las consecuencias de la modificación indebida de los contratos de agencia.

Puntos clave de la operación y gestión de contratos de agencia

(1) Inicializar el contrato lógico

Un atacante puede apoderarse de un contrato lógico no inicializado y potencialmente comprometer el sistema de contrato de proxy. Entonces, inicialice el contrato lógico después de la implementación, o use _disableInitializers() en el constructor del contrato lógico para deshabilitar automáticamente la inicialización.

(2) Garantizar la seguridad de la cuenta de gestión del agente

Un sistema de contrato actualizable generalmente requiere un rol privilegiado de "administrador de proxy" para administrar las actualizaciones de contrato. Si se filtra la clave de administración, el atacante puede actualizar libremente el contrato a un contrato malicioso, que puede robar los activos de los usuarios. Recomendamos una gestión cuidadosa de las claves privadas de las cuentas de administrador proxy para evitar cualquier riesgo potencial de piratería. Las billeteras de firmas múltiples se pueden usar para evitar fallas en la administración de claves de un solo punto.

(3) Use una cuenta separada para la administración de proxy transparente

La administración de proxy y el gobierno de la lógica deben ser direcciones separadas para evitar la pérdida de interacción con la implementación de la lógica. Si la gestión de proxy y el gobierno lógico se refieren a la misma dirección, no se reenvían llamadas para realizar funciones privilegiadas, lo que prohíbe cambios en las funciones de gobierno.

Relacionado con el almacenamiento del contrato de proxy

(1) Tenga cuidado al declarar variables de estado en contratos proxy

Como se explica en el truco de Audius, los contratos de proxy deben tener cuidado al declarar sus propias variables de estado. Las variables de estado declaradas de forma normal en los contratos de proxy pueden causar conflictos de datos al leer y escribir datos. Si el contrato de proxy requiere una variable de estado, guarde el valor en una ranura de almacenamiento como EIP1967 para evitar conflictos al ejecutar el código de contrato lógico.

(2) Mantener el orden de declaración variable y el tipo de contrato lógico

Cada versión del contrato lógico debe mantener el mismo orden y tipo de variables de estado, y es necesario agregar nuevas variables de estado al final de las variables existentes. De lo contrario, las llamadas delegadas pueden hacer que los contratos de proxy lean o sobrescriban valores almacenados incorrectos, y los datos antiguos pueden asociarse con variables declaradas recientemente, lo que puede causar problemas graves para las aplicaciones.

(3) Incluir brechas de almacenamiento en el contrato base

Los contratos lógicos deben incluir espacios de almacenamiento en el código del contrato para anticipar nuevas variables de estado al implementar nuevas implementaciones lógicas. Después de agregar una nueva variable de estado, el tamaño de la brecha debe actualizarse adecuadamente.

(4) No establezca el valor de la variable de estado en el constructor o proceso de declaración

La asignación de una variable de estado durante la declaración o en el constructor solo afecta el valor en el contrato lógico, no el contrato de proxy. Los parámetros no inmutables deben asignarse mediante la función initialize().

Contrato de herencia

(1) Los contratos actualizables solo pueden heredar de otros contratos actualizables

Los contratos actualizables tienen una estructura diferente a los contratos no actualizables. Por ejemplo, el constructor no es compatible con cambiar el estado del agente, usa la función initialize() para establecer variables de estado.

Cualquier contrato que herede de otro contrato necesita usar la función initialize() de su contrato heredado para asignar sus respectivas variables. Cuando utilice la biblioteca de OpenZeppelin o escriba su propio código, asegúrese de que los contratos actualizables solo puedan heredar otros contratos actualizables.

(2) No instanciar nuevos contratos en contratos lógicos

Los contratos creados e instanciados a través de Solidity no se podrán actualizar. Los contratos deben implementarse individualmente y pasar su dirección como parámetro al contrato de lógica actualizable para lograr un estado actualizable.

(3) Riesgo de inicialización del contrato matriz

Al inicializar el contrato principal, la función __{ContractName}_init inicializará su contrato principal. Múltiples llamadas a __{ContractName}_init pueden resultar en una segunda inicialización del contrato principal. Tenga en cuenta que __{ContractName}_init_unchained() solo inicializará los parámetros de {ContractName} y no llamará al inicializador de su contrato principal.

Sin embargo, esta no es una práctica recomendada, porque todos los contratos principales deben inicializarse y no inicializar los contratos requeridos causará problemas de ejecución en el futuro.

Implementación de contrato lógico

Evite autodestruirse() o selegatecall()/call() a contratos que no sean de confianza

Si hay autodestrucción() o delegar llamada() en el contrato, es posible que un atacante use estas funciones para romper la implementación lógica o ejecutar una lógica personalizada. Los desarrolladores deben validar la entrada del usuario y no permitir que los contratos ejecuten llamadas delegadas/llamadas a contratos que no son de confianza. Además, no se recomienda usar delegatecall() en contratos lógicos porque sería engorroso administrar el diseño de almacenamiento en la cadena de delegados de múltiples contratos.

Escrito al final

Los contratos de proxy eluden la naturaleza inmutable de las cadenas de bloques al permitir que los protocolos actualicen su lógica de código después de la implementación. Sin embargo, el desarrollo de contratos de representación aún debe ser muy cuidadoso, y una implementación incorrecta puede causar problemas de seguridad y lógica del proyecto.

En general, la mejor práctica es utilizar soluciones autorizadas y ampliamente probadas, ya que los modos Transparent, UUPS y Beacon Proxy tienen mecanismos de actualización comprobados para sus respectivos casos de uso. Además de esto, los roles privilegiados para escalar agentes también deben administrarse de forma segura para evitar que los atacantes alteren la lógica del agente.

El contrato de implementación lógica también debe tener cuidado de no usar delegar llamada(), lo que puede evitar que los atacantes ejecuten algún código malicioso, como autodestrucción().

Si bien seguir las mejores prácticas garantiza implementaciones de contratos de proxy estables y, al mismo tiempo, mantiene la flexibilidad actualizable, todo el código es propenso a nuevos problemas de seguridad o lógica que podrían poner en peligro el proyecto. Por lo tanto, es mejor que todo el código sea auditado por un equipo de expertos en seguridad con experiencia en auditoría y protección de protocolos de contrato de proxy.

Ver originales
El contenido es solo de referencia, no una solicitud u oferta. No se proporciona asesoramiento fiscal, legal ni de inversión. Consulte el Descargo de responsabilidad para obtener más información sobre los riesgos.
  • Recompensa
  • Comentar
  • Compartir
Comentar
0/400
Sin comentarios
  • Anclado
Comercie con criptomonedas en cualquier lugar y en cualquier momento
qrCode
Escanee para descargar la aplicación Gate.io
Comunidad
Español
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • ไทย
  • Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)