Пишем смарт-контракт Ethereum — это просто: Часть 5 — токен ERC20

В этом уроке мы разберем стандарт ERC20 и напишем собственную монету.

Предыдущий урок можно посмотреть тут.Полный список уроков тут.

В мире Ethereum существует стандарт токена или монеты. Если ваш контракт отвечает стандарту, то его можно назвать монетой (валютой или токеном — кому как удобно). И в этом случае кошельки и биржи смогут работать с вашей монетой.

Стандарт монеты на базе эфира называется — ERC20. А требования его примерно следующие:

Чтобы смарт-контракт считался валютой он должен содержать:

  1. Методы
  2. События

На момент написания статьи ERC20 сложно было назвать стандартом. Это было скорее соглашение между разработчиками. Во-первых ERC расшифровывается как Ethereum Request for Comments. Т.е это обсуждаемое решение. Во-вторых в ERC20 еще не были включены функции symbol, decimals и  name. И в-третьих было указано, что есть важные или обязательные требования:

The most important here are, transfer, balanceOf and the Transfer event.

Однако на сегодняшний день ERC20 уже официально является стандартом.

Контракт который выполняет обязательные требования можно назвать частично-совместимыми с ERC20. Однако такой контракт будет работать со многими приложениями.

Когда ERC20 еще не был стандартом существовали негласные правила (на вики это называлось — «дополнительная информация») — наличие трех полей в внутри контракта. На сегодняшний в ERC20 перечисленные поля уже включены.

На текущий момент эти правила также входят в стандарт ERC223.

В идеале ваша монета должна выполнять все соглашения.

А теперь давайте рассмотрим эти требования подробней:

  1. Метод

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

    Возвращает количество монет принадлежащих _owner. Может вызвать любой. Кошельки для отображения вашей монеты вызывают именно эту функцию.
  3. Метод

    Передает _value монет на адрес _to. Когда пользователь будет перемещать свои монеты на другой адрес вызываться будет именно эта функция. Соответственно монеты должны браться с баланса пользователя, который вызвал эту функцию. Метод должен создавать событие Transfer (описан будет далее) в случае успешного перемещения монет.
  4. Метод

    Передает _ value монет от _from к _to. Пользователь должен иметь разрешение на перемещение монеток между адресами, дабы любой желающий не смог управлять чужими кошельками. Фактически эта функция позволяет вашему доверенному лицу распоряжаться определенным объемом монеток на вашем счету. Дать разрешение на управление средствами можно следующей функцией. Метод должен создавать событие Transfer (описан будет далее) в случае успешного перемещения монет.
  5. Метод

    Разрешает пользователю _spender снимать с вашего счета (точнее со счета вызвавшего функцию пользователя) средства не более чем _value. На основе этого разрешения должна работать функция transferFrom. Метод должен создавать событие Approval (описан будет далее).
  6. Метод

    Возвращает сколько монет со своего счета разрешил снимать пользователь _owner пользователю _spender.
  7. Событие

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

    Событие должно возникать при получении разрешения на снятие монет. Фактически должно создаваться внутри функции  approve.
  9. Поле

    Хранит полное название вашей монеты
  10. Поле

    Хранит короткое название вашей монеты. Иначе говоря — символ. С этим символом ваша валюта будет отображаться на биржах и в кошельках.
  11. Поле

    Количество знаков после запятой. 18 — это наиболее распространенное значение.

Как видите требований не много и они простые. Давайте напишем нашу монету. Называться она будет «Simple coin token», символ «SCT» и иметь 18 знаков после запятой. Пока реализуем основные поля и функцию transfer .

Балансы всех пользователей у нас хранятся в поле balances. В котором каждому адресу пользователя соответствует количество монет.

Функция transfer довольно простая. В своей первой строчке она просто вычитает из баланса пользователя msg.sender (тот кто вызвал функцию) _value монет. А во второй строчке прибавляет к балансу пользователя _to то же количество монет. Потом создается событие и возвращается true в знак того что перемещение монет успешно выполнено.

Код функции transferFrom будет почти такой же.  Попробуйте его написать самостоятельно.

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

Стоит отметить что переменные могут хранить какое-то максимальное число. Если попытаться сохранить число больше то мы переменная переполнится и станет отрицательной. Именно поэтому добавляют еще проверку на переполнение. В нашем случае оно будет таким:

А полностью функция transfer будет выглядеть так

Добавьте такие же проверки в transferFrom самостоятельно.

После всех наших махинаций transferFrom должна выглядеть примерно так:

Давайте посмотрим как работает transfer  и transferFrom. Для того чтобы мы смогли выпускать новые монеты на баланс любого пользователя — добавьте функцию mint (в переводе — чеканить):

После этого залейте контракт в Remix (как это сделать написано в первом уроке).

Сейчас у нас ни у кого из пользователей нет наших монет. Проверим это вызвав balanceOf для произвольного адреса, например для 0xea15adb66dc92a4bbccc8bf32fd25e2e86a2a770.

Как видим баланс равен нулю. Теперь попробуем переместить с нашего баланса 100 монет на баланс другого пользователя с помощью  transfer.

В некоторых случаях remix напрямую не показывает результат выполнения. Тогда его можно посмотреть в консоле. Для этого нужно нажать на кнопочку details напротив нашей операции.

И тогда в консоле появится табличка с подробностями выполнения операции. Найдите строку с «decoded output» в левом столбце и увидите в правом результат.

Как видим функция вернула false. Потому что монет на нашем счету сейчас нет. Значит наша проверка отработала. То что монеты не перемещались вы также можете убедится еще раз вызвав balanceOf для адреса на который пытались переместить.

Давайте выпустим 100 монет на наш баланс, для этого вызовем функцию mint с нашим адресом. Напоминаю ваш текущий адрес в тестовой среде Remix указан в поле Account. Чтобы его скопировать нужно нажать на кнопочку рядом с полем как на картинке.

Итак вызываем mint

А затем balanceOf  на тот же адрес, чтобы проверить что монетки выпустились

Как видим на балансе у нас 100 монеток.

Теперь давайте переместим 40 монеток c нашего адреса на 0xea15adb66dc92a4bbccc8bf32fd25e2e86a2a770 с помощью transfer.

Посмотрим что вернул transfer в консоле.

transfer вернул нам true, значит успешно отработал. Теперь на аккаунте 0xea15adb66dc92a4bbccc8bf32fd25e2e86a2a770 должно быть 40 монет. Проверим с помощью balanceOf

Как мы и ожидали! А на балансе нашего аккаунта должно остаться 60, проверяем

Мы убедились что tranfer работает как надо. Протестируйте самостоятельно работу функции transferFrom.

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

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

Тут лучше всего подходит двойной mapping.  Первый ключ — адрес на снятие с которого предоставляется разрешение. Второй ключ — пользователь, которому предоставляется разрешение.

Добавьте это поле в контракт.

Займемся функцией allowance. Сумма которая разрешена пользователю _spender для снятия со счета _owner

Тогда наша функция allowance будет выглядеть так

Теперь напишем функцию approve.

Чтобы пользователю _spender предоставить разрешение на снятие _value монет со счета выполняющего контракт нужно записать

И не забудем добавить создания события Approval. И вот код approve

Осталось исправить transferFrom таким образом, чтобы была проверка на разрешенное количество монет.

И если снятие монет успешно, то уменьшаем разрешенное количество монет на величину снятия.

Новая версия transferFrom  теперь выглядит так:

Теперь осталось только сделать так чтобы выпуск новых монет был разрешен только владельцу контракта. В четвертом уроке для этого мы написали контракт Ownablе который содержит необходимый модификатор onlyOwner унаследуем наш контракт монеты от  Ownable и добавим к mint модификатор onlyOwner.

Теперь контракт нашей монетки выглядит так.

Если сравните требования ERC20 с нашим контрактом то заметите, что нет функции totalSupply. Дело в том, что для публичных полей функции возврата, так называемые геттеры, создаются автоматически, когда уже контракт будет залит в блокчейн.

Попробуйте поиграться и проверить как работают функции предоставления разрешения на снятие средств.

В этом уроке мы написали контракт нашей первой монетки.  И он полностью совместим с ERC20! На Эфире уже написано большое количество монет и сложились некоторые общие шаблоны. В следующем уроке мы рассмотрим один из распространенных способов написания монеты ERC20. И изменим наш контракт в соответствии с общепринятой практикой.

Продолжение читать тут. Предыдущий урок тут.

Если у вас возникли вопросы то можете смело писать на электронную почту (раздел «контакты«). Также приветствуется критика.

Если статья показалась вам полезной и вы желаете отблагодарить автора, то это можно сделать отослав немного эфира на адрес 0xEA15Adb66DC92a4BbCcC8Bf32fd25E2e86a2A770.

Полный список уроков тут.

 

1 Комментарий

  1. Проверка на переполнение.
    balances[msg.sender] + _value >= balances[msg.sender]

    А тут точно верно?

    Проверять нужно destination баланс на переполнение, то есть:
    balances[_to] + _value >= balances[_to]

  2. Здравствуйте.
    Мне не понятна суть событий (event) в солидити. Зачем они нужны? Что происходит, когда исполнение программного кода доходит до строк event Transfer(address indexed _from, address indexed _to, uint256 _value) или
    event Approval. Заранее спасибо.

    • События — это способ логирования. Какое-нибудь dapp может слушать нужные ей события. Например etherscan на основе события Transfer узнает кто кому сколько монет отправил и рассчитывает на основе этого доли монет у каждого инвестора.

  3. Здравствуйте inaword!
    Сделал transferOwner на новый адрес и от нового имени запустил «mint» 100.
    Вроде как функция «mint» отработала, но баланс =0.

    А вот с адреса прежнего Owner функция «mint» по прежнему отрабатывает правильно.
    В чём тут дело? Функция «mint» не может передана на newOwner?

    [vm] from:0xdd8…92148, to:browser/SCToken.sol:SimpleTokenCoin.mint(address,uint256) 0x860…24b9b, value:0 wei, data:0x40c…00064, 0 logs, hash:0x4e5…a6846
    Details
    Debug
    from 0xdd870fa1b7c4700f2bd7f44238821c26f7392148
    to browser/SCToken.sol:SimpleTokenCoin.mint(address,uint256) 0x8609a0806279c94bcc5432e36b57281b3d524b9b
    gas 3000000 gas
    transaction cost 3000000 gas
    execution cost 2977128 gas
    hash 0x4e54bd6cc7cf441903471fbd2f7c039d21a5c1ea8b0bbb10193353c5b13a6846
    input 0x40c10f19000000000000000000000000dd870fa1b7c4700f2bd7f44238821c26f73921480000000000000000000000000000000000000000000000000000000000000064
    decoded input {
    «address _to»: «0xdd870fa1b7c4700f2bd7f44238821c26f7392148»,
    «uint256 _value»: «100»
    }
    decoded output {}
    logs []
    value 0 wei
    transact to browser/SCToken.sol:SimpleTokenCoin.mint errored: VM error: invalid opcode.
    The constructor should be payable if you send value.
    The execution might have thrown.
    Debug the transaction to get more information.
    call to browser/SCToken.sol:SimpleTokenCoin.balanceOf
    [call] from: — , to:browser/SCToken.sol:SimpleTokenCoin.balanceOf(address) 0x860…24b9b, data:70a08…92148, return:
    Debug
    {
    «uint256 balance»: «0»

    • Разобрался:
      function mint работает правильно.

      Не срабатывала function transferOwnership:

      17 строка: owner == newOwner;
      нужно: owner = newOwner;

  4. Ещё вопрос: в первой строке функции не должно быть «onlyOwner»?

    function approve(address _spender, uint _value) returns (bool success) {
    allowed[msg.sender][_spender] = _value;
    Approval(msg.sender, _spender, _value);

    • Нет, потому что тот кто запускает функцию разрешает доступ только к своему балансу (msg.sedner — тот, кто вызывает функцию)
      allowed[msg.sender][_spender] = _value;

  5. В дополнение к предыдущему посту:
    Проверка function approve показала:
    1) в таком виде она даёт возможность любому flhtce установить лимит на транзакцию transferFrom со счёта Owner
    2) транзакции проводятся и свыше установленного лимита

    • 1) Код и подробности. Функция может установить любому на свой баланс и только на свой.
      2) Подробнее, там дальше по уроку защита добавлена

  6. Перечитал урок, перепроверил код:
    function approve работает правильно.
    У меня отсутствовала защита в функции transferFrom, поэтому был overspending.

    Извините за беспокойство.

  7. Добрый день, спасибо за туториал! P.S.: В первый раз, где вы описали метод transferFrom, нужно исправить событие Transfer(msg.sender, _to, _value); на Transfer(_from, _to, _value);

  8. 46 function transfer(address _to, uint _value) returns (bool success) {
    47 if(balances[msg.sender] >= _value && balances[msg.sender] + _value >= balances[msg.sender])

    Функция » transfer » строка 47 проверки: 1- достаточность средств на перевод 2 — переполнение баланса получателя -_to , верно будет условие ( balances[_to] + _value >= balances[_to])
    Возможно опечатка.
    С Уважением
    Валерий.

  9. 75 function allowance(address _owner, address _spender) constant returns (uint remaining) {
    return allowed[_owner][_spender];
    }
    я не очень понимаю почему constant returns (uint remaining) а не
    constant returns (uint allowed) ???

    42 function balanceOf(address _owner) constant returns (uint balance) {
    43 return balances[_owner]; и также тут???
    }

    С Уважением
    Валерий

    • Возвращаемое значение можно назвать как угодно. Но в стандарте именно так.
      По смыслу и ваш вариант подходит, и тот что в стандарте.

  10. Здравствуйте, у вас просто шикарный по степени полезности цикл материалов. В номинации «просто о сложном» вы — бесспорный лидер. Обязательно пришлю вам своих токенов, когда они будут чего-то стоить 🙂

    Единственное, к чему пришлось возвращаться (IQ<=42) — двойной маппинг. Эта инфографика (http://prntscr.com/h2yq68) не далась с первого раза. Может, перерисовать её?

  11. Добрый день! Подскажите, создал токен с идентичным кодом, как в этом уроке, не могу понять почему баланс отображает в онлайн кошельке 0,000000000000499250 FWL, а в ремиксе через запрос баланса 499250. Функцией минт создавал 499250.

    • Онлайн кошелек — это интерфейс. Он отображает следующим образом.

      • Вызывает balanceOf и получает 499250
      • Сдвигает запятую на значением decimals, т.е. на 18 и получает 0,000000000000499250
      • Отображает

      Внутри платформа эфира оперирует не в эфирах, а в Wei, т.е. 1 eth = 1000000000000000000 wei.

      Когда вы с кошелька инвестируете, то кошелек к примеру Ваши 0.5 eth переводит в wei и получает 500000000000000000 и потому уже с этим значением вызывает функцию mint. Если же вы напрямую вызываете функцию mint, то вы указываете в wei.

  12. Опечатка в названии

    string public constant name = «Simple Coint Token»;

    Из текста понятно, что должно быть

    string public constant name = «Simple Coin Token»;

  13. Возможно не тем языком напишу, сам только разбираюсь со всем этим, но тоже уже столкнулся с подобным.

    вот здесь мы указываем количество знаком после запятой:
    uint32 public constant decimals = 18

    И при создании монет мы задаем количество в мельчайшей доле (грубо говоря копейке), в онлайн кошельке показывается в формате 0.0000000…. в ремиксе в количестве мельчайших долей.

    Поправьте, если я ошибаюсь.

  14. Ошибка:

    function transferFrom(address _from, address _to, uint _value) returns (bool success) {
    if( allowed[_from][msg.sender] >= _value &&

    Здесь должно быть allowed[_from][_to]

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

    • Поторопился. Обдумал логику еще раз. Все правильно, ошибки нет 🙂

  15. в строчке » Transfer(_from, _to, _value)» забыли ; в конце

  16. Не понимаю mapping такого вида:
    allowed[msg.sender][_spender] = _value;

    Значение какого кошелька здесь будет получено?

    • _value тут это количество денег в wei которое участник с адресом кошелька msg.sender
      разрешил со своего кошелька тратить участнику с кошельком _spender

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *