При написании смарт-контрактов важно помнить, что после загрузки в блокчейн их уже невозможно изменить, а значит, невозможно внести какие-либо улучшения или исправить обнаруженные ошибки! Все мы знаем, что в любой программе есть ошибки, и если мы вернемся к коду, который написали пару месяцев назад, то всегда найдем что-то, что можно улучшить.
Как быть? Единственный возможный вариант — загрузить новый контракт с исправленным кодом.
Но что делать, если токены уже выпущены на основании существующего контракта? Миграция приходит нам на помощь! За последний год я попробовал много разных техник его реализации, проанализировал те, которые используются в других крупных блокчейн-проектах, и кое-что придумал сам.
Подробности под катом.
Сразу оговорюсь, что в рамках этого поста я не буду приводить листы готовых смарт-контрактов, а лишь буду рассматривать и анализировать различные методики.
Практически все они в той или иной форме были реализованы мной в контрактах на проекты, в которых мне довелось участвовать, и многое можно почерпнуть из них.
Миграция с контракта, совместимого с ERC20
Начнем с самого простого и распространенного случая, когда исходный контракт, уже загруженный в блокчейн, не содержит каких-либо специальных механизмов, помогающих нам с миграцией, т.е.по сути, мы имеем обычный ERC20-совместимый контракт. Единственное полезное, что мы можем взять из исходного контракта — это балансы всех держателей токенов и общее количество выпущенных токенов для проверки того, что мы никого не забыли во время миграции.
К сожалению, интерфейс ERC20-совместимого контракта не позволяет нам узнать список всех держателей токенов, поэтому при миграции нам придется узнавать полный список держателей из какого-то другого источника, например, скачав его с etherscan.io .contract ERC20base { uint public totalSupply; function balanceOf(address _who) public constant returns(uint); }
Пример контракта, на который осуществляется миграция, приведен в следующем листинге: contract NewContract {
uint public totalSupply;
mapping (address => uint) balanceOf;
function NewContract(address _migrationSource, address [] _holders) public {
for(uint i=0; i<_holders.length; ++i) {
uint balance = ERC20base(_migrationSource).
balanceOf(_holders[i]); balanceOf[_holders[i]] = balance; totalSupply += balance; } require(totalSupply == ERC20base(_migrationSource).
totalSupply());
}
}
Конструктор контракта получает в качестве параметров адрес исходного ERC20-совместимого контракта, а также список держателей токенов, загруженный вручную через etherscan.io. Следует отметить, что в последнем члене конструктора мы проверяем, что количество токенов не изменилось после миграции и, следовательно, ни один держатель токена не забыт. Необходимо учитывать, что такая миграция возможна только в том случае, если количество держателей токенов невелико и цикл через них всех возможен в рамках одной транзакции (лимит газа, установленный в Ethereum для одной транзакции).
Если же количество держателей токенов не позволяет осуществить миграцию за одну транзакцию, то этот функционал придется вынести в отдельную функцию, которую можно будет вызывать необходимое количество раз, и контракт в этом случае будет выглядеть так: contract NewContract {
uint public totalSupply;
mapping (address => uint) balanceOf;
address public migrationSource;
address public owner;
function NewContract(address _migrationSource) public {
migrationSource = _migrationSource;
owner = msg.sender;
}
function migrate(address [] _holders) public
require(msg.sender == owner);
for(uint i=0; i<_holders.length; ++i) {
uint balance = ERC20base(_migrationSource).
balanceOf(_holders[i]);
balanceOf[_holders[i]] = balance;
totalSupply += balance;
}
}
}
В конструкторе этого контракта запоминается адрес исходного контракта, а также инициализируется поле владельца для запоминания адреса владельца контракта, чтобы только он имел право вызывать функциюmigr(), путем вызвав это несколько раз, мы можем перенести любое количество держателей токенов из исходного контракта.
Недостатки данного решения заключаются в следующем:
- На старом смарт-контракте токены останутся у своих владельцев, а на новом их балансы просто продублируются.
Насколько это плохо, зависит от того, как составлен ваш договор о продаже токенов или любой другой документ, описывающий объем ваших обязательств перед держателями токенов вашего проекта, и от того, удвоятся ли ваши обязательства перед ними после создания «дубликата».
- Вы тратите свой газ на миграцию, но это, в общем-то, логично, потому что.
Вы придумали сделать миграцию и в любом случае это причиняет неудобства вашим пользователям, хотя и ограничивается тем, что что им нужно переписать адрес смарт-контракта в своих кошельках со старого на новый.
- В процессе миграции, если она не умещается в одну транзакцию, конечно, могут происходить переводы токенов между адресами их владельцев, в связи с чем могут добавляться новые держатели и изменяться баланс уже существующих.
Миграция между этапами краудсейла
В мире современных ICO довольно распространена практика, когда для разных этапов сбора средств заключаются отдельные контракты, мигрирующие выпущенные токены в новые контракты нового этапа.
Это, конечно, можно сделать так, как мы говорили выше, но если мы точно знаем, что нам придется мигрировать, то почему бы сразу не упростить себе жизнь? Для этого просто введите публичное поле address [] public holders;
Все держатели токенов должны быть добавлены в это поле.
Если контракт уже на ранних этапах сбора позволяет держателям перемещать токены, т.е.
реализует Transfer(), нужно позаботиться о том, чтобы массив обновлялся, например, как-то так mapping (address => bool) public isHolder;
address [] public holders;
….
if (isHolder[_who] != true) {
holders[holders.length++] = _who;
isHolder[_who] = true;
}
Теперь на стороне принимающего контракта можно использовать технологию миграции, аналогичную рассмотренной ранее, но теперь нет необходимости передавать массив в качестве параметра; достаточно сослаться на готовый массив в исходном контракте.
Также следует помнить, что размер массива может не позволять его перебор за одну транзакцию из-за лимита газа на транзакцию, и поэтому необходимо предусмотреть функциюmigr(), которая будет получать два индекса — числа начальные и конечные элементы массива для обработки в рамках этой транзакции.
Недостатки этого решения в целом такие же, как и у предыдущего, за исключением того, что теперь нет необходимости загружать список держателей токенов через etherscan.io.
Миграция с сжиганием оригинальных токенов
Тем не менее, поскольку мы говорим о миграции, а не о дублировании токенов в новом смарт-контракте, нам необходимо позаботиться о вопросе уничтожения (сжигания) токенов на исходном контракте при создании их копий на новом.Очевидно, что недопустимо оставлять в смарт-контракте «дыру», которая позволила бы любому, даже владельцу смарт-контракта, сжигать токены других держателей.
Такой смарт-контракт будет просто мошенничеством! Только их держатель может осуществлять такого рода манипуляции над своими токенами, а значит, миграцию должен осуществлять сам держатель.
Владелец смарт-контракта в этом случае может только начать эту миграцию (перевести смарт-контракт в состояние миграции).
Пример реализации такой миграции я нашел в проекте GOLEM (ссылка на их GitHub в конце поста), затем реализовал его в нескольких своих проектах.
В исходном контракте мы определим интерфейс MigrationAgent, который впоследствии необходимо реализовать в контракте, в который осуществляется миграция.
contract MigrationAgent {
function migrateFrom(address _from, uint256 _value);
}
В исходном контракте токена должны быть реализованы следующие дополнительные функции: contract TokenMigration is Token {
address public migrationAgent;
// Migrate tokens to the new token contract
function migrate() external {
require(migrationAgent != 0);
uint value = balanceOf[msg.sender];
balanceOf[msg.sender] -= value;
totalSupply -= value;
MigrationAgent(migrationAgent).
migrateFrom(msg.sender, value);
}
function setMigrationAgent(address _agent) external {
require(msg.sender == owner && migrationAgent == 0);
migrationAgent = _agent;
}
}
Таким образом, владелец исходного смарт-контракта должен вызвать setMigrationAgent(), передав ему в качестве параметра адрес смарт-контракта, в который осуществляется миграция.
После этого все держатели токенов исходного смарт-контракта должны вызвать функциюmigr(), которая уничтожит их токены в исходном смарт-контракте и добавит их в новый (путем вызова функцииmigrFrom() нового контракта).
Ну а новый контракт на самом деле должен содержать реализацию интерфейса MigrationAgent, например вот так: contract NewContact is MigrationAgent {
uint256 public totalSupply;
mapping (address => uint256) public balanceOf;
address public migrationHost;
function NewContract(address _migrationHost) {
migrationHost = _migrationHost;
}
function migrateFrom(address _from, uint256 _value) public {
require(migrationHost == msg.sender);
require(balanceOf[_from] + _value > balanceOf[_from]); // overflow?
balanceOf[_from] += _value;
totalSupply += _value;
}
}
Все в этом решении великолепно! Кроме того, пользователю необходимо вызвать функциюmigr().
Ситуация существенно осложняется тем, что лишь немногие кошельки поддерживают функции вызова и, как правило, они не самые удобные.
Поэтому, поверьте, если среди держателей ваших токенов есть не только криптогики, но и простые смертные, они вас просто проклянут, когда вы объясните им, что нужно установить какой-то Mist, а потом вызвать какую-то функцию (спасибо Боже, хоть без параметров).
Как быть? И сделать это можно очень просто! Ведь любой пользователь криптовалюты, даже самый начинающий, умеет хорошо делать одно — отправлять криптовалюту со своего адреса на какой-то другой.
Итак, пусть этот адрес будет адресом нашего смарт-контракта, а его резервная функция в режиме «миграции» будет просто вызывать миграцию().
Таким образом, для осуществления миграции держателю токена достаточно будет перевести хотя бы 1 wei на адрес смарт-контракта, находящегося в режиме «миграции», чтобы произошло чудо! function () payable {
if (state = State.Migration) {
migrate();
} else { … }
}
Заключение
Обсуждаемые решения концептуально охватывают все возможные способы реализации миграции токенов, хотя возможны вариации в конкретных реализациях.Отдельного внимания заслуживает подход «перегонный сосуд» (ссылка в конце поста).
Независимо от используемого вами подхода к миграции, помните, что смарт-контракт — это не просто программа, работающая внутри виртуальной машины Ethereum, а своего рода отчужденный независимый контракт, и любая миграция предполагает изменение вами условий этого контракта.
Вы уверены, что держатели токенов хотят изменить условия соглашения, которое они заключили при покупке токенов? На самом деле это хороший вопрос.
И очень хорошая практика — «спрашивать» держателей токенов, хотят ли они «перейти» на новый контракт. Именно реализацию миграции через голосование я реализовал в смарт-контракте своего проекта.
прувер , текст контракта можно найти на моем GitHub. И конечно, приглашаю вас присоединиться к ICO моего проекта.
прувер .
Надеюсь, что все это кому-то будет полезно и нужно :).
Полезные ссылки
- Моя видеопрезентация по созданию простого смарт-контракта для ICO
- Мой пост о написании смарт-контракта для ICO
- Реализация миграции токенов в проекте GOLEM
- Описание миграции методом «перегонного сосуда».
- Мой проект ПРОВЕР — Подготовка к ICO идет полным ходом!
- Очередной пост о моем проекте PROVER
- Видеопрезентация проекта PROVER (на английском языке)
-
Распространенные Ошибки Потребителей
19 Oct, 24 -
Плохая И Хорошая Кириллица
19 Oct, 24 -
Квантовая Механика В Фотосинтезе
19 Oct, 24