Я впервые решила написать что-то на русском языке в рамках своей работы в Airalab. Данная статья открывает цикл обучающих материалов для новичков в сфере блокчейн – будем знакомиться с тем, как работает Ethereum. Эта и следующие статьи, относящиеся к циклу, будут опубликованы с хэштегом #Airalab_learningcenter.
Известно, что лучший способ научиться что-либо делать – это попробовать сделать это на практике. Сегодня мы будем создавать свой собственный токен в сети Ethereum на основе первого урока.
!Уроки на сайте опубликованы в далеком 2014 году. За это время синтаксис языка Solidity немного изменился, поэтому в этой статье представлен не просто перевод урока с английского, но и мои комментарии.
Начнем по традиции с начала, а именно с установки Ethereum Wallet и синхронизации с сетью.
Установка Ethereum Wallet и синхронизация с сетью
С установкой все просто: идем на сайт, прокручиваем немного вниз, скачиваем приложение, устанавливаем.
Тeперь о синхронизации:
Синхронизироваться будем с тестовой сетью Ropsten. Почему? Потому что эфир даже при нынешнем курсе дорог, а у команды Airalab есть собственный майнер в сети Ropsten, т.е. мы можем поделиться тестовыми эфирками с теми, кто хочет получить знания.
!Как получить немного ропстеновского эфира для прохождения уроков? Присоединяемся к чату Airalab в Telegram, пишем сообщение: «Привет! Хочу пройти уроки в Ethereum по статье из #Airalab_learningcenter – дайте, пожалуйста, немного тестового эфира», адресуем его devOps @vourhey или @pad1a_evil.
Итак, мы выбрали сеть Ropsten и запустили синхронизацию. С какой проблемой столкнулась я: синхронизация доходила до 90+% и зависала, а за то время, пока у меня все висело, набегали новые блоки – получалась бесконечная гонка-преследование или почему Ахиллес не может догнать черепаху. Как решить:
!У меня, кстати, Windows, но давайте не будем это комментировать.
- Запускаем консоль (консоль, она же командная строка, в Windows находится в поиске по запросу cmd);
- В консоль вводим команду: cd %APPDATA%\Mist\binaries\geth и ищем geth.exe
!Путь до geth у вас может отличаться, поэтому если команда не работает, то указываем актуальный путь – у меня это был C:/program files/geth.
- Далее, когда geth нашелся, вводим следующую команду:
geth.exe --testnet --fast --bootnodes enode://9ed88cf5f9b7be85bd3600ec46233beda369343e2c3f9ef11e9cef3775120c0882507bfc22c8913f9362ef4ca424faf0a7c10df5c04a0c89fcc3066b6474de52@52.166.128.96:30303
или
geth.exe –testnet --bootnodes enode://9ed88cf5f9b7be85bd3600ec46233beda369343e2c3f9ef11e9cef3775120c0882507bfc22c8913f9362ef4ca424faf0a7c10df5c04a0c89fcc3066b6474de52@52.166.128.96:30303
!Здесь немного поясню. Эта команда для запуска клиента geth. Ему передаются следующие параметры:
‘--testnet’ означает синхронизацию с тестовой сетью (для geth сеть ropsten по умолчанию)
‘--fast’ для быстрой синхронизации. Можно почитать здесь.
‘--bootnodes enode://’ здесь первое слово bootnodes означает подключаться к определенным пирам. enode:// и все остальное - это идентификатор пира.
- Снова запускаем синхронизацию Ethereum Wallet;
- Ждем…
- Оп, готово! Немного танцев с бубном и Ethereum Wallet синхронизирован – можно переходить к следующему этапу.
Создание минимально жизнеспособного токена в сети Ethereum
Мы собираемся создать токен. Токены в экосистеме Ethereum могут представлять любой равноценный торгуемый товар: монеты, бонусные баллы, золотые сертификаты, долговые обязательства ит.д. Т.к. все токены осуществляют некоторые базовые функции стандартным образом, это также значит, что ваш токен сразу будет совместим с Ethereum Wallet и любым другим клиентом или контрактом, который использует те же стандарты.
!Здесь я пропущу код, который можно просто скопировать, чтобы создать токен, потому что мы сюда пришли учиться. Ну и потому что он не работает из-за обновлений в синтаксисе Solidity☺.
Давайте начнем с основ. Откройте приложение кошелька, перейдите к таблице Contracts и потом кликните «Deploy New Contract».
В текстовом поле «Solidity Contract Source code» введите код, указанный ниже:
contract MyToken {
/* This creates an array with all balances */
mapping (address => uint256) public balanceOf;
}
‘Mapping’ означает ассоциативное множество (associative array), где вы ассоциируете адреса с балансами. Адреса находятся в базовом шестнадцатеричном Ethereum формате, в то время как балансы – это целые числа от 0 до 115 quattuorvigintillion. Если вы не знаете, чему равен quattuorvigintillion, то это множество вингителлионов, это больше, чем любая цель, для которой вы планируете использовать свой токен. Публичное ключевое слово ‘public’ означает, что эта переменная будет доступна каждому в блокчейне, а также, что все балансы публичны (какими они и должны быть, чтобы клиенты их отображали).
Если вы опубликуете этот контракт сразу, то он будет работать, но не будет особо полезным: этот контракт сможет запрашивать баланс вашей монеты для любого адреса, но пока вы не создадите отдельную монету, каждый запрос будет возвращаться к 0. Таким образом, мы собираемся создать несколько токенов для начала. Добавьте этот код до последней закрывающейся скобки, сразу под mapping-строкой:
function MyToken() public {
balanceOf[msg.sender] = 21000000;
}
!В уроках после функции не указана область видимости public , что сейчас является необходимым в связи с обновлениями в Solidity. Далее в тексте я не буду каждый раз помечать эту ошибку.
Заметьте, что function MyToken имеет то же самое имя, что и contract MyToken. Это очень важно, и если вы переименуете что-то одно, то вам нужно переименовать и второе тоже: это специальная функция запуска, которая выполняется только один раз именно тогда, когда контракт впервые загружен в сеть. Эта функция отправит баланс msg.sender (пользователя, который использует контракт) с балансом в 21млн.
Выбор 21млн был произвольным, и вы можете изменить его на что угодно, но есть способ получше: вместо этого подставьте его как параметр функции, например, вот так:
function MyToken(uint256 initialSupply) public {
balanceOf[msg.sender] = initialSupply;
}
Взгляните на правую колонку рядом с контрактом, и вы увидите выпадающий список, подписанный «Pick a contract». Выберите контракт ‘MyToken’, и вы увидите, что сейчас он показывает секцию под названием Constructor parameters. Это изменяемые параметры вашего токена, поэтому в будущем вы можете снова использовать тот же код, меняя лишь эти переменные.
Прямо сейчас у нас есть функциональный контракт, который создал балансы токенов, но все, что он делает, пока нет функции для его выполнения, просто остается в том же аккаунте. Таким образом, сейчас мы собираемся его применить. Напишите следующий код ДО последней скобки:
/* Send coins */
function transfer(address _to, uint256 _value) public {
/* Add and subtract new balances */
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
}
Это очень простая функция: у нее есть получатель и ценность как параметр, и когда бы кто-либо ее не вызвал, она будет вычитать value из их баланса и добавлять его к balance. Сразу появляется очевидная проблема: что произойдет, если человек захочет отправить больше, чем у него есть? До тех пор, пока мы не хотим разбираться с долгом в этом конкретном контракте, мы просто собираемся сделать быструю проверку, и если у отправителя не достаточно средств, выполнение контракта будет просто остановлено. Это также является проверкой для переполнения (избытков), чтобы избежать возникновения такого большого числа, что оно снова станет нулем.
Чтобы остановить выполнение контракта в ходе выполнения, вы можете return или throw. Предыдущий будет стоить меньше газа, но это может быть большей головной болью, т.к. любые изменения, которые вы делали в контракте до настоящего времени, будут сохранены. С другой стороны, ‘throw’ будет отменять выполнение контракта полностью, возвращать любые изменения, которые транзакция могла бы сделать, и отправитель потеряет все эфиры, которые он отправил для газа. Но до тех пор, пока Wallet может распознать, что контракт будет throw, он всегда показывает предупреждение, таким образом, полностью предотвращая трату любых эфиров.
Заменяем функция transfer, которую мы уже ввели в текстовое поле, на следующую:
function transfer(address _to, uint256 _value) public {
/* Check if sender has balance and for overflows */
require(balanceOf[msg.sender] >= _value && balanceOf[_to] + _value >= balanceOf[_to]);
/* Add and subtract new balances */
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
}
Сейчас все, что отсутствует, имеет некоторую базовую информацию о контракте. В ближайшем будущем это может быть обработано реестром токена, но сейчас мы добавим их непосредственно в контракт. Объявляем переменные после mapping-строки:
string public name;
string public symbol;
uint8 public decimals;
А сейчас мы обновим constructor function, чтобы разрешить всем этим переменным быть установленными на старте. Заменяем уже введенную функцию MyToken на следующую:
/* Initializes contract with initial supply tokens to the creator of the contract */
function MyToken(uint256 initialSupply, string tokenName, string tokenSymbol, uint8 decimalUnits) public {
balanceOf[msg.sender] = initialSupply; // Give the creator all initial tokens
name = tokenName; // Set the name for display purposes
symbol = tokenSymbol; // Set the symbol for display purposes
decimals = decimalUnits; // Amount of decimals for display purposes
}
В итоге сейчас нам нужно то, что называется Events. Это специальные пустые функции, которые вы вызываете, чтобы помочь клиентам типа Ethereum Wallet хранить трек действий, происходящих в контракте. Events должны начинаться с заглавной буквы. Добавьте эту строку в начале контракта, чтобы объявить event:
event Transfer(address indexed from, address indexed to, uint256 value);
И потом вам просто нужно добавить эти две строчки внутрь функции ‘transfer’:
/* Notify anyone listening that this transfer took place */
emit Transfer(msg.sender, _to, _value);
!В уроках на сайте не добавлен префикс «emit», что я уже исправила в данной статье, т.к. вызов событий без данного префикса устарел. Далее в комментариях эту ошибку не отмечаю.
Ваш токен готов!
Заметили комментарии? Что за @notice и @param comments, спросите вы? Это Natspec – развивающийся стандарт для спецификации естественного языка, который позволяет кошелькам показывать пользователям описание естественного языка, что контракт собирается сделать. На данный момент он пока не поддерживается большим количеством кошельков, что будет изменено в будущем, так что будет неплохо быть подготовленным.
Как развернуть код? Если вы еще не сделали этого, то откройте Ethereum Wallet, перейдите к таблице контрактов и после кликните «Deploy new contract».
Сейчас возьмите следующий код токена и вставьте его в «Solidity source field»:
pragma solidity ^0.4.18;
contract MyToken {
/* This creates an array with all balances */
mapping (address => uint256) public balanceOf;
event Transfer(address indexed from, address indexed to, uint256 value);
string public name;
string public symbol;
uint8 public decimals;
/* Initializes contract with initial supply tokens to the creator of the contract */
function MyToken(uint256 initialSupply, string tokenName, string tokenSymbol, uint8 decimalUnits) public {
balanceOf[msg.sender] = initialSupply; // Give the creator all initial tokens
name = tokenName; // Set the name for display purposes
symbol = tokenSymbol; // Set the symbol for display purposes
decimals = decimalUnits; // Amount of decimals for display purposes
}
/* Send coins */
function transfer(address _to, uint256 _value) public {
/* Check if sender has balance and for overflows */
require(balanceOf[msg.sender] >= _value && balanceOf[_to] + _value >= balanceOf[_to]);
/* Add and subtract new balances */
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
/* Notify anyone listening that this transfer took place */
emit Transfer(msg.sender, _to, _value);
}
}
Если код компилируется без ошибок, вы увидите выпадающий список «Рick a contract» справа. Выберите контракт «MyToken». В правой колонке вы увидите параметры, которые вам нужны, чтобы персонализировать ваш собственный токен. Вы можете скорректировать их так, как вам хотелось бы, мы рекомендуем вам выбрать следующие параметры: 10,000 как supply, (любое имя), "%" для символа и 2 знака после запятой. Ваше приложение должно выглядеть так:
Прокрутите страницу до конца и вы увидите оценку вычисления затрат этого контракта. Вы можете выбрать fee, сколько эфиров вы хотели бы заплатить за это. Любой избыток эфиров, который вы не использовали, вернется к вам, поэтому вы можете оставить стандартные настройки, если хотите. Нажмите «Deploy», введите пароль от вашего аккаунта и подождите несколько секунд для сбора вашей транзакции.
Вы будете перенаправлены на основную страницу, где вы можете увидеть вашу транзакцию, ожидающую подтверждения. Нажмите на имя вашего основного аккаунта и не более минуты спустя вы должны увидеть, что у вас есть 100% акций, которые вы создали. Чтобы отправить несколько монет друзьям: выберите «Send», и затем выберите, какую валюту вы хотите отправить (эфир или ту, что вы только что создали), вставьте адрес вашего друга на поле «To» и нажмите «Send».
Если вы отправите это другу, он не пока ничего не увидит в своем кошельке. Потому что кошелек отслеживает только те токены, о которых знает, и вам нужно добавить созданные вами вручную. Сейчас перейдите к таблице «Contracts» и вы должны увидеть ссылку на ваш только что созданный контракт. Это очень простая страница контракта – просто кликните «Сopy address» и вставьте адрес контракта в редактор текста, скоро он вам потребуется.
Чтобы добавить токен к просмотру, перейдите на страницу контрактов и нажмите «Watch Token». Появится всплывающее окно, и вам всего лишь нужно вставить адрес контракта. Имя токена, символ и количество цифр после запятой (token name, symbol and decimal number) должны быть автоматически заполнены, но если этого не произошло, вы можете добавить что угодно (это повлияет только на то, как они отображаются в вашем кошельке). Как только вы это сделаете, вам автоматически будет показан ваш баланс этого токена, и у вас будет возможность отправить этот токен кому угодно еще.
И сейчас у вас есть ваш собственный крипто токен! Токены, созданные вами самостоятельно, могут быть полезны как обмен ценностями в локальных сообществах (value exchange on local communities), пути для хранения трека отработанных часов (ways to keep track of worked hours) или другие программы лояльности. Но можем ли мы сделать валюту, имеющую действительную стоимость, самостоятельно?
Усовершенствуйте ваш токен. Вы можете использовать ваш крипто токен даже без всякого соприкосновения со строкой кода, но настоящая магия случается, когда вы начинаете настраивать его. Следующие секции будут предложениями, какие функции вы можете добавить к вашему токену, чтобы сделать его более соответствующим вашим требованиям.
Добавьте больше базовых функций
Сейчас копируем код уже созданного токена из предыдущей части статьи в тестовое поле.
В этой части статьи вы заметите, что в вашем базовом контракте токена может быть чуть больше функций, таких как approve, sendFrom и другие. Эти функции нужны токену, чтобы взаимодействовать с другими контрактами. Если вы хотите, скажем, продать токены децентрализованной бирже, то просто отправить их на адрес будет недостаточно. Потому что биржа не будет осведомлена о новых токенах и о том, кто их отправил, так как контракты не могут быть подписаны на Events, только на function calls. Таким образом, для контрактов вы должны сначала подтвердить количество токенов, которые они могут переместить с вашего аккаунта, а потом вызвать эти контракты, позволив им узнать, что они должны выполнять задачи. Или же выполнить две функции в одной с approveAndCall.
Так как многие из этих функций должны заново осуществлять доставку токенов, имеет смысл изменить их на внутренние функции, которые могут быть вызваны лишь самим контрактом. Заменяем уже введенную функцию transfer на следующий код:
/* Internal transfer, can only be called by this contract */
function _transfer(address _from, address _to, uint _value) internal {
require (_to != 0x0); // Prevent transfer to 0x0 address. Use burn() instead
require (balanceOf[_from] >= _value); // Check if the sender has enough
require (balanceOf[_to] + _value > balanceOf[_to]); // Check for overflows
require(!frozenAccount[_from]); // Check if sender is frozen
require(!frozenAccount[_to]); // Check if recipient is frozen
balanceOf[_from] -= _value; // Subtract from the sender
balanceOf[_to] += _value; // Add the same to the recipient
emit Transfer(_from, _to, _value);
}
!После того, как вы добавите эту часть кода в текстовое поле, вы получите уведомление об ошибке «Необъявленный идентификатор»:
Не обращайте внимания на уведомление – далее в статье мы объявим этот идентификатор.
Сейчас все ваши функции, результатом которых является доставка монет, могут выполнять свои собственные проверки, а затем вызывать transfer с правильными параметрами. Заметьте, что эта функция будет перемещать монеты с любого аккаунта на любой другой аккаунт без требования чьего-либо разрешения: поэтому это и есть внутренняя функция, вызываемая только контрактом. Если вы добавляете любую функцию, вызывающую контракт, убедитесь, что она верно верифицирует должен ли вызывающий иметь разрешение переместить монеты.
Централизованный администратор
Все dapps являются полностью децентрализованными по умолчанию, но это не означает, что они не могут иметь что-то вроде центрального менеджера, если вы хотите, чтобы он был. Возможно, вы хотите иметь возможность «чеканить» больше монет, а, может, вы хотите запретить некоторым людям пользоваться вашей валютой. Вы можете добавить любую из этих функций, но уловка в том, что вы можете добавить их лишь в начале, чтобы, таким образом, все держатели токенов всегда точно знали правила игры до того, как они решат приобрести один из токенов.
Чтобы это произошло, вам нужен центральный контроллер валюты. Им может быть и простой аккаунт, и контракт, и, таким образом, решение по созданию бОльшего количества токенов будет зависеть от контракта: является ли это демократической организацией, которая может голосовать, или же, возможно, это просто путь ограничения власти владельца токена.
Чтобы сделать это, мы научимся очень полезному свойству контрактов – наследованию (inheritance). Наследование позволяет контракту приобрести свойства родительских контрактов без необходимости заново определять все из них. Это делает код чище и проще для повторного использования. Добавьте этот код к первой строчке вашего кода ДО contract MyToken {:
contract owned {
address public owner;
function owned() public {
owner = msg.sender;
}
modifier onlyOwner {
require(msg.sender == owner);
_;
}
function transferOwnership(address newOwner) public onlyOwner {
owner = newOwner;
}
}
Этим мы создаем базовый контракт, которые не делает ничего, кроме определения некоторых обобщенных функций того контракта, что может быть «в собственности». Сейчас следующий шаг – просто добавить текст is owned к контракту:
contract MyToken is owned {
/* the rest of the contract as usual */
Это означает, что все функции внутри MyToken сейчас могут иметь доступ к переменной owner и модификатору onlyOwner. Контракт также получает функцию для доставки собственности. До тех пор, пока добавление владельца контракта при запуске интересно, вы можете также добавить это к constructor function:
function MyToken(
uint256 initialSupply,
string tokenName,
uint8 decimalUnits,
string tokenSymbol,
address centralMinter
) public {
if(centralMinter != 0 ) owner = centralMinter;
}
Центральная «чеканка»
Предположим, вы хотите изменить количество монет в обращении. Это та ситуация, когда ваши токены на самом деле представляют собой вне-блокчейновый актив (такой как золотой сертификат или государственные валюты), и вы хотите, чтобы виртуальный реестр реагировал на реальный. Также это может быть ситуация, когда держатели валюты ожидают некоторого контроля цен на токен и хотят выпустить или удалить токены из обращения.
Для начала нам нужно добавить переменную для хранения totalSupply и назначить ее конструктору функций:
contract MyToken {
uint256 public totalSupply;
function MyToken(...) {
totalSupply = initialSupply;
Сейчас давайте добавим новую функцию, которая в итоге позволит владельцу создавать новые токены:
function mintToken(address target, uint256 mintedAmount) public onlyOwner {
balanceOf[target] += mintedAmount;
totalSupply += mintedAmount;
emit Transfer(0, owner, mintedAmount);
emit Transfer(owner, target, mintedAmount);
}
Заметьте модификатор onlyOwner в конце имени функции. Это значит, что данная функция будет перезаписана в компиляции, чтобы унаследовать код от модификатора onlyOwner, который мы определили до этого. Код этой функции будет вставлен туда, где подчеркивается функция модификатора, что означает, что эта конкретная функция может быть вызвана лишь аккаунтом, указанным в качестве владельца. Просто добавьте это к контракту с модификатором owner, и у вас будет возможность создать больше монет.
Заморозка активов
В зависимости от вашего сценария использования, вам, возможно, потребуются некоторые нормативные ограничения для тех, кто может или не может использовать ваши токены. Чтобы создать эти ограничения, вы можете добавить параметр, который позволяет владельцу контракта заморозить или разморозить активы.
Добавьте эти переменную и функцию где угодно внутри контракта. Вы можете вставить их где угодно, но для практики мы рекомендуем ставить mappings с другими mappings, а events с другими events:
mapping (address => bool) public frozenAccount;
event FrozenFunds(address target, bool frozen);
function freezeAccount(address target, bool freeze) public onlyOwner {
frozenAccount[target] = freeze;
emit FrozenFunds(target, freeze);
}
С этим кодом все аккаунты разморожены по умолчанию, но владелец может перевести любой из них в состояние заморозки с помощью вызова freezeAccount. К сожалению, заморозка не имеет практического эффекта, потому что мы ничего не добавили к функции transfer. Сейчас мы это изменим:
require(!frozenAccount[msg.sender]);
Сейчас на всех замороженных аккаунтах средства останутся нетронутыми, но аккаунты не могут перемещать эти средства. Все аккаунты разморожены по умолчанию, пока вы не заморозите их, но вы можете запросто вернуть это поведение в белый список, где вам нужно вручную подтвердить каждый аккаунт. Просто переименуйте frozenAccount в approvedAccount и измените последнюю строку на:
require(approvedAccount[msg.sender]);
Автоматическая продажа и покупка
Пока вы полагались на полезность и доверили ценности вашего токена. Но если вы хотите иметь возможность сделать ценность вашего токена поддерживаемой эфиром (или другими токенами) с помощью создания фонда, который автоматически продает и покупает их по рыночной стоимости, то для начала давайте выставим цену покупки и продажи:
uint256 public sellPrice;
uint256 public buyPrice;
function setPrices(uint256 newSellPrice, uint256 newBuyPrice) public onlyOwner {
sellPrice = newSellPrice;
buyPrice = newBuyPrice;
}
Это приемлемо для цены, которая меняется не очень часто, так как каждая новая цена будет требовать проведения вами транзакции и траты небольшого количества эфира. Если вы хотите, чтобы у вас была постоянно плавающая цена, то мы рекомендуем исследовать standard data feeds.
Следующий шаг – создать функции покупки и продажи:
function buy() public payable returns (uint amount){
amount = msg.value / buyPrice; // calculates the amount
require(balanceOf[this] >= amount); // checks if it has enough to sell
balanceOf[msg.sender] += amount; // adds the amount to buyer's balance
balanceOf[this] -= amount; // subtracts amount from seller's balance
emit Transfer(this, msg.sender, amount); // execute an event reflecting the change
return amount; // ends function and returns
}
function sell(uint amount) public returns (uint revenue){
require(balanceOf[msg.sender] >= amount); // checks if the sender has enough to sell
balanceOf[this] += amount; // adds the amount to owner's balance
balanceOf[msg.sender] -= amount; // subtracts the amount from seller's balance
revenue = amount * sellPrice;
msg.sender.transfer(revenue); // sends ether to the seller: it's important to do this last to prevent recursion attacks
emit Transfer(msg.sender, this, amount); // executes an event reflecting on the change
return revenue; // ends function and returns
}
Заметьте, что это не создаст новые токены, но изменит баланс на контракте. Контракт может содержать как собственные токены, так и эфир, в то же время этот контракт может устанавливать цены или в некоторых случаях создавать новые токены (если применимо), но он не может задействовать токены банка или эфир. Единственный способ, которым этот контракт может перемещать средства, это через продажу или покупку этих самых средств.
Замечание: покупка и продажа «цен» ставится не в эфирах, а в wei – минимальной валюте системы (эквивалентна центам в евро и долларах, либо сатоши в биткоине). Один эфир - 1000000000000000000 wei. Таким образом, если цены для вашего токена устанавливаются в эфирах, добавьте 18 нулей в конце.
При создании контракта отправьте ему достаточное количество эфира, чтобы он мог купить обратно все токены на рынке, иначе ваш контракт будет неплатежеспособным, а ваши пользователи не смогут продавать свои токены.
Предыдущие примеры, конечно, описывают контракт с единственным центральным покупателем и продавцом. Но гораздо более интересный контракт позволил бы любому на рынке предлагать разные цены или, возможно, загружал бы цены прямо из внешнего источника.
Автопополнение
Каждый раз, когда вы проводите транзакцию в Ethereum, вам нужно платить вознаграждение майнеру блока, который будет вычислять результат вашего смарт-контракта. Это может измениться в будущем, но на данный момент вознаграждения могут быть оплачены лишь в эфирах и, таким образом, все пользователи ваших токенов нуждаются в них. Токены в аккаунтах с балансами меньше, чем вознаграждение, «застревают», пока владелец не сможет оплатить необходимое вознаграждение. Но при некоторых раскладах, вы можете захотеть, чтобы ваши пользователи не думали об Ethereum, блокчейне или как получить эфир, поэтому единственный возможный подход – автоматически пополнять балансы пользователей, как только баланс становится опасно низким.
Чтобы добавить автопополнение, для начала вам нужно создать переменную, которая будет содержать лимитное значение и функцию для его изменения. Если вы не знаете конкретной стоимости, установите лимит на 5 finney (0.005 Ether).
uint minBalanceForAccounts;
function setMinBalance(uint minimumBalanceInFinney) public onlyOwner {
minBalanceForAccounts = minimumBalanceInFinney * 1 finney;
}
Потом добавьте эту строку к функции transfer, чтобы отправитель получил средства обратно:
/* Send coins */
function transfer(address _to, uint256 _value) {
...
if(msg.sender.balance < minBalanceForAccounts)
sell((minBalanceForAccounts - msg.sender.balance) / sellPrice);
}
Вы также можете изменить его вместо этого так, чтобы вознаграждение получателю оплачивалось отправителем вперед:
/* Send coins */
function transfer(address _to, uint256 _value) {
...
if(_to.balance<minBalanceForAccounts)
_to.transfer(sell((minBalanceForAccounts - _to.balance) / sellPrice));
}
!В оригинальных уроках указан оператор _to.send, который мы в соответствии с обновленным синтаксисом языка Solidity меняем на _to.transfer. Далее эту ошибку не комментирую, сразу даю верный код.
Это будет гарантировать, что ни у одного аккаунта, получающего токен, не будет меньше эфиров, чем необходимо для оплаты вознаграждения.
Доказательство работы (Proof of Work)
Существует несколько способов приравнять предложение вашей монеты к математической формуле. Одним из простейших способов сделать это мог бы быть «объединенный майнинг» (merged mining) с эфиром, означающий, что любой нашедший блок в Ethereum, также получит вознаграждение от вашей монеты, учитывая, что любой вызывает функцию вознаграждения на этом блоке. Вы можете сделать это, используя специальное ключевое слово рынка coinbase (special keyword coinbase), которое будет ссылаться на майнера, нашедшего блок.
function giveBlockReward() public {
balanceOf[block.coinbase] += 1;
}
Также можно добавить математическую формулу таким образом, чтобы любой, кто может выполнить вычисление, мог выиграть вознаграждение. В следующем примере вам нужно вычислить квадратный корень текущей задачи (current challenge), попасть в точку и получить право установить следующую задачу:
uint currentChallenge = 1; // Can you figure out the cubic root of this number?
function rewardMathGeniuses(uint answerToCurrentReward, uint nextChallenge) public {
require(answerToCurrentReward**3 == currentChallenge); // If answer is wrong do not continue
balanceOf[msg.sender] += 1; // Reward the player
currentChallenge = nextChallenge; // Set the next challenge
}
Конечно, вычисление квадратного корня в уме может быть сложным процессом, в то время, как это же вычисление очень просто выполнить с помощью калькулятора. Таким образом, эта игра может быть легко нарушена компьютером. А еще до тех пор, пока последний победитель может выбирать следующую задачу, он может выбирать что-то, что ему знакомо, поэтому такая игра была бы не очень честной для других игроков. Существуют задачи простые для людей, но сложные для компьютеров, но задачи такого типа обычно очень сложно закодировать в простые скрипты как, например, эти. Вместо этого более честная система должна быть такой, чтобы компьютеру было сложно выполнить вычисление, но не очень сложно верифицировать. Прекрасным вариантом было бы создать задачу хэшей, где испытуемый должен сгенерировать хэши из множественных чисел до того, как они не нашли бы один, который будет ниже, чем заданная сложность.
Этот процесс был впервые предложен Адамом Бэком (Adam Back) в 1997 и известен как Hashcash, а затем был применен в Bitcoin Satoshi Nakamoto как Proof of Work (далее – PoW) в 2008. Ethereum был запущен с использованием такой системы для его модели безопасности, но планируется перейти от модели безопасности PоW к смешанной модели Proof of Stake (PoS) и системы ставок (mixed proof of stake and betting system), называемой Casper.
Но если вам нравится Hashing как форма рандомного выпуска монет, вы все еще можете создать вашу валюту, основанную на Ethereum, c PoW выпуска.
!Перед тем, как добавить в текстовое поле код, указанный ниже, удаляем из него следующие строки:
uint currentChallenge = 1; // Can you figure out the cubic root of this number?
function rewardMathGeniuses(uint answerToCurrentReward, uint nextChallenge) public {
require(answerToCurrentReward**3 == currentChallenge); // If answer is wrong do not continue
balanceOf[msg.sender] += 1; // Reward the player
currentChallenge = nextChallenge; // Set the next challenge
}
А теперь добавляем код:
bytes32 public currentChallenge; // The coin starts with a challenge
uint public timeOfLastProof; // Variable to keep track of when rewards were given
uint public difficulty = 10**32; // Difficulty starts reasonably low
function proofOfWork(uint nonce) public {
bytes8 n = bytes8(keccak256(nonce, currentChallenge)); // Generate a random hash based on input
require(n >= bytes8(difficulty)); // Check if it's under the difficulty
uint timeSinceLastProof = (now - timeOfLastProof); // Calculate time since last reward was given
require(timeSinceLastProof >= 5 seconds); // Rewards cannot be given too quickly
balanceOf[msg.sender] += timeSinceLastProof / 60 seconds; // The reward to the winner grows by the minute
difficulty = difficulty * 10 minutes / timeSinceLastProof + 1; // Adjusts the difficulty
timeOfLastProof = now; // Reset the counter
currentChallenge = keccak256(nonce, currentChallenge, block.blockhash(block.number - 1)); // Save a hash that will be used as the next proof
}
!В уроках указана функция sha3, которая в данный момент является устаревшей и заменяется на keccak256.
Также измените Constructor function (ту, у которой то же имя, что и у контракта, вызываемого при первой загрузке), добавив эту строку:
timeOfLastProof = now;
Таким образом, сложность наладки не будет сумасшедшей.
Однажды, когда контракт будет онлайн, выберете функцию «Proof of work», добавьте ваше любимое число в поле nonce и попробуйте выполнить. Если окно подтверждения выдаст красное предупреждение, говорящее «Data can't be executed», вернитесь назад и выбирайте другие числа, пока не найдете то, которое позволит транзакции двигаться дальше: этот процесс рандомный. Если вы найдете число, вам будет начислено по одному токену за каждую минуту, прошедшую с тех пор, как было дано последнее вознаграждение, а затем сложность задачи будет увеличена или уменьшена с целью выйти в среднем на 10 минут за одно вознаграждение.
Процесс, при котором вы пытаетесь найти число, обеспечивающее вам вознаграждение, и есть майнинг: если сложность увеличивается, то найти счастливый номер может быть очень сложно, но верифицировать факт его нахождения всегда будет просто.
Улучшенная монета. Полный код монеты
Если вы добавили все продвинутые опции, то итоговый код будет выглядеть так:
pragma solidity ^0.4.18;
contract owned {
address public owner;
function owned() public {
owner = msg.sender;
}
modifier onlyOwner {
require(msg.sender == owner);
_;
}
function transferOwnership(address newOwner) public onlyOwner {
owner = newOwner;
}
}
contract MyToken is owned {
/* the rest of the contract as usual */
/* This creates an array with all balances */
mapping (address => uint256) public balanceOf;
mapping (address => bool) public frozenAccount;
mapping (address => bool) public approvedAccount;
uint256 public totalSupply;
string public name;
string public symbol;
uint8 public decimals;
uint256 public sellPrice;
uint256 public buyPrice;
bytes32 public currentChallenge; // The coin starts with a challenge
uint public timeOfLastProof; // Variable to keep track of when rewards were given
uint public difficulty = 10**32; // Difficulty starts reasonably low
event Transfer(address indexed from, address indexed to, uint256 value);
event FrozenFunds(address target, bool frozen);
event ApprovedFunds(address target, bool approved);
/* Initializes contract with initial supply tokens to the creator of the contract */
function MyToken(uint256 initialSupply, string tokenName, uint8 decimalUnits, string tokenSymbol, address centralMinter) public {
totalSupply = initialSupply;
balanceOf[msg.sender] = initialSupply; // Give the creator all initial tokens
name = tokenName; // Set the name for display purposes
symbol = tokenSymbol; // Set the symbol for display purposes
decimals = decimalUnits; // Amount of decimals for display purposes
if(centralMinter != 0 ) owner = centralMinter;
timeOfLastProof = now;
}
/* Send coins */
/* Internal transfer, can only be called by this contract */
function _transfer(address _from, address _to, uint _value) internal {
require (_to != 0x0); // Prevent transfer to 0x0 address. Use burn() instead
require (balanceOf[_from] >= _value); // Check if the sender has enough
require (balanceOf[_to] + _value > balanceOf[_to]); // Check for overflows
require(!frozenAccount[_from]); // Check if sender is frozen
require(!frozenAccount[_to]); // Check if recipient is frozen
balanceOf[_from] -= _value; // Subtract from the sender
balanceOf[_to] += _value; // Add the same to the recipient
emit Transfer(_from, _to, _value);
require(!frozenAccount[msg.sender]);
require(approvedAccount[msg.sender]);
if(_to.balance<minBalanceForAccounts)
_to.transfer(sell((minBalanceForAccounts - _to.balance) / sellPrice));
}
function mintToken(address target, uint256 mintedAmount)public onlyOwner {
balanceOf[target] += mintedAmount;
totalSupply += mintedAmount;
emit Transfer(0, owner, mintedAmount);
emit Transfer(owner, target, mintedAmount);
}
function freezeAccount(address target, bool freeze)public onlyOwner {
frozenAccount[target] = freeze;
emit FrozenFunds(target, freeze);
}
function approveAccount(address target, bool approve)public onlyOwner {
approvedAccount[target] = approve;
emit ApprovedFunds(target, approve);
}
function setPrices(uint256 newSellPrice, uint256 newBuyPrice) public onlyOwner {
sellPrice = newSellPrice;
buyPrice = newBuyPrice;
}
function buy() public payable returns (uint amount){
amount = msg.value / buyPrice; // calculates the amount
require(balanceOf[this] >= amount); // checks if it has enough to sell
balanceOf[msg.sender] += amount; // adds the amount to buyer's balance
balanceOf[this] -= amount; // subtracts amount from seller's balance
emit Transfer(this, msg.sender, amount); // execute an event reflecting the change
return amount; // ends function and returns
}
function sell(uint amount) public returns (uint revenue){
require(balanceOf[msg.sender] >= amount); // checks if the sender has enough to sell
balanceOf[this] += amount; // adds the amount to owner's balance
balanceOf[msg.sender] -= amount; // subtracts the amount from seller's balance
revenue = amount * sellPrice;
msg.sender.transfer(revenue); // sends ether to the seller: it's important to do this last to prevent recursion attacks
emit Transfer(msg.sender, this, amount); // executes an event reflecting on the change
return revenue; // ends function and returns
}
uint minBalanceForAccounts;
function setMinBalance(uint minimumBalanceInFinney) public onlyOwner {
minBalanceForAccounts = minimumBalanceInFinney * 1 finney;
}
function giveBlockReward() public {
balanceOf[block.coinbase] += 1;
}
function proofOfWork(uint nonce) public {
bytes8 n = bytes8(keccak256(nonce, currentChallenge)); // Generate a random hash based on input
require(n >= bytes8(difficulty)); // Check if it's under the difficulty
uint timeSinceLastProof = (now - timeOfLastProof); // Calculate time since last reward was given
require(timeSinceLastProof >= 5 seconds); // Rewards cannot be given too quickly
balanceOf[msg.sender] += timeSinceLastProof / 60 seconds; // The reward to the winner grows by the minute
difficulty = difficulty * 10 minutes / timeSinceLastProof + 1; // Adjusts the difficulty
timeOfLastProof = now; // Reset the counter
currentChallenge = keccak256(nonce, currentChallenge, block.blockhash(block.number - 1)); // Save a hash that will be used as the next proof
}
}
Развертывание
Прокрутите вниз и вы увидите оцененную стоимость развертывания контракта. Если хотите, то можете изменить регулятор на меньшее вознаграждение, но если цена будет слишком низкой относительно среднерыночной, ваша транзакция может собираться дольше. Нажмите «Deploy» и введите ваш пароль. Через несколько секунд вы будете перенаправлены на панель управления и под «Latest transactions» вы увидите строку «Сreating contract». Подождите несколько секунд того, кто соберет вашу транзакцию, а затем вы увидите голубой прямоугольник, показывающий, сколько еще нод видели вашу транзакцию и подтвердили ее. Чем больше подтверждений у вас есть, тем больше гарантия того, что ваш код будет использован.
Нажмите на ссылку Admin page и вы переместитесь на простейшую панель управления центрального банка, где вы сможете делать, что угодно с вашей только что созданной валютой.
Слева под «Read from contract» у вас есть все опции и функции, которые вы можете использовать, чтобы бесплатно прочитать информацию из контракта. Если у вашего токена есть владелец, то здесь будет отображаться адрес токена. Скопируйте адрес и вставьте его в «Balance of», это покажет вам баланс любого аккаунта.
Справа под «Write to Contract» вы увидете все функции, которые вы можете использовать, чтобы как угодно изменить блокчейн. Все это будет стоить газа. Если вы создали контракт, который позволяет вам чеканить новые монеты, у вас должна быть функция «Mint Token». Выберите ее.
Выберите адрес, где эти новые валюты будут созданы, а затем объем (если у вас есть десятичные значения, выберите 2 знака после запятой, затем добавьте 2 нуля после заданного объема, чтобы создать правильное количество). В «Execute from» выберите аккаунт, который назначает владельца, оставьте объем эфиров на нуле и затем нажмите «Execute».
После нескольких подтверждений, баланс получателя будет обновлен и отразит новый объем токенов. Но кошелек получателя может не показывать ваш токен по умолчанию: чтобы добавить информацию о купленных токенах, нужно добавить их вручную в лист просмотра в кошельке. Скопируйте адрес вашего токена (на странице администратора нажмите «Сopy address») и отправьте его получателю. Если получатель еще этого не сделал, то он должен перейти к таблице контрактов, нажать «Watch Token» и затем добавьте адрес туда. Имя, символ и десятичный номер могут быть настроены конечным пользователем, особенно если у них есть другие токены с похожими или такими же именами. Главное, чтобы иконка была неизменна. Пользователям необходимо обратить внимание на нее при отправлении и получении токенов, чтобы убедиться, что они имеют дело с реальной сделкой, а не с токеном-подражателем.
Использование созданной монеты
После того, как вы используете свои токены, они будут добавлены к вашему листу просмотренных токенов, а итоговый баланс будет показан в вашем аккаунте. Чтобы отправить токены, просто перейдите к таблице «Send». Токены, которые есть в аккаунте, будут перечислены прямо под Ether. Выберите их и потом введите количество токенов, которое вы хотите отправить.
Если вы хотите добавить чей-то еще токен, просто перейдите к таблице Contracts и нажмите «Watch token».
Что теперь?
Вы только что научились использовать Ethereum, чтобы выпустить токен, который может представлять собой все, что вам угодно. Но что можно делать с этими токенами? Вы можете использовать токены, чтобы, например, представить акцию компании (represent a share in a company). Или вы можете использовать центральный комитет (central committee), чтобы голосовать, когда выпускать новые монеты, с целью контроля инфляции. Вы также можете использовать их, чтобы собрать деньги для дела с помощью crowdsale.
Некоторые из этих тем будут разобраны в следующих статьях. Данная статья скоро будет переведена на английский и опубликована в блоге Airalab.