В этом уроке мы разберем стандарт ERC20 и напишем собственную монету.
Предыдущий урок можно посмотреть тут.Полный список уроков тут.
В мире Ethereum существует стандарт токена или монеты. Если ваш контракт отвечает стандарту, то его можно назвать монетой (валютой или токеном — кому как удобно). И в этом случае кошельки и биржи смогут работать с вашей монетой.
Стандарт монеты на базе эфира называется — ERC20. А требования его примерно следующие:
Чтобы смарт-контракт считался валютой он должен содержать:
- Методы
123456function totalSupply() constant returns (uint256 totalSupply)function balanceOf(address _owner) constant returns (uint256 balance)function transfer(address _to, uint256 _value) returns (bool success)function transferFrom(address _from, address _to, uint256 _value) returns (bool success)function approve(address _spender, uint256 _value) returns (bool success)function allowance(address _owner, address _spender) constant returns (uint256 remaining) - События
12event Transfer(address indexed _from, address indexed _to, uint256 _value)event Approval(address indexed _owner, address indexed _spender, uint256 _value)
На момент написания статьи 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 перечисленные поля уже включены.
1 2 3 |
string public constant name = "Token Name"; string public constant symbol = "SYM"; uint8 public constant decimals = 18; |
На текущий момент эти правила также входят в стандарт ERC223.
В идеале ваша монета должна выполнять все соглашения.
А теперь давайте рассмотрим эти требования подробней:
- Метод
1function totalSupply() constant returns (uint256 totalSupply)
Возвращает суммарное количество выпущенных монет. Эту функцию может вызвать любой. - Метод
1function balanceOf(address _owner) constant returns (uint256 balance)
Возвращает количество монет принадлежащих _owner. Может вызвать любой. Кошельки для отображения вашей монеты вызывают именно эту функцию. - Метод
1function transfer(address _to, uint256 _value) returns (bool success)
Передает _value монет на адрес _to. Когда пользователь будет перемещать свои монеты на другой адрес вызываться будет именно эта функция. Соответственно монеты должны браться с баланса пользователя, который вызвал эту функцию. Метод должен создавать событие Transfer (описан будет далее) в случае успешного перемещения монет. - Метод
1function transferFrom(address _from, address _to, uint256 _value) returns (bool success)
Передает _ value монет от _from к _to. Пользователь должен иметь разрешение на перемещение монеток между адресами, дабы любой желающий не смог управлять чужими кошельками. Фактически эта функция позволяет вашему доверенному лицу распоряжаться определенным объемом монеток на вашем счету. Дать разрешение на управление средствами можно следующей функцией. Метод должен создавать событие Transfer (описан будет далее) в случае успешного перемещения монет. - Метод
1function approve(address _spender, uint256 _value) returns (bool success)
Разрешает пользователю _spender снимать с вашего счета (точнее со счета вызвавшего функцию пользователя) средства не более чем _value. На основе этого разрешения должна работать функция transferFrom. Метод должен создавать событие Approval (описан будет далее). - Метод
1function allowance(address _owner, address _spender) constant returns (uint256 remaining)
Возвращает сколько монет со своего счета разрешил снимать пользователь _owner пользователю _spender. - Событие
1event Transfer(address indexed _from, address indexed _to, uint256 _value)
Событие, которое должно возникать при любом перемещении монет. Т.е. его нужно создавать внутри функций transfer и transferFrom в случае успешного перемещения монет. - Событие
1event Approval(address indexed _owner, address indexed _spender, uint256 _value)
Событие должно возникать при получении разрешения на снятие монет. Фактически должно создаваться внутри функции approve. - Поле
1string public constant name;
Хранит полное название вашей монеты - Поле
1string public constant symbol;
Хранит короткое название вашей монеты. Иначе говоря — символ. С этим символом ваша валюта будет отображаться на биржах и в кошельках. - Поле
1uint8 public constant decimals = 18;
Количество знаков после запятой. 18 — это наиболее распространенное значение.
Как видите требований не много и они простые. Давайте напишем нашу монету. Называться она будет «Simple coin token», символ «SCT» и иметь 18 знаков после запятой. Пока реализуем основные поля и функцию transfer .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
pragma solidity ^0.4.18; contract SimpleTokenCoin { string public constant name = "Simple Coin Token"; string public constant symbol = "SCT"; uint32 public constant decimals = 18; uint public totalSupply = 0; mapping (address => uint) balances; function balanceOf(address _owner) public constant returns (uint balance) { return balances[_owner]; } function transfer(address _to, uint _value) public returns (bool success) { balances[msg.sender] -= _value; balances[_to] += _value; Transfer(msg.sender, _to, _value); return true; } function transferFrom(address _from, address _to, uint _value) public returns (bool success) { return true; } function approve(address _spender, uint _value) public returns (bool success) { return false; } function allowance(address _owner, address _spender) public constant returns (uint remaining) { return 0; } event Transfer(address indexed _from, address indexed _to, uint _value); event Approval(address indexed _owner, address indexed _spender, uint _value); } |
Балансы всех пользователей у нас хранятся в поле balances. В котором каждому адресу пользователя соответствует количество монет.
Функция transfer довольно простая. В своей первой строчке она просто вычитает из баланса пользователя msg.sender (тот кто вызвал функцию) _value монет. А во второй строчке прибавляет к балансу пользователя _to то же количество монет. Потом создается событие и возвращается true в знак того что перемещение монет успешно выполнено.
Код функции transferFrom будет почти такой же. Попробуйте его написать самостоятельно.
Вернемся к transfer. Сейчас если мы попытаемся взять монет больше чем у пользователя есть, то у него появится отрицательное количество монет на балансе. Нам нужно это предотвратить.
1 2 3 4 5 6 7 8 9 |
function transfer(address _to, uint _value) public returns (bool success) { if(balances[msg.sender] >= _value) { balances[msg.sender] -= _value; balances[_to] += _value; Transfer(msg.sender, _to, _value); return true; } return false; } |
Стоит отметить что переменные могут хранить какое-то максимальное число. Если попытаться сохранить число больше то мы переменная переполнится и станет отрицательной. Именно поэтому добавляют еще проверку на переполнение. В нашем случае оно будет таким:
1 |
balances[_to] + _value >= balances[_to] |
А полностью функция transfer будет выглядеть так
1 2 3 4 5 6 7 8 9 |
function transfer(address _to, uint _value) returns (bool success) public { if(balances[msg.sender] >= _value && balances[_to] + _value >= balances[_to]) { balances[msg.sender] -= _value; balances[_to] += _value; Transfer(msg.sender, _to, _value); return true; } return false; } |
Добавьте такие же проверки в transferFrom самостоятельно.
После всех наших махинаций transferFrom должна выглядеть примерно так:
1 2 3 4 5 6 7 8 9 |
function transferFrom(address _from, address _to, uint _value) public returns (bool success) { if(balances[_from] >= _value && balances[_to] + _value >= balances[_to]) { balances[_from] -= _value; balances[_to] += _value; Transfer(_from, _to, _value); return true; } return false; } |
Давайте посмотрим как работает transfer и transferFrom. Для того чтобы мы смогли выпускать новые монеты на баланс любого пользователя — добавьте функцию mint (в переводе — чеканить):
1 2 3 4 5 |
function mint(address _to, uint _value) public { assert(totalSupply + _value >= totalSupply && balances[_to] + _value >= balances[_to]); balances[_to] += _value; totalSupply += _value; } |
После этого залейте контракт в 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. Первый ключ — адрес на снятие с которого предоставляется разрешение. Второй ключ — пользователь, которому предоставляется разрешение.
1 |
mapping (address => mapping (address => uint)) allowed; |
Добавьте это поле в контракт.
Займемся функцией allowance. Сумма которая разрешена пользователю _spender для снятия со счета _owner
1 |
allowed[_owner][_spender] |
Тогда наша функция allowance будет выглядеть так
1 2 3 |
function allowance(address _owner, address _spender) public constant returns (uint remaining) { return allowed[_owner][_spender]; } |
Теперь напишем функцию approve.
Чтобы пользователю _spender предоставить разрешение на снятие _value монет со счета выполняющего контракт нужно записать
1 |
allowed[msg.sender][_spender] = _value; |
И не забудем добавить создания события Approval. И вот код approve
1 2 3 4 5 |
function approve(address _spender, uint _value) public returns (bool success) { allowed[msg.sender][_spender] = _value; Approval(msg.sender, _spender, _value); return true; } |
Осталось исправить transferFrom таким образом, чтобы была проверка на разрешенное количество монет.
1 |
allowed[_from][msg.sender] >= _value |
И если снятие монет успешно, то уменьшаем разрешенное количество монет на величину снятия.
1 |
allowed[_from][msg.sender] -= _value; |
Новая версия transferFrom теперь выглядит так:
1 2 3 4 5 6 7 8 9 10 11 12 |
function transferFrom(address _from, address _to, uint _value) public returns (bool success) { if( allowed[_from][msg.sender] >= _value && balances[_from] >= _value && balances[_to] + _value >= balances[_to]) { allowed[_from][msg.sender] -= _value; balances[_from] -= _value; balances[_to] += _value; Transfer(_from, _to, _value); return true; } return false; } |
Теперь осталось только сделать так чтобы выпуск новых монет был разрешен только владельцу контракта. В четвертом уроке для этого мы написали контракт Ownablе который содержит необходимый модификатор onlyOwner унаследуем наш контракт монеты от Ownable и добавим к mint модификатор onlyOwner.
Теперь контракт нашей монетки выглядит так.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
pragma solidity ^0.4.18; contract Ownable { address public owner; function Ownable() public { owner = msg.sender; } modifier onlyOwner() { require(msg.sender == owner); _; } function transferOwnership(address newOwner) public onlyOwner { owner = newOwner; } } contract SimpleTokenCoin is Ownable { string public constant name = "Simple Coin Token"; string public constant symbol = "SCT"; uint32 public constant decimals = 18; uint public totalSupply = 0; mapping (address => uint) balances; mapping (address => mapping(address => uint)) allowed; function mint(address _to, uint _value) public onlyOwner { assert(totalSupply + _value >= totalSupply && balances[_to] + _value >= balances[_to]); balances[_to] += _value; totalSupply += _value; } function balanceOf(address _owner) public constant returns (uint balance) { return balances[_owner]; } function transfer(address _to, uint _value) public returns (bool success) { if(balances[msg.sender] >= _value && balances[_to] + _value >= balances[_to]) { balances[msg.sender] -= _value; balances[_to] += _value; Transfer(msg.sender, _to, _value); return true; } return false; } function transferFrom(address _from, address _to, uint _value) public returns (bool success) { if( allowed[_from][msg.sender] >= _value && balances[_from] >= _value && balances[_to] + _value >= balances[_to]) { allowed[_from][msg.sender] -= _value; balances[_from] -= _value; balances[_to] += _value; Transfer(_from, _to, _value); return true; } return false; } function approve(address _spender, uint _value) public returns (bool success) { allowed[msg.sender][_spender] = _value; Approval(msg.sender, _spender, _value); return true; } function allowance(address _owner, address _spender) public constant returns (uint remaining) { return allowed[_owner][_spender]; } event Transfer(address indexed _from, address indexed _to, uint _value); event Approval(address indexed _owner, address indexed _spender, uint _value); } |
Если сравните требования ERC20 с нашим контрактом то заметите, что нет функции totalSupply. Дело в том, что для публичных полей функции возврата, так называемые геттеры, создаются автоматически, когда уже контракт будет залит в блокчейн.
Попробуйте поиграться и проверить как работают функции предоставления разрешения на снятие средств.
В этом уроке мы написали контракт нашей первой монетки. И он полностью совместим с ERC20! На Эфире уже написано большое количество монет и сложились некоторые общие шаблоны. В следующем уроке мы рассмотрим один из распространенных способов написания монеты ERC20. И изменим наш контракт в соответствии с общепринятой практикой.
Продолжение читать тут. Предыдущий урок тут.
Если у вас возникли вопросы то можете смело писать на электронную почту (раздел «контакты«). Также приветствуется критика.
Если статья показалась вам полезной и вы желаете отблагодарить автора, то это можно сделать отослав немного эфира на адрес 0xEA15Adb66DC92a4BbCcC8Bf32fd25E2e86a2A770.
Проверка на переполнение.
balances[msg.sender] + _value >= balances[msg.sender]
А тут точно верно?
Проверять нужно destination баланс на переполнение, то есть:
balances[_to] + _value >= balances[_to]
Спасибо, исправил.
Здравствуйте.
Мне не понятна суть событий (event) в солидити. Зачем они нужны? Что происходит, когда исполнение программного кода доходит до строк event Transfer(address indexed _from, address indexed _to, uint256 _value) или
event Approval. Заранее спасибо.
События — это способ логирования. Какое-нибудь dapp может слушать нужные ей события. Например etherscan на основе события Transfer узнает кто кому сколько монет отправил и рассчитывает на основе этого доли монет у каждого инвестора.
Здравствуйте 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;
Верно, исправлю, спасибо.
Ещё вопрос: в первой строке функции не должно быть «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;
В дополнение к предыдущему посту:
Проверка function approve показала:
1) в таком виде она даёт возможность любому flhtce установить лимит на транзакцию transferFrom со счёта Owner
2) транзакции проводятся и свыше установленного лимита
1) Код и подробности. Функция может установить любому на свой баланс и только на свой.
2) Подробнее, там дальше по уроку защита добавлена
Код отправлю мылом.
Спасибо
Перечитал урок, перепроверил код:
function approve работает правильно.
У меня отсутствовала защита в функции transferFrom, поэтому был overspending.
Извините за беспокойство.
Добрый день, спасибо за туториал! P.S.: В первый раз, где вы описали метод transferFrom, нужно исправить событие Transfer(msg.sender, _to, _value); на Transfer(_from, _to, _value);
Спасибо, поправил.
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])
Возможно опечатка.
С Уважением
Валерий.
Да, это опечатка, спасибо, поправил.
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]; и также тут???
}
С Уважением
Валерий
Возвращаемое значение можно назвать как угодно. Но в стандарте именно так.
По смыслу и ваш вариант подходит, и тот что в стандарте.
Здравствуйте, у вас просто шикарный по степени полезности цикл материалов. В номинации «просто о сложном» вы — бесспорный лидер. Обязательно пришлю вам своих токенов, когда они будут чего-то стоить 🙂
Единственное, к чему пришлось возвращаться (IQ<=42) — двойной маппинг. Эта инфографика (http://prntscr.com/h2yq68) не далась с первого раза. Может, перерисовать её?
Согласен, коряво. Что-нибудь придумаем.
Добрый день! Подскажите, создал токен с идентичным кодом, как в этом уроке, не могу понять почему баланс отображает в онлайн кошельке 0,000000000000499250 FWL, а в ремиксе через запрос баланса 499250. Функцией минт создавал 499250.
Онлайн кошелек — это интерфейс. Он отображает следующим образом.
Внутри платформа эфира оперирует не в эфирах, а в Wei, т.е. 1 eth = 1000000000000000000 wei.
Когда вы с кошелька инвестируете, то кошелек к примеру Ваши 0.5 eth переводит в wei и получает 500000000000000000 и потому уже с этим значением вызывает функцию mint. Если же вы напрямую вызываете функцию mint, то вы указываете в wei.
Опечатка в названии
string public constant name = «Simple Coint Token»;
Из текста понятно, что должно быть
string public constant name = «Simple Coin Token»;
Спасибо, исправил.
Возможно не тем языком напишу, сам только разбираюсь со всем этим, но тоже уже столкнулся с подобным.
вот здесь мы указываем количество знаком после запятой:
uint32 public constant decimals = 18
И при создании монет мы задаем количество в мельчайшей доле (грубо говоря копейке), в онлайн кошельке показывается в формате 0.0000000…. в ремиксе в количестве мельчайших долей.
Поправьте, если я ошибаюсь.
Ошибка:
function transferFrom(address _from, address _to, uint _value) returns (bool success) {
if( allowed[_from][msg.sender] >= _value &&
Здесь должно быть allowed[_from][_to]
И почему бы не использовать функцию allowance, раз уж она у нас есть? Хотя тут я не уверен, как оно в смарт контрактах принято.
Поторопился. Обдумал логику еще раз. Все правильно, ошибки нет 🙂
в строчке » Transfer(_from, _to, _value)» забыли ; в конце
Не понимаю mapping такого вида:
allowed[msg.sender][_spender] = _value;
Значение какого кошелька здесь будет получено?
_value тут это количество денег в wei которое участник с адресом кошелька msg.sender
разрешил со своего кошелька тратить участнику с кошельком _spender
Друзья кто нить может объяснить ошибку такого характера:
Expected token LParen got ‘Identifier’
function transfer(address _to, uint _value) returns (bool success) public {
Спасибо за ранее
разобрался