Если эмитент контракта хочет иметь способ обновить код контракта, чтобы данные учетной записи и другие данные переносились, может ли Ethereum предоставить это? Также можно ли это сделать без изменения адреса контракта или всегда нужно развертывать новый контракт?
Существуют ли механизмы «приложений» для добавления новых функций в контракт без его полной перезаписи?
Да. Существует ряд подходов, с помощью которых вы можете обновить Contract1
до Contract2
, сохраняя его состояние (данные и баланс) с тем же адресом, что и раньше.
Как это работает? Один из способов — использовать прокси-контракт с fallback
функцией, в которой каждый вызов метода/trx делегируется контракту реализации (который содержит всю логику).
Вызов делегата аналогичен обычному вызову, за исключением того, что весь код выполняется в контексте вызывающего объекта (прокси), а не вызываемого объекта (реализация). По этой причине передача в коде контракта реализации приведет к переносу баланса прокси-сервера, а любые операции чтения или записи в хранилище контрактов будут считываться или записываться из хранилища прокси-сервера.
При таком подходе пользователи взаимодействуют только с прокси-контрактом, и мы можем изменить контракт реализации, сохранив тот же прокси-контракт.
Функция fallback
будет выполняться при любом запросе, перенаправляя запрос в реализацию и возвращая результирующее значение (используя коды операций).
Это было базовое объяснение, которого нам достаточно для работы с обновляемыми контрактами. В случае, если вы хотите углубиться в код контракта прокси и различные шаблоны прокси, ознакомьтесь с этими сообщениями.
Как я могу написать обновляемые смарт-контракты?
OpenZeppelin предоставляет отличные инструменты CLI и JS-библиотеки , которые заботятся обо всех вышеперечисленных сложных proxy
контрактах, связывая их с контрактами реализации (логики) и управляя всеми контрактами, которые вы развертываете, используя CLI для возможности обновления, «из коробки».
Единственное, что вам нужно сделать, это написать свои контракты и использовать OpenZeppelin CLI или библиотеки для развертывания контрактов.
ПРИМЕЧАНИЕ. Есть несколько ограничений , о которых вам следует знать, в отношении того, как вам нужно писать свои контракты и как вы должны их обновлять. Также в этом посте есть ряд обходных путей этих ограничений .
Как только контракт находится в блокчейне, он становится окончательным и не может быть изменен. Некоторые параметры, конечно, можно изменить, если они разрешены для изменения через исходный код.
Одним из способов обновления контрактов является использование системы управления версиями. Например, у вас может быть входной контракт, который просто перенаправляет все вызовы на самую последнюю версию контракта, как определено обновляемым параметром адреса. Вы также можете использовать реестр имен и обновить его, чтобы указать на самую последнюю версию контракта.
Другой метод — поместить ваш логический код в библиотеку, а затем использовать функцию CALLCODE через библиотеки в Solidity для вызова кода, расположенного по указанному обновляемому адресу. Таким образом, пользовательские данные сохраняются между версиями. Это имеет ограничение, заключающееся в том, что ABI логического контракта должен оставаться неизменным с течением времени.
Вот старый принцип, который я использовал для демонстрации разделения данных/кода некоторое время назад.
Усадьба Править:
Начиная с выпуска Homestead, теперь есть DELEGATECALL
код операции. Это позволяет по сути переадресовывать звонки на отдельный контракт с сохранением msg.sender
и всего хранилища.
Например, у вас может быть контракт, который поддерживает тот же адрес и хранилище, но перенаправляет все вызовы на адрес, хранящийся в переменной:
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; } }
, и мне нужно обновить контракт до второй версии, которая содержит следующий код contract DemoVersion2 { function checkVersion() returns (uint){ return 2; } }
, как я могу обрабатывать вызов методов контракта, может ли кто-нибудь объяснить или указать на подходящий пример.Один из методов заключается в использовании системы контрактов, как описано ниже:
Backend
;Frontend
с использованием Backend
;Register
и получите его адрес;Backend
и зарегистрировать адрес Backend
уже развернутого Register
;Register
в источник Backend
. Перед любым звонком Backend
от Frontend
вас следует позвонить своему Register
и узнать реальный адрес Backend
.Затем вы можете обновить свой Backend
контракт в любое время — просто разверните новый и перерегистрируйте его в Register
.
Вызов внешнего контракта: solidity.readthedocs.org...
Также смотрите обсуждение на форуме: forum.ethereum.org...
UPD: Тот же, но более эффективный способ (возможно)
Первое развертывание:
Register
, который может развертывать другие контракты с самим адресом в качестве аргумента конструктора;Register
адрес ;
Register
, предоставив своему конструктору данные - все остальные контракты с шага 2.Обновление:
Register
;
Register
можете развернуть другие контракты - дайте ему этоRegister
.Код контракта неизменяем, хранилище изменяемо, но вы не можете выполнить код, помещенный в хранилище, по крайней мере, пока.
Исправления в контрактах
Что касается исправлений ошибок, общий шаблон заключается в том, чтобы иметь прокси-контракты или контракты поиска, которые будут шлюзом к реальному, который в случае изменения или исправления будет заменен. Его замена также означает потерю старого содержимого хранилища.
Хранение
Если вам нужна возможность обновлять код, сохраняя при этом хранилище, вы можете подумать о разделении хранилища и логики. Иметь выделенный контракт на хранение, который принимает вызовы записи с доверенных адресов (например, логические контракты). Все важные хранилища должны быть связаны с этим.
Доступ к хранилищу после самоуничтожения
На сегодняшний день реальная обрезка не реализована даже в случае самоуничтожения, но это определенно должно произойти в будущем. Это обсуждается в нескольких EIP.
Даже если обрезка реализована, это не должно происходить мгновенно, и вы должны иметь возможность читать хранилище из последнего состояния. Также планируется иметь архивные узлы для хранения состояний на неопределенный срок — не уверен, что это возможно без ограничений, просто судя по росту блокчейна.
Повторное развертывание по тому же адресу
Вкратце: практически это невозможно. Адреса контрактов вычисляются из отправителя и одноразового номера. Nonce является последовательным, не может быть пробелов и не может быть дубликатов.
Теоретически можно получить тот же хэш с другой комбинацией одноразового номера и адреса, но вероятность этого мала.
Контракты, развернутые в блокчейне, неизменны, поэтому это означает:
Если проблемы с контрактом хотят иметь способ обновить код контракта, чтобы данные учетной записи и другие вещи переносились, какие средства Ethereum предоставляет для этого?
Простой способ расширить контракт C1 — убедиться, что у C1 есть функции/аксессоры, которые возвращают все имеющиеся у него данные. Можно написать новый контракт C2, который вызывает функции C1 и выполняет дополнительную или исправленную логику. (Обратите внимание, что если у C1 и C2 есть foo, где foo C1 ошибочен, а foo C2 исправлен, нет способа отключить вызов C1 foo.)
Можно использовать реестр, как описано в ответе @Alexander, чтобы другие DApps и контракты запрашивали в реестре адрес contractC, чтобы при «замене» C1 на C2 код DApp не требовалось изменять. Использование реестра таким образом предотвращает жесткое кодирование адреса C1 (чтобы C2, C3, C4 могли занять его место при необходимости), но DApp необходимо жестко закодировать адрес реестра.
РЕДАКТИРОВАТЬ: ENS, служба имен Ethereum, была только что развернута в тестовой сети (Ropsten).
См. вики ENS для быстрого старта и других подробностей. Вот введение:
ENS — это служба имен Ethereum, распределенная, расширяемая система имен, основанная на блокчейне Ethereum.
ENS можно использовать для разрешения широкого круга ресурсов. Первоначальный стандарт для ENS определяет разрешение для адресов Ethereum, но система является расширяемой по своему дизайну, что позволяет в будущем разрешать больше типов ресурсов без необходимости обновления основных компонентов ENS.
ENS развернут в тестовой сети Ropsten по адресу 0x112234455c3a32fd11230c42e7bccd4a84e02010.
Первоначальное обсуждение здесь .
Ответ, получивший наибольшее количество голосов, - это использовать, delegatecall
и его очень сложно понять правильно.
https://blog.trailofbits.com/2018/09/05/contract-upgrade-anti-patterns описывает некоторые методы обновления, а также критические соображения, чтобы вы не вводили больше ошибок или ошибочный метод обновления, который не т работать.
Рекомендации по шаблону прокси
Перед вызовом delegatecall проверьте существование целевого контракта. Solidity не будет выполнять эту проверку от вашего имени. Игнорирование проверки может привести к непреднамеренному поведению и проблемам безопасности. Вы несете ответственность за эти проверки, если полагаетесь на низкоуровневую функциональность.
Если вы используете шаблон прокси, вы должны:
Иметь детальное представление о внутреннем устройстве Ethereum , включая точную механику вызова делегата и детальное знание внутреннего устройства Solidity и EVM.
Тщательно продумайте порядок наследования , так как он влияет на структуру памяти.
Внимательно рассмотрите порядок объявления переменных. Например, затенение переменных или даже изменение типа (как указано ниже) могут повлиять на намерения программиста при взаимодействии с делегатом.
Имейте в виду, что компилятор может использовать заполнение и/или пакетные переменные вместе. Например, если два последовательных uint256 заменены на два uint8, компилятор может хранить две переменные в одном слоте вместо двух.
Подтвердите, что структура памяти переменных соблюдается , если используется другая версия solc или если включены другие оптимизации. Различные версии solc вычисляют смещения хранилища по-разному. Порядок хранения переменных может повлиять на стоимость газа, структуру памяти и, следовательно, на результат вызова делегата.
Внимательно рассмотрите инициализацию контракта. Согласно варианту прокси, переменные состояния не могут быть инициализированы во время построения. В результате существует потенциальное состояние гонки во время инициализации, которое необходимо смягчить.
Тщательно продумайте имена функций в прокси , чтобы избежать конфликта имен функций. Вместо этого будут вызываться прокси-функции с тем же хэшем Keccak, что и предполагаемая функция, что может привести к непредсказуемому или злонамеренному поведению.
delegatecall
использовать ENS: ethereum.stackexchange.com/questions/77520/…У @Nick Johnson есть базовый контракт для обновляемых контрактов.
По его словам , перед использованием нужно «полностью понять ограничения и недостатки».
/**
* 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
).Приходя к одному из основных принципов Ethereum, который заключается в том, что смарт-контракт не может быть изменен после развертывания.
Это нужно планировать с самого начала. Ключевым моментом является номер 4. Но все остальные необходимы для реального и плавного обновления смарт-контракта.
Итак, вам нужно будет разработать свой смарт-контракт с учетом следующих 5 пунктов:
Обновление нарушенных контрактов
Код необходимо будет изменить, если будут обнаружены ошибки или если необходимо внести улучшения. Нехорошо обнаружить ошибку, но не иметь возможности с ней справиться
...
Тем не менее, есть два основных подхода, которые наиболее часто используются. Проще всего иметь контракт реестра, содержащий адрес последней версии контракта. Более простой подход для контрактных пользователей — иметь контракт, который перенаправляет вызовы и данные в последнюю версию контракта.
Пример 1. Использование контракта реестра для хранения последней версии контракта
В этом примере вызовы не переадресовываются, поэтому пользователи должны каждый раз получать текущий адрес, прежде чем взаимодействовать с ним.
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;
}
}
У этого подхода есть два основных недостатка:
Пользователи всегда должны искать текущий адрес, и любой, кто этого не делает, рискует использовать старую версию контракта.
Вам нужно будет тщательно продумать, как обращаться с данными контракта при замене контракта.
Альтернативный подход заключается в том, чтобы контракт перенаправлял вызовы и данные в последнюю версию контракта:
Пример 2: Используйте DELEGATECALL для переадресации данных и вызовов
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));
}
}
Этот подход позволяет избежать предыдущих проблем, но имеет свои собственные проблемы. Вы должны быть предельно осторожны с тем, как вы храните данные в этом контракте. Если ваш новый контракт имеет другую схему хранения, чем первый, ваши данные могут быть повреждены. Кроме того, эта простая версия шаблона не может возвращать значения из функций, а только пересылать их, что ограничивает ее применимость. ( Более сложные реализации пытаются решить эту проблему с помощью встроенного ассемблерного кода и реестра возвращаемых размеров.)
Независимо от вашего подхода, важно иметь какой-то способ обновить ваши контракты, иначе они станут непригодными для использования, когда в них будут обнаружены неизбежные ошибки.
Тем не менее, я также рекомендую проверить прокси-библиотеки в Solidity , опубликованные Zeppelin Solutions и Aragon. По этому вопросу планируется разработать отраслевой стандарт.
Для этого я создал статью на Medium под заголовком « Основные соображения по дизайну для Ethereum dApps (1): обновляемые смарт-контракты» и предоставил образец для каждого пункта из 5 вышеперечисленных.
Мы (я и моя команда) недавно работали над проблемой обновляемых контрактов после того, как сослались на сообщение colony.io об обновляемых контрактах . Итак, мы пришли к решению, в котором у нас есть разные уровни контракта, а не один контракт.
Если я кратко опишу это, то нужно сделать часть хранилища очень общей, чтобы после ее создания вы могли хранить в ней все типы данных (с помощью методов установки) и получать к ней доступ (с помощью методов получения) . Это делает ваше хранилище данных вечным, которое вам не нужно менять в будущем.
Посмотрите на этот контракт хранилища данных, чтобы лучше понять его - https://goo.gl/aLmvJ5
Второй уровень должен быть основным контрактом с вашими функциями, который можно обновить позже, и для использования старого хранилища данных вы должны сделать контракт таким образом, чтобы вы могли указать свой недавно развернутый контракт на существующий (старый) хранилище данных, а затем вы можете убить старый контракт, после того как новый контракт правильно взаимодействует со старым хранилищем данных.
Посмотрите нашу кодовую базу, чтобы понять, как мы реализовали обновляемый контракт — https://goo.gl/p5zGEv.
Примечание: в приведенном выше репозитории GitHub мы используем три уровня контрактов из-за нашего варианта использования. Однако можно сделать контракт обновляемым только с двумя слоями.
Надеюсь это поможет.
zos представила нам платформу для простой реализации обновляемого смарт-контракта.
Позволяет вам иметь контракт со стабильным адресом, но полностью контролируемым и обновляемым поведением.
https://github.com/u2/эфирный маршрутизатор
https://github.com/ConsenSys/smart-contract-best-practices#upgrading-broken-contracts
В Blend мы использовали ZeppelinOS, чтобы сделать наши обычные смарт-контракты Ethereum обновляемыми. Вот наше пошаговое руководство и пример кода .
Настоящая проблема в обновляемом смарт-контракте заключается в переносе сохраненных значений из контракта.
Гораздо лучший способ создать обновляемый смарт-контракт — дифференцировать хранилище и логику в разных контрактах.
Сохраните все данные вашего контракта в одном смарт-контракте, который принимает вызовы только из вашего логического контракта.
Продолжайте менять логику вашего логического контракта. Однако вам нужно быть очень дальновидным при определении переменных контракта хранения.
Мухаммад Альтабба
сямисэн
А. Гупта