Смарт контракт ethereum habr

Смарт контракты Ethereum: что делать при ошибке в смартконтракте или техники миграции

При написании смартконтрактов важно помнить, что после загрузки в блокчейн, они уже не могут быть изменены, а следовательно, не могут быть внесены какие-либо улучшения или исправлены какие-то найденные ошибки! Все мы знаем, что ошибки есть в любой программе, а вернувшись к написанному пару месяцев назад коду мы всегда найдем, что там можно улучшить. Как же быть? Единственно возможный вариант – это загрузить новый контракт с исправленным кодом. Но как же быть, если на базе имеющегося контракта уже выпущены токены? На помощь нам приходит миграция! За последний год я попробовал много разных техник ее реализации, проанализировал применяемые в других крупных блокчейн проектах и что-то поизобретал сам. Подробности под катом.

Сразу оговорюсь, что в рамках данного поста я не буду приводить простыни готовых смартконтрактов, а буду лишь рассматривать и анализировать различные техники. Почти все они в том или ином виде были мной реализованы в контрактах для проектов, в которых мне доводилось участвовать, и многое можно взять с моего GitHub.

Миграция с ERC20-совместимого контракта

Начнем рассмотрение с самого простого и распространенного случая, когда исходный контракт, уже загруженный в блокчейн, не содержит каких-либо специальных механизмов для того, чтобы помочь нам с миграцией, т.е. по сути мы имеем обычный ERC20-совместимый контракт. Единственное, что мы полезного можем взять из исходного контракта, это балансы всех держателей токенов и общее количество выпущенных токенов для проверки, что мы никого не забыли при миграции.

К сожалению, интерфейс ERC20-совместимого контракта не позволяет узнать список всех держателей токенов, поэтому при миграции нам придется выяснить полный список держателей из какого-то другого источника, например, выгрузив его из etherscan.io. Пример контракта, на который осуществляется миграция, приведен в следующем листинге:

Конструктор контракта получает в качестве параметров адрес исходного ERC20-совместимого контракта, а также список держателей токенов, выгруженный вручную через etherscan.io. Следует обратить внимание, что в последней сроке конструктора проверяем, что количество токенов после миграции не изменилось, а следовательно, ни один держатель токенов не забыт. Необходимо учитывать, что такая миграция возможна лишь в том случае, если количество держателей токенов невелико и цикл по ним всем возможен в рамках одной транзакции (лимита газа, установленного в Ethereum для одной транзакции). Если все же количество держателей токенов не позволяет мигрировать за одну транзакцию, то эту функциональность придется вынести в отдельную функцию, которую можно будет вызвать необходимое количество раз, а контракт в этом случае будет выглядеть так:

В конструкторе данного контракта запоминается адрес исходного контракта, а также инициализируется поле owner, чтобы запомнить адрес владельца контракта, чтобы только он имел право вызывать функцию migrate(), вызвав которую несколько раз, мы можем мигрировать любое количество держателей токенов с исходного контракта.

Недостатки данного решения заключаются в следующем:

  1. На старом смартконтракте токены останутся у их владельцев, а на новом просто продублируются их балансы. Насколько это плохо, зависит от того, как составлен ваш Tokens sale agreement или любой другой документ, описывающий объем ваших обязательств перед держателями токенов вашего проекта, и не удвоятся ли ваши обязательства перед ними после создания «дубликата».
  2. На миграцию вы расходуете собственный газ, но это, вобщем-то, логично, т.к. делать миграцию придумали вы и в любом случае доставляете неудобства вашим пользователям, хоть оно и ограничивается тем, что им надо переписать в своих кошельках адрес смартконтракта со старого на новый.
  3. В процессе осуществления миграции, если она конечно не умещается в одну транзакцию, могут произойти трансферы токенов между адресами их владельцев, а следовательно, могут добавиться новые держатели и может измениться баланс существующих.
Читайте также:  Doge криптовалюта почему выросла

Но с этим, к сожалению, ничего поделать нельзя, а для более комфортной и удобной миграции следует предусматривать какие-то вспомогательные средства в исходном контракте.

Миграция между этапами краудсейла

В мире современных ICO довольно распространена практика, когда для различных этапов сбора средств делают отдельные контракты, мигрируя выданные токены на новые контракты нового этапа. Это, конечно, можно делать так, как мы рассмотрели выше, но, если мы точно знаем, что нам придется мигрировать, то почему бы сразу не упростить себе жизнь? Для этого достаточно ввести публичное поле

В это поле необходимо добавлять всех держателей токенов. Если контракт уже на ранних этапах сбора разрешает делать держателям перемещения токенов, т.е. реализует transfer(), необходимо позаботиться о том, чтобы массив обновлялся, например, как-то так

Теперь на стороне приемного контракта можно использовать аналогичную рассмотренной ранее технологию миграции, но теперь нет необходимости передавать массив в качестве параметра, достаточно обратиться к уже готовому массиву в исходном контракте. Также следует помнить, что размер массива может не позволить проитерировать его за одну транзакцию по причине ограничения газа на одну транзакцию, а следовательно, нужно предусмотреть функцию migrate(), которая будет получать два индекса – номера начального и конечного элементов массива для обработки в рамках данной транзакции.

Недостатки данного решения вобщем-то такие же как у предыдущего, разве что теперь нет необходимости делать выгрузку списка держателей токенов через etherscan.io.

Миграция со сжиганием исходных токенов

Все-таки раз мы говорим про миграцию, а не про дублирование токенов в новом смартконтракте, то необходимо озаботиться вопросом уничтожения (сжигания) токенов на исходном контракте при создании их копии на новом. Очевидно, что недопустимо оставлять в смартконтракте «дыру», которая позволила бы кому угодно, будь он даже владельцем смартконтракта, сжигать токены других держателей. Такой смартконтракт будет просто скамовым! Осуществлять такого рода манипуляции над своими токенами может только их держатель, а следовательно, и осуществлять миграцию должен сам держатель. Владелец смартконтракта в данном случае может только запустить эту миграцию (перевести смартконтракт в состояние миграции). Пример реализации такой миграции я встретил в проекте GOLEM (ссылка на их гитхаб в конце поста), затем реализовал ее в нескольких своих проектах.

В исходном контракте определим интерфейс MigrationAgent, который впоследствии должен быть реализован в контракте, на который осуществляется миграция.

В исходном контракте токена должна быть реализована следующая дополнительная функциональность:

Таким образом, владелец исходного смартконтракта должен вызвать setMigrationAgent(), передав ему в качестве параметра адрес смартконтракта, на который осуществляется миграция. После этого все держатели токенов исходного смартконтракта должны вызвать функцию migrate(), которая осуществит уничтожение их токенов в исходном смартконтракте и добавление в новом (путем вызова функции migrateFrom() нового контракта). Ну а новый контракт должен собственно содержать реализацию интерфейса MigrationAgent, например, так:

В этом решении прекрасно все! Кроме того, что пользователю надо вызвать функцию migrate(). Ситуация существенно осложняется тем, что вызов функций поддерживают лишь единицы кошельков и они, как правило, не являются самыми удобными. Поэтому, поверьте, если среди держателей ваших токенов есть не только криптогики, но и простые смертные люди, они вас просто проклянут, когда вы будете объяснять им, что надо установить какой-нибудь Mist, а затем вызвать какую-то функцию (слава Богу, хоть без параметров). Как же быть?

Читайте также:  Почему не подтверждается транзакция bitcoin

А можно поступить очень просто! Ведь любой пользователь криптовалюты, даже самый-самый начинающий, умеет хорошо делать одно – отправлять крипту со своего адреса на какой-то другой. Так пусть таким адресом будет адрес нашего смартконтракта, а его fallback функция в режиме «миграции» будет просто вызвать migrate(). Таким образом, держателю токенов для осуществления миграции будет достаточно перевести хотя бы 1 wei на адрес смартконтракта, находящегося в режиме «миграции», чтобы произошло чудо!

Заключение

Рассмотренные решения концептуально покрывают все возможные способы осуществления миграции токенов, хотя возможны вариации в конкретных реализациях. Отдельного внимания достоин подход «перегонного сосуда» (ссылка в конце поста). Независимо от используемого вами подхода к миграции, помните, что смартконтракт – это не просто программа, выполняемая внутри виртуальной машины Ethereum, а это некий отчужденный независимый договор, а любая миграция предполагает, что вы меняете условия этого договора. Уверены ли вы, что держатели токенов хотят поменять условия договора, который заключили, приобретая токены? Это на самом деле хороший вопрос. И существует очень правильная практика, «спрашивать» держателей токенов о том, хотят ли они «переехать» на новый контракт. Именно осуществление миграции через голосование я реализовал в смартконтракте своего проекта PROVER, с текстом контракта можно познакомиться на моем GitHub-е. Ну и конечно приглашаю присоединяться к ICO моего проекта PROVER.

Надеюсь, что все это кому-то полезно и нужно :).

Источник

Обновляемые смарт-контракты Ethereum

Почти перед каждым программистом, который пишет смарт-контракты Ethereum встают вопросы: «Что делать, если нужно будет расширить функционал контрактов? Как быть, если в контракте найдется баг, который повлечет за собой потерю средств? Что делать, если обнаружится уязвимость в компиляторе solidity (что бывало уже не раз)?» Ведь, контракты, которые мы загружаем в сеть, не могут быть изменены. Поначалу довольно сложно осознать: как это код нельзя обновить? Почему? Но в этом отчасти и сила смарт-контрактов Ethereum — пользователи, возможно, меньше бы стали доверять контрактам, которые можно менять.

Постараемся разобрать несколько подходов, которые все же позволяют менять смарт-контракты.

Эта статья рассчитана на тех, кто обладает хотя бы базовыми навыками программирования на языке solidity и понимает основные принципы работы сети Ethereum.

Разделить смарт-контракт на несколько связанных контрактов

При этом можно сохранить адреса активных в данный момент контрактов в storage какого-либо из контрактов. Нередко выделяют какой-то один контракт, который отвечает за хранение и изменение ссылок на части всей системы.

Как пример можно привести контракт распродажи токенов, в котором четко не прописаны правила вычисления количества токенов, которые нужно отправить на кошелек с которого пришли Этеры. Вычислением количества может заниматься отдельный контракт, который мы сможем подменять в случае надобности. Не будем долго останавливаться на этом варианте, потому что подобный подход часто используется не только в solidity.

Одним из главных минусов такого подхода является то, что никак нельзя изменить интерфейс какого-то контракта, который является внешним для всей системы. Нельзя добавить или удалить функцию.

Использовать delegatecall для проксирования вызова в другой контракт

В EIP-7 была предложена и реализована инструкция, которая позволяет вызывать код из другого контракта, но контекст вызова остается тем же самым, что и у текущего контракта. То есть, вызываемый контракт будет писать в storage вызывающего контракта, msg.sender и msg.value остаются такими же, как изначально.

Читайте также:  Etf тинькофф инвестиции комиссия

В сети можно найти несколько примеров реализации данного механизма. Все они включают использование solidity assembly. Без assembly невозможно добиться возвращения какого-либо значения из delegatecall.

Основная идея всех методов, которые используют delegatecall для проксирования — реализация fallback функции. В ней необходимо прочесть calldata и передать дальше через delegatecall.
Поближе посмотрим на несколько примеров реализации:

    Upgradeable хранит размеры возвращаемых значений в mapping.

Вот реализация fallback функции отсюда:

При этом размер возвращаемого значения (в байтах) хранится в mapping _sizes. Это поле в storage необходимо заполнять при обновлении контракта.

Минусом данного подхода является то, что размер возвращаемого значения жестко привязан к сигнатуре вызываемой функции, то есть вернуть строку произвольного размера или массив байтов не получится.

Кроме того, обращение к storage довольно дорого стоит. А в данном случае у нас будет аж два обращения к storage: когда мы обращаемся к полю _dest и когда мы обращаемся к полю _size.

EVM assembly tricks: всегда использовать размер ответа, равный 32 байтам.

Код очень похож на предыдущий пример, но всегда используется размер ответа, равный 32 байтам. Это довольно взвешенное решение. Во-первых, большинство типов в solidity умещаются именно в 32 байта, во-вторых, не обращаясь лишний раз к storage, мы экономим довольно приличное количество газа. Позже оценим, сколько примерно газа тратится в разных вариантах реализации
Использование новых инструкций resultdatasize и resultdatacopy
Эти инструкции появились в основной сети Ethereum только после последнего хардфорка (Byzantium — 17 октября 2017 года).

Инструкции позволяют получить размер ответа, который возвращен из call/delegatecall, а также скопировать сам ответ в память. То есть, мы получили возможность реализовать полноценный прокси для любых размеров returndata.

Вот итоговый assembly код:

Рассмотрим вопрос использования газа. Проведенное тестировние показывает, что все 3 приведенных метода увеличивают использование газа на значение от 1000 до 1500. Много это или мало? Это примерно 2% от более-менее средней стоимости транзакции, которая будет изменять storage.

Сложности в использовании

К сожалению, использование данных методик ограничено. Во-первых, чтобы такое обновление контрактов работало, нельзя менять структуру хранения данных в контракте (нельзя переставлять местами поля, удалять поля). В новые версии контракта поля можно добавлять.

Также необходимо очень аккуратно разграничивать доступ к функции, которая меняет адрес активного контракта.

Немаловажным фактом является то, что доверие пользователей к контракту будет меньше, чем к такому же неизменяемому. С другой стороны можно предусмотреть тестовый период времени, в течение которого новую версию контракта можно откатить, а после которого версия контракта будет зафиксирована и меняться больше не сможет.

Примеры реализации обновлений

Несколько контрактов, которые помогут сделать обновление проще и надежнее.
Upgradeable — в этом контракте реализована проверка на то, что поле target (адрес активной версии контракта) хранится в том же самом слоте, что и в текущей версии.

Аналогично можно реализовать проверки и на другие поля storage (пример можно посмотреть в Target.sol)
Если вы планируете реализовать Upgradeable контракты, то обязательно посмотрите на тесты для контракта Upgradeable.

Перед деплоем подобных контрактов в сеть, нужно обязательно тестировать все варианты. Иначе после очередного обновления можно остаться без функционирующего контракта и без возможности обновления.

Источник

Оцените статью