Si el emisor del contrato quiere tener una forma de actualizar el código del contrato, para que los datos de la cuenta y otras cosas se transfieran, ¿puede Ethereum proporcionar esto? ¿También se puede hacer esto sin cambiar la dirección del contrato o siempre es necesario implementar un nuevo contrato?
¿Existen mecanismos de "anexo" para agregar alguna funcionalidad nueva a un contrato sin una reescritura total?
Sí. Hay una serie de enfoques mediante los cuales puede actualizar un Contract1
a Contract2
, manteniendo su estado (datos y saldo) con la misma dirección que antes.
¿Como funciona esto? Una forma es usar un contrato de proxy con una fallback
función en la que cada llamada/trx de método se delegue al contrato de implementación (que contiene toda la lógica).
Una llamada de delegado es similar a una llamada regular, excepto que todo el código se ejecuta en el contexto de la persona que llama (proxy), no del destinatario (implementación). Debido a esto, una transferencia en el código del contrato de implementación transferirá el saldo del proxy, y cualquier lectura o escritura en el almacenamiento del contrato se leerá o escribirá desde el almacenamiento del proxy.
En este enfoque, los usuarios solo interactúan con el contrato de proxy y podemos cambiar el contrato de implementación manteniendo el mismo contrato de proxy.
La fallback
función se ejecutará en cualquier solicitud, redirigiendo la solicitud a la implementación y devolviendo el valor resultante (usando códigos de operación).
Esta fue una explicación básica que es suficiente para que podamos trabajar con contratos actualizables. En caso de que desee profundizar en el código de contrato de proxy y los diferentes patrones de proxy, consulte estas publicaciones.
¿Cómo puedo escribir contratos inteligentes actualizables?
OpenZeppelin proporciona increíbles herramientas CLI y bibliotecas JS que se encargan de todos los contratos complejos anteriores proxy
, vinculándolos al contrato de implementación (lógico) y administrando todos los contratos que implementa utilizando la CLI para la capacidad de actualización, lista para usar.
Lo único que debe hacer es escribir sus contratos y usar la CLI de OpenZeppelin o las bibliotecas para implementar los contratos.
NOTA: Hay algunas limitaciones que debe tener en cuenta, en términos de cómo debe escribir sus contratos y cómo debe actualizarlos. También hay una serie de soluciones alternativas a estas limitaciones en esta publicación .
Una vez que un contrato está en la cadena de bloques, es definitivo y no se puede cambiar. Ciertos parámetros, por supuesto, se pueden cambiar si se les permite cambiar a través del código original.
Un método para actualizar contratos es usar un sistema de control de versiones. Por ejemplo, podría tener un contrato de entrada que simplemente reenvíe todas las llamadas a la versión más reciente del contrato, según lo define un parámetro de dirección actualizable. También puede usar un registro de nombres y actualizarlo para que apunte a la versión de contrato más reciente.
Otro método es poner su código lógico en una biblioteca, luego usar la función CALLCODE, a través de bibliotecas en Solidity, para llamar al código ubicado en una dirección específica y actualizable. De esta manera, los datos de usuario persisten entre versiones. Esto tiene la limitación de que el ABI del contrato lógico debe permanecer igual en el tiempo.
Aquí hay una esencia antigua que usé para demostrar la segregación de datos/código hace un tiempo.
Edición de la hacienda:
Comenzando con el lanzamiento de Homestead, ahora hay un DELEGATECALL
código de operación. Esto le permite esencialmente reenviar llamadas a un contrato separado mientras mantiene msg.sender
y todo el almacenamiento.
Por ejemplo, podría tener un contrato que mantenga la misma dirección y almacenamiento, pero reenvíe todas las llamadas a una dirección almacenada en una variable:
contract Relay {
address public currentVersion;
address public owner;
function Relay(address initAddr){
currentVersion = initAddr;
owner = msg.sender;
}
function update(address newAddress){
if(msg.sender != owner) throw;
currentVersion = newAddress;
}
function(){
if(!currentVersion.delegatecall(msg.data)) throw;
}
}
contract DemoVersion1 { function checkVersion() returns (uint){ return 1; } }
y tengo que actualizar el contrato a la versión dos que contiene el siguiente código. contract DemoVersion2 { function checkVersion() returns (uint){ return 2; } }
¿Cómo puedo manejar llamar a los métodos? de contrato, ¿alguien puede explicar o señalar el ejemplo adecuado?Un método es usar un Sistema de Contratos como se describe a continuación:
Backend
;Frontend
usando Backend
;Register
y obtener la dirección;Backend
y registrar la dirección de Backend
en ya implementada Register
;Register
en la fuente de Backend
. Antes de cualquier llamada , debe llamar a su Backend
y obtener la dirección real de .Frontend
Register
Backend
Luego, puede actualizar su Backend
contrato en cualquier momento: simplemente implemente el nuevo y vuelva a registrarlo en el archivo Register
.
Llamando al contrato externo: solidity.readthedocs.org...
Vea también la discusión del foro: forum.ethereum.org...
UPD: la misma forma pero más eficiente (tal vez)
Primera implementación:
Register
que pueda implementar otros contratos con su propia dirección como argumento del constructor;Register
la dirección;
Register
la entrega de datos a su constructor: todos los demás contratos del paso 2.Mejora:
Register
;
Register
puede implementar otros contratos, dárselosRegister
.El código del contrato es inmutable, el almacenamiento es mutable, pero no puede ejecutar el código colocado en el almacenamiento, al menos por ahora.
Corrección de errores en los contratos
En cuanto a las correcciones de errores, el patrón común es tener contratos de proxy o de búsqueda para ser una puerta de entrada al real, que en caso de un cambio o corrección de errores sería reemplazado. Reemplazarlo también significa perder el antiguo contenido de almacenamiento.
mantenimiento de almacenamiento
Si desea tener la capacidad de actualizar el código y mantener el almacenamiento, podría pensar en separar el almacenamiento y la lógica. Tenga un contrato de almacenamiento dedicado, que acepte llamadas de escritura desde direcciones confiables (por ejemplo, los contratos lógicos). Todo el almacenamiento importante debe estar asociado con este.
Acceso al almacenamiento después de la autodestrucción
A día de hoy, no se ha implementado una poda real, incluso en el caso de la autodestrucción, pero eso definitivamente debería ocurrir en el futuro. Hay varios EIP que discuten esto.
Incluso si se implementa la poda, no debería ocurrir en un instante y debería poder leer el almacenamiento desde el último estado. También se planea tener nodos de archivo para mantener los estados indefinidamente; no estoy seguro de que sea factible sin limitaciones simplemente juzgando el crecimiento de la cadena de bloques.
Reimplementación en la misma dirección
En resumen: prácticamente esto no es posible. Las direcciones de contrato se calculan a partir del remitente y el nonce. El nonce es secuencial, no puede haber huecos y no puede haber duplicados.
En teoría, es posible llegar al mismo hash con una combinación diferente de dirección y nonce, pero la probabilidad es pequeña.
Los contratos implementados en una cadena de bloques son inmutables, por lo que esto significa:
Si los problemas del contrato quieren tener una forma de actualizar el código del contrato, para que los datos de la cuenta y otras cosas se transfieran, ¿qué medios proporciona Ethereum para esto?
Una forma simple de extender un contrato C1 es asegurarse de que C1 tenga funciones/accesorios que devuelvan todos los datos que tiene. Se puede escribir un nuevo contrato C2, que llama a las funciones C1 y hace lógica adicional o corregida. (Tenga en cuenta que si C1 y C2 tienen foo, donde foo de C1 tiene errores y foo de C2 está corregido, no hay forma de desactivar la llamada de foo de C1).
Se puede usar un registro, como se describe en la respuesta de @Alexander, para que otros DApps y contratos consulten el registro para la dirección de contractC, de modo que cuando C1 sea "reemplazado" por C2, no sea necesario cambiar el código DApp. El uso de un registro de esta manera evita codificar la dirección de C1 (para que C2, C3, C4 puedan ocupar su lugar cuando sea necesario), pero la DApp necesita codificar la dirección del registro.
EDITAR: El ENS, Ethereum Name Service, acaba de implementarse en la red de prueba (Ropsten).
Consulte la wiki de ENS para obtener un inicio rápido y otros detalles. Aquí hay una introducción:
ENS es el Servicio de nombres de Ethereum, un sistema de nombres extensible y distribuido basado en la cadena de bloques de Ethereum.
ENS se puede utilizar para resolver una amplia variedad de recursos. El estándar inicial para ENS define la resolución para las direcciones de Ethereum, pero el sistema es extensible por diseño, lo que permite resolver más tipos de recursos en el futuro sin que los componentes centrales de ENS requieran actualizaciones.
ENS se implementa en la red de prueba de Ropsten en 0x112234455c3a32fd11230c42e7bccd4a84e02010.
Discusión inicial aquí .
La respuesta más votada es utilizar delegatecall
y es muy complicado acertar.
https://blog.trailofbits.com/2018/09/05/contract-upgrade-anti-patterns describe algunos métodos de actualización, así como consideraciones críticas para que no introduzca más errores o un método de actualización con errores que no trabajo
Recomendaciones de patrones de proxy
Compruebe la existencia del contrato de destino antes de llamar a la llamada delegada. Solidity no realizará esta verificación en su nombre. Descuidar la verificación puede provocar un comportamiento no deseado y problemas de seguridad. Usted es responsable de estas comprobaciones si confía en una funcionalidad de bajo nivel.
Si está utilizando el patrón de proxy, debe:
Tener una comprensión detallada de las partes internas de Ethereum , incluida la mecánica precisa de la llamada de delegado y un conocimiento detallado de las partes internas de Solidity y EVM.
Considere cuidadosamente el orden de herencia , ya que afecta el diseño de la memoria.
Considere cuidadosamente el orden en que se declaran las variables. Por ejemplo, el sombreado de variables o incluso los cambios de tipo (como se indica a continuación) pueden afectar la intención del programador cuando interactúa con la llamada del delegado.
Tenga en cuenta que el compilador puede usar relleno y/o empaquetar variables juntas. Por ejemplo, si dos uint256 consecutivos se cambian a dos uint8, el compilador puede almacenar las dos variables en una ranura en lugar de dos.
Confirme que se respete el diseño de la memoria de las variables si se usa una versión diferente de solc o si se habilitan diferentes optimizaciones. Las diferentes versiones de solc calculan las compensaciones de almacenamiento de diferentes maneras. El orden de almacenamiento de las variables puede afectar los costos de gas, el diseño de la memoria y, por lo tanto, el resultado de la llamada delegada.
Considere cuidadosamente la inicialización del contrato. De acuerdo con la variante proxy, es posible que las variables de estado no se puedan inicializar durante la construcción. Como resultado, existe una posible condición de carrera durante la inicialización que debe mitigarse.
Considere cuidadosamente los nombres de las funciones en el proxy para evitar la colisión de nombres de funciones. En su lugar, se llamarán las funciones de proxy con el mismo hash de Keccak que la función prevista, lo que podría conducir a un comportamiento impredecible o malicioso.
delegatecall
usar ENS: ethereum.stackexchange.com/questions/77520/…@Nick Johnson tiene un contrato base para contratos actualizables.
Como él dice , antes de usar uno se debe "comprender completamente las limitaciones y los inconvenientes".
/**
* Base contract that all upgradeable contracts should use.
*
* Contracts implementing this interface are all called using delegatecall from
* a dispatcher. As a result, the _sizes and _dest variables are shared with the
* dispatcher contract, which allows the called contract to update these at will.
*
* _sizes is a map of function signatures to return value sizes. Due to EVM
* limitations, these need to be populated by the target contract, so the
* dispatcher knows how many bytes of data to return from called functions.
* Unfortunately, this makes variable-length return values impossible.
*
* _dest is the address of the contract currently implementing all the
* functionality of the composite contract. Contracts should update this by
* calling the internal function `replace`, which updates _dest and calls
* `initialize()` on the new contract.
*
* When upgrading a contract, restrictions on permissible changes to the set of
* storage variables must be observed. New variables may be added, but existing
* ones may not be deleted or replaced. Changing variable names is acceptable.
* Structs in arrays may not be modified, but structs in maps can be, following
* the same rules described above.
*/
contract Upgradeable {
mapping(bytes4=>uint32) _sizes;
address _dest;
/**
* This function is called using delegatecall from the dispatcher when the
* target contract is first initialized. It should use this opportunity to
* insert any return data sizes in _sizes, and perform any other upgrades
* necessary to change over from the old contract implementation (if any).
*
* Implementers of this function should either perform strictly harmless,
* idempotent operations like setting return sizes, or use some form of
* access control, to prevent outside callers.
*/
function initialize();
/**
* Performs a handover to a new implementing contract.
*/
function replace(address target) internal {
_dest = target;
target.delegatecall(bytes4(sha3("initialize()")));
}
}
/**
* The dispatcher is a minimal 'shim' that dispatches calls to a targeted
* contract. Calls are made using 'delegatecall', meaning all storage and value
* is kept on the dispatcher. As a result, when the target is updated, the new
* contract inherits all the stored data and value from the old contract.
*/
contract Dispatcher is Upgradeable {
function Dispatcher(address target) {
replace(target);
}
function initialize() {
// Should only be called by on target contracts, not on the dispatcher
throw;
}
function() {
bytes4 sig;
assembly { sig := calldataload(0) }
var len = _sizes[sig];
var target = _dest;
assembly {
// return _dest.delegatecall(msg.data)
calldatacopy(0x0, 0x0, calldatasize)
delegatecall(sub(gas, 10000), target, 0x0, calldatasize, 0, len)
return(0, len)
}
}
}
contract Example is Upgradeable {
uint _value;
function initialize() {
_sizes[bytes4(sha3("getUint()"))] = 32;
}
function getUint() returns (uint) {
return _value;
}
function setUint(uint value) {
_value = value;
}
}
_sizes[bytes4(sha3("getUint()"))] = 32
).Llegar a uno de los principios básicos en Ethereum que es un contrato inteligente no se puede modificar después de la implementación.
Esto tiene que ser planeado desde el principio. El punto clave es el número 4. Pero todos los demás son esenciales para tener una actualización de contrato inteligente real y sin problemas.
Por lo tanto, deberá diseñar su contrato inteligente teniendo en cuenta los siguientes 5 puntos:
Actualización de contratos rotos
Será necesario cambiar el código si se descubren errores o si es necesario realizar mejoras. No es bueno descubrir un error, pero no tener forma de lidiar con él.
...
Sin embargo, hay dos enfoques básicos que se utilizan con mayor frecuencia. El más simple de los dos es tener un contrato de registro que contenga la dirección de la última versión del contrato. Un enfoque más sencillo para los usuarios de contratos es tener un contrato que reenvíe llamadas y datos a la última versión del contrato.
Ejemplo 1: usar un contrato de registro para almacenar la última versión de un contrato
En este ejemplo, las llamadas no se reenvían, por lo que los usuarios deben buscar la dirección actual cada vez antes de interactuar con ella.
contract SomeRegister {
address backendContract;
address[] previousBackends;
address owner;
function SomeRegister() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner)
_;
}
function changeBackend(address newBackend) public
onlyOwner()
returns (bool)
{
if(newBackend != backendContract) {
previousBackends.push(backendContract);
backendContract = newBackend;
return true;
}
return false;
}
}
Hay dos desventajas principales en este enfoque:
Los usuarios siempre deben buscar la dirección actual, y cualquier persona que no lo haga corre el riesgo de utilizar una versión anterior del contrato.
Deberá pensar detenidamente sobre cómo tratar los datos del contrato cuando reemplace el contrato.
El enfoque alternativo es tener un contrato de reenvío de llamadas y datos a la última versión del contrato:
Ejemplo 2: Usar DELEGATECALL para desviar datos y llamadas
contract Relay {
address public currentVersion;
address public owner;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
function Relay(address initAddr) {
currentVersion = initAddr;
owner = msg.sender; // this owner may be another contract with multisig, not a single contract owner
}
function changeContract(address newVersion) public
onlyOwner()
{
currentVersion = newVersion;
}
function() {
require(currentVersion.delegatecall(msg.data));
}
}
Este enfoque evita los problemas anteriores pero tiene sus propios problemas. Debe tener mucho cuidado con la forma en que almacena los datos en este contrato. Si su nuevo contrato tiene un diseño de almacenamiento diferente al primero, sus datos pueden terminar corruptos. Además, esta versión simple del patrón no puede devolver valores de funciones, solo reenviarlos, lo que limita su aplicabilidad. ( Implementaciones más complejas intentan resolver esto con código ensamblador en línea y un registro de tamaños de retorno).
Independientemente de su enfoque, es importante tener alguna forma de actualizar sus contratos, o se volverán inutilizables cuando se descubran los errores inevitables en ellos.
Sin embargo, también recomiendo consultar las bibliotecas de proxy en Solidity publicadas por Zeppelin Solutions y Aragon. Hay una planificación para hacer un estándar de la industria para este asunto.
Creé una historia en Medium para esto con el título: Consideración de diseño esencial para Ethereum dApps (1): Contratos inteligentes actualizables y proporcioné una muestra para cada punto de los 5 anteriores.
Nosotros (mi equipo y yo) hemos trabajado recientemente en el problema de los contratos actualizables después de consultar la publicación de colony.io sobre los contratos actualizables . Entonces, encontramos una solución en la que tenemos diferentes capas de contrato en lugar de tener un solo contrato.
Si lo describo brevemente, entonces es necesario que la parte de almacenamiento sea muy genérica para que, una vez que la cree, pueda almacenar todo tipo de datos en ella (con la ayuda de los métodos setter) y acceder a ella (con la ayuda de los métodos getter) . Eso hace que su almacenamiento de datos sea eterno, que no necesita cambiar en el futuro.
Mire este contrato de almacén de datos para comprenderlo mejor: https://goo.gl/aLmvJ5
La segunda capa debe ser el contrato principal con sus funciones, que se puede actualizar en un momento posterior y, para usar el antiguo almacén de datos, debe hacer el contrato de manera que pueda apuntar su contrato recién implementado al existente (antiguo) almacén de datos y luego puede eliminar el contrato anterior, después de que el nuevo contrato se comunique correctamente con el almacén de datos anterior.
Mire nuestra base de código para comprender cómo hemos implementado el contrato actualizable: https://goo.gl/p5zGEv
Nota: en el repositorio de GitHub anterior, estamos usando tres capas de contratos debido a nuestro caso de uso. Sin embargo, es posible hacer que el contrato sea actualizable solo con dos capas.
Espero que esto ayude.
zos presentó un marco para que implementemos fácilmente un contrato inteligente actualizable
Le permite tener un contrato con una dirección estable, pero con un comportamiento totalmente controlable y actualizable.
https://github.com/u2/ether-enrutador
https://github.com/ConsenSys/smart-contract-best-practices#upgrading-broken-contracts
En Blend , usamos ZeppelinOS para hacer que nuestros contratos inteligentes regulares de Ethereum sean actualizables. Aquí está nuestra guía paso a paso y código de muestra .
El problema real en el contrato inteligente actualizable es migrar los valores almacenados del contrato.
Una manera mucho mejor de crear un contrato inteligente actualizable es diferenciar su almacenamiento y lógica en diferentes contratos.
Guarde todos los datos de su contrato en un contrato inteligente que solo acepta llamadas de su contrato lógico.
Siga cambiando las lógicas de su contrato lógico. Sin embargo, debe ser muy visionario al definir las variables del contrato de almacenamiento.
Mohamed Altabba
shamisén
A. Gupta