В первой части этой серии руководств по созданию DApps с Ethereum мы загрузили две версии локального блокчейна для разработки: версию Ganache и полную приватную версию PoA.
В этой части мы подробно расскажем о нем и создадим и развернем наш токен TNS, который пользователи будут использовать для голосования по предложениям в Story DAO.
Предпосылки
Подготовьте и запустите версию Ganache в соответствии с предыдущей частью. В качестве альтернативы, можно запустить любую локальную версию цепочки блоков, если вы не следуете первой части, но убедитесь, что вы можете подключиться к ней с помощью инструментов, которые нам понадобятся .
Мы предполагаем, что у вас есть работающий приватный блокчейн и возможность набирать команды в его консоли и терминале операционной системы через приложение Terminal или, в Windows, приложение, такое как Git Bash, Console, CMD Prompt, Powershell и т. Д.
Основные зависимости
Для разработки нашего приложения мы можем использовать одну из нескольких платформ и стартовых наборов: Dapp , eth-utils , Populus , Embark … и так далее. Но мы пойдем с нынешним королем экосистемы, Трюфелем .
Установите это со следующим:
npm install -g truffle
Это сделает truffle
команду доступной везде. Теперь мы можем начать проект с truffle init
.
Запуск токена
Давайте вникнем в это и создадим наш токен. Это будет стандартный маркер для печенья ERC20 с изюминкой. (Вы увидите, какой поворот ниже в этом посте.) Сначала мы добавим некоторые зависимости. Библиотеки OpenZeppelin — это проверенные в бою высококачественные контракты на надежность, которые можно использовать для расширения и создания контрактов.
npm install openzeppelin-solidity
Далее, давайте создадим новый файл токена:
truffle create contract TNSToken
Шаблон по умолчанию, который генерирует трюфель, немного устарел, поэтому давайте обновим его:
pragma solidity ^0.4.24; contract TNStoken { constructor() public { } }
До сих пор конструктор токен-контракта должен был называться так же, как и сам контракт, но для ясности он был изменен на constructor
. В нем также всегда должен быть модификатор, сообщающий компилятору, кому разрешено развертывать и взаимодействовать с этим контрактом ( public
есть каждый).
SafeMath
Единственный контракт Zeppelin, который мы будем использовать в этом случае, это контракт SafeMath
. В Solidity мы импортируем контракты с ключевым словом import
, в то время как компилятору обычно не требуется полный путь, только относительный, например:
pragma solidity ^0.4.24; import "../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol"; contract TNStoken { using SafeMath for uint256; constructor() public { } }
Итак, что такое SafeMath
? Давным-давно существовала проблема создания 184 миллиардов биткойнов из-за математической проблемы в коде. Чтобы предотвратить проблемы, даже отдаленно похожие на эти (не то, что это возможно в Ethereum), существует библиотека SafeMath. Когда два числа имеют размер MAX_INT
(т. MAX_INT
Максимально возможное число в операционной системе), их суммирование приведет к тому, что значение «обернется» до нуля, подобно тому, как одометр автомобиля сбрасывается в 0 после достижения 999999 километров. Таким образом, библиотека SafeMath имеет такие функции:
/** * @dev Adds two numbers, throws on overflow. */ function add(uint256 a, uint256 b) internal pure returns (uint256 c) { c = a + b; assert(c >= a); return c; }
Эта функция предотвращает эту проблему: она проверяет, является ли сумма двух чисел больше, чем каждый из двух операндов.
Хотя делать глупые ошибки при написании контрактов Solidity не так просто, все же лучше быть в безопасности, чем сожалеть.
using SafeMath for uint256
, мы заменяем эти «безопасные» версии стандартными числами uint256 в Solidity (256-битные без знака — иначе говоря, только положительные — целые числа). Вместо суммирования чисел, таких как: sum = someBigNumber + someBiggerNumber
, мы будем суммировать их следующим образом: sum = someBigNumber.add(someBiggerNumber)
, что обеспечивает безопасность наших вычислений.
ERC20 с нуля
С нашей математикой, сделанной безопасной, мы можем создать наш токен.
ERC20 — это стандарт с четко определенным интерфейсом, поэтому для справки давайте добавим его в контракт. Читайте о стандартах токенов здесь .
Итак, функции, которые должен иметь токен ERC20:
pragma solidity ^0.4.24; import "../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol"; contract ERC20 { function totalSupply() public view returns (uint256); function balanceOf(address who) public view returns (uint256); function transfer(address to, uint256 value) public returns (bool); event Transfer(address indexed from, address indexed to, uint256 value); function allowance(address owner, address spender) public view returns (uint256); function transferFrom(address from, address to, uint256 value) public returns (bool); function approve(address spender, uint256 value) public returns (bool); event Approval(address indexed owner, address indexed spender, uint256 value); } contract TNStoken { using SafeMath for uint256; constructor() public { } }
Это может показаться сложным, но на самом деле это очень просто. Это «каталог» функций, которые должен иметь наш токен, и мы будем строить их одну за другой, объясняя, что означает каждая из них. Рассмотрим выше интерфейс для нашего токена. Мы увидим, как и почему это полезно, когда мы создадим приложение Story DAO.
Основные остатки
Давайте начнем. Токен — это просто «электронная таблица» в блокчейне Ethereum, например:
| Name | Amount | |:--|:--| | Bruno | 4000 | | Joe | 5000 | | Anne | 0 | | Mike | 300 |
Итак, давайте создадим mapping
, которое по сути точно похоже на электронную таблицу в контракте:
mapping(address => uint256) balances;
В соответствии с интерфейсом выше, это должно сопровождаться функцией balanceOf
, которая может читать эту таблицу:
function balanceOf(address _owner) public view returns (uint256) { return balances[_owner]; }
Функция balanceOf
принимает один аргумент: _owner
является общедоступным (может использоваться любым), является функцией представления (что означает, что она бесплатна — не требует транзакции) и возвращает номер uint256
, остаток владельца адреса отправлено. Баланс токенов каждого читателя доступен для общественности.
Общее предложение
Знание общего запаса токена важно для его пользователей и для приложений отслеживания монет, поэтому давайте определим свойство контракта (переменную) для отслеживания этой и другой бесплатной функции, с помощью которой можно прочитать это:
uint256 totalSupply_; function totalSupply() public view returns (uint256) { return totalSupply_; }
Отправка токенов
Далее, давайте удостоверимся, что владелец ряда токенов может передать их кому-то еще. Мы также хотим знать, когда произошла передача, поэтому мы также определим событие Transfer. Событие Transfer позволяет нам прослушивать передачи в блокчейне через JavaScript, чтобы наши приложения могли знать, когда эти события генерируются, вместо того, чтобы постоянно вручную проверять, произошла ли передача. События объявляются вместе с переменными в контракте и отправляются с ключевым словом emit
. Давайте добавим следующее в наш контракт сейчас:
event Transfer(address indexed from, address indexed to, uint256 value); function transfer(address _to, uint256 _value) public returns (bool) { require(_to != address(0)); require(_value <= balances[msg.sender]); balances[msg.sender] = balances[msg.sender].sub(_value); balances[_to] = balances[_to].add(_value); emit Transfer(msg.sender, _to, _value); return true; }
Эта функция принимает два аргумента: _to
, который является адресом назначения, который получит токены, и value
, которое является количеством токенов. Важно помнить, что value
— это количество наименьших единиц токена, а не целых единиц. Таким образом, если объявлен токен с 10 десятичными знаками, то для отправки одного токена необходимо отправить 10000000000. Этот уровень детализации позволяет нам совершать незначительные суммы.
Функция общедоступна, то есть ее может использовать любой — и другие контракты, и пользователи — и она возвращает true
если операция прошла успешно.
Затем функция выполняет некоторые проверки работоспособности. Во-первых, он проверяет, что адрес назначения не является нулевым адресом. Другими словами, токены нельзя отправлять в забвение. Затем он проверяет, разрешено ли отправителю отправлять столько токенов, сравнивая их баланс ( balances[msg.sender]
) со значением, переданным для отправки. Если любая из этих проверок не пройдена, функция отклонит транзакцию и завершится неудачно. Он возместит любые отправленные токены, но будет потрачен газ, потраченный на выполнение функции до этой точки.
Следующие две строки вычитают количество токенов из баланса отправителя и добавляют эту сумму к балансу получателя. Затем событие отправляется с помощью emit
, и в него передаются некоторые значения: отправитель, получатель и сумма. Любой клиент, подписанный на события Transfer по этому контракту, теперь будет уведомлен об этом событии.
Хорошо, теперь наши держатели токенов могут отправлять токены. Верьте или нет, это все, что вам нужно для базового токена. Но мы идем дальше и добавляем больше функциональности.
пособие
Иногда третьему лицу может быть предоставлено разрешение на снятие средств с баланса другого счета. Это полезно для игр, которые могут облегчить внутриигровые покупки, децентрализованные обмены и многое другое. Мы делаем это путем построения многомерного mapping
называемого allowance
, в котором хранятся все такие разрешения. Давайте добавим следующее:
mapping (address => mapping (address => uint256)) internal allowed; event Approval(address indexed owner, address indexed spender, uint256 value);
Это событие для того, чтобы слушатели приложений могли знать, когда кто-то предварительно утвердил траты своего баланса кем-то другим — полезная функция и часть стандарта .
Отображение объединяет адреса с другим отображением, которое объединяет адреса с числами. Он в основном формирует электронную таблицу, такую как эта:
Таким образом, баланс Боба может быть потрачен Мэри до 1000 жетонов, а Билли до 50 жетонов. Баланс Мэри может быть потрачен Бобом до 750 жетонов. Баланс Билли может быть потрачен до 300 жетонов Мэри и 1500 Джо.
Учитывая, что это отображение является internal
, оно может использоваться только функциями в этом контракте и контрактах, которые используют этот контракт в качестве базы.
Чтобы утвердить чужие расходы с вашего аккаунта, вы вызываете функцию approve
с адресом лица, которому разрешено тратить ваши токены, суммой, которую им разрешено тратить, и в функции, которую вы генерируете событие Approval
:
function approve(address _spender, uint256 _value) public returns (bool) { allowed[msg.sender][_spender] = _value; emit Approval(msg.sender, _spender, _value); return true; }
Нам также нужен способ узнать, сколько пользователь может потратить с учетной записи другого пользователя:
function allowance(address _owner, address _spender) public view returns (uint256) { return allowed[_owner][_spender]; }
Так что это еще одна функция read only
( view
), что означает, что она может выполняться бесплатно. Он просто читает остаток средств на счету.
Так как же отправить за другого? С новой функцией TransferFrom:
function transferFrom(address _from, address _to, uint256 _value) public returns (bool) { require(_to != address(0)); require(_value <= balances[_from]); require(_value <= allowed[_from][msg.sender]); balances[_from] = balances[_from].sub(_value); balances[_to] = balances[_to].add(_value); allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value); emit Transfer(_from, _to, _value); return true; }
Как и прежде, существуют проверки работоспособности: адрес назначения не должен быть нулевым, поэтому не следует отправлять токены в черную дыру. Передаваемое значение также должно быть меньше или равно не только текущему балансу счета, с которого переводится значение, но также и балансу, который отправитель сообщения (адрес, инициирующий эту транзакцию) все еще может потратить на них.
Затем баланс обновляется, и разрешенный баланс синхронизируется с этим перед отправкой события о переносе.
Примечание: владелец токена может тратить токены без обновления allowed
отображения. Это может произойти, если владелец токена отправляет токены вручную, используя transfer
. В этом случае возможно, что у держателя будет меньше токенов, чем предписывает третье лицо.
Имея одобрения и допуски, мы также можем создавать функции, которые позволяют владельцу токена увеличивать или уменьшать чьи-то допуски, а не перезаписывать значение полностью. Попробуйте сделать это в качестве упражнения, а затем обратитесь к приведенному ниже исходному коду для решения.
function increaseApproval(address _spender, uint _addedValue) public returns (bool) { allowed[msg.sender][_spender] = ( allowed[msg.sender][_spender].add(_addedValue)); emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]); return true; } function decreaseApproval(address _spender, uint _subtractedValue) public returns (bool) { uint oldValue = allowed[msg.sender][_spender]; if (_subtractedValue > oldValue) { allowed[msg.sender][_spender] = 0; } else { allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue); } emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]); return true; }
Конструктор
Пока что мы просто создали токеновый «контракт». Но что это за знак? Как это называется? Сколько десятичных знаков у него есть? Как мы это используем?
В самом начале мы определили функцию constructor
. Теперь давайте закончим его тело и добавим атрибуты name
, symbol
и decimals
:
string public name; string public symbol; uint8 public decimals; constructor(string _name, string _symbol, uint8 _decimals, uint256 _totalSupply) public { name = _name; symbol = _symbol; decimals = _decimals; totalSupply_ = _totalSupply; }
Выполнение этого позволяет нам повторно использовать контракт позже для других токенов того же типа. Но поскольку мы точно знаем, что мы строим, давайте жестко закодируем эти значения:
string public name; string public symbol; uint8 public decimals; constructor() public { name = "The Neverending Story Token; symbol = "TNS"; decimals = 18; totalSupply_ = 100 * 10**6 * 10**18; }
Эти данные читаются различными инструментами и платформами Ethereum при отображении информации токена. Функция конструктора вызывается автоматически при развертывании контракта в сети Ethereum, поэтому эти значения будут автоматически настроены во время развертывания.
Слово о totalSupply_ = 100 * 10**6 * 10**18;
Это просто способ облегчить людям чтение числа. Поскольку все переводы в Ethereum выполняются с наименьшей единицей эфира или токена (включая десятичные дроби), наименьшая единица составляет 18 знаков после запятой. Вот почему один токен TNS равен 1 * 10**18*
. Более того, нам нужно 100 миллионов, то есть 100 * 10**6
или 100*10*10*10*10*10*10
. Это делает число намного более читабельным, чем 100000000000000000000000000
.
Поток альтернативного развития
В качестве альтернативы, мы можем просто продлить контракт Zeppelin, изменить некоторые атрибуты, и у нас есть наш токен. Это то, что делает большинство людей, но когда я имею дело с программным обеспечением, которое потенциально может обрабатывать миллионы денег других людей, я лично стремлюсь точно знать , что я вкладываю в код, поэтому повторное использование кода в моем случае минимально.
pragma solidity ^0.4.24; import "../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol"; import "../node_modules/openzeppelin-solidity/contracts/token/ERC827/ERC20Token.sol"; contract TNStoken is ERC20Token { using SafeMath for uint256; string public name; string public symbol; uint8 public decimals; uint256 totalSupply_; constructor() public { name = "The Neverending Story Token"; symbol = "TNS"; decimals = 18; totalSupply_ = 100 * 10**6 * 10**18; } }
В этом случае мы используем нотацию is
чтобы объявить наш токен is ERC20Token
. Это заставляет наш токен расширять контракт ERC20, который, в свою очередь, расширяет StandardToken и так далее…
В любом случае, наш токен готов. Но у кого сколько токенов для начала и как?
Первоначальный баланс
Давайте дадим создателю контракта все жетоны. В противном случае токены не будут отправлены никому. Обновите constructor
, добавив в конец следующую строку:
balances[msg.sender] = totalSupply_;
Блокировка токена
Поскольку мы намерены использовать токены в качестве средства голосования (т. Е. Сколько токенов, которые вы заблокировали во время голосования, показывает, насколько мощным является ваш голос), нам нужен способ запретить пользователям отправлять их после голосования, иначе наш DAO будет восприимчив к атака Сибил — один человек с миллионом токенов может зарегистрировать 100 адресов и получить право голоса в 100 миллионов токенов, просто отправив их по разным адресам и повторно проголосовав с новым адресом. Таким образом, мы предотвратим передачу точно такого количества токенов, сколько человек посвятил голосованию, кумулятивно за каждый голос по каждому предложению. Это поворот, о котором мы упоминали в начале этого поста. Давайте добавим следующее событие в наш контракт:
event Locked(address indexed owner, uint256 indexed amount);
Тогда давайте добавим методы блокировки:
function increaseLockedAmount(address _owner, uint256 _amount) onlyOwner public returns (uint256) { uint256 lockingAmount = locked[_owner].add(_amount); require(balanceOf(_owner) >= lockingAmount, "Locking amount must not exceed balance"); locked[_owner] = lockingAmount; emit Locked(_owner, lockingAmount); return lockingAmount; } function decreaseLockedAmount(address _owner, uint256 _amount) onlyOwner public returns (uint256) { uint256 amt = _amount; require(locked[_owner] > 0, "Cannot go negative. Already at 0 locked tokens."); if (amt > locked[_owner]) { amt = locked[_owner]; } uint256 lockingAmount = locked[_owner].sub(amt); locked[_owner] = lockingAmount; emit Locked(_owner, lockingAmount); return lockingAmount; }
Каждый метод гарантирует, что никакая незаконная сумма не может быть заблокирована или разблокирована, а затем генерирует событие после изменения заблокированной суммы для данного адреса. Каждая функция также возвращает новую сумму, которая теперь заблокирована для этого пользователя. Это все еще не мешает отправке, хотя. Изменим transfer
и transferFrom
:
function transfer(address _to, uint256 _value) public returns (bool) { require(_to != address(0)); require(_value <= balances[msg.sender] - locked[msg.sender]); // <-- THIS LINE IS DIFFERENT // ... function transferFrom(address _from, address _to, uint256 _value) public returns (bool) { require(_to != address(0)); require(_value <= balances[_from] - locked[_from]); require(_value <= allowed[_from][msg.sender] - locked[_from]); // <-- THIS LINE IS DIFFERENT // ...
Наконец, нам нужно знать, сколько токенов заблокировано или разблокировано для пользователя:
function getLockedAmount(address _owner) view public returns (uint256) { return locked[_owner]; } function getUnlockedAmount(address _owner) view public returns (uint256) { return balances[_owner].sub(locked[_owner]); }
Вот и все: наш токен теперь блокируется извне, но только владельцем токен-контракта (который будет представлять собой DAO Story, который мы будем строить в следующих уроках). Давайте сделаем токен-контракт Ownable — т.е. позволим ему иметь владельца. Импортировать Ownable
контракт с помощью import "../node_modules/openzeppelin-solidity/contracts/ownership/Ownable.sol";
а затем измените эту строку:
contract StoryDao {
… Быть этим:
contract StoryDao is Ownable {
Полный код
Полный код токена с комментариями для пользовательских функций на данный момент выглядит следующим образом:
pragma solidity ^0.4.24; import "../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol"; import "../node_modules/openzeppelin-solidity/contracts/ownership/Ownable.sol"; contract TNStoken is Ownable { using SafeMath for uint256; mapping(address => uint256) balances; mapping(address => uint256) locked; mapping (address => mapping (address => uint256)) internal allowed; uint256 totalSupply_; event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); event Locked(address indexed owner, uint256 indexed amount); string public name; string public symbol; uint8 public decimals; constructor() public { name = "The Neverending Story Token"; symbol = "TNS"; decimals = 18; totalSupply_ = 100 * 10**6 * 10**18; balances[msg.sender] = totalSupply_; } /** @dev _owner will be prevented from sending _amount of tokens. Anything beyond this amount will be spendable. */ function increaseLockedAmount(address _owner, uint256 _amount) public onlyOwner returns (uint256) { uint256 lockingAmount = locked[_owner].add(_amount); require(balanceOf(_owner) >= lockingAmount, "Locking amount must not exceed balance"); locked[_owner] = lockingAmount; emit Locked(_owner, lockingAmount); return lockingAmount; } /** @dev _owner will be allowed to send _amount of tokens again. Anything remaining locked will still not be spendable. If the _amount is greater than the locked amount, the locked amount is zeroed out. Cannot be neg. */ function decreaseLockedAmount(address _owner, uint256 _amount) public onlyOwner returns (uint256) { uint256 amt = _amount; require(locked[_owner] > 0, "Cannot go negative. Already at 0 locked tokens."); if (amt > locked[_owner]) { amt = locked[_owner]; } uint256 lockingAmount = locked[_owner].sub(amt); locked[_owner] = lockingAmount; emit Locked(_owner, lockingAmount); return lockingAmount; } function transfer(address _to, uint256 _value) public returns (bool) { require(_to != address(0)); require(_value <= balances[msg.sender] - locked[msg.sender]); balances[msg.sender] = balances[msg.sender].sub(_value); balances[_to] = balances[_to].add(_value); emit Transfer(msg.sender, _to, _value); return true; } function approve(address _spender, uint256 _value) public returns (bool) { allowed[msg.sender][_spender] = _value; emit Approval(msg.sender, _spender, _value); return true; } function transferFrom(address _from, address _to, uint256 _value) public returns (bool) { require(_to != address(0)); require(_value <= balances[_from] - locked[_from]); require(_value <= allowed[_from][msg.sender] - locked[_from]); balances[_from] = balances[_from].sub(_value); balances[_to] = balances[_to].add(_value); allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value); emit Transfer(_from, _to, _value); return true; } function increaseApproval(address _spender, uint _addedValue) public returns (bool) { allowed[msg.sender][_spender] = ( allowed[msg.sender][_spender].add(_addedValue)); emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]); return true; } function decreaseApproval(address _spender, uint _subtractedValue) public returns (bool) { uint oldValue = allowed[msg.sender][_spender]; if (_subtractedValue > oldValue) { allowed[msg.sender][_spender] = 0; } else { allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue); } emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]); return true; } /** @dev Returns number of tokens the address is still prevented from using */ function getLockedAmount(address _owner) public view returns (uint256) { return locked[_owner]; } /** @dev Returns number of tokens the address is allowed to send */ function getUnlockedAmount(address _owner) public view returns (uint256) { return balances[_owner].sub(locked[_owner]); } function balanceOf(address _owner) public view returns (uint256) { return balances[_owner]; } function totalSupply() public view returns (uint256) { return totalSupply_; } function allowance(address _owner, address _spender) public view returns (uint256) { return allowed[_owner][_spender]; } }
Вывод
Эта часть помогла нам создать базовый токен, который мы будем использовать в качестве токена участия / обмена в «Бесконечной истории». Несмотря на то, что токен имеет полезность , он по определению является активом, который контролирует решения большего органа токена безопасности . Помните о разнице .
В следующей части этой серии мы узнаем, как скомпилировать, развернуть и протестировать этот токен.